Initial commit

Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com>
Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com>
Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com>
Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com>
Co-Authored-By: Johan  <4934465+joolsus@users.noreply.github.com>
Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com>
Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
Michael Cartner
2024-01-26 11:52:55 +01:00
commit b9c60b07dc
2789 changed files with 868795 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
@use '../../../styles/scss-variables/length.scss';
.Root {
font-family: var(--font-family);
color: var(--theme-color-fg-default);
background-color: var(--theme-color-bg-3);
line-height: 1;
border: none;
padding: length.$property-panel-input-padding;
width: 100%;
box-sizing: border-box;
font-size: 12px;
transition: background-color var(--speed-turbo), color var(--speed-turbo);
&:hover {
color: var(--theme-color-fg-highlight);
}
&:focus,
&.is-faux-focused {
background-color: var(--theme-color-bg-1);
color: var(--theme-color-fg-highlight);
outline: none;
}
&.is-changed {
color: var(--theme-color-fg-highlight);
}
&.is-connected {
outline: 1px solid var(--theme-color-primary-dim);
}
&.has-hidden-caret {
caret-color: transparent;
}
&[type='number'] {
-moz-appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
}
&.has-small-text {
font-size: 10px;
padding-left: length.$property-panel-small-xy-padding;
padding-right: length.$property-panel-small-xy-padding;
text-align: center;
}
}

View File

