@tensor_1/tokens 2.48.0 → 2.49.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.
@@ -37,14 +37,19 @@
37
37
  .uix-th__sortbtn::before { content: ""; position: absolute; inset: 0; }
38
38
 
39
39
  .uix-table td { padding: 0 var(--uix-space-4); height: var(--uix-row-h); border-bottom: 1px solid var(--uix-border); color: var(--uix-text); vertical-align: middle; }
40
- .uix-table tbody tr { background: var(--uix-row); transition: background-color var(--uix-dur-fast) var(--uix-ease-out); }
41
- .uix-table tbody tr:nth-child(even) { background: var(--uix-row-alt); }
42
- .uix-table--no-zebra tbody tr, .uix-table--no-zebra tbody tr:nth-child(even) { background: var(--uix-row); }
43
- .uix-table tbody tr[data-pinned] { background: var(--uix-row-pinned-bg); }
44
- /* [data-selected] carries row-selection styling; aria-selected is invalid on a native
40
+ /* The row's current state is one value, --uix-row-bg: the row paints it, and a frozen cell paints
41
+ it too (below), so a frozen cell can never keep the zebra stripe on a selected row (operator
42
+ review 2026-10-23). Every state rule has the same specificity (:where), so the order decides:
43
+ hover > selected > pinned > zebra > plain. --uix-row-bg is a registered colour (@property at
44
+ the end of this file), so the row eases the value itself and its frozen cells ease with it.
45
+ [data-selected] carries row-selection styling; aria-selected is invalid on a native
45
46
  table row — AT state comes from the row checkbox (UIX-A11Y-2). */
46
- .uix-table tbody tr[data-selected] { background: var(--uix-row-selected-bg); }
47
- .uix-table tbody tr:hover { background: var(--uix-bg-hover); }
47
+ .uix-table tbody tr { --uix-row-bg: var(--uix-row); background: var(--uix-row-bg); transition: --uix-row-bg var(--uix-dur-fast) var(--uix-ease-out); }
48
+ .uix-table tbody tr:where(:nth-child(even)) { --uix-row-bg: var(--uix-row-alt); }
49
+ .uix-table--no-zebra tbody tr:where(:nth-child(even)) { --uix-row-bg: var(--uix-row); }
50
+ .uix-table tbody tr:where([data-pinned]) { --uix-row-bg: var(--uix-row-pinned-bg); }
51
+ .uix-table tbody tr:where([data-selected]) { --uix-row-bg: var(--uix-row-selected-bg); }
52
+ @media (hover: hover) { .uix-table tbody tr:where(:hover) { --uix-row-bg: var(--uix-bg-hover); } }
48
53
  /* Row links stay calm on hover/focus: the row tint is the affordance, so neither the base
49
54
  layer's `a:hover` underline nor link.css reaches them. `:not(.uix-btn)` keeps buttons
50
55
  styled; the global :focus-visible ring still marks keyboard focus. */
@@ -76,21 +81,17 @@
76
81
  sideways overflow (data-overflow="false") draws none. Without the measurement they stay. */
77
82
  .uix-table-wrap[data-overflow="false"] :is(.uix-table--pinned-col :is(th, td):first-child, .uix-table--pinned-actions :is(th, td):last-child) { box-shadow: none; }
78
83
  /* A pinned cell paints its row's state itself and must be opaque, or the cells scrolling
79
- under it show through. The dark pinned/selected tints are translucent, so they are layered
80
- over the surface the row sits on rather than painted bare. */
81
- .uix-table--pinned-col tbody tr td:first-child, .uix-table--pinned-actions tbody tr td:last-child { background: var(--uix-row); transition: background-color var(--uix-dur-fast) var(--uix-ease-out); }
82
- .uix-table--pinned-col tbody tr:nth-child(even) td:first-child, .uix-table--pinned-actions tbody tr:nth-child(even) td:last-child { background: var(--uix-row-alt); }
83
- /* same specificity as the even-row rule above and the state rules below, so the order decides:
84
- an :nth-child(even) here outranked hover, pinned and selected on every even row */
85
- .uix-table--pinned-col.uix-table--no-zebra tbody tr td:first-child,
86
- .uix-table--pinned-actions.uix-table--no-zebra tbody tr td:last-child { background: var(--uix-row); }
87
- .uix-table--pinned-col tbody tr[data-pinned] td:first-child, .uix-table--pinned-actions tbody tr[data-pinned] td:last-child { background: linear-gradient(var(--uix-row-pinned-bg), var(--uix-row-pinned-bg)) var(--uix-surface); }
88
- .uix-table--pinned-col tbody tr[data-selected] td:first-child, .uix-table--pinned-actions tbody tr[data-selected] td:last-child { background: linear-gradient(var(--uix-row-selected-bg), var(--uix-row-selected-bg)) var(--uix-surface); }
89
- .uix-table--pinned-col tbody tr:hover td:first-child, .uix-table--pinned-actions tbody tr:hover td:last-child { background: var(--uix-bg-hover); }
84
+ under it show through. The dark tints are translucent, so the row's --uix-row-bg is layered
85
+ over the surface rather than painted bare. */
86
+ .uix-table--pinned-col tbody tr td:first-child, .uix-table--pinned-actions tbody tr td:last-child { background: linear-gradient(var(--uix-row-bg), var(--uix-row-bg)), var(--uix-surface); }
90
87
  /* an expanded row's full-width detail cell is both first and last child: keep its own tint */
