@xenosystem/elements-react 0.0.1 → 0.0.3

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 (49) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +41 -41
  3. package/package.json +58 -58
  4. package/src/chrome-separated.css +132 -132
  5. package/src/chrome-unified.css +72 -72
  6. package/src/containers/Badges.css +116 -116
  7. package/src/containers/avatar.css +70 -70
  8. package/src/containers/card.css +75 -75
  9. package/src/containers/chip.css +103 -103
  10. package/src/containers/list-row.css +132 -132
  11. package/src/containers/message-bubble.css +112 -112
  12. package/src/containers/panel.css +120 -120
  13. package/src/containers/table.css +102 -102
  14. package/src/containers/tile.css +17 -17
  15. package/src/content/callout.css +77 -77
  16. package/src/content/caret.css +30 -30
  17. package/src/content/code-block.css +159 -159
  18. package/src/content/collapsible.css +80 -80
  19. package/src/content/inline-code.css +14 -14
  20. package/src/content/model-picker.css +219 -219
  21. package/src/content/source-card.css +99 -99
  22. package/src/content/sources-disclosure.css +118 -118
  23. package/src/controls.css +453 -453
  24. package/src/fonts.css +34 -34
  25. package/src/forms/Checkbox.css +114 -114
  26. package/src/forms/RadioGroup.css +115 -115
  27. package/src/goo.css +97 -97
  28. package/src/icon-motion.css +1909 -1909
  29. package/src/layout/ResizablePanel.css +79 -79
  30. package/src/nav/sidebar.css +296 -296
  31. package/src/overlays/date-time-picker.css +238 -238
  32. package/src/overlays/menu.css +195 -195
  33. package/src/overlays/modal.css +167 -167
  34. package/src/overlays/picker-field.css +81 -81
  35. package/src/overlays/pill-filter.css +129 -129
  36. package/src/overlays/reveal.css +56 -56
  37. package/src/overlays/segmented-control.css +185 -185
  38. package/src/overlays/tabs.css +143 -143
  39. package/src/overlays/tooltip.css +126 -126
  40. package/src/scrollbar.css +184 -184
  41. package/src/size.css +179 -179
  42. package/src/status/progress-bar.css +88 -88
  43. package/src/status/spinner.css +33 -33
  44. package/src/status/statusindicator.css +65 -65
  45. package/src/status/steptimeline.css +137 -137
  46. package/src/status/thinkingcube.css +75 -75
  47. package/src/xeno-element.css +60 -60
  48. package/src/xeno-elements.css +71 -71
  49. package/src/xeno-theme.css +99 -99
