@godxjp/ui 18.12.14 → 18.12.15

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.
@@ -32,12 +32,20 @@ export type TableProps = React.HTMLAttributes<HTMLTableElement> & {
32
32
  * its rem floor (`--table-action-collection-*-width-floor`, ~5 CJK glyphs per line) and the
33
33
  * table intentionally scrolls inside this wrapper — SC 1.4.10 permits one-dimensional scrolling
34
34
  * of a data table; compressing further would shred CJK headers one character per line.
35
+ *
36
+ * `"stacked-record-collection"` (gh#293 restore — SCR-215) is for a wide, heterogeneous record
37
+ * table that CANNOT be squeezed into any fixed narrow-frame measure — an admin detail row with
38
+ * many disparate fields, not a dense queue. Below `collapseBelow` the `<thead>` hides and every
39
+ * `<tr>` becomes a bordered key-value card: each `TableCell`'s own `label` prop (its column's
40
+ * header text, repeated per cell since `<thead>` is gone) renders inline above the value. Above
41
+ * the step it renders as a byte-for-byte ordinary table — mark `label` on every `TableCell`.
35
42
  */
36
43
  preset?: TablePresetProp;
37
44
  /**
38
- * Step at which `preset="action-collection"` switches to the priority measures, measured against
39
- * the TABLE's own container (not the viewport), so a table inside a rail collapses before the
40
- * page does. Defaults to `"sm"` (40rem). Ignored while `preset` is `"default"`.
45
+ * Step at which `preset="action-collection"`/`"stacked-record-collection"` switch away from the
46
+ * ordinary table, measured against the TABLE's own container (not the viewport), so a table
47
+ * inside a rail collapses before the page does. Defaults to `"sm"` (40rem). Ignored while
48
+ * `preset` is `"default"`.
41
49
  */
42
50
  collapseBelow?: BreakpointProp;
43
51
  };
@@ -73,12 +81,20 @@ export declare const Table: React.ForwardRefExoticComponent<React.HTMLAttributes
73
81
  * its rem floor (`--table-action-collection-*-width-floor`, ~5 CJK glyphs per line) and the
74
82
  * table intentionally scrolls inside this wrapper — SC 1.4.10 permits one-dimensional scrolling
75
83
  * of a data table; compressing further would shred CJK headers one character per line.
84
+ *
85
+ * `"stacked-record-collection"` (gh#293 restore — SCR-215) is for a wide, heterogeneous record
86
+ * table that CANNOT be squeezed into any fixed narrow-frame measure — an admin detail row with
87
+ * many disparate fields, not a dense queue. Below `collapseBelow` the `<thead>` hides and every
88
+ * `<tr>` becomes a bordered key-value card: each `TableCell`'s own `label` prop (its column's
89
+ * header text, repeated per cell since `<thead>` is gone) renders inline above the value. Above
90
+ * the step it renders as a byte-for-byte ordinary table — mark `label` on every `TableCell`.
76
91
  */
77
92
  preset?: TablePresetProp;
78
93
  /**
79
- * Step at which `preset="action-collection"` switches to the priority measures, measured against
80
- * the TABLE's own container (not the viewport), so a table inside a rail collapses before the
81
- * page does. Defaults to `"sm"` (40rem). Ignored while `preset` is `"default"`.
94
+ * Step at which `preset="action-collection"`/`"stacked-record-collection"` switch away from the
95
+ * ordinary table, measured against the TABLE's own container (not the viewport), so a table
96
+ * inside a rail collapses before the page does. Defaults to `"sm"` (40rem). Ignored while
97
+ * `preset` is `"default"`.
82
98
  */
83
99
  collapseBelow?: BreakpointProp;
84
100
  } & React.RefAttributes<HTMLTableElement>>;
@@ -94,6 +110,17 @@ export declare const TableRow: React.ForwardRefExoticComponent<React.HTMLAttribu
94
110
  type TableCellPriority = {
95
111
  priority?: TableColumnPriorityProp;
96
112
  };
