@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.
Files changed (60) hide show
  1. package/LICENSE +201 -0
  2. package/charts/ChartMenu/ChartMenu.vue +140 -0
  3. package/charts/ChartMenu/download.ts +44 -0
  4. package/charts/ChartTooltip/ChartTooltip.vue +97 -0
  5. package/charts/ChoroplethMap/ChoroplethMap.md +398 -0
  6. package/charts/ChoroplethMap/ChoroplethMap.vue +777 -0
  7. package/charts/ChoroplethMap/hsaMapping.ts +4116 -0
  8. package/charts/DataTable/DataTable.md +143 -0
  9. package/charts/DataTable/DataTable.vue +277 -0
  10. package/charts/LineChart/LineChart.md +472 -0
  11. package/charts/LineChart/LineChart.vue +1216 -0
  12. package/charts/index.ts +23 -0
  13. package/charts/tooltip-position.ts +49 -0
  14. package/components/Box/Box.md +49 -0
  15. package/components/Box/Box.vue +52 -0
  16. package/components/Button/Button.md +67 -0
  17. package/components/Button/Button.vue +81 -0
  18. package/components/Expander/Expander.md +34 -0
  19. package/components/Expander/Expander.vue +95 -0
  20. package/components/Hint/Hint.md +29 -0
  21. package/components/Hint/Hint.vue +83 -0
  22. package/components/Icon/Icon.md +67 -0
  23. package/components/Icon/Icon.vue +112 -0
  24. package/components/LightDarkToggle/LightDarkToggle.vue +49 -0
  25. package/components/NumberInput/NumberInput.md +305 -0
  26. package/components/NumberInput/NumberInput.vue +531 -0
  27. package/components/SelectBox/SelectBox.md +110 -0
  28. package/components/SelectBox/SelectBox.vue +195 -0
  29. package/components/SidebarLayout/SidebarLayout.md +104 -0
  30. package/components/SidebarLayout/SidebarLayout.vue +466 -0
  31. package/components/Spinner/Spinner.md +51 -0
  32. package/components/Spinner/Spinner.vue +55 -0
  33. package/components/TextInput/TextInput.md +82 -0
  34. package/components/TextInput/TextInput.vue +94 -0
  35. package/components/Toggle/Toggle.md +81 -0
  36. package/components/Toggle/Toggle.vue +81 -0
  37. package/components/index.ts +15 -0
  38. package/index.json +121 -0
  39. package/package.json +24 -0
  40. package/pyodide/index.ts +7 -0
  41. package/pyodide/pyodide.worker.ts +233 -0
  42. package/pyodide/pyodideWorkerApi.ts +102 -0
  43. package/pyodide/useModel.ts +86 -0
  44. package/pyodide/vitePlugin.js +51 -0
  45. package/shared/ModelOutput.ts +88 -0
  46. package/shared/csv.ts +22 -0
  47. package/shared/index.ts +24 -0
  48. package/shared/transferUtils.ts +126 -0
  49. package/shared/useUrlParams.ts +296 -0
  50. package/theme/all.js +5 -0
  51. package/theme/base.css +176 -0
  52. package/theme/cfasim.css +3 -0
  53. package/theme/theme.css +113 -0
  54. package/theme/themes/cdc.css +22 -0
  55. package/theme/utilities.css +518 -0
  56. package/wasm/index.ts +2 -0
  57. package/wasm/useModel.ts +53 -0
  58. package/wasm/vitePlugin.js +35 -0
  59. package/wasm/wasm.worker.ts +74 -0
  60. 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
+ ```