@siyahi/ui 0.1.9-next.35 → 0.1.9-next.37

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 (44) hide show
  1. package/DESIGN.md +80 -63
  2. package/README.md +10 -21
  3. package/THIRD_PARTY_NOTICES.md +15 -15
  4. package/agents/examples/chart-panels.tsx +0 -4
  5. package/agents/examples/metric-strip-headline.tsx +12 -0
  6. package/agents/examples/{section-cards-select.tsx → metric-strip-select.tsx} +5 -5
  7. package/agents/examples/metric-strip-trend.tsx +33 -0
  8. package/agents/examples/overview-dashboard.tsx +4 -4
  9. package/agents/examples/scatter-chart.tsx +0 -1
  10. package/agents/examples/stale-blocks.tsx +9 -9
  11. package/agents/examples/surfaces.tsx +1 -33
  12. package/agents/manifest.json +38 -53
  13. package/dist/components/registry/dashboardcn/kpi-row-card.d.ts +20 -6
  14. package/dist/components/registry/dashboardcn/kpi-row-card.js +52 -33
  15. package/dist/components/siyahi/callout.js +2 -1
  16. package/dist/components/siyahi/inline-code.d.ts +1 -1
  17. package/dist/components/siyahi/inline-code.js +1 -1
  18. package/dist/components/siyahi/page-header.js +1 -0
  19. package/dist/components/siyahi/panel-grid.d.ts +4 -6
  20. package/dist/components/siyahi/panel-grid.js +9 -12
  21. package/dist/components/siyahi/scatter-chart.d.ts +3 -3
  22. package/dist/components/siyahi/scatter-chart.js +6 -3
  23. package/dist/components/siyahi/siyahi-provider.d.ts +5 -15
  24. package/dist/components/siyahi/siyahi-provider.js +7 -17
  25. package/dist/index.d.ts +3 -4
  26. package/dist/index.js +3 -3
  27. package/dist/lib/chart-frame.d.ts +1 -1
  28. package/dist/lib/chart-frame.js +4 -2
  29. package/dist/lib/chart-series.d.ts +10 -2
  30. package/dist/lib/chart-series.js +10 -0
  31. package/dist/lib/fit-text.js +1 -1
  32. package/dist/lib/tone.d.ts +1 -1
  33. package/dist/lib/tone.js +1 -1
  34. package/package.json +2 -2
  35. package/registry.lock.json +0 -8
  36. package/styles/fallback-fonts.css +13 -13
  37. package/styles/fonts/m-plus-1-code-latin-ext-wght-normal.woff2 +0 -0
  38. package/styles/fonts/m-plus-1-code-latin-wght-normal.woff2 +0 -0
  39. package/styles/m-plus-1-code.css +21 -0
  40. package/styles/siyahi.css +166 -330
  41. package/agents/examples/section-cards-trend.tsx +0 -32
  42. package/agents/examples/section-cards.tsx +0 -48
  43. package/dist/components/registry/dashboard-01/section-cards.d.ts +0 -30
  44. package/dist/components/registry/dashboard-01/section-cards.js +0 -74
package/styles/siyahi.css CHANGED
@@ -1,9 +1,9 @@
1
1
  /*
2
2
  * Siyahi theme: the only file in the package that holds colour values.
3
- * shadcn/ui `neutral` greys for both schemes, with ink indigo (hue 275)
4
- * placed, not spread: links, the selected sidebar item's text and the first
5
- * chart series. Buttons, badges, toggles and focus stay monochrome, where
6
- * black and white read strongest. Plus three meaning
3
+ * A terminal: the mono face, shadcn/ui `neutral` greys for both schemes, and
4
+ * terminal amber placed, not spread: links, the eyebrow and panel numbers and
5
+ * the selected sidebar item's text. Buttons, badges, toggles and focus stay
6
+ * monochrome, where black and white read strongest. Plus three meaning
7
7
  * colours and a five-hue chart palette. `packages/tools/contrast.mjs` checks every value
8
8
  * here in the build; when a check fails, change the value, never the bar.
9
9
  */
@@ -11,7 +11,7 @@
11
11
  @import "tw-animate-css";
12
12
  @import "./shadcn.css";
13
13
  @import "@fontsource-variable/geist";
14
- @import "@fontsource-variable/geist-mono";
14
+ @import "./m-plus-1-code.css";
15
15
  @import "./fallback-fonts.css";
16
16
 
17
17
  /* Tailwind does not scan node_modules, so name the package's own classes. */
@@ -41,8 +41,13 @@
41
41
 