113
+ /**
114
+ * A `TableCell`'s own column label, read only by `Table preset="stacked-record-collection"`
115
+ * below its collapse step — where the real `<thead>` hides and this renders inline above the
116
+ * cell's value inside the stacked card, standing in for the header association that is no longer
117
+ * visually present. Rendered into the DOM unconditionally (so it exists for the preset's CSS to
118
+ * reveal) but visually hidden above the collapse step, where the real `<th>` already carries the
119
+ * label — an ordinary table with no `label` prop supplied gains no extra markup.
120
+ */
121
+ type TableCellLabel = {
122
+ label?: React.ReactNode;
123
+ };
97
124
  export declare const TableHead: React.ForwardRefExoticComponent<React.ThHTMLAttributes<HTMLTableCellElement> & TableCellPriority & React.RefAttributes<HTMLTableCellElement>>;
98
- export declare const TableCell: React.ForwardRefExoticComponent<React.TdHTMLAttributes<HTMLTableCellElement> & TableCellPriority & React.RefAttributes<HTMLTableCellElement>>;
125
+ export declare const TableCell: React.ForwardRefExoticComponent<React.TdHTMLAttributes<HTMLTableCellElement> & TableCellPriority & TableCellLabel & React.RefAttributes<HTMLTableCellElement>>;
99
126
  export {};
@@ -1,4 +1,4 @@
1
- import { jsx } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { tableHeadHeightClass } from "../../lib/control-styles.js";
4
4
  import { cn } from "../../lib/utils.js";
@@ -23,7 +23,8 @@ const Table = React.forwardRef(
23
23
  {
24
24
  className: cn(
25
25
  scrollable ? "relative w-full overflow-auto" : "relative w-full",
26
- preset !== "default" && "ui-table-collection"
26
+ preset === "action-collection" && "ui-table-collection",
27
+ preset === "stacked-record-collection" && "ui-table-stacked-collection"
27
28
  ),
28
29
  "data-preset": preset === "default" ? void 0 : preset,
29
30
  "data-collapse-below": preset === "default" ? void 0 : collapseBelow,
@@ -69,14 +70,18 @@ const TableHead = React.forwardRef(({ className, priority, ...props }, ref) => /
69
70
  }
70
71
  ));
71
72
  TableHead.displayName = "TableHead";
