diff --git a/Documentation/CommandForm/calendar-field.md b/Documentation/CommandForm/calendar-field.md new file mode 100644 index 0000000..9749515 --- /dev/null +++ b/Documentation/CommandForm/calendar-field.md @@ -0,0 +1,38 @@ +# CalendarField + +`CalendarField` wraps the PrimeReact `Calendar` component for selecting date values. + +## Usage + +```tsx +import { CommandDialog } from '@cratis/components'; +import { CalendarField } from '@cratis/components/CommandForm'; + + setVisible(false)}> + + value={c => c.startDate} + placeholder="Select a date" + showIcon + dateFormat="mm/dd/yy" + /> + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `value` | `(instance: TCommand) => unknown` | - | **Required.** Accessor function that returns the bound property from the command instance. Pass the command type as the generic parameter for full type safety. | +| `placeholder` | `string` | - | Placeholder text shown when no date is selected. | +| `dateFormat` | `string` | PrimeReact default | Date display and parsing format used by the calendar input. | +| `showIcon` | `boolean` | `false` | Displays a calendar icon button next to the input. | +| `showTime` | `boolean` | `false` | Enables time selection in addition to date selection. | +| `hourFormat` | `'12' \| '24'` | `24` | Hour format when `showTime` is enabled. | +| `minDate` | `Date` | - | Minimum selectable date. | +| `maxDate` | `Date` | - | Maximum selectable date. | + +## Behavior + +- Default value is `null`. +- The field spans full width within its container. +- Validation state is reflected via the PrimeReact `invalid` flag. diff --git a/Documentation/CommandForm/chips-field.md b/Documentation/CommandForm/chips-field.md new file mode 100644 index 0000000..b127a89 --- /dev/null +++ b/Documentation/CommandForm/chips-field.md @@ -0,0 +1,36 @@ +# ChipsField + +`ChipsField` wraps the PrimeReact `Chips` component for collecting multiple text values in a single field. + +## Usage + +```tsx +import { CommandDialog } from '@cratis/components'; +import { ChipsField } from '@cratis/components/CommandForm'; + + setVisible(false)}> + + value={c => c.tags} + placeholder="Add tags and press Enter" + separator="," + addOnBlur + /> + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `value` | `(instance: TCommand) => unknown` | - | **Required.** Accessor function that returns the bound property from the command instance. Pass the command type as the generic parameter for full type safety. | +| `placeholder` | `string` | - | Advisory text shown when no chip values exist. | +| `max` | `number` | - | Maximum number of chips allowed. | +| `separator` | `string` | - | Adds a chip when the separator character is typed. | +| `addOnBlur` | `boolean` | `false` | Adds the current input as a chip when the field loses focus. | +| `allowDuplicate` | `boolean` | `true` | Controls whether duplicate chip values are allowed. | + +## Behavior + +- Default value is an empty array. +- The field spans full width within its container. +- Validation state is reflected via the PrimeReact `invalid` flag. diff --git a/Documentation/CommandForm/color-picker-field.md b/Documentation/CommandForm/color-picker-field.md new file mode 100644 index 0000000..c46defb --- /dev/null +++ b/Documentation/CommandForm/color-picker-field.md @@ -0,0 +1,31 @@ +# ColorPickerField + +`ColorPickerField` wraps the PrimeReact `ColorPicker` component for selecting colors. + +## Usage + +```tsx +import { CommandDialog } from '@cratis/components'; +import { ColorPickerField } from '@cratis/components/CommandForm'; + + setVisible(false)}> + + value={c => c.primaryColor} + title="Primary color" + /> + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `value` | `(instance: TCommand) => unknown` | - | **Required.** Accessor function that returns the bound property from the command instance. Pass the command type as the generic parameter for full type safety. | +| `inline` | `boolean` | `false` | Renders the picker inline instead of using an overlay. | +| `defaultColor` | `string` | `ff0000` | Fallback color shown by PrimeReact when the bound value is empty. | + +## Behavior + +- Default value is an empty string. +- Values are persisted as color strings (hex format in the default PrimeReact mode). +- Validation state is reflected by applying the `p-invalid` class when the field is invalid. diff --git a/Documentation/CommandForm/multi-select-field.md b/Documentation/CommandForm/multi-select-field.md new file mode 100644 index 0000000..a614cb8 --- /dev/null +++ b/Documentation/CommandForm/multi-select-field.md @@ -0,0 +1,48 @@ +# MultiSelectField + +`MultiSelectField` wraps the PrimeReact `MultiSelect` component for selecting multiple values from a list. + +## Usage + +```tsx +import { CommandDialog } from '@cratis/components'; +import { MultiSelectField } from '@cratis/components/CommandForm'; + +const categoryOptions = [ + { id: 'finance', label: 'Finance' }, + { id: 'operations', label: 'Operations' }, + { id: 'engineering', label: 'Engineering' }, +]; + + setVisible(false)}> + + value={c => c.categories} + options={categoryOptions} + optionLabel="label" + optionValue="id" + placeholder="Select categories" + display="chip" + filter + /> + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `value` | `(instance: TCommand) => unknown` | - | **Required.** Accessor function that returns the bound property from the command instance. Pass the command type as the generic parameter for full type safety. | +| `options` | `Array>` | - | **Required.** Array of option objects. | +| `optionLabel` | `string` | - | Property name in each option object used as the display text. | +| `optionValue` | `string` | - | Property name in each option object used as the bound value. | +| `placeholder` | `string` | - | Placeholder text shown when no options are selected. | +| `display` | `'comma' \| 'chip'` | PrimeReact default | How selected values are rendered in the input. | +| `maxSelectedLabels` | `number` | PrimeReact default | Maximum number of labels to render before collapsing. | +| `filter` | `boolean` | PrimeReact default | Enables filtering in the options panel. | +| `showClear` | `boolean` | `false` | Displays a clear icon to reset selected values. | + +## Behavior + +- Default value is an empty array. +- The field spans full width within its container. +- Validation state is reflected via the PrimeReact `invalid` flag. diff --git a/Documentation/CommandForm/toc.yml b/Documentation/CommandForm/toc.yml index 44a724d..8584119 100644 --- a/Documentation/CommandForm/toc.yml +++ b/Documentation/CommandForm/toc.yml @@ -1,11 +1,19 @@ - name: Overview href: index.md +- name: CalendarField + href: calendar-field.md - name: CheckboxField href: checkbox-field.md +- name: ChipsField + href: chips-field.md +- name: ColorPickerField + href: color-picker-field.md - name: DropdownField href: dropdown-field.md - name: InputTextField href: input-text-field.md +- name: MultiSelectField + href: multi-select-field.md - name: NumberField href: number-field.md - name: SliderField diff --git a/Source/CommandForm/fields/CalendarField.tsx b/Source/CommandForm/fields/CalendarField.tsx new file mode 100644 index 0000000..fcd3923 --- /dev/null +++ b/Source/CommandForm/fields/CalendarField.tsx @@ -0,0 +1,39 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; +import { Calendar } from 'primereact/calendar'; +import React from 'react'; + +interface CalendarFieldComponentProps extends WrappedFieldProps { + placeholder?: string; + dateFormat?: string; + showIcon?: boolean; + showTime?: boolean; + hourFormat?: '12' | '24'; + minDate?: Date; + maxDate?: Date; +} + +export const CalendarField = asCommandFormField( + (props) => ( + props.onChange(e.value ?? null)} + onBlur={props.onBlur} + invalid={props.invalid} + placeholder={props.placeholder} + dateFormat={props.dateFormat} + showIcon={props.showIcon} + showTime={props.showTime} + hourFormat={props.hourFormat} + minDate={props.minDate} + maxDate={props.maxDate} + className="w-full" + /> + ), + { + defaultValue: null, + extractValue: (e: unknown) => e instanceof Date ? e : null + } +); diff --git a/Source/CommandForm/fields/ChipsField.tsx b/Source/CommandForm/fields/ChipsField.tsx new file mode 100644 index 0000000..d2e09b1 --- /dev/null +++ b/Source/CommandForm/fields/ChipsField.tsx @@ -0,0 +1,41 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; +import { Chips } from 'primereact/chips'; +import React from 'react'; + +interface ChipsFieldComponentProps extends WrappedFieldProps { + placeholder?: string; + max?: number; + separator?: string; + addOnBlur?: boolean; + allowDuplicate?: boolean; +} + +export const ChipsField = asCommandFormField( + (props) => ( + props.onChange(e.value ?? [])} + onBlur={props.onBlur} + invalid={props.invalid} + placeholder={props.placeholder} + max={props.max} + separator={props.separator} + addOnBlur={props.addOnBlur} + allowDuplicate={props.allowDuplicate} + className="w-full" + /> + ), + { + defaultValue: [], + extractValue: (e: unknown) => { + if (!Array.isArray(e)) { + return []; + } + + return e.filter((item): item is string => typeof item === 'string'); + } + } +); diff --git a/Source/CommandForm/fields/ColorPickerField.tsx b/Source/CommandForm/fields/ColorPickerField.tsx new file mode 100644 index 0000000..38e8266 --- /dev/null +++ b/Source/CommandForm/fields/ColorPickerField.tsx @@ -0,0 +1,28 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; +import { ColorPicker } from 'primereact/colorpicker'; +import React from 'react'; + +interface ColorPickerFieldComponentProps extends WrappedFieldProps { + inline?: boolean; + defaultColor?: string; +} + +export const ColorPickerField = asCommandFormField( + (props) => ( + props.onChange(typeof e.value === 'string' ? e.value : '')} + onBlur={props.onBlur} + inline={props.inline} + defaultColor={props.defaultColor} + className={props.invalid ? 'p-invalid' : undefined} + /> + ), + { + defaultValue: '', + extractValue: (e: unknown) => typeof e === 'string' ? e : '' + } +); diff --git a/Source/CommandForm/fields/Fields.stories.tsx b/Source/CommandForm/fields/Fields.stories.tsx index c31aa2b..55fa4bc 100644 --- a/Source/CommandForm/fields/Fields.stories.tsx +++ b/Source/CommandForm/fields/Fields.stories.tsx @@ -14,7 +14,11 @@ import { TextAreaField, CheckboxField, SliderField, - DropdownField + DropdownField, + CalendarField, + ColorPickerField, + MultiSelectField, + ChipsField } from './index'; const meta: Meta = { @@ -40,6 +44,10 @@ class FormFieldsCommand extends Command { new PropertyDescriptor('checkbox', Boolean), new PropertyDescriptor('slider', Number), new PropertyDescriptor('dropdown', String), + new PropertyDescriptor('calendarDate', Date), + new PropertyDescriptor('color', String), + new PropertyDescriptor('multiSelect', Array), + new PropertyDescriptor('chips', Array), ]; textInput = ''; @@ -50,6 +58,10 @@ class FormFieldsCommand extends Command { checkbox = false; slider = 50; dropdown = ''; + calendarDate: Date | null = null; + color = ''; + multiSelect: Array = []; + chips: string[] = []; constructor() { super(Object, false); @@ -68,7 +80,11 @@ class FormFieldsCommand extends Command { 'textArea', 'checkbox', 'slider', - 'dropdown' + 'dropdown', + 'calendarDate', + 'color', + 'multiSelect', + 'chips' ]; } } @@ -91,6 +107,13 @@ const dropdownOptions = [ { id: 'option4', name: 'Option 4' } ]; +const multiSelectOptions = [ + { id: 'feature1', name: 'Feature 1' }, + { id: 'feature2', name: 'Feature 2' }, + { id: 'feature3', name: 'Feature 3' }, + { id: 'feature4', name: 'Feature 4' } +]; + export const AllFields: Story = { render: () => { const [validationState, setValidationState] = useState<{ @@ -118,6 +141,10 @@ export const AllFields: Story = { checkbox: false, slider: 50, dropdown: '', + calendarDate: null, + color: '10b981', + multiSelect: ['feature1', 'feature3'], + chips: ['alpha', 'beta'], }} onFieldChange={async (command, fieldName) => { // Validate on field change @@ -245,6 +272,66 @@ export const AllFields: Story = { + +

