diff --git a/.gitignore b/.gitignore index 3c2d2a3a0..f0626771d 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,8 @@ CLAUDE.local.md # and is copied into this dir by a tiny Astro integration after typedoc # generation (see astro.config.mjs). apps/docs/src/content/docs/api/ + +# UI Library props + CSS-variable data, generated from @workflowbuilder/ui by +# apps/docs/scripts/generate-ui-api.mjs (TypeDoc + CSS extraction) on every +# docs build / dev. Source of truth is the library, so keep it out of git. +apps/docs/src/generated/ diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs index 599ee9638..71990073c 100644 --- a/apps/docs/astro.config.mjs +++ b/apps/docs/astro.config.mjs @@ -1,6 +1,7 @@ import { copyFileSync, mkdirSync } from 'node:fs'; import path from 'node:path'; +import react from '@astrojs/react'; import starlight from '@astrojs/starlight'; import umami from '@yeskunall/astro-umami'; import { defineConfig, passthroughImageService } from 'astro/config'; @@ -70,6 +71,7 @@ export default defineConfig({ }, integrations: [ icon(), + react(), umami({ id: UMAMI_WEBSITE_ID }), starlight({ plugins: [ @@ -113,7 +115,12 @@ export default defineConfig({ }, }), ], - customCss: ['./src/styles/custom.css'], + // `@workflowbuilder/ui` styles are safe to load globally: styles.css is + // just the @layer order + one :root var + opt-in `.ax-public-*` typography + // classes (no global reset), and tokens.css only defines `--ax-*` custom + // properties keyed on `html[data-theme]` — which Starlight already toggles, + // so the live component showcases follow the docs light/dark theme. + customCss: ['./src/styles/custom.css', '@workflowbuilder/ui/styles.css', '@workflowbuilder/ui/tokens.css'], components: { Head: './src/components/head.astro', Search: './src/components/search.astro', @@ -159,6 +166,15 @@ export default defineConfig({ { label: 'Node Schemas', autogenerate: { directory: 'node-schemas' } }, { label: 'Built-in Nodes', autogenerate: { directory: 'nodes' } }, { label: 'Plugins', autogenerate: { directory: 'plugins' } }, + { + label: 'UI Library', + items: [ + { label: 'Overview', link: '/ui-library/overview/' }, + { label: 'Design tokens', link: '/ui-library/design-tokens/' }, + { label: 'UI Components', autogenerate: { directory: 'ui-library/ui-components' } }, + { label: 'Diagram Components', autogenerate: { directory: 'ui-library/diagram-components' } }, + ], + }, // API Reference — pages auto-generated by `starlight-typedoc` from // packages/sdk's barrel into `src/content/docs/api//`. // Folder names match the `@category` tag in source TSDoc verbatim. diff --git a/apps/docs/package.json b/apps/docs/package.json index 9fe55bfe4..a1602b741 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -4,8 +4,9 @@ "private": true, "type": "module", "scripts": { - "dev": "pnpm clean:typedoc && astro dev", - "build": "pnpm clean:typedoc && node scripts/check-sidebar-categories.mjs && astro build && node scripts/touch-distribution-index.mjs", + "dev": "pnpm clean:typedoc && pnpm generate:ui-api && astro dev", + "build": "pnpm clean:typedoc && pnpm generate:ui-api && node scripts/check-sidebar-categories.mjs && astro build && node scripts/touch-distribution-index.mjs", + "generate:ui-api": "node scripts/generate-ui-api.mjs", "clean:typedoc": "node -e \"import('node:fs').then(fs => fs.rmSync('src/content/docs/api', { recursive: true, force: true }))\"", "preview": "astro preview", "typecheck": "astro check", @@ -14,18 +15,25 @@ "lint:fix": "eslint . --fix" }, "dependencies": { + "@astrojs/react": "^4.4.0", "@astrojs/starlight": "^0.37.6", + "@base-ui/react": "catalog:", "@iconify-json/ph": "^1.2.2", "@workflowbuilder/sdk": "workspace:*", + "@workflowbuilder/ui": "workspace:*", "@yeskunall/astro-umami": "^0.0.7", "astro": "^5.18.0", "astro-icon": "^1.1.5", "marked": "^15.0.0", + "react": "catalog:", + "react-dom": "catalog:", "rehype-external-links": "^3.0.0", "starlight-image-zoom": "^0.13.2" }, "devDependencies": { "@astrojs/check": "^0.9.6", + "@types/react": "catalog:", + "@types/react-dom": "^19.1.0", "eslint-plugin-astro": "^1.3.1", "starlight-typedoc": "^0.21.3", "typedoc": "^0.28.9", diff --git a/apps/docs/scripts/generate-ui-api.mjs b/apps/docs/scripts/generate-ui-api.mjs new file mode 100644 index 000000000..08f7e20ce --- /dev/null +++ b/apps/docs/scripts/generate-ui-api.mjs @@ -0,0 +1,223 @@ +/* + * Generates `src/generated/ui-api.json` for the UI Library docs. + * + * Props are extracted with TypeDoc (source of truth: the component prop types + * in `@workflowbuilder/ui`); CSS variables are extracted from each component's + * stylesheets. The per-component docs pages render this JSON, so the Props and + * CSS variables tables never drift from source. Run by `pnpm generate:ui-api` + * and as a prebuild step in `dev` / `build`. + */ +import { execFile } from 'node:child_process'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { globSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const docsRoot = path.resolve(here, '..'); +const repoRoot = path.resolve(docsRoot, '../..'); +const uiSrc = path.resolve(repoRoot, 'packages/ui/src'); +const outFile = path.resolve(docsRoot, 'src/generated/ui-api.json'); +const tdJson = path.resolve(docsRoot, 'node_modules/.cache/ui-typedoc.json'); + +// slug -> { name, propsType, dir }. `propsType` is the exported prop type the +// component accepts; `dir` is the component folder under packages/ui/src/components. +const COMPONENTS = [ + { slug: 'accordion', name: 'Accordion', propsType: 'AccordionProps', dir: 'accordion' }, + { slug: 'avatar', name: 'Avatar', propsType: 'AvatarProps', dir: 'avatar' }, + { slug: 'button', name: 'Button', propsType: 'BaseRegularButtonProps', dir: 'button' }, + { slug: 'checkbox', name: 'Checkbox', propsType: 'CheckboxProps', dir: 'checkbox' }, + { slug: 'date-picker', name: 'DatePicker', propsType: 'DatePickerProps', dir: 'date-picker' }, + { slug: 'input', name: 'Input', propsType: 'InputProps', dir: 'input' }, + { slug: 'menu', name: 'Menu', propsType: 'MenuProps', dir: 'menu' }, + { slug: 'modal', name: 'Modal', propsType: 'ModalProps', dir: 'modal' }, + { slug: 'nav-button', name: 'NavButton', propsType: 'NavBaseButtonProps', dir: 'button/nav-button' }, + { slug: 'radio', name: 'Radio', propsType: 'RadioProps', dir: 'radio-button' }, + { slug: 'segment-picker', name: 'SegmentPicker', propsType: 'SegmentPickerProps', dir: 'segment-picker' }, + { slug: 'select', name: 'Select', propsType: 'SelectBaseProps', dir: 'select' }, + { slug: 'separator', name: 'Separator', propsType: null, dir: 'separator' }, + { 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: 'tooltip', name: 'Tooltip', propsType: 'TooltipProps', dir: 'tooltip' }, + // Diagram components (props extracted the same way; NodePanel is a compound + // component documented narratively, so it has no flat props entry here). + { slug: 'node-icon', name: 'NodeIcon', propsType: 'NodeIconProps', dir: 'node/node-icon' }, + { slug: 'node-description', name: 'NodeDescription', propsType: 'NodeDescriptionProps', dir: 'node/node-description' }, + { slug: 'node-as-port-wrapper', name: 'NodeAsPortWrapper', propsType: 'NodeAsPortWrapperProps', dir: 'node/node-as-port-wrapper' }, + { slug: 'edge', name: 'EdgeLabel', propsType: 'EdgeLabelProps', dir: 'edge' }, +]; + +async function runTypedoc() { + await mkdir(path.dirname(tdJson), { recursive: true }); + const bin = path.resolve(docsRoot, 'node_modules/.bin/typedoc'); + await promisify(execFile)( + bin, + [ + '--json', tdJson, + '--entryPoints', path.resolve(uiSrc, 'index.ts'), + '--tsconfig', path.resolve(repoRoot, 'packages/ui/tsconfig.json'), + '--excludeExternals', '--excludePrivate', '--skipErrorChecking', '--logLevel', 'Error', + ], + { cwd: repoRoot, maxBuffer: 64 * 1024 * 1024 }, + ); + return JSON.parse(await readFile(tdJson, 'utf8')); +} + +function indexById(root) { + const byId = new Map(); + (function walk(node) { + if (node && typeof node.id === 'number') byId.set(node.id, node); + for (const child of node.children ?? []) walk(child); + })(root); + return byId; +} + +function findTypeByName(root, name) { + let found = null; + (function walk(node) { + if (found) return; + // 2097152 = TypeAlias, 256 = Interface + if (node.name === name && (node.kind === 2097152 || node.kind === 256)) found = node; + for (const child of node.children ?? []) walk(child); + })(root); + return found; +} + +function typeToString(t, byId, depth = 0) { + if (!t || depth > 6) return 'unknown'; + switch (t.type) { + case 'intrinsic': return t.name; + case 'literal': return typeof t.value === 'string' ? `'${t.value}'` : String(t.value); + case 'reference': { + const args = t.typeArguments?.length ? `<${t.typeArguments.map((a) => typeToString(a, byId, depth + 1)).join(', ')}>` : ''; + return `${t.name}${args}`; + } + case 'union': return t.types.map((x) => typeToString(x, byId, depth + 1)).join(' | '); + case 'intersection': return t.types.map((x) => typeToString(x, byId, depth + 1)).join(' & '); + case 'array': return `${typeToString(t.elementType, byId, depth + 1)}[]`; + case 'tuple': return `[${(t.elements ?? []).map((x) => typeToString(x, byId, depth + 1)).join(', ')}]`; + case 'reflection': { + const sig = t.declaration?.signatures?.[0]; + if (sig) { + const params = (sig.parameters ?? []).map((p) => `${p.name}: ${typeToString(p.type, byId, depth + 1)}`).join(', '); + return `(${params}) => ${typeToString(sig.type, byId, depth + 1)}`; + } + return '{ … }'; + } + case 'indexedAccess': return `${typeToString(t.objectType, byId, depth + 1)}[${typeToString(t.indexType, byId, depth + 1)}]`; + case 'templateLiteral': return 'string'; + case 'query': return typeToString(t.queryType, byId, depth + 1); + case 'predicate': return 'boolean'; + case 'typeOperator': return `${t.operator} ${typeToString(t.target, byId, depth + 1)}`; + default: return t.name ?? 'unknown'; + } +} + +function summaryText(comment) { + if (!comment?.summary) return ''; + return comment.summary.map((s) => s.text).join('').trim(); +} + +function defaultTag(comment) { + const tag = (comment?.blockTags ?? []).find((b) => b.tag === '@default' || b.tag === '@defaultValue'); + if (!tag) return null; + let value = tag.content.map((c) => c.text).join('').trim(); + value = value.replace(/^```[a-z]*\s*/i, '').replace(/\s*```$/, '').trim(); // strip ```ts … ``` fences + value = value.replace(/^`+|`+$/g, '').trim(); // strip inline backticks + return value || null; +} + +// Collect own properties from a prop type alias / interface, walking +// intersections and skipping referenced (extended / native HTML) members. +function collectProps(typeNode, byId, acc = new Map()) { + if (!typeNode) return acc; + // TypeAlias / Interface: plain object members land directly on `.children`; + // computed types (intersections etc.) land on `.type`. + if (typeNode.kind === 2097152 || typeNode.kind === 256) { + if (typeNode.children?.length) { + for (const child of typeNode.children) addProp(child, byId, acc); + return acc; + } + return collectProps(typeNode.type, byId, acc); + } + if (typeNode.type === 'intersection' || typeNode.type === 'union') { + for (const member of typeNode.types) collectProps(member, byId, acc); + return acc; + } + if (typeNode.type === 'reflection' && typeNode.declaration?.children) { + for (const child of typeNode.declaration.children) addProp(child, byId, acc); + return acc; + } + if (typeNode.type === 'reference' && typeof typeNode.target === 'number') { + const target = byId.get(typeNode.target); + // Only follow references into our own package's prop types, not native ones. + if (target && target.kind === 2097152) collectProps(target, byId, acc); + return acc; + } + return acc; +} + +function addProp(child, byId, acc) { + if (child.kind !== 1024 || acc.has(child.name)) return; // 1024 = Property + acc.set(child.name, { + name: child.name, + type: typeToString(child.type, byId), + required: !child.flags?.isOptional, + default: defaultTag(child.comment), + description: summaryText(child.comment), + }); +} + +function extractCssVariables(dir) { + const abs = path.resolve(uiSrc, 'components', dir); + const files = globSync('**/*.css', { cwd: abs }).sort(); + const seen = new Set(); + const vars = []; + for (const file of files) { + const css = readFileSync(path.resolve(abs, file), 'utf8'); + // Match `--ax-public-xxx:` declarations, capturing an optional same-line comment. + const re = /(--ax-public-[\w-]+)\s*:[^;]*?(?:\/\*\s*(.*?)\s*\*\/)?\s*;/g; + let m; + while ((m = re.exec(css))) { + if (seen.has(m[1])) continue; + seen.add(m[1]); + vars.push({ name: m[1], comment: (m[2] ?? '').trim() }); + } + } + return vars; +} + +async function main() { + const project = await runTypedoc(); + const byId = indexById(project); + const out = {}; + const warnings = []; + + for (const c of COMPONENTS) { + let props = []; + if (c.propsType) { + const typeNode = findTypeByName(project, c.propsType); + if (typeNode) { + props = [...collectProps(typeNode, byId).values()].sort((a, b) => a.name.localeCompare(b.name)); + } else { + warnings.push(`props type "${c.propsType}" not found for "${c.slug}"`); + } + } + out[c.slug] = { name: c.name, props, cssVariables: extractCssVariables(c.dir) }; + } + + await mkdir(path.dirname(outFile), { recursive: true }); + await writeFile(outFile, JSON.stringify(out, null, 2) + '\n'); + + const summary = Object.entries(out).map(([s, v]) => `${s}: ${v.props.length} props, ${v.cssVariables.length} vars`); + console.log('✔ ui-api.json generated\n ' + summary.join('\n ')); + if (warnings.length) console.warn('⚠ ' + warnings.join('\n⚠ ')); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/apps/docs/src/components/api/css-variables-table.astro b/apps/docs/src/components/api/css-variables-table.astro new file mode 100644 index 000000000..6ccfec8ee --- /dev/null +++ b/apps/docs/src/components/api/css-variables-table.astro @@ -0,0 +1,43 @@ +--- +// Renders a component's CSS custom properties from the generated ui-api.json +// (extracted from the component stylesheets - never hand-maintained). +import data from '../../generated/ui-api.json'; + +interface Props { + slug: string; +} + +const { slug } = Astro.props; +const entry = (data as Record)[slug]; +const cssVariables = entry?.cssVariables ?? []; + +interface CssVar { + name: string; + comment: string; +} +--- + +{ + cssVariables.length === 0 ? ( +

