@sableui/react 0.17.0 → 0.19.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 +10 -3
- package/CHANGELOG.md +235 -0
- package/README.md +11 -4
- package/bin/sable.mjs +266 -30
- package/dist/{_contract-CDa9-NdB.d.ts → _contract-DlklV0Im.d.ts} +2 -2
- package/dist/activity-DT8VK9fx.d.ts +25 -0
- package/dist/adapters/github.d.ts +41 -0
- package/dist/adapters/github.js +24 -0
- package/dist/blocks/index.d.ts +1 -1
- package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
- package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
- package/dist/components/activity-heatmap/index.js +9 -0
- package/dist/components/avatar/avatar.js +16 -7
- package/dist/components/avatar/avatar.meta.js +43 -10
- package/dist/components/avatar/avatar.variants.js +26 -2
- package/dist/components/code-block/code-block.js +5 -3
- package/dist/components/code-block/code-block.meta.js +6 -0
- package/dist/components/collapsible/collapsible.js +6 -5
- package/dist/components/collapsible/collapsible.meta.js +14 -5
- package/dist/components/data-table/data-table.js +132 -45
- package/dist/components/data-table/data-table.meta.js +63 -4
- package/dist/components/date-picker/date-picker.js +13 -6
- package/dist/components/date-picker/date-picker.meta.js +22 -0
- package/dist/components/dialog/dialog.js +25 -5
- package/dist/components/dialog/dialog.meta.js +26 -5
- package/dist/components/media-image/media-image.js +3 -1
- package/dist/components/media-image/media-image.meta.js +15 -1
- package/dist/components/media-tile/media-tile.js +13 -1
- package/dist/components/media-tile/media-tile.meta.js +10 -0
- package/dist/components/menu/menu.js +2 -0
- package/dist/components/menu/menu.meta.js +2 -2
- package/dist/components/page-header/page-header.js +9 -4
- package/dist/components/page-header/page-header.meta.js +26 -1
- package/dist/components/panel-group/panel-group.js +17 -1
- package/dist/components/panel-group/panel-group.meta.js +43 -3
- package/dist/components/pill/pill.meta.js +15 -6
- package/dist/components/pill/pill.variants.js +1 -0
- package/dist/components/search-input/search-input.js +30 -5
- package/dist/components/search-input/search-input.meta.js +29 -4
- package/dist/components/section-header/section-header.js +27 -17
- package/dist/components/section-header/section-header.meta.js +27 -5
- package/dist/components/sheet/sheet.js +15 -3
- package/dist/components/sheet/sheet.meta.js +2 -2
- package/dist/components/side-nav/side-nav.variants.js +8 -3
- package/dist/components/sidebar/index.js +4 -0
- package/dist/components/sidebar/sidebar.js +90 -11
- package/dist/components/sidebar/sidebar.meta.js +38 -4
- package/dist/components/table/table.js +7 -3
- package/dist/components/tabs/tabs.js +10 -2
- package/dist/components/tabs/tabs.meta.js +4 -0
- package/dist/components/workspace/workspace.js +13 -3
- package/dist/components/workspace/workspace.meta.js +109 -1
- package/dist/file-hashes.json +94 -80
- package/dist/foundations/typography.js +13 -0
- package/dist/icon/Icon.js +21 -2
- package/dist/icon/index.d.ts +36 -1
- package/dist/icon/index.js +5 -0
- package/dist/icon/names.js +240 -0
- package/dist/icon-sets/reicon.js +41 -1
- package/dist/index.d.ts +540 -75
- package/dist/index.js +18 -1
- package/dist/layout/AspectBox.js +2 -5
- package/dist/layout/Bleed.js +10 -6
- package/dist/layout/Center.js +11 -3
- package/dist/layout/Columns.js +6 -3
- package/dist/layout/Grid.js +14 -6
- package/dist/layout/GridOverlay.js +7 -7
- package/dist/layout/Region.js +18 -3
- package/dist/layout/Row.js +27 -20
- package/dist/layout/Spacer.js +13 -2
- package/dist/layout/SplitPane.js +1 -1
- package/dist/layout/Stack.js +25 -18
- package/dist/layout/index.d.ts +57 -6
- package/dist/lib/chart/activity.js +32 -0
- package/dist/lib/chart/calendar.js +67 -0
- package/dist/lib/chart/chart-tooltip.js +2 -2
- package/dist/lib/chart/heat.js +55 -0
- package/dist/lib/cn.js +1 -0
- package/dist/lib/focus-return.js +53 -0
- package/dist/lib/taxonomy.js +1 -0
- package/dist/lib/use-media-query.js +41 -0
- package/dist/{meta-DX5r5wQL.d.ts → meta-Dfm9AExe.d.ts} +6 -0
- package/dist/patterns/dashboard-page.js +23 -17
- package/dist/patterns/index.d.ts +7 -2
- package/dist/patterns/index.js +5 -1
- package/dist/patterns/tracker-page.js +392 -0
- package/dist/patterns/tracker-page.meta.js +40 -0
- package/dist/presets/index.d.ts +1 -1
- package/dist/styles.css +301 -188
- package/dist/{theme-provider-BydfM5Z0.d.ts → theme-provider-BUBealk_.d.ts} +1 -1
- package/dist/themes/_contract.js +9 -1
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/verdant/index.d.ts +1 -1
- package/llms/components/activity-heatmap.md +133 -0
- package/llms/components/avatar.md +32 -9
- package/llms/components/code-block.md +1 -0
- package/llms/components/collapsible.md +12 -4
- package/llms/components/data-table.md +40 -3
- package/llms/components/date-picker.md +15 -1
- package/llms/components/dialog.md +34 -3
- package/llms/components/media-image.md +12 -0
- package/llms/components/media-tile.md +21 -0
- package/llms/components/menu.md +1 -1
- package/llms/components/page-header.md +16 -1
- package/llms/components/panel-group.md +34 -2
- package/llms/components/pill.md +24 -5
- package/llms/components/search-input.md +37 -4
- package/llms/components/section-header.md +28 -4
- package/llms/components/sheet.md +2 -2
- package/llms/components/sidebar.md +17 -4
- package/llms/components/tabs.md +1 -0
- package/llms/components/workspace.md +81 -1
- package/llms/foundations/typography.md +4 -1
- package/llms/layout/aspect-box.md +1 -1
- package/llms/layout/bleed.md +1 -1
- package/llms/layout/center.md +1 -1
- package/llms/layout/columns.md +1 -1
- package/llms/layout/grid.md +1 -1
- package/llms/layout/region.md +1 -1
- package/llms/layout/row.md +9 -1
- package/llms/layout/spacer.md +1 -1
- package/llms/layout/split-pane.md +1 -1
- package/llms/layout/stack.md +1 -1
- package/llms/patterns/dashboard-page.md +18 -15
- package/llms/patterns/tracker-page.md +450 -0
- package/llms/reference/token-intent.md +3 -1
- package/llms/reference/utilities.md +4 -1
- package/llms/tasks/auditing-an-app.md +35 -1
- package/llms/tasks/building-a-page.md +3 -2
- package/llms/tasks/building-a-tool.md +101 -5
- package/llms/tasks/building-with-sable.md +11 -3
- package/llms/tasks/creating-a-preset.md +9 -0
- package/llms/tasks/getting-started.md +10 -3
- package/llms/tasks/using-presets.md +1 -2
- package/llms-full.txt +1215 -89
- package/llms.txt +11 -9
- package/package.json +7 -1
- package/registry.json +1021 -70
- package/scripts/adoption.mjs +252 -0
- package/scripts/agent-setup.mjs +110 -18
- package/scripts/app-css.mjs +129 -0
- package/scripts/grade.mjs +178 -2
- package/scripts/lint-rules.mjs +126 -0
- package/scripts/wiring.mjs +2 -1
- package/skills/entry/SKILL.md +1 -1
- package/skills/sable/SKILL.md +64 -6
- package/skills/sable/agents/openai.yaml +1 -1
- package/targets/ios.json +9 -0
|
@@ -5,9 +5,12 @@ import { useRender } from "@base-ui/react/use-render";
|
|
|
5
5
|
import {
|
|
6
6
|
createContext,
|
|
7
7
|
useContext,
|
|
8
|
+
useId,
|
|
8
9
|
useState
|
|
9
10
|
} from "react";
|
|
11
|
+
import { Icon } from "../../icon/index.js";
|
|
10
12
|
import { cn } from "../../lib/cn.js";
|
|
13
|
+
import { useDensity } from "../../lib/density.js";
|
|
11
14
|
import { HighlightGlide } from "../../lib/highlight-glide.js";
|
|
12
15
|
import { motionAttribute } from "../../motion/MotionProvider.js";
|
|
13
16
|
import { useAppShellFrame } from "../app-shell/app-shell-frame.js";
|
|
@@ -16,7 +19,8 @@ import { SimpleTooltip } from "../tooltip/index.js";
|
|
|
16
19
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
17
20
|
var SidebarContext = createContext({
|
|
18
21
|
collapsed: false,
|
|
19
|
-
icons: true
|
|
22
|
+
icons: true,
|
|
23
|
+
rung: "rail"
|
|
20
24
|
});
|
|
21
25
|
var LABEL_FADE = "starting:opacity-0 transition-opacity duration-(--motion-duration-base) ease-decelerate";
|
|
22
26
|
function useSidebar() {
|
|
@@ -58,7 +62,8 @@ function Sidebar({
|
|
|
58
62
|
const [hasCollapsed, setHasCollapsed] = useState(collapsed);
|
|
59
63
|
if (collapsed && !hasCollapsed) setHasCollapsed(true);
|
|
60
64
|
const inset = useAppShellFrame() === "inset";
|
|
61
|
-
|
|
65
|
+
const rung = useDensity() === "compact" ? "rail-compact" : "rail";
|
|
66
|
+
return /* @__PURE__ */ jsx(SidebarContext.Provider, { value: { collapsed, icons, reopened: hasCollapsed && !collapsed, rung }, children: /* @__PURE__ */ jsx(
|
|
62
67
|
"aside",
|
|
63
68
|
{
|
|
64
69
|
"data-motion": motionAttribute(motion),
|
|
@@ -103,12 +108,15 @@ function SidebarHeader({
|
|
|
103
108
|
className,
|
|
104
109
|
...props
|
|
105
110
|
}) {
|
|
106
|
-
const { collapsed } = useSidebar();
|
|
111
|
+
const { collapsed, rung } = useSidebar();
|
|
107
112
|
return /* @__PURE__ */ jsx(
|
|
108
113
|
"div",
|
|
109
114
|
{
|
|
110
115
|
className: cn(
|
|
111
|
-
"flex
|
|
116
|
+
"flex flex-wrap content-center items-center gap-y-2 px-3",
|
|
117
|
+
// Compact, the brand row is the height of the tool's own chrome bar,
|
|
118
|
+
// so it lines up with the WorkspaceBar beside it.
|
|
119
|
+
rung === "rail-compact" ? "min-h-(--chrome-bar-height) has-[>:nth-child(2)]:py-2" : "min-h-16 has-[>:nth-child(2)]:py-3",
|
|
112
120
|
// Collapsed, the brand mark centres on the same axis as the item icons.
|
|
113
121
|
collapsed && "justify-center px-0",
|
|
114
122
|
className
|
|
@@ -119,7 +127,7 @@ function SidebarHeader({
|
|
|
119
127
|
}
|
|
120
128
|
SidebarHeader.displayName = "SidebarHeader";
|
|
121
129
|
function SidebarBrand({ mark, label, render, className, "aria-label": ariaLabel, ...props }) {
|
|
122
|
-
const { collapsed, reopened } = useSidebar();
|
|
130
|
+
const { collapsed, reopened, rung } = useSidebar();
|
|
123
131
|
return useRender({
|
|
124
132
|
render: render ?? /* @__PURE__ */ jsx("div", {}),
|
|
125
133
|
props: {
|
|
@@ -135,7 +143,7 @@ function SidebarBrand({ mark, label, render, className, "aria-label": ariaLabel,
|
|
|
135
143
|
className
|
|
136
144
|
),
|
|
137
145
|
children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
138
|
-
/* @__PURE__ */ jsx("span", { className: sideNavItemIconVariants({ size:
|
|
146
|
+
/* @__PURE__ */ jsx("span", { className: sideNavItemIconVariants({ size: rung }), children: mark }),
|
|
139
147
|
collapsed ? null : /* @__PURE__ */ jsx("span", { className: cn("min-w-0 truncate text-title-sm", reopened && LABEL_FADE), children: label })
|
|
140
148
|
] })
|
|
141
149
|
},
|
|
@@ -148,6 +156,7 @@ function SidebarNav({
|
|
|
148
156
|
children,
|
|
149
157
|
...props
|
|
150
158
|
}) {
|
|
159
|
+
const { rung } = useSidebar();
|
|
151
160
|
return /* @__PURE__ */ jsxs(
|
|
152
161
|
"nav",
|
|
153
162
|
{
|
|
@@ -155,7 +164,8 @@ function SidebarNav({
|
|
|
155
164
|
// Thin scrollbar so a long nav does not hand a fat platform bar to a
|
|
156
165
|
// narrow rail, and contained overscroll so a wheel gesture that runs
|
|
157
166
|
// out of nav stops there instead of scrolling the page behind it.
|
|
158
|
-
"flex flex-1 flex-col
|
|
167
|
+
"flex flex-1 flex-col overflow-y-auto overscroll-contain px-3 scrollbar-quiet",
|
|
168
|
+
rung === "rail-compact" ? "gap-0.5 py-2" : "gap-1 py-4",
|
|
159
169
|
"[&[data-glide]_[data-active]]:bg-transparent",
|
|
160
170
|
className
|
|
161
171
|
),
|
|
@@ -173,11 +183,13 @@ function SidebarFooter({
|
|
|
173
183
|
...props
|
|
174
184
|
}) {
|
|
175
185
|
const inset = useAppShellFrame() === "inset";
|
|
186
|
+
const { rung } = useSidebar();
|
|
176
187
|
return /* @__PURE__ */ jsx(
|
|
177
188
|
"div",
|
|
178
189
|
{
|
|
179
190
|
className: cn(
|
|
180
|
-
"flex flex-col gap-1 border-t border-sidebar-border px-3
|
|
191
|
+
"flex flex-col gap-1 border-t border-sidebar-border px-3",
|
|
192
|
+
rung === "rail-compact" ? "py-2" : "py-4",
|
|
181
193
|
// In an inset shell the rail has no edge of its own (the gap is the
|
|
182
194
|
// divider), so a full-width rule ran out into that gap and stopped on
|
|
183
195
|
// nothing. Inset to the content column it spans what the rows span:
|
|
@@ -201,7 +213,7 @@ function SidebarItem({
|
|
|
201
213
|
"aria-label": ariaLabel,
|
|
202
214
|
...props
|
|
203
215
|
}) {
|
|
204
|
-
const { collapsed, icons, reopened } = useSidebar();
|
|
216
|
+
const { collapsed, icons, reopened, rung } = useSidebar();
|
|
205
217
|
const showIcon = icon != null && (icons || collapsed);
|
|
206
218
|
const element = useRender({
|
|
207
219
|
render: render ?? /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
@@ -214,12 +226,12 @@ function SidebarItem({
|
|
|
214
226
|
"aria-label": ariaLabel ?? (collapsed && typeof label === "string" ? label : void 0),
|
|
215
227
|
className: cn(
|
|
216
228
|
// SideNav's row recipe on the rail rung — one row, not a copy.
|
|
217
|
-
sideNavItemVariants({ size:
|
|
229
|
+
sideNavItemVariants({ size: rung, activeVariant: "sidebar" }),
|
|
218
230
|
collapsed && "justify-center px-0",
|
|
219
231
|
className
|
|
220
232
|
),
|
|
221
233
|
children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
222
|
-
showIcon ? /* @__PURE__ */ jsx("span", { className: sideNavItemIconVariants({ size:
|
|
234
|
+
showIcon ? /* @__PURE__ */ jsx("span", { className: sideNavItemIconVariants({ size: rung }), children: icon }) : null,
|
|
223
235
|
collapsed ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
224
236
|
/* @__PURE__ */ jsx("span", { className: cn("min-w-0 flex-1 truncate", reopened && LABEL_FADE), children: label }),
|
|
225
237
|
badge ? /* @__PURE__ */ jsx("span", { className: "ms-auto shrink-0", children: badge }) : null
|
|
@@ -234,10 +246,77 @@ function SidebarItem({
|
|
|
234
246
|
return element;
|
|
235
247
|
}
|
|
236
248
|
SidebarItem.displayName = "SidebarItem";
|
|
249
|
+
function SidebarGroup({ label, className, children, ...props }) {
|
|
250
|
+
const { collapsed, rung } = useSidebar();
|
|
251
|
+
const labelId = useId();
|
|
252
|
+
return /* @__PURE__ */ jsxs(
|
|
253
|
+
"div",
|
|
254
|
+
{
|
|
255
|
+
role: "group",
|
|
256
|
+
"aria-labelledby": label ? labelId : void 0,
|
|
257
|
+
className: cn("flex flex-col", rung === "rail-compact" ? "gap-0.5" : "gap-1", className),
|
|
258
|
+
...props,
|
|
259
|
+
children: [
|
|
260
|
+
label ? collapsed ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
261
|
+
/* @__PURE__ */ jsx("span", { id: labelId, className: "sr-only", children: label }),
|
|
262
|
+
/* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "mx-3 my-2 h-px bg-sidebar-border" })
|
|
263
|
+
] }) : /* @__PURE__ */ jsx("div", { id: labelId, className: "truncate px-3 pt-4 pb-1 text-caption text-muted-foreground", children: label }) : null,
|
|
264
|
+
children
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
SidebarGroup.displayName = "SidebarGroup";
|
|
270
|
+
function SidebarAccount({
|
|
271
|
+
avatar,
|
|
272
|
+
label,
|
|
273
|
+
detail,
|
|
274
|
+
indicator,
|
|
275
|
+
render,
|
|
276
|
+
className,
|
|
277
|
+
"aria-label": ariaLabel,
|
|
278
|
+
...props
|
|
279
|
+
}) {
|
|
280
|
+
const { collapsed, reopened, rung } = useSidebar();
|
|
281
|
+
const element = useRender({
|
|
282
|
+
// design-lint-disable-next-line raw-button -- a rail row with its own anatomy (avatar slot, two-line label, chevron) on SidebarItem's row recipe, and usually a MenuTrigger's render target; not a button-shaped action
|
|
283
|
+
render: render ?? /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
284
|
+
props: {
|
|
285
|
+
...props,
|
|
286
|
+
"aria-label": ariaLabel ?? (collapsed && typeof label === "string" ? label : void 0),
|
|
287
|
+
className: cn(
|
|
288
|
+
sideNavItemVariants({ size: rung, activeVariant: "sidebar" }),
|
|
289
|
+
// Holds the hover fill while its menu is open, so the row reads as
|
|
290
|
+
// the thing the menu came from.
|
|
291
|
+
"data-[popup-open]:bg-sidebar-accent data-[popup-open]:text-sidebar-accent-foreground",
|
|
292
|
+
collapsed && "justify-center px-0",
|
|
293
|
+
className
|
|
294
|
+
),
|
|
295
|
+
children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
296
|
+
/* @__PURE__ */ jsx("span", { className: sideNavItemIconVariants({ size: rung }), children: avatar }),
|
|
297
|
+
collapsed ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
298
|
+
/* @__PURE__ */ jsxs("span", { className: cn("flex min-w-0 flex-1 flex-col", reopened && LABEL_FADE), children: [
|
|
299
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: label }),
|
|
300
|
+
detail ? /* @__PURE__ */ jsx("span", { className: "truncate text-caption text-muted-foreground", children: detail }) : null
|
|
301
|
+
] }),
|
|
302
|
+
indicator === false ? null : /* @__PURE__ */ jsx("span", { className: "ms-auto flex shrink-0 text-muted-foreground", "aria-hidden": true, children: indicator ?? /* @__PURE__ */ jsx(Icon, { name: "chevron-up-down", className: "text-base" }) })
|
|
303
|
+
] })
|
|
304
|
+
] })
|
|
305
|
+
},
|
|
306
|
+
defaultTagName: "button"
|
|
307
|
+
});
|
|
308
|
+
if (collapsed) {
|
|
309
|
+
return /* @__PURE__ */ jsx(SimpleTooltip, { content: label, side: "right", children: element });
|
|
310
|
+
}
|
|
311
|
+
return element;
|
|
312
|
+
}
|
|
313
|
+
SidebarAccount.displayName = "SidebarAccount";
|
|
237
314
|
export {
|
|
238
315
|
Sidebar,
|
|
316
|
+
SidebarAccount,
|
|
239
317
|
SidebarBrand,
|
|
240
318
|
SidebarFooter,
|
|
319
|
+
SidebarGroup,
|
|
241
320
|
SidebarHeader,
|
|
242
321
|
SidebarItem,
|
|
243
322
|
SidebarNav,
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
// src/components/sidebar/sidebar.meta.ts
|
|
2
2
|
var sidebarMeta = {
|
|
3
3
|
name: "Sidebar",
|
|
4
|
-
summary: "Desktop side-navigation rail (in-flow sticky, hidden below lg) with SidebarHeader / SidebarBrand / SidebarNav / SidebarItem / SidebarFooter parts. Items are router-agnostic via the `render` prop; an optional collapsed icon-rail mode reveals labels in tooltips.",
|
|
4
|
+
summary: "Desktop side-navigation rail (in-flow sticky, hidden below lg) with SidebarHeader / SidebarBrand / SidebarNav / SidebarGroup / SidebarItem / SidebarFooter / SidebarAccount parts, all on one set of columns. Items are router-agnostic via the `render` prop; an optional collapsed icon-rail mode reveals labels in tooltips.",
|
|
5
5
|
status: "stable",
|
|
6
6
|
keywords: [
|
|
7
7
|
"sidebar",
|
|
8
8
|
"navigation",
|
|
9
9
|
"app chrome",
|
|
10
|
-
"rail"
|
|
10
|
+
"rail",
|
|
11
|
+
"nav group",
|
|
12
|
+
"section label",
|
|
13
|
+
"account",
|
|
14
|
+
"user menu",
|
|
15
|
+
"profile",
|
|
16
|
+
"account switcher",
|
|
17
|
+
"workspace switcher",
|
|
18
|
+
"sidebar footer"
|
|
11
19
|
],
|
|
12
20
|
usage: {
|
|
13
21
|
description: "Use Sidebar for persistent desktop navigation or application chrome outside the main content region.",
|
|
@@ -28,6 +36,18 @@ var sidebarMeta = {
|
|
|
28
36
|
guidance: "do",
|
|
29
37
|
description: `Keep surface="rail" inside AppShell frame="inset": the rail's tint becomes the ground the main panel floats on. Do not add a border-r class back \u2014 the gap is the divider.`
|
|
30
38
|
},
|
|
39
|
+
{
|
|
40
|
+
guidance: "do",
|
|
41
|
+
description: "Group items with SidebarGroup and end the rail with one SidebarAccount that opens the account menu (settings, appearance, sign out). Every part sits on the same icon and label columns, at either density."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
guidance: "dont",
|
|
45
|
+
description: "Do not put SideNav, an Entity or a stack of ghost Buttons in the rail. Each brings its own insets, so the icons and labels land on four different columns. SideNav is the in-page section list; the rail has its own parts."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
guidance: "dont",
|
|
49
|
+
description: "Do not put a Badge or pill in the account row. Its one quiet detail line carries the plan, role or 'Demo'."
|
|
50
|
+
},
|
|
31
51
|
{
|
|
32
52
|
guidance: "dont",
|
|
33
53
|
description: "Do not use Sidebar for page-local settings, filters, or temporary action groups."
|
|
@@ -93,7 +113,7 @@ var sidebarMeta = {
|
|
|
93
113
|
{
|
|
94
114
|
name: "SidebarHeader",
|
|
95
115
|
type: "div props",
|
|
96
|
-
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."
|
|
116
|
+
description: "The top of the rail, at least 64px tall (the chrome bar's height inside a compact Workspace, so it lines up with the WorkspaceBar beside it). 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."
|
|
97
117
|
},
|
|
98
118
|
{
|
|
99
119
|
name: "SidebarBrand",
|
|
@@ -103,7 +123,17 @@ var sidebarMeta = {
|
|
|
103
123
|
{
|
|
104
124
|
name: "SidebarItem",
|
|
105
125
|
type: "{ label: ReactNode; icon?: ReactNode; active?: boolean; badge?: ReactNode; render?: useRender.RenderProp } + button props",
|
|
106
|
-
description: 'A single nav row. label is the row text (inline when expanded, in a hover tooltip when collapsed); icon is the leading icon node (the consumer swaps fill weight for the active state); active marks the current route (accent fill + aria-current="page"); badge is a trailing node (e.g. a count) pushed to the row end and hidden when collapsed; render composes another element, e.g. render={<Link to="/x" />}.'
|
|
126
|
+
description: 'A single nav row, 44px on a page and 32px inside a compact scope such as a Workspace (the rail reads the density; nothing to set). label is the row text (inline when expanded, in a hover tooltip when collapsed); icon is the leading icon node (the consumer swaps fill weight for the active state); active marks the current route (accent fill + aria-current="page"); badge is a trailing node (e.g. a count) pushed to the row end and hidden when collapsed; render composes another element, e.g. render={<Link to="/x" />}.'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "SidebarGroup",
|
|
130
|
+
type: "{ label?: ReactNode } + div props",
|
|
131
|
+
description: `A labelled run of SidebarItems inside SidebarNav ("Workspace", "Favorites"). The label starts on the items' icon column; collapsed, a hairline stands in for it and the label stays as the group's accessible name.`
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "SidebarAccount",
|
|
135
|
+
type: "{ avatar: ReactNode; label: ReactNode; detail?: ReactNode; indicator?: ReactNode | false; render?: useRender.RenderProp } + button props",
|
|
136
|
+
description: "The account row at the foot of the rail, inside SidebarFooter: the avatar in the icon slot (Avatar xs on a page rail, 2xs in a compact one), the label on the label column, one muted detail line, and an up-down chevron. Make it the trigger of the account Menu (render it through MenuTrigger render) and put Settings, appearance and sign-out there; it holds its fill while the menu is open. Collapsed, the avatar alone, with the label in a tooltip."
|
|
107
137
|
}
|
|
108
138
|
],
|
|
109
139
|
motion: [
|
|
@@ -165,6 +195,10 @@ var sidebarMeta = {
|
|
|
165
195
|
label: "Collapsible (icon rail)",
|
|
166
196
|
code: 'const [collapsed, setCollapsed] = useState(false)\n<Sidebar collapsed={collapsed}><SidebarNav><SidebarItem icon={<Icon name="home" />} label="Home" active render={<Link to="/" />} /></SidebarNav><SidebarFooter><Button variant="ghost" size="icon" onClick={() => setCollapsed((c) => !c)} aria-label="Toggle sidebar"><Icon name="chevron-left" /></Button></SidebarFooter></Sidebar>'
|
|
167
197
|
},
|
|
198
|
+
{
|
|
199
|
+
label: "Grouped nav with an account menu",
|
|
200
|
+
code: '<Sidebar><SidebarHeader><SidebarBrand mark={<Avatar shape="square" size="xs" fallback="F" alt="" />} label="Forma Studio" /></SidebarHeader><SidebarNav><SidebarItem icon={<Icon name="inbox" />} label="Inbox" badge={<span className="text-ui-num">4</span>} /><SidebarGroup label="Workspace"><SidebarItem icon={<Icon name="folder" />} label="Projects" active /><SidebarItem icon={<Icon name="users" />} label="Clients" /></SidebarGroup></SidebarNav><SidebarFooter><Menu><MenuTrigger render={<SidebarAccount avatar={<Avatar size="xs" fallback="EL" alt="" />} label="Emma Lee" detail="Forma Studio" />} /><MenuContent side="top" align="start"><MenuItem>Settings</MenuItem><MenuSeparator /><MenuItem>Sign out</MenuItem></MenuContent></Menu></SidebarFooter></Sidebar>'
|
|
201
|
+
},
|
|
168
202
|
{
|
|
169
203
|
label: "Inline (inside a bounded frame)",
|
|
170
204
|
code: '<div className="flex h-80 overflow-hidden rounded-lg border border-border"><Sidebar variant="inline"><SidebarNav><SidebarItem icon={<Icon name="home" />} label="Home" active /></SidebarNav></Sidebar><div className="flex-1 p-6">Content</div></div>'
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
// src/components/table/table.tsx
|
|
2
4
|
import { cn } from "../../lib/cn.js";
|
|
5
|
+
import { useDensity } from "../../lib/density.js";
|
|
3
6
|
import { jsx } from "react/jsx-runtime";
|
|
4
7
|
function Table({ className, ...props }) {
|
|
8
|
+
const tier = useDensity() === "compact" ? "text-ui-body" : "text-body-sm";
|
|
5
9
|
return (
|
|
6
10
|
// Thin scrollbar so a wide table's horizontal bar reads as part of the
|
|
7
11
|
// table rather than as chrome, and contained overscroll so swiping past
|
|
@@ -9,7 +13,7 @@ function Table({ className, ...props }) {
|
|
|
9
13
|
/* @__PURE__ */ jsx("div", { className: "w-full overflow-x-auto overscroll-x-contain scrollbar-quiet", children: /* @__PURE__ */ jsx(
|
|
10
14
|
"table",
|
|
11
15
|
{
|
|
12
|
-
className: cn("w-full caption-bottom
|
|
16
|
+
className: cn("w-full caption-bottom", tier, className),
|
|
13
17
|
...props
|
|
14
18
|
}
|
|
15
19
|
) })
|
|
@@ -66,7 +70,7 @@ function TableHead({ className, numeric = false, ...props }) {
|
|
|
66
70
|
"th",
|
|
67
71
|
{
|
|
68
72
|
className: cn(
|
|
69
|
-
"h-10 px-3 text-start align-middle text-label-md text-muted-foreground first:ps-[var(--table-edge-px,--spacing(3))] last:pe-[var(--table-edge-px,--spacing(3))]",
|
|
73
|
+
"h-[var(--table-head-h,--spacing(10))] px-3 text-start align-middle text-label-md text-muted-foreground first:ps-[var(--table-edge-px,--spacing(3))] last:pe-[var(--table-edge-px,--spacing(3))]",
|
|
70
74
|
numeric && NUMERIC,
|
|
71
75
|
className
|
|
72
76
|
),
|
|
@@ -80,7 +84,7 @@ function TableCell({ className, numeric = false, ...props }) {
|
|
|
80
84
|
"td",
|
|
81
85
|
{
|
|
82
86
|
className: cn(
|
|
83
|
-
"px-3 py-2.5 align-middle first:ps-[var(--table-edge-px,--spacing(3))] last:pe-[var(--table-edge-px,--spacing(3))]",
|
|
87
|
+
"px-3 py-[var(--table-cell-py,--spacing(2.5))] align-middle first:ps-[var(--table-edge-px,--spacing(3))] last:pe-[var(--table-edge-px,--spacing(3))]",
|
|
84
88
|
numeric && NUMERIC,
|
|
85
89
|
className
|
|
86
90
|
),
|
|
@@ -21,7 +21,15 @@ function TabsList({ className, variant = "underline", children, ...props }) {
|
|
|
21
21
|
{
|
|
22
22
|
"data-variant": variant,
|
|
23
23
|
className: cn(
|
|
24
|
-
variant === "underline" ? cn(
|
|
24
|
+
variant === "underline" ? cn(
|
|
25
|
+
"relative inline-flex items-center gap-1 border-b border-border",
|
|
26
|
+
// In a WorkspaceBar the bar's hairline IS the divider: the list
|
|
27
|
+
// fills the bar's height, drops its own rule (which stopped
|
|
28
|
+
// after the last tab, a second line above the bar's), and the
|
|
29
|
+
// mark lands on the bar's edge.
|
|
30
|
+
"[[data-workspace-bar]_&]:self-stretch [[data-workspace-bar]_&]:border-b-0",
|
|
31
|
+
UNDERLINE_HANDOFF
|
|
32
|
+
) : cn("relative inline-flex items-center gap-1", PILL_HANDOFF),
|
|
25
33
|
className
|
|
26
34
|
),
|
|
27
35
|
...props,
|
|
@@ -48,7 +56,7 @@ function TabsTrigger({ className, children, ...props }) {
|
|
|
48
56
|
variant === "underline" ? (
|
|
49
57
|
// On the control ladder like the pill variant (it was py-2 plus
|
|
50
58
|
// the label's line, ~36px — no rung).
|
|
51
|
-
"-mb-px inline-flex h-(--control-md) items-center gap-(--control-gap) border-b-2 border-transparent px-3 data-[active]:border-foreground"
|
|
59
|
+
"-mb-px inline-flex h-(--control-md) items-center gap-(--control-gap) border-b-2 border-transparent px-3 data-[active]:border-foreground [[data-workspace-bar]_&]:h-full"
|
|
52
60
|
) : "inline-flex h-(--control-sm) items-center gap-(--control-gap) rounded-[var(--button-radius,9999px)] px-3 data-[active]:bg-muted",
|
|
53
61
|
className
|
|
54
62
|
),
|
|
@@ -16,6 +16,10 @@ var tabsMeta = {
|
|
|
16
16
|
guidance: "do",
|
|
17
17
|
description: "Use tabs when each panel is peer content and users may compare or switch often."
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
guidance: "do",
|
|
21
|
+
description: `In tool chrome (a WorkspaceBar, a compact panel header) use variant="pill" for a view switch such as Overview / Issues / Updates: it reads as a control beside the bar's other controls. Underline is the page-section idiom; inside a WorkspaceBar it sits on the bar's hairline rather than drawing its own.`
|
|
22
|
+
},
|
|
19
23
|
{
|
|
20
24
|
guidance: "do",
|
|
21
25
|
description: "Keep tab labels short and make the active tab unmistakable."
|
|
@@ -7,6 +7,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
function Workspace({
|
|
8
8
|
toolbar,
|
|
9
9
|
statusBar,
|
|
10
|
+
bottomNav,
|
|
10
11
|
density = "compact",
|
|
11
12
|
skipLinkLabel = "Skip to canvas",
|
|
12
13
|
className,
|
|
@@ -24,7 +25,7 @@ function Workspace({
|
|
|
24
25
|
{
|
|
25
26
|
"data-density": density,
|
|
26
27
|
className: cn(
|
|
27
|
-
"
|
|
28
|
+
"flex h-dvh flex-col overflow-hidden bg-canvas text-canvas-foreground",
|
|
28
29
|
className
|
|
29
30
|
),
|
|
30
31
|
...props,
|
|
@@ -38,8 +39,16 @@ function Workspace({
|
|
|
38
39
|
}
|
|
39
40
|
),
|
|
40
41
|
toolbar,
|
|
41
|
-
/* @__PURE__ */ jsx("div", { className: "grid min-h-0 grid-cols-1", children }),
|
|
42
|
-
statusBar
|
|
42
|
+
/* @__PURE__ */ jsx("div", { "data-workspace-body": "", className: "grid min-h-0 flex-1 grid-cols-1 grid-rows-[minmax(0,1fr)]", children }),
|
|
43
|
+
statusBar,
|
|
44
|
+
bottomNav ? /* @__PURE__ */ jsx(
|
|
45
|
+
"div",
|
|
46
|
+
{
|
|
47
|
+
"data-workspace-bottom-nav": "",
|
|
48
|
+
className: "h-[calc(var(--nav-height,56px)+env(safe-area-inset-bottom,0px))] shrink-0 lg:hidden",
|
|
49
|
+
children: bottomNav
|
|
50
|
+
}
|
|
51
|
+
) : null
|
|
43
52
|
]
|
|
44
53
|
}
|
|
45
54
|
) })
|
|
@@ -62,6 +71,7 @@ function WorkspaceBar({ className, children, ...props }) {
|
|
|
62
71
|
return /* @__PURE__ */ jsx(
|
|
63
72
|
"header",
|
|
64
73
|
{
|
|
74
|
+
"data-workspace-bar": "",
|
|
65
75
|
className: cn(
|
|
66
76
|
"flex h-(--chrome-bar-height) shrink-0 items-center gap-1 border-b border-hairline bg-panel px-2",
|
|
67
77
|
className
|
|
@@ -12,7 +12,18 @@ var workspaceMeta = {
|
|
|
12
12
|
"chrome",
|
|
13
13
|
"panels",
|
|
14
14
|
"density",
|
|
15
|
-
"viewport"
|
|
15
|
+
"viewport",
|
|
16
|
+
"issue tracker",
|
|
17
|
+
"tracker",
|
|
18
|
+
"inspector",
|
|
19
|
+
"reading view",
|
|
20
|
+
"mobile",
|
|
21
|
+
"responsive",
|
|
22
|
+
"bottom nav",
|
|
23
|
+
"media query",
|
|
24
|
+
"breakpoint",
|
|
25
|
+
"top bar",
|
|
26
|
+
"topbar"
|
|
16
27
|
],
|
|
17
28
|
usage: {
|
|
18
29
|
description: 'Use Workspace as the outer frame for a tool \u2014 an editor, a console, a canvas app. It owns the viewport, scopes the compact control density, and guarantees a skip link plus a main landmark on the canvas. Reach for AppShell instead for page-register products: it is built around a scrolling document, where a workspace is built around a fixed one. Workspace fills the viewport (h-dvh) by design; to embed one in a page (a docs preview, a modal-sized demo), wrap it in a bounded container and pass className="h-full".',
|
|
@@ -29,6 +40,18 @@ var workspaceMeta = {
|
|
|
29
40
|
guidance: "do",
|
|
30
41
|
description: "Keep the density scope at compact unless you have a specific reason; the workspace register's measured control ladder is what makes tool chrome read as tool chrome."
|
|
31
42
|
},
|
|
43
|
+
{
|
|
44
|
+
guidance: "do",
|
|
45
|
+
description: "Put a WorkspaceBar inside a column when the bar belongs to that column only. An issue tracker's bar (breadcrumb, view tabs, filters) sits above the list, while the sidebar and the inspector beside it run the full height: put the WorkspaceBar first inside the WorkspaceCanvas, not in the `toolbar` slot, which spans every panel. Don't hand-build a fixed-height header for it."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
guidance: "do",
|
|
49
|
+
description: 'Wrap a reading view inside the workspace (a project overview, an update, a doc, release notes) in <Density value="comfortable">. The compact scope maps body text to the 11\u201312px UI tiers, which is right for chrome and too small for paragraphs; the comfortable scope gives prose its page size again while the chrome around it stays compact.'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
guidance: "do",
|
|
53
|
+
description: 'Plan the phone layout with the shell rather than around it: pass a BottomNav to `bottomNav`, and give docked Panels `narrow="sheet"` (an inspector, a navigation rail) or `narrow="hidden"`. Don\'t measure the viewport with a resize listener, render the inspector twice, or subtract the nav\'s height from 100dvh.'
|
|
54
|
+
},
|
|
32
55
|
{
|
|
33
56
|
guidance: "dont",
|
|
34
57
|
description: "Nest a Region or Page inside it to set a width. Region caps are a page-register idea; a workspace is edge-to-edge and its panels are fixed-width chrome outside the content grid."
|
|
@@ -66,6 +89,11 @@ var workspaceMeta = {
|
|
|
66
89
|
name: "StatusBar",
|
|
67
90
|
required: false,
|
|
68
91
|
description: "Bottom chrome for ambient document facts."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "BottomNav",
|
|
95
|
+
required: false,
|
|
96
|
+
description: "Phone navigation in the `bottomNav` slot. Its row is reserved below lg and gone above it, so the body ends where the nav begins."
|
|
69
97
|
}
|
|
70
98
|
]
|
|
71
99
|
},
|
|
@@ -80,6 +108,11 @@ var workspaceMeta = {
|
|
|
80
108
|
description: "Top chrome, normally a WorkspaceBar."
|
|
81
109
|
},
|
|
82
110
|
{ name: "statusBar", type: "ReactNode", description: "Bottom chrome, normally a StatusBar." },
|
|
111
|
+
{
|
|
112
|
+
name: "bottomNav",
|
|
113
|
+
type: "ReactNode",
|
|
114
|
+
description: "Phone navigation, normally a BottomNav. Below lg (BottomNav's own breakpoint) the shell reserves the nav's height as its last row, so the body and the status bar end above the nav instead of under it; above lg the row is gone. Every slot is optional: the body is the shell's one growing row whichever of toolbar, statusBar and bottomNav are present."
|
|
115
|
+
},
|
|
83
116
|
{
|
|
84
117
|
name: "density",
|
|
85
118
|
type: '"compact" | "comfortable"',
|
|
@@ -103,6 +136,16 @@ var workspaceMeta = {
|
|
|
103
136
|
name: "WorkspaceCanvas",
|
|
104
137
|
type: "ComponentProps<'main'>",
|
|
105
138
|
description: "The work surface: renders main#main-content, fills the body, clips its overflow."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "useMediaQuery",
|
|
142
|
+
type: "(query: string, options?: { serverValue?: boolean }) => boolean",
|
|
143
|
+
description: "True while a media query matches, read through useSyncExternalStore: no resize listener, no setState in an effect, no hydration mismatch (the server reports serverValue, default false). Use it to branch structure a class cannot express; style-only changes stay as lg: classes."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "useBelowBreakpoint",
|
|
147
|
+
type: '(breakpoint?: "sm" | "md" | "lg" | "xl" | "2xl", options?: { serverValue?: boolean }) => boolean',
|
|
148
|
+
description: "True below a named breakpoint (default lg), flipping at the width where that breakpoint's classes start to apply (Tailwind's sm 40 / md 48 / lg 64 / xl 80 / 2xl 96rem). Panel's `narrow` uses it; reach for it instead of a hand-rolled window.innerWidth check."
|
|
106
149
|
}
|
|
107
150
|
],
|
|
108
151
|
a11y: [
|
|
@@ -164,6 +207,71 @@ var workspaceMeta = {
|
|
|
164
207
|
<div className="p-3 text-ui-num text-muted-foreground">log output\u2026</div>
|
|
165
208
|
</WorkspaceCanvas>
|
|
166
209
|
</PanelGroup>
|
|
210
|
+
</Workspace>`
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
label: "Issue tracker: column bar, docked inspector, phone sheet",
|
|
214
|
+
code: `function Tracker() {
|
|
215
|
+
const [selected, setSelected] = useState<string | null>("AES-104")
|
|
216
|
+
return (
|
|
217
|
+
<Workspace
|
|
218
|
+
bottomNav={
|
|
219
|
+
<BottomNav>
|
|
220
|
+
<BottomNavItem showLabel label="Issues" icon={<Icon name="list" />} active />
|
|
221
|
+
<BottomNavItem showLabel label="Inbox" icon={<Icon name="inbox" />} />
|
|
222
|
+
</BottomNav>
|
|
223
|
+
}
|
|
224
|
+
statusBar={<StatusBar><span>12 issues</span></StatusBar>}
|
|
225
|
+
>
|
|
226
|
+
<PanelGroup>
|
|
227
|
+
<Panel label="Navigation" width={220} narrow="hidden">
|
|
228
|
+
<PanelSection title="Workspace">
|
|
229
|
+
<PropertyRow label="Team"><Input defaultValue="Studio" /></PropertyRow>
|
|
230
|
+
</PanelSection>
|
|
231
|
+
</Panel>
|
|
232
|
+
<WorkspaceCanvas className="flex flex-col">
|
|
233
|
+
<WorkspaceBar>
|
|
234
|
+
<Breadcrumbs>
|
|
235
|
+
<BreadcrumbItem><BreadcrumbLink href="#">Studio</BreadcrumbLink></BreadcrumbItem>
|
|
236
|
+
<BreadcrumbSeparator />
|
|
237
|
+
<BreadcrumbItem><BreadcrumbPage>Digital flagship</BreadcrumbPage></BreadcrumbItem>
|
|
238
|
+
</Breadcrumbs>
|
|
239
|
+
</WorkspaceBar>
|
|
240
|
+
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-quiet">
|
|
241
|
+
<Button size="xs" variant="ghost" onClick={() => setSelected("AES-104")}>AES-104 Refine product page</Button>
|
|
242
|
+
</div>
|
|
243
|
+
</WorkspaceCanvas>
|
|
244
|
+
<Panel
|
|
245
|
+
label="Issue details"
|
|
246
|
+
width={320}
|
|
247
|
+
narrow="sheet"
|
|
248
|
+
open={selected !== null}
|
|
249
|
+
onOpenChange={(open) => !open && setSelected(null)}
|
|
250
|
+
>
|
|
251
|
+
<PanelSection title={selected ?? ""}>
|
|
252
|
+
<PropertyRow label="Status"><Input defaultValue="In progress" /></PropertyRow>
|
|
253
|
+
</PanelSection>
|
|
254
|
+
</Panel>
|
|
255
|
+
</PanelGroup>
|
|
256
|
+
</Workspace>
|
|
257
|
+
)
|
|
258
|
+
}`
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
label: "Reading view inside a workspace",
|
|
262
|
+
code: `<Workspace>
|
|
263
|
+
<PanelGroup>
|
|
264
|
+
<WorkspaceCanvas className="overflow-y-auto scrollbar-quiet">
|
|
265
|
+
<Density value="comfortable">
|
|
266
|
+
<article className="max-w-prose p-6">
|
|
267
|
+
<h2 className="text-heading-3">Project overview</h2>
|
|
268
|
+
<p className="mt-3 text-body-md text-muted-foreground">
|
|
269
|
+
Paragraphs keep their page size here, while the chrome around the canvas stays compact.
|
|
270
|
+
</p>
|
|
271
|
+
</article>
|
|
272
|
+
</Density>
|
|
273
|
+
</WorkspaceCanvas>
|
|
274
|
+
</PanelGroup>
|
|
167
275
|
</Workspace>`
|
|
168
276
|
},
|
|
169
277
|
{
|