91
88
  .uix-table--pinned-col tbody tr.uix-table__detail > td, .uix-table--pinned-actions tbody tr.uix-table__detail > td { background: var(--uix-bg-subtle); }
92
89
  @media (prefers-reduced-motion: reduce) {
93
- .uix-table tbody tr, .uix-table--pinned-col tbody tr td:first-child, .uix-table--pinned-actions tbody tr td:last-child { transition: none; }
90
+ .uix-table tbody tr { transition: none; }
91
+ }
92
+ /* forced colours keep a gradient image: a frozen cell is plain Canvas there, like its row */
93
+ @media (forced-colors: active) {
94
+ .uix-table--pinned-col tbody tr td:first-child, .uix-table--pinned-actions tbody tr td:last-child { background: Canvas; }
94
95
  }
95
96
 
96
97
  /* ── column sizing + cell behaviour (a shared vocabulary, not per-app px) ──────── */
@@ -142,19 +143,21 @@
142
143
  .uix-table th.uix-table__selcol, .uix-table td.uix-table__selcell { width: var(--uix-selcol-w); padding-left: var(--uix-space-3); padding-right: 0; text-align: center; }
143
144
 
144
145
  /* contextual bulk-action bar — appears when >=1 row is selected */
146
+ /* It wraps: on a phone the actions take a second row rather than run off the bar's edge, and
147
+ the count stays on one line (operator review 2026-10-23). */
145
148
  .uix-bulkbar {
146
- display: flex; align-items: center; gap: var(--uix-space-3);
149
+ display: flex; flex-wrap: wrap; align-items: center; gap: var(--uix-space-2) var(--uix-space-3); min-width: 0;
147
150
  padding: var(--uix-space-2) var(--uix-space-3); margin-bottom: var(--uix-space-3);
148
151
  border: 1px solid var(--uix-accent); border-radius: var(--uix-radius-md);
149
152
  background: var(--uix-brand-muted); color: var(--uix-text);
150
- animation: uix-bulkbar-in var(--uix-dur) var(--uix-ease-out);
153
+ /* enters from @starting-style on a transition (interruptible), not a keyframe (HAR-2578) */
154
+ transition: opacity var(--uix-dur) var(--uix-ease-out), transform var(--uix-dur) var(--uix-ease-out);
151
155
  }
156
+ @starting-style { .uix-bulkbar:not([hidden]) { opacity: 0; transform: translateY(calc(-1 * var(--uix-lift))); } }
152
157
  .uix-bulkbar[hidden] { display: none; }
153
158
  /* --uix-accent-text, not --uix-accent: the plain accent on the accent tint is 3.4:1 in dark (HAR-1388 specimen) */
154
- .uix-bulkbar__count { font-size: var(--uix-text-meta); font-weight: 600; color: var(--uix-accent-text); font-variant-numeric: tabular-nums; }
159
+ .uix-bulkbar__count { font-size: var(--uix-text-meta); font-weight: 600; color: var(--uix-accent-text); font-variant-numeric: tabular-nums; white-space: nowrap; }
155
160
  .uix-bulkbar__spacer { flex: 1; }
156
- @keyframes uix-bulkbar-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
157
- @media (prefers-reduced-motion: reduce) { .uix-bulkbar { animation: none; } }
158
161
 
159
162
  /* ── row hover actions + kebab ─────────────────────────────────────────────────── */
160
163
  .uix-table__actions { width: 84px; text-align: right; white-space: nowrap; }
@@ -227,3 +230,7 @@
227
230
  .uix-compare__head > div + div, .uix-compare__cell + .uix-compare__cell { border-left: 1px solid var(--uix-border); }
