@tensor_1/tokens 2.46.0 → 2.48.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 (66) hide show
  1. package/build/css/components.css +1 -1
  2. package/build/css/styles.css +1 -1
  3. package/package.json +1 -1
  4. package/styles/components/alert.css +6 -4
  5. package/styles/components/app-shell.css +40 -0
  6. package/styles/components/arrival.css +7 -1
  7. package/styles/components/attachment.css +4 -1
  8. package/styles/components/audit-log.css +29 -0
  9. package/styles/components/breadcrumbs.css +2 -0
  10. package/styles/components/busy-region.css +55 -0
  11. package/styles/components/button.css +42 -6
  12. package/styles/components/card.css +10 -0
  13. package/styles/components/change-highlight.css +58 -0
  14. package/styles/components/checkbox.css +18 -0
  15. package/styles/components/code-display.css +43 -0
  16. package/styles/components/combobox.css +4 -1
  17. package/styles/components/command-palette.css +16 -2
  18. package/styles/components/contact-card.css +17 -6
  19. package/styles/components/count.css +23 -0
  20. package/styles/components/data-workspace.css +42 -0
  21. package/styles/components/date-picker.css +14 -1
  22. package/styles/components/detail-layout.css +9 -0
  23. package/styles/components/diff-viewer.css +5 -0
  24. package/styles/components/display-preferences.css +38 -0
  25. package/styles/components/editorial-home.css +24 -2
  26. package/styles/components/flow.css +5 -0
  27. package/styles/components/heartbeat.css +8 -0
  28. package/styles/components/icon.css +14 -0
  29. package/styles/components/inline-edit.css +19 -3
  30. package/styles/components/input.css +62 -0
  31. package/styles/components/lightbox.css +13 -0
  32. package/styles/components/list.css +2 -0
  33. package/styles/components/match-review.css +40 -0
  34. package/styles/components/media.css +4 -2
  35. package/styles/components/metric-input.css +3 -1
  36. package/styles/components/modal.css +4 -0
  37. package/styles/components/page-header.css +12 -0
  38. package/styles/components/pagination.css +2 -0
  39. package/styles/components/popover.css +4 -0
  40. package/styles/components/print-layout.css +177 -0
  41. package/styles/components/progress.css +7 -0
  42. package/styles/components/qr-code.css +30 -0
  43. package/styles/components/qr-scanner.css +61 -0
  44. package/styles/components/relationship-graph.css +43 -8
  45. package/styles/components/rule-builder.css +2 -1
  46. package/styles/components/save-status.css +5 -0
  47. package/styles/components/scheduling-calendar.css +82 -1
  48. package/styles/components/search-suggest.css +2 -0
  49. package/styles/components/segmented.css +10 -1
  50. package/styles/components/select.css +11 -0
  51. package/styles/components/sidebar.css +51 -1
  52. package/styles/components/sortable-list.css +42 -0
  53. package/styles/components/spinner.css +5 -0
  54. package/styles/components/stat-tile.css +36 -12
  55. package/styles/components/states.css +5 -0
  56. package/styles/components/status-pill.css +28 -6
  57. package/styles/components/steps.css +2 -0
  58. package/styles/components/table-toolbar.css +1 -1
  59. package/styles/components/table.css +18 -5
  60. package/styles/components/tabs.css +8 -3
  61. package/styles/components/tag-input.css +21 -0
  62. package/styles/components/textarea.css +3 -0
  63. package/styles/components/toast.css +12 -0
  64. package/styles/components/tree.css +3 -1
  65. package/styles/components/view-menu.css +3 -0
  66. package/styles/motion.css +18 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tensor_1/tokens",
3
- "version": "2.46.0",
3
+ "version": "2.48.0",
4
4
  "description": "UIx v2 design tokens — the --uix-* contract for house products (Tensor, POSx, SHOPx). One DTCG source -> CSS variables, Tailwind theme, and typed TS constants.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -9,10 +9,12 @@
9
9
  .uix-alert__icon svg { width: var(--uix-icon-md); height: var(--uix-icon-md); }
10
10
  .uix-alert__title { font-weight: 600; margin-bottom: var(--uix-space-1); }
11
11
  .uix-alert__body { font-size: var(--uix-text-meta); color: var(--uix-text-hushed); }
12
- .uix-alert--info { --tone-bg: var(--uix-info-bg); --tone-border: color-mix(in srgb, var(--uix-info) 30%, transparent); --tone-fg: var(--uix-info-text); }
13
- .uix-alert--success { --tone-bg: var(--uix-success-bg); --tone-border: color-mix(in srgb, var(--uix-success) 30%, transparent); --tone-fg: var(--uix-success-text); }
14
- .uix-alert--warning { --tone-bg: var(--uix-warning-bg); --tone-border: color-mix(in srgb, var(--uix-warning) 40%, transparent); --tone-fg: var(--uix-warning-text); }
15
- .uix-alert--danger { --tone-bg: var(--uix-danger-bg); --tone-border: color-mix(in srgb, var(--uix-danger) 30%, transparent); --tone-fg: var(--uix-danger-text); }
12
+ /* the border is a 24 % hairline of the tone (HAR-1563): the tint already says the tone, and a
13
+ stronger edge made a page of alerts loud. The icon takes the text role, which clears 3:1. */
14
+ .uix-alert--info { --tone-bg: var(--uix-info-bg); --tone-border: color-mix(in srgb, var(--uix-info) 24%, transparent); --tone-fg: var(--uix-info-text); }
15
+ .uix-alert--success { --tone-bg: var(--uix-success-bg); --tone-border: color-mix(in srgb, var(--uix-success) 24%, transparent); --tone-fg: var(--uix-success-text); }
16
+ .uix-alert--warning { --tone-bg: var(--uix-warning-bg); --tone-border: color-mix(in srgb, var(--uix-warning) 24%, transparent); --tone-fg: var(--uix-warning-text); }
17
+ .uix-alert--danger { --tone-bg: var(--uix-danger-bg); --tone-border: color-mix(in srgb, var(--uix-danger) 24%, transparent); --tone-fg: var(--uix-danger-text); }
16
18
  }
17
19
 
18
20
  /* HAR-1614: the text column takes the free width and may shrink below its longest word, so a
@@ -57,3 +57,43 @@
57
57
  the gutter. Pair with a frozen identifier column for horizontal scroll. */
58
58
  .uix-shell__main--bleed { padding: 0; }
59
59
  }
