@duro-app/ui 0.15.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ActionBar/ActionBar.d.ts +3 -1
- package/dist/components/ActionBar/ActionBar.d.ts.map +1 -1
- package/dist/components/Button/Button.meta.d.ts +3 -0
- package/dist/components/Button/Button.meta.d.ts.map +1 -0
- package/dist/components/Checkbox/Checkbox.meta.d.ts +3 -0
- package/dist/components/Checkbox/Checkbox.meta.d.ts.map +1 -0
- package/dist/components/Cluster/Cluster.meta.d.ts +3 -0
- package/dist/components/Cluster/Cluster.meta.d.ts.map +1 -0
- package/dist/components/Field/Field.d.ts +2 -1
- package/dist/components/Field/Field.d.ts.map +1 -1
- package/dist/components/Field/Field.meta.d.ts +3 -0
- package/dist/components/Field/Field.meta.d.ts.map +1 -0
- package/dist/components/Field/FieldContext.d.ts +9 -1
- package/dist/components/Field/FieldContext.d.ts.map +1 -1
- package/dist/components/Fieldset/Fieldset.meta.d.ts +3 -0
- package/dist/components/Fieldset/Fieldset.meta.d.ts.map +1 -0
- package/dist/components/Form/Form.d.ts +11 -0
- package/dist/components/Form/Form.d.ts.map +1 -0
- package/dist/components/Form/Form.meta.d.ts +3 -0
- package/dist/components/Form/Form.meta.d.ts.map +1 -0
- package/dist/components/Form/Form.stories.d.ts +10 -0
- package/dist/components/Form/Form.stories.d.ts.map +1 -0
- package/dist/components/Form/FormContext.d.ts +3 -0
- package/dist/components/Form/FormContext.d.ts.map +1 -0
- package/dist/components/Grid/Grid.meta.d.ts +3 -0
- package/dist/components/Grid/Grid.meta.d.ts.map +1 -0
- package/dist/components/Inline/Inline.meta.d.ts +3 -0
- package/dist/components/Inline/Inline.meta.d.ts.map +1 -0
- package/dist/components/Input/Input.d.ts +3 -1
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/Input/Input.meta.d.ts +3 -0
- package/dist/components/Input/Input.meta.d.ts.map +1 -0
- package/dist/components/LinkButton/LinkButton.meta.d.ts +3 -0
- package/dist/components/LinkButton/LinkButton.meta.d.ts.map +1 -0
- package/dist/components/Menu/Menu.meta.d.ts +3 -0
- package/dist/components/Menu/Menu.meta.d.ts.map +1 -0
- package/dist/components/PageShell/PageShell.meta.d.ts +3 -0
- package/dist/components/PageShell/PageShell.meta.d.ts.map +1 -0
- package/dist/components/ScrollArea/ScrollArea.meta.d.ts +3 -0
- package/dist/components/ScrollArea/ScrollArea.meta.d.ts.map +1 -0
- package/dist/components/Select/Select.meta.d.ts +3 -0
- package/dist/components/Select/Select.meta.d.ts.map +1 -0
- package/dist/components/SideNav/SideNav.meta.d.ts +3 -0
- package/dist/components/SideNav/SideNav.meta.d.ts.map +1 -0
- package/dist/components/Stack/Stack.meta.d.ts +3 -0
- package/dist/components/Stack/Stack.meta.d.ts.map +1 -0
- package/dist/components/Switch/Switch.meta.d.ts +3 -0
- package/dist/components/Switch/Switch.meta.d.ts.map +1 -0
- package/dist/components/Table/Table.meta.d.ts +3 -0
- package/dist/components/Table/Table.meta.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.meta.d.ts +3 -0
- package/dist/components/Tabs/Tabs.meta.d.ts.map +1 -0
- package/dist/components/Textarea/Textarea.d.ts +3 -1
- package/dist/components/Textarea/Textarea.d.ts.map +1 -1
- package/dist/components/Textarea/Textarea.meta.d.ts +3 -0
- package/dist/components/Textarea/Textarea.meta.d.ts.map +1 -0
- package/dist/components/Toggle/Toggle.meta.d.ts +3 -0
- package/dist/components/Toggle/Toggle.meta.d.ts.map +1 -0
- package/dist/components/ToggleGroup/ToggleGroup.meta.d.ts +3 -0
- package/dist/components/ToggleGroup/ToggleGroup.meta.d.ts.map +1 -0
- package/dist/components/Tooltip/Tooltip.meta.d.ts +3 -0
- package/dist/components/Tooltip/Tooltip.meta.d.ts.map +1 -0
- package/dist/components/component-meta.d.ts +15 -0
- package/dist/components/component-meta.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1500 -1400
- package/dist/index.js.map +1 -1
- package/package.json +19 -2
- package/src/components/ActionBar/ActionBar.tsx +20 -15
- package/src/components/Button/Button.meta.ts +23 -0
- package/src/components/Checkbox/Checkbox.meta.ts +21 -0
- package/src/components/Cluster/Cluster.meta.ts +25 -0
- package/src/components/Field/Field.meta.ts +37 -0
- package/src/components/Field/Field.tsx +56 -3
- package/src/components/Field/FieldContext.ts +10 -0
- package/src/components/Fieldset/Fieldset.meta.ts +38 -0
- package/src/components/Form/Form.meta.ts +38 -0
- package/src/components/Form/Form.stories.tsx +252 -0
- package/src/components/Form/Form.tsx +59 -0
- package/src/components/Form/FormContext.ts +7 -0
- package/src/components/Grid/Grid.meta.ts +30 -0
- package/src/components/Inline/Inline.meta.ts +23 -0
- package/src/components/Input/Input.meta.ts +23 -0
- package/src/components/Input/Input.tsx +18 -4
- package/src/components/LinkButton/LinkButton.meta.ts +24 -0
- package/src/components/Menu/Menu.meta.ts +33 -0
- package/src/components/PageShell/PageShell.meta.ts +24 -0
- package/src/components/ScrollArea/ScrollArea.meta.ts +27 -0
- package/src/components/Select/Select.meta.ts +34 -0
- package/src/components/SideNav/SideNav.meta.ts +31 -0
- package/src/components/Stack/Stack.meta.ts +22 -0
- package/src/components/Switch/Switch.meta.ts +25 -0
- package/src/components/Table/Table.meta.ts +36 -0
- package/src/components/Tabs/Tabs.meta.ts +31 -0
- package/src/components/Textarea/Textarea.meta.ts +22 -0
- package/src/components/Textarea/Textarea.tsx +20 -4
- package/src/components/Toggle/Toggle.meta.ts +30 -0
- package/src/components/ToggleGroup/ToggleGroup.meta.ts +30 -0
- package/src/components/ToggleGroup/ToggleGroup.stories.tsx +2 -2
- package/src/components/Tooltip/Tooltip.meta.ts +22 -0
- package/src/components/component-meta.ts +8 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {type ReactNode, useCallback, useEffect, useRef} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {
|
|
4
|
+
useForm,
|
|
5
|
+
FormProvider,
|
|
6
|
+
type UseFormReturn,
|
|
7
|
+
type DefaultValues,
|
|
8
|
+
type FieldValues,
|
|
9
|
+
} from 'react-hook-form'
|
|
10
|
+
import {effectTsResolver} from '@hookform/resolvers/effect-ts'
|
|
11
|
+
import type {Schema} from 'effect'
|
|
12
|
+
import {FormContext} from './FormContext'
|
|
13
|
+
|
|
14
|
+
export interface FormProps<T extends FieldValues> {
|
|
15
|
+
schema: Schema.Schema<T>
|
|
16
|
+
defaultValues: DefaultValues<T>
|
|
17
|
+
onSubmit: (data: T) => void | Promise<void>
|
|
18
|
+
children: ReactNode | ((methods: UseFormReturn<T>) => ReactNode)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Form<T extends FieldValues>({
|
|
22
|
+
schema,
|
|
23
|
+
defaultValues,
|
|
24
|
+
onSubmit,
|
|
25
|
+
children,
|
|
26
|
+
}: FormProps<T>) {
|
|
27
|
+
const methods = useForm<T>({
|
|
28
|
+
resolver: effectTsResolver(schema),
|
|
29
|
+
defaultValues,
|
|
30
|
+
mode: 'onTouched',
|
|
31
|
+
reValidateMode: 'onChange',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const formRef = useRef<HTMLFormElement>(null)
|
|
35
|
+
const handleSubmit = useCallback(
|
|
36
|
+
(e: Event) => {
|
|
37
|
+
e.preventDefault()
|
|
38
|
+
void methods.handleSubmit(onSubmit)()
|
|
39
|
+
},
|
|
40
|
+
[methods, onSubmit],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const el = formRef.current
|
|
45
|
+
if (!el) return
|
|
46
|
+
el.addEventListener('submit', handleSubmit)
|
|
47
|
+
return () => el.removeEventListener('submit', handleSubmit)
|
|
48
|
+
}, [handleSubmit])
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<FormProvider {...methods}>
|
|
52
|
+
<FormContext.Provider value={true}>
|
|
53
|
+
<html.form ref={formRef}>
|
|
54
|
+
{typeof children === 'function' ? children(methods) : children}
|
|
55
|
+
</html.form>
|
|
56
|
+
</FormContext.Provider>
|
|
57
|
+
</FormProvider>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'CSS grid layout. Supports fixed column count (1-6) or responsive auto-fit via minColumnWidth.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Card grids, dashboard layouts, multi-column forms',
|
|
8
|
+
'Responsive layouts that should auto-adjust column count',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Single-column vertical layout — use Stack',
|
|
12
|
+
'Simple horizontal row — use Inline or Cluster',
|
|
13
|
+
],
|
|
14
|
+
relatedTo: [
|
|
15
|
+
{component: 'Stack', relationship: 'Single-column vertical layout'},
|
|
16
|
+
],
|
|
17
|
+
example: `// Responsive: columns auto-fit based on min width
|
|
18
|
+
<Grid minColumnWidth="280px" gap="md">
|
|
19
|
+
<Card>A</Card>
|
|
20
|
+
<Card>B</Card>
|
|
21
|
+
<Card>C</Card>
|
|
22
|
+
</Grid>
|
|
23
|
+
|
|
24
|
+
// Fixed: always 3 columns
|
|
25
|
+
<Grid columns={3} gap="md">
|
|
26
|
+
<Card>A</Card>
|
|
27
|
+
<Card>B</Card>
|
|
28
|
+
<Card>C</Card>
|
|
29
|
+
</Grid>`,
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Horizontal flex layout with NO wrapping. Items stay on one line and may overflow.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Toolbar or button group that must stay on one row',
|
|
8
|
+
'Horizontal alignment of a known, small number of items',
|
|
9
|
+
'Icon + text pairs, breadcrumbs, action rows',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Items that should wrap to the next line — use Cluster',
|
|
13
|
+
'Vertical stacking — use Stack',
|
|
14
|
+
],
|
|
15
|
+
relatedTo: [
|
|
16
|
+
{component: 'Cluster', relationship: 'Same but wraps to next line'},
|
|
17
|
+
{component: 'Stack', relationship: 'Vertical equivalent'},
|
|
18
|
+
],
|
|
19
|
+
example: `<Inline gap="sm" align="center">
|
|
20
|
+
<Button>Save</Button>
|
|
21
|
+
<Button variant="secondary">Cancel</Button>
|
|
22
|
+
</Inline>`,
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Text input with automatic Field/Form integration. Variant auto-switches to error when inside an invalid Field.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Single-line text entry (text, email, password, url, tel, number, search)',
|
|
8
|
+
],
|
|
9
|
+
whenNotToUse: [
|
|
10
|
+
'Multi-line text — use Textarea',
|
|
11
|
+
'Picking from predefined options — use Select',
|
|
12
|
+
],
|
|
13
|
+
relatedTo: [
|
|
14
|
+
{component: 'Field', relationship: 'Wrap in Field.Root for label + error'},
|
|
15
|
+
{component: 'InputGroup', relationship: 'Wrap in InputGroup.Root for prefix/suffix addons'},
|
|
16
|
+
{component: 'Textarea', relationship: 'Multi-line variant'},
|
|
17
|
+
],
|
|
18
|
+
example: `<Field.Root name="email">
|
|
19
|
+
<Field.Label>Email</Field.Label>
|
|
20
|
+
<Input type="email" placeholder="you@example.com" autoComplete="email" />
|
|
21
|
+
<Field.Error />
|
|
22
|
+
</Field.Root>`,
|
|
23
|
+
}
|
|
@@ -30,6 +30,8 @@ interface InputProps {
|
|
|
30
30
|
defaultValue?: string
|
|
31
31
|
disabled?: boolean
|
|
32
32
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
33
|
+
onBlur?: () => void
|
|
34
|
+
ref?: React.Ref<HTMLInputElement>
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export function Input({
|
|
@@ -45,10 +47,20 @@ export function Input({
|
|
|
45
47
|
defaultValue,
|
|
46
48
|
disabled,
|
|
47
49
|
onChange,
|
|
50
|
+
onBlur,
|
|
51
|
+
ref,
|
|
48
52
|
}: InputProps) {
|
|
49
53
|
const ctx = useFieldContext()
|
|
50
54
|
const groupCtx = useInputGroupContext()
|
|
51
55
|
|
|
56
|
+
const fieldCtx = ctx?.field
|
|
57
|
+
const effectiveName = name ?? fieldCtx?.name
|
|
58
|
+
const effectiveValue = value ?? (fieldCtx ? String(fieldCtx.value ?? '') : undefined)
|
|
59
|
+
const effectiveOnChange = onChange ?? fieldCtx?.onChange
|
|
60
|
+
const effectiveOnBlur = onBlur ?? fieldCtx?.onBlur
|
|
61
|
+
const effectiveRef = ref ?? fieldCtx?.ref
|
|
62
|
+
const effectiveVariant = ctx?.invalid ? 'error' : variant
|
|
63
|
+
|
|
52
64
|
// react-strict-dom omits web-only `pattern` from its types, but the
|
|
53
65
|
// underlying DOM element supports it. Type-assert to pass it through.
|
|
54
66
|
const extraProps = pattern !== undefined ? {pattern} : undefined
|
|
@@ -57,20 +69,22 @@ export function Input({
|
|
|
57
69
|
<html.input
|
|
58
70
|
id={ctx?.controlId}
|
|
59
71
|
type={type}
|
|
60
|
-
name={
|
|
72
|
+
name={effectiveName}
|
|
61
73
|
placeholder={placeholder}
|
|
62
74
|
required={required}
|
|
63
75
|
minLength={minLength}
|
|
64
76
|
autoComplete={autoComplete}
|
|
65
|
-
value={
|
|
77
|
+
value={effectiveValue}
|
|
66
78
|
defaultValue={defaultValue}
|
|
67
79
|
disabled={disabled}
|
|
68
80
|
aria-describedby={
|
|
69
81
|
ctx ? `${ctx.descriptionId} ${ctx.invalid ? ctx.errorId : ''}`.trim() : undefined
|
|
70
82
|
}
|
|
71
83
|
aria-invalid={ctx?.invalid || variant === 'error' || undefined}
|
|
72
|
-
onChange={onChange}
|
|
73
|
-
|
|
84
|
+
onChange={effectiveOnChange as StrictInputProps['onChange']}
|
|
85
|
+
onBlur={effectiveOnBlur as StrictInputProps['onBlur']}
|
|
86
|
+
ref={effectiveRef as React.Ref<HTMLInputElement>}
|
|
87
|
+
style={[styles.base, styles[effectiveVariant], groupCtx?.inGroup && styles.inGroup]}
|
|
74
88
|
{...(extraProps as Record<string, unknown>)}
|
|
75
89
|
/>
|
|
76
90
|
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Button-styled hyperlink. Renders an <a> tag, not a <button>. Use for navigation, not actions.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Navigation that should look like a button (e.g., "Get started", "View all")',
|
|
8
|
+
'Links to external pages with button styling',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Triggering in-page actions — use Button',
|
|
12
|
+
'Plain text links — use a styled <a> tag',
|
|
13
|
+
],
|
|
14
|
+
relatedTo: [
|
|
15
|
+
{component: 'Button', relationship: 'Button for actions; LinkButton for navigation'},
|
|
16
|
+
],
|
|
17
|
+
example: `<LinkButton href="/signup" variant="primary">
|
|
18
|
+
Get started
|
|
19
|
+
</LinkButton>
|
|
20
|
+
|
|
21
|
+
<LinkButton href="https://docs.example.com" target="_blank" variant="secondary">
|
|
22
|
+
Read the docs
|
|
23
|
+
</LinkButton>`,
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it).',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Context menu or "more actions" dropdown',
|
|
8
|
+
'Navigation links in a dropdown',
|
|
9
|
+
'Overflow menu for toolbar actions',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Picking a value — use Select',
|
|
13
|
+
'Primary navigation — use SideNav or Tabs',
|
|
14
|
+
],
|
|
15
|
+
anatomy: {
|
|
16
|
+
required: ['Root', 'Trigger', 'Popup', 'Item'],
|
|
17
|
+
optional: ['LinkItem'],
|
|
18
|
+
},
|
|
19
|
+
relatedTo: [
|
|
20
|
+
{component: 'Select', relationship: 'Select picks values; Menu triggers actions'},
|
|
21
|
+
{component: 'Button', relationship: 'Often used as Menu.Trigger content'},
|
|
22
|
+
],
|
|
23
|
+
example: `<Menu.Root>
|
|
24
|
+
<Menu.Trigger>
|
|
25
|
+
<Button variant="secondary">Actions</Button>
|
|
26
|
+
</Menu.Trigger>
|
|
27
|
+
<Menu.Popup>
|
|
28
|
+
<Menu.Item onClick={() => console.log('edit')}>Edit</Menu.Item>
|
|
29
|
+
<Menu.Item onClick={() => console.log('duplicate')}>Duplicate</Menu.Item>
|
|
30
|
+
<Menu.LinkItem href="/settings">Settings</Menu.LinkItem>
|
|
31
|
+
</Menu.Popup>
|
|
32
|
+
</Menu.Root>`,
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Page-level layout wrapper. Centers content with max-width, padding, and an optional sticky header.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Top-level page layout',
|
|
8
|
+
'Any full-page view that needs centered content with consistent padding',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Nested section layouts — use Stack or Card',
|
|
12
|
+
'Sidebar layouts — combine with SideNav manually',
|
|
13
|
+
],
|
|
14
|
+
relatedTo: [
|
|
15
|
+
{component: 'Stack', relationship: 'Use Stack for content within PageShell'},
|
|
16
|
+
{component: 'SideNav', relationship: 'Often placed alongside PageShell'},
|
|
17
|
+
],
|
|
18
|
+
example: `<PageShell maxWidth="lg" padding="md" header={<Heading level={1}>Dashboard</Heading>}>
|
|
19
|
+
<Stack gap="lg">
|
|
20
|
+
<Card>Content section 1</Card>
|
|
21
|
+
<Card>Content section 2</Card>
|
|
22
|
+
</Stack>
|
|
23
|
+
</PageShell>`,
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Custom scrollbar region with draggable thumb. Supports vertical and horizontal scrolling. Compound component — Root is required.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Custom-styled scrollbars for content overflow areas',
|
|
8
|
+
'Chat logs, code blocks, or long lists with constrained height',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Simple page-level scrolling — browser scroll is fine',
|
|
12
|
+
'Very short content that never overflows',
|
|
13
|
+
],
|
|
14
|
+
anatomy: {
|
|
15
|
+
required: ['Root', 'Viewport', 'Content', 'Scrollbar', 'Thumb'],
|
|
16
|
+
},
|
|
17
|
+
example: `<ScrollArea.Root>
|
|
18
|
+
<ScrollArea.Viewport maxHeight={300}>
|
|
19
|
+
<ScrollArea.Content>
|
|
20
|
+
{/* Long content here */}
|
|
21
|
+
</ScrollArea.Content>
|
|
22
|
+
</ScrollArea.Viewport>
|
|
23
|
+
<ScrollArea.Scrollbar orientation="vertical">
|
|
24
|
+
<ScrollArea.Thumb orientation="vertical" />
|
|
25
|
+
</ScrollArea.Scrollbar>
|
|
26
|
+
</ScrollArea.Root>`,
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Dropdown select for choosing one value from a list. Compound component — Root is required (throws without it).',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'User must pick one option from a predefined list',
|
|
8
|
+
'Form field for country, category, role selection',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Triggering actions — use Menu',
|
|
12
|
+
'Multiple selections — use ToggleGroup or Checkbox group',
|
|
13
|
+
'Fewer than 3 options — consider Toggle or ToggleGroup',
|
|
14
|
+
],
|
|
15
|
+
anatomy: {
|
|
16
|
+
required: ['Root', 'Trigger', 'Popup', 'Item'],
|
|
17
|
+
optional: ['Value', 'Icon', 'ItemText'],
|
|
18
|
+
},
|
|
19
|
+
relatedTo: [
|
|
20
|
+
{component: 'Menu', relationship: 'Menu triggers actions; Select picks a value'},
|
|
21
|
+
{component: 'ToggleGroup', relationship: 'For visible, small option sets'},
|
|
22
|
+
],
|
|
23
|
+
example: `<Select.Root name="role" defaultValue="viewer">
|
|
24
|
+
<Select.Trigger>
|
|
25
|
+
<Select.Value placeholder="Select role" />
|
|
26
|
+
<Select.Icon />
|
|
27
|
+
</Select.Trigger>
|
|
28
|
+
<Select.Popup>
|
|
29
|
+
<Select.Item value="admin">Admin</Select.Item>
|
|
30
|
+
<Select.Item value="editor">Editor</Select.Item>
|
|
31
|
+
<Select.Item value="viewer">Viewer</Select.Item>
|
|
32
|
+
</Select.Popup>
|
|
33
|
+
</Select.Root>`,
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Vertical side navigation with collapsible groups. Supports controlled and uncontrolled active item. Compound component — Root is required.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'App-level sidebar navigation',
|
|
8
|
+
'Settings navigation with grouped sections',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'In-page content switching — use Tabs',
|
|
12
|
+
'Top-level horizontal nav — use Inline with LinkButton',
|
|
13
|
+
],
|
|
14
|
+
anatomy: {
|
|
15
|
+
required: ['Root', 'Group', 'Item'],
|
|
16
|
+
},
|
|
17
|
+
relatedTo: [
|
|
18
|
+
{component: 'Tabs', relationship: 'Tabs switch content in-place; SideNav navigates pages'},
|
|
19
|
+
{component: 'PageShell', relationship: 'Often placed alongside PageShell'},
|
|
20
|
+
],
|
|
21
|
+
example: `<SideNav.Root defaultValue="dashboard">
|
|
22
|
+
<SideNav.Group label="Main">
|
|
23
|
+
<SideNav.Item value="dashboard">Dashboard</SideNav.Item>
|
|
24
|
+
<SideNav.Item value="analytics">Analytics</SideNav.Item>
|
|
25
|
+
</SideNav.Group>
|
|
26
|
+
<SideNav.Group label="Settings" defaultExpanded={false}>
|
|
27
|
+
<SideNav.Item value="profile">Profile</SideNav.Item>
|
|
28
|
+
<SideNav.Item value="billing">Billing</SideNav.Item>
|
|
29
|
+
</SideNav.Group>
|
|
30
|
+
</SideNav.Root>`,
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Vertical flex layout. Stacks children top-to-bottom with consistent gap.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Vertically stacking form fields, cards, or sections',
|
|
8
|
+
'Any column-direction layout with uniform spacing',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Horizontal layouts — use Inline (no wrap) or Cluster (wraps)',
|
|
12
|
+
'Grid layouts — use Grid',
|
|
13
|
+
],
|
|
14
|
+
relatedTo: [
|
|
15
|
+
{component: 'Inline', relationship: 'Horizontal equivalent (no wrap)'},
|
|
16
|
+
{component: 'Cluster', relationship: 'Horizontal equivalent (wraps)'},
|
|
17
|
+
],
|
|
18
|
+
example: `<Stack gap="md" align="stretch">
|
|
19
|
+
<Card>First</Card>
|
|
20
|
+
<Card>Second</Card>
|
|
21
|
+
</Stack>`,
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Toggle switch for on/off settings. Renders a hidden checkbox for form submission + a styled switch button.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'On/off toggles in settings (notifications, dark mode, feature flags)',
|
|
8
|
+
'Immediate-effect toggles where the change applies instantly',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Form opt-in checkboxes — use Checkbox',
|
|
12
|
+
'Selecting between multiple options — use Select or ToggleGroup',
|
|
13
|
+
],
|
|
14
|
+
relatedTo: [
|
|
15
|
+
{component: 'Checkbox', relationship: 'Checkbox for selection/opt-in; Switch for on/off settings'},
|
|
16
|
+
{component: 'Toggle', relationship: 'Toggle for toolbar pressed state; Switch for settings'},
|
|
17
|
+
],
|
|
18
|
+
example: `<Switch
|
|
19
|
+
name="notifications"
|
|
20
|
+
defaultChecked={true}
|
|
21
|
+
onCheckedChange={(checked) => console.log(checked)}
|
|
22
|
+
>
|
|
23
|
+
Enable notifications
|
|
24
|
+
</Switch>`,
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Data table with CSS grid layout. Supports striped, bordered variants. Compound component — Root is required (throws without it). Requires `columns` prop on Root.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Displaying tabular data with rows and columns',
|
|
8
|
+
'Data lists with sortable/filterable columns',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Simple key-value pairs — use Stack with Text',
|
|
12
|
+
'Card-based layouts — use Grid with Card',
|
|
13
|
+
],
|
|
14
|
+
anatomy: {
|
|
15
|
+
required: ['Root', 'Header', 'Body', 'Row', 'HeaderCell', 'Cell'],
|
|
16
|
+
},
|
|
17
|
+
relatedTo: [
|
|
18
|
+
{component: 'ScrollArea', relationship: 'Wrap Table in ScrollArea for horizontal overflow'},
|
|
19
|
+
],
|
|
20
|
+
example: `<Table.Root variant="striped" size="md" columns={3}>
|
|
21
|
+
<Table.Header>
|
|
22
|
+
<Table.Row>
|
|
23
|
+
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
24
|
+
<Table.HeaderCell>Role</Table.HeaderCell>
|
|
25
|
+
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
26
|
+
</Table.Row>
|
|
27
|
+
</Table.Header>
|
|
28
|
+
<Table.Body>
|
|
29
|
+
<Table.Row>
|
|
30
|
+
<Table.Cell>Alice</Table.Cell>
|
|
31
|
+
<Table.Cell>Admin</Table.Cell>
|
|
32
|
+
<Table.Cell>Active</Table.Cell>
|
|
33
|
+
</Table.Row>
|
|
34
|
+
</Table.Body>
|
|
35
|
+
</Table.Root>`,
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Tabbed interface with keyboard navigation. Supports horizontal and vertical orientation. Compound component — Root is required.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Switching between content panels in the same view',
|
|
8
|
+
'Settings pages with multiple sections',
|
|
9
|
+
'Organizing related content without page navigation',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Primary site navigation — use SideNav or links',
|
|
13
|
+
'Step-by-step wizard — use a custom stepper',
|
|
14
|
+
],
|
|
15
|
+
anatomy: {
|
|
16
|
+
required: ['Root', 'List', 'Tab', 'Panel'],
|
|
17
|
+
},
|
|
18
|
+
relatedTo: [
|
|
19
|
+
{component: 'SideNav', relationship: 'For persistent navigation, not content switching'},
|
|
20
|
+
],
|
|
21
|
+
example: `<Tabs.Root defaultValue="general">
|
|
22
|
+
<Tabs.List>
|
|
23
|
+
<Tabs.Tab value="general">General</Tabs.Tab>
|
|
24
|
+
<Tabs.Tab value="security">Security</Tabs.Tab>
|
|
25
|
+
<Tabs.Tab value="billing" disabled>Billing</Tabs.Tab>
|
|
26
|
+
</Tabs.List>
|
|
27
|
+
<Tabs.Panel value="general">General settings content</Tabs.Panel>
|
|
28
|
+
<Tabs.Panel value="security">Security settings content</Tabs.Panel>
|
|
29
|
+
<Tabs.Panel value="billing">Billing content</Tabs.Panel>
|
|
30
|
+
</Tabs.Root>`,
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Multi-line text input with automatic Field/Form integration. Same Field binding behavior as Input.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Multi-line text entry (comments, messages, descriptions)',
|
|
8
|
+
],
|
|
9
|
+
whenNotToUse: [
|
|
10
|
+
'Single-line input — use Input',
|
|
11
|
+
],
|
|
12
|
+
relatedTo: [
|
|
13
|
+
{component: 'Input', relationship: 'Single-line variant'},
|
|
14
|
+
{component: 'Field', relationship: 'Wrap in Field.Root for label + error'},
|
|
15
|
+
],
|
|
16
|
+
example: `<Field.Root name="message">
|
|
17
|
+
<Field.Label>Message</Field.Label>
|
|
18
|
+
<Textarea placeholder="Tell us what you think..." rows={4} />
|
|
19
|
+
<Field.Description>At least 10 characters</Field.Description>
|
|
20
|
+
<Field.Error />
|
|
21
|
+
</Field.Root>`,
|
|
22
|
+
}
|
|
@@ -2,6 +2,8 @@ import {html} from 'react-strict-dom'
|
|
|
2
2
|
import {useFieldContext} from '../Field/FieldContext'
|
|
3
3
|
import {styles} from './styles.css'
|
|
4
4
|
|
|
5
|
+
type StrictTextareaProps = React.ComponentProps<typeof html.textarea>
|
|
6
|
+
|
|
5
7
|
export type TextareaVariant = 'default' | 'error'
|
|
6
8
|
|
|
7
9
|
interface TextareaProps {
|
|
@@ -14,6 +16,8 @@ interface TextareaProps {
|
|
|
14
16
|
defaultValue?: string
|
|
15
17
|
disabled?: boolean
|
|
16
18
|
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
|
19
|
+
onBlur?: () => void
|
|
20
|
+
ref?: React.Ref<HTMLTextAreaElement>
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export function Textarea({
|
|
@@ -26,25 +30,37 @@ export function Textarea({
|
|
|
26
30
|
defaultValue,
|
|
27
31
|
disabled,
|
|
28
32
|
onChange,
|
|
33
|
+
onBlur,
|
|
34
|
+
ref,
|
|
29
35
|
}: TextareaProps) {
|
|
30
36
|
const ctx = useFieldContext()
|
|
31
37
|
|
|
38
|
+
const fieldCtx = ctx?.field
|
|
39
|
+
const effectiveName = name ?? fieldCtx?.name
|
|
40
|
+
const effectiveValue = value ?? (fieldCtx ? String(fieldCtx.value ?? '') : undefined)
|
|
41
|
+
const effectiveOnChange = onChange ?? fieldCtx?.onChange
|
|
42
|
+
const effectiveOnBlur = onBlur ?? fieldCtx?.onBlur
|
|
43
|
+
const effectiveRef = ref ?? fieldCtx?.ref
|
|
44
|
+
const effectiveVariant = ctx?.invalid ? 'error' : variant
|
|
45
|
+
|
|
32
46
|
return (
|
|
33
47
|
<html.textarea
|
|
34
48
|
id={ctx?.controlId}
|
|
35
|
-
name={
|
|
49
|
+
name={effectiveName}
|
|
36
50
|
placeholder={placeholder}
|
|
37
51
|
required={required}
|
|
38
52
|
rows={rows}
|
|
39
|
-
value={
|
|
53
|
+
value={effectiveValue}
|
|
40
54
|
defaultValue={defaultValue}
|
|
41
55
|
disabled={disabled}
|
|
42
56
|
aria-describedby={
|
|
43
57
|
ctx ? `${ctx.descriptionId} ${ctx.invalid ? ctx.errorId : ''}`.trim() : undefined
|
|
44
58
|
}
|
|
45
59
|
aria-invalid={ctx?.invalid || variant === 'error' || undefined}
|
|
46
|
-
onChange={onChange}
|
|
47
|
-
|
|
60
|
+
onChange={effectiveOnChange as StrictTextareaProps['onChange']}
|
|
61
|
+
onBlur={effectiveOnBlur as StrictTextareaProps['onBlur']}
|
|
62
|
+
ref={effectiveRef as React.Ref<HTMLTextAreaElement>}
|
|
63
|
+
style={[styles.base, styles[effectiveVariant]]}
|
|
48
64
|
/>
|
|
49
65
|
)
|
|
50
66
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Toggle button with pressed/unpressed state. Works standalone or within ToggleGroup for multi/single selection.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Toolbar buttons that toggle on/off (bold, italic, grid view)',
|
|
8
|
+
'Within ToggleGroup for selecting from visible options',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'On/off settings — use Switch',
|
|
12
|
+
'Form checkboxes — use Checkbox',
|
|
13
|
+
'Submitting actions — use Button',
|
|
14
|
+
],
|
|
15
|
+
relatedTo: [
|
|
16
|
+
{component: 'ToggleGroup', relationship: 'Group of Toggles with single/multi select'},
|
|
17
|
+
{component: 'Switch', relationship: 'Switch for settings; Toggle for toolbar actions'},
|
|
18
|
+
{component: 'Button', relationship: 'Button for actions; Toggle for state'},
|
|
19
|
+
],
|
|
20
|
+
example: `// Standalone
|
|
21
|
+
<Toggle pressed={isBold} onPressedChange={setIsBold} aria-label="Bold">
|
|
22
|
+
<Icon name="key" size={16} />
|
|
23
|
+
</Toggle>
|
|
24
|
+
|
|
25
|
+
// In a group
|
|
26
|
+
<ToggleGroup value={['grid']} onValueChange={setView} multiple={false}>
|
|
27
|
+
<Toggle value="list">List</Toggle>
|
|
28
|
+
<Toggle value="grid">Grid</Toggle>
|
|
29
|
+
</ToggleGroup>`,
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Container for Toggle buttons enabling single or multi selection. Provides context to child Toggles.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Visible option set where user picks one or more (view mode, filter categories)',
|
|
8
|
+
'Segmented control pattern',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Dropdown selection — use Select',
|
|
12
|
+
'Many options (5+) — use Select for space efficiency',
|
|
13
|
+
'Form checkboxes — use Checkbox group in Fieldset',
|
|
14
|
+
],
|
|
15
|
+
relatedTo: [
|
|
16
|
+
{component: 'Toggle', relationship: 'Toggle children get group context'},
|
|
17
|
+
{component: 'Select', relationship: 'Select for larger option sets'},
|
|
18
|
+
],
|
|
19
|
+
example: `<ToggleGroup
|
|
20
|
+
value={selected}
|
|
21
|
+
onValueChange={setSelected}
|
|
22
|
+
multiple={false}
|
|
23
|
+
orientation="horizontal"
|
|
24
|
+
size="default"
|
|
25
|
+
>
|
|
26
|
+
<Toggle value="list">List view</Toggle>
|
|
27
|
+
<Toggle value="grid">Grid view</Toggle>
|
|
28
|
+
<Toggle value="board">Board view</Toggle>
|
|
29
|
+
</ToggleGroup>`,
|
|
30
|
+
}
|
|
@@ -119,7 +119,7 @@ export const Vertical: Story = {
|
|
|
119
119
|
</ToggleGroup>
|
|
120
120
|
),
|
|
121
121
|
play: async ({canvas}) => {
|
|
122
|
-
const group = canvas.getByRole('
|
|
122
|
+
const group = canvas.getByRole('toolbar')
|
|
123
123
|
await expect(group).toHaveAttribute('aria-orientation', 'vertical')
|
|
124
124
|
},
|
|
125
125
|
}
|
|
@@ -153,7 +153,7 @@ export const AllVariants: Story = {
|
|
|
153
153
|
</html.div>
|
|
154
154
|
),
|
|
155
155
|
play: async ({canvas}) => {
|
|
156
|
-
const groups = canvas.getAllByRole('
|
|
156
|
+
const groups = canvas.getAllByRole('toolbar')
|
|
157
157
|
await expect(groups.length).toBe(4)
|
|
158
158
|
},
|
|
159
159
|
}
|