@misoto22/design 0.3.1 → 0.5.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.
Files changed (59) hide show
  1. package/dist/article.css +393 -0
  2. package/dist/components/AppShell/AppShell.d.ts +15 -1
  3. package/dist/components/AppShell/AppShell.js +8 -4
  4. package/dist/components/AppShell/AppShell.js.map +1 -1
  5. package/dist/components/Article/Article.d.ts +51 -0
  6. package/dist/components/Article/Article.js +21 -0
  7. package/dist/components/Article/Article.js.map +1 -0
  8. package/dist/components/Button/Button.js +2 -2
  9. package/dist/components/Button/Button.js.map +1 -1
  10. package/dist/components/Calendar/Calendar.d.ts +1 -1
  11. package/dist/components/Calendar/Calendar.js +259 -24
  12. package/dist/components/Calendar/Calendar.js.map +1 -1
  13. package/dist/components/Checkbox/Checkbox.js +1 -1
  14. package/dist/components/Checkbox/Checkbox.js.map +1 -1
  15. package/dist/components/Collapsible/Collapsible.d.ts +15 -1
  16. package/dist/components/Collapsible/Collapsible.js +18 -0
  17. package/dist/components/Collapsible/Collapsible.js.map +1 -1
  18. package/dist/components/Combobox/Combobox.js +1 -1
  19. package/dist/components/Combobox/Combobox.js.map +1 -1
  20. package/dist/components/Command/Command.js +7 -7
  21. package/dist/components/Command/Command.js.map +1 -1
  22. package/dist/components/ContextMenu/ContextMenu.js +2 -2
  23. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  24. package/dist/components/Diagram/Diagram.d.ts +81 -0
  25. package/dist/components/Diagram/Diagram.js +139 -0
  26. package/dist/components/Diagram/Diagram.js.map +1 -0
  27. package/dist/components/Dialog/Dialog.js +1 -1
  28. package/dist/components/Dialog/Dialog.js.map +1 -1
  29. package/dist/components/DropdownMenu/DropdownMenu.js +2 -2
  30. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  31. package/dist/components/FigureBand/FigureBand.js +41 -30
  32. package/dist/components/FigureBand/FigureBand.js.map +1 -1
  33. package/dist/components/Popover/Popover.js +1 -1
  34. package/dist/components/Popover/Popover.js.map +1 -1
  35. package/dist/components/Select/Select.js +2 -2
  36. package/dist/components/Select/Select.js.map +1 -1
  37. package/dist/components/Sheet/Sheet.js +1 -1
  38. package/dist/components/Sheet/Sheet.js.map +1 -1
  39. package/dist/components/Slider/Slider.js +1 -1
  40. package/dist/components/Slider/Slider.js.map +1 -1
  41. package/dist/components/Steps/Steps.d.ts +65 -0
  42. package/dist/components/Steps/Steps.js +55 -0
  43. package/dist/components/Steps/Steps.js.map +1 -0
  44. package/dist/components/Switch/Switch.js +1 -1
  45. package/dist/components/Switch/Switch.js.map +1 -1
  46. package/dist/components/Tooltip/Tooltip.js +1 -1
  47. package/dist/components/Tooltip/Tooltip.js.map +1 -1
  48. package/dist/index.d.ts +4 -1
  49. package/dist/index.js +3 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/lib/useSelectionIndicator.js +14 -10
  52. package/dist/lib/useSelectionIndicator.js.map +1 -1
  53. package/dist/styles.css +1 -1
  54. package/dist/themes.css +53 -7
  55. package/dist/tokens.css +75 -10
  56. package/dist/tokens.d.ts +8 -0
  57. package/dist/tokens.js +53 -8
  58. package/dist/tokens.json +53 -8
  59. package/package.json +22 -19
