@xenosystem/elements-react 0.0.1

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 (153) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +41 -0
  3. package/dist/XenoElement.d.ts +39 -0
  4. package/dist/XenoElement.js +25 -0
  5. package/dist/containers/Avatar.d.ts +42 -0
  6. package/dist/containers/Avatar.js +29 -0
  7. package/dist/containers/Badges.d.ts +61 -0
  8. package/dist/containers/Badges.js +43 -0
  9. package/dist/containers/Card.d.ts +24 -0
  10. package/dist/containers/Card.js +5 -0
  11. package/dist/containers/Chip.d.ts +34 -0
  12. package/dist/containers/Chip.js +17 -0
  13. package/dist/containers/DataTable.d.ts +28 -0
  14. package/dist/containers/DataTable.js +13 -0
  15. package/dist/containers/ListRow.d.ts +37 -0
  16. package/dist/containers/ListRow.js +40 -0
  17. package/dist/containers/MessageBubble.d.ts +33 -0
  18. package/dist/containers/MessageBubble.js +9 -0
  19. package/dist/containers/Panel.d.ts +24 -0
  20. package/dist/containers/Panel.js +6 -0
  21. package/dist/containers/Table.d.ts +39 -0
  22. package/dist/containers/Table.js +20 -0
  23. package/dist/containers/Tile.d.ts +19 -0
  24. package/dist/containers/Tile.js +7 -0
  25. package/dist/content/Callout.d.ts +32 -0
  26. package/dist/content/Callout.js +10 -0
  27. package/dist/content/Caret.d.ts +9 -0
  28. package/dist/content/Caret.js +5 -0
  29. package/dist/content/CodeBlock.d.ts +36 -0
  30. package/dist/content/CodeBlock.js +50 -0
  31. package/dist/content/Collapsible.d.ts +24 -0
  32. package/dist/content/Collapsible.js +19 -0
  33. package/dist/content/InlineCode.d.ts +12 -0
  34. package/dist/content/InlineCode.js +5 -0
  35. package/dist/content/ModelPicker.d.ts +45 -0
  36. package/dist/content/ModelPicker.js +124 -0
  37. package/dist/content/SourceCard.d.ts +23 -0
  38. package/dist/content/SourceCard.js +18 -0
  39. package/dist/content/SourcesDisclosure.d.ts +26 -0
  40. package/dist/content/SourcesDisclosure.js +20 -0
  41. package/dist/controls/Button.d.ts +39 -0
  42. package/dist/controls/Button.js +16 -0
  43. package/dist/controls/ConfirmButton.d.ts +22 -0
  44. package/dist/controls/ConfirmButton.js +22 -0
  45. package/dist/controls/IconButton.d.ts +45 -0
  46. package/dist/controls/IconButton.js +15 -0
  47. package/dist/controls/Switch.d.ts +11 -0
  48. package/dist/controls/Switch.js +23 -0
  49. package/dist/controls/TextInput.d.ts +13 -0
  50. package/dist/controls/TextInput.js +6 -0
  51. package/dist/controls/Textarea.d.ts +8 -0
  52. package/dist/controls/Textarea.js +5 -0
  53. package/dist/controls/ToggleButton.d.ts +23 -0
  54. package/dist/controls/ToggleButton.js +23 -0
  55. package/dist/controls/util.d.ts +48 -0
  56. package/dist/controls/util.js +23 -0
  57. package/dist/forms/Checkbox.d.ts +18 -0
  58. package/dist/forms/Checkbox.js +28 -0
  59. package/dist/forms/RadioGroup.d.ts +51 -0
  60. package/dist/forms/RadioGroup.js +83 -0
  61. package/dist/index.d.ts +75 -0
  62. package/dist/index.js +81 -0
  63. package/dist/layout/ResizablePanel.d.ts +40 -0
  64. package/dist/layout/ResizablePanel.js +108 -0
  65. package/dist/nav/Sidebar.d.ts +60 -0
  66. package/dist/nav/Sidebar.js +84 -0
  67. package/dist/overlays/DateTimePicker.d.ts +71 -0
  68. package/dist/overlays/DateTimePicker.js +191 -0
  69. package/dist/overlays/Menu.d.ts +28 -0
  70. package/dist/overlays/Menu.js +32 -0
  71. package/dist/overlays/MenuItem.d.ts +71 -0
  72. package/dist/overlays/MenuItem.js +15 -0
  73. package/dist/overlays/Modal.d.ts +36 -0
  74. package/dist/overlays/Modal.js +27 -0
  75. package/dist/overlays/PickerField.d.ts +32 -0
  76. package/dist/overlays/PickerField.js +40 -0
  77. package/dist/overlays/PillFilter.d.ts +25 -0
  78. package/dist/overlays/PillFilter.js +41 -0
  79. package/dist/overlays/Popover.d.ts +32 -0
  80. package/dist/overlays/Popover.js +38 -0
  81. package/dist/overlays/Reveal.d.ts +20 -0
  82. package/dist/overlays/Reveal.js +45 -0
  83. package/dist/overlays/SegmentedControl.d.ts +23 -0
  84. package/dist/overlays/SegmentedControl.js +43 -0
  85. package/dist/overlays/Tabs.d.ts +32 -0
  86. package/dist/overlays/Tabs.js +142 -0
  87. package/dist/overlays/Tooltip.d.ts +23 -0
  88. package/dist/overlays/Tooltip.js +61 -0
  89. package/dist/status/ProgressBar.d.ts +20 -0
  90. package/dist/status/ProgressBar.js +11 -0
  91. package/dist/status/Spinner.d.ts +16 -0
  92. package/dist/status/Spinner.js +12 -0
  93. package/dist/status/StatusIndicator.d.ts +19 -0
  94. package/dist/status/StatusIndicator.js +11 -0
  95. package/dist/status/StepTimeline.d.ts +26 -0
  96. package/dist/status/StepTimeline.js +16 -0
  97. package/dist/status/ThinkingCube.d.ts +24 -0
  98. package/dist/status/ThinkingCube.js +11 -0
  99. package/dist/useDialog.d.ts +45 -0
  100. package/dist/useDialog.js +122 -0
  101. package/dist/useGooPill.d.ts +57 -0
  102. package/dist/useGooPill.js +76 -0
  103. package/dist/useMenu.d.ts +50 -0
  104. package/dist/useMenu.js +119 -0
  105. package/dist/useTabs.d.ts +88 -0
  106. package/dist/useTabs.js +93 -0
  107. package/package.json +58 -0
  108. package/src/chrome-separated.css +132 -0
  109. package/src/chrome-unified.css +72 -0
  110. package/src/containers/Badges.css +116 -0
  111. package/src/containers/avatar.css +70 -0
  112. package/src/containers/card.css +75 -0
  113. package/src/containers/chip.css +103 -0
  114. package/src/containers/list-row.css +132 -0
  115. package/src/containers/message-bubble.css +112 -0
  116. package/src/containers/panel.css +120 -0
  117. package/src/containers/table.css +102 -0
  118. package/src/containers/tile.css +17 -0
  119. package/src/content/callout.css +77 -0
  120. package/src/content/caret.css +30 -0
  121. package/src/content/code-block.css +159 -0
  122. package/src/content/collapsible.css +80 -0
  123. package/src/content/inline-code.css +14 -0
  124. package/src/content/model-picker.css +219 -0
  125. package/src/content/source-card.css +99 -0
  126. package/src/content/sources-disclosure.css +118 -0
  127. package/src/controls.css +453 -0
  128. package/src/fonts.css +34 -0
  129. package/src/forms/Checkbox.css +114 -0
  130. package/src/forms/RadioGroup.css +115 -0
  131. package/src/goo.css +97 -0
  132. package/src/icon-motion.css +1909 -0
  133. package/src/layout/ResizablePanel.css +79 -0
  134. package/src/nav/sidebar.css +296 -0
  135. package/src/overlays/date-time-picker.css +238 -0
  136. package/src/overlays/menu.css +195 -0
  137. package/src/overlays/modal.css +167 -0
  138. package/src/overlays/picker-field.css +81 -0
  139. package/src/overlays/pill-filter.css +129 -0
  140. package/src/overlays/reveal.css +56 -0
  141. package/src/overlays/segmented-control.css +185 -0
  142. package/src/overlays/tabs.css +143 -0
  143. package/src/overlays/tooltip.css +126 -0
  144. package/src/scrollbar.css +184 -0
  145. package/src/size.css +179 -0
  146. package/src/status/progress-bar.css +88 -0
  147. package/src/status/spinner.css +33 -0
  148. package/src/status/statusindicator.css +65 -0
  149. package/src/status/steptimeline.css +137 -0
  150. package/src/status/thinkingcube.css +75 -0
  151. package/src/xeno-element.css +60 -0
  152. package/src/xeno-elements.css +71 -0
  153. package/src/xeno-theme.css +99 -0
