@cfasim-ui/components 0.1.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/LICENSE +201 -0
- package/package.json +30 -0
- package/src/Box/Box.md +41 -0
- package/src/Box/Box.spec.ts +13 -0
- package/src/Box/Box.test.ts +49 -0
- package/src/Box/Box.vue +52 -0
- package/src/Button/Button.md +55 -0
- package/src/Button/Button.spec.ts +18 -0
- package/src/Button/Button.test.ts +36 -0
- package/src/Button/Button.vue +81 -0
- package/src/Expander/Expander.md +23 -0
- package/src/Expander/Expander.spec.ts +14 -0
- package/src/Expander/Expander.vue +95 -0
- package/src/Hint/Hint.md +24 -0
- package/src/Hint/Hint.spec.ts +12 -0
- package/src/Hint/Hint.test.ts +34 -0
- package/src/Hint/Hint.vue +83 -0
- package/src/Icon/Icon.md +55 -0
- package/src/Icon/Icon.spec.ts +9 -0
- package/src/Icon/Icon.vue +112 -0
- package/src/NumberInput/NumberInput.md +169 -0
- package/src/NumberInput/NumberInput.spec.ts +10 -0
- package/src/NumberInput/NumberInput.test.ts +328 -0
- package/src/NumberInput/NumberInput.vue +349 -0
- package/src/SelectBox/SelectBox.md +56 -0
- package/src/SelectBox/SelectBox.spec.ts +9 -0
- package/src/SelectBox/SelectBox.test.ts +42 -0
- package/src/SelectBox/SelectBox.vue +190 -0
- package/src/SidebarLayout/SidebarLayout.vue +270 -0
- package/src/Spinner/Spinner.md +45 -0
- package/src/Spinner/Spinner.spec.ts +9 -0
- package/src/Spinner/Spinner.vue +55 -0
- package/src/TextInput/TextInput.md +41 -0
- package/src/TextInput/TextInput.spec.ts +10 -0
- package/src/TextInput/TextInput.test.ts +70 -0
- package/src/TextInput/TextInput.vue +90 -0
- package/src/Toggle/Toggle.md +68 -0
- package/src/Toggle/Toggle.spec.ts +13 -0
- package/src/Toggle/Toggle.test.ts +35 -0
- package/src/Toggle/Toggle.vue +81 -0
- package/src/index.ts +13 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
CollapsibleRoot,
|
|
4
|
+
CollapsibleTrigger,
|
|
5
|
+
CollapsibleContent,
|
|
6
|
+
} from "reka-ui";
|
|
7
|
+
|
|
8
|
+
const open = defineModel<boolean>("open", { default: false });
|
|
9
|
+
|
|
10
|
+
defineProps<{
|
|
11
|
+
label: string;
|
|
12
|
+
}>();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<CollapsibleRoot v-model:open="open" class="expander">
|
|
17
|
+
<CollapsibleTrigger class="expander-trigger">
|
|
18
|
+
<span class="expander-caret" :class="{ open }" />
|
|
19
|
+
{{ label }}
|
|
20
|
+
</CollapsibleTrigger>
|
|
21
|
+
<CollapsibleContent class="expander-content">
|
|
22
|
+
<slot />
|
|
23
|
+
</CollapsibleContent>
|
|
24
|
+
</CollapsibleRoot>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<style scoped>
|
|
28
|
+
.expander-trigger {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 0.5em;
|
|
32
|
+
width: 100%;
|
|
33
|
+
padding: 0.5em 0;
|
|
34
|
+
background: none;
|
|
35
|
+
border: none;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
font: inherit;
|
|
38
|
+
font-size: var(--font-size-sm);
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
letter-spacing: 0.05em;
|
|
42
|
+
color: var(--color-text-secondary);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.expander-trigger:hover {
|
|
46
|
+
color: var(--color-text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.expander-caret {
|
|
50
|
+
display: inline-block;
|
|
51
|
+
width: 0;
|
|
52
|
+
height: 0;
|
|
53
|
+
border-left: 0.35em solid currentColor;
|
|
54
|
+
border-top: 0.3em solid transparent;
|
|
55
|
+
border-bottom: 0.3em solid transparent;
|
|
56
|
+
transition: transform 0.15s;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.expander-caret.open {
|
|
60
|
+
transform: rotate(90deg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.expander-content {
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
gap: 0.75em;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.expander-content[data-state="open"] {
|
|
71
|
+
animation: slideDown 200ms ease-out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.expander-content[data-state="closed"] {
|
|
75
|
+
animation: slideUp 200ms ease-out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@keyframes slideDown {
|
|
79
|
+
from {
|
|
80
|
+
height: 0;
|
|
81
|
+
}
|
|
82
|
+
to {
|
|
83
|
+
height: var(--reka-collapsible-content-height);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@keyframes slideUp {
|
|
88
|
+
from {
|
|
89
|
+
height: var(--reka-collapsible-content-height);
|
|
90
|
+
}
|
|
91
|
+
to {
|
|
92
|
+
height: 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
</style>
|
package/src/Hint/Hint.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Hint
|
|
2
|
+
|
|
3
|
+
An info icon that shows a tooltip on hover. Used alongside form labels to provide additional context.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
<ComponentDemo>
|
|
8
|
+
<span style="display: flex; align-items: center; gap: 8px;">
|
|
9
|
+
Population size <Hint text="The total number of individuals in the simulation." />
|
|
10
|
+
</span>
|
|
11
|
+
|
|
12
|
+
<template #code>
|
|
13
|
+
|
|
14
|
+
```vue
|
|
15
|
+
<span style="display: flex; align-items: center; gap: 8px;">
|
|
16
|
+
Population size
|
|
17
|
+
<Hint text="The total number of individuals in the simulation." />
|
|
18
|
+
</span>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
</template>
|
|
22
|
+
</ComponentDemo>
|
|
23
|
+
|
|
24
|
+
<!--@include: ./_api/hint.md-->
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test("Hint page renders demos", async ({ page }) => {
|
|
4
|
+
await page.goto("/cfa-simulator/cfasim-ui/components/hint");
|
|
5
|
+
await expect(page.locator("h1")).toBeVisible();
|
|
6
|
+
const demos = page.locator(".demo-preview");
|
|
7
|
+
await expect(demos.first()).toBeVisible();
|
|
8
|
+
await expect(demos.first().getByText("Population size")).toBeVisible();
|
|
9
|
+
await expect(
|
|
10
|
+
demos.first().getByRole("button", { name: "More info" }),
|
|
11
|
+
).toBeVisible();
|
|
12
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mount } from "@vue/test-utils";
|
|
3
|
+
import Hint from "./Hint.vue";
|
|
4
|
+
|
|
5
|
+
describe("Hint", () => {
|
|
6
|
+
it("renders a trigger button with help icon", () => {
|
|
7
|
+
const wrapper = mount(Hint, {
|
|
8
|
+
props: { text: "Some hint text" },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const button = wrapper.find(".HintTrigger");
|
|
12
|
+
expect(button.exists()).toBe(true);
|
|
13
|
+
expect(button.attributes("aria-label")).toBe("More info");
|
|
14
|
+
expect(button.attributes("type")).toBe("button");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("renders the help icon inside the trigger", () => {
|
|
18
|
+
const wrapper = mount(Hint, {
|
|
19
|
+
props: { text: "Some hint text" },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const icon = wrapper.find(".HintTrigger .Icon");
|
|
23
|
+
expect(icon.exists()).toBe(true);
|
|
24
|
+
expect(icon.text()).toBe("help");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("does not show tooltip content by default", () => {
|
|
28
|
+
const wrapper = mount(Hint, {
|
|
29
|
+
props: { text: "Hidden until hover" },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(wrapper.text()).not.toContain("Hidden until hover");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
TooltipArrow,
|
|
4
|
+
TooltipContent,
|
|
5
|
+
TooltipPortal,
|
|
6
|
+
TooltipProvider,
|
|
7
|
+
TooltipRoot,
|
|
8
|
+
TooltipTrigger,
|
|
9
|
+
} from "reka-ui";
|
|
10
|
+
import Icon from "../Icon/Icon.vue";
|
|
11
|
+
|
|
12
|
+
defineProps<{
|
|
13
|
+
text: string;
|
|
14
|
+
}>();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<TooltipProvider>
|
|
19
|
+
<TooltipRoot :delay-duration="0" disable-closing-trigger>
|
|
20
|
+
<TooltipTrigger as-child>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
class="HintTrigger"
|
|
24
|
+
aria-label="More info"
|
|
25
|
+
@pointerdown.prevent
|
|
26
|
+
>
|
|
27
|
+
<Icon icon="help" :size="16" />
|
|
28
|
+
</button>
|
|
29
|
+
</TooltipTrigger>
|
|
30
|
+
<TooltipPortal>
|
|
31
|
+
<TooltipContent class="HintContent" side="top" :side-offset="4">
|
|
32
|
+
{{ text }}
|
|
33
|
+
<TooltipArrow class="HintArrow" :width="10" :height="5" />
|
|
34
|
+
</TooltipContent>
|
|
35
|
+
</TooltipPortal>
|
|
36
|
+
</TooltipRoot>
|
|
37
|
+
</TooltipProvider>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<style scoped>
|
|
41
|
+
.HintTrigger {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
width: 1.25em;
|
|
46
|
+
height: 1.25em;
|
|
47
|
+
padding: 0;
|
|
48
|
+
margin: 0;
|
|
49
|
+
border: none;
|
|
50
|
+
border-radius: 50%;
|
|
51
|
+
background: transparent;
|
|
52
|
+
color: var(--color-text-secondary);
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.HintTrigger:hover {
|
|
58
|
+
color: var(--color-text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.HintTrigger:focus-visible {
|
|
62
|
+
outline: none;
|
|
63
|
+
box-shadow: var(--shadow-focus);
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
66
|
+
|
|
67
|
+
<style>
|
|
68
|
+
.HintContent {
|
|
69
|
+
max-width: 15rem;
|
|
70
|
+
padding: 0.5em 0.75em;
|
|
71
|
+
font-size: var(--font-size-xs);
|
|
72
|
+
line-height: 1.4;
|
|
73
|
+
color: var(--color-bg-0);
|
|
74
|
+
background-color: var(--color-text);
|
|
75
|
+
border-radius: 0.25em;
|
|
76
|
+
box-shadow: var(--shadow-md);
|
|
77
|
+
z-index: 100;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.HintArrow {
|
|
81
|
+
fill: var(--color-text);
|
|
82
|
+
}
|
|
83
|
+
</style>
|
package/src/Icon/Icon.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Icon
|
|
2
|
+
|
|
3
|
+
Renders a [Material Symbols Outlined](https://fonts.google.com/icons) icon.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
### Sizes
|
|
8
|
+
|
|
9
|
+
<ComponentDemo>
|
|
10
|
+
<Icon icon="help" size="sm" aria-label="help" />
|
|
11
|
+
<Icon icon="help" size="md" aria-label="help" />
|
|
12
|
+
<Icon icon="help" size="lg" aria-label="help" />
|
|
13
|
+
|
|
14
|
+
<template #code>
|
|
15
|
+
|
|
16
|
+
```vue
|
|
17
|
+
<Icon icon="help" size="sm" aria-label="help" />
|
|
18
|
+
<Icon icon="help" size="md" aria-label="help" />
|
|
19
|
+
<Icon icon="help" size="lg" aria-label="help" />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
</template>
|
|
23
|
+
</ComponentDemo>
|
|
24
|
+
|
|
25
|
+
### Filled
|
|
26
|
+
|
|
27
|
+
<ComponentDemo>
|
|
28
|
+
<Icon icon="favorite" size="lg" aria-label="favorite" />
|
|
29
|
+
<Icon icon="favorite" size="lg" :fill="true" aria-label="favorite filled" />
|
|
30
|
+
|
|
31
|
+
<template #code>
|
|
32
|
+
|
|
33
|
+
```vue
|
|
34
|
+
<Icon icon="favorite" size="lg" aria-label="favorite" />
|
|
35
|
+
<Icon icon="favorite" size="lg" :fill="true" aria-label="favorite filled" />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
</template>
|
|
39
|
+
</ComponentDemo>
|
|
40
|
+
|
|
41
|
+
### Inline in text
|
|
42
|
+
|
|
43
|
+
<ComponentDemo>
|
|
44
|
+
<p style="margin: 0">Click the <Icon icon="help" size="sm" :inline="true" aria-label="help" /> icon for more info.</p>
|
|
45
|
+
|
|
46
|
+
<template #code>
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<p>Click the <Icon icon="help" size="sm" :inline="true" aria-label="help" /> icon for more info.</p>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
</template>
|
|
53
|
+
</ComponentDemo>
|
|
54
|
+
|
|
55
|
+
<!--@include: ./_api/icon.md-->
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test("Icon page renders demos", async ({ page }) => {
|
|
4
|
+
await page.goto("/cfa-simulator/cfasim-ui/components/icon");
|
|
5
|
+
await expect(page.locator("h1")).toBeVisible();
|
|
6
|
+
const demos = page.locator(".demo-preview");
|
|
7
|
+
await expect(demos.first()).toBeVisible();
|
|
8
|
+
await expect(demos.first().locator(".Icon")).toHaveCount(3);
|
|
9
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CSSProperties } from "vue";
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
|
|
5
|
+
export type IconSize = "sm" | "md" | "lg" | "xl";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
icon: string;
|
|
9
|
+
size?: IconSize | number;
|
|
10
|
+
fill?: boolean;
|
|
11
|
+
weight?: number;
|
|
12
|
+
grade?: number;
|
|
13
|
+
decorative?: boolean;
|
|
14
|
+
ariaLabel?: string;
|
|
15
|
+
inline?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
19
|
+
size: "md",
|
|
20
|
+
fill: false,
|
|
21
|
+
decorative: true,
|
|
22
|
+
inline: false,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const sizePreset = computed(() =>
|
|
26
|
+
typeof props.size === "string" ? props.size : undefined,
|
|
27
|
+
);
|
|
28
|
+
const numericSize = computed(() =>
|
|
29
|
+
typeof props.size === "number" ? props.size : undefined,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const inlineStyle = computed<CSSProperties>(() => {
|
|
33
|
+
const style: CSSProperties = {};
|
|
34
|
+
if (numericSize.value !== undefined) {
|
|
35
|
+
style.fontSize = `${numericSize.value}px`;
|
|
36
|
+
(style as Record<string, unknown>)["--icon-opsz"] = numericSize.value;
|
|
37
|
+
}
|
|
38
|
+
if (props.weight !== undefined) {
|
|
39
|
+
(style as Record<string, unknown>)["--icon-weight"] = props.weight;
|
|
40
|
+
}
|
|
41
|
+
if (props.grade !== undefined) {
|
|
42
|
+
(style as Record<string, unknown>)["--icon-grade"] = props.grade;
|
|
43
|
+
}
|
|
44
|
+
return style;
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<span
|
|
50
|
+
class="Icon"
|
|
51
|
+
:data-size="sizePreset"
|
|
52
|
+
:data-fill="fill ? 'true' : undefined"
|
|
53
|
+
:data-inline="inline ? 'true' : undefined"
|
|
54
|
+
:style="inlineStyle"
|
|
55
|
+
:aria-hidden="decorative ? true : undefined"
|
|
56
|
+
:aria-label="decorative ? undefined : ariaLabel"
|
|
57
|
+
:role="decorative ? undefined : 'img'"
|
|
58
|
+
>{{ icon }}</span
|
|
59
|
+
>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.Icon {
|
|
64
|
+
font-family: "Material Symbols Outlined", sans-serif;
|
|
65
|
+
font-weight: normal;
|
|
66
|
+
font-style: normal;
|
|
67
|
+
font-size: 24px;
|
|
68
|
+
line-height: 1;
|
|
69
|
+
letter-spacing: normal;
|
|
70
|
+
text-transform: none;
|
|
71
|
+
display: inline-block;
|
|
72
|
+
white-space: nowrap;
|
|
73
|
+
word-wrap: normal;
|
|
74
|
+
direction: ltr;
|
|
75
|
+
font-feature-settings: "liga";
|
|
76
|
+
-webkit-font-smoothing: antialiased;
|
|
77
|
+
font-variation-settings:
|
|
78
|
+
"FILL" var(--icon-fill, 0),
|
|
79
|
+
"wght" var(--icon-weight, 400),
|
|
80
|
+
"GRAD" var(--icon-grade, 0),
|
|
81
|
+
"opsz" var(--icon-opsz, 24);
|
|
82
|
+
color: inherit;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.Icon[data-size="sm"] {
|
|
86
|
+
font-size: 20px;
|
|
87
|
+
--icon-opsz: 20;
|
|
88
|
+
}
|
|
89
|
+
.Icon[data-size="md"] {
|
|
90
|
+
font-size: 24px;
|
|
91
|
+
--icon-opsz: 24;
|
|
92
|
+
}
|
|
93
|
+
.Icon[data-size="lg"] {
|
|
94
|
+
font-size: 28px;
|
|
95
|
+
--icon-opsz: 28;
|
|
96
|
+
}
|
|
97
|
+
.Icon[data-size="xl"] {
|
|
98
|
+
font-size: 32px;
|
|
99
|
+
--icon-opsz: 32;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.Icon[data-fill="true"] {
|
|
103
|
+
--icon-fill: 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.Icon[data-inline="true"] {
|
|
107
|
+
font-size: inherit;
|
|
108
|
+
vertical-align: middle;
|
|
109
|
+
transform: scale(1.2) translateY(-0.05em);
|
|
110
|
+
transform-origin: 50% 50%;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# NumberInput
|
|
2
|
+
|
|
3
|
+
A number input field with optional slider, percent mode, and validation.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
### Basic
|
|
8
|
+
|
|
9
|
+
<script setup>
|
|
10
|
+
import { ref } from 'vue'
|
|
11
|
+
const days = ref(10)
|
|
12
|
+
const population = ref(100000)
|
|
13
|
+
const coverage = ref(0.5)
|
|
14
|
+
const r0 = ref(3.5)
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<ComponentDemo>
|
|
18
|
+
<div style="width: 300px">
|
|
19
|
+
<NumberInput v-model="days" label="Days" placeholder="Number of days" />
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<template #code>
|
|
23
|
+
|
|
24
|
+
```vue
|
|
25
|
+
<script setup>
|
|
26
|
+
import { ref } from "vue";
|
|
27
|
+
const days = ref(10);
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<NumberInput v-model="days" label="Days" placeholder="Number of days" />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
</template>
|
|
34
|
+
</ComponentDemo>
|
|
35
|
+
|
|
36
|
+
### With hint and validation
|
|
37
|
+
|
|
38
|
+
<ComponentDemo>
|
|
39
|
+
<div style="width: 300px">
|
|
40
|
+
<NumberInput
|
|
41
|
+
v-model="population"
|
|
42
|
+
label="Population"
|
|
43
|
+
hint="Total number of individuals"
|
|
44
|
+
:min="1000"
|
|
45
|
+
:max="100000"
|
|
46
|
+
:step="1"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<template #code>
|
|
51
|
+
|
|
52
|
+
```vue
|
|
53
|
+
<NumberInput
|
|
54
|
+
v-model="population"
|
|
55
|
+
label="Population"
|
|
56
|
+
hint="Total number of individuals"
|
|
57
|
+
:min="1000"
|
|
58
|
+
:max="100000"
|
|
59
|
+
:step="1"
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
</template>
|
|
64
|
+
</ComponentDemo>
|
|
65
|
+
|
|
66
|
+
### Percent mode
|
|
67
|
+
|
|
68
|
+
<ComponentDemo>
|
|
69
|
+
<div style="width: 300px">
|
|
70
|
+
<NumberInput
|
|
71
|
+
v-model="coverage"
|
|
72
|
+
label="Vaccination coverage"
|
|
73
|
+
percent
|
|
74
|
+
:max="1"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<template #code>
|
|
79
|
+
|
|
80
|
+
```vue
|
|
81
|
+
<NumberInput v-model="coverage" label="Vaccination coverage" percent :max="1" />
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
</template>
|
|
85
|
+
</ComponentDemo>
|
|
86
|
+
|
|
87
|
+
### Slider
|
|
88
|
+
|
|
89
|
+
<ComponentDemo>
|
|
90
|
+
<div style="width: 300px">
|
|
91
|
+
<NumberInput
|
|
92
|
+
v-model="r0"
|
|
93
|
+
label="R0"
|
|
94
|
+
hint="Basic reproduction number"
|
|
95
|
+
:step="0.1"
|
|
96
|
+
:min="1"
|
|
97
|
+
:max="18"
|
|
98
|
+
slider
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<template #code>
|
|
103
|
+
|
|
104
|
+
```vue
|
|
105
|
+
<NumberInput
|
|
106
|
+
v-model="r0"
|
|
107
|
+
label="R0"
|
|
108
|
+
hint="Basic reproduction number"
|
|
109
|
+
:step="0.1"
|
|
110
|
+
:min="1"
|
|
111
|
+
:max="18"
|
|
112
|
+
slider
|
|
113
|
+
/>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
</template>
|
|
117
|
+
</ComponentDemo>
|
|
118
|
+
|
|
119
|
+
### Live slider
|
|
120
|
+
|
|
121
|
+
With `live`, the model updates while dragging the slider thumb rather than only on release.
|
|
122
|
+
|
|
123
|
+
<ComponentDemo>
|
|
124
|
+
<div style="width: 300px">
|
|
125
|
+
<NumberInput
|
|
126
|
+
v-model="coverage"
|
|
127
|
+
label="Vaccination coverage"
|
|
128
|
+
percent
|
|
129
|
+
slider
|
|
130
|
+
live
|
|
131
|
+
:max="1"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<template #code>
|
|
136
|
+
|
|
137
|
+
```vue
|
|
138
|
+
<NumberInput
|
|
139
|
+
v-model="coverage"
|
|
140
|
+
label="Vaccination coverage"
|
|
141
|
+
percent
|
|
142
|
+
slider
|
|
143
|
+
live
|
|
144
|
+
:max="1"
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
</template>
|
|
149
|
+
</ComponentDemo>
|
|
150
|
+
|
|
151
|
+
### Live input
|
|
152
|
+
|
|
153
|
+
With `live` on a regular input, the model updates as you type (debounced 300ms). Arrow keys and spinner buttons commit immediately.
|
|
154
|
+
|
|
155
|
+
<ComponentDemo>
|
|
156
|
+
<div style="width: 300px">
|
|
157
|
+
<NumberInput v-model="days" label="Days" live />
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<template #code>
|
|
161
|
+
|
|
162
|
+
```vue
|
|
163
|
+
<NumberInput v-model="days" label="Days" live />
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
</template>
|
|
167
|
+
</ComponentDemo>
|
|
168
|
+
|
|
169
|
+
<!--@include: ./_api/number-input.md-->
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test("NumberInput page renders demos", async ({ page }) => {
|
|
4
|
+
await page.goto("/cfa-simulator/cfasim-ui/components/number-input");
|
|
5
|
+
await expect(page.locator("h1")).toBeVisible();
|
|
6
|
+
const demos = page.locator(".demo-preview");
|
|
7
|
+
await expect(demos.first()).toBeVisible();
|
|
8
|
+
await expect(demos.first().getByText("Days")).toBeVisible();
|
|
9
|
+
await expect(demos.first().locator('input[type="number"]')).toBeVisible();
|
|
10
|
+
});
|