@revojs/ui 0.2.8 → 0.2.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 (47) hide show
  1. package/dist/components/button.d.ts +3 -2
  2. package/dist/components/container.d.ts +2 -0
  3. package/dist/components/icon.d.ts +3 -1
  4. package/dist/components/input.d.ts +3 -2
  5. package/dist/components/navigate.d.ts +3 -2
  6. package/dist/components/separator.d.ts +1 -6
  7. package/dist/components/stack.d.ts +12 -0
  8. package/dist/components/stack.js +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/module/icons.d.ts +2 -0
  11. package/dist/module/icons.js +12 -0
  12. package/dist/module/index.d.ts +1 -1
  13. package/dist/module/index.js +13 -23
  14. package/dist/style.css +7 -0
  15. package/dist/types/index.d.ts +0 -7
  16. package/dist/vue/components/breadcrumb.vue +14 -8
  17. package/dist/vue/components/button.vue +24 -19
  18. package/dist/vue/components/card.vue +21 -24
  19. package/dist/vue/components/checkbox.vue +11 -16
  20. package/dist/vue/components/collection.vue +15 -15
  21. package/dist/vue/components/container.vue +5 -6
  22. package/dist/vue/components/counter.vue +11 -14
  23. package/dist/vue/components/dialog.vue +11 -19
  24. package/dist/vue/components/field.vue +7 -16
  25. package/dist/vue/components/heading.vue +7 -8
  26. package/dist/vue/components/icon.vue +8 -42
  27. package/dist/vue/components/input.vue +18 -22
  28. package/dist/vue/components/link.vue +8 -9
  29. package/dist/vue/components/navigate.vue +2 -13
  30. package/dist/vue/components/popover.vue +57 -67
  31. package/dist/vue/components/progress.vue +9 -15
  32. package/dist/vue/components/radio.vue +9 -24
  33. package/dist/vue/components/range.vue +9 -21
  34. package/dist/vue/components/rating.vue +10 -20
  35. package/dist/vue/components/select.vue +86 -110
  36. package/dist/vue/components/separator.vue +2 -10
  37. package/dist/vue/components/sheet.vue +14 -19
  38. package/dist/vue/components/stack.vue +70 -0
  39. package/dist/vue/components/switch.vue +10 -21
  40. package/dist/vue/components/upload.vue +11 -23
  41. package/dist/vue/index.d.ts +3 -1
  42. package/dist/vue/index.js +5 -2
  43. package/dist/vue/utilities/use-focus-trap.d.ts +2 -0
  44. package/dist/vue/utilities/use-focus-trap.js +34 -0
  45. package/dist/vue/utilities/use-scroll-lock.d.ts +2 -0
  46. package/dist/vue/utilities/use-scroll-lock.js +33 -0
  47. package/package.json +5 -4
@@ -1,12 +1,12 @@
1
1
  <template>
2
- <div class="inline-flex flex-col gap-2">
2
+ <Stack>
3
3
  <span v-if="label" class="text-sm font-medium">{{ label }}</span>
4
4
 
5
- <div class="flex items-center" role="radiogroup" :aria-label="label">
5
+ <Stack direction="horizontal" align="center" gap="0" role="radiogroup" :aria-label="label">
6
6
  <Button
7
7
  v-for="value in max"
8
8
  :key="value"
9
- class="text-2xl transition-transform hover:scale-110 disabled:hover:scale-100"
9
+ class="text-2xl"
10
10
  :class="variants[state(value)]"
11
11
  size="icon"
12
12
  variant="ghost"
@@ -16,30 +16,20 @@
16
16
  :disabled="readonly || disabled"
17
17
  @click="$emit('update:modelValue', value)"
18
18
  >
19
- <Icon name="star" />
19
+ <Icon :is="StarIcon" />
20
20
  </Button>
21
- </div>
22
- </div>
21
+ </Stack>
22
+ </Stack>
23
23
  </template>
24
24
 
25
25
  <script setup lang="ts">
26
+ import StarIcon from "~icons/hugeicons/star";
26
27
  import Button from "./button.vue";
27
28
  import Icon from "./icon.vue";
