@revojs/ui 0.1.58

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 (82) hide show
  1. package/dist/components/breadcrumb.d.ts +4 -0
  2. package/dist/components/breadcrumb.js +1 -0
  3. package/dist/components/button.d.ts +18 -0
  4. package/dist/components/button.js +1 -0
  5. package/dist/components/card.d.ts +5 -0
  6. package/dist/components/card.js +1 -0
  7. package/dist/components/checkbox.d.ts +8 -0
  8. package/dist/components/checkbox.js +1 -0
  9. package/dist/components/collection.d.ts +6 -0
  10. package/dist/components/collection.js +1 -0
  11. package/dist/components/container.d.ts +4 -0
  12. package/dist/components/container.js +1 -0
  13. package/dist/components/control.d.ts +1 -0
  14. package/dist/components/control.js +1 -0
  15. package/dist/components/counter.d.ts +8 -0
  16. package/dist/components/counter.js +1 -0
  17. package/dist/components/dialog.d.ts +6 -0
  18. package/dist/components/dialog.js +1 -0
  19. package/dist/components/field.d.ts +7 -0
  20. package/dist/components/field.js +1 -0
  21. package/dist/components/heading.d.ts +7 -0
  22. package/dist/components/heading.js +1 -0
  23. package/dist/components/icon.d.ts +3 -0
  24. package/dist/components/icon.js +1 -0
  25. package/dist/components/input.d.ts +12 -0
  26. package/dist/components/input.js +1 -0
  27. package/dist/components/link.d.ts +6 -0
  28. package/dist/components/link.js +1 -0
  29. package/dist/components/navigate.d.ts +7 -0
  30. package/dist/components/navigate.js +1 -0
  31. package/dist/components/popover.d.ts +10 -0
  32. package/dist/components/popover.js +1 -0
  33. package/dist/components/progress.d.ts +5 -0
  34. package/dist/components/progress.js +1 -0
  35. package/dist/components/radio.d.ts +10 -0
  36. package/dist/components/radio.js +1 -0
  37. package/dist/components/range.d.ts +8 -0
  38. package/dist/components/range.js +1 -0
  39. package/dist/components/rating.d.ts +8 -0
  40. package/dist/components/rating.js +1 -0
  41. package/dist/components/select.d.ts +16 -0
  42. package/dist/components/select.js +1 -0
  43. package/dist/components/separator.d.ts +11 -0
  44. package/dist/components/separator.js +1 -0
  45. package/dist/components/sheet.d.ts +8 -0
  46. package/dist/components/sheet.js +1 -0
  47. package/dist/components/switch.d.ts +8 -0
  48. package/dist/components/switch.js +1 -0
  49. package/dist/components/upload.d.ts +10 -0
  50. package/dist/components/upload.js +1 -0
  51. package/dist/index.d.ts +25 -0
  52. package/dist/index.js +1 -0
  53. package/dist/module/index.d.ts +35 -0
  54. package/dist/module/index.js +123 -0
  55. package/dist/types/index.d.ts +20 -0
  56. package/dist/vue/components/breadcrumb.vue +20 -0
  57. package/dist/vue/components/button.vue +124 -0
  58. package/dist/vue/components/card.vue +53 -0
  59. package/dist/vue/components/checkbox.vue +54 -0
  60. package/dist/vue/components/collection.vue +51 -0
  61. package/dist/vue/components/container.vue +19 -0
  62. package/dist/vue/components/counter.vue +61 -0
  63. package/dist/vue/components/dialog.vue +76 -0
  64. package/dist/vue/components/field.vue +38 -0
  65. package/dist/vue/components/heading.vue +34 -0
  66. package/dist/vue/components/icon.vue +49 -0
  67. package/dist/vue/components/input.vue +82 -0
  68. package/dist/vue/components/link.vue +33 -0
  69. package/dist/vue/components/navigate.vue +30 -0
  70. package/dist/vue/components/popover.vue +123 -0
  71. package/dist/vue/components/progress.vue +44 -0
  72. package/dist/vue/components/radio.vue +59 -0
  73. package/dist/vue/components/range.vue +56 -0
  74. package/dist/vue/components/rating.vue +56 -0
  75. package/dist/vue/components/select.vue +99 -0
  76. package/dist/vue/components/separator.vue +31 -0
  77. package/dist/vue/components/sheet.vue +71 -0
  78. package/dist/vue/components/switch.vue +72 -0
  79. package/dist/vue/components/upload.vue +133 -0
  80. package/dist/vue/index.d.ts +25 -0
  81. package/dist/vue/index.js +25 -0
  82. package/package.json +46 -0
