@cfasim-ui/docs 0.3.11
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 +201 -0
- package/charts/ChartMenu/ChartMenu.vue +140 -0
- package/charts/ChartMenu/download.ts +44 -0
- package/charts/ChartTooltip/ChartTooltip.vue +97 -0
- package/charts/ChoroplethMap/ChoroplethMap.md +398 -0
- package/charts/ChoroplethMap/ChoroplethMap.vue +777 -0
- package/charts/ChoroplethMap/hsaMapping.ts +4116 -0
- package/charts/DataTable/DataTable.md +143 -0
- package/charts/DataTable/DataTable.vue +277 -0
- package/charts/LineChart/LineChart.md +472 -0
- package/charts/LineChart/LineChart.vue +1216 -0
- package/charts/index.ts +23 -0
- package/charts/tooltip-position.ts +49 -0
- package/components/Box/Box.md +49 -0
- package/components/Box/Box.vue +52 -0
- package/components/Button/Button.md +67 -0
- package/components/Button/Button.vue +81 -0
- package/components/Expander/Expander.md +34 -0
- package/components/Expander/Expander.vue +95 -0
- package/components/Hint/Hint.md +29 -0
- package/components/Hint/Hint.vue +83 -0
- package/components/Icon/Icon.md +67 -0
- package/components/Icon/Icon.vue +112 -0
- package/components/LightDarkToggle/LightDarkToggle.vue +49 -0
- package/components/NumberInput/NumberInput.md +305 -0
- package/components/NumberInput/NumberInput.vue +531 -0
- package/components/SelectBox/SelectBox.md +110 -0
- package/components/SelectBox/SelectBox.vue +195 -0
- package/components/SidebarLayout/SidebarLayout.md +104 -0
- package/components/SidebarLayout/SidebarLayout.vue +466 -0
- package/components/Spinner/Spinner.md +51 -0
- package/components/Spinner/Spinner.vue +55 -0
- package/components/TextInput/TextInput.md +82 -0
- package/components/TextInput/TextInput.vue +94 -0
- package/components/Toggle/Toggle.md +81 -0
- package/components/Toggle/Toggle.vue +81 -0
- package/components/index.ts +15 -0
- package/index.json +121 -0
- package/package.json +24 -0
- package/pyodide/index.ts +7 -0
- package/pyodide/pyodide.worker.ts +233 -0
- package/pyodide/pyodideWorkerApi.ts +102 -0
- package/pyodide/useModel.ts +86 -0
- package/pyodide/vitePlugin.js +51 -0
- package/shared/ModelOutput.ts +88 -0
- package/shared/csv.ts +22 -0
- package/shared/index.ts +24 -0
- package/shared/transferUtils.ts +126 -0
- package/shared/useUrlParams.ts +296 -0
- package/theme/all.js +5 -0
- package/theme/base.css +176 -0
- package/theme/cfasim.css +3 -0
- package/theme/theme.css +113 -0
- package/theme/themes/cdc.css +22 -0
- package/theme/utilities.css +518 -0
- package/wasm/index.ts +2 -0
- package/wasm/useModel.ts +53 -0
- package/wasm/vitePlugin.js +35 -0
- package/wasm/wasm.worker.ts +74 -0
- package/wasm/wasmWorkerApi.ts +38 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, computed } from "vue";
|
|
3
|
+
import { SliderRoot, SliderTrack, SliderRange, SliderThumb } from "reka-ui";
|
|
4
|
+
import Hint from "../Hint/Hint.vue";
|
|
5
|
+
|
|
6
|
+
const model = defineModel<number>();
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
label?: string;
|
|
10
|
+
hideLabel?: boolean;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
step?: number;
|
|
13
|
+
min?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
hint?: string;
|
|
16
|
+
percent?: boolean;
|
|
17
|
+
slider?: boolean;
|
|
18
|
+
live?: boolean;
|
|
19
|
+
numberType?: "integer" | "float";
|
|
20
|
+
required?: boolean;
|
|
21
|
+
decimals?: number;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const sliderMin = computed(() => props.min ?? (props.percent ? 0 : 0));
|
|
25
|
+
const sliderMax = computed(() => props.max ?? (props.percent ? 1 : 100));
|
|
26
|
+
const sliderStep = computed(() => props.step ?? (props.percent ? 0.01 : 1));
|
|
27
|
+
|
|
28
|
+
// Count fractional digits in a finite number's decimal representation.
|
|
29
|
+
// Uses toPrecision + parseFloat to mask float-multiplication artifacts
|
|
30
|
+
// (e.g. 0.007 * 100 === 0.7000000000000001 would otherwise return 16).
|
|
31
|
+
function countDecimals(n: number): number {
|
|
32
|
+
if (!Number.isFinite(n) || Number.isInteger(n)) return 0;
|
|
33
|
+
const s = parseFloat(Math.abs(n).toPrecision(12)).toString();
|
|
34
|
+
const dot = s.indexOf(".");
|
|
35
|
+
if (dot !== -1) return s.length - dot - 1;
|
|
36
|
+
const eNeg = s.indexOf("e-");
|
|
37
|
+
if (eNeg !== -1) return Number(s.slice(eNeg + 2));
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const inputStep = computed(() => {
|
|
42
|
+
if (props.step != null) return props.percent ? props.step * 100 : props.step;
|
|
43
|
+
return 1;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const inputMin = computed(() => {
|
|
47
|
+
if (props.min != null) return props.percent ? props.min * 100 : props.min;
|
|
48
|
+
return props.percent ? 0 : undefined;
|
|
49
|
+
});
|
|
50
|
+
const inputMax = computed(() => {
|
|
51
|
+
if (props.max != null) return props.percent ? props.max * 100 : props.max;
|
|
52
|
+
return props.percent ? 100 : undefined;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Display precision: explicit `decimals` wins; otherwise inferred from the
|
|
56
|
+
// input step (which is already in display units — see `inputStep`). Integer
|
|
57
|
+
// mode always collapses to 0.
|
|
58
|
+
const displayDecimals = computed(() => {
|
|
59
|
+
if (props.numberType === "integer") return 0;
|
|
60
|
+
if (props.decimals != null) return Math.max(0, props.decimals);
|
|
61
|
+
return countDecimals(inputStep.value);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function roundToDecimals(v: number, d: number): number {
|
|
65
|
+
const factor = Math.pow(10, d);
|
|
66
|
+
return Math.round(v * factor) / factor;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatSliderValue(v: number | undefined) {
|
|
70
|
+
if (v == null) return "";
|
|
71
|
+
const d = displayDecimals.value;
|
|
72
|
+
if (props.percent) return (v * 100).toFixed(d) + "%";
|
|
73
|
+
return v.toLocaleString("en-US", {
|
|
74
|
+
minimumFractionDigits: d,
|
|
75
|
+
maximumFractionDigits: d,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toDisplay(v: number | undefined) {
|
|
80
|
+
if (v == null) return v;
|
|
81
|
+
if (!props.percent) return v;
|
|
82
|
+
// Round in display units to hide float-multiplication artifacts like
|
|
83
|
+
// 0.1 * 100 === 10.000000000000002. Precision follows displayDecimals.
|
|
84
|
+
return roundToDecimals(v * 100, displayDecimals.value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fromDisplay(v: number) {
|
|
88
|
+
return props.percent ? v / 100 : v;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function coerceInteger(v: number): number {
|
|
92
|
+
if (props.numberType !== "integer") return v;
|
|
93
|
+
// Truncate the display value to an integer, then convert back
|
|
94
|
+
const display = toDisplay(v);
|
|
95
|
+
if (display == null) return v;
|
|
96
|
+
return fromDisplay(Math.trunc(display));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatWithCommas(v: number | undefined): string {
|
|
100
|
+
if (v == null) return "";
|
|
101
|
+
return v.toLocaleString("en-US");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatForDisplay(v: number | undefined): string {
|
|
105
|
+
if (v == null) return "";
|
|
106
|
+
const d = displayDecimals.value;
|
|
107
|
+
if (d > 0) {
|
|
108
|
+
return v.toLocaleString("en-US", {
|
|
109
|
+
minimumFractionDigits: d,
|
|
110
|
+
maximumFractionDigits: d,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const s = formatWithCommas(v);
|
|
114
|
+
if (props.numberType === "float" && Number.isInteger(v)) {
|
|
115
|
+
return s + ".0";
|
|
116
|
+
}
|
|
117
|
+
return s;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stripCommas(s: string): string {
|
|
121
|
+
return s.replace(/,/g, "");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const local = ref(formatForDisplay(toDisplay(model.value)));
|
|
125
|
+
const sliderLocal = ref(model.value);
|
|
126
|
+
const validationError = ref<string>();
|
|
127
|
+
|
|
128
|
+
watch(model, (v) => {
|
|
129
|
+
local.value = formatForDisplay(toDisplay(v));
|
|
130
|
+
sliderLocal.value = v;
|
|
131
|
+
validationError.value = validate(v);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Characters that can appear in a valid JS number literal: digits, thousands
|
|
135
|
+
// separators (commas), decimal point, sign, and scientific-notation exponent.
|
|
136
|
+
// Anything else is stripped when the value is committed.
|
|
137
|
+
const INVALID_NUMBER_CHARS = /[^0-9,.\-+eE]/g;
|
|
138
|
+
|
|
139
|
+
function reformatInput(event: Event) {
|
|
140
|
+
const input = event.target as HTMLInputElement;
|
|
141
|
+
const raw = stripCommas(input.value);
|
|
142
|
+
if (raw === "" || raw === "-") return;
|
|
143
|
+
if (raw.endsWith(".") || (raw.includes(".") && raw.endsWith("0"))) return;
|
|
144
|
+
const parsed = Number(raw);
|
|
145
|
+
if (Number.isNaN(parsed)) return;
|
|
146
|
+
|
|
147
|
+
const formatted = formatWithCommas(parsed);
|
|
148
|
+
if (formatted === input.value) return;
|
|
149
|
+
|
|
150
|
+
const cursorPos = input.selectionStart ?? 0;
|
|
151
|
+
const commasBefore = (input.value.slice(0, cursorPos).match(/,/g) || [])
|
|
152
|
+
.length;
|
|
153
|
+
local.value = formatted;
|
|
154
|
+
|
|
155
|
+
requestAnimationFrame(() => {
|
|
156
|
+
const rawPos = cursorPos - commasBefore;
|
|
157
|
+
let newPos = 0;
|
|
158
|
+
let rawCount = 0;
|
|
159
|
+
for (let i = 0; i < formatted.length; i++) {
|
|
160
|
+
if (formatted[i] !== ",") rawCount++;
|
|
161
|
+
if (rawCount >= rawPos) {
|
|
162
|
+
newPos = i + 1;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (rawCount < rawPos) newPos = formatted.length;
|
|
167
|
+
input.setSelectionRange(newPos, newPos);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function onBlur() {
|
|
172
|
+
commit();
|
|
173
|
+
if (local.value.trim() === "") return;
|
|
174
|
+
const parsed = Number(stripCommas(local.value));
|
|
175
|
+
if (!Number.isNaN(parsed)) {
|
|
176
|
+
local.value = formatForDisplay(parsed);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let liveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
181
|
+
function onInputEvent() {
|
|
182
|
+
if (!props.live || props.slider) return;
|
|
183
|
+
if (liveTimeout) clearTimeout(liveTimeout);
|
|
184
|
+
liveTimeout = setTimeout(commit, 300);
|
|
185
|
+
}
|
|
186
|
+
function onChangeEvent() {
|
|
187
|
+
if (!props.live || props.slider) return;
|
|
188
|
+
if (liveTimeout) clearTimeout(liveTimeout);
|
|
189
|
+
commit();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Validates a model value (or undefined for empty). Single source of truth
|
|
193
|
+
// for required / min / max errors — used on commit, programmatic updates,
|
|
194
|
+
// and arrow-key stepping.
|
|
195
|
+
function validate(v: number | undefined): string | undefined {
|
|
196
|
+
if (v == null) return props.required ? "Required" : undefined;
|
|
197
|
+
const display = toDisplay(v) as number;
|
|
198
|
+
if (inputMin.value != null && display < inputMin.value) {
|
|
199
|
+
return `Min ${inputMin.value}${props.percent ? "%" : ""}`;
|
|
200
|
+
}
|
|
201
|
+
if (inputMax.value != null && display > inputMax.value) {
|
|
202
|
+
return `Max ${inputMax.value}${props.percent ? "%" : ""}`;
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function commit() {
|
|
208
|
+
// An empty field clears the model — distinct from garbage input.
|
|
209
|
+
if (local.value.trim() === "") {
|
|
210
|
+
model.value = undefined;
|
|
211
|
+
sliderLocal.value = undefined;
|
|
212
|
+
validationError.value = validate(undefined);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Strip any characters that can't be part of a valid number literal.
|
|
216
|
+
// People are free to type anything while editing; we clean it up on commit.
|
|
217
|
+
const cleaned = local.value.replace(INVALID_NUMBER_CHARS, "");
|
|
218
|
+
// Require at least one digit. Otherwise Number("") === 0 would silently
|
|
219
|
+
// turn pure garbage ("abc") into 0. Reset to the current model value so
|
|
220
|
+
// invalid input doesn't linger in the field.
|
|
221
|
+
if (!/\d/.test(cleaned)) {
|
|
222
|
+
local.value = formatForDisplay(toDisplay(model.value));
|
|
223
|
+
validationError.value = validate(model.value);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (cleaned !== local.value) {
|
|
227
|
+
local.value = cleaned;
|
|
228
|
+
}
|
|
229
|
+
let parsed = Number(stripCommas(cleaned));
|
|
230
|
+
if (Number.isNaN(parsed)) return;
|
|
231
|
+
|
|
232
|
+
if (props.numberType === "integer") {
|
|
233
|
+
parsed = Math.trunc(parsed);
|
|
234
|
+
local.value = formatForDisplay(parsed);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const next = fromDisplay(parsed);
|
|
238
|
+
const error = validate(next);
|
|
239
|
+
validationError.value = error;
|
|
240
|
+
if (error) return;
|
|
241
|
+
|
|
242
|
+
model.value = next;
|
|
243
|
+
sliderLocal.value = model.value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function onSliderUpdate(v: number[] | undefined) {
|
|
247
|
+
if (!v) return;
|
|
248
|
+
const val = coerceInteger(v[0]);
|
|
249
|
+
sliderLocal.value = val;
|
|
250
|
+
local.value = formatForDisplay(toDisplay(val));
|
|
251
|
+
if (props.live) {
|
|
252
|
+
model.value = val;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function onSliderCommit(v: number[] | undefined) {
|
|
257
|
+
if (!v) return;
|
|
258
|
+
model.value = coerceInteger(v[0]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
const parsed = Number(stripCommas(local.value));
|
|
264
|
+
const current = Number.isNaN(parsed) ? 0 : parsed;
|
|
265
|
+
const step = inputStep.value * (event.shiftKey ? 10 : 1);
|
|
266
|
+
let next = current + step * direction;
|
|
267
|
+
if (props.numberType === "integer") next = Math.trunc(next);
|
|
268
|
+
if (inputMin.value != null) next = Math.max(next, inputMin.value);
|
|
269
|
+
if (inputMax.value != null) next = Math.min(next, inputMax.value);
|
|
270
|
+
local.value = formatForDisplay(next);
|
|
271
|
+
model.value = fromDisplay(next);
|
|
272
|
+
sliderLocal.value = model.value;
|
|
273
|
+
}
|
|
274
|
+
</script>
|
|
275
|
+
|
|
276
|
+
<template>
|
|
277
|
+
<label v-if="props.label" class="input-label">
|
|
278
|
+
<span
|
|
279
|
+
class="input-label-row"
|
|
280
|
+
:class="{ 'visually-hidden': props.hideLabel }"
|
|
281
|
+
>
|
|
282
|
+
{{ props.label }}
|
|
283
|
+
<Hint v-if="props.hint && !props.hideLabel" :text="props.hint" />
|
|
284
|
+
</span>
|
|
285
|
+
<span v-if="!props.slider" class="input-wrapper">
|
|
286
|
+
<input
|
|
287
|
+
type="text"
|
|
288
|
+
:inputmode="props.numberType === 'integer' ? 'numeric' : 'decimal'"
|
|
289
|
+
v-model="local"
|
|
290
|
+
:placeholder="props.placeholder"
|
|
291
|
+
:aria-invalid="!!validationError"
|
|
292
|
+
:aria-required="props.required || undefined"
|
|
293
|
+
:required="props.required"
|
|
294
|
+
@blur="onBlur"
|
|
295
|
+
@keydown.enter="commit"
|
|
296
|
+
@keydown.up="onArrowStep($event, 1)"
|
|
297
|
+
@keydown.down="onArrowStep($event, -1)"
|
|
298
|
+
@input="
|
|
299
|
+
reformatInput($event);
|
|
300
|
+
onInputEvent();
|
|
301
|
+
"
|
|
302
|
+
@change="onChangeEvent"
|
|
303
|
+
/>
|
|
304
|
+
<span v-if="props.percent" class="input-suffix">%</span>
|
|
305
|
+
</span>
|
|
306
|
+
<span v-if="validationError" class="input-error" role="alert">
|
|
307
|
+
{{ validationError }}
|
|
308
|
+
</span>
|
|
309
|
+
<div v-if="props.slider" class="slider-container">
|
|
310
|
+
<SliderRoot
|
|
311
|
+
class="slider-root"
|
|
312
|
+
:model-value="sliderLocal != null ? [sliderLocal] : [sliderMin]"
|
|
313
|
+
:min="sliderMin"
|
|
314
|
+
:max="sliderMax"
|
|
315
|
+
:step="sliderStep"
|
|
316
|
+
@update:model-value="onSliderUpdate"
|
|
317
|
+
@value-commit="onSliderCommit"
|
|
318
|
+
>
|
|
319
|
+
<SliderTrack class="slider-track">
|
|
320
|
+
<SliderRange class="slider-range" />
|
|
321
|
+
</SliderTrack>
|
|
322
|
+
<SliderThumb class="slider-thumb" :aria-label="props.label">
|
|
323
|
+
<span class="slider-current">
|
|
324
|
+
{{ formatSliderValue(sliderLocal) }}
|
|
325
|
+
</span>
|
|
326
|
+
</SliderThumb>
|
|
327
|
+
</SliderRoot>
|
|
328
|
+
<div class="slider-labels">
|
|
329
|
+
<span>{{ formatSliderValue(sliderMin) }}</span>
|
|
330
|
+
<span>{{ formatSliderValue(sliderMax) }}</span>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</label>
|
|
334
|
+
<div v-else>
|
|
335
|
+
<span v-if="!props.slider" class="input-wrapper">
|
|
336
|
+
<input
|
|
337
|
+
type="text"
|
|
338
|
+
:inputmode="props.numberType === 'integer' ? 'numeric' : 'decimal'"
|
|
339
|
+
v-model="local"
|
|
340
|
+
:placeholder="props.placeholder"
|
|
341
|
+
:aria-invalid="!!validationError"
|
|
342
|
+
:aria-required="props.required || undefined"
|
|
343
|
+
:required="props.required"
|
|
344
|
+
@blur="onBlur"
|
|
345
|
+
@keydown.enter="commit"
|
|
346
|
+
@keydown.up="onArrowStep($event, 1)"
|
|
347
|
+
@keydown.down="onArrowStep($event, -1)"
|
|
348
|
+
@input="
|
|
349
|
+
reformatInput($event);
|
|
350
|
+
onInputEvent();
|
|
351
|
+
"
|
|
352
|
+
@change="onChangeEvent"
|
|
353
|
+
/>
|
|
354
|
+
<span v-if="props.percent" class="input-suffix">%</span>
|
|
355
|
+
</span>
|
|
356
|
+
<span v-if="validationError" class="input-error" role="alert">
|
|
357
|
+
{{ validationError }}
|
|
358
|
+
</span>
|
|
359
|
+
<div v-if="props.slider" class="slider-container">
|
|
360
|
+
<SliderRoot
|
|
361
|
+
class="slider-root"
|
|
362
|
+
:model-value="sliderLocal != null ? [sliderLocal] : [sliderMin]"
|
|
363
|
+
:min="sliderMin"
|
|
364
|
+
:max="sliderMax"
|
|
365
|
+
:step="sliderStep"
|
|
366
|
+
@update:model-value="onSliderUpdate"
|
|
367
|
+
@value-commit="onSliderCommit"
|
|
368
|
+
>
|
|
369
|
+
<SliderTrack class="slider-track">
|
|
370
|
+
<SliderRange class="slider-range" />
|
|
371
|
+
</SliderTrack>
|
|
372
|
+
<SliderThumb class="slider-thumb" :aria-label="props.label">
|
|
373
|
+
<span class="slider-current">
|
|
374
|
+
{{ formatSliderValue(sliderLocal) }}
|
|
375
|
+
</span>
|
|
376
|
+
</SliderThumb>
|
|
377
|
+
</SliderRoot>
|
|
378
|
+
<div class="slider-labels">
|
|
379
|
+
<span>{{ formatSliderValue(sliderMin) }}</span>
|
|
380
|
+
<span>{{ formatSliderValue(sliderMax) }}</span>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
</template>
|
|
385
|
+
|
|
386
|
+
<style scoped>
|
|
387
|
+
.input-label {
|
|
388
|
+
display: flex;
|
|
389
|
+
flex-direction: column;
|
|
390
|
+
gap: 0.25em;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.input-label-row {
|
|
394
|
+
display: flex;
|
|
395
|
+
align-items: center;
|
|
396
|
+
justify-content: space-between;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.input-wrapper {
|
|
400
|
+
display: flex;
|
|
401
|
+
align-items: center;
|
|
402
|
+
gap: 0.25em;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.input-wrapper input {
|
|
406
|
+
flex: 1;
|
|
407
|
+
min-width: 0;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
input {
|
|
411
|
+
display: block;
|
|
412
|
+
width: 100%;
|
|
413
|
+
height: 2.5em;
|
|
414
|
+
padding: 0 0.75em;
|
|
415
|
+
font-size: inherit;
|
|
416
|
+
background-color: var(--color-bg-0);
|
|
417
|
+
color: var(--color-text);
|
|
418
|
+
border: 1px solid var(--color-border);
|
|
419
|
+
border-radius: 0.375em;
|
|
420
|
+
transition:
|
|
421
|
+
border-color var(--transition-fast),
|
|
422
|
+
box-shadow var(--transition-fast);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
input:hover {
|
|
426
|
+
border-color: var(--color-border-hover);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
input:focus {
|
|
430
|
+
outline: none;
|
|
431
|
+
border-color: var(--color-border-focus);
|
|
432
|
+
box-shadow: var(--shadow-focus);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
input[aria-invalid="true"] {
|
|
436
|
+
border-color: var(--color-error);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
input[aria-invalid="true"]:focus {
|
|
440
|
+
border-color: var(--color-error);
|
|
441
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 25%, transparent);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
input::placeholder {
|
|
445
|
+
color: var(--color-text-tertiary);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.input-suffix {
|
|
449
|
+
color: var(--color-text-secondary);
|
|
450
|
+
font-size: var(--font-size-sm);
|
|
451
|
+
flex-shrink: 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.input-error {
|
|
455
|
+
color: var(--color-error);
|
|
456
|
+
font-size: var(--font-size-xs);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.slider-container {
|
|
460
|
+
display: flex;
|
|
461
|
+
flex-direction: column;
|
|
462
|
+
gap: 0.25em;
|
|
463
|
+
padding-top: 1.5em;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.slider-current {
|
|
467
|
+
position: absolute;
|
|
468
|
+
bottom: 100%;
|
|
469
|
+
left: 50%;
|
|
470
|
+
transform: translateX(-50%);
|
|
471
|
+
margin-bottom: 1px;
|
|
472
|
+
font-size: var(--font-size-xs);
|
|
473
|
+
color: var(--color-text-secondary);
|
|
474
|
+
white-space: nowrap;
|
|
475
|
+
pointer-events: none;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.slider-root {
|
|
479
|
+
position: relative;
|
|
480
|
+
display: flex;
|
|
481
|
+
align-items: center;
|
|
482
|
+
width: 100%;
|
|
483
|
+
height: 1.5em;
|
|
484
|
+
touch-action: none;
|
|
485
|
+
user-select: none;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.slider-track {
|
|
489
|
+
position: relative;
|
|
490
|
+
flex-grow: 1;
|
|
491
|
+
height: 3px;
|
|
492
|
+
background-color: var(--color-bg-3);
|
|
493
|
+
border-radius: var(--radius-full);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.slider-range {
|
|
497
|
+
position: absolute;
|
|
498
|
+
height: 100%;
|
|
499
|
+
background-color: var(--color-primary);
|
|
500
|
+
border-radius: var(--radius-full);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.slider-thumb {
|
|
504
|
+
position: relative;
|
|
505
|
+
display: block;
|
|
506
|
+
width: 1em;
|
|
507
|
+
height: 1em;
|
|
508
|
+
background-color: var(--color-primary);
|
|
509
|
+
border-radius: var(--radius-full);
|
|
510
|
+
cursor: pointer;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.slider-thumb:hover {
|
|
514
|
+
background-color: var(--color-primary-hover);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.slider-thumb:active,
|
|
518
|
+
.slider-thumb:focus-visible {
|
|
519
|
+
outline: none;
|
|
520
|
+
box-shadow: 0 0 0 4px
|
|
521
|
+
color-mix(in srgb, var(--color-primary) 25%, transparent);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.slider-labels {
|
|
525
|
+
display: flex;
|
|
526
|
+
justify-content: space-between;
|
|
527
|
+
font-size: var(--font-size-xs);
|
|
528
|
+
color: var(--color-text-secondary);
|
|
529
|
+
margin-top: -0.5em;
|
|
530
|
+
}
|
|
531
|
+
</style>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# SelectBox
|
|
2
|
+
|
|
3
|
+
A dropdown select built on reka-ui.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
import { ref } from 'vue'
|
|
9
|
+
const interval = ref('weekly')
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<ComponentDemo>
|
|
13
|
+
<div style="width: 200px">
|
|
14
|
+
<SelectBox
|
|
15
|
+
v-model="interval"
|
|
16
|
+
label="Interval"
|
|
17
|
+
:options="[
|
|
18
|
+
{ value: 'daily', label: 'Daily' },
|
|
19
|
+
{ value: 'weekly', label: 'Weekly' },
|
|
20
|
+
{ value: 'monthly', label: 'Monthly' },
|
|
21
|
+
]"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<template #code>
|
|
26
|
+
|
|
27
|
+
```vue
|
|
28
|
+
<script setup>
|
|
29
|
+
import { ref } from "vue";
|
|
30
|
+
const interval = ref("weekly");
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<SelectBox
|
|
34
|
+
v-model="interval"
|
|
35
|
+
label="Interval"
|
|
36
|
+
:options="[
|
|
37
|
+
{ value: 'daily', label: 'Daily' },
|
|
38
|
+
{ value: 'weekly', label: 'Weekly' },
|
|
39
|
+
{ value: 'monthly', label: 'Monthly' },
|
|
40
|
+
]"
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
</template>
|
|
45
|
+
</ComponentDemo>
|
|
46
|
+
|
|
47
|
+
### Hidden label
|
|
48
|
+
|
|
49
|
+
Use `hide-label` to visually hide the label while keeping it available to
|
|
50
|
+
screen readers. Prefer this over `aria-label` whenever you have label text,
|
|
51
|
+
since a real `<label>` is translated by browsers and keeps the naming in the
|
|
52
|
+
DOM.
|
|
53
|
+
|
|
54
|
+
<ComponentDemo>
|
|
55
|
+
<div style="width: 200px">
|
|
56
|
+
<SelectBox
|
|
57
|
+
v-model="interval"
|
|
58
|
+
label="Interval"
|
|
59
|
+
hide-label
|
|
60
|
+
:options="[
|
|
61
|
+
{ value: 'daily', label: 'Daily' },
|
|
62
|
+
{ value: 'weekly', label: 'Weekly' },
|
|
63
|
+
{ value: 'monthly', label: 'Monthly' },
|
|
64
|
+
]"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<template #code>
|
|
69
|
+
|
|
70
|
+
```vue
|
|
71
|
+
<SelectBox
|
|
72
|
+
v-model="interval"
|
|
73
|
+
label="Interval"
|
|
74
|
+
hide-label
|
|
75
|
+
:options="[
|
|
76
|
+
{ value: 'daily', label: 'Daily' },
|
|
77
|
+
{ value: 'weekly', label: 'Weekly' },
|
|
78
|
+
{ value: 'monthly', label: 'Monthly' },
|
|
79
|
+
]"
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
</template>
|
|
84
|
+
</ComponentDemo>
|
|
85
|
+
|
|
86
|
+
## Model
|
|
87
|
+
|
|
88
|
+
| Name | Type |
|
|
89
|
+
|------|------|
|
|
90
|
+
| `v-model` | `string` |
|
|
91
|
+
|
|
92
|
+
## Props
|
|
93
|
+
|
|
94
|
+
| Prop | Type | Required | Default |
|
|
95
|
+
|------|------|----------|---------|
|
|
96
|
+
| `label` | `string` | No | — |
|
|
97
|
+
| `hideLabel` | `boolean` | No | — |
|
|
98
|
+
| `ariaLabel` | `string` | No | — |
|
|
99
|
+
| `options` | `SelectOption[]` | Yes | — |
|
|
100
|
+
| `placeholder` | `string` | No | — |
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
### SelectOption
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
interface SelectOption {
|
|
107
|
+
value: string;
|
|
108
|
+
label: string;
|
|
109
|
+
}
|
|
110
|
+
```
|