42
42
  @theme inline {
43
43
  --font-sans: "Geist Variable", "Geist Fallback", ui-sans-serif, system-ui, sans-serif;
44
- --font-mono: "Geist Mono Variable", "Geist Mono Fallback", ui-monospace, monospace;
45
- --font-heading: var(--font-sans);
44
+ /*
45
+ * M PLUS 1 Code: a mono half a font size wide (6px a character at 12px, Geist Mono's 7), so a terminal page fits
46
+ * a table's every column and a row's words on one line. 20KB for every weight.
47
+ */
48
+ --font-mono: "M PLUS 1 Code Variable", "M PLUS 1 Code Fallback", ui-monospace, monospace;
49
+ /* Titles are data labels on a terminal: the mono face, as the page itself is. */
50
+ --font-heading: var(--font-mono);
46
51
  /*
47
52
  * Tailwind's 65ch, measured in Geist, whose "0" is 0.663em wide. A ch
48
53
  * follows whichever font is drawing, so a plain 65ch is narrower while the
@@ -109,7 +114,8 @@
109
114
  --radius: 0rem;
110
115
  --background: oklch(0.985 0 0);
111
116
  --foreground: oklch(0.145 0 0);
112
- --card: oklch(1 0 0);
117
+ /* The page colour: a card is a block on the page, not a box, so what lies under its marks is the page. */
118
+ --card: oklch(0.985 0 0);
113
119
  --card-foreground: oklch(0.145 0 0);
114
120
  --popover: oklch(1 0 0);
115
121
  --popover-foreground: oklch(0.145 0 0);
@@ -130,12 +136,13 @@
130
136
  --input: oklch(0.64 0 0);
131
137
  /* Focus is the text colour, as black reads strongest: a field's edge thickens in it, any other control takes a halo. */
132
138
  --ring: oklch(0.145 0 0);
133
- /* Tailwind green-700, red-600, amber-700, with their chroma lowered to the sRGB edge. */
139
+ /* Tailwind green-700 and red-600, with their chroma lowered to the sRGB edge, and a mustard yellow. */
134
140
  --positive: oklch(0.527 0.145 150.069);
135
141
  --positive-foreground: oklch(0.985 0 0);
136
142
  --negative: oklch(0.577 0.235 27.325);
137
143
  --negative-foreground: oklch(0.985 0 0);
138
- --warning: oklch(0.555 0.149 48.998);
144
+ /* Yellow, not amber: amber is the accent, so caution takes its own hue. */
145
+ --warning: oklch(0.53 0.1 95);
139
146
  --warning-foreground: oklch(0.985 0 0);
140
147
  /*
141
148
  * Heat steps: how much of --positive or --negative each of five steps mixes
@@ -161,7 +168,16 @@
161
168
  * contrast check holds each bar colour's tint against the card and each slot's against the next slot's.
162
169
  */
163
170
  --bar-tint: 50%;
164
- --link: oklch(0.51 0.2 275);
171
+ /* Terminal amber, deepened to burnt orange so it reads as text on the light page. */
172
+ --link: oklch(0.55 0.13 58);
173
+ /*
174
+ * A headline MetricStrip's blocks (variant "blocks"): the orange, then the text colour, then a grey, each with the
175
+ * ink its figures take on it.
176
+ */
177
+ --headline: oklch(0.68 0.19 40);
178
+ --headline-foreground: oklch(0.145 0 0);
179
+ --headline-muted: oklch(0.88 0 0);
180
+ --headline-muted-foreground: oklch(0.145 0 0);
165
181
  /*
166
182
  * Indigo, amber, plum, sky, teal: no green or red, so no series reads as a
167
183
  * gain or a loss. Each slot reaches 3:1 against the canvas, the card and the
@@ -175,30 +191,30 @@
175
191
  --chart-1: oklch(0.4 0.17 275);
176
192
  --chart-2: oklch(0.66 0.13 70);
177
193
  --chart-3: oklch(0.38 0.13 335);
178
- --chart-4: oklch(0.65 0.11 235);
194
+ --chart-4: oklch(0.64 0.11 235);
179
195
  --chart-5: oklch(0.36 0.06 205);
180
196
  --sidebar: oklch(0.985 0 0);
181
197
  --sidebar-foreground: oklch(0.145 0 0);
182
198
  --sidebar-primary: oklch(0.205 0 0);
183
199
  --sidebar-primary-foreground: oklch(0.985 0 0);
184
200
  --sidebar-accent: oklch(0.97 0 0);
185
- --sidebar-accent-foreground: oklch(0.45 0.2 275);
201
+ --sidebar-accent-foreground: oklch(0.55 0.13 58);
186
202
  --sidebar-border: oklch(0.145 0 0 / 18%);
187
203
  --sidebar-ring: oklch(0.145 0 0);
188
204
 
189
205
  @variant dark {
190
206
  /*
191
207
  * The canvas lifts just off black and text sits a step below white:
192
- * near-white on near-black (19:1) blurs for many readers, and with the
193
- * cards' grey gone on a ruled page the canvas is what text sits on.
208
+ * near-white on near-black (19:1) blurs for many readers, and on an open
209
+ * page, where cards have no fill, the canvas is what text sits on.
194
210
  * Claude Code (#181818, about #e8e8e8 text) and T3 Code (#0a0a0a, about
195
211
  * #bebebe) both narrow that gap; this sits between them, at 14.9:1.
196
- * Cards sit one step above the canvas, so data reads as raised, as the
197
- * white card does on the light canvas; the sidebar is a shade darker.
212
+ * A popover sits one step above the canvas, so it reads as lifted over
213
+ * the page; the sidebar is a shade darker.
198
214
  */
199
215
  --background: oklch(0.14 0 0);
200
216
  --foreground: oklch(0.922 0 0);
201
- --card: oklch(0.215 0 0);
217
+ --card: oklch(0.14 0 0);
202
218
  --card-foreground: oklch(0.922 0 0);
203
219
  --popover: oklch(0.215 0 0);
204
220
  --popover-foreground: oklch(0.922 0 0);
@@ -218,14 +234,14 @@
218
234
  --ring: oklch(0.922 0 0);
219
235
  /*
220
236
  * Green toned down from Tailwind green-400, below red-400's lightness so a
221
- * treemap's deepest green tile keeps its light label at 4.5:1; amber-400.
222
- * Red, amber and the link sit at the sRGB edge.
237
+ * treemap's deepest green tile keeps its light label at 4.5:1; a yellow
238
+ * for caution, apart from the amber link. Red sits at the sRGB edge.
223
239
  */
224
240
  --positive: oklch(0.69 0.16 151.711);
225
241
  --positive-foreground: oklch(0.145 0 0);
226
242
  --negative: oklch(0.704 0.187 22.216);
227
243
  --negative-foreground: oklch(0.145 0 0);
228
- --warning: oklch(0.828 0.169 84.429);
244
+ --warning: oklch(0.8 0.15 98);
229
245
  --warning-foreground: oklch(0.145 0 0);
230
246
  /* A 12% tint of a dark card reads as the card itself, so dark starts at 25%. */
231
247
  --heat-1: 25%;
@@ -242,7 +258,11 @@
242
258
  --wash-edge: 55%;
243
259
  --field-wash: 12%;
244
260
  --bar-tint: 50%;
245
- --link: oklch(0.75 0.128 275);
261
+ --link: oklch(0.8 0.13 65);
262
+ --headline: oklch(0.7 0.19 40);
263
+ --headline-foreground: oklch(0.145 0 0);
264
+ --headline-muted: oklch(0.27 0 0);
265
+ --headline-muted-foreground: oklch(0.922 0 0);
246
266
  --chart-1: oklch(0.58 0.2 275);
247
267
  --chart-2: oklch(0.88 0.08 75);
248
268
  --chart-3: oklch(0.58 0.16 335);
@@ -253,7 +273,7 @@
253
273
  --sidebar-primary: oklch(0.922 0 0);
254
274
  --sidebar-primary-foreground: oklch(0.205 0 0);
255
275
  --sidebar-accent: oklch(0.269 0 0);
256
- --sidebar-accent-foreground: oklch(0.8 0.1 275);
276
+ --sidebar-accent-foreground: oklch(0.8 0.13 65);
257
277
  --sidebar-border: oklch(1 0 0 / 20%);
258
278
  --sidebar-ring: oklch(0.922 0 0);
259
279
  }
