@cfasim-ui/components 0.1.9 → 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.
Files changed (76) hide show
  1. package/dist/Box/Box.d.ts +24 -0
  2. package/dist/Box/Box.spec.d.ts +1 -0
  3. package/dist/Box/Box.test.d.ts +1 -0
  4. package/dist/Button/Button.d.ts +29 -0
  5. package/dist/Button/Button.spec.d.ts +1 -0
  6. package/dist/Button/Button.test.d.ts +1 -0
  7. package/dist/Expander/Expander.d.ts +28 -0
  8. package/dist/Expander/Expander.spec.d.ts +1 -0
  9. package/dist/Hint/Hint.d.ts +5 -0
  10. package/dist/Hint/Hint.spec.d.ts +1 -0
  11. package/dist/Hint/Hint.test.d.ts +1 -0
  12. package/dist/Icon/Icon.d.ts +18 -0
  13. package/dist/Icon/Icon.spec.d.ts +1 -0
  14. package/dist/LightDarkToggle/LightDarkToggle.d.ts +2 -0
  15. package/dist/NumberInput/NumberInput.d.ts +21 -0
  16. package/dist/NumberInput/NumberInput.spec.d.ts +1 -0
  17. package/dist/NumberInput/NumberInput.test.d.ts +1 -0
  18. package/dist/SelectBox/SelectBox.d.ts +19 -0
  19. package/dist/SelectBox/SelectBox.spec.d.ts +1 -0
  20. package/dist/SelectBox/SelectBox.test.d.ts +1 -0
  21. package/dist/SidebarLayout/SidebarLayout.d.ts +37 -0
  22. package/dist/SidebarLayout/SidebarLayout.test.d.ts +1 -0
  23. package/dist/Spinner/Spinner.d.ts +9 -0
  24. package/dist/Spinner/Spinner.spec.d.ts +1 -0
  25. package/dist/TextInput/TextInput.d.ts +14 -0
  26. package/dist/TextInput/TextInput.spec.d.ts +1 -0
  27. package/dist/TextInput/TextInput.test.d.ts +1 -0
  28. package/dist/Toggle/Toggle.d.ts +14 -0
  29. package/dist/Toggle/Toggle.spec.d.ts +1 -0
  30. package/dist/Toggle/Toggle.test.d.ts +1 -0
  31. package/dist/index.css +2 -0
  32. package/dist/index.d.ts +15 -0
  33. package/dist/index.js +700 -0
  34. package/package.json +17 -3
  35. package/src/Box/Box.md +0 -41
  36. package/src/Box/Box.spec.ts +0 -13
  37. package/src/Box/Box.test.ts +0 -49
  38. package/src/Box/Box.vue +0 -52
  39. package/src/Button/Button.md +0 -55
  40. package/src/Button/Button.spec.ts +0 -18
  41. package/src/Button/Button.test.ts +0 -36
  42. package/src/Button/Button.vue +0 -81
  43. package/src/Expander/Expander.md +0 -23
  44. package/src/Expander/Expander.spec.ts +0 -14
  45. package/src/Expander/Expander.vue +0 -95
  46. package/src/Hint/Hint.md +0 -24
  47. package/src/Hint/Hint.spec.ts +0 -12
  48. package/src/Hint/Hint.test.ts +0 -34
  49. package/src/Hint/Hint.vue +0 -83
  50. package/src/Icon/Icon.md +0 -55
  51. package/src/Icon/Icon.spec.ts +0 -9
  52. package/src/Icon/Icon.vue +0 -112
  53. package/src/LightDarkToggle/LightDarkToggle.vue +0 -49
  54. package/src/NumberInput/NumberInput.md +0 -187
  55. package/src/NumberInput/NumberInput.spec.ts +0 -10
  56. package/src/NumberInput/NumberInput.test.ts +0 -580
  57. package/src/NumberInput/NumberInput.vue +0 -446
  58. package/src/SelectBox/SelectBox.md +0 -56
  59. package/src/SelectBox/SelectBox.spec.ts +0 -9
  60. package/src/SelectBox/SelectBox.test.ts +0 -42
  61. package/src/SelectBox/SelectBox.vue +0 -190
  62. package/src/SidebarLayout/SidebarLayout.md +0 -104
  63. package/src/SidebarLayout/SidebarLayout.test.ts +0 -86
  64. package/src/SidebarLayout/SidebarLayout.vue +0 -465
  65. package/src/Spinner/Spinner.md +0 -45
  66. package/src/Spinner/Spinner.spec.ts +0 -9
  67. package/src/Spinner/Spinner.vue +0 -55
  68. package/src/TextInput/TextInput.md +0 -41
  69. package/src/TextInput/TextInput.spec.ts +0 -10
  70. package/src/TextInput/TextInput.test.ts +0 -70
  71. package/src/TextInput/TextInput.vue +0 -90
  72. package/src/Toggle/Toggle.md +0 -68
  73. package/src/Toggle/Toggle.spec.ts +0 -13
  74. package/src/Toggle/Toggle.test.ts +0 -35
  75. package/src/Toggle/Toggle.vue +0 -81
  76. package/src/index.ts +0 -15