72
- const TableCell = React.forwardRef(({ className, priority, ...props }, ref) => /* @__PURE__ */ jsx(
73
+ const TableCell = React.forwardRef(({ className, priority, label, children, ...props }, ref) => /* @__PURE__ */ jsxs(
73
74
  "td",
74
75
  {
75
76
  ref,
76
77
  "data-slot": "table-cell",
77
78
  "data-priority": priority,
78
79
  className: cn(className),
79
- ...props
80
+ ...props,
81
+ children: [
82
+ label !== void 0 ? /* @__PURE__ */ jsx("span", { className: "ui-table-stacked-collection-label", "aria-hidden": "true", children: label }) : null,
83
+ children
84
+ ]
80
85
  }
81
86
  ));
82
87
  TableCell.displayName = "TableCell";
@@ -81,8 +81,15 @@ export type StickyProp = boolean;
81
81
  * the desktop intrinsic column widths are replaced by token-owned column-PRIORITY measures under
82
82
  * `table-layout: fixed` and cells wrap, so every column — including the row actions — stays inside
83
83
  * the initial narrow frame. The real `<table>` semantics are never rewritten.
84
+ *
85
+ * `"stacked-record-collection"` (gh#293 restore — SCR-215) is the canonical wide-record collection:
86
+ * a table whose column count/content cannot be squeezed into any fixed-measure narrow frame at all
87
+ * (an admin detail row with many heterogeneous fields). Below `collapseBelow` the header row hides
88
+ * and every `<tr>` becomes a bordered key-value CARD stack — each `TableCell`'s own `label` prop
89
+ * renders inline above its value, taking over the accessible-name role the (now hidden) `<th>`
90
+ * association would otherwise carry. Above the step it is a byte-for-byte ordinary table.
84
91
  */
85
- export type TablePresetProp = "default" | "action-collection";
92
+ export type TablePresetProp = "default" | "action-collection" | "stacked-record-collection";
86
93
  /**
87
94
  * Relative importance of a table column, used by `preset="action-collection"` to allocate the
88
95
  * narrow-frame measure. `"primary"` is the row's subject, `"secondary"` its object/target,
@@ -32,7 +32,14 @@
32
32
  min-width: 0;
33
33
  min-height: 0;
34
34
  flex-direction: column;
35
- overflow: hidden;
35
+ /* Same ring-headroom contract as the topbar family (gh#291): nav rows are
36
+ * flush with the rail's inline edges, so a plain `hidden` clipped their
37
+ * focus ring on three sides. `clip` + margin (the margin is honoured only
38
+ * when BOTH axes clip) lets the ring paint over the rail border, while the
39
+ * rail still contains its content. Inner scroll regions are unaffected —
40
+ * `clip` creates no scroll container, exactly like `hidden`. */
41
+ overflow: clip;
42
+ overflow-clip-margin: var(--focus-ring-clip-margin);
36
43
  border-inline-end: 1px solid hsl(var(--border));
37
44
  background: hsl(var(--card));
38
45
  /* Opt-in brand-chrome wash — invisible by default (--sidebar-gradient: none). */
@@ -47,7 +54,15 @@
47
54
  * set to baseline; --scaling:1 alone is not enough — see foundation.css). */
48
55
  display: flex;
49
56
  min-width: 0;
50
- overflow: hidden;
57
+ /* Same both-axes-clip + margin contract as .ui-topbar (gh#291): Chromium
58
+ * honours overflow-clip-margin ONLY when BOTH axes are `clip` — a
59
+ * single-axis clip silently drops the margin, which would cut a flush
60
+ * control's ring whenever a theme sets --app-shell-bar-inset to 0. The 8px
61
+ * margin also covers the vertical ring of coarse-pointer 44px controls,
62
+ * which the 3.5rem coarse bar height already keeps inside the bar. Safari
63
+ * falls back to `visible` with the .ui-topbar family below. */
64
+ overflow: clip;
65
+ overflow-clip-margin: var(--focus-ring-clip-margin);
51
66
  align-items: center;
52
67
  gap: var(--app-shell-bar-gap);
53
68
  padding-inline: var(--app-shell-bar-inset);
@@ -1271,18 +1286,18 @@
1271
1286
  align-items: center;
1272
1287
  gap: var(--topbar-gap);
1273
1288
  padding-inline: var(--topbar-inset);
1274
- /* Horizontal-only clip (gh#291). `hidden` clipped BOTH axes with no clip-margin,
1275
- * cutting the focus ring of slot controls flat at top/bottom — the bar's content
1276
- * box hugs --control-height exactly, and the rails' overflow-clip-margin below
1277
- * cannot help when the parent already swallowed the ring. `clip` (not `hidden`)
1278
- * is what lets overflow-y stay `visible` a `hidden`/`visible` pair computes to
1279
- * `auto` and still clips. Vertically the ring paints into the shell bar's box,
1280
- * which has headroom; the horizontal shrink/truncation contract is unchanged. */
1281
- overflow-x: clip;
1282
- overflow-y: visible;
1283
- /* Ring headroom on the clipped axis: the end rail sits flush against this
1284
- * box, so without a margin the LAST control's ring loses its outer edge.
1285
- * Bare var() only — Chromium drops any calc() in overflow-clip-margin. */
1289
+ /* Ring headroom contract (gh#291), measured empirically:
1290
+ * - Chromium honours overflow-clip-margin ONLY when BOTH axes are `clip`;
1291
+ * a single-axis clip silently ignores the margin, and a flush-edge
1292
+ * control's ring vanishes on the clipped axis.
1293
+ * - `hidden` can never work here (no clip-margin, and pairing it with
1294
+ * `visible` computes to `auto`, which still clips).
1295
+ * - The margin must be a bare var() Chromium drops calc() at parse time.
1296
+ * So: clip BOTH axes and let the margin carry the ring on every side. The
1297
+ * gh#226 shrink/truncation contract is intact (content clips 8px outside
1298
+ * the box). Safari (clip yes, clip-margin no) falls back to `visible`
1299
+ * below. */
1300
+ overflow: clip;
1286
1301
  overflow-clip-margin: var(--focus-ring-clip-margin);
1287
1302
  }
1288
1303
  .ui-topbar-start,
@@ -1292,19 +1307,27 @@
1292
1307
  min-width: 0;
1293
1308
  align-items: center;
1294
1309
  gap: var(--topbar-gap);
1295
- /* Clip horizontally only (gh#291): the rails hug --control-height, so a
1296
- * vertical clip eats the focus ring even WITH the clip-margin the margin
1297
- * is 2px while e.g. the toggle ring paints 3px, and Safari does not
1298
- * implement overflow-clip-margin at all. The margin still buys horizontal
1299
- * ring headroom where supported. */
1300
- overflow-x: clip;
1301
- overflow-y: visible;
1302
- /* 4px headroom, NOT the 2px ring token: focus/open rings paint up to 3px,
1303
- * so 1x shaved a flush-edge control's ring (the locale picker is the first
1304
- * child of `end`). Bare var() only — Chromium drops any calc() in
1305
- * overflow-clip-margin at parse time, degrading the margin to 0. */
1310
+ /* Same both-axes-clip + margin contract as .ui-topbar above (gh#291): the
1311
+ * 8px margin covers the 3px ring on every side of a flush-edge control;
1312
+ * single-axis clip would silently drop the margin in Chromium. */
1313
+ overflow: clip;
1306
1314
  overflow-clip-margin: var(--focus-ring-clip-margin);
1307
1315
  }