60
+
61
+ /* Full-viewport mode (HAR-1815): an application's shell, not a framed demo. No border or radius,
62
+ the dynamic viewport height, and no fixed top-bar row: the sidebar runs the full height, and a
63
+ topbar, if there is one, takes the height it needs at the top of the main column. Main gets the
64
+ staff padding (space-7 / space-6 / space-6) and space-8 at the sides on a wide screen. Below
65
+ 40rem the shell is one column and the sidebar is hidden: the phone nav tier takes over. */
66
+ @layer uix.components {
67
+ .uix-shell--viewport { height: 100dvh; min-height: 0; border: 0; border-radius: 0; grid-template-rows: auto minmax(0, 1fr); }
68
+ .uix-shell--viewport > .uix-shell__topbar { grid-row: 1; padding-block: var(--uix-space-3); }
69
+ .uix-shell--viewport > .uix-shell__main { grid-row: 2; padding: var(--uix-space-7) var(--uix-space-6) var(--uix-space-6); }
70
+ .uix-shell--viewport:not(:has(> .uix-shell__topbar)) > .uix-shell__main { grid-row: 1 / 3; }
71
+ .uix-shell--viewport > .uix-shell__main.uix-shell__main--bleed { padding: 0; }
72
+ @media (min-width: 80rem) { .uix-shell--viewport > .uix-shell__main:not(.uix-shell__main--bleed) { padding-inline: var(--uix-space-8); } }
73
+ /* one column and no sidebar at phone width come from the phone tier below ([data-phone-nav],
74
+ the viewport default); with phoneNav="none" the sidebar column stays, as in the frame */
75
+ @media (max-width: 40rem) {
76
+ .uix-shell--viewport > .uix-shell__main { padding: var(--uix-space-5) var(--uix-space-4) var(--uix-space-6); }
77
+ }
78
+ }
79
+
80
+ /* The phone nav tier (HAR-1371, AppShell phoneNav="drawer"): at 40rem and under, the sidebar
81
+ column goes and a menu button opens the sidebar in a start-side drawer. The switch is a
82
+ breakpoint, not an animation; the drawer brings the kit's own enter and exit. */
83
+ @layer uix.components {
84
+ .uix-shell__nav-button { display: none; }
85
+ .uix-shell__phone-bar { display: none; }
86
+ /* the sidebar fills its column, so a Sidebar footer (the AccountMenu) sits at the bottom */
87
+ .uix-shell__sidebar > .uix-sidebar { min-height: 100%; }
88
+ /* in the drawer the sidebar grows to the drawer body (a flex column whose height is a min-height,
89
+ so min-height: 100% would not resolve) and the AccountMenu sits at the bottom, as in the column */
90
+ .uix-shell__nav-drawer .uix-sidebar { flex: 1 0 auto; width: auto; padding: 0; background: transparent; }
91
+ .uix-shell__nav-drawer-body { display: flex; flex-direction: column; min-height: 100%; }
92
+ @media (max-width: 40rem) {
93
+ .uix-shell[data-phone-nav], .uix-shell[data-phone-nav][data-nav] { grid-template-columns: minmax(0, 1fr); }
94
+ .uix-shell[data-phone-nav] > .uix-shell__sidebar { display: none; }
95
+ .uix-shell[data-phone-nav] > :is(.uix-shell__topbar, .uix-shell__main) { grid-column: 1; }
96
+ .uix-shell[data-phone-nav] .uix-shell__nav-button { display: inline-flex; flex: none; }
97
+ .uix-shell[data-phone-nav] .uix-shell__phone-bar { display: flex; margin-block-end: var(--uix-space-3); }
98
+ }
99
+ }
@@ -19,8 +19,14 @@
19
19
  0% { outline: 2px solid var(--uix-ring); outline-offset: var(--uix-space-1); }
20
20
  100% { outline: 2px solid transparent; outline-offset: var(--uix-space-1); }
21
21
  }
22
+ /* !important: the reduced-motion policy (motion.css) ends every keyframe animation at once, which
23
+ would end this steady outline before anyone saw it; a layered !important wins over it. The
24
+ in-app preference (data-motion="reduce", HAR-1372) gets the same outline. */
22
25
  @media (prefers-reduced-motion: reduce) {
23
- .uix-arrival { animation: uix-arrival-static var(--uix-dur-highlight) step-end both; }
26
+ .uix-arrival { animation: uix-arrival-static var(--uix-dur-highlight) step-end both !important; }
27
+ }
28
+ @media (prefers-reduced-motion: no-preference) {
29
+ :root[data-motion="reduce"] .uix-arrival { animation: uix-arrival-static var(--uix-dur-highlight) step-end both !important; }
24
30
  }
25
31
 
26
32
  @media (forced-colors: active) {
@@ -4,8 +4,11 @@
4
4
  .uix-attachment { display: flex; align-items: center; gap: var(--uix-space-3); padding: var(--uix-space-2) var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); background: var(--uix-surface); }
5
5
  .uix-attachment__icon { width: 32px; height: 32px; flex: none; border-radius: var(--uix-radius-sm); background: var(--uix-bg-subtle); display: grid; place-items: center; color: var(--uix-text-muted); }
6
6
  .uix-attachment__icon svg { width: var(--uix-icon-md); height: var(--uix-icon-md); }
7
- .uix-attachment__name { font-size: var(--uix-text-body); }
7
+ .uix-attachment__name { display: block; font-size: var(--uix-text-body); }
8
8
  .uix-attachment__size { font-size: var(--uix-text-meta); color: var(--uix-text-muted); }
9
+ /* plain markup: the size under the name, --uix-space-1 apart (HAR-1577). The React row stacks
10
+ its line and meta in .uix-attachment__body, whose gap is the same --uix-space-1. */
11
+ .uix-attachment__name + .uix-attachment__size { display: block; margin-top: var(--uix-space-1); }
9
12
  .uix-attachment__actions { margin-left: auto; }
10
13
  }
11
14
 
@@ -8,4 +8,33 @@
8
8
  .uix-audit__detail ins { background: var(--uix-success-bg); color: var(--uix-success); text-decoration: none; padding: 0 var(--uix-space-1); border-radius: var(--uix-radius-xs); }
9
9
  .uix-audit__detail del { background: var(--uix-danger-bg); color: var(--uix-danger-text); text-decoration: line-through; padding: 0 var(--uix-space-1); border-radius: var(--uix-radius-xs); }
10
10
  .uix-audit__time { color: var(--uix-text-muted); white-space: nowrap; }
11
+
12
+ /* React `AuditLog` (HAR-987): filters over a list of entries; a row is who · what (a labelled
13
+ action and its target) · when (relative, the exact time under it), with Details below. */
14
+ .uix-audit-log { display: grid; gap: var(--uix-space-3); min-width: 0; }
15
+ .uix-audit-log__filters { display: flex; flex-wrap: wrap; align-items: center; gap: var(--uix-space-2); }
16
+ /* a reload keeps the rows on show: BusyRegion dims them under its bar (HAR-2582) */
17
+ ol.uix-audit-log__list { margin: 0; padding: 0; list-style: none; }
18
+ .uix-audit-log__row { align-items: start; }
19
+ .uix-audit-log__what { display: flex; flex-wrap: wrap; align-items: center; gap: var(--uix-space-1) var(--uix-space-2); min-width: 0; color: var(--uix-text); }
20
+ .uix-audit-log__target { min-width: 0; overflow-wrap: anywhere; }
21
+ .uix-audit-log__when { display: grid; justify-items: end; gap: calc(var(--uix-space-1) / 2); text-align: end; }
22
+ .uix-audit-log__exact { font-size: var(--uix-text-eyebrow); color: var(--uix-text-muted); font-variant-numeric: tabular-nums; }
23
+ .uix-audit-log__more { grid-column: 1 / -1; }
24
+ /* Details: a quiet disclosure, not a link — the kit chevron (as CollapsibleSection), muted text. */
25
+ .uix-audit-log__more-summary { display: inline-flex; align-items: flex-start; gap: var(--uix-space-1); width: max-content; list-style: none; cursor: pointer; color: var(--uix-text-muted); font-weight: 500; }
26
+ .uix-audit-log__more-summary::-webkit-details-marker { display: none; }
27
+ .uix-audit-log__more-summary:focus-visible { outline: 2px solid var(--uix-ring); outline-offset: 2px; border-radius: var(--uix-radius-xs); }
28
+ .uix-audit-log__more[open] .uix-collapsible__chevron { transform: rotate(180deg); }
29
+ @media (hover: hover) and (pointer: fine) {
30
+ .uix-audit-log__more-summary:hover, .uix-audit-log__more-summary:hover .uix-collapsible__chevron { color: var(--uix-text); }
31
+ }
32
+ .uix-audit-log__detail { margin-top: var(--uix-space-2); padding: var(--uix-space-2) var(--uix-space-3); border-radius: var(--uix-radius-sm); background: var(--uix-bg-subtle); color: var(--uix-text-hushed); overflow-wrap: anywhere; }
33
+ .uix-audit-log__more-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--uix-space-2) var(--uix-space-3); }
34
+ .uix-audit-log__more-failed { color: var(--uix-danger-text); font-size: var(--uix-text-meta); }
35
+ /* a phone: who, then what, then when, one under the other */
36
+ @media (max-width: 30rem) {
37
+ .uix-audit-log__row { grid-template-columns: minmax(0, 1fr); gap: var(--uix-space-1); }
38
+ .uix-audit-log__when { justify-items: start; text-align: start; grid-auto-flow: column; justify-content: start; gap: var(--uix-space-2); }
39
+ }
11
40
  }
