@cfasim-ui/docs 0.3.16 → 0.3.17
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/components/NumberInput/NumberInput.md +113 -0
- package/components/NumberInput/NumberInput.vue +156 -84
- package/components/index.ts +1 -0
- package/index.json +1 -1
- package/package.json +1 -1
- package/theme/base.css +0 -5
|
@@ -12,6 +12,10 @@ const days = ref(10)
|
|
|
12
12
|
const population = ref(100000)
|
|
13
13
|
const coverage = ref(0.5)
|
|
14
14
|
const r0 = ref(3.5)
|
|
15
|
+
const ageRange = ref([18, 65])
|
|
16
|
+
const coverageRange = ref([0.2, 0.8])
|
|
17
|
+
const minAge = ref(18)
|
|
18
|
+
const maxAge = ref(65)
|
|
15
19
|
</script>
|
|
16
20
|
|
|
17
21
|
<ComponentDemo>
|
|
@@ -116,6 +120,112 @@ const days = ref(10);
|
|
|
116
120
|
</template>
|
|
117
121
|
</ComponentDemo>
|
|
118
122
|
|
|
123
|
+
### Range slider
|
|
124
|
+
|
|
125
|
+
Bind `v-model:range` with a `[low, high]` tuple to render a two-handle
|
|
126
|
+
slider. Range mode is enabled automatically by the binding — there's no
|
|
127
|
+
explicit toggle prop.
|
|
128
|
+
|
|
129
|
+
<ComponentDemo>
|
|
130
|
+
<div style="width: 300px">
|
|
131
|
+
<NumberInput
|
|
132
|
+
v-model:range="ageRange"
|
|
133
|
+
label="Age range"
|
|
134
|
+
:min="0"
|
|
135
|
+
:max="100"
|
|
136
|
+
number-type="integer"
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<template #code>
|
|
141
|
+
|
|
142
|
+
```vue
|
|
143
|
+
<script setup>
|
|
144
|
+
import { ref } from "vue";
|
|
145
|
+
const ageRange = ref([18, 65]);
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<NumberInput
|
|
149
|
+
v-model:range="ageRange"
|
|
150
|
+
label="Age range"
|
|
151
|
+
:min="0"
|
|
152
|
+
:max="100"
|
|
153
|
+
number-type="integer"
|
|
154
|
+
/>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
</template>
|
|
158
|
+
</ComponentDemo>
|
|
159
|
+
|
|
160
|
+
### Range slider with split bindings
|
|
161
|
+
|
|
162
|
+
When your state stores the bounds in separate refs (rather than as a tuple),
|
|
163
|
+
bind them directly with `v-model:lower` and `v-model:upper`. You can bind
|
|
164
|
+
either pair or combine them with `v-model:range` — writes from the component
|
|
165
|
+
go to every bound sink.
|
|
166
|
+
|
|
167
|
+
<ComponentDemo>
|
|
168
|
+
<div style="width: 300px">
|
|
169
|
+
<NumberInput
|
|
170
|
+
v-model:lower="minAge"
|
|
171
|
+
v-model:upper="maxAge"
|
|
172
|
+
label="Age range (split)"
|
|
173
|
+
:min="0"
|
|
174
|
+
:max="100"
|
|
175
|
+
number-type="integer"
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<template #code>
|
|
180
|
+
|
|
181
|
+
```vue
|
|
182
|
+
<script setup>
|
|
183
|
+
import { ref } from "vue";
|
|
184
|
+
const minAge = ref(18);
|
|
185
|
+
const maxAge = ref(65);
|
|
186
|
+
</script>
|
|
187
|
+
|
|
188
|
+
<NumberInput
|
|
189
|
+
v-model:lower="minAge"
|
|
190
|
+
v-model:upper="maxAge"
|
|
191
|
+
label="Age range"
|
|
192
|
+
:min="0"
|
|
193
|
+
:max="100"
|
|
194
|
+
number-type="integer"
|
|
195
|
+
/>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
</template>
|
|
199
|
+
</ComponentDemo>
|
|
200
|
+
|
|
201
|
+
Range mode works with `percent` and `live` as well:
|
|
202
|
+
|
|
203
|
+
<ComponentDemo>
|
|
204
|
+
<div style="width: 300px">
|
|
205
|
+
<NumberInput
|
|
206
|
+
v-model:range="coverageRange"
|
|
207
|
+
label="Coverage range"
|
|
208
|
+
percent
|
|
209
|
+
live
|
|
210
|
+
:max="1"
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<template #code>
|
|
215
|
+
|
|
216
|
+
```vue
|
|
217
|
+
<NumberInput
|
|
218
|
+
v-model:range="coverageRange"
|
|
219
|
+
label="Coverage range"
|
|
220
|
+
percent
|
|
221
|
+
live
|
|
222
|
+
:max="1"
|
|
223
|
+
/>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
</template>
|
|
227
|
+
</ComponentDemo>
|
|
228
|
+
|
|
119
229
|
### Live slider
|
|
120
230
|
|
|
121
231
|
With `live`, the model updates while dragging the slider thumb rather than only on release.
|
|
@@ -284,6 +394,9 @@ the input visually.
|
|
|
284
394
|
| Name | Type |
|
|
285
395
|
|------|------|
|
|
286
396
|
| `v-model` | `number` |
|
|
397
|
+
| `v-model:range` | `NumberRange` |
|
|
398
|
+
| `v-model:lower` | `number` |
|
|
399
|
+
| `v-model:upper` | `number` |
|
|
287
400
|
|
|
288
401
|
## Props
|
|
289
402
|
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, watch, computed } from "vue";
|
|
2
|
+
import { ref, watch, computed, onMounted, getCurrentInstance } from "vue";
|
|
3
3
|
import { SliderRoot, SliderTrack, SliderRange, SliderThumb } from "reka-ui";
|
|
4
4
|
import Hint from "../Hint/Hint.vue";
|
|
5
5
|
|
|
6
|
+
export type NumberRange = [number, number];
|
|
7
|
+
|
|
8
|
+
// The default `v-model` is always a scalar number. For range mode, bind
|
|
9
|
+
// `v-model:range` (a tuple) and/or the split `v-model:lower`/`v-model:upper`.
|
|
10
|
+
// Mode is auto-detected from which v-model bindings the parent provides;
|
|
11
|
+
// no explicit toggle prop. Precedence on read: lower/upper > range >
|
|
12
|
+
// slider defaults.
|
|
6
13
|
const model = defineModel<number>();
|
|
14
|
+
const range = defineModel<NumberRange>("range");
|
|
15
|
+
const lower = defineModel<number>("lower");
|
|
16
|
+
const upper = defineModel<number>("upper");
|
|
7
17
|
|
|
8
18
|
const props = defineProps<{
|
|
9
19
|
label?: string;
|
|
@@ -21,6 +31,37 @@ const props = defineProps<{
|
|
|
21
31
|
decimals?: number;
|
|
22
32
|
}>();
|
|
23
33
|
|
|
34
|
+
function isRangeValue(v: unknown): v is NumberRange {
|
|
35
|
+
return Array.isArray(v) && v.length === 2;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Auto-detect range mode from the parent's v-model bindings. We check both
|
|
39
|
+
// the listener (which Vue attaches as `onUpdate:<name>` on the vnode) and
|
|
40
|
+
// the initial value — that way one-way `:range="x"` bindings also work.
|
|
41
|
+
// Determined once at setup; mode doesn't change for the component's life.
|
|
42
|
+
const instance = getCurrentInstance();
|
|
43
|
+
const vnodeProps = instance?.vnode.props;
|
|
44
|
+
const isRange =
|
|
45
|
+
!!vnodeProps?.["onUpdate:range"] ||
|
|
46
|
+
!!vnodeProps?.["onUpdate:lower"] ||
|
|
47
|
+
!!vnodeProps?.["onUpdate:upper"] ||
|
|
48
|
+
range.value !== undefined ||
|
|
49
|
+
lower.value !== undefined ||
|
|
50
|
+
upper.value !== undefined;
|
|
51
|
+
|
|
52
|
+
// Range implies slider — a two-handle range has no sensible text-input form.
|
|
53
|
+
const isSlider = computed(() => !!props.slider || isRange);
|
|
54
|
+
|
|
55
|
+
// Warn if the parent bound the default `v-model` in range mode — it will
|
|
56
|
+
// never receive updates, which is almost always a bug.
|
|
57
|
+
onMounted(() => {
|
|
58
|
+
if (isRange && !!vnodeProps?.["onUpdate:modelValue"]) {
|
|
59
|
+
console.warn(
|
|
60
|
+
"[NumberInput] In range mode, the default `v-model` is unused. Bind `v-model:range` or `v-model:lower`/`v-model:upper` instead.",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
24
65
|
const sliderMin = computed(() => props.min ?? (props.percent ? 0 : 0));
|
|
25
66
|
const sliderMax = computed(() => props.max ?? (props.percent ? 1 : 100));
|
|
26
67
|
const sliderStep = computed(() => props.step ?? (props.percent ? 0.01 : 1));
|
|
@@ -121,13 +162,50 @@ function stripCommas(s: string): string {
|
|
|
121
162
|
return s.replace(/,/g, "");
|
|
122
163
|
}
|
|
123
164
|
|
|
124
|
-
|
|
125
|
-
|
|
165
|
+
// Resolve the current value across all bindings:
|
|
166
|
+
// - In range mode: lower/upper take precedence; falls back per-side to
|
|
167
|
+
// `range`; finally to slider min/max. The default `v-model` is
|
|
168
|
+
// unused in this mode.
|
|
169
|
+
// - In single mode: just the default `v-model`.
|
|
170
|
+
function effectiveValue(): number | NumberRange | undefined {
|
|
171
|
+
if (isRange) {
|
|
172
|
+
const tuple = range.value;
|
|
173
|
+
const lo = lower.value ?? tuple?.[0];
|
|
174
|
+
const hi = upper.value ?? tuple?.[1];
|
|
175
|
+
if (lo !== undefined || hi !== undefined) {
|
|
176
|
+
return [lo ?? sliderMin.value, hi ?? sliderMax.value];
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
return model.value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Initial single-value display string. The text input isn't rendered in
|
|
184
|
+
// range mode, so `local` is only consulted in single mode.
|
|
185
|
+
const initialEffective = effectiveValue();
|
|
186
|
+
const initialSingle =
|
|
187
|
+
typeof initialEffective === "number" ? initialEffective : undefined;
|
|
188
|
+
const local = ref(formatForDisplay(toDisplay(initialSingle)));
|
|
189
|
+
|
|
190
|
+
// Slider state is always an array, even in single mode (reka-ui's API).
|
|
191
|
+
// In range mode it holds [low, high]; in single mode it holds [value].
|
|
192
|
+
function modelToSliderArray(v: number | NumberRange | undefined): number[] {
|
|
193
|
+
if (isRange) {
|
|
194
|
+
if (isRangeValue(v)) return [v[0], v[1]];
|
|
195
|
+
return [sliderMin.value, sliderMax.value];
|
|
196
|
+
}
|
|
197
|
+
if (typeof v === "number") return [v];
|
|
198
|
+
return [sliderMin.value];
|
|
199
|
+
}
|
|
200
|
+
const sliderArrayLocal = ref<number[]>(modelToSliderArray(initialEffective));
|
|
126
201
|
const validationError = ref<string>();
|
|
127
202
|
|
|
128
|
-
watch(model, (
|
|
129
|
-
|
|
130
|
-
|
|
203
|
+
watch([model, range, lower, upper], () => {
|
|
204
|
+
const v = effectiveValue();
|
|
205
|
+
if (!isRange && !isRangeValue(v)) {
|
|
206
|
+
local.value = formatForDisplay(toDisplay(v as number | undefined));
|
|
207
|
+
}
|
|
208
|
+
sliderArrayLocal.value = modelToSliderArray(v);
|
|
131
209
|
validationError.value = validate(v);
|
|
132
210
|
});
|
|
133
211
|
|
|
@@ -179,21 +257,17 @@ function onBlur() {
|
|
|
179
257
|
|
|
180
258
|
let liveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
181
259
|
function onInputEvent() {
|
|
182
|
-
if (!props.live ||
|
|
260
|
+
if (!props.live || isSlider.value) return;
|
|
183
261
|
if (liveTimeout) clearTimeout(liveTimeout);
|
|
184
262
|
liveTimeout = setTimeout(commit, 300);
|
|
185
263
|
}
|
|
186
264
|
function onChangeEvent() {
|
|
187
|
-
if (!props.live ||
|
|
265
|
+
if (!props.live || isSlider.value) return;
|
|
188
266
|
if (liveTimeout) clearTimeout(liveTimeout);
|
|
189
267
|
commit();
|
|
190
268
|
}
|
|
191
269
|
|
|
192
|
-
|
|
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;
|
|
270
|
+
function validateScalar(v: number): string | undefined {
|
|
197
271
|
const display = toDisplay(v) as number;
|
|
198
272
|
if (inputMin.value != null && display < inputMin.value) {
|
|
199
273
|
return `Min ${inputMin.value}${props.percent ? "%" : ""}`;
|
|
@@ -204,11 +278,28 @@ function validate(v: number | undefined): string | undefined {
|
|
|
204
278
|
return undefined;
|
|
205
279
|
}
|
|
206
280
|
|
|
281
|
+
// Single source of truth for required / min / max errors — used on commit,
|
|
282
|
+
// programmatic updates, and arrow-key stepping. In range mode, returns the
|
|
283
|
+
// first failing handle's error, suffixed with "(lower)" or "(upper)".
|
|
284
|
+
function validate(v: number | NumberRange | undefined): string | undefined {
|
|
285
|
+
if (v == null) return props.required ? "Required" : undefined;
|
|
286
|
+
if (isRangeValue(v)) {
|
|
287
|
+
const lo = validateScalar(v[0]);
|
|
288
|
+
if (lo) return `${lo} (lower)`;
|
|
289
|
+
const hi = validateScalar(v[1]);
|
|
290
|
+
if (hi) return `${hi} (upper)`;
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
return validateScalar(v);
|
|
294
|
+
}
|
|
295
|
+
|
|
207
296
|
function commit() {
|
|
208
|
-
//
|
|
297
|
+
// commit() is only reachable when !isSlider (only text-input events call
|
|
298
|
+
// it). Default `v-model` is scalar-only.
|
|
299
|
+
const current = model.value;
|
|
209
300
|
if (local.value.trim() === "") {
|
|
210
301
|
model.value = undefined;
|
|
211
|
-
|
|
302
|
+
sliderArrayLocal.value = modelToSliderArray(undefined);
|
|
212
303
|
validationError.value = validate(undefined);
|
|
213
304
|
return;
|
|
214
305
|
}
|
|
@@ -219,8 +310,8 @@ function commit() {
|
|
|
219
310
|
// turn pure garbage ("abc") into 0. Reset to the current model value so
|
|
220
311
|
// invalid input doesn't linger in the field.
|
|
221
312
|
if (!/\d/.test(cleaned)) {
|
|
222
|
-
local.value = formatForDisplay(toDisplay(
|
|
223
|
-
validationError.value = validate(
|
|
313
|
+
local.value = formatForDisplay(toDisplay(current));
|
|
314
|
+
validationError.value = validate(current);
|
|
224
315
|
return;
|
|
225
316
|
}
|
|
226
317
|
if (cleaned !== local.value) {
|
|
@@ -240,22 +331,43 @@ function commit() {
|
|
|
240
331
|
if (error) return;
|
|
241
332
|
|
|
242
333
|
model.value = next;
|
|
243
|
-
|
|
334
|
+
sliderArrayLocal.value = [next];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function commitSliderArray(v: number[], asModel: boolean): void {
|
|
338
|
+
const coerced = v.map(coerceInteger);
|
|
339
|
+
sliderArrayLocal.value = coerced;
|
|
340
|
+
if (!isRange) {
|
|
341
|
+
local.value = formatForDisplay(toDisplay(coerced[0]));
|
|
342
|
+
}
|
|
343
|
+
if (asModel) {
|
|
344
|
+
if (isRange) {
|
|
345
|
+
// Emit to all range sinks; consumers without a matching v-model just
|
|
346
|
+
// ignore their `update:*` event. The default `v-model` is unused in
|
|
347
|
+
// range mode.
|
|
348
|
+
range.value = [coerced[0], coerced[1]] as NumberRange;
|
|
349
|
+
lower.value = coerced[0];
|
|
350
|
+
upper.value = coerced[1];
|
|
351
|
+
} else {
|
|
352
|
+
model.value = coerced[0];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function thumbAriaLabel(i: number): string | undefined {
|
|
358
|
+
if (!props.label) return undefined;
|
|
359
|
+
if (!isRange) return props.label;
|
|
360
|
+
return i === 0 ? `${props.label} (lower)` : `${props.label} (upper)`;
|
|
244
361
|
}
|
|
245
362
|
|
|
246
363
|
function onSliderUpdate(v: number[] | undefined) {
|
|
247
364
|
if (!v) return;
|
|
248
|
-
|
|
249
|
-
sliderLocal.value = val;
|
|
250
|
-
local.value = formatForDisplay(toDisplay(val));
|
|
251
|
-
if (props.live) {
|
|
252
|
-
model.value = val;
|
|
253
|
-
}
|
|
365
|
+
commitSliderArray(v, !!props.live);
|
|
254
366
|
}
|
|
255
367
|
|
|
256
368
|
function onSliderCommit(v: number[] | undefined) {
|
|
257
369
|
if (!v) return;
|
|
258
|
-
|
|
370
|
+
commitSliderArray(v, true);
|
|
259
371
|
}
|
|
260
372
|
|
|
261
373
|
function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
@@ -268,71 +380,26 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
268
380
|
if (inputMin.value != null) next = Math.max(next, inputMin.value);
|
|
269
381
|
if (inputMax.value != null) next = Math.min(next, inputMax.value);
|
|
270
382
|
local.value = formatForDisplay(next);
|
|
271
|
-
|
|
272
|
-
|
|
383
|
+
const nextModel = fromDisplay(next);
|
|
384
|
+
model.value = nextModel;
|
|
385
|
+
sliderArrayLocal.value = [nextModel];
|
|
273
386
|
}
|
|
274
387
|
</script>
|
|
275
388
|
|
|
276
389
|
<template>
|
|
277
|
-
<
|
|
390
|
+
<component
|
|
391
|
+
:is="props.label ? 'label' : 'div'"
|
|
392
|
+
:class="props.label ? 'input-label' : undefined"
|
|
393
|
+
>
|
|
278
394
|
<span
|
|
395
|
+
v-if="props.label"
|
|
279
396
|
class="input-label-row"
|
|
280
397
|
:class="{ 'visually-hidden': props.hideLabel }"
|
|
281
398
|
>
|
|
282
399
|
{{ props.label }}
|
|
283
400
|
<Hint v-if="props.hint && !props.hideLabel" :text="props.hint" />
|
|
284
401
|
</span>
|
|
285
|
-
<span v-if="!
|
|
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">
|
|
402
|
+
<span v-if="!isSlider" class="input-wrapper">
|
|
336
403
|
<input
|
|
337
404
|
type="text"
|
|
338
405
|
:inputmode="props.numberType === 'integer' ? 'numeric' : 'decimal'"
|
|
@@ -356,10 +423,10 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
356
423
|
<span v-if="validationError" class="input-error" role="alert">
|
|
357
424
|
{{ validationError }}
|
|
358
425
|
</span>
|
|
359
|
-
<div v-if="
|
|
426
|
+
<div v-if="isSlider" class="slider-container">
|
|
360
427
|
<SliderRoot
|
|
361
428
|
class="slider-root"
|
|
362
|
-
:model-value="
|
|
429
|
+
:model-value="sliderArrayLocal"
|
|
363
430
|
:min="sliderMin"
|
|
364
431
|
:max="sliderMax"
|
|
365
432
|
:step="sliderStep"
|
|
@@ -369,9 +436,14 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
369
436
|
<SliderTrack class="slider-track">
|
|
370
437
|
<SliderRange class="slider-range" />
|
|
371
438
|
</SliderTrack>
|
|
372
|
-
<SliderThumb
|
|
439
|
+
<SliderThumb
|
|
440
|
+
v-for="(v, i) in sliderArrayLocal"
|
|
441
|
+
:key="i"
|
|
442
|
+
class="slider-thumb"
|
|
443
|
+
:aria-label="thumbAriaLabel(i)"
|
|
444
|
+
>
|
|
373
445
|
<span class="slider-current">
|
|
374
|
-
{{ formatSliderValue(
|
|
446
|
+
{{ formatSliderValue(v) }}
|
|
375
447
|
</span>
|
|
376
448
|
</SliderThumb>
|
|
377
449
|
</SliderRoot>
|
|
@@ -380,7 +452,7 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
380
452
|
<span>{{ formatSliderValue(sliderMax) }}</span>
|
|
381
453
|
</div>
|
|
382
454
|
</div>
|
|
383
|
-
</
|
|
455
|
+
</component>
|
|
384
456
|
</template>
|
|
385
457
|
|
|
386
458
|
<style scoped>
|
package/components/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { default as Hint } from "./Hint/Hint.vue";
|
|
|
10
10
|
export { default as Icon } from "./Icon/Icon.vue";
|
|
11
11
|
export { default as LightDarkToggle } from "./LightDarkToggle/LightDarkToggle.vue";
|
|
12
12
|
export { default as NumberInput } from "./NumberInput/NumberInput.vue";
|
|
13
|
+
export type { NumberRange } from "./NumberInput/NumberInput.vue";
|
|
13
14
|
export { default as SelectBox } from "./SelectBox/SelectBox.vue";
|
|
14
15
|
export type { SelectOption } from "./SelectBox/SelectBox.vue";
|
|
15
16
|
export { default as SidebarLayout } from "./SidebarLayout/SidebarLayout.vue";
|
package/index.json
CHANGED
package/package.json
CHANGED