@sableui/react 0.11.0 → 0.13.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 -4
- package/CHANGELOG.md +199 -0
- package/DESIGN.md +3 -3
- package/README.md +15 -1
- package/bin/sable.mjs +254 -10
- package/dist/components/app-header/app-header.meta.js +2 -1
- package/dist/components/button/button.js +3 -0
- package/dist/components/button/button.meta.js +1 -1
- package/dist/components/button/button.variants.js +1 -1
- package/dist/components/button-group/button-group.js +17 -0
- package/dist/components/button-group/button-group.meta.js +72 -0
- package/dist/components/button-group/index.js +7 -0
- package/dist/components/caret-menu/caret-menu.js +221 -0
- package/dist/components/caret-menu/caret-menu.meta.js +119 -0
- package/dist/components/caret-menu/index.js +8 -0
- package/dist/components/chat-composer/chat-composer.js +30 -7
- package/dist/components/chat-composer/chat-composer.meta.js +27 -8
- package/dist/components/combobox/combobox.meta.js +26 -0
- package/dist/components/input-group/index.js +7 -0
- package/dist/components/input-group/input-group.js +66 -0
- package/dist/components/input-group/input-group.meta.js +106 -0
- package/dist/components/menu/index.js +2 -0
- package/dist/components/menu/menu.js +51 -9
- package/dist/components/menu/menu.meta.js +11 -2
- package/dist/components/menubar/index.js +8 -0
- package/dist/components/menubar/menubar.js +42 -0
- package/dist/components/menubar/menubar.meta.js +99 -0
- package/dist/components/message-scroller/message-scroller.meta.js +4 -0
- package/dist/components/navigation-menu/index.js +19 -0
- package/dist/components/navigation-menu/navigation-menu.js +108 -0
- package/dist/components/navigation-menu/navigation-menu.meta.js +141 -0
- package/dist/components/overflow-menu/overflow-menu.meta.js +13 -1
- package/dist/components/pagination/pagination.js +11 -5
- package/dist/components/pagination/pagination.meta.js +21 -3
- package/dist/components/scroller/scroller.js +8 -4
- package/dist/components/scroller/scroller.meta.js +6 -0
- package/dist/components/sheet/sheet.js +82 -9
- package/dist/components/sheet/sheet.meta.js +41 -4
- package/dist/components/sheet/sheet.variants.js +8 -1
- package/dist/components/sidebar/sidebar.js +13 -2
- package/dist/components/sidebar/sidebar.meta.js +10 -0
- package/dist/components/streaming-text/streaming-text.meta.js +5 -1
- package/dist/file-hashes.json +49 -33
- package/dist/icon-sets/reicon.js +2 -0
- package/dist/index.d.ts +660 -164
- package/dist/index.js +12 -1
- package/dist/lib/palette.js +46 -6
- package/dist/lib/taxonomy.js +5 -0
- package/dist/lib/use-caret-trigger.js +187 -0
- package/dist/lib/use-copy.js +6 -1
- package/dist/lib/use-smooth-wheel.js +164 -0
- package/dist/palette.css +243 -165
- package/dist/patterns/chat-panel.js +1 -1
- package/dist/patterns/chat-panel.meta.js +1 -0
- package/dist/styles.css +25 -0
- package/llms/components/app-header.md +1 -1
- package/llms/components/button-group.md +55 -0
- package/llms/components/button.md +1 -1
- package/llms/components/caret-menu.md +100 -0
- package/llms/components/chat-composer.md +24 -7
- package/llms/components/combobox.md +12 -0
- package/llms/components/input-group.md +78 -0
- package/llms/components/menu.md +16 -1
- package/llms/components/menubar.md +110 -0
- package/llms/components/message-scroller.md +1 -0
- package/llms/components/navigation-menu.md +113 -0
- package/llms/components/overflow-menu.md +26 -1
- package/llms/components/pagination.md +15 -3
- package/llms/components/scroller.md +1 -0
- package/llms/components/sheet.md +18 -3
- package/llms/components/sidebar.md +2 -0
- package/llms/components/streaming-text.md +2 -1
- package/llms/patterns/chat-panel.md +4 -1
- package/llms/reference/token-intent.md +1 -1
- package/llms/tasks/building-a-chat.md +158 -0
- package/llms/tasks/building-with-sable.md +4 -1
- package/llms/tasks/converting-an-app.md +7 -0
- package/llms/tasks/extending-a-component.md +170 -0
- package/llms/tasks/getting-started.md +22 -4
- package/llms/tasks/migrating-from-shadcn.md +351 -0
- package/llms-full.txt +1322 -27
- package/llms.txt +13 -4
- package/package.json +3 -2
- package/registry.json +979 -23
- package/scripts/lint-rules.mjs +35 -0
- package/scripts/wiring.mjs +541 -0
- package/skills/entry/SKILL.md +3 -2
- package/skills/sable/SKILL.md +26 -5
- package/targets/ios.json +45 -0
- package/dist/lib/use-wheel-to-horizontal.js +0 -65
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/components/navigation-menu/navigation-menu.meta.ts
|
|
2
|
+
var navigationMenuMeta = {
|
|
3
|
+
name: "NavigationMenu",
|
|
4
|
+
summary: "A site header's primary nav over Base UI NavigationMenu: a bar of links and triggers, where a trigger opens a panel of links on hover, click or Enter. One shared panel glides between triggers and resizes to each one's content. Links take any router's link through `render` and mark the current page with `active`.",
|
|
5
|
+
status: "stable",
|
|
6
|
+
keywords: ["navigation", "nav", "header nav", "site nav", "mega menu", "top nav", "navbar", "dropdown nav"],
|
|
7
|
+
usage: {
|
|
8
|
+
description: "Use NavigationMenu for a marketing or docs site's header: a few top-level destinations, some of which open a panel of related pages. Use Menu for actions rather than destinations, Tabs to switch views inside a page, Sidebar or SideNav for an app's persistent navigation, and AppHeader for an app's mobile top bar.",
|
|
9
|
+
bestPractices: [
|
|
10
|
+
{
|
|
11
|
+
guidance: "do",
|
|
12
|
+
description: "Keep the bar to a handful of entries. A trigger groups related pages; a destination with nothing beneath it is a plain NavigationMenuLink in its own item."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
guidance: "do",
|
|
16
|
+
description: 'Give panel links a `description` when titles alone are ambiguous ("Sync" \u2192 "Offline first, conflict free"), and lay the Content out with layout classes (`grid w-[30rem] grid-cols-2`); the shared panel sizes itself to it.'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
guidance: "do",
|
|
20
|
+
description: 'Pass your router\'s link through `render` (`<NavigationMenuLink render={<Link to="/pricing" />}>`) and set `active` on the current page\'s link, so the bar shows where the reader is and announces it as `aria-current="page"`.'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
guidance: "do",
|
|
24
|
+
description: 'Below `md`, hide the bar and offer the same destinations in a Sheet opened from a menu Button: SheetContent side="left" holding a SideNav, its groups mirroring the panels. A hover panel has nothing to hover on a phone, and a bar of five entries does not fit one.'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
guidance: "dont",
|
|
28
|
+
description: "Do not put actions in it (Sign in, New project). Those are Buttons beside the nav; the nav holds places to go."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
guidance: "dont",
|
|
32
|
+
description: "Do not assemble the panel yourself. The root renders the one Portal, Positioner, Popup and Viewport every trigger shares; a second one breaks the glide between triggers."
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
anatomy: [
|
|
36
|
+
{ name: "NavigationMenu", required: true, description: "The `<nav>` root; also renders the shared floating panel." },
|
|
37
|
+
{ name: "NavigationMenuList", required: true, description: "The bar: a `<ul>` the arrow keys rove across." },
|
|
38
|
+
{ name: "NavigationMenuItem", required: true, description: "One `<li>`: a link, or a trigger with its content." },
|
|
39
|
+
{ name: "NavigationMenuTrigger", required: false, description: "Opens its item's content; carries a chevron." },
|
|
40
|
+
{ name: "NavigationMenuContent", required: false, description: "The links a trigger reveals, rendered into the shared panel." },
|
|
41
|
+
{ name: "NavigationMenuLink", required: true, description: "A destination: a bar entry in an item, a two-line row in content." }
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
related: {
|
|
45
|
+
components: ["Menu", "SideNav", "Sheet", "AppHeader", "Tabs"]
|
|
46
|
+
},
|
|
47
|
+
props: [
|
|
48
|
+
{
|
|
49
|
+
name: "NavigationMenu (Root)",
|
|
50
|
+
type: "Base UI NavigationMenu.Root props (value, defaultValue, onValueChange, delay, closeDelay, orientation) + className",
|
|
51
|
+
default: "\u2014",
|
|
52
|
+
description: "The `<nav>`. `value` is the open item's value (null when closed); `delay` / `closeDelay` tune hover-to-open."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "align",
|
|
56
|
+
type: '"start" | "center" | "end"',
|
|
57
|
+
default: '"start"',
|
|
58
|
+
description: "Alignment of the shared panel along its trigger."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "sideOffset",
|
|
62
|
+
type: "number",
|
|
63
|
+
default: "8",
|
|
64
|
+
description: "Gap between the bar and the panel, in px. The pointer crosses it on a bridge, so the panel does not close on the way in."
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
subcomponents: [
|
|
68
|
+
{
|
|
69
|
+
name: "NavigationMenuList",
|
|
70
|
+
type: "Base UI NavigationMenu.List props + className",
|
|
71
|
+
description: "The bar of items, a `<ul>`. Arrow keys move between entries."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "NavigationMenuItem",
|
|
75
|
+
type: "Base UI NavigationMenu.Item props + className + value?: string",
|
|
76
|
+
description: "One `<li>` holding either a NavigationMenuLink or a NavigationMenuTrigger with its NavigationMenuContent."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "NavigationMenuTrigger",
|
|
80
|
+
type: "Base UI NavigationMenu.Trigger props + className",
|
|
81
|
+
description: "Opens its item's content on hover, click or Enter, with a chevron that turns while open. Styled like the bar's links: muted at rest, full ink when hovered or open."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "NavigationMenuContent",
|
|
85
|
+
type: "Base UI NavigationMenu.Content props + className + keepMounted?: boolean",
|
|
86
|
+
description: "The item's panel content. Size and lay it out with layout classes; the shared panel follows its size and slides it in from the side it was reached from."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "NavigationMenuLink",
|
|
90
|
+
type: "Base UI NavigationMenu.Link props + className + href?: string + active?: boolean + closeOnClick?: boolean + render?: ReactElement + description?: ReactNode",
|
|
91
|
+
description: 'A destination. In an item it is a bar entry; in content it is a row with an optional `description` line. `active` sets `aria-current="page"`; `render={<Link to="\u2026" />}` keeps the roving focus on your router\'s link.'
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
tokensUsed: ["popover", "popover-foreground", "border-floating", "accent", "accent-foreground", "muted-foreground", "foreground", "ring"],
|
|
95
|
+
motion: [
|
|
96
|
+
{
|
|
97
|
+
slot: "arrival",
|
|
98
|
+
recipe: "custom",
|
|
99
|
+
part: "Panel",
|
|
100
|
+
why: "The panel fades and scales in from its trigger like a popover, but it also resizes as the reader moves between triggers, which motion-pop cannot express: Base UI measures each content into --popup-width/--popup-height and the popup transitions to them on the motion tokens."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
slot: "transition",
|
|
104
|
+
recipe: "custom",
|
|
105
|
+
part: "Panel",
|
|
106
|
+
why: "Moving from one trigger to the next keeps one panel open: it glides along the bar and the new content slides in from the direction of travel, so the reader sees one surface changing rather than two popups swapping. Snaps on first open and on resize (data-instant), where a glide would mean nothing."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
slot: "response",
|
|
110
|
+
recipe: "motion-interactive",
|
|
111
|
+
part: "Trigger, Link",
|
|
112
|
+
why: "Bar entries and panel rows fill on hover and focus like the other interactive surfaces, so the bar reads as live before anything opens."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
slot: "activity",
|
|
116
|
+
recipe: "none",
|
|
117
|
+
part: "root",
|
|
118
|
+
why: "Nothing in a nav moves on its own."
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
a11y: [
|
|
122
|
+
'The root is a <nav>; give it an aria-label when the page has more than one nav ("Main").',
|
|
123
|
+
"Arrow keys move across the bar; Enter, Space or ArrowDown opens a trigger's content and moves focus into it; Esc closes and returns focus to the trigger.",
|
|
124
|
+
"A trigger is a button with aria-expanded; its content is reachable by Tab in reading order once open.",
|
|
125
|
+
'`active` on a link sets aria-current="page" and gives it full ink, so the current page is announced as well as shown.',
|
|
126
|
+
'Panel motion runs on the motion tokens, so reduced motion (or MotionProvider "reduced") collapses the glide and resize to instant.'
|
|
127
|
+
],
|
|
128
|
+
examples: [
|
|
129
|
+
{
|
|
130
|
+
label: "Bar with a panel",
|
|
131
|
+
code: '<NavigationMenu aria-label="Main">\n <NavigationMenuList>\n <NavigationMenuItem>\n <NavigationMenuTrigger>Product</NavigationMenuTrigger>\n <NavigationMenuContent className="grid w-[30rem] grid-cols-2">\n <NavigationMenuLink href="/editor" description="Write and review together.">Editor</NavigationMenuLink>\n <NavigationMenuLink href="/sync" description="Offline first, conflict free.">Sync</NavigationMenuLink>\n </NavigationMenuContent>\n </NavigationMenuItem>\n <NavigationMenuItem>\n <NavigationMenuLink href="/pricing">Pricing</NavigationMenuLink>\n </NavigationMenuItem>\n </NavigationMenuList>\n</NavigationMenu>'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
label: "With a router, marking the current page",
|
|
135
|
+
code: '<NavigationMenu aria-label="Main">\n <NavigationMenuList>\n <NavigationMenuItem>\n <NavigationMenuLink active={path === "/docs"} render={<Link to="/docs" />}>Docs</NavigationMenuLink>\n </NavigationMenuItem>\n <NavigationMenuItem>\n <NavigationMenuLink active={path === "/blog"} render={<Link to="/blog" />}>Blog</NavigationMenuLink>\n </NavigationMenuItem>\n </NavigationMenuList>\n</NavigationMenu>'
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
export {
|
|
140
|
+
navigationMenuMeta
|
|
141
|
+
};
|
|
@@ -7,7 +7,11 @@ var overflowMenuMeta = {
|
|
|
7
7
|
"dots menu",
|
|
8
8
|
"more actions",
|
|
9
9
|
"ellipsis",
|
|
10
|
-
"kebab"
|
|
10
|
+
"kebab",
|
|
11
|
+
"responsive actions",
|
|
12
|
+
"responsive action menu",
|
|
13
|
+
"collapse actions",
|
|
14
|
+
"actions overflow on mobile"
|
|
11
15
|
],
|
|
12
16
|
usage: {
|
|
13
17
|
description: "Use OverflowMenu for compact overflow actions when the primary action is already visible elsewhere.",
|
|
@@ -20,6 +24,10 @@ var overflowMenuMeta = {
|
|
|
20
24
|
guidance: "do",
|
|
21
25
|
description: "Keep labels explicit so the icon-only trigger does not obscure the command meaning."
|
|
22
26
|
},
|
|
27
|
+
{
|
|
28
|
+
guidance: "do",
|
|
29
|
+
description: "For actions that fit inline on a wide screen but not a narrow one, render them twice: inline (a Row inside a wrapper shown from a container width; Row sets its own display, so the wrapper does the hiding), and as MenuItems in an OverflowMenu hidden from that width (`@container` + `@md:`). display:none takes the hidden copy out of the accessibility tree, so only one set is ever announced. Keep the primary action inline at every width."
|
|
30
|
+
},
|
|
23
31
|
{
|
|
24
32
|
guidance: "dont",
|
|
25
33
|
description: "Do not hide the most important or most frequent action inside OverflowMenu."
|
|
@@ -92,6 +100,10 @@ var overflowMenuMeta = {
|
|
|
92
100
|
{
|
|
93
101
|
label: "With leading icons",
|
|
94
102
|
code: '<OverflowMenu><MenuItem><Icon name="check" className="text-sm" />Mark as read</MenuItem><MenuSeparator /><MenuItem variant="destructive"><Icon name="xmark" className="text-sm" />Delete</MenuItem></OverflowMenu>'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
label: "Responsive actions: inline when wide, in a menu when narrow",
|
|
106
|
+
code: '<div className="@container/actions flex w-full justify-end">\n <Row gap={1} align="center">\n <div className="hidden @md/actions:block">\n <Row gap={1}>\n <Button variant="ghost" size="sm" iconStart={<Icon name="share" />}>Share</Button>\n <Button variant="ghost" size="sm" iconStart={<Icon name="download" />}>Export</Button>\n </Row>\n </div>\n <div className="@md/actions:hidden">\n <OverflowMenu label="Report actions" size="icon-sm">\n <MenuItem icon={<Icon name="share" />}>Share</MenuItem>\n <MenuItem icon={<Icon name="download" />}>Export</MenuItem>\n </OverflowMenu>\n </div>\n <Button size="sm" iconStart={<Icon name="edit" />}>Edit report</Button>\n </Row>\n</div>'
|
|
95
107
|
}
|
|
96
108
|
]
|
|
97
109
|
};
|
|
@@ -29,6 +29,7 @@ function Pagination({
|
|
|
29
29
|
page,
|
|
30
30
|
total,
|
|
31
31
|
onPageChange,
|
|
32
|
+
pageLink,
|
|
32
33
|
siblingCount = 1,
|
|
33
34
|
label = "Pagination",
|
|
34
35
|
className,
|
|
@@ -38,10 +39,12 @@ function Pagination({
|
|
|
38
39
|
const safeTotal = Math.max(total, 1);
|
|
39
40
|
const current = Math.min(Math.max(page, 1), safeTotal);
|
|
40
41
|
const items = buildRange(current, safeTotal, siblingCount);
|
|
41
|
-
const goTo = (next) => {
|
|
42
|
+
const goTo = (next, event) => {
|
|
42
43
|
if (next < 1 || next > safeTotal || next === current) return;
|
|
43
|
-
|
|
44
|
+
if (pageLink && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) return;
|
|
45
|
+
onPageChange?.(next);
|
|
44
46
|
};
|
|
47
|
+
const linkTo = (next) => pageLink && next >= 1 && next <= safeTotal ? pageLink(next) : void 0;
|
|
45
48
|
return /* @__PURE__ */ jsxs(
|
|
46
49
|
"nav",
|
|
47
50
|
{
|
|
@@ -57,7 +60,8 @@ function Pagination({
|
|
|
57
60
|
size: "icon-sm",
|
|
58
61
|
"aria-label": "Previous page",
|
|
59
62
|
disabled: current <= 1,
|
|
60
|
-
|
|
63
|
+
render: linkTo(current - 1),
|
|
64
|
+
onClick: (event) => goTo(current - 1, event),
|
|
61
65
|
children: /* @__PURE__ */ jsx(Icon, { name: "chevron-left", className: "text-base" })
|
|
62
66
|
}
|
|
63
67
|
),
|
|
@@ -78,7 +82,8 @@ function Pagination({
|
|
|
78
82
|
className: "w-auto min-w-(--control-sm) px-1 tabular-nums",
|
|
79
83
|
"aria-label": `Page ${item}`,
|
|
80
84
|
"aria-current": item === current ? "page" : void 0,
|
|
81
|
-
|
|
85
|
+
render: linkTo(item),
|
|
86
|
+
onClick: (event) => goTo(item, event),
|
|
82
87
|
children: item
|
|
83
88
|
}
|
|
84
89
|
) }, item)
|
|
@@ -90,7 +95,8 @@ function Pagination({
|
|
|
90
95
|
size: "icon-sm",
|
|
91
96
|
"aria-label": "Next page",
|
|
92
97
|
disabled: current >= safeTotal,
|
|
93
|
-
|
|
98
|
+
render: linkTo(current + 1),
|
|
99
|
+
onClick: (event) => goTo(current + 1, event),
|
|
94
100
|
children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", className: "text-base" })
|
|
95
101
|
}
|
|
96
102
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/components/pagination/pagination.meta.ts
|
|
2
2
|
var paginationMeta = {
|
|
3
3
|
name: "Pagination",
|
|
4
|
-
summary: "Page navigation built from our own Button + Icon (no Base UI). A <nav aria-label='Pagination'> with prev/next icon buttons that disable at the bounds and numbered page buttons; the current page is a secondary button marked aria-current. Collapses to first/last/current plus a sibling window, bridging skipped runs with an ellipsis.",
|
|
4
|
+
summary: "Page navigation built from our own Button + Icon (no Base UI). A <nav aria-label='Pagination'> with prev/next icon buttons that disable at the bounds and numbered page buttons; the current page is a secondary button marked aria-current. With pageLink every control is a real link (crawlable, opens in a new tab, any router through render). Collapses to first/last/current plus a sibling window, bridging skipped runs with an ellipsis.",
|
|
5
5
|
keywords: [
|
|
6
6
|
"pagination",
|
|
7
7
|
"pager",
|
|
@@ -10,7 +10,10 @@ var paginationMeta = {
|
|
|
10
10
|
"results",
|
|
11
11
|
"navigation",
|
|
12
12
|
"next",
|
|
13
|
-
"previous"
|
|
13
|
+
"previous",
|
|
14
|
+
"page links",
|
|
15
|
+
"seo",
|
|
16
|
+
"url"
|
|
14
17
|
],
|
|
15
18
|
usage: {
|
|
16
19
|
description: "Use Pagination when a result set is split into discrete pages and the user needs predictable movement through them. Pair it with DataTable, list results, or server-paged views; use a load-more Button or infinite scroll when page numbers are not meaningful.",
|
|
@@ -23,6 +26,10 @@ var paginationMeta = {
|
|
|
23
26
|
guidance: "do",
|
|
24
27
|
description: 'Use a specific label when multiple pagers appear on a page, such as "Customer pages".'
|
|
25
28
|
},
|
|
29
|
+
{
|
|
30
|
+
guidance: "do",
|
|
31
|
+
description: "Pass pageLink when the page lives in the URL (search results, a blog, a catalogue): each page becomes a link a crawler can follow and a reader can open in a new tab. Use a router's Link there so navigation stays in the app."
|
|
32
|
+
},
|
|
26
33
|
{
|
|
27
34
|
guidance: "do",
|
|
28
35
|
description: "Reset to page 1 when filters or search terms materially change the result set."
|
|
@@ -81,7 +88,13 @@ var paginationMeta = {
|
|
|
81
88
|
name: "onPageChange",
|
|
82
89
|
type: "(page: number) => void",
|
|
83
90
|
default: "\u2014",
|
|
84
|
-
description: "Called with the next page when a control is activated. No-op for out-of-range or unchanged values."
|
|
91
|
+
description: "Called with the next page when a control is activated. No-op for out-of-range or unchanged values. Optional with pageLink, where the URL is the page state."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "pageLink",
|
|
95
|
+
type: "(page: number) => ReactElement",
|
|
96
|
+
default: "\u2014",
|
|
97
|
+
description: "Render each page control as a link: return the element for a page, (page) => <a href={`?page=${page}`} /> or a router's <Link to={`?page=${page}`} />. The current page keeps aria-current; previous/next at the bounds stay disabled buttons. onPageChange becomes optional and, if set, still fires on click."
|
|
85
98
|
},
|
|
86
99
|
{
|
|
87
100
|
name: "siblingCount",
|
|
@@ -115,6 +128,7 @@ var paginationMeta = {
|
|
|
115
128
|
"Renders a landmark <nav> with aria-label so assistive tech can jump to the pager.",
|
|
116
129
|
"The current page button carries aria-current='page'.",
|
|
117
130
|
"Prev/next buttons are real <button>s with aria-label and become disabled at the bounds.",
|
|
131
|
+
"With pageLink the controls are real <a> links (or the router's), so they are crawlable and open in a new tab; a bound arrow stays a disabled button rather than a link to nowhere.",
|
|
118
132
|
"Ellipsis slots are aria-hidden \u2014 they convey no navigable target."
|
|
119
133
|
],
|
|
120
134
|
examples: [
|
|
@@ -130,6 +144,10 @@ var paginationMeta = {
|
|
|
130
144
|
label: "Table results footer",
|
|
131
145
|
code: '<Stack gap={2}><DataTable data={rows} columns={columns} rowKey={(row) => row.id} /><Pagination label="Customer pages" page={page} total={12} onPageChange={setPage} /></Stack>'
|
|
132
146
|
},
|
|
147
|
+
{
|
|
148
|
+
label: "Page links (URL is the state)",
|
|
149
|
+
code: "<Pagination page={page} total={pageCount} pageLink={(page) => <Link to={`/search?q=${query}&page=${page}`} />} />"
|
|
150
|
+
},
|
|
133
151
|
{
|
|
134
152
|
label: "Server query state",
|
|
135
153
|
code: "const [page, setPage] = useState(searchParams.page ?? 1)\n<Pagination page={page} total={pageCount} onPageChange={(next) => { setPage(next); loadPage(next) }} />"
|
|
@@ -8,12 +8,13 @@ import { useEdgeFade } from "../../lib/use-edge-fade.js";
|
|
|
8
8
|
import { useScrollActivity } from "../../lib/use-scroll-activity.js";
|
|
9
9
|
import {
|
|
10
10
|
scrollRegionKeyDown,
|
|
11
|
-
|
|
12
|
-
} from "../../lib/use-wheel
|
|
11
|
+
useSmoothWheel
|
|
12
|
+
} from "../../lib/use-smooth-wheel.js";
|
|
13
13
|
import { jsx } from "react/jsx-runtime";
|
|
14
14
|
function Scroller({
|
|
15
15
|
orientation = "vertical",
|
|
16
16
|
wheelScroll = true,
|
|
17
|
+
smoothWheel = true,
|
|
17
18
|
label,
|
|
18
19
|
className,
|
|
19
20
|
viewportClassName,
|
|
@@ -27,7 +28,7 @@ function Scroller({
|
|
|
27
28
|
const regionRef = useRef(null);
|
|
28
29
|
const isHorizontal = orientation === "horizontal";
|
|
29
30
|
const trackActivity = useScrollActivity();
|
|
30
|
-
const trackWheel =
|
|
31
|
+
const trackWheel = useSmoothWheel({ axis: orientation, mapVertical: isHorizontal && wheelScroll, smooth: smoothWheel });
|
|
31
32
|
const setRegion = useCallback(
|
|
32
33
|
(node) => {
|
|
33
34
|
setRef(ref, node);
|
|
@@ -59,7 +60,10 @@ function Scroller({
|
|
|
59
60
|
// sizing, and is NOT masked — which is the entire point of the split. A
|
|
60
61
|
// flex column so the scroller below can be `flex-1 min-h-0`, which bounds
|
|
61
62
|
// off `h-*`, `max-h-*` and `flex-1` alike with no percentage to resolve.
|
|
62
|
-
|
|
63
|
+
// `min-w-0` because this box, not the scroller inside it, is what a grid
|
|
64
|
+
// or flex parent sizes: left at `min-width: auto`, a horizontal strip
|
|
65
|
+
// widened its column to the full length of its content and never scrolled.
|
|
66
|
+
/* @__PURE__ */ jsx("div", { style, className: cn("flex min-w-0 flex-col", className), children: /* @__PURE__ */ jsx(
|
|
63
67
|
"div",
|
|
64
68
|
{
|
|
65
69
|
ref: setRegion,
|
|
@@ -65,6 +65,12 @@ var scrollerMeta = {
|
|
|
65
65
|
default: "true",
|
|
66
66
|
description: "On a horizontal Scroller, let a vertical mouse wheel scroll it sideways. Ignored when vertical. Turn it off only if the region is inside something that owns the wheel itself (a map, a canvas)."
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
name: "smoothWheel",
|
|
70
|
+
type: "boolean",
|
|
71
|
+
default: "true",
|
|
72
|
+
description: "Ease mouse-wheel steps toward their target, on either axis: the inertia a smooth-scroll library such as Lenis adds, scoped to this region. Only a mouse wheel is eased; trackpads and touch keep their own momentum, keys and scrollbar drags stay native, and reduced motion (the OS preference or MotionProvider) turns it off. Anything else that moves the region (scrollIntoView, a stick-to-bottom chat) stops the easing at once. For a scroll container of your own, `useSmoothWheel()` returns the same behaviour as a ref callback."
|
|
73
|
+
},
|
|
68
74
|
{
|
|
69
75
|
name: "label",
|
|
70
76
|
type: "string",
|
|
@@ -20,54 +20,127 @@ var SWIPE_DIRECTION = {
|
|
|
20
20
|
top: "up",
|
|
21
21
|
bottom: "down"
|
|
22
22
|
};
|
|
23
|
-
var
|
|
24
|
-
function Sheet(
|
|
23
|
+
var SheetRootContext = createContext(null);
|
|
24
|
+
function Sheet({
|
|
25
|
+
snapPoints,
|
|
26
|
+
snapPoint,
|
|
27
|
+
defaultSnapPoint,
|
|
28
|
+
onSnapPointChange,
|
|
29
|
+
snapToSequentialPoints,
|
|
30
|
+
...props
|
|
31
|
+
}) {
|
|
25
32
|
const [side, setSide] = useState("right");
|
|
26
|
-
|
|
33
|
+
const snaps = Boolean(snapPoints?.length);
|
|
34
|
+
const context = useMemo(() => ({ reportSide: setSide, snaps }), [snaps]);
|
|
35
|
+
return /* @__PURE__ */ jsx(SheetRootContext.Provider, { value: context, children: /* @__PURE__ */ jsx(
|
|
36
|
+
Base.Root,
|
|
37
|
+
{
|
|
38
|
+
...props,
|
|
39
|
+
snapPoints: snaps && side === "bottom" ? snapPoints : void 0,
|
|
40
|
+
snapPoint,
|
|
41
|
+
defaultSnapPoint,
|
|
42
|
+
onSnapPointChange,
|
|
43
|
+
snapToSequentialPoints,
|
|
44
|
+
swipeDirection: SWIPE_DIRECTION[side]
|
|
45
|
+
}
|
|
46
|
+
) });
|
|
47
|
+
}
|
|
48
|
+
function isDevelopment() {
|
|
49
|
+
try {
|
|
50
|
+
return process.env.NODE_ENV !== "production";
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function useKeyboardInset(enabled) {
|
|
56
|
+
const [inset, setInset] = useState(0);
|
|
57
|
+
useIsomorphicLayoutEffect(() => {
|
|
58
|
+
const viewport = typeof window === "undefined" ? void 0 : window.visualViewport;
|
|
59
|
+
if (!enabled || !viewport) return;
|
|
60
|
+
const measure = () => setInset(
|
|
61
|
+
Math.abs(viewport.scale - 1) > 0.01 ? 0 : Math.max(0, Math.round(window.innerHeight - viewport.height - viewport.offsetTop))
|
|
62
|
+
);
|
|
63
|
+
measure();
|
|
64
|
+
viewport.addEventListener("resize", measure);
|
|
65
|
+
viewport.addEventListener("scroll", measure);
|
|
66
|
+
return () => {
|
|
67
|
+
viewport.removeEventListener("resize", measure);
|
|
68
|
+
viewport.removeEventListener("scroll", measure);
|
|
69
|
+
setInset(0);
|
|
70
|
+
};
|
|
71
|
+
}, [enabled]);
|
|
72
|
+
return inset;
|
|
27
73
|
}
|
|
28
74
|
var SheetTrigger = Base.Trigger;
|
|
29
75
|
var SheetClose = Base.Close;
|
|
30
76
|
var SheetPortal = withPortalThemeScope(Base.Portal);
|
|
77
|
+
var warnedSnapSide = false;
|
|
31
78
|
function SheetContent({
|
|
32
79
|
className,
|
|
33
80
|
children,
|
|
34
81
|
side,
|
|
35
82
|
initialFocus,
|
|
36
83
|
ref,
|
|
84
|
+
style,
|
|
37
85
|
...props
|
|
38
86
|
}) {
|
|
39
|
-
const
|
|
87
|
+
const root = useContext(SheetRootContext);
|
|
40
88
|
const resolvedSide = side ?? "right";
|
|
89
|
+
const reportSide = root?.reportSide;
|
|
41
90
|
useIsomorphicLayoutEffect(() => {
|
|
42
91
|
reportSide?.(resolvedSide);
|
|
43
92
|
}, [reportSide, resolvedSide]);
|
|
93
|
+
const snaps = Boolean(root?.snaps) && resolvedSide === "bottom";
|
|
94
|
+
if (root?.snaps && resolvedSide !== "bottom" && !warnedSnapSide && isDevelopment()) {
|
|
95
|
+
warnedSnapSide = true;
|
|
96
|
+
console.warn(`Sable: <Sheet snapPoints> applies to side="bottom" only; this sheet opens from the ${resolvedSide}, so its snap points are ignored.`);
|
|
97
|
+
}
|
|
44
98
|
const popupRef = useRef(null);
|
|
45
99
|
const [popup, setPopup] = useState(null);
|
|
46
100
|
const mergedRef = useMemo(() => mergeRefs(popupRef, ref, setPopup), [ref]);
|
|
101
|
+
const keyboardInset = useKeyboardInset(resolvedSide === "bottom" && popup !== null);
|
|
102
|
+
const keyboardVars = { "--sheet-keyboard-inset": `${keyboardInset}px` };
|
|
47
103
|
useIsomorphicLayoutEffect(() => {
|
|
48
104
|
if (!popup || resolvedSide !== "left" && resolvedSide !== "right") return;
|
|
49
|
-
const
|
|
105
|
+
const root2 = document.documentElement;
|
|
50
106
|
const name = `--sable-sheet-${resolvedSide}-w`;
|
|
51
|
-
const publish = () =>
|
|
107
|
+
const publish = () => root2.style.setProperty(name, `${popup.offsetWidth}px`);
|
|
52
108
|
publish();
|
|
53
109
|
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(publish);
|
|
54
110
|
observer?.observe(popup);
|
|
55
111
|
return () => {
|
|
56
112
|
observer?.disconnect();
|
|
57
|
-
|
|
113
|
+
root2.style.removeProperty(name);
|
|
58
114
|
};
|
|
59
115
|
}, [popup, resolvedSide]);
|
|
60
116
|
return /* @__PURE__ */ jsxs(SheetPortal, { children: [
|
|
61
117
|
/* @__PURE__ */ jsx(Base.Backdrop, { className: "fixed inset-0 z-(--z-overlay) bg-overlay/(--scrim-alpha) opacity-[calc(1_-_var(--drawer-swipe-progress))] motion-fade data-[swiping]:transition-none" }),
|
|
62
|
-
/* @__PURE__ */ jsx(Base.Viewport, { className: "fixed inset-0 z-(--z-overlay)", children: /* @__PURE__ */
|
|
118
|
+
/* @__PURE__ */ jsx(Base.Viewport, { className: "fixed inset-0 z-(--z-overlay)", children: /* @__PURE__ */ jsxs(
|
|
63
119
|
Base.Popup,
|
|
64
120
|
{
|
|
65
121
|
ref: mergedRef,
|
|
66
122
|
initialFocus: initialFocus === void 0 ? (type) => type === "touch" ? popupRef.current : true : initialFocus,
|
|
67
123
|
"data-sable-sheet": resolvedSide,
|
|
124
|
+
"data-snap-points": snaps ? "" : void 0,
|
|
68
125
|
className: cn(sheetVariants({ side }), className),
|
|
69
126
|
...props,
|
|
70
|
-
|
|
127
|
+
style: typeof style === "function" ? (state) => ({ ...keyboardVars, ...style(state) }) : { ...keyboardVars, ...style },
|
|
128
|
+
children: [
|
|
129
|
+
snaps ? (
|
|
130
|
+
// The grip says "drag me". Decorative: the stops are reached by
|
|
131
|
+
// swipe, and the sheet's content stays reachable by scrolling
|
|
132
|
+
// and by keyboard whichever stop it rests at.
|
|
133
|
+
/* @__PURE__ */ jsx(
|
|
134
|
+
"div",
|
|
135
|
+
{
|
|
136
|
+
"aria-hidden": true,
|
|
137
|
+
"data-slot": "sheet-handle",
|
|
138
|
+
className: "-mt-(--sheet-handle-pull) mb-0 h-1 w-10 shrink-0 self-center rounded-full bg-border-strong"
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
) : null,
|
|
142
|
+
/* @__PURE__ */ jsx(Base.Content, { className: "contents", children: /* @__PURE__ */ jsx(PresentationProvider, { value: resolvedSide === "bottom" ? "sheet" : "page", children }) })
|
|
143
|
+
]
|
|
71
144
|
}
|
|
72
145
|
) })
|
|
73
146
|
] });
|
|
@@ -8,7 +8,10 @@ var sheetMeta = {
|
|
|
8
8
|
"drawer",
|
|
9
9
|
"side panel",
|
|
10
10
|
"overlay",
|
|
11
|
-
"swipe to dismiss"
|
|
11
|
+
"swipe to dismiss",
|
|
12
|
+
"bottom sheet",
|
|
13
|
+
"snap points",
|
|
14
|
+
"detents"
|
|
12
15
|
],
|
|
13
16
|
usage: {
|
|
14
17
|
description: "Use Sheet for secondary workflows that slide in while preserving context from the underlying page.",
|
|
@@ -21,6 +24,10 @@ var sheetMeta = {
|
|
|
21
24
|
guidance: "do",
|
|
22
25
|
description: "Include clear title, close path, and footer actions for task-oriented sheets."
|
|
23
26
|
},
|
|
27
|
+
{
|
|
28
|
+
guidance: "do",
|
|
29
|
+
description: "Give a bottom sheet snap points when it shows a summary first and detail on demand (a map result, a now-playing panel): `snapPoints={[0.4, 1]}`. The first stop is where it opens; put what matters at that height, because the rest is a drag away."
|
|
30
|
+
},
|
|
24
31
|
{
|
|
25
32
|
guidance: "dont",
|
|
26
33
|
description: "Do not use Sheet for simple confirmations or deeply nested workflows."
|
|
@@ -83,15 +90,39 @@ var sheetMeta = {
|
|
|
83
90
|
props: [
|
|
84
91
|
{
|
|
85
92
|
name: "Sheet (Root)",
|
|
86
|
-
type: "Base UI Drawer.Root props (open, defaultOpen, onOpenChange, onOpenChangeComplete, modal, disablePointerDismissal, actionsRef, handle) \u2014 minus swipeDirection
|
|
93
|
+
type: "Base UI Drawer.Root props (open, defaultOpen, onOpenChange, onOpenChangeComplete, modal, disablePointerDismissal, actionsRef, handle, snapPoints, snapPoint, defaultSnapPoint, onSnapPointChange, snapToSequentialPoints) \u2014 minus swipeDirection",
|
|
87
94
|
description: `Controls open state. Uncontrolled by default. The dismiss swipe direction is not a prop: it follows SheetContent's side. onOpenChange reports reason "swipe" when a swipe closed it.`
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "snapPoints",
|
|
98
|
+
type: "(number | string)[]",
|
|
99
|
+
default: "\u2014",
|
|
100
|
+
description: `Stops a bottom sheet rests at, smallest first: a fraction of the viewport (0.4), pixels (320), or a px/rem string ("20rem"). The panel is the full stop's height and opens at the first stop; a swipe moves between stops and a swipe below the first closes it. side="bottom" only: other sides ignore them, with a development warning.`
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "snapPoint",
|
|
104
|
+
type: "number | string | null",
|
|
105
|
+
default: "\u2014",
|
|
106
|
+
description: "The active stop, controlled. Pair with onSnapPointChange; use defaultSnapPoint to open at a different stop uncontrolled."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "onSnapPointChange",
|
|
110
|
+
type: "(snapPoint: number | string | null, eventDetails) => void",
|
|
111
|
+
default: "\u2014",
|
|
112
|
+
description: "Fires when a swipe (or your code) moves the sheet to another stop."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "snapToSequentialPoints",
|
|
116
|
+
type: "boolean",
|
|
117
|
+
default: "false",
|
|
118
|
+
description: "Move one stop per swipe, by distance. Off, a fast flick can skip past a stop."
|
|
88
119
|
}
|
|
89
120
|
],
|
|
90
121
|
subcomponents: [
|
|
91
122
|
{
|
|
92
123
|
name: "SheetContent",
|
|
93
124
|
type: 'Base UI Drawer.Popup props (initialFocus, finalFocus, \u2026) + side: "right" | "left" | "top" | "bottom" + className',
|
|
94
|
-
description: 'Backdrop + edge-anchored popup; portals to <body>. side picks which viewport edge the sheet anchors to and slides in from (defaults to "right"). Left/right are full-height side panels; top/bottom span the width up to 80vh. On touch it swipes closed toward that same edge (right swipes right, left swipes left, top swipes up, bottom swipes down).'
|
|
125
|
+
description: 'Backdrop + edge-anchored popup; portals to <body>. side picks which viewport edge the sheet anchors to and slides in from (defaults to "right"). Left/right are full-height side panels; top/bottom span the width up to 80vh. A bottom sheet with snapPoints is as tall as its highest stop allows (the viewport less a top gap), shows a drag handle, and translates down to the active stop; the part below the screen becomes bottom padding, so its scroll area and SheetFooter end at the visible edge. A bottom sheet rides above the on-screen keyboard (measured from visualViewport). On touch it swipes closed toward that same edge (right swipes right, left swipes left, top swipes up, bottom swipes down).'
|
|
95
126
|
},
|
|
96
127
|
{
|
|
97
128
|
name: "SheetTrigger",
|
|
@@ -141,7 +172,9 @@ var sheetMeta = {
|
|
|
141
172
|
"Focus matches Dialog: the first tabbable on mouse/keyboard open, the panel itself on touch (so the virtual keyboard stays down); focus returns to the trigger on close.",
|
|
142
173
|
"Render a SheetClose inside SheetContent so screen-reader users can escape the popup.",
|
|
143
174
|
"Slide animation runs via data-[starting-style]/data-[ending-style] attributes on the motion-duration token; while swiping the panel tracks the finger directly, and a released swipe finishes on the same token (scaled by flick speed), so reduced motion makes it instant.",
|
|
144
|
-
"A mouse drag that starts on sheet content selects text rather than dragging the panel; mouse dragging only starts from the padding edge."
|
|
175
|
+
"A mouse drag that starts on sheet content selects text rather than dragging the panel; mouse dragging only starts from the padding edge.",
|
|
176
|
+
"Snap points add stops, not a gate: at any stop the content scrolls within the visible part and every control stays reachable by keyboard, so nothing needs the drag. The handle is decorative (aria-hidden).",
|
|
177
|
+
"A bottom sheet stays above the on-screen keyboard: its bottom edge is lifted by the height the keyboard takes from the visual viewport, so a focused field is not covered. An app that sets interactive-widget=resizes-content in its viewport meta gets the same result from the browser, and the inset reads 0."
|
|
145
178
|
],
|
|
146
179
|
examples: [
|
|
147
180
|
{
|
|
@@ -156,6 +189,10 @@ var sheetMeta = {
|
|
|
156
189
|
label: "Left side",
|
|
157
190
|
code: '<Sheet><SheetTrigger render={<Button variant="secondary">Menu</Button>} /><SheetContent side="left"><SheetHeader><SheetTitle>Navigation</SheetTitle></SheetHeader><SheetFooter><SheetClose render={<Button variant="secondary">Close</Button>} /></SheetFooter></SheetContent></Sheet>'
|
|
158
191
|
},
|
|
192
|
+
{
|
|
193
|
+
label: "Bottom sheet with snap points",
|
|
194
|
+
code: '<Sheet snapPoints={[0.4, 1]}><SheetTrigger render={<Button variant="secondary">Nearby</Button>} /><SheetContent side="bottom"><SheetHeader><SheetTitle>12 places nearby</SheetTitle><SheetDescription>Drag up for the full list.</SheetDescription></SheetHeader><SheetList>{places.map((place) => <Entity key={place.id} title={place.name} subtitle={place.distance} />)}</SheetList></SheetContent></Sheet>'
|
|
195
|
+
},
|
|
159
196
|
{
|
|
160
197
|
label: "Bottom side",
|
|
161
198
|
code: '<Sheet><SheetTrigger render={<Button variant="secondary">Details</Button>} /><SheetContent side="bottom"><SheetHeader><SheetTitle>Order details</SheetTitle></SheetHeader><SheetFooter><SheetClose render={<Button variant="secondary">Close</Button>} /></SheetFooter></SheetContent></Sheet>'
|
|
@@ -8,7 +8,14 @@ var sheetVariants = cva(
|
|
|
8
8
|
right: "inset-y-0 right-0 [transform:translateX(var(--drawer-swipe-movement-x))] h-full w-95 max-w-[90vw] border-l data-[ending-style]:translate-x-full data-[starting-style]:translate-x-full",
|
|
9
9
|
left: "inset-y-0 left-0 [transform:translateX(var(--drawer-swipe-movement-x))] h-full w-95 max-w-[90vw] border-r data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full",
|
|
10
10
|
top: "inset-x-0 top-0 [transform:translateY(var(--drawer-swipe-movement-y))] h-auto max-h-[80vh] w-full border-b data-[ending-style]:-translate-y-full data-[starting-style]:-translate-y-full",
|
|
11
|
-
bottom:
|
|
11
|
+
bottom: [
|
|
12
|
+
"inset-x-0 bottom-(--sheet-keyboard-inset,0) [transform:translateY(calc(var(--drawer-snap-point-offset,0px)_+_var(--drawer-swipe-movement-y,0px)))] h-auto max-h-[min(80vh,calc(100dvh_-_var(--sheet-keyboard-inset,0px)_-_var(--sheet-top-gap)))] w-full border-t [--sheet-top-gap:--spacing(10)] data-[ending-style]:translate-y-full data-[starting-style]:translate-y-full",
|
|
13
|
+
// Snap points: the panel is the full stop's height, and the active stop
|
|
14
|
+
// is Base UI's offset, translated down. The hidden part becomes bottom
|
|
15
|
+
// padding, so the scroll area, and SheetFooter's mt-auto, end at the
|
|
16
|
+
// visible edge rather than under the screen's.
|
|
17
|
+
"data-[snap-points]:h-[calc(100dvh_-_var(--sheet-keyboard-inset,0px)_-_var(--sheet-top-gap))] data-[snap-points]:max-h-none data-[snap-points]:pb-[calc(var(--sheet-p)_+_var(--drawer-snap-point-offset,0px))] data-[snap-points]:[--sheet-handle-pull:calc(var(--sheet-p)_-_--spacing(3))]"
|
|
18
|
+
].join(" ")
|
|
12
19
|
}
|
|
13
20
|
},
|
|
14
21
|
defaultVariants: {
|
|
@@ -31,10 +31,18 @@ var SIDEBAR_INK = [
|
|
|
31
31
|
"[--accent:var(--sidebar-accent)] [--accent-foreground:var(--sidebar-accent-foreground)]",
|
|
32
32
|
"[--border:var(--sidebar-border)] [--ring:var(--sidebar-ring)]"
|
|
33
33
|
].join(" ");
|
|
34
|
+
var PAGE_INK = [
|
|
35
|
+
"[--sidebar:var(--background)] [--sidebar-foreground:var(--foreground)]",
|
|
36
|
+
"[--sidebar-muted-foreground:var(--muted-foreground)]",
|
|
37
|
+
"[--sidebar-primary:var(--primary)] [--sidebar-primary-foreground:var(--primary-foreground)]",
|
|
38
|
+
"[--sidebar-accent:var(--muted)] [--sidebar-accent-foreground:var(--foreground)]",
|
|
39
|
+
"[--sidebar-border:var(--border)] [--sidebar-ring:var(--ring)]"
|
|
40
|
+
].join(" ");
|
|
34
41
|
function Sidebar({
|
|
35
42
|
collapsed = false,
|
|
36
43
|
icons = true,
|
|
37
44
|
variant = "app",
|
|
45
|
+
surface = "rail",
|
|
38
46
|
className,
|
|
39
47
|
children,
|
|
40
48
|
...props
|
|
@@ -44,15 +52,18 @@ function Sidebar({
|
|
|
44
52
|
{
|
|
45
53
|
"data-collapsed": collapsed || void 0,
|
|
46
54
|
"data-variant": variant,
|
|
55
|
+
"data-surface": surface,
|
|
47
56
|
className: cn(
|
|
48
|
-
"shrink-0 flex-col
|
|
57
|
+
"shrink-0 flex-col bg-sidebar text-sidebar-foreground",
|
|
49
58
|
// The rail is its own surface. Anything placed in it that inks with
|
|
50
59
|
// page tokens — an Entity account row, an Avatar, a ghost Button,
|
|
51
60
|
// an OverflowMenu — reads the rail's set instead, so a theme can paint a
|
|
52
61
|
// dark rail on a light page (Meridian) and the footer row stays
|
|
53
62
|
// legible. Muted ink is the rail's ink at 62% over the rail, unless
|
|
54
63
|
// a theme names --sidebar-muted-foreground.
|
|
55
|
-
|
|
64
|
+
// On the page surface the rail is separated by space and its
|
|
65
|
+
// content, not a rule and a tonal step.
|
|
66
|
+
surface === "rail" ? cn("border-r border-sidebar-border", SIDEBAR_INK) : PAGE_INK,
|
|
56
67
|
SIDEBAR_VARIANT[variant],
|
|
57
68
|
"w-[var(--sidebar-width,16rem)] data-[collapsed]:w-[var(--sidebar-width-collapsed,4rem)]",
|
|
58
69
|
className
|