@@ -5,6 +5,8 @@
5
5
  /* already a quiet link by its own design — hover/focus parity with the quiet-link
6
6
  contract in link.css, so the keyboard state matches the pointer state. */
7
7
  .uix-breadcrumbs a:is(:hover, :focus-visible) { color: var(--uix-text); text-decoration: underline; }
8
+ /* a long unbroken title (a German compound, a file name) wraps instead of widening a phone page */
9
+ .uix-breadcrumbs > li { min-inline-size: 0; overflow-wrap: anywhere; }
8
10
  .uix-breadcrumbs__sep { opacity: .55; }
9
11
  .uix-breadcrumbs [aria-current="page"] { color: var(--uix-text); font-weight: 500; }
10
12
  }
@@ -0,0 +1,55 @@
1
+ /* uix busy region — keep what is on screen while it is fetched again (HAR-2582, motion
2
+ standard §2.5). A refetch dims the content under an indeterminate bar along the top instead
3
+ of swapping it for skeletons, so nothing the person was looking at vanishes. The React
4
+ BusyBar sets [data-shown] after 300 ms and holds it 500 ms; the parent reads it with :has(),
5
+ so `BusyRegion` and `TableWrap refreshing` stay server components. Depends only on --uix-*
6
+ tokens. */
7
+ @layer uix.components {
8
+ .uix-busy-region { position: relative; }
9
+ .uix-busy-region__content { transition: opacity var(--uix-dur-exit) var(--uix-ease-out); }
10
+ .uix-busy-region:has(> .uix-busy-bar[data-shown]) > .uix-busy-region__content { opacity: .8; transition-duration: var(--uix-dur); }
11
+
12
+ .uix-busy-bar {
13
+ position: absolute; inset-block-start: 0; inset-inline: 0; z-index: var(--uix-z-sticky);
14
+ display: block; height: 2px; overflow: hidden; pointer-events: none;
15
+ opacity: 0; transition: opacity var(--uix-dur-exit) var(--uix-ease-out);
16
+ }
17
+ .uix-busy-bar[data-shown] { opacity: 1; transition-duration: var(--uix-dur); }
18
+ .uix-busy-bar::before {
19
+ content: ""; position: absolute; inset-block: 0; inset-inline-start: 0; width: 40%;
20
+ background: var(--uix-accent); transform: translateX(-100%);
21
+ }
22
+ /* constant motion is linear (§2.4) */
23
+ .uix-busy-bar[data-shown]::before { animation: uix-busy-sweep var(--uix-loop-progress) linear infinite; }
24
+ @keyframes uix-busy-sweep { from { transform: translateX(-100%); } to { transform: translateX(250%); } }
25
+ /* right to left: the stripe starts at the right (inline-start) edge and sweeps leftwards */
26
+ @keyframes uix-busy-sweep-rtl { from { transform: translateX(100%); } to { transform: translateX(-250%); } }
27
+ .uix-busy-bar[data-shown]:dir(rtl)::before { animation-name: uix-busy-sweep-rtl; }
28
+
29
+ /* Reduced motion: no sweep; the full bar pulses (the kit's uix-pulse), so "still working" survives (§2.7). */
30
+ @media (prefers-reduced-motion: reduce) {
31
+ .uix-busy-bar::before { width: 100%; transform: none; }
32
+ /* !important: the kit's reduced-motion policy ends keyframes at once; a still-working signal opts back in */
33
+ .uix-busy-bar[data-shown]::before, .uix-busy-bar[data-shown]:dir(rtl)::before { animation: uix-pulse var(--uix-loop-pulse) var(--uix-ease-in-out) infinite !important; }
34
+ }
35
+ @media (forced-colors: active) {
36
+ .uix-busy-bar::before { background: Highlight; }
37
+ }
38
+
39
+ /* A refreshing table: the bar sticks to the top of the scrolling wrap (over the sticky
40
+ header) and takes no room: it is 0 tall and draws its 2 px stripe below its own edge. It
41
+ clips sideways only (overflow-x: clip), so the sweep never widens the wrap's scroll area.
42
+ The rows dim; the header stays. */
43
+ .uix-table-wrap > .uix-busy-bar { position: sticky; inset-inline-start: 0; height: 0; overflow-x: clip; overflow-y: visible; z-index: calc(var(--uix-z-sticky) + 1); }
44
+ .uix-table-wrap > .uix-busy-bar::before { inset-block: 0 auto; height: 2px; }
45
+ .uix-table-wrap > .uix-table > tbody { transition: opacity var(--uix-dur-exit) var(--uix-ease-out); }
46
+ .uix-table-wrap:has(> .uix-busy-bar[data-shown]) > .uix-table > tbody { opacity: .8; transition-duration: var(--uix-dur); }
47
+ }
48
+
49
+ /* the in-app reduced-motion preference (HAR-1372, <html data-motion="reduce">): the same rule */
50
+ @layer uix.components {
51
+ @media (prefers-reduced-motion: no-preference) {
52
+ :root[data-motion="reduce"] .uix-busy-bar::before { width: 100%; transform: none; }
53
+ :root[data-motion="reduce"] .uix-busy-bar[data-shown]::before { animation: uix-pulse var(--uix-loop-pulse) var(--uix-ease-in-out) infinite !important; }
54
+ }
55
+ }
@@ -36,6 +36,8 @@
36
36
  .uix-btn--icon { width: var(--uix-control-h); padding: 0; }
37
37
  .uix-btn--icon.uix-btn--xs { width: 24px; }
38
38
  .uix-btn--icon.uix-btn--sm { width: 28px; }
39
+ /* A small link button is still a link: no side padding, so it lines up with the text around it. */
40
+ .uix-btn--link.uix-btn--sm { padding-inline: 0; }
39
41
  .uix-btn--icon.uix-btn--lg { width: 44px; }
40
42
 