This component does not expose its own CSS variables.

+ ) : ( + + + + + + + + + {cssVariables.map((cssVar) => ( + + + + + ))} + +
VariableNotes
+ {cssVar.name} + {cssVar.comment}
+ ) +} diff --git a/apps/docs/src/components/api/props-table.astro b/apps/docs/src/components/api/props-table.astro new file mode 100644 index 000000000..3cda026d6 --- /dev/null +++ b/apps/docs/src/components/api/props-table.astro @@ -0,0 +1,53 @@ +--- +// Renders a component's props table from the generated ui-api.json (extracted +// from `@workflowbuilder/ui` source with TypeDoc - never hand-maintained). +import data from '../../generated/ui-api.json'; + +interface Props { + slug: string; +} + +const { slug } = Astro.props; +const entry = (data as Record)[slug]; +const props = entry?.props ?? []; + +interface PropRow { + name: string; + type: string; + required: boolean; + default: string | null; + description: string; +} +--- + +{ + props.length === 0 ? ( +

This component has no configurable props.

+ ) : ( + + + + + + + + + + + {props.map((prop) => ( + + + + + + + ))} + +
PropTypeDefaultDescription
+ {prop.name} + {!prop.required && ?} + + {prop.type} + {prop.default ? {prop.default} : '-'}{prop.description.replace(/\n/g, ' ')}
+ ) +} diff --git a/apps/docs/src/components/ui-examples/accordion.tsx b/apps/docs/src/components/ui-examples/accordion.tsx new file mode 100644 index 000000000..d3b3fb895 --- /dev/null +++ b/apps/docs/src/components/ui-examples/accordion.tsx @@ -0,0 +1,18 @@ +import { Accordion } from '@workflowbuilder/ui'; + +import frame from './example-frame.module.css'; + +export function AccordionExample() { + return ( +
+
+ + Our component library, built on Base UI. + + + Yes - override the --ax-* design tokens. + +
+
+ ); +} diff --git a/apps/docs/src/components/ui-examples/avatar.tsx b/apps/docs/src/components/ui-examples/avatar.tsx new file mode 100644 index 000000000..33ed17cc8 --- /dev/null +++ b/apps/docs/src/components/ui-examples/avatar.tsx @@ -0,0 +1,32 @@ +import { Avatar } from '@workflowbuilder/ui'; + +import frame from './example-frame.module.css'; + +const AVATAR = + 'data:image/svg+xml;utf8,' + + encodeURIComponent( + "AL", + ); + +export function AvatarExample() { + return ( +
+ + + + +
+ ); +} diff --git a/apps/docs/src/components/ui-examples/button.tsx b/apps/docs/src/components/ui-examples/button.tsx new file mode 100644 index 000000000..c00c4e9ad --- /dev/null +++ b/apps/docs/src/components/ui-examples/button.tsx @@ -0,0 +1,20 @@ +import { Button } from '@workflowbuilder/ui'; + +import frame from './example-frame.module.css'; + +export function ButtonExample() { + return ( +
+ + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/ui-examples/checkbox.tsx b/apps/docs/src/components/ui-examples/checkbox.tsx new file mode 100644 index 000000000..5a7e94713 --- /dev/null +++ b/apps/docs/src/components/ui-examples/checkbox.tsx @@ -0,0 +1,25 @@ +import { Checkbox } from '@workflowbuilder/ui'; +import { useState } from 'react'; + +import frame from './example-frame.module.css'; + +export function CheckboxExample() { + const [checked, setChecked] = useState(true); + + return ( +
+ + + +
+ ); +} diff --git a/apps/docs/src/components/ui-examples/date-picker.tsx b/apps/docs/src/components/ui-examples/date-picker.tsx new file mode 100644 index 000000000..32e6a0b9e --- /dev/null +++ b/apps/docs/src/components/ui-examples/date-picker.tsx @@ -0,0 +1,24 @@ +import { DatePicker } from '@workflowbuilder/ui'; +import { useState } from 'react'; + +import frame from './example-frame.module.css'; + +export function DatePickerExample() { + const [date, setDate] = useState(null); + + return ( +
+
+ +
+
+ ); +} diff --git a/apps/docs/src/components/ui-examples/example-frame.module.css b/apps/docs/src/components/ui-examples/example-frame.module.css new file mode 100644 index 000000000..08c894602 --- /dev/null +++ b/apps/docs/src/components/ui-examples/example-frame.module.css @@ -0,0 +1,36 @@ +.frame { + display: flex; + flex-wrap: wrap; + gap: 1rem 1.5rem; + align-items: center; + padding: 1.5rem; + border: 1px solid var(--sl-color-gray-5); + border-radius: 0.5rem; + background: var(--sl-color-black); + margin-top: 1rem; +} + +.stack { + display: flex; + flex-direction: column; + gap: 0.75rem; + min-width: 14rem; + max-width: 22rem; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.fieldLabel { + font-size: 0.8125rem; + color: var(--sl-color-gray-2); +} + +.inline { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} diff --git a/apps/docs/src/components/ui-examples/input.tsx b/apps/docs/src/components/ui-examples/input.tsx new file mode 100644 index 000000000..468f52dd1 --- /dev/null +++ b/apps/docs/src/components/ui-examples/input.tsx @@ -0,0 +1,23 @@ +import { Input } from '@workflowbuilder/ui'; +import { useState } from 'react'; + +import frame from './example-frame.module.css'; + +export function InputExample() { + const [value, setValue] = useState(''); + + return ( +
+
+ + +
+
+ ); +} diff --git a/apps/docs/src/components/ui-examples/menu.tsx b/apps/docs/src/components/ui-examples/menu.tsx new file mode 100644 index 000000000..e56da8112 --- /dev/null +++ b/apps/docs/src/components/ui-examples/menu.tsx @@ -0,0 +1,20 @@ +import { Button, Menu } from '@workflowbuilder/ui'; + +import frame from './example-frame.module.css'; + +export function MenuExample() { + return ( +
+ {} }, + { label: 'Duplicate', onClick: () => {} }, + { type: 'separator' }, + { label: 'Delete', destructive: true, onClick: () => {} }, + ]} + > + + +
+ ); +} diff --git a/apps/docs/src/components/ui-examples/modal.tsx b/apps/docs/src/components/ui-examples/modal.tsx new file mode 100644 index 000000000..50dcb1415 --- /dev/null +++ b/apps/docs/src/components/ui-examples/modal.tsx @@ -0,0 +1,29 @@ +import { Button, Modal } from '@workflowbuilder/ui'; +import { useState } from 'react'; + +import frame from './example-frame.module.css'; + +export function ModalExample() { + const [open, setOpen] = useState(false); + + return ( +
+ + setOpen(false)} + title="Example modal" + subtitle="Rendered live from @workflowbuilder/ui" + footer={ + + } + > + The backdrop and popup fade in and out via the Base UI transition lifecycle. + +
+ ); +} diff --git a/apps/docs/src/components/ui-examples/nav-button.tsx b/apps/docs/src/components/ui-examples/nav-button.tsx new file mode 100644 index 000000000..6de4d38cf --- /dev/null +++ b/apps/docs/src/components/ui-examples/nav-button.tsx @@ -0,0 +1,12 @@ +import { NavButton } from '@workflowbuilder/ui'; + +import frame from './example-frame.module.css'; + +export function NavButtonExample() { + return ( +
+ Default + Selected +
+ ); +} diff --git a/apps/docs/src/components/ui-examples/radio.tsx b/apps/docs/src/components/ui-examples/radio.tsx new file mode 100644 index 000000000..a865737c5 --- /dev/null +++ b/apps/docs/src/components/ui-examples/radio.tsx @@ -0,0 +1,19 @@ +import { Radio } from '@workflowbuilder/ui'; +import { useState } from 'react'; + +import frame from './example-frame.module.css'; + +export function RadioExample() { + const [value, setValue] = useState('daily'); + + return ( +
+ {['daily', 'weekly', 'monthly'].map((v) => ( + + setValue(v)} /> + {v} + + ))} +
+ ); +} diff --git a/apps/docs/src/components/ui-examples/segment-picker.tsx b/apps/docs/src/components/ui-examples/segment-picker.tsx new file mode 100644 index 000000000..03b6fb465 --- /dev/null +++ b/apps/docs/src/components/ui-examples/segment-picker.tsx @@ -0,0 +1,18 @@ +import { SegmentPicker } from '@workflowbuilder/ui'; +import { useState } from 'react'; + +import frame from './example-frame.module.css'; + +export function SegmentPickerExample() { + const [view, setView] = useState('list'); + + return ( +
+ setView(next)}> + List + Grid + Board + +
+ ); +} diff --git a/apps/docs/src/components/ui-examples/select.tsx b/apps/docs/src/components/ui-examples/select.tsx new file mode 100644 index 000000000..91d868b25 --- /dev/null +++ b/apps/docs/src/components/ui-examples/select.tsx @@ -0,0 +1,30 @@ +import { Select, type SelectItem } from '@workflowbuilder/ui'; +import { useState } from 'react'; + +import frame from './example-frame.module.css'; + +const ITEMS: SelectItem[] = [ + { value: 'opus', label: 'Claude Opus' }, + { value: 'sonnet', label: 'Claude Sonnet' }, + { value: 'haiku', label: 'Claude Haiku' }, +]; + +export function SelectExample() { + const [model, setModel] = useState('opus'); + + return ( +
+
+