28
- import type { RatingState } from "../../components/rating.ts";
29
+ import Stack from "./stack.vue";
30
+ import type { RatingProps, RatingState } from "../../components/rating";
29
31
 
30
- const {
31
- modelValue = 0,
32
- max = 5,
33
- label = undefined,
34
- readonly = false,
35
- disabled = false,
36
- } = defineProps<{
37
- modelValue?: number;
38
- max?: number;
39
- label?: string;
40
- readonly?: boolean;
41
- disabled?: boolean;
42
- }>();
32
+ const { modelValue = 0, max = 5, label, readonly = false, disabled = false } = defineProps<RatingProps>();
43
33
 
44
34
  defineEmits<{
45
35
  "update:modelValue": [value: number];
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="flex flex-col gap-2">
2
+ <Stack>
3
3
  <span v-if="label" class="text-sm font-medium">
4
4
  {{ label }}
5
5
  </span>
@@ -7,9 +7,10 @@
7
7
  <input v-if="name" type="hidden" :name="name" :value="modelValue" :required="required" />
8
8
 
9
9
  <div ref="container" class="relative">
10
- <div
11
- ref="trigger"
12
- class="flex min-h-10 w-full cursor-text items-center gap-2 rounded-ui border border-input px-3 text-sm ring-ring focus-within:ring-2"
10
+ <Stack
11
+ direction="horizontal"
12
+ align="center"
13
+ class="min-h-10 w-full cursor-text rounded-ui border border-input px-3 text-sm ring-ring focus-within:ring-2"
13
14
  :class="disabled ? 'cursor-not-allowed opacity-50' : ''"
14
15
  @click="search?.focus()"
15
16
  >
@@ -21,13 +22,15 @@
21
22
  :placeholder="placeholder ?? 'Select'"
22
23
  :aria-expanded="open"
23
24
  :aria-controls="listboxId"
25
+ :aria-activedescendant="activeId"
24
26
  aria-autocomplete="list"
25
27
  :disabled="disabled"
26
28
  @focus="open = true"
27
- @input="open = true"
28
- @keydown.enter.prevent="open ? filtered.length > 0 && select(filtered[0]) : (open = true)"
29
+ @input="input"
30
+ @keydown.enter.prevent="confirm"
29
31
  @keydown.escape="open = false"
30
- @keydown.arrow-down.prevent="open = true"
32
+ @keydown.arrow-down.prevent="stepHighlight(1)"
33
+ @keydown.arrow-up.prevent="stepHighlight(-1)"
31
34
  />
32
35
 
33
36
  <button
@@ -38,24 +41,22 @@
38
41
  :disabled="disabled"
39
42
  @click.stop="open = !open"
40
43
  >
41
- <Icon name="arrow-down-01" />
44
+ <Icon :is="ArrowDownIcon" :size="iconSize" />
42
45
  </button>
43
- </div>
46
+ </Stack>
44
47
 
45
48
  <div
46
- ref="dropdown"
47
- :popover="popoverSupported ? 'manual' : undefined"
49
+ v-if="open"
48
50
  :id="listboxId"
49
51
  role="listbox"
50
52
  :aria-label="label"
51
- class="m-0 max-h-[min(16rem,50vh)] overflow-y-auto rounded-ui border border-border bg-popover p-1 text-popover-foreground shadow-xl"
52
- :class="fallbackClass"
53
- :style="popoverSupported ? panel : undefined"
53
+ class="absolute inset-x-0 top-full z-10 mt-1 max-h-[min(16rem,50vh)] overflow-y-auto rounded-ui border border-border bg-popover p-1 text-popover-foreground shadow-xl"
54
54
  >
55
55
  <Button
56
- v-for="option in filtered"
56
+ v-for="(option, index) in filtered"
57
57
  :key="option.value"
58
- class="w-full"
58
+ :id="`${listboxId}-${index}`"
59
+ :class="['w-full', highlighted === index ? 'bg-accent!' : '']"
59
60
  variant="ghost"
60
61
  layout="left"
61
62
  :disabled="option.disabled"
@@ -63,48 +64,43 @@
63
64
  :aria-selected="option.value === modelValue"
64
65
  @click="select(option)"
65
66
  >
66
- <span class="flex w-full items-center justify-between gap-4 text-left">
67
+ <Stack direction="horizontal" align="center" justify="between" gap="1" class="w-full text-left">
67
68
  <slot name="option" :option="option" :selected="option.value === modelValue">
68
69
  {{ option.label }}
69
70
  </slot>
70
71
 
71
- <Icon v-if="option.value === modelValue" name="tick-02" />
72
- </span>
72
+ <Icon v-if="option.value === modelValue" :is="TickIcon" :size="iconSize" />
73
+ </Stack>
73
74
  </Button>
74
75
 
75
- <p v-if="filtered.length === 0" class="px-2 py-6 text-center text-sm opacity-60">
76
+ <p v-if="filtered.length === 0" class="px-1 py-5 text-center text-sm opacity-60">
76
77
  {{ emptyLabel }}
77
78
  </p>
78
79
  </div>
79
80
  </div>
80
- </div>
81
+ </Stack>
81
82
  </template>
82
83
 
83
84
  <script setup lang="ts">
84
- import { computed, nextTick, onMounted, onUnmounted, ref, useId, useTemplateRef, watch } from "vue";
85
- import type { SelectOption, SelectValue } from "../../components/select.ts";
85
+ import ArrowDownIcon from "~icons/hugeicons/arrow-down-01";
86
+ import TickIcon from "~icons/hugeicons/tick-02";
87
+ import { computed, onUnmounted, ref, useId, useTemplateRef, watch } from "vue";
88
+ import type { SelectOption, SelectProps, SelectValue } from "../../components/select";
86
89
  import Button from "./button.vue";
87
90
  import Icon from "./icon.vue";
91
+ import Stack from "./stack.vue";
92
+ import { useFocusTrap } from "../utilities/use-focus-trap";
88
93
 
89
94
  const {
90
- modelValue = undefined,
95
+ modelValue,
91
96
  options,
92
- label = undefined,
93
- placeholder = undefined,
97
+ label,
98
+ placeholder,
94
99
  emptyLabel = "No results found",
95
- name = undefined,
100
+ name,
96
101
  required = false,
97
102
  disabled = false,
98
- } = defineProps<{
99
- modelValue?: SelectValue;
100
- options: Array<SelectOption>;
101
- label?: string;
102
- placeholder?: string;
103
- emptyLabel?: string;
104
- name?: string;
105
- required?: boolean;
106
- disabled?: boolean;
107
- }>();
103
+ } = defineProps<SelectProps>();
108
104
 