41
43
  /* A label that may wrap (HAR-1813, WCAG 1.4.10). The size sets a floor instead of a fixed
@@ -52,20 +54,49 @@
52
54
  .uix-btn--wrap.uix-btn--lg:not(.uix-btn--icon) { min-height: 44px; }
53
55
  .uix-btn--wrap.uix-btn--link { min-height: 0; padding-block: 0; }
54
56
 
55
- .uix-btn[data-loading] { color: transparent !important; position: relative; pointer-events: none; }
57
+ /* Pending (React `loading`, HAR-2580): the button stops taking presses at once but looks the
58
+ same for the first 300 ms; a fast action shows nothing new. It keeps full colour while it
59
+ works (a spinner, never a greyed-out control), which overrides the aria-disabled dim. */
60
+ .uix-btn[data-pending], .uix-btn:has(> [data-uix-btn-busy]) { opacity: 1; pointer-events: none; }
61
+
62
+ /* The spinner: `data-loading` (plain markup, shows at once) or React's delayed busy marker. */
63
+ .uix-btn:has(> [data-uix-btn-busy]), .uix-btn[data-loading] { color: transparent !important; position: relative; pointer-events: none; }
56
64
  /* The label is hidden (color: transparent), so the ring cannot inherit
57
65
  currentColor from the button; each variant names its own foreground.
58
66
  TENSOR RX-125 (UIX-01): the ring was on-accent for every variant, so a
59
67
  loading secondary/outline/ghost button rendered blank. */
60
- .uix-btn[data-loading]::after {
68
+ .uix-btn:has(> [data-uix-btn-busy])::after, .uix-btn[data-loading]::after {
61
69
  content: ""; position: absolute; width: 1em; height: 1em;
62
70
  border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%;
63
71
  color: var(--uix-text); animation: uix-spin var(--uix-loop-spin) linear infinite;
64
72
  }
65
- @media (prefers-reduced-motion: reduce) { .uix-btn[data-loading]::after { animation: uix-pulse var(--uix-loop-pulse) var(--uix-ease-in-out) infinite !important; } }
66
- .uix-btn--primary[data-loading]::after { color: var(--uix-accent-fg); }
67
- .uix-btn--danger[data-loading]::after { color: var(--uix-danger-fg); }
68
- .uix-btn--link[data-loading]::after { color: var(--uix-link); }
73
+ @media (prefers-reduced-motion: reduce) { .uix-btn:has(> [data-uix-btn-busy])::after, .uix-btn[data-loading]::after { animation: uix-pulse var(--uix-loop-pulse) var(--uix-ease-in-out) infinite !important; } }
74
+ .uix-btn--primary:has(> [data-uix-btn-busy])::after, .uix-btn--primary[data-loading]::after { color: var(--uix-accent-fg); }
75
+ .uix-btn--danger:has(> [data-uix-btn-busy])::after, .uix-btn--danger[data-loading]::after { color: var(--uix-danger-fg); }
76
+ .uix-btn--link:has(> [data-uix-btn-busy])::after, .uix-btn--link[data-loading]::after { color: var(--uix-link); }
77
+ /* The result face hides with the label while the spinner shows. */
78
+ .uix-btn:has(> [data-uix-btn-busy]) .uix-btn__result { visibility: hidden; }
79
+
80
+ /* Result (React `result`, HAR-2581): the label and both result wordings share one grid cell,
81
+ so the button is always as wide as the widest and nothing beside it moves. A result
82
+ crossfades in over the label (opacity only), holds, and fades back. Text and icon carry
83
+ the meaning; colour only tints the icon. */
84
+ .uix-btn__face { display: inline-grid; justify-items: center; align-items: center; }
85
+ .uix-btn__face > * { grid-area: 1 / 1; display: inline-flex; align-items: center; gap: .5em; transition: opacity var(--uix-dur) var(--uix-ease-out); }
86
+ .uix-btn__result { opacity: 0; transition-duration: var(--uix-dur-exit); }
87
+ .uix-btn__face[data-result] > .uix-btn__label { opacity: 0; transition-duration: var(--uix-dur-exit-fast); }
88
+ .uix-btn__face[data-result="success"] > [data-kind="success"],
89
+ .uix-btn__face[data-result="error"] > [data-kind="error"] { opacity: 1; transition-duration: var(--uix-dur); }
90
+ .uix-btn--secondary .uix-btn__result[data-kind="success"] svg,
91
+ .uix-btn--outline .uix-btn__result[data-kind="success"] svg,
92
+ .uix-btn--ghost .uix-btn__result[data-kind="success"] svg { color: var(--uix-success-text); }
93
+ .uix-btn--secondary .uix-btn__result[data-kind="error"] svg,
94
+ .uix-btn--outline .uix-btn__result[data-kind="error"] svg,
95
+ .uix-btn--ghost .uix-btn__result[data-kind="error"] svg { color: var(--uix-danger-text); }
96
+ .uix-btn--wrap .uix-btn__face > * { white-space: normal; }
97
+ .uix-btn--xs .uix-btn__face > * { gap: .25em; }
98
+ /* An icon-only button keeps its square: the result is the mark alone (the words are spoken). */
99
+ .uix-btn--icon .uix-btn__result-text { display: none; }
69
100
 
70
101
  /* attached group */
71
102
  .uix-btn-group { display: inline-flex; }
@@ -73,3 +104,8 @@
73
104
  .uix-btn-group .uix-btn:first-child { border-top-left-radius: var(--uix-radius-md); border-bottom-left-radius: var(--uix-radius-md); }
74
105
  .uix-btn-group .uix-btn:last-child { border-radius: 0 var(--uix-radius-md) var(--uix-radius-md) 0; border-right-width: 1px; }
75
106
  }
107
+
108
+ /* the in-app reduced-motion preference (HAR-1372, <html data-motion="reduce">): the same pulse */
109
+ @layer uix.components {
110
+ @media (prefers-reduced-motion: no-preference) { :root[data-motion="reduce"] .uix-btn:has(> [data-uix-btn-busy])::after, :root[data-motion="reduce"] .uix-btn[data-loading]::after { animation: uix-pulse var(--uix-loop-pulse) var(--uix-ease-in-out) infinite !important; } }
111
+ }
@@ -36,3 +36,13 @@
36
36
  .uix-collapsible--compact > .uix-collapsible__summary { padding: var(--uix-space-2) 0; border-radius: var(--uix-radius-sm); }
37
37
  .uix-collapsible--compact > .uix-collapsible__body { padding: 0 0 var(--uix-space-3); }
38
38
  }
