@revojs/ui 0.2.11 → 0.2.13

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 (59) hide show
  1. package/dist/components/button.d.ts +1 -0
  2. package/dist/components/card.d.ts +8 -2
  3. package/dist/components/collection.d.ts +3 -0
  4. package/dist/components/container.d.ts +2 -3
  5. package/dist/components/dialog.d.ts +3 -3
  6. package/dist/components/entry.d.ts +6 -0
  7. package/dist/components/entry.js +1 -0
  8. package/dist/components/field.d.ts +4 -0
  9. package/dist/components/input.d.ts +3 -0
  10. package/dist/components/notification.d.ts +9 -0
  11. package/dist/components/notification.js +1 -0
  12. package/dist/components/pagination.d.ts +8 -0
  13. package/dist/components/pagination.js +1 -0
  14. package/dist/components/popover.d.ts +9 -1
  15. package/dist/components/select.d.ts +8 -5
  16. package/dist/components/sheet.d.ts +3 -3
  17. package/dist/components/shell.d.ts +6 -0
  18. package/dist/components/shell.js +1 -0
  19. package/dist/components/textarea.d.ts +13 -0
  20. package/dist/components/textarea.js +1 -0
  21. package/dist/components/upload.d.ts +2 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/module/index.d.ts +16 -0
  24. package/dist/module/index.js +32 -0
  25. package/dist/style.css +16 -0
  26. package/dist/vue/components/breadcrumb.vue +6 -1
  27. package/dist/vue/components/button.vue +34 -13
  28. package/dist/vue/components/card.vue +27 -14
  29. package/dist/vue/components/checkbox.vue +18 -4
  30. package/dist/vue/components/collection.vue +22 -7
  31. package/dist/vue/components/container.vue +11 -2
  32. package/dist/vue/components/counter.vue +7 -4
  33. package/dist/vue/components/dialog.vue +53 -36
  34. package/dist/vue/components/entry.vue +52 -0
  35. package/dist/vue/components/field.vue +44 -9
  36. package/dist/vue/components/input.vue +33 -6
  37. package/dist/vue/components/link.vue +1 -1
  38. package/dist/vue/components/navigate.vue +1 -0
  39. package/dist/vue/components/notification.vue +58 -0
  40. package/dist/vue/components/pagination.vue +24 -0
  41. package/dist/vue/components/popover.vue +46 -18
  42. package/dist/vue/components/progress.vue +2 -2
  43. package/dist/vue/components/radio.vue +19 -5
  44. package/dist/vue/components/range.vue +1 -1
  45. package/dist/vue/components/rating.vue +2 -2
  46. package/dist/vue/components/select.vue +116 -35
  47. package/dist/vue/components/separator.vue +2 -2
  48. package/dist/vue/components/sheet.vue +56 -22
  49. package/dist/vue/components/shell.vue +38 -0
  50. package/dist/vue/components/switch.vue +21 -7
  51. package/dist/vue/components/textarea.vue +49 -0
  52. package/dist/vue/components/upload.vue +74 -23
  53. package/dist/vue/index.d.ts +5 -0
  54. package/dist/vue/index.js +6 -1
  55. package/dist/vue/utilities/use-control-attributes.d.ts +11 -0
  56. package/dist/vue/utilities/use-control-attributes.js +14 -0
  57. package/dist/vue/utilities/use-modal-dialog.d.ts +9 -0
  58. package/dist/vue/utilities/use-modal-dialog.js +48 -0
  59. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <Stack>
3
- <span v-if="label" class="text-sm font-medium">{{ label }}</span>
3
+ <span v-if="label" class="text-sm font-normal">{{ label }}</span>
4
4
 
5
5
  <Stack direction="horizontal" align="center" gap="0" role="radiogroup" :aria-label="label">
6
6
  <Button
@@ -16,7 +16,7 @@
16
16
  :disabled="readonly || disabled"
17
17
  @click="$emit('update:modelValue', value)"
18
18
  >
19
- <Icon :is="StarIcon" />
19
+ <Icon :is="StarIcon" :class="{ '[&_path]:fill-current': value <= modelValue }" />
20
20
  </Button>
21
21
  </Stack>
22
22
  </Stack>
@@ -1,56 +1,99 @@
1
1
  <template>
2
- <Stack>
3
- <span v-if="label" class="text-sm font-medium">
2
+ <Stack v-bind="rootAttributes">
3
+ <label v-if="label" :for="controlAttributes.id ?? controlId" class="text-sm font-normal">
4
4
  {{ label }}