109
105
  const emit = defineEmits<{
110
106
  "update:modelValue": [value: SelectValue];
@@ -114,40 +110,32 @@ const open = ref(false);
114
110
 
115
111
  const container = useTemplateRef("container");
116
112
 
117
- const trigger = useTemplateRef("trigger");
113
+ useFocusTrap(container, open);
118
114
 
119
115
  const search = useTemplateRef("search");
120
116
 
121
- const dropdown = useTemplateRef("dropdown");
122
-
123
117
  const listboxId = `select-${useId()}`;
124
118
 
125
- const popoverSupported = ref(true);
119
+ const iconSize = 20;
126
120
 
127
121
  const selected = computed(() => options.find((option) => option.value === modelValue));
128
122
 
129
123
  const query = ref(selected.value?.label ?? "");
130
124
 
131
- const panel = ref<Record<string, string>>({});
132
-
133
125
  const filtered = computed(() => {
134
126
  const term = query.value.trim().toLowerCase();
135
127
 
136
128
  return term === "" ? options : options.filter((option) => option.label.toLowerCase().includes(term));
137
129
  });
138
130
 
139
- const fallbackClass = computed(() => {
140
- if (popoverSupported.value) {
141
- return undefined;
142
- }
131
+ const highlighted = ref(-1);
143
132
 
144
- return open.value ? "absolute inset-x-0 top-full z-50 mt-2" : "hidden";
145
- });
133
+ const activeId = computed(() =>
134
+ open.value && highlighted.value >= 0 ? `${listboxId}-${highlighted.value}` : undefined,
135
+ );
146
136
 
147
137
  function handlePointerDown(event: PointerEvent) {
148
- const target = event.target as Node;
149
-
150
- if (!container.value?.contains(target) && !dropdown.value?.contains(target)) {
138
+ if (!container.value?.contains(event.target as Node)) {
151
139
  open.value = false;
152
140
  }
153
141
  }
@@ -159,65 +147,71 @@ function select(option: SelectOption) {
159
147
  }
160
148
  }
161
149
 
162
- function updatePanel() {
163
- const anchor = trigger.value;
164
- const list = dropdown.value;
150
+ function highlight(step: 1 | -1) {
151
+ const count = filtered.value.length;
165
152
 
166
- if (!anchor || !list) {
153
+ if (count === 0) {
167
154
  return;
168
155
  }
169
156
 
170
- const height = window.visualViewport?.height ?? window.innerHeight;
171
- const width = window.visualViewport?.width ?? window.innerWidth;
172
- const bounds = anchor.getBoundingClientRect();
173
- const listHeight = list.offsetHeight;
174
- const listWidth = bounds.width;
175
- const left = Math.max(8, Math.min(Math.max(8, bounds.left), width - listWidth - 8));
176
- const top = bounds.bottom + listHeight + 8 <= height ? bounds.bottom + 8 : Math.max(8, bounds.top - listHeight - 8);
177
-
178
- panel.value = {
179
- bottom: "auto",
180
- left: `${left}px`,
181
- position: "fixed",
182
- right: "auto",
183
- top: `${top}px`,
184
- width: `${listWidth}px`,
185
- };
157
+ let index = highlighted.value;
158
+
159
+ for (let round = 0; round < count; round++) {
160
+ index = (index + step + count) % count;
161
+
162
+ if (!filtered.value[index].disabled) {
163
+ highlighted.value = index;
164
+
165
+ document.getElementById(`${listboxId}-${index}`)?.scrollIntoView({ block: "nearest" });
166
+
167
+ return;
168
+ }
169
+ }
186
170
  }
187
171
 
188
- watch(selected, (value) => {
172
+ function stepHighlight(step: 1 | -1) {
189
173
  if (!open.value) {
190
- query.value = value?.label ?? "";
174
+ open.value = true;
175
+
176
+ return;
191
177
  }
192
- });
193
178
 
194
- watch(open, async (value) => {
195
- const list = dropdown.value;
179
+ highlight(step);
180
+ }
181
+
182
+ function confirm() {
183
+ if (!open.value) {
184
+ open.value = true;
196
185
 
197
- if (!list) {
198
186
  return;
199
187
  }
200
188
 
201
- if (value) {
202
- query.value = "";
189
+ const option = filtered.value[highlighted.value] ?? filtered.value[0];
203
190
 
204
- document.addEventListener("pointerdown", handlePointerDown);
205
-
206
- window.addEventListener("resize", updatePanel);
191
+ if (option) {
192
+ select(option);
193
+ }
194
+ }
207
195
 
208
- window.addEventListener("scroll", updatePanel, true);
196
+ function input() {
197
+ open.value = true;
209
198
 
210
- window.visualViewport?.addEventListener("resize", updatePanel);
199
+ highlighted.value = -1;
200
+ }
211
201
 
212
- if (popoverSupported.value && !list.matches(":popover-open")) {
213
- list.showPopover();
214
- }
202
+ watch(selected, (value) => {
203
+ if (!open.value) {
204
+ query.value = value?.label ?? "";
205
+ }
206
+ });
215
207
 
216
- await nextTick();
208
+ watch(open, (value) => {
209
+ if (value) {
210
+ query.value = "";
217
211
 
218
- updatePanel();
212
+ highlighted.value = filtered.value.findIndex((option) => option.value === modelValue);
219
213
 
220
- requestAnimationFrame(updatePanel);
214
+ document.addEventListener("pointerdown", handlePointerDown);
221
215
 
222
216
  if (document.activeElement !== search.value) {
223
217
  search.value?.focus();
@@ -225,31 +219,13 @@ watch(open, async (value) => {
225
219
  } else {
226
220
  document.removeEventListener("pointerdown", handlePointerDown);
227
221
 
228
- window.removeEventListener("resize", updatePanel);
229
-
230
- window.removeEventListener("scroll", updatePanel, true);
231
-
232
- window.visualViewport?.removeEventListener("resize", updatePanel);
233
-
234
- if (popoverSupported.value && list.matches(":popover-open")) {
235
- list.hidePopover();
236
- }
222
+ highlighted.value = -1;
237
223
 
238
224
  query.value = selected.value?.label ?? "";
239
225
  }
240
226
  });
241
227
 
242
- onMounted(() => {
243
- popoverSupported.value = typeof dropdown.value?.showPopover === "function";
244
- });
245
-
246
228
  onUnmounted(() => {
247
229
  document.removeEventListener("pointerdown", handlePointerDown);
248
-
249
- window.removeEventListener("resize", updatePanel);
250
-
251
- window.removeEventListener("scroll", updatePanel, true);
252
-
253
- window.visualViewport?.removeEventListener("resize", updatePanel);
254
230
  });
255
231
  </script>
@@ -9,17 +9,9 @@
9
9
 
10
10
  <script setup lang="ts">
11
11
  import { computed } from "vue";
12
- import type { DividerVariant, SeparatorOrientation } from "../../components/separator.ts";
12
+ import type { SeparatorOrientation, SeparatorProps } from "../../components/separator";
13
13
 
14
- const {
15
- orientation = "horizontal",
16
- variant = undefined,
17
- decorative = false,
18
- } = defineProps<{
19
- orientation?: SeparatorOrientation;
20
- variant?: DividerVariant;
21
- decorative?: boolean;
22
- }>();
14
+ const { orientation = "horizontal", variant, decorative = false } = defineProps<SeparatorProps>();
23
15
 
24
16
  const selected = computed(() => variant ?? orientation);
25
17
 
@@ -11,7 +11,7 @@
11
11
  <Button
12
12
  size="icon"
13
13
  variant="ghost"
14
- icon-prefix="multiplication-sign"
14
+ :icon-prefix="CloseIcon"
15
15
  :aria-label="closeLabel"
16
16
  @click="sheet?.close()"
17
17
  />
@@ -29,24 +29,15 @@
29
29
  </template>
30
30
 
31
31
  <script setup lang="ts">
32
+ import CloseIcon from "~icons/hugeicons/multiplication-sign";
32
33
  import { useTemplateRef, watchEffect } from "vue";
33
- import type { SheetSide } from "../../components/sheet.ts";
34
+ import type { SheetProps, SheetSide } from "../../components/sheet";
34
35
  import Button from "./button.vue";
35
36
  import Card from "./card.vue";
37
+ import { useFocusTrap } from "../utilities/use-focus-trap";
38
+ import { useScrollLock } from "../utilities/use-scroll-lock";
36
39
 
37
- const {
38
- modelValue = false,
39
- side = "right",
40
- title = undefined,
41
- description = undefined,
42
- closeLabel = "Close",
43
- } = defineProps<{
44
- modelValue?: boolean;
45
- side?: SheetSide;
46
- title?: string;
47
- description?: string;
48
- closeLabel?: string;
49
- }>();
40
+ const { modelValue = false, side = "right", title, description, closeLabel = "Close" } = defineProps<SheetProps>();
50
41
 
51
42
  defineEmits<{
52
43
  "update:modelValue": [value: boolean];
@@ -54,6 +45,10 @@ defineEmits<{
54
45
 
55
46
  const sheet = useTemplateRef("sheet");
56
47
 
48
+ useFocusTrap(sheet, () => modelValue);
49
+
50
+ useScrollLock(() => modelValue);
51
+
57
52
  const positions: Record<SheetSide, string> = {
58
53
  top: "bottom-auto left-0 right-0 top-0 h-auto w-full",
59
54
  right: "bottom-0 left-auto right-0 top-0 h-full max-h-full w-[min(28rem,90vw)]",
@@ -62,10 +57,10 @@ const positions: Record<SheetSide, string> = {
62
57
  };
63
58
 
64
59
  const cardClasses: Record<SheetSide, string> = {
65
- top: "max-h-[85vh] w-full shadow-2xl",
66
- right: "h-full w-full shadow-2xl",
67
- bottom: "max-h-[85vh] w-full shadow-2xl",
68
- left: "h-full w-full shadow-2xl",
60
+ top: "max-h-[85vh] w-full border-l-0 border-r-0 border-t-0 shadow-sheet-top",
61
+ right: "h-full w-full border-b-0 border-r-0 border-t-0 shadow-sheet-right",
62
+ bottom: "max-h-[85vh] w-full border-b-0 border-l-0 border-r-0 shadow-sheet-bottom",
63
+ left: "h-full w-full border-b-0 border-l-0 border-t-0 shadow-sheet-left",
69
64
  };
70
65
 
71
66
  watchEffect(() => {
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <component
3
+ :is="is"
4
+ class="flex min-w-0"
5
+ :class="[
6
+ directions[direction],
7
+ gaps[gap],
8
+ paddings[padding],
9
+ aligns[align],
10
+ justifies[justify],
11
+ wrap ? 'flex-wrap' : '',
12
+ ]"
13
+ >
14
+ <slot />
15
+ </component>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import type { Component } from "vue";
20
+ import type { StackAlign, StackDirection, StackGap, StackJustify, StackProps } from "../../components/stack";
21
+
22
+ const {
23
+ is = "div",
24
+ direction = "vertical",
25
+ gap = "1",
26
+ padding = "0",
27
+ align = "stretch",
28
+ justify = "start",
29
+ wrap = false,
30
+ } = defineProps<
31
+ StackProps & {
32
+ is?: string | Component;
33
+ }
34
+ >();
35
+
36
+ const directions: Record<StackDirection, string> = {
37
+ horizontal: "flex-row",
38
+ vertical: "flex-col",
39
+ };
40
+
41
+ const gaps: Record<StackGap, string> = {
42
+ "0": "gap-0",
43
+ "1": "gap-1",
44
+ "3": "gap-3",
45
+ "5": "gap-5",
46
+ "7": "gap-7",
47
+ };
48
+
49
+ const paddings: Record<StackGap, string> = {
50
+ "0": "p-0",
51
+ "1": "p-1",
52
+ "3": "p-3",
53
+ "5": "p-5",
54
+ "7": "p-7",
55
+ };
56
+
57
+ const aligns: Record<StackAlign, string> = {
58
+ center: "items-center",
59
+ end: "items-end",
60
+ start: "items-start",
61
+ stretch: "items-stretch",
62
+ };
63
+
64
+ const justifies: Record<StackJustify, string> = {
65
+ between: "justify-between",
66
+ center: "justify-center",
67
+ end: "justify-end",
68
+ start: "justify-start",
69
+ };
70
+ </script>
@@ -1,9 +1,9 @@
1
1
  <template>
2
- <label class="inline-flex items-start gap-3" :class="interactionVariants[interaction]">
2
+ <Stack is="label" direction="horizontal" align="start" gap="3" :class="interactionVariants[interaction]">
3
3
  <input v-if="name" type="hidden" :name="name" :value="modelValue ? 'on' : 'off'" />
4
4
 
5
5
  <button
6
- class="relative mt-0.5 flex h-6 w-11 shrink-0 items-center rounded-full outline-ring transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed"
6
+ class="relative mt-0.5 flex h-6 w-11 shrink-0 items-center rounded-ui outline-ring transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed"
7
7
  :class="variants[state]"
8
8
  type="button"
9
9
  role="switch"
@@ -11,37 +11,26 @@
11
11
  :disabled="disabled"
12
12
  @click="$emit('update:modelValue', !modelValue)"
13
13
  >
14
- <span class="block size-5 rounded-full shadow transition-transform" :class="thumbVariants[state]" />
14
+ <span class="block size-5 rounded-ui shadow transition-transform" :class="thumbVariants[state]" />
15
15
  </button>
16
16
 
17
- <span v-if="label || description || $slots.default" class="flex flex-col gap-0.5">
17
+ <Stack is="span" v-if="label || description || $slots.default" gap="0">
18
18
  <span class="text-sm font-medium">
19
19
  <slot> {{ label }} </slot>
20
20
  </span>
21
21
 
22
22
  <span v-if="description" class="text-sm opacity-60"> {{ description }} </span>
23
- </span>
24
- </label>
23
+ </Stack>
24
+ </Stack>
25
25
  </template>
26
26
 
27
27
  <script setup lang="ts">
28
28
  import { computed } from "vue";
29
29
  import type { ControlInteraction } from "../../components/control.ts";
30
- import type { SwitchState } from "../../components/switch.ts";
30
+ import type { SwitchProps, SwitchState } from "../../components/switch";
31
+ import Stack from "./stack.vue";
31
32
 
32
- const {
33
- modelValue = false,
34
- label = undefined,
35
- description = undefined,
36
- name = undefined,
37
- disabled = false,
38
- } = defineProps<{
39
- modelValue?: boolean;
40
- label?: string;
41
- description?: string;
42
- name?: string;
43
- disabled?: boolean;
44
- }>();
33
+ const { modelValue = false, label, description, name, disabled = false } = defineProps<SwitchProps>();
45
34
 
46
35
  defineEmits<{
47
36
  "update:modelValue": [value: boolean];
@@ -58,7 +47,7 @@ const interactionVariants: Record<ControlInteraction, string> = {
58
47
 
59
48
  const thumbVariants: Record<SwitchState, string> = {
60
49
  checked: "translate-x-5 bg-primary-foreground",
61
- unchecked: "translate-x-0.5 bg-white",
50
+ unchecked: "translate-x-1 bg-white",
62
51
  };
63
52
 
64
53
  const variants: Record<SwitchState, string> = {
@@ -1,11 +1,14 @@
1
1
  <template>
2
- <div class="flex w-full flex-col gap-2">
2
+ <Stack>
3
3
  <span v-if="label" class="text-sm font-medium">
4
4
  {{ label }}<span v-if="required" aria-hidden="true"> *</span>
5
5
  </span>
6
6
 
7
- <div
8
- class="relative flex min-h-32 flex-col items-center justify-center gap-3 rounded-ui border border-input border-dashed p-5 text-center transition-colors"
7
+ <Stack
8
+ align="center"
9
+ justify="center"
10
+ gap="3"
11
+ class="relative min-h-32 rounded-ui border border-input border-dashed p-5 text-center transition-colors"
9
12
  :class="[variants[state], interactionVariants[interaction]]"
10
13
  @dragenter.prevent="isOver = true"
11
14
  @dragover.prevent="isOver = true"
@@ -35,35 +38,20 @@
35
38
  :disabled="disabled"
36
39
  @change="change"
37
40
  />
38
- </div>
41
+ </Stack>
39
42
 
40
43
  <span v-if="description" class="text-sm opacity-60">{{ description }}</span>
41
- </div>
44
+ </Stack>
42
45
  </template>
43
46
 
44
47
  <script setup lang="ts">
45
48
  import { computed, ref, useTemplateRef } from "vue";
46
49
  import type { ControlInteraction } from "../../components/control.ts";
47
- import type { UploadState } from "../../components/upload.ts";
50
+ import type { UploadProps, UploadState } from "../../components/upload";
48
51
  import Button from "./button.vue";
52
+ import Stack from "./stack.vue";
49
53
 
50
- const {
51
- modelValue = undefined,
52
- label = undefined,
53
- description = undefined,
54
- accept = undefined,
55
- name = undefined,
56
- required = false,
57
- disabled = false,
58
- } = defineProps<{
59
- modelValue?: Blob | File;
60
- label?: string;
61
- description?: string;
62
- accept?: string;
63
- name?: string;
64
- required?: boolean;
65
- disabled?: boolean;
66
- }>();
54
+ const { modelValue, label, description, accept, name, required = false, disabled = false } = defineProps<UploadProps>();
67
55
 
68
56
  const emit = defineEmits<{
69
57
  "update:modelValue": [value?: Blob | File];