39
+
40
+ /* Inset (HAR-1818): a bordered, sunken well inside a card body for a preview or a specimen (a
41
+ rendered header, an image crop, a code sample). Not a nested card: no shadow, no card padding,
42
+ no card semantics. The consumer lays out what is inside. */
43
+ @layer uix.components {
44
+ .uix-inset { min-width: 0; padding: var(--uix-space-4); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); background: var(--uix-bg-subtle); overflow-wrap: anywhere; }
45
+ .uix-inset--sm { padding: var(--uix-space-3); }
46
+ /* the well on a sunken page (a bg-subtle panel) takes the surface instead, so it still reads as a well */
47
+ .uix-inset--surface { background: var(--uix-surface); }
48
+ }
@@ -0,0 +1,58 @@
1
+ /* uix change highlight — "this changed": the sibling of .uix-arrival for a value that changed in
2
+ place (a live update, a transition's result, a rolled-back optimistic change; HAR-2585, motion
3
+ standard §2.5). The element is tinted at once, holds a moment, and the tint fades back to the
4
+ element's own background. React's ChangeHighlight / useChangeHighlight set [data-uix-changed]
5
+ (never on the first render); plain markup adds .uix-changed (remove it, force a reflow and add
6
+ it again to replay). Depends only on --uix-* tokens. */
7
+ @layer uix.components {
8
+ .uix-change-highlight, .uix-changed {
9
+ /* opaque, so a pinned table cell never turns see-through while it is tinted */
10
+ --uix-change-tint: color-mix(in srgb, var(--uix-accent) 14%, var(--uix-surface));
11
+ --uix-change-ring: var(--uix-ring);
12
+ /* about 1.3 s: long enough to find, short enough to be gone before the next glance */
13
+ --uix-change-duration: calc(var(--uix-dur-slower) * 4);
14
+ }
15
+
16
+ /* No fill-mode: the last frame is the element's own background, so a pill or a zebra row keeps
17
+ its colour once the tint is gone. */
18
+ @keyframes uix-changed { 0%, 40% { background-color: var(--uix-change-tint); } }
19
+ @keyframes uix-changed-ring { 0%, 40% { outline: 2px solid var(--uix-change-ring); outline-offset: 2px; } 100% { outline: 2px solid transparent; outline-offset: 2px; } }
20
+ /* reduced motion and forced colours: a steady outline for the same time, then nothing — just
21
+ inside the edge of a row or field, just outside a ringed pill (inside it would cover the label) */
22
+ @keyframes uix-changed-static-ring { 0% { outline: 2px solid var(--uix-change-ring); outline-offset: 2px; } 100% { outline: 2px solid transparent; outline-offset: 2px; } }
23
+ @keyframes uix-changed-static { 0% { outline: 2px solid var(--uix-change-ring); outline-offset: calc(-1 * var(--uix-space-1)); } 100% { outline: 2px solid transparent; outline-offset: calc(-1 * var(--uix-space-1)); } }
24
+
25
+ /* held, then an even fade: an ease-out would drain most of the tint before anyone looks */
26
+ .uix-changed, .uix-change-highlight[data-uix-changed] { animation: uix-changed var(--uix-change-duration) var(--uix-ease-in-out); }
27
+ /* the ring hugs what it wraps (a pill, a star): inline-flex, pill-round */
28
+ .uix-change-highlight--ring { display: inline-flex; vertical-align: middle; border-radius: var(--uix-radius-pill); }
29
+ .uix-change-highlight--ring[data-uix-changed], .uix-changed.uix-change-highlight--ring { animation-name: uix-changed-ring; }
30
+ /* a row tints its cells: cells paint over the row's own background */
31
+ tr.uix-changed, tr.uix-change-highlight[data-uix-changed] { animation: none; }
32
+ tr.uix-changed > :is(td, th), tr.uix-change-highlight[data-uix-changed] > :is(td, th) { animation: uix-changed var(--uix-change-duration) var(--uix-ease-in-out); }
33
+
34
+ @media (prefers-reduced-motion: reduce) {
35
+ /* !important: the kit's reduced-motion policy ends keyframes at once; this one is the signal.
36
+ A row is outlined as one box, not cell by cell. */
37
+ .uix-changed, .uix-change-highlight[data-uix-changed],
38
+ tr.uix-changed, tr.uix-change-highlight[data-uix-changed] { animation: uix-changed-static var(--uix-change-duration) step-end !important; }
39
+ .uix-change-highlight--ring[data-uix-changed] { animation: uix-changed-static-ring var(--uix-change-duration) step-end !important; }
40
+ tr.uix-changed > :is(td, th), tr.uix-change-highlight[data-uix-changed] > :is(td, th) { animation: none !important; }
41
+ }
42
+ @media (forced-colors: active) {
43
+ .uix-change-highlight, .uix-changed { --uix-change-ring: Highlight; }
44
+ .uix-changed, .uix-change-highlight[data-uix-changed],
45
+ tr.uix-changed, tr.uix-change-highlight[data-uix-changed] { animation: uix-changed-static var(--uix-change-duration) step-end; }
46
+ .uix-change-highlight--ring[data-uix-changed] { animation: uix-changed-static-ring var(--uix-change-duration) step-end; }
47
+ tr.uix-changed > :is(td, th), tr.uix-change-highlight[data-uix-changed] > :is(td, th) { animation: none; }
48
+ }
49
+ }
50
+
51
+ /* the in-app reduced-motion preference (HAR-1372, <html data-motion="reduce">): the same outline */
52
+ @layer uix.components {
53
+ @media (prefers-reduced-motion: no-preference) {
54
+ :root[data-motion="reduce"] :is(.uix-changed, .uix-change-highlight[data-uix-changed]) { animation: uix-changed-static var(--uix-change-duration) step-end !important; }
55
+ :root[data-motion="reduce"] .uix-change-highlight--ring[data-uix-changed] { animation: uix-changed-static-ring var(--uix-change-duration) step-end !important; }
56
+ :root[data-motion="reduce"] :is(tr.uix-changed, tr.uix-change-highlight[data-uix-changed]) > :is(td, th) { animation: none !important; }
57
+ }
58
+ }
@@ -19,3 +19,21 @@
19
19
  .uix-checkbox > input:focus-visible + .uix-checkbox__box { outline: 2px solid var(--uix-ring); outline-offset: 2px; }
20
20
  .uix-checkbox > input:disabled ~ * { opacity: .5; }
21
21
  }