5
- </span>
6
-
7
- <input v-if="name" type="hidden" :name="name" :value="modelValue" :required="required" />
5
+ </label>
6
+
7
+ <input v-if="name && searchable" type="hidden" :name="name" :value="modelValue" :disabled="disabled" />
8
+
9
+ <select
10
+ v-if="!searchable"
11
+ v-bind="controlAttributes"
12
+ ref="nativeControl"
13
+ :id="controlAttributes.id ?? controlId"
14
+ :value="modelValue ?? ''"
15
+ :name="name"
16
+ :required="required"
17
+ :disabled="disabled"
18
+ :aria-invalid="invalid ?? controlAttributes['aria-invalid']"
19
+ class="w-full rounded-ui bg-secondary p-3 text-sm ring-1 ring-input ring-inset outline-ring focus-visible:outline-2 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2 aria-invalid:ring-destructive"
20
+ @change="
21
+ const option = options.find(
22
+ (option) => String(option.value) === ($event.currentTarget as HTMLSelectElement).value,
23
+ );
24
+ if (option) {
25
+ select(option);
26
+ }
27
+ "
28
+ >
29
+ <option v-if="modelValue === undefined" value="" disabled>{{ placeholder ?? "Select" }}</option>
30
+
31
+ <option
32
+ v-for="option in options"
33
+ :key="option.value"
34
+ :value="option.value"
35
+ :selected="option.value === modelValue"
36
+ :disabled="option.disabled"
37
+ >
38
+ {{ option.label }}
39
+ </option>
40
+ </select>
8
41
 
9
- <div ref="container" class="relative">
42
+ <div v-else ref="container" class="relative">
10
43
  <Stack
11
44
  direction="horizontal"
12
45
  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"
14
- :class="disabled ? 'cursor-not-allowed opacity-50' : ''"
15
- @click="search?.focus()"
46
+ class="w-full cursor-text rounded-ui bg-secondary p-3 text-sm font-semibold ring-1 ring-input ring-inset focus-within:ring-2 focus-within:ring-ring"
47
+ :class="[
48
+ disabled ? 'cursor-not-allowed opacity-50' : '',
49
+ (invalid ??
50
+ (controlAttributes['aria-invalid'] === true || controlAttributes['aria-invalid'] === 'true'))
51
+ ? 'ring-2 ring-destructive'
52
+ : '',
53
+ ]"
54
+ @click="
55
+ if (!disabled) {
56
+ open = true;
57
+ search?.focus();
58
+ }
59
+ "
16
60
  >
17
61
  <input
62
+ v-bind="controlAttributes"
63
+ :id="controlAttributes.id ?? controlId"
18
64
  ref="search"
19
65
  v-model="query"
20
66
  role="combobox"
21
- class="w-full min-w-0 bg-transparent py-2 outline-none placeholder:opacity-50"
67
+ class="w-full min-w-0 bg-transparent px-1 outline-none placeholder:opacity-50"
22
68
  :placeholder="placeholder ?? 'Select'"
23
69
  :aria-expanded="open"
24
70
  :aria-controls="listboxId"
25
71
  :aria-activedescendant="activeId"
26
72
  aria-autocomplete="list"
27
73
  :disabled="disabled"
28
- @focus="open = true"
74
+ :required="required"
75
+ :aria-invalid="invalid ?? controlAttributes['aria-invalid']"
76
+ @invalid="open = true"
29
77
  @input="input"
30
78
  @keydown.enter.prevent="confirm"
31
- @keydown.escape="open = false"
79
+ @keydown.escape="escape"
80
+ @keydown.tab="open = false"
32
81
  @keydown.arrow-down.prevent="stepHighlight(1)"
33
82
  @keydown.arrow-up.prevent="stepHighlight(-1)"
34
83
  />
35
84
 
36
- <button
37
- type="button"
38
- tabindex="-1"
39
- class="shrink-0 opacity-60 transition-transform"
40
- :class="open ? 'rotate-180' : ''"
41
- :disabled="disabled"
42
- @click.stop="open = !open"
43
- >
44
- <Icon :is="ArrowDownIcon" :size="iconSize" />
85
+ <button type="button" tabindex="-1" class="shrink-0" :disabled="disabled" @click.stop="open = !open">
86
+ <Icon :is="open ? ArrowUpIcon : ArrowDownIcon" :size="iconSize" />
45
87
  </button>