@@ -267,7 +287,16 @@
267
287
  @apply bg-background text-foreground;
268
288
  font-variant-numeric: tabular-nums;
269
289
  }
290
+ /*
291
+ * A terminal: the page is set in the mono face, so figures, labels, controls and titles share one grid. Text that
292
+ * reads as sentences (a paragraph, a quote, a card's or a dialog's description) takes the sans, where
293
+ * mono tires the eye over a line. Not a list item: a sidebar row, a menu option or a tile is one, and its contents
294
+ * are labels. Zero specificity, so a component's own font class wins.
295
+ */
270
296
  html {
297
+ @apply font-mono;
298
+ }
299
+ :where(p, blockquote, dd, figcaption, [data-slot$="description"]) {
271
300
  @apply font-sans;
272
301
  }
273
302
  @media (prefers-reduced-motion: reduce) {
@@ -283,12 +312,9 @@
283
312
  }
284
313
 
285
314
  /*
286
- * Every selected toggle, a view switch or a filter chip alike, fills with the
287
- * foreground, black on white and white on black, so the reader sees at once
288
- * what is on; stock shadcn marks it with a faint grey. A default (pill) tab
289
- * selects the same way: stock fills it with the canvas colour, 1.05:1 against
290
- * its grey list, lifted on a drop shadow, which the fill makes needless. Unlayered, to outrank the stock
291
- * aria-pressed:bg-muted.
315
+ * A selected toggle, a view switch or a filter chip alike, and a selected pill tab fill with the foreground, black on
316
+ * white and white on black. Stock shadcn marks them with a faint grey, 1.05:1 against the canvas, so the reader cannot
317
+ * see which one is on. Unlayered, to outrank the stock aria-pressed:bg-muted.
292
318
  */
293
319
  :is([data-slot="toggle"], [data-slot="toggle-group-item"]):is([aria-pressed="true"], [data-pressed]),
294
320
  [data-slot="tabs-list"][data-variant="default"] [data-slot="tabs-trigger"][data-active] {
@@ -298,34 +324,9 @@
298
324
  --tw-shadow: 0 0 #0000;
299
325
  }
300
326
 