package/src/size.css ADDED
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,65 @@
1
+ /*
2
+ * StatusIndicator — an inline square mark + label.
3
+ *
4
+ * CSS-first: the mark's paint is driven by `data-tone`. The mark is a rounded SQUARE (radius-xs),
5
+ * never a circle. `neutral`/`active` stay monochrome (theme greys); `success` colours from the injected
6
+ * `--xeno-status-tone` var (set from the status token by the renderer); `danger` uses `--xeno-danger`.
7
+ * The only motion is the `active` mark's breathing, gated behind `prefers-reduced-motion: no-preference`.
8
+ * No literal colours here.
9
+ */
10
+
11
+ .xeno-statusindicator {
12
+ display: inline-flex;
13
+ align-items: center;
14
+ gap: 6px;
15
+ font-family: inherit;
16
+ font-size: 13px;
17
+ line-height: 1;
18
+ color: var(--xeno-text);
19
+ }
20
+
21
+ .xeno-statusindicator-mark {
22
+ flex: none;
23
+ width: 8px;
24
+ height: 8px;
25
+ border-radius: var(--xeno-radius-xs);
26
+ background: var(--xeno-muted);
27
+ }
28
+
29
+ .xeno-statusindicator[data-tone='active'] .xeno-statusindicator-mark {
30
+ background: var(--xeno-active);
31
+ }
32
+
33
+ .xeno-statusindicator[data-tone='success'] .xeno-statusindicator-mark {
34
+ background: var(--xeno-status-tone);
35
+ }
36
+
37
+ .xeno-statusindicator[data-tone='danger'] .xeno-statusindicator-mark {
38
+ background: var(--xeno-danger);
39
+ }
40
+
41
+ /* neutral label reads a touch dimmer; the tonal ones keep the mark as the accent */
42
+ .xeno-statusindicator[data-tone='neutral'] .xeno-statusindicator-label {
43
+ color: var(--xeno-muted);
44
+ }
45
+
46
+ .xeno-statusindicator[data-tone='danger'] .xeno-statusindicator-label {
47
+ color: var(--xeno-danger);
48
+ }
49
+
50
+ @media (prefers-reduced-motion: no-preference) {
51
+ @keyframes xeno-statusindicator-breathe {
52
+ 0%,
53
+ 100% {
54
+ transform: scale(1);
55
+ opacity: 1;
56
+ }
57
+ 50% {
58
+ transform: scale(0.72);
59
+ opacity: 0.6;
60
+ }
61
+ }
62
+ .xeno-statusindicator[data-tone='active'] .xeno-statusindicator-mark {
63
+ animation: xeno-statusindicator-breathe 1.6s var(--xeno-ease) infinite;
64
+ }
65
+ }
@@ -0,0 +1,137 @@
1
+ /*
2
+ * StepTimeline — a vertical run of steps threaded by a hairline spine.
3
+ *
4
+ * CSS-first: paint is driven by `data-status` on each step; the size mark is a rounded SQUARE
5
+ * (radius-xs), never a circle. Monochrome throughout — spine + marks are greys from `xeno-theme.css`.
6
+ * The only motion is the `active` mark's breathing, gated behind `prefers-reduced-motion: no-preference`.
7
+ * No literal colours here.
8
+ */
9
+
10
+ .xeno-steptimeline {
11
+ list-style: none;
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ /* Two columns: the rail (fixed mark width) + the body. Rows stack with no gap so the spine of one
17
+ step reaches the mark of the next. */
18
+ .xeno-steptimeline-step {
19
+ display: grid;
20
+ grid-template-columns: 16px 1fr;
21
+ column-gap: 12px;
22
+ }
23
+
24
+ /* ── Rail (mark + spine) — decorative, aria-hidden ───────────────────────────────────── */
25
+ .xeno-steptimeline-rail {
26
+ display: flex;
27
+ flex-direction: column;
28
+ align-items: center;
29
+ /* stretch to the row height so the spine's flex fill spans down to the next mark */
30
+ }
31
+
32
+ .xeno-steptimeline-mark {
33
+ box-sizing: border-box;
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ flex: none;
38
+ width: 16px;
39
+ height: 16px;
40
+ margin-top: 1px;
41
+ border-radius: var(--xeno-radius-xs);
42
+ }
43
+
44
+ /* pending — an outline square, not yet reached */
45
+ .xeno-steptimeline-step[data-status='pending'] .xeno-steptimeline-mark {
46
+ background: transparent;
47
+ border: 1.5px solid var(--xeno-muted);
48
+ }
49
+
50
+ /* active — a filled square, breathing (see reduced-motion gate below) */
51
+ .xeno-steptimeline-step[data-status='active'] .xeno-steptimeline-mark {
52
+ background: var(--xeno-active);
53
+ }
54
+
55
+ /* done — filled to text colour; the check glyph inherits `--xeno-on-accent` via currentColor */
56
+ .xeno-steptimeline-step[data-status='done'] .xeno-steptimeline-mark {
57
+ background: var(--xeno-text);
58
+ color: var(--xeno-on-accent);
59
+ }
60
+
61
+ /* the connecting spine — a 1.5px rounded border-coloured bar filling to the next mark */
62
+ .xeno-steptimeline-spine {
63
+ flex: 1 1 auto;
64
+ width: 1.5px;
65
+ min-height: 12px;
66
+ margin: 2px 0;
67
+ background: var(--xeno-border);
68
+ border-radius: var(--xeno-radius-hair);
69
+ }
70
+
71
+ /* ── Body (label + optional time) ────────────────────────────────────────────────────── */
72
+ .xeno-steptimeline-body {
73
+ display: flex;
74
+ align-items: baseline;
75
+ gap: 12px;
76
+ min-height: 18px;
77
+ }
78
+
79
+ /* space between steps — every step but the last pads its body so the spine has length */
80
+ .xeno-steptimeline-step:not(:last-child) .xeno-steptimeline-body {
81
+ padding-bottom: 20px;
82
+ }
83
+
84
+ .xeno-steptimeline-label {
85
+ flex: 1 1 auto;
86
+ font-family: inherit;
87
+ font-size: 14px;
88
+ line-height: 1.3;
89
+ color: var(--xeno-text);
90
+ }
91
+
92
+ .xeno-steptimeline-step[data-status='pending'] .xeno-steptimeline-label {
93
+ color: var(--xeno-muted);
94
+ }
95
+
96
+ .xeno-steptimeline-step[data-status='active'] .xeno-steptimeline-label {
97
+ font-weight: 500;
98
+ }
99
+
100
+ .xeno-steptimeline-time {
101
+ flex: none;
102
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
103
+ font-size: 12px;
104
+ line-height: 1.3;
105
+ font-variant-numeric: tabular-nums;
106
+ color: var(--xeno-muted);
107
+ }
108
+
109
+ /* screen-reader-only status word — the visible mark is decorative */
110
+ .xeno-steptimeline-sr {
111
+ position: absolute;
112
+ width: 1px;
113
+ height: 1px;
114
+ padding: 0;
115
+ margin: -1px;
116
+ overflow: hidden;
117
+ clip: rect(0 0 0 0);
118
+ white-space: nowrap;
119
+ border: 0;
120
+ }
121
+
122
+ @media (prefers-reduced-motion: no-preference) {
123
+ @keyframes xeno-steptimeline-breathe {
124
+ 0%,
125
+ 100% {
126
+ transform: scale(1);
127
+ opacity: 1;
128
+ }
129
+ 50% {
130
+ transform: scale(0.78);
131
+ opacity: 0.6;
132
+ }
133
+ }
134
+ .xeno-steptimeline-step[data-status='active'] .xeno-steptimeline-mark {
135
+ animation: xeno-steptimeline-breathe 1.8s var(--xeno-ease) infinite;
136
+ }
137
+ }
@@ -0,0 +1,75 @@
1
+ /*
2
+ * ThinkingCube — the LOCKED branded model avatar. A single rounded SQUARE that either BREATHES
3
+ * (thinking: one continuous rotate + scale loop) or LANDS (settled: a one-shot grow while the fill
4
+ * grows opaque canvas → muted). Monochrome shell: muted hairline outline, canvas/muted fill only.
5
+ * No circles, no coloured glow. Motion is transform/opacity + background-color; a reduced-motion
6
+ * override stills the spin and leaves `settled` as a static filled square.
7
+ */
8
+
9
+ .xeno-cube {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ width: var(--xeno-cube-size, 24px);
14
+ height: var(--xeno-cube-size, 24px);
15
+ line-height: 0;
16
+ flex: none;
17
+ }
18
+
19
+ .xeno-cube-body {
20
+ box-sizing: border-box;
21
+ width: 100%;
22
+ height: 100%;
23
+ border: 1.5px solid var(--xeno-muted);
24
+ border-radius: var(--xeno-radius-xs);
25
+ background-color: var(--xeno-canvas);
26
+ transform-origin: center;
27
+ will-change: transform, background-color;
28
+ }
29
+
30
+ /* thinking — ONE continuous loop: rotate 0→360° while the scale breathes 1 → .52 → 1. */
31
+ .xeno-cube[data-state='thinking'] .xeno-cube-body {
32
+ background-color: var(--xeno-canvas);
33
+ animation: xeno-cube-life 1.5s cubic-bezier(0.45, 0, 0.55, 1) infinite;
34
+ }
35
+
36
+ /* settled — runs ONCE: grow .8 → 1.14 → 1 while the SAME square fills canvas → muted, then holds. */
37
+ .xeno-cube[data-state='settled'] .xeno-cube-body {
38
+ background-color: var(--xeno-muted);
39
+ animation: xeno-cube-settle 0.52s var(--xeno-ease) both;
40
+ }
41
+
42
+ @keyframes xeno-cube-life {
43
+ 0% {
44
+ transform: rotate(0deg) scale(1);
45
+ }
46
+ 50% {
47
+ transform: rotate(180deg) scale(0.52);
48
+ }
49
+ 100% {
50
+ transform: rotate(360deg) scale(1);
51
+ }
52
+ }
53
+
54
+ @keyframes xeno-cube-settle {
55
+ 0% {
56
+ transform: scale(0.8);
57
+ background-color: var(--xeno-canvas);
58
+ }
59
+ 60% {
60
+ transform: scale(1.14);
61
+ }
62
+ 100% {
63
+ transform: scale(1);
64
+ background-color: var(--xeno-muted);
65
+ }
66
+ }
67
+
68
+ /* Reduced motion: no spin, no grow. `settled` still rests as its filled muted square (base rule above);
69
+ `thinking` rests as a still, muted-outlined canvas square. */
70
+ @media (prefers-reduced-motion: reduce) {
71
+ .xeno-cube[data-state='thinking'] .xeno-cube-body,
72
+ .xeno-cube[data-state='settled'] .xeno-cube-body {
73
+ animation: none;
74
+ }
75
+ }
@@ -0,0 +1,60 @@
1
+ /*
2
+ * Base presentation for a XENO element. CSS-FIRST, 0 kb JS: an element's discrete state lives in
3
+ * `data-<axis>` attributes on the root, and this stylesheet is where any transition attaches — never a
4
+ * JavaScript animation runtime (SPEC §6, so elements survive sandboxed iframes and web export).
5
+ *
6
+ * Geometry variant swaps (bookmark off↔on) are re-rendered from `state` by the renderer. Channel
7
+ * transitions declared as BINDINGS (opacity/scale/fill over ms) land here as they are added; the axis
8
+ * attributes below are the selectors they will hang off. `currentColor` keeps every glyph monochrome
9
+ * and inheriting the surrounding text colour.
10
+ */
11
+
12
+ .xeno-element {
13
+ display: inline-block;
14
+ vertical-align: middle;
15
+ line-height: 0;
16
+ color: inherit;
17
+ }
18
+
19
+ /* An element the product marks unavailable reads as dimmed everywhere, from one rule. */
20
+ .xeno-element[data-availability='disabled'] {
21
+ opacity: 0.4;
22
+ }
23
+ .xeno-element[data-availability='busy'] {
24
+ cursor: progress;
25
+ }
26
+
27
+ @media (prefers-reduced-motion: no-preference) {
28
+ .xeno-element,
29
+ .xeno-element * {
30
+ transition:
31
+ opacity 160ms ease,
32
+ transform 160ms ease,
33
+ fill 160ms ease,
34
+ stroke 160ms ease;
35
+ }
36
+ }
37
+
38
+
39
+ /* ── Morphing between geometry variants ───────────────────────────────────────────────
40
+ * When a declaration's active variant is interpolable with `base` — same primitive count, same kind at
41
+ * every index — the renderer marks the icon `data-morph="on"` and hands each path its own `d` a second
42
+ * time, as `--part-d`. That is all the seam needs: `d` is an animatable CSS property, a custom property
43
+ * changing is a computed-value change, so the swap becomes a transition and the icon draws itself from
44
+ * one shape into the other.
45
+ *
46
+ * Still no animation runtime. The renderer never animates; it states that an animation is possible and
47
+ * the stylesheet decides whether to take it — the same division the rest of this system runs on.
48
+ *
49
+ * Behind `@supports` because `d` as a CSS property is not universal. Where it is missing, the `d`
50
+ * ATTRIBUTE is still correct and still swapped on re-render, so the icon changes instantly instead of
51
+ * morphing: the worse animation, never the wrong picture. */
52
+ @supports (d: path('M0 0')) {
53
+ .xeno-element[data-morph='on'] .xeno-part {
54
+ d: var(--part-d);
55
+ transition: d var(--xeno-dur-morph, 320ms) var(--xeno-ease-morph, cubic-bezier(0.22, 0.7, 0.2, 1));
56
+ }
57
+ @media (prefers-reduced-motion: reduce) {
58
+ .xeno-element[data-morph='on'] .xeno-part { transition: none; }
59
+ }
60
+ }