@@ -0,0 +1,393 @@
1
+ /* ==========================================================================
2
+ ARTICLE — the long-form reading surface
3
+ --------------------------------------------------------------------------
4
+ The one place in this package that styles TAGS rather than components, and
5
+ the reason is that its input is not JSX: it is whatever a Markdown or MDX
6
+ pipeline produced. There is no component to hang a class on, so the contract
7
+ has to be the element names themselves.
8
+
9
+ Scoped to `[data-m22-article]`, which the `Article` component writes. Nothing
10
+ here leaks: a page that does not opt in is untouched, and a component
11
+ rendered INSIDE an article keeps its own classes, because every rule below
12
+ is a bare element selector at specificity 0,1,1 — a utility on a component
13
+ wins.
14
+
15
+ It reads only tokens. Re-point the palette or move the radius factor and the
16
+ article moves with everything else, which is the whole reason this is here
17
+ rather than in each consuming site: three sites hand-rolling a prose
18
+ stylesheet is three prose stylesheets that drift.
19
+ ========================================================================== */
20
+
21
+ [data-m22-article] {
22
+ /* A measure, not a width. A narrower column still wins, and a figure or a
23
+ table opts out of it explicitly below. */
24
+ max-width: var(--w-reading);
25
+ color: var(--ink-2);
26
+ font-family: var(--sans);
27
+ font-size: 1rem;
28
+ font-weight: 300;
29
+ line-height: 1.75;
30
+ }
31
+
32
+ /* ─── Headings ─────────────────────────────────────────────────────────────
33
+ The type ladder, unchanged. An article does not get its own scale — it is
34
+ the same five steps every other surface climbs, which is what makes a post
35
+ and a component page read as one publication. */
36
+
37
+ [data-m22-article] :is(h1, h2, h3, h4, h5, h6) {
38
+ margin: 0;
39
+ color: var(--ink);
40
+ font-family: var(--font-heading, var(--serif));
41
+ font-weight: 400;
42
+ line-height: 1.25;
43
+ /* An anchored heading has to clear the masthead it scrolls under. */
44
+ scroll-margin-top: var(--scroll-offset);
45
+ }
46
+ [data-m22-article] h1 {
47
+ font-size: var(--fs-title);
48
+ letter-spacing: -0.02em;
49
+ }
50
+ [data-m22-article] h2 {
51
+ font-size: var(--fs-heading);
52
+ }
53
+ [data-m22-article] h3 {
54
+ font-size: var(--fs-sub);
55
+ }
56
+ [data-m22-article] h4 {
57
+ font-size: var(--fs-item);
58
+ }
59
+ [data-m22-article] :is(h5, h6) {
60
+ font-family: var(--mono);
61
+ font-size: 11px;
62
+ font-weight: 400;
63
+ letter-spacing: 0.2em;
64
+ text-transform: uppercase;
65
+ color: var(--ink-3-aa);
66
+ }
67
+
68
+ /* ─── Rhythm ───────────────────────────────────────────────────────────────
69
+ Every block gets air ABOVE it and none below, the first one gets none at
70
+ all, and a heading gets more air above than below so it belongs to what
71
+ follows rather than to what it interrupts.
72
+
73
+ The reset uses `:where()`, and that is the load-bearing part. A plain
74
+ `[data-m22-article] p { margin: 0 }` is specificity 0,1,1 and outranks
75
+ `[data-m22-article] > * + *` at 0,1,0 — so the reset silently won and every
76
+ paragraph in the article lost its spacing, which is exactly the defect this
77
+ replaces. `:where()` contributes nothing, so the reset and the rhythm sit at
78
+ the same weight and source order decides, which is what a reset is for. */
79
+
80
+ [data-m22-article] :where(*) {
81
+ margin: 0;
82
+ }
83
+ [data-m22-article] > * + * {
84
+ margin-block-start: 1.25em;
85
+ }
86
+ /* `:is()` takes the specificity of its heaviest argument, so these two land at
87
+ 0,1,1 and win over the general rule above whatever their order. */
88
+ [data-m22-article] > :is(h1, h2, h3, h4, h5, h6) {
89
+ margin-block-start: 2.25em;
90
+ }
91
+ [data-m22-article] > :is(h1, h2, h3, h4, h5, h6) + * {
92
+ margin-block-start: 0.75em;
93
+ }
94
+ [data-m22-article] > :first-child {
95
+ margin-block-start: 0;
96
+ }
97
+
98
+ /* The opening paragraph carries the piece. Marked, not guessed: an author
99
+ writes `<p class="lead">` (or a pipeline emits it), rather than the
100
+ stylesheet deciding that whatever came first is a standfirst. */
101
+ [data-m22-article] p.lead {
102
+ font-size: var(--fs-item);
103
+ line-height: 1.6;
104
+ color: var(--ink);
105
+ }
106
+
107
+ /* ─── Inline ─────────────────────────────────────────────────────────────── */
108
+
109
+ [data-m22-article] a {
110
+ color: var(--ink);
111
+ text-decoration: underline;
112
+ text-decoration-color: var(--rule-2);
113
+ text-underline-offset: 4px;
114
+ transition: text-decoration-color var(--fast) var(--ease);
115
+ }
116
+ [data-m22-article] a:hover {
117
+ text-decoration-color: var(--ink);
118
+ }
119
+ [data-m22-article] strong {
120
+ color: var(--ink);
121
+ font-weight: 500;
122
+ }
123
+ [data-m22-article] em {
124
+ font-style: italic;
125
+ }
126
+ [data-m22-article] mark {
127
+ background: var(--accent-muted);
128
+ color: var(--ink);
129
+ padding-inline: 0.2em;
130
+ }
131
+ [data-m22-article] abbr[title] {
132
+ text-decoration: underline dotted var(--rule-2);
133
+ text-underline-offset: 3px;
134
+ }
135
+ [data-m22-article] :is(sub, sup) {
136
+ font-size: 0.75em;
137
+ line-height: 0;
138
+ }
139
+
140
+ /* ─── Lists ────────────────────────────────────────────────────────────────
141
+ The marker is a token, not a glyph the browser picks: a default disc is a
142
+ filled circle at whatever size the font decides, and this system's only
143
+ filled circle is a status dot. */
144
+
145
+ [data-m22-article] :is(ul, ol) {
146
+ padding-inline-start: 1.4em;
147
+ }
148
+ [data-m22-article] :is(ul, ol) :is(ul, ol) {
149
+ margin-block-start: 0.4em;
150
+ }
151
+ [data-m22-article] li + li {
152
+ margin-block-start: 0.4em;
153
+ }
154
+ /* A paragraph or a fence nested inside a list item is still a block. */
155
+ [data-m22-article] li > * + * {
156
+ margin-block-start: 0.6em;
157
+ }
158
+ [data-m22-article] li::marker {
159
+ color: var(--ink-3-aa);
160
+ font-size: 0.9em;
161
+ }
162
+ [data-m22-article] ul {
163
+ list-style: none;
164
+ padding-inline-start: 1.15em;
165
+ }
166
+ [data-m22-article] ul > li {
167
+ position: relative;
168
+ }
169
+ /* A hairline dash, which is the marker the rest of the system uses. */
170
+ [data-m22-article] ul > li::before {
171
+ content: '';
172
+ position: absolute;
173
+ inset-inline-start: -1.15em;
174
+ inset-block-start: 0.85em;
175
+ width: 0.55em;
176
+ height: 1px;
177
+ background: var(--rule-2);
178
+ }
179
+ /* A task list already has its own marker — the checkbox. Both spellings,
180
+ because a pipeline may wrap the input in a label and `:has()` is what makes
181
+ the rule work either way. */
182
+ [data-m22-article] ul > li:has(input[type='checkbox'])::before {
183
+ content: none;
184
+ }
185
+ [data-m22-article] li:has(input[type='checkbox']) {
186
+ padding-inline-start: 0;
187
+ }
188
+ [data-m22-article] input[type='checkbox'] {
189
+ accent-color: var(--accent);
190
+ margin-inline-end: 0.5em;
191
+ /* A published checklist REPORTS state; it does not collect it. Disabled at
192
+ full opacity, because the dimming a browser applies to a disabled control
193
+ is a hint that it is unavailable, not that it is unticked. */
194
+ opacity: 1;
195
+ }
196
+ [data-m22-article] li:has(input[type='checkbox']) label {
197
+ display: inline;
198
+ cursor: default;
199
+ }
200
+
201
+ [data-m22-article] dt {
202
+ color: var(--ink);
203
+ font-weight: 500;
204
+ }
205
+ [data-m22-article] dd {
206
+ margin-block-end: 0.75em;
207
+ padding-inline-start: 1em;
208
+ border-inline-start: 1px solid var(--rule);
209
+ }
210
+
211
+ /* ─── Quotation ────────────────────────────────────────────────────────────
212
+ A rule and a change of face, not italics and not a giant quote mark. */
213
+
214
+ [data-m22-article] blockquote {
215
+ padding-inline-start: 1.25em;
216
+ border-inline-start: 2px solid var(--rule-hard);
217
+ color: var(--ink);
218
+ font-family: var(--serif);
219
+ font-size: var(--fs-item);
220
+ line-height: 1.55;
221
+ }
222
+ [data-m22-article] blockquote p + p {
223
+ margin-block-start: 0.6em;
224
+ }
225
+ [data-m22-article] blockquote cite {
226
+ display: block;
227
+ margin-block-start: 0.75em;
228
+ color: var(--ink-3-aa);
229
+ font-family: var(--mono);
230
+ font-size: 11px;
231
+ font-style: normal;
232
+ letter-spacing: 0.08em;
233
+ }
234
+
235
+ /* ─── Code ─────────────────────────────────────────────────────────────────
236
+ An inline span is a chip; a block is a plate. Neither is a second surface
237
+ colour — both sit on the one elevated step the system has. */
238
+
239
+ [data-m22-article] code {
240
+ background: var(--stone);
241
+ border-radius: var(--radius-sm);
242
+ padding: 0.15em 0.4em;
243
+ color: var(--ink);
244
+ font-family: var(--mono);
245
+ font-size: 0.85em;
246
+ }
247
+ [data-m22-article] pre {
248
+ padding: 1rem 1.125rem;
249
+ overflow-x: auto;
250
+ background: var(--paper-2);
251
+ border: 1px solid var(--rule);
252
+ border-radius: var(--radius-lg);
253
+ font-size: 13px;
254
+ line-height: 1.7;
255
+ scrollbar-width: thin;
256
+ scrollbar-color: var(--rule-2) transparent;
257
+ }
258
+ [data-m22-article] pre code {
259
+ background: none;
260
+ border-radius: 0;
261
+ padding: 0;
262
+ font-size: inherit;
263
+ }
264
+
265
+ /* ─── Figures, media and tables ────────────────────────────────────────────
266
+ These are the three things allowed out of the measure: a table with six
267
+ columns and an image with a subject in it are both unreadable at 46rem. */
268
+
269
+ [data-m22-article] :is(figure, table, .m22-wide) {
270
+ width: 100%;
271
+ max-width: none;
272
+ }
273
+ [data-m22-article] > figure {
274
+ margin-block-start: 2em;
275
+ }
276
+ [data-m22-article] :is(img, video) {
277
+ display: block;
278
+ max-width: 100%;
279
+ height: auto;
280
+ border-radius: var(--radius);
281
+ }
282
+ [data-m22-article] figcaption {
283
+ margin-block-start: 0.75em;
284
+ color: var(--ink-3-aa);
285
+ font-family: var(--mono);
286
+ font-size: 11px;
287
+ letter-spacing: 0.08em;
288
+ line-height: 1.6;
289
+ }
290
+
291
+ [data-m22-article] .m22-table-scroll {
292
+ overflow-x: auto;
293
+ scrollbar-width: thin;
294
+ scrollbar-color: var(--rule-2) transparent;
295
+ }
296
+ [data-m22-article] table {
297
+ border-collapse: collapse;
298
+ font-size: 0.9375rem;
299
+ text-align: start;
300
+ }
301
+ [data-m22-article] th {
302
+ padding: 0.65em 1.25em 0.65em 0;
303
+ border-block-end: 1px solid var(--rule-hard);
304
+ color: var(--ink-3-aa);
305
+ font-family: var(--mono);
306
+ font-size: 11px;
307
+ font-weight: 400;
308
+ letter-spacing: 0.2em;
309
+ text-transform: uppercase;
310
+ text-align: start;
311
+ white-space: nowrap;
312
+ }
313
+ [data-m22-article] td {
314
+ padding: 0.65em 1.25em 0.65em 0;
315
+ border-block-end: 1px solid var(--rule);
316
+ color: var(--ink-2);
317
+ vertical-align: baseline;
318
+ }
319
+ [data-m22-article] :is(th, td):last-child {
320
+ padding-inline-end: 0;
321
+ }
322
+
323
+ [data-m22-article] > hr {
324
+ margin-block-start: 2.5em;
325
+ }
326
+ [data-m22-article] hr {
327
+ border: 0;
328
+ border-block-start: 1px solid var(--rule);
329
+ }
330
+
331
+ [data-m22-article] kbd {
332
+ border: 1px solid var(--rule-2);
333
+ border-radius: var(--radius-sm);
334
+ padding: 0.1em 0.4em;
335
+ color: var(--ink-2);
336
+ font-family: var(--mono);
337
+ font-size: 0.8em;
338
+ }
339
+
340
+ /* ─── Mathematics ──────────────────────────────────────────────────────────
341
+ MathML, which is why there is no font to load and no second stylesheet: the
342
+ browser sets it in the surrounding face. A display formula is centred and
343
+ allowed to scroll, because a long equation is the one block that genuinely
344
+ cannot wrap. */
345
+
346
+ [data-m22-article] math {
347
+ color: var(--ink);
348
+ font-size: 1.05em;
349
+ }
350
+ [data-m22-article] .m22-math {
351
+ /* `overflow-x: auto` alone turns the OTHER axis into `auto` too, which drew
352
+ a vertical scrollbar down the side of every formula. */
353
+ overflow-x: auto;
354
+ overflow-y: hidden;
355
+ padding-block: 0.35em;
356
+ scrollbar-width: thin;
357
+ scrollbar-color: var(--rule-2) transparent;
358
+ }
359
+ /* A block-level `math` is not affected by the container's `text-align`, so it
360
+ is centred the way any block is: by its own margins. */
361
+ [data-m22-article] math[display='block'] {
362
+ display: block;
363
+ width: fit-content;
364
+ margin-inline: auto;
365
+ }
366
+
367
+ /* ─── Footnotes ────────────────────────────────────────────────────────────
368
+ Ruled off and set small: they are apparatus, not the piece. */
369
+
370
+ [data-m22-article] > .footnotes {
371
+ margin-block-start: 3em;
372
+ }
373
+ [data-m22-article] .footnotes {
374
+ padding-block-start: 1.5em;
375
+ border-block-start: 1px solid var(--rule);
376
+ font-size: 0.875rem;
377
+ }
378
+ [data-m22-article] .footnotes h2 {
379
+ font-family: var(--mono);
380
+ font-size: 11px;
381
+ letter-spacing: 0.2em;
382
+ text-transform: uppercase;
383
+ color: var(--ink-3-aa);
384
+ }
385
+ /* The reference and its back-arrow keep the underline every other link in the
386
+ article has. Stripping it was a WCAG 1.4.1 failure and axe says so: a link
387
+ inside a block of text has to be distinguishable from the text without
388
+ relying on colour, and ink on --ink-2 is 2.14:1 — well under the 3:1 that
389
+ would let colour carry it alone. */
390
+ [data-m22-article] .footnote-backref {
391
+ /* The glyph is the affordance; a name is not. */
392
+ font-size: 0.9em;
393
+ }
@@ -18,6 +18,20 @@ interface AppShellProps extends HTMLAttributes<HTMLDivElement> {
18
18
  * longer answer "where is the content".
19
19
  */
20
20
  contentAs?: 'main' | 'div';
21
+ /**
22
+ * Names the sidebar landmark.
23
+ *
24
+ * Two `complementary` landmarks with the same name cannot be told apart, and
25
+ * a shell rendered inside another page — a preview, a screenshot harness —
26
+ * makes exactly that pair. It is also the only way a non-English app gets a
27
+ * landmark name its readers can read.
28
+ */
29
+ sidebarLabel?: string;
30
+ /** Names the navigation landmark inside the sidebar. */
31
+ navLabel?: string;
32
+ /** The drawer toggle, closed and open. */
33
+ openLabel?: string;
34
+ closeLabel?: string;
21
35
  children: ReactNode;
22
36
  }
