@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.
- package/LICENSE +18 -0
- package/README.md +41 -0
- package/dist/XenoElement.d.ts +39 -0
- package/dist/XenoElement.js +25 -0
- package/dist/containers/Avatar.d.ts +42 -0
- package/dist/containers/Avatar.js +29 -0
- package/dist/containers/Badges.d.ts +61 -0
- package/dist/containers/Badges.js +43 -0
- package/dist/containers/Card.d.ts +24 -0
- package/dist/containers/Card.js +5 -0
- package/dist/containers/Chip.d.ts +34 -0
- package/dist/containers/Chip.js +17 -0
- package/dist/containers/DataTable.d.ts +28 -0
- package/dist/containers/DataTable.js +13 -0
- package/dist/containers/ListRow.d.ts +37 -0
- package/dist/containers/ListRow.js +40 -0
- package/dist/containers/MessageBubble.d.ts +33 -0
- package/dist/containers/MessageBubble.js +9 -0
- package/dist/containers/Panel.d.ts +24 -0
- package/dist/containers/Panel.js +6 -0
- package/dist/containers/Table.d.ts +39 -0
- package/dist/containers/Table.js +20 -0
- package/dist/containers/Tile.d.ts +19 -0
- package/dist/containers/Tile.js +7 -0
- package/dist/content/Callout.d.ts +32 -0
- package/dist/content/Callout.js +10 -0
- package/dist/content/Caret.d.ts +9 -0
- package/dist/content/Caret.js +5 -0
- package/dist/content/CodeBlock.d.ts +36 -0
- package/dist/content/CodeBlock.js +50 -0
- package/dist/content/Collapsible.d.ts +24 -0
- package/dist/content/Collapsible.js +19 -0
- package/dist/content/InlineCode.d.ts +12 -0
- package/dist/content/InlineCode.js +5 -0
- package/dist/content/ModelPicker.d.ts +45 -0
- package/dist/content/ModelPicker.js +124 -0
- package/dist/content/SourceCard.d.ts +23 -0
- package/dist/content/SourceCard.js +18 -0
- package/dist/content/SourcesDisclosure.d.ts +26 -0
- package/dist/content/SourcesDisclosure.js +20 -0
- package/dist/controls/Button.d.ts +39 -0
- package/dist/controls/Button.js +16 -0
- package/dist/controls/ConfirmButton.d.ts +22 -0
- package/dist/controls/ConfirmButton.js +22 -0
- package/dist/controls/IconButton.d.ts +45 -0
- package/dist/controls/IconButton.js +15 -0
- package/dist/controls/Switch.d.ts +11 -0
- package/dist/controls/Switch.js +23 -0
- package/dist/controls/TextInput.d.ts +13 -0
- package/dist/controls/TextInput.js +6 -0
- package/dist/controls/Textarea.d.ts +8 -0
- package/dist/controls/Textarea.js +5 -0
- package/dist/controls/ToggleButton.d.ts +23 -0
- package/dist/controls/ToggleButton.js +23 -0
- package/dist/controls/util.d.ts +48 -0
- package/dist/controls/util.js +23 -0
- package/dist/forms/Checkbox.d.ts +18 -0
- package/dist/forms/Checkbox.js +28 -0
- package/dist/forms/RadioGroup.d.ts +51 -0
- package/dist/forms/RadioGroup.js +83 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +81 -0
- package/dist/layout/ResizablePanel.d.ts +40 -0
- package/dist/layout/ResizablePanel.js +108 -0
- package/dist/nav/Sidebar.d.ts +60 -0
- package/dist/nav/Sidebar.js +84 -0
- package/dist/overlays/DateTimePicker.d.ts +71 -0
- package/dist/overlays/DateTimePicker.js +191 -0
- package/dist/overlays/Menu.d.ts +28 -0
- package/dist/overlays/Menu.js +32 -0
- package/dist/overlays/MenuItem.d.ts +71 -0
- package/dist/overlays/MenuItem.js +15 -0
- package/dist/overlays/Modal.d.ts +36 -0
- package/dist/overlays/Modal.js +27 -0
- package/dist/overlays/PickerField.d.ts +32 -0
- package/dist/overlays/PickerField.js +40 -0
- package/dist/overlays/PillFilter.d.ts +25 -0
- package/dist/overlays/PillFilter.js +41 -0
- package/dist/overlays/Popover.d.ts +32 -0
- package/dist/overlays/Popover.js +38 -0
- package/dist/overlays/Reveal.d.ts +20 -0
- package/dist/overlays/Reveal.js +45 -0
- package/dist/overlays/SegmentedControl.d.ts +23 -0
- package/dist/overlays/SegmentedControl.js +43 -0
- package/dist/overlays/Tabs.d.ts +32 -0
- package/dist/overlays/Tabs.js +142 -0
- package/dist/overlays/Tooltip.d.ts +23 -0
- package/dist/overlays/Tooltip.js +61 -0
- package/dist/status/ProgressBar.d.ts +20 -0
- package/dist/status/ProgressBar.js +11 -0
- package/dist/status/Spinner.d.ts +16 -0
- package/dist/status/Spinner.js +12 -0
- package/dist/status/StatusIndicator.d.ts +19 -0
- package/dist/status/StatusIndicator.js +11 -0
- package/dist/status/StepTimeline.d.ts +26 -0
- package/dist/status/StepTimeline.js +16 -0
- package/dist/status/ThinkingCube.d.ts +24 -0
- package/dist/status/ThinkingCube.js +11 -0
- package/dist/useDialog.d.ts +45 -0
- package/dist/useDialog.js +122 -0
- package/dist/useGooPill.d.ts +57 -0
- package/dist/useGooPill.js +76 -0
- package/dist/useMenu.d.ts +50 -0
- package/dist/useMenu.js +119 -0
- package/dist/useTabs.d.ts +88 -0
- package/dist/useTabs.js +93 -0
- package/package.json +58 -0
- package/src/chrome-separated.css +132 -0
- package/src/chrome-unified.css +72 -0
- package/src/containers/Badges.css +116 -0
- package/src/containers/avatar.css +70 -0
- package/src/containers/card.css +75 -0
- package/src/containers/chip.css +103 -0
- package/src/containers/list-row.css +132 -0
- package/src/containers/message-bubble.css +112 -0
- package/src/containers/panel.css +120 -0
- package/src/containers/table.css +102 -0
- package/src/containers/tile.css +17 -0
- package/src/content/callout.css +77 -0
- package/src/content/caret.css +30 -0
- package/src/content/code-block.css +159 -0
- package/src/content/collapsible.css +80 -0
- package/src/content/inline-code.css +14 -0
- package/src/content/model-picker.css +219 -0
- package/src/content/source-card.css +99 -0
- package/src/content/sources-disclosure.css +118 -0
- package/src/controls.css +453 -0
- package/src/fonts.css +34 -0
- package/src/forms/Checkbox.css +114 -0
- package/src/forms/RadioGroup.css +115 -0
- package/src/goo.css +97 -0
- package/src/icon-motion.css +1909 -0
- package/src/layout/ResizablePanel.css +79 -0
- package/src/nav/sidebar.css +296 -0
- package/src/overlays/date-time-picker.css +238 -0
- package/src/overlays/menu.css +195 -0
- package/src/overlays/modal.css +167 -0
- package/src/overlays/picker-field.css +81 -0
- package/src/overlays/pill-filter.css +129 -0
- package/src/overlays/reveal.css +56 -0
- package/src/overlays/segmented-control.css +185 -0
- package/src/overlays/tabs.css +143 -0
- package/src/overlays/tooltip.css +126 -0
- package/src/scrollbar.css +184 -0
- package/src/size.css +179 -0
- package/src/status/progress-bar.css +88 -0
- package/src/status/spinner.css +33 -0
- package/src/status/statusindicator.css +65 -0
- package/src/status/steptimeline.css +137 -0
- package/src/status/thinkingcube.css +75 -0
- package/src/xeno-element.css +60 -0
- package/src/xeno-elements.css +71 -0
- package/src/xeno-theme.css +99 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* <SegmentedControl> — connected options in one bordered track (rounded square). CSS-first: the chosen
|
|
3
|
+
* option carries `data-selection="on"`; a disabled option carries `data-availability="disabled"`. Size
|
|
4
|
+
* metrics arrive as inline vars. Monochrome only.
|
|
5
|
+
*
|
|
6
|
+
* The fill is ONE thumb that slides between options, not a background each option paints for itself —
|
|
7
|
+
* the same shared-indicator idea as the sidebar's goo pill, laid on its side. A fill that simply
|
|
8
|
+
* appears on the new option and vanishes from the old one tells you nothing about where it came from;
|
|
9
|
+
* one that travels does.
|
|
10
|
+
*
|
|
11
|
+
* How it can slide with no measuring. The track is a GRID of `1fr` columns, so every segment is exactly
|
|
12
|
+
* as wide as the widest label — which makes a segment's width pure arithmetic (track minus padding
|
|
13
|
+
* minus gaps, over the count) instead of something only layout knows. React supplies the two numbers it
|
|
14
|
+
* already has, `--xeno-seg-count` and `--xeno-seg-index`, as plain data; CSS does the rest. No
|
|
15
|
+
* measuring, no animation runtime, and the thumb lands on the segment to the pixel.
|
|
16
|
+
*
|
|
17
|
+
* It must be grid, not `flex: 1 1 0`. A flex item's `min-width` defaults to `auto`, i.e. its own
|
|
18
|
+
* min-content, so each option gets clamped to the width of its own label and the segments come out
|
|
19
|
+
* UNEQUAL — the thumb then computes an average width and sits beside the option instead of on it.
|
|
20
|
+
* Grid's `1fr` has no such floor: every track resolves to the largest contribution, so they are equal
|
|
21
|
+
* by construction and the arithmetic below is exact.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/* Registering the index as a real `<number>` is what makes the slide smooth. An unregistered custom
|
|
25
|
+
property is a string as far as the engine is concerned: it flips from "0" to "2" in one frame, and
|
|
26
|
+
anything computed from it flips with it. Typed, it interpolates — so the transition below runs on the
|
|
27
|
+
NUMBER, and the thumb's offset is simply recomputed every frame from a value that is genuinely
|
|
28
|
+
travelling. That is also why the thumb itself does not transition `translate`: two easings stacked on
|
|
29
|
+
the same movement smear it. */
|
|
30
|
+
@property --xeno-seg-index {
|
|
31
|
+
syntax: '<number>';
|
|
32
|
+
inherits: true;
|
|
33
|
+
initial-value: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.xeno-segmented {
|
|
37
|
+
/* The goo. Mid-flight the thumb ELONGATES along the direction it is travelling and thins across it —
|
|
38
|
+
a body of liquid reaching for where it is going — then rebounds a hair the other way as it lands.
|
|
39
|
+
Both amounts are multiplied by how far it is going (`--xeno-seg-dist`, from React), so a jump
|
|
40
|
+
across the whole control stretches visibly more than a step to the neighbour. That is the part
|
|
41
|
+
that sells it as matter rather than as a rectangle with an easing curve. */
|
|
42
|
+
--xeno-seg-stretch: 0.16;
|
|
43
|
+
--xeno-seg-squeeze: 0.07;
|
|
44
|
+
--xeno-seg-goo-x: calc(1 + var(--xeno-seg-stretch) * var(--xeno-seg-dist, 1));
|
|
45
|
+
--xeno-seg-goo-y: calc(1 - var(--xeno-seg-squeeze) * var(--xeno-seg-dist, 1));
|
|
46
|
+
/* The rebound: a small counter-squash on the way out of the stretch. */
|
|
47
|
+
--xeno-seg-goo-x2: calc(1 - var(--xeno-seg-squeeze) * 0.45);
|
|
48
|
+
--xeno-seg-goo-y2: calc(1 + var(--xeno-seg-squeeze) * 0.45);
|
|
49
|
+
/* ONE duration for the travel and the deformation. If they drift apart the stretch peaks somewhere
|
|
50
|
+
other than mid-flight and the whole thing reads as a glitch. */
|
|
51
|
+
--xeno-seg-dur: 300ms;
|
|
52
|
+
--xeno-seg-gap: 2px;
|
|
53
|
+
/* The track's own inset, named once: the thumb's width subtracts exactly this, twice. Hard-coding it
|
|
54
|
+
in two places is how a thumb drifts a couple of pixels off its segment. */
|
|
55
|
+
--xeno-seg-pad: 2px;
|
|
56
|
+
--xeno-seg-ease: cubic-bezier(0.34, 1.12, 0.5, 1);
|
|
57
|
+
position: relative;
|
|
58
|
+
isolation: isolate;
|
|
59
|
+
display: grid;
|
|
60
|
+
grid-auto-flow: column;
|
|
61
|
+
grid-auto-columns: 1fr; /* equal segments, by construction */
|
|
62
|
+
/* Explicit content-width so a column parent (align-items:stretch) cannot stretch the track to full
|
|
63
|
+
width — a segmented control always hugs its options. */
|
|
64
|
+
width: fit-content;
|
|
65
|
+
align-items: stretch;
|
|
66
|
+
gap: var(--xeno-seg-gap);
|
|
67
|
+
padding: var(--xeno-seg-pad);
|
|
68
|
+
background: var(--xeno-surface);
|
|
69
|
+
border: 1px solid var(--xeno-border);
|
|
70
|
+
border-radius: var(--xeno-radius-md);
|
|
71
|
+
/* The travel itself: the index slides, and everything computed from it slides with it. */
|
|
72
|
+
transition: --xeno-seg-index var(--xeno-seg-dur) var(--xeno-seg-ease);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* The travelling fill. `100%` in `translate` is the thumb's OWN width, so one segment plus one gap is
|
|
76
|
+
exactly `100% + var(--xeno-seg-gap)` — the stride, without ever naming a pixel width. */
|
|
77
|
+
.xeno-segmented-thumb {
|
|
78
|
+
position: absolute;
|
|
79
|
+
z-index: -1;
|
|
80
|
+
top: var(--xeno-seg-pad);
|
|
81
|
+
bottom: var(--xeno-seg-pad);
|
|
82
|
+
left: var(--xeno-seg-pad);
|
|
83
|
+
/* `100%` here is the track's PADDING box, so the options' row is that minus the padding on both
|
|
84
|
+
sides; take out the gaps and divide by the count and you have one segment, exactly. */
|
|
85
|
+
width: calc(
|
|
86
|
+
(100% - var(--xeno-seg-pad) * 2 - var(--xeno-seg-gap) * (var(--xeno-seg-count, 1) - 1)) /
|
|
87
|
+
var(--xeno-seg-count, 1)
|
|
88
|
+
);
|
|
89
|
+
translate: calc(var(--xeno-seg-index, 0) * (100% + var(--xeno-seg-gap))) 0;
|
|
90
|
+
background: var(--xeno-control);
|
|
91
|
+
border-radius: var(--xeno-radius-xs);
|
|
92
|
+
pointer-events: none;
|
|
93
|
+
}
|
|
94
|
+
/* Nothing selected (a `value` matching no option) — no thumb rather than a thumb parked on the first. */
|
|
95
|
+
.xeno-segmented[data-selection='off'] .xeno-segmented-thumb {
|
|
96
|
+
opacity: 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.xeno-segmented-option {
|
|
100
|
+
position: relative;
|
|
101
|
+
z-index: 1; /* over the thumb */
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
min-width: 0; /* the grid track owns the width; do not let the label put a floor under it */
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
gap: var(--xeno-gap, 6px);
|
|
107
|
+
height: calc(var(--xeno-h, 32px) - 4px);
|
|
108
|
+
padding: 0 var(--xeno-padx, 12px);
|
|
109
|
+
font-family: inherit;
|
|
110
|
+
font-size: var(--xeno-font, 14px);
|
|
111
|
+
font-weight: 500;
|
|
112
|
+
line-height: 1;
|
|
113
|
+
white-space: nowrap;
|
|
114
|
+
color: var(--xeno-muted);
|
|
115
|
+
background: transparent;
|
|
116
|
+
border: none;
|
|
117
|
+
border-radius: var(--xeno-radius-xs);
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
user-select: none;
|
|
120
|
+
transition:
|
|
121
|
+
color var(--xeno-dur) var(--xeno-ease),
|
|
122
|
+
background var(--xeno-dur) var(--xeno-ease);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Hover brightens the LABEL and nothing else. A tint behind an unselected option is a second filled
|
|
126
|
+
surface sliding under the thumb — which is exactly what reads as the background being wrong mid-swap.
|
|
127
|
+
One control, one fill: the thumb. */
|
|
128
|
+
.xeno-segmented-option:hover:not([data-availability='disabled']):not([data-selection='on']) {
|
|
129
|
+
color: var(--xeno-text);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* The selected option no longer paints a fill of its own — the thumb under it is the fill. */
|
|
133
|
+
.xeno-segmented-option[data-selection='on'] {
|
|
134
|
+
color: var(--xeno-text);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.xeno-segmented-option:focus-visible {
|
|
138
|
+
outline: none;
|
|
139
|
+
box-shadow:
|
|
140
|
+
0 0 0 2px var(--xeno-canvas),
|
|
141
|
+
0 0 0 4px var(--xeno-border);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.xeno-segmented-option[data-availability='disabled'] {
|
|
145
|
+
opacity: 0.4;
|
|
146
|
+
cursor: not-allowed;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* The squash, keyed off the track's `data-index`. One keyframe track per position, all identical, for
|
|
150
|
+
the same reason as the sidebar's: moving to another segment only changes which rule matches, and a
|
|
151
|
+
browser will not restart an animation whose name did not change. Eight positions — a segmented
|
|
152
|
+
control with more options than that wants to be a Select. Past eight the thumb still travels; it
|
|
153
|
+
just stops squashing. */
|
|
154
|
+
.xeno-segmented[data-index='0'] .xeno-segmented-thumb { animation: xeno-seg-goo-0 var(--xeno-seg-dur) 1; }
|
|
155
|
+
.xeno-segmented[data-index='1'] .xeno-segmented-thumb { animation: xeno-seg-goo-1 var(--xeno-seg-dur) 1; }
|
|
156
|
+
.xeno-segmented[data-index='2'] .xeno-segmented-thumb { animation: xeno-seg-goo-2 var(--xeno-seg-dur) 1; }
|
|
157
|
+
.xeno-segmented[data-index='3'] .xeno-segmented-thumb { animation: xeno-seg-goo-3 var(--xeno-seg-dur) 1; }
|
|
158
|
+
.xeno-segmented[data-index='4'] .xeno-segmented-thumb { animation: xeno-seg-goo-4 var(--xeno-seg-dur) 1; }
|
|
159
|
+
.xeno-segmented[data-index='5'] .xeno-segmented-thumb { animation: xeno-seg-goo-5 var(--xeno-seg-dur) 1; }
|
|
160
|
+
.xeno-segmented[data-index='6'] .xeno-segmented-thumb { animation: xeno-seg-goo-6 var(--xeno-seg-dur) 1; }
|
|
161
|
+
.xeno-segmented[data-index='7'] .xeno-segmented-thumb { animation: xeno-seg-goo-7 var(--xeno-seg-dur) 1; }
|
|
162
|
+
/* Armed only once the value has actually changed — otherwise every segmented control on the page
|
|
163
|
+
squashes itself on load. */
|
|
164
|
+
.xeno-segmented:not([data-motion='ready']) .xeno-segmented-thumb { animation: none; }
|
|
165
|
+
|
|
166
|
+
@keyframes xeno-seg-goo-0 { 0% { scale: 1 1; } 34% { scale: var(--xeno-seg-goo-x) var(--xeno-seg-goo-y); } 72% { scale: var(--xeno-seg-goo-x2) var(--xeno-seg-goo-y2); } 100% { scale: 1 1; } }
|
|
167
|
+
@keyframes xeno-seg-goo-1 { 0% { scale: 1 1; } 34% { scale: var(--xeno-seg-goo-x) var(--xeno-seg-goo-y); } 72% { scale: var(--xeno-seg-goo-x2) var(--xeno-seg-goo-y2); } 100% { scale: 1 1; } }
|
|
168
|
+
@keyframes xeno-seg-goo-2 { 0% { scale: 1 1; } 34% { scale: var(--xeno-seg-goo-x) var(--xeno-seg-goo-y); } 72% { scale: var(--xeno-seg-goo-x2) var(--xeno-seg-goo-y2); } 100% { scale: 1 1; } }
|
|
169
|
+
@keyframes xeno-seg-goo-3 { 0% { scale: 1 1; } 34% { scale: var(--xeno-seg-goo-x) var(--xeno-seg-goo-y); } 72% { scale: var(--xeno-seg-goo-x2) var(--xeno-seg-goo-y2); } 100% { scale: 1 1; } }
|
|
170
|
+
@keyframes xeno-seg-goo-4 { 0% { scale: 1 1; } 34% { scale: var(--xeno-seg-goo-x) var(--xeno-seg-goo-y); } 72% { scale: var(--xeno-seg-goo-x2) var(--xeno-seg-goo-y2); } 100% { scale: 1 1; } }
|
|
171
|
+
@keyframes xeno-seg-goo-5 { 0% { scale: 1 1; } 34% { scale: var(--xeno-seg-goo-x) var(--xeno-seg-goo-y); } 72% { scale: var(--xeno-seg-goo-x2) var(--xeno-seg-goo-y2); } 100% { scale: 1 1; } }
|
|
172
|
+
@keyframes xeno-seg-goo-6 { 0% { scale: 1 1; } 34% { scale: var(--xeno-seg-goo-x) var(--xeno-seg-goo-y); } 72% { scale: var(--xeno-seg-goo-x2) var(--xeno-seg-goo-y2); } 100% { scale: 1 1; } }
|
|
173
|
+
@keyframes xeno-seg-goo-7 { 0% { scale: 1 1; } 34% { scale: var(--xeno-seg-goo-x) var(--xeno-seg-goo-y); } 72% { scale: var(--xeno-seg-goo-x2) var(--xeno-seg-goo-y2); } 100% { scale: 1 1; } }
|
|
174
|
+
|
|
175
|
+
@media (prefers-reduced-motion: reduce) {
|
|
176
|
+
/* The travel lives on the track (the index), so that is where it has to be switched off. */
|
|
177
|
+
.xeno-segmented,
|
|
178
|
+
.xeno-segmented-option {
|
|
179
|
+
transition: none;
|
|
180
|
+
}
|
|
181
|
+
/* The thumb still marks the segment — it just gets there without travelling or squashing. */
|
|
182
|
+
.xeno-segmented-thumb {
|
|
183
|
+
animation: none !important;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* <Tabs> — a horizontal tablist over a hairline baseline, plus an optional panel. CSS-first: the
|
|
3
|
+
* selected tab carries `data-selection="on"`, a disabled tab carries `data-availability="disabled"`.
|
|
4
|
+
* Size metrics arrive as inline `--xeno-h/-padx/-gap/-font` vars from the size tokens. Monochrome only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
.xeno-tabs {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: 16px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.xeno-tablist {
|
|
14
|
+
position: relative; /* the containing block the indicator's measured offset is relative to */
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
align-items: stretch;
|
|
17
|
+
gap: 2px;
|
|
18
|
+
border-bottom: 1px solid var(--xeno-border);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.xeno-tab {
|
|
22
|
+
position: relative;
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
gap: var(--xeno-gap, 6px);
|
|
27
|
+
height: var(--xeno-h, 32px);
|
|
28
|
+
padding: 0 var(--xeno-padx, 12px);
|
|
29
|
+
font-family: inherit;
|
|
30
|
+
font-size: var(--xeno-font, 14px);
|
|
31
|
+
font-weight: 500;
|
|
32
|
+
line-height: 1;
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
color: var(--xeno-muted);
|
|
35
|
+
background: transparent;
|
|
36
|
+
border: none;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
user-select: none;
|
|
39
|
+
transition: color var(--xeno-dur) var(--xeno-ease);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* The underline — ONE hairline bar for the whole tablist, which SLIDES from the tab you left to the tab
|
|
43
|
+
you chose. It used to be a `::after` on each tab scaling from 0 to 1, so it appeared on the new tab
|
|
44
|
+
and vanished from the old one: two events, no journey, and no clue where it came from.
|
|
45
|
+
Its position and width arrive as `--xeno-tab-x` / `--xeno-tab-w`, measured by the component (a tab is
|
|
46
|
+
as wide as its label — layout is the only thing that can answer that). Everything after those two
|
|
47
|
+
numbers is CSS: the travel, the stretch, and the settle. */
|
|
48
|
+
.xeno-tab-indicator {
|
|
49
|
+
/* Mid-flight the bar ELONGATES along its travel and thins — reaching for where it is going — then
|
|
50
|
+
rebounds a hair as it lands. Both scale with how far it has to go (`--xeno-tab-dist`). */
|
|
51
|
+
--xeno-tab-stretch: 0.22;
|
|
52
|
+
--xeno-tab-thin: 0.2;
|
|
53
|
+
--xeno-tab-goo-x: calc(1 + var(--xeno-tab-stretch) * var(--xeno-tab-dist, 1));
|
|
54
|
+
--xeno-tab-goo-y: calc(1 - var(--xeno-tab-thin) * var(--xeno-tab-dist, 1));
|
|
55
|
+
--xeno-tab-goo-x2: calc(1 - var(--xeno-tab-thin) * 0.25);
|
|
56
|
+
--xeno-tab-goo-y2: calc(1 + var(--xeno-tab-thin) * 0.25);
|
|
57
|
+
/* ONE duration for the travel and the deformation, or the stretch peaks off-centre and reads as a
|
|
58
|
+
glitch rather than as weight. */
|
|
59
|
+
--xeno-tab-dur: 300ms;
|
|
60
|
+
--xeno-tab-ease: cubic-bezier(0.34, 1.12, 0.5, 1);
|
|
61
|
+
position: absolute;
|
|
62
|
+
left: 0;
|
|
63
|
+
bottom: -1px;
|
|
64
|
+
width: var(--xeno-tab-w, 0px);
|
|
65
|
+
height: 2px;
|
|
66
|
+
translate: var(--xeno-tab-x, 0px) 0;
|
|
67
|
+
background: var(--xeno-text);
|
|
68
|
+
/* Square. At 2px tall even `--xeno-radius-hair` is 75% of the height — semicircular ends, i.e. a
|
|
69
|
+
capsule. Below about 4px there is no radius that reads as a corner, so a hairline stays a hairline. */
|
|
70
|
+
border-radius: 0;
|
|
71
|
+
pointer-events: none;
|
|
72
|
+
}
|
|
73
|
+
/* Not armed until the selection has actually changed once — otherwise the first measurement after mount
|
|
74
|
+
would slide the bar in from the left edge on page load. */
|
|
75
|
+
.xeno-tablist[data-motion='ready'] .xeno-tab-indicator {
|
|
76
|
+
transition:
|
|
77
|
+
translate var(--xeno-tab-dur) var(--xeno-tab-ease),
|
|
78
|
+
width var(--xeno-tab-dur) var(--xeno-tab-ease);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.xeno-tab:hover:not([data-availability='disabled']) {
|
|
82
|
+
color: var(--xeno-text);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.xeno-tab[data-selection='on'] {
|
|
86
|
+
color: var(--xeno-text);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* The stretch, keyed off the tablist's `data-index`. One keyframe track per position, all identical:
|
|
90
|
+
moving to another tab only changes which rule matches, and a browser will not restart an animation
|
|
91
|
+
whose name did not change. Eight positions; past that the bar still travels, it just stops
|
|
92
|
+
stretching. */
|
|
93
|
+
.xeno-tablist[data-motion='ready'][data-index='0'] .xeno-tab-indicator { animation: xeno-tab-goo-0 var(--xeno-tab-dur) 1; }
|
|
94
|
+
.xeno-tablist[data-motion='ready'][data-index='1'] .xeno-tab-indicator { animation: xeno-tab-goo-1 var(--xeno-tab-dur) 1; }
|
|
95
|
+
.xeno-tablist[data-motion='ready'][data-index='2'] .xeno-tab-indicator { animation: xeno-tab-goo-2 var(--xeno-tab-dur) 1; }
|
|
96
|
+
.xeno-tablist[data-motion='ready'][data-index='3'] .xeno-tab-indicator { animation: xeno-tab-goo-3 var(--xeno-tab-dur) 1; }
|
|
97
|
+
.xeno-tablist[data-motion='ready'][data-index='4'] .xeno-tab-indicator { animation: xeno-tab-goo-4 var(--xeno-tab-dur) 1; }
|
|
98
|
+
.xeno-tablist[data-motion='ready'][data-index='5'] .xeno-tab-indicator { animation: xeno-tab-goo-5 var(--xeno-tab-dur) 1; }
|
|
99
|
+
.xeno-tablist[data-motion='ready'][data-index='6'] .xeno-tab-indicator { animation: xeno-tab-goo-6 var(--xeno-tab-dur) 1; }
|
|
100
|
+
.xeno-tablist[data-motion='ready'][data-index='7'] .xeno-tab-indicator { animation: xeno-tab-goo-7 var(--xeno-tab-dur) 1; }
|
|
101
|
+
@keyframes xeno-tab-goo-0 { 0% { scale: 1 1; } 34% { scale: var(--xeno-tab-goo-x) var(--xeno-tab-goo-y); } 72% { scale: var(--xeno-tab-goo-x2) var(--xeno-tab-goo-y2); } 100% { scale: 1 1; } }
|
|
102
|
+
@keyframes xeno-tab-goo-1 { 0% { scale: 1 1; } 34% { scale: var(--xeno-tab-goo-x) var(--xeno-tab-goo-y); } 72% { scale: var(--xeno-tab-goo-x2) var(--xeno-tab-goo-y2); } 100% { scale: 1 1; } }
|
|
103
|
+
@keyframes xeno-tab-goo-2 { 0% { scale: 1 1; } 34% { scale: var(--xeno-tab-goo-x) var(--xeno-tab-goo-y); } 72% { scale: var(--xeno-tab-goo-x2) var(--xeno-tab-goo-y2); } 100% { scale: 1 1; } }
|
|
104
|
+
@keyframes xeno-tab-goo-3 { 0% { scale: 1 1; } 34% { scale: var(--xeno-tab-goo-x) var(--xeno-tab-goo-y); } 72% { scale: var(--xeno-tab-goo-x2) var(--xeno-tab-goo-y2); } 100% { scale: 1 1; } }
|
|
105
|
+
@keyframes xeno-tab-goo-4 { 0% { scale: 1 1; } 34% { scale: var(--xeno-tab-goo-x) var(--xeno-tab-goo-y); } 72% { scale: var(--xeno-tab-goo-x2) var(--xeno-tab-goo-y2); } 100% { scale: 1 1; } }
|
|
106
|
+
@keyframes xeno-tab-goo-5 { 0% { scale: 1 1; } 34% { scale: var(--xeno-tab-goo-x) var(--xeno-tab-goo-y); } 72% { scale: var(--xeno-tab-goo-x2) var(--xeno-tab-goo-y2); } 100% { scale: 1 1; } }
|
|
107
|
+
@keyframes xeno-tab-goo-6 { 0% { scale: 1 1; } 34% { scale: var(--xeno-tab-goo-x) var(--xeno-tab-goo-y); } 72% { scale: var(--xeno-tab-goo-x2) var(--xeno-tab-goo-y2); } 100% { scale: 1 1; } }
|
|
108
|
+
@keyframes xeno-tab-goo-7 { 0% { scale: 1 1; } 34% { scale: var(--xeno-tab-goo-x) var(--xeno-tab-goo-y); } 72% { scale: var(--xeno-tab-goo-x2) var(--xeno-tab-goo-y2); } 100% { scale: 1 1; } }
|
|
109
|
+
|
|
110
|
+
.xeno-tab:focus-visible {
|
|
111
|
+
outline: none;
|
|
112
|
+
border-radius: var(--xeno-radius-xs);
|
|
113
|
+
box-shadow:
|
|
114
|
+
0 0 0 2px var(--xeno-canvas),
|
|
115
|
+
0 0 0 4px var(--xeno-border);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.xeno-tab[data-availability='disabled'] {
|
|
119
|
+
opacity: 0.4;
|
|
120
|
+
cursor: not-allowed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.xeno-tab-panel {
|
|
124
|
+
color: var(--xeno-text);
|
|
125
|
+
outline: none;
|
|
126
|
+
}
|
|
127
|
+
.xeno-tab-panel:focus-visible {
|
|
128
|
+
border-radius: var(--xeno-radius-sm);
|
|
129
|
+
box-shadow:
|
|
130
|
+
0 0 0 2px var(--xeno-canvas),
|
|
131
|
+
0 0 0 4px var(--xeno-border);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@media (prefers-reduced-motion: reduce) {
|
|
135
|
+
.xeno-tab,
|
|
136
|
+
.xeno-tablist[data-motion='ready'] .xeno-tab-indicator {
|
|
137
|
+
transition: none;
|
|
138
|
+
}
|
|
139
|
+
/* The bar still marks the tab — it just gets there without travelling or stretching. */
|
|
140
|
+
.xeno-tab-indicator {
|
|
141
|
+
animation: none !important;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tooltip — a hover/focus hint. CSS-first reveal driven by `data-state` (JS-controlled), positioned on
|
|
3
|
+
* a `data-side` of the trigger. Monochrome shell: an elevated panel with a hairline border and muted
|
|
4
|
+
* 12px mono text; rounded squares only. Motion is transform/opacity, behind reduced-motion.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
.xeno-tooltip {
|
|
8
|
+
position: relative;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.xeno-tooltip-panel {
|
|
13
|
+
position: absolute;
|
|
14
|
+
z-index: 50;
|
|
15
|
+
width: max-content;
|
|
16
|
+
max-width: 240px;
|
|
17
|
+
padding: 6px 8px;
|
|
18
|
+
background: var(--xeno-elevated);
|
|
19
|
+
border: 1px solid var(--xeno-border);
|
|
20
|
+
border-radius: var(--xeno-radius-sm);
|
|
21
|
+
color: var(--xeno-muted);
|
|
22
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
23
|
+
font-size: 12px;
|
|
24
|
+
line-height: 1.4;
|
|
25
|
+
letter-spacing: 0.01em;
|
|
26
|
+
text-align: left;
|
|
27
|
+
white-space: normal;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
opacity: 0;
|
|
30
|
+
visibility: hidden;
|
|
31
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.24);
|
|
32
|
+
transition:
|
|
33
|
+
opacity var(--xeno-dur) var(--xeno-ease),
|
|
34
|
+
transform var(--xeno-dur) var(--xeno-ease),
|
|
35
|
+
visibility 0s linear var(--xeno-dur);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Sides — centre with inset + auto margins (works even when the panel is wider than the trigger) so
|
|
39
|
+
* the entrance transform stays free to animate. A small gap separates the panel from the trigger. */
|
|
40
|
+
.xeno-tooltip-panel[data-side='top'] {
|
|
41
|
+
bottom: 100%;
|
|
42
|
+
left: 0;
|
|
43
|
+
right: 0;
|
|
44
|
+
margin-inline: auto;
|
|
45
|
+
margin-bottom: 8px;
|
|
46
|
+
transform: translateY(4px);
|
|
47
|
+
}
|
|
48
|
+
.xeno-tooltip-panel[data-side='bottom'] {
|
|
49
|
+
top: 100%;
|
|
50
|
+
left: 0;
|
|
51
|
+
right: 0;
|
|
52
|
+
margin-inline: auto;
|
|
53
|
+
margin-top: 8px;
|
|
54
|
+
transform: translateY(-4px);
|
|
55
|
+
}
|
|
56
|
+
.xeno-tooltip-panel[data-side='left'] {
|
|
57
|
+
right: 100%;
|
|
58
|
+
top: 0;
|
|
59
|
+
bottom: 0;
|
|
60
|
+
margin-block: auto;
|
|
61
|
+
margin-right: 8px;
|
|
62
|
+
transform: translateX(4px);
|
|
63
|
+
}
|
|
64
|
+
.xeno-tooltip-panel[data-side='right'] {
|
|
65
|
+
left: 100%;
|
|
66
|
+
top: 0;
|
|
67
|
+
bottom: 0;
|
|
68
|
+
margin-block: auto;
|
|
69
|
+
margin-left: 8px;
|
|
70
|
+
transform: translateX(-4px);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Revealed — settle to the resting position; keep visibility on until the fade completes on close. */
|
|
74
|
+
.xeno-tooltip-panel[data-state='open'] {
|
|
75
|
+
opacity: 1;
|
|
76
|
+
visibility: visible;
|
|
77
|
+
transform: translate(0, 0);
|
|
78
|
+
transition:
|
|
79
|
+
opacity var(--xeno-dur) var(--xeno-ease),
|
|
80
|
+
transform var(--xeno-dur) var(--xeno-ease),
|
|
81
|
+
visibility 0s linear 0s;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Arrow — a small rounded square rotated 45°, overlapping the panel edge so it reads as a pointer. */
|
|
85
|
+
.xeno-tooltip-arrow {
|
|
86
|
+
position: absolute;
|
|
87
|
+
width: 7px;
|
|
88
|
+
height: 7px;
|
|
89
|
+
background: var(--xeno-elevated);
|
|
90
|
+
border-radius: var(--xeno-radius-hair);
|
|
91
|
+
}
|
|
92
|
+
.xeno-tooltip-panel[data-side='top'] .xeno-tooltip-arrow {
|
|
93
|
+
left: 50%;
|
|
94
|
+
bottom: -3px;
|
|
95
|
+
transform: translateX(-50%) rotate(45deg);
|
|
96
|
+
}
|
|
97
|
+
.xeno-tooltip-panel[data-side='bottom'] .xeno-tooltip-arrow {
|
|
98
|
+
left: 50%;
|
|
99
|
+
top: -3px;
|
|
100
|
+
transform: translateX(-50%) rotate(45deg);
|
|
101
|
+
}
|
|
102
|
+
.xeno-tooltip-panel[data-side='left'] .xeno-tooltip-arrow {
|
|
103
|
+
top: 50%;
|
|
104
|
+
right: -3px;
|
|
105
|
+
transform: translateY(-50%) rotate(45deg);
|
|
106
|
+
}
|
|
107
|
+
.xeno-tooltip-panel[data-side='right'] .xeno-tooltip-arrow {
|
|
108
|
+
top: 50%;
|
|
109
|
+
left: -3px;
|
|
110
|
+
transform: translateY(-50%) rotate(45deg);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@media (prefers-reduced-motion: reduce) {
|
|
114
|
+
.xeno-tooltip-panel {
|
|
115
|
+
transform: none;
|
|
116
|
+
transition:
|
|
117
|
+
opacity var(--xeno-dur) linear,
|
|
118
|
+
visibility 0s linear var(--xeno-dur);
|
|
119
|
+
}
|
|
120
|
+
.xeno-tooltip-panel[data-state='open'] {
|
|
121
|
+
transform: none;
|
|
122
|
+
transition:
|
|
123
|
+
opacity var(--xeno-dur) linear,
|
|
124
|
+
visibility 0s linear 0s;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The XENO scrollbar.
|
|
3
|
+
*
|
|
4
|
+
* A scrollbar is the one control the user does not summon: it appears because there is more, and it
|
|
5
|
+
* has to say so without becoming furniture. XENO's answer is to not draw it at all until you are in
|
|
6
|
+
* the region — and then to draw it properly, as an instrument with a rail, rather than as a lone mark
|
|
7
|
+
* floating in space. Three states:
|
|
8
|
+
*
|
|
9
|
+
* idle NOTHING. The gutter is reserved but nothing is painted.
|
|
10
|
+
* container hovered the rail appears — a hairline track with a thumb riding it
|
|
11
|
+
* thumb hovered brightest, and thicker, and it stays that way while you drag
|
|
12
|
+
*
|
|
13
|
+
* The rail is the half people get wrong. A thumb alone, arriving out of nowhere, reads as a stray
|
|
14
|
+
* mark; the same thumb sitting on a track reads as a control that was always there and simply became
|
|
15
|
+
* visible. It also gives the thumb something to be measured against — you can see how much document a
|
|
16
|
+
* drag covers, which a floating bar never tells you.
|
|
17
|
+
*
|
|
18
|
+
* The gutter is reserved even while nothing is drawn, so appearing costs no reflow: the content does
|
|
19
|
+
* not shift sideways the moment your pointer enters.
|
|
20
|
+
*
|
|
21
|
+
* ## What it cannot do
|
|
22
|
+
*
|
|
23
|
+
* It appears rather than FADES in. The pseudo-elements below take no transitions in any engine that
|
|
24
|
+
* implements them, so hiding and showing is a snap either way. A true fade — and a bar that also wakes
|
|
25
|
+
* on a keyboard scroll, where there is no pointer to hover — needs the overlay model: a real element
|
|
26
|
+
* over the content, positioned from `scrollTop`. That costs a listener per scroll container and a
|
|
27
|
+
* component wrapping every one of them, which is not a trade this library makes for its default.
|
|
28
|
+
* The playground's Scrollbar gallery has that version standing next to this one.
|
|
29
|
+
*
|
|
30
|
+
* ## The two engines are mutually exclusive — this is the part that bites
|
|
31
|
+
*
|
|
32
|
+
* There are two ways to style a scrollbar and they cannot be combined. `scrollbar-width` /
|
|
33
|
+
* `scrollbar-color` are the standard properties, and since Chrome 121 setting EITHER of them makes the
|
|
34
|
+
* `::-webkit-scrollbar` pseudo-elements be ignored on that element. Declare both and Blink silently
|
|
35
|
+
* drops the detailed version, which is exactly how a stylesheet ends up with a thumb that has no
|
|
36
|
+
* corner radius and no hover state despite ten lines asking for them.
|
|
37
|
+
*
|
|
38
|
+
* So they are branched on `@supports selector(::-webkit-scrollbar)`: the pseudo-elements where they
|
|
39
|
+
* exist (Blink, WebKit), the standard properties where they do not (Gecko). Firefox gets the colours
|
|
40
|
+
* and the thin width; it does not get the radius or the growth, because the standard properties cannot
|
|
41
|
+
* express them.
|
|
42
|
+
*
|
|
43
|
+
* ## Scope
|
|
44
|
+
*
|
|
45
|
+
* Only inside `.xeno`. A component library has no business restyling the host page's scrollbars, and a
|
|
46
|
+
* consumer who does want them can opt in by putting `.xeno-scroll` on `<html>` (with the tokens in
|
|
47
|
+
* scope). Everything is wrapped in `:where()`, so the specificity is zero and any component can
|
|
48
|
+
* override it by naming its own class — see the time picker, which only re-declares the SIZE token.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/* ---- Gecko: the standard properties, on their own ----------------------------
|
|
52
|
+
`scrollbar-color` cannot express a rail, so Firefox gets the hover gating and the colours and
|
|
53
|
+
nothing else: absent at rest, thumb-on-a-faint-track when you are in the region. */
|
|
54
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
55
|
+
:where(.xeno, .xeno *, .xeno-scroll) {
|
|
56
|
+
scrollbar-width: thin;
|
|
57
|
+
scrollbar-color: transparent transparent;
|
|
58
|
+
}
|
|
59
|
+
:where(.xeno, .xeno *, .xeno-scroll):hover {
|
|
60
|
+
scrollbar-color: var(--xeno-scroll-thumb-hover) var(--xeno-scroll-rail);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ---- Blink / WebKit: the pseudo-elements ------------------------------------- */
|
|
65
|
+
@supports selector(::-webkit-scrollbar) {
|
|
66
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar {
|
|
67
|
+
width: var(--xeno-scroll-size);
|
|
68
|
+
height: var(--xeno-scroll-size);
|
|
69
|
+
}
|
|
70
|
+
/* The stepper arrows are a relic; nothing in the system has them. */
|
|
71
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar-button {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ---- Idle: the gutter is there, nothing is painted in it ---- */
|
|
76
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar-track,
|
|
77
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar-corner,
|
|
78
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar-thumb {
|
|
79
|
+
background-color: transparent;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* Both pieces are drawn the same way: a transparent border plus `background-clip: padding-box`
|
|
84
|
+
* insets the paint without anything having to be positioned. The border is what sets the WIDTH —
|
|
85
|
+
* 5px of border on a 12px gutter leaves a 2px rail, 3px leaves a 6px thumb, and 2px leaves the 8px
|
|
86
|
+
* the thumb grows to under the pointer. Because only the border changes, the gutter never does, so
|
|
87
|
+
* nothing reflows at any point.
|
|
88
|
+
*
|
|
89
|
+
* Radius is `--xeno-radius-hair` (1.5px of 6px, 25%), plus the border so the outer edge stays true.
|
|
90
|
+
* `--xeno-radius-xs` would be half the thumb's own width — a capsule with semicircular ends, and
|
|
91
|
+
* the grammar has no circles in it.
|
|
92
|
+
*/
|
|
93
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar-track,
|
|
94
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar-thumb {
|
|
95
|
+
background-clip: padding-box;
|
|
96
|
+
border: solid transparent;
|
|
97
|
+
}
|
|
98
|
+
/* The rail is square. Its painted strip is only 2px wide, where `--xeno-radius-hair` would be 75%
|
|
99
|
+
of the width — semicircular ends. The radius here is exactly the border, so the PAINTED corner
|
|
100
|
+
comes out at zero. */
|
|
101
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar-track {
|
|
102
|
+
border-width: var(--xeno-scroll-rail-pad);
|
|
103
|
+
border-radius: var(--xeno-scroll-rail-pad);
|
|
104
|
+
}
|
|
105
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar-thumb {
|
|
106
|
+
border-width: var(--xeno-scroll-pad);
|
|
107
|
+
border-radius: calc(var(--xeno-radius-hair, 1.5px) + var(--xeno-scroll-pad));
|
|
108
|
+
min-height: 28px; /* never let a long document shrink the grab target to nothing */
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ---- In the region: the rail arrives, with the thumb on it ---- */
|
|
112
|
+
:where(.xeno, .xeno *, .xeno-scroll):hover::-webkit-scrollbar-track {
|
|
113
|
+
background-color: var(--xeno-scroll-rail);
|
|
114
|
+
}
|
|
115
|
+
:where(.xeno, .xeno *, .xeno-scroll):hover::-webkit-scrollbar-thumb {
|
|
116
|
+
background-color: var(--xeno-scroll-thumb-hover);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ---- On the thumb: brightest, and thicker, and it holds while you drag ---- */
|
|
120
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar-thumb:hover,
|
|
121
|
+
:where(.xeno, .xeno *, .xeno-scroll)::-webkit-scrollbar-thumb:active {
|
|
122
|
+
background-color: var(--xeno-scroll-thumb-active);
|
|
123
|
+
border-width: var(--xeno-scroll-pad-hover);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
* ## `.xeno-scroll-wake` — for a scroller where `:hover` means nothing
|
|
129
|
+
*
|
|
130
|
+
* The document element is hovered whenever the pointer is anywhere in the window, so gating the page's
|
|
131
|
+
* own bar on hover leaves it permanently on — the opposite of the intent. This variant gates on
|
|
132
|
+
* ACTIVITY instead: it shows while `data-scrolling` is present and hides when it is gone.
|
|
133
|
+
*
|
|
134
|
+
* That one attribute is the app's job, because it is the only part a stylesheet cannot observe. Set it
|
|
135
|
+
* on scroll, drop it a beat after the last event:
|
|
136
|
+
*
|
|
137
|
+
* let idle
|
|
138
|
+
* addEventListener('scroll', () => {
|
|
139
|
+
* root.setAttribute('data-scrolling', '')
|
|
140
|
+
* clearTimeout(idle)
|
|
141
|
+
* idle = setTimeout(() => root.removeAttribute('data-scrolling'), 600)
|
|
142
|
+
* }, { passive: true })
|
|
143
|
+
*
|
|
144
|
+
* Pointing at the gutter still reveals the thumb even while idle — an invisible thumb still occupies
|
|
145
|
+
* its track, so going for it deliberately works.
|
|
146
|
+
*/
|
|
147
|
+
@supports selector(::-webkit-scrollbar) {
|
|
148
|
+
.xeno-scroll-wake:not([data-scrolling])::-webkit-scrollbar-track,
|
|
149
|
+
.xeno-scroll-wake:not([data-scrolling])::-webkit-scrollbar-thumb {
|
|
150
|
+
background-color: transparent;
|
|
151
|
+
}
|
|
152
|
+
.xeno-scroll-wake[data-scrolling]::-webkit-scrollbar-track {
|
|
153
|
+
background-color: var(--xeno-scroll-rail);
|
|
154
|
+
}
|
|
155
|
+
.xeno-scroll-wake[data-scrolling]::-webkit-scrollbar-thumb {
|
|
156
|
+
background-color: var(--xeno-scroll-thumb-hover);
|
|
157
|
+
}
|
|
158
|
+
/* Under the pointer still wins, exactly as everywhere else. */
|
|
159
|
+
.xeno-scroll-wake::-webkit-scrollbar-thumb:hover,
|
|
160
|
+
.xeno-scroll-wake::-webkit-scrollbar-thumb:active {
|
|
161
|
+
background-color: var(--xeno-scroll-thumb-active);
|
|
162
|
+
border-width: var(--xeno-scroll-pad-hover);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
166
|
+
.xeno-scroll-wake:not([data-scrolling]) {
|
|
167
|
+
scrollbar-color: transparent transparent;
|
|
168
|
+
}
|
|
169
|
+
.xeno-scroll-wake[data-scrolling] {
|
|
170
|
+
scrollbar-color: var(--xeno-scroll-thumb-hover) var(--xeno-scroll-rail);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
* Opt-in: reserve the gutter so content does not jump sideways the moment it becomes scrollable. Not
|
|
176
|
+
* on by default — it costs the width permanently, which is wrong in a narrow panel and right in a long
|
|
177
|
+
* document. `stable both-edges` keeps the centring honest when the content is centred.
|
|
178
|
+
*/
|
|
179
|
+
.xeno-scroll-gutter {
|
|
180
|
+
scrollbar-gutter: stable;
|
|
181
|
+
}
|
|
182
|
+
.xeno-scroll-gutter-both {
|
|
183
|
+
scrollbar-gutter: stable both-edges;
|
|
184
|
+
}
|