@sableui/react 0.7.0 → 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 +115 -0
- package/README.md +2 -0
- package/bin/sable.mjs +34 -0
- package/dist/blocks/traffic-sources-block.js +1 -1
- package/dist/blocks/traffic-trend-block.js +3 -3
- 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/card/card.js +7 -4
- 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/pie-chart/pie-chart.js +3 -3
- package/dist/components/pie-chart/pie-chart.meta.js +3 -3
- package/dist/components/radial-chart/radial-chart.js +3 -3
- package/dist/components/radial-chart/radial-chart.meta.js +2 -2
- package/dist/components/settings-menu/settings-menu.js +10 -1
- package/dist/components/settings-menu/settings-menu.meta.js +1 -1
- package/dist/components/side-nav/side-nav.variants.js +3 -2
- 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/table/table.js +1 -1
- 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 +88 -15
- package/dist/index.js +1 -1
- package/dist/layout/index.d.ts +5 -2
- package/dist/lib/chart/format.js +5 -0
- package/dist/lib/component-guidance.js +7 -2
- package/dist/lib/heading-row.js +3 -2
- package/dist/lib/trim-label.js +17 -4
- package/dist/presets/index.d.ts +7 -1
- package/dist/presets/index.js +10 -0
- package/dist/styles.css +27 -2
- package/llms/blocks/traffic-sources-block.md +1 -1
- package/llms/blocks/traffic-trend-block.md +5 -3
- 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/pie-chart.md +2 -2
- package/llms/components/radial-chart.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/layout/region.md +16 -3
- package/llms/tasks/building-a-page.md +34 -3
- package/llms/tasks/creating-a-preset.md +4 -1
- package/llms/tasks/patching-a-component.md +48 -0
- package/llms/tasks/reviewing-a-design.md +9 -0
- package/llms/tasks/using-presets.md +19 -0
- package/llms-full.txt +207 -27
- package/llms.txt +1 -1
- package/package.json +4 -2
- package/registry.json +132 -30
- package/scripts/design-memory.mjs +7 -2
- package/scripts/design-presets.mjs +15 -4
- package/scripts/lint-rules.mjs +32 -1
- package/scripts/patches.mjs +219 -0
- package/scripts/sources.mjs +247 -0
- package/skills/sable/SKILL.md +16 -4
|
@@ -5,6 +5,7 @@ import { useMemo, useState } from "react";
|
|
|
5
5
|
import { Icon } from "../../icon/index.js";
|
|
6
6
|
import { cn } from "../../lib/cn.js";
|
|
7
7
|
import { Button } from "../button/button.js";
|
|
8
|
+
import { Checkbox } from "../checkbox/index.js";
|
|
8
9
|
import {
|
|
9
10
|
Table,
|
|
10
11
|
TableBody,
|
|
@@ -28,6 +29,10 @@ function DataTable({
|
|
|
28
29
|
rowKey,
|
|
29
30
|
onRowClick,
|
|
30
31
|
isRowSelected,
|
|
32
|
+
selectedKeys,
|
|
33
|
+
onSelectedKeysChange,
|
|
34
|
+
rowSelectionLabel = () => "Select row",
|
|
35
|
+
selectAllLabel = (count) => `Select all ${count} shown`,
|
|
31
36
|
defaultSort,
|
|
32
37
|
empty = "No results.",
|
|
33
38
|
caption,
|
|
@@ -58,82 +63,130 @@ function DataTable({
|
|
|
58
63
|
return sort.direction === "asc" ? c : -c;
|
|
59
64
|
});
|
|
60
65
|
}, [data, sort, columns]);
|
|
66
|
+
const selectable = Boolean(onSelectedKeysChange);
|
|
67
|
+
const dataIndex = selectable ? new Map(data.map((row, i) => [row, i])) : null;
|
|
68
|
+
const keyOf = (row) => {
|
|
69
|
+
const i = dataIndex?.get(row) ?? 0;
|
|
70
|
+
return rowKey ? rowKey(row, i) : i;
|
|
71
|
+
};
|
|
72
|
+
const shownKeys = selectable ? rows.map(keyOf) : [];
|
|
73
|
+
const checkedCount = selectable ? shownKeys.filter((k) => selectedKeys?.has(k)).length : 0;
|
|
74
|
+
const allChecked = shownKeys.length > 0 && checkedCount === shownKeys.length;
|
|
75
|
+
function setChecked(keys, checked) {
|
|
76
|
+
const next = new Set(selectedKeys);
|
|
77
|
+
for (const k of keys) {
|
|
78
|
+
if (checked) next.add(k);
|
|
79
|
+
else next.delete(k);
|
|
80
|
+
}
|
|
81
|
+
onSelectedKeysChange?.(next);
|
|
82
|
+
}
|
|
61
83
|
return /* @__PURE__ */ jsxs(Table, { className, children: [
|
|
62
84
|
caption ? /* @__PURE__ */ jsx(TableCaption, { children: caption }) : null,
|
|
63
|
-
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
TableHead,
|
|
85
|
+
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
86
|
+
selectable ? /* @__PURE__ */ jsx(TableHead, { className: "w-0", children: /* @__PURE__ */ jsx(
|
|
87
|
+
Checkbox,
|
|
67
88
|
{
|
|
68
|
-
"aria-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
89
|
+
"aria-label": selectAllLabel(shownKeys.length),
|
|
90
|
+
checked: allChecked,
|
|
91
|
+
indeterminate: checkedCount > 0 && !allChecked,
|
|
92
|
+
disabled: shownKeys.length === 0,
|
|
93
|
+
onCheckedChange: (checked) => setChecked(shownKeys, checked),
|
|
94
|
+
className: "align-middle"
|
|
95
|
+
}
|
|
96
|
+
) }) : null,
|
|
97
|
+
columns.map((col) => {
|
|
98
|
+
const active = sort?.key === col.key;
|
|
99
|
+
return /* @__PURE__ */ jsx(
|
|
100
|
+
TableHead,
|
|
101
|
+
{
|
|
102
|
+
"aria-sort": active ? sort.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
103
|
+
className: cn(col.align && ALIGN[col.align], col.className),
|
|
104
|
+
children: col.sortable ? (
|
|
105
|
+
// The negative inline margin cancels Button's padding so
|
|
106
|
+
// the label stays on the column's text edge; the header's
|
|
107
|
+
// own type and ink win over Button's action tier. In an
|
|
108
|
+
// end-aligned column the chevron leads, so the LABEL is
|
|
109
|
+
// what sits on the numbers' edge.
|
|
110
|
+
/* @__PURE__ */ jsxs(
|
|
111
|
+
Button,
|
|
112
|
+
{
|
|
113
|
+
variant: "ghost",
|
|
114
|
+
size: "xs",
|
|
115
|
+
onClick: () => toggleSort(col.key),
|
|
116
|
+
className: cn(
|
|
117
|
+
"-mx-2 gap-1 text-label-md text-muted-foreground hover:text-foreground",
|
|
118
|
+
col.align === "end" && "flex-row-reverse"
|
|
119
|
+
),
|
|
120
|
+
children: [
|
|
121
|
+
col.header,
|
|
122
|
+
/* @__PURE__ */ jsx(
|
|
123
|
+
Icon,
|
|
124
|
+
{
|
|
125
|
+
name: active && sort.direction === "desc" ? "chevron-down" : "chevron-up",
|
|
126
|
+
className: cn("text-xs", !active && "opacity-60")
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
) : col.header
|
|
133
|
+
},
|
|
134
|
+
col.key
|
|
135
|
+
);
|
|
136
|
+
})
|
|
137
|
+
] }) }),
|
|
103
138
|
/* @__PURE__ */ jsx(TableBody, { children: rows.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
104
139
|
TableCell,
|
|
105
140
|
{
|
|
106
|
-
colSpan: columns.length,
|
|
141
|
+
colSpan: columns.length + (selectable ? 1 : 0),
|
|
107
142
|
className: cn(
|
|
108
143
|
"text-center text-muted-foreground",
|
|
109
144
|
typeof empty === "string" && "py-8"
|
|
110
145
|
),
|
|
111
146
|
children: empty
|
|
112
147
|
}
|
|
113
|
-
) }) : rows.map((row, i) =>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
e
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
) }) : rows.map((row, i) => {
|
|
149
|
+
const key = selectable ? keyOf(row) : i;
|
|
150
|
+
const checked = selectable && Boolean(selectedKeys?.has(key));
|
|
151
|
+
return /* @__PURE__ */ jsxs(
|
|
152
|
+
TableRow,
|
|
153
|
+
{
|
|
154
|
+
"data-selected": checked || isRowSelected?.(row) ? "" : void 0,
|
|
155
|
+
onClick: onRowClick ? () => onRowClick(row) : void 0,
|
|
156
|
+
tabIndex: onRowClick ? 0 : void 0,
|
|
157
|
+
onKeyDown: onRowClick ? (e) => {
|
|
158
|
+
if (e.target !== e.currentTarget) return;
|
|
159
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
onRowClick(row);
|
|
162
|
+
} : void 0,
|
|
163
|
+
className: onRowClick ? "cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" : void 0,
|
|
164
|
+
children: [
|
|
165
|
+
selectable ? (
|
|
166
|
+
// A click on the box checks it; it never opens the row.
|
|
167
|
+
/* @__PURE__ */ jsx(TableCell, { className: "w-0", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
|
|
168
|
+
Checkbox,
|
|
169
|
+
{
|
|
170
|
+
"aria-label": rowSelectionLabel(row),
|
|
171
|
+
checked,
|
|
172
|
+
onCheckedChange: (next) => setChecked([key], next),
|
|
173
|
+
className: "align-middle"
|
|
174
|
+
}
|
|
175
|
+
) })
|
|
176
|
+
) : null,
|
|
177
|
+
columns.map((col) => /* @__PURE__ */ jsx(
|
|
178
|
+
TableCell,
|
|
179
|
+
{
|
|
180
|
+
className: cn(col.align && ALIGN[col.align], col.className),
|
|
181
|
+
children: col.cell ? col.cell(row) : row[col.key]
|
|
182
|
+
},
|
|
183
|
+
col.key
|
|
184
|
+
))
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
rowKey ? rowKey(row, i) : i
|
|
188
|
+
);
|
|
189
|
+
}) })
|
|
137
190
|
] });
|
|
138
191
|
}
|
|
139
192
|
DataTable.displayName = "DataTable";
|
|
@@ -93,7 +93,29 @@ var dataTableMeta = {
|
|
|
93
93
|
{
|
|
94
94
|
name: "isRowSelected",
|
|
95
95
|
type: "(row) => boolean",
|
|
96
|
-
description: "Mark a row visually selected (data-selected). Derive it from a stable id (row.id === selectedId), not the row index, so selection survives re-sorts."
|
|
96
|
+
description: "Mark a row visually selected (data-selected), e.g. the one open in a detail pane. Derive it from a stable id (row.id === selectedId), not the row index, so selection survives re-sorts. For checking several rows, use selectedKeys."
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "selectedKeys",
|
|
100
|
+
type: "ReadonlySet<string | number>",
|
|
101
|
+
description: "The checked rows, by rowKey, for bulk actions. With onSelectedKeysChange it adds a checkbox column (each box named by rowSelectionLabel) and a select-all box in the header that acts on the rows shown and goes mixed when some are checked. Checked rows get the selected fill. Controlled: when the rows shown change (a filter, another view), set the keys to keep, usually none. Needs rowKey."
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "onSelectedKeysChange",
|
|
105
|
+
type: "(keys: Set<string | number>) => void",
|
|
106
|
+
description: "Called with the new set of checked keys. Its presence turns selection on. A click on a box never fires onRowClick."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "rowSelectionLabel",
|
|
110
|
+
type: "(row) => string",
|
|
111
|
+
default: '() => "Select row"',
|
|
112
|
+
description: "A row checkbox's accessible name. Name the row, e.g. (t) => `Select ${t.subject}`, so a screen reader can tell the boxes apart."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "selectAllLabel",
|
|
116
|
+
type: "(count: number) => string",
|
|
117
|
+
default: "(count) => `Select all ${count} shown`",
|
|
118
|
+
description: `The select-all checkbox's accessible name; say what it acts on ("Select all 7 tickets shown").`
|
|
97
119
|
},
|
|
98
120
|
{
|
|
99
121
|
name: "defaultSort",
|
|
@@ -119,6 +141,10 @@ var dataTableMeta = {
|
|
|
119
141
|
"Inherits the Table's scrollable frame and semantic <table>/<th>/<td> structure."
|
|
120
142
|
],
|
|
121
143
|
examples: [
|
|
144
|
+
{
|
|
145
|
+
label: "Checkbox selection with a bulk-action bar",
|
|
146
|
+
code: 'const [selected, setSelected] = useState<Set<string | number>>(new Set())\n\n<Stack gap={2}>\n {/* Mounted from the start, so a screen reader hears each change. */}\n <span role="status" className="sr-only">{selected.size ? `${selected.size} of ${tickets.length} selected` : ""}</span>\n {selected.size ? (\n <Toolbar aria-label="Bulk actions">\n <span className="text-label-md">{selected.size} of {tickets.length} selected</span>\n <Button size="sm" variant="secondary" onClick={() => resolve(selected)}>Resolve</Button>\n </Toolbar>\n ) : null}\n <DataTable\n data={tickets}\n rowKey={(t) => t.id}\n columns={columns}\n selectedKeys={selected}\n onSelectedKeysChange={setSelected}\n rowSelectionLabel={(t) => `Select ${t.subject}`}\n selectAllLabel={(n) => `Select all ${n} tickets shown`}\n />\n</Stack>'
|
|
147
|
+
},
|
|
122
148
|
{
|
|
123
149
|
label: "Sortable columns",
|
|
124
150
|
code: '<DataTable data={users} columns={[{ key: "name", header: "Name", sortable: true }, { key: "role", header: "Role" }, { key: "joined", header: "Joined", sortable: true, sortValue: (u) => u.joinedAt, cell: (u) => formatDate(u.joinedAt) }]} />'
|
|
@@ -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",
|
|
@@ -5,7 +5,7 @@ import { useId } from "react";
|
|
|
5
5
|
import { ChartFrame } from "../../lib/chart/chart-frame.js";
|
|
6
6
|
import { ChartLegend } from "../../lib/chart/chart-legend.js";
|
|
7
7
|
import { ChartTooltipContent } from "../../lib/chart/chart-tooltip.js";
|
|
8
|
-
import { categoryText, chartFormatters } from "../../lib/chart/format.js";
|
|
8
|
+
import { categoryText, centerValueText, chartFormatters } from "../../lib/chart/format.js";
|
|
9
9
|
import { arcPath, polar } from "../../lib/chart/geometry.js";
|
|
10
10
|
import { DRAW_DURATION, morphTransition, pathStyle } from "../../lib/chart/motion.js";
|
|
11
11
|
import {
|
|
@@ -177,8 +177,8 @@ function PieChart({
|
|
|
177
177
|
);
|
|
178
178
|
}) : null,
|
|
179
179
|
centerLabel && g.inner > 0 ? /* @__PURE__ */ jsxs("text", { x: g.cx, y: g.cy, textAnchor: "middle", className: "[[data-reveal=out]_&]:opacity-0", style: { transition: morphTransition() }, children: [
|
|
180
|
-
/* @__PURE__ */ jsx("tspan", { x: g.cx, dy:
|
|
181
|
-
centerLabel.label ? /* @__PURE__ */ jsx("tspan", { x: g.cx, dy: "1.
|
|
180
|
+
/* @__PURE__ */ jsx("tspan", { x: g.cx, dy: "0.35em", className: "fill-foreground text-heading-2 tabular-nums", children: centerValueText(centerLabel.value, locale) }),
|
|
181
|
+
centerLabel.label ? /* @__PURE__ */ jsx("tspan", { x: g.cx, dy: "1.9em", className: "fill-muted-foreground text-caption", children: centerLabel.label }) : null
|
|
182
182
|
] }) : null
|
|
183
183
|
] });
|
|
184
184
|
}
|
|
@@ -83,8 +83,8 @@ var pieChartMeta = {
|
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
name: "centerLabel",
|
|
86
|
-
type: "{ value: string; label?: string }",
|
|
87
|
-
description: "A value and caption in the hole of a donut."
|
|
86
|
+
type: "{ value: string | number; label?: string }",
|
|
87
|
+
description: "A value and caption in the hole of a donut. The value holds the centre and the caption hangs below it. A number is formatted for you: in full up to 9,999, compact from 10,000 (10.2K, 102.1K, 1.2M); pass a string for units."
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
name: "separator",
|
|
@@ -197,7 +197,7 @@ var pieChartMeta = {
|
|
|
197
197
|
examples: [
|
|
198
198
|
{
|
|
199
199
|
label: "Donut with a total",
|
|
200
|
-
code: 'const sources = [\n { source: "Search", visitors: 275 },\n { source: "Direct", visitors: 200 },\n { source: "Social", visitors: 187 },\n { source: "Email", visitors: 90 },\n]\n\n<PieChart\n label="Visitors by source"\n data={sources}\n index="source"\n valueKey="visitors"\n donut\n centerLabel={{ value:
|
|
200
|
+
code: 'const sources = [\n { source: "Search", visitors: 275 },\n { source: "Direct", visitors: 200 },\n { source: "Social", visitors: 187 },\n { source: "Email", visitors: 90 },\n]\n\n<PieChart\n label="Visitors by source"\n data={sources}\n index="source"\n valueKey="visitors"\n donut\n centerLabel={{ value: 752, label: "Visitors" }}\n/>'
|
|
201
201
|
},
|
|
202
202
|
{
|
|
203
203
|
label: "Labelled slices",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { ChartFrame } from "../../lib/chart/chart-frame.js";
|
|
5
5
|
import { ChartLegend } from "../../lib/chart/chart-legend.js";
|
|
6
6
|
import { ChartTooltipContent } from "../../lib/chart/chart-tooltip.js";
|
|
7
|
-
import { categoryText, chartFormatters } from "../../lib/chart/format.js";
|
|
7
|
+
import { categoryText, centerValueText, chartFormatters } from "../../lib/chart/format.js";
|
|
8
8
|
import { polar, strokeArcPath } from "../../lib/chart/geometry.js";
|
|
9
9
|
import { drawTransition, morphTransition, pathStyle, stagger } from "../../lib/chart/motion.js";
|
|
10
10
|
import {
|
|
@@ -159,8 +159,8 @@ function RadialChart({
|
|
|
159
159
|
className: "[[data-reveal=out]_&]:opacity-0",
|
|
160
160
|
style: { transition: morphTransition() },
|
|
161
161
|
children: [
|
|
162
|
-
/* @__PURE__ */ jsx("tspan", { x: g.cx, dy: gauge && sweep <= 180 ? centerLabel.label ? "-1.4em" : "0" :
|
|
163
|
-
centerLabel.label ? /* @__PURE__ */ jsx("tspan", { x: g.cx, dy: "1.6em", className: "fill-muted-foreground text-caption", children: centerLabel.label }) : null
|
|
162
|
+
/* @__PURE__ */ jsx("tspan", { x: g.cx, dy: gauge && sweep <= 180 ? centerLabel.label ? "-1.4em" : "0" : "0.35em", className: "fill-foreground text-heading-2 tabular-nums", children: centerValueText(centerLabel.value, locale) }),
|
|
163
|
+
centerLabel.label ? /* @__PURE__ */ jsx("tspan", { x: g.cx, dy: gauge && sweep <= 180 ? "1.6em" : "1.9em", className: "fill-muted-foreground text-caption", children: centerLabel.label }) : null
|
|
164
164
|
]
|
|
165
165
|
}
|
|
166
166
|
) : null
|
|
@@ -107,8 +107,8 @@ var radialChartMeta = {
|
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
name: "centerLabel",
|
|
110
|
-
type: "{ value: string; label?: string }",
|
|
111
|
-
description:
|
|
110
|
+
type: "{ value: string | number; label?: string }",
|
|
111
|
+
description: 'A value and caption in the middle. On a full ring the value holds the centre and the caption hangs below it. A number is formatted for you: in full up to 9,999, compact from 10,000 (10.2K); pass a string for units ("7.2 GB", "72%").'
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
name: "legend",
|
|
@@ -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",
|
|
@@ -20,9 +20,10 @@ var sideNavItemVariants = cva(
|
|
|
20
20
|
}
|
|
21
21
|
);
|
|
22
22
|
var sideNavItemIconVariants = cva(
|
|
23
|
-
"
|
|
23
|
+
"flex shrink-0 items-center justify-center",
|
|
24
24
|
{
|
|
25
|
-
|
|
25
|
+
// md 20px icon in a 20px box; lg 20px in 24px; rail 24px in 24px.
|
|
26
|
+
variants: { size: { md: "size-[1em] text-xl", lg: "size-[1.2em] text-xl", rail: "size-[1em] text-2xl" } },
|
|
26
27
|
defaultVariants: { size: "md" }
|
|
27
28
|
}
|
|
28
29
|
);
|
|
@@ -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",
|
|
@@ -51,7 +51,7 @@ function TableRow({ className, ...props }) {
|
|
|
51
51
|
"tr",
|
|
52
52
|
{
|
|
53
53
|
className: cn(
|
|
54
|
-
"border-b border-border motion-interactive hover:bg-muted/50",
|
|
54
|
+
"border-b border-border motion-interactive [tbody>&:hover]:bg-muted/50",
|
|
55
55
|
"*:motion-interactive data-[selected]:*:bg-muted data-[selected]:*:shadow-[1px_0_0_var(--muted)]",
|
|
56
56
|
className
|
|
57
57
|
),
|
|
@@ -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
|