@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
package/registry.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://sable.design/registry.schema.json",
|
|
3
3
|
"name": "@sableui/react",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.19.0",
|
|
5
5
|
"description": "Themeable React components, semantic tokens, layout patterns, presets, and package-owned agent skills. Default theme: Sable.",
|
|
6
6
|
"engine": {
|
|
7
7
|
"behavior": "@base-ui/react",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"composition": "Inspect a relevant pattern skeleton. Reuse when its information architecture fits; original compositions from layout primitives are equally valid.",
|
|
64
64
|
"review": "sable docs reviewing-a-design",
|
|
65
65
|
"finish": "sable check <source paths> runs doctor, lint and grade in one pass and exits 1 on any failure, then lists the review steps no tool can verify. Optional Claude Code Stop hook: sable setup --agent --target claude --hook",
|
|
66
|
+
"adoption": "sable check reports how much of the UI composes Sable and the app-built components Sable already ships (grade --json → adoption). Never a failure; at level none or low, ask the user before moving working screens: sable docs auditing-an-app → Adoption.",
|
|
66
67
|
"evidenceTemplate": "sable grade --template",
|
|
67
68
|
"learning": "sable docs learning-from-builds",
|
|
68
69
|
"localMemory": ".sable/lessons/ for causal observations; .sable/preferences/ for scoped judgments; both project-owned and preserved across package upgrades",
|
|
@@ -78,8 +79,9 @@
|
|
|
78
79
|
"claude": ".claude/skills/sable"
|
|
79
80
|
},
|
|
80
81
|
"canonical": "skills/sable/SKILL.md in the installed package; entries resolve it on every task",
|
|
81
|
-
"
|
|
82
|
-
"
|
|
82
|
+
"instructions": "A short managed block in AGENTS.md (Codex, Cursor) and CLAUDE.md (Claude Code), between <!-- sable:begin --> and <!-- sable:end -->, that sends every UI request through the skill even when it doesn't name Sable. --no-instructions skips it.",
|
|
83
|
+
"upgrade": "pnpm exec sable upgrade (updates the package, then runs the new version's init). From 0.17 or earlier: pnpm update @sableui/react --latest; pnpm exec sable init",
|
|
84
|
+
"preserves": "Local edits, project instructions outside the Sable block, presets and preferences; setup only refreshes unchanged managed entries and blocks, and refuses linked destinations."
|
|
83
85
|
},
|
|
84
86
|
"distributionStatus": "Public on npm as @sableui/react; install with pnpm add @sableui/react.",
|
|
85
87
|
"ios": {
|
|
@@ -96,7 +98,7 @@
|
|
|
96
98
|
"command": "pnpm exec sable init",
|
|
97
99
|
"preview": "pnpm exec sable init --dry-run",
|
|
98
100
|
"after": "Run it once the package is installed; before that `npx sable` resolves an unrelated package.",
|
|
99
|
-
"does": "Installs missing packages with the lockfile's package manager (React, Tailwind and its build plugin), writes the css imports in order into the global stylesheet, adds bundlerDedupe and the Tailwind plugin to a Vite config (a postcss.config.mjs for Next.js), loads the fonts, wraps a Vite starter's <App /> in IconProvider, runs agentSetup, then sable doctor.",
|
|
101
|
+
"does": "Installs missing packages with the lockfile's package manager (React, Tailwind and its build plugin), writes the css imports in order into the global stylesheet, adds bundlerDedupe and the Tailwind plugin to a Vite config (a postcss.config.mjs for Next.js), loads the fonts, wraps a Vite starter's <App /> in IconProvider, runs agentSetup (skill entries plus the AGENTS.md / CLAUDE.md block; in a terminal it offers the Stop hook), then sable doctor.",
|
|
100
102
|
"frameworks": "Vite and Next.js (App Router) get every step. Other setups get the packages and the stylesheet; the bundler and provider steps are printed.",
|
|
101
103
|
"manual": "Each edit it cannot make safely (an unfamiliar config shape, a Next.js layout, a stylesheet split across files) is printed as the exact snippet and the file it belongs in. Apply them, then run sable doctor.",
|
|
102
104
|
"flags": "--dry-run previews; --no-agent skips the skill; --no-install prints install commands instead of running them.",
|
|
@@ -980,6 +982,16 @@
|
|
|
980
982
|
]
|
|
981
983
|
},
|
|
982
984
|
"typographyOverrides": [
|
|
985
|
+
{
|
|
986
|
+
"name": "type-base",
|
|
987
|
+
"intent": "The base size the page type scale is built on, as a unitless px number — 16 by default. Every page tier keeps its ratio to it, so `14` rebuilds the whole scale at 14, and the baseline step and spacing follow the body down.",
|
|
988
|
+
"whenToUse": "When the product asks for smaller or larger type everywhere: a dense technical tool at 13-14, a reading product at 17. Set it on :root, a theme scope or a preset (`tokens.shared[\"type-base\"]: \"14\"`), not per component.",
|
|
989
|
+
"insteadOf": "Changing the root font-size, which also moves every rem the app wrote and the control ladder; or overriding tier sizes one by one, which breaks the ratios between them. Unitless: `14`, never `14px`.",
|
|
990
|
+
"pairsWith": [
|
|
991
|
+
"leading-scale",
|
|
992
|
+
"baseline-step"
|
|
993
|
+
]
|
|
994
|
+
},
|
|
983
995
|
{
|
|
984
996
|
"name": "type-display-4xl-size",
|
|
985
997
|
"intent": "The largest type: display-lg through display-4xl. Marketing-scale, one per view at most.",
|
|
@@ -1886,8 +1898,120 @@
|
|
|
1886
1898
|
"at-sign",
|
|
1887
1899
|
"replay",
|
|
1888
1900
|
"motion",
|
|
1889
|
-
"emoji"
|
|
1890
|
-
|
|
1901
|
+
"emoji",
|
|
1902
|
+
"briefcase",
|
|
1903
|
+
"layers",
|
|
1904
|
+
"microphone",
|
|
1905
|
+
"video",
|
|
1906
|
+
"broadcast",
|
|
1907
|
+
"newspaper",
|
|
1908
|
+
"palette",
|
|
1909
|
+
"gamepad",
|
|
1910
|
+
"users",
|
|
1911
|
+
"archive",
|
|
1912
|
+
"kanban",
|
|
1913
|
+
"sliders",
|
|
1914
|
+
"help",
|
|
1915
|
+
"sidebar",
|
|
1916
|
+
"chevron-up-down"
|
|
1917
|
+
],
|
|
1918
|
+
"aliases": {
|
|
1919
|
+
"chevrons-up-down": "chevron-up-down",
|
|
1920
|
+
"check-circle": "circle-check",
|
|
1921
|
+
"check-circle-2": "circle-check",
|
|
1922
|
+
"circle-check-big": "circle-check",
|
|
1923
|
+
"settings": "gear",
|
|
1924
|
+
"settings-2": "sliders",
|
|
1925
|
+
"cog": "gear",
|
|
1926
|
+
"x": "xmark",
|
|
1927
|
+
"close": "xmark",
|
|
1928
|
+
"x-circle": "xmark",
|
|
1929
|
+
"more-horizontal": "ellipsis",
|
|
1930
|
+
"more-vertical": "ellipsis",
|
|
1931
|
+
"more": "ellipsis",
|
|
1932
|
+
"dots-horizontal": "ellipsis",
|
|
1933
|
+
"ellipsis-horizontal": "ellipsis",
|
|
1934
|
+
"message-square": "comment",
|
|
1935
|
+
"message-circle": "comment",
|
|
1936
|
+
"message": "comment",
|
|
1937
|
+
"messages": "comment",
|
|
1938
|
+
"chat": "comment",
|
|
1939
|
+
"menu": "bars",
|
|
1940
|
+
"hamburger": "bars",
|
|
1941
|
+
"help-circle": "help",
|
|
1942
|
+
"circle-help": "help",
|
|
1943
|
+
"question": "help",
|
|
1944
|
+
"circle-question": "help",
|
|
1945
|
+
"alert-triangle": "warning",
|
|
1946
|
+
"triangle-alert": "warning",
|
|
1947
|
+
"info-circle": "info",
|
|
1948
|
+
"circle-info": "info",
|
|
1949
|
+
"external-link": "external",
|
|
1950
|
+
"loader": "spinner",
|
|
1951
|
+
"loader-2": "spinner",
|
|
1952
|
+
"loader-circle": "spinner",
|
|
1953
|
+
"log-out": "logout",
|
|
1954
|
+
"sign-out": "logout",
|
|
1955
|
+
"monitor": "desktop",
|
|
1956
|
+
"laptop": "desktop",
|
|
1957
|
+
"layout-grid": "grid",
|
|
1958
|
+
"layout-dashboard": "layout",
|
|
1959
|
+
"file-text": "file",
|
|
1960
|
+
"document": "file",
|
|
1961
|
+
"rotate-cw": "refresh",
|
|
1962
|
+
"rotate-ccw": "refresh",
|
|
1963
|
+
"refresh-cw": "refresh",
|
|
1964
|
+
"refresh-ccw": "refresh",
|
|
1965
|
+
"funnel": "filter",
|
|
1966
|
+
"user-circle": "user",
|
|
1967
|
+
"circle-user": "user",
|
|
1968
|
+
"person": "user",
|
|
1969
|
+
"people": "users",
|
|
1970
|
+
"team": "users",
|
|
1971
|
+
"user-group": "users",
|
|
1972
|
+
"users-round": "users",
|
|
1973
|
+
"box": "archive",
|
|
1974
|
+
"archive-box": "archive",
|
|
1975
|
+
"columns": "kanban",
|
|
1976
|
+
"columns-3": "kanban",
|
|
1977
|
+
"board": "kanban",
|
|
1978
|
+
"square-kanban": "kanban",
|
|
1979
|
+
"sliders-horizontal": "sliders",
|
|
1980
|
+
"sliders-vertical": "sliders",
|
|
1981
|
+
"adjustments": "sliders",
|
|
1982
|
+
"panel-left": "sidebar",
|
|
1983
|
+
"sidebar-left": "sidebar",
|
|
1984
|
+
"calendar-days": "calendar",
|
|
1985
|
+
"house": "home",
|
|
1986
|
+
"pencil": "edit",
|
|
1987
|
+
"edit-2": "edit",
|
|
1988
|
+
"edit-3": "edit",
|
|
1989
|
+
"square-pen": "edit",
|
|
1990
|
+
"trash-2": "trash",
|
|
1991
|
+
"delete": "trash",
|
|
1992
|
+
"envelope": "mail",
|
|
1993
|
+
"arrow-path": "refresh",
|
|
1994
|
+
"eye-slash": "eye-off",
|
|
1995
|
+
"lock-closed": "lock",
|
|
1996
|
+
"paper-airplane": "send",
|
|
1997
|
+
"paper-clip": "paperclip",
|
|
1998
|
+
"arrow-down-tray": "download",
|
|
1999
|
+
"arrow-up-tray": "upload",
|
|
2000
|
+
"magnifying-glass": "search",
|
|
2001
|
+
"cog-6-tooth": "gear",
|
|
2002
|
+
"bars-3": "bars",
|
|
2003
|
+
"chat-bubble-left": "comment",
|
|
2004
|
+
"exclamation-triangle": "warning",
|
|
2005
|
+
"information-circle": "info",
|
|
2006
|
+
"question-mark-circle": "help",
|
|
2007
|
+
"adjustments-horizontal": "sliders",
|
|
2008
|
+
"squares-2x2": "grid",
|
|
2009
|
+
"view-columns": "kanban",
|
|
2010
|
+
"arrow-top-right-on-square": "external",
|
|
2011
|
+
"computer-desktop": "desktop",
|
|
2012
|
+
"x-mark": "xmark",
|
|
2013
|
+
"document-text": "file"
|
|
2014
|
+
}
|
|
1891
2015
|
},
|
|
1892
2016
|
"layoutPrimitives": {
|
|
1893
2017
|
"doctrine": [
|
|
@@ -1903,7 +2027,7 @@
|
|
|
1903
2027
|
"name": "Region",
|
|
1904
2028
|
"status": "stable",
|
|
1905
2029
|
"path": "layout",
|
|
1906
|
-
"summary": "The page region: fluid, edge-to-edge with the fluid page inset (--page-inset). Opt-in centered max cap picked by ROLE (prose/form/feed/content/wide/full), never a global page max-width. The root of nearly every route.",
|
|
2030
|
+
"summary": "The page region: fluid, edge-to-edge with the fluid page inset (--page-inset). Opt-in centered max cap picked by ROLE (prose/form/feed/content/wide/full), never a global page max-width. The root of nearly every route. The inset and cap are classes (px-(--page-inset), max-w-…), so className overrides them at a breakpoint (max-sm:px-0); only safeArea padding is inline.",
|
|
1907
2031
|
"keywords": [
|
|
1908
2032
|
"region",
|
|
1909
2033
|
"page width",
|
|
@@ -2072,7 +2196,7 @@
|
|
|
2072
2196
|
"name": "Grid",
|
|
2073
2197
|
"status": "stable",
|
|
2074
2198
|
"path": "layout",
|
|
2075
|
-
"summary": "CSS Grid within a region. Two modes: fixed/responsive cols count, or minItemWidth so cells breathe with NO breakpoints (the default choice for card grids). A minItemWidth grid keeps its empty columns (track=\"fill\"), so one result stays one cell wide; track=\"fit\" stretches a fixed set across the row.",
|
|
2199
|
+
"summary": "CSS Grid within a region. Two modes: fixed/responsive cols count, or minItemWidth so cells breathe with NO breakpoints (the default choice for card grids). A minItemWidth grid keeps its empty columns (track=\"fill\"), so one result stays one cell wide; track=\"fit\" stretches a fixed set across the row. display is the grid class and the gap a CSS variable, so className utilities win at any breakpoint (hidden md:grid, md:gap-6, lg:grid-cols-3).",
|
|
2076
2200
|
"keywords": [
|
|
2077
2201
|
"grid",
|
|
2078
2202
|
"card grid",
|
|
@@ -2168,7 +2292,7 @@
|
|
|
2168
2292
|
"name": "Columns",
|
|
2169
2293
|
"status": "stable",
|
|
2170
2294
|
"path": "layout",
|
|
2171
|
-
"summary": "An explicit N-column track (default 12) for proportioned layouts — pair with Col spans (list-detail, asymmetric editorial layouts). For uniform card grids prefer Grid minItemWidth.",
|
|
2295
|
+
"summary": "An explicit N-column track (default 12) for proportioned layouts — pair with Col spans (list-detail, asymmetric editorial layouts). For uniform card grids prefer Grid minItemWidth. display is the grid class, so hidden md:grid works; a gap-* utility replaces both gaps and the column-gap cap.",
|
|
2172
2296
|
"keywords": [
|
|
2173
2297
|
"columns",
|
|
2174
2298
|
"12 column",
|
|
@@ -2245,7 +2369,7 @@
|
|
|
2245
2369
|
"name": "Stack",
|
|
2246
2370
|
"status": "stable",
|
|
2247
2371
|
"path": "layout",
|
|
2248
|
-
"summary": "Vertical flow on the 8px rhythm — the default container for stacked content. Gap by role: tight within a group (1.5–2), looser between groups (3–4), generous between sections (6+).",
|
|
2372
|
+
"summary": "Vertical flow on the 8px rhythm — the default container for stacked content. Gap by role: tight within a group (1.5–2), looser between groups (3–4), generous between sections (6+). Layout lives in classes (flex flex-col items-* justify-*) and the gap in a CSS variable, never inline style, so className utilities override any prop at a breakpoint (max-lg:hidden, md:flex-row, md:gap-8).",
|
|
2249
2373
|
"keywords": [
|
|
2250
2374
|
"stack",
|
|
2251
2375
|
"vertical rhythm",
|
|
@@ -2309,7 +2433,7 @@
|
|
|
2309
2433
|
"name": "Row",
|
|
2310
2434
|
"status": "stable",
|
|
2311
2435
|
"path": "layout",
|
|
2312
|
-
"summary": "Horizontal flex flow — inline rows of controls, toolbars, action clusters. Defaults to center-aligned, no wrap.",
|
|
2436
|
+
"summary": "Horizontal flex flow — inline rows of controls, toolbars, action clusters. Defaults to center-aligned, no wrap. Layout lives in classes (flex flex-row items-* justify-*) and the gap in a CSS variable, never inline style, so className utilities override any prop at a breakpoint (hidden md:flex, max-md:flex-col, md:gap-6).",
|
|
2313
2437
|
"keywords": [
|
|
2314
2438
|
"row",
|
|
2315
2439
|
"toolbar",
|
|
@@ -2372,6 +2496,11 @@
|
|
|
2372
2496
|
"label": "Action cluster",
|
|
2373
2497
|
"code": "<Row gap={2} justify=\"end\"><Button variant=\"ghost\">Cancel</Button><Button>Save changes</Button></Row>",
|
|
2374
2498
|
"imports": "import { Button, Row } from \"@sableui/react\""
|
|
2499
|
+
},
|
|
2500
|
+
{
|
|
2501
|
+
"label": "Responsive through className",
|
|
2502
|
+
"code": "<Row justify=\"between\"><span className=\"text-title-sm\">Acme</span><Row gap={1} className=\"max-md:hidden\"><Button variant=\"ghost\" size=\"sm\">Docs</Button><Button variant=\"ghost\" size=\"sm\">Pricing</Button></Row></Row>",
|
|
2503
|
+
"imports": "import { Button, Row } from \"@sableui/react\""
|
|
2375
2504
|
}
|
|
2376
2505
|
]
|
|
2377
2506
|
},
|
|
@@ -2379,7 +2508,7 @@
|
|
|
2379
2508
|
"name": "Spacer",
|
|
2380
2509
|
"status": "stable",
|
|
2381
2510
|
"path": "layout",
|
|
2382
|
-
"summary": "Explicit space on the rhythm, or a flexible grow pusher between siblings (e.g. pushing a footer item to the end of a Row).",
|
|
2511
|
+
"summary": "Explicit space on the rhythm, or a flexible grow pusher between siblings (e.g. pushing a footer item to the end of a Row). The size is a class reading a CSS variable, so className resizes or hides it at a breakpoint.",
|
|
2383
2512
|
"keywords": [
|
|
2384
2513
|
"spacer",
|
|
2385
2514
|
"spacing",
|
|
@@ -2441,7 +2570,7 @@
|
|
|
2441
2570
|
"name": "Center",
|
|
2442
2571
|
"status": "stable",
|
|
2443
2572
|
"path": "layout",
|
|
2444
|
-
"summary": "Horizontally center a block, optionally capping its width (commonly \"prose\" for a reading column inside a wider layout).",
|
|
2573
|
+
"summary": "Horizontally center a block, optionally capping its width (commonly \"prose\" for a reading column inside a wider layout). Centering and cap are classes, so className overrides them at a breakpoint.",
|
|
2445
2574
|
"keywords": [
|
|
2446
2575
|
"center",
|
|
2447
2576
|
"measure",
|
|
@@ -2492,7 +2621,7 @@
|
|
|
2492
2621
|
"name": "AspectBox",
|
|
2493
2622
|
"status": "stable",
|
|
2494
2623
|
"path": "layout",
|
|
2495
|
-
"summary": "Reserves space via aspect-ratio so async media loads with zero layout shift. Wrap images, video, embeds.",
|
|
2624
|
+
"summary": "Reserves space via aspect-ratio so async media loads with zero layout shift. Wrap images, video, embeds. The ratio is a class reading a CSS variable, so className reshapes it at a breakpoint (md:aspect-video).",
|
|
2496
2625
|
"keywords": [
|
|
2497
2626
|
"aspect box",
|
|
2498
2627
|
"aspect ratio",
|
|
@@ -2544,7 +2673,7 @@
|
|
|
2544
2673
|
"name": "Bleed",
|
|
2545
2674
|
"status": "stable",
|
|
2546
2675
|
"path": "layout",
|
|
2547
|
-
"summary": "Escape a width-capped or inset ancestor to span the full CONTENT area (container-query units, so it stops at a fixed Sidebar instead of bleeding under it). Hero bands, profile covers, full-bleed media inside a capped Page.",
|
|
2676
|
+
"summary": "Escape a width-capped or inset ancestor to span the full CONTENT area (container-query units, so it stops at a fixed Sidebar instead of bleeding under it). Hero bands, profile covers, full-bleed media inside a capped Page. Width and margins are classes, so a breakpoint can turn the bleed off (lg:mx-0 lg:w-auto).",
|
|
2548
2677
|
"keywords": [
|
|
2549
2678
|
"bleed",
|
|
2550
2679
|
"full bleed",
|
|
@@ -2589,7 +2718,7 @@
|
|
|
2589
2718
|
"name": "SplitPane",
|
|
2590
2719
|
"status": "stable",
|
|
2591
2720
|
"path": "layout",
|
|
2592
|
-
"summary": "Responsive two-pane layout: side-by-side at/above a breakpoint, stacked (primary first) below. List-detail, media + metadata.",
|
|
2721
|
+
"summary": "Responsive two-pane layout: side-by-side at/above a breakpoint, stacked (primary first) below. List-detail, media + metadata. display is the grid class and the breakpoint rules are layered, so className utilities (hidden lg:grid, gap-8) win.",
|
|
2593
2722
|
"keywords": [
|
|
2594
2723
|
"split pane",
|
|
2595
2724
|
"two pane",
|
|
@@ -2955,6 +3084,308 @@
|
|
|
2955
3084
|
}
|
|
2956
3085
|
]
|
|
2957
3086
|
},
|
|
3087
|
+
{
|
|
3088
|
+
"name": "ActivityHeatmap",
|
|
3089
|
+
"status": "stable",
|
|
3090
|
+
"group": "Charts",
|
|
3091
|
+
"register": "either",
|
|
3092
|
+
"path": "components/activity-heatmap",
|
|
3093
|
+
"summary": "A calendar heatmap: one value per day as a grid of weeks by weekdays — the GitHub contribution graph, for any daily measure. Levels by quartile, categories by source, a real ARIA grid with one tab stop, and a sideways scroll on narrow screens that opens on the latest week.",
|
|
3094
|
+
"keywords": [
|
|
3095
|
+
"heatmap",
|
|
3096
|
+
"calendar heatmap",
|
|
3097
|
+
"contribution graph",
|
|
3098
|
+
"contributions",
|
|
3099
|
+
"activity",
|
|
3100
|
+
"github",
|
|
3101
|
+
"streak",
|
|
3102
|
+
"habit",
|
|
3103
|
+
"daily",
|
|
3104
|
+
"commits",
|
|
3105
|
+
"uptime"
|
|
3106
|
+
],
|
|
3107
|
+
"usage": {
|
|
3108
|
+
"description": "Use ActivityHeatmap to show how often something happened day by day over weeks or months: commits, habits kept, orders, uptime. Pass days as ISO dates with a value; levels, colours, labels, keyboard and tooltips work with no further props. Put it inside a Card and compose any totals beside it (NumberTicker, StatRowBlock). For a value over time that a reader needs to compare precisely, use AreaChart or BarChart; for a month calendar with marked days (a streak calendar), use Calendar.",
|
|
3109
|
+
"bestPractices": [
|
|
3110
|
+
{
|
|
3111
|
+
"guidance": "do",
|
|
3112
|
+
"description": "Pass dates as \"YYYY-MM-DD\" strings. The grid reads them as calendar days, so no time zone can shift a day — never build them with `new Date(...).toISOString()` from a local date."
|
|
3113
|
+
},
|
|
3114
|
+
{
|
|
3115
|
+
"guidance": "do",
|
|
3116
|
+
"description": "Give it a `unit` (\"contribution\" / \"contributions\") so the tooltip and every cell's name say what the number counts."
|
|
3117
|
+
},
|
|
3118
|
+
{
|
|
3119
|
+
"guidance": "do",
|
|
3120
|
+
"description": "Let the default quantile scale pick levels — one exceptional day then does not wash every other day out. Pass a day's own `level` when the source already has one (GitHub's API)."
|
|
3121
|
+
},
|
|
3122
|
+
{
|
|
3123
|
+
"guidance": "dont",
|
|
3124
|
+
"description": "Use it for a handful of values or for values a reader must compare exactly; a heatmap shows rhythm, not amounts. Use BarChart.",
|
|
3125
|
+
"instead": [
|
|
3126
|
+
"BarChart",
|
|
3127
|
+
"AreaChart"
|
|
3128
|
+
]
|
|
3129
|
+
},
|
|
3130
|
+
{
|
|
3131
|
+
"guidance": "dont",
|
|
3132
|
+
"description": "Use it for a single month of marked days with a streak; that is Calendar with modifiers.",
|
|
3133
|
+
"instead": [
|
|
3134
|
+
"Calendar"
|
|
3135
|
+
]
|
|
3136
|
+
},
|
|
3137
|
+
{
|
|
3138
|
+
"guidance": "dont",
|
|
3139
|
+
"description": "Fetch inside the component or call GitHub from the browser with a token. Fetch on your server and pass days in; `@sableui/react/adapters/github` converts GitHub's response."
|
|
3140
|
+
}
|
|
3141
|
+
],
|
|
3142
|
+
"anatomy": [
|
|
3143
|
+
{
|
|
3144
|
+
"name": "Weekday labels",
|
|
3145
|
+
"required": false,
|
|
3146
|
+
"description": "Short weekday names beside the rows, outside the scroller so the weeks never slide under them."
|
|
3147
|
+
},
|
|
3148
|
+
{
|
|
3149
|
+
"name": "Month labels",
|
|
3150
|
+
"required": false,
|
|
3151
|
+
"description": "Short month names over each month's first week; they scroll with their weeks."
|
|
3152
|
+
},
|
|
3153
|
+
{
|
|
3154
|
+
"name": "Cell grid",
|
|
3155
|
+
"required": true,
|
|
3156
|
+
"description": "A role=grid table, one gridcell per day, inside a horizontal Scroller that opens on the latest week."
|
|
3157
|
+
},
|
|
3158
|
+
{
|
|
3159
|
+
"name": "Tooltip",
|
|
3160
|
+
"required": false,
|
|
3161
|
+
"description": "The chart tooltip card, anchored to the hovered, focused or tapped day."
|
|
3162
|
+
},
|
|
3163
|
+
{
|
|
3164
|
+
"name": "Legend",
|
|
3165
|
+
"required": false,
|
|
3166
|
+
"description": "A Less–More ramp for intensity, or swatches for categories."
|
|
3167
|
+
}
|
|
3168
|
+
]
|
|
3169
|
+
},
|
|
3170
|
+
"related": {
|
|
3171
|
+
"components": [
|
|
3172
|
+
"Card",
|
|
3173
|
+
"NumberTicker",
|
|
3174
|
+
"BarChart",
|
|
3175
|
+
"Calendar",
|
|
3176
|
+
"Scroller"
|
|
3177
|
+
]
|
|
3178
|
+
},
|
|
3179
|
+
"props": [
|
|
3180
|
+
{
|
|
3181
|
+
"name": "label",
|
|
3182
|
+
"type": "string",
|
|
3183
|
+
"description": "What the grid shows (\"Contributions in 2026\") — its caption and accessible name."
|
|
3184
|
+
},
|
|
3185
|
+
{
|
|
3186
|
+
"name": "data",
|
|
3187
|
+
"type": "ActivityDay[] | Record<string, number>",
|
|
3188
|
+
"description": "The days: `{ date: \"YYYY-MM-DD\", value, level?, category? }[]`, or a map of date to value. Sparse is fine — a missing day is 0; duplicates sum."
|
|
3189
|
+
},
|
|
3190
|
+
{
|
|
3191
|
+
"name": "start",
|
|
3192
|
+
"type": "string",
|
|
3193
|
+
"default": "53 weeks before end",
|
|
3194
|
+
"description": "First day drawn, \"YYYY-MM-DD\". The default starts at the beginning of that week, as GitHub's does."
|
|
3195
|
+
},
|
|
3196
|
+
{
|
|
3197
|
+
"name": "end",
|
|
3198
|
+
"type": "string",
|
|
3199
|
+
"default": "today",
|
|
3200
|
+
"description": "Last day drawn. Days after today inside the range are drawn hollow and skipped by the keyboard."
|
|
3201
|
+
},
|
|
3202
|
+
{
|
|
3203
|
+
"name": "year",
|
|
3204
|
+
"type": "number",
|
|
3205
|
+
"description": "Draw Jan 1 – Dec 31 of this year. Wins over start / end."
|
|
3206
|
+
},
|
|
3207
|
+
{
|
|
3208
|
+
"name": "weekStart",
|
|
3209
|
+
"type": "0 | 1",
|
|
3210
|
+
"default": "the locale's",
|
|
3211
|
+
"description": "0 puts Sunday on the top row, 1 Monday."
|
|
3212
|
+
},
|
|
3213
|
+
{
|
|
3214
|
+
"name": "levels",
|
|
3215
|
+
"type": "number",
|
|
3216
|
+
"default": "5",
|
|
3217
|
+
"description": "How many levels, including the empty level 0."
|
|
3218
|
+
},
|
|
3219
|
+
{
|
|
3220
|
+
"name": "levelScale",
|
|
3221
|
+
"type": "\"quantile\" | \"linear\" | number[] | ((value, stats) => number)",
|
|
3222
|
+
"default": "\"quantile\"",
|
|
3223
|
+
"description": "How a value becomes a level. Quantile splits the positive values evenly, as GitHub does; an array gives each level's lower bound from 1 up; a function decides. A day's own `level` always wins."
|
|
3224
|
+
},
|
|
3225
|
+
{
|
|
3226
|
+
"name": "tone",
|
|
3227
|
+
"type": "ChartTone",
|
|
3228
|
+
"default": "\"1\"",
|
|
3229
|
+
"description": "The hue of the intensity ramp. Decorative tones lead, as on every chart; \"success\" gives the GitHub look."
|
|
3230
|
+
},
|
|
3231
|
+
{
|
|
3232
|
+
"name": "categories",
|
|
3233
|
+
"type": "ChartCategories",
|
|
3234
|
+
"description": "Colour days by `day.category` (label, tone or colour per key). Pair with `levels={2}` for flat category colours."
|
|
3235
|
+
},
|
|
3236
|
+
{
|
|
3237
|
+
"name": "unit",
|
|
3238
|
+
"type": "string | { one: string; other: string }",
|
|
3239
|
+
"description": "Names the value in the tooltip and each cell's accessible name, pluralised with Intl.PluralRules."
|
|
3240
|
+
},
|
|
3241
|
+
{
|
|
3242
|
+
"name": "formatValue",
|
|
3243
|
+
"type": "(value: number) => string",
|
|
3244
|
+
"default": "full to 9,999, compact past",
|
|
3245
|
+
"description": "Formats a value wherever it is shown or read."
|
|
3246
|
+
},
|
|
3247
|
+
{
|
|
3248
|
+
"name": "tooltip",
|
|
3249
|
+
"type": "boolean | ((day: ActivityHeatmapDay) => ReactNode)",
|
|
3250
|
+
"default": "true",
|
|
3251
|
+
"description": "Show the tooltip on hover, focus and tap; a function replaces its content on the same card."
|
|
3252
|
+
},
|
|
3253
|
+
{
|
|
3254
|
+
"name": "legend",
|
|
3255
|
+
"type": "boolean",
|
|
3256
|
+
"default": "true",
|
|
3257
|
+
"description": "The Less–More ramp for intensity, swatches for categories."
|
|
3258
|
+
},
|
|
3259
|
+
{
|
|
3260
|
+
"name": "legendLabels",
|
|
3261
|
+
"type": "[string, string]",
|
|
3262
|
+
"default": "[\"Less\", \"More\"]",
|
|
3263
|
+
"description": "The ramp's end words."
|
|
3264
|
+
},
|
|
3265
|
+
{
|
|
3266
|
+
"name": "monthLabels",
|
|
3267
|
+
"type": "boolean",
|
|
3268
|
+
"default": "true",
|
|
3269
|
+
"description": "Short month names over each month's first week."
|
|
3270
|
+
},
|
|
3271
|
+
{
|
|
3272
|
+
"name": "weekdayLabels",
|
|
3273
|
+
"type": "\"alternate\" | \"all\" | \"none\"",
|
|
3274
|
+
"default": "\"alternate\"",
|
|
3275
|
+
"description": "Which weekday names to show beside the rows; alternate is Mon, Wed, Fri."
|
|
3276
|
+
},
|
|
3277
|
+
{
|
|
3278
|
+
"name": "size",
|
|
3279
|
+
"type": "\"sm\" | \"md\" | \"lg\"",
|
|
3280
|
+
"default": "\"md\"",
|
|
3281
|
+
"description": "The smallest a cell may get (10, 12, 14 px) before the grid scrolls instead of shrinking."
|
|
3282
|
+
},
|
|
3283
|
+
{
|
|
3284
|
+
"name": "stretch",
|
|
3285
|
+
"type": "boolean",
|
|
3286
|
+
"default": "true",
|
|
3287
|
+
"description": "Grow cells to fill the width, up to 20 px. false keeps them at `size`."
|
|
3288
|
+
},
|
|
3289
|
+
{
|
|
3290
|
+
"name": "activeDate",
|
|
3291
|
+
"type": "string | null",
|
|
3292
|
+
"description": "The hovered or focused day, controlled. Pair with defaultActiveDate / onActiveDateChange."
|
|
3293
|
+
},
|
|
3294
|
+
{
|
|
3295
|
+
"name": "selectedDate",
|
|
3296
|
+
"type": "string | null",
|
|
3297
|
+
"description": "The selected day, controlled. Selection is on only when onSelectDate is set."
|
|
3298
|
+
},
|
|
3299
|
+
{
|
|
3300
|
+
"name": "onSelectDate",
|
|
3301
|
+
"type": "(date: string) => void",
|
|
3302
|
+
"description": "Called when a day is clicked or chosen with Enter / Space — to filter a feed below, as GitHub does."
|
|
3303
|
+
},
|
|
3304
|
+
{
|
|
3305
|
+
"name": "renderCell",
|
|
3306
|
+
"type": "(day, cell) => ReactNode",
|
|
3307
|
+
"description": "Draw something over a cell's fill — an icon for a milestone, a reward, an outage."
|
|
3308
|
+
},
|
|
3309
|
+
{
|
|
3310
|
+
"name": "loading",
|
|
3311
|
+
"type": "boolean",
|
|
3312
|
+
"default": "false",
|
|
3313
|
+
"description": "Draw the empty grid, shimmering, while data loads."
|
|
3314
|
+
},
|
|
3315
|
+
{
|
|
3316
|
+
"name": "motion",
|
|
3317
|
+
"type": "\"full\" | \"subtle\" | \"off\"",
|
|
3318
|
+
"default": "\"full\"",
|
|
3319
|
+
"description": "Turns the arrival down; `subtle` shows the finished grid at once."
|
|
3320
|
+
},
|
|
3321
|
+
{
|
|
3322
|
+
"name": "locale",
|
|
3323
|
+
"type": "string",
|
|
3324
|
+
"default": "runtime locale",
|
|
3325
|
+
"description": "Month and weekday names, numbers and plurals."
|
|
3326
|
+
}
|
|
3327
|
+
],
|
|
3328
|
+
"tokensUsed": [
|
|
3329
|
+
"foreground",
|
|
3330
|
+
"muted-foreground",
|
|
3331
|
+
"border-strong",
|
|
3332
|
+
"ring",
|
|
3333
|
+
"tone-1-mark",
|
|
3334
|
+
"tone-2-mark",
|
|
3335
|
+
"tone-3-mark",
|
|
3336
|
+
"success-mark",
|
|
3337
|
+
"tone-info-mark"
|
|
3338
|
+
],
|
|
3339
|
+
"motion": [
|
|
3340
|
+
{
|
|
3341
|
+
"slot": "arrival",
|
|
3342
|
+
"recipe": "custom",
|
|
3343
|
+
"part": "Cell grid",
|
|
3344
|
+
"why": "Weeks fade in left to right as the grid first scrolls into view, staggered on the chart motion helpers; expressive, so subtle shows the finished grid."
|
|
3345
|
+
},
|
|
3346
|
+
{
|
|
3347
|
+
"slot": "response",
|
|
3348
|
+
"recipe": "custom",
|
|
3349
|
+
"part": "Cell",
|
|
3350
|
+
"why": "The active day takes a ring outline at once and the tooltip glides to it; the other cells stay still — dimming hundreds of cells reads as a flash."
|
|
3351
|
+
},
|
|
3352
|
+
{
|
|
3353
|
+
"slot": "transition",
|
|
3354
|
+
"recipe": "custom",
|
|
3355
|
+
"part": "Cell",
|
|
3356
|
+
"why": "When data or the range changes, each cell's fill cross-fades on the base duration token, so the reader sees which days changed."
|
|
3357
|
+
},
|
|
3358
|
+
{
|
|
3359
|
+
"slot": "activity",
|
|
3360
|
+
"recipe": "motion-shimmer",
|
|
3361
|
+
"part": "Cell grid",
|
|
3362
|
+
"why": "`loading` shimmers the empty grid until days arrive."
|
|
3363
|
+
}
|
|
3364
|
+
],
|
|
3365
|
+
"a11y": [
|
|
3366
|
+
"The grid is a role=grid table, as GitHub's is: weekday row headers and month column headers carry full names for screen readers, and the caption is `label`.",
|
|
3367
|
+
"One tab stop. Arrows move a day or a week; Home / End go to the ends of the row, Page Up / Page Down to the ends of the column, Ctrl+Home / Ctrl+End to the first and last day. Enter or Space selects when onSelectDate is set; Escape hides the tooltip.",
|
|
3368
|
+
"Each cell is named with its date and value (\"Tuesday, October 14, 2026: 12 contributions\") and described by its level's words, which the legend repeats visually.",
|
|
3369
|
+
"Levels rise in lightness, so they read without colour; under forced colours each level draws as a larger inner square."
|
|
3370
|
+
],
|
|
3371
|
+
"examples": [
|
|
3372
|
+
{
|
|
3373
|
+
"label": "Contributions, last year",
|
|
3374
|
+
"code": "const days = [\n\t{ date: \"2026-10-12\", value: 4 },\n\t{ date: \"2026-10-13\", value: 11 },\n\t{ date: \"2026-10-14\", value: 2 },\n]\n\n<ActivityHeatmap label=\"Contributions in the last year\" data={days} tone=\"success\" unit={{ one: \"contribution\", other: \"contributions\" }} />",
|
|
3375
|
+
"imports": "import { ActivityHeatmap } from \"@sableui/react\""
|
|
3376
|
+
},
|
|
3377
|
+
{
|
|
3378
|
+
"label": "Two sources, in a card",
|
|
3379
|
+
"code": "const github = [{ date: \"2026-10-13\", value: 6 }]\nconst codex = [{ date: \"2026-10-13\", value: 1 }, { date: \"2026-10-14\", value: 3 }]\n\n<Card>\n\t<CardHeader>\n\t\t<CardTitle>Building, day by day</CardTitle>\n\t</CardHeader>\n\t<CardContent>\n\t\t<ActivityHeatmap\n\t\t\tlabel=\"GitHub and Codex activity in 2026\"\n\t\t\tyear={2026}\n\t\t\tlevels={2}\n\t\t\tdata={mergeActivity({ github, codex })}\n\t\t\tcategories={{ github: { label: \"GitHub\", tone: \"success\" }, codex: { label: \"Codex\", tone: \"info\" }, both: { label: \"Both\", tone: \"2\" } }}\n\t\t/>\n\t</CardContent>\n</Card>",
|
|
3380
|
+
"imports": "import { ActivityHeatmap, Card, CardContent, CardHeader, CardTitle, mergeActivity } from \"@sableui/react\""
|
|
3381
|
+
},
|
|
3382
|
+
{
|
|
3383
|
+
"label": "Selecting a day",
|
|
3384
|
+
"code": "const [day, setDay] = useState<string | null>(null)\n\n<ActivityHeatmap label=\"Orders per day\" data={{ \"2026-10-13\": 18, \"2026-10-14\": 25 }} unit=\"orders\" selectedDate={day} onSelectDate={setDay} />",
|
|
3385
|
+
"imports": "import { ActivityHeatmap } from \"@sableui/react\""
|
|
3386
|
+
}
|
|
3387
|
+
]
|
|
3388
|
+
},
|
|
2958
3389
|
{
|
|
2959
3390
|
"name": "AppHeader",
|
|
2960
3391
|
"status": "stable",
|
|
@@ -3700,7 +4131,7 @@
|
|
|
3700
4131
|
"group": "Data display",
|
|
3701
4132
|
"register": "either",
|
|
3702
4133
|
"path": "components/avatar",
|
|
3703
|
-
"summary": "Circular profile image over Base UI Avatar — automatic image-load tracking with an initials/glyph fallback. Ergonomic Avatar plus low-level AvatarRoot/AvatarImage/AvatarFallback parts, and AvatarGroup for an overlapping stack with a \"+N\" overflow count.",
|
|
4134
|
+
"summary": "Circular (or square) profile image over Base UI Avatar — automatic image-load tracking with an initials/glyph fallback. Ergonomic Avatar plus low-level AvatarRoot/AvatarImage/AvatarFallback parts, and AvatarGroup for an overlapping stack with a \"+N\" overflow count.",
|
|
3704
4135
|
"keywords": [
|
|
3705
4136
|
"avatar",
|
|
3706
4137
|
"profile",
|
|
@@ -3713,7 +4144,12 @@
|
|
|
3713
4144
|
"stack",
|
|
3714
4145
|
"overflow",
|
|
3715
4146
|
"collaborators",
|
|
3716
|
-
"facepile"
|
|
4147
|
+
"facepile",
|
|
4148
|
+
"monogram",
|
|
4149
|
+
"project mark",
|
|
4150
|
+
"workspace icon",
|
|
4151
|
+
"org logo",
|
|
4152
|
+
"assignee"
|
|
3717
4153
|
],
|
|
3718
4154
|
"usage": {
|
|
3719
4155
|
"description": "Use Avatar to represent a person, team, or account with a compact visual identity. Wrap several in AvatarGroup for collaborators, assignees, or attendees — it overlaps them, rings each in the surface colour, and collapses the rest into a \"+N\" count; use the low-level parts only when you need custom image/fallback layout.",
|
|
@@ -3732,12 +4168,16 @@
|
|
|
3732
4168
|
},
|
|
3733
4169
|
{
|
|
3734
4170
|
"guidance": "do",
|
|
3735
|
-
"description": "Pick the size by the job: sm in dense rows and tables, md (the default) beside one line of text, lg in a row with a title and subtitle, xl (96px) for a profile header beside the person's name, 2xl (160px) for an artist or profile hero and roster portraits. Keep one size within a list or group."
|
|
4171
|
+
"description": "Pick the size by the job: 2xs (20px) or xs (24px) in a compact workspace row (an assignee in an issue list, a project mark in a nav item), sm in dense rows and tables, md (the default) beside one line of text, lg in a row with a title and subtitle, xl (96px) for a profile header beside the person's name, 2xl (160px) for an artist or profile hero and roster portraits. Keep one size within a list or group."
|
|
3736
4172
|
},
|
|
3737
4173
|
{
|
|
3738
4174
|
"guidance": "do",
|
|
3739
4175
|
"description": "Use AvatarGroup for a stack of people, and give it an aria-label with the real count (\"12 collaborators\"). Set `surface` (or `--avatar-group-ring`) to whatever the group sits on, so the separating ring matches a card or popover, not the page. When it shares a row with actions, set `fit` so it gives up avatars to the \"+N\" count before the row has to wrap."
|
|
3740
4176
|
},
|
|
4177
|
+
{
|
|
4178
|
+
"guidance": "do",
|
|
4179
|
+
"description": "Use `shape=\"square\"` for what is not a person (a workspace, a project, an org, a team), with `tone` set to a palette family so each one keeps its own colour: `<Avatar shape=\"square\" tone=\"teal\" fallback=\"N\" alt=\"Northwind storefront\" size=\"xs\" />`. Circles mean people; keep that distinction across the app."
|
|
4180
|
+
},
|
|
3741
4181
|
{
|
|
3742
4182
|
"guidance": "dont",
|
|
3743
4183
|
"description": "Hand-build a stack with negative margins and per-avatar ring classes, or fake the overflow as an Avatar with \"+4\" initials — screen readers hear \"plus 4\" with no context. Use AvatarGroup's `max`/`total`, or AvatarGroupCount."
|
|
@@ -3752,7 +4192,7 @@
|
|
|
3752
4192
|
},
|
|
3753
4193
|
{
|
|
3754
4194
|
"guidance": "dont",
|
|
3755
|
-
"description": "
|
|
4195
|
+
"description": "Resize an Avatar with size classes (`className=\"size-5\"`, `className=\"size-40\"`) — the initials, the status indicator and the image fade stay at the size they were drawn for. Use size=\"2xs\"/\"xs\" for dense rows and size=\"xl\"/\"2xl\" for portraits, which scale all three. Don't hand-build a tinted initials tile for a project or workspace mark either: that is Avatar shape=\"square\" with a tone. Past 160px, or for a cover, it is media: an AspectBox, not an avatar."
|
|
3756
4196
|
}
|
|
3757
4197
|
],
|
|
3758
4198
|
"anatomy": [
|
|
@@ -3824,9 +4264,21 @@
|
|
|
3824
4264
|
},
|
|
3825
4265
|
{
|
|
3826
4266
|
"name": "size",
|
|
3827
|
-
"type": "\"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"",
|
|
4267
|
+
"type": "\"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"",
|
|
3828
4268
|
"default": "\"md\"",
|
|
3829
|
-
"description": "Square footprint: 32px / 40px / 48px for rows; 96px / 160px for portraits (a profile header, an artist hero, a roster tile). The initials, a status indicator and its cut-out ring scale with it, and at the portrait sizes the photo fades in over the resting surface."
|
|
4269
|
+
"description": "Square footprint: 20px / 24px for compact workspace rows; 32px / 40px / 48px for rows; 96px / 160px for portraits (a profile header, an artist hero, a roster tile). The initials, a status indicator and its cut-out ring scale with it, and at the portrait sizes the photo fades in over the resting surface."
|
|
4270
|
+
},
|
|
4271
|
+
{
|
|
4272
|
+
"name": "shape",
|
|
4273
|
+
"type": "\"circle\" | \"square\"",
|
|
4274
|
+
"default": "\"circle\"",
|
|
4275
|
+
"description": "The frame's corner. circle is a person. square is what is not a face — a workspace, a project, an org, a team — with a radius from the theme's steps that grows with the size (4px at 20–24px to 20px at 160px), so a square theme stays square. A status indicator sits on the square's corner."
|
|
4276
|
+
},
|
|
4277
|
+
{
|
|
4278
|
+
"name": "tone",
|
|
4279
|
+
"type": "\"neutral\" | \"red\" | \"orange\" | \"yellow\" | \"green\" | \"teal\" | \"blue\" | \"purple\" | \"pink\"",
|
|
4280
|
+
"default": "\"neutral\"",
|
|
4281
|
+
"description": "The fallback's colour. neutral is the muted surface; a palette family paints its wash behind the initials and its deep ink on them (a soft Badge's pair, held to contrast in every theme), for a category colour such as one per project. Ignored by variant=\"art\", which colours by seed."
|
|
3830
4282
|
},
|
|
3831
4283
|
{
|
|
3832
4284
|
"name": "variant",
|
|
@@ -3843,7 +4295,7 @@
|
|
|
3843
4295
|
"subcomponents": [
|
|
3844
4296
|
{
|
|
3845
4297
|
"name": "AvatarRoot",
|
|
3846
|
-
"type": "Base UI Avatar.Root props + size + className",
|
|
4298
|
+
"type": "Base UI Avatar.Root props + size + shape + tone + className",
|
|
3847
4299
|
"description": "Low-level circular frame for full composition control — clips the image and holds the `size` axis. Wraps AvatarImage and AvatarFallback."
|
|
3848
4300
|
},
|
|
3849
4301
|
{
|
|
@@ -3858,23 +4310,29 @@
|
|
|
3858
4310
|
},
|
|
3859
4311
|
{
|
|
3860
4312
|
"name": "AvatarGroup",
|
|
3861
|
-
"type": "{ size?: \"sm\" | \"md\" | \"lg\"; surface?: \"background\" | \"card\" | \"popover\" | \"muted\"; max?: number; fit?: boolean; total?: number; countLabel?: (count: number) => string; \"aria-label\"?: string; className?: string; children: ReactNode }",
|
|
4313
|
+
"type": "{ size?: \"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\"; surface?: \"background\" | \"card\" | \"popover\" | \"muted\"; max?: number; fit?: boolean; total?: number; countLabel?: (count: number) => string; \"aria-label\"?: string; className?: string; children: ReactNode }",
|
|
3862
4314
|
"description": "Overlapping stack (role=group) — name it with aria-label (\"12 collaborators\"). `size` applies to every avatar inside. `max` shows that many and collapses the rest into a \"+N\" count; `total` is the real head-count when you only pass some people. `fit` shows only as many avatars as fit the width the group is given (it fills the space left in a flex row) and folds the rest into the count, so a tight row reads \"MO SR +5\" instead of wrapping. The separating ring reads the inherited `--avatar-group-ring` (default: the page background); `surface` sets it to card, popover, or muted."
|
|
3863
4315
|
},
|
|
3864
4316
|
{
|
|
3865
4317
|
"name": "AvatarGroupCount",
|
|
3866
|
-
"type": "{ count: number; label?: string; size?: \"sm\" | \"md\" | \"lg\"; className?: string }",
|
|
4318
|
+
"type": "{ count: number; label?: string; size?: \"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\"; className?: string }",
|
|
3867
4319
|
"description": "The \"+N\" avatar. Shows \"+4\" and reads \"and 4 more\" (override with label). AvatarGroup renders it for you from max/total; use it directly only when composing the stack yourself."
|
|
3868
4320
|
}
|
|
3869
4321
|
],
|
|
3870
4322
|
"variants": {
|
|
3871
4323
|
"size": [
|
|
4324
|
+
"2xs",
|
|
4325
|
+
"xs",
|
|
3872
4326
|
"sm",
|
|
3873
4327
|
"md",
|
|
3874
4328
|
"lg",
|
|
3875
4329
|
"xl",
|
|
3876
4330
|
"2xl"
|
|
3877
4331
|
],
|
|
4332
|
+
"shape": [
|
|
4333
|
+
"circle",
|
|
4334
|
+
"square"
|
|
4335
|
+
],
|
|
3878
4336
|
"variant": [
|
|
3879
4337
|
"neutral",
|
|
3880
4338
|
"art"
|
|
@@ -3948,6 +4406,16 @@
|
|
|
3948
4406
|
"code": "<div className=\"flex flex-col items-center gap-3 text-center\">\n\t<Avatar variant=\"art\" size=\"2xl\" fallback=\"LV\" alt=\"Lena Vale\" status={<StatusDot tone=\"success\" />} />\n\t<div className=\"flex flex-col gap-1\">\n\t\t<h1 className=\"text-heading-1\">Lena Vale</h1>\n\t\t<p className=\"text-body-sm text-muted-foreground\">1.2M monthly listeners</p>\n\t</div>\n</div>",
|
|
3949
4407
|
"imports": "import { Avatar, StatusDot } from \"@sableui/react\""
|
|
3950
4408
|
},
|
|
4409
|
+
{
|
|
4410
|
+
"label": "Dense row sizes",
|
|
4411
|
+
"code": "<Avatar fallback=\"EL\" alt=\"Emma Lee\" size=\"2xs\" />\n<Avatar fallback=\"EL\" alt=\"Emma Lee\" size=\"xs\" />",
|
|
4412
|
+
"imports": "import { Avatar } from \"@sableui/react\""
|
|
4413
|
+
},
|
|
4414
|
+
{
|
|
4415
|
+
"label": "Project marks",
|
|
4416
|
+
"code": "<Avatar shape=\"square\" tone=\"teal\" size=\"xs\" fallback=\"N\" alt=\"Northwind storefront\" />\n<Avatar shape=\"square\" tone=\"orange\" size=\"xs\" fallback=\"H\" alt=\"Harbor brand refresh\" />\n<Avatar shape=\"square\" tone=\"purple\" size=\"xs\" fallback=\"L\" alt=\"Lumen editorial\" />",
|
|
4417
|
+
"imports": "import { Avatar } from \"@sableui/react\""
|
|
4418
|
+
},
|
|
3951
4419
|
{
|
|
3952
4420
|
"label": "Low-level parts",
|
|
3953
4421
|
"code": "<AvatarRoot size=\"lg\"><AvatarImage src=\"/pfp.png\" alt=\"Mara Okafor\" /><AvatarFallback>MO</AvatarFallback></AvatarRoot>",
|
|
@@ -7252,6 +7720,12 @@
|
|
|
7252
7720
|
"name": "prompt",
|
|
7253
7721
|
"type": "string",
|
|
7254
7722
|
"description": "A shell prompt (\"$\", \">\"). Makes it a one-line command: the prompt is dimmed and unselectable, the line truncates, and copy takes only code."
|
|
7723
|
+
},
|
|
7724
|
+
{
|
|
7725
|
+
"name": "wrap",
|
|
7726
|
+
"type": "boolean",
|
|
7727
|
+
"default": "false",
|
|
7728
|
+
"description": "Wrap long lines instead of scrolling them. For prose in a code face (a prompt to paste into an agent, a log line); code keeps scrolling, since wrapping breaks its indentation."
|
|
7255
7729
|
}
|
|
7256
7730
|
],
|
|
7257
7731
|
"subcomponents": [
|
|
@@ -7327,7 +7801,12 @@
|
|
|
7327
7801
|
"collapsible",
|
|
7328
7802
|
"disclosure",
|
|
7329
7803
|
"expand",
|
|
7330
|
-
"collapse"
|
|
7804
|
+
"collapse",
|
|
7805
|
+
"chevron",
|
|
7806
|
+
"caret",
|
|
7807
|
+
"rotate",
|
|
7808
|
+
"group header",
|
|
7809
|
+
"section toggle"
|
|
7331
7810
|
],
|
|
7332
7811
|
"usage": {
|
|
7333
7812
|
"description": "Use Collapsible for a single piece of optional content that can expand in place.",
|
|
@@ -7410,8 +7889,8 @@
|
|
|
7410
7889
|
},
|
|
7411
7890
|
{
|
|
7412
7891
|
"name": "CollapsibleIndicator",
|
|
7413
|
-
"type": "{ className? }",
|
|
7414
|
-
"description": "Auto-rotating disclosure chevron. Place it inside the trigger content
|
|
7892
|
+
"type": "{ direction?: \"down\" | \"end\"; className? }",
|
|
7893
|
+
"description": "Auto-rotating disclosure chevron. Place it anywhere inside the trigger content; it reads the trigger's open state itself, no class to add. direction=\"down\" (default) flips 180° on open; direction=\"end\" points to the inline end while closed and turns down on open — the caret for a grouped list's section header or a tree row. Use it instead of rotating your own chevron with CSS, which snaps with no transition."
|
|
7415
7894
|
}
|
|
7416
7895
|
],
|
|
7417
7896
|
"tokensUsed": [
|
|
@@ -7441,7 +7920,7 @@
|
|
|
7441
7920
|
"slot": "transition",
|
|
7442
7921
|
"recipe": "motion-interactive",
|
|
7443
7922
|
"part": "CollapsibleIndicator",
|
|
7444
|
-
"why": "The chevron turns with the panel rather than flipping in one frame: motion-interactive lists `rotate`, the property rotate-180
|
|
7923
|
+
"why": "The chevron turns with the panel rather than flipping in one frame: motion-interactive lists `rotate`, the property rotate-180 and rotate-0 write."
|
|
7445
7924
|
},
|
|
7446
7925
|
{
|
|
7447
7926
|
"slot": "activity",
|
|
@@ -7469,7 +7948,12 @@
|
|
|
7469
7948
|
},
|
|
7470
7949
|
{
|
|
7471
7950
|
"label": "With rotating chevron",
|
|
7472
|
-
"code": "<Collapsible><CollapsibleTrigger
|
|
7951
|
+
"code": "<Collapsible><CollapsibleTrigger render={<Button variant=\"ghost\">Details <CollapsibleIndicator /></Button>} /><CollapsiblePanel><p className=\"pt-3\">Revealed on toggle.</p></CollapsiblePanel></Collapsible>",
|
|
7952
|
+
"imports": "import { Button, Collapsible, CollapsibleIndicator, CollapsiblePanel, CollapsibleTrigger } from \"@sableui/react\""
|
|
7953
|
+
},
|
|
7954
|
+
{
|
|
7955
|
+
"label": "Group header with a side caret",
|
|
7956
|
+
"code": "<Collapsible defaultOpen><CollapsibleTrigger render={<Button variant=\"ghost\" size=\"xs\"><CollapsibleIndicator direction=\"end\" />In progress <span className=\"text-ui-num text-muted-foreground\">5</span></Button>} /><CollapsiblePanel><ul className=\"text-ui-body\"><li>Refine product detail page</li><li>Build the ingredient library</li></ul></CollapsiblePanel></Collapsible>",
|
|
7473
7957
|
"imports": "import { Button, Collapsible, CollapsibleIndicator, CollapsiblePanel, CollapsibleTrigger } from \"@sableui/react\""
|
|
7474
7958
|
}
|
|
7475
7959
|
]
|
|
@@ -8783,7 +9267,7 @@
|
|
|
8783
9267
|
"group": "Data display",
|
|
8784
9268
|
"register": "either",
|
|
8785
9269
|
"path": "components/data-table",
|
|
8786
|
-
"summary": "A data-driven table over the Table primitives — a columns def + data array with built-in client-side sorting (click a sortable header to cycle asc → desc → none), optional row click / selection, and an empty state. For server data or paging, sort/page the data and pair with Pagination.",
|
|
9270
|
+
"summary": "A data-driven table over the Table primitives — a columns def + data array with built-in client-side sorting (click a sortable header to cycle asc → desc → none), optional row click / selection, rows grouped under collapsible header rows (groupBy), and an empty state. For server data or paging, sort/page the data and pair with Pagination.",
|
|
8787
9271
|
"keywords": [
|
|
8788
9272
|
"data table",
|
|
8789
9273
|
"table",
|
|
@@ -8792,7 +9276,14 @@
|
|
|
8792
9276
|
"rows",
|
|
8793
9277
|
"empty state",
|
|
8794
9278
|
"pagination",
|
|
8795
|
-
"list"
|
|
9279
|
+
"list",
|
|
9280
|
+
"grouped",
|
|
9281
|
+
"group by",
|
|
9282
|
+
"sections",
|
|
9283
|
+
"issue list",
|
|
9284
|
+
"issue tracker",
|
|
9285
|
+
"status groups",
|
|
9286
|
+
"collapsible groups"
|
|
8796
9287
|
],
|
|
8797
9288
|
"usage": {
|
|
8798
9289
|
"description": "Use DataTable when a data array and column definition should render a sortable, semantic table with optional row actions and an empty state. Use the lower-level Table parts for hand-authored markup or highly custom row layouts.",
|
|
@@ -8815,7 +9306,11 @@
|
|
|
8815
9306
|
},
|
|
8816
9307
|
{
|
|
8817
9308
|
"guidance": "do",
|
|
8818
|
-
"description": "
|
|
9309
|
+
"description": "Group rows by status for an issue tracker (or by owner, by day) with groupBy + groups on ONE DataTable: every group shares the header and the column widths, each header row carries the group's count and an add action, and sorting applies within each group. Do not stack one DataTable per group inside Collapsibles; the header repeats and the columns stop lining up."
|
|
9310
|
+
},
|
|
9311
|
+
{
|
|
9312
|
+
"guidance": "do",
|
|
9313
|
+
"description": "Move to TanStack Table, rendered with Sable's Table parts, once the table needs server-side sorting or filtering, column visibility or resizing, nested or aggregated grouping, or virtualised rows. DataTable stays dependency-free and does not grow into a data grid; `sable docs tables-with-tanstack` maps one onto the other."
|
|
8819
9314
|
},
|
|
8820
9315
|
{
|
|
8821
9316
|
"guidance": "dont",
|
|
@@ -8842,6 +9337,11 @@
|
|
|
8842
9337
|
"required": false,
|
|
8843
9338
|
"description": "Content spanning all columns when the row array is empty."
|
|
8844
9339
|
},
|
|
9340
|
+
{
|
|
9341
|
+
"name": "Group header row",
|
|
9342
|
+
"required": false,
|
|
9343
|
+
"description": "With groupBy: one row per group heading its own tbody (a th with scope=rowgroup across every column) holding the disclosure toggle, the group's icon, label and count, and an optional trailing action."
|
|
9344
|
+
},
|
|
8845
9345
|
{
|
|
8846
9346
|
"name": "Caption",
|
|
8847
9347
|
"required": false,
|
|
@@ -8925,6 +9425,32 @@
|
|
|
8925
9425
|
"name": "caption",
|
|
8926
9426
|
"type": "ReactNode",
|
|
8927
9427
|
"description": "Bottom table caption."
|
|
9428
|
+
},
|
|
9429
|
+
{
|
|
9430
|
+
"name": "groupBy",
|
|
9431
|
+
"type": "(row) => string",
|
|
9432
|
+
"description": "Group the rows under header rows by the key this returns (a status, an owner, a day). One header serves every group, so the columns line up across them; sorting applies within each group."
|
|
9433
|
+
},
|
|
9434
|
+
{
|
|
9435
|
+
"name": "groups",
|
|
9436
|
+
"type": "{ key: string; label?: ReactNode; icon?: ReactNode; action?: ReactNode; defaultExpanded?: boolean }[]",
|
|
9437
|
+
"description": "Order and presentation of the groups, by key: the header row's label (defaults to the key), a leading icon such as a status mark, a trailing action such as an icon Button that adds a row to the group, and whether it starts open (default true). Groups groupBy returns that are not listed follow, in the order their first row appears in data. A listed group with no rows is not shown."
|
|
9438
|
+
},
|
|
9439
|
+
{
|
|
9440
|
+
"name": "collapsible",
|
|
9441
|
+
"type": "boolean",
|
|
9442
|
+
"default": "true",
|
|
9443
|
+
"description": "Whether each group's header row is a toggle that opens and closes the group. A closed group keeps its header row and count; its rows are hidden and select-all skips them."
|
|
9444
|
+
},
|
|
9445
|
+
{
|
|
9446
|
+
"name": "expandedGroups",
|
|
9447
|
+
"type": "ReadonlySet<string>",
|
|
9448
|
+
"description": "The open groups, by key (controlled). Without it each group starts at its defaultExpanded and the table holds the state."
|
|
9449
|
+
},
|
|
9450
|
+
{
|
|
9451
|
+
"name": "onExpandedGroupsChange",
|
|
9452
|
+
"type": "(keys: Set<string>) => void",
|
|
9453
|
+
"description": "Called with the new set of open group keys when a group header is toggled."
|
|
8928
9454
|
}
|
|
8929
9455
|
],
|
|
8930
9456
|
"tokensUsed": [
|
|
@@ -8957,6 +9483,18 @@
|
|
|
8957
9483
|
"part": "Row",
|
|
8958
9484
|
"why": "A checked row's fill eases in across its cells, so selecting by checkbox reads as the row being picked rather than a band stamped over it."
|
|
8959
9485
|
},
|
|
9486
|
+
{
|
|
9487
|
+
"slot": "transition",
|
|
9488
|
+
"recipe": "motion-interactive",
|
|
9489
|
+
"part": "Group chevron",
|
|
9490
|
+
"why": "A group's chevron turns from pointing along the reading direction (closed) to down (open), so the group reads as folding rather than a glyph being swapped."
|
|
9491
|
+
},
|
|
9492
|
+
{
|
|
9493
|
+
"slot": "arrival",
|
|
9494
|
+
"recipe": "none",
|
|
9495
|
+
"part": "Group rows",
|
|
9496
|
+
"why": "Opening a group shows its rows at once: table rows cannot animate their height without leaving the table layout, and the rows are the content the reader asked for."
|
|
9497
|
+
},
|
|
8960
9498
|
{
|
|
8961
9499
|
"slot": "activity",
|
|
8962
9500
|
"recipe": "none",
|
|
@@ -8967,7 +9505,8 @@
|
|
|
8967
9505
|
"a11y": [
|
|
8968
9506
|
"Sorted columns set aria-sort=ascending/descending on the header cell.",
|
|
8969
9507
|
"Sortable headers are real <button>s with a focus-visible ring, keyboard-operable.",
|
|
8970
|
-
"Inherits the Table's scrollable frame and semantic <table>/<th>/<td> structure."
|
|
9508
|
+
"Inherits the Table's scrollable frame and semantic <table>/<th>/<td> structure.",
|
|
9509
|
+
"Grouped, each group is its own <tbody> (a rowgroup) headed by a <th scope=\"rowgroup\"> spanning the columns, so group headers are never read as data rows; the toggle is a real button with aria-expanded and aria-controls naming that tbody."
|
|
8971
9510
|
],
|
|
8972
9511
|
"examples": [
|
|
8973
9512
|
{
|
|
@@ -8995,6 +9534,11 @@
|
|
|
8995
9534
|
"code": "<Card><CardHeader><CardTitle>Deployments</CardTitle></CardHeader><CardContent flush><DataTable data={deployments} rowKey={(row) => row.id} columns={[{ key: \"project\", header: \"Project\", sortable: true }, { key: \"duration\", header: \"Duration\", align: \"end\" }]} /></CardContent></Card>",
|
|
8996
9535
|
"imports": "import { Card, CardContent, CardHeader, CardTitle, DataTable } from \"@sableui/react\""
|
|
8997
9536
|
},
|
|
9537
|
+
{
|
|
9538
|
+
"label": "Issues grouped by status",
|
|
9539
|
+
"code": "const STATUSES = [\n\t{ key: \"in-progress\", label: \"In progress\", icon: <StatusDot tone=\"warning\" /> },\n\t{ key: \"in-review\", label: \"In review\", icon: <StatusDot tone=\"purple\" /> },\n\t{ key: \"todo\", label: \"Todo\", icon: <StatusDot /> },\n\t{ key: \"done\", label: \"Done\", icon: <StatusDot tone=\"success\" />, defaultExpanded: false },\n]\n\n<DataTable\n\tdata={issues}\n\trowKey={(t) => t.id}\n\tgroupBy={(t) => t.status}\n\tgroups={STATUSES.map((s) => ({ ...s, action: <Button variant=\"ghost\" size=\"icon-xs\" aria-label={`Add issue to ${s.label}`} onClick={() => create(s.key)}><Icon name=\"plus\" /></Button> }))}\n\tonRowClick={(t) => setSelectedId(t.id)}\n\tisRowSelected={(t) => t.id === selectedId}\n\tcolumns={[\n\t\t{ key: \"id\", header: \"ID\", className: \"w-20\", cell: (t) => <span className=\"text-mono-caption text-muted-foreground\">{t.id}</span> },\n\t\t{ key: \"title\", header: \"Issue\", sortable: true },\n\t\t{ key: \"due\", header: \"Due\", sortable: true, sortValue: (t) => t.due.getTime(), cell: (t) => formatDate(t.due) },\n\t]}\n/>",
|
|
9540
|
+
"imports": "import { Button, DataTable, Icon, StatusDot } from \"@sableui/react\""
|
|
9541
|
+
},
|
|
8998
9542
|
{
|
|
8999
9543
|
"label": "Empty state",
|
|
9000
9544
|
"code": "<DataTable data={[]} columns={[{ key: \"name\", header: \"Name\" }, { key: \"status\", header: \"Status\" }]} empty={<EmptyState icon={<Icon name=\"search\" />} title=\"No deployments\" description=\"Try a different filter or create a deployment.\" action={<Button>Create deployment</Button>} />} />",
|
|
@@ -9011,6 +9555,9 @@
|
|
|
9011
9555
|
"summary": "Date (and optionally time) picker: a field-shaped trigger (the Input / SelectTrigger shell) opens a Popover holding Sable's Calendar. With granularity=\"day\" selecting a day fires onValueChange and closes; granularity=\"minute\" adds an hour + minute row of Selects and keeps the time-of-day when the day changes.",
|
|
9012
9556
|
"keywords": [
|
|
9013
9557
|
"date picker",
|
|
9558
|
+
"due date",
|
|
9559
|
+
"property row",
|
|
9560
|
+
"inspector",
|
|
9014
9561
|
"calendar",
|
|
9015
9562
|
"date input",
|
|
9016
9563
|
"date time picker",
|
|
@@ -9110,6 +9657,17 @@
|
|
|
9110
9657
|
"type": "Date",
|
|
9111
9658
|
"description": "Latest selectable day (inclusive). Later days are disabled and navigation is bounded."
|
|
9112
9659
|
},
|
|
9660
|
+
{
|
|
9661
|
+
"name": "size",
|
|
9662
|
+
"type": "\"md\" | \"xs\"",
|
|
9663
|
+
"default": "\"md\"",
|
|
9664
|
+
"description": "The trigger's rung. md is the form field beside Input and Select. xs is the property-grid rung (24px in a workspace): an inset panel-field well with no border, the same shape as SelectTrigger size=\"xs\" and ScrubField on the neighbouring rows of an inspector — use it for a due date in a PropertyRow instead of a native date input."
|
|
9665
|
+
},
|
|
9666
|
+
{
|
|
9667
|
+
"name": "formatOptions",
|
|
9668
|
+
"type": "Intl.DateTimeFormatOptions",
|
|
9669
|
+
"description": "How the trigger writes the chosen date, as Intl.DateTimeFormat options in the reader's locale — { month: \"short\", day: \"numeric\" } for \"Oct 23\" where the rest of a list writes dates that way. Omitted: \"Oct 23, 2026\" (with \"at 14:30\" under granularity=\"minute\")."
|
|
9670
|
+
},
|
|
9113
9671
|
{
|
|
9114
9672
|
"name": "className",
|
|
9115
9673
|
"type": "string",
|
|
@@ -9143,6 +9701,11 @@
|
|
|
9143
9701
|
"With granularity=\"minute\" the hour and minute Selects are full Base UI listboxes (typeahead, ARIA) labelled \"Hour\" and \"Minute\"; selecting a day keeps the chosen time-of-day, so day and time can be set in any order."
|
|
9144
9702
|
],
|
|
9145
9703
|
"examples": [
|
|
9704
|
+
{
|
|
9705
|
+
"label": "In an inspector row",
|
|
9706
|
+
"code": "<PanelSection title=\"Properties\">\n\t<PropertyRow label=\"Due date\">\n\t\t<DatePicker size=\"xs\" aria-label=\"Due date\" value={due} onValueChange={setDue} formatOptions={{ month: \"short\", day: \"numeric\" }} />\n\t</PropertyRow>\n</PanelSection>",
|
|
9707
|
+
"imports": "import { DatePicker, PanelSection, PropertyRow } from \"@sableui/react\""
|
|
9708
|
+
},
|
|
9146
9709
|
{
|
|
9147
9710
|
"label": "Controlled",
|
|
9148
9711
|
"code": "const [date, setDate] = useState<Date>()\n<DatePicker value={date} onValueChange={setDate} />",
|
|
@@ -9501,6 +10064,18 @@
|
|
|
9501
10064
|
"InlineStatus"
|
|
9502
10065
|
]
|
|
9503
10066
|
},
|
|
10067
|
+
{
|
|
10068
|
+
"guidance": "do",
|
|
10069
|
+
"description": "Open a dialog from your own button with controlled open/onOpenChange when the trigger is not a DialogTrigger (a menu item, a row action, a keyboard shortcut). Focus returns on close to whatever held it when the dialog opened, with no finalFocus wiring; pass finalFocus only to send it somewhere else."
|
|
10070
|
+
},
|
|
10071
|
+
{
|
|
10072
|
+
"guidance": "do",
|
|
10073
|
+
"description": "Set keepMounted on DialogContent for a draft the user may come back to (a compose form, a long comment, a half-filled request). Closing by Esc or a stray click then hides the dialog instead of unmounting it, so the fields are still filled when it reopens. Clear the draft yourself on submit or discard."
|
|
10074
|
+
},
|
|
10075
|
+
{
|
|
10076
|
+
"guidance": "dont",
|
|
10077
|
+
"description": "Set keepMounted by default. A dialog that should open fresh (a confirmation, a create form after a successful save) would show stale input, and live content (polling, video) keeps running while hidden."
|
|
10078
|
+
},
|
|
9504
10079
|
{
|
|
9505
10080
|
"guidance": "do",
|
|
9506
10081
|
"description": "Use the AlertDialog family for a confirmation that must be answered (discard changes, leave without saving). It looks identical but is role=\"alertdialog\" and a backdrop click does not dismiss it — only Cancel, Esc, or the action. For irreversible deletes, use DestructiveActionModal, which is built on it."
|
|
@@ -9554,14 +10129,30 @@
|
|
|
9554
10129
|
{
|
|
9555
10130
|
"name": "Dialog (Root)",
|
|
9556
10131
|
"type": "Base UI Dialog.Root props (open, defaultOpen, onOpenChange, modal)",
|
|
9557
|
-
"description": "Controls open state. Uncontrolled by default."
|
|
10132
|
+
"description": "Controls open state. Uncontrolled by default. Records the element that holds focus when the dialog opens (through a DialogTrigger, or your own button flipping a controlled open) and returns focus there on close."
|
|
10133
|
+
},
|
|
10134
|
+
{
|
|
10135
|
+
"name": "open",
|
|
10136
|
+
"type": "boolean",
|
|
10137
|
+
"description": "Controlled open state. Pair with onOpenChange — for a dialog opened from your own button instead of a DialogTrigger."
|
|
10138
|
+
},
|
|
10139
|
+
{
|
|
10140
|
+
"name": "defaultOpen",
|
|
10141
|
+
"type": "boolean",
|
|
10142
|
+
"default": "false",
|
|
10143
|
+
"description": "Initial open state when uncontrolled."
|
|
10144
|
+
},
|
|
10145
|
+
{
|
|
10146
|
+
"name": "onOpenChange",
|
|
10147
|
+
"type": "(open: boolean, eventDetails) => void",
|
|
10148
|
+
"description": "Called when the dialog asks to open or close (a trigger, Esc, an outside click, DialogClose)."
|
|
9558
10149
|
}
|
|
9559
10150
|
],
|
|
9560
10151
|
"subcomponents": [
|
|
9561
10152
|
{
|
|
9562
10153
|
"name": "DialogContent",
|
|
9563
|
-
"type": "Base UI Popup props + size?: \"default\" | \"fullscreen\" + showCloseButton?: boolean + scroll?: boolean + className",
|
|
9564
|
-
"description": "Backdrop + popup; portals to <body>. Composes DialogOverlay by default. size=\"fullscreen\" is the media viewer: a near-opaque scrim (bg-overlay/95), an edge-to-edge popup that centres its children with no card chrome, motion-fade instead of motion-pop, and a safe-area-aware floating close button (Button variant=\"floating\"); at that size showCloseButton defaults to true and scroll to false, and the media fits itself with max-h-full max-w-full object-contain. At the default size, showCloseButton (defaults to false) renders a built-in close (X) button in the top-end corner, and the header leaves room for it (Icon name=\"xmark\"). The popup caps at the viewport height less a 1rem margin and wraps its children in a scrolling region, so a tall dialog scrolls instead of overflowing a viewport whose own scroll is locked; pass scroll={false} when the dialog owns its internal layout (a pinned header over a scrolling body) and supplies its own min-h-0 scroll region."
|
|
10154
|
+
"type": "Base UI Popup props (initialFocus, finalFocus) + size?: \"default\" | \"fullscreen\" + showCloseButton?: boolean + scroll?: boolean + keepMounted?: boolean + className",
|
|
10155
|
+
"description": "Backdrop + popup; portals to <body>. Composes DialogOverlay by default. size=\"fullscreen\" is the media viewer: a near-opaque scrim (bg-overlay/95), an edge-to-edge popup that centres its children with no card chrome, motion-fade instead of motion-pop, and a safe-area-aware floating close button (Button variant=\"floating\"); at that size showCloseButton defaults to true and scroll to false, and the media fits itself with max-h-full max-w-full object-contain. At the default size, showCloseButton (defaults to false) renders a built-in close (X) button in the top-end corner, and the header leaves room for it (Icon name=\"xmark\"). The popup caps at the viewport height less a 1rem margin and wraps its children in a scrolling region, so a tall dialog scrolls instead of overflowing a viewport whose own scroll is locked; pass scroll={false} when the dialog owns its internal layout (a pinned header over a scrolling body) and supplies its own min-h-0 scroll region. keepMounted (default false) keeps the content mounted and hidden while closed, so a draft survives close and reopen. finalFocus defaults to the element focused when the dialog opened, trigger or not; an explicit finalFocus (a ref, a function, or false) wins."
|
|
9565
10156
|
},
|
|
9566
10157
|
{
|
|
9567
10158
|
"name": "DialogOverlay",
|
|
@@ -9601,7 +10192,7 @@
|
|
|
9601
10192
|
{
|
|
9602
10193
|
"name": "AlertDialog (Root)",
|
|
9603
10194
|
"type": "Base UI AlertDialog.Root props (open, defaultOpen, onOpenChange)",
|
|
9604
|
-
"description": "Root for a confirmation that needs an answer. Always modal; the popup is role=\"alertdialog\" and a click outside does not dismiss — only an AlertDialogClose, an action that sets open to false, or Esc."
|
|
10195
|
+
"description": "Root for a confirmation that needs an answer. Always modal; the popup is role=\"alertdialog\" and a click outside does not dismiss — only an AlertDialogClose, an action that sets open to false, or Esc. Returns focus on close to the element focused when it opened, exactly as Dialog does."
|
|
9605
10196
|
},
|
|
9606
10197
|
{
|
|
9607
10198
|
"name": "AlertDialogTrigger",
|
|
@@ -9666,6 +10257,8 @@
|
|
|
9666
10257
|
"a11y": [
|
|
9667
10258
|
"Base UI provides focus trapping, scroll lock, aria-modal, and labelled-by wiring (DialogTitle).",
|
|
9668
10259
|
"Content taller than the viewport scrolls inside the dialog (thin scrollbar, contained overscroll) rather than overflowing off screen — the page behind is scroll-locked, so anything past the edge would be unreachable.",
|
|
10260
|
+
"On close, focus returns to the element that held it when the dialog opened, whether a DialogTrigger opened it or your own button set a controlled open. It is captured before the content mounts, so an autoFocus field inside cannot displace it. If that element has left the page, Base UI's default (the trigger, if any) applies; an explicit finalFocus on the content always wins. AlertDialog and Sheet behave the same.",
|
|
10261
|
+
"keepMounted hides closed content with the hidden attribute, so a kept draft is out of the accessibility tree and the tab order until the dialog reopens.",
|
|
9669
10262
|
"Esc and click-outside dismiss by default. AlertDialog keeps Esc but ignores click-outside, and its popup is role=\"alertdialog\".",
|
|
9670
10263
|
"Animate via data-[starting-style]/data-[ending-style] attributes.",
|
|
9671
10264
|
"size=\"fullscreen\" has no title chrome, so name the popup yourself: aria-label on DialogContent (or a visually hidden DialogTitle), plus alt text on the media — the zoomed image is a separate image to assistive technology. Its built-in close button carries aria-label=\"Close\" and clears the safe-area insets (notch, home indicator)."
|
|
@@ -9691,6 +10284,11 @@
|
|
|
9691
10284
|
"code": "<Dialog><DialogTrigger render={<Button variant=\"secondary\">Rename collection</Button>} /><DialogContent showCloseButton><DialogHeader><DialogTitle>Rename collection</DialogTitle><DialogDescription>Update the display name shown to collectors.</DialogDescription></DialogHeader><Form><Field label=\"Collection name\"><Input defaultValue=\"Midnight Archive\" /></Field><DialogFooter><DialogClose render={<Button variant=\"secondary\">Cancel</Button>} /><Button type=\"submit\">Save changes</Button></DialogFooter></Form></DialogContent></Dialog>",
|
|
9692
10285
|
"imports": "import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Field, Form, Input } from \"@sableui/react\""
|
|
9693
10286
|
},
|
|
10287
|
+
{
|
|
10288
|
+
"label": "Opened from your own button, keeping a draft",
|
|
10289
|
+
"code": "const [open, setOpen] = useState(false)\n\n<Button variant=\"secondary\" onClick={() => setOpen(true)}>New message</Button>\n<Dialog open={open} onOpenChange={setOpen}>\n\t<DialogContent keepMounted showCloseButton>\n\t\t<DialogHeader>\n\t\t\t<DialogTitle>New message</DialogTitle>\n\t\t\t<DialogDescription>Close it any time. Your draft is here when you come back.</DialogDescription>\n\t\t</DialogHeader>\n\t\t<Field label=\"Message\"><Textarea /></Field>\n\t\t<DialogFooter>\n\t\t\t<DialogClose render={<Button variant=\"secondary\">Close</Button>} />\n\t\t\t<Button onClick={() => setOpen(false)}>Send</Button>\n\t\t</DialogFooter>\n\t</DialogContent>\n</Dialog>",
|
|
10290
|
+
"imports": "import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Textarea } from \"@sableui/react\""
|
|
10291
|
+
},
|
|
9694
10292
|
{
|
|
9695
10293
|
"label": "Destructive confirmation",
|
|
9696
10294
|
"code": "<AlertDialog><AlertDialogTrigger render={<Button variant=\"destructive\">Delete project</Button>} /><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>Delete project?</AlertDialogTitle><AlertDialogDescription>This removes the project and cannot be undone.</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogClose render={<Button variant=\"secondary\">Cancel</Button>} /><Button variant=\"destructive\" onClick={deleteProject}>Delete project</Button></AlertDialogFooter></AlertDialogContent></AlertDialog>",
|
|
@@ -12865,6 +13463,10 @@
|
|
|
12865
13463
|
"guidance": "do",
|
|
12866
13464
|
"description": "Leave `alt` empty when a visible title sits next to the image — a tile, a row — and describe it when the image stands alone. A set `alt` also names the fallback, so the description survives a broken URL."
|
|
12867
13465
|
},
|
|
13466
|
+
{
|
|
13467
|
+
"guidance": "do",
|
|
13468
|
+
"description": "Set `position` when a cover crop would cut off the subject — a face high in a portrait, a product at one edge. Store the focus with the image (`\"35% 50%\"`) rather than re-cropping the file."
|
|
13469
|
+
},
|
|
12868
13470
|
{
|
|
12869
13471
|
"guidance": "dont",
|
|
12870
13472
|
"description": "Do not hand-write `<img onError={() => setBroken(true)}>`. An image that fails before hydration never fires the handler on a prerendered page; MediaImage preloads off the DOM and catches it."
|
|
@@ -12911,6 +13513,12 @@
|
|
|
12911
13513
|
"default": "\"cover\"",
|
|
12912
13514
|
"description": "`cover` crops to fill (artwork, photos); `contain` letterboxes on the muted surface (a logo, a product that must not be cropped)."
|
|
12913
13515
|
},
|
|
13516
|
+
{
|
|
13517
|
+
"name": "position",
|
|
13518
|
+
"type": "CSSProperties[\"objectPosition\"]",
|
|
13519
|
+
"default": "\"center\"",
|
|
13520
|
+
"description": "Where the image sits in its frame — CSS `object-position`, the sibling of `fit`. Under `cover` it is the part the crop keeps (`\"50% 30%\"` for a face, `\"bottom\"` for a horizon); under `contain` it is the side the letterbox gives way to."
|
|
13521
|
+
},
|
|
12914
13522
|
{
|
|
12915
13523
|
"name": "srcSet",
|
|
12916
13524
|
"type": "string",
|
|
@@ -13000,6 +13608,11 @@
|
|
|
13000
13608
|
"label": "Logo, never cropped",
|
|
13001
13609
|
"code": "<AspectBox ratio={16 / 9} className=\"rounded-lg\">\n\t<MediaImage src={brand.logo} alt={brand.name} fit=\"contain\" />\n</AspectBox>",
|
|
13002
13610
|
"imports": "import { AspectBox, MediaImage } from \"@sableui/react\""
|
|
13611
|
+
},
|
|
13612
|
+
{
|
|
13613
|
+
"label": "Crop focus per image",
|
|
13614
|
+
"code": "<AspectBox ratio={1} className=\"rounded-md\">\n\t<MediaImage src={project.thumbnail} seed={project.title} position={project.focus ?? \"center\"} />\n</AspectBox>",
|
|
13615
|
+
"imports": "import { AspectBox, MediaImage } from \"@sableui/react\""
|
|
13003
13616
|
}
|
|
13004
13617
|
]
|
|
13005
13618
|
},
|
|
@@ -13139,6 +13752,12 @@
|
|
|
13139
13752
|
"type": "string | number",
|
|
13140
13753
|
"description": "Seeds the fallback art. Defaults to `title` when it is a string."
|
|
13141
13754
|
},
|
|
13755
|
+
{
|
|
13756
|
+
"name": "position",
|
|
13757
|
+
"type": "CSSProperties[\"objectPosition\"]",
|
|
13758
|
+
"default": "\"center\"",
|
|
13759
|
+
"description": "Which part of the cover the crop keeps — CSS `object-position`, forwarded to the cover's MediaImage: `\"50% 30%\"` for a face, `\"top\"` for a poster's title. Store it with the image when each one frames differently."
|
|
13760
|
+
},
|
|
13142
13761
|
{
|
|
13143
13762
|
"name": "aspect",
|
|
13144
13763
|
"type": "\"square\" | \"video\" | \"poster\" | number",
|
|
@@ -13273,6 +13892,11 @@
|
|
|
13273
13892
|
"code": "<Grid minItemWidth=\"8rem\" gap={2}>\n\t{photos.map((photo) => (\n\t\t<MediaTile key={photo.id} size=\"sm\" title={photo.caption} src={photo.url} alt={photo.description} />\n\t))}\n</Grid>",
|
|
13274
13893
|
"imports": "import { Grid, MediaTile } from \"@sableui/react\""
|
|
13275
13894
|
},
|
|
13895
|
+
{
|
|
13896
|
+
"label": "Portfolio grid with crop focus",
|
|
13897
|
+
"code": "<Grid minItemWidth=\"16rem\" gap={4}>\n\t{projects.map((project) => (\n\t\t<MediaTile\n\t\t\tkey={project.slug}\n\t\t\taspect=\"video\"\n\t\t\ttitle={project.title}\n\t\t\tsubtitle={project.client}\n\t\t\tsrc={project.thumbnail}\n\t\t\tposition={project.focus}\n\t\t\thref={`/work/${project.slug}`}\n\t\t/>\n\t))}\n</Grid>",
|
|
13898
|
+
"imports": "import { Grid, MediaTile } from \"@sableui/react\""
|
|
13899
|
+
},
|
|
13276
13900
|
{
|
|
13277
13901
|
"label": "Poster shelf",
|
|
13278
13902
|
"code": "<MediaTile aspect=\"poster\" size=\"lg\" titleAs=\"h3\" title=\"The Long Dark\" subtitle=\"2026 · Drama\" href=\"/films/the-long-dark\" />",
|
|
@@ -13400,8 +14024,8 @@
|
|
|
13400
14024
|
},
|
|
13401
14025
|
{
|
|
13402
14026
|
"name": "MenuSubmenuTrigger",
|
|
13403
|
-
"type": "Base UI Menu.SubmenuTrigger props + value: ReactNode + className",
|
|
13404
|
-
"description": "The row that opens a submenu: item styling, a trailing chevron, and an optional `value` showing the current choice so the row reads \"Effort High ›\" rather than making the reader open it to find out."
|
|
14027
|
+
"type": "Base UI Menu.SubmenuTrigger props + value: ReactNode + icon: ReactNode + className",
|
|
14028
|
+
"description": "The row that opens a submenu: item styling, a trailing chevron, and an optional `value` showing the current choice so the row reads \"Effort High ›\" rather than making the reader open it to find out. `icon` leads the row as MenuItem's does; give it one whenever the items around it have icons, or its label starts a column to their left."
|
|
13405
14029
|
},
|
|
13406
14030
|
{
|
|
13407
14031
|
"name": "MenuRadioGroup",
|
|
@@ -15485,7 +16109,11 @@
|
|
|
15485
16109
|
"page header",
|
|
15486
16110
|
"title",
|
|
15487
16111
|
"lede",
|
|
15488
|
-
"page actions"
|
|
16112
|
+
"page actions",
|
|
16113
|
+
"page title size",
|
|
16114
|
+
"display title",
|
|
16115
|
+
"editorial header",
|
|
16116
|
+
"hero title"
|
|
15489
16117
|
],
|
|
15490
16118
|
"usage": {
|
|
15491
16119
|
"description": "Use PageHeader to establish a page's title, supporting description, and primary page actions.",
|
|
@@ -15498,6 +16126,14 @@
|
|
|
15498
16126
|
"guidance": "do",
|
|
15499
16127
|
"description": "Keep the title literal and put explanation in the lede or nearby content."
|
|
15500
16128
|
},
|
|
16129
|
+
{
|
|
16130
|
+
"guidance": "do",
|
|
16131
|
+
"description": "Use size=\"lg\" for an editorial page (a personal site, a marketing page, an article index): a display-tier title over a body-lg lede. Pair it with SectionHeader size=\"lg\" for the sections below, so the steps stay display → heading-2 → body."
|
|
16132
|
+
},
|
|
16133
|
+
{
|
|
16134
|
+
"guidance": "dont",
|
|
16135
|
+
"description": "Use size=\"lg\" on product routes (settings, lists, dashboards). Display type is never product chrome: there it competes with the content it frames. Keep md, and do not restyle the title with a display class through className either."
|
|
16136
|
+
},
|
|
15501
16137
|
{
|
|
15502
16138
|
"guidance": "dont",
|
|
15503
16139
|
"description": "Do not use PageHeader inside repeated cards, dialogs, or compact panels."
|
|
@@ -15546,8 +16182,20 @@
|
|
|
15546
16182
|
"type": "\"h1\" | \"h2\"",
|
|
15547
16183
|
"default": "\"h1\"",
|
|
15548
16184
|
"description": "The title's heading element. Keep h1 — the route's one h1. Use h2 only when something above already owns the h1: a shell that renders the route title, or a page showing a PageHeader as an example."
|
|
16185
|
+
},
|
|
16186
|
+
{
|
|
16187
|
+
"name": "size",
|
|
16188
|
+
"type": "\"md\" | \"lg\"",
|
|
16189
|
+
"default": "\"md\"",
|
|
16190
|
+
"description": "The title's scale. md: text-heading-2 over a text-body-sm description, for product pages. lg: text-display-lg over a text-body-lg lede capped at the prose measure, with more space between and below, for an editorial page (personal site, marketing page, article index). Beside actions or a back control, a title line taller than the controls lifts by half the difference, so its first line stays centred on them."
|
|
15549
16191
|
}
|
|
15550
16192
|
],
|
|
16193
|
+
"variants": {
|
|
16194
|
+
"size": [
|
|
16195
|
+
"md",
|
|
16196
|
+
"lg"
|
|
16197
|
+
]
|
|
16198
|
+
},
|
|
15551
16199
|
"tokensUsed": [
|
|
15552
16200
|
"foreground",
|
|
15553
16201
|
"muted-foreground"
|
|
@@ -15562,6 +16210,11 @@
|
|
|
15562
16210
|
"code": "<PageHeader title=\"Storage credits\" description=\"Fund permanent storage.\" actions={<Button>Top up</Button>} />",
|
|
15563
16211
|
"imports": "import { Button, PageHeader } from \"@sableui/react\""
|
|
15564
16212
|
},
|
|
16213
|
+
{
|
|
16214
|
+
"label": "Editorial page title (size lg)",
|
|
16215
|
+
"code": "<PageHeader size=\"lg\" title=\"Field notes\" description=\"Essays on making things slowly, from a studio by the sea.\" actions={<Button variant=\"outline\">Subscribe</Button>} />",
|
|
16216
|
+
"imports": "import { Button, PageHeader } from \"@sableui/react\""
|
|
16217
|
+
},
|
|
15565
16218
|
{
|
|
15566
16219
|
"label": "One per page, inside the region",
|
|
15567
16220
|
"code": "// PageHeader introduces the current route: title, optional description, and the\n// actions that act on THIS page.\n\n<Region max=\"content\">\n <PageHeader\n title=\"Invoices\"\n description=\"Every charge on this account.\"\n actions={<Button>Export</Button>}\n />\n <p className=\"text-body-md text-muted-foreground\">Page content.</p>\n</Region>\n\n// ✗ Outside the Region. The header then sits at a different measure from the\n// content it introduces, and the page reads as two documents.\n// ✗ Two on one page. A second one is a section heading: a SectionHeader\n// (an h2 on text-heading-3, same title row and actions), not another PageHeader.\n// ✗ Global actions in it (account, search). Those persist across routes and\n// belong in AppHeader.",
|
|
@@ -15858,7 +16511,13 @@
|
|
|
15858
16511
|
"tool",
|
|
15859
16512
|
"chrome",
|
|
15860
16513
|
"inspector",
|
|
15861
|
-
"layout"
|
|
16514
|
+
"layout",
|
|
16515
|
+
"mobile",
|
|
16516
|
+
"responsive",
|
|
16517
|
+
"sheet",
|
|
16518
|
+
"drawer",
|
|
16519
|
+
"narrow",
|
|
16520
|
+
"detail pane"
|
|
15862
16521
|
],
|
|
15863
16522
|
"usage": {
|
|
15864
16523
|
"description": "Use PanelGroup for the body of a tool workspace: fixed-width Panels docked left and/or right, a flexible Panel in the middle for the canvas, and a PanelResizer between them. Panels scroll internally and the group never scrolls. This is the workspace-register counterpart to SplitPane, which only switches between side-by-side and stacked at a breakpoint and cannot be resized.",
|
|
@@ -15875,6 +16534,14 @@
|
|
|
15875
16534
|
"guidance": "do",
|
|
15876
16535
|
"description": "Label every Panel — they are landmarks, and 'Layers' or 'Properties' is what a screen-reader user navigates by."
|
|
15877
16536
|
},
|
|
16537
|
+
{
|
|
16538
|
+
"guidance": "do",
|
|
16539
|
+
"description": "Decide what each docked Panel becomes on a phone with `narrow`: \"sheet\" for an inspector or a navigation rail the user opens on demand, \"hidden\" for chrome a phone does without. Drive a sheet's `open` from the same state that selects the record, so selecting a row opens the inspector at every width."
|
|
16540
|
+
},
|
|
16541
|
+
{
|
|
16542
|
+
"guidance": "dont",
|
|
16543
|
+
"description": "Render the same inspector twice, once in a Panel and once in a Sheet behind a hand-rolled viewport check. `narrow=\"sheet\"` renders the content once, in whichever place applies."
|
|
16544
|
+
},
|
|
15878
16545
|
{
|
|
15879
16546
|
"guidance": "dont",
|
|
15880
16547
|
"description": "Let the group scroll. Only panels and the canvas scroll; a scrolling group means the shell lost its height constraint."
|
|
@@ -15919,8 +16586,8 @@
|
|
|
15919
16586
|
"subcomponents": [
|
|
15920
16587
|
{
|
|
15921
16588
|
"name": "Panel",
|
|
15922
|
-
"type": "{ label: string; width?: number; scroll?: boolean }",
|
|
15923
|
-
"description": "A docked or flexible column. `label` names the landmark; omit `width` for the flexible centre. `scroll` defaults to true — set it false for a header + scrolling body + pinned footer, so the footer doesn't scroll away with the list. When you own the layout, give the header and footer shrink-0 and the middle min-h-0 flex-1 overflow-y-auto. overflow-y-auto only activates if every flex ancestor can shrink: a single missing min-h-0 anywhere up the chain makes the panel grow instead of scroll, and nothing reports it."
|
|
16589
|
+
"type": "{ label: string; width?: number; scroll?: boolean; narrow?: \"dock\" | \"sheet\" | \"hidden\"; open?: boolean; onOpenChange?: (open: boolean) => void; side?: \"left\" | \"right\"; below?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" }",
|
|
16590
|
+
"description": "A docked or flexible column. Below `below` (default lg, where Sidebar and BottomNav swap), `narrow` decides what it becomes: \"dock\" (default) stays docked, \"sheet\" renders the same content once inside a Sheet from `side` (default \"right\"; a panel before the canvas is \"left\"), titled by `label`, open while `open` is true and closed through `onOpenChange` (Esc, scrim, swipe), and \"hidden\" drops it. `open` defaults to true docked (false removes the column, so the canvas takes its width) and false as a sheet. Crossing the breakpoint remounts the content, so keep state that must survive it in the app. `label` names the landmark; omit `width` for the flexible centre. `scroll` defaults to true — set it false for a header + scrolling body + pinned footer, so the footer doesn't scroll away with the list. When you own the layout, give the header and footer shrink-0 and the middle min-h-0 flex-1 overflow-y-auto. overflow-y-auto only activates if every flex ancestor can shrink: a single missing min-h-0 anywhere up the chain makes the panel grow instead of scroll, and nothing reports it."
|
|
15924
16591
|
},
|
|
15925
16592
|
{
|
|
15926
16593
|
"name": "PanelResizer",
|
|
@@ -15981,6 +16648,11 @@
|
|
|
15981
16648
|
"code": "function Body() {\n const [left, setLeft, leftBounds] = usePanelWidth(240, { storageKey: \"app:left\" })\n const [right, setRight, rightBounds] = usePanelWidth(256, { storageKey: \"app:right\" })\n return (\n <PanelGroup>\n <Panel label=\"Layers\" width={left} />\n <PanelResizer side=\"left\" width={left} onWidthChange={setLeft} label=\"Resize layers panel\" {...leftBounds} />\n <Panel label=\"Canvas\" />\n <PanelResizer side=\"right\" width={right} onWidthChange={setRight} label=\"Resize properties panel\" {...rightBounds} />\n <Panel label=\"Properties\" width={right}>\n <PanelSection title=\"Blending\">\n <PropertyPair>\n <ScrubField label=\"Opacity\" icon=\"grid\" suffix=\"%\" value={100} min={0} max={100} />\n <ScrubField label=\"Blur\" value={0} min={0} max={64} suffix=\"px\" />\n </PropertyPair>\n </PanelSection>\n </Panel>\n </PanelGroup>\n )\n}",
|
|
15982
16649
|
"imports": "import { Panel, PanelGroup, PanelResizer, PanelSection, PropertyPair, ScrubField, usePanelWidth } from \"@sableui/react\""
|
|
15983
16650
|
},
|
|
16651
|
+
{
|
|
16652
|
+
"label": "Inspector that becomes a sheet on phones",
|
|
16653
|
+
"code": "function Body() {\n const [selected, setSelected] = useState<string | null>(null)\n return (\n <PanelGroup>\n <Panel label=\"Issues\">\n <div className=\"p-2\">\n <Button size=\"xs\" variant=\"ghost\" onClick={() => setSelected(\"AES-104\")}>AES-104 Refine product page</Button>\n </div>\n </Panel>\n <Panel\n label=\"Issue details\"\n width={320}\n narrow=\"sheet\"\n open={selected !== null}\n onOpenChange={(open) => !open && setSelected(null)}\n >\n <PanelSection title={selected ?? \"\"}>\n <PropertyRow label=\"Status\"><Input defaultValue=\"In progress\" /></PropertyRow>\n </PanelSection>\n </Panel>\n </PanelGroup>\n )\n}",
|
|
16654
|
+
"imports": "import { Button, Input, Panel, PanelGroup, PanelSection, PropertyRow } from \"@sableui/react\""
|
|
16655
|
+
},
|
|
15984
16656
|
{
|
|
15985
16657
|
"label": "Fixed panel, no resizer",
|
|
15986
16658
|
"code": "<PanelGroup>\n <Panel label=\"Tools\" width={56}>\n <div className=\"flex flex-col gap-1 p-1\">\n <Button size=\"icon-xs\" variant=\"ghost\" aria-label=\"Toggle grid\"><Icon name=\"grid\" /></Button>\n <Button size=\"icon-xs\" variant=\"ghost\" aria-label=\"Hide\"><Icon name=\"eye\" /></Button>\n </div>\n </Panel>\n <Panel label=\"Canvas\" />\n</PanelGroup>",
|
|
@@ -16486,7 +17158,7 @@
|
|
|
16486
17158
|
"group": "Badges & pills",
|
|
16487
17159
|
"register": "page",
|
|
16488
17160
|
"path": "components/pill",
|
|
16489
|
-
"summary": "A chip someone clicks: a filter, a tag choice, an option in a row. Always a button with aria-pressed. appearance=\"solid\" (filters; selected lifts onto the primary surface) or \"outline\" (quiet chips that annotate content, like reactions); size md or sm. A static label is a Badge.",
|
|
17161
|
+
"summary": "A chip someone clicks: a filter, a tag choice, an option in a row. Always a button with aria-pressed. appearance=\"solid\" (filters; selected lifts onto the primary surface) or \"outline\" (quiet chips that annotate content, like reactions); size lg, md or sm. A static label is a Badge.",
|
|
16490
17162
|
"keywords": [
|
|
16491
17163
|
"pill",
|
|
16492
17164
|
"chip",
|
|
@@ -16506,6 +17178,10 @@
|
|
|
16506
17178
|
"guidance": "do",
|
|
16507
17179
|
"description": "Reflect the current choice with selected, so the active filter reads at a glance."
|
|
16508
17180
|
},
|
|
17181
|
+
{
|
|
17182
|
+
"guidance": "do",
|
|
17183
|
+
"description": "In a toolbar where filter chips share a row with a search field (\"All 8 · Products 5 · Open source 2 … [Find a project]\"), pair Pill size=\"lg\" with SearchInput size=\"sm\", and Button size=\"sm\" for any action beside them. All three stand on --control-sm (32px, 28 compact), the rung the spacing doctrine gives toolbar controls, so the row is one height in either density. Pill md (28px) has no text field of its height in the page register, so md beside a field always reads as a mismatch."
|
|
17184
|
+
},
|
|
16509
17185
|
{
|
|
16510
17186
|
"guidance": "dont",
|
|
16511
17187
|
"description": "Do not use Pill as a static label. A pressable-looking chip that does nothing is the most common cause of dead clicks in a filter row — that is a Badge.",
|
|
@@ -16519,14 +17195,15 @@
|
|
|
16519
17195
|
},
|
|
16520
17196
|
{
|
|
16521
17197
|
"guidance": "dont",
|
|
16522
|
-
"description": "Do not restyle a Pill with border or background classes to get a quieter chip. That is appearance=\"outline\" (and size=\"sm\" for a small one); an override is a look that exists nowhere else in the system."
|
|
17198
|
+
"description": "Do not restyle a Pill with border or background classes to get a quieter chip. That is appearance=\"outline\" (and size=\"sm\" for a small one); an override is a look that exists nowhere else in the system. Likewise never set h-* on a Pill to match a neighbour: pick the size that shares its rung."
|
|
16523
17199
|
}
|
|
16524
17200
|
]
|
|
16525
17201
|
},
|
|
16526
17202
|
"related": {
|
|
16527
17203
|
"components": [
|
|
16528
17204
|
"Badge",
|
|
16529
|
-
"ToggleGroup"
|
|
17205
|
+
"ToggleGroup",
|
|
17206
|
+
"SearchInput"
|
|
16530
17207
|
]
|
|
16531
17208
|
},
|
|
16532
17209
|
"props": [
|
|
@@ -16544,9 +17221,9 @@
|
|
|
16544
17221
|
},
|
|
16545
17222
|
{
|
|
16546
17223
|
"name": "size",
|
|
16547
|
-
"type": "\"md\" | \"sm\"",
|
|
17224
|
+
"type": "\"lg\" | \"md\" | \"sm\"",
|
|
16548
17225
|
"default": "\"md\"",
|
|
16549
|
-
"description": "
|
|
17226
|
+
"description": "lg sits on --control-sm (32px, 28 compact), the toolbar rung it shares with SearchInput sm and Button sm, for a filter row beside a search field. md sits on --control-xs (28px, 24 compact), the chip in a row of its own or among content. sm is a fixed 24px (the target floor, which density does not shrink), for chips under or inside other content."
|
|
16550
17227
|
},
|
|
16551
17228
|
{
|
|
16552
17229
|
"name": "tone",
|
|
@@ -16566,6 +17243,7 @@
|
|
|
16566
17243
|
"outline"
|
|
16567
17244
|
],
|
|
16568
17245
|
"size": [
|
|
17246
|
+
"lg",
|
|
16569
17247
|
"md",
|
|
16570
17248
|
"sm"
|
|
16571
17249
|
],
|
|
@@ -16650,6 +17328,11 @@
|
|
|
16650
17328
|
"code": "<Row gap={1}><Pill selected onClick={() => {}}>All</Pill><Pill onClick={() => {}}>Projects</Pill><Pill onClick={() => {}}>Reports</Pill></Row>",
|
|
16651
17329
|
"imports": "import { Pill, Row } from \"@sableui/react\""
|
|
16652
17330
|
},
|
|
17331
|
+
{
|
|
17332
|
+
"label": "Filters with search",
|
|
17333
|
+
"code": "const [filter, setFilter] = useState(\"all\")\nconst [q, setQ] = useState(\"\")\n\n<Row gap={2} align=\"center\" justify=\"between\" wrap>\n\t<Row gap={1}>\n\t\t<Pill size=\"lg\" selected={filter === \"all\"} onClick={() => setFilter(\"all\")}>All 8</Pill>\n\t\t<Pill size=\"lg\" selected={filter === \"products\"} onClick={() => setFilter(\"products\")}>Products 5</Pill>\n\t\t<Pill size=\"lg\" selected={filter === \"oss\"} onClick={() => setFilter(\"oss\")}>Open source 2</Pill>\n\t</Row>\n\t<SearchInput size=\"sm\" value={q} onValueChange={setQ} placeholder=\"Find a project…\" aria-label=\"Find a project\" className=\"max-w-64\" />\n</Row>",
|
|
17334
|
+
"imports": "import { Pill, Row, SearchInput } from \"@sableui/react\""
|
|
17335
|
+
},
|
|
16653
17336
|
{
|
|
16654
17337
|
"label": "Outline, small (reactions)",
|
|
16655
17338
|
"code": "<Row gap={1}><Pill appearance=\"outline\" size=\"sm\" selected onClick={() => {}}>🎉 3</Pill><Pill appearance=\"outline\" size=\"sm\" onClick={() => {}}>👀</Pill></Row>",
|
|
@@ -18418,7 +19101,7 @@
|
|
|
18418
19101
|
"group": "Forms",
|
|
18419
19102
|
"register": "either",
|
|
18420
19103
|
"path": "components/search-input",
|
|
18421
|
-
"summary": "Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), the theme's field surface, 2px focus ring; the clear button appears only when there is a value.",
|
|
19104
|
+
"summary": "Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), the theme's field surface, 2px focus ring; the clear button appears only when there is a value. size md (form), sm (toolbar, matching Pill lg and Button sm) or xs (inset tool-panel field).",
|
|
18422
19105
|
"keywords": [
|
|
18423
19106
|
"search",
|
|
18424
19107
|
"input",
|
|
@@ -18444,6 +19127,14 @@
|
|
|
18444
19127
|
"guidance": "do",
|
|
18445
19128
|
"description": "Light the query in the results it filters with `useMatchHighlight(query)` — a callback ref for the results container, with `selector` to keep it to titles. It paints on the `highlight` pair without wrapping matches in `<mark>`, the same paint Combobox and CommandMenu use for their rows."
|
|
18446
19129
|
},
|
|
19130
|
+
{
|
|
19131
|
+
"guidance": "do",
|
|
19132
|
+
"description": "In a toolbar beside filter chips, use size=\"sm\" with Pill size=\"lg\" (and Button size=\"sm\" for actions). All three stand on --control-sm (32px, 28 compact), the rung the spacing doctrine gives toolbar controls, so the row is one height in either density. Keep md for a search field in a form or a page header beside md buttons, and xs for a filter inside a workspace panel, where it matches Input xs's inset surface."
|
|
19133
|
+
},
|
|
19134
|
+
{
|
|
19135
|
+
"guidance": "dont",
|
|
19136
|
+
"description": "Do not match a neighbour's height with h-* on className or inputClassName. The magnifier, padding and clear button would stay sized for the old rung; pick the size that shares the neighbour's rung."
|
|
19137
|
+
},
|
|
18447
19138
|
{
|
|
18448
19139
|
"guidance": "dont",
|
|
18449
19140
|
"description": "Do not use SearchInput as a command palette; use CommandMenu for global commands, opened from the header by CommandMenuTrigger. A SearchInput that opens a dialog on focus is a text box you cannot type in."
|
|
@@ -18458,10 +19149,17 @@
|
|
|
18458
19149
|
"components": [
|
|
18459
19150
|
"Input",
|
|
18460
19151
|
"CommandMenu",
|
|
18461
|
-
"Combobox"
|
|
19152
|
+
"Combobox",
|
|
19153
|
+
"Pill"
|
|
18462
19154
|
]
|
|
18463
19155
|
},
|
|
18464
19156
|
"props": [
|
|
19157
|
+
{
|
|
19158
|
+
"name": "size",
|
|
19159
|
+
"type": "\"md\" | \"sm\" | \"xs\"",
|
|
19160
|
+
"default": "\"md\"",
|
|
19161
|
+
"description": "The rung of the control ladder, named as on Input, Select and Combobox. md is the form field (--control-md, 40px / 32 compact). sm is the toolbar field (--control-sm, 32px / 28 compact), the rung Pill lg and Button sm share, so filters + search + actions in one row are one height. xs is the tool-panel field (--control-xs, 28px / 24 compact) on Input xs's inset panel-field surface. The magnifier, inline padding and clear button scale with the rung and stay vertically centred."
|
|
19162
|
+
},
|
|
18465
19163
|
{
|
|
18466
19164
|
"name": "value",
|
|
18467
19165
|
"type": "string",
|
|
@@ -18507,7 +19205,7 @@
|
|
|
18507
19205
|
{
|
|
18508
19206
|
"name": "...props",
|
|
18509
19207
|
"type": "ComponentPropsWithRef<\"input\">",
|
|
18510
|
-
"description": "All native input attributes (name, autoFocus, ref, aria-invalid, …) pass through to the <input> except value/onChange, which are owned by value/onValueChange,
|
|
19208
|
+
"description": "All native input attributes (name, autoFocus, ref, aria-invalid, …) pass through to the <input> except value/onChange, which are owned by value/onValueChange, className, which sizes the whole field, and the native numeric size attribute, which the size prop replaces."
|
|
18511
19209
|
}
|
|
18512
19210
|
],
|
|
18513
19211
|
"tokensUsed": [
|
|
@@ -18516,7 +19214,9 @@
|
|
|
18516
19214
|
"foreground",
|
|
18517
19215
|
"muted-foreground",
|
|
18518
19216
|
"ring",
|
|
18519
|
-
"destructive"
|
|
19217
|
+
"destructive",
|
|
19218
|
+
"panel-field",
|
|
19219
|
+
"panel-field-hover"
|
|
18520
19220
|
],
|
|
18521
19221
|
"motion": [
|
|
18522
19222
|
{
|
|
@@ -18572,6 +19272,16 @@
|
|
|
18572
19272
|
"code": "const [q, setQ] = useState(\"\")\nconst results = useMatchHighlight<HTMLUListElement>(q, { selector: \"[data-title]\" })\nconst shown = posts.filter((post) => post.title.toLowerCase().includes(q.trim().toLowerCase()))\n\n<SearchInput value={q} onValueChange={setQ} placeholder=\"Search posts\" />\n<ul ref={results}>\n\t{shown.map((post) => (\n\t\t<li key={post.id}><span data-title>{post.title}</span></li>\n\t))}\n</ul>",
|
|
18573
19273
|
"imports": "import { SearchInput, useMatchHighlight } from \"@sableui/react\""
|
|
18574
19274
|
},
|
|
19275
|
+
{
|
|
19276
|
+
"label": "Filters with search",
|
|
19277
|
+
"code": "const [filter, setFilter] = useState(\"all\")\nconst [q, setQ] = useState(\"\")\n\n<Row gap={2} align=\"center\" justify=\"between\" wrap>\n\t<Row gap={1}>\n\t\t<Pill size=\"lg\" selected={filter === \"all\"} onClick={() => setFilter(\"all\")}>All 8</Pill>\n\t\t<Pill size=\"lg\" selected={filter === \"products\"} onClick={() => setFilter(\"products\")}>Products 5</Pill>\n\t\t<Pill size=\"lg\" selected={filter === \"oss\"} onClick={() => setFilter(\"oss\")}>Open source 2</Pill>\n\t</Row>\n\t<SearchInput size=\"sm\" value={q} onValueChange={setQ} placeholder=\"Find a project…\" aria-label=\"Find a project\" className=\"max-w-64\" />\n</Row>",
|
|
19278
|
+
"imports": "import { Pill, Row, SearchInput } from \"@sableui/react\""
|
|
19279
|
+
},
|
|
19280
|
+
{
|
|
19281
|
+
"label": "Sizes",
|
|
19282
|
+
"code": "<Stack gap={1}>\n\t<SearchInput size=\"md\" placeholder=\"Search posts\" aria-label=\"Search posts\" />\n\t<SearchInput size=\"sm\" placeholder=\"Find a project…\" aria-label=\"Find a project\" />\n\t<SearchInput size=\"xs\" placeholder=\"Filter layers\" aria-label=\"Filter layers\" />\n</Stack>",
|
|
19283
|
+
"imports": "import { SearchInput, Stack } from \"@sableui/react\""
|
|
19284
|
+
},
|
|
18575
19285
|
{
|
|
18576
19286
|
"label": "Disabled",
|
|
18577
19287
|
"code": "<SearchInput disabled placeholder=\"Search\" />",
|
|
@@ -18597,7 +19307,9 @@
|
|
|
18597
19307
|
"h2",
|
|
18598
19308
|
"subsection",
|
|
18599
19309
|
"optical alignment",
|
|
18600
|
-
"bleed"
|
|
19310
|
+
"bleed",
|
|
19311
|
+
"editorial section",
|
|
19312
|
+
"large section title"
|
|
18601
19313
|
],
|
|
18602
19314
|
"usage": {
|
|
18603
19315
|
"description": "Use SectionHeader to head each section below the page title — a home-page shelf with View all, a settings group, a list with its own actions. PageHeader is the route's single h1; every section under it takes a SectionHeader, with titleId wired to the section's aria-labelledby.",
|
|
@@ -18608,7 +19320,7 @@
|
|
|
18608
19320
|
},
|
|
18609
19321
|
{
|
|
18610
19322
|
"guidance": "do",
|
|
18611
|
-
"description": "Pick titleAs by the document outline (h2 under the page h1, h3 inside an h2 section) and size by visual weight: md for a page's sections and shelves, sm for denser groups like settings."
|
|
19323
|
+
"description": "Pick titleAs by the document outline (h2 under the page h1, h3 inside an h2 section) and size by visual weight: lg for an editorial page's sections under a PageHeader size=\"lg\", md for a page's sections and shelves, sm for denser groups like settings."
|
|
18612
19324
|
},
|
|
18613
19325
|
{
|
|
18614
19326
|
"guidance": "do",
|
|
@@ -18696,13 +19408,14 @@
|
|
|
18696
19408
|
},
|
|
18697
19409
|
{
|
|
18698
19410
|
"name": "size",
|
|
18699
|
-
"type": "\"md\" | \"sm\"",
|
|
19411
|
+
"type": "\"lg\" | \"md\" | \"sm\"",
|
|
18700
19412
|
"default": "\"md\"",
|
|
18701
|
-
"description": "Visual weight, independent of the heading level: md (text-heading-3) for a page's sections and shelves; sm (text-heading-4) for denser sets — settings groups, subsections."
|
|
19413
|
+
"description": "Visual weight, independent of the heading level: lg (text-heading-2 over a text-body-md description) for an editorial page's sections, under a PageHeader size=\"lg\"; md (text-heading-3) for a page's sections and shelves; sm (text-heading-4) for denser sets — settings groups, subsections. lg stops at heading-2: heading-1 is the page title's, one per route. The actions stay centred on the title's first line at every size."
|
|
18702
19414
|
}
|
|
18703
19415
|
],
|
|
18704
19416
|
"variants": {
|
|
18705
19417
|
"size": [
|
|
19418
|
+
"lg",
|
|
18706
19419
|
"md",
|
|
18707
19420
|
"sm"
|
|
18708
19421
|
]
|
|
@@ -18758,6 +19471,11 @@
|
|
|
18758
19471
|
"code": "<section aria-labelledby=\"playlists\" className=\"flex flex-col gap-4\">\n\t<SectionHeader\n\t\ttitleId=\"playlists\"\n\t\ttitle=\"Your playlists\"\n\t\tactions={\n\t\t\t<>\n\t\t\t\t<Button variant=\"ghost\" size=\"sm\" iconStart={<Icon name=\"filter\" />}>Filter</Button>\n\t\t\t\t<Button size=\"sm\" iconStart={<Icon name=\"plus\" />}>New playlist</Button>\n\t\t\t</>\n\t\t}\n\t/>\n\t<Stack gap={1}>\n\t\t<Entity className=\"[--entity-px:0]\" title=\"Late drives\" subtitle=\"42 tracks\" />\n\t\t<Entity className=\"[--entity-px:0]\" title=\"Focus\" subtitle=\"18 tracks\" />\n\t</Stack>\n</section>",
|
|
18759
19472
|
"imports": "import { Button, Entity, Icon, SectionHeader, Stack } from \"@sableui/react\""
|
|
18760
19473
|
},
|
|
19474
|
+
{
|
|
19475
|
+
"label": "An editorial section (size lg)",
|
|
19476
|
+
"code": "<section aria-labelledby=\"selected-work\" className=\"flex flex-col gap-6\">\n\t<SectionHeader\n\t\ttitleId=\"selected-work\"\n\t\tsize=\"lg\"\n\t\ttitle=\"Selected work\"\n\t\tdescription=\"Five projects from the last two years.\"\n\t\tactions={\n\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/work\" />} aria-label=\"View all work\">\n\t\t\t\tView all\n\t\t\t</Button>\n\t\t}\n\t/>\n\t<Grid minItemWidth=\"16rem\" gap={3}>\n\t\t<Card><CardHeader><CardTitle>Tidewater</CardTitle><CardDescription>Brand identity</CardDescription></CardHeader></Card>\n\t\t<Card><CardHeader><CardTitle>Northlight</CardTitle><CardDescription>Product design</CardDescription></CardHeader></Card>\n\t</Grid>\n</section>",
|
|
19477
|
+
"imports": "import { Button, Card, CardDescription, CardHeader, CardTitle, Grid, SectionHeader } from \"@sableui/react\""
|
|
19478
|
+
},
|
|
18761
19479
|
{
|
|
18762
19480
|
"label": "A subsection inside a section",
|
|
18763
19481
|
"code": "<SectionHeader titleAs=\"h3\" size=\"sm\" title=\"Appears on\" />",
|
|
@@ -19721,7 +20439,7 @@
|
|
|
19721
20439
|
{
|
|
19722
20440
|
"name": "Sheet (Root)",
|
|
19723
20441
|
"type": "Base UI Drawer.Root props (open, defaultOpen, onOpenChange, onOpenChangeComplete, modal, disablePointerDismissal, actionsRef, handle, snapPoints, snapPoint, defaultSnapPoint, onSnapPointChange, snapToSequentialPoints) — minus swipeDirection",
|
|
19724
|
-
"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."
|
|
20442
|
+
"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. Records the element focused when the sheet opens, trigger or not, and returns focus there on close."
|
|
19725
20443
|
},
|
|
19726
20444
|
{
|
|
19727
20445
|
"name": "snapPoints",
|
|
@@ -19809,7 +20527,7 @@
|
|
|
19809
20527
|
"The panel scrolls its own content (thin scrollbar, contained overscroll) when it outgrows its bound — the full height on left/right, 80vh on top/bottom.",
|
|
19810
20528
|
"Esc and click-outside dismiss by default; on touch, a swipe toward the sheet's own edge dismisses too. A short or slow drag springs back.",
|
|
19811
20529
|
"Swipe is an extra path, never the only one: Esc, click-outside and SheetClose all still work.",
|
|
19812
|
-
"Focus matches Dialog: the first tabbable on mouse/keyboard open, the panel itself on touch (so the virtual keyboard stays down)
|
|
20530
|
+
"Focus matches Dialog: the first tabbable on mouse/keyboard open, the panel itself on touch (so the virtual keyboard stays down). On close, focus returns to the element that held it when the sheet opened: the SheetTrigger, or your own button when it drives a controlled open. It is captured before the content mounts, so an autoFocus field inside cannot displace it; an explicit finalFocus on SheetContent wins.",
|
|
19813
20531
|
"Render a SheetClose inside SheetContent so screen-reader users can escape the popup.",
|
|
19814
20532
|
"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.",
|
|
19815
20533
|
"A mouse drag that starts on sheet content selects text rather than dragging the panel; mouse dragging only starts from the padding edge.",
|
|
@@ -19972,12 +20690,20 @@
|
|
|
19972
20690
|
"group": "Navigation",
|
|
19973
20691
|
"register": "page",
|
|
19974
20692
|
"path": "components/sidebar",
|
|
19975
|
-
"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.",
|
|
20693
|
+
"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.",
|
|
19976
20694
|
"keywords": [
|
|
19977
20695
|
"sidebar",
|
|
19978
20696
|
"navigation",
|
|
19979
20697
|
"app chrome",
|
|
19980
|
-
"rail"
|
|
20698
|
+
"rail",
|
|
20699
|
+
"nav group",
|
|
20700
|
+
"section label",
|
|
20701
|
+
"account",
|
|
20702
|
+
"user menu",
|
|
20703
|
+
"profile",
|
|
20704
|
+
"account switcher",
|
|
20705
|
+
"workspace switcher",
|
|
20706
|
+
"sidebar footer"
|
|
19981
20707
|
],
|
|
19982
20708
|
"usage": {
|
|
19983
20709
|
"description": "Use Sidebar for persistent desktop navigation or application chrome outside the main content region.",
|
|
@@ -19998,6 +20724,18 @@
|
|
|
19998
20724
|
"guidance": "do",
|
|
19999
20725
|
"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 — the gap is the divider."
|
|
20000
20726
|
},
|
|
20727
|
+
{
|
|
20728
|
+
"guidance": "do",
|
|
20729
|
+
"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."
|
|
20730
|
+
},
|
|
20731
|
+
{
|
|
20732
|
+
"guidance": "dont",
|
|
20733
|
+
"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."
|
|
20734
|
+
},
|
|
20735
|
+
{
|
|
20736
|
+
"guidance": "dont",
|
|
20737
|
+
"description": "Do not put a Badge or pill in the account row. Its one quiet detail line carries the plan, role or 'Demo'."
|
|
20738
|
+
},
|
|
20001
20739
|
{
|
|
20002
20740
|
"guidance": "dont",
|
|
20003
20741
|
"description": "Do not use Sidebar for page-local settings, filters, or temporary action groups."
|
|
@@ -20063,7 +20801,7 @@
|
|
|
20063
20801
|
{
|
|
20064
20802
|
"name": "SidebarHeader",
|
|
20065
20803
|
"type": "div props",
|
|
20066
|
-
"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."
|
|
20804
|
+
"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."
|
|
20067
20805
|
},
|
|
20068
20806
|
{
|
|
20069
20807
|
"name": "SidebarBrand",
|
|
@@ -20073,7 +20811,17 @@
|
|
|
20073
20811
|
{
|
|
20074
20812
|
"name": "SidebarItem",
|
|
20075
20813
|
"type": "{ label: ReactNode; icon?: ReactNode; active?: boolean; badge?: ReactNode; render?: useRender.RenderProp } + button props",
|
|
20076
|
-
"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\" />}."
|
|
20814
|
+
"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\" />}."
|
|
20815
|
+
},
|
|
20816
|
+
{
|
|
20817
|
+
"name": "SidebarGroup",
|
|
20818
|
+
"type": "{ label?: ReactNode } + div props",
|
|
20819
|
+
"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."
|
|
20820
|
+
},
|
|
20821
|
+
{
|
|
20822
|
+
"name": "SidebarAccount",
|
|
20823
|
+
"type": "{ avatar: ReactNode; label: ReactNode; detail?: ReactNode; indicator?: ReactNode | false; render?: useRender.RenderProp } + button props",
|
|
20824
|
+
"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."
|
|
20077
20825
|
}
|
|
20078
20826
|
],
|
|
20079
20827
|
"variants": {
|
|
@@ -20140,6 +20888,11 @@
|
|
|
20140
20888
|
"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>",
|
|
20141
20889
|
"imports": "import { Button, Icon, Sidebar, SidebarFooter, SidebarItem, SidebarNav } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
|
|
20142
20890
|
},
|
|
20891
|
+
{
|
|
20892
|
+
"label": "Grouped nav with an account menu",
|
|
20893
|
+
"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>",
|
|
20894
|
+
"imports": "import { Avatar, Icon, Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger, Sidebar, SidebarAccount, SidebarBrand, SidebarFooter, SidebarGroup, SidebarHeader, SidebarItem, SidebarNav } from \"@sableui/react\""
|
|
20895
|
+
},
|
|
20143
20896
|
{
|
|
20144
20897
|
"label": "Inline (inside a bounded frame)",
|
|
20145
20898
|
"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>",
|
|
@@ -21951,6 +22704,10 @@
|
|
|
21951
22704
|
"guidance": "do",
|
|
21952
22705
|
"description": "Use tabs when each panel is peer content and users may compare or switch often."
|
|
21953
22706
|
},
|
|
22707
|
+
{
|
|
22708
|
+
"guidance": "do",
|
|
22709
|
+
"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."
|
|
22710
|
+
},
|
|
21954
22711
|
{
|
|
21955
22712
|
"guidance": "do",
|
|
21956
22713
|
"description": "Keep tab labels short and make the active tab unmistakable."
|
|
@@ -24303,7 +25060,18 @@
|
|
|
24303
25060
|
"chrome",
|
|
24304
25061
|
"panels",
|
|
24305
25062
|
"density",
|
|
24306
|
-
"viewport"
|
|
25063
|
+
"viewport",
|
|
25064
|
+
"issue tracker",
|
|
25065
|
+
"tracker",
|
|
25066
|
+
"inspector",
|
|
25067
|
+
"reading view",
|
|
25068
|
+
"mobile",
|
|
25069
|
+
"responsive",
|
|
25070
|
+
"bottom nav",
|
|
25071
|
+
"media query",
|
|
25072
|
+
"breakpoint",
|
|
25073
|
+
"top bar",
|
|
25074
|
+
"topbar"
|
|
24307
25075
|
],
|
|
24308
25076
|
"usage": {
|
|
24309
25077
|
"description": "Use Workspace as the outer frame for a tool — 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\".",
|
|
@@ -24320,6 +25088,18 @@
|
|
|
24320
25088
|
"guidance": "do",
|
|
24321
25089
|
"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."
|
|
24322
25090
|
},
|
|
25091
|
+
{
|
|
25092
|
+
"guidance": "do",
|
|
25093
|
+
"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."
|
|
25094
|
+
},
|
|
25095
|
+
{
|
|
25096
|
+
"guidance": "do",
|
|
25097
|
+
"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–12px 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."
|
|
25098
|
+
},
|
|
25099
|
+
{
|
|
25100
|
+
"guidance": "do",
|
|
25101
|
+
"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."
|
|
25102
|
+
},
|
|
24323
25103
|
{
|
|
24324
25104
|
"guidance": "dont",
|
|
24325
25105
|
"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."
|
|
@@ -24357,6 +25137,11 @@
|
|
|
24357
25137
|
"name": "StatusBar",
|
|
24358
25138
|
"required": false,
|
|
24359
25139
|
"description": "Bottom chrome for ambient document facts."
|
|
25140
|
+
},
|
|
25141
|
+
{
|
|
25142
|
+
"name": "BottomNav",
|
|
25143
|
+
"required": false,
|
|
25144
|
+
"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."
|
|
24360
25145
|
}
|
|
24361
25146
|
]
|
|
24362
25147
|
},
|
|
@@ -24380,6 +25165,11 @@
|
|
|
24380
25165
|
"type": "ReactNode",
|
|
24381
25166
|
"description": "Bottom chrome, normally a StatusBar."
|
|
24382
25167
|
},
|
|
25168
|
+
{
|
|
25169
|
+
"name": "bottomNav",
|
|
25170
|
+
"type": "ReactNode",
|
|
25171
|
+
"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."
|
|
25172
|
+
},
|
|
24383
25173
|
{
|
|
24384
25174
|
"name": "density",
|
|
24385
25175
|
"type": "\"compact\" | \"comfortable\"",
|
|
@@ -24403,6 +25193,16 @@
|
|
|
24403
25193
|
"name": "WorkspaceCanvas",
|
|
24404
25194
|
"type": "ComponentProps<'main'>",
|
|
24405
25195
|
"description": "The work surface: renders main#main-content, fills the body, clips its overflow."
|
|
25196
|
+
},
|
|
25197
|
+
{
|
|
25198
|
+
"name": "useMediaQuery",
|
|
25199
|
+
"type": "(query: string, options?: { serverValue?: boolean }) => boolean",
|
|
25200
|
+
"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."
|
|
25201
|
+
},
|
|
25202
|
+
{
|
|
25203
|
+
"name": "useBelowBreakpoint",
|
|
25204
|
+
"type": "(breakpoint?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", options?: { serverValue?: boolean }) => boolean",
|
|
25205
|
+
"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."
|
|
24406
25206
|
}
|
|
24407
25207
|
],
|
|
24408
25208
|
"tokensUsed": [
|
|
@@ -24430,6 +25230,16 @@
|
|
|
24430
25230
|
"code": "<Workspace statusBar={<StatusBar><span>Connected</span></StatusBar>}>\n <PanelGroup>\n <Panel label=\"Sources\" width={240}>\n <PanelSection title=\"Sources\">\n <PropertyRow label=\"Filter\"><Input placeholder=\"Search\" /></PropertyRow>\n </PanelSection>\n </Panel>\n <WorkspaceCanvas className=\"overflow-y-auto scrollbar-quiet\">\n <div className=\"p-3 text-ui-num text-muted-foreground\">log output…</div>\n </WorkspaceCanvas>\n </PanelGroup>\n</Workspace>",
|
|
24431
25231
|
"imports": "import { Input, Panel, PanelGroup, PanelSection, PropertyRow, StatusBar, Workspace, WorkspaceCanvas } from \"@sableui/react\""
|
|
24432
25232
|
},
|
|
25233
|
+
{
|
|
25234
|
+
"label": "Issue tracker: column bar, docked inspector, phone sheet",
|
|
25235
|
+
"code": "function Tracker() {\n const [selected, setSelected] = useState<string | null>(\"AES-104\")\n return (\n <Workspace\n bottomNav={\n <BottomNav>\n <BottomNavItem showLabel label=\"Issues\" icon={<Icon name=\"list\" />} active />\n <BottomNavItem showLabel label=\"Inbox\" icon={<Icon name=\"inbox\" />} />\n </BottomNav>\n }\n statusBar={<StatusBar><span>12 issues</span></StatusBar>}\n >\n <PanelGroup>\n <Panel label=\"Navigation\" width={220} narrow=\"hidden\">\n <PanelSection title=\"Workspace\">\n <PropertyRow label=\"Team\"><Input defaultValue=\"Studio\" /></PropertyRow>\n </PanelSection>\n </Panel>\n <WorkspaceCanvas className=\"flex flex-col\">\n <WorkspaceBar>\n <Breadcrumbs>\n <BreadcrumbItem><BreadcrumbLink href=\"#\">Studio</BreadcrumbLink></BreadcrumbItem>\n <BreadcrumbSeparator />\n <BreadcrumbItem><BreadcrumbPage>Digital flagship</BreadcrumbPage></BreadcrumbItem>\n </Breadcrumbs>\n </WorkspaceBar>\n <div className=\"min-h-0 flex-1 overflow-y-auto scrollbar-quiet\">\n <Button size=\"xs\" variant=\"ghost\" onClick={() => setSelected(\"AES-104\")}>AES-104 Refine product page</Button>\n </div>\n </WorkspaceCanvas>\n <Panel\n label=\"Issue details\"\n width={320}\n narrow=\"sheet\"\n open={selected !== null}\n onOpenChange={(open) => !open && setSelected(null)}\n >\n <PanelSection title={selected ?? \"\"}>\n <PropertyRow label=\"Status\"><Input defaultValue=\"In progress\" /></PropertyRow>\n </PanelSection>\n </Panel>\n </PanelGroup>\n </Workspace>\n )\n}",
|
|
25236
|
+
"imports": "import { BottomNav, BottomNavItem, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, Breadcrumbs, BreadcrumbSeparator, Button, Icon, Input, Panel, PanelGroup, PanelSection, PropertyRow, StatusBar, Workspace, WorkspaceBar, WorkspaceCanvas } from \"@sableui/react\""
|
|
25237
|
+
},
|
|
25238
|
+
{
|
|
25239
|
+
"label": "Reading view inside a workspace",
|
|
25240
|
+
"code": "<Workspace>\n <PanelGroup>\n <WorkspaceCanvas className=\"overflow-y-auto scrollbar-quiet\">\n <Density value=\"comfortable\">\n <article className=\"max-w-prose p-6\">\n <h2 className=\"text-heading-3\">Project overview</h2>\n <p className=\"mt-3 text-body-md text-muted-foreground\">\n Paragraphs keep their page size here, while the chrome around the canvas stays compact.\n </p>\n </article>\n </Density>\n </WorkspaceCanvas>\n </PanelGroup>\n</Workspace>",
|
|
25241
|
+
"imports": "import { Density, PanelGroup, Workspace, WorkspaceCanvas } from \"@sableui/react\""
|
|
25242
|
+
},
|
|
24433
25243
|
{
|
|
24434
25244
|
"label": "Comfortable density",
|
|
24435
25245
|
"code": "<Workspace density=\"comfortable\" toolbar={<WorkspaceBar><span className=\"text-ui-md\">Review</span></WorkspaceBar>}>\n <PanelGroup>\n <WorkspaceCanvas>\n <div className=\"grid h-full place-items-center text-body-md text-muted-foreground\">Touch-friendly review mode</div>\n </WorkspaceCanvas>\n </PanelGroup>\n</Workspace>",
|
|
@@ -24567,8 +25377,8 @@
|
|
|
24567
25377
|
"Each seat-usage Meter has an aria-label naming the account and aria-valuetext reading \"184 of 250 seats used\".",
|
|
24568
25378
|
"Avatars beside a visible name use an empty alt so the name is not read twice."
|
|
24569
25379
|
],
|
|
24570
|
-
"imports": "import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, DataTable, Entity, Grid, Icon, MenuItem, MenuSeparator, Meter, NotificationBadge,
|
|
24571
|
-
"code": "// Account status → Badge tone. Sable has no status vocabulary of its own;\n// an app maps ITS states onto tones once, here, and every badge follows.\nconst STATUS_TONE = { active: \"success\", onboarding: \"info\", full: \"1\", cancelled: \"neutral\" } as const\nconst STATUS_LABEL = { active: \"Active\", onboarding: \"Onboarding\", full: \"Seats full\", cancelled: \"Cancelled\" } as const\ntype AccountStatus = keyof typeof STATUS_TONE\n\nconst MIN = 60 * 1000\nconst HOUR = 60 * MIN\nconst DAY = 24 * HOUR\n\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 })\nconst usdCents = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\" })\nconst shortDate = (offset: number) =>\n\tnew Date(Date.now() + offset).toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" })\n\nconst stats = [\n\t{ label: \"Revenue\", value: \"$18,240\", delta: \"+8.2%\", up: true, note: \"vs. $16,857\" },\n\t{ label: \"Active seats\", value: \"1,284\", delta: \"+3.1%\", up: true, note: \"182 added\" },\n\t{ label: \"Accounts\", value: \"312\", delta: \"+12.4%\", up: true, note: \"Best month yet\" },\n\t{ label: \"Seat usage\", value: \"64%\", delta: \"−1.8%\", up: false, note: \"6 near their limit\" },\n]\n\nconst accounts = [\n\t{ id: \"ac_01\", title: \"Northwind\", status: \"active\" as const, used: 184, seats: 250, price: 16, started: -6 * DAY },\n\t{ id: \"ac_02\", title: \"Harbor Labs\", status: \"full\" as const, used: 100, seats: 100, price: 20, started: -19 * DAY },\n\t{ id: \"ac_03\", title: \"Brightline\", status: \"active\" as const, used: 61, seats: 150, price: 16, started: -2 * DAY },\n\t{ id: \"ac_04\", title: \"Tidewater Co.\", status: \"active\" as const, used: 212, seats: 300, price: 12, started: -27 * DAY },\n\t{ id: \"ac_05\", title: \"Saltworks\", status: \"cancelled\" as const, used: 38, seats: 75, price: 20, started: -44 * DAY },\n\t{ id: \"ac_06\", title: \"Paper Moon\", status: \"full\" as const, used: 50, seats: 50, price: 24, started: -58 * DAY },\n\t{ id: \"ac_07\", title: \"Signal Studio\", status: \"onboarding\" as const, used: 0, seats: 200, price: 16, started: 3 * DAY },\n\t{ id: \"ac_08\", title: \"Bluewater\", status: \"cancelled\" as const, used: 72, seats: 120, price: 12, started: -71 * DAY },\n\t{ id: \"ac_09\", title: \"Ember & Field\", status: \"full\" as const, used: 80, seats: 80, price: 20, started: -86 * DAY },\n]\nconst accountCount = 312\n\nconst payout = {\n\tamount: 8862.64,\n\tdate: 5 * DAY,\n\tlines: [\n\t\t{ label: \"Subscriptions\", value: 9127.33 },\n\t\t{ label: \"Processing (2.9%)\", value: -264.69 },\n\t\t{ label: \"Deposits to\", value: \"Checking ••4821\" },\n\t],\n}\n\nconst customers = [\n\t{ id: \"cu_01\", name: \"Northwind\", handle: \"northwind.io\", initials: \"NW\", seats: 184, spent: 2944 },\n\t{ id: \"cu_02\", name: \"Tidewater Co.\", handle: \"tidewater.co\", initials: \"TC\", seats: 212, spent: 2544 },\n\t{ id: \"cu_03\", name: \"Harbor Labs\", handle: \"harborlabs.com\", initials: \"HL\", seats: 100, spent: 2000 },\n\t{ id: \"cu_04\", name: \"Ember & Field\", handle: \"emberfield.com\", initials: \"EF\", seats: 80, spent: 1600 },\n]\n\nconst upcoming = [\n\t{ id: \"up_01\", title: \"Version 2.4\", detail: \"Shared reports · All plans\", at: 3 * DAY },\n\t{ id: \"up_02\", title: \"Audit log export\", detail: \"Business plan · Beta\", at: 12 * DAY },\n\t{ id: \"up_03\", title: \"Version 2.5\", detail: \"Offline mode · All plans\", at: 26 * DAY },\n\t{ id: \"up_04\", title: \"Usage-based billing\", detail: \"Studio and Business · Beta\", at: 34 * DAY },\n]\n\nconst activity = [\n\t{ id: \"ev_01\", who: \"Nina Simone\", initials: \"NS\", what: \"added 12 seats to Northwind · 184 of 250\", when: 2 * MIN },\n\t{ id: \"ev_02\", who: \"Grace Hopper\", initials: \"GH\", what: \"upgraded Brightline to Studio\", when: 26 * MIN },\n\t{ id: \"ev_03\", who: \"Frida Kahlo\", initials: \"FK\", what: \"paid invoice INV-0042 for Tidewater Co.\", when: 1 * HOUR },\n\t{ id: \"ev_04\", who: \"Mary Shelley\", initials: \"MS\", what: \"joined the Harbor Labs workspace\", when: 3 * HOUR },\n\t{ id: \"ev_05\", who: \"Nina Simone\", initials: \"NS\", what: \"booked onboarding for Signal Studio\", when: 6 * HOUR },\n]\n\nexport function DashboardPage() {\n\treturn (\n\t\t<AppShell\n\t\t\t// Sidebar hides below lg, so the same destinations ride in the shell's\n\t\t\t// mobile slots — without them the route has no navigation under 1024px.\n\t\t\theader={\n\t\t\t\t<AppHeader\n\t\t\t\t\ttitle={\n\t\t\t\t\t\t<Row gap={1} align=\"center\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t\tactions={<Avatar variant=\"art\" size=\"sm\" fallback=\"AL\" alt=\"Ada Lovelace\" />}\n\t\t\t\t/>\n\t\t\t}\n\t\t\tbottomNav={\n\t\t\t\t<BottomNav aria-label=\"Primary\">\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"grid\" />} label=\"Overview\" showLabel active render={<a href=\"/\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"folder\" />} label=\"Accounts\" showLabel render={<a href=\"/accounts\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"user\" />} label=\"Customers\" showLabel render={<a href=\"/customers\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"bell\" />} label=\"Activity\" showLabel render={<a href=\"/activity\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"gear\" />} label=\"Settings\" showLabel render={<a href=\"/settings\" />} />\n\t\t\t\t</BottomNav>\n\t\t\t}\n\t\t\tsidebar={\n\t\t\t\t<Sidebar>\n\t\t\t\t\t<SidebarHeader>\n\t\t\t\t\t\t<SidebarBrand mark={<Icon name=\"gem\" variant=\"solid\" />} label=\"Atelier\" render={<a href=\"/\" />} />\n\t\t\t\t\t</SidebarHeader>\n\t\t\t\t\t<SidebarNav>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"grid\" />} label=\"Overview\" active render={<a href=\"/\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"folder\" />} label=\"Accounts\" render={<a href=\"/accounts\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"user\" />} label=\"Customers\" render={<a href=\"/customers\" />} />\n\t\t\t\t\t\t<SidebarItem\n\t\t\t\t\t\t\ticon={<Icon name=\"bell\" />}\n\t\t\t\t\t\t\tlabel=\"Activity\"\n\t\t\t\t\t\t\tbadge={<NotificationBadge count={4} tone=\"neutral\" />}\n\t\t\t\t\t\t\trender={<a href=\"/activity\" />}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"gear\" />} label=\"Settings\" render={<a href=\"/settings\" />} />\n\t\t\t\t\t</SidebarNav>\n\t\t\t\t\t<SidebarFooter>\n\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\tclassName=\"[--entity-py:0]\"\n\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback=\"AL\" alt=\"\" />}\n\t\t\t\t\t\t\ttitle=\"Ada Lovelace\"\n\t\t\t\t\t\t\tsubtitle=\"@ada\"\n\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t<OverflowMenu label=\"Account\" size=\"icon-sm\">\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/@ada\" />}>View profile</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/settings\" />}>Settings</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t\t\t<MenuItem>Sign out</MenuItem>\n\t\t\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</SidebarFooter>\n\t\t\t\t</Sidebar>\n\t\t\t}\n\t\t>\n\t\t\t<Page max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t\t<PageHeader\n\t\t\t\t\ttitle=\"Overview\"\n\t\t\t\t\tdescription=\"Revenue and account activity for the last 30 days.\"\n\t\t\t\t\tactions={\n\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t<Button variant=\"outline\">\n\t\t\t\t\t\t\t\t<Icon name=\"download\" />\n\t\t\t\t\t\t\t\tExport\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button>\n\t\t\t\t\t\t\t\t<Icon name=\"plus\" />\n\t\t\t\t\t\t\t\tNew account\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t<Grid cols={{ base: 2, xl: 4 }} gap={3}>\n\t\t\t\t\t{stats.map((stat) => (\n\t\t\t\t\t\t<Card key={stat.label}>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardDescription>{stat.label}</CardDescription>\n\t\t\t\t\t\t\t\t<CardTitle className=\"text-mono-xl\">{stat.value}</CardTitle>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<Row gap={1} wrap>\n\t\t\t\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\t\t\t\ttone={stat.up ? \"success\" : \"warning\"}\n\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\ticon={<Icon name={stat.up ? \"arrow-up\" : \"arrow-down\"} />}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{stat.delta}\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t<span className=\"text-caption text-muted-foreground\">{stat.note}</span>\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t))}\n\t\t\t\t</Grid>\n\t\t\t\t<Grid cols={{ base: 1, xl: 3 }} gap={3}>\n\t\t\t\t\t<Card className=\"flex flex-col xl:col-span-2\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Accounts</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Seats across your customers, newest first.</CardDescription>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardContent flush className=\"mt-4 flex-1\">\n\t\t\t\t\t\t\t<DataTable\n\t\t\t\t\t\t\t\tdata={accounts}\n\t\t\t\t\t\t\t\trowKey={(row) => row.id}\n\t\t\t\t\t\t\t\tdefaultSort={{ key: \"started\", direction: \"desc\" }}\n\t\t\t\t\t\t\t\tcolumns={[\n\t\t\t\t\t\t\t\t\t{ key: \"title\", header: \"Account\", sortable: true, className: \"text-label-md whitespace-nowrap\" },\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"status\",\n\t\t\t\t\t\t\t\t\t\theader: \"Status\",\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<Badge appearance=\"plain\" tone={STATUS_TONE[row.status as AccountStatus]} dot>\n\t\t\t\t\t\t\t\t\t\t\t\t{STATUS_LABEL[row.status as AccountStatus]}\n\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"used\",\n\t\t\t\t\t\t\t\t\t\theader: \"Seats\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tsortValue: (row) => row.used / row.seats,\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={3} align=\"center\" className=\"min-w-40\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Meter\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue={row.used}\n\t\t\t\t\t\t\t\t\t\t\t\t\tmax={row.seats}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttone={row.used === row.seats ? \"success\" : \"primary\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-label={`${row.title} seats used`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tgetAriaValueText={(_, v) => `${v} of ${row.seats} seats used`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-caption whitespace-nowrap text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{row.used}/{row.seats}\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"revenue\",\n\t\t\t\t\t\t\t\t\t\theader: \"Revenue\",\n\t\t\t\t\t\t\t\t\t\talign: \"end\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tsortValue: (row) => row.used * row.price,\n\t\t\t\t\t\t\t\t\t\tcell: (row) =>\n\t\t\t\t\t\t\t\t\t\t\trow.used > 0 ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm\">{usd.format(row.used * row.price)}</span>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm text-muted-foreground\">—</span>\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"started\",\n\t\t\t\t\t\t\t\t\t\theader: \"Started\",\n\t\t\t\t\t\t\t\t\t\talign: \"end\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-body-sm whitespace-nowrap text-muted-foreground\">{shortDate(row.started)}</span>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t<CardFooter className=\"justify-between\">\n\t\t\t\t\t\t\t<span className=\"text-caption text-muted-foreground\">\n\t\t\t\t\t\t\t\tShowing {accounts.length} of {accountCount} accounts\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" bleed=\"end\" render={<a href=\"/accounts\" />}>\n\t\t\t\t\t\t\t\tView all accounts\n\t\t\t\t\t\t\t\t<Icon name=\"arrow-right\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardDescription>Next payout</CardDescription>\n\t\t\t\t\t\t\t\t<CardTitle className=\"text-mono-xl\">{usdCents.format(payout.amount)}</CardTitle>\n\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t<Badge tone=\"info\" size=\"sm\" icon={<Icon name=\"calendar\" />}>\n\t\t\t\t\t\t\t\t\t\t{shortDate(payout.date)}\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<dl className=\"sable-list-dl text-body-sm\">\n\t\t\t\t\t\t\t\t\t{payout.lines.map((line) => (\n\t\t\t\t\t\t\t\t\t\t<div key={line.label} className=\"contents\">\n\t\t\t\t\t\t\t\t\t\t\t<dt>{line.label}</dt>\n\t\t\t\t\t\t\t\t\t\t\t<dd className={typeof line.value === \"number\" ? \"text-end text-mono-sm\" : \"text-end\"}>\n\t\t\t\t\t\t\t\t\t\t\t\t{typeof line.value === \"number\" ? usdCents.format(line.value) : line.value}\n\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</dl>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardTitle>Top customers</CardTitle>\n\t\t\t\t\t\t\t\t<CardDescription>By spend this month.</CardDescription>\n\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/customers\" />}>\n\t\t\t\t\t\t\t\t\t\tView all\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t{customers.map((person) => (\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tkey={person.id}\n\t\t\t\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback={person.initials} alt=\"\" />}\n\t\t\t\t\t\t\t\t\t\ttitle={person.name}\n\t\t\t\t\t\t\t\t\t\tsubtitle={`${person.handle} · ${person.seats} seats`}\n\t\t\t\t\t\t\t\t\t\taction={<span className=\"text-mono-sm\">{usd.format(person.spent)}</span>}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Stack>\n\t\t\t\t</Grid>\n\t\t\t\t<Grid cols={{ base: 1, xl: 3 }} gap={3}>\n\t\t\t\t\t<Card className=\"flex flex-col\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Upcoming releases</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Scheduled to ship to customers.</CardDescription>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t{upcoming.map((release) => (\n\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\tkey={release.id}\n\t\t\t\t\t\t\t\t\ttitle={release.title}\n\t\t\t\t\t\t\t\t\tsubtitle={release.detail}\n\t\t\t\t\t\t\t\t\taction={<RelativeTime date={Date.now() + release.at} className=\"text-caption text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t<CardFooter className=\"mt-auto\">\n\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" render={<a href=\"/releases/new\" />}>\n\t\t\t\t\t\t\t\t<Icon name=\"calendar\" />\n\t\t\t\t\t\t\t\tSchedule a release\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Card className=\"xl:col-span-2\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Activity</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Seats, upgrades, and invoices.</CardDescription>\n\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/activity\" />}>\n\t\t\t\t\t\t\t\t\tView all\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t{activity.map((item) => (\n\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback={item.initials} alt=\"\" />}\n\t\t\t\t\t\t\t\t\ttitle={item.who}\n\t\t\t\t\t\t\t\t\tsubtitle={item.what}\n\t\t\t\t\t\t\t\t\taction={<RelativeTime date={Date.now() - item.when} className=\"text-caption text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</CardList>\n\t\t\t\t\t</Card>\n\t\t\t\t</Grid>\n\t\t\t</Page>\n\t\t</AppShell>\n\t)\n}"
|
|
25380
|
+
"imports": "import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, DataTable, Entity, Grid, Icon, Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger, Meter, NotificationBadge, Page, PageHeader, RelativeTime, Row, Sidebar, SidebarAccount, SidebarBrand, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, Stack } from \"@sableui/react\"",
|
|
25381
|
+
"code": "// Account status → Badge tone. Sable has no status vocabulary of its own;\n// an app maps ITS states onto tones once, here, and every badge follows.\nconst STATUS_TONE = { active: \"success\", onboarding: \"info\", full: \"1\", cancelled: \"neutral\" } as const\nconst STATUS_LABEL = { active: \"Active\", onboarding: \"Onboarding\", full: \"Seats full\", cancelled: \"Cancelled\" } as const\ntype AccountStatus = keyof typeof STATUS_TONE\n\nconst MIN = 60 * 1000\nconst HOUR = 60 * MIN\nconst DAY = 24 * HOUR\n\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 })\nconst usdCents = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\" })\nconst shortDate = (offset: number) =>\n\tnew Date(Date.now() + offset).toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" })\n\nconst stats = [\n\t{ label: \"Revenue\", value: \"$18,240\", delta: \"+8.2%\", up: true, note: \"vs. $16,857\" },\n\t{ label: \"Active seats\", value: \"1,284\", delta: \"+3.1%\", up: true, note: \"182 added\" },\n\t{ label: \"Accounts\", value: \"312\", delta: \"+12.4%\", up: true, note: \"Best month yet\" },\n\t{ label: \"Seat usage\", value: \"64%\", delta: \"−1.8%\", up: false, note: \"6 near their limit\" },\n]\n\nconst accounts = [\n\t{ id: \"ac_01\", title: \"Northwind\", status: \"active\" as const, used: 184, seats: 250, price: 16, started: -6 * DAY },\n\t{ id: \"ac_02\", title: \"Harbor Labs\", status: \"full\" as const, used: 100, seats: 100, price: 20, started: -19 * DAY },\n\t{ id: \"ac_03\", title: \"Brightline\", status: \"active\" as const, used: 61, seats: 150, price: 16, started: -2 * DAY },\n\t{ id: \"ac_04\", title: \"Tidewater Co.\", status: \"active\" as const, used: 212, seats: 300, price: 12, started: -27 * DAY },\n\t{ id: \"ac_05\", title: \"Saltworks\", status: \"cancelled\" as const, used: 38, seats: 75, price: 20, started: -44 * DAY },\n\t{ id: \"ac_06\", title: \"Paper Moon\", status: \"full\" as const, used: 50, seats: 50, price: 24, started: -58 * DAY },\n\t{ id: \"ac_07\", title: \"Signal Studio\", status: \"onboarding\" as const, used: 0, seats: 200, price: 16, started: 3 * DAY },\n\t{ id: \"ac_08\", title: \"Bluewater\", status: \"cancelled\" as const, used: 72, seats: 120, price: 12, started: -71 * DAY },\n\t{ id: \"ac_09\", title: \"Ember & Field\", status: \"full\" as const, used: 80, seats: 80, price: 20, started: -86 * DAY },\n]\nconst accountCount = 312\n\nconst payout = {\n\tamount: 8862.64,\n\tdate: 5 * DAY,\n\tlines: [\n\t\t{ label: \"Subscriptions\", value: 9127.33 },\n\t\t{ label: \"Processing (2.9%)\", value: -264.69 },\n\t\t{ label: \"Deposits to\", value: \"Checking ••4821\" },\n\t],\n}\n\nconst customers = [\n\t{ id: \"cu_01\", name: \"Northwind\", handle: \"northwind.io\", initials: \"NW\", seats: 184, spent: 2944 },\n\t{ id: \"cu_02\", name: \"Tidewater Co.\", handle: \"tidewater.co\", initials: \"TC\", seats: 212, spent: 2544 },\n\t{ id: \"cu_03\", name: \"Harbor Labs\", handle: \"harborlabs.com\", initials: \"HL\", seats: 100, spent: 2000 },\n\t{ id: \"cu_04\", name: \"Ember & Field\", handle: \"emberfield.com\", initials: \"EF\", seats: 80, spent: 1600 },\n]\n\nconst upcoming = [\n\t{ id: \"up_01\", title: \"Version 2.4\", detail: \"Shared reports · All plans\", at: 3 * DAY },\n\t{ id: \"up_02\", title: \"Audit log export\", detail: \"Business plan · Beta\", at: 12 * DAY },\n\t{ id: \"up_03\", title: \"Version 2.5\", detail: \"Offline mode · All plans\", at: 26 * DAY },\n\t{ id: \"up_04\", title: \"Usage-based billing\", detail: \"Studio and Business · Beta\", at: 34 * DAY },\n]\n\nconst activity = [\n\t{ id: \"ev_01\", who: \"Nina Simone\", initials: \"NS\", what: \"added 12 seats to Northwind · 184 of 250\", when: 2 * MIN },\n\t{ id: \"ev_02\", who: \"Grace Hopper\", initials: \"GH\", what: \"upgraded Brightline to Studio\", when: 26 * MIN },\n\t{ id: \"ev_03\", who: \"Frida Kahlo\", initials: \"FK\", what: \"paid invoice INV-0042 for Tidewater Co.\", when: 1 * HOUR },\n\t{ id: \"ev_04\", who: \"Mary Shelley\", initials: \"MS\", what: \"joined the Harbor Labs workspace\", when: 3 * HOUR },\n\t{ id: \"ev_05\", who: \"Nina Simone\", initials: \"NS\", what: \"booked onboarding for Signal Studio\", when: 6 * HOUR },\n]\n\nexport function DashboardPage() {\n\treturn (\n\t\t<AppShell\n\t\t\t// Sidebar hides below lg, so the same destinations ride in the shell's\n\t\t\t// mobile slots — without them the route has no navigation under 1024px.\n\t\t\theader={\n\t\t\t\t<AppHeader\n\t\t\t\t\ttitle={\n\t\t\t\t\t\t<Row gap={1} align=\"center\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t\tactions={<Avatar variant=\"art\" size=\"sm\" fallback=\"AL\" alt=\"Ada Lovelace\" />}\n\t\t\t\t/>\n\t\t\t}\n\t\t\tbottomNav={\n\t\t\t\t<BottomNav aria-label=\"Primary\">\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"grid\" />} label=\"Overview\" showLabel active render={<a href=\"/\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"folder\" />} label=\"Accounts\" showLabel render={<a href=\"/accounts\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"user\" />} label=\"Customers\" showLabel render={<a href=\"/customers\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"bell\" />} label=\"Activity\" showLabel render={<a href=\"/activity\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"gear\" />} label=\"Settings\" showLabel render={<a href=\"/settings\" />} />\n\t\t\t\t</BottomNav>\n\t\t\t}\n\t\t\tsidebar={\n\t\t\t\t<Sidebar>\n\t\t\t\t\t<SidebarHeader>\n\t\t\t\t\t\t<SidebarBrand mark={<Icon name=\"gem\" variant=\"solid\" />} label=\"Atelier\" render={<a href=\"/\" />} />\n\t\t\t\t\t</SidebarHeader>\n\t\t\t\t\t<SidebarNav>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"grid\" />} label=\"Overview\" active render={<a href=\"/\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"folder\" />} label=\"Accounts\" render={<a href=\"/accounts\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"user\" />} label=\"Customers\" render={<a href=\"/customers\" />} />\n\t\t\t\t\t\t<SidebarItem\n\t\t\t\t\t\t\ticon={<Icon name=\"bell\" />}\n\t\t\t\t\t\t\tlabel=\"Activity\"\n\t\t\t\t\t\t\tbadge={<NotificationBadge count={4} tone=\"neutral\" />}\n\t\t\t\t\t\t\trender={<a href=\"/activity\" />}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"gear\" />} label=\"Settings\" render={<a href=\"/settings\" />} />\n\t\t\t\t\t</SidebarNav>\n\t\t\t\t\t<SidebarFooter>\n\t\t\t\t\t\t<Menu>\n\t\t\t\t\t\t\t<MenuTrigger\n\t\t\t\t\t\t\t\trender={\n\t\t\t\t\t\t\t\t\t<SidebarAccount\n\t\t\t\t\t\t\t\t\t\tavatar={<Avatar variant=\"art\" size=\"xs\" fallback=\"AL\" alt=\"\" />}\n\t\t\t\t\t\t\t\t\t\tlabel=\"Ada Lovelace\"\n\t\t\t\t\t\t\t\t\t\tdetail=\"@ada\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<MenuContent side=\"top\" align=\"start\">\n\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/@ada\" />}>View profile</MenuItem>\n\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/settings\" />}>Settings</MenuItem>\n\t\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t\t<MenuItem>Sign out</MenuItem>\n\t\t\t\t\t\t\t</MenuContent>\n\t\t\t\t\t\t</Menu>\n\t\t\t\t\t</SidebarFooter>\n\t\t\t\t</Sidebar>\n\t\t\t}\n\t\t>\n\t\t\t<Page max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t\t<PageHeader\n\t\t\t\t\ttitle=\"Overview\"\n\t\t\t\t\tdescription=\"Revenue and account activity for the last 30 days.\"\n\t\t\t\t\tactions={\n\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t<Button variant=\"outline\">\n\t\t\t\t\t\t\t\t<Icon name=\"download\" />\n\t\t\t\t\t\t\t\tExport\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button>\n\t\t\t\t\t\t\t\t<Icon name=\"plus\" />\n\t\t\t\t\t\t\t\tNew account\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t<Grid cols={{ base: 2, xl: 4 }} gap={3}>\n\t\t\t\t\t{stats.map((stat) => (\n\t\t\t\t\t\t<Card key={stat.label}>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardDescription>{stat.label}</CardDescription>\n\t\t\t\t\t\t\t\t<CardTitle className=\"text-mono-xl\">{stat.value}</CardTitle>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<Row gap={1} wrap>\n\t\t\t\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\t\t\t\ttone={stat.up ? \"success\" : \"warning\"}\n\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\ticon={<Icon name={stat.up ? \"arrow-up\" : \"arrow-down\"} />}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{stat.delta}\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t<span className=\"text-caption text-muted-foreground\">{stat.note}</span>\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t))}\n\t\t\t\t</Grid>\n\t\t\t\t<Grid cols={{ base: 1, xl: 3 }} gap={3}>\n\t\t\t\t\t<Card className=\"flex flex-col xl:col-span-2\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Accounts</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Seats across your customers, newest first.</CardDescription>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardContent flush className=\"mt-4 flex-1\">\n\t\t\t\t\t\t\t<DataTable\n\t\t\t\t\t\t\t\tdata={accounts}\n\t\t\t\t\t\t\t\trowKey={(row) => row.id}\n\t\t\t\t\t\t\t\tdefaultSort={{ key: \"started\", direction: \"desc\" }}\n\t\t\t\t\t\t\t\tcolumns={[\n\t\t\t\t\t\t\t\t\t{ key: \"title\", header: \"Account\", sortable: true, className: \"text-label-md whitespace-nowrap\" },\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"status\",\n\t\t\t\t\t\t\t\t\t\theader: \"Status\",\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<Badge appearance=\"plain\" tone={STATUS_TONE[row.status as AccountStatus]} dot>\n\t\t\t\t\t\t\t\t\t\t\t\t{STATUS_LABEL[row.status as AccountStatus]}\n\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"used\",\n\t\t\t\t\t\t\t\t\t\theader: \"Seats\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tsortValue: (row) => row.used / row.seats,\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={3} align=\"center\" className=\"min-w-40\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Meter\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue={row.used}\n\t\t\t\t\t\t\t\t\t\t\t\t\tmax={row.seats}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttone={row.used === row.seats ? \"success\" : \"primary\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-label={`${row.title} seats used`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tgetAriaValueText={(_, v) => `${v} of ${row.seats} seats used`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-caption whitespace-nowrap text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{row.used}/{row.seats}\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"revenue\",\n\t\t\t\t\t\t\t\t\t\theader: \"Revenue\",\n\t\t\t\t\t\t\t\t\t\talign: \"end\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tsortValue: (row) => row.used * row.price,\n\t\t\t\t\t\t\t\t\t\tcell: (row) =>\n\t\t\t\t\t\t\t\t\t\t\trow.used > 0 ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm\">{usd.format(row.used * row.price)}</span>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm text-muted-foreground\">—</span>\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"started\",\n\t\t\t\t\t\t\t\t\t\theader: \"Started\",\n\t\t\t\t\t\t\t\t\t\talign: \"end\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-body-sm whitespace-nowrap text-muted-foreground\">{shortDate(row.started)}</span>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t<CardFooter className=\"justify-between\">\n\t\t\t\t\t\t\t<span className=\"text-caption text-muted-foreground\">\n\t\t\t\t\t\t\t\tShowing {accounts.length} of {accountCount} accounts\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" bleed=\"end\" render={<a href=\"/accounts\" />}>\n\t\t\t\t\t\t\t\tView all accounts\n\t\t\t\t\t\t\t\t<Icon name=\"arrow-right\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardDescription>Next payout</CardDescription>\n\t\t\t\t\t\t\t\t<CardTitle className=\"text-mono-xl\">{usdCents.format(payout.amount)}</CardTitle>\n\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t<Badge tone=\"info\" size=\"sm\" icon={<Icon name=\"calendar\" />}>\n\t\t\t\t\t\t\t\t\t\t{shortDate(payout.date)}\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<dl className=\"sable-list-dl text-body-sm\">\n\t\t\t\t\t\t\t\t\t{payout.lines.map((line) => (\n\t\t\t\t\t\t\t\t\t\t<div key={line.label} className=\"contents\">\n\t\t\t\t\t\t\t\t\t\t\t<dt>{line.label}</dt>\n\t\t\t\t\t\t\t\t\t\t\t<dd className={typeof line.value === \"number\" ? \"text-end text-mono-sm\" : \"text-end\"}>\n\t\t\t\t\t\t\t\t\t\t\t\t{typeof line.value === \"number\" ? usdCents.format(line.value) : line.value}\n\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</dl>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardTitle>Top customers</CardTitle>\n\t\t\t\t\t\t\t\t<CardDescription>By spend this month.</CardDescription>\n\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/customers\" />}>\n\t\t\t\t\t\t\t\t\t\tView all\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t{customers.map((person) => (\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tkey={person.id}\n\t\t\t\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback={person.initials} alt=\"\" />}\n\t\t\t\t\t\t\t\t\t\ttitle={person.name}\n\t\t\t\t\t\t\t\t\t\tsubtitle={`${person.handle} · ${person.seats} seats`}\n\t\t\t\t\t\t\t\t\t\taction={<span className=\"text-mono-sm\">{usd.format(person.spent)}</span>}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Stack>\n\t\t\t\t</Grid>\n\t\t\t\t<Grid cols={{ base: 1, xl: 3 }} gap={3}>\n\t\t\t\t\t<Card className=\"flex flex-col\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Upcoming releases</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Scheduled to ship to customers.</CardDescription>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t{upcoming.map((release) => (\n\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\tkey={release.id}\n\t\t\t\t\t\t\t\t\ttitle={release.title}\n\t\t\t\t\t\t\t\t\tsubtitle={release.detail}\n\t\t\t\t\t\t\t\t\taction={<RelativeTime date={Date.now() + release.at} className=\"text-caption text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t<CardFooter className=\"mt-auto\">\n\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" render={<a href=\"/releases/new\" />}>\n\t\t\t\t\t\t\t\t<Icon name=\"calendar\" />\n\t\t\t\t\t\t\t\tSchedule a release\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Card className=\"xl:col-span-2\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Activity</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Seats, upgrades, and invoices.</CardDescription>\n\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/activity\" />}>\n\t\t\t\t\t\t\t\t\tView all\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t{activity.map((item) => (\n\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback={item.initials} alt=\"\" />}\n\t\t\t\t\t\t\t\t\ttitle={item.who}\n\t\t\t\t\t\t\t\t\tsubtitle={item.what}\n\t\t\t\t\t\t\t\t\taction={<RelativeTime date={Date.now() - item.when} className=\"text-caption text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</CardList>\n\t\t\t\t\t</Card>\n\t\t\t\t</Grid>\n\t\t\t</Page>\n\t\t</AppShell>\n\t)\n}"
|
|
24572
25382
|
},
|
|
24573
25383
|
{
|
|
24574
25384
|
"name": "FeedPage",
|
|
@@ -24667,6 +25477,45 @@
|
|
|
24667
25477
|
],
|
|
24668
25478
|
"imports": "import { useState } from \"react\"\nimport { AppHeader, AppShell, Avatar, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Col, Columns, DestructiveActionModal, Entity, Field, Form, Grid, Icon, Input, MenuItem, MenuSeparator, NotificationBadge, OverflowMenu, Page, PageHeader, Row, Select, SelectContent, SelectItem, SelectTrigger, Sidebar, SidebarBrand, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, SideNav, SideNavItem, Stack, Switch, Textarea } from \"@sableui/react\"",
|
|
24669
25479
|
"code": "const sections = [\n\t{ id: \"profile\", label: \"Profile\" },\n\t{ id: \"notifications\", label: \"Notifications\" },\n\t{ id: \"payouts\", label: \"Payouts\" },\n\t{ id: \"account\", label: \"Account\" },\n]\n\nconst timezones = [\n\t{ value: \"europe-london\", label: \"London (GMT+1)\" },\n\t{ value: \"europe-paris\", label: \"Paris (GMT+2)\" },\n\t{ value: \"america-new-york\", label: \"New York (GMT−4)\" },\n\t{ value: \"america-los-angeles\", label: \"Los Angeles (GMT−7)\" },\n\t{ value: \"asia-tokyo\", label: \"Tokyo (GMT+9)\" },\n]\n\nconst notifications = [\n\t{ name: \"notifyComments\", title: \"New comments\", description: \"When someone comments on your work\", on: true },\n\t{ name: \"notifyInvoicePaid\", title: \"Invoice paid\", description: \"When a customer pays an invoice\", on: true },\n\t{ name: \"notifyMembers\", title: \"New members\", description: \"When someone joins your workspace\", on: false },\n\t{ name: \"notifyDigest\", title: \"Weekly digest\", description: \"Activity and usage, every Monday\", on: true },\n\t{ name: \"notifyProduct\", title: \"Product updates\", description: \"New Atelier features\", on: false },\n]\n\nconst payouts = [\n\t{ label: \"Next payout\", value: \"$1,284.60\", note: \"Friday, Oct 16\" },\n\t{ label: \"Pending\", value: \"$412.00\", note: \"Clears in 7 days\" },\n\t{ label: \"Paid this year\", value: \"$18,920.00\", note: \"Across 214 invoices\" },\n]\n\nexport function SettingsPage() {\n\tconst [activeSection, setActiveSection] = useState(\"profile\")\n\tconst [deleteOpen, setDeleteOpen] = useState(false)\n\n\treturn (\n\t\t<AppShell\n\t\t\t// Sidebar hides below lg, so the same destinations ride in the shell's\n\t\t\t// mobile slots — without them the route has no navigation under 1024px.\n\t\t\theader={\n\t\t\t\t<AppHeader\n\t\t\t\t\ttitle={\n\t\t\t\t\t\t<Row gap={1} align=\"center\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t\tactions={<Avatar variant=\"art\" size=\"sm\" fallback=\"AL\" alt=\"Ada Lovelace\" />}\n\t\t\t\t/>\n\t\t\t}\n\t\t\tbottomNav={\n\t\t\t\t<BottomNav aria-label=\"Primary\">\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"grid\" />} label=\"Overview\" showLabel render={<a href=\"/\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"folder\" />} label=\"Accounts\" showLabel render={<a href=\"/accounts\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"user\" />} label=\"Customers\" showLabel render={<a href=\"/customers\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"bell\" />} label=\"Activity\" showLabel render={<a href=\"/activity\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"gear\" />} label=\"Settings\" showLabel active render={<a href=\"/settings\" />} />\n\t\t\t\t</BottomNav>\n\t\t\t}\n\t\t\tsidebar={\n\t\t\t\t<Sidebar>\n\t\t\t\t\t<SidebarHeader>\n\t\t\t\t\t\t<SidebarBrand mark={<Icon name=\"gem\" variant=\"solid\" />} label=\"Atelier\" render={<a href=\"/\" />} />\n\t\t\t\t\t</SidebarHeader>\n\t\t\t\t\t<SidebarNav>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"grid\" />} label=\"Overview\" render={<a href=\"/\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"folder\" />} label=\"Accounts\" render={<a href=\"/accounts\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"user\" />} label=\"Customers\" render={<a href=\"/customers\" />} />\n\t\t\t\t\t\t<SidebarItem\n\t\t\t\t\t\t\ticon={<Icon name=\"bell\" />}\n\t\t\t\t\t\t\tlabel=\"Activity\"\n\t\t\t\t\t\t\tbadge={<NotificationBadge count={4} tone=\"neutral\" />}\n\t\t\t\t\t\t\trender={<a href=\"/activity\" />}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"gear\" />} label=\"Settings\" active render={<a href=\"/settings\" />} />\n\t\t\t\t\t</SidebarNav>\n\t\t\t\t\t<SidebarFooter>\n\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\tclassName=\"[--entity-py:0]\"\n\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback=\"AL\" alt=\"\" />}\n\t\t\t\t\t\t\ttitle=\"Ada Lovelace\"\n\t\t\t\t\t\t\tsubtitle=\"@ada\"\n\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t<OverflowMenu label=\"Account\" size=\"icon-sm\">\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/@ada\" />}>View profile</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/settings\" />}>Settings</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t\t\t<MenuItem>Sign out</MenuItem>\n\t\t\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</SidebarFooter>\n\t\t\t\t</Sidebar>\n\t\t\t}\n\t\t>\n\t\t\t{/* No `as=\"main\"`: AppShell already renders <main id=\"main-content\">. */}\n\t\t\t<Page max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t\t<PageHeader\n\t\t\t\t\ttitle=\"Settings\"\n\t\t\t\t\tdescription=\"Manage your profile, notifications, and payouts.\"\n\t\t\t\t/>\n\t\t\t\t<Columns gap={4}>\n\t\t\t\t\t{/* The section rail is a desktop aid; on mobile the sections simply stack. */}\n\t\t\t\t\t<Col span={{ base: 12, lg: 3 }} className=\"hidden lg:block\">\n\t\t\t\t\t\t<SideNav aria-label=\"Settings sections\" className=\"sticky top-6\">\n\t\t\t\t\t\t\t{sections.map((section) => (\n\t\t\t\t\t\t\t\t<SideNavItem\n\t\t\t\t\t\t\t\t\tkey={section.id}\n\t\t\t\t\t\t\t\t\tlabel={section.label}\n\t\t\t\t\t\t\t\t\tactive={activeSection === section.id}\n\t\t\t\t\t\t\t\t\tonClick={() => setActiveSection(section.id)}\n\t\t\t\t\t\t\t\t\trender={<a href={`#${section.id}`} />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</SideNav>\n\t\t\t\t\t</Col>\n\t\t\t\t\t<Col span={{ base: 12, lg: 9 }}>\n\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t<Card id=\"profile\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Profile</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>How your team sees you on Atelier.</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t{/* The one explicit save on the page: profile text is drafted, then\n\t\t\t\t\t\t\t\t committed. Toggles below apply the moment they change. */}\n\t\t\t\t\t\t\t\t<Form onSubmit={(event) => event.preventDefault()} className=\"gap-0\">\n\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Avatar variant=\"art\" size=\"lg\" fallback=\"AL\" alt=\"Ada Lovelace\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<Stack gap={0.5} align=\"start\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button type=\"button\" variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tUpload photo\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button type=\"button\" variant=\"ghost\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tRemove\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-caption text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tSquare PNG or JPG, at least 400 px.\n\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Display name\" name=\"displayName\" required>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Input defaultValue=\"Ada Lovelace\" autoComplete=\"name\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Handle\" name=\"handle\" description=\"atelier.com/@ada\" required>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Input defaultValue=\"ada\" autoComplete=\"username\" spellCheck={false} />\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Bio\" name=\"bio\" description=\"Shown on your profile and in comments.\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Textarea\n\t\t\t\t\t\t\t\t\t\t\t\t\trows={3}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdefaultValue=\"Product designer working on tools for small teams. Writing about process every month.\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Timezone\" name=\"timezone\" description=\"Due dates use this zone.\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Select defaultValue=\"europe-london\" items={timezones}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectTrigger />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{timezones.map((zone) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectItem key={zone.value} value={zone.value}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{zone.label}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t<CardFooter className=\"justify-end\">\n\t\t\t\t\t\t\t\t\t\t<Button type=\"submit\">Save profile</Button>\n\t\t\t\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t\t\t\t</Form>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card id=\"notifications\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Notifications</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\t\t\t\tEmail updates about your projects. Changes save automatically.\n\t\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t{notifications.map((item) => (\n\t\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\t\tkey={item.name}\n\t\t\t\t\t\t\t\t\t\t\ttitle={item.title}\n\t\t\t\t\t\t\t\t\t\t\tsubtitle={item.description}\n\t\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t\t<Switch\n\t\t\t\t\t\t\t\t\t\t\t\t\tname={item.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdefaultChecked={item.on}\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-label={item.title}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card id=\"payouts\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Payouts</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>Paid in USD every Friday to Chase checking ••4821.</CardDescription>\n\t\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" render={<a href=\"/settings/payouts\" />}>\n\t\t\t\t\t\t\t\t\t\t\tManage payouts\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 3 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t{payouts.map((stat) => (\n\t\t\t\t\t\t\t\t\t\t\t<Stack key={stat.label} gap={0.5}>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{stat.label}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-mono-lg text-foreground\">{stat.value}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-caption text-muted-foreground\">{stat.note}</p>\n\t\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card id=\"account\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Account</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>Sign-in details and your data.</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tleading={<Icon name=\"mail\" className=\"text-lg text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t\t\ttitle=\"Email\"\n\t\t\t\t\t\t\t\t\t\tsubtitle=\"ada@atelier.art\"\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\tChange\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tleading={<Icon name=\"download\" className=\"text-lg text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t\t\ttitle=\"Export data\"\n\t\t\t\t\t\t\t\t\t\tsubtitle=\"Projects, invoices, and members\"\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\tExport\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Delete account</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\t\t\t\tPermanently remove your profile, projects, and billing history. Files you shared\n\t\t\t\t\t\t\t\t\t\tstay with your team.\n\t\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardFooter className=\"justify-end\">\n\t\t\t\t\t\t\t\t\t<Button variant=\"destructive\" onClick={() => setDeleteOpen(true)}>\n\t\t\t\t\t\t\t\t\t\tDelete account\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Col>\n\t\t\t\t</Columns>\n\t\t\t</Page>\n\t\t\t<DestructiveActionModal\n\t\t\t\topen={deleteOpen}\n\t\t\t\tonOpenChange={setDeleteOpen}\n\t\t\t\ttitle=\"Delete your account?\"\n\t\t\t\tdescription=\"This permanently removes your profile, 12 projects, and billing history. Your final invoice is still sent. This can't be undone.\"\n\t\t\t\tconfirmLabel=\"Delete account\"\n\t\t\t\tconfirmingLabel=\"Deleting…\"\n\t\t\t\tconfirmText=\"ada\"\n\t\t\t\tonConfirm={() => setDeleteOpen(false)}\n\t\t\t/>\n\t\t</AppShell>\n\t)\n}"
|
|
25480
|
+
},
|
|
25481
|
+
{
|
|
25482
|
+
"name": "TrackerPage",
|
|
25483
|
+
"path": "patterns/tracker-page.tsx",
|
|
25484
|
+
"summary": "Records workspace in the tool register: a Sidebar rail of grouped views ending in an account menu, a WorkspaceBar of Breadcrumbs over pill Tabs, a filter row, one grouped DataTable, and a docked inspector of PanelSection property rows with a pinned comment form. Below lg the rail and the inspector become sheets and a BottomNav takes the last row.",
|
|
25485
|
+
"intent": "Reach for this when a team works through records in a dense tool — an issue tracker, a support queue, a pipeline: a grouped list, a docked inspector for the selected record, and a rail of views. For records a person reads and edits one at a time on a page, ListDetailPage is the page-register shape; for monitoring with headline numbers, DashboardPage.",
|
|
25486
|
+
"keywords": [
|
|
25487
|
+
"issue tracker",
|
|
25488
|
+
"tracker",
|
|
25489
|
+
"linear",
|
|
25490
|
+
"like linear",
|
|
25491
|
+
"jira",
|
|
25492
|
+
"project management",
|
|
25493
|
+
"task manager",
|
|
25494
|
+
"support queue",
|
|
25495
|
+
"triage",
|
|
25496
|
+
"crm",
|
|
25497
|
+
"pipeline",
|
|
25498
|
+
"records workspace",
|
|
25499
|
+
"inspector",
|
|
25500
|
+
"backlog"
|
|
25501
|
+
],
|
|
25502
|
+
"notes": [
|
|
25503
|
+
"Workspace sets the compact density for everything inside: 32px rail rows, 28px tables, 24px property fields. Nothing in the pattern sizes a control or a row by hand. The Overview tab is a reading view, so it sits in `<Density value=\"comfortable\">` and its prose gets page type back.",
|
|
25504
|
+
"The rail is Sable's own parts on one grid: SidebarBrand, SidebarGroup, SidebarItem and SidebarAccount share the icon and label columns. The account row is the trigger of the account Menu, which holds Settings, an Appearance submenu wired to useSableTheme (light, dark, system) and Sign out. No Settings button or badge sits in the footer.",
|
|
25505
|
+
"There is no Workspace `toolbar`: the bars belong to the list column, so the inspector runs the full height beside them. The first WorkspaceBar carries Breadcrumbs; the second carries pill Tabs, a view switch in chrome, which sits beside the bar's other controls rather than drawing a rule.",
|
|
25506
|
+
"One DataTable groups the rows (`groupBy`, `groups`): one header for every group, a StatusDot, a count and an add action per group header, and Done starting closed. The segmented ToggleGroup switches the grouping between status and assignee. Filtering to nothing swaps the table for an EmptyState with a Clear filters action.",
|
|
25507
|
+
"Workflow states map onto StatusDot tones once (`STATUSES`), so group headers and the status Select read from one place. A product with a richer status glyph keeps that one mapping and swaps the icon.",
|
|
25508
|
+
"The inspector is a Panel with `narrow=\"sheet\"`: docked at 320px from lg, a right sheet below it, its content rendered once. Below lg it opens only when a row is picked (`inspecting`), so a phone never loads with a sheet over the list. Properties are PanelSection + PropertyRow with xs Select triggers and an xs DatePicker; Activity uses RelativeTime; the comment Form is pinned under the scrolling body.",
|
|
25509
|
+
"The rail is a Panel with `narrow=\"sheet\" side=\"left\"`, opened from a `lg:hidden` menu button in the first bar; in the sheet the Sidebar fills its width. Workspace `bottomNav` reserves the BottomNav's row below lg, so the status bar ends above it."
|
|
25510
|
+
],
|
|
25511
|
+
"a11y": [
|
|
25512
|
+
"Workspace renders a skip link to the canvas (`Skip to issues`) and WorkspaceCanvas is the main landmark; the rail and the inspector are labelled complementary regions (`Workspace navigation`, `Issue details`), and as sheets they keep those labels as their dialog titles.",
|
|
25513
|
+
"The page has one level-1 heading (visually hidden; the breadcrumb shows the place) and the inspector's title is a level-2 heading.",
|
|
25514
|
+
"Group header rows are real buttons with aria-expanded; rows open the inspector from the keyboard (DataTable's row activation). Every icon-only action has an accessible name and a tooltip.",
|
|
25515
|
+
"Each SidebarGroup is a named group; the account row names the person, and its menu is a standard Menu with a radio group for the colour mode."
|
|
25516
|
+
],
|
|
25517
|
+
"imports": "import { useState } from \"react\"\nimport { Avatar, Badge, BottomNav, BottomNavItem, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, Breadcrumbs, BreadcrumbSeparator, Button, DataTable, DatePicker, Density, EmptyState, Form, Icon, Kbd, Menu, MenuContent, MenuItem, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSubmenu, MenuSubmenuTrigger, MenuTrigger, Panel, PanelGroup, PanelSection, PropertyRow, RelativeTime, SearchInput, Select, SelectContent, SelectItem, SelectTrigger, Sidebar, SidebarAccount, SidebarBrand, SidebarFooter, SidebarGroup, SidebarHeader, SidebarItem, SidebarNav, SimpleTooltip, StatusBar, StatusBarGroup, StatusDot, Tabs, TabsList, TabsPanel, TabsTrigger, Textarea, Toggle, ToggleGroup, useBelowBreakpoint, useSableTheme, Workspace, WorkspaceBar, WorkspaceCanvas, type DataTableColumn } from \"@sableui/react\"",
|
|
25518
|
+
"code": "// Workflow states → StatusDot tones, mapped once here so every group header,\n// row and select follows. Sable has no status vocabulary of its own.\nconst STATUSES = [\n\t{ key: \"progress\", label: \"In progress\", tone: \"warning\" },\n\t{ key: \"review\", label: \"In review\", tone: \"2\" },\n\t{ key: \"todo\", label: \"Todo\", tone: \"neutral\" },\n\t{ key: \"done\", label: \"Done\", tone: \"success\" },\n] as const\ntype Status = (typeof STATUSES)[number][\"key\"]\n\nconst PRIORITIES = [\"Urgent\", \"High\", \"Medium\", \"Low\"] as const\ntype Priority = (typeof PRIORITIES)[number]\n\nconst people: Record<string, string> = {\n\tAK: \"Ama Kusi\",\n\tJR: \"Jonas Reiter\",\n\tLM: \"Lena Moreau\",\n\tTO: \"Tomás Ortega\",\n}\n\nconst MIN = 60 * 1000\nconst HOUR = 60 * MIN\nconst DAY = 24 * HOUR\nconst isoDay = (offset: number) => {\n\tconst d = new Date(Date.now() + offset)\n\treturn new Date(d.getFullYear(), d.getMonth(), d.getDate())\n}\n\ntype Note = { id: string; who: string; text: string; at: number }\ntype Issue = {\n\tid: string\n\ttitle: string\n\tdetail: string\n\tstatus: Status\n\tpriority: Priority\n\tassignee: string\n\tlabel: string\n\tdue: Date\n\tnotes: Note[]\n}\n\nconst initialIssues: Issue[] = [\n\t{ id: \"FW-104\", title: \"Sync offline edits when the app reconnects\", detail: \"Edits made offline are queued but replayed out of order after a reconnect. Replay them by timestamp and surface conflicts.\", status: \"progress\", priority: \"Urgent\", assignee: \"AK\", label: \"Sync\", due: isoDay(2 * DAY), notes: [{ id: \"n1\", who: \"JR\", text: \"Repro steps are in the test plan. It only happens after a cold start.\", at: -3 * HOUR }] },\n\t{ id: \"FW-105\", title: \"Onboarding checklist on first launch\", detail: \"Three steps: connect a calendar, invite a teammate, create a first project.\", status: \"progress\", priority: \"Medium\", assignee: \"LM\", label: \"Onboarding\", due: isoDay(5 * DAY), notes: [] },\n\t{ id: \"FW-108\", title: \"Push notification settings per project\", detail: \"Let people mute a project without muting the workspace.\", status: \"review\", priority: \"High\", assignee: \"TO\", label: \"Notifications\", due: isoDay(1 * DAY), notes: [] },\n\t{ id: \"FW-110\", title: \"Crash when attaching a large photo\", detail: \"Photos over 20 MB crash the uploader on older devices.\", status: \"review\", priority: \"Urgent\", assignee: \"JR\", label: \"Bug\", due: isoDay(-1 * DAY), notes: [] },\n\t{ id: \"FW-112\", title: \"Search across archived projects\", detail: \"Archived projects are left out of search today.\", status: \"todo\", priority: \"Low\", assignee: \"AK\", label: \"Search\", due: isoDay(9 * DAY), notes: [] },\n\t{ id: \"FW-113\", title: \"Keyboard shortcuts for the board view\", detail: \"J and K move between cards; C creates an issue in the focused column.\", status: \"todo\", priority: \"Medium\", assignee: \"LM\", label: \"Board\", due: isoDay(12 * DAY), notes: [] },\n\t{ id: \"FW-114\", title: \"Export a project as CSV\", detail: \"One row per issue, with status, assignee and dates.\", status: \"todo\", priority: \"Low\", assignee: \"TO\", label: \"Export\", due: isoDay(14 * DAY), notes: [] },\n\t{ id: \"FW-101\", title: \"Dark mode for the widget\", detail: \"The home-screen widget ignores the system appearance.\", status: \"done\", priority: \"Medium\", assignee: \"JR\", label: \"Widget\", due: isoDay(-4 * DAY), notes: [] },\n]\n\nconst MODE_LABEL = { light: \"Light\", dark: \"Dark\", system: \"System\" } as const\n\n/** One icon-only action with a visible name on hover and focus. */\nexport function TrackerPage() {\n\tconst [issues, setIssues] = useState(initialIssues)\n\tconst [selectedId, setSelectedId] = useState<string | null>(\"FW-104\")\n\t// Below lg the inspector is a sheet; it opens only when someone picks a row,\n\t// so a phone does not load with a sheet over the list.\n\tconst [inspecting, setInspecting] = useState(false)\n\tconst [navOpen, setNavOpen] = useState(false)\n\tconst [query, setQuery] = useState(\"\")\n\tconst [priority, setPriority] = useState<\"all\" | Priority>(\"all\")\n\tconst [groupBy, setGroupBy] = useState<\"status\" | \"assignee\">(\"status\")\n\tconst [tab, setTab] = useState(\"issues\")\n\tconst [draft, setDraft] = useState(\"\")\n\tconst narrow = useBelowBreakpoint(\"lg\")\n\tconst { preference, setPreference } = useSableTheme()\n\n\tconst needle = query.trim().toLowerCase()\n\tconst visible = issues.filter(\n\t\t(i) =>\n\t\t\t(priority === \"all\" || i.priority === priority) &&\n\t\t\t`${i.id} ${i.title} ${i.label}`.toLowerCase().includes(needle),\n\t)\n\tconst selected = issues.find((i) => i.id === selectedId) ?? null\n\tconst done = issues.filter((i) => i.status === \"done\").length\n\n\tfunction update(id: string, patch: Partial<Issue>) {\n\t\tsetIssues((all) => all.map((i) => (i.id === id ? { ...i, ...patch } : i)))\n\t}\n\tfunction open(id: string) {\n\t\tsetSelectedId(id)\n\t\tsetInspecting(true)\n\t}\n\tfunction close() {\n\t\tsetInspecting(false)\n\t\tif (!narrow) setSelectedId(null)\n\t}\n\tfunction addIssue(status: Status = \"todo\") {\n\t\tconst id = `FW-${115 + issues.length}`\n\t\tsetIssues((all) => [\n\t\t\t...all,\n\t\t\t{ id, title: \"New issue\", detail: \"\", status, priority: \"Medium\", assignee: \"AK\", label: \"Triage\", due: isoDay(7 * DAY), notes: [] },\n\t\t])\n\t\topen(id)\n\t}\n\tfunction addNote() {\n\t\tif (!selected || !draft.trim()) return\n\t\tupdate(selected.id, { notes: [...selected.notes, { id: `n${Date.now()}`, who: \"AK\", text: draft.trim(), at: 0 }] })\n\t\tsetDraft(\"\")\n\t}\n\n\tconst columns: DataTableColumn<Issue>[] = [\n\t\t{ key: \"id\", header: \"ID\", className: \"w-20 whitespace-nowrap\", cell: (i) => <span className=\"text-ui-num text-muted-foreground\">{i.id}</span> },\n\t\t{ key: \"title\", header: \"Issue\", className: \"w-full max-w-0\", cell: (i) => <span className=\"block truncate\">{i.title}</span> },\n\t\t{ key: \"label\", header: \"Label\", className: \"w-28 max-sm:hidden\", cell: (i) => <Badge appearance=\"outline\" size=\"sm\">{i.label}</Badge> },\n\t\t{ key: \"due\", header: \"Due\", className: \"w-20 whitespace-nowrap max-sm:hidden\", sortValue: (i) => i.due.getTime(), cell: (i) => <span className=\"text-muted-foreground\">{i.due.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" })}</span> },\n\t\t{ key: \"assignee\", header: <span className=\"sr-only\">Assignee</span>, className: \"w-10\", cell: (i) => <Avatar size=\"2xs\" fallback={i.assignee} alt={people[i.assignee]} /> },\n\t]\n\n\tconst groups =\n\t\tgroupBy === \"status\"\n\t\t\t? STATUSES.map((s) => ({\n\t\t\t\t\tkey: s.key,\n\t\t\t\t\tlabel: s.label,\n\t\t\t\t\ticon: <StatusDot tone={s.tone} />,\n\t\t\t\t\taction: <SimpleTooltip content={`Add to ${s.label}`}><Button size=\"icon-xs\" variant=\"ghost\" aria-label={`Add to ${s.label}`} onClick={() => addIssue(s.key)}><Icon name=\"plus\" /></Button></SimpleTooltip>,\n\t\t\t\t\tdefaultExpanded: s.key !== \"done\",\n\t\t\t\t}))\n\t\t\t: Object.entries(people).map(([key, name]) => ({\n\t\t\t\t\tkey,\n\t\t\t\t\tlabel: name,\n\t\t\t\t\ticon: <Avatar size=\"2xs\" fallback={key} alt=\"\" />,\n\t\t\t\t}))\n\n\tconst sidebar = (\n\t\t<Sidebar variant=\"inline\" className=\"h-full w-full\">\n\t\t\t<SidebarHeader>\n\t\t\t\t<SidebarBrand mark={<Avatar shape=\"square\" size=\"2xs\" tone=\"teal\" fallback=\"F\" alt=\"\" />} label=\"Fieldwork\" />\n\t\t\t\t<Button variant=\"outline\" size=\"sm\" className=\"basis-full justify-between\">\n\t\t\t\t\t<span className=\"flex items-center gap-2\">\n\t\t\t\t\t\t<Icon name=\"search\" />\n\t\t\t\t\t\tSearch\n\t\t\t\t\t</span>\n\t\t\t\t\t<Kbd>Ctrl K</Kbd>\n\t\t\t\t</Button>\n\t\t\t</SidebarHeader>\n\t\t\t<SidebarNav aria-label=\"Workspace\">\n\t\t\t\t<SidebarItem icon={<Icon name=\"inbox\" />} label=\"Inbox\" badge={<span className=\"text-ui-num text-muted-foreground\">3</span>} />\n\t\t\t\t<SidebarItem icon={<Icon name=\"circle-check\" />} label=\"My issues\" />\n\t\t\t\t<SidebarGroup label=\"Mobile app\">\n\t\t\t\t\t<SidebarItem icon={<Icon name=\"layers\" />} label=\"Issues\" active />\n\t\t\t\t\t<SidebarItem icon={<Icon name=\"refresh\" />} label=\"Cycles\" />\n\t\t\t\t\t<SidebarItem icon={<Icon name=\"archive\" />} label=\"Backlog\" />\n\t\t\t\t</SidebarGroup>\n\t\t\t\t<SidebarGroup label=\"Workspace\">\n\t\t\t\t\t<SidebarItem icon={<Icon name=\"folder\" />} label=\"Projects\" />\n\t\t\t\t\t<SidebarItem icon={<Icon name=\"users\" />} label=\"Members\" />\n\t\t\t\t</SidebarGroup>\n\t\t\t</SidebarNav>\n\t\t\t<SidebarFooter>\n\t\t\t\t<Menu>\n\t\t\t\t\t<MenuTrigger\n\t\t\t\t\t\trender={<SidebarAccount avatar={<Avatar size=\"2xs\" fallback=\"AK\" alt=\"\" />} label=\"Ama Kusi\" detail=\"Fieldwork · Admin\" />}\n\t\t\t\t\t/>\n\t\t\t\t\t<MenuContent side=\"top\" align=\"start\">\n\t\t\t\t\t\t<MenuItem icon={<Icon name=\"gear\" />}>Settings</MenuItem>\n\t\t\t\t\t\t<MenuSubmenu>\n\t\t\t\t\t\t\t<MenuSubmenuTrigger\n\t\t\t\t\t\t\t\ticon={<Icon name={preference === \"dark\" ? \"moon\" : preference === \"light\" ? \"sun\" : \"desktop\"} />}\n\t\t\t\t\t\t\t\tvalue={MODE_LABEL[preference]}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tAppearance\n\t\t\t\t\t\t\t</MenuSubmenuTrigger>\n\t\t\t\t\t\t\t<MenuContent side=\"inline-end\">\n\t\t\t\t\t\t\t\t<MenuRadioGroup value={preference} onValueChange={(v) => setPreference(v as typeof preference)}>\n\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"light\">Light</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"dark\">Dark</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"system\">System</MenuRadioItem>\n\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t</MenuContent>\n\t\t\t\t\t\t</MenuSubmenu>\n\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t<MenuItem icon={<Icon name=\"logout\" />}>Sign out</MenuItem>\n\t\t\t\t\t</MenuContent>\n\t\t\t\t</Menu>\n\t\t\t</SidebarFooter>\n\t\t</Sidebar>\n\t)\n\n\tconst list =\n\t\tvisible.length === 0 ? (\n\t\t\t<EmptyState\n\t\t\t\ticon={<Icon name=\"search\" />}\n\t\t\t\ttitle=\"No issues match\"\n\t\t\t\tdescription=\"Try another search, or clear the priority filter.\"\n\t\t\t\taction={\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\tsetQuery(\"\")\n\t\t\t\t\t\t\tsetPriority(\"all\")\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\tClear filters\n\t\t\t\t\t</Button>\n\t\t\t\t}\n\t\t\t/>\n\t\t) : (\n\t\t\t<DataTable\n\t\t\t\tcolumns={columns}\n\t\t\t\tdata={visible}\n\t\t\t\trowKey={(i) => i.id}\n\t\t\t\tgroupBy={(i) => (groupBy === \"status\" ? i.status : i.assignee)}\n\t\t\t\tgroups={groups}\n\t\t\t\tonRowClick={(i) => open(i.id)}\n\t\t\t\t// A phone shows the selection only while its sheet is open.\n\t\t\t\tisRowSelected={(i) => i.id === selectedId && (!narrow || inspecting)}\n\t\t\t/>\n\t\t)\n\n\tconst inspector = selected ? (\n\t\t<div className=\"flex h-full min-h-0 flex-col\">\n\t\t\t<WorkspaceBar className=\"justify-between\">\n\t\t\t\t<span className=\"text-ui-num text-muted-foreground\">{selected.id}</span>\n\t\t\t\t<SimpleTooltip content=\"Close issue\"><Button size=\"icon-xs\" variant=\"ghost\" aria-label=\"Close issue\" onClick={close}><Icon name=\"xmark\" /></Button></SimpleTooltip>\n\t\t\t</WorkspaceBar>\n\t\t\t<div className=\"min-h-0 flex-1 overflow-y-auto scrollbar-quiet\">\n\t\t\t\t<div className=\"flex flex-col gap-2 px-2 pt-4 pb-3\">\n\t\t\t\t\t<h2 className=\"text-title-lg\">{selected.title}</h2>\n\t\t\t\t\t<p className=\"text-ui-body text-muted-foreground\">{selected.detail || \"Add the details the team needs.\"}</p>\n\t\t\t\t</div>\n\t\t\t\t<PanelSection title=\"Properties\">\n\t\t\t\t\t<PropertyRow label=\"Status\">\n\t\t\t\t\t\t<Select value={selected.status} items={STATUSES.map((s) => ({ value: s.key, label: s.label }))} onValueChange={(v) => v && update(selected.id, { status: v as Status })}>\n\t\t\t\t\t\t\t<SelectTrigger size=\"xs\" aria-label=\"Status\" />\n\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t{STATUSES.map((s) => (\n\t\t\t\t\t\t\t\t\t<SelectItem key={s.key} value={s.key}>{s.label}</SelectItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t</Select>\n\t\t\t\t\t</PropertyRow>\n\t\t\t\t\t<PropertyRow label=\"Priority\">\n\t\t\t\t\t\t<Select value={selected.priority} onValueChange={(v) => v && update(selected.id, { priority: v as Priority })}>\n\t\t\t\t\t\t\t<SelectTrigger size=\"xs\" aria-label=\"Priority\" />\n\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t{PRIORITIES.map((p) => (\n\t\t\t\t\t\t\t\t\t<SelectItem key={p} value={p}>{p}</SelectItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t</Select>\n\t\t\t\t\t</PropertyRow>\n\t\t\t\t\t<PropertyRow label=\"Assignee\">\n\t\t\t\t\t\t<Select value={selected.assignee} items={people} onValueChange={(v) => v && update(selected.id, { assignee: v })}>\n\t\t\t\t\t\t\t<SelectTrigger size=\"xs\" aria-label=\"Assignee\" />\n\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t{Object.entries(people).map(([key, name]) => (\n\t\t\t\t\t\t\t\t\t<SelectItem key={key} value={key}>{name}</SelectItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t</Select>\n\t\t\t\t\t</PropertyRow>\n\t\t\t\t\t<PropertyRow label=\"Due date\">\n\t\t\t\t\t\t<DatePicker\n\t\t\t\t\t\t\tsize=\"xs\"\n\t\t\t\t\t\t\taria-label=\"Due date\"\n\t\t\t\t\t\t\tvalue={selected.due}\n\t\t\t\t\t\t\tformatOptions={{ month: \"short\", day: \"numeric\", year: \"numeric\" }}\n\t\t\t\t\t\t\tonValueChange={(d) => d && update(selected.id, { due: d })}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</PropertyRow>\n\t\t\t\t</PanelSection>\n\t\t\t\t<PanelSection title=\"Activity\">\n\t\t\t\t\t<div className=\"flex flex-col gap-3 pb-4\">\n\t\t\t\t\t\t<div className=\"flex items-center gap-2 text-ui-sm text-muted-foreground\">\n\t\t\t\t\t\t\t<Icon name=\"plus\" />\n\t\t\t\t\t\t\t<span>Created in Mobile app</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{selected.notes.map((n) => (\n\t\t\t\t\t\t\t<div key={n.id} className=\"flex items-start gap-2\">\n\t\t\t\t\t\t\t\t<Avatar size=\"2xs\" fallback={n.who} alt=\"\" />\n\t\t\t\t\t\t\t\t<div className=\"min-w-0\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-ui-md\">{people[n.who]}</span>\n\t\t\t\t\t\t\t\t\t\t<RelativeTime date={new Date(Date.now() + n.at)} className=\"text-ui-sm text-muted-foreground\" />\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-ui-body\">{n.text}</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</div>\n\t\t\t\t</PanelSection>\n\t\t\t</div>\n\t\t\t<Form\n\t\t\t\tclassName=\"flex shrink-0 flex-col gap-2 border-t border-hairline p-2\"\n\t\t\t\tonSubmit={(e) => {\n\t\t\t\t\te.preventDefault()\n\t\t\t\t\taddNote()\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Textarea aria-label=\"Add a comment\" placeholder=\"Leave a comment…\" rows={2} value={draft} onChange={(e) => setDraft(e.target.value)} />\n\t\t\t\t<Button size=\"xs\" variant=\"secondary\" type=\"submit\" className=\"self-end\" disabled={!draft.trim()} iconStart={<Icon name=\"arrow-up\" />}>\n\t\t\t\t\tComment\n\t\t\t\t</Button>\n\t\t\t</Form>\n\t\t</div>\n\t) : null\n\n\treturn (\n\t\t<Workspace\n\t\t\tskipLinkLabel=\"Skip to issues\"\n\t\t\tstatusBar={\n\t\t\t\t<StatusBar>\n\t\t\t\t\t<span className=\"flex items-center gap-2\">\n\t\t\t\t\t\t<StatusDot tone=\"success\" size=\"sm\" />\n\t\t\t\t\t\tSaved\n\t\t\t\t\t</span>\n\t\t\t\t\t<span className=\"max-sm:hidden\">\n\t\t\t\t\t\t{issues.length} issues · {done} done\n\t\t\t\t\t</span>\n\t\t\t\t\t<StatusBarGroup align=\"end\">\n\t\t\t\t\t\t<span className=\"max-sm:hidden\">Ctrl K to search</span>\n\t\t\t\t\t</StatusBarGroup>\n\t\t\t\t</StatusBar>\n\t\t\t}\n\t\t\tbottomNav={\n\t\t\t\t<BottomNav aria-label=\"Primary\">\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"inbox\" />} label=\"Inbox\" showLabel />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"layers\" />} label=\"Issues\" showLabel active />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"folder\" />} label=\"Projects\" showLabel />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"users\" />} label=\"Members\" showLabel />\n\t\t\t\t</BottomNav>\n\t\t\t}\n\t\t>\n\t\t\t<PanelGroup>\n\t\t\t\t{/* Docked from lg; below it the same rail is a left sheet. */}\n\t\t\t\t<Panel label=\"Workspace navigation\" width={232} narrow=\"sheet\" side=\"left\" open={narrow ? navOpen : true} onOpenChange={setNavOpen} scroll={false}>\n\t\t\t\t\t{sidebar}\n\t\t\t\t</Panel>\n\t\t\t\t<WorkspaceCanvas className=\"flex flex-col\">\n\t\t\t\t\t<WorkspaceBar className=\"gap-2 px-3\">\n\t\t\t\t\t\t<span className=\"lg:hidden\">\n\t\t\t\t\t\t\t<SimpleTooltip content=\"Open navigation\"><Button size=\"icon-xs\" variant=\"ghost\" aria-label=\"Open navigation\" onClick={() => setNavOpen(true)}><Icon name=\"bars\" /></Button></SimpleTooltip>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<h1 className=\"sr-only\">Mobile app issues</h1>\n\t\t\t\t\t\t<Breadcrumbs className=\"min-w-0\">\n\t\t\t\t\t\t\t<BreadcrumbItem className=\"max-sm:hidden\">\n\t\t\t\t\t\t\t\t<BreadcrumbLink href=\"#\">Fieldwork</BreadcrumbLink>\n\t\t\t\t\t\t\t</BreadcrumbItem>\n\t\t\t\t\t\t\t<BreadcrumbSeparator className=\"max-sm:hidden\" />\n\t\t\t\t\t\t\t<BreadcrumbItem>\n\t\t\t\t\t\t\t\t<BreadcrumbPage>Mobile app</BreadcrumbPage>\n\t\t\t\t\t\t\t</BreadcrumbItem>\n\t\t\t\t\t\t</Breadcrumbs>\n\t\t\t\t\t</WorkspaceBar>\n\t\t\t\t\t<Tabs value={tab} onValueChange={(v) => setTab(String(v))} className=\"flex min-h-0 flex-1 flex-col\">\n\t\t\t\t\t\t{/* A view switch in tool chrome: pill, beside the bar's other controls. */}\n\t\t\t\t\t\t<WorkspaceBar className=\"px-3\">\n\t\t\t\t\t\t\t<TabsList variant=\"pill\">\n\t\t\t\t\t\t\t\t<TabsTrigger value=\"issues\">\n\t\t\t\t\t\t\t\t\tIssues <span className=\"text-ui-num text-muted-foreground\">{issues.length}</span>\n\t\t\t\t\t\t\t\t</TabsTrigger>\n\t\t\t\t\t\t\t\t<TabsTrigger value=\"overview\">Overview</TabsTrigger>\n\t\t\t\t\t\t\t</TabsList>\n\t\t\t\t\t\t</WorkspaceBar>\n\t\t\t\t\t\t<TabsPanel value=\"issues\" className=\"flex min-h-0 flex-1 flex-col\">\n\t\t\t\t\t\t\t<div className=\"flex flex-wrap items-center gap-2 border-b border-hairline px-3 py-2\">\n\t\t\t\t\t\t\t\t<Menu>\n\t\t\t\t\t\t\t\t\t<MenuTrigger render={<Button variant=\"ghost\" size=\"xs\" iconStart={<Icon name=\"filter\" />}>{priority === \"all\" ? \"Filter\" : priority}</Button>} />\n\t\t\t\t\t\t\t\t\t<MenuContent>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioGroup value={priority} onValueChange={(v) => setPriority(v as typeof priority)}>\n\t\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"all\">All priorities</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t\t{PRIORITIES.map((p) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<MenuRadioItem key={p} value={p}>{p}</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t\t</MenuContent>\n\t\t\t\t\t\t\t\t</Menu>\n\t\t\t\t\t\t\t\t<SearchInput size=\"xs\" value={query} onValueChange={setQuery} placeholder=\"Find issue…\" aria-label=\"Find issue\" className=\"w-40\" />\n\t\t\t\t\t\t\t\t<ToggleGroup appearance=\"segmented\" value={[groupBy]} onValueChange={(v) => v[0] && setGroupBy(v[0] as typeof groupBy)} aria-label=\"Group by\" className=\"ms-auto\">\n\t\t\t\t\t\t\t\t\t<Toggle value=\"status\">Status</Toggle>\n\t\t\t\t\t\t\t\t\t<Toggle value=\"assignee\">Assignee</Toggle>\n\t\t\t\t\t\t\t\t</ToggleGroup>\n\t\t\t\t\t\t\t\t<Button size=\"xs\" iconStart={<Icon name=\"plus\" />} onClick={() => addIssue()}>\n\t\t\t\t\t\t\t\t\tNew issue\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"min-h-0 flex-1 overflow-y-auto scrollbar-quiet\">{list}</div>\n\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t\t<TabsPanel value=\"overview\" className=\"flex min-h-0 flex-1 flex-col\">\n\t\t\t\t\t\t\t{/* A reading view: page type comes back inside the compact workspace. */}\n\t\t\t\t\t\t\t<Density value=\"comfortable\" className=\"min-h-0 flex-1 overflow-y-auto scrollbar-quiet\">\n\t\t\t\t\t\t\t\t<article className=\"flex max-w-prose flex-col gap-4 p-8 max-sm:p-5\">\n\t\t\t\t\t\t\t\t\t<h2 className=\"text-heading-3\">Mobile app</h2>\n\t\t\t\t\t\t\t\t\t<p className=\"text-body-md text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tThe phone app for field teams: capture work on site, sync it when the signal comes back, and hand it to the office without retyping anything.\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<dl className=\"sable-list-dl\">\n\t\t\t\t\t\t\t\t\t\t<dt>Lead</dt>\n\t\t\t\t\t\t\t\t\t\t<dd>{people.AK}</dd>\n\t\t\t\t\t\t\t\t\t\t<dt>Open issues</dt>\n\t\t\t\t\t\t\t\t\t\t<dd>{issues.length - done}</dd>\n\t\t\t\t\t\t\t\t\t\t<dt>Target</dt>\n\t\t\t\t\t\t\t\t\t\t<dd>{isoDay(30 * DAY).toLocaleDateString(\"en-US\", { month: \"long\", day: \"numeric\" })}</dd>\n\t\t\t\t\t\t\t\t\t</dl>\n\t\t\t\t\t\t\t\t</article>\n\t\t\t\t\t\t\t</Density>\n\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t</Tabs>\n\t\t\t\t</WorkspaceCanvas>\n\t\t\t\t{/* Docked beside the list from lg; a sheet below it. Its content renders once. */}\n\t\t\t\t<Panel\n\t\t\t\t\tlabel=\"Issue details\"\n\t\t\t\t\twidth={320}\n\t\t\t\t\tnarrow=\"sheet\"\n\t\t\t\t\topen={narrow ? inspecting && selected !== null : selected !== null}\n\t\t\t\t\tonOpenChange={(next) => !next && close()}\n\t\t\t\t\tscroll={false}\n\t\t\t\t\tclassName=\"border-s border-hairline\"\n\t\t\t\t>\n\t\t\t\t\t{inspector}\n\t\t\t\t</Panel>\n\t\t\t</PanelGroup>\n\t\t</Workspace>\n\t)\n}"
|
|
24670
25519
|
}
|
|
24671
25520
|
]
|
|
24672
25521
|
},
|
|
@@ -25740,6 +26589,14 @@
|
|
|
25740
26589
|
"rule": "text-wrap: balance on headings, pretty on ledes, neither on long prose.",
|
|
25741
26590
|
"why": "balance evens short headlines; pretty only refuses a one-word last line. Both cost layout work that long prose does not repay."
|
|
25742
26591
|
},
|
|
26592
|
+
{
|
|
26593
|
+
"rule": "To make all the type smaller or larger, set the base: --type-base: 14 (a unitless px number; 16 is the default). Never retune tiers one by one for it.",
|
|
26594
|
+
"why": "Every tier is authored on a 16px base and keeps its ratio to it, so one number rebuilds the whole scale, headings to captions, and the baseline step and spacing follow the body. Shrinking tiers individually breaks the ratios between them. Set it on :root, a theme scope or a preset (tokens.shared[\"type-base\"]) so the rhythm follows too."
|
|
26595
|
+
},
|
|
26596
|
+
{
|
|
26597
|
+
"rule": "Inside a Workspace, a panel's title is text-ui-title (14px · 600); compact text-title-lg resolves to it. A reading view inside the tool (an overview, a document, a long form) is wrapped in <Density value=\"comfortable\"> and gets the page tiers back.",
|
|
26598
|
+
"why": "The compact register remaps page tiers onto the 10-12px chrome ramp, which is right for chrome and wrong for prose: an issue overview set in a tool rendered its paragraphs at 11px. The remap stops at a comfortable boundary, so the wrapper is the whole fix."
|
|
26599
|
+
},
|
|
25743
26600
|
{
|
|
25744
26601
|
"rule": "A mark attached to a line of type (a dot, a bullet, a timeline marker, a leading icon) sits in a box one line tall (h-[1lh] items-center) and is sized in em.",
|
|
25745
26602
|
"why": "1lh is the line-height of the text beside it, so the mark centres on that line in every tier, density and theme. A px size with a mt-[3px] nudge is right for one font at one size and wrong everywhere else."
|
|
@@ -25773,6 +26630,11 @@
|
|
|
25773
26630
|
"instead": "Ragged-right at body-md; body-xs only for dense data.",
|
|
25774
26631
|
"why": "Justification opens rivers on the web, which has no hyphenation control; 12px prose fails people with low vision."
|
|
25775
26632
|
},
|
|
26633
|
+
{
|
|
26634
|
+
"never": "Changing the root font-size (html { font-size: 14px }) to make a product denser.",
|
|
26635
|
+
"instead": "--type-base: 14, with Workspace or <Density value=\"compact\"> for denser controls.",
|
|
26636
|
+
"why": "The root also sizes every rem the app wrote: widths, insets, media queries. --type-base moves only the type and the rhythm built on it, and density moves the controls, so each can be chosen on its own."
|
|
26637
|
+
},
|
|
25776
26638
|
{
|
|
25777
26639
|
"never": "A new font family, or raw font-feature-settings / font-variation-settings.",
|
|
25778
26640
|
"instead": "The theme's sans, mono and optional display; font-variant-numeric and font-weight.",
|
|
@@ -27267,6 +28129,27 @@
|
|
|
27267
28129
|
"small caps label"
|
|
27268
28130
|
]
|
|
27269
28131
|
},
|
|
28132
|
+
{
|
|
28133
|
+
"name": "text-ui-title",
|
|
28134
|
+
"kind": "utility",
|
|
28135
|
+
"family": "text",
|
|
28136
|
+
"intent": "The title of what a panel or inspector shows — the open issue, the selected object: 14px at 600 on a 20px line, one step above `text-ui-md`.",
|
|
28137
|
+
"whenToUse": "The one title at the top of an inspector, a docked panel or a peek view inside the workspace register. Compact `text-title-lg` resolves to it on its own.",
|
|
28138
|
+
"insteadOf": "`text-ui-md` made heavier (a title the size of the description under it reads as a label), or `text-heading-*`, which is page structure rather than chrome.",
|
|
28139
|
+
"example": "<h2 className=\"text-ui-title text-panel-foreground\">Refine product detail page</h2>",
|
|
28140
|
+
"keywords": [
|
|
28141
|
+
"panel title",
|
|
28142
|
+
"inspector title",
|
|
28143
|
+
"issue title",
|
|
28144
|
+
"peek title",
|
|
28145
|
+
"selection header",
|
|
28146
|
+
"workspace title"
|
|
28147
|
+
],
|
|
28148
|
+
"pairsWith": [
|
|
28149
|
+
"text-ui-body",
|
|
28150
|
+
"text-ui-md"
|
|
28151
|
+
]
|
|
28152
|
+
},
|
|
27270
28153
|
{
|
|
27271
28154
|
"name": "text-ui-md",
|
|
27272
28155
|
"kind": "utility",
|
|
@@ -28149,6 +29032,36 @@
|
|
|
28149
29032
|
"render",
|
|
28150
29033
|
"style"
|
|
28151
29034
|
],
|
|
29035
|
+
"ActivityHeatmap": [
|
|
29036
|
+
"activeDate",
|
|
29037
|
+
"categories",
|
|
29038
|
+
"data",
|
|
29039
|
+
"defaultActiveDate",
|
|
29040
|
+
"end",
|
|
29041
|
+
"formatValue",
|
|
29042
|
+
"label",
|
|
29043
|
+
"legend",
|
|
29044
|
+
"legendLabels",
|
|
29045
|
+
"levelScale",
|
|
29046
|
+
"levels",
|
|
29047
|
+
"loading",
|
|
29048
|
+
"locale",
|
|
29049
|
+
"monthLabels",
|
|
29050
|
+
"motion",
|
|
29051
|
+
"onActiveDateChange",
|
|
29052
|
+
"onSelectDate",
|
|
29053
|
+
"renderCell",
|
|
29054
|
+
"selectedDate",
|
|
29055
|
+
"size",
|
|
29056
|
+
"start",
|
|
29057
|
+
"stretch",
|
|
29058
|
+
"tone",
|
|
29059
|
+
"tooltip",
|
|
29060
|
+
"unit",
|
|
29061
|
+
"weekStart",
|
|
29062
|
+
"weekdayLabels",
|
|
29063
|
+
"year"
|
|
29064
|
+
],
|
|
28152
29065
|
"AlertDialog": [
|
|
28153
29066
|
"actionsRef",
|
|
28154
29067
|
"children",
|
|
@@ -28170,6 +29083,7 @@
|
|
|
28170
29083
|
"className",
|
|
28171
29084
|
"finalFocus",
|
|
28172
29085
|
"initialFocus",
|
|
29086
|
+
"keepMounted",
|
|
28173
29087
|
"render",
|
|
28174
29088
|
"scroll",
|
|
28175
29089
|
"showCloseButton",
|
|
@@ -28262,10 +29176,12 @@
|
|
|
28262
29176
|
"fallbackDelay",
|
|
28263
29177
|
"render",
|
|
28264
29178
|
"seed",
|
|
29179
|
+
"shape",
|
|
28265
29180
|
"size",
|
|
28266
29181
|
"src",
|
|
28267
29182
|
"status",
|
|
28268
29183
|
"style",
|
|
29184
|
+
"tone",
|
|
28269
29185
|
"variant"
|
|
28270
29186
|
],
|
|
28271
29187
|
"AvatarFallback": [
|
|
@@ -28288,8 +29204,10 @@
|
|
|
28288
29204
|
"count",
|
|
28289
29205
|
"label",
|
|
28290
29206
|
"render",
|
|
29207
|
+
"shape",
|
|
28291
29208
|
"size",
|
|
28292
|
-
"style"
|
|
29209
|
+
"style",
|
|
29210
|
+
"tone"
|
|
28293
29211
|
],
|
|
28294
29212
|
"AvatarImage": [
|
|
28295
29213
|
"className",
|
|
@@ -28300,8 +29218,10 @@
|
|
|
28300
29218
|
"AvatarRoot": [
|
|
28301
29219
|
"className",
|
|
28302
29220
|
"render",
|
|
29221
|
+
"shape",
|
|
28303
29222
|
"size",
|
|
28304
|
-
"style"
|
|
29223
|
+
"style",
|
|
29224
|
+
"tone"
|
|
28305
29225
|
],
|
|
28306
29226
|
"Badge": [
|
|
28307
29227
|
"appearance",
|
|
@@ -28630,7 +29550,8 @@
|
|
|
28630
29550
|
"filename",
|
|
28631
29551
|
"highlight",
|
|
28632
29552
|
"prompt",
|
|
28633
|
-
"showCopy"
|
|
29553
|
+
"showCopy",
|
|
29554
|
+
"wrap"
|
|
28634
29555
|
],
|
|
28635
29556
|
"CodeHighlightProvider": [
|
|
28636
29557
|
"children",
|
|
@@ -28650,7 +29571,8 @@
|
|
|
28650
29571
|
"style"
|
|
28651
29572
|
],
|
|
28652
29573
|
"CollapsibleIndicator": [
|
|
28653
|
-
"className"
|
|
29574
|
+
"className",
|
|
29575
|
+
"direction"
|
|
28654
29576
|
],
|
|
28655
29577
|
"CollapsiblePanel": [
|
|
28656
29578
|
"className",
|
|
@@ -28990,11 +29912,16 @@
|
|
|
28990
29912
|
"DataTable": [
|
|
28991
29913
|
"caption",
|
|
28992
29914
|
"className",
|
|
29915
|
+
"collapsible",
|
|
28993
29916
|
"columns",
|
|
28994
29917
|
"data",
|
|
28995
29918
|
"defaultSort",
|
|
28996
29919
|
"empty",
|
|
29920
|
+
"expandedGroups",
|
|
29921
|
+
"groupBy",
|
|
29922
|
+
"groups",
|
|
28997
29923
|
"isRowSelected",
|
|
29924
|
+
"onExpandedGroupsChange",
|
|
28998
29925
|
"onRowClick",
|
|
28999
29926
|
"onSelectedKeysChange",
|
|
29000
29927
|
"rowKey",
|
|
@@ -29007,6 +29934,7 @@
|
|
|
29007
29934
|
"defaultTime",
|
|
29008
29935
|
"defaultValue",
|
|
29009
29936
|
"disabled",
|
|
29937
|
+
"formatOptions",
|
|
29010
29938
|
"granularity",
|
|
29011
29939
|
"maxDate",
|
|
29012
29940
|
"minDate",
|
|
@@ -29014,6 +29942,7 @@
|
|
|
29014
29942
|
"onChange",
|
|
29015
29943
|
"onValueChange",
|
|
29016
29944
|
"placeholder",
|
|
29945
|
+
"size",
|
|
29017
29946
|
"value"
|
|
29018
29947
|
],
|
|
29019
29948
|
"DateTile": [
|
|
@@ -29064,6 +29993,7 @@
|
|
|
29064
29993
|
"className",
|
|
29065
29994
|
"finalFocus",
|
|
29066
29995
|
"initialFocus",
|
|
29996
|
+
"keepMounted",
|
|
29067
29997
|
"render",
|
|
29068
29998
|
"scroll",
|
|
29069
29999
|
"showCloseButton",
|
|
@@ -29380,6 +30310,7 @@
|
|
|
29380
30310
|
"fallback",
|
|
29381
30311
|
"fit",
|
|
29382
30312
|
"loading",
|
|
30313
|
+
"position",
|
|
29383
30314
|
"seed",
|
|
29384
30315
|
"sizes",
|
|
29385
30316
|
"src",
|
|
@@ -29393,6 +30324,7 @@
|
|
|
29393
30324
|
"href",
|
|
29394
30325
|
"loading",
|
|
29395
30326
|
"meta",
|
|
30327
|
+
"position",
|
|
29396
30328
|
"render",
|
|
29397
30329
|
"seed",
|
|
29398
30330
|
"size",
|
|
@@ -29569,6 +30501,7 @@
|
|
|
29569
30501
|
"closeDelay",
|
|
29570
30502
|
"delay",
|
|
29571
30503
|
"disabled",
|
|
30504
|
+
"icon",
|
|
29572
30505
|
"id",
|
|
29573
30506
|
"label",
|
|
29574
30507
|
"nativeButton",
|
|
@@ -29810,6 +30743,7 @@
|
|
|
29810
30743
|
"actions",
|
|
29811
30744
|
"back",
|
|
29812
30745
|
"description",
|
|
30746
|
+
"size",
|
|
29813
30747
|
"title",
|
|
29814
30748
|
"titleAs"
|
|
29815
30749
|
],
|
|
@@ -29828,9 +30762,14 @@
|
|
|
29828
30762
|
"total"
|
|
29829
30763
|
],
|
|
29830
30764
|
"Panel": [
|
|
30765
|
+
"below",
|
|
29831
30766
|
"children",
|
|
29832
30767
|
"label",
|
|
30768
|
+
"narrow",
|
|
30769
|
+
"onOpenChange",
|
|
30770
|
+
"open",
|
|
29833
30771
|
"scroll",
|
|
30772
|
+
"side",
|
|
29834
30773
|
"width"
|
|
29835
30774
|
],
|
|
29836
30775
|
"PanelResizer": [
|
|
@@ -30178,6 +31117,7 @@
|
|
|
30178
31117
|
"inputClassName",
|
|
30179
31118
|
"onClear",
|
|
30180
31119
|
"onValueChange",
|
|
31120
|
+
"size",
|
|
30181
31121
|
"value"
|
|
30182
31122
|
],
|
|
30183
31123
|
"SectionHeader": [
|
|
@@ -30395,11 +31335,21 @@
|
|
|
30395
31335
|
"surface",
|
|
30396
31336
|
"variant"
|
|
30397
31337
|
],
|
|
31338
|
+
"SidebarAccount": [
|
|
31339
|
+
"avatar",
|
|
31340
|
+
"detail",
|
|
31341
|
+
"indicator",
|
|
31342
|
+
"label",
|
|
31343
|
+
"render"
|
|
31344
|
+
],
|
|
30398
31345
|
"SidebarBrand": [
|
|
30399
31346
|
"label",
|
|
30400
31347
|
"mark",
|
|
30401
31348
|
"render"
|
|
30402
31349
|
],
|
|
31350
|
+
"SidebarGroup": [
|
|
31351
|
+
"label"
|
|
31352
|
+
],
|
|
30403
31353
|
"SidebarItem": [
|
|
30404
31354
|
"active",
|
|
30405
31355
|
"badge",
|
|
@@ -30932,6 +31882,7 @@
|
|
|
30932
31882
|
"name"
|
|
30933
31883
|
],
|
|
30934
31884
|
"Workspace": [
|
|
31885
|
+
"bottomNav",
|
|
30935
31886
|
"children",
|
|
30936
31887
|
"density",
|
|
30937
31888
|
"skipLinkLabel",
|