@xenosystem/elements-react 0.0.4 → 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 -112
@@ -1,296 +1,296 @@
1
- /*
2
- * Sidebar — the slide-in left navigation panel. Locked geometry: SQUARE corners, flush to the viewport
3
- * edge; a fixed ~260px column on `var(--xeno-surface)`. CSS-first: the only state that drives appearance
4
- * is `data-state="open|closed"` on the panel + backdrop. The only animated property is `transform`
5
- * (translateX), guarded by reduced-motion. Monochrome — the scrim is a neutral, hueless black.
6
- */
7
-
8
- .xeno-sidebar {
9
- /* One row's stride, in one place: the pill's travel is computed from these, so a row that changes
10
- height can never leave the pill landing between two rows. */
11
- --xeno-sidebar-row: 36px;
12
- --xeno-sidebar-row-gap: 2px;
13
- /* How much the goo pill misbehaves. The squash is deliberately restrained — enough deformation to
14
- read as mass, not enough to look like jelly on a nav you use all day — and the travel easing
15
- overshoots only slightly (1.12, against the 1.4 of the library's overshoot token). Turn these
16
- three up together to get the full CodePen bounce back. */
17
- --xeno-goo-squash-x: 0.95;
18
- --xeno-goo-squash-y: 1.1;
19
- --xeno-goo-ease: cubic-bezier(0.34, 1.12, 0.5, 1);
20
- position: fixed;
21
- inset-block: 0;
22
- inset-inline-start: 0;
23
- z-index: 40;
24
- display: flex;
25
- flex-direction: column;
26
- width: 260px;
27
- max-width: 85vw;
28
- height: 100%;
29
- background: var(--xeno-surface);
30
- border-inline-end: 1px solid var(--xeno-border);
31
- border-radius: 0; /* locked: square, flush */
32
- color: var(--xeno-text);
33
- transform: translateX(0);
34
- transition: transform var(--xeno-dur) var(--xeno-ease);
35
- will-change: transform;
36
- }
37
- .xeno-sidebar[data-state='closed'] {
38
- transform: translateX(-100%);
39
- }
40
-
41
- /* ── Header (brand + search) ─────────────────────────────────────────────────────── */
42
- .xeno-sidebar-header {
43
- display: flex;
44
- flex-direction: column;
45
- gap: 12px;
46
- flex: none;
47
- padding: 16px;
48
- border-bottom: 1px solid var(--xeno-border);
49
- }
50
- .xeno-sidebar-brand {
51
- font-size: 15px;
52
- font-weight: 600;
53
- line-height: 1;
54
- letter-spacing: 0.04em;
55
- color: var(--xeno-text);
56
- }
57
- /* Make the shared TextInput fill the header width. */
58
- .xeno-sidebar-search {
59
- display: flex;
60
- width: 100%;
61
- }
62
-
63
- /* ── Nav (scrollable list of rows) ───────────────────────────────────────────────── */
64
- .xeno-sidebar-nav {
65
- flex: 1;
66
- min-height: 0;
67
- overflow-y: auto;
68
- padding: 8px;
69
- }
70
- .xeno-sidebar-section {
71
- display: flex;
72
- flex-direction: column;
73
- gap: var(--xeno-sidebar-row-gap);
74
- }
75
- /* The rows of one group + the pill that backs them. `position: relative` makes this the pill's
76
- containing block, so its travel is measured from the first row of THIS group — which is why the
77
- heading can sit outside and the trick survives sections of different shapes. */
78
- .xeno-sidebar-items {
79
- position: relative;
80
- display: flex;
81
- flex-direction: column;
82
- gap: var(--xeno-sidebar-row-gap);
83
- }
84
- .xeno-sidebar-section + .xeno-sidebar-section {
85
- margin-top: 8px;
86
- }
87
- .xeno-sidebar-heading {
88
- padding: 10px 10px 4px;
89
- font-size: 11px;
90
- font-weight: 600;
91
- line-height: 1;
92
- letter-spacing: 0.08em;
93
- text-transform: uppercase;
94
- color: var(--xeno-muted);
95
- }
96
-
97
- /* ── Row (self-contained; NOT ListRow) ───────────────────────────────────────────── */
98
- .xeno-sidebar-item {
99
- position: relative;
100
- z-index: 1; /* over the pill, which is painted behind the whole group */
101
- display: flex;
102
- align-items: center;
103
- gap: 10px;
104
- width: 100%;
105
- height: var(--xeno-sidebar-row);
106
- padding: 0 10px;
107
- font-family: inherit;
108
- font-size: 14px;
109
- font-weight: 500;
110
- line-height: 1;
111
- text-align: left;
112
- color: var(--xeno-muted);
113
- background: transparent;
114
- border: 1px solid transparent;
115
- border-radius: var(--xeno-radius-md);
116
- cursor: pointer;
117
- user-select: none;
118
- transition:
119
- background var(--xeno-dur) ease,
120
- color var(--xeno-dur) ease;
121
- }
122
- /* Neither hover nor selection paints its OWN background any more — the travelling pill below is the
123
- single filled surface for the whole group. Two of them would read as two selections. */
124
- .xeno-sidebar-item:hover,
125
- .xeno-sidebar-item[data-selection='on'] {
126
- color: var(--xeno-text);
127
- }
128
- .xeno-sidebar-item:focus-visible {
129
- outline: none;
130
- box-shadow:
131
- 0 0 0 2px var(--xeno-surface),
132
- 0 0 0 4px var(--xeno-border);
133
- }
134
- .xeno-sidebar-item[data-availability='disabled'] {
135
- opacity: 0.4;
136
- cursor: not-allowed;
137
- }
138
-
139
- /* ── The goo pill (the travelling hover surface) ──────────────────────────────────────
140
- * ONE filled pill per group, painted behind the rows, that FLIES to whichever row you point at and
141
- * squashes on the way — narrower and taller as it launches, back to true as it lands. That squash is
142
- * the whole illusion: a rectangle that only slid would read as a scrollbar; one that deforms reads as
143
- * something with mass. Adapted from One_div's gooey nav, in XENO ink: the pill is `--xeno-control`,
144
- * the monochrome step above the panel, not an accent colour.
145
- *
146
- * Pure CSS, no measuring: the pill is the LAST child of the group, so every row keeps its natural
147
- * `:nth-child` number and a hovered row reaches the pill with `~`. Travel is `row index x stride`, and
148
- * the stride comes from the row tokens above, so it cannot drift out of step with the layout. */
149
- .xeno-sidebar-goo {
150
- position: absolute;
151
- z-index: 0;
152
- left: 0;
153
- top: calc(var(--xeno-goo-row, 0) * (var(--xeno-sidebar-row) + var(--xeno-sidebar-row-gap)));
154
- width: 100%;
155
- height: var(--xeno-sidebar-row);
156
- background: var(--xeno-control);
157
- border-radius: var(--xeno-radius-md);
158
- opacity: 0;
159
- pointer-events: none;
160
- transition:
161
- top var(--xeno-dur-entrance, 240ms) var(--xeno-goo-ease),
162
- opacity var(--xeno-dur, 0.14s) ease;
163
- }
164
- /* The pill RESTS on the selected row of its group (so the selection indicator is the same object
165
- that flies), and is invisible in a group with no selection. */
166
- .xeno-sidebar-item[data-selection='on'] ~ .xeno-sidebar-goo { opacity: 1; }
167
- .xeno-sidebar-item[data-selection='on']:nth-child(1) ~ .xeno-sidebar-goo { --xeno-goo-row: 0; }
168
- .xeno-sidebar-item[data-selection='on']:nth-child(2) ~ .xeno-sidebar-goo { --xeno-goo-row: 1; }
169
- .xeno-sidebar-item[data-selection='on']:nth-child(3) ~ .xeno-sidebar-goo { --xeno-goo-row: 2; }
170
- .xeno-sidebar-item[data-selection='on']:nth-child(4) ~ .xeno-sidebar-goo { --xeno-goo-row: 3; }
171
- .xeno-sidebar-item[data-selection='on']:nth-child(5) ~ .xeno-sidebar-goo { --xeno-goo-row: 4; }
172
- .xeno-sidebar-item[data-selection='on']:nth-child(6) ~ .xeno-sidebar-goo { --xeno-goo-row: 5; }
173
- .xeno-sidebar-item[data-selection='on']:nth-child(7) ~ .xeno-sidebar-goo { --xeno-goo-row: 6; }
174
- .xeno-sidebar-item[data-selection='on']:nth-child(8) ~ .xeno-sidebar-goo { --xeno-goo-row: 7; }
175
- .xeno-sidebar-item[data-selection='on']:nth-child(9) ~ .xeno-sidebar-goo { --xeno-goo-row: 8; }
176
- .xeno-sidebar-item[data-selection='on']:nth-child(10) ~ .xeno-sidebar-goo { --xeno-goo-row: 9; }
177
- .xeno-sidebar-item[data-selection='on']:nth-child(11) ~ .xeno-sidebar-goo { --xeno-goo-row: 10; }
178
- .xeno-sidebar-item[data-selection='on']:nth-child(12) ~ .xeno-sidebar-goo { --xeno-goo-row: 11; }
179
-
180
- /* Hover takes it over. Same specificity as the rules above ON PURPOSE — these come later, so pointing
181
- at a row always wins over the resting position, and letting go hands it back. */
182
- .xeno-sidebar-item:hover ~ .xeno-sidebar-goo { opacity: 1; }
183
- .xeno-sidebar-item:nth-child(1):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 0; animation: xeno-goo-1 var(--xeno-dur-entrance, 240ms) 1; }
184
- .xeno-sidebar-item:nth-child(2):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 1; animation: xeno-goo-2 var(--xeno-dur-entrance, 240ms) 1; }
185
- .xeno-sidebar-item:nth-child(3):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 2; animation: xeno-goo-3 var(--xeno-dur-entrance, 240ms) 1; }
186
- .xeno-sidebar-item:nth-child(4):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 3; animation: xeno-goo-4 var(--xeno-dur-entrance, 240ms) 1; }
187
- .xeno-sidebar-item:nth-child(5):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 4; animation: xeno-goo-5 var(--xeno-dur-entrance, 240ms) 1; }
188
- .xeno-sidebar-item:nth-child(6):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 5; animation: xeno-goo-6 var(--xeno-dur-entrance, 240ms) 1; }
189
- .xeno-sidebar-item:nth-child(7):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 6; animation: xeno-goo-7 var(--xeno-dur-entrance, 240ms) 1; }
190
- .xeno-sidebar-item:nth-child(8):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 7; animation: xeno-goo-8 var(--xeno-dur-entrance, 240ms) 1; }
191
- .xeno-sidebar-item:nth-child(9):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 8; animation: xeno-goo-9 var(--xeno-dur-entrance, 240ms) 1; }
192
- .xeno-sidebar-item:nth-child(10):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 9; animation: xeno-goo-10 var(--xeno-dur-entrance, 240ms) 1; }
193
- .xeno-sidebar-item:nth-child(11):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 10; animation: xeno-goo-11 var(--xeno-dur-entrance, 240ms) 1; }
194
- .xeno-sidebar-item:nth-child(12):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 11; animation: xeno-goo-12 var(--xeno-dur-entrance, 240ms) 1; }
195
-
196
- /* One keyframe track PER ROW, all identical. They have to be separate NAMES: re-pointing at another row
197
- only swaps which rule matches, and a browser does not restart an animation whose name did not change —
198
- so a single shared `xeno-goo` would squash once and then never again as you slide down the list. */
199
- @keyframes xeno-goo-1 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
200
- @keyframes xeno-goo-2 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
201
- @keyframes xeno-goo-3 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
202
- @keyframes xeno-goo-4 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
203
- @keyframes xeno-goo-5 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
204
- @keyframes xeno-goo-6 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
205
- @keyframes xeno-goo-7 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
206
- @keyframes xeno-goo-8 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
207
- @keyframes xeno-goo-9 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
208
- @keyframes xeno-goo-10 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
209
- @keyframes xeno-goo-11 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
210
- @keyframes xeno-goo-12 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
211
-
212
- /* ── The XENO nav hover animation ─────────────────────────────────────────────────────
213
- * The icon + label NUDGE right — the icon leads, the label follows a beat later (a small cascade), on
214
- * the interaction easing (E2). There is no left rail: the goo pill above already marks both the hovered
215
- * row and the active one, and a second marker on the same edge only competed with it. */
216
- .xeno-sidebar-item-icon {
217
- display: inline-flex;
218
- flex: none;
219
- color: var(--xeno-muted);
220
- transition:
221
- color var(--xeno-dur) ease,
222
- transform var(--xeno-dur-entrance, 240ms) var(--xeno-ease-stagger, cubic-bezier(0.22, 0.7, 0.2, 1));
223
- }
224
- .xeno-sidebar-item-label {
225
- min-width: 0;
226
- overflow: hidden;
227
- text-overflow: ellipsis;
228
- white-space: nowrap;
229
- transition: transform var(--xeno-dur-entrance, 240ms) var(--xeno-ease-stagger, cubic-bezier(0.22, 0.7, 0.2, 1));
230
- }
231
- .xeno-sidebar-item:hover .xeno-sidebar-item-icon,
232
- .xeno-sidebar-item[data-selection='on'] .xeno-sidebar-item-icon {
233
- color: var(--xeno-text);
234
- }
235
- .xeno-sidebar-item:hover .xeno-sidebar-item-icon {
236
- transform: translateX(6px);
237
- }
238
- .xeno-sidebar-item:hover .xeno-sidebar-item-label {
239
- transform: translateX(6px);
240
- transition-delay: 45ms; /* label follows the icon — the cascade */
241
- }
242
-
243
- /* Per-glyph icon animations are global now — see `icon-motion.css`, triggered by
244
- * `.xeno-sidebar-item:hover .xeno-element[data-glyph]`. The nudge above stays sidebar-specific. */
245
-
246
- /* ── Footer ──────────────────────────────────────────────────────────────────────── */
247
- .xeno-sidebar-footer {
248
- flex: none;
249
- padding: 12px 16px;
250
- border-top: 1px solid var(--xeno-border);
251
- }
252
-
253
- /* ── Backdrop (small screens only) ───────────────────────────────────────────────── */
254
- .xeno-sidebar-backdrop {
255
- position: fixed;
256
- inset: 0;
257
- z-index: 39;
258
- background: rgba(0, 0, 0, 0.5); /* neutral scrim — hueless, like a shadow */
259
- opacity: 0;
260
- visibility: hidden;
261
- transition:
262
- opacity var(--xeno-dur) ease,
263
- visibility var(--xeno-dur) ease;
264
- }
265
- .xeno-sidebar-backdrop[data-state='open'] {
266
- opacity: 1;
267
- visibility: visible;
268
- }
269
- @media (min-width: 900px) {
270
- .xeno-sidebar-backdrop {
271
- display: none;
272
- }
273
- }
274
-
275
- @media (prefers-reduced-motion: reduce) {
276
- .xeno-sidebar,
277
- .xeno-sidebar-backdrop,
278
- .xeno-sidebar-item,
279
- .xeno-sidebar-item-icon,
280
- .xeno-sidebar-item-label,
281
- .xeno-sidebar-goo {
282
- transition: none;
283
- }
284
- /* The pill still MARKS the row it belongs to — it just gets there without flying or squashing. */
285
- .xeno-sidebar-goo {
286
- animation: none !important;
287
- }
288
- /* States still apply (the pill marks the row, colours change); motion does not — no nudge, no per-glyph play. */
289
- .xeno-sidebar-item:hover .xeno-sidebar-item-icon,
290
- .xeno-sidebar-item:hover .xeno-sidebar-item-label {
291
- transform: none;
292
- }
293
- .xeno-sidebar-item-icon .xeno-element {
294
- animation: none !important;
295
- }
296
- }
1
+ /*
2
+ * Sidebar — the slide-in left navigation panel. Locked geometry: SQUARE corners, flush to the viewport
3
+ * edge; a fixed ~260px column on `var(--xeno-surface)`. CSS-first: the only state that drives appearance
4
+ * is `data-state="open|closed"` on the panel + backdrop. The only animated property is `transform`
5
+ * (translateX), guarded by reduced-motion. Monochrome — the scrim is a neutral, hueless black.
6
+ */
7
+
8
+ .xeno-sidebar {
9
+ /* One row's stride, in one place: the pill's travel is computed from these, so a row that changes
10
+ height can never leave the pill landing between two rows. */
11
+ --xeno-sidebar-row: 36px;
12
+ --xeno-sidebar-row-gap: 2px;
13
+ /* How much the goo pill misbehaves. The squash is deliberately restrained — enough deformation to
14
+ read as mass, not enough to look like jelly on a nav you use all day — and the travel easing
15
+ overshoots only slightly (1.12, against the 1.4 of the library's overshoot token). Turn these
16
+ three up together to get the full CodePen bounce back. */
17
+ --xeno-goo-squash-x: 0.95;
18
+ --xeno-goo-squash-y: 1.1;
19
+ --xeno-goo-ease: cubic-bezier(0.34, 1.12, 0.5, 1);
20
+ position: fixed;
21
+ inset-block: 0;
22
+ inset-inline-start: 0;
23
+ z-index: 40;
24
+ display: flex;
25
+ flex-direction: column;
26
+ width: 260px;
27
+ max-width: 85vw;
28
+ height: 100%;
29
+ background: var(--xeno-surface);
30
+ border-inline-end: 1px solid var(--xeno-border);
31
+ border-radius: 0; /* locked: square, flush */
32
+ color: var(--xeno-text);
33
+ transform: translateX(0);
34
+ transition: transform var(--xeno-dur) var(--xeno-ease);
35
+ will-change: transform;
36
+ }
37
+ .xeno-sidebar[data-state='closed'] {
38
+ transform: translateX(-100%);
39
+ }
40
+
41
+ /* ── Header (brand + search) ─────────────────────────────────────────────────────── */
42
+ .xeno-sidebar-header {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 12px;
46
+ flex: none;
47
+ padding: 16px;
48
+ border-bottom: 1px solid var(--xeno-border);
49
+ }
50
+ .xeno-sidebar-brand {
51
+ font-size: 15px;
52
+ font-weight: 600;
53
+ line-height: 1;
54
+ letter-spacing: 0.04em;
55
+ color: var(--xeno-text);
56
+ }
57
+ /* Make the shared TextInput fill the header width. */
58
+ .xeno-sidebar-search {
59
+ display: flex;
60
+ width: 100%;
61
+ }
62
+
63
+ /* ── Nav (scrollable list of rows) ───────────────────────────────────────────────── */
64
+ .xeno-sidebar-nav {
65
+ flex: 1;
66
+ min-height: 0;
67
+ overflow-y: auto;
68
+ padding: 8px;
69
+ }
70
+ .xeno-sidebar-section {
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: var(--xeno-sidebar-row-gap);
74
+ }
75
+ /* The rows of one group + the pill that backs them. `position: relative` makes this the pill's
76
+ containing block, so its travel is measured from the first row of THIS group — which is why the
77
+ heading can sit outside and the trick survives sections of different shapes. */
78
+ .xeno-sidebar-items {
79
+ position: relative;
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: var(--xeno-sidebar-row-gap);
83
+ }
84
+ .xeno-sidebar-section + .xeno-sidebar-section {
85
+ margin-top: 8px;
86
+ }
87
+ .xeno-sidebar-heading {
88
+ padding: 10px 10px 4px;
89
+ font-size: 11px;
90
+ font-weight: 600;
91
+ line-height: 1;
92
+ letter-spacing: 0.08em;
93
+ text-transform: uppercase;
94
+ color: var(--xeno-muted);
95
+ }
96
+
97
+ /* ── Row (self-contained; NOT ListRow) ───────────────────────────────────────────── */
98
+ .xeno-sidebar-item {
99
+ position: relative;
100
+ z-index: 1; /* over the pill, which is painted behind the whole group */
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 10px;
104
+ width: 100%;
105
+ height: var(--xeno-sidebar-row);
106
+ padding: 0 10px;
107
+ font-family: inherit;
108
+ font-size: 14px;
109
+ font-weight: 500;
110
+ line-height: 1;
111
+ text-align: left;
112
+ color: var(--xeno-muted);
113
+ background: transparent;
114
+ border: 1px solid transparent;
115
+ border-radius: var(--xeno-radius-md);
116
+ cursor: pointer;
117
+ user-select: none;
118
+ transition:
119
+ background var(--xeno-dur) ease,
120
+ color var(--xeno-dur) ease;
121
+ }
122
+ /* Neither hover nor selection paints its OWN background any more — the travelling pill below is the
123
+ single filled surface for the whole group. Two of them would read as two selections. */
124
+ .xeno-sidebar-item:hover,
125
+ .xeno-sidebar-item[data-selection='on'] {
126
+ color: var(--xeno-text);
127
+ }
128
+ .xeno-sidebar-item:focus-visible {
129
+ outline: none;
130
+ box-shadow:
131
+ 0 0 0 2px var(--xeno-surface),
132
+ 0 0 0 4px var(--xeno-border);
133
+ }
134
+ .xeno-sidebar-item[data-availability='disabled'] {
135
+ opacity: 0.4;
136
+ cursor: not-allowed;
137
+ }
138
+
139
+ /* ── The goo pill (the travelling hover surface) ──────────────────────────────────────
140
+ * ONE filled pill per group, painted behind the rows, that FLIES to whichever row you point at and
141
+ * squashes on the way — narrower and taller as it launches, back to true as it lands. That squash is
142
+ * the whole illusion: a rectangle that only slid would read as a scrollbar; one that deforms reads as
143
+ * something with mass. Adapted from One_div's gooey nav, in XENO ink: the pill is `--xeno-control`,
144
+ * the monochrome step above the panel, not an accent colour.
145
+ *
146
+ * Pure CSS, no measuring: the pill is the LAST child of the group, so every row keeps its natural
147
+ * `:nth-child` number and a hovered row reaches the pill with `~`. Travel is `row index x stride`, and
148
+ * the stride comes from the row tokens above, so it cannot drift out of step with the layout. */
149
+ .xeno-sidebar-goo {
150
+ position: absolute;
151
+ z-index: 0;
152
+ left: 0;
153
+ top: calc(var(--xeno-goo-row, 0) * (var(--xeno-sidebar-row) + var(--xeno-sidebar-row-gap)));
154
+ width: 100%;
155
+ height: var(--xeno-sidebar-row);
156
+ background: var(--xeno-control);
157
+ border-radius: var(--xeno-radius-md);
158
+ opacity: 0;
159
+ pointer-events: none;
160
+ transition:
161
+ top var(--xeno-dur-entrance, 240ms) var(--xeno-goo-ease),
162
+ opacity var(--xeno-dur, 0.14s) ease;
163
+ }
164
+ /* The pill RESTS on the selected row of its group (so the selection indicator is the same object
165
+ that flies), and is invisible in a group with no selection. */
166
+ .xeno-sidebar-item[data-selection='on'] ~ .xeno-sidebar-goo { opacity: 1; }
167
+ .xeno-sidebar-item[data-selection='on']:nth-child(1) ~ .xeno-sidebar-goo { --xeno-goo-row: 0; }
168
+ .xeno-sidebar-item[data-selection='on']:nth-child(2) ~ .xeno-sidebar-goo { --xeno-goo-row: 1; }
169
+ .xeno-sidebar-item[data-selection='on']:nth-child(3) ~ .xeno-sidebar-goo { --xeno-goo-row: 2; }
170
+ .xeno-sidebar-item[data-selection='on']:nth-child(4) ~ .xeno-sidebar-goo { --xeno-goo-row: 3; }
171
+ .xeno-sidebar-item[data-selection='on']:nth-child(5) ~ .xeno-sidebar-goo { --xeno-goo-row: 4; }
172
+ .xeno-sidebar-item[data-selection='on']:nth-child(6) ~ .xeno-sidebar-goo { --xeno-goo-row: 5; }
173
+ .xeno-sidebar-item[data-selection='on']:nth-child(7) ~ .xeno-sidebar-goo { --xeno-goo-row: 6; }
174
+ .xeno-sidebar-item[data-selection='on']:nth-child(8) ~ .xeno-sidebar-goo { --xeno-goo-row: 7; }
175
+ .xeno-sidebar-item[data-selection='on']:nth-child(9) ~ .xeno-sidebar-goo { --xeno-goo-row: 8; }
176
+ .xeno-sidebar-item[data-selection='on']:nth-child(10) ~ .xeno-sidebar-goo { --xeno-goo-row: 9; }
177
+ .xeno-sidebar-item[data-selection='on']:nth-child(11) ~ .xeno-sidebar-goo { --xeno-goo-row: 10; }
178
+ .xeno-sidebar-item[data-selection='on']:nth-child(12) ~ .xeno-sidebar-goo { --xeno-goo-row: 11; }
179
+
180
+ /* Hover takes it over. Same specificity as the rules above ON PURPOSE — these come later, so pointing
181
+ at a row always wins over the resting position, and letting go hands it back. */
182
+ .xeno-sidebar-item:hover ~ .xeno-sidebar-goo { opacity: 1; }
183
+ .xeno-sidebar-item:nth-child(1):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 0; animation: xeno-goo-1 var(--xeno-dur-entrance, 240ms) 1; }
184
+ .xeno-sidebar-item:nth-child(2):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 1; animation: xeno-goo-2 var(--xeno-dur-entrance, 240ms) 1; }
185
+ .xeno-sidebar-item:nth-child(3):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 2; animation: xeno-goo-3 var(--xeno-dur-entrance, 240ms) 1; }
186
+ .xeno-sidebar-item:nth-child(4):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 3; animation: xeno-goo-4 var(--xeno-dur-entrance, 240ms) 1; }
187
+ .xeno-sidebar-item:nth-child(5):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 4; animation: xeno-goo-5 var(--xeno-dur-entrance, 240ms) 1; }
188
+ .xeno-sidebar-item:nth-child(6):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 5; animation: xeno-goo-6 var(--xeno-dur-entrance, 240ms) 1; }
189
+ .xeno-sidebar-item:nth-child(7):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 6; animation: xeno-goo-7 var(--xeno-dur-entrance, 240ms) 1; }
190
+ .xeno-sidebar-item:nth-child(8):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 7; animation: xeno-goo-8 var(--xeno-dur-entrance, 240ms) 1; }
191
+ .xeno-sidebar-item:nth-child(9):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 8; animation: xeno-goo-9 var(--xeno-dur-entrance, 240ms) 1; }
192
+ .xeno-sidebar-item:nth-child(10):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 9; animation: xeno-goo-10 var(--xeno-dur-entrance, 240ms) 1; }
193
+ .xeno-sidebar-item:nth-child(11):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 10; animation: xeno-goo-11 var(--xeno-dur-entrance, 240ms) 1; }
194
+ .xeno-sidebar-item:nth-child(12):hover ~ .xeno-sidebar-goo { --xeno-goo-row: 11; animation: xeno-goo-12 var(--xeno-dur-entrance, 240ms) 1; }
195
+
196
+ /* One keyframe track PER ROW, all identical. They have to be separate NAMES: re-pointing at another row
197
+ only swaps which rule matches, and a browser does not restart an animation whose name did not change —
198
+ so a single shared `xeno-goo` would squash once and then never again as you slide down the list. */
199
+ @keyframes xeno-goo-1 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
200
+ @keyframes xeno-goo-2 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
201
+ @keyframes xeno-goo-3 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
202
+ @keyframes xeno-goo-4 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
203
+ @keyframes xeno-goo-5 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
204
+ @keyframes xeno-goo-6 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
205
+ @keyframes xeno-goo-7 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
206
+ @keyframes xeno-goo-8 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
207
+ @keyframes xeno-goo-9 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
208
+ @keyframes xeno-goo-10 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
209
+ @keyframes xeno-goo-11 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
210
+ @keyframes xeno-goo-12 { 0% { scale: 1 1; } 50% { scale: var(--xeno-goo-squash-x) var(--xeno-goo-squash-y); } 100% { scale: 1 1; } }
211
+
212
+ /* ── The XENO nav hover animation ─────────────────────────────────────────────────────
213
+ * The icon + label NUDGE right — the icon leads, the label follows a beat later (a small cascade), on
214
+ * the interaction easing (E2). There is no left rail: the goo pill above already marks both the hovered
215
+ * row and the active one, and a second marker on the same edge only competed with it. */
216
+ .xeno-sidebar-item-icon {
217
+ display: inline-flex;
218
+ flex: none;
219
+ color: var(--xeno-muted);
220
+ transition:
221
+ color var(--xeno-dur) ease,
222
+ transform var(--xeno-dur-entrance, 240ms) var(--xeno-ease-stagger, cubic-bezier(0.22, 0.7, 0.2, 1));
223
+ }
224
+ .xeno-sidebar-item-label {
225
+ min-width: 0;
226
+ overflow: hidden;
227
+ text-overflow: ellipsis;
228
+ white-space: nowrap;
229
+ transition: transform var(--xeno-dur-entrance, 240ms) var(--xeno-ease-stagger, cubic-bezier(0.22, 0.7, 0.2, 1));
230
+ }
231
+ .xeno-sidebar-item:hover .xeno-sidebar-item-icon,
232
+ .xeno-sidebar-item[data-selection='on'] .xeno-sidebar-item-icon {
233
+ color: var(--xeno-text);
234
+ }
235
+ .xeno-sidebar-item:hover .xeno-sidebar-item-icon {
236
+ transform: translateX(6px);
237
+ }
238
+ .xeno-sidebar-item:hover .xeno-sidebar-item-label {
239
+ transform: translateX(6px);
240
+ transition-delay: 45ms; /* label follows the icon — the cascade */
241
+ }
242
+
243
+ /* Per-glyph icon animations are global now — see `icon-motion.css`, triggered by
244
+ * `.xeno-sidebar-item:hover .xeno-element[data-glyph]`. The nudge above stays sidebar-specific. */
245
+
246
+ /* ── Footer ──────────────────────────────────────────────────────────────────────── */
247
+ .xeno-sidebar-footer {
248
+ flex: none;
249
+ padding: 12px 16px;
250
+ border-top: 1px solid var(--xeno-border);
251
+ }
252
+
253
+ /* ── Backdrop (small screens only) ───────────────────────────────────────────────── */
254
+ .xeno-sidebar-backdrop {
255
+ position: fixed;
256
+ inset: 0;
257
+ z-index: 39;
258
+ background: rgba(0, 0, 0, 0.5); /* neutral scrim — hueless, like a shadow */
259
+ opacity: 0;
260
+ visibility: hidden;
261
+ transition:
262
+ opacity var(--xeno-dur) ease,
263
+ visibility var(--xeno-dur) ease;
264
+ }
265
+ .xeno-sidebar-backdrop[data-state='open'] {
266
+ opacity: 1;
267
+ visibility: visible;
268
+ }
269
+ @media (min-width: 900px) {
270
+ .xeno-sidebar-backdrop {
271
+ display: none;
272
+ }
273
+ }
274
+
275
+ @media (prefers-reduced-motion: reduce) {
276
+ .xeno-sidebar,
277
+ .xeno-sidebar-backdrop,
278
+ .xeno-sidebar-item,
279
+ .xeno-sidebar-item-icon,
280
+ .xeno-sidebar-item-label,
281
+ .xeno-sidebar-goo {
282
+ transition: none;
283
+ }
284
+ /* The pill still MARKS the row it belongs to — it just gets there without flying or squashing. */
285
+ .xeno-sidebar-goo {
286
+ animation: none !important;
287
+ }
288
+ /* States still apply (the pill marks the row, colours change); motion does not — no nudge, no per-glyph play. */
289
+ .xeno-sidebar-item:hover .xeno-sidebar-item-icon,
290
+ .xeno-sidebar-item:hover .xeno-sidebar-item-label {
291
+ transform: none;
292
+ }
293
+ .xeno-sidebar-item-icon .xeno-element {
294
+ animation: none !important;
295
+ }
296
+ }