22
+
23
+ /* CheckboxGroup (HAR-1799): a fieldset whose legend names the boxes, with the field's message
24
+ slot. The UA fieldset chrome is stripped so it reads like a Field. `--uix-checkbox-group-columns`
25
+ (React `columns`) is a ceiling: a column never gets narrower than 10rem, so a phone gets one. */
26
+ @layer uix.components {
27
+ .uix-checkbox-group { display: flex; flex-direction: column; gap: var(--uix-space-2); min-width: 0; min-inline-size: auto; margin: 0; padding: 0; border: 0; }
28
+ .uix-checkbox-group__legend { padding: 0; margin: 0 0 var(--uix-space-2); font-size: var(--uix-text-meta); font-weight: 600; color: var(--uix-text); overflow-wrap: anywhere; }
29
+ .uix-checkbox-group__legend[data-required]::after { content: " *"; color: var(--uix-danger-text); }
30
+ .uix-checkbox-group__options {
31
+ --uix-checkbox-group-columns: 1;
32
+ display: grid; gap: var(--uix-space-2) var(--uix-space-5); align-items: start;
33
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, max(10rem, calc((100% - (var(--uix-checkbox-group-columns) - 1) * var(--uix-space-5)) / var(--uix-checkbox-group-columns)))), 1fr));
34
+ }
35
+ .uix-checkbox-group__options > .uix-checkbox { align-items: flex-start; min-width: 0; overflow-wrap: anywhere; }
36
+ /* the box sits on the first line of a label that wraps */
37
+ .uix-checkbox-group__options > .uix-checkbox > .uix-checkbox__box { translate: 0 calc((1lh - 18px) / 2); }
38
+ .uix-checkbox-group[data-invalid] .uix-checkbox__box { border-color: var(--uix-danger); }
39
+ }
@@ -0,0 +1,43 @@
1
+ /* uix code display (HAR-1253) — a one-time code shown large: React CodeDisplay.
2
+ Groups wrap as whole groups on a narrow screen; a single group too long for the line breaks
3
+ between characters rather than leaving the page. The expired state is said in words (the meta
4
+ line) and the code is struck through, so it never rests on colour alone. */
5
+ @layer uix.components {
6
+ .uix-code-display { display: inline-grid; justify-items: start; gap: var(--uix-space-2); max-width: 100%; min-width: 0; }
7
+ .uix-code-display__label { color: var(--uix-text-muted); font-size: var(--uix-text-meta); font-weight: 500; }
8
+ .uix-code-display__row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--uix-space-2) var(--uix-space-3); max-width: 100%; min-width: 0; }
9
+ .uix-code-display__code {
10
+ display: flex;
11
+ flex-wrap: wrap;
12
+ align-items: baseline;
13
+ min-width: 0;
14
+ max-width: 100%;
15
+ color: var(--uix-text);
16
+ font-family: var(--uix-font-mono);
17
+ font-size: var(--uix-text-data-hero);
18
+ font-weight: 600;
19
+ line-height: var(--uix-leading-data-hero);
20
+ font-variant-numeric: tabular-nums slashed-zero;
21
+ letter-spacing: .06em;
22
+ }
23
+ /* lg is the hero of a card or a till screen: it takes the width it is given and fits a
24
+ three-by-three code (11 characters with the separators, ~8.4em of mono at this tracking) on
25
+ one line, down to the h2 size; only below that do the groups wrap (review of PR #166: in a
26
+ card at 320–360 px the last group fell to its own line). */
27
+ .uix-code-display--lg { display: grid; inline-size: 100%; container-type: inline-size; }
28
+ .uix-code-display--lg .uix-code-display__code { font-size: clamp(var(--uix-text-h2), calc(100cqi / 8.4), var(--uix-text-display)); line-height: var(--uix-leading-display); }
29
+ .uix-code-display__group { min-width: 0; overflow-wrap: anywhere; }
30
+ .uix-code-display__sep { margin-inline: .2em; color: var(--uix-text-hushed); font-weight: 400; }
31
+ /* the spoken copy is for screen readers only; a pointer selection of the code never picks it up */
32
+ .uix-code-display__spoken { user-select: none; }
33
+ .uix-code-display__meta { color: var(--uix-text-muted); font-size: var(--uix-text-meta); font-variant-numeric: tabular-nums; }
34
+
35
+ /* the last minute (warnBefore): the countdown turns to a warning; nothing moves per tick */
36
+ .uix-code-display[data-state="soon"] .uix-code-display__meta { color: var(--uix-warning-text); font-weight: 600; }
37
+ .uix-code-display[data-state="expired"] .uix-code-display__code { color: var(--uix-text-hushed); text-decoration: line-through; text-decoration-thickness: .06em; }
38
+ .uix-code-display[data-state="expired"] .uix-code-display__meta { color: var(--uix-danger-text); font-weight: 600; }
39
+
40
+ @media print {
41
+ .uix-code-display__code { color: var(--uix-print-ink); }
42
+ }
43
+ }
@@ -2,7 +2,10 @@
2
2
  @layer uix.components {
3
3
  .uix-combobox { position: relative; }
4
4
  .uix-combobox > .uix-input { width: 100%; }
5
- .uix-combobox__listbox { position: absolute; z-index: var(--uix-z-dropdown); inset-inline: 0; top: calc(100% + var(--uix-space-1)); max-height: 240px; overflow-y: auto; }
5
+ /* a static list right under the field (plain HTML); the React Combobox draws its list in a
6
+ top-layer popup instead (HAR-1588), styled as Select's */
7
+ .uix-combobox > .uix-combobox__listbox { position: absolute; z-index: var(--uix-z-dropdown); inset-inline: 0; top: calc(100% + var(--uix-space-1)); max-height: 240px; overflow-y: auto; }
8
+ .uix-combobox__popup { max-width: min(32rem, calc(100vw - 2 * var(--uix-space-4))); }
6
9
  .uix-listbox {
7
10
  list-style: none; margin: 0; padding: var(--uix-space-1); min-width: 200px;
8
11
  background: var(--uix-surface-2); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md);
@@ -1,6 +1,6 @@
1
1
  /* uix command palette (⌘K) — inner surface; mounts in a dialog/popover (overlay mechanics in motion + overlays). */
2
2
  @layer uix.components {
3
- .uix-cmdk { width: min(560px, 92vw); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-lg); background: var(--uix-surface-2); box-shadow: var(--uix-shadow-overlay); overflow: hidden; }
3
+ .uix-cmdk { width: min(560px, 92vw); max-width: 100%; border: 1px solid var(--uix-border); border-radius: var(--uix-radius-lg); background: var(--uix-surface-2); box-shadow: var(--uix-shadow-overlay); overflow: hidden; }
4
4
  .uix-cmdk__input { width: 100%; height: 48px; border: 0; border-bottom: 1px solid var(--uix-border); background: transparent; color: var(--uix-text); font: inherit; font-size: var(--uix-text-h3); padding: 0 var(--uix-space-4); }
5
5
  .uix-cmdk__input:focus { outline: none; }
6
6
  /* visible focus replacement for the suppressed outline (UIX-A11Y-1): the divider under the
@@ -10,8 +10,22 @@
10
10
  .uix-cmdk__group { font-size: var(--uix-text-eyebrow); letter-spacing: var(--uix-tracking-eyebrow); text-transform: uppercase; color: var(--uix-text-muted); padding: var(--uix-space-2) var(--uix-space-3) var(--uix-space-1); }
11
11
  .uix-cmdk__item { display: flex; align-items: center; gap: var(--uix-space-3); padding: var(--uix-space-3) var(--uix-space-3); border-radius: var(--uix-radius-sm); color: var(--uix-text); cursor: pointer; font-size: var(--uix-text-body); }
12
12
  .uix-cmdk__item svg { width: var(--uix-icon-md); height: var(--uix-icon-md); color: var(--uix-text-muted); }
13
- .uix-cmdk__item:hover, .uix-cmdk__item[data-active] { background: var(--uix-bg-hover); }
13
+ .uix-cmdk__item[data-active] { background: var(--uix-bg-hover); }
14
+ /* items mode moves the active row with the pointer, so a hover tint there would mark a second row */
15
+ @media (hover: hover) and (pointer: fine) { .uix-cmdk:not([data-items]) .uix-cmdk__item:hover { background: var(--uix-bg-hover); } }
14
16
  .uix-cmdk__item .uix-kbd { margin-left: auto; }
17
+ /* items mode (HAR-1378): label + description, matched text, the shortcut at the end, empty state */
18
+ .uix-cmdk__icon { display: inline-flex; flex: none; }
19
+ .uix-cmdk__text { display: grid; flex: 1; min-width: 0; gap: calc(var(--uix-space-1) / 2); }
20
+ .uix-cmdk__label { overflow-wrap: anywhere; }
21
+ .uix-cmdk__desc { font-size: var(--uix-text-meta); color: var(--uix-text-muted); overflow-wrap: anywhere; }
22
+ .uix-cmdk__mark { background: transparent; color: inherit; font-weight: 600; }
23
+ .uix-cmdk__shortcut { flex: none; margin-left: auto; }
24
+ .uix-cmdk__item[aria-disabled="true"] { color: var(--uix-text-muted); cursor: default; }
25
+ .uix-cmdk__empty { margin: 0; padding: var(--uix-space-6) var(--uix-space-4); text-align: center; color: var(--uix-text-muted); font-size: var(--uix-text-body); }
26
+ /* the palette's own dialog: the palette is the visible box, the dialog only hosts it */
27
+ .uix-dialog.uix-cmdk-dialog { width: auto; max-width: min(560px, 92vw); margin: 12vh auto auto; padding: 0; border: 0; background: transparent; box-shadow: none; overflow: visible; }
28
+ .uix-cmdk-dialog .uix-cmdk { width: min(560px, 92vw); }
15
29
  /* ⌘K is keyboard-triggered and high-frequency → open instantly, no animation (Emil's rule) */
16
30
  dialog[data-uix-cmdk-dialog], dialog[data-uix-cmdk-dialog]::backdrop { transition: none; }
17
31
  }
