@sableui/react 0.7.1 → 0.8.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/AGENTS.md +2 -2
- package/CHANGELOG.md +73 -0
- package/README.md +2 -0
- package/bin/sable.mjs +34 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +10 -9
- package/dist/components/breadcrumbs/breadcrumbs.meta.js +2 -2
- package/dist/components/button/button.meta.js +4 -0
- package/dist/components/chat-composer/chat-composer.js +4 -2
- package/dist/components/chat-composer/chat-composer.meta.js +8 -2
- package/dist/components/command-menu/command-menu.js +9 -2
- package/dist/components/command-menu/command-menu.meta.js +3 -3
- package/dist/components/data-table/data-table.js +117 -64
- package/dist/components/data-table/data-table.meta.js +27 -1
- package/dist/components/form/form.js +30 -9
- package/dist/components/form/form.meta.js +17 -3
- package/dist/components/message-scroller/message-scroller.meta.js +1 -1
- package/dist/components/page-header/page-header.js +1 -0
- package/dist/components/page-header/page-header.meta.js +1 -1
- package/dist/components/settings-menu/settings-menu.js +10 -1
- package/dist/components/settings-menu/settings-menu.meta.js +1 -1
- package/dist/components/sidebar/sidebar.js +1 -1
- package/dist/components/sidebar/sidebar.meta.js +5 -0
- package/dist/components/switch/switch.js +1 -1
- package/dist/components/switch/switch.meta.js +2 -2
- package/dist/components/toggle/toggle.js +2 -1
- package/dist/file-hashes.json +603 -0
- package/dist/icon-sets/reicon.js +13 -1
- package/dist/index.d.ts +80 -13
- package/dist/index.js +1 -1
- package/dist/lib/heading-row.js +3 -2
- package/dist/lib/trim-label.js +17 -4
- package/dist/styles.css +27 -2
- package/llms/components/breadcrumbs.md +1 -1
- package/llms/components/button.md +1 -0
- package/llms/components/chat-composer.md +3 -2
- package/llms/components/command-menu.md +2 -2
- package/llms/components/data-table.md +33 -1
- package/llms/components/form.md +19 -2
- package/llms/components/message-scroller.md +1 -1
- package/llms/components/page-header.md +1 -1
- package/llms/components/settings-menu.md +1 -1
- package/llms/components/sidebar.md +1 -0
- package/llms/components/switch.md +2 -2
- package/llms/tasks/patching-a-component.md +48 -0
- package/llms/tasks/reviewing-a-design.md +5 -0
- package/llms-full.txt +121 -13
- package/llms.txt +1 -1
- package/package.json +4 -2
- package/registry.json +97 -20
- package/scripts/design-memory.mjs +7 -2
- package/scripts/lint-rules.mjs +32 -1
- package/scripts/patches.mjs +219 -0
- package/scripts/sources.mjs +247 -0
- package/skills/sable/SKILL.md +5 -2
|
@@ -17,13 +17,16 @@ function Form({
|
|
|
17
17
|
onSubmit,
|
|
18
18
|
onFormSubmit,
|
|
19
19
|
noValidate,
|
|
20
|
+
validationMode,
|
|
21
|
+
errors,
|
|
22
|
+
actionsRef,
|
|
20
23
|
...props
|
|
21
24
|
}) {
|
|
22
25
|
const blockedRef = useRef(false);
|
|
23
26
|
const handleSubmit = (event) => {
|
|
24
27
|
blockedRef.current = false;
|
|
25
28
|
const form = event.currentTarget;
|
|
26
|
-
if (!
|
|
29
|
+
if (!form.checkValidity()) {
|
|
27
30
|
event.preventDefault();
|
|
28
31
|
blockedRef.current = true;
|
|
29
32
|
form.reportValidity();
|
|
@@ -31,10 +34,24 @@ function Form({
|
|
|
31
34
|
}
|
|
32
35
|
onSubmit?.(event);
|
|
33
36
|
};
|
|
37
|
+
if (noValidate) {
|
|
38
|
+
return /* @__PURE__ */ jsx(
|
|
39
|
+
"form",
|
|
40
|
+
{
|
|
41
|
+
noValidate: true,
|
|
42
|
+
className: cn("flex flex-col gap-6", className),
|
|
43
|
+
onSubmit,
|
|
44
|
+
...props
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
34
48
|
return /* @__PURE__ */ jsx(
|
|
35
49
|
BaseForm,
|
|
36
50
|
{
|
|
37
51
|
className: cn("flex flex-col gap-6", className),
|
|
52
|
+
validationMode,
|
|
53
|
+
errors,
|
|
54
|
+
actionsRef,
|
|
38
55
|
onSubmit: handleSubmit,
|
|
39
56
|
onFormSubmit: onFormSubmit ? (values, details) => {
|
|
40
57
|
if (!blockedRef.current) onFormSubmit(values, details);
|
|
@@ -49,6 +66,7 @@ function Field({
|
|
|
49
66
|
description,
|
|
50
67
|
error,
|
|
51
68
|
required,
|
|
69
|
+
messages,
|
|
52
70
|
disabled,
|
|
53
71
|
name,
|
|
54
72
|
validate,
|
|
@@ -98,14 +116,17 @@ function Field({
|
|
|
98
116
|
children: description
|
|
99
117
|
}
|
|
100
118
|
) : null;
|
|
101
|
-
const errorNode = /* @__PURE__ */ jsx(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
const errorNode = /* @__PURE__ */ jsx(BaseField.Validity, { children: ({ validity }) => {
|
|
120
|
+
const flag = messages ? Object.keys(messages).find((key) => validity[key]) : void 0;
|
|
121
|
+
return /* @__PURE__ */ jsx(
|
|
122
|
+
BaseField.Error,
|
|
123
|
+
{
|
|
124
|
+
id: errId,
|
|
125
|
+
className: "text-body-sm text-destructive",
|
|
126
|
+
...hasError ? { match: true, children: error } : flag ? { children: messages?.[flag] } : null
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
} });
|
|
109
130
|
return /* @__PURE__ */ jsx(
|
|
110
131
|
BaseField.Root,
|
|
111
132
|
{
|
|
@@ -72,7 +72,7 @@ var formMeta = {
|
|
|
72
72
|
{
|
|
73
73
|
name: "Form",
|
|
74
74
|
type: '{ validationMode?: "onSubmit" | "onBlur" | "onChange"; errors?: Record<string, string | string[]>; onFormSubmit?: (values, details) => void; actionsRef?: RefObject<Form.Actions> } + ComponentPropsWithRef<"form">',
|
|
75
|
-
description: 'A <form> laid out as a vertical column with comfortable field spacing (override with className, e.g. gap-4). On submit it validates every Field (native constraints plus Field validate), shows each message under its field, focuses the first invalid control, and only then calls onSubmit. validationMode defaults to "onSubmit" (then live re-validation on change). errors shows external (e.g. server) messages keyed by Field name and clears one when its field is edited. onFormSubmit receives the named Field values and prevents the native submit. A control outside any Field is still checked by the browser before onSubmit runs
|
|
75
|
+
description: 'A <form> laid out as a vertical column with comfortable field spacing (override with className, e.g. gap-4). On submit it validates every Field (native constraints plus Field validate), shows each message under its field, focuses the first invalid control, and only then calls onSubmit. validationMode defaults to "onSubmit" (then live re-validation on change). errors shows external (e.g. server) messages keyed by Field name and clears one when its field is edited. onFormSubmit receives the named Field values and prevents the native submit. A control outside any Field is still checked by the browser before onSubmit runs. Validating in your own code (react-hook-form, a schema, state)? Pass noValidate: onSubmit always runs and each Field shows the error you set; without it an empty required Field stops the submit before your handler can set that error.'
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
name: "validationMode",
|
|
@@ -90,6 +90,12 @@ var formMeta = {
|
|
|
90
90
|
type: "(values: Record<string, unknown>, details) => void",
|
|
91
91
|
description: "Called with the named Field values after validation passes; prevents the native submit."
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: "noValidate",
|
|
95
|
+
type: "boolean",
|
|
96
|
+
default: "false",
|
|
97
|
+
description: "Your code owns validation: nothing is checked before onSubmit (neither the Fields nor the browser), so it always runs and each Field shows the error you pass. required Fields keep their asterisk and aria-required. validationMode, errors, onFormSubmit and actionsRef do nothing with it."
|
|
98
|
+
},
|
|
93
99
|
{
|
|
94
100
|
name: "actionsRef",
|
|
95
101
|
type: "RefObject<{ validate(name?: string): void }>",
|
|
@@ -99,8 +105,8 @@ var formMeta = {
|
|
|
99
105
|
subcomponents: [
|
|
100
106
|
{
|
|
101
107
|
name: "Field",
|
|
102
|
-
type: '{ label?: ReactNode; description?: ReactNode; error?: ReactNode; required?: boolean; disabled?: boolean; name?: string; validate?: (value, formValues) => string | string[] | null | Promise<\u2026>; validationMode?: "onSubmit" | "onBlur" | "onChange"; validationDebounceTime?: number; orientation?: "vertical" | "horizontal"; className?: string; children: ReactElement }',
|
|
103
|
-
description: `Wraps a single control element (e.g. <Input />) on Base UI Field and wires label (htmlFor + aria-labelledby), helper text and error (aria-describedby), aria-invalid, and required onto it automatically. Base UI-backed controls (Input, Textarea, Select, Combobox, NumberField, Checkbox, Switch, Slider, OtpField\u2026) register through context; the child is also cloned so plain elements keep the same wiring. error renders a destructive message and takes precedence; without it the slot shows Base UI's validation message
|
|
108
|
+
type: '{ label?: ReactNode; description?: ReactNode; error?: ReactNode; required?: boolean; messages?: Partial<Record<"valueMissing" | "typeMismatch" | "patternMismatch" | "tooShort" | "tooLong" | "rangeUnderflow" | "rangeOverflow" | "stepMismatch" | "badInput", ReactNode>>; disabled?: boolean; name?: string; validate?: (value, formValues) => string | string[] | null | Promise<\u2026>; validationMode?: "onSubmit" | "onBlur" | "onChange"; validationDebounceTime?: number; orientation?: "vertical" | "horizontal"; className?: string; children: ReactElement }',
|
|
109
|
+
description: `Wraps a single control element (e.g. <Input />) on Base UI Field and wires label (htmlFor + aria-labelledby), helper text and error (aria-describedby), aria-invalid, and required onto it automatically. Base UI-backed controls (Input, Textarea, Select, Combobox, NumberField, Checkbox, Switch, Slider, OtpField\u2026) register through context; the child is also cloned so plain elements keep the same wiring. error renders a destructive message and takes precedence; without it the slot shows the field's messages entry for the native constraint that failed (messages={{ valueMissing: "Enter a subject." }}), else Base UI's validation message (the browser's wording for a native constraint, validate, or Form errors). required adds the native attribute plus a visual asterisk (defaults to false). disabled disables the control and dims the label. name keys the field in Form errors / onFormSubmit. validationMode overrides the Form's. orientation defaults to "vertical" (label \u2192 control); "horizontal" places control then label inline for a single checkbox / switch / radio \u2014 the control centres on the label's first line (it stays there when the label wraps) and description/error sit in the label's column. The root, label, description, error and control carry data-invalid / data-valid, data-touched, data-dirty, data-filled, data-focused and data-disabled for state styling.`
|
|
104
110
|
}
|
|
105
111
|
],
|
|
106
112
|
tokensUsed: ["muted-foreground", "destructive"],
|
|
@@ -119,6 +125,14 @@ var formMeta = {
|
|
|
119
125
|
label: "Field with an error",
|
|
120
126
|
code: '<Field label="Password" error="Must be at least 8 characters."><Input type="password" /></Field>'
|
|
121
127
|
},
|
|
128
|
+
{
|
|
129
|
+
label: "Your own wording for a missing value",
|
|
130
|
+
code: '<Form onSubmit={handleSubmit}><Field label="Subject" required messages={{ valueMissing: "Enter a subject." }}><Input name="subject" /></Field><Button type="submit">Send</Button></Form>'
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
label: "Validation in your own code",
|
|
134
|
+
code: '<Form noValidate onSubmit={handleSubmit}><Field label="Subject" required error={errors.subject}><Input name="subject" /></Field><Button type="submit">Send</Button></Form>'
|
|
135
|
+
},
|
|
122
136
|
{
|
|
123
137
|
label: "Validated on submit",
|
|
124
138
|
code: '<Form onSubmit={handleSubmit}><Field label="Handle" required validate={(value) => String(value).length < 3 ? "At least 3 characters." : null}><Input name="handle" /></Field><Button type="submit">Claim</Button></Form>'
|
|
@@ -86,7 +86,7 @@ var messageScrollerMeta = {
|
|
|
86
86
|
{
|
|
87
87
|
name: "ref",
|
|
88
88
|
type: "Ref<MessageScrollerHandle>",
|
|
89
|
-
description: "Imperative commands, not the DOM node: `scrollToBottom(opts)`, `scrollToTop(opts)`, `scrollToMessage(id, opts)` (returns false when the message is not rendered), `isFollowing()`, and `element` for the scrolling node itself."
|
|
89
|
+
description: "Imperative commands, not the DOM node: `scrollToBottom(opts)`, `scrollToTop(opts)`, `scrollToMessage(id, opts)` (returns false when the message is not rendered), `isFollowing()`, and `element` for the scrolling node itself. `opts` is `ScrollCommandOptions`: `{ animate?: boolean }` (default true, and off whenever motion is), not the DOM's `behavior`."
|
|
90
90
|
},
|
|
91
91
|
{
|
|
92
92
|
name: "autoFollow",
|
|
@@ -57,7 +57,7 @@ var pageHeaderMeta = {
|
|
|
57
57
|
{
|
|
58
58
|
name: "actions",
|
|
59
59
|
type: "ReactNode",
|
|
60
|
-
description: "Trailing actions aligned to the title row's end. They drop under the title when the
|
|
60
|
+
description: "Trailing actions aligned to the title row's end. They drop under the title when the title would have less than 20rem beside them (on a phone, nearly always), and the title wraps rather than truncating. A ghost or link Button at either end of the group bleeds by its own padding, so its label meets the content edge (the end on the title row, the start once wrapped); filled buttons keep their edge on the line. Direct children only."
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
name: "titleAs",
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// src/components/settings-menu/settings-menu.tsx
|
|
2
|
+
import { Children, isValidElement } from "react";
|
|
2
3
|
import { Icon } from "../../icon/index.js";
|
|
3
4
|
import { cn } from "../../lib/cn.js";
|
|
4
5
|
import { useTypeTier } from "../../lib/density.js";
|
|
5
6
|
import {
|
|
6
7
|
Menu,
|
|
7
8
|
MenuContent,
|
|
9
|
+
MenuGroup,
|
|
10
|
+
MenuGroupLabel,
|
|
8
11
|
MenuSubmenu,
|
|
9
12
|
MenuSubmenuTrigger,
|
|
10
13
|
MenuTrigger
|
|
@@ -29,6 +32,12 @@ function SettingsMenu({
|
|
|
29
32
|
...props
|
|
30
33
|
}) {
|
|
31
34
|
const tier = useTypeTier();
|
|
35
|
+
const rows = Children.toArray(children);
|
|
36
|
+
const only = rows.length === 1 && isValidElement(rows[0]) && rows[0].type === SettingsRow ? rows[0] : null;
|
|
37
|
+
const content = only?.props.children && !only.props.disabled ? /* @__PURE__ */ jsxs(MenuGroup, { children: [
|
|
38
|
+
/* @__PURE__ */ jsx(MenuGroupLabel, { children: only.props.label }),
|
|
39
|
+
only.props.children
|
|
40
|
+
] }) : children;
|
|
32
41
|
return /* @__PURE__ */ jsxs(Menu, { ...props, children: [
|
|
33
42
|
/* @__PURE__ */ jsxs(
|
|
34
43
|
MenuTrigger,
|
|
@@ -49,7 +58,7 @@ function SettingsMenu({
|
|
|
49
58
|
]
|
|
50
59
|
}
|
|
51
60
|
),
|
|
52
|
-
/* @__PURE__ */ jsx(MenuContent, { side, align, className: "min-w-56", children })
|
|
61
|
+
/* @__PURE__ */ jsx(MenuContent, { side, align, className: "min-w-56", children: content })
|
|
53
62
|
] });
|
|
54
63
|
}
|
|
55
64
|
SettingsMenu.displayName = "SettingsMenu";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/components/settings-menu/settings-menu.meta.ts
|
|
2
2
|
var settingsMenuMeta = {
|
|
3
3
|
name: "SettingsMenu",
|
|
4
|
-
summary: "A compact trigger that states the current settings, over a menu of rows that change them. Each row shows its own value and opens a submenu.",
|
|
4
|
+
summary: "A compact trigger that states the current settings, over a menu of rows that change them. Each row shows its own value and opens a submenu; with one setting, the menu opens straight to its options under the row's label.",
|
|
5
5
|
status: "stable",
|
|
6
6
|
keywords: [
|
|
7
7
|
"settings",
|
|
@@ -72,7 +72,7 @@ function SidebarHeader({
|
|
|
72
72
|
"div",
|
|
73
73
|
{
|
|
74
74
|
className: cn(
|
|
75
|
-
"flex h-16 items-center px-3",
|
|
75
|
+
"flex min-h-16 flex-wrap content-center items-center gap-y-2 px-3 has-[>:nth-child(2)]:py-3",
|
|
76
76
|
// Collapsed, the brand mark centres on the same axis as the item icons.
|
|
77
77
|
collapsed && "justify-center px-0",
|
|
78
78
|
className
|
|
@@ -70,6 +70,11 @@ var sidebarMeta = {
|
|
|
70
70
|
}
|
|
71
71
|
],
|
|
72
72
|
subcomponents: [
|
|
73
|
+
{
|
|
74
|
+
name: "SidebarHeader",
|
|
75
|
+
type: "div props",
|
|
76
|
+
description: "The top of the rail, at least 64px tall. It holds SidebarBrand and may take a second row under it, such as a search field or a notifications button: give that row basis-full and the header grows to hold it instead of clipping."
|
|
77
|
+
},
|
|
73
78
|
{
|
|
74
79
|
name: "SidebarBrand",
|
|
75
80
|
type: "{ mark: ReactNode; label: ReactNode; render?: useRender.RenderProp } + div props",
|
|
@@ -16,7 +16,7 @@ function Switch({ className, ...props }) {
|
|
|
16
16
|
className
|
|
17
17
|
),
|
|
18
18
|
...props,
|
|
19
|
-
children: /* @__PURE__ */ jsx(Base.Thumb, { className: "block h-5 w-5 rounded-full bg-(--switch-thumb) shadow-(--knob-shadow) motion-interactive ease-spring data-[checked]:translate-x-4 rtl:data-[checked]:-translate-x-4 data-[checked]:bg-
|
|
19
|
+
children: /* @__PURE__ */ jsx(Base.Thumb, { className: "block h-5 w-5 rounded-full bg-(--switch-thumb) shadow-(--knob-shadow) motion-interactive ease-spring data-[checked]:translate-x-4 rtl:data-[checked]:-translate-x-4 data-[checked]:bg-(--switch-thumb-on)" })
|
|
20
20
|
}
|
|
21
21
|
);
|
|
22
22
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/components/switch/switch.meta.ts
|
|
2
2
|
var switchMeta = {
|
|
3
3
|
name: "Switch",
|
|
4
|
-
summary: "Binary on/off toggle for immediate-effect settings. Track fills with the primary tone when checked; the thumb slides on a spring-eased transform (mirrored in RTL).",
|
|
4
|
+
summary: "Binary on/off toggle for immediate-effect settings. Track fills with the primary tone when checked; the thumb slides on a spring-eased transform (mirrored in RTL). The thumb is white in both states, like every physical toggle, and turns dark only on a near-white track (a dark mode's light primary). Themes can set --theme-switch-thumb-on.",
|
|
5
5
|
status: "stable",
|
|
6
6
|
keywords: [
|
|
7
7
|
"switch",
|
|
@@ -82,7 +82,7 @@ var switchMeta = {
|
|
|
82
82
|
description: "Identifies the field when the enclosing form is submitted."
|
|
83
83
|
}
|
|
84
84
|
],
|
|
85
|
-
tokensUsed: ["input", "primary", "
|
|
85
|
+
tokensUsed: ["input", "primary", "ring"],
|
|
86
86
|
motion: [
|
|
87
87
|
{
|
|
88
88
|
slot: "arrival",
|
|
@@ -22,7 +22,8 @@ function Toggle({ className, ...props }) {
|
|
|
22
22
|
),
|
|
23
23
|
"data-[pressed]:bg-accent data-[pressed]:text-accent-foreground",
|
|
24
24
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
25
|
-
|
|
25
|
+
// An icon beside a label is spaced like Button's; without it they touched.
|
|
26
|
+
"gap-(--control-gap) [&_svg]:shrink-0",
|
|
26
27
|
className
|
|
27
28
|
),
|
|
28
29
|
...props
|