@xenosystem/elements-react 0.0.3 → 0.0.5

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 (51) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +41 -41
  3. package/dist/containers/Chip.js +1 -1
  4. package/dist/containers/Panel.js +1 -1
  5. package/package.json +58 -58
  6. package/src/chrome-separated.css +132 -132
  7. package/src/chrome-unified.css +72 -72
  8. package/src/containers/Badges.css +116 -116
  9. package/src/containers/avatar.css +70 -70
  10. package/src/containers/card.css +75 -75
  11. package/src/containers/chip.css +103 -103
  12. package/src/containers/list-row.css +132 -132
  13. package/src/containers/message-bubble.css +112 -112
  14. package/src/containers/panel.css +125 -120
  15. package/src/containers/table.css +102 -102
  16. package/src/containers/tile.css +17 -17
  17. package/src/content/callout.css +77 -77
  18. package/src/content/caret.css +30 -30
  19. package/src/content/code-block.css +159 -159
  20. package/src/content/collapsible.css +80 -80
  21. package/src/content/inline-code.css +14 -14
  22. package/src/content/model-picker.css +219 -219
  23. package/src/content/source-card.css +99 -99
  24. package/src/content/sources-disclosure.css +118 -118
  25. package/src/controls.css +453 -453
  26. package/src/fonts.css +34 -34
  27. package/src/forms/Checkbox.css +114 -114
  28. package/src/forms/RadioGroup.css +115 -115
  29. package/src/goo.css +97 -97
  30. package/src/icon-motion.css +1909 -1909
  31. package/src/layout/ResizablePanel.css +79 -79
  32. package/src/nav/sidebar.css +296 -296
  33. package/src/overlays/date-time-picker.css +238 -238
  34. package/src/overlays/menu.css +195 -195
  35. package/src/overlays/modal.css +167 -167
  36. package/src/overlays/picker-field.css +81 -81
  37. package/src/overlays/pill-filter.css +129 -129
  38. package/src/overlays/reveal.css +56 -56
  39. package/src/overlays/segmented-control.css +185 -185
  40. package/src/overlays/tabs.css +143 -143
  41. package/src/overlays/tooltip.css +126 -126
  42. package/src/scrollbar.css +184 -184
  43. package/src/size.css +179 -179
  44. package/src/status/progress-bar.css +88 -88
  45. package/src/status/spinner.css +33 -33
  46. package/src/status/statusindicator.css +65 -65
  47. package/src/status/steptimeline.css +137 -137
  48. package/src/status/thinkingcube.css +75 -75
  49. package/src/xeno-element.css +60 -60
  50. package/src/xeno-elements.css +71 -71
  51. package/src/xeno-theme.css +112 -99
