@cfasim-ui/docs 0.6.4 → 0.7.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/charts/ChoroplethMap/ChoroplethMap.md +99 -29
- package/charts/ChoroplethMap/ChoroplethMap.vue +456 -127
- package/charts/DataTable/DataTable.vue +1 -7
- package/charts/_shared/ChartZoomControls.vue +138 -0
- package/charts/_shared/index.ts +4 -15
- package/charts/_shared/seriesCsv.ts +2 -2
- package/charts/_shared/touch.ts +8 -0
- package/charts/_shared/useChartFoundation.ts +0 -1
- package/components/MultiSelect/MultiSelect.md +1 -1
- package/components/MultiSelect/MultiSelect.vue +24 -83
- package/components/NumberInput/NumberInput.md +5 -5
- package/components/NumberInput/NumberInput.vue +12 -59
- package/components/SelectBox/SelectBox.md +1 -0
- package/components/SelectBox/SelectBox.vue +34 -125
- package/components/TextInput/TextInput.md +2 -1
- package/components/TextInput/TextInput.vue +11 -51
- package/components/ToggleGroup/ToggleGroup.md +2 -2
- package/components/ToggleGroup/ToggleGroup.vue +21 -26
- package/components/_internal/FieldLabel.vue +27 -0
- package/components/_internal/field.ts +27 -0
- package/components/_internal/input.css +54 -0
- package/components/_internal/listbox.css +53 -0
- package/components/env.d.ts +4 -0
- package/index.json +8 -8
- package/package.json +1 -1
- package/pyodide/messages.ts +23 -0
- package/pyodide/pyodide.worker.ts +2 -21
- package/pyodide/pyodideWorkerApi.ts +7 -44
- package/shared/index.ts +1 -0
- package/shared/workerError.ts +17 -0
- package/wasm/messages.ts +6 -0
- package/wasm/wasm.worker.ts +1 -7
- package/wasm/wasmWorkerApi.ts +8 -26
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import ChartMenu from "../ChartMenu/ChartMenu.vue";
|
|
10
10
|
import type { ChartMenuItem } from "../ChartMenu/ChartMenu.vue";
|
|
11
11
|
import { downloadCsv } from "../ChartMenu/download.js";
|
|
12
|
+
import { escapeCsvField } from "../_shared/seriesCsv.js";
|
|
12
13
|
|
|
13
14
|
export type TableRecord = Record<string, ArrayLike<number | string | boolean>>;
|
|
14
15
|
export type TableData = TableRecord | ModelOutput;
|
|
@@ -193,13 +194,6 @@ function menuFilename() {
|
|
|
193
194
|
return typeof props.menu === "string" ? props.menu : "data";
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
function escapeCsvField(val: string): string {
|
|
197
|
-
if (val.includes(",") || val.includes('"') || val.includes("\n")) {
|
|
198
|
-
return `"${val.replace(/"/g, '""')}"`;
|
|
199
|
-
}
|
|
200
|
-
return val;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
197
|
function toCsv(): string {
|
|
204
198
|
if (typeof props.csv === "function") return props.csv();
|
|
205
199
|
if (typeof props.csv === "string") return props.csv;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
canZoomIn?: boolean;
|
|
5
|
+
canZoomOut?: boolean;
|
|
6
|
+
/** Reset is a no-op at the identity transform — disable it there. */
|
|
7
|
+
canReset?: boolean;
|
|
8
|
+
/** Insets the stack further from the corner while the chart fills the
|
|
9
|
+
* window, matching the ChartMenu trigger area. */
|
|
10
|
+
isFullscreen?: boolean;
|
|
11
|
+
}>(),
|
|
12
|
+
{ canZoomIn: true, canZoomOut: true, canReset: true, isFullscreen: false },
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
(e: "zoomIn"): void;
|
|
17
|
+
(e: "zoomOut"): void;
|
|
18
|
+
(e: "reset"): void;
|
|
19
|
+
}>();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div
|
|
24
|
+
class="chart-zoom-controls"
|
|
25
|
+
:class="{ 'chart-zoom-controls--expanded': isFullscreen }"
|
|
26
|
+
>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
class="chart-zoom-button"
|
|
30
|
+
aria-label="Zoom in"
|
|
31
|
+
:disabled="!canZoomIn"
|
|
32
|
+
@click="emit('zoomIn')"
|
|
33
|
+
>
|
|
34
|
+
<svg
|
|
35
|
+
width="14"
|
|
36
|
+
height="14"
|
|
37
|
+
viewBox="0 0 14 14"
|
|
38
|
+
fill="none"
|
|
39
|
+
stroke="currentColor"
|
|
40
|
+
stroke-width="1.5"
|
|
41
|
+
stroke-linecap="round"
|
|
42
|
+
aria-hidden="true"
|
|
43
|
+
>
|
|
44
|
+
<path d="M7 2v10M2 7h10" />
|
|
45
|
+
</svg>
|
|
46
|
+
</button>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
class="chart-zoom-button"
|
|
50
|
+
aria-label="Zoom out"
|
|
51
|
+
:disabled="!canZoomOut"
|
|
52
|
+
@click="emit('zoomOut')"
|
|
53
|
+
>
|
|
54
|
+
<svg
|
|
55
|
+
width="14"
|
|
56
|
+
height="14"
|
|
57
|
+
viewBox="0 0 14 14"
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
stroke-width="1.5"
|
|
61
|
+
stroke-linecap="round"
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
>
|
|
64
|
+
<path d="M2 7h10" />
|
|
65
|
+
</svg>
|
|
66
|
+
</button>
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
class="chart-zoom-button"
|
|
70
|
+
aria-label="Reset view"
|
|
71
|
+
:disabled="!canReset"
|
|
72
|
+
@click="emit('reset')"
|
|
73
|
+
>
|
|
74
|
+
<!-- Counterclockwise "reset" arrow (rotate-ccw). -->
|
|
75
|
+
<svg
|
|
76
|
+
width="14"
|
|
77
|
+
height="14"
|
|
78
|
+
viewBox="0 0 14 14"
|
|
79
|
+
fill="none"
|
|
80
|
+
stroke="currentColor"
|
|
81
|
+
stroke-width="1.5"
|
|
82
|
+
stroke-linecap="round"
|
|
83
|
+
stroke-linejoin="round"
|
|
84
|
+
aria-hidden="true"
|
|
85
|
+
>
|
|
86
|
+
<path
|
|
87
|
+
d="M1.75 7a5.25 5.25 0 1 0 5.25-5.25c-1.48 0-2.9.6-3.94 1.6L1.75 4.7"
|
|
88
|
+
/>
|
|
89
|
+
<path d="M1.75 1.75v2.95h2.95" />
|
|
90
|
+
</svg>
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
.chart-zoom-controls {
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: 0.5em;
|
|
99
|
+
left: 0.5em;
|
|
100
|
+
z-index: 1;
|
|
101
|
+
display: flex;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
gap: 4px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Mirror .chart-menu-trigger-area--expanded: while the chart fills the
|
|
107
|
+
window the wrapper's padding doesn't move absolute children, so inset
|
|
108
|
+
further for modal-style breathing room. */
|
|
109
|
+
.chart-zoom-controls--expanded {
|
|
110
|
+
top: 1.25em;
|
|
111
|
+
left: 1.25em;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Same look as .chart-menu-button, but always visible — no hover reveal. */
|
|
115
|
+
.chart-zoom-button {
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
width: 28px;
|
|
120
|
+
height: 28px;
|
|
121
|
+
padding: 0;
|
|
122
|
+
border: 1px solid var(--color-border, #e5e7eb);
|
|
123
|
+
border-radius: 0.25em;
|
|
124
|
+
background: var(--color-bg-0, #fff);
|
|
125
|
+
color: var(--color-text-secondary, #555);
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.chart-zoom-button:hover:not(:disabled) {
|
|
130
|
+
background: var(--color-bg-1, rgba(0, 0, 0, 0.05));
|
|
131
|
+
color: var(--color-text);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.chart-zoom-button:disabled {
|
|
135
|
+
opacity: 0.4;
|
|
136
|
+
cursor: default;
|
|
137
|
+
}
|
|
138
|
+
</style>
|
package/charts/_shared/index.ts
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
export {
|
|
2
|
-
snap,
|
|
3
|
-
niceStep,
|
|
4
|
-
intervalValues,
|
|
5
|
-
formatTick,
|
|
6
|
-
type ChartData,
|
|
7
|
-
} from "./axes.js";
|
|
1
|
+
export { snap, formatTick, type ChartData } from "./axes.js";
|
|
8
2
|
export { computeTickValues, type TickValueOptions } from "./computeTicks.js";
|
|
9
3
|
export {
|
|
10
4
|
scaleFraction,
|
|
11
5
|
clampExtentForScale,
|
|
12
6
|
computeLogTickValues,
|
|
13
|
-
LOG_FLOOR,
|
|
14
7
|
type ScaleType,
|
|
15
8
|
} from "./scale.js";
|
|
16
9
|
export { useChartSize, type ChartSizeOptions } from "./useChartSize.js";
|
|
@@ -36,6 +29,8 @@ export {
|
|
|
36
29
|
} from "./useChartTooltip.js";
|
|
37
30
|
export { useChartMenu, type ChartMenuOptions } from "./useChartMenu.js";
|
|
38
31
|
export { useChartFullscreen } from "./useChartFullscreen.js";
|
|
32
|
+
export { isTouchDevice } from "./touch.js";
|
|
33
|
+
export { default as ChartZoomControls } from "./ChartZoomControls.vue";
|
|
39
34
|
export { seriesToCsv, categoricalToCsv, type CsvSeries } from "./seriesCsv.js";
|
|
40
35
|
export { default as ChartAnnotations } from "./ChartAnnotations.vue";
|
|
41
36
|
export type { ChartAnnotation } from "./annotations.js";
|
|
@@ -54,13 +49,7 @@ export type {
|
|
|
54
49
|
BlendMode,
|
|
55
50
|
LineMarkStyle,
|
|
56
51
|
} from "./chartProps.js";
|
|
57
|
-
export {
|
|
58
|
-
parseRgb,
|
|
59
|
-
relativeLuminance,
|
|
60
|
-
resolveColorToRgb,
|
|
61
|
-
pickContrastColor,
|
|
62
|
-
type Rgb,
|
|
63
|
-
} from "./contrast.js";
|
|
52
|
+
export { resolveColorToRgb, pickContrastColor, type Rgb } from "./contrast.js";
|
|
64
53
|
export {
|
|
65
54
|
parseDate,
|
|
66
55
|
isDateLike,
|
|
@@ -51,7 +51,7 @@ export function categoricalToCsv(
|
|
|
51
51
|
: [categoryHeader, ...series.map((s, i) => s.label || `series_${i}`)];
|
|
52
52
|
const rows = [headers.join(",")];
|
|
53
53
|
for (let r = 0; r < categories.length; r++) {
|
|
54
|
-
const cells = [
|
|
54
|
+
const cells = [escapeCsvField(categories[r])];
|
|
55
55
|
for (const s of series) {
|
|
56
56
|
cells.push(r < s.data.length ? String(s.data[r]) : "");
|
|
57
57
|
}
|
|
@@ -60,7 +60,7 @@ export function categoricalToCsv(
|
|
|
60
60
|
return rows.join("\n");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function
|
|
63
|
+
export function escapeCsvField(value: string): string {
|
|
64
64
|
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
65
65
|
return `"${value.replace(/"/g, '""')}"`;
|
|
66
66
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the current device has a touch screen. A function (not a
|
|
3
|
+
* module-level constant) so unit tests can mock it per-case and so SSR
|
|
4
|
+
* evaluation is deferred until an event actually needs the answer.
|
|
5
|
+
*/
|
|
6
|
+
export function isTouchDevice(): boolean {
|
|
7
|
+
return typeof window !== "undefined" && "ontouchstart" in window;
|
|
8
|
+
}
|
|
@@ -128,9 +128,9 @@ MultiSelect is built on reka-ui's Combobox, which implements the
|
|
|
128
128
|
| `label` | `string` | No | — |
|
|
129
129
|
| `hideLabel` | `boolean` | No | — |
|
|
130
130
|
| `ariaLabel` | `string` | No | — |
|
|
131
|
+
| `hint` | `string` | No | — |
|
|
131
132
|
| `options` | `MultiSelectOption[]` | Yes | — |
|
|
132
133
|
| `placeholder` | `string` | No | — |
|
|
133
|
-
| `hint` | `string` | No | — |
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
### MultiSelectOption
|
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
ComboboxRoot,
|
|
11
11
|
ComboboxTrigger,
|
|
12
12
|
ComboboxViewport,
|
|
13
|
-
useId,
|
|
14
13
|
} from "reka-ui";
|
|
15
14
|
import { computed, nextTick, ref, watch } from "vue";
|
|
16
15
|
import Icon from "../Icon/Icon.vue";
|
|
17
|
-
import
|
|
16
|
+
import FieldLabel from "../_internal/FieldLabel.vue";
|
|
17
|
+
import { useField, type FieldProps } from "../_internal/field";
|
|
18
|
+
import "../_internal/listbox.css";
|
|
18
19
|
|
|
19
20
|
export interface MultiSelectOption {
|
|
20
21
|
value: string;
|
|
@@ -23,16 +24,14 @@ export interface MultiSelectOption {
|
|
|
23
24
|
|
|
24
25
|
const model = defineModel<string[]>({ default: () => [] });
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
label?: string;
|
|
28
|
-
hideLabel?: boolean;
|
|
29
|
-
ariaLabel?: string;
|
|
27
|
+
interface Props extends FieldProps {
|
|
30
28
|
options: MultiSelectOption[];
|
|
31
29
|
placeholder?: string;
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const props = defineProps<Props>();
|
|
34
33
|
|
|
35
|
-
const
|
|
34
|
+
const { labelId, ariaProps } = useField(props);
|
|
36
35
|
const fieldRef = ref<HTMLElement | null>(null);
|
|
37
36
|
|
|
38
37
|
// In multiple mode the list stays open after a selection, so reka keeps
|
|
@@ -70,15 +69,13 @@ function onInputKeydown(event: KeyboardEvent) {
|
|
|
70
69
|
|
|
71
70
|
<template>
|
|
72
71
|
<div class="multi-select">
|
|
73
|
-
<
|
|
74
|
-
v-if="label"
|
|
75
|
-
:id="`${id}-label`"
|
|
72
|
+
<FieldLabel
|
|
76
73
|
class="multi-select-label"
|
|
77
|
-
:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
:label="label"
|
|
75
|
+
:label-id="labelId"
|
|
76
|
+
:hide-label="hideLabel"
|
|
77
|
+
:hint="hint"
|
|
78
|
+
/>
|
|
82
79
|
<ComboboxRoot
|
|
83
80
|
v-model="model"
|
|
84
81
|
multiple
|
|
@@ -107,8 +104,7 @@ function onInputKeydown(event: KeyboardEvent) {
|
|
|
107
104
|
<ComboboxInput
|
|
108
105
|
class="multi-select-input"
|
|
109
106
|
:placeholder="selectedOptions.length ? undefined : placeholder"
|
|
110
|
-
|
|
111
|
-
:aria-label="!label ? ariaLabel : undefined"
|
|
107
|
+
v-bind="ariaProps"
|
|
112
108
|
@keydown="onInputKeydown"
|
|
113
109
|
/>
|
|
114
110
|
</div>
|
|
@@ -117,38 +113,29 @@ function onInputKeydown(event: KeyboardEvent) {
|
|
|
117
113
|
aria-label="Toggle options"
|
|
118
114
|
>
|
|
119
115
|
<span class="multi-select-icon" aria-hidden="true">
|
|
120
|
-
<
|
|
121
|
-
width="12"
|
|
122
|
-
height="12"
|
|
123
|
-
viewBox="0 0 12 12"
|
|
124
|
-
fill="none"
|
|
125
|
-
stroke="currentColor"
|
|
126
|
-
stroke-width="2"
|
|
127
|
-
stroke-linecap="round"
|
|
128
|
-
stroke-linejoin="round"
|
|
129
|
-
>
|
|
130
|
-
<path d="M3 4.5L6 7.5L9 4.5" />
|
|
131
|
-
</svg>
|
|
116
|
+
<Icon icon="keyboard_arrow_down" :size="16" />
|
|
132
117
|
</span>
|
|
133
118
|
</ComboboxTrigger>
|
|
134
119
|
</ComboboxAnchor>
|
|
135
120
|
<ComboboxPortal>
|
|
136
121
|
<ComboboxContent
|
|
137
|
-
class="multi-select-content"
|
|
122
|
+
class="cfasim-listbox-content multi-select-content"
|
|
138
123
|
position="popper"
|
|
139
124
|
:side-offset="4"
|
|
140
125
|
:body-lock="false"
|
|
141
126
|
>
|
|
142
|
-
<ComboboxViewport class="
|
|
143
|
-
<ComboboxEmpty class="
|
|
127
|
+
<ComboboxViewport class="cfasim-listbox-viewport">
|
|
128
|
+
<ComboboxEmpty class="cfasim-listbox-empty">
|
|
129
|
+
No matches
|
|
130
|
+
</ComboboxEmpty>
|
|
144
131
|
<ComboboxItem
|
|
145
132
|
v-for="opt in options"
|
|
146
133
|
:key="opt.value"
|
|
147
134
|
:value="opt.value"
|
|
148
|
-
class="
|
|
135
|
+
class="cfasim-listbox-item"
|
|
149
136
|
>
|
|
150
137
|
<span>{{ opt.label }}</span>
|
|
151
|
-
<ComboboxItemIndicator class="
|
|
138
|
+
<ComboboxItemIndicator class="cfasim-listbox-indicator">
|
|
152
139
|
<Icon icon="check" :size="14" />
|
|
153
140
|
</ComboboxItemIndicator>
|
|
154
141
|
</ComboboxItem>
|
|
@@ -284,55 +271,9 @@ function onInputKeydown(event: KeyboardEvent) {
|
|
|
284
271
|
</style>
|
|
285
272
|
|
|
286
273
|
<style>
|
|
274
|
+
/* Sizing only — the dropdown skin lives in _internal/listbox.css. */
|
|
287
275
|
.multi-select-content {
|
|
288
|
-
z-index: 100;
|
|
289
|
-
background: var(--color-bg-0);
|
|
290
|
-
border: 1px solid var(--color-border);
|
|
291
|
-
border-radius: 0.25em;
|
|
292
|
-
box-shadow:
|
|
293
|
-
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
294
|
-
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
295
276
|
width: var(--reka-combobox-trigger-width);
|
|
296
277
|
max-height: var(--reka-combobox-content-available-height);
|
|
297
278
|
}
|
|
298
|
-
|
|
299
|
-
.multi-select-viewport {
|
|
300
|
-
padding: 0.25em;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.multi-select-empty {
|
|
304
|
-
padding: 0.5em;
|
|
305
|
-
font-size: var(--font-size-sm);
|
|
306
|
-
color: var(--color-text-secondary);
|
|
307
|
-
text-align: center;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
.multi-select-item {
|
|
311
|
-
display: flex;
|
|
312
|
-
align-items: center;
|
|
313
|
-
justify-content: space-between;
|
|
314
|
-
gap: 0.5em;
|
|
315
|
-
padding: 0.25em 0.5em;
|
|
316
|
-
border-radius: 0.25em;
|
|
317
|
-
font-size: var(--font-size-sm);
|
|
318
|
-
white-space: nowrap;
|
|
319
|
-
cursor: pointer;
|
|
320
|
-
user-select: none;
|
|
321
|
-
outline: none;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
.multi-select-item[data-highlighted] {
|
|
325
|
-
background: var(--color-primary);
|
|
326
|
-
color: white;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
.multi-select-item[data-state="checked"] {
|
|
330
|
-
font-weight: 600;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
.multi-select-indicator {
|
|
334
|
-
display: flex;
|
|
335
|
-
align-items: center;
|
|
336
|
-
flex-shrink: 0;
|
|
337
|
-
}
|
|
338
279
|
</style>
|
|
@@ -467,19 +467,19 @@ the input visually.
|
|
|
467
467
|
|------|------|----------|---------|
|
|
468
468
|
| `label` | `string` | No | — |
|
|
469
469
|
| `hideLabel` | `boolean` | No | — |
|
|
470
|
+
| `ariaLabel` | `string` | No | — |
|
|
471
|
+
| `hint` | `string` | No | — |
|
|
470
472
|
| `placeholder` | `string` | No | — |
|
|
471
473
|
| `step` | `number` | No | — |
|
|
472
474
|
| `min` | `number` | No | — |
|
|
473
475
|
| `max` | `number` | No | — |
|
|
474
|
-
| `
|
|
475
|
-
|
|
476
|
+
| `percent` | `1"`) may not round-trip through the text input — use
|
|
477
|
+
// `percent: true` for value scaling and `format` for display shaping.
|
|
478
|
+
format?: NumberFormat` | Yes | — |
|
|
476
479
|
| `slider` | `boolean` | No | — |
|
|
477
480
|
| `live` | `boolean` | No | — |
|
|
478
481
|
| `numberType` | `"integer" \| "float"` | No | — |
|
|
479
482
|
| `required` | `boolean` | No | — |
|
|
480
483
|
| `decimals` | `number` | No | — |
|
|
481
|
-
| `percent` | `1"`) may not round-trip through the text input — use
|
|
482
|
-
// `percent: true` for value scaling and `format` for display shaping.
|
|
483
|
-
format?: NumberFormat` | Yes | — |
|
|
484
484
|
| `sliderDisplay` | `(value: number) => string` | No | — |
|
|
485
485
|
|
|
@@ -3,6 +3,8 @@ import { ref, watch, computed, onMounted, getCurrentInstance } from "vue";
|
|
|
3
3
|
import { SliderRoot, SliderTrack, SliderRange, SliderThumb } from "reka-ui";
|
|
4
4
|
import { formatNumber, type NumberFormat } from "@cfasim-ui/shared";
|
|
5
5
|
import Hint from "../Hint/Hint.vue";
|
|
6
|
+
import type { FieldProps } from "../_internal/field";
|
|
7
|
+
import "../_internal/input.css";
|
|
6
8
|
|
|
7
9
|
export type NumberRange = [number, number];
|
|
8
10
|
|
|
@@ -16,14 +18,11 @@ const range = defineModel<NumberRange>("range");
|
|
|
16
18
|
const lower = defineModel<number>("lower");
|
|
17
19
|
const upper = defineModel<number>("upper");
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
label?: string;
|
|
21
|
-
hideLabel?: boolean;
|
|
21
|
+
interface Props extends FieldProps {
|
|
22
22
|
placeholder?: string;
|
|
23
23
|
step?: number;
|
|
24
24
|
min?: number;
|
|
25
25
|
max?: number;
|
|
26
|
-
hint?: string;
|
|
27
26
|
percent?: boolean;
|
|
28
27
|
slider?: boolean;
|
|
29
28
|
live?: boolean;
|
|
@@ -42,7 +41,9 @@ const props = defineProps<{
|
|
|
42
41
|
/** @deprecated Use `format` instead. Still honored for slider labels
|
|
43
42
|
* when `format` is unset, but will be removed in a future release. */
|
|
44
43
|
sliderDisplay?: (value: number) => string;
|
|
45
|
-
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const props = defineProps<Props>();
|
|
46
47
|
|
|
47
48
|
function isRangeValue(v: unknown): v is NumberRange {
|
|
48
49
|
return Array.isArray(v) && v.length === 2;
|
|
@@ -75,7 +76,7 @@ onMounted(() => {
|
|
|
75
76
|
}
|
|
76
77
|
});
|
|
77
78
|
|
|
78
|
-
const sliderMin = computed(() => props.min ??
|
|
79
|
+
const sliderMin = computed(() => props.min ?? 0);
|
|
79
80
|
const sliderMax = computed(() => props.max ?? (props.percent ? 1 : 100));
|
|
80
81
|
const sliderStep = computed(() => props.step ?? (props.percent ? 0.01 : 1));
|
|
81
82
|
|
|
@@ -408,11 +409,11 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
408
409
|
<template>
|
|
409
410
|
<component
|
|
410
411
|
:is="props.label ? 'label' : 'div'"
|
|
411
|
-
:class="props.label ? 'input-label' : undefined"
|
|
412
|
+
:class="props.label ? 'cfasim-input-label' : undefined"
|
|
412
413
|
>
|
|
413
414
|
<span
|
|
414
415
|
v-if="props.label"
|
|
415
|
-
class="input-label-row"
|
|
416
|
+
class="cfasim-input-label-row"
|
|
416
417
|
:class="{ 'visually-hidden': props.hideLabel }"
|
|
417
418
|
>
|
|
418
419
|
{{ props.label }}
|
|
@@ -421,9 +422,11 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
421
422
|
<span v-if="!isSlider" class="input-wrapper">
|
|
422
423
|
<input
|
|
423
424
|
type="text"
|
|
425
|
+
class="cfasim-input"
|
|
424
426
|
:inputmode="props.numberType === 'integer' ? 'numeric' : 'decimal'"
|
|
425
427
|
v-model="local"
|
|
426
428
|
:placeholder="props.placeholder"
|
|
429
|
+
:aria-label="!props.label ? props.ariaLabel : undefined"
|
|
427
430
|
:aria-invalid="!!validationError"
|
|
428
431
|
:aria-required="props.required || undefined"
|
|
429
432
|
:required="props.required"
|
|
@@ -475,67 +478,17 @@ function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
|
|
|
475
478
|
</template>
|
|
476
479
|
|
|
477
480
|
<style scoped>
|
|
478
|
-
.input-label {
|
|
479
|
-
display: flex;
|
|
480
|
-
flex-direction: column;
|
|
481
|
-
gap: 0.25em;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
.input-label-row {
|
|
485
|
-
display: flex;
|
|
486
|
-
align-items: center;
|
|
487
|
-
justify-content: space-between;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
481
|
.input-wrapper {
|
|
491
482
|
display: flex;
|
|
492
483
|
align-items: center;
|
|
493
484
|
gap: 0.25em;
|
|
494
485
|
}
|
|
495
486
|
|
|
496
|
-
.input-wrapper input {
|
|
487
|
+
.input-wrapper .cfasim-input {
|
|
497
488
|
flex: 1;
|
|
498
489
|
min-width: 0;
|
|
499
490
|
}
|
|
500
491
|
|
|
501
|
-
input {
|
|
502
|
-
display: block;
|
|
503
|
-
width: 100%;
|
|
504
|
-
height: 2.5em;
|
|
505
|
-
padding: 0 0.75em;
|
|
506
|
-
font-size: inherit;
|
|
507
|
-
background-color: var(--color-bg-0);
|
|
508
|
-
color: var(--color-text);
|
|
509
|
-
border: 1px solid var(--color-border);
|
|
510
|
-
border-radius: 0.375em;
|
|
511
|
-
transition:
|
|
512
|
-
border-color var(--transition-fast),
|
|
513
|
-
box-shadow var(--transition-fast);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
input:hover {
|
|
517
|
-
border-color: var(--color-border-hover);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
input:focus {
|
|
521
|
-
outline: none;
|
|
522
|
-
border-color: var(--color-border-focus);
|
|
523
|
-
box-shadow: var(--shadow-focus);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
input[aria-invalid="true"] {
|
|
527
|
-
border-color: var(--color-error);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
input[aria-invalid="true"]:focus {
|
|
531
|
-
border-color: var(--color-error);
|
|
532
|
-
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 25%, transparent);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
input::placeholder {
|
|
536
|
-
color: var(--color-text-tertiary);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
492
|
.input-suffix {
|
|
540
493
|
color: var(--color-text-secondary);
|
|
541
494
|
font-size: var(--font-size-sm);
|
|
@@ -166,6 +166,7 @@ reka-ui Combobox following the
|
|
|
166
166
|
| `label` | `string` | No | — |
|
|
167
167
|
| `hideLabel` | `boolean` | No | — |
|
|
168
168
|
| `ariaLabel` | `string` | No | — |
|
|
169
|
+
| `hint` | `string` | No | — |
|
|
169
170
|
| `options` | `SelectOption[]` | Yes | — |
|
|
170
171
|
| `placeholder` | `string` | No | — |
|
|
171
172
|
| `autocomplete` | `boolean` | No | — |
|