@misoto22/design 0.8.0 → 0.9.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/agent/Accordion.md +1 -1
- package/dist/agent/Alert.md +1 -1
- package/dist/agent/AppShell.md +1 -1
- package/dist/agent/ArchitectureFigure.md +1 -1
- package/dist/agent/AreaChart.md +1 -1
- package/dist/agent/Article.md +1 -1
- package/dist/agent/AspectRatio.md +1 -1
- package/dist/agent/Avatar.md +1 -1
- package/dist/agent/Badge.md +1 -1
- package/dist/agent/BarChart.md +1 -1
- package/dist/agent/BarList.md +1 -1
- package/dist/agent/BigNumber.md +1 -1
- package/dist/agent/BoxPlot.md +1 -1
- package/dist/agent/Breadcrumb.md +1 -1
- package/dist/agent/BulletChart.md +1 -1
- package/dist/agent/Button.md +1 -1
- package/dist/agent/Calendar.md +1 -1
- package/dist/agent/Card.md +1 -1
- package/dist/agent/Checkbox.md +1 -1
- package/dist/agent/Code.md +1 -1
- package/dist/agent/CodeBlock.md +1 -1
- package/dist/agent/Collapsible.md +1 -1
- package/dist/agent/ColorPicker.md +75 -0
- package/dist/agent/Combobox.md +1 -1
- package/dist/agent/Command.md +1 -1
- package/dist/agent/ComposedChart.md +1 -1
- package/dist/agent/ContextMenu.md +1 -1
- package/dist/agent/DataflowFigure.md +1 -1
- package/dist/agent/DatePicker.md +1 -1
- package/dist/agent/DescriptionList.md +1 -1
- package/dist/agent/Diagram.md +1 -1
- package/dist/agent/DiagramCanvas.md +1 -1
- package/dist/agent/DiagramExportMenu.md +1 -1
- package/dist/agent/DiagramInspector.md +1 -1
- package/dist/agent/DiagramLegend.md +1 -1
- package/dist/agent/DiagramMinimap.md +1 -1
- package/dist/agent/DiagramToolbar.md +1 -1
- package/dist/agent/Dialog.md +1 -1
- package/dist/agent/DropdownMenu.md +1 -1
- package/dist/agent/EmptyState.md +1 -1
- package/dist/agent/ErrorState.md +1 -1
- package/dist/agent/Facet.md +1 -1
- package/dist/agent/Field.md +1 -1
- package/dist/agent/FigureBand.md +1 -1
- package/dist/agent/FloatingIconButton.md +1 -1
- package/dist/agent/FunnelChart.md +1 -1
- package/dist/agent/Heading.md +1 -1
- package/dist/agent/Heatmap.md +1 -1
- package/dist/agent/Histogram.md +1 -1
- package/dist/agent/Input.md +1 -1
- package/dist/agent/Kbd.md +1 -1
- package/dist/agent/LifecycleFigure.md +1 -1
- package/dist/agent/LineChart.md +1 -1
- package/dist/agent/LinkArrow.md +1 -1
- package/dist/agent/Markdown.md +1 -1
- package/dist/agent/NativeSelect.md +1 -1
- package/dist/agent/NavItem.md +1 -1
- package/dist/agent/NumberField.md +72 -0
- package/dist/agent/Pagination.md +1 -1
- package/dist/agent/PieChart.md +1 -1
- package/dist/agent/Popover.md +1 -1
- package/dist/agent/Progress.md +1 -1
- package/dist/agent/RadarChart.md +1 -1
- package/dist/agent/RadialChart.md +1 -1
- package/dist/agent/RadioGroup.md +1 -1
- package/dist/agent/SankeyChart.md +1 -1
- package/dist/agent/ScatterChart.md +1 -1
- package/dist/agent/ScrollArea.md +1 -1
- package/dist/agent/SearchableMenu.md +1 -1
- package/dist/agent/Select.md +1 -1
- package/dist/agent/Separator.md +1 -1
- package/dist/agent/SequenceFigure.md +1 -1
- package/dist/agent/Sheet.md +1 -1
- package/dist/agent/Sidebar.md +193 -0
- package/dist/agent/Skeleton.md +1 -1
- package/dist/agent/Slider.md +7 -3
- package/dist/agent/Sparkline.md +1 -1
- package/dist/agent/Spinner.md +1 -1
- package/dist/agent/StatusDot.md +1 -1
- package/dist/agent/StatusPill.md +1 -1
- package/dist/agent/Steps.md +1 -1
- package/dist/agent/Switch.md +1 -1
- package/dist/agent/Table.md +3 -1
- package/dist/agent/Tabs.md +1 -1
- package/dist/agent/Tag.md +1 -1
- package/dist/agent/Text.md +1 -1
- package/dist/agent/Textarea.md +1 -1
- package/dist/agent/Timestamp.md +1 -1
- package/dist/agent/Toast.md +1 -1
- package/dist/agent/ToggleGroup.md +1 -1
- package/dist/agent/Toolbar.md +1 -1
- package/dist/agent/Tooltip.md +1 -1
- package/dist/agent/TreemapChart.md +1 -1
- package/dist/agent/WaterfallChart.md +1 -1
- package/dist/agent/WorkflowFigure.md +1 -1
- package/dist/agent/catalog.json +361 -2
- package/dist/agent/index.md +8 -5
- package/dist/charts/BulletChart/BulletChart.js +16 -2
- package/dist/charts/BulletChart/BulletChart.js.map +1 -1
- package/dist/charts/RadarChart/RadarChart.js +4 -2
- package/dist/charts/RadarChart/RadarChart.js.map +1 -1
- package/dist/components/ColorPicker/ColorPicker.d.ts +67 -0
- package/dist/components/ColorPicker/ColorPicker.js +334 -0
- package/dist/components/ColorPicker/ColorPicker.js.map +1 -0
- package/dist/components/Command/Command.js +1 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js +1 -1
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/Dialog/Dialog.js +1 -1
- package/dist/components/Dialog/Dialog.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/ErrorState/ErrorState.js +1 -1
- package/dist/components/ErrorState/ErrorState.js.map +1 -1
- package/dist/components/NavItem/NavItem.js +1 -1
- package/dist/components/NavItem/NavItem.js.map +1 -1
- package/dist/components/NumberField/NumberField.d.ts +69 -0
- package/dist/components/NumberField/NumberField.js +162 -0
- package/dist/components/NumberField/NumberField.js.map +1 -0
- package/dist/components/Popover/Popover.js +1 -1
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Select/Select.js +1 -1
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Sheet/Sheet.js +1 -1
- package/dist/components/Sheet/Sheet.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.d.ts +349 -0
- package/dist/components/Sidebar/Sidebar.js +455 -0
- package/dist/components/Sidebar/Sidebar.js.map +1 -0
- package/dist/components/Slider/Slider.d.ts +22 -1
- package/dist/components/Slider/Slider.js +72 -8
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Table/Table.d.ts +14 -0
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tabs/Tabs.js +7 -1
- package/dist/components/Tabs/Tabs.js.map +1 -1
- package/dist/diagrams/lib/route.d.ts +22 -1
- package/dist/diagrams/lib/route.js +24 -1
- package/dist/diagrams/lib/route.js.map +1 -1
- package/dist/diagrams/lib/wires.js +9 -5
- package/dist/diagrams/lib/wires.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/color.d.ts +80 -0
- package/dist/lib/color.js +189 -0
- package/dist/lib/color.js.map +1 -0
- package/dist/lib/numeric.d.ts +35 -0
- package/dist/lib/numeric.js +29 -0
- package/dist/lib/numeric.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/themes.css +8 -0
- package/dist/tokens.css +66 -3
- package/dist/tokens.d.ts +3 -0
- package/dist/tokens.js +17 -0
- package/dist/tokens.json +17 -0
- package/package.json +1 -1
- package/skills/misoto22-design/rules/forms.md +3 -1
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
ChevronRight,
|
|
5
|
+
PanelLeftClose,
|
|
6
|
+
PanelLeftOpen,
|
|
7
|
+
PanelRightClose,
|
|
8
|
+
PanelRightOpen
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import {
|
|
11
|
+
createContext,
|
|
12
|
+
useCallback,
|
|
13
|
+
useContext,
|
|
14
|
+
useEffect,
|
|
15
|
+
useId,
|
|
16
|
+
useMemo,
|
|
17
|
+
useState
|
|
18
|
+
} from "react";
|
|
19
|
+
import { cn } from "../../lib/cn.js";
|
|
20
|
+
import { warnBlankName } from "../../lib/warn.js";
|
|
21
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../Collapsible/Collapsible.js";
|
|
22
|
+
import { NavItem } from "../NavItem/NavItem.js";
|
|
23
|
+
import { Tooltip, TooltipProvider } from "../Tooltip/Tooltip.js";
|
|
24
|
+
const DRAWER = {
|
|
25
|
+
sm: {
|
|
26
|
+
query: "(max-width: 39.99rem)",
|
|
27
|
+
frame: "max-sm:inset-y-0 max-sm:z-(--z-modal) max-sm:w-(--sidebar-w) max-sm:m-0 max-sm:rounded-none max-sm:border-0 max-sm:transition-[inset-inline-start,inset-inline-end]",
|
|
28
|
+
fixed: "max-sm:fixed",
|
|
29
|
+
absolute: "max-sm:absolute",
|
|
30
|
+
start: "max-sm:border-e max-sm:border-(--rule)",
|
|
31
|
+
end: "max-sm:border-s max-sm:border-(--rule)",
|
|
32
|
+
openStart: "max-sm:start-0",
|
|
33
|
+
openEnd: "max-sm:end-0",
|
|
34
|
+
shutStart: "max-sm:-start-(--sidebar-w)",
|
|
35
|
+
shutEnd: "max-sm:-end-(--sidebar-w)"
|
|
36
|
+
},
|
|
37
|
+
md: {
|
|
38
|
+
query: "(max-width: 47.99rem)",
|
|
39
|
+
frame: "max-md:inset-y-0 max-md:z-(--z-modal) max-md:w-(--sidebar-w) max-md:m-0 max-md:rounded-none max-md:border-0 max-md:transition-[inset-inline-start,inset-inline-end]",
|
|
40
|
+
fixed: "max-md:fixed",
|
|
41
|
+
absolute: "max-md:absolute",
|
|
42
|
+
start: "max-md:border-e max-md:border-(--rule)",
|
|
43
|
+
end: "max-md:border-s max-md:border-(--rule)",
|
|
44
|
+
openStart: "max-md:start-0",
|
|
45
|
+
openEnd: "max-md:end-0",
|
|
46
|
+
shutStart: "max-md:-start-(--sidebar-w)",
|
|
47
|
+
shutEnd: "max-md:-end-(--sidebar-w)"
|
|
48
|
+
},
|
|
49
|
+
lg: {
|
|
50
|
+
query: "(max-width: 63.99rem)",
|
|
51
|
+
frame: "max-lg:inset-y-0 max-lg:z-(--z-modal) max-lg:w-(--sidebar-w) max-lg:m-0 max-lg:rounded-none max-lg:border-0 max-lg:transition-[inset-inline-start,inset-inline-end]",
|
|
52
|
+
fixed: "max-lg:fixed",
|
|
53
|
+
absolute: "max-lg:absolute",
|
|
54
|
+
start: "max-lg:border-e max-lg:border-(--rule)",
|
|
55
|
+
end: "max-lg:border-s max-lg:border-(--rule)",
|
|
56
|
+
openStart: "max-lg:start-0",
|
|
57
|
+
openEnd: "max-lg:end-0",
|
|
58
|
+
shutStart: "max-lg:-start-(--sidebar-w)",
|
|
59
|
+
shutEnd: "max-lg:-end-(--sidebar-w)"
|
|
60
|
+
},
|
|
61
|
+
xl: {
|
|
62
|
+
query: "(max-width: 79.99rem)",
|
|
63
|
+
frame: "max-xl:inset-y-0 max-xl:z-(--z-modal) max-xl:w-(--sidebar-w) max-xl:m-0 max-xl:rounded-none max-xl:border-0 max-xl:transition-[inset-inline-start,inset-inline-end]",
|
|
64
|
+
fixed: "max-xl:fixed",
|
|
65
|
+
absolute: "max-xl:absolute",
|
|
66
|
+
start: "max-xl:border-e max-xl:border-(--rule)",
|
|
67
|
+
end: "max-xl:border-s max-xl:border-(--rule)",
|
|
68
|
+
openStart: "max-xl:start-0",
|
|
69
|
+
openEnd: "max-xl:end-0",
|
|
70
|
+
shutStart: "max-xl:-start-(--sidebar-w)",
|
|
71
|
+
shutEnd: "max-xl:-end-(--sidebar-w)"
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const SidebarContext = createContext(null);
|
|
75
|
+
const UNWRAPPED = {
|
|
76
|
+
open: true,
|
|
77
|
+
setOpen: () => {
|
|
78
|
+
},
|
|
79
|
+
toggle: () => {
|
|
80
|
+
},
|
|
81
|
+
collapsible: "none",
|
|
82
|
+
iconOnly: false,
|
|
83
|
+
side: "start",
|
|
84
|
+
variant: "flush",
|
|
85
|
+
mobile: false,
|
|
86
|
+
contained: false,
|
|
87
|
+
breakpoint: null
|
|
88
|
+
};
|
|
89
|
+
function useSidebarState() {
|
|
90
|
+
return useContext(SidebarContext) ?? UNWRAPPED;
|
|
91
|
+
}
|
|
92
|
+
function useSidebar() {
|
|
93
|
+
const state = useContext(SidebarContext);
|
|
94
|
+
if (!state) throw new Error("useSidebar must be used inside a <SidebarProvider>");
|
|
95
|
+
return state;
|
|
96
|
+
}
|
|
97
|
+
function SidebarProvider({
|
|
98
|
+
open: controlled,
|
|
99
|
+
defaultOpen = true,
|
|
100
|
+
onOpenChange,
|
|
101
|
+
collapsible = "icon",
|
|
102
|
+
shortcut = "b",
|
|
103
|
+
side = "start",
|
|
104
|
+
variant = "flush",
|
|
105
|
+
breakpoint = "md",
|
|
106
|
+
persist = null,
|
|
107
|
+
contained = false,
|
|
108
|
+
children
|
|
109
|
+
}) {
|
|
110
|
+
const [uncontrolled, setUncontrolled] = useState(defaultOpen);
|
|
111
|
+
const [drawer, setDrawer] = useState(false);
|
|
112
|
+
const [mobile, setMobile] = useState(false);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (breakpoint === null) return;
|
|
115
|
+
const query = window.matchMedia(DRAWER[breakpoint].query);
|
|
116
|
+
const sync = () => setMobile(query.matches);
|
|
117
|
+
sync();
|
|
118
|
+
query.addEventListener("change", sync);
|
|
119
|
+
return () => query.removeEventListener("change", sync);
|
|
120
|
+
}, [breakpoint]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (mobile) setDrawer(false);
|
|
123
|
+
}, [mobile]);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!persist) return;
|
|
126
|
+
try {
|
|
127
|
+
const stored = window.localStorage.getItem(persist);
|
|
128
|
+
if (stored === "open" || stored === "closed") setUncontrolled(stored === "open");
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
}, [persist]);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!persist) return;
|
|
134
|
+
try {
|
|
135
|
+
window.localStorage.setItem(persist, uncontrolled ? "open" : "closed");
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}, [persist, uncontrolled]);
|
|
139
|
+
const docked = controlled ?? uncontrolled;
|
|
140
|
+
const open = mobile ? drawer : docked;
|
|
141
|
+
const setOpen = useCallback(
|
|
142
|
+
(next) => {
|
|
143
|
+
if (mobile) setDrawer(next);
|
|
144
|
+
else if (controlled === void 0) setUncontrolled(next);
|
|
145
|
+
onOpenChange?.(next);
|
|
146
|
+
},
|
|
147
|
+
[mobile, controlled, onOpenChange]
|
|
148
|
+
);
|
|
149
|
+
const toggle = useCallback(() => setOpen(!open), [open, setOpen]);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!shortcut || collapsible === "none") return;
|
|
152
|
+
const onKey = (event) => {
|
|
153
|
+
if (event.key.toLowerCase() !== shortcut.toLowerCase()) return;
|
|
154
|
+
if (!event.metaKey && !event.ctrlKey) return;
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
toggle();
|
|
157
|
+
};
|
|
158
|
+
window.addEventListener("keydown", onKey);
|
|
159
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
160
|
+
}, [shortcut, collapsible, toggle]);
|
|
161
|
+
const value = useMemo(
|
|
162
|
+
() => ({
|
|
163
|
+
open,
|
|
164
|
+
setOpen,
|
|
165
|
+
toggle,
|
|
166
|
+
collapsible,
|
|
167
|
+
// Never at drawer width: closed there means the rail is off the page, and
|
|
168
|
+
// a column of icons is the answer for a layout that still has a column.
|
|
169
|
+
iconOnly: !mobile && !open && collapsible === "icon",
|
|
170
|
+
side,
|
|
171
|
+
variant,
|
|
172
|
+
mobile,
|
|
173
|
+
contained,
|
|
174
|
+
breakpoint
|
|
175
|
+
}),
|
|
176
|
+
[open, setOpen, toggle, collapsible, side, variant, mobile, contained, breakpoint]
|
|
177
|
+
);
|
|
178
|
+
return /* @__PURE__ */ jsx(SidebarContext.Provider, { value, children: /* @__PURE__ */ jsx(TooltipProvider, { children }) });
|
|
179
|
+
}
|
|
180
|
+
function Sidebar({
|
|
181
|
+
label,
|
|
182
|
+
scrimLabel = "Close the navigation",
|
|
183
|
+
className,
|
|
184
|
+
children,
|
|
185
|
+
...rest
|
|
186
|
+
}) {
|
|
187
|
+
const { open, setOpen, collapsible, iconOnly, side, variant, mobile, contained, breakpoint } = useSidebarState();
|
|
188
|
+
warnBlankName("Sidebar", "label", label, "the rail is announced as an unnamed navigation");
|
|
189
|
+
const gone = !open && collapsible === "offcanvas";
|
|
190
|
+
const drawer = breakpoint === null ? null : DRAWER[breakpoint];
|
|
191
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
192
|
+
drawer && mobile && open && /* @__PURE__ */ jsx(
|
|
193
|
+
"button",
|
|
194
|
+
{
|
|
195
|
+
type: "button",
|
|
196
|
+
"aria-label": scrimLabel,
|
|
197
|
+
onClick: () => setOpen(false),
|
|
198
|
+
"data-m22-animated": true,
|
|
199
|
+
className: cn(
|
|
200
|
+
"inset-0 z-(--z-scrim) bg-(--scrim) animate-[m22-fade-in_var(--duration-fast)_var(--ease)]",
|
|
201
|
+
contained ? "absolute" : "fixed"
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
),
|
|
205
|
+
/* @__PURE__ */ jsx(
|
|
206
|
+
"nav",
|
|
207
|
+
{
|
|
208
|
+
"aria-label": label,
|
|
209
|
+
"data-state": open ? "open" : "collapsed",
|
|
210
|
+
"data-collapsible": collapsible,
|
|
211
|
+
"data-side": side,
|
|
212
|
+
"data-variant": variant,
|
|
213
|
+
inert: drawer && mobile && !open || void 0,
|
|
214
|
+
className: cn(
|
|
215
|
+
"group/sidebar flex shrink-0 flex-col overflow-hidden bg-(--paper) transition-[width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none",
|
|
216
|
+
gone ? "w-0" : iconOnly ? "w-(--sidebar-w-icon)" : "w-(--sidebar-w)",
|
|
217
|
+
// `inset` gives the border to `SidebarInset`, which is the panel in
|
|
218
|
+
// that arrangement; the rail is the ground it sits on and draws no
|
|
219
|
+
// edge of its own, or the two lines run parallel a few pixels apart.
|
|
220
|
+
variant === "floating" && "my-2 rounded-(--radius-frame) border border-(--rule)",
|
|
221
|
+
variant === "floating" && (side === "end" ? "me-2" : "ms-2"),
|
|
222
|
+
variant === "flush" && !gone && (side === "end" ? "border-s border-(--rule)" : "border-e border-(--rule)"),
|
|
223
|
+
// The drawer, below the step. Everything above is the column; these
|
|
224
|
+
// override it inside one media query, so the first paint is already
|
|
225
|
+
// right and no effect has to run for the page to be the right width.
|
|
226
|
+
drawer && [
|
|
227
|
+
drawer.frame,
|
|
228
|
+
contained ? drawer.absolute : drawer.fixed,
|
|
229
|
+
side === "end" ? drawer.end : drawer.start,
|
|
230
|
+
side === "end" ? open ? drawer.openEnd : drawer.shutEnd : open ? drawer.openStart : drawer.shutStart
|
|
231
|
+
],
|
|
232
|
+
className
|
|
233
|
+
),
|
|
234
|
+
...rest,
|
|
235
|
+
children: /* @__PURE__ */ jsx("div", { className: cn("flex h-full flex-col", gone ? "w-(--sidebar-w)" : "w-full"), children })
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
] });
|
|
239
|
+
}
|
|
240
|
+
function SidebarInset({ className, children, ...rest }) {
|
|
241
|
+
const { variant } = useSidebarState();
|
|
242
|
+
return /* @__PURE__ */ jsx(
|
|
243
|
+
"div",
|
|
244
|
+
{
|
|
245
|
+
className: cn(
|
|
246
|
+
"flex min-w-0 flex-1 flex-col",
|
|
247
|
+
variant === "inset" && "my-2 me-2 overflow-hidden rounded-(--radius-frame) border border-(--rule) bg-(--paper) ms-0",
|
|
248
|
+
className
|
|
249
|
+
),
|
|
250
|
+
...rest,
|
|
251
|
+
children
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
function SidebarHeader({ className, children, ...rest }) {
|
|
256
|
+
return /* @__PURE__ */ jsx(
|
|
257
|
+
"div",
|
|
258
|
+
{
|
|
259
|
+
className: cn(
|
|
260
|
+
"flex min-h-14 shrink-0 items-center gap-2 border-b border-(--rule) px-3",
|
|
261
|
+
className
|
|
262
|
+
),
|
|
263
|
+
...rest,
|
|
264
|
+
children
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
function SidebarContent({ className, children, ...rest }) {
|
|
269
|
+
return /* @__PURE__ */ jsx(
|
|
270
|
+
"div",
|
|
271
|
+
{
|
|
272
|
+
className: cn("flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-2 scroll-slim", className),
|
|
273
|
+
...rest,
|
|
274
|
+
children
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
function SidebarFooter({ className, children, ...rest }) {
|
|
279
|
+
return /* @__PURE__ */ jsx(
|
|
280
|
+
"div",
|
|
281
|
+
{
|
|
282
|
+
className: cn("flex shrink-0 flex-col gap-1 border-t border-(--rule) p-2", className),
|
|
283
|
+
...rest,
|
|
284
|
+
children
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
function SidebarSeparator({ className, ...rest }) {
|
|
289
|
+
return /* @__PURE__ */ jsx("hr", { className: cn("my-1 border-0 border-t border-(--rule)", className), ...rest });
|
|
290
|
+
}
|
|
291
|
+
function SidebarTrigger({
|
|
292
|
+
labels = { open: "Open the sidebar", close: "Close the sidebar" },
|
|
293
|
+
className,
|
|
294
|
+
onClick,
|
|
295
|
+
...rest
|
|
296
|
+
}) {
|
|
297
|
+
const { open, toggle, side } = useSidebarState();
|
|
298
|
+
const Icon = side === "end" ? open ? PanelRightClose : PanelRightOpen : open ? PanelLeftClose : PanelLeftOpen;
|
|
299
|
+
return /* @__PURE__ */ jsx(
|
|
300
|
+
"button",
|
|
301
|
+
{
|
|
302
|
+
type: "button",
|
|
303
|
+
"aria-label": open ? labels.close : labels.open,
|
|
304
|
+
"aria-expanded": open,
|
|
305
|
+
onClick: (event) => {
|
|
306
|
+
toggle();
|
|
307
|
+
onClick?.(event);
|
|
308
|
+
},
|
|
309
|
+
className: cn(
|
|
310
|
+
"grid size-8 shrink-0 place-items-center rounded-(--radius) text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink)",
|
|
311
|
+
className
|
|
312
|
+
),
|
|
313
|
+
...rest,
|
|
314
|
+
children: /* @__PURE__ */ jsx(Icon, { size: 16, strokeWidth: 1.5, "aria-hidden": true })
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
function SidebarGroup({
|
|
319
|
+
label,
|
|
320
|
+
count,
|
|
321
|
+
badge,
|
|
322
|
+
action,
|
|
323
|
+
collapsible = true,
|
|
324
|
+
defaultOpen = true,
|
|
325
|
+
children,
|
|
326
|
+
className
|
|
327
|
+
}) {
|
|
328
|
+
const { iconOnly } = useSidebarState();
|
|
329
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
330
|
+
const labelId = useId();
|
|
331
|
+
const rows = /* @__PURE__ */ jsx("div", { className: "group/rows flex flex-col gap-0.5", children });
|
|
332
|
+
if (iconOnly) {
|
|
333
|
+
return /* @__PURE__ */ jsx("div", { role: "group", "aria-label": label, className: cn("group/rows flex flex-col gap-0.5", className), children });
|
|
334
|
+
}
|
|
335
|
+
const heading = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
336
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium text-(--ink)", children: label }),
|
|
337
|
+
badge,
|
|
338
|
+
count !== void 0 && /* @__PURE__ */ jsx("span", { className: "ms-auto text-xs tabular-nums text-(--ink-3-aa)", children: count })
|
|
339
|
+
] });
|
|
340
|
+
if (!collapsible) {
|
|
341
|
+
return /* @__PURE__ */ jsxs("div", { role: "group", "aria-labelledby": labelId, className: cn("flex flex-col gap-1", className), children: [
|
|
342
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-h-8 items-center gap-1.5 px-2 py-1", id: labelId, children: [
|
|
343
|
+
heading,
|
|
344
|
+
action
|
|
345
|
+
] }),
|
|
346
|
+
/* @__PURE__ */ jsx("div", { className: "ms-[0.9rem] flex flex-col gap-0.5 border-s border-(--rule) ps-2", children: rows })
|
|
347
|
+
] });
|
|
348
|
+
}
|
|
349
|
+
return (
|
|
350
|
+
// Radix rather than `{open && …}`: a conditional render cannot animate — the
|
|
351
|
+
// rows are simply not there on the next frame — and Radix publishes the
|
|
352
|
+
// measured height, so the panel opens to what it actually is rather than to
|
|
353
|
+
// a guessed maximum.
|
|
354
|
+
/* @__PURE__ */ jsxs(Collapsible, { open, onOpenChange: setOpen, className: cn("flex flex-col gap-1", className), children: [
|
|
355
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-h-9 items-center gap-1.5", children: [
|
|
356
|
+
/* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "group/heading flex min-h-8 flex-1 items-center gap-1.5 rounded-(--radius-sm) px-2 py-1 text-start transition-colors duration-(--duration-fast) hover:bg-(--stone)", children: [
|
|
357
|
+
/* @__PURE__ */ jsx(
|
|
358
|
+
ChevronRight,
|
|
359
|
+
{
|
|
360
|
+
size: 12,
|
|
361
|
+
strokeWidth: 2,
|
|
362
|
+
"aria-hidden": true,
|
|
363
|
+
className: "shrink-0 text-(--ink-3-aa) transition-transform duration-(--duration-base) ease-(--ease-out-expo) group-data-[state=open]/heading:rotate-90 motion-reduce:transition-none"
|
|
364
|
+
}
|
|
365
|
+
),
|
|
366
|
+
heading
|
|
367
|
+
] }),
|
|
368
|
+
action
|
|
369
|
+
] }),
|
|
370
|
+
/* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx("div", { className: "ms-[0.9rem] flex flex-col gap-0.5 border-s border-(--rule) ps-2", children: rows }) })
|
|
371
|
+
] })
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const MOVING_HIGHLIGHT = "group-hover/rows:not-hover:bg-transparent group-hover/rows:not-hover:font-normal group-hover/rows:not-hover:text-(--ink-3-aa)";
|
|
375
|
+
function SidebarBranch({
|
|
376
|
+
label,
|
|
377
|
+
icon: Icon,
|
|
378
|
+
trailing,
|
|
379
|
+
defaultOpen = false,
|
|
380
|
+
open: controlled,
|
|
381
|
+
onOpenChange,
|
|
382
|
+
children,
|
|
383
|
+
className
|
|
384
|
+
}) {
|
|
385
|
+
const { iconOnly, side } = useSidebarState();
|
|
386
|
+
const [uncontrolled, setUncontrolled] = useState(defaultOpen);
|
|
387
|
+
const open = controlled ?? uncontrolled;
|
|
388
|
+
const setOpen = (next) => {
|
|
389
|
+
if (controlled === void 0) setUncontrolled(next);
|
|
390
|
+
onOpenChange?.(next);
|
|
391
|
+
};
|
|
392
|
+
if (iconOnly && Icon) {
|
|
393
|
+
return /* @__PURE__ */ jsx(Tooltip, { content: label, side: side === "end" ? "left" : "right", children: /* @__PURE__ */ jsx(
|
|
394
|
+
"button",
|
|
395
|
+
{
|
|
396
|
+
type: "button",
|
|
397
|
+
"aria-label": label,
|
|
398
|
+
className: cn(
|
|
399
|
+
"flex min-h-(--control-h-sm) items-center justify-center rounded-(--radius) text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink)",
|
|
400
|
+
className
|
|
401
|
+
),
|
|
402
|
+
children: /* @__PURE__ */ jsx(Icon, { size: 18, strokeWidth: 1.5, "aria-hidden": true })
|
|
403
|
+
}
|
|
404
|
+
) });
|
|
405
|
+
}
|
|
406
|
+
return /* @__PURE__ */ jsxs(Collapsible, { open, onOpenChange: setOpen, className: cn("flex flex-col gap-0.5", className), children: [
|
|
407
|
+
/* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "group/branch flex min-h-(--control-h-sm) items-center gap-3 rounded-(--radius) px-3 py-2 text-start text-sm text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink)", children: [
|
|
408
|
+
Icon && /* @__PURE__ */ jsx(Icon, { size: 18, strokeWidth: 1.5, "aria-hidden": true, className: "shrink-0" }),
|
|
409
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: label }),
|
|
410
|
+
trailing !== void 0 && /* @__PURE__ */ jsx("span", { className: "ms-auto shrink-0", children: trailing }),
|
|
411
|
+
trailing === void 0 && /* @__PURE__ */ jsx(
|
|
412
|
+
ChevronRight,
|
|
413
|
+
{
|
|
414
|
+
size: 12,
|
|
415
|
+
strokeWidth: 2,
|
|
416
|
+
"aria-hidden": true,
|
|
417
|
+
className: "ms-auto shrink-0 transition-transform duration-(--duration-base) ease-(--ease-out-expo) group-data-[state=open]/branch:rotate-90 motion-reduce:transition-none"
|
|
418
|
+
}
|
|
419
|
+
)
|
|
420
|
+
] }),
|
|
421
|
+
/* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx("div", { className: "group/rows ms-[1.35rem] flex flex-col gap-0.5 border-s border-(--rule) ps-2", children }) })
|
|
422
|
+
] });
|
|
423
|
+
}
|
|
424
|
+
function SidebarItem({ trailing, children, className, onClick, ...rest }) {
|
|
425
|
+
const { iconOnly, side, mobile, setOpen } = useSidebarState();
|
|
426
|
+
const take = (event) => {
|
|
427
|
+
if (mobile) setOpen(false);
|
|
428
|
+
onClick?.(event);
|
|
429
|
+
};
|
|
430
|
+
if (iconOnly && rest.icon && !rest.asChild) {
|
|
431
|
+
return /* @__PURE__ */ jsx(Tooltip, { content: children, side: side === "end" ? "left" : "right", children: /* @__PURE__ */ jsx(NavItem, { ...rest, onClick: take, className: cn("justify-center px-0", className), children: /* @__PURE__ */ jsx("span", { className: "sr-only", children }) }) });
|
|
432
|
+
}
|
|
433
|
+
if (rest.asChild) {
|
|
434
|
+
return /* @__PURE__ */ jsx(NavItem, { ...rest, onClick: take, className: cn(MOVING_HIGHLIGHT, className), children });
|
|
435
|
+
}
|
|
436
|
+
return /* @__PURE__ */ jsxs(NavItem, { ...rest, onClick: take, className: cn(MOVING_HIGHLIGHT, className), children: [
|
|
437
|
+
children,
|
|
438
|
+
trailing !== void 0 && /* @__PURE__ */ jsx("span", { className: "ms-auto shrink-0", children: trailing })
|
|
439
|
+
] });
|
|
440
|
+
}
|
|
441
|
+
export {
|
|
442
|
+
Sidebar,
|
|
443
|
+
SidebarBranch,
|
|
444
|
+
SidebarContent,
|
|
445
|
+
SidebarFooter,
|
|
446
|
+
SidebarGroup,
|
|
447
|
+
SidebarHeader,
|
|
448
|
+
SidebarInset,
|
|
449
|
+
SidebarItem,
|
|
450
|
+
SidebarProvider,
|
|
451
|
+
SidebarSeparator,
|
|
452
|
+
SidebarTrigger,
|
|
453
|
+
useSidebar
|
|
454
|
+
};
|
|
455
|
+
//# sourceMappingURL=Sidebar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["'use client'\n\nimport {\n ChevronRight,\n PanelLeftClose,\n PanelLeftOpen,\n PanelRightClose,\n PanelRightOpen,\n type LucideIcon,\n} from 'lucide-react'\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n type ComponentProps,\n type MouseEvent,\n type ReactNode,\n} from 'react'\nimport { cn } from '../../lib/cn'\nimport { warnBlankName } from '../../lib/warn'\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../Collapsible/Collapsible'\nimport { NavItem, type NavItemProps } from '../NavItem/NavItem'\nimport { Tooltip, TooltipProvider } from '../Tooltip/Tooltip'\n\n/** What happens to the rail when it is closed. See {@link SidebarProviderProps.collapsible}. */\nexport type SidebarCollapsible = 'icon' | 'offcanvas' | 'none'\n\n/** Which edge of the layout the rail is on. Logical, so it follows `dir`. */\nexport type SidebarSide = 'start' | 'end'\n\n/** How the rail meets the page. See {@link SidebarProviderProps.variant}. */\nexport type SidebarVariant = 'flush' | 'inset' | 'floating'\n\n/** Where the rail stops being a column. See {@link SidebarProviderProps.breakpoint}. */\nexport type SidebarBreakpoint = 'sm' | 'md' | 'lg' | 'xl'\n\n/**\n * The drawer, written as CSS rather than decided in JavaScript.\n *\n * The first version asked `matchMedia` and rendered a different tree from the\n * answer. That answer does not exist until an effect runs, so the server sent —\n * and a phone painted — a 256px column beside a 390px screen, and the page\n * scrolled sideways until React caught up. A layout that is wrong for a frame\n * is wrong: it is the first frame, it is the one a reader sees on a slow phone,\n * and on this site's own suite it was a real failure on every route.\n *\n * So the element is the same in both states and the media query decides, which\n * is what a media query is. JavaScript is left with the two things CSS cannot\n * say — `inert`, and whether to render the scrim — and being a frame late on\n * either of those changes nothing anyone can see.\n *\n * The breakpoints are NAMED rather than numeric because these are literal class\n * strings: Tailwind emits what it can read in the source, and a class built\n * from a runtime number is a class that does not exist. Naming them also puts\n * the rail on the same four steps as everything else in the page around it,\n * which a number never quite lands on.\n *\n * It slides on `inset-inline-start`, not on `translate-x`. A transform is a\n * PHYSICAL axis, so the closed drawer needed an `rtl:` twin of every offset —\n * which is the physical-with-a-patch shape this system forbids everywhere else,\n * and which cost 3.6 kB of stylesheet on its own: each `rtl:` selector carries\n * the whole `:lang()` list, four breakpoints deep. A logical inset needs no\n * twin, is one property in each direction, and puts the rail back under the\n * same rule as every `ps-` and `me-` in the package.\n */\nconst DRAWER: Record<\n SidebarBreakpoint,\n { query: string; frame: string; fixed: string; absolute: string; start: string; end: string; openStart: string; openEnd: string; shutStart: string; shutEnd: string }\n> = {\n sm: {\n query: '(max-width: 39.99rem)',\n frame: 'max-sm:inset-y-0 max-sm:z-(--z-modal) max-sm:w-(--sidebar-w) max-sm:m-0 max-sm:rounded-none max-sm:border-0 max-sm:transition-[inset-inline-start,inset-inline-end]',\n fixed: 'max-sm:fixed',\n absolute: 'max-sm:absolute',\n start: 'max-sm:border-e max-sm:border-(--rule)',\n end: 'max-sm:border-s max-sm:border-(--rule)',\n openStart: 'max-sm:start-0',\n openEnd: 'max-sm:end-0',\n shutStart: 'max-sm:-start-(--sidebar-w)',\n shutEnd: 'max-sm:-end-(--sidebar-w)',\n },\n md: {\n query: '(max-width: 47.99rem)',\n frame: 'max-md:inset-y-0 max-md:z-(--z-modal) max-md:w-(--sidebar-w) max-md:m-0 max-md:rounded-none max-md:border-0 max-md:transition-[inset-inline-start,inset-inline-end]',\n fixed: 'max-md:fixed',\n absolute: 'max-md:absolute',\n start: 'max-md:border-e max-md:border-(--rule)',\n end: 'max-md:border-s max-md:border-(--rule)',\n openStart: 'max-md:start-0',\n openEnd: 'max-md:end-0',\n shutStart: 'max-md:-start-(--sidebar-w)',\n shutEnd: 'max-md:-end-(--sidebar-w)',\n },\n lg: {\n query: '(max-width: 63.99rem)',\n frame: 'max-lg:inset-y-0 max-lg:z-(--z-modal) max-lg:w-(--sidebar-w) max-lg:m-0 max-lg:rounded-none max-lg:border-0 max-lg:transition-[inset-inline-start,inset-inline-end]',\n fixed: 'max-lg:fixed',\n absolute: 'max-lg:absolute',\n start: 'max-lg:border-e max-lg:border-(--rule)',\n end: 'max-lg:border-s max-lg:border-(--rule)',\n openStart: 'max-lg:start-0',\n openEnd: 'max-lg:end-0',\n shutStart: 'max-lg:-start-(--sidebar-w)',\n shutEnd: 'max-lg:-end-(--sidebar-w)',\n },\n xl: {\n query: '(max-width: 79.99rem)',\n frame: 'max-xl:inset-y-0 max-xl:z-(--z-modal) max-xl:w-(--sidebar-w) max-xl:m-0 max-xl:rounded-none max-xl:border-0 max-xl:transition-[inset-inline-start,inset-inline-end]',\n fixed: 'max-xl:fixed',\n absolute: 'max-xl:absolute',\n start: 'max-xl:border-e max-xl:border-(--rule)',\n end: 'max-xl:border-s max-xl:border-(--rule)',\n openStart: 'max-xl:start-0',\n openEnd: 'max-xl:end-0',\n shutStart: 'max-xl:-start-(--sidebar-w)',\n shutEnd: 'max-xl:-end-(--sidebar-w)',\n },\n}\n\ninterface SidebarState {\n /** Whether the rail is showing — the docked column, or the open drawer. */\n open: boolean\n setOpen: (open: boolean) => void\n toggle: () => void\n collapsible: SidebarCollapsible\n /** True only in the state where labels are hidden and rows are icons. */\n iconOnly: boolean\n side: SidebarSide\n variant: SidebarVariant\n /**\n * True while the viewport is under the provider's breakpoint, where the rail\n * is a drawer over the page rather than a column beside it.\n */\n mobile: boolean\n /** The drawer covers its positioned ancestor rather than the window. */\n contained: boolean\n /** The step below which the rail is a drawer, or `null` for never. */\n breakpoint: SidebarBreakpoint | null\n}\n\nconst SidebarContext = createContext<SidebarState | null>(null)\n\n/**\n * What the parts read when nobody wrapped them: open, and not collapsible.\n *\n * A rail without a provider used to THROW, which is defensible for a hook a\n * consumer called by hand and wrong for the component itself — `<Sidebar>` on\n * its own is the first thing anybody writes, and the documentation site's own\n * props panel renders exactly that. A component that cannot draw without a\n * wrapper is not strict, it is unusable; so the parts fall back to the state a\n * rail with no controls would be in, and the button that would toggle it does\n * nothing because there is nothing holding the answer.\n */\nconst UNWRAPPED: SidebarState = {\n open: true,\n setOpen: () => {},\n toggle: () => {},\n collapsible: 'none',\n iconOnly: false,\n side: 'start',\n variant: 'flush',\n mobile: false,\n contained: false,\n breakpoint: null,\n}\n\n/** The state the parts read, wrapped or not. */\nfunction useSidebarState(): SidebarState {\n return useContext(SidebarContext) ?? UNWRAPPED\n}\n\n/**\n * The rail's own state, for anything that has to answer to it.\n *\n * A page beside the rail needs it to reserve the right width; a control inside\n * needs to know whether its label is being drawn.\n *\n * This one THROWS outside a provider, and the parts above do not, and the\n * difference is who made the mistake. A part rendered on its own is somebody\n * writing `<Sidebar>` to see what it looks like; a call to this hook is code\n * asking for state that nothing is keeping, and returning a plausible default\n * there is a layout that is wrong in one state and right in the other with\n * nothing to say which.\n */\nexport function useSidebar(): SidebarState {\n const state = useContext(SidebarContext)\n if (!state) throw new Error('useSidebar must be used inside a <SidebarProvider>')\n return state\n}\n\nexport interface SidebarProviderProps {\n /** Controlled open state. Leave off to let the provider own it. */\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** What closing does to the rail. See {@link SidebarProps.collapsible}. */\n collapsible?: SidebarCollapsible\n /**\n * The keyboard shortcut that toggles the rail, as a single letter.\n *\n * `b`, with the platform's own modifier, because that is what every editor\n * and every application shell already uses for the same thing. Pass `null`\n * to bind nothing — an app that already owns that chord should not have it\n * taken twice, and a rail that cannot be closed does not need a shortcut for\n * closing it.\n */\n shortcut?: string | null\n /** Which edge the rail is on. Logical: `end` is the right in LTR, the left in RTL. */\n side?: SidebarSide\n /**\n * How the rail meets the page.\n *\n * `flush` is a column with a hairline down its inner edge — the rail and the\n * page are one surface divided by a line. `floating` lifts the rail off that\n * surface as its own bordered panel with the page ground showing around it.\n * `inset` is the same gesture the other way up: the rail sits on the page\n * ground and `SidebarInset` draws the CONTENT as the panel.\n *\n * `floating` and `inset` both need a ground to sit on — put `bg-(--stone)` on\n * the element holding the provider, or they are a panel on the same colour as\n * the thing behind it.\n */\n variant?: SidebarVariant\n /**\n * The step below which the rail becomes a drawer over the page.\n *\n * A rail is a column when there is a column's worth of room and an overlay\n * when there is not; below this the rail is `fixed`, covers the page from its\n * own edge, and closes onto a scrim. `null` pins it as a column at every\n * width, for a layout that is never narrow — an embedded console, a preview\n * frame.\n *\n * Named, not a number: the switch is a media query in the stylesheet, and a\n * class built from a runtime number is a class the compiler never emitted. It\n * also puts the rail on the same four steps as the page around it.\n */\n breakpoint?: SidebarBreakpoint | null\n /**\n * A `localStorage` key under which the DOCKED state is remembered.\n *\n * A reader who put the rail away did not mean \"until the next page\". Only the\n * docked state is kept: an open drawer is a thing a reader did to this screen,\n * and restoring it on the next visit is a page that opens with its navigation\n * over the top of itself.\n *\n * Read after mount, never during render — a value from storage in the first\n * pass is a hydration mismatch, and the markup the server sent is the one the\n * client has to agree with.\n */\n persist?: string | null\n /**\n * The drawer covers its nearest positioned ancestor rather than the window.\n *\n * For a rail inside a bounded frame — a device preview, an embedded console,\n * an example on a documentation page. `fixed` resolves against the viewport\n * wherever the markup sits, so without this the drawer inside a 400px preview\n * opens across the whole page it is previewed on. Give the frame `relative`.\n */\n contained?: boolean\n children: ReactNode\n}\n\n/**\n * Holds whether the rail is open, and binds the shortcut that changes it.\n *\n * Separate from `Sidebar` itself because the answer is needed on BOTH sides of\n * the layout: the rail draws itself from it, and the content beside the rail\n * reserves width from it. A state that lived inside the rail could only ever\n * be read downwards.\n */\nexport function SidebarProvider({\n open: controlled,\n defaultOpen = true,\n onOpenChange,\n collapsible = 'icon',\n shortcut = 'b',\n side = 'start',\n variant = 'flush',\n breakpoint = 'md',\n persist = null,\n contained = false,\n children,\n}: SidebarProviderProps) {\n const [uncontrolled, setUncontrolled] = useState(defaultOpen)\n // The drawer is its own answer, not the docked column's. They are different\n // questions — \"keep the rail beside the page\" and \"show me the rail now\" —\n // and one state answering both is a phone that loads with its navigation\n // covering the page, because `defaultOpen` was written for the column.\n const [drawer, setDrawer] = useState(false)\n const [mobile, setMobile] = useState(false)\n\n // The same query the classes carry, asked in JavaScript for the two things a\n // class cannot answer: whether the closed rail is `inert`, and whether there\n // is a scrim. The LAYOUT is not waiting on this — see `DRAWER`.\n useEffect(() => {\n if (breakpoint === null) return\n const query = window.matchMedia(DRAWER[breakpoint].query)\n const sync = () => setMobile(query.matches)\n sync()\n query.addEventListener('change', sync)\n return () => query.removeEventListener('change', sync)\n }, [breakpoint])\n\n // Crossing INTO drawer width closes it. A rail that was docked a moment ago\n // would otherwise become an overlay sitting open over the page nobody asked\n // to have covered.\n useEffect(() => {\n if (mobile) setDrawer(false)\n }, [mobile])\n\n useEffect(() => {\n if (!persist) return\n try {\n const stored = window.localStorage.getItem(persist)\n if (stored === 'open' || stored === 'closed') setUncontrolled(stored === 'open')\n } catch {\n // A storage-blocked context just starts at `defaultOpen` every time.\n }\n }, [persist])\n\n useEffect(() => {\n if (!persist) return\n try {\n window.localStorage.setItem(persist, uncontrolled ? 'open' : 'closed')\n } catch {\n // Same.\n }\n }, [persist, uncontrolled])\n\n const docked = controlled ?? uncontrolled\n const open = mobile ? drawer : docked\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (mobile) setDrawer(next)\n else if (controlled === undefined) setUncontrolled(next)\n onOpenChange?.(next)\n },\n [mobile, controlled, onOpenChange],\n )\n\n const toggle = useCallback(() => setOpen(!open), [open, setOpen])\n\n useEffect(() => {\n if (!shortcut || collapsible === 'none') return\n const onKey = (event: KeyboardEvent) => {\n // `metaKey` OR `ctrlKey`, not the platform's own: a Mac keyboard on a\n // Linux browser and a PC keyboard on a Mac both exist, and a shortcut\n // that works for one of them is a shortcut with a bug report attached.\n if (event.key.toLowerCase() !== shortcut.toLowerCase()) return\n if (!event.metaKey && !event.ctrlKey) return\n event.preventDefault()\n toggle()\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [shortcut, collapsible, toggle])\n\n const value = useMemo<SidebarState>(\n () => ({\n open,\n setOpen,\n toggle,\n collapsible,\n // Never at drawer width: closed there means the rail is off the page, and\n // a column of icons is the answer for a layout that still has a column.\n iconOnly: !mobile && !open && collapsible === 'icon',\n side,\n variant,\n mobile,\n contained,\n breakpoint,\n }),\n [open, setOpen, toggle, collapsible, side, variant, mobile, contained, breakpoint],\n )\n\n // The tooltip provider is supplied here rather than demanded of the app. A\n // collapsed rail is rows of icons, every one of which names itself through a\n // tooltip — so without it the component does not degrade, it throws, and only\n // in the state a reader reaches by pressing the button on it. Radix scopes\n // nested providers, so an app that already has one keeps its own settings for\n // everything outside this subtree.\n return (\n <SidebarContext.Provider value={value}>\n <TooltipProvider>{children}</TooltipProvider>\n </SidebarContext.Provider>\n )\n}\n\nexport interface SidebarProps extends Omit<ComponentProps<'nav'>, 'children'> {\n /**\n * Names the landmark. Required: a page with two navigations in it announces\n * two things called \"navigation\" unless each of them says which it is.\n */\n label: string\n /**\n * What the scrim behind the open drawer announces.\n *\n * It is a button — tapping beside a drawer closes it, and that has to be a\n * control a keyboard and a screen reader can reach, not a decorated `<div>`.\n */\n scrimLabel?: string\n children: ReactNode\n}\n\n/**\n * A navigation rail down the side of an application.\n *\n * A `<nav>`, not an `<aside>`. The element decides the landmark, and a rail of\n * links announced as \"complementary\" is not the one a screen reader user jumps\n * to when they go looking for the navigation.\n *\n * Composed rather than configured: a rail is a header, a scrolling middle and a\n * footer, and every product wants different things in all three. What this owns\n * is the part that is the same everywhere — the width, the edge, the scroll\n * behaviour, and what happens when it closes.\n *\n * **Closing has three shapes**, and `collapsible` on the provider picks one.\n * `icon` keeps the rail and drops the labels, which is right when the rows are\n * a fixed set a reader learns the shape of. `offcanvas` takes the whole rail\n * away, which is right when the rows are a long index nobody memorises.\n * `none` is a rail that does not close.\n *\n * The trigger belongs INSIDE the rail — `SidebarHeader` places it — rather than\n * out in an application's masthead. A control that hides a thing should live on\n * the thing: in the masthead it is one more anonymous icon in a row of them,\n * and nothing connects it to the column it operates.\n *\n * **Under the provider's `breakpoint` it is a drawer**, not a column: `fixed`\n * against its own edge, over a scrim, and `inert` while closed. Merely\n * translating it off-screen is not closed — it keeps focus and it is still read\n * aloud, so a shut drawer puts its whole index between the reader and the page\n * they were on. The scrim is a real button, because tapping beside a drawer is\n * how a drawer is closed and that gesture has to exist for a keyboard too.\n *\n * `fixed` resolves against the viewport unless an ancestor has a `transform`,\n * `filter` or `perspective`, which makes that ancestor the containing block\n * instead. Do not put one on an element wrapping the provider, or the drawer\n * opens inside it.\n *\n * @example\n * <SidebarProvider>\n * <Sidebar label=\"Documentation\">\n * <SidebarHeader>Acme</SidebarHeader>\n * <SidebarContent>\n * <SidebarGroup label=\"Guide\" count={2}>\n * <SidebarItem href=\"/start\" icon={Home}>Getting started</SidebarItem>\n * </SidebarGroup>\n * </SidebarContent>\n * </Sidebar>\n * </SidebarProvider>\n */\nexport function Sidebar({\n label,\n scrimLabel = 'Close the navigation',\n className,\n children,\n ...rest\n}: SidebarProps) {\n const { open, setOpen, collapsible, iconOnly, side, variant, mobile, contained, breakpoint } =\n useSidebarState()\n warnBlankName('Sidebar', 'label', label, 'the rail is announced as an unnamed navigation')\n\n const gone = !open && collapsible === 'offcanvas'\n const drawer = breakpoint === null ? null : DRAWER[breakpoint]\n\n return (\n <>\n {/* Only while it is actually an overlay. On a column there is nothing to\n close onto, and a full-page button over a docked rail is a page that\n cannot be clicked. */}\n {drawer && mobile && open && (\n <button\n type=\"button\"\n aria-label={scrimLabel}\n onClick={() => setOpen(false)}\n data-m22-animated\n className={cn(\n 'inset-0 z-(--z-scrim) bg-(--scrim) animate-[m22-fade-in_var(--duration-fast)_var(--ease)]',\n contained ? 'absolute' : 'fixed',\n )}\n />\n )}\n <nav\n aria-label={label}\n data-state={open ? 'open' : 'collapsed'}\n data-collapsible={collapsible}\n data-side={side}\n data-variant={variant}\n // Off-screen is not closed. Without this the drawer keeps focus and\n // keeps being read aloud while it sits beyond the edge of the page.\n // JavaScript decides it, and a frame's delay costs nothing: the\n // POSITION is CSS, so nothing has moved.\n inert={(drawer && mobile && !open) || undefined}\n // `group`, so every descendant can answer the rail's state without\n // being handed a prop through four levels of composition.\n className={cn(\n 'group/sidebar flex shrink-0 flex-col overflow-hidden bg-(--paper) transition-[width] duration-(--duration-base) ease-(--ease-out-expo) motion-reduce:transition-none',\n gone ? 'w-0' : iconOnly ? 'w-(--sidebar-w-icon)' : 'w-(--sidebar-w)',\n // `inset` gives the border to `SidebarInset`, which is the panel in\n // that arrangement; the rail is the ground it sits on and draws no\n // edge of its own, or the two lines run parallel a few pixels apart.\n variant === 'floating' && 'my-2 rounded-(--radius-frame) border border-(--rule)',\n variant === 'floating' && (side === 'end' ? 'me-2' : 'ms-2'),\n variant === 'flush' &&\n !gone &&\n (side === 'end' ? 'border-s border-(--rule)' : 'border-e border-(--rule)'),\n // The drawer, below the step. Everything above is the column; these\n // override it inside one media query, so the first paint is already\n // right and no effect has to run for the page to be the right width.\n drawer && [\n drawer.frame,\n contained ? drawer.absolute : drawer.fixed,\n side === 'end' ? drawer.end : drawer.start,\n side === 'end'\n ? open\n ? drawer.openEnd\n : drawer.shutEnd\n : open\n ? drawer.openStart\n : drawer.shutStart,\n ],\n className,\n )}\n {...rest}\n >\n {/* The inner column is the full width at every state, so the rows\n inside do not reflow while the rail animates — they are clipped by\n the rail instead, which is what makes the motion read as a panel\n sliding rather than as a column of text reflowing under a wipe. */}\n <div className={cn('flex h-full flex-col', gone ? 'w-(--sidebar-w)' : 'w-full')}>\n {children}\n </div>\n </nav>\n </>\n )\n}\n\n/**\n * The page beside the rail.\n *\n * Every layout built on this component was writing the same\n * `flex min-w-0 flex-1 flex-col` by hand, and `min-w-0` is the half everybody\n * forgets: a flex child's floor is its content, so one wide table inside pushes\n * the whole page past the viewport and takes the rail's width with it.\n *\n * It is also the other half of `variant=\"inset\"`. There the rail is the ground\n * and this is the panel — a bordered `--paper` surface with the ground showing\n * around it — which is why the variant lives on the provider rather than on\n * either piece: one setting, two components, and no way to set half of it.\n *\n * A `<div>`, not a `<main>`. What goes in here is usually a masthead AND the\n * page under it, and only one of those is the main landmark.\n */\nexport function SidebarInset({ className, children, ...rest }: ComponentProps<'div'>) {\n const { variant } = useSidebarState()\n\n return (\n <div\n className={cn(\n 'flex min-w-0 flex-1 flex-col',\n variant === 'inset' &&\n 'my-2 me-2 overflow-hidden rounded-(--radius-frame) border border-(--rule) bg-(--paper) ms-0',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n )\n}\n\n/**\n * The block at the top of the rail: a brand, a workspace, a switcher.\n *\n * It is also where `SidebarTrigger` belongs, and the layout assumes one:\n * anything passed as `children` takes the space and the trigger sits at the\n * inline end of the row.\n */\nexport function SidebarHeader({ className, children, ...rest }: ComponentProps<'div'>) {\n return (\n <div\n className={cn(\n 'flex min-h-14 shrink-0 items-center gap-2 border-b border-(--rule) px-3',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n )\n}\n\n/** The scrolling middle. Everything that is a list of places goes here. */\nexport function SidebarContent({ className, children, ...rest }: ComponentProps<'div'>) {\n return (\n <div\n className={cn('flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-2 scroll-slim', className)}\n {...rest}\n >\n {children}\n </div>\n )\n}\n\n/**\n * The block at the bottom: the utilities, the account, the thing a rail ends on.\n *\n * A separate landmark from the content above it because it is a separate kind\n * of thing — a reader scanning the index does not want Trash and Help in it.\n */\nexport function SidebarFooter({ className, children, ...rest }: ComponentProps<'div'>) {\n return (\n <div\n className={cn('flex shrink-0 flex-col gap-1 border-t border-(--rule) p-2', className)}\n {...rest}\n >\n {children}\n </div>\n )\n}\n\n/** A rule between blocks of rows, inset to the rail's own padding. */\nexport function SidebarSeparator({ className, ...rest }: ComponentProps<'hr'>) {\n return <hr className={cn('my-1 border-0 border-t border-(--rule)', className)} {...rest} />\n}\n\nexport interface SidebarTriggerProps extends ComponentProps<'button'> {\n /** What the button announces. Both states, because it says both things. */\n labels?: { open: string; close: string }\n}\n\n/**\n * The control that opens and closes the rail.\n *\n * Its accessible name changes with what it will DO, and `aria-expanded` reports\n * what is true now — a button permanently called \"Toggle sidebar\" tells a\n * screen reader user nothing about which way it will go.\n */\nexport function SidebarTrigger({\n labels = { open: 'Open the sidebar', close: 'Close the sidebar' },\n className,\n onClick,\n ...rest\n}: SidebarTriggerProps) {\n const { open, toggle, side } = useSidebarState()\n // The glyph points at the edge the rail is on, so the button is a picture of\n // what it will do rather than a picture of somebody else's layout.\n const Icon =\n side === 'end' ? (open ? PanelRightClose : PanelRightOpen) : open ? PanelLeftClose : PanelLeftOpen\n\n return (\n <button\n type=\"button\"\n aria-label={open ? labels.close : labels.open}\n aria-expanded={open}\n onClick={(event) => {\n toggle()\n onClick?.(event)\n }}\n className={cn(\n 'grid size-8 shrink-0 place-items-center rounded-(--radius) text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink)',\n className,\n )}\n {...rest}\n >\n <Icon size={16} strokeWidth={1.5} aria-hidden />\n </button>\n )\n}\n\nexport interface SidebarGroupProps {\n /** The heading over the rows. */\n label: string\n /** How many rows are inside, printed on the far side of the heading. */\n count?: number\n /**\n * A mark that belongs to the GROUP — \"Beta\", \"3 new\".\n *\n * Beside the label, not out at the end with the count: it qualifies the\n * words, and a qualifier that has drifted to the other side of the row reads\n * as a second, unrelated fact.\n */\n badge?: ReactNode\n /**\n * A control on the heading row — a menu, an \"add\" button.\n *\n * Sits between the label and the count, and is NOT rendered inside the\n * heading's own button: a control nested in a control is one the keyboard\n * reaches by pressing the thing it is inside.\n */\n action?: ReactNode\n /** Whether the group folds at all. A group of two rows usually should not. */\n collapsible?: boolean\n defaultOpen?: boolean\n children: ReactNode\n className?: string\n}\n\n/**\n * A labelled block of rows, optionally foldable.\n *\n * The heading is the same SIZE as the rows beneath it and outranks them by\n * weight and by ink. Both halves of that are corrections. Smaller, it inverted\n * the hierarchy it exists to express — a group read as a footnote over a list\n * rather than as a title over its own contents. In the MONO face, which is\n * where it went next, ten of them stacked in a column read as a terminal\n * listing: mono is this system's voice for code, metadata and figures, and a\n * navigation heading is none of those. Rank belongs to weight and to a step up\n * the ink ladder, which are the two signals that can outrank a row without\n * changing what kind of thing it is.\n *\n * An open group draws a hairline down its rows. Fifty rows under seven headings\n * have nothing in them saying which heading any given row belongs to — only the\n * distance to the last one, which is gone the moment the list is scrolled.\n *\n * The whole block hides when the rail is collapsed to icons: a heading with no\n * room for its own word is two or three letters and a number, and the rows are\n * still there underneath as icons.\n */\nexport function SidebarGroup({\n label,\n count,\n badge,\n action,\n collapsible = true,\n defaultOpen = true,\n children,\n className,\n}: SidebarGroupProps) {\n const { iconOnly } = useSidebarState()\n const [open, setOpen] = useState(defaultOpen)\n const labelId = useId()\n\n const rows = <div className=\"group/rows flex flex-col gap-0.5\">{children}</div>\n\n if (iconOnly) {\n // No heading, and no rule: both of them are words and an indent, and there\n // is room for neither. The rows keep their own group semantics from the\n // label, which is still announced even though it is not drawn.\n return (\n <div role=\"group\" aria-label={label} className={cn('group/rows flex flex-col gap-0.5', className)}>\n {children}\n </div>\n )\n }\n\n const heading = (\n <>\n <span className=\"truncate text-sm font-medium text-(--ink)\">{label}</span>\n {badge}\n {count !== undefined && (\n <span className=\"ms-auto text-xs tabular-nums text-(--ink-3-aa)\">{count}</span>\n )}\n </>\n )\n\n if (!collapsible) {\n return (\n <div role=\"group\" aria-labelledby={labelId} className={cn('flex flex-col gap-1', className)}>\n <div className=\"flex min-h-8 items-center gap-1.5 px-2 py-1\" id={labelId}>\n {heading}\n {action}\n </div>\n <div className=\"ms-[0.9rem] flex flex-col gap-0.5 border-s border-(--rule) ps-2\">{rows}</div>\n </div>\n )\n }\n\n return (\n // Radix rather than `{open && …}`: a conditional render cannot animate — the\n // rows are simply not there on the next frame — and Radix publishes the\n // measured height, so the panel opens to what it actually is rather than to\n // a guessed maximum.\n <Collapsible open={open} onOpenChange={setOpen} className={cn('flex flex-col gap-1', className)}>\n <div className=\"flex min-h-9 items-center gap-1.5\">\n <CollapsibleTrigger className=\"group/heading flex min-h-8 flex-1 items-center gap-1.5 rounded-(--radius-sm) px-2 py-1 text-start transition-colors duration-(--duration-fast) hover:bg-(--stone)\">\n <ChevronRight\n size={12}\n strokeWidth={2}\n aria-hidden\n className=\"shrink-0 text-(--ink-3-aa) transition-transform duration-(--duration-base) ease-(--ease-out-expo) group-data-[state=open]/heading:rotate-90 motion-reduce:transition-none\"\n />\n {heading}\n </CollapsibleTrigger>\n {action}\n </div>\n <CollapsibleContent>\n <div className=\"ms-[0.9rem] flex flex-col gap-0.5 border-s border-(--rule) ps-2\">{rows}</div>\n </CollapsibleContent>\n </Collapsible>\n )\n}\n\n/**\n * One filled row at a time, and it follows the pointer.\n *\n * A rail draws the current page filled and fills a row under the pointer, and\n * those are two different facts wearing the same paint — so a reader with the\n * mouse in the list sees two rows claiming to be where they are. The fill is\n * the POINTER'S while the pointer is in the list, and returns to the current\n * page when it leaves; `aria-current` never moves, so nothing about what is\n * true changes, only what is drawn.\n *\n * `not-hover` rather than an ordering trick: composed with `group-hover`, it\n * says \"the list is hovered and this row is not\" in one selector, which does\n * not depend on which of two equally specific rules Tailwind emitted last.\n */\nconst MOVING_HIGHLIGHT =\n 'group-hover/rows:not-hover:bg-transparent group-hover/rows:not-hover:font-normal group-hover/rows:not-hover:text-(--ink-3-aa)'\n\nexport interface SidebarBranchProps {\n /** The row's own words, and the name of the group it opens. */\n label: string\n /** Drawn at the start of the row, and the whole of the row when collapsed. */\n icon?: LucideIcon\n /** A count or a state at the end of the row. */\n trailing?: ReactNode\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n children: ReactNode\n className?: string\n}\n\n/**\n * A row that opens onto more rows.\n *\n * The thing a rail is for and the thing a flat list of groups cannot do: a\n * workspace with projects in it, a folder with documents in it, a service with\n * its environments. `SidebarGroup` is a HEADING over a set — it is not itself a\n * place, and it has no icon and no state. This is a place that contains places,\n * so it is a row like any other and it carries the same icon, trailing slot and\n * hover as its children.\n *\n * The children sit behind the same hairline a group draws, one indent further\n * in, so nesting reads as depth rather than as two unrelated lists. Two levels\n * is what the indent has room for at this width; a third is a tree, and a tree\n * in a 16rem column is a horizontal scrollbar with an outline in it.\n *\n * Collapsed to icons the row becomes its icon and the children are not drawn —\n * there is nowhere for an indent to go, and a nested icon under an unnested one\n * is two glyphs with no visible relationship.\n *\n * @example\n * <SidebarBranch label=\"Acme HQ\" icon={Building} defaultOpen>\n * <SidebarItem href=\"/hq\" icon={Home}>Home</SidebarItem>\n * <SidebarItem href=\"/hq/tasks\" icon={CheckSquare}>My tasks</SidebarItem>\n * </SidebarBranch>\n */\nexport function SidebarBranch({\n label,\n icon: Icon,\n trailing,\n defaultOpen = false,\n open: controlled,\n onOpenChange,\n children,\n className,\n}: SidebarBranchProps) {\n const { iconOnly, side } = useSidebarState()\n const [uncontrolled, setUncontrolled] = useState(defaultOpen)\n const open = controlled ?? uncontrolled\n\n const setOpen = (next: boolean) => {\n if (controlled === undefined) setUncontrolled(next)\n onOpenChange?.(next)\n }\n\n if (iconOnly && Icon) {\n // The row, and nothing under it. A branch collapsed to an icon is a place\n // you can still see; its contents are a level of structure the width no\n // longer has.\n return (\n <Tooltip content={label} side={side === 'end' ? 'left' : 'right'}>\n <button\n type=\"button\"\n aria-label={label}\n className={cn(\n 'flex min-h-(--control-h-sm) items-center justify-center rounded-(--radius) text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink)',\n className,\n )}\n >\n <Icon size={18} strokeWidth={1.5} aria-hidden />\n </button>\n </Tooltip>\n )\n }\n\n return (\n <Collapsible open={open} onOpenChange={setOpen} className={cn('flex flex-col gap-0.5', className)}>\n <CollapsibleTrigger className=\"group/branch flex min-h-(--control-h-sm) items-center gap-3 rounded-(--radius) px-3 py-2 text-start text-sm text-(--ink-3-aa) transition-colors duration-(--duration-fast) hover:bg-(--stone) hover:text-(--ink)\">\n {Icon && <Icon size={18} strokeWidth={1.5} aria-hidden className=\"shrink-0\" />}\n <span className=\"truncate\">{label}</span>\n {trailing !== undefined && <span className=\"ms-auto shrink-0\">{trailing}</span>}\n {/* The chevron goes at the END and only when nothing else is there, so\n a row with a count does not put two marks in the same corner. */}\n {trailing === undefined && (\n <ChevronRight\n size={12}\n strokeWidth={2}\n aria-hidden\n className=\"ms-auto shrink-0 transition-transform duration-(--duration-base) ease-(--ease-out-expo) group-data-[state=open]/branch:rotate-90 motion-reduce:transition-none\"\n />\n )}\n </CollapsibleTrigger>\n <CollapsibleContent>\n <div className=\"group/rows ms-[1.35rem] flex flex-col gap-0.5 border-s border-(--rule) ps-2\">\n {children}\n </div>\n </CollapsibleContent>\n </Collapsible>\n )\n}\n\nexport interface SidebarItemProps extends NavItemProps {\n /** A count or a state on the far side of the row. Hidden with the label. */\n trailing?: ReactNode\n}\n\n/**\n * One row.\n *\n * `NavItem` with the two things a RAIL adds: a trailing slot, and an answer for\n * the state where there is no room for words. Collapsed to icons the label is\n * removed from the layout rather than hidden with CSS — a `sr-only` label still\n * occupies the flex row's gap — and moves into a tooltip, because an icon on\n * its own is a guess for everyone and unusable for a screen reader.\n *\n * A row with no `icon` keeps its label collapsed, since hiding it would leave a\n * blank row: the icon is what makes the collapsed state legible, and a rail\n * that collapses needs one on every row.\n */\nexport function SidebarItem({ trailing, children, className, onClick, ...rest }: SidebarItemProps) {\n const { iconOnly, side, mobile, setOpen } = useSidebarState()\n\n // A drawer closes when a row in it is taken. On a phone the rail is over the\n // page, so following a link inside it and leaving it open is a reader landing\n // on a destination they cannot see — and every application that ships this\n // pattern without it has the same bug report.\n const take = (event: MouseEvent<HTMLAnchorElement>) => {\n if (mobile) setOpen(false)\n onClick?.(event)\n }\n\n if (iconOnly && rest.icon && !rest.asChild) {\n return (\n <Tooltip content={children} side={side === 'end' ? 'left' : 'right'}>\n <NavItem {...rest} onClick={take} className={cn('justify-center px-0', className)}>\n <span className=\"sr-only\">{children}</span>\n </NavItem>\n </Tooltip>\n )\n }\n\n // `asChild` hands the row to a router's own Link, and Radix's Slot takes\n // exactly ONE child — a second one, or even the `false` an unused conditional\n // leaves behind, throws rather than renders. So a slotted row passes its\n // children straight through and puts its own trailing content inside the\n // child it was handed. NavItem's `icon` has the same constraint for the same\n // reason, and says so.\n if (rest.asChild) {\n return (\n <NavItem {...rest} onClick={take} className={cn(MOVING_HIGHLIGHT, className)}>\n {children}\n </NavItem>\n )\n }\n\n return (\n <NavItem {...rest} onClick={take} className={cn(MOVING_HIGHLIGHT, className)}>\n {children}\n {trailing !== undefined && <span className=\"ms-auto shrink-0\">{trailing}</span>}\n </NavItem>\n )\n}\n"],"mappings":";AAoYM,SAmFF,UAnFE,KAmFF,YAnFE;AAlYN;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,UAAU;AACnB,SAAS,qBAAqB;AAC9B,SAAS,aAAa,oBAAoB,0BAA0B;AACpE,SAAS,eAAkC;AAC3C,SAAS,SAAS,uBAAuB;AA2CzC,MAAM,SAGF;AAAA,EACF,IAAI;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,OAAO;AAAA,IACP,KAAK;AAAA,IACL,WAAW;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AAAA,EACA,IAAI;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,OAAO;AAAA,IACP,KAAK;AAAA,IACL,WAAW;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AAAA,EACA,IAAI;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,OAAO;AAAA,IACP,KAAK;AAAA,IACL,WAAW;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AAAA,EACA,IAAI;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,OAAO;AAAA,IACP,KAAK;AAAA,IACL,WAAW;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AACF;AAuBA,MAAM,iBAAiB,cAAmC,IAAI;AAa9D,MAAM,YAA0B;AAAA,EAC9B,MAAM;AAAA,EACN,SAAS,MAAM;AAAA,EAAC;AAAA,EAChB,QAAQ,MAAM;AAAA,EAAC;AAAA,EACf,aAAa;AAAA,EACb,UAAU;AAAA,EACV,MAAM;AAAA,EACN,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,YAAY;AACd;AAGA,SAAS,kBAAgC;AACvC,SAAO,WAAW,cAAc,KAAK;AACvC;AAeO,SAAS,aAA2B;AACzC,QAAM,QAAQ,WAAW,cAAc;AACvC,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,oDAAoD;AAChF,SAAO;AACT;AAkFO,SAAS,gBAAgB;AAAA,EAC9B,MAAM;AAAA,EACN,cAAc;AAAA,EACd;AAAA,EACA,cAAc;AAAA,EACd,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AAAA,EACV,aAAa;AAAA,EACb,UAAU;AAAA,EACV,YAAY;AAAA,EACZ;AACF,GAAyB;AACvB,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAK5D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAK1C,YAAU,MAAM;AACd,QAAI,eAAe,KAAM;AACzB,UAAM,QAAQ,OAAO,WAAW,OAAO,UAAU,EAAE,KAAK;AACxD,UAAM,OAAO,MAAM,UAAU,MAAM,OAAO;AAC1C,SAAK;AACL,UAAM,iBAAiB,UAAU,IAAI;AACrC,WAAO,MAAM,MAAM,oBAAoB,UAAU,IAAI;AAAA,EACvD,GAAG,CAAC,UAAU,CAAC;AAKf,YAAU,MAAM;AACd,QAAI,OAAQ,WAAU,KAAK;AAAA,EAC7B,GAAG,CAAC,MAAM,CAAC;AAEX,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,QAAI;AACF,YAAM,SAAS,OAAO,aAAa,QAAQ,OAAO;AAClD,UAAI,WAAW,UAAU,WAAW,SAAU,iBAAgB,WAAW,MAAM;AAAA,IACjF,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,QAAI;AACF,aAAO,aAAa,QAAQ,SAAS,eAAe,SAAS,QAAQ;AAAA,IACvE,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,SAAS,YAAY,CAAC;AAE1B,QAAM,SAAS,cAAc;AAC7B,QAAM,OAAO,SAAS,SAAS;AAE/B,QAAM,UAAU;AAAA,IACd,CAAC,SAAkB;AACjB,UAAI,OAAQ,WAAU,IAAI;AAAA,eACjB,eAAe,OAAW,iBAAgB,IAAI;AACvD,qBAAe,IAAI;AAAA,IACrB;AAAA,IACA,CAAC,QAAQ,YAAY,YAAY;AAAA,EACnC;AAEA,QAAM,SAAS,YAAY,MAAM,QAAQ,CAAC,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC;AAEhE,YAAU,MAAM;AACd,QAAI,CAAC,YAAY,gBAAgB,OAAQ;AACzC,UAAM,QAAQ,CAAC,UAAyB;AAItC,UAAI,MAAM,IAAI,YAAY,MAAM,SAAS,YAAY,EAAG;AACxD,UAAI,CAAC,MAAM,WAAW,CAAC,MAAM,QAAS;AACtC,YAAM,eAAe;AACrB,aAAO;AAAA,IACT;AACA,WAAO,iBAAiB,WAAW,KAAK;AACxC,WAAO,MAAM,OAAO,oBAAoB,WAAW,KAAK;AAAA,EAC1D,GAAG,CAAC,UAAU,aAAa,MAAM,CAAC;AAElC,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA;AAAA,MAGA,UAAU,CAAC,UAAU,CAAC,QAAQ,gBAAgB;AAAA,MAC9C;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,MAAM,SAAS,QAAQ,aAAa,MAAM,SAAS,QAAQ,WAAW,UAAU;AAAA,EACnF;AAQA,SACE,oBAAC,eAAe,UAAf,EAAwB,OACvB,8BAAC,mBAAiB,UAAS,GAC7B;AAEJ;AAiEO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAiB;AACf,QAAM,EAAE,MAAM,SAAS,aAAa,UAAU,MAAM,SAAS,QAAQ,WAAW,WAAW,IACzF,gBAAgB;AAClB,gBAAc,WAAW,SAAS,OAAO,gDAAgD;AAEzF,QAAM,OAAO,CAAC,QAAQ,gBAAgB;AACtC,QAAM,SAAS,eAAe,OAAO,OAAO,OAAO,UAAU;AAE7D,SACE,iCAIG;AAAA,cAAU,UAAU,QACnB;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAY;AAAA,QACZ,SAAS,MAAM,QAAQ,KAAK;AAAA,QAC5B,qBAAiB;AAAA,QACjB,WAAW;AAAA,UACT;AAAA,UACA,YAAY,aAAa;AAAA,QAC3B;AAAA;AAAA,IACF;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,cAAY;AAAA,QACZ,cAAY,OAAO,SAAS;AAAA,QAC5B,oBAAkB;AAAA,QAClB,aAAW;AAAA,QACX,gBAAc;AAAA,QAKd,OAAQ,UAAU,UAAU,CAAC,QAAS;AAAA,QAGtC,WAAW;AAAA,UACT;AAAA,UACA,OAAO,QAAQ,WAAW,yBAAyB;AAAA;AAAA;AAAA;AAAA,UAInD,YAAY,cAAc;AAAA,UAC1B,YAAY,eAAe,SAAS,QAAQ,SAAS;AAAA,UACrD,YAAY,WACV,CAAC,SACA,SAAS,QAAQ,6BAA6B;AAAA;AAAA;AAAA;AAAA,UAIjD,UAAU;AAAA,YACR,OAAO;AAAA,YACP,YAAY,OAAO,WAAW,OAAO;AAAA,YACrC,SAAS,QAAQ,OAAO,MAAM,OAAO;AAAA,YACrC,SAAS,QACL,OACE,OAAO,UACP,OAAO,UACT,OACE,OAAO,YACP,OAAO;AAAA,UACf;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA,QAMJ,8BAAC,SAAI,WAAW,GAAG,wBAAwB,OAAO,oBAAoB,QAAQ,GAC3E,UACH;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;AAkBO,SAAS,aAAa,EAAE,WAAW,UAAU,GAAG,KAAK,GAA0B;AACpF,QAAM,EAAE,QAAQ,IAAI,gBAAgB;AAEpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,YAAY,WACV;AAAA,QACF;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AASO,SAAS,cAAc,EAAE,WAAW,UAAU,GAAG,KAAK,GAA0B;AACrF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAGO,SAAS,eAAe,EAAE,WAAW,UAAU,GAAG,KAAK,GAA0B;AACtF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,sEAAsE,SAAS;AAAA,MAC5F,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAQO,SAAS,cAAc,EAAE,WAAW,UAAU,GAAG,KAAK,GAA0B;AACrF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,6DAA6D,SAAS;AAAA,MACnF,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAGO,SAAS,iBAAiB,EAAE,WAAW,GAAG,KAAK,GAAyB;AAC7E,SAAO,oBAAC,QAAG,WAAW,GAAG,0CAA0C,SAAS,GAAI,GAAG,MAAM;AAC3F;AAcO,SAAS,eAAe;AAAA,EAC7B,SAAS,EAAE,MAAM,oBAAoB,OAAO,oBAAoB;AAAA,EAChE;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwB;AACtB,QAAM,EAAE,MAAM,QAAQ,KAAK,IAAI,gBAAgB;AAG/C,QAAM,OACJ,SAAS,QAAS,OAAO,kBAAkB,iBAAkB,OAAO,iBAAiB;AAEvF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,OAAO,OAAO,QAAQ,OAAO;AAAA,MACzC,iBAAe;AAAA,MACf,SAAS,CAAC,UAAU;AAClB,eAAO;AACP,kBAAU,KAAK;AAAA,MACjB;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ,8BAAC,QAAK,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC;AAAA;AAAA,EAChD;AAEJ;AAmDO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAsB;AACpB,QAAM,EAAE,SAAS,IAAI,gBAAgB;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,WAAW;AAC5C,QAAM,UAAU,MAAM;AAEtB,QAAM,OAAO,oBAAC,SAAI,WAAU,oCAAoC,UAAS;AAEzE,MAAI,UAAU;AAIZ,WACE,oBAAC,SAAI,MAAK,SAAQ,cAAY,OAAO,WAAW,GAAG,oCAAoC,SAAS,GAC7F,UACH;AAAA,EAEJ;AAEA,QAAM,UACJ,iCACE;AAAA,wBAAC,UAAK,WAAU,6CAA6C,iBAAM;AAAA,IAClE;AAAA,IACA,UAAU,UACT,oBAAC,UAAK,WAAU,kDAAkD,iBAAM;AAAA,KAE5E;AAGF,MAAI,CAAC,aAAa;AAChB,WACE,qBAAC,SAAI,MAAK,SAAQ,mBAAiB,SAAS,WAAW,GAAG,uBAAuB,SAAS,GACxF;AAAA,2BAAC,SAAI,WAAU,+CAA8C,IAAI,SAC9D;AAAA;AAAA,QACA;AAAA,SACH;AAAA,MACA,oBAAC,SAAI,WAAU,mEAAmE,gBAAK;AAAA,OACzF;AAAA,EAEJ;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKE,qBAAC,eAAY,MAAY,cAAc,SAAS,WAAW,GAAG,uBAAuB,SAAS,GAC5F;AAAA,2BAAC,SAAI,WAAU,qCACb;AAAA,6BAAC,sBAAmB,WAAU,qKAC5B;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAM;AAAA,cACN,aAAa;AAAA,cACb,eAAW;AAAA,cACX,WAAU;AAAA;AAAA,UACZ;AAAA,UACC;AAAA,WACH;AAAA,QACC;AAAA,SACH;AAAA,MACA,oBAAC,sBACC,8BAAC,SAAI,WAAU,mEAAmE,gBAAK,GACzF;AAAA,OACF;AAAA;AAEJ;AAgBA,MAAM,mBACJ;AAyCK,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA,cAAc;AAAA,EACd,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,EAAE,UAAU,KAAK,IAAI,gBAAgB;AAC3C,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAC5D,QAAM,OAAO,cAAc;AAE3B,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,eAAe,OAAW,iBAAgB,IAAI;AAClD,mBAAe,IAAI;AAAA,EACrB;AAEA,MAAI,YAAY,MAAM;AAIpB,WACE,oBAAC,WAAQ,SAAS,OAAO,MAAM,SAAS,QAAQ,SAAS,SACvD;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEA,8BAAC,QAAK,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC;AAAA;AAAA,IAChD,GACF;AAAA,EAEJ;AAEA,SACE,qBAAC,eAAY,MAAY,cAAc,SAAS,WAAW,GAAG,yBAAyB,SAAS,GAC9F;AAAA,yBAAC,sBAAmB,WAAU,oNAC3B;AAAA,cAAQ,oBAAC,QAAK,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC,WAAU,YAAW;AAAA,MAC5E,oBAAC,UAAK,WAAU,YAAY,iBAAM;AAAA,MACjC,aAAa,UAAa,oBAAC,UAAK,WAAU,oBAAoB,oBAAS;AAAA,MAGvE,aAAa,UACZ;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN,aAAa;AAAA,UACb,eAAW;AAAA,UACX,WAAU;AAAA;AAAA,MACZ;AAAA,OAEJ;AAAA,IACA,oBAAC,sBACC,8BAAC,SAAI,WAAU,+EACZ,UACH,GACF;AAAA,KACF;AAEJ;AAoBO,SAAS,YAAY,EAAE,UAAU,UAAU,WAAW,SAAS,GAAG,KAAK,GAAqB;AACjG,QAAM,EAAE,UAAU,MAAM,QAAQ,QAAQ,IAAI,gBAAgB;AAM5D,QAAM,OAAO,CAAC,UAAyC;AACrD,QAAI,OAAQ,SAAQ,KAAK;AACzB,cAAU,KAAK;AAAA,EACjB;AAEA,MAAI,YAAY,KAAK,QAAQ,CAAC,KAAK,SAAS;AAC1C,WACE,oBAAC,WAAQ,SAAS,UAAU,MAAM,SAAS,QAAQ,SAAS,SAC1D,8BAAC,WAAS,GAAG,MAAM,SAAS,MAAM,WAAW,GAAG,uBAAuB,SAAS,GAC9E,8BAAC,UAAK,WAAU,WAAW,UAAS,GACtC,GACF;AAAA,EAEJ;AAQA,MAAI,KAAK,SAAS;AAChB,WACE,oBAAC,WAAS,GAAG,MAAM,SAAS,MAAM,WAAW,GAAG,kBAAkB,SAAS,GACxE,UACH;AAAA,EAEJ;AAEA,SACE,qBAAC,WAAS,GAAG,MAAM,SAAS,MAAM,WAAW,GAAG,kBAAkB,SAAS,GACxE;AAAA;AAAA,IACA,aAAa,UAAa,oBAAC,UAAK,WAAU,oBAAoB,oBAAS;AAAA,KAC1E;AAEJ;","names":[]}
|
|
@@ -18,6 +18,20 @@ interface SliderProps extends ComponentProps<typeof SliderPrimitive.Root> {
|
|
|
18
18
|
* as "Minimum" over "10 – 90".
|
|
19
19
|
*/
|
|
20
20
|
showValue?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Turns that readout into a box the number can be typed into, and implies
|
|
23
|
+
* `showValue`.
|
|
24
|
+
*
|
|
25
|
+
* A slider is a control for a NEIGHBOURHOOD; someone who needs 1,150 rather
|
|
26
|
+
* than roughly 1,200 is dragging a 16px thumb across a hundred steps to get
|
|
27
|
+
* it. This is the way out, in the place the value already is, rather than a
|
|
28
|
+
* second field beside the track that has to be kept in step by hand.
|
|
29
|
+
*
|
|
30
|
+
* The box shows `format`'s output at rest and the bare number while it has
|
|
31
|
+
* focus, so a reader still sees "$1,200" and a typist is never asked to type
|
|
32
|
+
* a currency symbol back.
|
|
33
|
+
*/
|
|
34
|
+
editable?: boolean;
|
|
21
35
|
/**
|
|
22
36
|
* Renders the value with a unit or a currency, e.g. `(n) => n + '%'`.
|
|
23
37
|
*
|
|
@@ -39,6 +53,11 @@ interface SliderProps extends ComponentProps<typeof SliderPrimitive.Root> {
|
|
|
39
53
|
* A 44px hit area sits invisibly around the 16px thumb, because a thumb sized
|
|
40
54
|
* for the design is well under any pointer-target guideline.
|
|
41
55
|
*
|
|
56
|
+
* `editable` is the answer to the thing a slider cannot do. Reach for it
|
|
57
|
+
* whenever the exact figure is the point — a budget, a timeout, a price — and
|
|
58
|
+
* leave it off when the value is genuinely approximate, because a box invites
|
|
59
|
+
* precision the setting may not have.
|
|
60
|
+
*
|
|
42
61
|
* Inside a `Field`, the hint, the error and the requirement land on the THUMB,
|
|
43
62
|
* which is the element carrying `role="slider"` — on the root they would sit on
|
|
44
63
|
* a `<span>` with no role and announce nothing. The NAME still comes from
|
|
@@ -47,7 +66,9 @@ interface SliderProps extends ComponentProps<typeof SliderPrimitive.Root> {
|
|
|
47
66
|
*
|
|
48
67
|
* @example
|
|
49
68
|
* <Slider label="Quality" defaultValue={[80]} max={100} step={5} showValue format={(n) => `${n}%`} />
|
|
69
|
+
* @example
|
|
70
|
+
* <Slider label="Monthly budget" defaultValue={[1200]} max={5000} step={50} editable format={(n) => `$${n}`} />
|
|
50
71
|
*/
|
|
51
|
-
declare function Slider({ label, showValue, format, className, onValueChange, 'aria-describedby': describedBy, 'aria-required': ariaRequired, 'aria-invalid': ariaInvalid, ...props }: SliderProps): react.JSX.Element;
|
|
72
|
+
declare function Slider({ label, showValue, editable, format, className, onValueChange, value: controlledValue, defaultValue, min, max, step, 'aria-describedby': describedBy, 'aria-required': ariaRequired, 'aria-invalid': ariaInvalid, ...props }: SliderProps): react.JSX.Element;
|
|
52
73
|
|
|
53
74
|
export { Slider, type SliderProps, Slider as default };
|