301
- /*
302
- * A toggle group's items sit flush and share their borders, one control with
303
- * its parts side by side; stock spaces them 8px apart. A group set to
304
- * spacing={0} already joins its items by dropping their inner borders. The
305
- * pressed item rises over its neighbours, so its own edge shows on both sides.
306
- */
307
- [data-slot="toggle-group"]:not([data-spacing="0"]) {
308
- gap: 0;
309
- > [data-slot="toggle-group-item"] {
310
- &[aria-pressed="true"],
311
- &[data-pressed] {
312
- z-index: 1;
313
- }
314
- &[data-variant="outline"] + [data-slot="toggle-group-item"] {
315
- margin-inline-start: -1px;
316
- }
317
- }
318
- &[data-orientation="vertical"]
319
- > [data-slot="toggle-group-item"][data-variant="outline"]
320
- + [data-slot="toggle-group-item"] {
321
- margin-inline-start: 0;
322
- margin-block-start: -1px;
323
- }
324
- }
325
-
326
327
  /*
327
328
  * A component that lifts a part of itself with a z-index is a stacking context
328
- * of its own: SectionCards' picker button, an avatar's badge, a calendar's
329
+ * of its own: MetricStrip's picker buttons, an avatar's badge, a calendar's
329
330
  * days, a focused toggle, a code block's buttons, a price chart's layers. The
330
331
  * lifted part then ranks only against the rest of its component, and can never
331
332
  * sit over, or take a tap from, a page's sticky header or section strip (also
@@ -352,43 +353,10 @@
352
353
  }
353
354
 
354
355
  /*
355
- * A copy button sits in a row above the code, never over it. Core floats a
356
- * CodeBlockActions written straight in the block over the code's top right
357
- * (z-10, shown on hover), where it covers the end of a line and painted over
358
- * the page's sticky section strip; here it takes a row of its own, as in a
359
- * CodeBlockHeader, always shown, with the header's plain button.
360
- */
361
- [data-slot="code-block"] > [data-slot="code-block-actions"] {
362
- position: static;
363
- align-self: flex-end;
364
- margin: 0.5rem 0.5rem 0;
365
- opacity: 1;
366
- }
367
- /*
368
- * Core's floating fill and faint resting ring go. Its fill outranks the ghost
369
- * button's own hover fill, so that is set again here, as a header's button
370
- * shows it; the focus ring is left alone.
371
- */
372
- [data-slot="code-block"] > [data-slot="code-block-actions"] > button {
373
- background-color: transparent;
374
- &:hover {
375
- background-color: var(--muted);
376
- @variant dark {
377
- background-color: color-mix(in oklab, var(--muted) 50%, transparent);
378
- }
379
- }
380
- &:not(:focus-visible) {
381
- --tw-ring-shadow: 0 0 #0000;
382
- }
383
- }
384
-
385
- /*
386
- * --input is the edge of a field, where the reader types or picks. A button,
387
- * a toggle and a code block are framed by their label or their content, so
388
- * their outline stays the faint divider colour; scoping the variable keeps
389
- * the fill each derives from it faint too. A button is matched by its own
390
- * group/button class: rendered as a popover or menu trigger, it takes that
391
- * trigger's data-slot instead of "button".
356
+ * --input is the edge of a field, raised to 3:1 so the reader sees where to type. Stock buttons, toggles and code
357
+ * blocks also edge and fill themselves from it (dark:bg-input/30), which the raised value would make heavy, so for
358
+ * them it stays the faint divider colour, as stock draws them. A button is matched by its own group/button class:
359
+ * rendered as a popover or menu trigger, it takes that trigger's data-slot instead of "button".
392
360
  */
393
361
  :is([class~="group/button"], [data-slot="toggle"], [data-slot="toggle-group-item"], [data-slot="code-block"]) {
394
362
  --input: var(--border);
@@ -590,85 +558,6 @@ svg :is(rect, circle, path)[tabindex]:focus-visible,
590
558
  background-color: color-mix(in oklab, var(--destructive) var(--field-wash), var(--card));
591
559
  }
592
560
 