@@ -1,446 +0,0 @@
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
- placeholder?: string;
11
- step?: number;
12
- min?: number;
13
- max?: number;
14
- hint?: string;
15
- percent?: boolean;
16
- slider?: boolean;
17
- live?: boolean;
18
- numberType?: "integer" | "float";
19
- }>();
20
-
21
- const sliderMin = computed(() => props.min ?? (props.percent ? 0 : 0));
22
- const sliderMax = computed(() => props.max ?? (props.percent ? 1 : 100));
23
- const sliderStep = computed(() => props.step ?? (props.percent ? 0.01 : 1));
24
-
25
- function formatSliderValue(v: number | undefined) {
26
- if (v == null) return "";
27
- if (props.percent) return (v * 100).toFixed(0) + "%";
28
- return v.toLocaleString("en-US");
29
- }
30
-
31
- function toDisplay(v: number | undefined) {
32
- if (v == null) return v;
33
- return props.percent ? Math.round(v * 10000) / 100 : v;
34
- }
35
-
36
- function fromDisplay(v: number) {
37
- return props.percent ? v / 100 : v;
38
- }
39
-
40
- function coerceInteger(v: number): number {
41
- if (props.numberType !== "integer") return v;
42
- // Truncate the display value to an integer, then convert back
43
- const display = toDisplay(v);
44
- if (display == null) return v;
45
- return fromDisplay(Math.trunc(display));
46
- }
47
-
48
- function formatWithCommas(v: number | undefined): string {
49
- if (v == null) return "";
50
- return v.toLocaleString("en-US");
51
- }
52
-
53
- function formatForDisplay(v: number | undefined): string {
54
- const s = formatWithCommas(v);
55
- if (props.numberType === "float" && v != null && Number.isInteger(v)) {
56
- return s + ".0";
57
- }
58
- return s;
59
- }
60
-
61
- function stripCommas(s: string): string {
62
- return s.replace(/,/g, "");
63
- }
64
-
65
- const local = ref(formatForDisplay(toDisplay(model.value)));
66
- const sliderLocal = ref(model.value);
67
- const validationError = ref<string>();
68
-
69
- watch(model, (v) => {
70
- local.value = formatForDisplay(toDisplay(v));
71
- sliderLocal.value = v;
72
- });
73
-
74
- function reformatInput(event: Event) {
75
- const input = event.target as HTMLInputElement;
76
- const raw = stripCommas(input.value);
77
- if (raw === "" || raw === "-") return;
78
- if (raw.endsWith(".") || (raw.includes(".") && raw.endsWith("0"))) return;
79
- const parsed = Number(raw);
80
- if (Number.isNaN(parsed)) return;
81
-
82
- const formatted = formatWithCommas(parsed);
83
- if (formatted === input.value) return;
84
-
85
- const cursorPos = input.selectionStart ?? 0;
86
- const commasBefore = (input.value.slice(0, cursorPos).match(/,/g) || [])
87
- .length;
88
- local.value = formatted;
89
-
90
- requestAnimationFrame(() => {
91
- const rawPos = cursorPos - commasBefore;
92
- let newPos = 0;
93
- let rawCount = 0;
94
- for (let i = 0; i < formatted.length; i++) {
95
- if (formatted[i] !== ",") rawCount++;
96
- if (rawCount >= rawPos) {
97
- newPos = i + 1;
98
- break;
99
- }
100
- }
101
- if (rawCount < rawPos) newPos = formatted.length;
102
- input.setSelectionRange(newPos, newPos);
103
- });
104
- }
105
-
106
- function onBlur() {
107
- commit();
108
- const parsed = Number(stripCommas(local.value));
109
- if (!Number.isNaN(parsed)) {
110
- local.value = formatForDisplay(parsed);
111
- }
112
- }
113
-
114
- let liveTimeout: ReturnType<typeof setTimeout> | null = null;
115
- function onInputEvent() {
116
- if (!props.live || props.slider) return;
117
- if (liveTimeout) clearTimeout(liveTimeout);
118
- liveTimeout = setTimeout(commit, 300);
119
- }
120
- function onChangeEvent() {
121
- if (!props.live || props.slider) return;
122
- if (liveTimeout) clearTimeout(liveTimeout);
123
- commit();
124
- }
125
-
126
- function validate(displayValue: number): string | undefined {
127
- if (inputMin.value != null && displayValue < inputMin.value) {
128
- return `Min ${inputMin.value}${props.percent ? "%" : ""}`;
129
- }
130
- if (inputMax.value != null && displayValue > inputMax.value) {
131
- return `Max ${inputMax.value}${props.percent ? "%" : ""}`;
132
- }
133
- return undefined;
134
- }
135
-
136
- function commit() {
137
- let parsed = Number(stripCommas(local.value));
138
- if (Number.isNaN(parsed)) return;
139
-
140
- if (props.numberType === "integer") {
141
- parsed = Math.trunc(parsed);
142
- local.value = formatForDisplay(parsed);
143
- }
144
-
145
- const error = validate(parsed);
146
- validationError.value = error;
147
- if (error) return;
148
-
149
- model.value = fromDisplay(parsed);
150
- sliderLocal.value = model.value;
151
- }
152
-
153
- function onSliderUpdate(v: number[] | undefined) {
154
- if (!v) return;
155
- const val = coerceInteger(v[0]);
156
- sliderLocal.value = val;
157
- local.value = formatForDisplay(toDisplay(val));
158
- if (props.live) {
159
- model.value = val;
160
- }
161
- }
162
-
163
- function onSliderCommit(v: number[] | undefined) {
164
- if (!v) return;
165
- model.value = coerceInteger(v[0]);
166
- }
167
-
168
- const inputStep = computed(() => {
169
- if (props.step != null) return props.percent ? props.step * 100 : props.step;
170
- return 1;
171
- });
172
-
173
- function onArrowStep(event: KeyboardEvent, direction: 1 | -1) {
174
- event.preventDefault();
175
- const parsed = Number(stripCommas(local.value));
176
- const current = Number.isNaN(parsed) ? 0 : parsed;
177
- const step = inputStep.value * (event.shiftKey ? 10 : 1);
178
- let next = current + step * direction;
179
- if (props.numberType === "integer") next = Math.trunc(next);
180
- if (inputMin.value != null) next = Math.max(next, inputMin.value);
181
- if (inputMax.value != null) next = Math.min(next, inputMax.value);
182
- local.value = formatForDisplay(next);
183
- validationError.value = undefined;
184
- model.value = fromDisplay(next);
185
- sliderLocal.value = model.value;
186
- }
187
-
188
- const inputMin = computed(() => {
189
- if (props.min != null) return props.percent ? props.min * 100 : props.min;
190
- return props.percent ? 0 : undefined;
191
- });
192
- const inputMax = computed(() => {
193
- if (props.max != null) return props.percent ? props.max * 100 : props.max;
194
- return props.percent ? 100 : undefined;
195
- });
196
- </script>
197
-
198
- <template>
199
- <label v-if="props.label" class="input-label">
200
- <span class="input-label-row">
201
- {{ props.label }}
202
- <Hint v-if="props.hint" :text="props.hint" />
203
- </span>
204
- <span v-if="!props.slider" class="input-wrapper">
205
- <input
206
- type="text"
207
- :inputmode="props.numberType === 'integer' ? 'numeric' : 'decimal'"
208
- v-model="local"
209
- :placeholder="props.placeholder"
210
- :aria-invalid="!!validationError"
211
- @blur="onBlur"
212
- @keydown.enter="commit"
213
- @keydown.up="onArrowStep($event, 1)"
214
- @keydown.down="onArrowStep($event, -1)"
215
- @input="
216
- reformatInput($event);
217
- onInputEvent();
218
- "
219
- @change="onChangeEvent"
220
- />
221
- <span v-if="props.percent" class="input-suffix">%</span>
222
- </span>
223
- <span v-if="validationError" class="input-error" role="alert">
224
- {{ validationError }}
225
- </span>
226
- <div v-if="props.slider" class="slider-container">
227
- <SliderRoot
228
- class="slider-root"
229
- :model-value="sliderLocal != null ? [sliderLocal] : [sliderMin]"
230
- :min="sliderMin"
231
- :max="sliderMax"
232
- :step="sliderStep"
233
- @update:model-value="onSliderUpdate"
234
- @value-commit="onSliderCommit"
235
- >
236
- <SliderTrack class="slider-track">
237
- <SliderRange class="slider-range" />
238
- </SliderTrack>
239
- <SliderThumb class="slider-thumb" :aria-label="props.label">
240
- <span class="slider-current">
241
- {{ formatSliderValue(sliderLocal) }}
242
- </span>
243
- </SliderThumb>
244
- </SliderRoot>
245
- <div class="slider-labels">
246
- <span>{{ formatSliderValue(sliderMin) }}</span>
247
- <span>{{ formatSliderValue(sliderMax) }}</span>
248
- </div>
249
- </div>
250
- </label>
251
- <div v-else>
252
- <span v-if="!props.slider" class="input-wrapper">
253
- <input
254
- type="text"
255
- :inputmode="props.numberType === 'integer' ? 'numeric' : 'decimal'"
256
- v-model="local"
257
- :placeholder="props.placeholder"
258
- :aria-invalid="!!validationError"
259
- @blur="onBlur"
260
- @keydown.enter="commit"
261
- @keydown.up="onArrowStep($event, 1)"
262
- @keydown.down="onArrowStep($event, -1)"
263
- @input="
264
- reformatInput($event);
265
- onInputEvent();
266
- "
267
- @change="onChangeEvent"
268
- />
269
- <span v-if="props.percent" class="input-suffix">%</span>
270
- </span>
271
- <span v-if="validationError" class="input-error" role="alert">
272
- {{ validationError }}
273
- </span>
274
- <div v-if="props.slider" class="slider-container">
275
- <SliderRoot
276
- class="slider-root"
277
- :model-value="sliderLocal != null ? [sliderLocal] : [sliderMin]"
278
- :min="sliderMin"
279
- :max="sliderMax"
280
- :step="sliderStep"
281
- @update:model-value="onSliderUpdate"
282
- @value-commit="onSliderCommit"
283
- >
284
- <SliderTrack class="slider-track">
285
- <SliderRange class="slider-range" />
286
- </SliderTrack>
287
- <SliderThumb class="slider-thumb" :aria-label="props.label">
288
- <span class="slider-current">
289
- {{ formatSliderValue(sliderLocal) }}
290
- </span>
291
- </SliderThumb>
292
- </SliderRoot>
293
- <div class="slider-labels">
294
- <span>{{ formatSliderValue(sliderMin) }}</span>
295
- <span>{{ formatSliderValue(sliderMax) }}</span>
296
- </div>
297
- </div>
298
- </div>
299
- </template>
300
-
301
- <style scoped>
302
- .input-label {
303
- display: flex;
304
- flex-direction: column;
305
- gap: 0.25em;
306
- }
307
-
308
- .input-label-row {
309
- display: flex;
310
- align-items: center;
311
- justify-content: space-between;
312
- }
313
-
314
- .input-wrapper {
315
- display: flex;
316
- align-items: center;
317
- gap: 0.25em;
318
- }
319
-
320
- .input-wrapper input {
321
- flex: 1;
322
- min-width: 0;
323
- }
324
-
325
- input {
326
- display: block;
327
- width: 100%;
328
- height: 2.5em;
329
- padding: 0 0.75em;
330
- font-size: inherit;
331
- background-color: var(--color-bg-0);
332
- color: var(--color-text);
333
- border: 1px solid var(--color-border);
334
- border-radius: 0.375em;
335
- transition:
336
- border-color var(--transition-fast),
337
- box-shadow var(--transition-fast);
338
- }
339
-
340
- input:hover {
341
- border-color: var(--color-border-hover);
342
- }
343
-
344
- input:focus {
345
- outline: none;
346
- border-color: var(--color-border-focus);
347
- box-shadow: var(--shadow-focus);
348
- }
349
-
350
- input[aria-invalid="true"] {
351
- border-color: var(--color-error);
352
- }
353
-
354
- input[aria-invalid="true"]:focus {
355
- border-color: var(--color-error);
356
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 25%, transparent);
357
- }
358
-
359
- input::placeholder {
360
- color: var(--color-text-tertiary);
361
- }
362
-
363
- .input-suffix {
364
- color: var(--color-text-secondary);
365
- font-size: var(--font-size-sm);
366
- flex-shrink: 0;
367
- }
368
-
369
- .input-error {
370
- color: var(--color-error);
371
- font-size: var(--font-size-xs);
372
- }
373
-
374
- .slider-container {
375
- display: flex;
376
- flex-direction: column;
377
- gap: 0.25em;
378
- padding-top: 1.5em;
379
- }
380
-
381
- .slider-current {
382
- position: absolute;
383
- bottom: 100%;
384
- left: 50%;
385
- transform: translateX(-50%);
386
- margin-bottom: 1px;
387
- font-size: var(--font-size-xs);
388
- color: var(--color-text-secondary);
389
- white-space: nowrap;
390
- pointer-events: none;
391
- }
392
-
393
- .slider-root {
394
- position: relative;
395
- display: flex;
396
- align-items: center;
397
- width: 100%;
398
- height: 1.5em;
399
- touch-action: none;
400
- user-select: none;
401
- }
402
-
403
- .slider-track {
404
- position: relative;
405
- flex-grow: 1;
406
- height: 3px;
407
- background-color: var(--color-bg-3);
408
- border-radius: var(--radius-full);
409
- }
410
-
411
- .slider-range {
412
- position: absolute;
413
- height: 100%;
414
- background-color: var(--color-primary);
415
- border-radius: var(--radius-full);
416
- }
417
-
418
- .slider-thumb {
419
- position: relative;
420
- display: block;
421
- width: 1em;
422
- height: 1em;
423
- background-color: var(--color-primary);
424
- border-radius: var(--radius-full);
425
- cursor: pointer;
426
- }
427
-
428
- .slider-thumb:hover {
429
- background-color: var(--color-primary-hover);
430
- }
431
-
432
- .slider-thumb:active,
433
- .slider-thumb:focus-visible {
434
- outline: none;
435
- box-shadow: 0 0 0 4px
436
- color-mix(in srgb, var(--color-primary) 25%, transparent);
437
- }
438
-
439
- .slider-labels {
440
- display: flex;
441
- justify-content: space-between;
442
- font-size: var(--font-size-xs);
443
- color: var(--color-text-secondary);
444
- margin-top: -0.5em;
445
- }
446
- </style>
@@ -1,56 +0,0 @@
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
- <!--@include: ./_api/select-box.md-->
48
-
49
- ### SelectOption
50
-
51
- ```ts
52
- interface SelectOption {
53
- value: string;
54
- label: string;
55
- }
56
- ```
@@ -1,9 +0,0 @@
1
- import { test, expect } from "@playwright/test";
2
-
3
- test("SelectBox page renders demos", async ({ page }) => {
4
- await page.goto("./cfasim-ui/components/select-box");
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("Interval")).toBeVisible();
9
- });
@@ -1,42 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { mount } from "@vue/test-utils";
3
- import SelectBox from "./SelectBox.vue";
4
-
5
- const options = [
6
- { value: "daily", label: "Daily" },
7
- { value: "weekly", label: "Weekly" },
8
- ];
9
-
10
- describe("SelectBox", () => {
11
- it("renders with label", () => {
12
- const wrapper = mount(SelectBox, {
13
- props: { label: "Interval", options, modelValue: "daily" },
14
- });
15
- expect(wrapper.text()).toContain("Interval");
16
- });
17
-
18
- it("renders without label", () => {
19
- const wrapper = mount(SelectBox, {
20
- props: { options, modelValue: "weekly" },
21
- });
22
- expect(wrapper.find(".select-label").exists()).toBe(false);
23
- });
24
-
25
- it("renders trigger element", () => {
26
- const wrapper = mount(SelectBox, {
27
- props: { options, modelValue: "weekly" },
28
- });
29
- const trigger = wrapper.find(".select-trigger");
30
- expect(trigger.exists()).toBe(true);
31
- expect(trigger.element.tagName).toBe("BUTTON");
32
- });
33
-
34
- it("has accessible trigger button", () => {
35
- const wrapper = mount(SelectBox, {
36
- props: { label: "Interval", options, modelValue: "daily" },
37
- });
38
- const trigger = wrapper.find(".select-trigger");
39
- expect(trigger.attributes("role")).toBe("combobox");
40
- expect(trigger.attributes("aria-labelledby")).toBeDefined();
41
- });
42
- });