@@ -1,12 +1,23 @@
1
- /* uix contact / customer card (CRM) — depends only on --uix-* tokens. */
1
+ /* uix contact / customer card (CRM) — depends only on --uix-* tokens. React: ContactCard (HAR-1379). */
2
2
  @layer uix.components {
3
- .uix-contact { display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--uix-space-1); padding: var(--uix-space-5); }
4
- .uix-contact__name { font-size: var(--uix-text-h3); font-weight: 600; margin-top: var(--uix-space-2); }
5
- .uix-contact__role { font-size: var(--uix-text-meta); color: var(--uix-text-muted); }
6
- .uix-contact__actions { display: flex; gap: var(--uix-space-2); margin-top: var(--uix-space-3); align-self: stretch; }
7
- .uix-contact__actions > .uix-btn { flex: 1; }
3
+ .uix-contact { display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--uix-space-1); padding: var(--uix-space-5); min-width: 0; }
4
+ /* bare: inside a surface that already has its padding (a card body, a peek, a popover) */
5
+ .uix-contact--bare { padding: 0; }
6
+ .uix-contact__name { font-size: var(--uix-text-h3); font-weight: 600; margin: var(--uix-space-2) 0 0; max-width: 100%; overflow-wrap: anywhere; }
7
+ .uix-contact__role { font-size: var(--uix-text-meta); color: var(--uix-text-muted); max-width: 100%; overflow-wrap: anywhere; }
8
+ .uix-contact__actions { display: flex; flex-wrap: wrap; gap: var(--uix-space-2); margin-top: var(--uix-space-3); align-self: stretch; }
9
+ .uix-contact__actions > .uix-btn { flex: 1 1 7rem; }
8
10
  .uix-contact__stats { display: flex; gap: var(--uix-space-5); margin-top: var(--uix-space-4); }
9
11
  .uix-contact__stat { text-align: center; }
10
12
  .uix-contact__stat b { display: block; font-size: var(--uix-text-h3); }
11
13
  .uix-contact__stat span { font-size: var(--uix-text-meta); color: var(--uix-text-muted); }
14
+
15
+ /* the address and number as text, selectable; a long address breaks rather than widening the card */
16
+ .uix-contact__details { display: flex; flex-direction: column; gap: var(--uix-space-1); margin-top: var(--uix-space-2); max-width: 100%; font-size: var(--uix-text-meta); }
17
+ .uix-contact__detail { overflow-wrap: anywhere; }
18
+ /* an erased or unavailable contact: a neutral placeholder and a line that says why */
19
+ .uix-contact__placeholder { background: var(--uix-bg-subtle); color: var(--uix-text-muted); border: 1px solid var(--uix-border); }
20
+ .uix-contact__placeholder svg { width: 55%; height: 55%; }
21
+ .uix-contact__note { color: var(--uix-text-muted); font-size: var(--uix-text-meta); font-style: italic; }
22
+ .uix-contact[data-state="purged"] .uix-contact__name { color: var(--uix-text-muted); font-weight: 500; }
12
23
  }