593
- /*
594
- * A field's label sits above it in small monospaced capitals, muted, as the
595
- * labels over figures do: FieldLabel in a Field (directly or in its
596
- * FieldContent), the home a page gets it from. A label for a checkbox, a
597
- * switch or a radio reads as a sentence and keeps the sans face, whatever the
598
- * Field's orientation.
599
- */
600
- [data-slot="field"]:not(:has([role="checkbox"], [role="switch"], [role="radio"])) [data-slot="field-label"] {
601
- @apply font-mono text-2xs tracking-wider text-muted-foreground uppercase;
602
- }
603
-
604
- /*
605
- * A group's label in a menu (dropdown, context, menubar), a select's or a combobox's list or the
606
- * command palette is a label over its items, set as one.
607
- */
608
- :is(
609
- [data-slot$="menu-label"],
610
- [data-slot="menubar-label"],
611
- [data-slot="select-label"],
612
- [data-slot="combobox-label"],
613
- [data-slot="command-group"] [cmdk-group-heading]
614
- ) {
615
- @apply font-mono text-2xs tracking-wider text-muted-foreground uppercase;
616
- }
617
-
618
- /*
619
- * A floating surface reads as a sheet of the page, not a bubble over it: no shadow (the theme has none) and the
620
- * card's hairline for an edge. Stock shadcn edges a menu, a list, a popover, a dialog or a card with a fainter ring
621
- * (ring-foreground/10, also behind a variant such as data-open:); it takes the hairline wherever it appears, on
622
- * a component added later too. The
623
- * chart's tooltip, which Recharts draws, is named.
624
- */
625
- [class*="ring-foreground/10"] {
626
- --tw-ring-color: var(--border);
627
- }
628
- [data-slot="chart"] .recharts-tooltip-wrapper > div {
629
- border-color: var(--border);
630
- }
631
-
632
- /*
633
- * Square corners everywhere (DESIGN.md): every element and its ::before and ::after are square, whatever rounding
634
- * a component asks for (rounded-full, rounded-[4px], hover:rounded-full, after:rounded-full), on a component added
635
- * later too. Only what is round by nature stays round, by what it is rather than its classes: an avatar, a radio
636
- * and a radio's dot (a questionnaire's radio choice too), a status dot and a spinner. A chart draws its round marks
637
- * in SVG, which this does not touch.
638
- */
639
- :not(
640
- [data-slot^="avatar"],
641
- [data-slot="message-avatar"],
642
- [role="radio"],
643
- [role="radio"] *,
644
- [data-type="radio"] [data-slot$="indicator"],
645
- [data-type="radio"] [data-slot$="indicator-dot"],
646
- [data-slot="status-dot"],
647
- .animate-spin
648
- ),
649
- :not(
650
- [data-slot^="avatar"],
651
- [data-slot="message-avatar"],
652
- [role="radio"],
653
- [role="radio"] *,
654
- [data-type="radio"] [data-slot$="indicator"],
655
- [data-type="radio"] [data-slot$="indicator-dot"],
656
- [data-slot="status-dot"],
657
- .animate-spin
658
- )::before,
659
- :not(
660
- [data-slot^="avatar"],
661
- [data-slot="message-avatar"],
662
- [role="radio"],
663
- [role="radio"] *,
664
- [data-type="radio"] [data-slot$="indicator"],
665
- [data-type="radio"] [data-slot$="indicator-dot"],
666
- [data-slot="status-dot"],
667
- .animate-spin
668
- )::after {
669
- border-radius: 0;
670
- }
671
-
672
561
  /*
673
562
  * Muted text is already the faintest that reads at 4.5:1 (contrast.mjs). Stock shadcn fades it further on some
674
563
  * parts (text-muted-foreground/70 on a trace's description), under that bar, so a faded muted class reads as the
@@ -705,65 +594,50 @@ svg :is(rect, circle, path)[tabindex]:focus-visible,
705
594
  }
706
595
 
707
596
  /*
708
- * Column heads are labels, in small monospaced capitals; a sort header's
709
- * button and a data table's column title take the head's type rather than
710
- * their own. A total row reads as a
711
- * ledger's: one rule above it and a double rule under it in the text colour,
712
- * with no band.
597
+ * A label over data reads as a terminal's column head: small capitals, muted. A table's column heads (a sort
598
+ * header's button and a data table's column title take the head's type rather than their own), a sidebar group's
599
+ * label, and a group's label in a menu, a select's or a combobox's list or the command palette.
713
600
  */