23
37
  /**
@@ -33,6 +47,6 @@ interface AppShellProps extends HTMLAttributes<HTMLDivElement> {
33
47
  * <Card>…</Card>
34
48
  * </AppShell>
35
49
  */
36
- declare function AppShell({ sidebar, topbar, brand, contentAs: Content, children, className, ...rest }: AppShellProps): react.JSX.Element;
50
+ declare function AppShell({ sidebar, topbar, brand, contentAs: Content, sidebarLabel, navLabel, openLabel, closeLabel, children, className, ...rest }: AppShellProps): react.JSX.Element;
37
51
 
38
52
  export { AppShell, type AppShellProps, AppShell as default };
@@ -8,6 +8,10 @@ function AppShell({
8
8
  topbar,
9
9
  brand,
10
10
  contentAs: Content = "main",
11
+ sidebarLabel = "Sidebar",
12
+ navLabel = "Primary",
13
+ openLabel = "Open navigation",
14
+ closeLabel = "Close navigation",
11
15
  children,
12
16
  className,
13
17
  ...rest
@@ -32,7 +36,7 @@ function AppShell({
32
36
  "button",
33
37
  {
34
38
  type: "button",
35
- "aria-label": "Close navigation",
39
+ "aria-label": closeLabel,
36
40
  onClick: () => setOpen(false),
37
41
  className: "fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden"
38
42
  }
@@ -41,7 +45,7 @@ function AppShell({
41
45
  "aside",
42
46
  {
43
47
  id: sidebarId,
44
- "aria-label": "Sidebar",
48
+ "aria-label": sidebarLabel,
45
49
  className: cn(
46
50
  "fixed inset-y-0 start-0 z-(--z-modal) flex w-60 flex-col border-e border-(--rule) bg-(--paper) transition-transform duration-(--duration-slow) ease-(--ease-out-expo)",
47
51
  // The drawer slides in from the edge reading STARTS at, so in an
@@ -54,7 +58,7 @@ function AppShell({
54
58
  /* @__PURE__ */ jsx(
55
59
  "nav",
56
60
  {
57
- "aria-label": "Primary",
61
+ "aria-label": navLabel,
58
62
  className: "flex flex-1 flex-col gap-1 overflow-y-auto p-3 scroll-slim",
59
63
  children: sidebar
60
64
  }
@@ -68,7 +72,7 @@ function AppShell({
68
72
  "button",
69
73
  {
70
74
  type: "button",
71
- "aria-label": open ? "Close navigation" : "Open navigation",
75
+ "aria-label": open ? closeLabel : openLabel,
72
76
  "aria-expanded": open,
73
77
  "aria-controls": sidebarId,
74
78
  onClick: () => setOpen((previous) => !previous),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["'use client'\n\nimport { Menu, X } from 'lucide-react'\nimport { useEffect, useId, useState } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Navigation content for the sidebar (e.g. a stack of `NavItem`s). */\n sidebar: ReactNode\n /** Optional content for the sticky topbar, laid out after the mobile toggle. */\n topbar?: ReactNode\n /** Optional brand lockup pinned to the top of the sidebar. */\n brand?: ReactNode\n /**\n * Which element the content well renders as.\n *\n * `main` is right for the shell of an application, and is the default. A\n * document may contain exactly one `main` landmark, so an AppShell rendered\n * INSIDE another page — a preview on a documentation site, a screenshot\n * harness — must pass `div`, or the page has two and assistive tech can no\n * longer answer \"where is the content\".\n */\n contentAs?: 'main' | 'div'\n children: ReactNode\n}\n\n/**\n * Two columns on a desktop, one column and a drawer on a phone.\n *\n * The drawer closes on Escape as well as on the scrim, because a drawer that\n * only closes by tapping outside it strands a keyboard user inside a menu they\n * cannot leave. The scrim is a `<button>` for the same reason — a `<div>` with\n * an `onClick` is not reachable by keyboard and not announced as anything.\n *\n * @example\n * <AppShell brand={<Wordmark />} sidebar={<NavItem href=\"/\">Home</NavItem>}>\n * <Card>…</Card>\n * </AppShell>\n */\nexport function AppShell({\n sidebar,\n topbar,\n brand,\n contentAs: Content = 'main',\n children,\n className,\n ...rest\n}: AppShellProps) {\n const [open, setOpen] = useState(false)\n const sidebarId = useId()\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false)\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [open])\n\n return (\n <div\n className={cn('min-h-svh bg-(--paper) md:grid md:grid-cols-[15rem_1fr]', className)}\n {...rest}\n >\n {open && (\n <button\n type=\"button\"\n aria-label=\"Close navigation\"\n onClick={() => setOpen(false)}\n className=\"fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden\"\n />\n )}\n\n <aside\n id={sidebarId}\n aria-label=\"Sidebar\"\n className={cn(\n 'fixed inset-y-0 start-0 z-(--z-modal) flex w-60 flex-col border-e border-(--rule) bg-(--paper) transition-transform duration-(--duration-slow) ease-(--ease-out-expo)',\n // The drawer slides in from the edge reading STARTS at, so in an\n // right-to-left document it comes from the right.\n open ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full',\n 'md:static md:z-auto md:translate-x-0',\n )}\n >\n {brand && (\n <div className=\"flex h-14 items-center border-b border-(--rule) px-5\">{brand}</div>\n )}\n <nav\n aria-label=\"Primary\"\n className=\"flex flex-1 flex-col gap-1 overflow-y-auto p-3 scroll-slim\"\n >\n {sidebar}\n </nav>\n </aside>\n\n <div className=\"flex min-w-0 flex-col\">\n <header className=\"sticky top-0 z-(--z-sticky) flex h-14 items-center gap-3 border-b border-(--rule) bg-(--paper)/85 px-(--page-pad) backdrop-blur\">\n <button\n type=\"button\"\n aria-label={open ? 'Close navigation' : 'Open navigation'}\n aria-expanded={open}\n aria-controls={sidebarId}\n onClick={() => setOpen((previous) => !previous)}\n className=\"-ms-2.5 grid size-11 place-items-center rounded-(--radius) text-(--ink-2) transition-colors duration-(--duration-fast) hover:text-(--ink) md:hidden\"\n >\n {open ? <X size={20} strokeWidth={1.5} aria-hidden /> : <Menu size={20} strokeWidth={1.5} aria-hidden />}\n </button>\n {topbar}\n </header>\n\n <Content className=\"mx-auto w-full max-w-(--w-page) px-(--page-pad) py-8\">{children}</Content>\n </div>\n </div>\n )\n}\n\nexport default AppShell\n"],"mappings":";AAmEQ,cAQF,YARE;AAjER,SAAS,MAAM,SAAS;AACxB,SAAS,WAAW,OAAO,gBAAgB;AAE3C,SAAS,UAAU;AAmCZ,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,UAAU;AAAA,EACrB;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,YAAY,MAAM;AAExB,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,CAAC,UAAyB;AACtC,UAAI,MAAM,QAAQ,SAAU,SAAQ,KAAK;AAAA,IAC3C;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAC,IAAI,CAAC;AAET,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,2DAA2D,SAAS;AAAA,MACjF,GAAG;AAAA,MAEH;AAAA,gBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAW;AAAA,YACX,SAAS,MAAM,QAAQ,KAAK;AAAA,YAC5B,WAAU;AAAA;AAAA,QACZ;AAAA,QAGF;AAAA,UAAC;AAAA;AAAA,YACC,IAAI;AAAA,YACJ,cAAW;AAAA,YACX,WAAW;AAAA,cACT;AAAA;AAAA;AAAA,cAGA,OAAO,kBAAkB;AAAA,cACzB;AAAA,YACF;AAAA,YAEC;AAAA,uBACC,oBAAC,SAAI,WAAU,wDAAwD,iBAAM;AAAA,cAE/E;AAAA,gBAAC;AAAA;AAAA,kBACC,cAAW;AAAA,kBACX,WAAU;AAAA,kBAET;AAAA;AAAA,cACH;AAAA;AAAA;AAAA,QACF;AAAA,QAEA,qBAAC,SAAI,WAAU,yBACb;AAAA,+BAAC,YAAO,WAAU,mIAChB;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO,qBAAqB;AAAA,gBACxC,iBAAe;AAAA,gBACf,iBAAe;AAAA,gBACf,SAAS,MAAM,QAAQ,CAAC,aAAa,CAAC,QAAQ;AAAA,gBAC9C,WAAU;AAAA,gBAET,iBAAO,oBAAC,KAAE,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC,IAAK,oBAAC,QAAK,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC;AAAA;AAAA,YACxG;AAAA,YACC;AAAA,aACH;AAAA,UAEA,oBAAC,WAAQ,WAAU,wDAAwD,UAAS;AAAA,WACtF;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,mBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["'use client'\n\nimport { Menu, X } from 'lucide-react'\nimport { useEffect, useId, useState } from 'react'\nimport type { HTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface AppShellProps extends HTMLAttributes<HTMLDivElement> {\n /** Navigation content for the sidebar (e.g. a stack of `NavItem`s). */\n sidebar: ReactNode\n /** Optional content for the sticky topbar, laid out after the mobile toggle. */\n topbar?: ReactNode\n /** Optional brand lockup pinned to the top of the sidebar. */\n brand?: ReactNode\n /**\n * Which element the content well renders as.\n *\n * `main` is right for the shell of an application, and is the default. A\n * document may contain exactly one `main` landmark, so an AppShell rendered\n * INSIDE another page — a preview on a documentation site, a screenshot\n * harness — must pass `div`, or the page has two and assistive tech can no\n * longer answer \"where is the content\".\n */\n contentAs?: 'main' | 'div'\n /**\n * Names the sidebar landmark.\n *\n * Two `complementary` landmarks with the same name cannot be told apart, and\n * a shell rendered inside another page — a preview, a screenshot harness —\n * makes exactly that pair. It is also the only way a non-English app gets a\n * landmark name its readers can read.\n */\n sidebarLabel?: string\n /** Names the navigation landmark inside the sidebar. */\n navLabel?: string\n /** The drawer toggle, closed and open. */\n openLabel?: string\n closeLabel?: string\n children: ReactNode\n}\n\n/**\n * Two columns on a desktop, one column and a drawer on a phone.\n *\n * The drawer closes on Escape as well as on the scrim, because a drawer that\n * only closes by tapping outside it strands a keyboard user inside a menu they\n * cannot leave. The scrim is a `<button>` for the same reason — a `<div>` with\n * an `onClick` is not reachable by keyboard and not announced as anything.\n *\n * @example\n * <AppShell brand={<Wordmark />} sidebar={<NavItem href=\"/\">Home</NavItem>}>\n * <Card>…</Card>\n * </AppShell>\n */\nexport function AppShell({\n sidebar,\n topbar,\n brand,\n contentAs: Content = 'main',\n sidebarLabel = 'Sidebar',\n navLabel = 'Primary',\n openLabel = 'Open navigation',\n closeLabel = 'Close navigation',\n children,\n className,\n ...rest\n}: AppShellProps) {\n const [open, setOpen] = useState(false)\n const sidebarId = useId()\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false)\n }\n document.addEventListener('keydown', onKey)\n return () => document.removeEventListener('keydown', onKey)\n }, [open])\n\n return (\n <div\n className={cn('min-h-svh bg-(--paper) md:grid md:grid-cols-[15rem_1fr]', className)}\n {...rest}\n >\n {open && (\n <button\n type=\"button\"\n aria-label={closeLabel}\n onClick={() => setOpen(false)}\n className=\"fixed inset-0 z-(--z-scrim) bg-(--scrim) md:hidden\"\n />\n )}\n\n <aside\n id={sidebarId}\n aria-label={sidebarLabel}\n className={cn(\n 'fixed inset-y-0 start-0 z-(--z-modal) flex w-60 flex-col border-e border-(--rule) bg-(--paper) transition-transform duration-(--duration-slow) ease-(--ease-out-expo)',\n // The drawer slides in from the edge reading STARTS at, so in an\n // right-to-left document it comes from the right.\n open ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full',\n 'md:static md:z-auto md:translate-x-0',\n )}\n >\n {brand && (\n <div className=\"flex h-14 items-center border-b border-(--rule) px-5\">{brand}</div>\n )}\n <nav\n aria-label={navLabel}\n className=\"flex flex-1 flex-col gap-1 overflow-y-auto p-3 scroll-slim\"\n >\n {sidebar}\n </nav>\n </aside>\n\n <div className=\"flex min-w-0 flex-col\">\n <header className=\"sticky top-0 z-(--z-sticky) flex h-14 items-center gap-3 border-b border-(--rule) bg-(--paper)/85 px-(--page-pad) backdrop-blur\">\n <button\n type=\"button\"\n aria-label={open ? closeLabel : openLabel}\n aria-expanded={open}\n aria-controls={sidebarId}\n onClick={() => setOpen((previous) => !previous)}\n className=\"-ms-2.5 grid size-11 place-items-center rounded-(--radius) text-(--ink-2) transition-colors duration-(--duration-fast) hover:text-(--ink) md:hidden\"\n >\n {open ? <X size={20} strokeWidth={1.5} aria-hidden /> : <Menu size={20} strokeWidth={1.5} aria-hidden />}\n </button>\n {topbar}\n </header>\n\n <Content className=\"mx-auto w-full max-w-(--w-page) px-(--page-pad) py-8\">{children}</Content>\n </div>\n </div>\n )\n}\n\nexport default AppShell\n"],"mappings":";AAqFQ,cAQF,YARE;AAnFR,SAAS,MAAM,SAAS;AACxB,SAAS,WAAW,OAAO,gBAAgB;AAE3C,SAAS,UAAU;AAiDZ,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,UAAU;AAAA,EACrB,eAAe;AAAA,EACf,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,YAAY,MAAM;AAExB,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,CAAC,UAAyB;AACtC,UAAI,MAAM,QAAQ,SAAU,SAAQ,KAAK;AAAA,IAC3C;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAC,IAAI,CAAC;AAET,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,2DAA2D,SAAS;AAAA,MACjF,GAAG;AAAA,MAEH;AAAA,gBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY;AAAA,YACZ,SAAS,MAAM,QAAQ,KAAK;AAAA,YAC5B,WAAU;AAAA;AAAA,QACZ;AAAA,QAGF;AAAA,UAAC;AAAA;AAAA,YACC,IAAI;AAAA,YACJ,cAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA;AAAA;AAAA,cAGA,OAAO,kBAAkB;AAAA,cACzB;AAAA,YACF;AAAA,YAEC;AAAA,uBACC,oBAAC,SAAI,WAAU,wDAAwD,iBAAM;AAAA,cAE/E;AAAA,gBAAC;AAAA;AAAA,kBACC,cAAY;AAAA,kBACZ,WAAU;AAAA,kBAET;AAAA;AAAA,cACH;AAAA;AAAA;AAAA,QACF;AAAA,QAEA,qBAAC,SAAI,WAAU,yBACb;AAAA,+BAAC,YAAO,WAAU,mIAChB;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO,aAAa;AAAA,gBAChC,iBAAe;AAAA,gBACf,iBAAe;AAAA,gBACf,SAAS,MAAM,QAAQ,CAAC,aAAa,CAAC,QAAQ;AAAA,gBAC9C,WAAU;AAAA,gBAET,iBAAO,oBAAC,KAAE,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC,IAAK,oBAAC,QAAK,MAAM,IAAI,aAAa,KAAK,eAAW,MAAC;AAAA;AAAA,YACxG;AAAA,YACC;AAAA,aACH;AAAA,UAEA,oBAAC,WAAQ,WAAU,wDAAwD,UAAS;AAAA,WACtF;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,mBAAQ;","names":[]}
@@ -0,0 +1,51 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes } from 'react';
3
+
4
+ interface ArticleProps extends HTMLAttributes<HTMLElement> {
5
+ /**
6
+ * Rendered HTML from a Markdown or MDX pipeline.
7
+ *
8
+ * Trusted markup only. This sets `dangerouslySetInnerHTML`, so it must come
9
+ * from content you control — a repository's own posts, a CMS you author —
10
+ * and never from a reader. Untrusted Markdown has to be sanitised before it
11
+ * reaches here; that is a pipeline decision, not a component one, and a
12
+ * sanitiser bolted on inside a styling primitive would be the wrong place to
13
+ * make it and the easiest place to get it wrong.
14
+ */
15
+ html?: string;
16
+ /** The element to render. `article` by default; pass `div` for a fragment. */
17
+ as?: 'article' | 'div' | 'section';
18
+ }
19
+ /**
20
+ * The long-form reading surface.
21
+ *
22
+ * Everything a Markdown pipeline can emit — headings, prose, lists, tables,
23
+ * quotations, code, figures, footnotes, MathML — set in this system's type,
24
+ * colour and rules. The styles live in `article.css` rather than in a class
25
+ * string, because the input is not JSX: there is no component to hang a class
26
+ * on when the markup arrived as a string, so the contract has to be the element
27
+ * names themselves.
28
+ *
29
+ * That file is published on its own as `@misoto22/design/article.css`, so a
30
+ * site with its own Markdown pipeline can take the reading surface without
31
+ * taking the components — which is the case this exists for. Three sites
32
+ * hand-rolling a prose stylesheet is three prose stylesheets that drift.
33
+ *
34
+ * Composable either way: pass `html` for a rendered string, or children for
35
+ * real elements. A post that mixes the two — prose with a `Diagram` dropped
36
+ * into the middle — renders its blocks in order and gives each one an
37
+ * `Article`; a component inside keeps its own classes, because every rule in
38
+ * the layer is a bare element selector that a utility outranks.
39
+ *
40
+ * @example
41
+ * <Article html={renderedMarkdown} />
42
+ * @example
43
+ * <Article>
44
+ * <h1>The White Reset</h1>
45
+ * <p className="lead">A monochrome system for software and writing.</p>
46
+ * <Diagram spec={spec} />
47
+ * </Article>
48
+ */
49
+ declare function Article({ html, as: Comp, className, children, ...rest }: ArticleProps): react.JSX.Element;
50
+
51
+ export { Article, type ArticleProps, Article as default };
@@ -0,0 +1,21 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ function Article({ html, as: Comp = "article", className, children, ...rest }) {
3
+ if (html !== void 0) {
4
+ return /* @__PURE__ */ jsx(
5
+ Comp,
6
+ {
7
+ "data-m22-article": true,
8
+ className,
9
+ dangerouslySetInnerHTML: { __html: html },
10
+ ...rest
11
+ }
12
+ );
13
+ }
14
+ return /* @__PURE__ */ jsx(Comp, { "data-m22-article": true, className, ...rest, children });
15
+ }
16
+ var Article_default = Article;
17
+ export {
18
+ Article,
19
+ Article_default as default
20
+ };
21
+ //# sourceMappingURL=Article.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/Article/Article.tsx"],"sourcesContent":["import type { HTMLAttributes } from 'react'\n\nexport interface ArticleProps extends HTMLAttributes<HTMLElement> {\n /**\n * Rendered HTML from a Markdown or MDX pipeline.\n *\n * Trusted markup only. This sets `dangerouslySetInnerHTML`, so it must come\n * from content you control — a repository's own posts, a CMS you author —\n * and never from a reader. Untrusted Markdown has to be sanitised before it\n * reaches here; that is a pipeline decision, not a component one, and a\n * sanitiser bolted on inside a styling primitive would be the wrong place to\n * make it and the easiest place to get it wrong.\n */\n html?: string\n /** The element to render. `article` by default; pass `div` for a fragment. */\n as?: 'article' | 'div' | 'section'\n}\n\n/**\n * The long-form reading surface.\n *\n * Everything a Markdown pipeline can emit — headings, prose, lists, tables,\n * quotations, code, figures, footnotes, MathML — set in this system's type,\n * colour and rules. The styles live in `article.css` rather than in a class\n * string, because the input is not JSX: there is no component to hang a class\n * on when the markup arrived as a string, so the contract has to be the element\n * names themselves.\n *\n * That file is published on its own as `@misoto22/design/article.css`, so a\n * site with its own Markdown pipeline can take the reading surface without\n * taking the components — which is the case this exists for. Three sites\n * hand-rolling a prose stylesheet is three prose stylesheets that drift.\n *\n * Composable either way: pass `html` for a rendered string, or children for\n * real elements. A post that mixes the two — prose with a `Diagram` dropped\n * into the middle — renders its blocks in order and gives each one an\n * `Article`; a component inside keeps its own classes, because every rule in\n * the layer is a bare element selector that a utility outranks.\n *\n * @example\n * <Article html={renderedMarkdown} />\n * @example\n * <Article>\n * <h1>The White Reset</h1>\n * <p className=\"lead\">A monochrome system for software and writing.</p>\n * <Diagram spec={spec} />\n * </Article>\n */\nexport function Article({ html, as: Comp = 'article', className, children, ...rest }: ArticleProps) {\n // One or the other, never both. The rhythm rules are child combinators, so a\n // wrapper around the rendered string — even a `display: contents` one, which\n // removes the box but not the DOM node — would take every heading and\n // paragraph out of the article's direct children and cost them their\n // spacing. A page with both is a page with two Articles, and it should say\n // so.\n if (html !== undefined) {\n return (\n <Comp\n data-m22-article\n className={className}\n dangerouslySetInnerHTML={{ __html: html }}\n {...rest}\n />\n )\n }\n\n return (\n <Comp data-m22-article className={className} {...rest}>\n {children}\n </Comp>\n )\n}\n\nexport default Article\n"],"mappings":"AAyDM;AATC,SAAS,QAAQ,EAAE,MAAM,IAAI,OAAO,WAAW,WAAW,UAAU,GAAG,KAAK,GAAiB;AAOlG,MAAI,SAAS,QAAW;AACtB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,oBAAgB;AAAA,QAChB;AAAA,QACA,yBAAyB,EAAE,QAAQ,KAAK;AAAA,QACvC,GAAG;AAAA;AAAA,IACN;AAAA,EAEJ;AAEA,SACE,oBAAC,QAAK,oBAAgB,MAAC,WAAuB,GAAG,MAC9C,UACH;AAEJ;AAEA,IAAO,kBAAQ;","names":[]}
@@ -2,7 +2,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Slot } from "@radix-ui/react-slot";
3
3
  import { cn } from "../../lib/cn.js";
4
4
  import { Spinner } from "../Spinner/Spinner.js";
5
- const BASE = "inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius-pill) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none";
5
+ const BASE = "inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none";
6
6
  const VARIANT = {
7
7
  primary: "bg-(--accent) text-(--accent-foreground) border border-(--accent) hover:opacity-85",
8
8
  secondary: "border border-(--rule-2) text-(--ink) bg-transparent hover:border-(--ink)",
@@ -23,7 +23,7 @@ const ICON_SIZE = {
23
23
  };
24
24
  const SPINNER_SIZE = { sm: "sm", md: "sm", lg: "md" };
25
25
  function Keycap({ glyph }) {
26
- return /* @__PURE__ */ jsx("span", { className: "rounded-(--radius-pill) border border-current px-2 py-0.5 font-mono text-[11px] opacity-55", children: glyph });
26
+ return /* @__PURE__ */ jsx("span", { className: "rounded-(--radius-sm) border border-current px-2 py-0.5 font-mono text-[11px] opacity-55", children: glyph });
27
27
  }
28
28
  function Button(props) {
29
29
  const {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from '@radix-ui/react-slot'\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { Spinner } from '../Spinner/Spinner'\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'\nexport type ButtonSize = 'sm' | 'md' | 'lg'\n\n/**\n * The base every variant shares.\n *\n * A pill, and it does not move on hover. An earlier base lifted a pixel and\n * asked for a shadow the White Reset does not have — `--shadow` resolves to\n * `none`, so that hover state was invisible on every button in the system while\n * still costing a transition. Opacity carries the filled hover; the border\n * carries the outlined one.\n *\n * The transition names its properties rather than using `all`, so a layout\n * change (a width that grows when the label becomes \"Saving…\") is instant\n * instead of sliding.\n */\nconst BASE =\n 'inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius-pill) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none'\n\nconst VARIANT: Record<ButtonVariant, string> = {\n primary: 'bg-(--accent) text-(--accent-foreground) border border-(--accent) hover:opacity-85',\n secondary: 'border border-(--rule-2) text-(--ink) bg-transparent hover:border-(--ink)',\n ghost: 'border border-transparent text-(--ink-2) hover:text-(--ink) hover:bg-(--stone)',\n /* The one place chroma is allowed on a control, and only because the action\n is destructive — that is a state, not a brand colour. */\n danger: 'bg-(--danger) text-(--paper) border border-(--danger) hover:opacity-85',\n}\n\n/**\n * `min-h` rather than `h`, so a button that wraps to two lines grows instead of\n * clipping.\n *\n * The heights come from the density tokens rather than from a literal, so a\n * subtree marked `data-density=\"compact\"` shrinks its controls without a single\n * call site being told. At the default density `md` is 44px — the pointer\n * target WCAG 2.5.5 asks for.\n */\nconst SIZE: Record<ButtonSize, string> = {\n sm: 'min-h-(--control-h-sm) px-(--control-px-sm) py-(--control-py-sm) text-[13px]',\n md: 'min-h-(--control-h-md) px-(--control-px-md) py-(--control-py-md) text-sm',\n lg: 'min-h-(--control-h-lg) px-(--control-px-lg) py-(--control-py-lg) text-[15px]',\n}\n\n/** Square, because an icon has no label to give the box its width. */\nconst ICON_SIZE: Record<ButtonSize, string> = {\n sm: 'size-(--control-h-sm) p-0',\n md: 'size-(--control-h-md) p-0',\n lg: 'size-(--control-h-lg) p-0',\n}\n\nconst SPINNER_SIZE: Record<ButtonSize, 'sm' | 'md'> = { sm: 'sm', md: 'sm', lg: 'md' }\n\ninterface CommonProps {\n children?: ReactNode\n /**\n * Which action this is. `primary` is the one thing the screen wants you to\n * do, so there is at most one per view; `danger` is reserved for destructive\n * actions and is the only place chroma is allowed on a control.\n */\n variant?: ButtonVariant\n /** 36 / 44 / 48px tall. `md` meets the pointer-target floor on its own. */\n size?: ButtonSize\n /** Optional mono keycap glyph rendered after the label (e.g. \"P\"). */\n keycap?: string\n /**\n * Swaps the leading content for a spinner and blocks interaction. The label\n * stays put — a button that empties out while it works loses its width, and\n * the page reflows under the pointer that just clicked it.\n */\n loading?: boolean\n /**\n * Square control with no label. REQUIRES `aria-label`: an icon-only button\n * with no accessible name is invisible to a screen reader, and this is the\n * single most common way a design system ships an unusable control.\n */\n iconOnly?: boolean\n /**\n * Render the child element instead of a `<button>`, keeping these styles.\n * Use it to hand the styling to a router's `Link` — `asChild` is what keeps\n * this package free of any one router.\n *\n * The decoration slots (`keycap`, `loading`) are not injected into a slotted\n * child; compose them inside it yourself.\n *\n * @example\n * <Button asChild><Link href=\"/work\">Work</Link></Button>\n */\n asChild?: boolean\n className?: string\n}\n\ntype ButtonAsLink = CommonProps &\n Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className' | 'children'> & { href: string }\n\ntype ButtonAsButton = CommonProps &\n Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> & { href?: undefined }\n\nexport type ButtonProps = ButtonAsLink | ButtonAsButton\n\nfunction Keycap({ glyph }: { glyph: string }) {\n return (\n <span className=\"rounded-(--radius-pill) border border-current px-2 py-0.5 font-mono text-[11px] opacity-55\">\n {glyph}\n </span>\n )\n}\n\n/**\n * The system's action.\n *\n * Renders a `<button>`, or an `<a>` when given `href`, or whatever you hand it\n * via `asChild`. No router is imported, so the package stays framework-agnostic\n * and a Next or React Router app wires its own `Link` at the call site.\n *\n * Server-component friendly: there is no client boundary here, so it renders in\n * a static page as well as an interactive one.\n *\n * @example\n * <Button variant=\"secondary\" keycap=\"P\">View projects</Button>\n * @example\n * <Button loading disabled>Saving…</Button>\n * @example\n * <Button iconOnly aria-label=\"Close\"><X size={16} /></Button>\n */\nexport function Button(props: ButtonProps) {\n const {\n children,\n variant = 'primary',\n size = 'md',\n keycap,\n loading = false,\n iconOnly = false,\n asChild = false,\n className,\n } = props\n\n const cls = cn(BASE, VARIANT[variant], iconOnly ? ICON_SIZE[size] : SIZE[size], className)\n\n // A filled ground needs a spinner in the inherited colour; an outlined one\n // reads better against the page's own rule scale.\n const spinnerTone = variant === 'primary' || variant === 'danger' ? 'current' : 'default'\n const decoration = (\n <>\n {loading && <Spinner size={SPINNER_SIZE[size]} tone={spinnerTone} label={null} />}\n {children}\n {keycap && <Keycap glyph={keycap} />}\n </>\n )\n\n if (asChild) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <Slot className={cls} {...rest}>\n {children}\n </Slot>\n )\n }\n\n if (props.href !== undefined) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, href, ...rest } = props\n return (\n <a\n href={href}\n className={cls}\n // A link cannot be `disabled`; `aria-disabled` plus the pointer-events\n // rule in BASE is the accessible equivalent.\n aria-disabled={loading || undefined}\n {...rest}\n >\n {decoration}\n </a>\n )\n }\n\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <button\n type={rest.type ?? 'button'}\n aria-busy={loading || undefined}\n disabled={rest.disabled || loading}\n className={cls}\n {...rest}\n >\n {decoration}\n </button>\n )\n}\n\nexport default Button\n"],"mappings":"AA0GI,SAyCA,UAzCA,KAyCA,YAzCA;AA1GJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,eAAe;AAkBxB,MAAM,OACJ;AAEF,MAAM,UAAyC;AAAA,EAC7C,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA;AAAA;AAAA,EAGP,QAAQ;AACV;AAWA,MAAM,OAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGA,MAAM,YAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAgD,EAAE,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK;AAiDrF,SAAS,OAAO,EAAE,MAAM,GAAsB;AAC5C,SACE,oBAAC,UAAK,WAAU,8FACb,iBACH;AAEJ;AAmBO,SAAS,OAAO,OAAoB;AACzC,QAAM;AAAA,IACJ;AAAA,IACA,UAAU;AAAA,IACV,OAAO;AAAA,IACP;AAAA,IACA,UAAU;AAAA,IACV,WAAW;AAAA,IACX,UAAU;AAAA,IACV;AAAA,EACF,IAAI;AAEJ,QAAM,MAAM,GAAG,MAAM,QAAQ,OAAO,GAAG,WAAW,UAAU,IAAI,IAAI,KAAK,IAAI,GAAG,SAAS;AAIzF,QAAM,cAAc,YAAY,aAAa,YAAY,WAAW,YAAY;AAChF,QAAM,aACJ,iCACG;AAAA,eAAW,oBAAC,WAAQ,MAAM,aAAa,IAAI,GAAG,MAAM,aAAa,OAAO,MAAM;AAAA,IAC9E;AAAA,IACA,UAAU,oBAAC,UAAO,OAAO,QAAQ;AAAA,KACpC;AAGF,MAAI,SAAS;AACX,UAAM,EAAE,UAAUA,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,GAAGC,MAAK,IAAI;AAC7H,WACE,oBAAC,QAAK,WAAW,KAAM,GAAGA,OACvB,UACH;AAAA,EAEJ;AAEA,MAAI,MAAM,SAAS,QAAW;AAC5B,UAAM,EAAE,UAAUR,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,MAAM,GAAGC,MAAK,IAAI;AACnI,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,QAGX,iBAAe,WAAW;AAAA,QACzB,GAAGA;AAAA,QAEH;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,EAAE,UAAU,IAAI,SAAS,IAAI,MAAM,IAAI,QAAQ,IAAI,SAAS,IAAI,UAAU,IAAI,SAAS,IAAI,WAAW,KAAK,GAAG,KAAK,IAAI;AAC7H,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,KAAK,QAAQ;AAAA,MACnB,aAAW,WAAW;AAAA,MACtB,UAAU,KAAK,YAAY;AAAA,MAC3B,WAAW;AAAA,MACV,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,iBAAQ;","names":["_c","_v","_s","_k","_l","_i","_a","_cn","rest"]}
1
+ {"version":3,"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["import { Slot } from '@radix-ui/react-slot'\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\nimport { Spinner } from '../Spinner/Spinner'\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'\nexport type ButtonSize = 'sm' | 'md' | 'lg'\n\n/**\n * The base every variant shares.\n *\n * It reads `--radius`, the CONTROL step, which is the same corner an `Input`,\n * a `Select` trigger and a `Textarea` draw. It used to be a pill, and a pill\n * beside an 8px field is two different ideas of what a control is — the one\n * inconsistency in the system that every reader noticed and none of the\n * component pages could explain. Everything on the control line now agrees,\n * and the capsule is kept for the things that genuinely are one: a badge, a\n * status pill, a segmented strip, a progress track.\n *\n * It does not move on hover. An earlier base lifted a pixel and asked for a\n * shadow the White Reset does not have — `--shadow` resolves to `none`, so that\n * hover state was invisible on every button in the system while still costing a\n * transition. Opacity carries the filled hover; the border carries the outlined\n * one.\n *\n * The transition names its properties rather than using `all`, so a layout\n * change (a width that grows when the label becomes \"Saving…\") is instant\n * instead of sliding.\n */\nconst BASE =\n 'inline-flex items-center justify-center gap-(--control-gap) rounded-(--radius) font-sans transition-[opacity,color,border-color,background-color] duration-(--duration-fast) ease-(--ease-out-expo) disabled:opacity-(--disabled-opacity) disabled:pointer-events-none aria-disabled:opacity-(--disabled-opacity) aria-disabled:pointer-events-none'\n\nconst VARIANT: Record<ButtonVariant, string> = {\n primary: 'bg-(--accent) text-(--accent-foreground) border border-(--accent) hover:opacity-85',\n secondary: 'border border-(--rule-2) text-(--ink) bg-transparent hover:border-(--ink)',\n ghost: 'border border-transparent text-(--ink-2) hover:text-(--ink) hover:bg-(--stone)',\n /* The one place chroma is allowed on a control, and only because the action\n is destructive — that is a state, not a brand colour. */\n danger: 'bg-(--danger) text-(--paper) border border-(--danger) hover:opacity-85',\n}\n\n/**\n * `min-h` rather than `h`, so a button that wraps to two lines grows instead of\n * clipping.\n *\n * The heights come from the density tokens rather than from a literal, so a\n * subtree marked `data-density=\"compact\"` shrinks its controls without a single\n * call site being told. At the default density `md` is 44px — the pointer\n * target WCAG 2.5.5 asks for.\n */\nconst SIZE: Record<ButtonSize, string> = {\n sm: 'min-h-(--control-h-sm) px-(--control-px-sm) py-(--control-py-sm) text-[13px]',\n md: 'min-h-(--control-h-md) px-(--control-px-md) py-(--control-py-md) text-sm',\n lg: 'min-h-(--control-h-lg) px-(--control-px-lg) py-(--control-py-lg) text-[15px]',\n}\n\n/** Square, because an icon has no label to give the box its width. */\nconst ICON_SIZE: Record<ButtonSize, string> = {\n sm: 'size-(--control-h-sm) p-0',\n md: 'size-(--control-h-md) p-0',\n lg: 'size-(--control-h-lg) p-0',\n}\n\nconst SPINNER_SIZE: Record<ButtonSize, 'sm' | 'md'> = { sm: 'sm', md: 'sm', lg: 'md' }\n\ninterface CommonProps {\n children?: ReactNode\n /**\n * Which action this is. `primary` is the one thing the screen wants you to\n * do, so there is at most one per view; `danger` is reserved for destructive\n * actions and is the only place chroma is allowed on a control.\n */\n variant?: ButtonVariant\n /** 36 / 44 / 48px tall. `md` meets the pointer-target floor on its own. */\n size?: ButtonSize\n /** Optional mono keycap glyph rendered after the label (e.g. \"P\"). */\n keycap?: string\n /**\n * Swaps the leading content for a spinner and blocks interaction. The label\n * stays put — a button that empties out while it works loses its width, and\n * the page reflows under the pointer that just clicked it.\n */\n loading?: boolean\n /**\n * Square control with no label. REQUIRES `aria-label`: an icon-only button\n * with no accessible name is invisible to a screen reader, and this is the\n * single most common way a design system ships an unusable control.\n */\n iconOnly?: boolean\n /**\n * Render the child element instead of a `<button>`, keeping these styles.\n * Use it to hand the styling to a router's `Link` — `asChild` is what keeps\n * this package free of any one router.\n *\n * The decoration slots (`keycap`, `loading`) are not injected into a slotted\n * child; compose them inside it yourself.\n *\n * @example\n * <Button asChild><Link href=\"/work\">Work</Link></Button>\n */\n asChild?: boolean\n className?: string\n}\n\ntype ButtonAsLink = CommonProps &\n Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className' | 'children'> & { href: string }\n\ntype ButtonAsButton = CommonProps &\n Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> & { href?: undefined }\n\nexport type ButtonProps = ButtonAsLink | ButtonAsButton\n\nfunction Keycap({ glyph }: { glyph: string }) {\n return (\n <span className=\"rounded-(--radius-sm) border border-current px-2 py-0.5 font-mono text-[11px] opacity-55\">\n {glyph}\n </span>\n )\n}\n\n/**\n * The system's action.\n *\n * Renders a `<button>`, or an `<a>` when given `href`, or whatever you hand it\n * via `asChild`. No router is imported, so the package stays framework-agnostic\n * and a Next or React Router app wires its own `Link` at the call site.\n *\n * Server-component friendly: there is no client boundary here, so it renders in\n * a static page as well as an interactive one.\n *\n * @example\n * <Button variant=\"secondary\" keycap=\"P\">View projects</Button>\n * @example\n * <Button loading disabled>Saving…</Button>\n * @example\n * <Button iconOnly aria-label=\"Close\"><X size={16} /></Button>\n */\nexport function Button(props: ButtonProps) {\n const {\n children,\n variant = 'primary',\n size = 'md',\n keycap,\n loading = false,\n iconOnly = false,\n asChild = false,\n className,\n } = props\n\n const cls = cn(BASE, VARIANT[variant], iconOnly ? ICON_SIZE[size] : SIZE[size], className)\n\n // A filled ground needs a spinner in the inherited colour; an outlined one\n // reads better against the page's own rule scale.\n const spinnerTone = variant === 'primary' || variant === 'danger' ? 'current' : 'default'\n const decoration = (\n <>\n {loading && <Spinner size={SPINNER_SIZE[size]} tone={spinnerTone} label={null} />}\n {children}\n {keycap && <Keycap glyph={keycap} />}\n </>\n )\n\n if (asChild) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <Slot className={cls} {...rest}>\n {children}\n </Slot>\n )\n }\n\n if (props.href !== undefined) {\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, href, ...rest } = props\n return (\n <a\n href={href}\n className={cls}\n // A link cannot be `disabled`; `aria-disabled` plus the pointer-events\n // rule in BASE is the accessible equivalent.\n aria-disabled={loading || undefined}\n {...rest}\n >\n {decoration}\n </a>\n )\n }\n\n const { children: _c, variant: _v, size: _s, keycap: _k, loading: _l, iconOnly: _i, asChild: _a, className: _cn, ...rest } = props\n return (\n <button\n type={rest.type ?? 'button'}\n aria-busy={loading || undefined}\n disabled={rest.disabled || loading}\n className={cls}\n {...rest}\n >\n {decoration}\n </button>\n )\n}\n\nexport default Button\n"],"mappings":"AAkHI,SAyCA,UAzCA,KAyCA,YAzCA;AAlHJ,SAAS,YAAY;AAErB,SAAS,UAAU;AACnB,SAAS,eAAe;AA0BxB,MAAM,OACJ;AAEF,MAAM,UAAyC;AAAA,EAC7C,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA;AAAA;AAAA,EAGP,QAAQ;AACV;AAWA,MAAM,OAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGA,MAAM,YAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,eAAgD,EAAE,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK;AAiDrF,SAAS,OAAO,EAAE,MAAM,GAAsB;AAC5C,SACE,oBAAC,UAAK,WAAU,4FACb,iBACH;AAEJ;AAmBO,SAAS,OAAO,OAAoB;AACzC,QAAM;AAAA,IACJ;AAAA,IACA,UAAU;AAAA,IACV,OAAO;AAAA,IACP;AAAA,IACA,UAAU;AAAA,IACV,WAAW;AAAA,IACX,UAAU;AAAA,IACV;AAAA,EACF,IAAI;AAEJ,QAAM,MAAM,GAAG,MAAM,QAAQ,OAAO,GAAG,WAAW,UAAU,IAAI,IAAI,KAAK,IAAI,GAAG,SAAS;AAIzF,QAAM,cAAc,YAAY,aAAa,YAAY,WAAW,YAAY;AAChF,QAAM,aACJ,iCACG;AAAA,eAAW,oBAAC,WAAQ,MAAM,aAAa,IAAI,GAAG,MAAM,aAAa,OAAO,MAAM;AAAA,IAC9E;AAAA,IACA,UAAU,oBAAC,UAAO,OAAO,QAAQ;AAAA,KACpC;AAGF,MAAI,SAAS;AACX,UAAM,EAAE,UAAUA,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,GAAGC,MAAK,IAAI;AAC7H,WACE,oBAAC,QAAK,WAAW,KAAM,GAAGA,OACvB,UACH;AAAA,EAEJ;AAEA,MAAI,MAAM,SAAS,QAAW;AAC5B,UAAM,EAAE,UAAUR,KAAI,SAASC,KAAI,MAAMC,KAAI,QAAQC,KAAI,SAASC,KAAI,UAAUC,KAAI,SAASC,KAAI,WAAWC,MAAK,MAAM,GAAGC,MAAK,IAAI;AACnI,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,QAGX,iBAAe,WAAW;AAAA,QACzB,GAAGA;AAAA,QAEH;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,EAAE,UAAU,IAAI,SAAS,IAAI,MAAM,IAAI,QAAQ,IAAI,SAAS,IAAI,UAAU,IAAI,SAAS,IAAI,WAAW,KAAK,GAAG,KAAK,IAAI;AAC7H,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,KAAK,QAAQ;AAAA,MACnB,aAAW,WAAW;AAAA,MACtB,UAAU,KAAK,YAAY;AAAA,MAC3B,WAAW;AAAA,MACV,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,iBAAQ;","names":["_c","_v","_s","_k","_l","_i","_a","_cn","rest"]}
@@ -23,6 +23,6 @@ type CalendarProps = DayPickerProps;
23
23
  * // A birth date: widen the years, since the default span is deliberately short.
24
24
  * <Calendar mode="single" startMonth={new Date(1920, 0)} endMonth={new Date()} />
25
25
  */
26
- declare function Calendar({ className, classNames, captionLayout, components, ...props }: CalendarProps): react.JSX.Element;
26
+ declare function Calendar({ className, classNames, components, ...props }: CalendarProps): react.JSX.Element;
27
27
 
28
28
  export { Calendar, type CalendarProps, Calendar as default };