diff --git a/.changeset/form-fields-redesign.md b/.changeset/form-fields-redesign.md new file mode 100644 index 000000000..80716dc62 --- /dev/null +++ b/.changeset/form-fields-redesign.md @@ -0,0 +1,6 @@ +--- +'@workflowbuilder/ui': major +'@workflowbuilder/sdk': minor +--- + +Input and TextArea now use `state` instead of `error`, letter-based sizes, `prefixIcon`/`suffixIcon` instead of `startAdornment`/`endAdornment`, and an optional clear action. Fields now provide associated label and helper text composition, while NumberField adds nullable values and bounded keyboard and stepper controls. diff --git a/apps/docs/scripts/generate-ui-api.mjs b/apps/docs/scripts/generate-ui-api.mjs index 72918571d..ac9e83ac4 100644 --- a/apps/docs/scripts/generate-ui-api.mjs +++ b/apps/docs/scripts/generate-ui-api.mjs @@ -72,7 +72,9 @@ function findTypeByName(root, name, warnings) { for (const child of node.children ?? []) walk(child); })(root); if (matches.length > 1 && warnings) { - warnings.push(`type name "${name}" is ambiguous (${matches.length} declarations) - the table would document whichever TypeDoc emitted first`); + warnings.push( + `type name "${name}" is ambiguous (${matches.length} declarations) - the table would document whichever TypeDoc emitted first`, + ); } return matches[0] ?? null; } @@ -270,8 +272,7 @@ function collectVariantProps(propsTypeNames, project, byId, warnings, slug, cont const sharedByAll = occurrences.length === perVariant.length && distinctTypes.size === 1; // Required in every variant, else the table documents an impossible call. - const requiredEverywhere = - occurrences.length === perVariant.length && occurrences.every((o) => o.prop.required); + const requiredEverywhere = occurrences.length === perVariant.length && occurrences.every((o) => o.prop.required); const requiredInItsVariants = !requiredEverywhere && occurrences.every((o) => o.prop.required); const base = occurrences[0].prop; @@ -299,7 +300,7 @@ function collectVariantProps(propsTypeNames, project, byId, warnings, slug, cont return merged; } -function extractCssVariables(directory, warnings, slug) { +function extractCssVariables(directory, cssSources, warnings, slug) { // No directory - the entry documents an API, not a styled component. if (!directory) return []; @@ -316,11 +317,17 @@ function extractCssVariables(directory, warnings, slug) { const files = globSync('**/*.css', { cwd: abs }) .filter((file) => !nestedPrefixes.some((prefix) => file.startsWith(prefix))) - .sort(); + .sort() + .map((file) => path.resolve(abs, file)); + for (const source of cssSources ?? []) { + const sourcePath = path.resolve(uiSource, source); + if (existsSync(sourcePath)) files.push(sourcePath); + else warnings.push(`"${slug}": CSS source ${source} does not exist`); + } const seen = new Set(); const variables = []; for (const file of files) { - const css = readFileSync(path.resolve(abs, file), 'utf8'); + const css = readFileSync(file, 'utf8'); const re = /(--ax-public-[\w-]+)\s*:\s*([^;]*?)(?:\/\*\s*(.*?)\s*\*\/)?\s*;/g; let m; while ((m = re.exec(css))) { @@ -374,9 +381,9 @@ async function main() { let props = []; const context = { warnings, slug: component.slug }; if (Array.isArray(component.propsType)) { - props = [...collectVariantProps(component.propsType, project, byId, warnings, component.slug, context).values()].sort( - (a, b) => a.name.localeCompare(b.name), - ); + props = [ + ...collectVariantProps(component.propsType, project, byId, warnings, component.slug, context).values(), + ].sort((a, b) => a.name.localeCompare(b.name)); } else if (component.propsType) { const typeNode = findTypeByName(project, component.propsType, warnings); if (typeNode) { @@ -392,7 +399,7 @@ async function main() { name: component.name, props, nativeElement: context.nativeElement ?? null, - cssVariables: extractCssVariables(component.dir, warnings, component.slug), + cssVariables: extractCssVariables(component.dir, component.cssSources, warnings, component.slug), }; } diff --git a/apps/docs/scripts/ui-components.mjs b/apps/docs/scripts/ui-components.mjs index 8abe83075..ec3f49a6a 100644 --- a/apps/docs/scripts/ui-components.mjs +++ b/apps/docs/scripts/ui-components.mjs @@ -18,9 +18,22 @@ export const COMPONENTS = [ { slug: 'collapsible', name: 'Collapsible', propsType: 'CollapsibleProps', dir: 'collapsible' }, { slug: 'date-picker', name: 'DatePicker', propsType: 'DatePickerProps', dir: 'date-picker' }, { slug: 'icon-switch', name: 'IconSwitch', propsType: 'IconSwitchProps', dir: 'switch/icon-switch' }, - { slug: 'input', name: 'Input', propsType: 'InputProps', dir: 'input' }, + { + slug: 'input', + name: 'Input', + propsType: 'InputProps', + dir: 'input', + cssSources: ['shared/styles/field-control-size.module.css'], + }, { slug: 'menu', name: 'Menu', propsType: 'MenuProps', dir: 'menu' }, { slug: 'modal', name: 'Modal', propsType: 'ModalProps', dir: 'modal' }, + { + slug: 'number-field', + name: 'NumberField', + propsType: 'NumberFieldProps', + dir: 'number-field', + cssSources: ['shared/styles/field-control-size.module.css'], + }, { slug: 'nav-button', name: 'NavButton', @@ -39,7 +52,13 @@ export const COMPONENTS = [ { slug: 'snackbar', name: 'Snackbar', propsType: 'SnackbarProps', dir: 'snackbar' }, { slug: 'status', name: 'Status', propsType: 'StatusProps', dir: 'status' }, { slug: 'switch', name: 'Switch', propsType: 'BaseSwitchProps', dir: 'switch' }, - { slug: 'text-area', name: 'TextArea', propsType: 'TextAreaProps', dir: 'text-area' }, + { + slug: 'text-area', + name: 'TextArea', + propsType: 'TextAreaProps', + dir: 'text-area', + cssSources: ['shared/styles/field-control-size.module.css'], + }, { slug: 'tooltip', name: 'Tooltip', propsType: 'TooltipProps', dir: 'tooltip' }, // Diagram components. { slug: 'node-icon', name: 'NodeIcon', propsType: 'NodeIconProps', dir: 'node/node-icon' }, diff --git a/apps/docs/src/components/ui-examples/input.tsx b/apps/docs/src/components/ui-examples/input.tsx index f1d4cc4b2..c0153de99 100644 --- a/apps/docs/src/components/ui-examples/input.tsx +++ b/apps/docs/src/components/ui-examples/input.tsx @@ -1,3 +1,4 @@ +import { MagnifyingGlass } from '@phosphor-icons/react'; import { Input } from '@workflowbuilder/ui'; import { useState } from 'react'; @@ -8,7 +9,37 @@ export function InputExample() { return ( - setValue(event.target.value)} /> +
+ } + placeholder="Large input" + value={value} + onChange={(event) => setValue(event.target.value)} + onClear={() => setValue('')} + /> + setValue(event.target.value)} + /> + setValue(event.target.value)} + /> + +
); } diff --git a/apps/docs/src/components/ui-examples/number-field.tsx b/apps/docs/src/components/ui-examples/number-field.tsx new file mode 100644 index 000000000..fa82f2746 --- /dev/null +++ b/apps/docs/src/components/ui-examples/number-field.tsx @@ -0,0 +1,22 @@ +import { NumberField } from '@workflowbuilder/ui'; +import { useState } from 'react'; + +import { ComponentPreview } from './component-preview'; + +export function NumberFieldExample() { + const [value, setValue] = useState(3); + + return ( + + + + ); +} diff --git a/apps/docs/src/components/ui-examples/text-area.tsx b/apps/docs/src/components/ui-examples/text-area.tsx index d0d45749d..175405c5a 100644 --- a/apps/docs/src/components/ui-examples/text-area.tsx +++ b/apps/docs/src/components/ui-examples/text-area.tsx @@ -8,7 +8,27 @@ export function TextAreaExample() { return ( -