46
88
  </Stack>
47
89
 
48
- <div
90
+ <Stack
49
91
  v-if="open"
50
92
  :id="listboxId"
51
93
  role="listbox"
52
94
  :aria-label="label"
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"
95
+ padding="1"
96
+ 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 text-popover-foreground shadow-xl [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
54
97
  >
55
98
  <Button
56
99
  v-for="(option, index) in filtered"
@@ -60,13 +103,14 @@
60
103
  variant="ghost"
61
104
  layout="left"
62
105
  :disabled="option.disabled"
106
+ tabindex="-1"
63
107
  role="option"
64
108
  :aria-selected="option.value === modelValue"
65
109
  @click="select(option)"
66
110
  >
67
111
  <Stack direction="horizontal" align="center" justify="between" gap="1" class="w-full text-left">
68
112
  <slot name="option" :option="option" :selected="option.value === modelValue">
69
- {{ option.label }}
113
+ <span class="min-w-0 flex-1 truncate px-1">{{ option.label }}</span>
70
114
  </slot>
71
115
 
72
116
  <Icon v-if="option.value === modelValue" :is="TickIcon" :size="iconSize" />
@@ -76,20 +120,23 @@
76
120
  <p v-if="filtered.length === 0" class="px-1 py-5 text-center text-sm opacity-60">
77
121
  {{ emptyLabel }}
78
122
  </p>
79
- </div>
123
+ </Stack>
80
124
  </div>
81
125
  </Stack>
82
126
  </template>
83
127
 
84
- <script setup lang="ts">
128
+ <script setup lang="ts" generic="Value extends SelectValue = SelectValue">
85
129
  import ArrowDownIcon from "~icons/hugeicons/arrow-down-01";
130
+ import ArrowUpIcon from "~icons/hugeicons/arrow-up-01";
86
131
  import TickIcon from "~icons/hugeicons/tick-02";
87
- import { computed, onUnmounted, ref, useId, useTemplateRef, watch } from "vue";
132
+ import { computed, onUnmounted, ref, useId, useTemplateRef, watch, watchEffect } from "vue";
88
133
  import type { SelectOption, SelectProps, SelectValue } from "../../components/select";
89
134
  import Button from "./button.vue";
90
135
  import Icon from "./icon.vue";
91
136
  import Stack from "./stack.vue";
92
- import { useFocusTrap } from "../utilities/use-focus-trap";
137
+ import { useControlAttributes } from "../utilities/use-control-attributes";
138
+
139
+ defineOptions({ inheritAttrs: false });
93
140
 
94
141
  const {
95
142
  modelValue,
@@ -100,30 +147,41 @@ const {
100
147
  name,
101
148
  required = false,
102
149
  disabled = false,
103
- } = defineProps<SelectProps>();
150
+ searchable = true,
151
+ invalid = undefined,
152
+ requiredLabel = "Please select an option",
153
+ } = defineProps<SelectProps<Value>>();
104
154
 
105
155
  const emit = defineEmits<{
106
- "update:modelValue": [value: SelectValue];
156
+ "update:modelValue": [value: Value];
107
157
  }>();
108
158
 
109
159
  const open = ref(false);
110
160
 
111
161
  const container = useTemplateRef("container");
112
162
 
113
- useFocusTrap(container, open);
163
+ const { rootAttributes, controlAttributes } = useControlAttributes();
114
164
 
115
165
  const search = useTemplateRef("search");
116
166
 
167
+ const nativeControl = useTemplateRef("nativeControl");
168
+
117
169
  const listboxId = `select-${useId()}`;
118
170
 
171
+ const controlId = `${listboxId}-control`;
172
+
173
+ defineExpose({ focus: () => (search.value ?? nativeControl.value)?.focus() });
174
+
119
175
  const iconSize = 20;
120
176
 
121
177
  const selected = computed(() => options.find((option) => option.value === modelValue));
122
178
 
123
179
  const query = ref(selected.value?.label ?? "");
124
180
 
181
+ const searching = ref(false);
182
+
125
183
  const filtered = computed(() => {
126
- const term = query.value.trim().toLowerCase();
184
+ const term = searching.value ? query.value.trim().toLowerCase() : "";
127
185
 
128
186
  return term === "" ? options : options.filter((option) => option.label.toLowerCase().includes(term));
129
187
  });
@@ -140,10 +198,19 @@ function handlePointerDown(event: PointerEvent) {
140
198
  }
141
199
  }
