@tunji_xy/ui 0.0.0-stage → 0.10.0
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/LICENSE +21 -0
- package/README.md +68 -2
- package/app/assets/css/main.css +193 -0
- package/app/components/Button.vue +126 -0
- package/app/components/Checkbox.vue +130 -0
- package/app/components/DropdownMenu.vue +444 -0
- package/app/components/Input.vue +172 -0
- package/app/components/Modal.vue +210 -0
- package/app/components/OtpInput.vue +251 -0
- package/app/components/ProgressBar.vue +122 -0
- package/app/components/Radio.vue +131 -0
- package/app/components/Select.vue +680 -0
- package/app/components/Switch.vue +141 -0
- package/app/components/Tabs.vue +288 -0
- package/app/components/Textarea.vue +188 -0
- package/app/components/Tooltip.vue +292 -0
- package/app/lib/cn.ts +25 -0
- package/nuxt.config.ts +22 -0
- package/package.json +57 -4
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="cn('flex flex-col gap-1 text-foreground', attrs.class as string)"
|
|
4
|
+
>
|
|
5
|
+
<div
|
|
6
|
+
:class="
|
|
7
|
+
cn(
|
|
8
|
+
'flex items-start',
|
|
9
|
+
sizes[size].row,
|
|
10
|
+
disabled && 'cursor-not-allowed opacity-60',
|
|
11
|
+
)
|
|
12
|
+
"
|
|
13
|
+
>
|
|
14
|
+
<span class="relative flex h-lh shrink-0 items-center">
|
|
15
|
+
<input
|
|
16
|
+
:id="inputId"
|
|
17
|
+
ref="input"
|
|
18
|
+
v-model="model"
|
|
19
|
+
v-bind="inputAttrs"
|
|
20
|
+
type="radio"
|
|
21
|
+
:class="circleClasses"
|
|
22
|
+
:name="name"
|
|
23
|
+
:value="value"
|
|
24
|
+
:disabled="disabled"
|
|
25
|
+
:required="required"
|
|
26
|
+
:aria-describedby="message ? messageId : undefined"
|
|
27
|
+
/>
|
|
28
|
+
<CircleIcon
|
|
29
|
+
:class="
|
|
30
|
+
cn(
|
|
31
|
+
'pointer-events-none absolute inset-0 m-auto text-primary opacity-0 transition-opacity peer-checked:opacity-100',
|
|
32
|
+
sizes[size].dot,
|
|
33
|
+
)
|
|
34
|
+
"
|
|
35
|
+
fill="currentColor"
|
|
36
|
+
/>
|
|
37
|
+
</span>
|
|
38
|
+
|
|
39
|
+
<label
|
|
40
|
+
v-if="label || $slots.default"
|
|
41
|
+
:for="inputId"
|
|
42
|
+
:class="
|
|
43
|
+
cn('select-none', disabled ? 'cursor-not-allowed' : 'cursor-pointer')
|
|
44
|
+
"
|
|
45
|
+
>
|
|
46
|
+
<slot>{{ label }}</slot>
|
|
47
|
+
</label>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<p
|
|
51
|
+
v-if="message"
|
|
52
|
+
:id="messageId"
|
|
53
|
+
:class="cn('text-sm', sizes[size].message, 'text-muted-foreground')"
|
|
54
|
+
>
|
|
55
|
+
{{ message }}
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<script setup lang="ts">
|
|
61
|
+
import { computed, useAttrs, useId, useTemplateRef } from "vue";
|
|
62
|
+
import { CircleIcon } from "@lucide/vue";
|
|
63
|
+
import { cn } from "../lib/cn";
|
|
64
|
+
|
|
65
|
+
export interface RadioProps {
|
|
66
|
+
label?: string;
|
|
67
|
+
hint?: string;
|
|
68
|
+
size?: "sm" | "md" | "lg";
|
|
69
|
+
disabled?: boolean;
|
|
70
|
+
required?: boolean;
|
|
71
|
+
id?: string;
|
|
72
|
+
/** Use the same name on every radio in a group, so arrow keys move between them. */
|
|
73
|
+
name?: string;
|
|
74
|
+
/** What the v-model is set to when this radio is picked. */
|
|
75
|
+
value: string | number | boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const props = withDefaults(defineProps<RadioProps>(), {
|
|
79
|
+
size: "md",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// The `value` of the picked radio in the group.
|
|
83
|
+
const model = defineModel<string | number | boolean | null>();
|
|
84
|
+
|
|
85
|
+
defineSlots<{
|
|
86
|
+
/** Label content, when it needs more than plain text (e.g. a link). */
|
|
87
|
+
default?: () => unknown;
|
|
88
|
+
}>();
|
|
89
|
+
|
|
90
|
+
// `class` styles the outer wrapper; every other attribute and listener
|
|
91
|
+
// (@change, @focus, data-*) goes to the <input> itself.
|
|
92
|
+
defineOptions({ inheritAttrs: false });
|
|
93
|
+
const attrs = useAttrs();
|
|
94
|
+
const inputAttrs = computed(() => {
|
|
95
|
+
const { class: _class, ...rest } = attrs;
|
|
96
|
+
return rest;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const autoId = useId();
|
|
100
|
+
const inputId = computed(() => props.id ?? `ui-radio-${autoId}`);
|
|
101
|
+
const messageId = computed(() => `${inputId.value}-message`);
|
|
102
|
+
const message = computed(() => props.hint);
|
|
103
|
+
|
|
104
|
+
// Lucide's circle fills its whole grid, so the dot gets its own size, about
|
|
105
|
+
// 40% of the circle around it.
|
|
106
|
+
const sizes = {
|
|
107
|
+
sm: { row: "gap-2 text-sm", circle: "size-4", dot: "size-1.5", message: "pl-6" },
|
|
108
|
+
md: { row: "gap-2 text-sm", circle: "size-5", dot: "size-2", message: "pl-7" },
|
|
109
|
+
lg: { row: "gap-3 text-base", circle: "size-6", dot: "size-2.5", message: "pl-9" },
|
|
110
|
+
} as const;
|
|
111
|
+
|
|
112
|
+
// The native input is restyled into the circle, so keyboard, forms and
|
|
113
|
+
// screen readers behave exactly like a plain radio button.
|
|
114
|
+
const circleClasses = computed(() =>
|
|
115
|
+
cn(
|
|
116
|
+
"peer m-0 cursor-pointer appearance-none rounded-full border border-border bg-transparent outline-none " +
|
|
117
|
+
"transition-[border-color,box-shadow] checked:border-primary " +
|
|
118
|
+
"focus-visible:ring-3 disabled:cursor-not-allowed",
|
|
119
|
+
sizes[props.size].circle,
|
|
120
|
+
"focus-visible:ring-primary/35",
|
|
121
|
+
!props.disabled && "not-checked:hover:border-border-strong",
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const inputEl = useTemplateRef<HTMLInputElement>("input");
|
|
126
|
+
|
|
127
|
+
defineExpose({
|
|
128
|
+
focus: () => inputEl.value?.focus(),
|
|
129
|
+
el: inputEl,
|
|
130
|
+
});
|
|
131
|
+
</script>
|