1316
+ /* Safari implements `overflow: clip` but NOT overflow-clip-margin, so the
1317
+ * both-axes clip above would eat every flush-edge focus ring there with no
1318
+ * margin to save it. Trade slot containment for ring visibility on that
1319
+ * engine only — the .app-topbar horizontal clip still prevents bar blowout
1320
+ * and document scroll. */
1321
+ @supports not (overflow-clip-margin: 1px) {
1322
+ .app-sidebar,
1323
+ .app-topbar,
1324
+ .ui-topbar,
1325
+ .ui-topbar-start,
1326
+ .ui-topbar-center,
1327
+ .ui-topbar-end {
1328
+ overflow: visible;
1329
+ }
1330
+ }
1308
1331
  .ui-topbar-start {
1309
1332
  flex: 0 1 auto;
1310
1333
  }
@@ -119,7 +119,14 @@
119
119
  }
120
120
 
121
121
  .ui-data-table-surface {
122
- overflow: hidden;
122
+ /* `clip`, NOT `hidden` (gh#291 family): both clip the table to the rounded
123
+ * border, but `hidden` makes this box a SCROLL CONTAINER, which silently
124
+ * captured the sticky context of `stickyHeader` (default true) and of the
125
+ * pinned columns — they anchored to a box that never scrolls, so the header
126
+ * scrolled away with the body inside `.ui-data-table-scroll`. `clip` is not
127
+ * a scroll container, so sticky binds to the real scroll region and works.
128
+ * Measured: hidden → thead moved 300px per 300px scrolled; clip → 1px. */
129
+ overflow: clip;
123
130
  /* Narrow-viewport legibility floor — a TOKEN, not the old `min-w-[640px]` literal (gh#253,
124
131
  * rule #45). Cleared at the `sm` step below, and opted out of entirely by the collection
125
132
  * preset, which owns its width through the column-priority measures. */
@@ -363,6 +370,29 @@
363
370
  }
364
371
  }
365
372
 