@@ -1,112 +1,112 @@
1
- /*
2
- * MessageBubble — the chat message container. Monochrome SHELL: the container carries no colour of its
3
- * own; only `children` may bring content colour. A `user` turn is a right-aligned bubble; an `assistant`
4
- * turn is a bare left-aligned prose block (the locked design). State rides the root as `data-role` and,
5
- * while a turn is still being written, `data-streaming`. Only the theme custom properties are used here.
6
- */
7
-
8
- .xeno-message {
9
- display: flex;
10
- flex-direction: column;
11
- gap: 6px;
12
- max-width: 100%;
13
- /* How wide a user bubble may run before it wraps. A knob rather than a prop, because the answer is
14
- usually a MEDIA QUERY — a bubble that reads well at 80% on a desktop column is cramped at 80% of
15
- a phone — and a breakpoint belongs in the product's stylesheet, not threaded through React state.
16
- Override it on the turn, on the transcript, or in an `@media` block; the default stands alone. */
17
- --xeno-message-max: 80%;
18
- }
19
-
20
- /* Sides: user hugs the right rail, assistant the left. */
21
- .xeno-message[data-role='user'] {
22
- align-items: flex-end;
23
- }
24
- .xeno-message[data-role='assistant'] {
25
- align-items: flex-start;
26
- }
27
-
28
- .xeno-message-body {
29
- min-width: 0;
30
- color: var(--xeno-text);
31
- font-size: 15px;
32
- line-height: 1.55;
33
- overflow-wrap: anywhere;
34
- }
35
-
36
- /* Attachments — what was sent WITH the message, above the body and outside the bubble. They share the
37
- turn's alignment and take no fill of their own: an image already has edges, and wrapping it in a
38
- border plus 12px of surface only draws a second frame around the first. */
39
- .xeno-message-attachments {
40
- display: flex;
41
- flex-wrap: wrap;
42
- gap: 6px;
43
- max-width: var(--xeno-message-max);
44
- min-width: 0;
45
- }
46
- .xeno-message[data-role='user'] .xeno-message-attachments {
47
- justify-content: flex-end;
48
- }
49
-
50
- /* User: a bubble — surface fill, hairline border, card radius with the bottom-right corner squared. */
51
- .xeno-message[data-role='user'] .xeno-message-body {
52
- max-width: var(--xeno-message-max);
53
- /* A person's own words keep the shape they were typed in — the blank line they left between two
54
- thoughts is part of the message. Only on this side: an assistant turn is rendered prose, where
55
- the source newlines are markup and honouring them would double every paragraph break. */
56
- white-space: pre-wrap;
57
- padding: 12px;
58
- background: var(--xeno-surface);
59
- border: 1px solid var(--xeno-border);
60
- border-radius: var(--xeno-radius-card);
61
- border-bottom-right-radius: var(--xeno-radius-xs);
62
- }
63
-
64
- /* Assistant: no bubble, just prose (locked design). */
65
- .xeno-message[data-role='assistant'] .xeno-message-body {
66
- max-width: 100%;
67
- }
68
-
69
- /* Meta row: timestamp + hover action bar, aligned under the turn. */
70
- .xeno-message-meta {
71
- display: flex;
72
- align-items: center;
73
- gap: 8px;
74
- min-height: 20px;
75
- }
76
-
77
- .xeno-message-time {
78
- font-size: 12px;
79
- line-height: 1;
80
- color: var(--xeno-muted);
81
- font-variant-numeric: tabular-nums;
82
- }
83
-
84
- /* Hover action bar — fades in on hover/focus of the whole turn. */
85
- .xeno-message-actions {
86
- display: inline-flex;
87
- align-items: center;
88
- gap: 2px;
89
- opacity: 0;
90
- /* Transparent is not gone. At `opacity: 0` these buttons still take the pointer, so the strip of
91
- nothing under a message swallowed clicks and — worse — sometimes ran Copy or Regenerate on a turn
92
- whose bar the reader could not see. Faded out means out of reach; hover puts it back. */
93
- pointer-events: none;
94
- transition: opacity 0.18s var(--xeno-ease);
95
- }
96
- .xeno-message:hover .xeno-message-actions,
97
- .xeno-message:focus-within .xeno-message-actions {
98
- opacity: 1;
99
- pointer-events: auto;
100
- }
101
-
102
- /* While streaming the bar stays hidden and inert, even on hover/focus (this rule wins by source order). */
103
- .xeno-message[data-streaming] .xeno-message-actions {
104
- opacity: 0;
105
- pointer-events: none;
106
- }
107
-
108
- @media (prefers-reduced-motion: reduce) {
109
- .xeno-message-actions {
110
- transition: none;
111
- }
112
- }
1
+ /*
2
+ * MessageBubble — the chat message container. Monochrome SHELL: the container carries no colour of its
3
+ * own; only `children` may bring content colour. A `user` turn is a right-aligned bubble; an `assistant`
4
+ * turn is a bare left-aligned prose block (the locked design). State rides the root as `data-role` and,
5
+ * while a turn is still being written, `data-streaming`. Only the theme custom properties are used here.
6
+ */
7
+
8
+ .xeno-message {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 6px;
12
+ max-width: 100%;
13
+ /* How wide a user bubble may run before it wraps. A knob rather than a prop, because the answer is
14
+ usually a MEDIA QUERY — a bubble that reads well at 80% on a desktop column is cramped at 80% of
15
+ a phone — and a breakpoint belongs in the product's stylesheet, not threaded through React state.
16
+ Override it on the turn, on the transcript, or in an `@media` block; the default stands alone. */
17
+ --xeno-message-max: 80%;
18
+ }
19
+
20
+ /* Sides: user hugs the right rail, assistant the left. */
21
+ .xeno-message[data-role='user'] {
22
+ align-items: flex-end;
23
+ }
24
+ .xeno-message[data-role='assistant'] {
25
+ align-items: flex-start;
26
+ }
27
+
28
+ .xeno-message-body {
29
+ min-width: 0;
30
+ color: var(--xeno-text);
31
+ font-size: 15px;
32
+ line-height: 1.55;
33
+ overflow-wrap: anywhere;
34
+ }
35
+
36
+ /* Attachments — what was sent WITH the message, above the body and outside the bubble. They share the
37
+ turn's alignment and take no fill of their own: an image already has edges, and wrapping it in a
38
+ border plus 12px of surface only draws a second frame around the first. */
39
+ .xeno-message-attachments {
40
+ display: flex;
41
+ flex-wrap: wrap;
42
+ gap: 6px;
43
+ max-width: var(--xeno-message-max);
44
+ min-width: 0;
45
+ }
46
+ .xeno-message[data-role='user'] .xeno-message-attachments {
47
+ justify-content: flex-end;
48
+ }
49
+
50
+ /* User: a bubble — surface fill, hairline border, card radius with the bottom-right corner squared. */
51
+ .xeno-message[data-role='user'] .xeno-message-body {
52
+ max-width: var(--xeno-message-max);
53
+ /* A person's own words keep the shape they were typed in — the blank line they left between two
54
+ thoughts is part of the message. Only on this side: an assistant turn is rendered prose, where
55
+ the source newlines are markup and honouring them would double every paragraph break. */
56
+ white-space: pre-wrap;
57
+ padding: 12px;
58
+ background: var(--xeno-surface);
59
+ border: 1px solid var(--xeno-border);
60
+ border-radius: var(--xeno-radius-card);
61
+ border-bottom-right-radius: var(--xeno-radius-xs);
62
+ }
63
+
64
+ /* Assistant: no bubble, just prose (locked design). */
65
+ .xeno-message[data-role='assistant'] .xeno-message-body {
66
+ max-width: 100%;
67
+ }
68
+
69
+ /* Meta row: timestamp + hover action bar, aligned under the turn. */
70
+ .xeno-message-meta {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 8px;
74
+ min-height: 20px;
75
+ }
76
+
77
+ .xeno-message-time {
78
+ font-size: 12px;
79
+ line-height: 1;
80
+ color: var(--xeno-muted);
81
+ font-variant-numeric: tabular-nums;
82
+ }
83
+
84
+ /* Hover action bar — fades in on hover/focus of the whole turn. */
85
+ .xeno-message-actions {
86
+ display: inline-flex;
87
+ align-items: center;
88
+ gap: 2px;
89
+ opacity: 0;
90
+ /* Transparent is not gone. At `opacity: 0` these buttons still take the pointer, so the strip of
91
+ nothing under a message swallowed clicks and — worse — sometimes ran Copy or Regenerate on a turn
92
+ whose bar the reader could not see. Faded out means out of reach; hover puts it back. */
93
+ pointer-events: none;
94
+ transition: opacity 0.18s var(--xeno-ease);
95
+ }
96
+ .xeno-message:hover .xeno-message-actions,
97
+ .xeno-message:focus-within .xeno-message-actions {
98
+ opacity: 1;
99
+ pointer-events: auto;
100
+ }
101
+
102
+ /* While streaming the bar stays hidden and inert, even on hover/focus (this rule wins by source order). */
103
+ .xeno-message[data-streaming] .xeno-message-actions {
104
+ opacity: 0;
105
+ pointer-events: none;
106
+ }
107
+
108
+ @media (prefers-reduced-motion: reduce) {
109
+ .xeno-message-actions {
110
+ transition: none;
111
+ }
112
+ }
@@ -1,120 +1,120 @@
1
- /*
2
- * Panel — a chrome stack: heading, body, footer.
3
- *
4
- * Every construction value comes from the `--xeno-chrome-*` tokens, so the SAME DOM renders as either
5
- * chrome. Under `separated` (the default, required by DESIGN_SYSTEM.md §"Panel Structure") the wrapper
6
- * paints the page colour and the three plates float on it with a 2px gap. Under `unified` the gap
7
- * collapses to 0, the plates go transparent, the wrapper paints the surface and clips them, and the
8
- * dividers come back — one card, exactly as this set rendered before the chrome axis existed.
9
- *
10
- * Nothing here knows a theme exists. That is the point: no component is built twice.
11
- *
12
- * Scrolling is chrome-independent — give the panel a bounded height and the body scrolls on its own
13
- * (`min-height: 0` lets a flex child shrink below its content).
14
- */
15
- .xeno-panel {
16
- display: flex;
17
- flex-direction: column;
18
- gap: var(--xeno-chrome-gap);
19
- padding: var(--xeno-chrome-inset);
20
- min-height: 0;
21
- background: var(--xeno-chrome-shell);
22
- /* `separated` draws no border on the wrapper — the plates carry their own. `unified` needs one,
23
- and gets it because the shell is painted and clipped. Both read the same token. */
24
- border: 1px solid var(--xeno-chrome-border);
25
- border-radius: var(--xeno-chrome-radius);
26
- overflow: hidden;
27
- color: var(--xeno-text);
28
- }
29
-
30
- /*
31
- * Heading plate. §"Panel Structure" fixes it at 34px so headings line up with sidebar navigation
32
- * across a product; the padding sets the text inside that rather than defining the height.
33
- */
34
- .xeno-panel-header {
35
- display: flex;
36
- align-items: center;
37
- justify-content: space-between;
38
- gap: 12px;
39
- flex: 0 0 auto;
40
- min-height: var(--xeno-chrome-panel-header-min-h);
41
- padding: var(--xeno-chrome-panel-header-pad);
42
- background: var(--xeno-chrome-plate);
43
- -webkit-backdrop-filter: var(--xeno-chrome-plate-filter);
44
- backdrop-filter: var(--xeno-chrome-plate-filter);
45
- border-bottom: var(--xeno-chrome-divider);
46
- }
47
-
48
- /*
49
- * §"Headers and Footers": the header text is "small, uppercase, with letter-spacing. It functions as
50
- * a label, not a headline." Matches the convention the code-block language chip already uses.
51
- */
52
- .xeno-panel-title {
53
- min-width: 0;
54
- color: var(--xeno-chrome-title-color);
55
- font-size: var(--xeno-chrome-title-size);
56
- font-weight: 600;
57
- letter-spacing: var(--xeno-chrome-title-spacing);
58
- text-transform: var(--xeno-chrome-title-transform);
59
- line-height: 1.2;
60
- overflow: hidden;
61
- text-overflow: ellipsis;
62
- white-space: nowrap;
63
- }
64
-
65
- .xeno-panel-actions {
66
- display: inline-flex;
67
- align-items: center;
68
- gap: 6px;
69
- flex: 0 0 auto;
70
- }
71
-
72
- /* Body plate — the scroll region. */
73
- .xeno-panel-body {
74
- flex: 1 1 auto;
75
- min-height: 0;
76
- padding: var(--xeno-chrome-panel-body-pad);
77
- overflow: auto;
78
- background: var(--xeno-chrome-body);
79
- color: var(--xeno-text);
80
- }
81
-
82
- /*
83
- * Footer plate — §"Headers and Footers": "Footer padding matches header padding exactly. Headers and
84
- * footers are visually symmetric." Every box metric below is the header's, duplicated deliberately
85
- * rather than shared through a common class: the symmetry is a design rule a future change must break
86
- * on purpose, and a shared class would let one drift silently while the other did not.
87
- *
88
- * `space-between` gives the dismiss-left / confirm-right arrangement the products already use.
89
- */
90
- .xeno-panel-footer {
91
- display: flex;
92
- align-items: center;
93
- justify-content: space-between;
94
- gap: 12px;
95
- flex: 0 0 auto;
96
- min-height: var(--xeno-chrome-panel-header-min-h);
97
- padding: var(--xeno-chrome-panel-header-pad);
98
- background: var(--xeno-chrome-plate);
99
- border-top: var(--xeno-chrome-divider);
100
- }
101
-
102
- /*
103
- * §4 "Radius Is Per-Corner, Not Per-Element": a corner is rounded only where the surface meets
104
- * NOTHING. Each plate faces its neighbour across the 2px gap, so the facing corners are SQUARE — a
105
- * uniform radius on every plate is the tell that this section was skipped, and it was.
106
- *
107
- * Declared on first/last child rather than per plate so a stack with no header, or no footer, still
108
- * presents a rounded outer edge instead of a square one against the rounded shell.
109
- */
110
- .xeno-panel > * {
111
- border-radius: 0;
112
- }
113
- .xeno-panel > :first-child {
114
- border-top-left-radius: var(--xeno-chrome-radius-plate);
115
- border-top-right-radius: var(--xeno-chrome-radius-plate);
116
- }
117
- .xeno-panel > :last-child {
118
- border-bottom-left-radius: var(--xeno-chrome-radius-plate);
119
- border-bottom-right-radius: var(--xeno-chrome-radius-plate);
120
- }
1
+ /*
2
+ * Panel — a chrome stack: heading, body, footer.
3
+ *
4
+ * Every construction value comes from the `--xeno-chrome-*` tokens, so the SAME DOM renders as either
5
+ * chrome. Under `separated` (the default, required by DESIGN_SYSTEM.md §"Panel Structure") the wrapper
6
+ * paints the page colour and the three plates float on it with a 2px gap. Under `unified` the gap
7
+ * collapses to 0, the plates go transparent, the wrapper paints the surface and clips them, and the
8
+ * dividers come back — one card, exactly as this set rendered before the chrome axis existed.
9
+ *
10
+ * Nothing here knows a theme exists. That is the point: no component is built twice.
11
+ *
12
+ * Scrolling is chrome-independent — give the panel a bounded height and the body scrolls on its own
13
+ * (`min-height: 0` lets a flex child shrink below its content).
14
+ */
15
+ .xeno-panel {
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: var(--xeno-chrome-gap);
19
+ padding: var(--xeno-chrome-inset);
20
+ min-height: 0;
21
+ background: var(--xeno-chrome-shell);
22
+ /* `separated` draws no border on the wrapper — the plates carry their own. `unified` needs one,
23
+ and gets it because the shell is painted and clipped. Both read the same token. */
24
+ border: 1px solid var(--xeno-chrome-border);
25
+ border-radius: var(--xeno-chrome-radius);
26
+ overflow: hidden;
27
+ color: var(--xeno-text);
28
+ }
29
+
30
+ /*
31
+ * Heading plate. §"Panel Structure" fixes it at 34px so headings line up with sidebar navigation
32
+ * across a product; the padding sets the text inside that rather than defining the height.
33
+ */
34
+ .xeno-panel-header {
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: space-between;
38
+ gap: 12px;
39
+ flex: 0 0 auto;
40
+ min-height: var(--xeno-chrome-panel-header-min-h);
41
+ padding: var(--xeno-chrome-panel-header-pad);
42
+ background: var(--xeno-chrome-plate);
43
+ -webkit-backdrop-filter: var(--xeno-chrome-plate-filter);
44
+ backdrop-filter: var(--xeno-chrome-plate-filter);
45
+ border-bottom: var(--xeno-chrome-divider);
46
+ }
47
+
48
+ /*
49
+ * §"Headers and Footers": the header text is "small, uppercase, with letter-spacing. It functions as
50
+ * a label, not a headline." Matches the convention the code-block language chip already uses.
51
+ */
52
+ .xeno-panel-title {
53
+ min-width: 0;
54
+ color: var(--xeno-chrome-title-color);
55
+ font-size: var(--xeno-chrome-title-size);
56
+ font-weight: 600;
57
+ letter-spacing: var(--xeno-chrome-title-spacing);
58
+ text-transform: var(--xeno-chrome-title-transform);
59
+ line-height: 1.2;
60
+ overflow: hidden;
61
+ text-overflow: ellipsis;
62
+ white-space: nowrap;
63
+ }
64
+
65
+ .xeno-panel-actions {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ gap: 6px;
69
+ flex: 0 0 auto;
70
+ }
71
+
72
+ /* Body plate — the scroll region. */
73
+ .xeno-panel-body {
74
+ flex: 1 1 auto;
75
+ min-height: 0;
76
+ padding: var(--xeno-chrome-panel-body-pad);
77
+ overflow: auto;
78
+ background: var(--xeno-chrome-body);
79
+ color: var(--xeno-text);
80
+ }
81
+
82
+ /*
83
+ * Footer plate — §"Headers and Footers": "Footer padding matches header padding exactly. Headers and
84
+ * footers are visually symmetric." Every box metric below is the header's, duplicated deliberately
85
+ * rather than shared through a common class: the symmetry is a design rule a future change must break
86
+ * on purpose, and a shared class would let one drift silently while the other did not.
87
+ *
88
+ * `space-between` gives the dismiss-left / confirm-right arrangement the products already use.
89
+ */
90
+ .xeno-panel-footer {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: space-between;
94
+ gap: 12px;
95
+ flex: 0 0 auto;
96
+ min-height: var(--xeno-chrome-panel-header-min-h);
97
+ padding: var(--xeno-chrome-panel-header-pad);
98
+ background: var(--xeno-chrome-plate);
99
+ border-top: var(--xeno-chrome-divider);
100
+ }
101
+
102
+ /*
103
+ * §4 "Radius Is Per-Corner, Not Per-Element": a corner is rounded only where the surface meets
104
+ * NOTHING. Each plate faces its neighbour across the 2px gap, so the facing corners are SQUARE — a
105
+ * uniform radius on every plate is the tell that this section was skipped, and it was.
106
+ *
107
+ * Declared on first/last child rather than per plate so a stack with no header, or no footer, still
108
+ * presents a rounded outer edge instead of a square one against the rounded shell.
109
+ */
110
+ .xeno-panel > * {
111
+ border-radius: 0;
112
+ }
113
+ .xeno-panel > :first-child {
114
+ border-top-left-radius: var(--xeno-chrome-radius-plate);
115
+ border-top-right-radius: var(--xeno-chrome-radius-plate);
116
+ }
117
+ .xeno-panel > :last-child {
118
+ border-bottom-left-radius: var(--xeno-chrome-radius-plate);
119
+ border-bottom-right-radius: var(--xeno-chrome-radius-plate);
120
+ }
@@ -1,102 +1,102 @@
1
- /*
2
- * Dense table styles — the locked grammar for Table / DataTable. CSS-first: appearance is driven by
3
- * the `xeno-*` classes plus the `data-align` / `data-selection` seams, reading only the theme custom
4
- * properties from `xeno-theme.css`. Border-collapse with hairline BOTTOM row rules only — no vertical
5
- * rules, no zebra striping. Monochrome throughout; no literal colours.
6
- */
7
-
8
- .xeno-table {
9
- width: 100%;
10
- border-collapse: collapse;
11
- font-size: 0.9em;
12
- color: var(--xeno-text);
13
- }
14
-
15
- .xeno-th,
16
- .xeno-td {
17
- padding: 6px 11px;
18
- text-align: left;
19
- vertical-align: middle;
20
- }
21
-
22
- .xeno-th {
23
- color: var(--xeno-muted);
24
- font-weight: 600;
25
- font-size: 0.85em;
26
- }
27
-
28
- /* Bottom border only on rows — the sole rule; header row divider comes for free. */
29
- .xeno-tr {
30
- border-bottom: 1px solid var(--xeno-border);
31
- }
32
-
33
- /* ── Tracking a row ───────────────────────────────────────────────────────────────────
34
- * A dense table is READ by running your eye along a line, and until now nothing under the pointer
35
- * answered — which is what makes a table feel like a picture of data instead of data.
36
- *
37
- * The restraint here is deliberate and is the opposite of the choice made for the toggles: no flood,
38
- * no travel. You cross a dozen rows in a second while scanning, and anything with a 300ms gesture
39
- * would still be arriving when you have already left. A wash that is simply THERE is the correct
40
- * answer; the only thing that moves is a hairline on the leading edge, and it moves in 140ms.
41
- *
42
- * Body rows only. The header is not a row you track, and neither is the empty-state row — there is
43
- * nothing under the pointer there to answer for. */
44
- .xeno-tbody .xeno-tr:hover:not(:has(.xeno-table-empty)) {
45
- background: var(--xeno-hover);
46
- }
47
-
48
- /* Selection axis — a step ABOVE the hover wash, not the same token. They used to share
49
- `--xeno-hover`, so once rows answered the pointer a hovered row and a selected row were the same
50
- colour, and the selection stopped meaning anything. */
51
- .xeno-tr[data-selection='on'] {
52
- background: var(--xeno-control);
53
- }
54
-
55
- /* The leading-edge mark: a hairline that grows from the middle of the row, on hover and on selection.
56
- It lives on the first cell rather than the row, because a `<tr>` is not a reliable containing block
57
- under `border-collapse` — cells are. */
58
- .xeno-tbody .xeno-tr > :first-child {
59
- position: relative;
60
- }
61
- .xeno-tbody .xeno-tr > :first-child::before {
62
- content: '';
63
- position: absolute;
64
- left: 0;
65
- top: 0;
66
- bottom: 0;
67
- width: 2px;
68
- background: var(--xeno-text);
69
- border-radius: var(--xeno-radius-hair, 1.5px);
70
- transform: scaleY(0);
71
- transform-origin: center;
72
- }
73
- .xeno-tbody .xeno-tr:hover:not(:has(.xeno-table-empty)) > :first-child::before,
74
- .xeno-tbody .xeno-tr[data-selection='on'] > :first-child::before {
75
- transform: scaleY(1);
76
- }
77
-
78
- /* Alignment overrides (data-* seam). */
79
- .xeno-th[data-align='center'],
80
- .xeno-td[data-align='center'] {
81
- text-align: center;
82
- }
83
- .xeno-th[data-align='right'],
84
- .xeno-td[data-align='right'] {
85
- text-align: right;
86
- }
87
-
88
- /* Empty state — muted, roomy, column-spanning. */
89
- .xeno-table-empty {
90
- padding: 18px 11px;
91
- color: var(--xeno-muted);
92
- text-align: center;
93
- }
94
-
95
- @media (prefers-reduced-motion: no-preference) {
96
- .xeno-tr {
97
- transition: background var(--xeno-dur) var(--xeno-ease);
98
- }
99
- .xeno-tbody .xeno-tr > :first-child::before {
100
- transition: transform var(--xeno-dur) var(--xeno-ease);
101
- }
102
- }
1
+ /*
2
+ * Dense table styles — the locked grammar for Table / DataTable. CSS-first: appearance is driven by
3
+ * the `xeno-*` classes plus the `data-align` / `data-selection` seams, reading only the theme custom
4
+ * properties from `xeno-theme.css`. Border-collapse with hairline BOTTOM row rules only — no vertical
5
+ * rules, no zebra striping. Monochrome throughout; no literal colours.
6
+ */
7
+
8
+ .xeno-table {
9
+ width: 100%;
10
+ border-collapse: collapse;
11
+ font-size: 0.9em;
12
+ color: var(--xeno-text);
13
+ }
14
+
15
+ .xeno-th,
16
+ .xeno-td {
17
+ padding: 6px 11px;
18
+ text-align: left;
19
+ vertical-align: middle;
20
+ }
21
+
22
+ .xeno-th {
23
+ color: var(--xeno-muted);
24
+ font-weight: 600;
25
+ font-size: 0.85em;
26
+ }
27
+
28
+ /* Bottom border only on rows — the sole rule; header row divider comes for free. */
29
+ .xeno-tr {
30
+ border-bottom: 1px solid var(--xeno-border);
31
+ }
32
+
33
+ /* ── Tracking a row ───────────────────────────────────────────────────────────────────
34
+ * A dense table is READ by running your eye along a line, and until now nothing under the pointer
35
+ * answered — which is what makes a table feel like a picture of data instead of data.
36
+ *
37
+ * The restraint here is deliberate and is the opposite of the choice made for the toggles: no flood,
38
+ * no travel. You cross a dozen rows in a second while scanning, and anything with a 300ms gesture
39
+ * would still be arriving when you have already left. A wash that is simply THERE is the correct
40
+ * answer; the only thing that moves is a hairline on the leading edge, and it moves in 140ms.
41
+ *
42
+ * Body rows only. The header is not a row you track, and neither is the empty-state row — there is
43
+ * nothing under the pointer there to answer for. */
44
+ .xeno-tbody .xeno-tr:hover:not(:has(.xeno-table-empty)) {
45
+ background: var(--xeno-hover);
46
+ }
47
+
48
+ /* Selection axis — a step ABOVE the hover wash, not the same token. They used to share
49
+ `--xeno-hover`, so once rows answered the pointer a hovered row and a selected row were the same
50
+ colour, and the selection stopped meaning anything. */
51
+ .xeno-tr[data-selection='on'] {
52
+ background: var(--xeno-control);
53
+ }
54
+
55
+ /* The leading-edge mark: a hairline that grows from the middle of the row, on hover and on selection.
56
+ It lives on the first cell rather than the row, because a `<tr>` is not a reliable containing block
57
+ under `border-collapse` — cells are. */
58
+ .xeno-tbody .xeno-tr > :first-child {
59
+ position: relative;
60
+ }
61
+ .xeno-tbody .xeno-tr > :first-child::before {
62
+ content: '';
63
+ position: absolute;
64
+ left: 0;
65
+ top: 0;
66
+ bottom: 0;
67
+ width: 2px;
68
+ background: var(--xeno-text);
69
+ border-radius: var(--xeno-radius-hair, 1.5px);
70
+ transform: scaleY(0);
71
+ transform-origin: center;
72
+ }
73
+ .xeno-tbody .xeno-tr:hover:not(:has(.xeno-table-empty)) > :first-child::before,
74
+ .xeno-tbody .xeno-tr[data-selection='on'] > :first-child::before {
75
+ transform: scaleY(1);
76
+ }
77
+
78
+ /* Alignment overrides (data-* seam). */
79
+ .xeno-th[data-align='center'],
80
+ .xeno-td[data-align='center'] {
81
+ text-align: center;
82
+ }
83
+ .xeno-th[data-align='right'],
84
+ .xeno-td[data-align='right'] {
85
+ text-align: right;
86
+ }
87
+
88
+ /* Empty state — muted, roomy, column-spanning. */
89
+ .xeno-table-empty {
90
+ padding: 18px 11px;
91
+ color: var(--xeno-muted);
92
+ text-align: center;
93
+ }
94
+
95
+ @media (prefers-reduced-motion: no-preference) {
96
+ .xeno-tr {
97
+ transition: background var(--xeno-dur) var(--xeno-ease);
98
+ }
99
+ .xeno-tbody .xeno-tr > :first-child::before {
100
+ transition: transform var(--xeno-dur) var(--xeno-ease);
101
+ }
102
+ }