@marianmeres/stuic 3.172.0 → 3.174.0
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/AGENTS.md +19 -4
- package/API.md +56 -2
- package/README.md +21 -5
- package/dist/actions/resizable-width.svelte.d.ts +14 -28
- package/dist/actions/resizable-width.svelte.js +16 -171
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/resizable.d.ts +113 -0
- package/dist/attachments/resizable.fixture.svelte +53 -0
- package/dist/attachments/resizable.fixture.svelte.d.ts +10 -0
- package/dist/attachments/resizable.js +295 -0
- package/dist/components/Button/README.md +11 -10
- package/dist/components/ButtonGroupRadio/README.md +52 -23
- package/dist/components/ButtonGroupRadio/index.css +6 -2
- package/dist/components/PricingTable/PricingTable.svelte +8 -14
- package/dist/components/PricingTable/README.md +29 -6
- package/dist/components/PricingTable/index.css +54 -0
- package/dist/components/RangeSlider/README.md +291 -0
- package/dist/components/RangeSlider/RangeSlider.svelte +763 -0
- package/dist/components/RangeSlider/RangeSlider.svelte.d.ts +130 -0
- package/dist/components/RangeSlider/i18n-sk.d.ts +17 -0
- package/dist/components/RangeSlider/i18n-sk.js +19 -0
- package/dist/components/RangeSlider/i18n.d.ts +33 -0
- package/dist/components/RangeSlider/i18n.js +41 -0
- package/dist/components/RangeSlider/index.css +430 -0
- package/dist/components/RangeSlider/index.d.ts +3 -0
- package/dist/components/RangeSlider/index.js +3 -0
- package/dist/components/Rating/README.md +206 -0
- package/dist/components/Rating/Rating.svelte +355 -0
- package/dist/components/Rating/Rating.svelte.d.ts +82 -0
- package/dist/components/Rating/i18n-sk.d.ts +17 -0
- package/dist/components/Rating/i18n-sk.js +21 -0
- package/dist/components/Rating/i18n.d.ts +34 -0
- package/dist/components/Rating/i18n.js +42 -0
- package/dist/components/Rating/index.css +170 -0
- package/dist/components/Rating/index.d.ts +3 -0
- package/dist/components/Rating/index.js +3 -0
- package/dist/components/SplitPane/README.md +169 -0
- package/dist/components/SplitPane/SplitPane.svelte +202 -0
- package/dist/components/SplitPane/SplitPane.svelte.d.ts +67 -0
- package/dist/components/SplitPane/i18n-sk.d.ts +17 -0
- package/dist/components/SplitPane/i18n-sk.js +18 -0
- package/dist/components/SplitPane/i18n.d.ts +31 -0
- package/dist/components/SplitPane/i18n.js +39 -0
- package/dist/components/SplitPane/index.css +153 -0
- package/dist/components/SplitPane/index.d.ts +3 -0
- package/dist/components/SplitPane/index.js +3 -0
- package/dist/components/WithSidePanel/README.md +19 -16
- package/dist/icons/index.d.ts +2 -0
- package/dist/icons/index.js +3 -0
- package/dist/index.css +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/docs/{maybe-todo.md → _archive/maybe-todo.md} +18 -7
- package/docs/architecture.md +1 -1
- package/docs/conventions.md +27 -7
- package/docs/domains/actions.md +18 -18
- package/docs/domains/attachments.md +72 -9
- package/docs/domains/components.md +175 -32
- package/docs/domains/theming.md +57 -13
- package/package.json +5 -5
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ResizableOptions } from "./resizable.js";
|
|
2
|
+
import { type ResizableWidthOptions } from "../actions/resizable-width.svelte.js";
|
|
3
|
+
declare const Resizable: import("svelte").Component<{
|
|
4
|
+
options?: ResizableOptions;
|
|
5
|
+
action?: ResizableWidthOptions | undefined;
|
|
6
|
+
external?: boolean;
|
|
7
|
+
vertical?: boolean;
|
|
8
|
+
}, {}, "">;
|
|
9
|
+
type Resizable = ReturnType<typeof Resizable>;
|
|
10
|
+
export default Resizable;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { getId } from "../utils/get-id.js";
|
|
2
|
+
import { localStorageState, sessionStorageState, } from "../utils/persistent-state.svelte.js";
|
|
3
|
+
import { twMerge } from "../utils/tw-merge.js";
|
|
4
|
+
const DEFAULT_LABEL = "Resize";
|
|
5
|
+
/**
|
|
6
|
+
* Makes an element's width (`axis: "x"`) or height (`axis: "y"`) resizable by dragging
|
|
7
|
+
* its edge. The handle is a keyboard-operable ARIA window splitter (`role="separator"`,
|
|
8
|
+
* focusable, `aria-valuenow` in `units`): arrow keys along the axis move it by `step`
|
|
9
|
+
* (×10 with Shift), Home / End go to `min` / `max`, Enter (like double-click) resets.
|
|
10
|
+
* Pointer Events with capture cover mouse, touch and pen in one code path.
|
|
11
|
+
*
|
|
12
|
+
* By default a thin handle strip with a grip is appended inside the element on the
|
|
13
|
+
* resized edge (the element becomes `position: relative`); pass `handle` to drive an
|
|
14
|
+
* element of your own (a sibling separator, say) instead. The size can be persisted
|
|
15
|
+
* under `key`. `min` / `max` clamp every path (drag, keys, `api.set()`).
|
|
16
|
+
*
|
|
17
|
+
* A factory: call it with the options (or a function returning them — reactive reads
|
|
18
|
+
* inside re-run the attachment) and attach the result.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```svelte
|
|
22
|
+
* <script>
|
|
23
|
+
* import { resizable } from "@marianmeres/stuic";
|
|
24
|
+
* </script>
|
|
25
|
+
*
|
|
26
|
+
* <!-- side by side: the first flex child gets a draggable width -->
|
|
27
|
+
* <div class="flex">
|
|
28
|
+
* <aside {@attach resizable({ initial: 300, min: 200, max: 600, key: "sidebar" })}>…</aside>
|
|
29
|
+
* <main class="flex-1">…</main>
|
|
30
|
+
* </div>
|
|
31
|
+
*
|
|
32
|
+
* <!-- stacked: a draggable height, in % of the (definite-height) parent -->
|
|
33
|
+
* <div class="flex flex-col h-96">
|
|
34
|
+
* <div {@attach resizable({ axis: "y", units: "%", initial: 40, max: 80 })}>…</div>
|
|
35
|
+
* <div class="flex-1">…</div>
|
|
36
|
+
* </div>
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @param options - {@link ResizableOptions}, or a function returning them
|
|
40
|
+
*/
|
|
41
|
+
export function resizable(options = {}) {
|
|
42
|
+
return (el) => setup(el, typeof options === "function" ? options() : options);
|
|
43
|
+
}
|
|
44
|
+
// internals //////////////////////////////////////////////////////////////////////////
|
|
45
|
+
const HANDLE_CLS_COMMON = [
|
|
46
|
+
"group",
|
|
47
|
+
"absolute",
|
|
48
|
+
"bg-black/20 hover:bg-black/30",
|
|
49
|
+
"dark:bg-white/10 dark:hover:bg-white/20",
|
|
50
|
+
"transition-colors duration-200",
|
|
51
|
+
"touch-none",
|
|
52
|
+
"focus-visible:outline-2 focus-visible:outline-(--stuic-color-ring)",
|
|
53
|
+
];
|
|
54
|
+
const HANDLE_CLS_X = [...HANDLE_CLS_COMMON, "top-0 bottom-0 w-[1px] cursor-col-resize"];
|
|
55
|
+
const HANDLE_CLS_Y = [...HANDLE_CLS_COMMON, "left-0 right-0 h-[1px] cursor-row-resize"];
|
|
56
|
+
const GRIP_CLS_COMMON = [
|
|
57
|
+
"absolute",
|
|
58
|
+
"rounded border border-black/20 dark:border-white/20",
|
|
59
|
+
"bg-gray-300 group-hover:bg-gray-400",
|
|
60
|
+
"dark:bg-gray-600 dark:group-hover:bg-gray-500",
|
|
61
|
+
"transition-colors duration-200",
|
|
62
|
+
"touch-none",
|
|
63
|
+
];
|
|
64
|
+
const GRIP_CLS_X = [
|
|
65
|
+
...GRIP_CLS_COMMON,
|
|
66
|
+
"h-[20px] w-[9px] -translate-x-[4px] top-1/2 -translate-y-1/2 cursor-col-resize",
|
|
67
|
+
];
|
|
68
|
+
const GRIP_CLS_Y = [
|
|
69
|
+
...GRIP_CLS_COMMON,
|
|
70
|
+
"w-[20px] h-[9px] -translate-y-[4px] left-1/2 -translate-x-1/2 cursor-row-resize",
|
|
71
|
+
];
|
|
72
|
+
function create_handle(el, isX, reverse, handleClass, handleDragClass) {
|
|
73
|
+
const handle = document.createElement("div");
|
|
74
|
+
handle.setAttribute("data-handle", "true");
|
|
75
|
+
const grip = document.createElement("div");
|
|
76
|
+
grip.classList.add(...twMerge((isX ? GRIP_CLS_X : GRIP_CLS_Y).join(" "), handleDragClass).split(" "));
|
|
77
|
+
handle.appendChild(grip);
|
|
78
|
+
const edge = isX ? (reverse ? "left-0" : "right-0") : reverse ? "top-0" : "bottom-0";
|
|
79
|
+
handle.classList.add(...twMerge((isX ? HANDLE_CLS_X : HANDLE_CLS_Y).join(" "), edge, handleClass).split(" "));
|
|
80
|
+
el.appendChild(handle);
|
|
81
|
+
return handle;
|
|
82
|
+
}
|
|
83
|
+
function setup(el, options) {
|
|
84
|
+
const { enabled = true, axis = "x", initial = 0, min = 0, max = 0, units = "px", reverse = false, key, storage = "session", step = units === "%" ? 1 : 10, label = DEFAULT_LABEL, resetTo = initial, handle: providedHandle, handleClass = "", handleDragClass = "", onResize, onInit, debug, } = options;
|
|
85
|
+
const _debug = (...args) => debug?.("[resizable]", ...args);
|
|
86
|
+
_debug("setup", options);
|
|
87
|
+
if (!enabled)
|
|
88
|
+
return;
|
|
89
|
+
const container = el.parentElement;
|
|
90
|
+
if (!container)
|
|
91
|
+
return;
|
|
92
|
+
const isX = axis === "x";
|
|
93
|
+
const dim = isX ? "width" : "height";
|
|
94
|
+
// storage: a stored size wins over `initial`
|
|
95
|
+
const stored = key
|
|
96
|
+
? (storage === "local" ? localStorageState : sessionStorageState)(`resizable-${dim}-${key}`, initial)
|
|
97
|
+
: null;
|
|
98
|
+
// handle /////////////////////////////////////////////////////////////////////////
|
|
99
|
+
const handle = providedHandle ?? create_handle(el, isX, reverse, handleClass, handleDragClass);
|
|
100
|
+
// what a provided handle looked like before, so the cleanup can restore it
|
|
101
|
+
const prevAttrs = new Map();
|
|
102
|
+
const set_attr = (name, value) => {
|
|
103
|
+
if (!prevAttrs.has(name))
|
|
104
|
+
prevAttrs.set(name, handle.getAttribute(name));
|
|
105
|
+
handle.setAttribute(name, value);
|
|
106
|
+
};
|
|
107
|
+
const prevPosition = el.style.position;
|
|
108
|
+
const prevTouchAction = handle.style.touchAction;
|
|
109
|
+
if (!providedHandle)
|
|
110
|
+
el.style.position = "relative"; // the created handle is absolute
|
|
111
|
+
handle.style.touchAction = "none"; // no scrolling / zooming mid-drag
|
|
112
|
+
const assignedId = el.id ? null : (el.id = getId("stuic-resizable-"));
|
|
113
|
+
set_attr("role", "separator");
|
|
114
|
+
set_attr("tabindex", "0");
|
|
115
|
+
set_attr("aria-orientation", isX ? "vertical" : "horizontal");
|
|
116
|
+
set_attr("aria-label", label);
|
|
117
|
+
set_attr("aria-controls", el.id);
|
|
118
|
+
// sizing /////////////////////////////////////////////////////////////////////////
|
|
119
|
+
const measure = () => (isX ? container.offsetWidth : container.offsetHeight);
|
|
120
|
+
const current_px = () => parseFloat(getComputedStyle(el)[dim]) || 0;
|
|
121
|
+
// px -> units; with no measurable container (hidden) a % size is left as it is
|
|
122
|
+
const to_units = (px, cs) => units === "%" ? (cs > 0 ? (px / cs) * 100 : current) : px;
|
|
123
|
+
const clamp = (value) => {
|
|
124
|
+
const before = value;
|
|
125
|
+
if (min)
|
|
126
|
+
value = Math.max(min, value);
|
|
127
|
+
if (max)
|
|
128
|
+
value = Math.min(max, value);
|
|
129
|
+
if (before !== value)
|
|
130
|
+
_debug("clamped", before, "->", value, units);
|
|
131
|
+
return value;
|
|
132
|
+
};
|
|
133
|
+
let current = 0; // last applied size, in units
|
|
134
|
+
function update_aria(cs) {
|
|
135
|
+
set_attr("aria-valuemin", `${min || 0}`);
|
|
136
|
+
set_attr("aria-valuemax", `${max || (units === "%" ? 100 : Math.round(cs))}`);
|
|
137
|
+
set_attr("aria-valuenow", `${Math.round(current)}`);
|
|
138
|
+
set_attr("aria-valuetext", `${Math.round(current)}${units}`);
|
|
139
|
+
}
|
|
140
|
+
function commit(size, cs) {
|
|
141
|
+
size = clamp(units === "%" ? Math.min(100, size) : size);
|
|
142
|
+
el.style[dim] = `${size}${units}`;
|
|
143
|
+
current = size;
|
|
144
|
+
_debug("applied", size, units);
|
|
145
|
+
update_aria(cs);
|
|
146
|
+
const info = { size, units, axis, container: cs };
|
|
147
|
+
onResize?.(info);
|
|
148
|
+
if (stored)
|
|
149
|
+
stored.current = size;
|
|
150
|
+
return info;
|
|
151
|
+
}
|
|
152
|
+
/** Applies a size given in `units` — needs no container measurement, so it also works while hidden. */
|
|
153
|
+
function apply(size) {
|
|
154
|
+
commit(size, measure());
|
|
155
|
+
}
|
|
156
|
+
/** Applies a size given in px (a drag), against the container as measured when it started. */
|
|
157
|
+
function apply_px(px, cs) {
|
|
158
|
+
commit(to_units(px, cs), cs);
|
|
159
|
+
}
|
|
160
|
+
function reset() {
|
|
161
|
+
if (resetTo)
|
|
162
|
+
apply(resetTo);
|
|
163
|
+
}
|
|
164
|
+
// pointer ////////////////////////////////////////////////////////////////////////
|
|
165
|
+
let dragging = false;
|
|
166
|
+
let pointerId = -1;
|
|
167
|
+
let startCoord = 0;
|
|
168
|
+
let startPx = 0;
|
|
169
|
+
let containerSize = 0;
|
|
170
|
+
function on_pointerdown(e) {
|
|
171
|
+
if (e.button !== 0 && e.pointerType === "mouse")
|
|
172
|
+
return;
|
|
173
|
+
dragging = true;
|
|
174
|
+
pointerId = e.pointerId;
|
|
175
|
+
startCoord = isX ? e.clientX : e.clientY;
|
|
176
|
+
startPx = current_px();
|
|
177
|
+
containerSize = measure();
|
|
178
|
+
try {
|
|
179
|
+
handle.setPointerCapture(e.pointerId);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
/* capture is best-effort (synthetic pointers have no id to capture) */
|
|
183
|
+
}
|
|
184
|
+
document.body.style.cursor = isX ? "col-resize" : "row-resize";
|
|
185
|
+
document.body.style.userSelect = "none";
|
|
186
|
+
handle.setAttribute("data-resizing", "");
|
|
187
|
+
}
|
|
188
|
+
function on_pointermove(e) {
|
|
189
|
+
if (!dragging || e.pointerId !== pointerId)
|
|
190
|
+
return;
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
const delta = (isX ? e.clientX : e.clientY) - startCoord;
|
|
193
|
+
apply_px(reverse ? startPx - delta : startPx + delta, containerSize);
|
|
194
|
+
}
|
|
195
|
+
function on_pointerup(e) {
|
|
196
|
+
if (!dragging || e.pointerId !== pointerId)
|
|
197
|
+
return;
|
|
198
|
+
stop();
|
|
199
|
+
}
|
|
200
|
+
function stop() {
|
|
201
|
+
if (!dragging)
|
|
202
|
+
return;
|
|
203
|
+
dragging = false;
|
|
204
|
+
try {
|
|
205
|
+
handle.releasePointerCapture(pointerId);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
/* noop */
|
|
209
|
+
}
|
|
210
|
+
document.body.style.cursor = "";
|
|
211
|
+
document.body.style.userSelect = "";
|
|
212
|
+
handle.removeAttribute("data-resizing");
|
|
213
|
+
}
|
|
214
|
+
// keyboard ///////////////////////////////////////////////////////////////////////
|
|
215
|
+
function on_keydown(e) {
|
|
216
|
+
const amount = e.shiftKey ? step * 10 : step;
|
|
217
|
+
// delta along the screen direction (+ = right / down), like a pointer drag
|
|
218
|
+
let delta;
|
|
219
|
+
if (e.key === (isX ? "ArrowRight" : "ArrowDown"))
|
|
220
|
+
delta = amount;
|
|
221
|
+
else if (e.key === (isX ? "ArrowLeft" : "ArrowUp"))
|
|
222
|
+
delta = -amount;
|
|
223
|
+
else if (e.key === "Home")
|
|
224
|
+
delta = NaN;
|
|
225
|
+
else if (e.key === "End")
|
|
226
|
+
delta = NaN;
|
|
227
|
+
else if (e.key === "Enter")
|
|
228
|
+
delta = NaN;
|
|
229
|
+
else
|
|
230
|
+
return;
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
if (e.key === "Home")
|
|
233
|
+
return apply(min || 0);
|
|
234
|
+
if (e.key === "End")
|
|
235
|
+
return apply(max || (units === "%" ? 100 : measure()));
|
|
236
|
+
if (e.key === "Enter")
|
|
237
|
+
return reset();
|
|
238
|
+
apply(current + (reverse ? -delta : delta));
|
|
239
|
+
}
|
|
240
|
+
function on_dblclick() {
|
|
241
|
+
reset();
|
|
242
|
+
}
|
|
243
|
+
function on_selectstart(e) {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
}
|
|
246
|
+
// go /////////////////////////////////////////////////////////////////////////////
|
|
247
|
+
const startSize = stored ? (stored.current ?? initial) : initial;
|
|
248
|
+
if (startSize) {
|
|
249
|
+
apply(startSize);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const cs = measure();
|
|
253
|
+
current = to_units(current_px(), cs);
|
|
254
|
+
update_aria(cs);
|
|
255
|
+
}
|
|
256
|
+
handle.addEventListener("pointerdown", on_pointerdown);
|
|
257
|
+
handle.addEventListener("pointermove", on_pointermove);
|
|
258
|
+
handle.addEventListener("pointerup", on_pointerup);
|
|
259
|
+
handle.addEventListener("pointercancel", on_pointerup);
|
|
260
|
+
handle.addEventListener("keydown", on_keydown);
|
|
261
|
+
handle.addEventListener("dblclick", on_dblclick);
|
|
262
|
+
handle.addEventListener("selectstart", on_selectstart);
|
|
263
|
+
onInit?.({
|
|
264
|
+
get current() {
|
|
265
|
+
return current;
|
|
266
|
+
},
|
|
267
|
+
set: apply,
|
|
268
|
+
reset,
|
|
269
|
+
});
|
|
270
|
+
return () => {
|
|
271
|
+
stop();
|
|
272
|
+
handle.removeEventListener("pointerdown", on_pointerdown);
|
|
273
|
+
handle.removeEventListener("pointermove", on_pointermove);
|
|
274
|
+
handle.removeEventListener("pointerup", on_pointerup);
|
|
275
|
+
handle.removeEventListener("pointercancel", on_pointerup);
|
|
276
|
+
handle.removeEventListener("keydown", on_keydown);
|
|
277
|
+
handle.removeEventListener("dblclick", on_dblclick);
|
|
278
|
+
handle.removeEventListener("selectstart", on_selectstart);
|
|
279
|
+
if (providedHandle) {
|
|
280
|
+
for (const [name, value] of prevAttrs) {
|
|
281
|
+
if (value === null)
|
|
282
|
+
handle.removeAttribute(name);
|
|
283
|
+
else
|
|
284
|
+
handle.setAttribute(name, value);
|
|
285
|
+
}
|
|
286
|
+
handle.style.touchAction = prevTouchAction;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
handle.remove();
|
|
290
|
+
el.style.position = prevPosition;
|
|
291
|
+
}
|
|
292
|
+
if (assignedId && el.id === assignedId)
|
|
293
|
+
el.removeAttribute("id");
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -174,16 +174,17 @@ Notes:
|
|
|
174
174
|
|
|
175
175
|
### Component Tokens
|
|
176
176
|
|
|
177
|
-
| Variable | Default
|
|
178
|
-
| ------------------------------ |
|
|
179
|
-
| `--stuic-button-radius` | `--radius-
|
|
180
|
-
| `--stuic-button-
|
|
181
|
-
| `--stuic-button-font-
|
|
182
|
-
| `--stuic-button-
|
|
183
|
-
| `--stuic-button-
|
|
184
|
-
| `--stuic-button-ring-
|
|
185
|
-
| `--stuic-button-
|
|
186
|
-
| `--stuic-button-raised-
|
|
177
|
+
| Variable | Default | Description |
|
|
178
|
+
| ------------------------------ | ------------------------------ | ------------------- |
|
|
179
|
+
| `--stuic-button-radius` | `--stuic-radius-button` | Border radius |
|
|
180
|
+
| `--stuic-button-border-width` | `--stuic-border-width-button` | Border width |
|
|
181
|
+
| `--stuic-button-font-family` | `--font-sans` | Font family |
|
|
182
|
+
| `--stuic-button-font-weight` | `--font-weight-medium` | Font weight |
|
|
183
|
+
| `--stuic-button-transition` | `--stuic-transition` (`150ms`) | Transition duration |
|
|
184
|
+
| `--stuic-button-ring-width` | `3px` | Focus ring width |
|
|
185
|
+
| `--stuic-button-ring-color` | `--stuic-color-ring` | Focus ring color |
|
|
186
|
+
| `--stuic-button-raised-offset` | `2px` | 3D effect offset |
|
|
187
|
+
| `--stuic-button-raised-color` | `rgb(0 0 0 / 0.8)` | 3D shadow color |
|
|
187
188
|
|
|
188
189
|
### Size Tokens
|
|
189
190
|
|
|
@@ -129,34 +129,63 @@ disable the whole group instead, use the top-level `disabled` prop.
|
|
|
129
129
|
|
|
130
130
|
## CSS Variables
|
|
131
131
|
|
|
132
|
+
All tokens below are **declared** in the component's `:root`. That means a `:root` override
|
|
133
|
+
or a `style` prop override both work, but the `var(--token, <fallback>)` fallbacks visible in
|
|
134
|
+
`index.css` never fire — the defaults are the `:root` values listed here.
|
|
135
|
+
|
|
132
136
|
### Component Tokens
|
|
133
137
|
|
|
134
|
-
| Variable | Default | Description
|
|
135
|
-
| ---------------------------------------- | ------------------------- |
|
|
136
|
-
| `--stuic-button-group-radius` | `
|
|
137
|
-
| `--stuic-button-group-padding`
|
|
138
|
-
| `--stuic-button-group-
|
|
139
|
-
| `--stuic-button-group-
|
|
140
|
-
| `--stuic-button-group-
|
|
141
|
-
| `--stuic-button-group-ring-width` | `
|
|
142
|
-
| `--stuic-button-group-ring-color` | `var(--stuic-color-ring)` | Focus ring color
|
|
143
|
-
| `--stuic-button-group-button-padding-x` | `0.75rem` | Button horizontal padding
|
|
144
|
-
| `--stuic-button-group-button-padding-y` | `0.
|
|
145
|
-
| `--stuic-button-group-button-min-height` | `2.
|
|
138
|
+
| Variable | Default | Description |
|
|
139
|
+
| ---------------------------------------- | ------------------------- | ---------------------------------------------- |
|
|
140
|
+
| `--stuic-button-group-radius` | `9999px` | Border radius for container and buttons (pill) |
|
|
141
|
+
| `--stuic-button-group-padding-x` | `4px` | Container horizontal padding |
|
|
142
|
+
| `--stuic-button-group-padding-y` | `3px` | Container vertical padding |
|
|
143
|
+
| `--stuic-button-group-gap` | `0.25rem` | Gap between buttons |
|
|
144
|
+
| `--stuic-button-group-border-width` | `1px` | Container border width |
|
|
145
|
+
| `--stuic-button-group-ring-width` | `4px` | Focus ring width |
|
|
146
|
+
| `--stuic-button-group-ring-color` | `var(--stuic-color-ring)` | Focus ring color |
|
|
147
|
+
| `--stuic-button-group-button-padding-x` | `0.75rem` | Button horizontal padding |
|
|
148
|
+
| `--stuic-button-group-button-padding-y` | `0.375rem` | Button vertical padding |
|
|
149
|
+
| `--stuic-button-group-button-min-height` | `2.25rem` | Button min height (36px) |
|
|
150
|
+
| `--stuic-button-group-transition` | `var(--stuic-transition)` | Transition duration (not declared — fallback) |
|
|
151
|
+
|
|
152
|
+
`--stuic-button-group-transition` is the one exception: it is _not_ declared, so it resolves
|
|
153
|
+
through its usage-site fallback to `--stuic-transition`.
|
|
146
154
|
|
|
147
155
|
### Color Tokens
|
|
148
156
|
|
|
149
|
-
| Variable
|
|
150
|
-
|
|
|
151
|
-
| `--stuic-button-group-bg`
|
|
152
|
-
| `--stuic-button-group-text`
|
|
153
|
-
| `--stuic-button-group-border`
|
|
154
|
-
| `--stuic-button-group-
|
|
155
|
-
| `--stuic-button-group-button-
|
|
156
|
-
| `--stuic-button-group-button-
|
|
157
|
-
| `--stuic-button-group-button-bg-
|
|
158
|
-
| `--stuic-button-group-button-text-
|
|
159
|
-
| `--stuic-button-group-button-bg-active
|
|
157
|
+
| Variable | Default | Description |
|
|
158
|
+
| ----------------------------------------------- | ----------------------------------------- | -------------------------------- |
|
|
159
|
+
| `--stuic-button-group-bg` | `var(--stuic-color-muted)` | Container background |
|
|
160
|
+
| `--stuic-button-group-text` | `var(--stuic-color-muted-foreground)` | Container text color |
|
|
161
|
+
| `--stuic-button-group-border` | `var(--stuic-input-border)` | Container border color |
|
|
162
|
+
| `--stuic-button-group-border-focus` | `var(--stuic-color-primary)` | Container border on focus-within |
|
|
163
|
+
| `--stuic-button-group-button-bg` | `transparent` | Inactive button background |
|
|
164
|
+
| `--stuic-button-group-button-text` | `var(--stuic-color-muted-foreground)` | Inactive button text |
|
|
165
|
+
| `--stuic-button-group-button-bg-hover` | `transparent` | Inactive button hover background |
|
|
166
|
+
| `--stuic-button-group-button-text-hover` | `var(--stuic-color-foreground)` | Inactive button hover text |
|
|
167
|
+
| `--stuic-button-group-button-bg-active` | `var(--stuic-color-surface-1)` | Active button background |
|
|
168
|
+
| `--stuic-button-group-button-text-active` | `var(--stuic-color-surface-1-foreground)` | Active button text |
|
|
169
|
+
| `--stuic-button-group-button-bg-active-hover` | `var(--stuic-color-surface-1)` | Active button hover background |
|
|
170
|
+
| `--stuic-button-group-button-text-active-hover` | `var(--stuic-color-surface-1-foreground)` | Active button hover text |
|
|
171
|
+
|
|
172
|
+
### Size Variants
|
|
173
|
+
|
|
174
|
+
`size` sets `data-size` on the container, which re-declares a subset of the tokens above.
|
|
175
|
+
`md` is the `:root` default; `sm` and `lg` override these:
|
|
176
|
+
|
|
177
|
+
| Token | `sm` | `md` (default) | `lg` |
|
|
178
|
+
| ---------------------------------------- | ---------- | -------------- | ---------- |
|
|
179
|
+
| `--stuic-button-group-padding-x` | `3px` | `4px` | `8px` |
|
|
180
|
+
| `--stuic-button-group-padding-y` | `2px` | `3px` | `6px` |
|
|
181
|
+
| `--stuic-button-group-gap` | `0.125rem` | `0.25rem` | `0.375rem` |
|
|
182
|
+
| `--stuic-button-group-button-padding-x` | `0.5rem` | `0.75rem` | `1rem` |
|
|
183
|
+
| `--stuic-button-group-button-padding-y` | `0.375rem` | `0.375rem` | `0.625rem` |
|
|
184
|
+
| `--stuic-button-group-button-min-height` | `2.25rem` | `2.25rem` | `3rem` |
|
|
185
|
+
| overall height | 42px | 44px | 62px |
|
|
186
|
+
|
|
187
|
+
Because the size variants declare these on the container itself, they beat the inherited
|
|
188
|
+
`:root` values — but an inline `style` on the same element still wins over both.
|
|
160
189
|
|
|
161
190
|
### Customization Examples
|
|
162
191
|
|
|
@@ -78,7 +78,9 @@
|
|
|
78
78
|
============================================================================ */
|
|
79
79
|
|
|
80
80
|
.stuic-button-group[data-size="sm"] {
|
|
81
|
-
|
|
81
|
+
/* overall height = 36px + 2×2px + 2×1px = 42px */
|
|
82
|
+
--stuic-button-group-padding-x: 3px;
|
|
83
|
+
--stuic-button-group-padding-y: 2px;
|
|
82
84
|
--stuic-button-group-gap: 0.125rem;
|
|
83
85
|
--stuic-button-group-button-padding-x: 0.5rem;
|
|
84
86
|
--stuic-button-group-button-padding-y: 0.375rem;
|
|
@@ -86,7 +88,9 @@
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
.stuic-button-group[data-size="lg"] {
|
|
89
|
-
|
|
91
|
+
/* overall height = 48px + 2×6px + 2×1px = 62px */
|
|
92
|
+
--stuic-button-group-padding-x: 8px;
|
|
93
|
+
--stuic-button-group-padding-y: 6px;
|
|
90
94
|
--stuic-button-group-gap: 0.375rem;
|
|
91
95
|
--stuic-button-group-button-padding-x: 1rem;
|
|
92
96
|
--stuic-button-group-button-padding-y: 0.625rem;
|
|
@@ -177,6 +177,11 @@
|
|
|
177
177
|
let _classTiers = $derived(
|
|
178
178
|
unstyled ? classTiersProp : twMerge("stuic-pricing-table-tiers", classTiersProp)
|
|
179
179
|
);
|
|
180
|
+
// The toggle's pill look lives in index.css behind --stuic-pricing-table-toggle-*
|
|
181
|
+
// tokens (an inline declaration here would outrank any :root override).
|
|
182
|
+
let _classToggle = $derived(
|
|
183
|
+
unstyled ? classToggleProp : twMerge("stuic-pricing-table-toggle", classToggleProp)
|
|
184
|
+
);
|
|
180
185
|
</script>
|
|
181
186
|
|
|
182
187
|
<div bind:this={el} class={_class} {style} {...rest}>
|
|
@@ -195,20 +200,9 @@
|
|
|
195
200
|
onButtonClick={(i) => {
|
|
196
201
|
billingPeriod = i === 0 ? "monthly" : "annual";
|
|
197
202
|
}}
|
|
198
|
-
class={
|
|
199
|
-
style=
|
|
200
|
-
|
|
201
|
-
--stuic-button-group-radius: 9999px;
|
|
202
|
-
--stuic-button-group-padding: 0.25rem;
|
|
203
|
-
--stuic-button-group-bg: var(--stuic-color-muted);
|
|
204
|
-
--stuic-button-group-border-width: 0;
|
|
205
|
-
--stuic-button-group-button-bg-active: var(--stuic-color-background);
|
|
206
|
-
--stuic-button-group-button-text-active: var(--stuic-color-foreground);
|
|
207
|
-
--stuic-button-group-button-bg-active-hover: var(--stuic-color-background);
|
|
208
|
-
--stuic-button-group-button-text-active-hover: var(--stuic-color-foreground);
|
|
209
|
-
{styleToggle ?? ''}
|
|
210
|
-
"
|
|
211
|
-
classButtonActive="shadow"
|
|
203
|
+
class={_classToggle}
|
|
204
|
+
style={styleToggle}
|
|
205
|
+
classButtonActive={unstyled ? undefined : "shadow"}
|
|
212
206
|
/>
|
|
213
207
|
{/if}
|
|
214
208
|
{/if}
|
|
@@ -161,12 +161,35 @@ A data-driven pricing tiers component for displaying plan comparisons with featu
|
|
|
161
161
|
|
|
162
162
|
### Toggle
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
The billing toggle is an internal `ButtonGroupRadio`. Its pill look is expressed through the
|
|
165
|
+
tokens below, so a theme can reach it from `:root` like any other part of the component:
|
|
166
|
+
|
|
167
|
+
```css
|
|
168
|
+
:root {
|
|
169
|
+
/* square toggle to match a flat theme */
|
|
170
|
+
--stuic-pricing-table-toggle-radius: var(--stuic-radius-button);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
| Variable | Default | Description |
|
|
175
|
+
| ------------------------------------------------- | ---------------- | ------------------------------------------- |
|
|
176
|
+
| `--stuic-pricing-table-toggle-radius` | `9999px` | Track and option border radius |
|
|
177
|
+
| `--stuic-pricing-table-toggle-bg` | muted | Toggle track background |
|
|
178
|
+
| `--stuic-pricing-table-toggle-border-width` | `0` | Toggle track border width |
|
|
179
|
+
| `--stuic-pricing-table-toggle-padding-x` | `0.25rem` | Track horizontal padding around the options |
|
|
180
|
+
| `--stuic-pricing-table-toggle-padding-y` | `0.25rem` | Track vertical padding around the options |
|
|
181
|
+
| `--stuic-pricing-table-toggle-color` | muted-foreground | Inactive option text color |
|
|
182
|
+
| `--stuic-pricing-table-toggle-color-hover` | foreground | Inactive option text color on hover |
|
|
183
|
+
| `--stuic-pricing-table-toggle-bg-active` | background | Active option background |
|
|
184
|
+
| `--stuic-pricing-table-toggle-color-active` | foreground | Active option text color |
|
|
185
|
+
| `--stuic-pricing-table-toggle-bg-active-hover` | `-bg-active` | Active option background on hover |
|
|
186
|
+
| `--stuic-pricing-table-toggle-color-active-hover` | `-color-active` | Active option text color on hover |
|
|
187
|
+
|
|
188
|
+
For anything these do not cover, `styleToggle` sets inline styles on the group (including raw
|
|
189
|
+
`--stuic-button-group-*` tokens) and wins over all of the above; `classToggle` adds classes.
|
|
190
|
+
Note that `--stuic-button-group-*` set at `:root` does **not** reach the pricing toggle for the
|
|
191
|
+
properties listed above — the component declares them on the element, so use the
|
|
192
|
+
`--stuic-pricing-table-toggle-*` tokens instead.
|
|
170
193
|
|
|
171
194
|
### Typography
|
|
172
195
|
|
|
@@ -17,6 +17,60 @@
|
|
|
17
17
|
width: 100%;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/* ============================================================================
|
|
21
|
+
BILLING TOGGLE (an internal ButtonGroupRadio)
|
|
22
|
+
|
|
23
|
+
The opinionated pill look lives here rather than in the component's inline
|
|
24
|
+
`style` attribute, because an inline custom property declaration outranks any
|
|
25
|
+
`:root` declaration for that element and its subtree -- which made the toggle
|
|
26
|
+
the one part of this component a theme could not reach. Each value is a
|
|
27
|
+
never-declared `--stuic-pricing-table-toggle-*` component token, so
|
|
28
|
+
`:root { --stuic-pricing-table-toggle-radius: 0; }` works like every other
|
|
29
|
+
token here. `styleToggle` still wins over all of it (inline).
|
|
30
|
+
============================================================================ */
|
|
31
|
+
|
|
32
|
+
.stuic-pricing-table-toggle {
|
|
33
|
+
/* hug the options instead of spanning the row (the group defaults to 100%) */
|
|
34
|
+
width: auto;
|
|
35
|
+
|
|
36
|
+
--stuic-button-group-radius: var(--stuic-pricing-table-toggle-radius, 9999px);
|
|
37
|
+
--stuic-button-group-padding-x: var(--stuic-pricing-table-toggle-padding-x, 0.25rem);
|
|
38
|
+
--stuic-button-group-padding-y: var(--stuic-pricing-table-toggle-padding-y, 0.25rem);
|
|
39
|
+
--stuic-button-group-bg: var(
|
|
40
|
+
--stuic-pricing-table-toggle-bg,
|
|
41
|
+
var(--stuic-color-muted)
|
|
42
|
+
);
|
|
43
|
+
--stuic-button-group-border-width: var(--stuic-pricing-table-toggle-border-width, 0);
|
|
44
|
+
|
|
45
|
+
/* Inactive option */
|
|
46
|
+
--stuic-button-group-button-text: var(
|
|
47
|
+
--stuic-pricing-table-toggle-color,
|
|
48
|
+
var(--stuic-color-muted-foreground)
|
|
49
|
+
);
|
|
50
|
+
--stuic-button-group-button-text-hover: var(
|
|
51
|
+
--stuic-pricing-table-toggle-color-hover,
|
|
52
|
+
var(--stuic-color-foreground)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/* Active option (hover falls back to the resting value = no hover shift) */
|
|
56
|
+
--stuic-button-group-button-bg-active: var(
|
|
57
|
+
--stuic-pricing-table-toggle-bg-active,
|
|
58
|
+
var(--stuic-color-background)
|
|
59
|
+
);
|
|
60
|
+
--stuic-button-group-button-text-active: var(
|
|
61
|
+
--stuic-pricing-table-toggle-color-active,
|
|
62
|
+
var(--stuic-color-foreground)
|
|
63
|
+
);
|
|
64
|
+
--stuic-button-group-button-bg-active-hover: var(
|
|
65
|
+
--stuic-pricing-table-toggle-bg-active-hover,
|
|
66
|
+
var(--stuic-pricing-table-toggle-bg-active, var(--stuic-color-background))
|
|
67
|
+
);
|
|
68
|
+
--stuic-button-group-button-text-active-hover: var(
|
|
69
|
+
--stuic-pricing-table-toggle-color-active-hover,
|
|
70
|
+
var(--stuic-pricing-table-toggle-color-active, var(--stuic-color-foreground))
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
20
74
|
/* ============================================================================
|
|
21
75
|
TIERS GRID
|
|
22
76
|
============================================================================ */
|