373
+ @layer components {
374
+ /* ── Table · `preset="stacked-record-collection"` (gh#293 restore — SCR-215) ─────────────────
375
+ * The canonical WIDE, heterogeneous record collection: a table whose column count/content
376
+ * cannot be squeezed into any fixed narrow-frame measure at all — an admin detail row with many
377
+ * disparate, free-text fields of unpredictable length, not a dense queue (contrast
378
+ * `action-collection` above, which keeps the real table and only re-sizes columns). Below
379
+ * `collapseBelow` the `<thead>` hides and every `<tr>` becomes a bordered key-value CARD: each
380
+ * `TableCell`'s own `label` (its column header text, carried per-cell since the `<th>`
381
+ * association is now hidden) renders inline above the value, taking over the accessible-name
382
+ * role. Above the step it is a byte-for-byte ordinary table. Measured against the TABLE'S OWN
383
+ * container, not the viewport, same as `action-collection`. */
384
+ .ui-table-stacked-collection {
385
+ container: ui-table-stacked-collection / inline-size;
386
+ }
387
+
388
+ /* Rendered into the DOM unconditionally so it exists for the container query below to reveal
389
+ * (see TableCell in table.tsx) but hidden above the collapse step, where the real `<th>` already
390
+ * carries the label — an ordinary table gains no visible extra markup. */
391
+ .ui-table-stacked-collection-label {
392
+ display: none;
393
+ }
394
+ }
395
+
366
396
  /* The compact tier lives in `godxjp-ui-responsive` — the LAST layer, declared after Tailwind in
367
397
  * styles/base.css — not in `@layer components`. `<table>` carries `text-sm`, a Tailwind utility,
368
398
  * and `utilities` outranks `components` by LAYER ORDER, so a `font-size:` re-point written here as
@@ -536,4 +566,179 @@
536
566
  --table-action-collection-flex-width: var(--table-action-collection-flex-width-floor);
537
567
  }
538
568
  }
569
+
570
+ /* ── `preset="stacked-record-collection"` (gh#293 restore — SCR-215) ────────────────────────
571
+ * Below the step the `<thead>` hides and each `<tr>` becomes a bordered key-value card. This is
572
+ * a genuine layout-mode swap (`display: block`) rather than a measure re-point, because the
573
+ * whole point of this preset is fields too heterogeneous for ANY fixed column measure to hold —
574
+ * unlike `action-collection`'s fixed-layout column budget above. Above the step, none of this
575
+ * matches and the table renders exactly as an ordinary `<table>`. One block per canonical step,
576
+ * same sm/md/lg/xl scale as `action-collection`. */
577
+ @container ui-table-stacked-collection (width < 40rem) {
578
+ [data-collapse-below="sm"] [data-slot="table"] {
579
+ display: block;
580
+ }
581
+
582
+ [data-collapse-below="sm"] [data-slot="table"] > thead {
583
+ display: none;
584
+ }
585
+
586
+ [data-collapse-below="sm"] [data-slot="table"] > tbody {
587
+ display: block;
588
+ }
589
+
590
+ [data-collapse-below="sm"] .ui-table-row {
591
+ display: block;
592
+ border: 1px solid hsl(var(--border));
593
+ border-radius: var(--radius-md);
594
+ padding: var(--table-stacked-collection-card-padding-y)
595
+ var(--table-stacked-collection-card-padding-x);
596
+ margin-block-end: var(--table-stacked-collection-card-gap);
597
+ }
598
+
599
+ [data-collapse-below="sm"] .ui-table-row:last-child {
600
+ margin-block-end: 0;
601
+ }
602
+
603
+ [data-collapse-below="sm"] [data-slot="table-cell"] {
604
+ display: block;
605
+ padding-block: var(--table-stacked-collection-cell-padding-y);
606
+ white-space: normal;
607
+ overflow-wrap: anywhere;
608
+ }
609
+
610
+ [data-collapse-below="sm"] .ui-table-stacked-collection-label {
611
+ display: block;
612
+ font-size: var(--table-stacked-collection-label-font-size);
613
+ font-weight: var(--font-weight-medium);
614
+ color: hsl(var(--muted-foreground));
615
+ margin-block-end: var(--space-stack-xs);
616
+ }
617
+ }
618
+
619
+ @container ui-table-stacked-collection (width < 48rem) {
620
+ [data-collapse-below="md"] [data-slot="table"] {
621
+ display: block;
622
+ }
623
+
624
+ [data-collapse-below="md"] [data-slot="table"] > thead {
625
+ display: none;
626
+ }
627
+
628
+ [data-collapse-below="md"] [data-slot="table"] > tbody {
629
+ display: block;
630
+ }
631
+
632
+ [data-collapse-below="md"] .ui-table-row {
633
+ display: block;
634
+ border: 1px solid hsl(var(--border));
635
+ border-radius: var(--radius-md);
636
+ padding: var(--table-stacked-collection-card-padding-y)
637
+ var(--table-stacked-collection-card-padding-x);
638
+ margin-block-end: var(--table-stacked-collection-card-gap);
639
+ }
640
+
641
+ [data-collapse-below="md"] .ui-table-row:last-child {
642
+ margin-block-end: 0;
643
+ }
644
+
645
+ [data-collapse-below="md"] [data-slot="table-cell"] {
646
+ display: block;
647
+ padding-block: var(--table-stacked-collection-cell-padding-y);
648
+ white-space: normal;
649
+ overflow-wrap: anywhere;
650
+ }
651
+
652
+ [data-collapse-below="md"] .ui-table-stacked-collection-label {
653
+ display: block;
654
+ font-size: var(--table-stacked-collection-label-font-size);
655
+ font-weight: var(--font-weight-medium);
656
+ color: hsl(var(--muted-foreground));
657
+ margin-block-end: var(--space-stack-xs);
658
+ }
659
+ }
660
+
661
+ @container ui-table-stacked-collection (width < 64rem) {
662
+ [data-collapse-below="lg"] [data-slot="table"] {
663
+ display: block;
664
+ }
665
+
666
+ [data-collapse-below="lg"] [data-slot="table"] > thead {
667
+ display: none;
668
+ }
669
+
670
+ [data-collapse-below="lg"] [data-slot="table"] > tbody {
671
+ display: block;
672
+ }
673
+
674
+ [data-collapse-below="lg"] .ui-table-row {
675
+ display: block;
676
+ border: 1px solid hsl(var(--border));
677
+ border-radius: var(--radius-md);
678
+ padding: var(--table-stacked-collection-card-padding-y)
679
+ var(--table-stacked-collection-card-padding-x);
680
+ margin-block-end: var(--table-stacked-collection-card-gap);
681
+ }
682
+
683
+ [data-collapse-below="lg"] .ui-table-row:last-child {
684
+ margin-block-end: 0;
685
+ }
686
+
687
+ [data-collapse-below="lg"] [data-slot="table-cell"] {
688
+ display: block;
689
+ padding-block: var(--table-stacked-collection-cell-padding-y);
690
+ white-space: normal;
691
+ overflow-wrap: anywhere;
692
+ }
693
+
694
+ [data-collapse-below="lg"] .ui-table-stacked-collection-label {
695
+ display: block;
696
+ font-size: var(--table-stacked-collection-label-font-size);
697
+ font-weight: var(--font-weight-medium);
698
+ color: hsl(var(--muted-foreground));
699
+ margin-block-end: var(--space-stack-xs);
700
+ }
701
+ }
702
+
703
+ @container ui-table-stacked-collection (width < 80rem) {
704
+ [data-collapse-below="xl"] [data-slot="table"] {
705
+ display: block;
706
+ }
707
+
708
+ [data-collapse-below="xl"] [data-slot="table"] > thead {
709
+ display: none;
710
+ }
711
+
712
+ [data-collapse-below="xl"] [data-slot="table"] > tbody {
713
+ display: block;
714
+ }
715
+
716
+ [data-collapse-below="xl"] .ui-table-row {
717
+ display: block;
718
+ border: 1px solid hsl(var(--border));
719
+ border-radius: var(--radius-md);
720
+ padding: var(--table-stacked-collection-card-padding-y)
721
+ var(--table-stacked-collection-card-padding-x);
722
+ margin-block-end: var(--table-stacked-collection-card-gap);
723
+ }
724
+
725
+ [data-collapse-below="xl"] .ui-table-row:last-child {
726
+ margin-block-end: 0;
727
+ }
728
+
729
+ [data-collapse-below="xl"] [data-slot="table-cell"] {
730
+ display: block;
731
+ padding-block: var(--table-stacked-collection-cell-padding-y);
732
+ white-space: normal;
733
+ overflow-wrap: anywhere;
734
+ }
735
+
736
+ [data-collapse-below="xl"] .ui-table-stacked-collection-label {
737
+ display: block;
738
+ font-size: var(--table-stacked-collection-label-font-size);
739
+ font-weight: var(--font-weight-medium);
740
+ color: hsl(var(--muted-foreground));
741
+ margin-block-end: var(--space-stack-xs);
742
+ }
743
+ }
539
744
  }
