@sableui/react 0.16.0 → 0.18.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 +132 -0
- package/README.md +11 -4
- package/bin/sable.mjs +160 -18
- 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/approval-block.js +30 -48
- package/dist/blocks/approval-block.meta.js +7 -8
- 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/code-block/code-block.js +5 -3
- package/dist/components/code-block/code-block.meta.js +6 -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/page-header/page-header.js +9 -4
- package/dist/components/page-header/page-header.meta.js +26 -1
- 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/section-rail/index.js +7 -0
- package/dist/components/section-rail/section-rail.js +153 -0
- package/dist/components/section-rail/section-rail.meta.js +189 -0
- package/dist/components/sheet/sheet.js +15 -3
- package/dist/components/sheet/sheet.meta.js +2 -2
- package/dist/file-hashes.json +53 -39
- package/dist/icon-sets/reicon.js +23 -1
- package/dist/index.d.ts +364 -27
- package/dist/index.js +11 -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/focus-return.js +53 -0
- package/dist/lib/taxonomy.js +2 -0
- package/dist/lib/use-active-section.js +59 -0
- package/dist/styles.css +122 -114
- package/llms/blocks/approval-block.md +40 -55
- package/llms/components/activity-heatmap.md +133 -0
- package/llms/components/code-block.md +1 -0
- 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/page-header.md +16 -1
- 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/section-rail.md +101 -0
- package/llms/components/sheet.md +2 -2
- 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/tasks/auditing-an-app.md +31 -0
- package/llms/tasks/getting-started.md +10 -3
- package/llms/tasks/using-presets.md +1 -2
- package/llms-full.txt +516 -90
- package/llms.txt +5 -3
- package/package.json +6 -1
- package/registry.json +745 -46
- package/scripts/adoption.mjs +239 -0
- package/scripts/agent-setup.mjs +110 -18
- package/scripts/grade.mjs +17 -0
- package/scripts/wiring.mjs +2 -1
- package/skills/entry/SKILL.md +1 -1
- package/skills/sable/SKILL.md +33 -4
- package/skills/sable/agents/openai.yaml +1 -1
- package/targets/ios.json +18 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/components/activity-heatmap/activity-heatmap.meta.ts
|
|
2
|
+
var activityHeatmapMeta = {
|
|
3
|
+
name: "ActivityHeatmap",
|
|
4
|
+
summary: "A calendar heatmap: one value per day as a grid of weeks by weekdays \u2014 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.",
|
|
5
|
+
keywords: [
|
|
6
|
+
"heatmap",
|
|
7
|
+
"calendar heatmap",
|
|
8
|
+
"contribution graph",
|
|
9
|
+
"contributions",
|
|
10
|
+
"activity",
|
|
11
|
+
"github",
|
|
12
|
+
"streak",
|
|
13
|
+
"habit",
|
|
14
|
+
"daily",
|
|
15
|
+
"commits",
|
|
16
|
+
"uptime"
|
|
17
|
+
],
|
|
18
|
+
usage: {
|
|
19
|
+
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.",
|
|
20
|
+
bestPractices: [
|
|
21
|
+
{
|
|
22
|
+
guidance: "do",
|
|
23
|
+
description: 'Pass dates as "YYYY-MM-DD" strings. The grid reads them as calendar days, so no time zone can shift a day \u2014 never build them with `new Date(...).toISOString()` from a local date.'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
guidance: "do",
|
|
27
|
+
description: 'Give it a `unit` ("contribution" / "contributions") so the tooltip and every cell\'s name say what the number counts.'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
guidance: "do",
|
|
31
|
+
description: "Let the default quantile scale pick levels \u2014 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)."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
guidance: "dont",
|
|
35
|
+
description: "Use it for a handful of values or for values a reader must compare exactly; a heatmap shows rhythm, not amounts. Use BarChart.",
|
|
36
|
+
instead: ["BarChart", "AreaChart"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
guidance: "dont",
|
|
40
|
+
description: "Use it for a single month of marked days with a streak; that is Calendar with modifiers.",
|
|
41
|
+
instead: ["Calendar"]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
guidance: "dont",
|
|
45
|
+
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."
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
anatomy: [
|
|
49
|
+
{ name: "Weekday labels", required: false, description: "Short weekday names beside the rows, outside the scroller so the weeks never slide under them." },
|
|
50
|
+
{ name: "Month labels", required: false, description: "Short month names over each month's first week; they scroll with their weeks." },
|
|
51
|
+
{ name: "Cell grid", required: true, description: "A role=grid table, one gridcell per day, inside a horizontal Scroller that opens on the latest week." },
|
|
52
|
+
{ name: "Tooltip", required: false, description: "The chart tooltip card, anchored to the hovered, focused or tapped day." },
|
|
53
|
+
{ name: "Legend", required: false, description: "A Less\u2013More ramp for intensity, or swatches for categories." }
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
related: {
|
|
57
|
+
components: ["Card", "NumberTicker", "BarChart", "Calendar", "Scroller"]
|
|
58
|
+
},
|
|
59
|
+
status: "stable",
|
|
60
|
+
props: [
|
|
61
|
+
{ name: "label", type: "string", description: 'What the grid shows ("Contributions in 2026") \u2014 its caption and accessible name.' },
|
|
62
|
+
{
|
|
63
|
+
name: "data",
|
|
64
|
+
type: "ActivityDay[] | Record<string, number>",
|
|
65
|
+
description: 'The days: `{ date: "YYYY-MM-DD", value, level?, category? }[]`, or a map of date to value. Sparse is fine \u2014 a missing day is 0; duplicates sum.'
|
|
66
|
+
},
|
|
67
|
+
{ name: "start", type: "string", default: "53 weeks before end", description: `First day drawn, "YYYY-MM-DD". The default starts at the beginning of that week, as GitHub's does.` },
|
|
68
|
+
{ name: "end", type: "string", default: "today", description: "Last day drawn. Days after today inside the range are drawn hollow and skipped by the keyboard." },
|
|
69
|
+
{ name: "year", type: "number", description: "Draw Jan 1 \u2013 Dec 31 of this year. Wins over start / end." },
|
|
70
|
+
{ name: "weekStart", type: "0 | 1", default: "the locale's", description: "0 puts Sunday on the top row, 1 Monday." },
|
|
71
|
+
{ name: "levels", type: "number", default: "5", description: "How many levels, including the empty level 0." },
|
|
72
|
+
{
|
|
73
|
+
name: "levelScale",
|
|
74
|
+
type: '"quantile" | "linear" | number[] | ((value, stats) => number)',
|
|
75
|
+
default: '"quantile"',
|
|
76
|
+
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."
|
|
77
|
+
},
|
|
78
|
+
{ name: "tone", type: "ChartTone", default: '"1"', description: 'The hue of the intensity ramp. Decorative tones lead, as on every chart; "success" gives the GitHub look.' },
|
|
79
|
+
{ name: "categories", type: "ChartCategories", description: "Colour days by `day.category` (label, tone or colour per key). Pair with `levels={2}` for flat category colours." },
|
|
80
|
+
{ name: "unit", type: "string | { one: string; other: string }", description: "Names the value in the tooltip and each cell's accessible name, pluralised with Intl.PluralRules." },
|
|
81
|
+
{ name: "formatValue", type: "(value: number) => string", default: "full to 9,999, compact past", description: "Formats a value wherever it is shown or read." },
|
|
82
|
+
{ name: "tooltip", type: "boolean | ((day: ActivityHeatmapDay) => ReactNode)", default: "true", description: "Show the tooltip on hover, focus and tap; a function replaces its content on the same card." },
|
|
83
|
+
{ name: "legend", type: "boolean", default: "true", description: "The Less\u2013More ramp for intensity, swatches for categories." },
|
|
84
|
+
{ name: "legendLabels", type: "[string, string]", default: '["Less", "More"]', description: "The ramp's end words." },
|
|
85
|
+
{ name: "monthLabels", type: "boolean", default: "true", description: "Short month names over each month's first week." },
|
|
86
|
+
{ name: "weekdayLabels", type: '"alternate" | "all" | "none"', default: '"alternate"', description: "Which weekday names to show beside the rows; alternate is Mon, Wed, Fri." },
|
|
87
|
+
{ name: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "The smallest a cell may get (10, 12, 14 px) before the grid scrolls instead of shrinking." },
|
|
88
|
+
{ name: "stretch", type: "boolean", default: "true", description: "Grow cells to fill the width, up to 20 px. false keeps them at `size`." },
|
|
89
|
+
{ name: "activeDate", type: "string | null", description: "The hovered or focused day, controlled. Pair with defaultActiveDate / onActiveDateChange." },
|
|
90
|
+
{ name: "selectedDate", type: "string | null", description: "The selected day, controlled. Selection is on only when onSelectDate is set." },
|
|
91
|
+
{ name: "onSelectDate", type: "(date: string) => void", description: "Called when a day is clicked or chosen with Enter / Space \u2014 to filter a feed below, as GitHub does." },
|
|
92
|
+
{ name: "renderCell", type: "(day, cell) => ReactNode", description: "Draw something over a cell's fill \u2014 an icon for a milestone, a reward, an outage." },
|
|
93
|
+
{ name: "loading", type: "boolean", default: "false", description: "Draw the empty grid, shimmering, while data loads." },
|
|
94
|
+
{ name: "motion", type: '"full" | "subtle" | "off"', default: '"full"', description: "Turns the arrival down; `subtle` shows the finished grid at once." },
|
|
95
|
+
{ name: "locale", type: "string", default: "runtime locale", description: "Month and weekday names, numbers and plurals." }
|
|
96
|
+
],
|
|
97
|
+
tokensUsed: ["foreground", "muted-foreground", "border-strong", "ring", "tone-1-mark", "tone-2-mark", "tone-3-mark", "success-mark", "tone-info-mark"],
|
|
98
|
+
motion: [
|
|
99
|
+
{
|
|
100
|
+
slot: "arrival",
|
|
101
|
+
recipe: "custom",
|
|
102
|
+
part: "Cell grid",
|
|
103
|
+
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."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
slot: "response",
|
|
107
|
+
recipe: "custom",
|
|
108
|
+
part: "Cell",
|
|
109
|
+
why: "The active day takes a ring outline at once and the tooltip glides to it; the other cells stay still \u2014 dimming hundreds of cells reads as a flash."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
slot: "transition",
|
|
113
|
+
recipe: "custom",
|
|
114
|
+
part: "Cell",
|
|
115
|
+
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."
|
|
116
|
+
},
|
|
117
|
+
{ slot: "activity", recipe: "motion-shimmer", part: "Cell grid", why: "`loading` shimmers the empty grid until days arrive." }
|
|
118
|
+
],
|
|
119
|
+
a11y: [
|
|
120
|
+
"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`.",
|
|
121
|
+
"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.",
|
|
122
|
+
`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.`,
|
|
123
|
+
"Levels rise in lightness, so they read without colour; under forced colours each level draws as a larger inner square."
|
|
124
|
+
],
|
|
125
|
+
examples: [
|
|
126
|
+
{
|
|
127
|
+
label: "Contributions, last year",
|
|
128
|
+
code: 'const days = [\n { date: "2026-10-12", value: 4 },\n { date: "2026-10-13", value: 11 },\n { 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" }} />'
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
label: "Two sources, in a card",
|
|
132
|
+
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 <CardHeader>\n <CardTitle>Building, day by day</CardTitle>\n </CardHeader>\n <CardContent>\n <ActivityHeatmap\n label="GitHub and Codex activity in 2026"\n year={2026}\n levels={2}\n data={mergeActivity({ github, codex })}\n categories={{ github: { label: "GitHub", tone: "success" }, codex: { label: "Codex", tone: "info" }, both: { label: "Both", tone: "2" } }}\n />\n </CardContent>\n</Card>'
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
label: "Selecting a day",
|
|
136
|
+
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} />'
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
};
|
|
140
|
+
export {
|
|
141
|
+
activityHeatmapMeta
|
|
142
|
+
};
|
|
@@ -25,6 +25,7 @@ function CodeBlock({
|
|
|
25
25
|
showCopy = true,
|
|
26
26
|
highlight,
|
|
27
27
|
prompt,
|
|
28
|
+
wrap = false,
|
|
28
29
|
className,
|
|
29
30
|
...props
|
|
30
31
|
}) {
|
|
@@ -73,10 +74,11 @@ function CodeBlock({
|
|
|
73
74
|
"pre",
|
|
74
75
|
{
|
|
75
76
|
ref: preRef,
|
|
76
|
-
onScroll: fade.sync,
|
|
77
|
-
style: fade.style,
|
|
77
|
+
onScroll: wrap ? void 0 : fade.sync,
|
|
78
|
+
style: wrap ? void 0 : fade.style,
|
|
78
79
|
className: cn(
|
|
79
|
-
"
|
|
80
|
+
"p-4 text-mono-md text-foreground",
|
|
81
|
+
wrap ? "whitespace-pre-wrap break-words" : "overflow-x-auto overscroll-x-contain scrollbar-quiet",
|
|
80
82
|
copyColumn && "min-w-0 flex-1 pe-2"
|
|
81
83
|
),
|
|
82
84
|
children: /* @__PURE__ */ jsx("code", { children: highlighter ? highlighter(code) : code })
|
|
@@ -69,6 +69,12 @@ var codeBlockMeta = {
|
|
|
69
69
|
name: "prompt",
|
|
70
70
|
type: "string",
|
|
71
71
|
description: 'A shell prompt ("$", ">"). Makes it a one-line command: the prompt is dimmed and unselectable, the line truncates, and copy takes only code.'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "wrap",
|
|
75
|
+
type: "boolean",
|
|
76
|
+
default: "false",
|
|
77
|
+
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."
|
|
72
78
|
}
|
|
73
79
|
],
|
|
74
80
|
subcomponents: [
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
// src/components/dialog/dialog.tsx
|
|
2
2
|
import { AlertDialog as AlertBase } from "@base-ui/react/alert-dialog";
|
|
3
3
|
import { Dialog as Base } from "@base-ui/react/dialog";
|
|
4
|
+
import { useMemo } from "react";
|
|
4
5
|
import { Icon } from "../../icon/index.js";
|
|
5
6
|
import { cn } from "../../lib/cn.js";
|
|
7
|
+
import { FocusReturnProvider, useFocusReturn, useFocusReturnDefault } from "../../lib/focus-return.js";
|
|
6
8
|
import { withPortalThemeScope } from "../../lib/portal-theme-scope.js";
|
|
7
9
|
import { PresentationProvider } from "../../lib/presentation.js";
|
|
10
|
+
import { mergeRefs } from "../../lib/refs.js";
|
|
8
11
|
import { Button } from "../button/button.js";
|
|
9
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
-
|
|
13
|
+
function Dialog({ open, onOpenChange, ...props }) {
|
|
14
|
+
const focus = useFocusReturn(open, onOpenChange);
|
|
15
|
+
return /* @__PURE__ */ jsx(FocusReturnProvider, { value: focus.focusReturn, children: /* @__PURE__ */ jsx(Base.Root, { ...props, open, onOpenChange: focus.onOpenChange }) });
|
|
16
|
+
}
|
|
11
17
|
var DialogTrigger = Base.Trigger;
|
|
12
18
|
var DialogClose = Base.Close;
|
|
13
19
|
var DialogPortal = withPortalThemeScope(Base.Portal);
|
|
@@ -29,13 +35,22 @@ function DialogContent({
|
|
|
29
35
|
size = "default",
|
|
30
36
|
showCloseButton,
|
|
31
37
|
scroll,
|
|
38
|
+
finalFocus,
|
|
39
|
+
ref,
|
|
32
40
|
...props
|
|
33
41
|
}) {
|
|
34
|
-
|
|
42
|
+
const focusReturn = useFocusReturnDefault();
|
|
43
|
+
const popupRef = useMemo(
|
|
44
|
+
() => focusReturn ? mergeRefs(focusReturn.popupRef, ref) : ref,
|
|
45
|
+
[focusReturn, ref]
|
|
46
|
+
);
|
|
47
|
+
const focus = { ref: popupRef, finalFocus: finalFocus === void 0 ? focusReturn?.finalFocus : finalFocus };
|
|
48
|
+
return size === "fullscreen" ? /* @__PURE__ */ jsx(FullscreenContent, { showCloseButton: showCloseButton ?? true, ...focus, ...props }) : /* @__PURE__ */ jsx(
|
|
35
49
|
CardContent,
|
|
36
50
|
{
|
|
37
51
|
showCloseButton: showCloseButton ?? false,
|
|
38
52
|
scroll: scroll ?? true,
|
|
53
|
+
...focus,
|
|
39
54
|
...props
|
|
40
55
|
}
|
|
41
56
|
);
|
|
@@ -44,9 +59,10 @@ function FullscreenContent({
|
|
|
44
59
|
className,
|
|
45
60
|
children,
|
|
46
61
|
showCloseButton,
|
|
62
|
+
keepMounted,
|
|
47
63
|
...props
|
|
48
64
|
}) {
|
|
49
|
-
return /* @__PURE__ */ jsxs(DialogPortal, { children: [
|
|
65
|
+
return /* @__PURE__ */ jsxs(DialogPortal, { keepMounted, children: [
|
|
50
66
|
/* @__PURE__ */ jsx(DialogOverlay, { size: "fullscreen" }),
|
|
51
67
|
/* @__PURE__ */ jsxs(
|
|
52
68
|
Base.Popup,
|
|
@@ -93,9 +109,10 @@ function CardContent({
|
|
|
93
109
|
children,
|
|
94
110
|
showCloseButton,
|
|
95
111
|
scroll,
|
|
112
|
+
keepMounted,
|
|
96
113
|
...props
|
|
97
114
|
}) {
|
|
98
|
-
return /* @__PURE__ */ jsxs(DialogPortal, { children: [
|
|
115
|
+
return /* @__PURE__ */ jsxs(DialogPortal, { keepMounted, children: [
|
|
99
116
|
/* @__PURE__ */ jsx(DialogOverlay, {}),
|
|
100
117
|
/* @__PURE__ */ jsxs(
|
|
101
118
|
Base.Popup,
|
|
@@ -175,7 +192,10 @@ function DialogFooter({
|
|
|
175
192
|
}
|
|
176
193
|
);
|
|
177
194
|
}
|
|
178
|
-
|
|
195
|
+
function AlertDialog({ open, onOpenChange, ...props }) {
|
|
196
|
+
const focus = useFocusReturn(open, onOpenChange);
|
|
197
|
+
return /* @__PURE__ */ jsx(FocusReturnProvider, { value: focus.focusReturn, children: /* @__PURE__ */ jsx(AlertBase.Root, { ...props, open, onOpenChange: focus.onOpenChange }) });
|
|
198
|
+
}
|
|
179
199
|
var AlertDialogTrigger = AlertBase.Trigger;
|
|
180
200
|
var AlertDialogClose = DialogClose;
|
|
181
201
|
var AlertDialogContent = DialogContent;
|
|
@@ -59,6 +59,18 @@ var dialogMeta = {
|
|
|
59
59
|
description: "Open a dialog for passive status updates. Use Toast, Banner, or InlineStatus instead.",
|
|
60
60
|
instead: ["Toast", "Banner", "InlineStatus"]
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
guidance: "do",
|
|
64
|
+
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."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
guidance: "do",
|
|
68
|
+
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."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
guidance: "dont",
|
|
72
|
+
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."
|
|
73
|
+
},
|
|
62
74
|
{
|
|
63
75
|
guidance: "do",
|
|
64
76
|
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 \u2014 only Cancel, Esc, or the action. For irreversible deletes, use DestructiveActionModal, which is built on it.'
|
|
@@ -101,8 +113,11 @@ var dialogMeta = {
|
|
|
101
113
|
{
|
|
102
114
|
name: "Dialog (Root)",
|
|
103
115
|
type: "Base UI Dialog.Root props (open, defaultOpen, onOpenChange, modal)",
|
|
104
|
-
description: "Controls open state. Uncontrolled by default."
|
|
105
|
-
}
|
|
116
|
+
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."
|
|
117
|
+
},
|
|
118
|
+
{ name: "open", type: "boolean", description: "Controlled open state. Pair with onOpenChange \u2014 for a dialog opened from your own button instead of a DialogTrigger." },
|
|
119
|
+
{ name: "defaultOpen", type: "boolean", default: "false", description: "Initial open state when uncontrolled." },
|
|
120
|
+
{ name: "onOpenChange", type: "(open: boolean, eventDetails) => void", description: "Called when the dialog asks to open or close (a trigger, Esc, an outside click, DialogClose)." }
|
|
106
121
|
],
|
|
107
122
|
variants: {
|
|
108
123
|
size: ["default", "fullscreen"]
|
|
@@ -110,8 +125,8 @@ var dialogMeta = {
|
|
|
110
125
|
subcomponents: [
|
|
111
126
|
{
|
|
112
127
|
name: "DialogContent",
|
|
113
|
-
type: 'Base UI Popup props + size?: "default" | "fullscreen" + showCloseButton?: boolean + scroll?: boolean + className',
|
|
114
|
-
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.'
|
|
128
|
+
type: 'Base UI Popup props (initialFocus, finalFocus) + size?: "default" | "fullscreen" + showCloseButton?: boolean + scroll?: boolean + keepMounted?: boolean + className',
|
|
129
|
+
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.'
|
|
115
130
|
},
|
|
116
131
|
{
|
|
117
132
|
name: "DialogOverlay",
|
|
@@ -151,7 +166,7 @@ var dialogMeta = {
|
|
|
151
166
|
{
|
|
152
167
|
name: "AlertDialog (Root)",
|
|
153
168
|
type: "Base UI AlertDialog.Root props (open, defaultOpen, onOpenChange)",
|
|
154
|
-
description: 'Root for a confirmation that needs an answer. Always modal; the popup is role="alertdialog" and a click outside does not dismiss \u2014 only an AlertDialogClose, an action that sets open to false, or Esc.'
|
|
169
|
+
description: 'Root for a confirmation that needs an answer. Always modal; the popup is role="alertdialog" and a click outside does not dismiss \u2014 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.'
|
|
155
170
|
},
|
|
156
171
|
{
|
|
157
172
|
name: "AlertDialogTrigger",
|
|
@@ -210,6 +225,8 @@ var dialogMeta = {
|
|
|
210
225
|
a11y: [
|
|
211
226
|
"Base UI provides focus trapping, scroll lock, aria-modal, and labelled-by wiring (DialogTitle).",
|
|
212
227
|
"Content taller than the viewport scrolls inside the dialog (thin scrollbar, contained overscroll) rather than overflowing off screen \u2014 the page behind is scroll-locked, so anything past the edge would be unreachable.",
|
|
228
|
+
"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.",
|
|
229
|
+
"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.",
|
|
213
230
|
'Esc and click-outside dismiss by default. AlertDialog keeps Esc but ignores click-outside, and its popup is role="alertdialog".',
|
|
214
231
|
"Animate via data-[starting-style]/data-[ending-style] attributes.",
|
|
215
232
|
'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 \u2014 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).'
|
|
@@ -231,6 +248,10 @@ var dialogMeta = {
|
|
|
231
248
|
label: "Settings form",
|
|
232
249
|
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>'
|
|
233
250
|
},
|
|
251
|
+
{
|
|
252
|
+
label: "Opened from your own button, keeping a draft",
|
|
253
|
+
code: 'const [open, setOpen] = useState(false)\n\n<Button variant="secondary" onClick={() => setOpen(true)}>New message</Button>\n<Dialog open={open} onOpenChange={setOpen}>\n <DialogContent keepMounted showCloseButton>\n <DialogHeader>\n <DialogTitle>New message</DialogTitle>\n <DialogDescription>Close it any time. Your draft is here when you come back.</DialogDescription>\n </DialogHeader>\n <Field label="Message"><Textarea /></Field>\n <DialogFooter>\n <DialogClose render={<Button variant="secondary">Close</Button>} />\n <Button onClick={() => setOpen(false)}>Send</Button>\n </DialogFooter>\n </DialogContent>\n</Dialog>'
|
|
254
|
+
},
|
|
234
255
|
{
|
|
235
256
|
label: "Destructive confirmation",
|
|
236
257
|
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>'
|
|
@@ -12,6 +12,7 @@ function MediaImage({
|
|
|
12
12
|
seed,
|
|
13
13
|
fallback,
|
|
14
14
|
fit = "cover",
|
|
15
|
+
position,
|
|
15
16
|
srcSet,
|
|
16
17
|
sizes,
|
|
17
18
|
loading = "lazy",
|
|
@@ -42,7 +43,8 @@ function MediaImage({
|
|
|
42
43
|
loading,
|
|
43
44
|
decoding: "async",
|
|
44
45
|
onLoadingStatusChange: (next) => setTracked({ src, status: next }),
|
|
45
|
-
className: cn("size-full motion-fade", fit === "contain" ? "object-contain" : "object-cover")
|
|
46
|
+
className: cn("size-full motion-fade", fit === "contain" ? "object-contain" : "object-cover"),
|
|
47
|
+
style: position != null ? { objectPosition: position } : void 0
|
|
46
48
|
}
|
|
47
49
|
) : null,
|
|
48
50
|
showFallback ? /* @__PURE__ */ jsx("div", { "data-slot": "media-image-fallback", "aria-hidden": true, className: "absolute inset-0", children: fallback ?? /* @__PURE__ */ jsx(GradientArt, { seed: seed ?? (alt || void 0), className: "size-full" }) }) : null
|
|
@@ -30,6 +30,10 @@ var mediaImageMeta = {
|
|
|
30
30
|
guidance: "do",
|
|
31
31
|
description: "Leave `alt` empty when a visible title sits next to the image \u2014 a tile, a row \u2014 and describe it when the image stands alone. A set `alt` also names the fallback, so the description survives a broken URL."
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
guidance: "do",
|
|
35
|
+
description: 'Set `position` when a cover crop would cut off the subject \u2014 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.'
|
|
36
|
+
},
|
|
33
37
|
{
|
|
34
38
|
guidance: "dont",
|
|
35
39
|
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."
|
|
@@ -57,6 +61,12 @@ var mediaImageMeta = {
|
|
|
57
61
|
default: '"cover"',
|
|
58
62
|
description: "`cover` crops to fill (artwork, photos); `contain` letterboxes on the muted surface (a logo, a product that must not be cropped)."
|
|
59
63
|
},
|
|
64
|
+
{
|
|
65
|
+
name: "position",
|
|
66
|
+
type: 'CSSProperties["objectPosition"]',
|
|
67
|
+
default: '"center"',
|
|
68
|
+
description: 'Where the image sits in its frame \u2014 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.'
|
|
69
|
+
},
|
|
60
70
|
{ name: "srcSet", type: "string", description: "Responsive sources, passed to the `<img>`." },
|
|
61
71
|
{ name: "sizes", type: "string", description: "The `sizes` attribute for `srcSet`." },
|
|
62
72
|
{ name: "loading", type: '"lazy" | "eager"', default: '"lazy"', description: "Native lazy loading. Use `eager` for the first row of a grid above the fold." },
|
|
@@ -84,7 +94,11 @@ var mediaImageMeta = {
|
|
|
84
94
|
{ label: "Cover with fallback", code: '<AspectBox ratio={1} className="rounded-md">\n <MediaImage src={album.cover} seed={album.title} />\n</AspectBox>' },
|
|
85
95
|
{ label: "No image yet", code: '<AspectBox ratio={1} className="rounded-md">\n <MediaImage seed="Low tide, high noon" />\n</AspectBox>' },
|
|
86
96
|
{ label: "Row thumbnail", code: '<MediaImage src={track.art} seed={track.title} className="size-12 shrink-0 rounded-sm" />' },
|
|
87
|
-
{ label: "Logo, never cropped", code: '<AspectBox ratio={16 / 9} className="rounded-lg">\n <MediaImage src={brand.logo} alt={brand.name} fit="contain" />\n</AspectBox>' }
|
|
97
|
+
{ label: "Logo, never cropped", code: '<AspectBox ratio={16 / 9} className="rounded-lg">\n <MediaImage src={brand.logo} alt={brand.name} fit="contain" />\n</AspectBox>' },
|
|
98
|
+
{
|
|
99
|
+
label: "Crop focus per image",
|
|
100
|
+
code: '<AspectBox ratio={1} className="rounded-md">\n <MediaImage src={project.thumbnail} seed={project.title} position={project.focus ?? "center"} />\n</AspectBox>'
|
|
101
|
+
}
|
|
88
102
|
]
|
|
89
103
|
};
|
|
90
104
|
export {
|
|
@@ -34,6 +34,7 @@ function MediaTile({
|
|
|
34
34
|
sizes,
|
|
35
35
|
alt,
|
|
36
36
|
seed,
|
|
37
|
+
position,
|
|
37
38
|
aspect = "square",
|
|
38
39
|
size = "md",
|
|
39
40
|
href,
|
|
@@ -107,7 +108,18 @@ function MediaTile({
|
|
|
107
108
|
"data-slot": "media-tile-cover",
|
|
108
109
|
className: "order-first rounded-(--media-tile-radius)",
|
|
109
110
|
children: [
|
|
110
|
-
/* @__PURE__ */ jsx(
|
|
111
|
+
/* @__PURE__ */ jsx(
|
|
112
|
+
MediaImage,
|
|
113
|
+
{
|
|
114
|
+
src,
|
|
115
|
+
srcSet,
|
|
116
|
+
sizes,
|
|
117
|
+
alt,
|
|
118
|
+
seed: seed ?? titleText,
|
|
119
|
+
position,
|
|
120
|
+
loading
|
|
121
|
+
}
|
|
122
|
+
),
|
|
111
123
|
linked || action != null ? (
|
|
112
124
|
// The tile answers the pointer and the keyboard alike: a light scrim
|
|
113
125
|
// deepens the cover so the action reads over any artwork. Hover only
|
|
@@ -70,6 +70,12 @@ var mediaTileMeta = {
|
|
|
70
70
|
{ name: "sizes", type: "string", description: 'The cover\'s layout width for `srcSet`, e.g. `"(min-width: 64rem) 25vw, 50vw"`.' },
|
|
71
71
|
{ name: "alt", type: "string", default: '""', description: "Cover alt text. Empty by default: the title below already names the item." },
|
|
72
72
|
{ name: "seed", type: "string | number", description: "Seeds the fallback art. Defaults to `title` when it is a string." },
|
|
73
|
+
{
|
|
74
|
+
name: "position",
|
|
75
|
+
type: 'CSSProperties["objectPosition"]',
|
|
76
|
+
default: '"center"',
|
|
77
|
+
description: 'Which part of the cover the crop keeps \u2014 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.'
|
|
78
|
+
},
|
|
73
79
|
{
|
|
74
80
|
name: "aspect",
|
|
75
81
|
type: '"square" | "video" | "poster" | number',
|
|
@@ -130,6 +136,10 @@ var mediaTileMeta = {
|
|
|
130
136
|
label: "Gallery, no links",
|
|
131
137
|
code: '<Grid minItemWidth="8rem" gap={2}>\n {photos.map((photo) => (\n <MediaTile key={photo.id} size="sm" title={photo.caption} src={photo.url} alt={photo.description} />\n ))}\n</Grid>'
|
|
132
138
|
},
|
|
139
|
+
{
|
|
140
|
+
label: "Portfolio grid with crop focus",
|
|
141
|
+
code: '<Grid minItemWidth="16rem" gap={4}>\n {projects.map((project) => (\n <MediaTile\n key={project.slug}\n aspect="video"\n title={project.title}\n subtitle={project.client}\n src={project.thumbnail}\n position={project.focus}\n href={`/work/${project.slug}`}\n />\n ))}\n</Grid>'
|
|
142
|
+
},
|
|
133
143
|
{
|
|
134
144
|
label: "Poster shelf",
|
|
135
145
|
code: '<MediaTile aspect="poster" size="lg" titleAs="h3" title="The Long Dark" subtitle="2026 \xB7 Drama" href="/films/the-long-dark" />'
|
|
@@ -2,30 +2,35 @@
|
|
|
2
2
|
import { cn } from "../../lib/cn.js";
|
|
3
3
|
import { HeadingRow } from "../../lib/heading-row.js";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
var SIZE = {
|
|
6
|
+
md: { root: "gap-2 pb-4", title: "text-heading-2", description: "text-body-sm" },
|
|
7
|
+
lg: { root: "gap-3 pb-6", title: "text-display-lg", description: "max-w-(--measure-prose) text-body-lg" }
|
|
8
|
+
};
|
|
5
9
|
function PageHeader({
|
|
6
10
|
title,
|
|
7
11
|
description,
|
|
8
12
|
back,
|
|
9
13
|
actions,
|
|
10
14
|
titleAs: Title = "h1",
|
|
15
|
+
size = "md",
|
|
11
16
|
className,
|
|
12
17
|
children,
|
|
13
18
|
...props
|
|
14
19
|
}) {
|
|
15
|
-
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col
|
|
20
|
+
return /* @__PURE__ */ jsxs("div", { "data-size": size, className: cn("flex flex-col", SIZE[size].root, className), ...props, children: [
|
|
16
21
|
/* @__PURE__ */ jsx(
|
|
17
22
|
HeadingRow,
|
|
18
23
|
{
|
|
19
24
|
as: Title,
|
|
20
25
|
title,
|
|
21
|
-
titleClassName:
|
|
22
|
-
band: "flex min-h-(--control-md) items-center",
|
|
26
|
+
titleClassName: SIZE[size].title,
|
|
27
|
+
band: "flex min-h-(--control-md) items-center mt-[min(0px,calc((var(--control-md)_-_1lh)/2))]",
|
|
23
28
|
titleBasis: "basis-[min(100%,20rem)]",
|
|
24
29
|
leading: back,
|
|
25
30
|
actions
|
|
26
31
|
}
|
|
27
32
|
),
|
|
28
|
-
description ? /* @__PURE__ */ jsx("p", { className: "text-
|
|
33
|
+
description ? /* @__PURE__ */ jsx("p", { className: cn(SIZE[size].description, "text-muted-foreground"), children: description }) : null,
|
|
29
34
|
children
|
|
30
35
|
] });
|
|
31
36
|
}
|
|
@@ -7,7 +7,11 @@ var pageHeaderMeta = {
|
|
|
7
7
|
"page header",
|
|
8
8
|
"title",
|
|
9
9
|
"lede",
|
|
10
|
-
"page actions"
|
|
10
|
+
"page actions",
|
|
11
|
+
"page title size",
|
|
12
|
+
"display title",
|
|
13
|
+
"editorial header",
|
|
14
|
+
"hero title"
|
|
11
15
|
],
|
|
12
16
|
usage: {
|
|
13
17
|
description: "Use PageHeader to establish a page's title, supporting description, and primary page actions.",
|
|
@@ -20,6 +24,14 @@ var pageHeaderMeta = {
|
|
|
20
24
|
guidance: "do",
|
|
21
25
|
description: "Keep the title literal and put explanation in the lede or nearby content."
|
|
22
26
|
},
|
|
27
|
+
{
|
|
28
|
+
guidance: "do",
|
|
29
|
+
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 \u2192 heading-2 \u2192 body.'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
guidance: "dont",
|
|
33
|
+
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.'
|
|
34
|
+
},
|
|
23
35
|
{
|
|
24
36
|
guidance: "dont",
|
|
25
37
|
description: "Do not use PageHeader inside repeated cards, dialogs, or compact panels."
|
|
@@ -64,8 +76,17 @@ var pageHeaderMeta = {
|
|
|
64
76
|
type: '"h1" | "h2"',
|
|
65
77
|
default: '"h1"',
|
|
66
78
|
description: "The title's heading element. Keep h1 \u2014 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."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "size",
|
|
82
|
+
type: '"md" | "lg"',
|
|
83
|
+
default: '"md"',
|
|
84
|
+
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."
|
|
67
85
|
}
|
|
68
86
|
],
|
|
87
|
+
variants: {
|
|
88
|
+
size: ["md", "lg"]
|
|
89
|
+
},
|
|
69
90
|
a11y: [
|
|
70
91
|
"Renders the title as an <h1>; use one PageHeader per page.",
|
|
71
92
|
"Its title renders the route's `h1`. Do not render another `h1` elsewhere on the page: heading navigation would report two documents, and the outline a screen-reader user builds from it stops matching the page."
|
|
@@ -76,6 +97,10 @@ var pageHeaderMeta = {
|
|
|
76
97
|
label: "With actions",
|
|
77
98
|
code: '<PageHeader title="Storage credits" description="Fund permanent storage." actions={<Button>Top up</Button>} />'
|
|
78
99
|
},
|
|
100
|
+
{
|
|
101
|
+
label: "Editorial page title (size lg)",
|
|
102
|
+
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>} />'
|
|
103
|
+
},
|
|
79
104
|
{
|
|
80
105
|
label: "One per page, inside the region",
|
|
81
106
|
code: `// PageHeader introduces the current route: title, optional description, and the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/components/pill/pill.meta.ts
|
|
2
2
|
var pillMeta = {
|
|
3
3
|
name: "Pill",
|
|
4
|
-
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.',
|
|
4
|
+
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.',
|
|
5
5
|
status: "stable",
|
|
6
6
|
keywords: [
|
|
7
7
|
"pill",
|
|
@@ -22,6 +22,10 @@ var pillMeta = {
|
|
|
22
22
|
guidance: "do",
|
|
23
23
|
description: "Reflect the current choice with selected, so the active filter reads at a glance."
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
guidance: "do",
|
|
27
|
+
description: 'In a toolbar where filter chips share a row with a search field ("All 8 \xB7 Products 5 \xB7 Open source 2 \u2026 [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.'
|
|
28
|
+
},
|
|
25
29
|
{
|
|
26
30
|
guidance: "dont",
|
|
27
31
|
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 \u2014 that is a Badge.",
|
|
@@ -33,14 +37,15 @@ var pillMeta = {
|
|
|
33
37
|
},
|
|
34
38
|
{
|
|
35
39
|
guidance: "dont",
|
|
36
|
-
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.'
|
|
40
|
+
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.'
|
|
37
41
|
}
|
|
38
42
|
]
|
|
39
43
|
},
|
|
40
44
|
related: {
|
|
41
45
|
components: [
|
|
42
46
|
"Badge",
|
|
43
|
-
"ToggleGroup"
|
|
47
|
+
"ToggleGroup",
|
|
48
|
+
"SearchInput"
|
|
44
49
|
]
|
|
45
50
|
},
|
|
46
51
|
props: [
|
|
@@ -58,9 +63,9 @@ var pillMeta = {
|
|
|
58
63
|
},
|
|
59
64
|
{
|
|
60
65
|
name: "size",
|
|
61
|
-
type: '"md" | "sm"',
|
|
66
|
+
type: '"lg" | "md" | "sm"',
|
|
62
67
|
default: '"md"',
|
|
63
|
-
description: "
|
|
68
|
+
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."
|
|
64
69
|
},
|
|
65
70
|
{
|
|
66
71
|
name: "tone",
|
|
@@ -76,7 +81,7 @@ var pillMeta = {
|
|
|
76
81
|
],
|
|
77
82
|
variants: {
|
|
78
83
|
appearance: ["solid", "outline"],
|
|
79
|
-
size: ["md", "sm"],
|
|
84
|
+
size: ["lg", "md", "sm"],
|
|
80
85
|
selected: ["true", "false"],
|
|
81
86
|
tone: ["neutral", "red", "orange", "yellow", "green", "teal", "blue", "purple", "pink"]
|
|
82
87
|
},
|
|
@@ -143,6 +148,10 @@ var pillMeta = {
|
|
|
143
148
|
label: "Filter row",
|
|
144
149
|
code: "<Row gap={1}><Pill selected onClick={() => {}}>All</Pill><Pill onClick={() => {}}>Projects</Pill><Pill onClick={() => {}}>Reports</Pill></Row>"
|
|
145
150
|
},
|
|
151
|
+
{
|
|
152
|
+
label: "Filters with search",
|
|
153
|
+
code: 'const [filter, setFilter] = useState("all")\nconst [q, setQ] = useState("")\n\n<Row gap={2} align="center" justify="between" wrap>\n <Row gap={1}>\n <Pill size="lg" selected={filter === "all"} onClick={() => setFilter("all")}>All 8</Pill>\n <Pill size="lg" selected={filter === "products"} onClick={() => setFilter("products")}>Products 5</Pill>\n <Pill size="lg" selected={filter === "oss"} onClick={() => setFilter("oss")}>Open source 2</Pill>\n </Row>\n <SearchInput size="sm" value={q} onValueChange={setQ} placeholder="Find a project\u2026" aria-label="Find a project" className="max-w-64" />\n</Row>'
|
|
154
|
+
},
|
|
146
155
|
{
|
|
147
156
|
label: "Outline, small (reactions)",
|
|
148
157
|
code: '<Row gap={1}><Pill appearance="outline" size="sm" selected onClick={() => {}}>\u{1F389} 3</Pill><Pill appearance="outline" size="sm" onClick={() => {}}>\u{1F440}</Pill></Row>'
|