mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-13 07:42:55 +01:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelBaseInput';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelButton'
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = {};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelCheckbox';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelIconRadioInput';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'),
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelInput'
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelLengthUnitInput';
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 = {};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelMarginPadding';
|
||||
@@ -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 = {};
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelNumberInput';
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelPasswordInput';
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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' };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './PropertyPanelSection';
|
||||
// TODO: Delete this component in favour of the other Section?
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelSelectInput';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelSliderInput';
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelTextInput';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PropertyPanelTextRadioInput';
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user