@@ -24,6 +24,9 @@
24
24
  /* DXS application-shell geometry. These defaults mirror the checked-in
25
25
  * Admin/Console hi-fi source while remaining themeable by consumers. */
26
26
  --app-shell-bar-height: 3rem;
27
+ /* (the coarse-pointer override lives at the end of this file — Rule #24 grows
28
+ * controls to 44px, and a 48px bar cannot hold a 44px control plus its 3px
29
+ * focus ring at the very top of the viewport, where overflow cannot help.) */
27
30
  /* Inline inset and slot gap of the AppShell top bar. `-compact` applies below the shell
28
31
  * breakpoint (the docked sidebar is gone, so the bar tightens against the viewport edge). */
29
32
  --app-shell-bar-inset: var(--space-4);
@@ -346,3 +349,13 @@
346
349
  * page is the plain page canvas, not the app's `--muted` chrome. */
347
350
  --centered-shell-landing-background: initial;
348
351
  }
352
+
353
+ /* Rule #24 companion (gh#291): on coarse pointers --control-height grows to 2.75rem
354
+ * (44px tap floor), and the shell bar sits flush with the viewport top — a 3rem bar
355
+ * leaves the control's 3px focus ring painting ABOVE y=0, off-screen, which no
356
+ * overflow setting can recover. 3.5rem gives 6px of breathing per side. */
357
+ @media (pointer: coarse) {
358
+ :root {
359
+ --app-shell-bar-height: 3.5rem;
360
+ }
361
+ }
@@ -136,3 +136,15 @@
136
136
  * Japanese at the compact type tier) and the table scrolls instead of crushing. */