Calendar

+ + + value={c => c.calendarDate} + title="Calendar Field" + description="PrimeReact Calendar component for selecting dates" + placeholder="Select a date" + showIcon + dateFormat="mm/dd/yy" + /> +
+ + + + +

Color Picker

+ + + value={c => c.color} + title="Color Picker Field" + description="PrimeReact ColorPicker for selecting hex colors" + /> +
+ + + + +

MultiSelect

+ + + value={c => c.multiSelect} + title="MultiSelect Field" + description="PrimeReact MultiSelect for selecting multiple options" + placeholder="Choose one or more features" + options={multiSelectOptions} + optionValue="id" + optionLabel="name" + display="chip" + filter + showClear + /> +
+ + + + +

Chips

+ + + value={c => c.chips} + title="Chips Field" + description="PrimeReact Chips for entering multiple text values" + placeholder="Add tags and press Enter" + separator="," + /> +
+ + +

Slider

@@ -449,3 +536,97 @@ export const CheckboxFieldExample: Story = { ); } }; + +export const CalendarFieldExample: Story = { + render: () => { + return ( + +

CalendarField

+

PrimeReact Calendar for selecting date values.

+ + + command={FormFieldsCommand} + initialValues={{ calendarDate: null }} + > + + value={c => c.calendarDate} + title="Start Date" + placeholder="Pick a date" + showIcon + /> + +
+ ); + } +}; + +export const ColorPickerFieldExample: Story = { + render: () => { + return ( + +

ColorPickerField

+

PrimeReact ColorPicker for selecting hex colors.

+ + + command={FormFieldsCommand} + initialValues={{ color: '0ea5e9' }} + > + + value={c => c.color} + title="Brand Color" + /> + +
+ ); + } +}; + +export const MultiSelectFieldExample: Story = { + render: () => { + return ( + +

MultiSelectField

+

PrimeReact MultiSelect for selecting multiple options from a list.

+ + + command={FormFieldsCommand} + initialValues={{ multiSelect: ['feature2'] }} + > + + value={c => c.multiSelect} + title="Features" + placeholder="Select features" + options={multiSelectOptions} + optionValue="id" + optionLabel="name" + display="chip" + filter + /> + +
+ ); + } +}; + +export const ChipsFieldExample: Story = { + render: () => { + return ( + +

ChipsField

+

PrimeReact Chips for entering and managing multiple text values.

+ + + command={FormFieldsCommand} + initialValues={{ chips: ['urgent'] }} + > + + value={c => c.chips} + title="Tags" + placeholder="Type a tag and press Enter" + addOnBlur + /> + +
+ ); + } +}; diff --git a/Source/CommandForm/fields/MultiSelectField.tsx b/Source/CommandForm/fields/MultiSelectField.tsx new file mode 100644 index 0000000..518a46a --- /dev/null +++ b/Source/CommandForm/fields/MultiSelectField.tsx @@ -0,0 +1,47 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; +import { MultiSelect } from 'primereact/multiselect'; +import React from 'react'; + +interface MultiSelectFieldComponentProps extends WrappedFieldProps> { + options: Array>; + optionValue?: string; + optionLabel?: string; + placeholder?: string; + display?: 'comma' | 'chip'; + maxSelectedLabels?: number; + filter?: boolean; + showClear?: boolean; +} + +export const MultiSelectField = asCommandFormField( + (props) => ( + | undefined }) => props.onChange(e.value ?? [])} + onBlur={props.onBlur} + options={props.options} + optionValue={props.optionValue} + optionLabel={props.optionLabel} + placeholder={props.placeholder} + display={props.display} + maxSelectedLabels={props.maxSelectedLabels} + filter={props.filter} + showClear={props.showClear} + invalid={props.invalid} + className="w-full" + /> + ), + { + defaultValue: [], + extractValue: (e: unknown) => { + if (!Array.isArray(e)) { + return []; + } + + return e.filter((item): item is string | number => typeof item === 'string' || typeof item === 'number'); + } + } +); diff --git a/Source/CommandForm/fields/index.ts b/Source/CommandForm/fields/index.ts index 1100f1b..9d7c9ca 100644 --- a/Source/CommandForm/fields/index.ts +++ b/Source/CommandForm/fields/index.ts @@ -7,3 +7,7 @@ export * from './CheckboxField'; export * from './TextAreaField'; export * from './DropdownField'; export * from './SliderField'; +export * from './CalendarField'; +export * from './ColorPickerField'; +export * from './MultiSelectField'; +export * from './ChipsField';