diff --git a/.changeset/form-fields-redesign.md b/.changeset/form-fields-redesign.md new file mode 100644 index 000000000..38aa5c80d --- /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. Their shared field composition now provides associated labels and helper text. 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 d8e15e5cf..72627dafc 100644 --- a/apps/docs/scripts/ui-components.mjs +++ b/apps/docs/scripts/ui-components.mjs @@ -18,7 +18,17 @@ 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/components/field/field.module.css', + 'shared/styles/field-control-height.module.css', + 'shared/styles/field-control-size.module.css', + ], + }, { slug: 'menu', name: 'Menu', propsType: 'MenuProps', dir: 'menu' }, { slug: 'modal', name: 'Modal', propsType: 'ModalProps', dir: 'modal' }, { @@ -39,7 +49,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/components/field/field.module.css', '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..4ef9748e6 100644 --- a/apps/docs/src/components/ui-examples/input.tsx +++ b/apps/docs/src/components/ui-examples/input.tsx @@ -1,14 +1,48 @@ +import { MagnifyingGlass } from '@phosphor-icons/react'; import { Input } from '@workflowbuilder/ui'; import { useState } from 'react'; import { ComponentPreview } from './component-preview'; export function InputExample() { - const [value, setValue] = useState(''); + const [search, setSearch] = useState(''); + const [projectName, setProjectName] = useState(''); + const [displayName, setDisplayName] = useState(''); return ( - setValue(event.target.value)} /> +
+ } + placeholder="Large input" + value={search} + onChange={(event) => setSearch(event.target.value)} + onClear={() => setSearch('')} + clearLabel="Clear search" + /> + setProjectName(event.target.value)} + /> + setDisplayName(event.target.value)} + /> + +
); } diff --git a/apps/docs/src/components/ui-examples/text-area.tsx b/apps/docs/src/components/ui-examples/text-area.tsx index d0d45749d..31a73258a 100644 --- a/apps/docs/src/components/ui-examples/text-area.tsx +++ b/apps/docs/src/components/ui-examples/text-area.tsx @@ -4,11 +4,33 @@ import { useState } from 'react'; import { ComponentPreview } from './component-preview'; export function TextAreaExample() { - const [value, setValue] = useState(''); + const [description, setDescription] = useState(''); + const [summary, setSummary] = useState(''); return ( -