142
200
 
143
- function select(option: SelectOption) {
201
+ function escape(event: KeyboardEvent) {
202
+ if (open.value) {
203
+ event.preventDefault();
204
+ event.stopPropagation();
205
+ open.value = false;
206
+ }
207
+ }
208
+
209
+ function select(option: SelectOption<Value>) {
144
210
  if (!option.disabled) {
145
211
  emit("update:modelValue", option.value);
146
212
  open.value = false;
213
+ search.value?.focus();
147
214
  }
148
215
  }
149
216
 
@@ -194,6 +261,12 @@ function confirm() {
194
261
  }
195
262
 
196
263
  function input() {
264
+ if (!searchable) {
265
+ return;
266
+ }
267
+
268
+ searching.value = true;
269
+
197
270
  open.value = true;
198
271
 
199
272
  highlighted.value = -1;
@@ -205,10 +278,16 @@ watch(selected, (value) => {
205
278
  }
206
279
  });
207
280
 
281
+ watchEffect(() => {
282
+ search.value?.setCustomValidity(required && !disabled && !selected.value ? requiredLabel : "");
283
+
284
+ if (disabled) {
285
+ open.value = false;
286
+ }
287
+ });
288
+
208
289
  watch(open, (value) => {
209
290
  if (value) {
210
- query.value = "";
211
-
212
291
  highlighted.value = filtered.value.findIndex((option) => option.value === modelValue);
213
292
 
214
293
  document.addEventListener("pointerdown", handlePointerDown);
@@ -221,6 +300,8 @@ watch(open, (value) => {
221
300
 
222
301
  highlighted.value = -1;
223
302
 
303
+ searching.value = false;
304
+
224
305
  query.value = selected.value?.label ?? "";
225
306
  }
226
307
  });
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="shrink-0 bg-border"
3
+ class="shrink-0 self-center bg-border"
4
4
  :class="variants[selected]"
5
5
  :role="decorative ? 'none' : 'separator'"
6
6
  :aria-orientation="decorative ? undefined : selected"
@@ -17,6 +17,6 @@ const selected = computed(() => variant ?? orientation);
17
17
 
18
18
  const variants: Record<SeparatorOrientation, string> = {
19
19
  horizontal: "h-px w-full",
20
- vertical: "h-full min-h-5 w-px",
20
+ vertical: "h-5 w-px",
21
21
  };
22
22
  </script>
@@ -1,25 +1,47 @@
1
1
  <template>
2
2
  <dialog
3
3
  ref="sheet"
4
- class="m-0 max-w-none border-0 bg-transparent p-0 backdrop:bg-black/50"
4
+ class="m-0 max-w-none border-0 bg-transparent p-0 backdrop:bg-black/50 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
5
5
  :class="positions[side]"
6
- @close="$emit('update:modelValue', false)"
7
- @click="$event.target === $event.currentTarget && ($event.currentTarget as HTMLDialogElement).close()"
6
+ :aria-labelledby="label && !$slots.header ? (labelId ?? id) : ($attrs as HTMLAttributes)['aria-labelledby']"
7
+ @close="handleClose"
8
+ @cancel="dismiss"
9
+ @click="$event.target === $event.currentTarget && dismiss($event)"
8
10
  >
9
- <Card :title="title" :description="description" :class="cardClasses[side]">
11
+ <Card
12
+ :label="label"
13
+ :label-id="labelId ?? id"
14
+ :body-padding="bodyPadding"
15
+ :body-gap="bodyGap"
16
+ :class="cardClasses[side]"
17
+ >
18
+ <template v-if="$slots.header" #header>
19
+ <slot name="header" />
20
+ </template>
21
+
10
22
  <template #actions>
23
+ <slot name="actions" />
24
+
11
25
  <Button
12
26
  size="icon"
13
27
  variant="ghost"
14
28
  :icon-prefix="CloseIcon"
15
29
  :aria-label="closeLabel"
16
- @click="sheet?.close()"
30
+ :disabled="!dismissible"
31
+ @click="dismiss"
17
32
  />
18
33
  </template>
19
34
 
20
- <div class="flex-1 overflow-auto">
35
+ <Stack
36
+ :gap="bodyGap"
37
+ :direction="bodyDirection"
38
+ :align="bodyAlign"
39
+ :justify="bodyJustify"
40
+ :wrap="bodyWrap"
41
+ class="min-h-0 flex-1 overflow-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
42
+ >
21
43
  <slot />
22
- </div>
44
+ </Stack>
23
45
 
24
46
  <template v-if="$slots.footer" #footer>
25
47
  <slot name="footer" />
@@ -29,25 +51,45 @@
29
51
  </template>
30
52
 
31
53
  <script setup lang="ts">
54
+ import { useId, useTemplateRef, type HTMLAttributes } from "vue";
32
55
  import CloseIcon from "~icons/hugeicons/multiplication-sign";
33
- import { useTemplateRef, watchEffect } from "vue";
34
56
  import type { SheetProps, SheetSide } from "../../components/sheet";
57
+ import { useModalDialog } from "../utilities/use-modal-dialog";
35
58
  import Button from "./button.vue";
36
59
  import Card from "./card.vue";
37
- import { useFocusTrap } from "../utilities/use-focus-trap";
38
- import { useScrollLock } from "../utilities/use-scroll-lock";
60
+ import Stack from "./stack.vue";
39
61
 
40
- const { modelValue = false, side = "right", title, description, closeLabel = "Close" } = defineProps<SheetProps>();
62
+ const {
63
+ modelValue = undefined,
64
+ side = "right",
65
+ label,
66
+ labelId,
67
+ bodyPadding,
68
+ bodyGap = "5",
69
+ bodyDirection,
70
+ bodyAlign,
71
+ bodyJustify,
72
+ bodyWrap,
73
+ closeLabel = "Close",
74
+ dismissible = true,
75
+ } = defineProps<SheetProps>();
41
76
 
42
- defineEmits<{
77
+ const emit = defineEmits<{
43
78
  "update:modelValue": [value: boolean];
44
79
  }>();
45
80
 
46
81
  const sheet = useTemplateRef("sheet");
47
82
 
48
- useFocusTrap(sheet, () => modelValue);
83
+ const id = `sheet-${useId()}`;
49
84
 
50
- useScrollLock(() => modelValue);
85
+ const { open, close, toggle, dismiss, handleClose } = useModalDialog(
86
+ sheet,
87
+ () => modelValue,
88
+ (value) => emit("update:modelValue", value),
89
+ () => dismissible,
90
+ );
91
+
92
+ defineExpose({ open, close, toggle });
51
93
 
52
94
  const positions: Record<SheetSide, string> = {
53
95
  top: "bottom-auto left-0 right-0 top-0 h-auto w-full",
@@ -62,12 +104,4 @@ const cardClasses: Record<SheetSide, string> = {
62
104
  bottom: "max-h-[85vh] w-full border-b-0 border-l-0 border-r-0 shadow-sheet-bottom",
63
105
  left: "h-full w-full border-b-0 border-l-0 border-t-0 shadow-sheet-left",
64
106
  };
65
-
66
- watchEffect(() => {
67
- if (modelValue && !sheet.value?.open) {
68
- sheet.value?.showModal();
69
- } else if (!modelValue && sheet.value?.open) {
70
- sheet.value.close();
71
- }
72
- });
73
107
  </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <header class="z-10 shrink-0 border-b border-border bg-background" :class="sticky ? 'sticky top-0' : ''">
3
+ <Stack direction="horizontal" align="center" gap="1" padding="1">
4
+ <slot name="prefix" />
5
+
6
+ <h1 v-if="label || $slots.heading" class="min-w-0 flex-1 truncate p-3 text-sm font-semibold">
7
+ <slot name="heading">{{ label }}</slot>
8
+ </h1>
9
+ <template v-else-if="headingLabel">
10
+ <h1 class="sr-only">{{ headingLabel }}</h1>
11
+
12
+ <span aria-hidden="true" class="min-w-0 flex-1" />
13
+ </template>
14
+ <span v-else aria-hidden="true" class="min-w-0 flex-1" />
15
+
16
+ <slot name="actions"><slot /></slot>
17
+ </Stack>
18
+
19
+ <Stack
20
+ v-if="$slots.navigation"
21
+ is="nav"
22
+ :aria-label="navigationLabel"
23
+ direction="horizontal"
24
+ gap="1"
25
+ padding="1"
26
+ class="overflow-x-auto border-t border-border [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
27
+ >
28
+ <slot name="navigation" />
29
+ </Stack>
30
+ </header>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import type { ShellProps } from "../../components/shell";
35
+ import Stack from "./stack.vue";
36
+
37
+ const { label, headingLabel, sticky = true, navigationLabel = "Page navigation" } = defineProps<ShellProps>();
38
+ </script>
@@ -1,21 +1,35 @@
1
1
  <template>
2
- <Stack is="label" direction="horizontal" align="start" gap="3" :class="interactionVariants[interaction]">
2
+ <Stack
3
+ is="label"
4
+ direction="horizontal"
5
+ align="center"
6
+ gap="3"
7
+ class="group"
8
+ :class="interactionVariants[interaction]"
9
+ >
3
10
  <input v-if="name" type="hidden" :name="name" :value="modelValue ? 'on' : 'off'" />
4
11
 
5
12
  <button
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
- :class="variants[state]"
13
+ class="relative flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-ui p-0.5 outline-ring focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed"
14
+ :class="[
15
+ variants[state],
16
+ disabled
17
+ ? ''
18
+ : modelValue
19
+ ? 'group-hover:bg-primary-hover group-active:bg-primary-active'
20
+ : 'group-hover:bg-secondary-hover group-active:bg-secondary-active',
21
+ ]"
8
22
  type="button"
9
23
  role="switch"
10
24
  :aria-checked="modelValue"
11
25
  :disabled="disabled"
12
26
  @click="$emit('update:modelValue', !modelValue)"
13
27
  >
14
- <span class="block size-5 rounded-ui shadow transition-transform" :class="thumbVariants[state]" />
28
+ <span class="block size-5 rounded-ui shadow" :class="thumbVariants[state]" />
15
29
  </button>
16
30
 
17
31
  <Stack is="span" v-if="label || description || $slots.default" gap="0">
18
- <span class="text-sm font-medium">
32
+ <span class="text-sm font-normal">
19
33
  <slot> {{ label }} </slot>
20
34
  </span>
21
35
 
@@ -47,11 +61,11 @@ const interactionVariants: Record<ControlInteraction, string> = {
47
61
 
48
62
  const thumbVariants: Record<SwitchState, string> = {
49
63
  checked: "translate-x-5 bg-primary-foreground",
50
- unchecked: "translate-x-1 bg-white",
64
+ unchecked: "translate-x-0 bg-foreground",
51
65
  };
52
66
 
53
67
  const variants: Record<SwitchState, string> = {
54
68
  checked: "bg-primary",
55
- unchecked: "bg-input",
69
+ unchecked: "bg-background ring-1 ring-input ring-inset",
56
70
  };
57
71
  </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <textarea
3
+ ref="control"
4
+ class="block w-full min-w-0 rounded-ui outline-ring placeholder:opacity-50 focus-visible:outline-2 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2 aria-invalid:ring-destructive"
5
+ :class="[texts[text], resizes[resize], variants[variant]]"
6
+ :value="modelValue"
7
+ :disabled="disabled"
8
+ :readonly="readonly"
9
+ :required="required"
10
+ :aria-invalid="invalid ?? ($attrs as HTMLAttributes)['aria-invalid']"
11
+ @input="$emit('update:modelValue', ($event.currentTarget as HTMLTextAreaElement).value)"
12
+ ></textarea>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { useTemplateRef, type HTMLAttributes } from "vue";
17
+ import type { TextareaProps, TextareaResize, TextareaText, TextareaVariant } from "../../components/textarea";
18
+
19
+ const {
20
+ modelValue = "",
21
+ text = "sm",
22
+ resize = "vertical",
23
+ variant = "default",
24
+ disabled = false,
25
+ readonly = false,
26
+ required = false,
27
+ invalid = undefined,
28
+ } = defineProps<TextareaProps>();
29
+
30
+ defineEmits<{ "update:modelValue": [value: string] }>();
31
+
32
+ const control = useTemplateRef("control");
33
+
34
+ defineExpose({ focus: () => control.value?.focus() });
35
+
36
+ const texts: Record<TextareaText, string> = { sm: "text-sm", md: "text-base" };
37
+
38
+ const resizes: Record<TextareaResize, string> = {
39
+ none: "resize-none",
40
+ vertical: "resize-y",
41
+ horizontal: "resize-x",
42
+ both: "resize",
43
+ };
44
+
45
+ const variants: Record<TextareaVariant, string> = {
46
+ default: "bg-secondary p-3 ring-1 ring-input ring-inset",
47
+ plain: "bg-transparent p-0",
48
+ };
49
+ </script>