Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/form-fields-redesign.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 17 additions & 10 deletions apps/docs/scripts/generate-ui-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 [];

Expand All @@ -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))) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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),
};
}

Expand Down
23 changes: 21 additions & 2 deletions apps/docs/scripts/ui-components.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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' },
Expand Down
33 changes: 32 additions & 1 deletion apps/docs/src/components/ui-examples/input.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MagnifyingGlass } from '@phosphor-icons/react';
import { Input } from '@workflowbuilder/ui';
import { useState } from 'react';

Expand All @@ -8,7 +9,37 @@ export function InputExample() {

return (
<ComponentPreview>
<Input placeholder="Type something" value={value} onChange={(event) => setValue(event.target.value)} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--wb-space-100)' }}>
<Input
size="l"
label="Search"
prefixIcon={<MagnifyingGlass />}
placeholder="Large input"
value={value}
onChange={(event) => setValue(event.target.value)}
onClear={() => setValue('')}
/>
<Input
size="m"
state="critical"
label="Project name"
helperText="Enter at least three characters"
isRequired
placeholder="Critical input"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
<Input
size="s"
state="success"
label="Display name"
helperText="Name is available"
placeholder="Successful input"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
<Input label="Identifier" size="xs" state="read-only" value={value || 'Read-only value'} />
</div>
</ComponentPreview>
);
}
22 changes: 22 additions & 0 deletions apps/docs/src/components/ui-examples/number-field.tsx
Original file line number Diff line number Diff line change
@@ -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<number | null>(3);

return (
<ComponentPreview>
<NumberField
label="Quantity"
helperText="Choose between 0 and 10"
value={value}
min={0}
max={10}
step={1}
onValueChange={setValue}
/>
</ComponentPreview>
);
}
22 changes: 21 additions & 1 deletion apps/docs/src/components/ui-examples/text-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,27 @@ export function TextAreaExample() {

return (
<ComponentPreview>
<TextArea placeholder="Multi-line input" value={value} onChange={(event) => setValue(event.target.value)} />
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--wb-space-100)' }}>
<TextArea
size="l"
label="Description"
placeholder="Large text area"
value={value}
onChange={(event) => setValue(event.target.value)}
onClear={() => setValue('')}
/>
<TextArea
size="m"
state="critical"
label="Summary"
helperText="A summary is required"
isRequired
placeholder="Critical text area"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
<TextArea label="Notes" size="s" state="read-only" value={value || 'Read-only value'} />
</div>
</ComponentPreview>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ live, interactive example plus the component's props and CSS variables.
- [Checkbox](/ui-library/ui-components/checkbox/) - Checked, unchecked, and indeterminate states.
- [Collapsible](/ui-library/ui-components/collapsible/) - Expand and collapse a content section.
- [DatePicker](/ui-library/ui-components/date-picker/) - Date selection with a calendar popover.
- [Input](/ui-library/ui-components/input/) - Text input with adornments and an error state.
- [Input](/ui-library/ui-components/input/) - Text input with icons and explicit field states.
- [Menu](/ui-library/ui-components/menu/) - Popup menu for dropdowns.
- [Modal](/ui-library/ui-components/modal/) - Dialog overlay with a backdrop.
- [NavButton](/ui-library/ui-components/nav-button/) - Compact icon / label navigation button.
- [NumberField](/ui-library/ui-components/number-field/) - Numeric input with stepper controls.
- [Radio](/ui-library/ui-components/radio/) - Radio button for a single choice.
- [SegmentPicker](/ui-library/ui-components/segment-picker/) - Segmented single-choice control.
- [Select](/ui-library/ui-components/select/) - Dropdown select.
Expand Down
22 changes: 19 additions & 3 deletions apps/docs/src/content/docs/ui-library/ui-components/input.mdx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
---
title: Input
description: A single-line text field with optional adornments and an error state.
description: A single-line text field with icons, explicit states, and four sizes.
---

import CssVariablesTable from '../../../../components/api/css-variables-table.astro';
import PropsTable from '../../../../components/api/props-table.astro';
import { InputExample } from '../../../../components/ui-examples/input';

An `Input` is a single-line text field for collecting short, free-form text. It
supports start and end adornments, an error state, and the standard input sizes.
supports prefix and suffix icons, a clear affordance, four letter sizes, and the
`default`, `critical`, `success`, and `read-only` states. A read-only field stays
focusable so its value can be selected and copied.

<InputExample client:visible />

Expand All @@ -20,7 +22,21 @@ import { useState } from 'react';

function Example() {
const [value, setValue] = useState('');
return <Input placeholder="Type something" value={value} onChange={(event) => setValue(event.target.value)} />;
const hasError = value.length > 0 && value.length < 3;

return (
<Input
size="m"
state={hasError ? 'critical' : 'default'}
label="Project name"
helperText={hasError ? 'Enter at least three characters' : 'Shown throughout the workspace'}
isRequired
placeholder="Type something"
value={value}
onChange={(event) => setValue(event.target.value)}
onClear={() => setValue('')}
/>
);
}
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: NumberField
description: A numeric input with keyboard stepping and always-visible stepper controls.
---

import CssVariablesTable from '../../../../components/api/css-variables-table.astro';
import PropsTable from '../../../../components/api/props-table.astro';
import { NumberFieldExample } from '../../../../components/ui-examples/number-field';

A `NumberField` accepts numeric values and keeps increment and decrement controls
visible on its trailing edge. Arrow keys and the stepper respect `min`, `max`,
and `step`.

<NumberFieldExample client:visible />

## Usage

```tsx
import { NumberField } from '@workflowbuilder/ui';
import { useState } from 'react';

function Example() {
const [value, setValue] = useState<number | null>(3);

return <NumberField label="Quantity" value={value} min={0} max={10} onValueChange={setValue} />;
}
```

## Props

<PropsTable slug="number-field" />

## CSS variables

<CssVariablesTable slug="number-field" />
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import PropsTable from '../../../../components/api/props-table.astro';
import { TextAreaExample } from '../../../../components/ui-examples/text-area';

A `TextArea` is a multi-line text field for longer, free-form input. It
auto-resizes to fit its content and supports row limits, an error state, and the
standard input sizes.
auto-resizes to fit its content and supports row limits, letter sizes, icon
slots, a clear affordance, and explicit validation and read-only states.

<TextAreaExample client:visible />

Expand All @@ -21,7 +21,17 @@ import { useState } from 'react';

function Example() {
const [value, setValue] = useState('');
return <TextArea placeholder="Multi-line input" value={value} onChange={(event) => setValue(event.target.value)} />;
return (
<TextArea
size="m"
state={value ? 'success' : 'default'}
label="Description"
helperText="Explain what this workflow does"
placeholder="Multi-line input"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
);
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function TextAreaControl(props: TextAreaControlProps) {
placeholder={placeholder}
onChange={onChange}
onBlur={onBlur}
size="medium"
size="m"
/>
</ControlWrapper>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function TextControl(props: TextControlProps) {
value={inputValue}
onChange={onChange}
onBlur={onBlur}
error={hasErrors}
state={hasErrors ? 'critical' : 'default'}
disabled={isDisabled}
placeholder={placeholder}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
position: relative;
}

.time-with-adornment {
position: relative;
min-width: 0;
}

.adornment--select {
position: absolute;
top: 50%;
Expand All @@ -40,3 +45,11 @@
transform: translateY(-50%);
z-index: 1;
}

.adornment--time {
position: absolute;
top: 50%;
right: var(--wb-space-150);
transform: translateY(-50%);
z-index: 1;
}
Loading
Loading