228
231
  .uix-compare__attr { grid-column: 1 / -1; background: var(--uix-bg-subtle); padding: var(--uix-space-1) var(--uix-space-3); border-top: 1px solid var(--uix-border); font-size: var(--uix-text-eyebrow); letter-spacing: var(--uix-tracking-eyebrow); text-transform: uppercase; color: var(--uix-text-muted); }
229
232
  }
233
+
234
+ /* The row's state colour (above) is registered so it can be eased: the row and its frozen cells,
235
+ which paint the same value, change together. Outside the layer: @property is global. */
236
+ @property --uix-row-bg { syntax: '<color>'; inherits: true; initial-value: transparent; }
@@ -68,5 +68,29 @@
68
68
  .uix-tabs-scroller__prev svg, .uix-tabs-scroller__next svg { width: var(--uix-icon-sm); height: var(--uix-icon-sm); }
69
69
  .uix-tabs-scroller__prev:dir(rtl) svg, .uix-tabs-scroller__next:dir(rtl) svg { transform: scaleX(-1); }
70
70
 
71
+ /* Sliding indicator (React Tabs, HAR-2589). At rest each tab shows its own mark. When a click
72
+ changes the tab, one element carries the line's underline or the pill's fill from the old
73
+ tab to the new one (transform only, about 160 ms); while it slides ([data-uix-indicator])
74
+ the new tab's own mark steps aside, then it hides again. Keyboard changes jump. */
75
+ /* the indicator is the last child: isolate + z-index -1 paint it above the row's own
76
+ background and beneath the tabs */
77
+ .uix-tabs { position: relative; isolation: isolate; }
78
+ /* clipped while it slides (a resize must not widen the page); a scrolling row contains it */
79
+ .uix-tabs[data-uix-indicator]:not(.uix-tabs--scroll) { overflow: clip; overflow-clip-margin: var(--uix-space-1); }
80
+ .uix-tabs__indicator { position: absolute; z-index: calc(-1 * var(--uix-z-raised)); pointer-events: none; transform-origin: 0 0; }
81
+ .uix-tabs__indicator[data-moving] { transition: transform var(--uix-dur) var(--uix-ease-in-out); }
82
+ .uix-tabs--line > .uix-tabs__indicator::after {
83
+ content: ""; position: absolute; left: 8px; right: 8px; bottom: -1px; height: 2px;
84
+ background: var(--uix-accent); border-radius: var(--uix-radius-pill);
85
+ }
86
+ .uix-tabs--scroll.uix-tabs--line > .uix-tabs__indicator::after { bottom: 0; }
87
+ .uix-tabs--pill > .uix-tabs__indicator { background: var(--uix-surface); box-shadow: var(--uix-shadow-sm); border-radius: calc(var(--uix-radius-md) - 3px); }
88
+ @media (forced-colors: none) {
89
+ .uix-tabs--line[data-uix-indicator] .uix-tab::after { visibility: hidden; }
90
+ .uix-tabs--pill[data-uix-indicator] .uix-tab[aria-selected="true"] { background: transparent; box-shadow: none; }
91
+ }
92
+ /* forced colours: the tabs' own marks are clearer than a painted box */
93
+ @media (forced-colors: active) { .uix-tabs__indicator { display: none; } }
94
+
71
95
  @media (prefers-reduced-motion: reduce) { .uix-tabs--scroll { scroll-behavior: auto; } }
72
96
  }
@@ -4,15 +4,18 @@
4
4
  .uix-toast {
5
5
  display: flex; gap: var(--uix-space-3); align-items: flex-start; padding: var(--uix-space-3) var(--uix-space-4); border-radius: var(--uix-radius-md);
6
6
  background: var(--uix-surface-2); border: 1px solid var(--uix-border); box-shadow: var(--uix-shadow-popover);
7
- animation: uix-slide-in-right var(--uix-dur-slow) var(--uix-ease-out);
7
+ /* Transitions, not keyframes, so a toast that is updated or re-shown mid-exit turns around
8
+ instead of restarting (HAR-2578). Enters from the toaster's edge at --uix-dur-slow; leaves the
9
+ same way at --uix-dur-exit (faster, still ease-out). `translate` is the swipe offset the React
10
+ Toast writes while a finger drags it; the Toaster glides the neighbours when one leaves. */
11
+ transition: opacity var(--uix-dur-slow) var(--uix-ease-out), transform var(--uix-dur-slow) var(--uix-ease-out),
12
+ translate var(--uix-dur) var(--uix-ease-out);
13
+ touch-action: pan-y;
8
14
  }