@@ -0,0 +1,69 @@
import classNames from 'classnames';
import React, { FocusEventHandler, KeyboardEventHandler, MouseEventHandler } from 'react';
import css from './PropertyPanelBaseInput.module.scss';
export interface PropertyPanelBaseInputProps<ValueType = string | number> {
value: ValueType;
type: string;
isChanged?: boolean;
isConnected?: boolean;
isFauxFocused?: boolean;
hasHiddenCaret?: boolean;
hasSmallText?: boolean;
onChange?: (value: ValueType) => void;
onClick?: MouseEventHandler<HTMLButtonElement>;
onMouseEnter?: MouseEventHandler<HTMLButtonElement>;
onMouseLeave?: MouseEventHandler<HTMLButtonElement>;
onFocus?: FocusEventHandler<HTMLButtonElement>;
onBlur?: FocusEventHandler<HTMLButtonElement>;
onKeyDown?: KeyboardEventHandler;
onError?: (error: Error) => void;
className?: string;
}
export function PropertyPanelBaseInput({
value,
type,
isChanged,
isConnected,
isFauxFocused,
hasHiddenCaret,
hasSmallText,
onChange,
onClick,
onMouseEnter,
onMouseLeave,
onFocus,
onBlur,
onKeyDown,
className
}: PropertyPanelBaseInputProps) {
return (
<input
className={classNames(
css['Root'],
isChanged && css['is-changed'],
isConnected && css['is-connected'],
hasHiddenCaret && css['has-hidden-caret'],
isFauxFocused && css['is-faux-focused'],
hasSmallText && css['has-small-text']
)}
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
/>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelBaseInput';

View File

@@ -0,0 +1,32 @@
@use '../../../styles/scss-variables/length.scss';
.Root {
display: flex;
flex-direction: column;
}
.Button {
font-family: var(--font-family);
color: var(--theme-color-fg-default);
font-size: 12px;
text-align: center;
background-color: var(--theme-color-bg-3);
padding: length.$property-panel-input-padding;
border: none;
cursor: pointer;
&:hover {
background-color: var(--theme-color-bg-1);
color: var(--theme-color-fg-highlight);
}
&.is-primary {
font-weight: 600;
color: var(--theme-color-bg-1);
background-color: var(--theme-color-primary);
&:hover {
background-color: var(--theme-color-primary-highlight);
}
}
}

View File

@@ -0,0 +1,31 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { PropertyPanelButton } from '@noodl-core-ui/components/property-panel/PropertyPanelButton';
export default {
title: 'Property Panel/Button',
component: PropertyPanelButton,
argTypes: {}
} as ComponentMeta<typeof PropertyPanelButton>;
const Template: ComponentStory<typeof PropertyPanelButton> = (args) => (
<div style={{ width: 280 }}>
<PropertyPanelButton {...args} />
</div>
);
export const Common = Template.bind({});
Common.args = {
properties: {
buttonLabel: 'Verify API Key'
}
};
export const Primary = Template.bind({});
Primary.args = {
properties: {
isPrimary: true,
buttonLabel: 'Verify API Key'
}
};

View File

@@ -0,0 +1,25 @@
import classNames from 'classnames';
import React from 'react';
import css from './PropertyPanelButton.module.scss';
export interface PropertyPanelButtonProps {
properties: {
isPrimary?: boolean;
buttonLabel: string;
onClick?: () => void;
};
}
export function PropertyPanelButton({ properties }: PropertyPanelButtonProps) {
return (
<div className={css['Root']}>
<button
className={classNames([css['Button'], properties.isPrimary && css['is-primary']])}
onClick={properties.onClick}
>
{properties.buttonLabel}
</button>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelButton'

View File

@@ -0,0 +1,51 @@
@use '../../../styles/scss-variables/length.scss';
.Root {
display: flex;
position: relative;
}
.Checkbox {
position: absolute;
width: length.$property-panel-input-size;
height: length.$property-panel-input-size;
opacity: 0;
cursor: pointer;
}
.FauxCheckbox {
display: flex;
justify-content: center;
align-items: center;
width: length.$property-panel-input-size;
height: length.$property-panel-input-size;
background-color: var(--theme-color-bg-3);
cursor: pointer;
svg {
pointer-events: none;
path {
stroke: var(--theme-color-fg-default);
}
}
&.is-connected {
outline: 1px solid var(--theme-color-primary-dim);
}
&.is-changed,
&:hover {
svg path {
stroke: var(--theme-color-fg-highlight);
}
}
.Checkbox:focus + & {
background-color: var(--theme-color-bg-1);
svg path {
stroke: var(--theme-color-fg-highlight);
}
}
}

View File

@@ -0,0 +1,19 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { PropertyPanelCheckbox } from '@noodl-core-ui/components/property-panel/PropertyPanelCheckbox';
export default {
title: 'Property Panel/Checkbox',
component: PropertyPanelCheckbox,
argTypes: {}
} as ComponentMeta<typeof PropertyPanelCheckbox>;
const Template: ComponentStory<typeof PropertyPanelCheckbox> = (args) => (
<div style={{ width: 280 }}>
<PropertyPanelCheckbox {...args} />
</div>
);
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,29 @@
import classNames from 'classnames';
import React from 'react';
import { PropertyPanelBaseInputProps } from '@noodl-core-ui/components/property-panel/PropertyPanelBaseInput';
import { ReactComponent as CheckmarkIcon } from '../../../assets/icons/checkmark.svg';
import css from './PropertyPanelCheckbox.module.scss';
export interface PropertyPanelCheckboxProps extends Omit<PropertyPanelBaseInputProps<boolean>, 'type'> {}
export function PropertyPanelCheckbox({ value, onChange, isConnected, isChanged }: PropertyPanelCheckboxProps) {
return (
<div className={css['Root']}>
<input
type="checkbox"
checked={value}
value={null} // TODO: a bit ugly
className={css['Checkbox']}
onChange={() => onChange(!value)}
/>
<div
className={classNames(css['FauxCheckbox'], isChanged && css['is-changed'], isConnected && css['is-connected'])}
>
{value && <CheckmarkIcon />}
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelCheckbox';

View File

@@ -0,0 +1,59 @@
.Root {
display: flex;
margin-left: -3px; // using -3 for visual compensation
}
.Option {
position: relative;
cursor: pointer;
user-select: none;
display: block;
margin-left: 2px;
background-color: var(--theme-color-bg-3);
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
&:hover {
svg path {
fill: var(--theme-color-fg-highlight);
}
}
svg {
display: block;
pointer-events: none;
path {
fill: var(--theme-color-fg-default);
}
}
&.is-selected {
background-color: var(--theme-color-bg-1);
svg {
path {
fill: var(--theme-color-fg-highlight);
}
}
}
&.is-disabled {
cursor: not-allowed;
background-color: var(--theme-color-bg-2);
svg {
path {
fill: var(--theme-color-fg-muted);
}
}
}
}
.Input {
position: absolute;
opacity: 0;
height: 0;
}

View File

@@ -0,0 +1,54 @@
import classNames from 'classnames';
import React, { useMemo } from 'react';
import { PropertyPanelBaseInputProps } from '@noodl-core-ui/components/property-panel/PropertyPanelBaseInput';
import { SingleSlot } from '@noodl-core-ui/types/global';
import css from './PropertyPanelIconRadioInput.module.scss';
export enum PropertyPanelIconRadioSize {
Default = 'default',
Large = 'large'
}
interface PropertyPanelIconRadioProperties {
name: string;
options: {
icon: SingleSlot;
value: string | number;
isDisabled?: boolean;
}[];
}
export interface PropertyPanelIconRadioInputProps extends Omit<PropertyPanelBaseInputProps, 'type' | 'onClick'> {
onChange?: (value: string | number) => void;
properties?: PropertyPanelIconRadioProperties;
}
export function PropertyPanelIconRadioInput({ value, properties, onChange }: PropertyPanelIconRadioInputProps) {
const timestamp = useMemo(() => Date.now().toString(), []);
return (
<div className={css['Root']}>
{properties?.options.map((option) => (
<label
className={classNames(
css['Option'],
value === option.value && css['is-selected'],
option.isDisabled && css['is-disabled']
)}
>
<input
className={css['Input']}
type="radio"
name={timestamp}
value={option.value}
checked={value === option.value}
onClick={() => onChange && onChange(option.value)}
disabled={option.isDisabled}
/>
{option.icon}
</label>
))}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelIconRadioInput';

View File

@@ -0,0 +1,27 @@
.Root {
display: flex;
margin-bottom: 8px;
align-items: center;
user-select: none;
}
.Label {
font-family: var(--font-family);
color: var(--theme-color-fg-default);
padding-right: 8px;
font-size: 12px;
width: 37%;
flex-grow: 0;
flex-shrink: 0;
text-overflow: ellipsis;
max-height: 28px;
line-clamp: 2;
&.is-changed {
color: var(--theme-color-secondary-as-fg);
}
}
.InputContainer {
flex: 1 1 auto;
}

View File

@@ -0,0 +1,157 @@
import React, { useState } from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import {
PropertyPanelInput,
PropertyPanelInputType,
} from './PropertyPanelInput';
import { PropertyPanelSection } from '@noodl-core-ui/components/property-panel/PropertyPanelSection';
import { ReactComponent as AlignLeftIcon } from '../../../assets/icons/align-left.svg';
import { ReactComponent as AlignCenterIcon } from '../../../assets/icons/align-center.svg';
import { ReactComponent as AlignRightcon } from '../../../assets/icons/align-right.svg';
export default {
title: 'Property Panel/# Generic',
component: PropertyPanelInput,
argTypes: {},
} as ComponentMeta<typeof PropertyPanelInput>;
const Template: ComponentStory<typeof PropertyPanelInput> = (args) => {
const [value, setValue] = useState(args.value || '');
return (
<div style={{ width: 280 }}>
<PropertyPanelSection title="Input demo">
<PropertyPanelInput {...args} value={value} onChange={setValue} />
</PropertyPanelSection>
<div style={{ paddingTop: 30 }}>
Stored value:{' '}
<input value={value} onChange={(e) => setValue(e.target.value)} />
</div>
</div>
);
};
export const Common = Template.bind({});
Common.args = { label: 'Label' };
export const Text = Template.bind({});
Text.args = {
inputType: PropertyPanelInputType.Text,
label: 'Text',
};
export const Number = Template.bind({});
Number.args = {
inputType: PropertyPanelInputType.Number,
label: 'Number',
};
export const LengthUnit = Template.bind({});
LengthUnit.args = {
inputType: PropertyPanelInputType.LengthUnit,
label: 'Length unit',
value: '200px',
};
export const Slider = Template.bind({});
Slider.args = {
inputType: PropertyPanelInputType.Slider,
label: 'Slider',
value: 50,
properties: {
min: 10,
max: 90,
step: 5,
},
};
export const Select = Template.bind({});
Select.args = {
inputType: PropertyPanelInputType.Select,
label: 'Select',
value: 'first',
properties: {
options: [
{
label: 'First option',
value: 'first',
},
{
label: 'Second option',
value: 'second',
},
{
label: 'Disabled option',
value: 'third',
isDisabled: true,
},
],
},
};
export const TextRadio = Template.bind({});
TextRadio.args = {
inputType: PropertyPanelInputType.TextRadio,
label: 'Text radio',
value: 'one',
properties: {
options: [
{
label: 'One',
value: 'one',
},
{
label: 'Two',
value: 'two',
},
{
label: 'Disabled',
value: 'three',
isDisabled: true,
},
],
},
};
export const IconRadio = Template.bind({});
IconRadio.args = {
inputType: PropertyPanelInputType.IconRadio,
label: 'Icon radio',
value: 'left',
properties: {
options: [
{
icon: <AlignLeftIcon />,
value: 'left',
},
{
icon: <AlignCenterIcon />,
value: 'center',
},
{
icon: <AlignRightcon />,
value: 'right',
isDisabled: true,
},
],
},
};
export const Checkbox = Template.bind({});
Checkbox.args = {
inputType: PropertyPanelInputType.Checkbox,
label: 'Checkbox',
value: true,
};
export const Button = Template.bind({});
Button.args = {
inputType: PropertyPanelInputType.Button,
label: 'Button',
properties: {
buttonLabel: 'Click me',
onClick: () => alert('hello'),
},
};

View File

@@ -0,0 +1,112 @@
import classNames from 'classnames';
import React, { useMemo } from 'react';
import { PropertyPanelBaseInputProps } from '@noodl-core-ui/components/property-panel/PropertyPanelBaseInput';
import { PropertyPanelButton } from '@noodl-core-ui/components/property-panel/PropertyPanelButton';
import { PropertyPanelCheckbox } from '@noodl-core-ui/components/property-panel/PropertyPanelCheckbox';
import { PropertyPanelIconRadioInput } from '@noodl-core-ui/components/property-panel/PropertyPanelIconRadioInput';
import { PropertyPanelLengthUnitInput } from '@noodl-core-ui/components/property-panel/PropertyPanelLengthUnitInput';
import { PropertyPanelNumberInput } from '@noodl-core-ui/components/property-panel/PropertyPanelNumberInput';
import { PropertyPanelSelectInput } from '@noodl-core-ui/components/property-panel/PropertyPanelSelectInput';
import { PropertyPanelSliderInput } from '@noodl-core-ui/components/property-panel/PropertyPanelSliderInput';
import { PropertyPanelTextInput } from '@noodl-core-ui/components/property-panel/PropertyPanelTextInput';
import { PropertyPanelTextRadioInput } from '@noodl-core-ui/components/property-panel/PropertyPanelTextRadioInput';
import { Slot } from '@noodl-core-ui/types/global';
import css from './PropertyPanelInput.module.scss';
export enum PropertyPanelInputType {
Text = 'text',
Number = 'number',
LengthUnit = 'length-unit',
Slider = 'slider',
Select = 'select',
Color = 'color',
TextRadio = 'text-radio',
IconRadio = 'icon-radio',
Checkbox = 'checkbox',
Button = 'button'
// MarginPadding = 'margin-padding',
// SizeMode = 'size-mode',
}
export interface PropertyPanelInputProps extends Omit<PropertyPanelBaseInputProps, 'type'> {
label: string;
inputType: PropertyPanelInputType;
properties: TSFixme;
}
export function PropertyPanelInput({
label,
value,
inputType = PropertyPanelInputType.Text,
properties,
isChanged,
isConnected,
onChange
}: PropertyPanelInputProps) {
const Input = useMemo(() => {
switch (inputType) {
case PropertyPanelInputType.Text:
return PropertyPanelTextInput;
case PropertyPanelInputType.Number:
return PropertyPanelNumberInput;
case PropertyPanelInputType.LengthUnit:
return PropertyPanelLengthUnitInput;
case PropertyPanelInputType.Select:
return PropertyPanelSelectInput;
case PropertyPanelInputType.Slider:
return PropertyPanelSliderInput;
case PropertyPanelInputType.TextRadio:
return PropertyPanelTextRadioInput;
case PropertyPanelInputType.IconRadio:
return PropertyPanelIconRadioInput;
case PropertyPanelInputType.Checkbox:
return PropertyPanelCheckbox;
case PropertyPanelInputType.Button:
return PropertyPanelButton;
}
}, [inputType]);
return (
<div className={css['Root']}>
<div className={classNames(css['Label'], isChanged && css['is-changed'])}>{label}</div>
<div className={css['InputContainer']}>
{
// FIXME: fix below ts-ignore with better typing
// this is caused by PropertyPanelBaseInputProps having a generic for "value"
// i want to pass a boolan to the checkbox value that will be used in checked for a better API
<Input
// @ts-expect-error
value={value}
// @ts-expect-error
onChange={onChange}
// @ts-expect-error
isChanged={isChanged}
// @ts-expect-error
isConnected={isConnected}
// @ts-expect-error
properties={properties}
/>
}
</div>
</div>
);
}
export interface PropertyPanelRowProps {
isChanged?: boolean;
label: string;
children: Slot;
}
export function PropertyPanelRow({ isChanged, label, children }: PropertyPanelRowProps) {
return (
<div className={css['Root']}>
<div className={classNames(css['Label'], isChanged && css['is-changed'])}>{label}</div>
<div className={css['InputContainer']}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelInput'

View File

@@ -0,0 +1,27 @@
.Root {
font-family: var(--font-family);
color: var(--theme-color-fg-default);
display: flex;
position: relative;
&.is-faux-focused {
z-index: 1;
}
}
.UnitContainer {
width: 32px;
margin-left: 2px;
flex-grow: 0;
flex-shrink: 0;
position: relative;
&.has-small-text {
width: 24px;
margin-left: -4px;
}
input {
text-align: center;
}
}

View File

@@ -0,0 +1,166 @@
import classNames from 'classnames';
import React, { useState, useEffect } from 'react';
import { Collapsible } from '@noodl-core-ui/components/layout/Collapsible';
import {
PropertyPanelBaseInput,
PropertyPanelBaseInputProps
} from '@noodl-core-ui/components/property-panel/PropertyPanelBaseInput';
import { PropertyPanelSelectInput } from '@noodl-core-ui/components/property-panel/PropertyPanelSelectInput';
import { stripCssUnit } from '@noodl-core-ui/utils/stripCssUnit';
import { extractLetters } from '../../../utils/extractLetters';
import { extractNumber } from '../../../utils/extractNumber';
import { normalizeAlphanumericString } from '../../../utils/normalizeAlphanumericString';
import css from './PropertyPanelLengthUnitInput.module.scss';
export interface PropertyPanelLengthUnitInputProps extends Omit<PropertyPanelBaseInputProps, 'type'> {
properties?: TSFixme;
hasSmallText?: boolean;
}
export function PropertyPanelLengthUnitInput({
value,
isChanged,
isConnected,
hasSmallText,
onChange,
onError
}: PropertyPanelLengthUnitInputProps) {
const [isFauxFocused, setIsFauxFocused] = useState(false);
const cleanedValue = normalizeAlphanumericString(value);
const numberValue = extractNumber(cleanedValue) || '';
const unitValue = extractLetters(cleanedValue).replace(' ', '');
const [displayedInputValue, setDisplayedInputValue] = useState(numberValue);
useEffect(() => {
handleInputUpdate(value);
}, [value]);
// FIXME: this is very temporary and should be fetched from the panel config
const unitSelectProps = {
options: [
{ label: 'px', value: 'px' },
{ label: 'em', value: 'em' },
{ label: 'vh', value: 'vh' },
{ label: '%', value: '%' }
]
};
function getUnit() {
const unitOption = unitSelectProps.options.find((option) => option.value === unitValue);
if (unitOption) return unitOption.value;
return 'px'; // px as default for some reason
}
function handleInputUpdate(inputValue) {
// TODO: increase/decrease with arrows
// TODO: handle drag up/down to increase/decrease
// TODO: handle Arithmetic Error nicer!
if (inputValue === '') {
onChange && onChange(inputValue + unitValue);
return;
}
let strippedValue = stripCssUnit(inputValue.toString());
const unit = inputValue.toString().slice(strippedValue.length); // lol
const newUnitValue = Boolean(unit) ? unitSelectProps.options.find((option) => option.label === unit)?.value : null;
if (isArithmeticExpression(strippedValue)) {
try {
strippedValue = eval(strippedValue);
} catch (err) {
console.log(err);
onError && onError(err);
}
}
const newNumber = roundCssLengthNumber(parseFloat(strippedValue));
if (newNumber === 'Error') {
const error = new Error('Arithmetic Error: Infinite or unsafe number.');
onError && onError(error);
console.log(error);
setDisplayedInputValue(displayedInputValue);
} else if (!isNaN(newNumber)) {
const finalNumber = newNumber === 'Error' ? strippedValue : newNumber;
const finalUnit = newUnitValue || unitValue;
onChange && onChange(finalNumber + finalUnit);
setDisplayedInputValue(finalNumber);
}
}
function handleUnitUpdate(unit: string) {
onChange && onChange(numberValue + unit);
}
return (
<div className={classNames(css['Root'], isFauxFocused && css['is-faux-focused'])}>
<PropertyPanelBaseInput
value={displayedInputValue}
type="text"
isChanged={isChanged}
isConnected={isConnected}
onChange={(value) => setDisplayedInputValue(value)}
isFauxFocused={isFauxFocused}
onFocus={() => setIsFauxFocused(() => true)}
onKeyDown={(e) => e.key === 'Enter' && handleInputUpdate(displayedInputValue)}
onBlur={() => {
setIsFauxFocused(() => false);
handleInputUpdate(displayedInputValue);
}}
hasSmallText={hasSmallText}
/>
<div className={classNames(css['UnitContainer'], hasSmallText && css['has-small-text'])}>
<PropertyPanelSelectInput
value={getUnit()}
properties={unitSelectProps}
onChange={handleUnitUpdate}
isFauxFocused={isFauxFocused}
onFocus={() => setIsFauxFocused(() => true)}
onBlur={() => setIsFauxFocused(() => false)}
hasHiddenCaret
hasSmallText={hasSmallText}
/>
</div>
</div>
);
}
function isArithmeticExpression(str) {
const arithmeticRegex = /^[-+*/()\d\s.]+$/;
return arithmeticRegex.test(str);
}
function roundCssLengthNumber(num) {
if (isNaN(num)) {
console.warn('Passed in NaN to PropertyPanelLengthInput. Changing it to empty string');
return '';
}
if (!isFinite(num) || num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) {
return 'Error';
} else if (Number.isInteger(num)) {
return num.toFixed(0); // Display the number without decimals
} else {
const roundedNum = num.toFixed(2); // Round the number to two decimal places
const decimalPart = roundedNum.split('.')[1];
if (decimalPart === '00') {
return roundedNum.split('.')[0] + '.0'; // Show only one decimal if the second decimal is "0"
} else if (decimalPart?.endsWith('0')) {
return roundedNum.split('.')[0] + '.' + decimalPart[0]; // Show only one decimal if the second decimal is "0"
} else {
return roundedNum;
}
}
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelLengthUnitInput';

View File

@@ -0,0 +1,43 @@
.Root {
font-family: var(--font-family);
color: var(--theme-color-fg-default);
min-width: 236px;
}
.InputContainer {
flex: 0 0 49px;
padding-left: 4px;
padding-right: 4px;
&.is-hidden {
opacity: 0;
pointer-events: none;
}
}
.TopContainer,
.BottomContainer {
display: flex;
justify-content: center;
}
.TopContainer {
padding-bottom: 4px;
}
.BottomContainer {
padding-top: 4px;
}
.CenterContainer {
display: flex;
justify-content: space-between;
align-items: center;
margin-left: -4px;
margin-right: -4px;
}
.PaddingContainer {
padding: 4px;
border: 2px solid var(--theme-color-bg-3);
}

View File

@@ -0,0 +1,86 @@
import { useEffect } from '@storybook/addons';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React, { useState } from 'react';
import { PropertyPanelSection } from '@noodl-core-ui/components/property-panel/PropertyPanelSection';
import { PropertyPanelMarginPadding } from './PropertyPanelMarginPadding';
export default {
title: 'Property Panel/Margin Padding',
component: PropertyPanelMarginPadding,
argTypes: {}
} as ComponentMeta<typeof PropertyPanelMarginPadding>;
const Template: ComponentStory<typeof PropertyPanelMarginPadding> = (args) => {
const [values, setValues] = useState({
padding: { top: '10px', bottom: '10px', left: '10px', right: '10px' },
margin: { top: '10px', bottom: '10px', left: '10px', right: '10px' }
});
useEffect(() => {
console.log(values);
}, [values]);
function handleChange(value, type, direction) {
const newValues = { ...values };
newValues[type][direction] = value;
setValues(newValues);
}
return (
<div style={{ width: 280 }}>
<PropertyPanelSection title="Margin and padding">
<PropertyPanelMarginPadding {...args} values={values} onChange={setValues} />
</PropertyPanelSection>
<div style={{ paddingTop: 30 }}>
Top padding:
<br />
<input value={values.padding.top} onChange={(e) => handleChange(e.target.value, 'padding', 'top')} />
<br />
<br />
Bottom padding:
<br />
<input value={values.padding.bottom} onChange={(e) => handleChange(e.target.value, 'padding', 'bottom')} />
<br />
<br />
Left padding:
<br />
<input value={values.padding.left} onChange={(e) => handleChange(e.target.value, 'padding', 'left')} />
<br />
<br />
Right padding:
<br />
<input value={values.padding.right} onChange={(e) => handleChange(e.target.value, 'padding', 'right')} />
<br />
<br />
Top margin:
<br />
<input value={values.margin.top} onChange={(e) => handleChange(e.target.value, 'margin', 'top')} />
<br />
<br />
Bottom margin:
<br />
<input value={values.margin.bottom} onChange={(e) => handleChange(e.target.value, 'margin', 'bottom')} />
<br />
<br />
Left margin:
<br />
<input value={values.margin.left} onChange={(e) => handleChange(e.target.value, 'margin', 'left')} />
<br />
<br />
Right margin:
<br />
<input value={values.margin.right} onChange={(e) => handleChange(e.target.value, 'margin', 'right')} />
<br />
<br />
</div>
</div>
);
};
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,128 @@
import classNames from 'classnames';
import React from 'react';
import { PropertyPanelLengthUnitInput } from '@noodl-core-ui/components/property-panel/PropertyPanelLengthUnitInput';
import css from './PropertyPanelMarginPadding.module.scss';
export interface PropertyPanelMarginPaddingValues {
padding?: {
top?: string;
bottom?: string;
left?: string;
right?: string;
};
margin?: {
top?: string;
bottom?: string;
left?: string;
right?: string;
};
}
export interface PropertyPanelMarginPaddingProps {
values?: PropertyPanelMarginPaddingValues;
hasHiddenPadding?: boolean;
hasHiddenMargin?: boolean;
onChange?: (PropertyPanelMarginPaddingValues) => void;
}
export function PropertyPanelMarginPadding({
values,
hasHiddenMargin,
hasHiddenPadding,
onChange
}: PropertyPanelMarginPaddingProps) {
function handleChange(value, type, direction) {
const newValues = { ...values };
newValues[type][direction] = value;
onChange && onChange(newValues);
}
return (
<div className={css['Root']}>
<div className={css['TopContainer']}>
<div className={classNames(css['InputContainer'], hasHiddenMargin && css['is-hidden'])}>
<PropertyPanelLengthUnitInput
value={values.margin.top}
onChange={(value) => handleChange(value, 'margin', 'top')}
hasSmallText={true}
/>
</div>
</div>
<div className={css['CenterContainer']}>
<div className={classNames(css['InputContainer'], hasHiddenMargin && css['is-hidden'])}>
<PropertyPanelLengthUnitInput
value={values.margin.left}
onChange={(value) => handleChange(value, 'margin', 'left')}
hasSmallText={true}
/>
</div>
<div className={css['PaddingContainer']}>
<div className={css['TopContainer']}>
<div className={classNames(css['InputContainer'], hasHiddenPadding && css['is-hidden'])}>
<PropertyPanelLengthUnitInput
value={values.padding.top}
onChange={(value) => handleChange(value, 'padding', 'top')}
hasSmallText={true}
/>
</div>
</div>
<div className={css['CenterContainer']}>
<div className={classNames(css['InputContainer'], hasHiddenPadding && css['is-hidden'])}>
<PropertyPanelLengthUnitInput
value={values.padding.left}
onChange={(value) => handleChange(value, 'padding', 'left')}
hasSmallText={true}
/>
</div>
<div className={classNames(css['InputContainer'], hasHiddenPadding && css['is-hidden'])}>
<PropertyPanelLengthUnitInput
value={values.padding.right}
onChange={(value) => handleChange(value, 'padding', 'right')}
hasSmallText={true}
/>
</div>
</div>
<div className={css['BottomContainer']}>
<div className={classNames(css['InputContainer'], hasHiddenPadding && css['is-hidden'])}>
<PropertyPanelLengthUnitInput
value={values.padding.bottom}
onChange={(value) => handleChange(value, 'padding', 'bottom')}
hasSmallText={true}
/>
</div>
</div>
</div>
<div className={classNames(css['InputContainer'], hasHiddenMargin && css['is-hidden'])}>
<PropertyPanelLengthUnitInput
value={values.margin.right}
onChange={(value) => handleChange(value, 'margin', 'right')}
hasSmallText={true}
/>
</div>
</div>
<div className={css['BottomContainer']}>
<div className={classNames(css['InputContainer'], hasHiddenMargin && css['is-hidden'])}>
<PropertyPanelLengthUnitInput
value={values.margin.bottom}
onChange={(value) => handleChange(value, 'margin', 'bottom')}
hasSmallText={true}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelMarginPadding';

View File

@@ -0,0 +1,19 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { PropertyPanelNumberInput } from './PropertyPanelNumberInput';
export default {
title: 'Property Panel/Number',
component: PropertyPanelNumberInput,
argTypes: {}
} as ComponentMeta<typeof PropertyPanelNumberInput>;
const Template: ComponentStory<typeof PropertyPanelNumberInput> = (args) => (
<div style={{ width: 280 }}>
<PropertyPanelNumberInput {...args} />
</div>
);
export const Common = Template.bind({});
Common.args = {};

View File

@@ -0,0 +1,70 @@
import React, { useState, useEffect } from 'react';
import {
PropertyPanelBaseInput,
PropertyPanelBaseInputProps
} from '@noodl-core-ui/components/property-panel/PropertyPanelBaseInput';
import { extractNumber } from '../../../utils/extractNumber';
export interface PropertyPanelNumberInputProps extends Omit<PropertyPanelBaseInputProps, 'type'> {
properties?: TSFixme;
}
export function PropertyPanelNumberInput({
value,
isChanged,
isConnected,
onChange,
onFocus,
onBlur,
onKeyDown
}: PropertyPanelNumberInputProps) {
// TODO: This component doesnt handle the value types correct
const [displayedInputValue, setDisplayedInputValue] = useState(value?.toString() || '');
useEffect(() => {
handleUpdate(value?.toString() || '');
}, [value]);
function handleUpdate(inputValue: string) {
// TODO: increase/decrease with arrows
// TODO: handle drag up/down to increase/decrease
// TODO: add basic arithmetic
if (inputValue === '') {
onChange && onChange(inputValue);
return;
}
const newNumber = extractNumber(inputValue);
if (!isNaN(newNumber)) {
onChange && onChange(newNumber);
setDisplayedInputValue(newNumber.toString());
}
}
return (
<PropertyPanelBaseInput
value={displayedInputValue}
type="text"
isChanged={isChanged}
isConnected={isConnected}
onChange={(value) => setDisplayedInputValue(String(value))}
onFocus={onFocus}
onBlur={(e) => {
handleUpdate(displayedInputValue);
onBlur && onBlur(e);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleUpdate(displayedInputValue);
}
onKeyDown && onKeyDown(e);
}}
/>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelNumberInput';

View File

@@ -0,0 +1,21 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { PropertyPanelPasswordInput } from './PropertyPanelPasswordInput';
export default {
title: 'Property Panel/Password',
component: PropertyPanelPasswordInput,
argTypes: {}
} as ComponentMeta<typeof PropertyPanelPasswordInput>;
const Template: ComponentStory<typeof PropertyPanelPasswordInput> = (args) => (
<div style={{ width: 280 }}>
<PropertyPanelPasswordInput {...args} />
</div>
);
export const Common = Template.bind({});
Common.args = {
value: 'Hello World'
};

View File

@@ -0,0 +1,52 @@
import React, { useEffect, useState } from 'react';
import {
PropertyPanelBaseInput,
PropertyPanelBaseInputProps
} from '@noodl-core-ui/components/property-panel/PropertyPanelBaseInput';
export interface PropertyPanelPasswordInputProps extends Omit<PropertyPanelBaseInputProps<string>, 'type'> {
properties?: TSFixme;
}
export function PropertyPanelPasswordInput({
value,
isChanged,
isConnected,
onChange
}: PropertyPanelPasswordInputProps) {
const [displayedInputValue, setDisplayedInputValue] = useState(value);
const [focused, setFocused] = useState(false);
function handleUpdate(inputValue: string) {
onChange && onChange(inputValue);
setDisplayedInputValue(inputValue);
}
function handleBlur() {
handleUpdate(displayedInputValue);
setFocused(false);
}
function handleFocus() {
setFocused(true);
}
useEffect(() => {
handleUpdate(value);
}, [value]);
return (
<PropertyPanelBaseInput
value={displayedInputValue}
type={focused ? 'text' : 'password'}
isChanged={isChanged}
isConnected={isConnected}
onChange={(value) => setDisplayedInputValue(String(value))}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyDown={(e) => e.key === 'Enter' && handleUpdate(displayedInputValue)}
/>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelPasswordInput';

View File

@@ -0,0 +1,21 @@
.Root {
padding: 12px 18px 24px;
border-bottom: 2px solid var(--theme-color-bg-1);
background-color: var(--theme-color-bg-2);
&.has-no-bottom-padding {
padding-bottom: 0;
}
}
.Header {
color: var(--theme-color-fg-highlight);
font-family: var(--font-family);
font-weight: var(--font-weight-semibold);
padding-bottom: 12px;
font-size: 12px;
}
.Items {
width: 100%;
}

View File

@@ -0,0 +1,15 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { PropertyPanelSection } from './PropertyPanelSection';
export default {
title: 'Property Panel/Property Panel Section',
component: PropertyPanelSection,
argTypes: {}
} as ComponentMeta<typeof PropertyPanelSection>;
const Template: ComponentStory<typeof PropertyPanelSection> = (args) => <PropertyPanelSection {...args} />;
export const Common = Template.bind({});
Common.args = { title: 'Section title' };

View File

@@ -0,0 +1,23 @@
import { Slot } from '@noodl-core-ui/types/global';
import classNames from 'classnames';
import React from 'react';
import css from './PropertyPanelSection.module.scss';
export interface PropertyPanelSectionProps {
title: string;
children: Slot;
hasNoBottomPadding?: boolean;
}
export function PropertyPanelSection({
title,
children,
hasNoBottomPadding
}: PropertyPanelSectionProps) {
return (
<div className={classNames(css['Root'], hasNoBottomPadding && css['has-no-bottom-padding'])}>
<header className={css['Header']}>{title}</header>
<div className={css['Items']}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,2 @@
export * from './PropertyPanelSection';
// TODO: Delete this component in favour of the other Section?

View File

@@ -0,0 +1,75 @@
@use '../../../styles/scss-variables/length.scss';
.Root {
font-family: var(--font-family);
color: var(--theme-color-fg-default);
position: relative;
input {
cursor: pointer;
user-select: none;
}
&.has-caret {
input {
padding-right: length.$property-panel-input-size;
}
}
}
.Caret {
position: absolute;
top: 0;
bottom: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
width: length.$property-panel-input-size;
transition: transform var(--speed-quick) var(--easing-base);
pointer-events: none;
.Root:hover & {
path {
fill: var(--theme-color-fg-highlight);
}
}
&.is-indicating-close {
transform: rotate(180deg);
}
}
.Options {
background-color: var(--theme-color-bg-3);
list-style: none;
margin: 0;
padding: 0;
font-family: var(--font-family);
color: var(--theme-color-fg-default);
user-select: none;
}
.Option {
padding: length.$property-panel-input-padding;
border-bottom: 1px solid var(--theme-color-bg-2);
font-size: 12px;
color: var(--theme-color-fg-default);
cursor: pointer;
&:hover {
color: var(--theme-color-fg-highlight);
}
&.is-disabled {
cursor: not-allowed;
color: var(--theme-color-fg-muted) !important;
}
&.has-small-text {
font-size: 10px;
padding-left: length.$property-panel-small-xy-padding;
padding-right: length.$property-panel-small-xy-padding;
text-align: center;
}
}

View File

@@ -0,0 +1,41 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { PropertyPanelSelectInput } from './PropertyPanelSelectInput';
export default {
title: 'Property Panel/Select',
component: PropertyPanelSelectInput,
argTypes: {}
} as ComponentMeta<typeof PropertyPanelSelectInput>;
const Template: ComponentStory<typeof PropertyPanelSelectInput> = (args) => (
<div style={{ width: 280 }}>
<PropertyPanelSelectInput {...args} />
</div>
);
export const Common = Template.bind({});
Common.args = {
value: 'disabled',
properties: {
options: [
{ label: 'Disabled', value: 'disabled' },
{ label: 'Limited Beta (gpt-3)', value: 'limited-beta' },
{ label: 'Full Beta (gpt-4)', value: 'full-beta' }
]
}
};
export const hasSmallText = Template.bind({});
hasSmallText.args = {
value: 'disabled',
properties: {
options: [
{ label: 'Disabled', value: 'disabled' },
{ label: 'Limited Beta (gpt-3)', value: 'limited-beta' },
{ label: 'Full Beta (gpt-4)', value: 'full-beta' }
]
},
hasSmallText: true
};

View File

@@ -0,0 +1,111 @@
import classNames from 'classnames';
import React, { useRef, useState } from 'react';
import { BaseDialog, DialogBackground, BaseDialogVariant } from '@noodl-core-ui/components/layout/BaseDialog';
import {
PropertyPanelBaseInput,
PropertyPanelBaseInputProps
} from '@noodl-core-ui/components/property-panel/PropertyPanelBaseInput';
import { ReactComponent as CaretIcon } from '../../../assets/icons/caret-down.svg';
import css from './PropertyPanelSelectInput.module.scss';
export interface PropertyPanelSelectProperties {
options: {
label: string;
value: string | number;
isDisabled?: boolean;
}[];
}
export interface PropertyPanelSelectInputProps extends Omit<PropertyPanelBaseInputProps, 'type' | 'onClick'> {
onChange?: (value: string | number) => void;
properties: PropertyPanelSelectProperties;
hasSmallText?: boolean;
}
export function PropertyPanelSelectInput({
value,
properties,
isFauxFocused,
onChange,
onFocus,
onBlur,
hasHiddenCaret,
hasSmallText
}: PropertyPanelSelectInputProps) {
const [isSelectCollapsed, setIsSelectCollapsed] = useState(true);
const rootRef = useRef<HTMLDivElement>();
const displayValue = properties?.options.find((option) => option.value === value)?.label;
return (
<div className={classNames(css['Root'], !hasHiddenCaret && css['has-caret'])} ref={rootRef}>
<PropertyPanelBaseInput
type="text"
value={displayValue}
hasHiddenCaret
onClick={() => {
setIsSelectCollapsed((prev) => !prev);
}}
onBlur={(e) => {
setTimeout(() => {
setIsSelectCollapsed(true);
}, 250);
if (onBlur) onBlur(e);
}}
onFocus={onFocus}
isFauxFocused={isFauxFocused}
hasSmallText={hasSmallText}
/>
{!hasHiddenCaret && (
<div
className={classNames(css['Caret'], !isSelectCollapsed && css['is-indicating-close'])}
onClick={() => {
setIsSelectCollapsed((prev) => !prev);
}}
>
<CaretIcon />
</div>
)}
<BaseDialog
isVisible={!isSelectCollapsed}
triggerRef={rootRef}
background={DialogBackground.Transparent}
variant={BaseDialogVariant.Select}
>
<ul className={css['Options']}>
{properties?.options?.map((option) => {
if (option.value === value) return null;
return (
<li
key={option.value}
className={classNames(
css['Option'],
option.isDisabled && css['is-disabled'],
hasSmallText && css['has-small-text']
)}
onClick={() => {
setIsSelectCollapsed(true);
if (option.isDisabled) return null;
onChange && onChange(option.value);
}}
>
{option.label}
</li>
);
})}
</ul>
</BaseDialog>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelSelectInput';

View File

@@ -0,0 +1,65 @@
.Root {
display: flex;
}
.NumberContainer {
width: 56px;
margin-right: 16px;
flex: 0 0 auto;
}
.Input {
flex: 1 1 auto;
width: calc(100% - 72px); // .NumberContainer width+margin
background-color: transparent;
-webkit-appearance: none;
&:focus {
outline: none;
}
@mixin thumb {
height: 14px;
width: 14px;
margin-top: -6px;
border-radius: 50%;
cursor: pointer;
-webkit-appearance: none;
background-color: var(--theme-color-fg-highlight);
border: none;
}
@mixin track {
width: 100%;
height: 2px;
cursor: pointer;
}
&::-moz-range-track {
@include track;
background: linear-gradient(
90deg,
var(--theme-color-secondary),
calc(var(--thumb-percentage) * 1%),
var(--theme-color-bg-4) calc(var(--thumb-percentage) * 1%)
);
}
&::-webkit-slider-runnable-track {
@include track;
background: linear-gradient(
90deg,
var(--theme-color-secondary),
calc(var(--thumb-percentage) * 1%),
var(--theme-color-bg-4) calc(var(--thumb-percentage) * 1%)
);
}
&::-moz-range-thumb {
@include thumb;
}
&::-webkit-slider-thumb {
@include thumb;
}
}

View File

@@ -0,0 +1,83 @@
import React, { CSSProperties, useMemo, useState, useEffect } from 'react';
import {
PropertyPanelBaseInput,
PropertyPanelBaseInputProps
} from '@noodl-core-ui/components/property-panel/PropertyPanelBaseInput';
import { linearMap } from '../../../utils/linearMap';
import css from './PropertyPanelSliderInput.module.scss';
export interface PropertyPanelSliderInputProps extends Omit<PropertyPanelBaseInputProps, 'type'> {
properties: TSFixme;
}
export function PropertyPanelSliderInput({
value,
onChange,
isChanged,
isConnected,
properties
}: PropertyPanelSliderInputProps) {
const [numberInputValue, setNumberInputValue] = useState(value);
useEffect(() => {
setNumberInputValue(value);
}, [value]);
function handleNumberUpdate() {
let value = numberInputValue;
if (numberInputValue > properties.max) {
value = properties.max;
}
if (numberInputValue < properties.min) {
value = properties.min;
}
onChange(value);
setNumberInputValue(value);
}
function handleSliderChange(value: string) {
onChange(value);
setNumberInputValue(value);
}
const thumbPercentage = useMemo(
() => linearMap(parseInt(value.toString()), properties.min, properties.max, 0, 100),
[value, properties]
);
return (
<div className={css['Root']}>
<div className={css['NumberContainer']}>
<PropertyPanelBaseInput
type="text"
value={numberInputValue}
onChange={setNumberInputValue}
onBlur={handleNumberUpdate}
isConnected={isConnected}
isChanged={isChanged}
onKeyDown={(e) => e.key === 'Enter' && handleNumberUpdate()}
/>
</div>
<input
style={
{
'--thumb-percentage': thumbPercentage
} as CSSProperties
}
className={css['Input']}
type="range"
value={value}
onChange={(e) => handleSliderChange(e.target.value)}
min={properties.min}
max={properties.max}
step={properties.step}
/>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelSliderInput';

View File

@@ -0,0 +1,21 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { PropertyPanelTextInput } from './PropertyPanelTextInput';
export default {
title: 'Property Panel/Text',
component: PropertyPanelTextInput,
argTypes: {}
} as ComponentMeta<typeof PropertyPanelTextInput>;
const Template: ComponentStory<typeof PropertyPanelTextInput> = (args) => (
<div style={{ width: 280 }}>
<PropertyPanelTextInput {...args} />
</div>
);
export const Common = Template.bind({});
Common.args = {
value: 'Hello World'
};

View File

@@ -0,0 +1,41 @@
import React, { useEffect, useState } from 'react';
import {
PropertyPanelBaseInput,
PropertyPanelBaseInputProps
} from '@noodl-core-ui/components/property-panel/PropertyPanelBaseInput';
export interface PropertyPanelTextInputProps extends Omit<PropertyPanelBaseInputProps<string>, 'type'> {
properties?: TSFixme;
}
export function PropertyPanelTextInput({
value,
isChanged,
isConnected,
onChange
}: PropertyPanelTextInputProps) {
const [displayedInputValue, setDisplayedInputValue] = useState(value);
function handleUpdate(inputValue: string) {
onChange && onChange(inputValue);
setDisplayedInputValue(inputValue);
}
useEffect(() => {
handleUpdate(value);
}, [value]);
return (
<PropertyPanelBaseInput
value={displayedInputValue}
type="text"
isChanged={isChanged}
isConnected={isConnected}
onChange={(value) => setDisplayedInputValue(String(value))}
onBlur={(e) => handleUpdate(displayedInputValue)}
onKeyDown={(e) => e.key === 'Enter' && handleUpdate(displayedInputValue)}
/>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelTextInput';

View File

@@ -0,0 +1,42 @@
@use '../../../styles/scss-variables/length.scss';
.Root {
font-family: var(--font-family);
color: var(--theme-color-fg-default);
font-size: 12px;
display: flex;
}
.Option {
padding: length.$property-panel-input-padding;
position: relative;
cursor: pointer;
background-color: var(--theme-color-bg-3);
user-select: none;
display: flex;
flex-grow: 1;
justify-content: center;
&:not(:last-child) {
border-right: 2px solid var(--theme-color-bg-2);
}
&:hover {
color: var(--theme-color-fg-highlight);
}
&.is-selected {
background-color: var(--theme-color-bg-1);
color: var(--theme-color-fg-highlight);
}
&.is-disabled {
color: var(--theme-color-fg-muted);
cursor: not-allowed;
}
}
.Input {
position: absolute;
opacity: 0;
}

View File

@@ -0,0 +1,38 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { PropertyPanelTextRadioInput } from './PropertyPanelTextRadioInput';
export default {
title: 'Property Panel/Radio',
component: PropertyPanelTextRadioInput,
argTypes: {}
} as ComponentMeta<typeof PropertyPanelTextRadioInput>;
const Template: ComponentStory<typeof PropertyPanelTextRadioInput> = (args) => (
<div style={{ width: 280 }}>
<PropertyPanelTextRadioInput {...args} />
</div>
);
export const Common = Template.bind({});
Common.args = {
value: 'one',
properties: {
options: [
{
label: 'One',
value: 'one'
},
{
label: 'Two',
value: 'two'
},
{
label: 'Disabled',
value: 'three',
isDisabled: true
}
]
}
};

View File

@@ -0,0 +1,49 @@
import classNames from 'classnames';
import React, { useMemo } from 'react';
import { PropertyPanelBaseInputProps } from '@noodl-core-ui/components/property-panel/PropertyPanelBaseInput';
import css from './PropertyPanelTextRadioInput.module.scss';
interface PropertyPanelTextRadioProperties {
name: string;
options: {
label: string;
value: string | number;
isDisabled?: boolean;
}[];
}
export interface PropertyPanelTextRadioInputProps extends Omit<PropertyPanelBaseInputProps, 'type' | 'onClick'> {
onChange: (value: string | number) => void;
properties?: PropertyPanelTextRadioProperties;
}
export function PropertyPanelTextRadioInput({ value, properties, onChange }: PropertyPanelTextRadioInputProps) {
const timestamp = useMemo(() => Date.now().toString(), []);
return (
<div className={css['Root']}>
{properties?.options.map((option) => (
<label
className={classNames(
css['Option'],
value === option.value && css['is-selected'],
option.isDisabled && css['is-disabled']
)}
>
<input
className={css['Input']}
type="radio"
name={timestamp}
value={option.value}
checked={value === option.value}
onClick={() => onChange(option.value)}
disabled={option.isDisabled}
/>
{option.label}
</label>
))}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './PropertyPanelTextRadioInput';

View File

@@ -0,0 +1,8 @@
Components are very basic at the moment, bnut here is the philosophy
- everything is not properly styled yet, and not all components are built, but at least its a start :D
- components are isoloated from rest of design system and are not using any components outside of components/property-panel. this is so that we can bundle them separately for the future editor SDK. maybe this has to be rethought in the future.
- PropertyPanelInput takes a config object and returns the type of input specified
- all components are controlled
- value is sent through value prop, other details are sent with the properties prop
- the typable inputs save the value in a displayedInputValue state to allow for a decoupling of updating the value and showing it. this is done to allow manipulating the value before storing it. storing is done on enter press or blur.