@@ -0,0 +1,23 @@
1
+ /* uix count — a number that ticks once when it changes (HAR-2587, motion standard §2.8): a badge,
2
+ a stat value, an unread total. React's Count sets [data-uix-tick="up" | "down"] after a change,
3
+ never on the first render: the new figure comes in 4 px from below (up) or above (down) with a
4
+ fade. One tick, never a loop, never a count-up. Depends only on --uix-* tokens. */
5
+ @layer uix.components {
6
+ .uix-count { display: inline-block; font-variant-numeric: tabular-nums; }
7
+ @keyframes uix-count-up { from { opacity: .3; transform: translateY(var(--uix-lift)); } }
8
+ @keyframes uix-count-down { from { opacity: .3; transform: translateY(calc(-1 * var(--uix-lift))); } }
9
+ @keyframes uix-count-fade { from { opacity: .3; } }
10
+ .uix-count[data-uix-tick="up"] { animation: uix-count-up var(--uix-dur) var(--uix-ease-out); }
11
+ .uix-count[data-uix-tick="down"] { animation: uix-count-down var(--uix-dur) var(--uix-ease-out); }
12
+ @media (prefers-reduced-motion: reduce) {
13
+ /* no travel; the fade stays (!important: the reduced-motion policy ends keyframes at once) */
14
+ .uix-count[data-uix-tick="up"], .uix-count[data-uix-tick="down"] { animation: uix-count-fade var(--uix-dur) var(--uix-ease-out) !important; }
15
+ }
16
+ }
17
+
18
+ /* the in-app reduced-motion preference (HAR-1372, <html data-motion="reduce">): the same fade */
19
+ @layer uix.components {
20
+ @media (prefers-reduced-motion: no-preference) {
21
+ :root[data-motion="reduce"] .uix-count:is([data-uix-tick="up"], [data-uix-tick="down"]) { animation: uix-count-fade var(--uix-dur) var(--uix-ease-out) !important; }
22
+ }
23
+ }
@@ -0,0 +1,42 @@
1
+ /* uix data workspace — the generic presentation half of a product's data table (React
2
+ `DataWorkspace`, HAR-1388, ADR-0005): toolbar · bulk bar · table · footer, composed from
3
+ .uix-toolbar, .uix-chip, .uix-searchbar, .uix-bulkbar, .uix-table and .uix-pagination.
4
+ Depends only on --uix-* tokens. */
5
+ @layer uix.components {
6
+ .uix-data-workspace { display: grid; gap: var(--uix-space-3); min-width: 0; }
7
+ .uix-data-workspace > .uix-toolbar { margin-bottom: 0; }
8
+ .uix-data-workspace__search { flex: 0 1 18rem; min-width: min(12rem, 100%); }
9
+ .uix-data-workspace__chip, .uix-data-workspace__menu { display: inline-flex; }
10
+ /* the row-actions column takes the width of its buttons, not the table's 84px */
11
+ .uix-data-workspace .uix-table__actions { width: 1%; }
12
+ /* with selection, the id column is frozen too, right after the checkbox column */
13
+ .uix-data-workspace__grid--pin-id :is(th, td):nth-child(2) { position: sticky; left: var(--uix-selcol-w); z-index: var(--uix-z-raised); box-shadow: 1px 0 0 var(--uix-border); }
14
+ .uix-data-workspace__grid--pin-id :is(th, td):first-child { box-shadow: none; }
15
+ /* A squeezed table shrinks the checkbox column to its content (30px), which would open a gap
16
+ before the id frozen at --uix-selcol-w: an empty, 0-high block holds the column at that width
17
+ (the checkbox itself stays as it is, centred, its input over its box). */
18
+ .uix-data-workspace__grid--pin-id :is(th, td):first-child::after { content: ""; display: block; inline-size: calc(var(--uix-selcol-w) - var(--uix-space-3)); }
19
+ .uix-data-workspace__grid--pin-id thead th:nth-child(2) { z-index: var(--uix-z-sticky); background: var(--uix-surface); }
20
+ .uix-data-workspace__grid--pin-id tbody tr td:nth-child(2) { background: var(--uix-row); transition: background-color var(--uix-dur-fast) var(--uix-ease-out); }
21
+ .uix-data-workspace__grid--pin-id:not(.uix-table--no-zebra) tbody tr:nth-child(even) td:nth-child(2) { background: var(--uix-row-alt); }
22
+ .uix-data-workspace__grid--pin-id tbody tr[data-pinned] td:nth-child(2) { background: linear-gradient(var(--uix-row-pinned-bg), var(--uix-row-pinned-bg)) var(--uix-surface); }
23
+ .uix-data-workspace__grid--pin-id tbody tr[data-selected] td:nth-child(2) { background: linear-gradient(var(--uix-row-selected-bg), var(--uix-row-selected-bg)) var(--uix-surface); }
24
+ .uix-data-workspace__grid--pin-id tbody tr:hover td:nth-child(2) { background: var(--uix-bg-hover); }
25
+ .uix-table-wrap[data-overflow="false"] .uix-data-workspace__grid--pin-id :is(th, td):nth-child(2) { box-shadow: none; }
26
+ @media (prefers-reduced-motion: reduce) { .uix-data-workspace__grid--pin-id tbody tr td:nth-child(2) { transition: none; } }
27
+ /* the record-id cell: the id opens a peek, ↗ opens the full record */
28
+ .uix-data-workspace__id { display: inline-flex; align-items: center; gap: var(--uix-space-1); white-space: nowrap; }
29
+ .uix-data-workspace__open { display: inline-grid; place-items: center; width: 24px; height: 24px; border-radius: var(--uix-radius-xs); color: var(--uix-text-muted); }
30
+ .uix-data-workspace__open:focus-visible { outline: 2px solid var(--uix-ring); outline-offset: 1px; }
31
+ @media (hover: hover) and (pointer: fine) { .uix-data-workspace__open:hover { background: var(--uix-bg-hover); color: var(--uix-text); } }
32
+ .uix-data-workspace__footer { display: flex; flex-wrap: wrap; align-items: center; gap: var(--uix-space-2) var(--uix-space-4); color: var(--uix-text-muted); font-size: var(--uix-text-meta); }
33
+ .uix-data-workspace__range { font-variant-numeric: tabular-nums; }
34
+ .uix-data-workspace__size { display: inline-flex; align-items: center; gap: var(--uix-space-2); white-space: nowrap; }
35
+ /* maximized: the workspace takes the window; Escape or the toggle restores it */
36
+ .uix-data-workspace--maximized {
37
+ position: fixed; inset: 0; z-index: var(--uix-z-overlay); align-content: start; overflow: auto;
38
+ padding: var(--uix-space-4); background: var(--uix-bg-app);
39
+ }
40
+ /* a phone: the search takes its own row */
41
+ @media (max-width: 40rem) { .uix-data-workspace__search { flex: 1 1 100%; } }
42
+ }
@@ -47,10 +47,23 @@
47
47
  /* --- DateTimePicker: the date field, a time input and the zone they are in ---------------- */
48
48
  .uix-date-time-picker { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--uix-space-2); min-width: 0; }
49
49
  .uix-date-time-picker > .uix-date-picker { flex: 1 1 8.5rem; }
50
- .uix-date-time-picker__time { flex: 0 1 auto; width: auto; min-width: 6.5rem; font-variant-numeric: tabular-nums; }
50
+ /* the time part is a TimePicker (HAR-1835): narrower than the date, never under a "12:30 PM" */
51
+ .uix-date-time-picker > .uix-date-time-picker__time { flex: 0 1 8.5rem; min-width: 7rem; }
51
52
  .uix-date-time-picker__zone { display: inline-flex; align-items: center; min-height: var(--uix-control-h); color: var(--uix-text-muted); font-size: var(--uix-text-meta); white-space: nowrap; }
52
53
  .uix-date-time-picker--sm .uix-date-time-picker__zone { min-height: 28px; }
53
54
 
55
+ /* --- TimePicker (HAR-1835): the date field's shape, a clock button and a list of times ------- */
56
+ .uix-time-picker { max-width: 9rem; }
57
+ .uix-time-picker__input { text-overflow: ellipsis; }
58
+ .uix-time-picker__popover { padding: var(--uix-space-1); }
59
+ .uix-time-picker__list {
60
+ min-width: 8rem; max-height: min(16rem, 50vh); overflow-y: auto; overscroll-behavior: contain;
61
+ padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none; font-variant-numeric: tabular-nums;
62
+ }
63
+ .uix-time-picker__list:focus { outline: none; }
64
+ /* the list has focus, its active time is the ring (aria-activedescendant) */
65
+ .uix-time-picker__list:focus-visible .uix-listbox__option[data-active] { outline: 2px solid var(--uix-ring); outline-offset: -2px; }
66
+
54
67
  /* --- DateRangePicker mode="field": a trigger that shows the range ------------------------- */
55
68
  .uix-date-range-field { display: grid; width: 100%; min-width: 0; max-width: 18rem; }
56
69
  .uix-date-range-field__trigger { display: flex; align-items: center; gap: var(--uix-space-2); min-width: 0; text-align: start; cursor: pointer; }
@@ -13,3 +13,12 @@
13
13
  .uix-detail-page__metrics dd { margin: var(--uix-space-1) 0 0; color: var(--uix-text); font-size: var(--uix-text-h3); font-weight: 600; }
14
14
  @media (max-width: 820px) { .uix-detail { grid-template-columns: 1fr; } }
15
15
  }
16
+
17
+ /* Staff preset (HAR-1815): the record page TENSOR and MOTUS share. */
18
+ @layer uix.components {
19
+ .uix-detail-page--staff .uix-page-header__title { font-size: var(--uix-text-h2); line-height: var(--uix-leading-h2); }
20
+ .uix-detail-page--staff .uix-detail-page__back { display: inline-flex; align-items: center; gap: var(--uix-space-1); color: var(--uix-text-hushed); text-decoration: none; }
21
+ @media (hover: hover) { .uix-detail-page--staff .uix-detail-page__back:hover { color: var(--uix-text); } }
22
+ .uix-detail-page--staff .uix-detail-page__metrics dt { font-size: var(--uix-text-eyebrow); line-height: var(--uix-leading-eyebrow); letter-spacing: var(--uix-tracking-eyebrow); text-transform: uppercase; font-weight: 600; color: var(--uix-text-muted); }
23
+ .uix-detail-page--staff .uix-detail-page__metrics dd { font-size: var(--uix-text-h3); line-height: var(--uix-leading-h3); font-weight: 400; font-variant-numeric: tabular-nums; }
24
+ }