714
- [data-slot="table-head"] {
715
- @apply font-mono text-2xs tracking-wider uppercase;
716
- :is([class~="group/button"], [data-slot="column-title"]) {
717
- font: inherit;
718
- letter-spacing: inherit;
719
- text-transform: inherit;
720
- }
601
+ :is(
602
+ [data-slot="table-head"],
603
+ [data-slot="sidebar-group-label"],
604
+ [data-slot$="menu-label"],
605
+ [data-slot="menubar-label"],
606
+ [data-slot="select-label"],
607
+ [data-slot="combobox-label"],
608
+ [data-slot="command-group"] [cmdk-group-heading]
609
+ ) {
610
+ @apply text-2xs tracking-wider text-muted-foreground uppercase;
611
+ }
612
+ [data-slot="table-head"] :is([class~="group/button"], [data-slot="column-title"]) {
613
+ font: inherit;
614
+ letter-spacing: inherit;
615
+ text-transform: inherit;
721
616
  }
617
+
722
618
  /*
723
- * Tables are dense, as a ledger is: compact rows, a rule in the text colour
724
- * under the column heads, the first column (what each row is) in medium
725
- * weight, and figures in the mono face, a size down so a column of them takes
726
- * no more room than the sans did. A figure is a cell marked data-numeric (the
727
- * data table's number columns, or a plain TableCell the page marks), which
728
- * also right-aligns it, and any Delta in a cell. Alignment alone says nothing:
729
- * a right-aligned word stays in the sans. Row hairlines stay; no stripes, no
730
- * column rules.
619
+ * Tables are dense, so a screen shows more rows and columns: 13px text, 24px column heads, cells 2px by 6px. Row
620
+ * hairlines stay; no stripes, no column rules.
731
621
  */
622
+ [data-slot="table"] {
623
+ font-size: 0.8125rem;
624
+ }
732
625
  [data-slot="table-head"] {
733
- height: 1.75rem;
626
+ height: 1.5rem;
627
+ padding-inline: 0.375rem;
734
628
  }
735
629
  [data-slot="table-cell"] {
736
- padding-block: 0.2rem;
737
- }
738
- [data-slot="table-header"] [data-slot="table-row"] {
739
- border-bottom-color: var(--foreground);
740
- }
741
- /* A data table's sticky header draws its rule as a line of its own, which stays when the rows scroll under it. */
742
- [data-slot="table-header"]::after {
743
- background-color: var(--foreground);
744
- }
745
- [data-slot="table-body"] [data-slot="table-cell"]:first-child {
746
- font-weight: 500;
747
- }
748
- :is([data-slot="table-cell"][data-numeric], [data-slot="table-cell"] [data-slot="delta"]) {
749
- @apply font-mono;
750
- font-size: 0.8125rem;
751
- }
752
- [data-slot="table-footer"] {
753
- background: none;
754
- border-top: 1px solid var(--foreground);
755
- border-bottom: 3px double var(--foreground);
756
- font-weight: 600;
630
+ padding: 0.125rem 0.375rem;
757
631
  }
758
632
 
759
633
  /*
760
- * An exception the page marks, a stat tile (SectionCard's tone), a MetricStrip
761
- * figure (its item's tone) or a table row (<TableRow data-tone="negative">, a
762
- * DataTableBody's getRowTone), takes a faint wash of its colour over the card,
763
- * and a tile or figure an outline of it, so the one figure needing attention
764
- * stands out of a run of neutral ones. Unlayered, to outrank a row's hover band.
634
+ * An exception the page marks, a MetricStrip figure (its item's tone) or a
635
+ * table row (<TableRow data-tone="negative">, a DataTableBody's getRowTone),
636
+ * takes a faint wash of its colour over the card, and a figure an outline of
637
+ * it, so the one figure needing attention stands out of a run of neutral
638
+ * ones. Unlayered, to outrank a row's hover band.
765
639
  */
766
- :is([data-slot="card"], [data-slot="metric-strip-item"], [data-slot="table-row"]) {
640
+ :is([data-slot="metric-strip-item"], [data-slot="table-row"]) {
767
641
  &[data-tone="negative"] {
768
642
  --exception: var(--negative);
769
643
  }
@@ -771,24 +645,49 @@ svg :is(rect, circle, path)[tabindex]:focus-visible,
771
645
  --exception: var(--warning);
772
646
  }
773
647
  }
774
- :is([data-slot="card"], [data-slot="metric-strip-item"]):is([data-tone="negative"], [data-tone="warning"]) {
775
- background-color: color-mix(in oklab, var(--exception) var(--wash), var(--card));
776
- }
777
- [data-slot="card"]:is([data-tone="negative"], [data-tone="warning"]) {
778
- --tw-ring-color: color-mix(in oklab, var(--exception) var(--wash-edge), transparent);
648
+ /*
649
+ * A MetricStrip figure has no padding of its own, so its text lines up with its neighbours'; its wash and outline
650
+ * reach 8px past it, into the gap, as a picked figure's outline does. --figure-wash is the wash the whole box takes.
651
+ */
652
+ [data-slot="metric-strip-item"] {
653
+ --figure-reach: calc(var(--spacing) * 2);
654
+ &:is([data-tone="negative"], [data-tone="warning"]) {
655
+ --figure-wash: color-mix(in oklab, var(--exception) var(--wash), var(--card));
656
+ background-color: var(--figure-wash);
657
+ box-shadow: 0 0 0 var(--figure-reach) var(--figure-wash);
658
+ outline: 1px solid color-mix(in oklab, var(--exception) var(--wash-edge), transparent);
659
+ outline-offset: var(--figure-reach);
660
+ }
661
+ &[data-picked] {
662
+ outline: 2px solid var(--primary);
663
+ outline-offset: var(--figure-reach);
664
+ }
779
665
  }
780
666
  /*
781
- * A figure's outline lies on the four hairlines around it, as a tile's ring does: its neighbours' rules before and
782
- * above it, and its own end and bottom rules, which sit outside the padding box the inset counts from.
667
+ * A headline MetricStrip sets each figure on a solid block, in turn the headline orange, the text colour and a grey,
668
+ * touching. Everything in a block takes the block's ink: its label, note and a change's trend icon would not read on
669
+ * the fill in their own colours, and the sign still says gain or loss. A picked block's outline sits inside it.
783
670
  */
784
- [data-slot="metric-strip-item"]:is([data-tone="negative"], [data-tone="warning"]) {
785
- position: relative;
786
- &::after {
787
- content: "";
788
- position: absolute;
789
- inset: -1px;
790
- border: 1px solid color-mix(in oklab, var(--exception) var(--wash-edge), transparent);
791
- pointer-events: none;
671
+ [data-slot="metric-strip"][data-variant="blocks"] [data-slot="metric-strip-item"] {
672
+ background-color: var(--headline);
673
+ color: var(--headline-foreground);
674
+ &:nth-child(3n + 2) {
675
+ background-color: var(--foreground);
676
+ color: var(--background);
677
+ }
678
+ &:nth-child(3n) {
679
+ background-color: var(--headline-muted);
680
+ color: var(--headline-muted-foreground);
681
+ }
682
+ & > * {
683
+ --foreground: currentColor;
684
+ --muted-foreground: currentColor;
685
+ --positive: currentColor;
686
+ --negative: currentColor;
687
+ }
688
+ &[data-picked] {
689
+ outline-offset: -4px;
690
+ outline-color: currentColor;
792
691
  }
793
692
  }
794
693
  /* A pinned cell paints the card under itself (niko-table's lib/styles), so it lays the row's wash over that. */
@@ -803,24 +702,17 @@ svg :is(rect, circle, path)[tabindex]:focus-visible,
803
702
  /*
804
703
  * Panels in a PanelGrid are numbered 01, 02 before their title, in the mono
805
704
  * face and the link colour, so the text can point to one. A panel without a
806
- * card title, such as a MetricStrip with none, takes no number; a block
807
- * wrapped in its own SiyahiProvider (which draws no box) still counts. The panel
705
+ * card title, such as a MetricStrip with none, takes no number. The panel
808
706
  * counts, not the title: a card header is a size container, whose style
809
707
  * containment would keep each title's count to itself.
810
708
  */
811
709
  [data-slot="panel-grid"] {
812
710
  counter-reset: panel;
813
711
  }
814
- [data-slot="panel"]:has(
815
- > [data-slot="card"] > [data-slot="card-header"] > [data-slot="card-title"],
816
- > [data-slot="siyahi-provider"] > [data-slot="card"] > [data-slot="card-header"] > [data-slot="card-title"]
817
- ) {
712
+ [data-slot="panel"]:has(> [data-slot="card"] > [data-slot="card-header"] > [data-slot="card-title"]) {
818
713
  counter-increment: panel;
819
714
  }
820
- :is([data-slot="panel"], [data-slot="panel"] > [data-slot="siyahi-provider"])
821
- > [data-slot="card"]
822
- > [data-slot="card-header"]
823
- > [data-slot="card-title"]::before {
715
+ [data-slot="panel"] > [data-slot="card"] > [data-slot="card-header"] > [data-slot="card-title"]::before {
824
716
  content: counter(panel, decimal-leading-zero);
825
717
  @apply font-mono text-2xs font-medium text-link;
826
718
  letter-spacing: 0.04em;
@@ -830,22 +722,11 @@ svg :is(rect, circle, path)[tabindex]:focus-visible,
830
722
  }
831
723
 
832
724
  /*
833
- * A data table inside a card drops its own border: the card's ring or the
834
- * ruled page's hairlines already frame it. On an open page, with no frame,
835
- * it keeps its border.
836
- */
837
- @scope (:root, [data-surface="ruled"], [data-surface="card"]) to ([data-surface="open"]) {
838
- [data-slot="card"] [data-slot="table-container"] {
839
- border-color: transparent;
840
- }
841
- }
842
- /*
843
- * The opaque fill a data table's sticky header and pinned cells paint, so rows
844
- * scrolling under them stay hidden: the colour under the table, the card it
845
- * sits in or, on an open surface, whose cards have no fill, the page.
725
+ * The opaque fill a data table's sticky header and pinned cells paint, so rows scrolling under them stay hidden: the
726
+ * page, which a card does not cover.
846
727
  */
847
728
  [data-slot="table-container"] {
848
- --table-fill: var(--card);
729
+ --table-fill: var(--background);
849
730
  }
850
731
 
851
732
  /*
@@ -888,45 +769,40 @@ svg :is(rect, circle, path)[tabindex]:focus-visible,
888
769
  }
889
770
 
890
771
  /*
891
- * A block the page marks stale (stale on a chart card or a SectionCards tile)
892
- * loses its colour: its chart, sparkline and badge turn grey, and a tile's
893
- * figure and badge turn muted. The chart and sparkline also draw at 65%, so a
772
+ * A block the page marks stale (stale on a chart card or a MetricStrip figure)
773
+ * loses its colour: its chart, sparkline and badge turn grey, and a figure
774
+ * turns muted. The chart and sparkline also draw at 65%, so a
894
775
  * dark slot's line does not read heavier than a fresh one: a stale block is
895
776
  * meant to be harder to read, so its marks are exempt from the 3:1 mark bar.
896
777
  * The small "Stale · 3 Nov" note under the title and the figure keep their
897
778
  * text contrast.
898
779
  */
899
- [data-slot="card"][data-stale] :is([data-slot="chart"], [data-slot="sparkline"], [data-slot="badge"]) {
780
+ :is([data-slot="card"], [data-slot="metric-strip-item"])[data-stale]
781
+ :is([data-slot="chart"], [data-slot="sparkline"], [data-slot="badge"], [data-slot="delta"]) {
900
782
  filter: grayscale(1);
901
783
  }
902
- [data-slot="card"][data-stale] :is([data-slot="chart"], [data-slot="sparkline"]) {
784
+ :is([data-slot="card"], [data-slot="metric-strip-item"])[data-stale] :is([data-slot="chart"], [data-slot="sparkline"]) {
903
785
  opacity: 0.65;
904
786
  }
905
- [data-slot="section-card"] [data-slot="card"][data-stale] :is([data-slot="card-title"], [data-slot="badge"]) {
787
+ [data-slot="metric-strip-item"][data-stale] [data-slot="metric-strip-value"] {
906
788
  color: var(--muted-foreground);
907
789
  }
908
790
 
909
791
  /*
910
- * Chart gridlines take the full border colour, solid: stock shadcn draws them
911
- * dashed at half of it, which all but vanishes on a dark card (5% white).
792
+ * Chart gridlines take the full border colour: stock shadcn draws them at half of it, which all but vanishes on the
793
+ * dark page (10% white).
912
794
  */
913
795
  [data-slot="chart"] .recharts-cartesian-grid line {
914
796
  stroke: var(--border);
915
797
  }
916
798
 
917
799
  /*
918
- * The sidebar is parted from the page by a hairline on its page-side edge,
919
- * its group labels are small monospaced capitals, as every label over content
920
- * is, and its rows are compact. Fill, icons and the current-page mark stay as
921
- * shadcn draws them.
800
+ * The sidebar's rows are compact, as a table's are. shadcn edges a collapsible sidebar on its page side already; a
801
+ * non-collapsible one, set in the page's flow, has none, so it takes the hairline here.
922
802
  */
923
- /* shadcn edges a collapsible sidebar on its page side already; a non-collapsible one, set in the page's flow, has none. */
924
803
  div[data-slot="sidebar"]:not([data-side], [data-mobile]) {
925
804
  border-inline-end: 1px solid var(--sidebar-border);
926
805
  }
927
- [data-slot="sidebar-group-label"] {
928
- @apply font-mono text-2xs tracking-wider uppercase;
929
- }
930
806
  [data-slot="sidebar-menu"] {
931
807
  gap: 0;
932
808
  }
@@ -942,19 +818,6 @@ div[data-slot="sidebar"]:not([data-side], [data-mobile]) {
942
818
  height: 1.5rem;
943
819
  }
944
820
 
945
- /*
946
- * The selected sub-page marks its stretch of the sidebar's rail in the accent,
947
- * so the current place reads at a glance in a long list, as a docs index does.
948
- */
949
- [data-slot="sidebar-menu-sub-item"]:has(> [data-active])::before {
950
- content: "";
951
- position: absolute;
952
- inset-block: 2px;
953
- left: -11px;
954
- width: 2px;
955
- background: var(--sidebar-accent-foreground);
956
- }
957
-
958
821
  /* Touch targets follow input capability, including touch laptops. */
959
822
  @media (pointer: coarse) {
960
823
  :is(
@@ -1002,51 +865,24 @@ div[data-slot="sidebar"]:not([data-side], [data-mobile]) {
1002
865
  }
1003
866
 
1004
867
  /*
1005
- * Surfaces, from SiyahiProvider's surface prop. Ruled, the default, keeps
1006
- * every card's fill and drops its ring: PanelGrid and SectionCards fill their
1007
- * grid with the card colour and draw hairlines between the blocks, with a rule
1008
- * above and below the group and none on its left and right edges, and a card
1009
- * that stands alone, such as a MetricStrip or a table's card, takes those
1010
- * rules above and below itself. Card is a filled square card with a hairline
1011
- * ring and no shadow, 8px from the next, for a page of separate blocks. Open
1012
- * takes the box off every core Card: no fill, no ring, no tinted footer band,
1013
- * and no padding, so a block's content sits level with the page's text. Each
1014
- * scope stops at a provider that says otherwise. Unlayered, like the rules
1015
- * above, to outrank the core components' utility classes; a picked
1016
- * SectionCards tile keeps its ring, which is important, and an exception
1017
- * keeps its outline, which is more specific.
868
+ * A Card is a block on the page, not a box: no fill, no ring, no tinted footer band and no padding, so its content
869
+ * sits level with the page's text, and space, not a box or a rule, parts it from the next. --card is the page colour
870
+ * (above), so what a block mixes into, a forecast's halo, a bar's tint or an exception's wash, is what lies under it.
871
+ * Unlayered, like the rules above, to outrank the core component's utility classes.
1018
872
  */
1019
- @scope (:root, [data-surface="ruled"]) to ([data-surface="card"], [data-surface="open"]) {
1020
- [data-slot="card"] {
1021
- --tw-ring-color: transparent;
1022
- }
1023
- /*
1024
- * A card inside a panel or a tile's cell, or nested in another card, is ruled by what holds it; only a holder
1025
- * inside this scope counts, so a ruled provider inside a card starts its own lone cards afresh.
1026
- */
1027
- [data-slot="card"]:not(
1028
- :scope :is([data-slot="panel"], [data-slot="section-card"], [data-slot="card"]) [data-slot="card"]
1029
- ) {
1030
- border-block: 1px solid var(--border);
1031
- }
873
+ [data-slot="card"] {
874
+ /* No box, so no edge to clip at: a figure's wash or picker reaching past its text into the gap stays whole. */
875
+ overflow: visible;
876
+ background: none;
877
+ --tw-ring-color: transparent;
878
+ --card-spacing: 0px;
879
+ gap: calc(var(--spacing) * 3);
1032
880
  }
1033
-
1034
- @scope ([data-surface="open"]) to ([data-surface="ruled"], [data-surface="card"]) {
1035
- [data-slot="table-container"] {
1036
- --table-fill: var(--background);
1037
- }
1038
- [data-slot="card"] {
1039
- background: none;
1040
- --tw-ring-color: transparent;
1041
- --card-spacing: 0px;
1042
- gap: calc(var(--spacing) * 4);
1043
- }
1044
- [data-slot="card-footer"] {
1045
- background: none;
1046
- border-top-width: 0;
1047
- /* The band's own top padding stacked on the card's gap, and with no band it read as a hole under the figure. */
1048
- padding-top: 0;
1049
- }
881
+ [data-slot="card-footer"] {
882
+ background: none;
883
+ border-top-width: 0;
884
+ /* The band's own top padding stacked on the card's gap, and with no band it read as a hole under the figure. */
885
+ padding-top: 0;
1050
886
  }
1051
887
 
1052
888
  /*