@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.
- package/LICENSE +18 -18
- package/README.md +41 -41
- package/package.json +58 -58
- package/src/chrome-separated.css +132 -132
- package/src/chrome-unified.css +72 -72
- package/src/containers/Badges.css +116 -116
- package/src/containers/avatar.css +70 -70
- package/src/containers/card.css +75 -75
- package/src/containers/chip.css +103 -103
- package/src/containers/list-row.css +132 -132
- package/src/containers/message-bubble.css +112 -112
- package/src/containers/panel.css +120 -120
- package/src/containers/table.css +102 -102
- package/src/containers/tile.css +17 -17
- package/src/content/callout.css +77 -77
- package/src/content/caret.css +30 -30
- package/src/content/code-block.css +159 -159
- package/src/content/collapsible.css +80 -80
- package/src/content/inline-code.css +14 -14
- package/src/content/model-picker.css +219 -219
- package/src/content/source-card.css +99 -99
- package/src/content/sources-disclosure.css +118 -118
- package/src/controls.css +453 -453
- package/src/fonts.css +34 -34
- package/src/forms/Checkbox.css +114 -114
- package/src/forms/RadioGroup.css +115 -115
- package/src/goo.css +97 -97
- package/src/icon-motion.css +1909 -1909
- package/src/layout/ResizablePanel.css +79 -79
- package/src/nav/sidebar.css +296 -296
- package/src/overlays/date-time-picker.css +238 -238
- package/src/overlays/menu.css +195 -195
- package/src/overlays/modal.css +167 -167
- package/src/overlays/picker-field.css +81 -81
- package/src/overlays/pill-filter.css +129 -129
- package/src/overlays/reveal.css +56 -56
- package/src/overlays/segmented-control.css +185 -185
- package/src/overlays/tabs.css +143 -143
- package/src/overlays/tooltip.css +126 -126
- package/src/scrollbar.css +184 -184
- package/src/size.css +179 -179
- package/src/status/progress-bar.css +88 -88
- package/src/status/spinner.css +33 -33
- package/src/status/statusindicator.css +65 -65
- package/src/status/steptimeline.css +137 -137
- package/src/status/thinkingcube.css +75 -75
- package/src/xeno-element.css +60 -60
- package/src/xeno-elements.css +71 -71
- package/src/xeno-theme.css +99 -99
package/src/fonts.css
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The XENO typeface — vendored, not merely declared.
|
|
3
|
-
*
|
|
4
|
-
* `font-family: 'Inter'` alone SILENTLY falls back when Inter is not installed, and on Windows that
|
|
5
|
-
* means Segoe UI: a fine reading face and the wrong one for dense dark chrome. The app then looks
|
|
6
|
-
* identical to having declared nothing, which is most of why an interface reads as cheap. It is a
|
|
7
|
-
* live bug in xeno-hub today. Vendoring the woff2 means it works offline and needs no CSP exception.
|
|
8
|
-
*
|
|
9
|
-
* OPT-IN, deliberately. This is a library, not an app: forcing ~100 kB of font into every consumer's
|
|
10
|
-
* bundle is not ours to decide, and a product with its own licensed face must be able to say no. Same
|
|
11
|
-
* shape as the `motion` entry point — the platform SHIPS it so nobody re-derives it, and the consumer
|
|
12
|
-
* chooses.
|
|
13
|
-
*
|
|
14
|
-
* import '@xenosystem/elements-react/fonts.css'
|
|
15
|
-
*
|
|
16
|
-
* A product that skips this gets the fallback stack below and should know that it has.
|
|
17
|
-
*/
|
|
18
|
-
@import '@fontsource-variable/inter/index.css';
|
|
19
|
-
|
|
20
|
-
.xeno {
|
|
21
|
-
/*
|
|
22
|
-
* `--xeno-font-*` and NOT `--xeno-text-*`. That prefix is already the colour ramp, and reusing it
|
|
23
|
-
* would silently overwrite `--xeno-text` app-wide — a documented failure, not a hypothetical.
|
|
24
|
-
*/
|
|
25
|
-
--xeno-font-sans: 'Inter Variable', 'Inter', -apple-system, 'Segoe UI', system-ui, sans-serif;
|
|
26
|
-
--xeno-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
27
|
-
|
|
28
|
-
font-family: var(--xeno-font-sans);
|
|
29
|
-
/* cv11 disambiguates 1/l/I — the one that matters most in dense UI and in code-adjacent labels. */
|
|
30
|
-
font-feature-settings: 'cv02' 1, 'cv03' 1, 'cv04' 1, 'cv11' 1, 'ss01' 1;
|
|
31
|
-
font-optical-sizing: auto;
|
|
32
|
-
/* Inter runs a touch wide at UI sizes. */
|
|
33
|
-
letter-spacing: -0.006em;
|
|
34
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* The XENO typeface — vendored, not merely declared.
|
|
3
|
+
*
|
|
4
|
+
* `font-family: 'Inter'` alone SILENTLY falls back when Inter is not installed, and on Windows that
|
|
5
|
+
* means Segoe UI: a fine reading face and the wrong one for dense dark chrome. The app then looks
|
|
6
|
+
* identical to having declared nothing, which is most of why an interface reads as cheap. It is a
|
|
7
|
+
* live bug in xeno-hub today. Vendoring the woff2 means it works offline and needs no CSP exception.
|
|
8
|
+
*
|
|
9
|
+
* OPT-IN, deliberately. This is a library, not an app: forcing ~100 kB of font into every consumer's
|
|
10
|
+
* bundle is not ours to decide, and a product with its own licensed face must be able to say no. Same
|
|
11
|
+
* shape as the `motion` entry point — the platform SHIPS it so nobody re-derives it, and the consumer
|
|
12
|
+
* chooses.
|
|
13
|
+
*
|
|
14
|
+
* import '@xenosystem/elements-react/fonts.css'
|
|
15
|
+
*
|
|
16
|
+
* A product that skips this gets the fallback stack below and should know that it has.
|
|
17
|
+
*/
|
|
18
|
+
@import '@fontsource-variable/inter/index.css';
|
|
19
|
+
|
|
20
|
+
.xeno {
|
|
21
|
+
/*
|
|
22
|
+
* `--xeno-font-*` and NOT `--xeno-text-*`. That prefix is already the colour ramp, and reusing it
|
|
23
|
+
* would silently overwrite `--xeno-text` app-wide — a documented failure, not a hypothetical.
|
|
24
|
+
*/
|
|
25
|
+
--xeno-font-sans: 'Inter Variable', 'Inter', -apple-system, 'Segoe UI', system-ui, sans-serif;
|
|
26
|
+
--xeno-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
27
|
+
|
|
28
|
+
font-family: var(--xeno-font-sans);
|
|
29
|
+
/* cv11 disambiguates 1/l/I — the one that matters most in dense UI and in code-adjacent labels. */
|
|
30
|
+
font-feature-settings: 'cv02' 1, 'cv03' 1, 'cv04' 1, 'cv11' 1, 'ss01' 1;
|
|
31
|
+
font-optical-sizing: auto;
|
|
32
|
+
/* Inter runs a touch wide at UI sizes. */
|
|
33
|
+
letter-spacing: -0.006em;
|
|
34
|
+
}
|
package/src/forms/Checkbox.css
CHANGED
|
@@ -1,114 +1,114 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* `<Checkbox>` — rounded-SQUARE box + optional label. CSS-first: paint is keyed off `data-selection`
|
|
3
|
-
* (off|on|mixed) and `data-availability`. Monochrome shell — only theme tokens, no literal colours.
|
|
4
|
-
*/
|
|
5
|
-
.xeno-checkbox {
|
|
6
|
-
display: inline-flex;
|
|
7
|
-
align-items: center;
|
|
8
|
-
gap: 8px;
|
|
9
|
-
margin: 0;
|
|
10
|
-
padding: 0;
|
|
11
|
-
background: transparent;
|
|
12
|
-
border: none;
|
|
13
|
-
color: var(--xeno-text);
|
|
14
|
-
font-family: inherit;
|
|
15
|
-
font-size: 14px;
|
|
16
|
-
line-height: 1;
|
|
17
|
-
text-align: left;
|
|
18
|
-
cursor: pointer;
|
|
19
|
-
user-select: none;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.xeno-checkbox-box {
|
|
23
|
-
display: inline-flex;
|
|
24
|
-
align-items: center;
|
|
25
|
-
justify-content: center;
|
|
26
|
-
flex: none;
|
|
27
|
-
width: 17px;
|
|
28
|
-
height: 17px;
|
|
29
|
-
/* The stamped glyph inherits this via currentColor; only visible once the box is filled. */
|
|
30
|
-
color: var(--xeno-on-accent);
|
|
31
|
-
background: var(--xeno-canvas);
|
|
32
|
-
border: 1px solid var(--xeno-border);
|
|
33
|
-
border-radius: var(--xeno-radius-sm);
|
|
34
|
-
transition:
|
|
35
|
-
background var(--xeno-dur) var(--xeno-ease),
|
|
36
|
-
border-color var(--xeno-dur) var(--xeno-ease);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.xeno-checkbox:hover:not(:disabled) .xeno-checkbox-box {
|
|
40
|
-
border-color: var(--xeno-muted);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.xeno-checkbox[data-selection='on'] .xeno-checkbox-box,
|
|
44
|
-
.xeno-checkbox[data-selection='mixed'] .xeno-checkbox-box {
|
|
45
|
-
background: var(--xeno-text);
|
|
46
|
-
border-color: var(--xeno-text);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/* ── Ticking ──────────────────────────────────────────────────────────────────────────
|
|
50
|
-
* The tick DRAWS itself, heel to tip. Nothing new is authored here: `xeno.check` already carries that
|
|
51
|
-
* animation in `icon-motion.css`, where it plays on hover — the glyph's own rule sets `--part-anim`,
|
|
52
|
-
* `--part-dur` and the `stroke-dasharray` it needs. All this does is play the SAME animation on the
|
|
53
|
-
* event that actually matters for a checkbox, which is the tick appearing.
|
|
54
|
-
*
|
|
55
|
-
* Reading it through `var(--part-anim)` rather than naming the keyframe keeps the two loosely coupled:
|
|
56
|
-
* redraw the check glyph's animation and the checkbox follows it, with no edit here.
|
|
57
|
-
*
|
|
58
|
-
* The 900ms the gallery uses is a showcase pace; a control that answers a click wants ~a third of that.
|
|
59
|
-
*
|
|
60
|
-
* Everything is gated on `data-motion="ready"`, which the component sets only after `checked` has
|
|
61
|
-
* genuinely changed — otherwise a form rendered with ticked boxes draws all of them on load. */
|
|
62
|
-
.xeno-checkbox[data-motion='ready'][data-selection='on'] .xeno-part[data-part='0'] {
|
|
63
|
-
--part-dur: 340ms;
|
|
64
|
-
animation: var(--part-anim) var(--part-dur) var(--xeno-icon-ease);
|
|
65
|
-
}
|
|
66
|
-
/* The box answers with it: a quick compress and overshoot, so the fill lands with the tick instead of
|
|
67
|
-
snapping into place a beat ahead of it. */
|
|
68
|
-
.xeno-checkbox[data-motion='ready'][data-selection='on'] .xeno-checkbox-box,
|
|
69
|
-
.xeno-checkbox[data-motion='ready'][data-selection='mixed'] .xeno-checkbox-box {
|
|
70
|
-
animation: xeno-checkbox-stamp 280ms var(--xeno-ease-overshoot, cubic-bezier(0.34, 1.4, 0.5, 1));
|
|
71
|
-
}
|
|
72
|
-
@keyframes xeno-checkbox-stamp {
|
|
73
|
-
0% { scale: 0.86; }
|
|
74
|
-
60% { scale: 1.06; }
|
|
75
|
-
100% { scale: 1; }
|
|
76
|
-
}
|
|
77
|
-
/* Indeterminate has no stroke to draw — the bar just arrives, widening from the middle. */
|
|
78
|
-
.xeno-checkbox[data-motion='ready'][data-selection='mixed'] .xeno-part[data-part='0'] {
|
|
79
|
-
animation: xeno-checkbox-dash 240ms var(--xeno-ease, cubic-bezier(0.22, 1, 0.36, 1));
|
|
80
|
-
}
|
|
81
|
-
@keyframes xeno-checkbox-dash {
|
|
82
|
-
0% { scale: 0 1; opacity: 0; }
|
|
83
|
-
100% { scale: 1 1; opacity: 1; }
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.xeno-checkbox-label {
|
|
87
|
-
color: var(--xeno-text);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.xeno-checkbox:focus-visible {
|
|
91
|
-
outline: none;
|
|
92
|
-
}
|
|
93
|
-
.xeno-checkbox:focus-visible .xeno-checkbox-box {
|
|
94
|
-
box-shadow:
|
|
95
|
-
0 0 0 2px var(--xeno-canvas),
|
|
96
|
-
0 0 0 4px var(--xeno-border);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.xeno-checkbox:disabled,
|
|
100
|
-
.xeno-checkbox[data-availability='disabled'] {
|
|
101
|
-
opacity: 0.4;
|
|
102
|
-
cursor: not-allowed;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
@media (prefers-reduced-motion: reduce) {
|
|
106
|
-
.xeno-checkbox-box {
|
|
107
|
-
transition: none;
|
|
108
|
-
}
|
|
109
|
-
/* The tick is still there — it just does not draw itself in. */
|
|
110
|
-
.xeno-checkbox-box,
|
|
111
|
-
.xeno-checkbox .xeno-part {
|
|
112
|
-
animation: none !important;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* `<Checkbox>` — rounded-SQUARE box + optional label. CSS-first: paint is keyed off `data-selection`
|
|
3
|
+
* (off|on|mixed) and `data-availability`. Monochrome shell — only theme tokens, no literal colours.
|
|
4
|
+
*/
|
|
5
|
+
.xeno-checkbox {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: 8px;
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
background: transparent;
|
|
12
|
+
border: none;
|
|
13
|
+
color: var(--xeno-text);
|
|
14
|
+
font-family: inherit;
|
|
15
|
+
font-size: 14px;
|
|
16
|
+
line-height: 1;
|
|
17
|
+
text-align: left;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
user-select: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.xeno-checkbox-box {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
flex: none;
|
|
27
|
+
width: 17px;
|
|
28
|
+
height: 17px;
|
|
29
|
+
/* The stamped glyph inherits this via currentColor; only visible once the box is filled. */
|
|
30
|
+
color: var(--xeno-on-accent);
|
|
31
|
+
background: var(--xeno-canvas);
|
|
32
|
+
border: 1px solid var(--xeno-border);
|
|
33
|
+
border-radius: var(--xeno-radius-sm);
|
|
34
|
+
transition:
|
|
35
|
+
background var(--xeno-dur) var(--xeno-ease),
|
|
36
|
+
border-color var(--xeno-dur) var(--xeno-ease);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.xeno-checkbox:hover:not(:disabled) .xeno-checkbox-box {
|
|
40
|
+
border-color: var(--xeno-muted);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.xeno-checkbox[data-selection='on'] .xeno-checkbox-box,
|
|
44
|
+
.xeno-checkbox[data-selection='mixed'] .xeno-checkbox-box {
|
|
45
|
+
background: var(--xeno-text);
|
|
46
|
+
border-color: var(--xeno-text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ── Ticking ──────────────────────────────────────────────────────────────────────────
|
|
50
|
+
* The tick DRAWS itself, heel to tip. Nothing new is authored here: `xeno.check` already carries that
|
|
51
|
+
* animation in `icon-motion.css`, where it plays on hover — the glyph's own rule sets `--part-anim`,
|
|
52
|
+
* `--part-dur` and the `stroke-dasharray` it needs. All this does is play the SAME animation on the
|
|
53
|
+
* event that actually matters for a checkbox, which is the tick appearing.
|
|
54
|
+
*
|
|
55
|
+
* Reading it through `var(--part-anim)` rather than naming the keyframe keeps the two loosely coupled:
|
|
56
|
+
* redraw the check glyph's animation and the checkbox follows it, with no edit here.
|
|
57
|
+
*
|
|
58
|
+
* The 900ms the gallery uses is a showcase pace; a control that answers a click wants ~a third of that.
|
|
59
|
+
*
|
|
60
|
+
* Everything is gated on `data-motion="ready"`, which the component sets only after `checked` has
|
|
61
|
+
* genuinely changed — otherwise a form rendered with ticked boxes draws all of them on load. */
|
|
62
|
+
.xeno-checkbox[data-motion='ready'][data-selection='on'] .xeno-part[data-part='0'] {
|
|
63
|
+
--part-dur: 340ms;
|
|
64
|
+
animation: var(--part-anim) var(--part-dur) var(--xeno-icon-ease);
|
|
65
|
+
}
|
|
66
|
+
/* The box answers with it: a quick compress and overshoot, so the fill lands with the tick instead of
|
|
67
|
+
snapping into place a beat ahead of it. */
|
|
68
|
+
.xeno-checkbox[data-motion='ready'][data-selection='on'] .xeno-checkbox-box,
|
|
69
|
+
.xeno-checkbox[data-motion='ready'][data-selection='mixed'] .xeno-checkbox-box {
|
|
70
|
+
animation: xeno-checkbox-stamp 280ms var(--xeno-ease-overshoot, cubic-bezier(0.34, 1.4, 0.5, 1));
|
|
71
|
+
}
|
|
72
|
+
@keyframes xeno-checkbox-stamp {
|
|
73
|
+
0% { scale: 0.86; }
|
|
74
|
+
60% { scale: 1.06; }
|
|
75
|
+
100% { scale: 1; }
|
|
76
|
+
}
|
|
77
|
+
/* Indeterminate has no stroke to draw — the bar just arrives, widening from the middle. */
|
|
78
|
+
.xeno-checkbox[data-motion='ready'][data-selection='mixed'] .xeno-part[data-part='0'] {
|
|
79
|
+
animation: xeno-checkbox-dash 240ms var(--xeno-ease, cubic-bezier(0.22, 1, 0.36, 1));
|
|
80
|
+
}
|
|
81
|
+
@keyframes xeno-checkbox-dash {
|
|
82
|
+
0% { scale: 0 1; opacity: 0; }
|
|
83
|
+
100% { scale: 1 1; opacity: 1; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.xeno-checkbox-label {
|
|
87
|
+
color: var(--xeno-text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.xeno-checkbox:focus-visible {
|
|
91
|
+
outline: none;
|
|
92
|
+
}
|
|
93
|
+
.xeno-checkbox:focus-visible .xeno-checkbox-box {
|
|
94
|
+
box-shadow:
|
|
95
|
+
0 0 0 2px var(--xeno-canvas),
|
|
96
|
+
0 0 0 4px var(--xeno-border);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.xeno-checkbox:disabled,
|
|
100
|
+
.xeno-checkbox[data-availability='disabled'] {
|
|
101
|
+
opacity: 0.4;
|
|
102
|
+
cursor: not-allowed;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (prefers-reduced-motion: reduce) {
|
|
106
|
+
.xeno-checkbox-box {
|
|
107
|
+
transition: none;
|
|
108
|
+
}
|
|
109
|
+
/* The tick is still there — it just does not draw itself in. */
|
|
110
|
+
.xeno-checkbox-box,
|
|
111
|
+
.xeno-checkbox .xeno-part {
|
|
112
|
+
animation: none !important;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/forms/RadioGroup.css
CHANGED
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* `<RadioGroup>` / `<RadioRow>` — SQUARE markers (radius-xs) with a smaller inner square, never circles.
|
|
3
|
-
* CSS-first: paint is keyed off `data-selection` (off|on) and `data-availability`; layout off
|
|
4
|
-
* `data-orientation`. Monochrome shell — only theme tokens, no literal colours.
|
|
5
|
-
*/
|
|
6
|
-
.xeno-radiogroup {
|
|
7
|
-
display: flex;
|
|
8
|
-
flex-direction: column;
|
|
9
|
-
gap: 2px;
|
|
10
|
-
}
|
|
11
|
-
.xeno-radiogroup[data-orientation='horizontal'] {
|
|
12
|
-
flex-direction: row;
|
|
13
|
-
gap: 12px;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.xeno-radio {
|
|
17
|
-
display: inline-flex;
|
|
18
|
-
align-items: center;
|
|
19
|
-
gap: 9px;
|
|
20
|
-
margin: 0;
|
|
21
|
-
padding: 6px 8px;
|
|
22
|
-
background: transparent;
|
|
23
|
-
border: 1px solid transparent;
|
|
24
|
-
border-radius: var(--xeno-radius-md);
|
|
25
|
-
color: var(--xeno-text);
|
|
26
|
-
font-family: inherit;
|
|
27
|
-
font-size: 14px;
|
|
28
|
-
line-height: 1;
|
|
29
|
-
text-align: left;
|
|
30
|
-
cursor: pointer;
|
|
31
|
-
user-select: none;
|
|
32
|
-
transition: background var(--xeno-dur) var(--xeno-ease);
|
|
33
|
-
}
|
|
34
|
-
.xeno-radio:hover:not(:disabled) {
|
|
35
|
-
background: var(--xeno-hover);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.xeno-radio-marker {
|
|
39
|
-
display: inline-flex;
|
|
40
|
-
align-items: center;
|
|
41
|
-
justify-content: center;
|
|
42
|
-
flex: none;
|
|
43
|
-
width: 16px;
|
|
44
|
-
height: 16px;
|
|
45
|
-
background: var(--xeno-canvas);
|
|
46
|
-
border: 1px solid var(--xeno-border);
|
|
47
|
-
border-radius: var(--xeno-radius-xs);
|
|
48
|
-
transition: border-color var(--xeno-dur) var(--xeno-ease);
|
|
49
|
-
}
|
|
50
|
-
.xeno-radio:hover:not(:disabled) .xeno-radio-marker {
|
|
51
|
-
border-color: var(--xeno-muted);
|
|
52
|
-
}
|
|
53
|
-
.xeno-radio[data-selection='on'] .xeno-radio-marker {
|
|
54
|
-
border-color: var(--xeno-text);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/* The mark is a SQUARE, and at 8px it has to be drawn like one. It carried `--xeno-radius-xs` (3px),
|
|
58
|
-
which is 37% of its own width — enough rounding that at this size it reads as a dot rather than as a
|
|
59
|
-
square. `--xeno-radius-hair` puts it at 19%, in the same family as the switch knob's 21%. */
|
|
60
|
-
.xeno-radio-dot {
|
|
61
|
-
width: 8px;
|
|
62
|
-
height: 8px;
|
|
63
|
-
background: var(--xeno-text);
|
|
64
|
-
border-radius: var(--xeno-radius-hair, 1.5px);
|
|
65
|
-
scale: 0;
|
|
66
|
-
opacity: 0;
|
|
67
|
-
transition:
|
|
68
|
-
scale var(--xeno-dur) var(--xeno-ease),
|
|
69
|
-
opacity var(--xeno-dur) var(--xeno-ease);
|
|
70
|
-
}
|
|
71
|
-
.xeno-radio[data-selection='on'] .xeno-radio-dot {
|
|
72
|
-
scale: 1;
|
|
73
|
-
opacity: 1;
|
|
74
|
-
}
|
|
75
|
-
/* Choosing STAMPS: the square overshoots and settles, so the choice lands rather than fades in. Only on
|
|
76
|
-
the way in — letting go is quiet, and the transition above still handles it.
|
|
77
|
-
Armed at the group, which is safe HERE and would not be on a multi-select: exactly one radio can be
|
|
78
|
-
on, so the only one whose rule starts matching when the group arms is the one that just changed. */
|
|
79
|
-
.xeno-radiogroup[data-motion='ready'] .xeno-radio[data-selection='on'] .xeno-radio-dot {
|
|
80
|
-
animation: xeno-radio-stamp 260ms var(--xeno-ease-overshoot, cubic-bezier(0.34, 1.4, 0.5, 1));
|
|
81
|
-
}
|
|
82
|
-
@keyframes xeno-radio-stamp {
|
|
83
|
-
0% { scale: 0; opacity: 0; }
|
|
84
|
-
45% { scale: 1.22; opacity: 1; }
|
|
85
|
-
100% { scale: 1; opacity: 1; }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.xeno-radio-label {
|
|
89
|
-
color: var(--xeno-text);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.xeno-radio:focus-visible {
|
|
93
|
-
outline: none;
|
|
94
|
-
box-shadow:
|
|
95
|
-
0 0 0 2px var(--xeno-canvas),
|
|
96
|
-
0 0 0 4px var(--xeno-border);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.xeno-radio:disabled,
|
|
100
|
-
.xeno-radio[data-availability='disabled'] {
|
|
101
|
-
opacity: 0.4;
|
|
102
|
-
cursor: not-allowed;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
@media (prefers-reduced-motion: reduce) {
|
|
106
|
-
.xeno-radio,
|
|
107
|
-
.xeno-radio-marker,
|
|
108
|
-
.xeno-radio-dot {
|
|
109
|
-
transition: none;
|
|
110
|
-
}
|
|
111
|
-
/* The mark still appears — it just does not stamp. */
|
|
112
|
-
.xeno-radio-dot {
|
|
113
|
-
animation: none !important;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* `<RadioGroup>` / `<RadioRow>` — SQUARE markers (radius-xs) with a smaller inner square, never circles.
|
|
3
|
+
* CSS-first: paint is keyed off `data-selection` (off|on) and `data-availability`; layout off
|
|
4
|
+
* `data-orientation`. Monochrome shell — only theme tokens, no literal colours.
|
|
5
|
+
*/
|
|
6
|
+
.xeno-radiogroup {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: 2px;
|
|
10
|
+
}
|
|
11
|
+
.xeno-radiogroup[data-orientation='horizontal'] {
|
|
12
|
+
flex-direction: row;
|
|
13
|
+
gap: 12px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.xeno-radio {
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 9px;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 6px 8px;
|
|
22
|
+
background: transparent;
|
|
23
|
+
border: 1px solid transparent;
|
|
24
|
+
border-radius: var(--xeno-radius-md);
|
|
25
|
+
color: var(--xeno-text);
|
|
26
|
+
font-family: inherit;
|
|
27
|
+
font-size: 14px;
|
|
28
|
+
line-height: 1;
|
|
29
|
+
text-align: left;
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
user-select: none;
|
|
32
|
+
transition: background var(--xeno-dur) var(--xeno-ease);
|
|
33
|
+
}
|
|
34
|
+
.xeno-radio:hover:not(:disabled) {
|
|
35
|
+
background: var(--xeno-hover);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.xeno-radio-marker {
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
flex: none;
|
|
43
|
+
width: 16px;
|
|
44
|
+
height: 16px;
|
|
45
|
+
background: var(--xeno-canvas);
|
|
46
|
+
border: 1px solid var(--xeno-border);
|
|
47
|
+
border-radius: var(--xeno-radius-xs);
|
|
48
|
+
transition: border-color var(--xeno-dur) var(--xeno-ease);
|
|
49
|
+
}
|
|
50
|
+
.xeno-radio:hover:not(:disabled) .xeno-radio-marker {
|
|
51
|
+
border-color: var(--xeno-muted);
|
|
52
|
+
}
|
|
53
|
+
.xeno-radio[data-selection='on'] .xeno-radio-marker {
|
|
54
|
+
border-color: var(--xeno-text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* The mark is a SQUARE, and at 8px it has to be drawn like one. It carried `--xeno-radius-xs` (3px),
|
|
58
|
+
which is 37% of its own width — enough rounding that at this size it reads as a dot rather than as a
|
|
59
|
+
square. `--xeno-radius-hair` puts it at 19%, in the same family as the switch knob's 21%. */
|
|
60
|
+
.xeno-radio-dot {
|
|
61
|
+
width: 8px;
|
|
62
|
+
height: 8px;
|
|
63
|
+
background: var(--xeno-text);
|
|
64
|
+
border-radius: var(--xeno-radius-hair, 1.5px);
|
|
65
|
+
scale: 0;
|
|
66
|
+
opacity: 0;
|
|
67
|
+
transition:
|
|
68
|
+
scale var(--xeno-dur) var(--xeno-ease),
|
|
69
|
+
opacity var(--xeno-dur) var(--xeno-ease);
|
|
70
|
+
}
|
|
71
|
+
.xeno-radio[data-selection='on'] .xeno-radio-dot {
|
|
72
|
+
scale: 1;
|
|
73
|
+
opacity: 1;
|
|
74
|
+
}
|
|
75
|
+
/* Choosing STAMPS: the square overshoots and settles, so the choice lands rather than fades in. Only on
|
|
76
|
+
the way in — letting go is quiet, and the transition above still handles it.
|
|
77
|
+
Armed at the group, which is safe HERE and would not be on a multi-select: exactly one radio can be
|
|
78
|
+
on, so the only one whose rule starts matching when the group arms is the one that just changed. */
|
|
79
|
+
.xeno-radiogroup[data-motion='ready'] .xeno-radio[data-selection='on'] .xeno-radio-dot {
|
|
80
|
+
animation: xeno-radio-stamp 260ms var(--xeno-ease-overshoot, cubic-bezier(0.34, 1.4, 0.5, 1));
|
|
81
|
+
}
|
|
82
|
+
@keyframes xeno-radio-stamp {
|
|
83
|
+
0% { scale: 0; opacity: 0; }
|
|
84
|
+
45% { scale: 1.22; opacity: 1; }
|
|
85
|
+
100% { scale: 1; opacity: 1; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.xeno-radio-label {
|
|
89
|
+
color: var(--xeno-text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.xeno-radio:focus-visible {
|
|
93
|
+
outline: none;
|
|
94
|
+
box-shadow:
|
|
95
|
+
0 0 0 2px var(--xeno-canvas),
|
|
96
|
+
0 0 0 4px var(--xeno-border);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.xeno-radio:disabled,
|
|
100
|
+
.xeno-radio[data-availability='disabled'] {
|
|
101
|
+
opacity: 0.4;
|
|
102
|
+
cursor: not-allowed;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (prefers-reduced-motion: reduce) {
|
|
106
|
+
.xeno-radio,
|
|
107
|
+
.xeno-radio-marker,
|
|
108
|
+
.xeno-radio-dot {
|
|
109
|
+
transition: none;
|
|
110
|
+
}
|
|
111
|
+
/* The mark still appears — it just does not stamp. */
|
|
112
|
+
.xeno-radio-dot {
|
|
113
|
+
animation: none !important;
|
|
114
|
+
}
|
|
115
|
+
}
|