137
137
  --table-action-collection-min-inline-size-compact: 0;
138
138
  }
139
+
140
+ /* Table · stacked-record-collection preset (gh#293 restore — SCR-215) — the canonical WIDE,
141
+ * heterogeneous record collection. Below the collapse step every `<tr>` becomes a bordered
142
+ * key-value card; these tokens own the card's own geometry (unlike action-collection, there is no
143
+ * column-priority measure to retune — the whole row becomes one block). */
144
+ :root {
145
+ --table-stacked-collection-card-padding-y: var(--space-stack-sm);
146
+ --table-stacked-collection-card-padding-x: var(--space-inline-md);
147
+ --table-stacked-collection-card-gap: var(--space-stack-sm);
148
+ --table-stacked-collection-cell-padding-y: var(--space-stack-xs);
149
+ --table-stacked-collection-label-font-size: var(--font-size-xs);
150
+ }
@@ -156,8 +156,11 @@
156
156
  /* Ring headroom for clipped chrome (`overflow-clip-margin` on the topbar family, gh#291).
157
157
  * MUST stay a plain length consumed via a bare var(): Chromium rejects any calc() in
158
158
  * overflow-clip-margin at parse time (even calc(2 * 2px)), silently dropping the
159
- * declaration to 0. Value = max ring paint (3px open/focus) + 1px breathing. */
160
- --focus-ring-clip-margin: 4px;
159
+ * declaration to 0. Value = max ring paint (3px open/focus) + rounding immunity:
160
+ * at 4px a flush-edge ring had only 1px of slack, which device-pixel rounding on
161
+ * non-retina (dpr 1) displays or fractional browser zoom could consume, shaving the
162
+ * ring's outer edge. 8px keeps ≥5px of slack under any rounding regime. */
163
+ --focus-ring-clip-margin: 8px;
161
164
  /* REGION focus ring — the keyboard-scroll affordance on large scroll REGIONS (`.app-main`,
162
165
  * gh#271/gh#276), separate from the control ring above. A control-strength 2px brand frame
163
166
  * around the entire content area reads as a glitch to mouse-first users (`:focus-visible`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "18.12.14",
4
- "godxUiMcp": "18.12.14",
3
+ "version": "18.12.15",
4
+ "godxUiMcp": "18.12.15",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",