package/src/size.css CHANGED
@@ -1,179 +1,179 @@
1
- /*
2
- * Control size metrics, as CSS.
3
- *
4
- * These four numbers used to be written onto every control as an INLINE style, computed in JS from
5
- * `controlSize` at render time. That worked, but it put the box metrics in the one place nothing can
6
- * override: an inline style beats every stylesheet rule there is. Which means a surface — a product
7
- * rendering the same elements for touch instead of a mouse — had no seam to reach them through.
8
- *
9
- * Here they are ambient instead. A control emits `data-xeno-size="md"` and reads the four variables;
10
- * a surface redefines the variables for the sizes it wants bigger, once, at the root:
11
- *
12
- * .xeno[data-surface='mobile'] [data-xeno-size='md'] { --xeno-h: 44px; }
13
- *
14
- * Nothing at the call site changes, no context is threaded through the tree, and a product that sets
15
- * no surface keeps exactly what it had.
16
- *
17
- * The attribute is namespaced (`data-xeno-size`, not `data-size`) for two reasons: it cannot collide
18
- * with a host application's own `data-size`, and it therefore does not need to be scoped under
19
- * `.xeno` — a control rendered outside the theme root still gets its metrics.
20
- *
21
- * DRIFT: the numbers below mirror `controlSize` in `@xenosystem/elements/tokens`, which stays the
22
- * source of truth. `size.test.ts` fails if the two ever disagree.
23
- *
24
- * NOT here: glyph px. A `<XenoElement>` takes its size as a number prop, so an icon's scale is still
25
- * resolved in JS (`iconPx`). Making icons follow a surface too is a separate change.
26
- */
27
-
28
- [data-xeno-size='xs'] {
29
- --xeno-h: 24px;
30
- --xeno-padx: 8px;
31
- --xeno-gap: 5px;
32
- --xeno-font: 12px;
33
- --xeno-icon: 15px;
34
- }
35
- [data-xeno-size='sm'] {
36
- --xeno-h: 28px;
37
- --xeno-padx: 10px;
38
- --xeno-gap: 6px;
39
- --xeno-font: 13px;
40
- --xeno-icon: 16px;
41
- }
42
- [data-xeno-size='md'] {
43
- --xeno-h: 32px;
44
- --xeno-padx: 12px;
45
- --xeno-gap: 6px;
46
- --xeno-font: 14px;
47
- --xeno-icon: 16px;
48
- }
49
- [data-xeno-size='lg'] {
50
- --xeno-h: 36px;
51
- --xeno-padx: 14px;
52
- --xeno-gap: 7px;
53
- --xeno-font: 14px;
54
- --xeno-icon: 18px;
55
- }
56
-
57
- /* ── The touch surface ────────────────────────────────────────────────────────────────
58
- * One block, declared by the product at its root:
59
- *
60
- * <div className="xeno" data-surface="mobile">
61
- *
62
- * `font` at md is 16px and that is not a taste call — iOS Safari ZOOMS the page when a focused input
63
- * has a font under 16px, so a 14px text field makes the whole layout jump the instant it is tapped. */
64
- .xeno[data-surface='mobile'] [data-xeno-size='xs'] {
65
- --xeno-h: 28px;
66
- --xeno-padx: 10px;
67
- --xeno-gap: 6px;
68
- --xeno-font: 13px;
69
- --xeno-icon: 16px;
70
- }
71
- .xeno[data-surface='mobile'] [data-xeno-size='sm'] {
72
- --xeno-h: 32px;
73
- --xeno-padx: 12px;
74
- --xeno-gap: 7px;
75
- --xeno-font: 14px;
76
- --xeno-icon: 17px;
77
- }
78
- .xeno[data-surface='mobile'] [data-xeno-size='md'] {
79
- --xeno-h: 40px;
80
- --xeno-padx: 16px;
81
- --xeno-gap: 8px;
82
- --xeno-font: 16px;
83
- --xeno-icon: 20px;
84
- }
85
- .xeno[data-surface='mobile'] [data-xeno-size='lg'] {
86
- --xeno-h: 48px;
87
- --xeno-padx: 18px;
88
- --xeno-gap: 9px;
89
- --xeno-font: 16px;
90
- --xeno-icon: 22px;
91
- }
92
-
93
- /* ── Glyphs follow the surface ────────────────────────────────────────────────────────
94
- * A 16px glyph inside a 40px button is not the same design as a 16px glyph inside a 32px one — the
95
- * ratio is what reads, and it has to hold. Desktop md is 32/16; touch md is 40/20. Same 50%.
96
- *
97
- * The renderer writes `width`/`height` as ATTRIBUTES on the svg, and a CSS rule outranks a
98
- * presentation attribute — so this needs no renderer change, no new prop and no context, and the
99
- * attributes stay put for the "renders correctly with no stylesheet" invariant.
100
- *
101
- * Only under a surface: with no surface declared the attribute already carries the right number, and
102
- * a rule competing with it by default would be pure risk for no gain.
103
- *
104
- * Each owner is named rather than reaching every descendant, and that is the whole point — a blanket
105
- * rule would also flatten the glyphs that are deliberately SMALLER than the control's own. The chip's
106
- * remove × and the model picker's are `glyph - 2` by decision, and they keep their attribute. */
107
- .xeno[data-surface='mobile'] .xeno-btn > .xeno-element,
108
- .xeno[data-surface='mobile'] .xeno-input-icon > .xeno-element,
109
- .xeno[data-surface='mobile'] .xeno-tab > .xeno-element,
110
- .xeno[data-surface='mobile'] .xeno-chip-body > .xeno-element {
111
- width: var(--xeno-icon);
112
- height: var(--xeno-icon);
113
- }
114
-
115
- /* ── The hit target ───────────────────────────────────────────────────────────────────
116
- * 44px square, Apple's HIG minimum and WCAG 2.1 AAA. It is the TAPPABLE area, not the visual one —
117
- * keeping those separate is what lets the interface stay dense under a finger instead of becoming a
118
- * column of slabs. Material does the same thing: 24dp of ink inside a 48dp target.
119
- *
120
- * It is gated on `pointer: coarse`, NOT on the surface, and the distinction matters. The surface is a
121
- * decision the PRODUCT makes about density; the hit target answers a fact about the DEVICE. A phone
122
- * opening the web platform has fingers whether or not anyone declared a surface, and it gets safe
123
- * targets either way.
124
- *
125
- * The pseudo-element only exists inside the query, so on a precise pointer there is no extra layer
126
- * over any control at all. `::after` on purpose: `::before` is already the goo fill on the toggle and
127
- * the filter pill. Rows that can hold their own actions (list rows, table rows) are left out — an
128
- * overlay across them would swallow clicks meant for a trailing button. */
129
- .xeno {
130
- --xeno-hit: 44px;
131
- }
132
-
133
- @media (pointer: coarse) {
134
- .xeno-btn,
135
- .xeno-icon-btn,
136
- .xeno-chip,
137
- .xeno-pill,
138
- .xeno-tab,
139
- .xeno-segmented-option,
140
- .xeno-switch,
141
- .xeno-checkbox,
142
- .xeno-radio,
143
- .xeno-sidebar-item {
144
- position: relative;
145
- }
146
- .xeno-btn::after,
147
- .xeno-icon-btn::after,
148
- .xeno-chip::after,
149
- .xeno-pill::after,
150
- .xeno-tab::after,
151
- .xeno-segmented-option::after,
152
- .xeno-switch::after,
153
- .xeno-checkbox::after,
154
- .xeno-radio::after,
155
- .xeno-sidebar-item::after {
156
- content: '';
157
- position: absolute;
158
- left: 50%;
159
- top: 50%;
160
- translate: -50% -50%;
161
- width: 100%;
162
- height: 100%;
163
- min-width: var(--xeno-hit, 44px);
164
- min-height: var(--xeno-hit, 44px);
165
- /* Invisible, and it must never paint over the control it is extending. */
166
- background: none;
167
- border-radius: inherit;
168
- }
169
-
170
- /*
171
- * Text fields are the exception, and they have to be: an overlay across a field would take the tap
172
- * that was meant to focus it — a pseudo-element belongs to the wrapper, and clicking a div does not
173
- * put a caret in the input inside it. A field cannot fake its target, so it earns one for real.
174
- */
175
- .xeno-input,
176
- .xeno-textarea {
177
- min-height: var(--xeno-hit, 44px);
178
- }
179
- }
1
+ /*
2
+ * Control size metrics, as CSS.
3
+ *
4
+ * These four numbers used to be written onto every control as an INLINE style, computed in JS from
5
+ * `controlSize` at render time. That worked, but it put the box metrics in the one place nothing can
6
+ * override: an inline style beats every stylesheet rule there is. Which means a surface — a product
7
+ * rendering the same elements for touch instead of a mouse — had no seam to reach them through.
8
+ *
9
+ * Here they are ambient instead. A control emits `data-xeno-size="md"` and reads the four variables;
10
+ * a surface redefines the variables for the sizes it wants bigger, once, at the root:
11
+ *
12
+ * .xeno[data-surface='mobile'] [data-xeno-size='md'] { --xeno-h: 44px; }
13
+ *
14
+ * Nothing at the call site changes, no context is threaded through the tree, and a product that sets
15
+ * no surface keeps exactly what it had.
16
+ *
17
+ * The attribute is namespaced (`data-xeno-size`, not `data-size`) for two reasons: it cannot collide
18
+ * with a host application's own `data-size`, and it therefore does not need to be scoped under
19
+ * `.xeno` — a control rendered outside the theme root still gets its metrics.
20
+ *
21
+ * DRIFT: the numbers below mirror `controlSize` in `@xenosystem/elements/tokens`, which stays the
22
+ * source of truth. `size.test.ts` fails if the two ever disagree.
23
+ *
24
+ * NOT here: glyph px. A `<XenoElement>` takes its size as a number prop, so an icon's scale is still
25
+ * resolved in JS (`iconPx`). Making icons follow a surface too is a separate change.
26
+ */
27
+
28
+ [data-xeno-size='xs'] {
29
+ --xeno-h: 24px;
30
+ --xeno-padx: 8px;
31
+ --xeno-gap: 5px;
32
+ --xeno-font: 12px;
33
+ --xeno-icon: 15px;
34
+ }
35
+ [data-xeno-size='sm'] {
36
+ --xeno-h: 28px;
37
+ --xeno-padx: 10px;
38
+ --xeno-gap: 6px;
39
+ --xeno-font: 13px;
40
+ --xeno-icon: 16px;
41
+ }
42
+ [data-xeno-size='md'] {
43
+ --xeno-h: 32px;
44
+ --xeno-padx: 12px;
45
+ --xeno-gap: 6px;
46
+ --xeno-font: 14px;
47
+ --xeno-icon: 16px;
48
+ }
49
+ [data-xeno-size='lg'] {
50
+ --xeno-h: 36px;
51
+ --xeno-padx: 14px;
52
+ --xeno-gap: 7px;
53
+ --xeno-font: 14px;
54
+ --xeno-icon: 18px;
55
+ }
56
+
57
+ /* ── The touch surface ────────────────────────────────────────────────────────────────
58
+ * One block, declared by the product at its root:
59
+ *
60
+ * <div className="xeno" data-surface="mobile">
61
+ *
62
+ * `font` at md is 16px and that is not a taste call — iOS Safari ZOOMS the page when a focused input
63
+ * has a font under 16px, so a 14px text field makes the whole layout jump the instant it is tapped. */
64
+ .xeno[data-surface='mobile'] [data-xeno-size='xs'] {
65
+ --xeno-h: 28px;
66
+ --xeno-padx: 10px;
67
+ --xeno-gap: 6px;
68
+ --xeno-font: 13px;
69
+ --xeno-icon: 16px;
70
+ }
71
+ .xeno[data-surface='mobile'] [data-xeno-size='sm'] {
72
+ --xeno-h: 32px;
73
+ --xeno-padx: 12px;
74
+ --xeno-gap: 7px;
75
+ --xeno-font: 14px;
76
+ --xeno-icon: 17px;
77
+ }
78
+ .xeno[data-surface='mobile'] [data-xeno-size='md'] {
79
+ --xeno-h: 40px;
80
+ --xeno-padx: 16px;
81
+ --xeno-gap: 8px;
82
+ --xeno-font: 16px;
83
+ --xeno-icon: 20px;
84
+ }
85
+ .xeno[data-surface='mobile'] [data-xeno-size='lg'] {
86
+ --xeno-h: 48px;
87
+ --xeno-padx: 18px;
88
+ --xeno-gap: 9px;
89
+ --xeno-font: 16px;
90
+ --xeno-icon: 22px;
91
+ }
92
+
93
+ /* ── Glyphs follow the surface ────────────────────────────────────────────────────────
94
+ * A 16px glyph inside a 40px button is not the same design as a 16px glyph inside a 32px one — the
95
+ * ratio is what reads, and it has to hold. Desktop md is 32/16; touch md is 40/20. Same 50%.
96
+ *
97
+ * The renderer writes `width`/`height` as ATTRIBUTES on the svg, and a CSS rule outranks a
98
+ * presentation attribute — so this needs no renderer change, no new prop and no context, and the
99
+ * attributes stay put for the "renders correctly with no stylesheet" invariant.
100
+ *
101
+ * Only under a surface: with no surface declared the attribute already carries the right number, and
102
+ * a rule competing with it by default would be pure risk for no gain.
103
+ *
104
+ * Each owner is named rather than reaching every descendant, and that is the whole point — a blanket
105
+ * rule would also flatten the glyphs that are deliberately SMALLER than the control's own. The chip's
106
+ * remove × and the model picker's are `glyph - 2` by decision, and they keep their attribute. */
107
+ .xeno[data-surface='mobile'] .xeno-btn > .xeno-element,
108
+ .xeno[data-surface='mobile'] .xeno-input-icon > .xeno-element,
109
+ .xeno[data-surface='mobile'] .xeno-tab > .xeno-element,
110
+ .xeno[data-surface='mobile'] .xeno-chip-body > .xeno-element {
111
+ width: var(--xeno-icon);
112
+ height: var(--xeno-icon);
113
+ }
114
+
115
+ /* ── The hit target ───────────────────────────────────────────────────────────────────
116
+ * 44px square, Apple's HIG minimum and WCAG 2.1 AAA. It is the TAPPABLE area, not the visual one —
117
+ * keeping those separate is what lets the interface stay dense under a finger instead of becoming a
118
+ * column of slabs. Material does the same thing: 24dp of ink inside a 48dp target.
119
+ *
120
+ * It is gated on `pointer: coarse`, NOT on the surface, and the distinction matters. The surface is a
121
+ * decision the PRODUCT makes about density; the hit target answers a fact about the DEVICE. A phone
122
+ * opening the web platform has fingers whether or not anyone declared a surface, and it gets safe
123
+ * targets either way.
124
+ *
125
+ * The pseudo-element only exists inside the query, so on a precise pointer there is no extra layer
126
+ * over any control at all. `::after` on purpose: `::before` is already the goo fill on the toggle and
127
+ * the filter pill. Rows that can hold their own actions (list rows, table rows) are left out — an
128
+ * overlay across them would swallow clicks meant for a trailing button. */
129
+ .xeno {
130
+ --xeno-hit: 44px;
131
+ }
132
+
133
+ @media (pointer: coarse) {
134
+ .xeno-btn,
135
+ .xeno-icon-btn,
136
+ .xeno-chip,
137
+ .xeno-pill,
138
+ .xeno-tab,
139
+ .xeno-segmented-option,
140
+ .xeno-switch,
141
+ .xeno-checkbox,
142
+ .xeno-radio,
143
+ .xeno-sidebar-item {
144
+ position: relative;
145
+ }
146
+ .xeno-btn::after,
147
+ .xeno-icon-btn::after,
148
+ .xeno-chip::after,
149
+ .xeno-pill::after,
150
+ .xeno-tab::after,
151
+ .xeno-segmented-option::after,
152
+ .xeno-switch::after,
153
+ .xeno-checkbox::after,
154
+ .xeno-radio::after,
155
+ .xeno-sidebar-item::after {
156
+ content: '';
157
+ position: absolute;
158
+ left: 50%;
159
+ top: 50%;
160
+ translate: -50% -50%;
161
+ width: 100%;
162
+ height: 100%;
163
+ min-width: var(--xeno-hit, 44px);
164
+ min-height: var(--xeno-hit, 44px);
165
+ /* Invisible, and it must never paint over the control it is extending. */
166
+ background: none;
167
+ border-radius: inherit;
168
+ }
169
+
170
+ /*
171
+ * Text fields are the exception, and they have to be: an overlay across a field would take the tap
172
+ * that was meant to focus it — a pseudo-element belongs to the wrapper, and clicking a div does not
173
+ * put a caret in the input inside it. A field cannot fake its target, so it earns one for real.
174
+ */
175
+ .xeno-input,
176
+ .xeno-textarea {
177
+ min-height: var(--xeno-hit, 44px);
178
+ }
179
+ }
@@ -1,88 +1,88 @@
1
- /*
2
- * ProgressBar — a determinate progress meter. A flat track (`--xeno-control`, `radius-sm`, ~4px tall)
3
- * holds a fill (`--xeno-text`) whose width is the clamped value (0..1) as a percentage. An optional
4
- * header row carries the label + a tabular percent readout; it is `aria-hidden` because the track
5
- * (`role="progressbar"`) already announces name + value. Rounded squares, monochrome shell, theme
6
- * tokens only — no literal colours.
7
- */
8
- .xeno-progressbar {
9
- display: flex;
10
- flex-direction: column;
11
- gap: 6px;
12
- width: 100%;
13
- }
14
-
15
- /* label + percent readout above the track */
16
- .xeno-progressbar-header {
17
- display: flex;
18
- align-items: baseline;
19
- justify-content: space-between;
20
- gap: 8px;
21
- font-family: inherit;
22
- font-size: 12px;
23
- line-height: 1;
24
- color: var(--xeno-muted);
25
- }
26
- .xeno-progressbar-value {
27
- font-variant-numeric: tabular-nums;
28
- color: var(--xeno-text);
29
- }
30
-
31
- /* The track IS the progressbar element.
32
- *
33
- * A radius only reads as a CORNER while it stays well under half the smaller dimension; at half it is
34
- * a semicircle and the bar is a capsule, whatever the token was called. `--xeno-radius-sm` (5px) on a
35
- * 4px bar was 125% of its own height — as round as a rounded shape can get.
36
- *
37
- * The fix is height, not a smaller number: 6px with `--xeno-radius-hair` puts it at 25%, the same
38
- * ratio as the scrollbar thumb, and the corner is actually visible at that size. Squeezing the radius
39
- * instead would have meant a sub-token literal for a bar too thin to show it anyway. */
40
- .xeno-progressbar-track {
41
- display: block;
42
- width: 100%;
43
- height: 6px;
44
- background: var(--xeno-control);
45
- border-radius: var(--xeno-radius-hair);
46
- overflow: hidden;
47
- }
48
-
49
- .xeno-progressbar-fill {
50
- display: block;
51
- height: 100%;
52
- min-width: 0;
53
- background: var(--xeno-text);
54
- border-radius: inherit;
55
- transition: width var(--xeno-dur) var(--xeno-ease);
56
- }
57
-
58
- /* ── Indeterminate ────────────────────────────────────────────────────────────────────
59
- * Work is happening; how much is left is not known. A short segment sweeps the track end to end,
60
- * because that is the one motion that says "running" without also claiming a position — a bar that
61
- * grows says something about progress whether or not it means to.
62
- *
63
- * The sweep is deliberately not symmetric: it enters at speed, slows across the middle where the eye
64
- * can read it, then leaves. `translate` on a fixed-width segment, so nothing is laid out per frame.
65
- * The track already clips (`overflow: hidden`), so the segment simply runs off both ends. */
66
- .xeno-progressbar-track[data-state='indeterminate'] .xeno-progressbar-fill {
67
- width: 34%;
68
- transition: none;
69
- animation: xeno-progress-sweep 1400ms var(--xeno-ease-stagger, cubic-bezier(0.22, 0.7, 0.2, 1))
70
- infinite;
71
- }
72
- @keyframes xeno-progress-sweep {
73
- from { translate: -110% 0; }
74
- to { translate: 330% 0; }
75
- }
76
-
77
- @media (prefers-reduced-motion: reduce) {
78
- .xeno-progressbar-fill {
79
- transition: none;
80
- }
81
- /* A perpetual sweep is exactly what reduced-motion is asking not to see. The bar still says "busy"
82
- — it just says it by sitting there at a partial width instead of running. */
83
- .xeno-progressbar-track[data-state='indeterminate'] .xeno-progressbar-fill {
84
- animation: none;
85
- width: 100%;
86
- opacity: 0.45;
87
- }
88
- }
1
+ /*
2
+ * ProgressBar — a determinate progress meter. A flat track (`--xeno-control`, `radius-sm`, ~4px tall)
3
+ * holds a fill (`--xeno-text`) whose width is the clamped value (0..1) as a percentage. An optional
4
+ * header row carries the label + a tabular percent readout; it is `aria-hidden` because the track
5
+ * (`role="progressbar"`) already announces name + value. Rounded squares, monochrome shell, theme
6
+ * tokens only — no literal colours.
7
+ */
8
+ .xeno-progressbar {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 6px;
12
+ width: 100%;
13
+ }
14
+
15
+ /* label + percent readout above the track */
16
+ .xeno-progressbar-header {
17
+ display: flex;
18
+ align-items: baseline;
19
+ justify-content: space-between;
20
+ gap: 8px;
21
+ font-family: inherit;
22
+ font-size: 12px;
23
+ line-height: 1;
24
+ color: var(--xeno-muted);
25
+ }
26
+ .xeno-progressbar-value {
27
+ font-variant-numeric: tabular-nums;
28
+ color: var(--xeno-text);
29
+ }
30
+
31
+ /* The track IS the progressbar element.
32
+ *
33
+ * A radius only reads as a CORNER while it stays well under half the smaller dimension; at half it is
34
+ * a semicircle and the bar is a capsule, whatever the token was called. `--xeno-radius-sm` (5px) on a
35
+ * 4px bar was 125% of its own height — as round as a rounded shape can get.
36
+ *
37
+ * The fix is height, not a smaller number: 6px with `--xeno-radius-hair` puts it at 25%, the same
38
+ * ratio as the scrollbar thumb, and the corner is actually visible at that size. Squeezing the radius
39
+ * instead would have meant a sub-token literal for a bar too thin to show it anyway. */
40
+ .xeno-progressbar-track {
41
+ display: block;
42
+ width: 100%;
43
+ height: 6px;
44
+ background: var(--xeno-control);
45
+ border-radius: var(--xeno-radius-hair);
46
+ overflow: hidden;
47
+ }
48
+
49
+ .xeno-progressbar-fill {
50
+ display: block;
51
+ height: 100%;
52
+ min-width: 0;
53
+ background: var(--xeno-text);
54
+ border-radius: inherit;
55
+ transition: width var(--xeno-dur) var(--xeno-ease);
56
+ }
57
+
58
+ /* ── Indeterminate ────────────────────────────────────────────────────────────────────
59
+ * Work is happening; how much is left is not known. A short segment sweeps the track end to end,
60
+ * because that is the one motion that says "running" without also claiming a position — a bar that
61
+ * grows says something about progress whether or not it means to.
62
+ *
63
+ * The sweep is deliberately not symmetric: it enters at speed, slows across the middle where the eye
64
+ * can read it, then leaves. `translate` on a fixed-width segment, so nothing is laid out per frame.
65
+ * The track already clips (`overflow: hidden`), so the segment simply runs off both ends. */
66
+ .xeno-progressbar-track[data-state='indeterminate'] .xeno-progressbar-fill {
67
+ width: 34%;
68
+ transition: none;
69
+ animation: xeno-progress-sweep 1400ms var(--xeno-ease-stagger, cubic-bezier(0.22, 0.7, 0.2, 1))
70
+ infinite;
71
+ }
72
+ @keyframes xeno-progress-sweep {
73
+ from { translate: -110% 0; }
74
+ to { translate: 330% 0; }
75
+ }
76
+
77
+ @media (prefers-reduced-motion: reduce) {
78
+ .xeno-progressbar-fill {
79
+ transition: none;
80
+ }
81
+ /* A perpetual sweep is exactly what reduced-motion is asking not to see. The bar still says "busy"
82
+ — it just says it by sitting there at a partial width instead of running. */
83
+ .xeno-progressbar-track[data-state='indeterminate'] .xeno-progressbar-fill {
84
+ animation: none;
85
+ width: 100%;
86
+ opacity: 0.45;
87
+ }
88
+ }
@@ -1,33 +1,33 @@
1
- /*
2
- * Spinner — an indeterminate loader. A small rounded SQUARE outline (never a circle): all four edges
3
- * sit in `--xeno-muted` as the track and the top edge burns to `--xeno-text` as the bright leading
4
- * edge, the whole square rotating. Size + stroke arrive as inline vars (`--xeno-spinner-size`/
5
- * `--xeno-spinner-stroke`) set by the component from `size`, so the metrics never drift from the data.
6
- * Motion is gated behind `prefers-reduced-motion: no-preference`. Monochrome shell, theme tokens only.
7
- */
8
- .xeno-spinner {
9
- display: inline-block;
10
- box-sizing: border-box;
11
- width: var(--xeno-spinner-size, 16px);
12
- height: var(--xeno-spinner-size, 16px);
13
- /* Track and leading edge are named, and default to the two inks. A consumer that needs the loader
14
- to belong to something coloured — a status row for one live activity, say — can say so here
15
- instead of reassigning `--xeno-text` on the element, which is the whole app's ink and means
16
- something else. Omit them and nothing changes. */
17
- border: var(--xeno-spinner-stroke, 2px) solid var(--xeno-spinner-track, var(--xeno-muted));
18
- border-top-color: var(--xeno-spinner-edge, var(--xeno-text));
19
- border-radius: var(--xeno-radius-xs);
20
- vertical-align: middle;
21
- }
22
-
23
- @media (prefers-reduced-motion: no-preference) {
24
- .xeno-spinner {
25
- animation: xeno-spinner-rot 0.7s linear infinite;
26
- }
27
- }
28
-
29
- @keyframes xeno-spinner-rot {
30
- to {
31
- transform: rotate(360deg);
32
- }
33
- }
1
+ /*
2
+ * Spinner — an indeterminate loader. A small rounded SQUARE outline (never a circle): all four edges
3
+ * sit in `--xeno-muted` as the track and the top edge burns to `--xeno-text` as the bright leading
4
+ * edge, the whole square rotating. Size + stroke arrive as inline vars (`--xeno-spinner-size`/
5
+ * `--xeno-spinner-stroke`) set by the component from `size`, so the metrics never drift from the data.
6
+ * Motion is gated behind `prefers-reduced-motion: no-preference`. Monochrome shell, theme tokens only.
7
+ */
8
+ .xeno-spinner {
9
+ display: inline-block;
10
+ box-sizing: border-box;
11
+ width: var(--xeno-spinner-size, 16px);
12
+ height: var(--xeno-spinner-size, 16px);
13
+ /* Track and leading edge are named, and default to the two inks. A consumer that needs the loader
14
+ to belong to something coloured — a status row for one live activity, say — can say so here
15
+ instead of reassigning `--xeno-text` on the element, which is the whole app's ink and means
16
+ something else. Omit them and nothing changes. */
17
+ border: var(--xeno-spinner-stroke, 2px) solid var(--xeno-spinner-track, var(--xeno-muted));
18
+ border-top-color: var(--xeno-spinner-edge, var(--xeno-text));
19
+ border-radius: var(--xeno-radius-xs);
20
+ vertical-align: middle;
21
+ }
22
+
23
+ @media (prefers-reduced-motion: no-preference) {
24
+ .xeno-spinner {
25
+ animation: xeno-spinner-rot 0.7s linear infinite;
26
+ }
27
+ }
28
+
29
+ @keyframes xeno-spinner-rot {
30
+ to {
31
+ transform: rotate(360deg);
32
+ }
33
+ }