@cfasim-ui/components 0.1.8 → 0.1.10
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/dist/Box/Box.d.ts +24 -0
- package/dist/Box/Box.spec.d.ts +1 -0
- package/dist/Box/Box.test.d.ts +1 -0
- package/dist/Button/Button.d.ts +29 -0
- package/dist/Button/Button.spec.d.ts +1 -0
- package/dist/Button/Button.test.d.ts +1 -0
- package/dist/Expander/Expander.d.ts +28 -0
- package/dist/Expander/Expander.spec.d.ts +1 -0
- package/dist/Hint/Hint.d.ts +5 -0
- package/dist/Hint/Hint.spec.d.ts +1 -0
- package/dist/Hint/Hint.test.d.ts +1 -0
- package/dist/Icon/Icon.d.ts +18 -0
- package/dist/Icon/Icon.spec.d.ts +1 -0
- package/dist/LightDarkToggle/LightDarkToggle.d.ts +2 -0
- package/dist/NumberInput/NumberInput.d.ts +21 -0
- package/dist/NumberInput/NumberInput.spec.d.ts +1 -0
- package/dist/NumberInput/NumberInput.test.d.ts +1 -0
- package/dist/SelectBox/SelectBox.d.ts +19 -0
- package/dist/SelectBox/SelectBox.spec.d.ts +1 -0
- package/dist/SelectBox/SelectBox.test.d.ts +1 -0
- package/dist/SidebarLayout/SidebarLayout.d.ts +37 -0
- package/dist/SidebarLayout/SidebarLayout.test.d.ts +1 -0
- package/dist/Spinner/Spinner.d.ts +9 -0
- package/dist/Spinner/Spinner.spec.d.ts +1 -0
- package/dist/TextInput/TextInput.d.ts +14 -0
- package/dist/TextInput/TextInput.spec.d.ts +1 -0
- package/dist/TextInput/TextInput.test.d.ts +1 -0
- package/dist/Toggle/Toggle.d.ts +14 -0
- package/dist/Toggle/Toggle.spec.d.ts +1 -0
- package/dist/Toggle/Toggle.test.d.ts +1 -0
- package/dist/index.css +2 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +700 -0
- package/package.json +17 -3
- package/src/Box/Box.md +0 -41
- package/src/Box/Box.spec.ts +0 -13
- package/src/Box/Box.test.ts +0 -49
- package/src/Box/Box.vue +0 -52
- package/src/Button/Button.md +0 -55
- package/src/Button/Button.spec.ts +0 -18
- package/src/Button/Button.test.ts +0 -36
- package/src/Button/Button.vue +0 -81
- package/src/Expander/Expander.md +0 -23
- package/src/Expander/Expander.spec.ts +0 -14
- package/src/Expander/Expander.vue +0 -95
- package/src/Hint/Hint.md +0 -24
- package/src/Hint/Hint.spec.ts +0 -12
- package/src/Hint/Hint.test.ts +0 -34
- package/src/Hint/Hint.vue +0 -83
- package/src/Icon/Icon.md +0 -55
- package/src/Icon/Icon.spec.ts +0 -9
- package/src/Icon/Icon.vue +0 -112
- package/src/LightDarkToggle/LightDarkToggle.vue +0 -49
- package/src/NumberInput/NumberInput.md +0 -187
- package/src/NumberInput/NumberInput.spec.ts +0 -10
- package/src/NumberInput/NumberInput.test.ts +0 -580
- package/src/NumberInput/NumberInput.vue +0 -446
- package/src/SelectBox/SelectBox.md +0 -56
- package/src/SelectBox/SelectBox.spec.ts +0 -9
- package/src/SelectBox/SelectBox.test.ts +0 -42
- package/src/SelectBox/SelectBox.vue +0 -190
- package/src/SidebarLayout/SidebarLayout.md +0 -104
- package/src/SidebarLayout/SidebarLayout.test.ts +0 -86
- package/src/SidebarLayout/SidebarLayout.vue +0 -465
- package/src/Spinner/Spinner.md +0 -45
- package/src/Spinner/Spinner.spec.ts +0 -9
- package/src/Spinner/Spinner.vue +0 -55
- package/src/TextInput/TextInput.md +0 -41
- package/src/TextInput/TextInput.spec.ts +0 -10
- package/src/TextInput/TextInput.test.ts +0 -70
- package/src/TextInput/TextInput.vue +0 -90
- package/src/Toggle/Toggle.md +0 -68
- package/src/Toggle/Toggle.spec.ts +0 -13
- package/src/Toggle/Toggle.test.ts +0 -35
- package/src/Toggle/Toggle.vue +0 -81
- package/src/index.ts +0 -15
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
SelectContent,
|
|
4
|
-
SelectItem,
|
|
5
|
-
SelectItemIndicator,
|
|
6
|
-
SelectItemText,
|
|
7
|
-
SelectPortal,
|
|
8
|
-
SelectRoot,
|
|
9
|
-
SelectTrigger,
|
|
10
|
-
SelectValue,
|
|
11
|
-
SelectViewport,
|
|
12
|
-
useId,
|
|
13
|
-
} from "reka-ui";
|
|
14
|
-
|
|
15
|
-
export interface SelectOption {
|
|
16
|
-
value: string;
|
|
17
|
-
label: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const model = defineModel<string>();
|
|
21
|
-
|
|
22
|
-
const props = defineProps<{
|
|
23
|
-
label?: string;
|
|
24
|
-
ariaLabel?: string;
|
|
25
|
-
options: SelectOption[];
|
|
26
|
-
placeholder?: string;
|
|
27
|
-
}>();
|
|
28
|
-
|
|
29
|
-
const id = useId();
|
|
30
|
-
</script>
|
|
31
|
-
|
|
32
|
-
<template>
|
|
33
|
-
<div class="select-box">
|
|
34
|
-
<label v-if="label" :id="`${id}-label`" class="select-label">{{
|
|
35
|
-
label
|
|
36
|
-
}}</label>
|
|
37
|
-
<SelectRoot v-model="model">
|
|
38
|
-
<SelectTrigger
|
|
39
|
-
class="select-trigger"
|
|
40
|
-
:aria-labelledby="props.label ? `${id}-label` : undefined"
|
|
41
|
-
:aria-label="!props.label ? props.ariaLabel : undefined"
|
|
42
|
-
>
|
|
43
|
-
<SelectValue :placeholder="placeholder" />
|
|
44
|
-
<span class="select-icon" aria-hidden="true">
|
|
45
|
-
<svg
|
|
46
|
-
width="12"
|
|
47
|
-
height="12"
|
|
48
|
-
viewBox="0 0 12 12"
|
|
49
|
-
fill="none"
|
|
50
|
-
stroke="currentColor"
|
|
51
|
-
stroke-width="2"
|
|
52
|
-
stroke-linecap="round"
|
|
53
|
-
stroke-linejoin="round"
|
|
54
|
-
>
|
|
55
|
-
<path d="M3 4.5L6 7.5L9 4.5" />
|
|
56
|
-
</svg>
|
|
57
|
-
</span>
|
|
58
|
-
</SelectTrigger>
|
|
59
|
-
<SelectPortal>
|
|
60
|
-
<SelectContent
|
|
61
|
-
class="select-content"
|
|
62
|
-
position="popper"
|
|
63
|
-
:side-offset="4"
|
|
64
|
-
:body-lock="false"
|
|
65
|
-
>
|
|
66
|
-
<SelectViewport class="select-viewport">
|
|
67
|
-
<SelectItem
|
|
68
|
-
v-for="opt in options"
|
|
69
|
-
:key="opt.value"
|
|
70
|
-
:value="opt.value"
|
|
71
|
-
class="select-item"
|
|
72
|
-
>
|
|
73
|
-
<SelectItemText>{{ opt.label }}</SelectItemText>
|
|
74
|
-
<SelectItemIndicator class="select-indicator">
|
|
75
|
-
<svg
|
|
76
|
-
width="12"
|
|
77
|
-
height="12"
|
|
78
|
-
viewBox="0 0 12 12"
|
|
79
|
-
fill="none"
|
|
80
|
-
stroke="currentColor"
|
|
81
|
-
stroke-width="2"
|
|
82
|
-
stroke-linecap="round"
|
|
83
|
-
stroke-linejoin="round"
|
|
84
|
-
>
|
|
85
|
-
<path d="M2 6L5 9L10 3" />
|
|
86
|
-
</svg>
|
|
87
|
-
</SelectItemIndicator>
|
|
88
|
-
</SelectItem>
|
|
89
|
-
</SelectViewport>
|
|
90
|
-
</SelectContent>
|
|
91
|
-
</SelectPortal>
|
|
92
|
-
</SelectRoot>
|
|
93
|
-
</div>
|
|
94
|
-
</template>
|
|
95
|
-
|
|
96
|
-
<style scoped>
|
|
97
|
-
.select-box {
|
|
98
|
-
display: flex;
|
|
99
|
-
flex-direction: column;
|
|
100
|
-
gap: 0.25em;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.select-label {
|
|
104
|
-
font-size: var(--font-size-sm);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.select-trigger {
|
|
108
|
-
display: inline-flex;
|
|
109
|
-
align-items: center;
|
|
110
|
-
justify-content: space-between;
|
|
111
|
-
gap: 0.5em;
|
|
112
|
-
font-size: var(--font-size-sm);
|
|
113
|
-
height: 2.5em;
|
|
114
|
-
padding: 0 0.75em;
|
|
115
|
-
border: 1px solid var(--color-border);
|
|
116
|
-
border-radius: 0.375em;
|
|
117
|
-
background: var(--color-bg-0);
|
|
118
|
-
cursor: pointer;
|
|
119
|
-
width: auto;
|
|
120
|
-
font-family: inherit;
|
|
121
|
-
color: inherit;
|
|
122
|
-
line-height: 1.4;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.select-trigger:hover {
|
|
126
|
-
border-color: var(--color-border-hover);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.select-trigger:focus-visible {
|
|
130
|
-
outline: 2px solid var(--color-primary);
|
|
131
|
-
outline-offset: -1px;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.select-trigger[data-placeholder] {
|
|
135
|
-
color: var(--color-text-secondary);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.select-icon {
|
|
139
|
-
display: flex;
|
|
140
|
-
align-items: center;
|
|
141
|
-
flex-shrink: 0;
|
|
142
|
-
}
|
|
143
|
-
</style>
|
|
144
|
-
|
|
145
|
-
<style>
|
|
146
|
-
.select-content {
|
|
147
|
-
z-index: 100;
|
|
148
|
-
background: var(--color-bg-0);
|
|
149
|
-
border: 1px solid var(--color-border);
|
|
150
|
-
border-radius: 0.25em;
|
|
151
|
-
box-shadow:
|
|
152
|
-
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
153
|
-
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
154
|
-
min-width: var(--reka-select-trigger-width);
|
|
155
|
-
max-height: var(--reka-select-content-available-height);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.select-viewport {
|
|
159
|
-
padding: 0.25em;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.select-item {
|
|
163
|
-
display: flex;
|
|
164
|
-
align-items: center;
|
|
165
|
-
justify-content: space-between;
|
|
166
|
-
gap: 0.5em;
|
|
167
|
-
padding: 0.25em 0.5em;
|
|
168
|
-
border-radius: 0.25em;
|
|
169
|
-
font-size: var(--font-size-sm);
|
|
170
|
-
white-space: nowrap;
|
|
171
|
-
cursor: pointer;
|
|
172
|
-
user-select: none;
|
|
173
|
-
outline: none;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.select-item[data-highlighted] {
|
|
177
|
-
background: var(--color-primary);
|
|
178
|
-
color: white;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.select-item[data-state="checked"] {
|
|
182
|
-
font-weight: 600;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
.select-indicator {
|
|
186
|
-
display: flex;
|
|
187
|
-
align-items: center;
|
|
188
|
-
flex-shrink: 0;
|
|
189
|
-
}
|
|
190
|
-
</style>
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# SidebarLayout
|
|
2
|
-
|
|
3
|
-
A responsive two-panel layout with a collapsible sidebar and main content area. On mobile, the sidebar becomes an overlay.
|
|
4
|
-
|
|
5
|
-
## Demo
|
|
6
|
-
|
|
7
|
-
<a href="/cfa-simulator/docs/demos/sidebar-layout/index.html" target="_blank">Open in full window ↗</a>
|
|
8
|
-
|
|
9
|
-
<div style="border: 1px solid var(--vp-c-border); border-radius: 8px; overflow: hidden; height: 500px;">
|
|
10
|
-
<iframe src="/cfa-simulator/docs/demos/sidebar-layout/index.html" style="width: 100%; height: 100%; border: none;" />
|
|
11
|
-
</div>
|
|
12
|
-
|
|
13
|
-
## Tabs Demo (Router Mode)
|
|
14
|
-
|
|
15
|
-
<a href="/cfa-simulator/docs/demos/sidebar-tabs/index.html" target="_blank">Open in full window ↗</a>
|
|
16
|
-
|
|
17
|
-
<div style="border: 1px solid var(--vp-c-border); border-radius: 8px; overflow: hidden; height: 500px;">
|
|
18
|
-
<iframe src="/cfa-simulator/docs/demos/sidebar-tabs/index.html" style="width: 100%; height: 100%; border: none;" />
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
## Usage
|
|
22
|
-
|
|
23
|
-
```vue
|
|
24
|
-
<SidebarLayout>
|
|
25
|
-
<template #sidebar>
|
|
26
|
-
<h2>Controls</h2>
|
|
27
|
-
<NumberInput v-model="value" label="Parameter" slider live />
|
|
28
|
-
</template>
|
|
29
|
-
<h1>Main Content</h1>
|
|
30
|
-
<p>Your charts and data go here.</p>
|
|
31
|
-
</SidebarLayout>
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Slots
|
|
35
|
-
|
|
36
|
-
| Slot | Description |
|
|
37
|
-
| --------- | ------------------------------------------ |
|
|
38
|
-
| `sidebar` | Content rendered in the left sidebar panel |
|
|
39
|
-
| `default` | Main content area |
|
|
40
|
-
|
|
41
|
-
## Props
|
|
42
|
-
|
|
43
|
-
| Prop | Type | Default | Description |
|
|
44
|
-
| ------------- | --------- | ----------- | ---------------------------------------------------- |
|
|
45
|
-
| `hideTopbar` | `boolean` | `false` | Hides the topbar that contains the light/dark toggle |
|
|
46
|
-
| `tabs` | `Tab[]` | `undefined` | Array of tab definitions to render in the main area |
|
|
47
|
-
| `v-model:tab` | `string` | `undefined` | The active tab value (two-way binding) |
|
|
48
|
-
|
|
49
|
-
### Tab type
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
interface Tab {
|
|
53
|
-
value: string; // unique identifier
|
|
54
|
-
label: string; // display text
|
|
55
|
-
to?: string; // optional route path for vue-router integration
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Tabs
|
|
60
|
-
|
|
61
|
-
When the `tabs` prop is provided, a tab bar renders at the top of the main content area. Tabs support two modes:
|
|
62
|
-
|
|
63
|
-
### Local mode
|
|
64
|
-
|
|
65
|
-
Use `v-model:tab` to control which tab is active. Render content conditionally in the default slot.
|
|
66
|
-
|
|
67
|
-
```vue
|
|
68
|
-
<script setup>
|
|
69
|
-
import { ref } from "vue";
|
|
70
|
-
const activeTab = ref("chart");
|
|
71
|
-
</script>
|
|
72
|
-
|
|
73
|
-
<SidebarLayout
|
|
74
|
-
v-model:tab="activeTab"
|
|
75
|
-
:tabs="[
|
|
76
|
-
{ value: 'chart', label: 'Chart' },
|
|
77
|
-
{ value: 'data', label: 'Data' },
|
|
78
|
-
]"
|
|
79
|
-
>
|
|
80
|
-
<template #sidebar>
|
|
81
|
-
<h2>Controls</h2>
|
|
82
|
-
</template>
|
|
83
|
-
<div v-if="activeTab === 'chart'">Chart content</div>
|
|
84
|
-
<div v-if="activeTab === 'data'">Data table</div>
|
|
85
|
-
</SidebarLayout>
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Router mode
|
|
89
|
-
|
|
90
|
-
When tabs include a `to` property and vue-router is installed, clicking a tab navigates to that route. The active tab is automatically determined from the current route.
|
|
91
|
-
|
|
92
|
-
```vue
|
|
93
|
-
<SidebarLayout
|
|
94
|
-
:tabs="[
|
|
95
|
-
{ value: 'chart', label: 'Chart', to: '/model/chart' },
|
|
96
|
-
{ value: 'data', label: 'Data', to: '/model/data' },
|
|
97
|
-
]"
|
|
98
|
-
>
|
|
99
|
-
<template #sidebar>
|
|
100
|
-
<h2>Controls</h2>
|
|
101
|
-
</template>
|
|
102
|
-
<RouterView />
|
|
103
|
-
</SidebarLayout>
|
|
104
|
-
```
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { mount } from "@vue/test-utils";
|
|
3
|
-
import SidebarLayout from "./SidebarLayout.vue";
|
|
4
|
-
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
// SidebarLayout uses window.matchMedia
|
|
7
|
-
window.matchMedia = vi.fn().mockReturnValue({
|
|
8
|
-
matches: false,
|
|
9
|
-
addEventListener: vi.fn(),
|
|
10
|
-
removeEventListener: vi.fn(),
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const tabs = [
|
|
15
|
-
{ value: "chart", label: "Chart" },
|
|
16
|
-
{ value: "data", label: "Data" },
|
|
17
|
-
{ value: "table", label: "Table" },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
describe("SidebarLayout tabs", () => {
|
|
21
|
-
it("renders without tabs by default", () => {
|
|
22
|
-
const wrapper = mount(SidebarLayout);
|
|
23
|
-
expect(wrapper.find("[role='tablist']").exists()).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("renders tab triggers when tabs prop is provided", () => {
|
|
27
|
-
const wrapper = mount(SidebarLayout, { props: { tabs } });
|
|
28
|
-
const triggers = wrapper.findAll("[role='tab']");
|
|
29
|
-
expect(triggers).toHaveLength(3);
|
|
30
|
-
expect(triggers[0].text()).toBe("Chart");
|
|
31
|
-
expect(triggers[1].text()).toBe("Data");
|
|
32
|
-
expect(triggers[2].text()).toBe("Table");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("defaults to first tab as active", () => {
|
|
36
|
-
const wrapper = mount(SidebarLayout, { props: { tabs } });
|
|
37
|
-
const triggers = wrapper.findAll("[role='tab']");
|
|
38
|
-
expect(triggers[0].attributes("data-state")).toBe("active");
|
|
39
|
-
expect(triggers[1].attributes("data-state")).toBe("inactive");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("activates tab matching v-model:tab", () => {
|
|
43
|
-
const wrapper = mount(SidebarLayout, {
|
|
44
|
-
props: { tabs, tab: "data" },
|
|
45
|
-
});
|
|
46
|
-
const triggers = wrapper.findAll("[role='tab']");
|
|
47
|
-
expect(triggers[1].attributes("data-state")).toBe("active");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("emits update:tab when a tab is clicked", async () => {
|
|
51
|
-
let updated: string | undefined;
|
|
52
|
-
const wrapper = mount(SidebarLayout, {
|
|
53
|
-
props: {
|
|
54
|
-
tabs,
|
|
55
|
-
tab: "chart",
|
|
56
|
-
"onUpdate:tab": (v: string | undefined) => {
|
|
57
|
-
updated = v;
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
const triggers = wrapper.findAll("[role='tab']");
|
|
62
|
-
// reka-ui triggers need mousedown for activation in happy-dom
|
|
63
|
-
await triggers[2].trigger("mousedown");
|
|
64
|
-
await triggers[2].trigger("focus");
|
|
65
|
-
await triggers[2].trigger("mouseup");
|
|
66
|
-
await triggers[2].trigger("click");
|
|
67
|
-
expect(updated).toBe("table");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("renders default slot content inside tabbed layout", () => {
|
|
71
|
-
const wrapper = mount(SidebarLayout, {
|
|
72
|
-
props: { tabs },
|
|
73
|
-
slots: { default: '<p class="test-content">Hello</p>' },
|
|
74
|
-
});
|
|
75
|
-
expect(wrapper.find(".test-content").text()).toBe("Hello");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("renders sidebar slot alongside tabs", () => {
|
|
79
|
-
const wrapper = mount(SidebarLayout, {
|
|
80
|
-
props: { tabs },
|
|
81
|
-
slots: { sidebar: '<div class="sidebar-content">Sidebar</div>' },
|
|
82
|
-
});
|
|
83
|
-
expect(wrapper.find(".sidebar-content").text()).toBe("Sidebar");
|
|
84
|
-
expect(wrapper.findAll("[role='tab']")).toHaveLength(3);
|
|
85
|
-
});
|
|
86
|
-
});
|