9
- .uix-toast[data-leaving] { animation: uix-toast-out var(--uix-dur-exit) var(--uix-ease-out) forwards; }
10
- @media (prefers-reduced-motion: reduce) {
11
- .uix-toast { animation: uix-fade-in var(--uix-dur) var(--uix-ease-out) !important; }
12
- .uix-toast[data-leaving] { animation: uix-toast-fade-out var(--uix-dur-exit) var(--uix-ease-out) forwards !important; }
13
- }
14
- @keyframes uix-toast-fade-out { to { opacity: 0; } }
15
- @keyframes uix-toast-out { to { opacity: 0; transform: translateX(16px); } }
15
+ @starting-style { .uix-toast { opacity: 0; transform: var(--uix-toast-from, translateX(var(--uix-space-4))); } }
16
+ .uix-toast[data-leaving] { opacity: 0; transform: var(--uix-toast-from, translateX(var(--uix-space-4))); pointer-events: none; transition-duration: var(--uix-dur-exit); }
17
+ /* while dragged the toast follows the finger; on release it snaps back or flies out */
18
+ .uix-toast[data-swiping] { transition: none; user-select: none; }
16
19
  .uix-toast__icon { flex: none; margin-top: 1px; }
17
20
  .uix-toast__icon svg { width: var(--uix-icon-md); height: var(--uix-icon-md); }
18
21
  .uix-toast__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--uix-space-1); }
@@ -40,15 +43,15 @@
40
43
  .uix-toast__action:focus-visible, .uix-toast__close:focus-visible { outline: 2px solid var(--uix-ring); outline-offset: 1px; }
41
44
  .uix-toast__spinner { width: var(--uix-icon-md); height: var(--uix-icon-md); }
42
45
  .uix-toaster[data-position^="top"] { top: var(--uix-space-4); bottom: auto; }
46
+ /* a toast enters from, and leaves toward, the toaster's own edge */
47
+ .uix-toaster[data-position$="start"] { --uix-toast-from: translateX(calc(-1 * var(--uix-space-4))); }
48
+ .uix-toaster[data-position="bottom-center"] { --uix-toast-from: translateY(var(--uix-space-4)); }
49
+ .uix-toaster[data-position="top-center"] { --uix-toast-from: translateY(calc(-1 * var(--uix-space-4))); }
43
50
  .uix-toaster[data-position$="start"] { left: var(--uix-space-4); right: auto; }
44
51
  .uix-toaster[data-position$="center"] { left: 50%; right: auto; transform: translateX(-50%); }
45
52
  }
46
53
 
47
- /* the in-app reduced-motion preference (HAR-1372, <html data-motion="reduce">): the same fades */
48
- @layer uix.components {
49
- @media (prefers-reduced-motion: no-preference) {
50
- :root[data-motion="reduce"] .uix-toast { animation: uix-fade-in var(--uix-dur) var(--uix-ease-out) !important; }
51
- :root[data-motion="reduce"] .uix-toast[data-leaving] { animation: uix-toast-fade-out var(--uix-dur-exit) var(--uix-ease-out) forwards !important; }
52
- }
53
- }
54
+ /* The in-app reduced-motion preference (HAR-1372, <html data-motion="reduce">) needs no rule here: toasts
55
+ move on transitions (HAR-2578), which the motion.css policy narrows to fades for both the OS setting
56
+ and data-motion="reduce". */
54
57
 
@@ -15,12 +15,16 @@
15
15
  padding: var(--uix-space-2) var(--uix-space-3); border-radius: var(--uix-radius-sm);
16
16
  /* no pointer-events:none — the bubble must be hoverable (WCAG 1.4.13, UIX-A11Y-1) */
17
17
  box-shadow: var(--uix-shadow-popover); white-space: normal;
18
- opacity: 0; transition: opacity var(--uix-dur-fast) var(--uix-ease-out);
18
+ /* grows out of the trigger (--uix-anchor-origin); the closed state carries the faster exit. The
19
+ React Tooltip waits --uix-delay-tooltip before it opens on hover, and a neighbour hovered within
20
+ --uix-delay-tooltip-skip of one closing opens at once with [data-uix-instant] (no fade). */
21
+ opacity: 0; transform: scale(.97); transform-origin: var(--uix-anchor-origin, bottom center);
22
+ transition: opacity var(--uix-dur-exit-fast) var(--uix-ease-out), transform var(--uix-dur-exit-fast) var(--uix-ease-out);
19
23
  }