@@ -0,0 +1,124 @@
1
+ <template>
2
+ <component
3
+ :is="is"
4
+ class="inline-flex cursor-pointer items-center justify-center gap-2 border border-transparent font-[inherit] font-medium leading-none no-underline transition-[background-color,border-color,color,opacity] duration-150 hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:hover:opacity-50"
5
+ :class="[
6
+ config.radius,
7
+ `outline-${config.light.ring}`,
8
+ `dark:outline-${config.dark.ring}`,
9
+ fonts[font],
10
+ layouts[layout],
11
+ sizes[size],
12
+ texts[text],
13
+ variants[variant],
14
+ ]"
15
+ :type="attributes.type ?? 'button'"
16
+ :disabled="disabled"
17
+ >
18
+ <Icon v-if="iconPrefix" :name="iconPrefix" />
19
+
20
+ <slot>
21
+ <span v-if="label" class="truncate px-1" :class="labelLayouts[layout]">{{ label }}</span>
22
+ </slot>
23
+
24
+ <Icon v-if="iconSuffix" :name="iconSuffix" />
25
+ </component>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import config from "#config/ui";
30
+ import { useAttrs, type Component } from "vue";
31
+ import type { ButtonFont, ButtonLayout, ButtonSize, ButtonText, ButtonVariant } from "../../components/button.ts";
32
+ import Icon from "./icon.vue";
33
+
34
+ const {
35
+ is = "button",
36
+ iconPrefix = undefined,
37
+ iconSuffix = undefined,
38
+ label = undefined,
39
+ font = "medium",
40
+ layout = "center",
41
+ size = "default",
42
+ text = "sm",
43
+ variant = "primary",
44
+ disabled = false,
45
+ } = defineProps<{
46
+ is?: string | Component;
47
+ iconPrefix?: string;
48
+ iconSuffix?: string;
49
+ label?: string;
50
+ font?: ButtonFont;
51
+ layout?: ButtonLayout;
52
+ size?: ButtonSize;
53
+ text?: ButtonText;
54
+ variant?: ButtonVariant;
55
+ disabled?: boolean;
56
+ }>();
57
+
58
+ const attributes = useAttrs();
59
+
60
+ const fonts: Record<ButtonFont, string> = {
61
+ normal: "font-normal",
62
+ medium: "font-medium",
63
+ semibold: "font-semibold",
64
+ };
65
+
66
+ const layouts: Record<ButtonLayout, string> = {
67
+ left: "justify-start text-left",
68
+ center: "justify-center text-center",
69
+ right: "justify-end text-right",
70
+ };
71
+
72
+ const labelLayouts: Record<ButtonLayout, string> = {
73
+ left: "flex-1",
74
+ center: "",
75
+ right: "flex-1",
76
+ };
77
+
78
+ const sizes: Record<ButtonSize, string> = {
79
+ default: "min-h-10 px-4 py-2",
80
+ icon: "size-10 shrink-0 p-0",
81
+ };
82
+
83
+ const texts: Record<ButtonText, string> = {
84
+ sm: "text-sm",
85
+ md: "text-base",
86
+ };
87
+
88
+ const variants: Record<ButtonVariant, Array<string>> = {
89
+ primary: [
90
+ `bg-${config.light.primary}`,
91
+ `text-${config.light.primaryForeground}`,
92
+ `dark:bg-${config.dark.primary}`,
93
+ `dark:text-${config.dark.primaryForeground}`,
94
+ ],
95
+ secondary: [
96
+ `bg-${config.light.secondary}`,
97
+ `text-${config.light.secondaryForeground}`,
98
+ `dark:bg-${config.dark.secondary}`,
99
+ `dark:text-${config.dark.secondaryForeground}`,
100
+ ],
101
+ outline: [
102
+ `border-${config.light.border}`,
103
+ `bg-${config.light.background}`,
104
+ `text-${config.light.foreground}`,
105
+ `dark:border-${config.dark.border}`,
106
+ `dark:bg-${config.dark.background}`,
107
+ `dark:text-${config.dark.foreground}`,
108
+ ],
109
+ ghost: [`text-${config.light.foreground}`, `dark:text-${config.dark.foreground}`],
110
+ destructive: [
111
+ `bg-${config.light.destructive}`,
112
+ `text-${config.light.destructiveForeground}`,
113
+ `dark:bg-${config.dark.destructive}`,
114
+ `dark:text-${config.dark.destructiveForeground}`,
115
+ ],
116
+ danger: [
117
+ `bg-${config.light.destructive}`,
118
+ `text-${config.light.destructiveForeground}`,
119
+ `dark:bg-${config.dark.destructive}`,
120
+ `dark:text-${config.dark.destructiveForeground}`,
121
+ ],
122
+ text: [`text-${config.light.foreground}`, `dark:text-${config.dark.foreground}`],
123
+ };
124
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <section
3
+ class="flex flex-col overflow-hidden border shadow-sm"
4
+ :class="[
5
+ config.radius,
6
+ `border-${config.light.border}`,
7
+ `bg-${config.light.card}`,
8
+ `text-${config.light.cardForeground}`,
9
+ `dark:border-${config.dark.border}`,
10
+ `dark:bg-${config.dark.card}`,
11
+ `dark:text-${config.dark.cardForeground}`,
12
+ ]"
13
+ >
14
+ <header
15
+ v-if="label || title || description || $slots.header || $slots.actions"
16
+ class="flex items-start justify-between gap-4 border-b p-5"
17
+ :class="[`border-${config.light.border}`, `dark:border-${config.dark.border}`]"
18
+ >
19
+ <slot name="header">
20
+ <div class="flex flex-col gap-1.5">
21
+ <h3 v-if="label || title" class="text-lg font-semibold leading-none">{{ label ?? title }}</h3>
22
+ <p v-if="description" class="text-sm opacity-70">{{ description }}</p>
23
+ </div>
24
+ </slot>
25
+
26
+ <div v-if="$slots.actions" class="flex shrink-0 items-center gap-1">
27
+ <slot name="actions" />
28
+ </div>
29
+ </header>
30
+
31
+ <div class="flex flex-1 flex-col gap-4 p-5">
32
+ <slot />
33
+ </div>
34
+
35
+ <footer v-if="$slots.footer" class="flex items-center gap-3 px-5 pb-5">
36
+ <slot name="footer" />
37
+ </footer>
38
+ </section>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+ import config from "#config/ui";
43
+
44
+ const {
45
+ label = undefined,
46
+ title = undefined,
47
+ description = undefined,
48
+ } = defineProps<{
49
+ label?: string;
50
+ title?: string;
51
+ description?: string;
52
+ }>();
53
+ </script>
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <label class="inline-flex items-start gap-3" :class="interactionVariants[interaction]">
3
+ <input
4
+ class="mt-0.5 size-5 shrink-0 cursor-inherit accent-current"
5
+ :class="[`text-${config.light.primary}`, `dark:text-${config.dark.primary}`]"
6
+ type="checkbox"
7
+ :checked="modelValue"
8
+ :name="name"
9
+ :required="required"
10
+ :disabled="disabled"
11
+ @change="$emit('update:modelValue', ($event.currentTarget as HTMLInputElement).checked)"
12
+ />
13
+
14
+ <span v-if="label || description || $slots.default" class="flex flex-col gap-0.5">
15
+ <span class="text-sm font-medium"
16
+ ><slot>{{ label }}</slot></span
17
+ >
18
+ <span v-if="description" class="text-sm opacity-60">{{ description }}</span>
19
+ </span>
20
+ </label>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import config from "#config/ui";
25
+ import { computed } from "vue";
26
+ import type { ControlInteraction } from "../../components/control.ts";
27
+
28
+ const {
29
+ modelValue = false,
30
+ label = undefined,
31
+ description = undefined,
32
+ name = undefined,
33
+ required = false,
34
+ disabled = false,
35
+ } = defineProps<{
36
+ modelValue?: boolean;
37
+ label?: string;
38
+ description?: string;
39
+ name?: string;
40
+ required?: boolean;
41
+ disabled?: boolean;
42
+ }>();
43
+
44
+ defineEmits<{
45
+ "update:modelValue": [value: boolean];
46
+ }>();
47
+
48
+ const interaction = computed<ControlInteraction>(() => (disabled ? "disabled" : "enabled"));
49
+
50
+ const interactionVariants: Record<ControlInteraction, string> = {
51
+ disabled: "cursor-not-allowed opacity-50",
52
+ enabled: "cursor-pointer",
53
+ };
54
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <div
3
+ v-if="items"
4
+ class="flex w-full flex-col divide-y overflow-hidden border"
5
+ :class="[
6
+ config.radius,
7
+ `divide-${config.light.border}`,
8
+ `border-${config.light.border}`,
9
+ `bg-${config.light.card}`,
10
+ `dark:divide-${config.dark.border}`,
11
+ `dark:border-${config.dark.border}`,
12
+ `dark:bg-${config.dark.card}`,
13
+ ]"
14
+ >
15
+ <template v-if="items.length">
16
+ <div v-for="(item, index) in items" :key="index">
17
+ <slot :item="item" :index="index" />
18
+ </div>
19
+ </template>
20
+ <div v-else class="flex flex-col items-center p-5 text-sm opacity-60">{{ emptyLabel }}</div>
21
+ </div>
22
+ <div v-else class="grid gap-4" :class="columnsClass[columns]">
23
+ <slot />
24
+ </div>
25
+ </template>
26
+
27
+ <script setup generic="T" lang="ts">
28
+ import config from "#config/ui";
29
+ import type { CollectionColumns } from "../../components/collection.ts";
30
+
31
+ const {
32
+ items = undefined,
33
+ columns = 1,
34
+ emptyLabel = "Nothing found",
35
+ } = defineProps<{
36
+ items?: Array<T>;
37
+ columns?: CollectionColumns;
38
+ emptyLabel?: string;
39
+ }>();
40
+
41
+ defineSlots<{
42
+ default: (() => unknown) & ((props: { item: T; index: number }) => unknown);
43
+ }>();
44
+
45
+ const columnsClass: Record<CollectionColumns, string> = {
46
+ 1: "grid-cols-1",
47
+ 2: "grid-cols-1 sm:grid-cols-2",
48
+ 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
49
+ 4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
50
+ };
51
+ </script>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <div class="mx-auto flex w-full flex-col gap-5 p-5" :class="sizes[size]">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import type { ContainerSize } from "../../components/container.ts";
9
+
10
+ const { size = "5xl" } = defineProps<{
11
+ size?: ContainerSize;
12
+ }>();
13
+
14
+ const sizes: Record<ContainerSize, string> = {
15
+ "3xl": "max-w-3xl",
16
+ "5xl": "max-w-5xl",
17
+ "7xl": "max-w-7xl",
18
+ };
19
+ </script>
@@ -0,0 +1,61 @@
1
+ <template>
2
+ <div class="inline-flex flex-col gap-2">
3
+ <span v-if="label" class="text-sm font-medium">{{ label }}</span>
4
+
5
+ <div
6
+ class="grid h-10 grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center overflow-hidden border"
7
+ :class="[config.radius, `border-${config.light.input}`, `dark:border-${config.dark.input}`]"
8
+ >
9
+ <Button
10
+ class="text-lg"
11
+ size="icon"
12
+ variant="ghost"
13
+ icon-prefix="minus-sign"
14
+ :disabled="disabled || modelValue <= min"
15
+ aria-label="Decrease"
16
+ @click="change(-step)"
17
+ />
18
+
19
+ <output class="px-2 text-center tabular-nums">{{ modelValue }}</output>
20
+
21
+ <Button
22
+ class="text-lg"
23
+ size="icon"
24
+ variant="ghost"
25
+ icon-prefix="add-01"
26
+ :disabled="disabled || modelValue >= max"
27
+ aria-label="Increase"
28
+ @click="change(step)"
29
+ />
30
+ </div>
31
+ </div>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import config from "#config/ui";
36
+ import Button from "./button.vue";
37
+
38
+ const {
39
+ modelValue = 0,
40
+ label = undefined,
41
+ min = -Infinity,
42
+ max = Infinity,
43
+ step = 1,
44
+ disabled = false,
45
+ } = defineProps<{
46
+ modelValue?: number;
47
+ label?: string;
48
+ min?: number;
49
+ max?: number;
50
+ step?: number;
51
+ disabled?: boolean;
52
+ }>();
53
+
54
+ const emit = defineEmits<{
55
+ "update:modelValue": [value: number];
56
+ }>();
57
+
58
+ function change(amount: number) {
59
+ emit("update:modelValue", Math.min(max, Math.max(min, modelValue + amount)));
60
+ }
61
+ </script>
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <dialog
3
+ ref="dialog"
4
+ class="m-auto w-[min(32rem,calc(100%-2rem))] border-0 bg-transparent p-0 backdrop:bg-black/50"
5
+ @close="$emit('update:modelValue', false)"
6
+ @click="$event.target === $event.currentTarget && ($event.currentTarget as HTMLDialogElement).close()"
7
+ >
8
+ <Card :title="title" :description="description" class="shadow-2xl">
9
+ <template #actions>
10
+ <Button
11
+ size="icon"
12
+ variant="ghost"
13
+ icon-prefix="multiplication-sign"
14
+ :aria-label="closeLabel"
15
+ @click="close"
16
+ />
17
+ </template>
18
+ <slot :open="open" :close="close" :toggle="toggle" />
19
+
20
+ <template v-if="$slots.footer" #footer>
21
+ <slot name="footer" />
22
+ </template>
23
+ </Card>
24
+ </dialog>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import { useTemplateRef, watchEffect } from "vue";
29
+ import Button from "./button.vue";
30
+ import Card from "./card.vue";
31
+
32
+ const {
33
+ modelValue = false,
34
+ title = undefined,
35
+ description = undefined,
36
+ closeLabel = "Close",
37
+ } = defineProps<{
38
+ modelValue?: boolean;
39
+ title?: string;
40
+ description?: string;
41
+ closeLabel?: string;
42
+ }>();
43
+
44
+ const emit = defineEmits<{
45
+ "update:modelValue": [value: boolean];
46
+ }>();
47
+
48
+ const dialog = useTemplateRef("dialog");
49
+
50
+ function open() {
51
+ emit("update:modelValue", true);
52
+ }
53
+
54
+ function close() {
55
+ dialog.value?.close();
56
+ emit("update:modelValue", false);
57
+ }
58
+
59
+ function toggle() {
60
+ if (modelValue) {
61
+ close();
62
+ } else {
63
+ open();
64
+ }
65
+ }
66
+
67
+ watchEffect(() => {
68
+ if (modelValue && !dialog.value?.open) {
69
+ dialog.value?.showModal();
70
+ } else if (!modelValue && dialog.value?.open) {
71
+ dialog.value.close();
72
+ }
73
+ });
74
+
75
+ defineExpose({ open, close, toggle });
76
+ </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <fieldset class="flex min-w-0 flex-col gap-2" :disabled="disabled">
3
+ <legend v-if="label" class="mb-2 text-sm font-medium">
4
+ {{ label }}<span v-if="required" aria-hidden="true"> *</span>
5
+ </legend>
6
+
7
+ <slot />
8
+
9
+ <p
10
+ v-if="error"
11
+ class="text-sm"
12
+ :class="[`text-${config.light.destructive}`, `dark:text-${config.dark.destructive}`]"
13
+ >
14
+ {{ error }}
15
+ </p>
16
+ <p v-else-if="description" class="text-sm opacity-60">
17
+ {{ description }}
18
+ </p>
19
+ </fieldset>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import config from "#config/ui";
24
+
25
+ const {
26
+ label = undefined,
27
+ description = undefined,
28
+ error = undefined,
29
+ required = false,
30
+ disabled = false,
31
+ } = defineProps<{
32
+ label?: string;
33
+ description?: string;
34
+ error?: string;
35
+ required?: boolean;
36
+ disabled?: boolean;
37
+ }>();
38
+ </script>
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <component :is="is" class="font-bold text-pretty" :class="[texts[text], layouts[layout]]">
3
+ <slot> {{ label }} </slot>
4
+ </component>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import type { Component } from "vue";
9
+ import type { HeadingLayout, HeadingText } from "../../components/heading.ts";
10
+
11
+ const {
12
+ is = "h2",
13
+ label = undefined,
14
+ text = "xl",
15
+ layout = "left",
16
+ } = defineProps<{
17
+ is?: string | Component;
18
+ label?: string;
19
+ text?: HeadingText;
20
+ layout?: HeadingLayout;
21
+ }>();
22
+
23
+ const layouts: Record<HeadingLayout, string> = {
24
+ left: "text-left",
25
+ center: "text-center",
26
+ right: "text-right",
27
+ };
28
+
29
+ const texts: Record<HeadingText, string> = {
30
+ sm: "text-sm",
31
+ xl: "text-xl",
32
+ "3xl": "text-3xl",
33
+ };
34
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <span aria-hidden="true" class="block size-[1em] shrink-0">
3
+ <component :is="icon" class="block size-full" />
4
+ </span>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import iconSets from "#ui/icons";
9
+ import { getIconData, iconToSVG, replaceIDs } from "@iconify/utils";
10
+ import { computed, defineAsyncComponent, h, markRaw } from "vue";
11
+
12
+ const { name } = defineProps<{
13
+ name: string;
14
+ }>();
15
+
16
+ const icon = computed(() => {
17
+ const separator = name.indexOf(":");
18
+ const prefix = separator === -1 ? undefined : name.slice(0, separator);
19
+ const iconName = separator === -1 ? name : name.slice(separator + 1);
20
+
21
+ return defineAsyncComponent(async () => {
22
+ for (const [key, resolve] of Object.entries(iconSets)) {
23
+ if (prefix && key !== prefix) {
24
+ continue;
25
+ }
26
+
27
+ const set = await resolve();
28
+
29
+ if (prefix && set.prefix !== prefix) {
30
+ continue;
31
+ }
32
+
33
+ const data = getIconData(set, iconName);
34
+
35
+ if (data) {
36
+ const svg = iconToSVG(data);
37
+ const body = replaceIDs(svg.body);
38
+
39
+ return markRaw({
40
+ name: `${set.prefix}-${iconName}`,
41
+ render: () => h("svg", { ...svg.attributes, innerHTML: body }),
42
+ });
43
+ }
44
+ }
45
+
46
+ return markRaw({ render: () => null });
47
+ });
48
+ });
49
+ </script>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <span
3
+ class="flex min-h-10 w-full cursor-text items-center gap-2 border px-3 text-sm focus-within:ring-2"
4
+ :class="[
5
+ config.radius,
6
+ `border-${config.light.input}`,
7
+ `ring-${config.light.ring}`,
8
+ `dark:border-${config.dark.input}`,
9
+ `dark:ring-${config.dark.ring}`,
10
+ interactionVariants[interaction],
11
+ ]"
12
+ >
13
+ <slot name="leading" />
14
+
15
+ <Icon v-if="iconPrefix" :name="iconPrefix" />
16
+
17
+ <input
18
+ class="w-full min-w-0 appearance-none bg-transparent py-2 outline-none placeholder:opacity-50"
19
+ :value="modelValue"
20
+ :type="type"
21
+ :name="name"
22
+ :spellcheck="spellcheck"
23
+ :placeholder="placeholder"
24
+ :required="required"
25
+ :readonly="readonly"
26
+ :disabled="disabled"
27
+ @input="input"
28
+ />
29
+
30
+ <Icon v-if="iconSuffix" :name="iconSuffix" />
31
+
32
+ <slot name="trailing" />
33
+ </span>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import config from "#config/ui";
38
+ import { computed } from "vue";
39
+ import type { ControlInteraction } from "../../components/control.ts";
40
+ import Icon from "./icon.vue";
41
+
42
+ const {
43
+ modelValue = undefined,
44
+ type = "text",
45
+ iconPrefix = undefined,
46
+ iconSuffix = undefined,
47
+ placeholder = undefined,
48
+ name = undefined,
49
+ spellcheck = undefined,
50
+ required = false,
51
+ readonly = false,
52
+ disabled = false,
53
+ } = defineProps<{
54
+ modelValue?: string | number;
55
+ type?: string;
56
+ iconPrefix?: string;
57
+ iconSuffix?: string;
58
+ placeholder?: string;
59
+ name?: string;
60
+ spellcheck?: boolean;
61
+ required?: boolean;
62
+ readonly?: boolean;
63
+ disabled?: boolean;
64
+ }>();
65
+
66
+ const emit = defineEmits<{
67
+ "update:modelValue": [value: string | number];
68
+ }>();
69
+
70
+ const interaction = computed<ControlInteraction>(() => (disabled ? "disabled" : "enabled"));
71
+
72
+ const interactionVariants: Record<ControlInteraction, string> = {
73
+ disabled: "cursor-not-allowed opacity-50",
74
+ enabled: "",
75
+ };
76
+
77
+ function input(event: Event) {
78
+ const value = (event.currentTarget as HTMLInputElement).value;
79
+
80
+ emit("update:modelValue", type === "number" ? Number(value) : value);
81
+ }
82
+ </script>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <component
3
+ :is="is"
4
+ class="cursor-pointer underline-offset-4 transition-colors hover:underline"
5
+ :class="variants[variant]"
6
+ :href="href"
7
+ >
8
+ <slot>{{ label }}</slot>
9
+ </component>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import config from "#config/ui";
14
+ import type { Component } from "vue";
15
+ import type { LinkVariant } from "../../components/link.ts";
16
+
17
+ const {
18
+ is = "a",
19
+ href = undefined,
20
+ label = undefined,
21
+ variant = "primary",
22
+ } = defineProps<{
23
+ is?: string | Component;
24
+ href?: string;
25
+ label?: string;
26
+ variant?: LinkVariant;
27
+ }>();
28
+
29
+ const variants: Record<LinkVariant, Array<string>> = {
30
+ primary: [`text-${config.light.foreground}`, `dark:text-${config.dark.foreground}`],
31
+ muted: [`text-${config.light.mutedForeground}`, `dark:text-${config.dark.mutedForeground}`],
32
+ };
33
+ </script>