20
24
  /* rich content (HAR-1349): a short list or a few lines, never interactive */
21
25
  .uix-tooltip__bubble :is(ul, ol) { margin: 0; padding-left: 1.1em; }
22
26
  .uix-tooltip__bubble :is(p, ul, ol) + :is(p, ul, ol) { margin-top: var(--uix-space-1); }
23
- .uix-tooltip__bubble:popover-open { opacity: 1; }
27
+ .uix-tooltip__bubble:popover-open { opacity: 1; transform: none; transition-duration: var(--uix-dur-fast); }
24
28
  /* A shortcut in the bubble (HAR-2031: Tooltip `shortcut`, Button `shortcut`): after the text,
25
29
  the keys drawn as outlines in the bubble's own colour, so they keep its contrast in both
26
30
  themes. A Button's hint bubble is one line: the name of an icon button, then the keys. */
@@ -28,7 +32,7 @@
28
32
  .uix-tooltip__shortcut:first-child { margin-inline-start: 0; }
29
33
  .uix-tooltip__shortcut .uix-kbd { color: inherit; background: transparent; border-color: color-mix(in srgb, currentColor 45%, transparent); }
30
34
  .uix-tooltip__bubble--hint { white-space: nowrap; }
31
- @starting-style { .uix-tooltip__bubble:popover-open { opacity: 0; } }
35
+ @starting-style { .uix-tooltip__bubble:popover-open { opacity: 0; transform: scale(.97); } }
32
36
 
33
37
  /* legacy CSS-only tooltip — clipped by overflow ancestors; superseded by .uix-tooltip__bubble */
34
38
  [data-uix-tip] { position: relative; }
package/styles/motion.css CHANGED
@@ -69,8 +69,9 @@
69
69
  .uix-lift-hover:hover { transform: translateY(calc(-1 * var(--uix-lift))); box-shadow: var(--uix-shadow-popover); }
70
70
  }
71
71
 
72
- /* Instant surfaces. Keyboard-initiated and 100×-a-day surfaces (the command palette, an overlay opened
73
- from the keyboard) open and close with no animation — the frequency budget is "none". Overlays set
74
- [data-uix-instant] themselves (Modal `instant`, CommandPalette, keyboard-opened menus and selects). */
75
- [data-uix-instant], [data-uix-instant]::backdrop { transition: none !important; animation: none !important; }
72
+ /* Instant surfaces. Keyboard-initiated and 100×-a-day surfaces open and close with no animation — the
73
+ frequency budget is "none". [data-uix-instant] is a standing choice (Modal `instant`, CommandPalette,
74
+ a tooltip opened inside its delay group); [data-uix-key-opened] is set per open or close by the React
75
+ overlays (Modal, Drawer, Peek, Popover and Menu, Select) when a key press led to it. */
76
+ :is([data-uix-instant], [data-uix-key-opened]), :is([data-uix-instant], [data-uix-key-opened])::backdrop { transition: none !important; animation: none !important; }
76
77
  }
@@ -21,6 +21,12 @@
21
21
  "dur-exit-slow": { "$type": "duration", "$value": "160ms", "$description": "Exit paired with --uix-dur-slow (modal, drawer, side peek)." },
22
22
  "dur-highlight": { "$type": "duration", "$value": "2400ms", "$description": "How long an arrival or change tint takes to fade back to the row colour. Not a transition budget: the tint is a held signal, not movement." },
23
23
 
24
+ "dur-change": { "$type": "duration", "$value": "1200ms", "$description": "How long a change tint (ChangeHighlight, .uix-changed) takes to fade on the row or field that changed. Shorter than --uix-dur-highlight: a change in place, not a landing." },
25
+
26
+ "delay-pending": { "$type": "duration", "$value": "300ms", "$description": "How long a pending action waits before it shows a pending indicator: a spinner that would flash for less is noise." },
27
+ "hold-pending": { "$type": "duration", "$value": "500ms", "$description": "Once a pending indicator shows, the least time it stays, so it never flickers." },
28
+ "hold-result": { "$type": "duration", "$value": "1500ms", "$description": "How long an in-place result (a button's success or error state, a copied check) holds before it returns to rest." },
29
+
24
30
  "delay-tooltip": { "$type": "duration", "$value": "400ms", "$description": "Hover delay before a tooltip opens. Keyboard focus opens it at once." },
25
31
  "delay-tooltip-skip": { "$type": "duration", "$value": "300ms", "$description": "Delay-group window: a neighbouring tooltip hovered within this long after one closed opens at once." },
26
32