@soave/ui 0.1.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/dist/build.config.d.ts +2 -0
- package/dist/build.config.mjs +14 -0
- package/dist/components/ui/Alert.vue +39 -0
- package/dist/components/ui/AlertDescription.vue +12 -0
- package/dist/components/ui/AlertTitle.vue +12 -0
- package/dist/components/ui/Button.vue +59 -0
- package/dist/components/ui/Card.vue +15 -0
- package/dist/components/ui/CardContent.vue +12 -0
- package/dist/components/ui/CardDescription.vue +12 -0
- package/dist/components/ui/CardFooter.vue +12 -0
- package/dist/components/ui/CardHeader.vue +12 -0
- package/dist/components/ui/CardTitle.vue +12 -0
- package/dist/components/ui/Checkbox.vue +73 -0
- package/dist/components/ui/Dialog.vue +93 -0
- package/dist/components/ui/DialogDescription.vue +12 -0
- package/dist/components/ui/DialogFooter.vue +12 -0
- package/dist/components/ui/DialogHeader.vue +12 -0
- package/dist/components/ui/DialogTitle.vue +12 -0
- package/dist/components/ui/DropdownMenu.vue +33 -0
- package/dist/components/ui/DropdownMenuContent.vue +66 -0
- package/dist/components/ui/DropdownMenuItem.vue +77 -0
- package/dist/components/ui/DropdownMenuLabel.vue +20 -0
- package/dist/components/ui/DropdownMenuSeparator.vue +16 -0
- package/dist/components/ui/DropdownMenuTrigger.vue +38 -0
- package/dist/components/ui/FileInput.vue +153 -0
- package/dist/components/ui/FormError.vue +20 -0
- package/dist/components/ui/FormField.vue +12 -0
- package/dist/components/ui/FormInput.vue +46 -0
- package/dist/components/ui/FormLabel.vue +19 -0
- package/dist/components/ui/FormTextarea.vue +39 -0
- package/dist/components/ui/Input.vue +49 -0
- package/dist/components/ui/Popover.vue +36 -0
- package/dist/components/ui/PopoverContent.vue +62 -0
- package/dist/components/ui/PopoverTrigger.vue +36 -0
- package/dist/components/ui/RadioGroup.vue +42 -0
- package/dist/components/ui/RadioItem.vue +41 -0
- package/dist/components/ui/Select.vue +55 -0
- package/dist/components/ui/SelectContent.vue +29 -0
- package/dist/components/ui/SelectItem.vue +51 -0
- package/dist/components/ui/SelectTrigger.vue +38 -0
- package/dist/components/ui/SelectValue.vue +16 -0
- package/dist/components/ui/Sheet.vue +140 -0
- package/dist/components/ui/SheetDescription.vue +15 -0
- package/dist/components/ui/SheetFooter.vue +15 -0
- package/dist/components/ui/SheetHeader.vue +15 -0
- package/dist/components/ui/SheetTitle.vue +15 -0
- package/dist/components/ui/Switch.vue +43 -0
- package/dist/components/ui/Textarea.vue +50 -0
- package/dist/components/ui/Toast.vue +107 -0
- package/dist/components/ui/Toaster.vue +80 -0
- package/dist/components/ui/Tooltip.vue +42 -0
- package/dist/components/ui/TooltipContent.vue +68 -0
- package/dist/components/ui/TooltipTrigger.vue +39 -0
- package/dist/components/ui/UIProvider.vue +19 -0
- package/dist/components/ui/index.d.ts +52 -0
- package/dist/components/ui/index.mjs +52 -0
- package/dist/composables/index.d.ts +17 -0
- package/dist/composables/index.mjs +17 -0
- package/dist/composables/useButton.d.ts +8 -0
- package/dist/composables/useButton.mjs +49 -0
- package/dist/composables/useCard.d.ts +8 -0
- package/dist/composables/useCard.mjs +24 -0
- package/dist/composables/useCheckbox.d.ts +7 -0
- package/dist/composables/useCheckbox.mjs +51 -0
- package/dist/composables/useDialog.d.ts +6 -0
- package/dist/composables/useDialog.mjs +19 -0
- package/dist/composables/useDropdown.d.ts +24 -0
- package/dist/composables/useDropdown.mjs +170 -0
- package/dist/composables/useFileInput.d.ts +6 -0
- package/dist/composables/useFileInput.mjs +152 -0
- package/dist/composables/useForm.d.ts +7 -0
- package/dist/composables/useForm.mjs +159 -0
- package/dist/composables/useInput.d.ts +8 -0
- package/dist/composables/useInput.mjs +52 -0
- package/dist/composables/usePopover.d.ts +20 -0
- package/dist/composables/usePopover.mjs +113 -0
- package/dist/composables/useRadio.d.ts +7 -0
- package/dist/composables/useRadio.mjs +55 -0
- package/dist/composables/useSelect.d.ts +17 -0
- package/dist/composables/useSelect.mjs +71 -0
- package/dist/composables/useSwitch.d.ts +7 -0
- package/dist/composables/useSwitch.mjs +50 -0
- package/dist/composables/useTextarea.d.ts +7 -0
- package/dist/composables/useTextarea.mjs +50 -0
- package/dist/composables/useTheme.d.ts +15 -0
- package/dist/composables/useTheme.mjs +89 -0
- package/dist/composables/useToast.d.ts +11 -0
- package/dist/composables/useToast.mjs +64 -0
- package/dist/composables/useTooltip.d.ts +23 -0
- package/dist/composables/useTooltip.mjs +125 -0
- package/dist/composables/useUIConfig.d.ts +28 -0
- package/dist/composables/useUIConfig.mjs +36 -0
- package/dist/constants/errors.d.ts +22 -0
- package/dist/constants/errors.mjs +18 -0
- package/dist/constants/index.d.ts +2 -0
- package/dist/constants/index.mjs +2 -0
- package/dist/constants/logs.d.ts +17 -0
- package/dist/constants/logs.mjs +17 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +5 -0
- package/dist/types/alert.d.ts +15 -0
- package/dist/types/alert.mjs +0 -0
- package/dist/types/button.d.ts +20 -0
- package/dist/types/button.mjs +0 -0
- package/dist/types/card.d.ts +23 -0
- package/dist/types/card.mjs +0 -0
- package/dist/types/checkbox.d.ts +19 -0
- package/dist/types/checkbox.mjs +0 -0
- package/dist/types/config.d.ts +30 -0
- package/dist/types/config.mjs +15 -0
- package/dist/types/dialog.d.ts +29 -0
- package/dist/types/dialog.mjs +0 -0
- package/dist/types/dropdown.d.ts +27 -0
- package/dist/types/dropdown.mjs +0 -0
- package/dist/types/file-input.d.ts +35 -0
- package/dist/types/file-input.mjs +0 -0
- package/dist/types/form.d.ts +70 -0
- package/dist/types/form.mjs +0 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.mjs +20 -0
- package/dist/types/input.d.ts +27 -0
- package/dist/types/input.mjs +0 -0
- package/dist/types/popover.d.ts +15 -0
- package/dist/types/popover.mjs +0 -0
- package/dist/types/radio.d.ts +29 -0
- package/dist/types/radio.mjs +1 -0
- package/dist/types/select.d.ts +36 -0
- package/dist/types/select.mjs +1 -0
- package/dist/types/sheet.d.ts +11 -0
- package/dist/types/sheet.mjs +0 -0
- package/dist/types/switch.d.ts +17 -0
- package/dist/types/switch.mjs +0 -0
- package/dist/types/textarea.d.ts +25 -0
- package/dist/types/textarea.mjs +0 -0
- package/dist/types/theme.d.ts +43 -0
- package/dist/types/theme.mjs +42 -0
- package/dist/types/toast.d.ts +38 -0
- package/dist/types/toast.mjs +0 -0
- package/dist/types/tooltip.d.ts +25 -0
- package/dist/types/tooltip.mjs +0 -0
- package/dist/types/utils.d.ts +12 -0
- package/dist/types/utils.mjs +0 -0
- package/dist/utils/cn.d.ts +6 -0
- package/dist/utils/cn.mjs +5 -0
- package/dist/utils/deepMerge.d.ts +6 -0
- package/dist/utils/deepMerge.mjs +18 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.mjs +2 -0
- package/package.json +53 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import { cn } from "../utils/cn.mjs";
|
|
3
|
+
const FILE_ERRORS = {
|
|
4
|
+
MAX_SIZE_EXCEEDED: (max) => `\u30D5\u30A1\u30A4\u30EB\u30B5\u30A4\u30BA\u304C${formatFileSize(max)}\u3092\u8D85\u3048\u3066\u3044\u307E\u3059`,
|
|
5
|
+
MAX_FILES_EXCEEDED: (max) => `\u6700\u5927${max}\u30D5\u30A1\u30A4\u30EB\u307E\u3067\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u3067\u304D\u307E\u3059`,
|
|
6
|
+
INVALID_TYPE: "\u8A31\u53EF\u3055\u308C\u3066\u3044\u306A\u3044\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F\u3067\u3059"
|
|
7
|
+
};
|
|
8
|
+
const formatFileSize = (bytes) => {
|
|
9
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
10
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
11
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
12
|
+
};
|
|
13
|
+
const createPreviewUrl = (file) => {
|
|
14
|
+
if (file.type.startsWith("image/")) {
|
|
15
|
+
return URL.createObjectURL(file);
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
export const useFileInput = (props, input_ref) => {
|
|
20
|
+
const is_disabled = computed(() => props.value.disabled ?? false);
|
|
21
|
+
const is_dragging = ref(false);
|
|
22
|
+
const files = ref([]);
|
|
23
|
+
const error = ref(null);
|
|
24
|
+
const base_classes = computed(
|
|
25
|
+
() => cn(
|
|
26
|
+
"relative",
|
|
27
|
+
is_disabled.value && "opacity-50 cursor-not-allowed"
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
const dropzone_classes = computed(
|
|
31
|
+
() => cn(
|
|
32
|
+
"flex flex-col items-center justify-center w-full p-6",
|
|
33
|
+
"border-2 border-dashed rounded-lg",
|
|
34
|
+
"transition-colors cursor-pointer",
|
|
35
|
+
"hover:border-primary hover:bg-primary/5",
|
|
36
|
+
is_dragging.value && "border-primary bg-primary/10",
|
|
37
|
+
error.value && "border-destructive",
|
|
38
|
+
is_disabled.value && "pointer-events-none"
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
const aria_attributes = computed(() => ({
|
|
42
|
+
"aria-disabled": is_disabled.value || void 0,
|
|
43
|
+
"aria-invalid": !!error.value || void 0
|
|
44
|
+
}));
|
|
45
|
+
const validateFile = (file) => {
|
|
46
|
+
const { accept, max_size } = props.value;
|
|
47
|
+
if (max_size && file.size > max_size) {
|
|
48
|
+
return FILE_ERRORS.MAX_SIZE_EXCEEDED(max_size);
|
|
49
|
+
}
|
|
50
|
+
if (accept) {
|
|
51
|
+
const accepted_types = accept.split(",").map((t) => t.trim());
|
|
52
|
+
const is_valid = accepted_types.some((type) => {
|
|
53
|
+
if (type.startsWith(".")) {
|
|
54
|
+
return file.name.toLowerCase().endsWith(type.toLowerCase());
|
|
55
|
+
}
|
|
56
|
+
if (type.endsWith("/*")) {
|
|
57
|
+
return file.type.startsWith(type.replace("/*", "/"));
|
|
58
|
+
}
|
|
59
|
+
return file.type === type;
|
|
60
|
+
});
|
|
61
|
+
if (!is_valid) {
|
|
62
|
+
return FILE_ERRORS.INVALID_TYPE;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
const handleFiles = (file_list) => {
|
|
68
|
+
if (!file_list || is_disabled.value) return;
|
|
69
|
+
error.value = null;
|
|
70
|
+
const { multiple, max_files } = props.value;
|
|
71
|
+
const new_files = [];
|
|
72
|
+
const files_to_process = Array.from(file_list);
|
|
73
|
+
for (const file of files_to_process) {
|
|
74
|
+
const validation_error = validateFile(file);
|
|
75
|
+
if (validation_error) {
|
|
76
|
+
error.value = validation_error;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
new_files.push({
|
|
80
|
+
file,
|
|
81
|
+
name: file.name,
|
|
82
|
+
size: file.size,
|
|
83
|
+
type: file.type,
|
|
84
|
+
preview_url: createPreviewUrl(file)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (multiple) {
|
|
88
|
+
const total = files.value.length + new_files.length;
|
|
89
|
+
if (max_files && total > max_files) {
|
|
90
|
+
error.value = FILE_ERRORS.MAX_FILES_EXCEEDED(max_files);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
files.value.push(...new_files);
|
|
94
|
+
} else {
|
|
95
|
+
files.value.forEach((f) => {
|
|
96
|
+
if (f.preview_url) URL.revokeObjectURL(f.preview_url);
|
|
97
|
+
});
|
|
98
|
+
files.value = new_files.slice(0, 1);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const handleDragEnter = (event) => {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
if (!is_disabled.value) {
|
|
104
|
+
is_dragging.value = true;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const handleDragLeave = (event) => {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
is_dragging.value = false;
|
|
110
|
+
};
|
|
111
|
+
const handleDrop = (event) => {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
is_dragging.value = false;
|
|
114
|
+
if (!is_disabled.value) {
|
|
115
|
+
handleFiles(event.dataTransfer?.files ?? null);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const removeFile = (index) => {
|
|
119
|
+
const removed = files.value.splice(index, 1)[0];
|
|
120
|
+
if (removed?.preview_url) {
|
|
121
|
+
URL.revokeObjectURL(removed.preview_url);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const clearFiles = () => {
|
|
125
|
+
files.value.forEach((f) => {
|
|
126
|
+
if (f.preview_url) URL.revokeObjectURL(f.preview_url);
|
|
127
|
+
});
|
|
128
|
+
files.value = [];
|
|
129
|
+
error.value = null;
|
|
130
|
+
};
|
|
131
|
+
const openFilePicker = () => {
|
|
132
|
+
if (!is_disabled.value && input_ref.value) {
|
|
133
|
+
input_ref.value.click();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
base_classes,
|
|
138
|
+
dropzone_classes,
|
|
139
|
+
is_disabled,
|
|
140
|
+
is_dragging,
|
|
141
|
+
files,
|
|
142
|
+
error,
|
|
143
|
+
aria_attributes,
|
|
144
|
+
handleFiles,
|
|
145
|
+
handleDragEnter,
|
|
146
|
+
handleDragLeave,
|
|
147
|
+
handleDrop,
|
|
148
|
+
removeFile,
|
|
149
|
+
clearFiles,
|
|
150
|
+
openFilePicker
|
|
151
|
+
};
|
|
152
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { reactive, computed, readonly } from "vue";
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
import { FORM_ERRORS } from "../constants/errors.mjs";
|
|
4
|
+
export const useForm = (schema) => {
|
|
5
|
+
const form_state = reactive({
|
|
6
|
+
values: {},
|
|
7
|
+
errors: {},
|
|
8
|
+
touched: {},
|
|
9
|
+
is_submitting: false,
|
|
10
|
+
is_dirty: false
|
|
11
|
+
});
|
|
12
|
+
const is_valid = computed(() => {
|
|
13
|
+
const result = schema.safeParse(form_state.values);
|
|
14
|
+
return result.success;
|
|
15
|
+
});
|
|
16
|
+
const validateField = (field) => {
|
|
17
|
+
form_state.touched[field] = true;
|
|
18
|
+
form_state.is_dirty = true;
|
|
19
|
+
try {
|
|
20
|
+
const zod_object = schema;
|
|
21
|
+
const field_schema = zod_object.shape[field];
|
|
22
|
+
if (!field_schema) {
|
|
23
|
+
throw new Error(FORM_ERRORS.FIELD_NOT_FOUND);
|
|
24
|
+
}
|
|
25
|
+
field_schema.parse(form_state.values[field]);
|
|
26
|
+
form_state.errors[field] = void 0;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof ZodError) {
|
|
29
|
+
form_state.errors[field] = error.errors[0].message;
|
|
30
|
+
} else if (error instanceof Error) {
|
|
31
|
+
form_state.errors[field] = error.message;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const validateAll = () => {
|
|
36
|
+
try {
|
|
37
|
+
schema.parse(form_state.values);
|
|
38
|
+
form_state.errors = {};
|
|
39
|
+
return true;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof ZodError) {
|
|
42
|
+
form_state.errors = {};
|
|
43
|
+
error.errors.forEach((err) => {
|
|
44
|
+
const field = err.path[0];
|
|
45
|
+
form_state.errors[field] = err.message;
|
|
46
|
+
form_state.touched[field] = true;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const reset = () => {
|
|
53
|
+
form_state.values = {};
|
|
54
|
+
form_state.errors = {};
|
|
55
|
+
form_state.touched = {};
|
|
56
|
+
form_state.is_dirty = false;
|
|
57
|
+
};
|
|
58
|
+
const setValues = (values) => {
|
|
59
|
+
Object.assign(form_state.values, values);
|
|
60
|
+
form_state.is_dirty = true;
|
|
61
|
+
};
|
|
62
|
+
const setFieldValue = (field, value) => {
|
|
63
|
+
form_state.values[field] = value;
|
|
64
|
+
form_state.is_dirty = true;
|
|
65
|
+
};
|
|
66
|
+
const submit = async (on_submit) => {
|
|
67
|
+
if (!validateAll()) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
form_state.is_submitting = true;
|
|
71
|
+
try {
|
|
72
|
+
const validated_data = schema.parse(form_state.values);
|
|
73
|
+
await on_submit(validated_data);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof ZodError) {
|
|
76
|
+
error.errors.forEach((err) => {
|
|
77
|
+
const field = err.path[0];
|
|
78
|
+
form_state.errors[field] = err.message;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
} finally {
|
|
83
|
+
form_state.is_submitting = false;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const getFieldArray = (field) => {
|
|
87
|
+
const getArray = () => {
|
|
88
|
+
const value = form_state.values[field];
|
|
89
|
+
if (!Array.isArray(value)) {
|
|
90
|
+
form_state.values[field] = [];
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
};
|
|
95
|
+
const helpers = {
|
|
96
|
+
get fields() {
|
|
97
|
+
return getArray();
|
|
98
|
+
},
|
|
99
|
+
append: (value) => {
|
|
100
|
+
const array = getArray();
|
|
101
|
+
array.push(value);
|
|
102
|
+
form_state.is_dirty = true;
|
|
103
|
+
},
|
|
104
|
+
prepend: (value) => {
|
|
105
|
+
const array = getArray();
|
|
106
|
+
array.unshift(value);
|
|
107
|
+
form_state.is_dirty = true;
|
|
108
|
+
},
|
|
109
|
+
insert: (index, value) => {
|
|
110
|
+
const array = getArray();
|
|
111
|
+
array.splice(index, 0, value);
|
|
112
|
+
form_state.is_dirty = true;
|
|
113
|
+
},
|
|
114
|
+
remove: (index) => {
|
|
115
|
+
const array = getArray();
|
|
116
|
+
array.splice(index, 1);
|
|
117
|
+
form_state.is_dirty = true;
|
|
118
|
+
},
|
|
119
|
+
move: (from_index, to_index) => {
|
|
120
|
+
const array = getArray();
|
|
121
|
+
const item = array.splice(from_index, 1)[0];
|
|
122
|
+
array.splice(to_index, 0, item);
|
|
123
|
+
form_state.is_dirty = true;
|
|
124
|
+
},
|
|
125
|
+
swap: (index_a, index_b) => {
|
|
126
|
+
const array = getArray();
|
|
127
|
+
const temp = array[index_a];
|
|
128
|
+
array[index_a] = array[index_b];
|
|
129
|
+
array[index_b] = temp;
|
|
130
|
+
form_state.is_dirty = true;
|
|
131
|
+
},
|
|
132
|
+
replace: (index, value) => {
|
|
133
|
+
const array = getArray();
|
|
134
|
+
array[index] = value;
|
|
135
|
+
form_state.is_dirty = true;
|
|
136
|
+
},
|
|
137
|
+
clear: () => {
|
|
138
|
+
form_state.values[field] = [];
|
|
139
|
+
form_state.is_dirty = true;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
return helpers;
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
values: form_state.values,
|
|
146
|
+
errors: readonly(form_state.errors),
|
|
147
|
+
touched: readonly(form_state.touched),
|
|
148
|
+
is_valid,
|
|
149
|
+
is_submitting: readonly(computed(() => form_state.is_submitting)),
|
|
150
|
+
is_dirty: readonly(computed(() => form_state.is_dirty)),
|
|
151
|
+
validateField,
|
|
152
|
+
validateAll,
|
|
153
|
+
reset,
|
|
154
|
+
setValues,
|
|
155
|
+
setFieldValue,
|
|
156
|
+
submit,
|
|
157
|
+
getFieldArray
|
|
158
|
+
};
|
|
159
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Ref } from "vue";
|
|
2
|
+
import type { InputProps, InputReturn } from "../types/input";
|
|
3
|
+
/**
|
|
4
|
+
* Inputコンポーネントのロジックを提供するComposable
|
|
5
|
+
* size, error, disabled, readonlyに基づいてクラスとaria属性を生成
|
|
6
|
+
* Providerから設定されたデフォルト値を使用
|
|
7
|
+
*/
|
|
8
|
+
export declare const useInput: (props: Ref<InputProps>) => InputReturn;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import { cn } from "../utils/cn.mjs";
|
|
3
|
+
import { useUI } from "./useUIConfig.mjs";
|
|
4
|
+
export const useInput = (props) => {
|
|
5
|
+
const ui_config = useUI("input");
|
|
6
|
+
const is_focused = ref(false);
|
|
7
|
+
const has_error = computed(() => !!props.value.error);
|
|
8
|
+
const is_disabled = computed(() => props.value.disabled ?? false);
|
|
9
|
+
const is_readonly = computed(() => props.value.readonly ?? false);
|
|
10
|
+
const size_classes = computed(() => {
|
|
11
|
+
const size_map = {
|
|
12
|
+
sm: "h-9 text-sm",
|
|
13
|
+
md: "h-10",
|
|
14
|
+
lg: "h-11 text-lg"
|
|
15
|
+
};
|
|
16
|
+
return size_map[props.value.size ?? ui_config.default_size];
|
|
17
|
+
});
|
|
18
|
+
const base_classes = computed(
|
|
19
|
+
() => cn(
|
|
20
|
+
"flex w-full rounded-md border border-input bg-background px-3 py-2",
|
|
21
|
+
"text-sm ring-offset-background",
|
|
22
|
+
"file:border-0 file:bg-transparent file:text-sm file:font-medium",
|
|
23
|
+
"placeholder:text-muted-foreground",
|
|
24
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
25
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
26
|
+
size_classes.value,
|
|
27
|
+
has_error.value && "border-destructive focus-visible:ring-destructive",
|
|
28
|
+
is_readonly.value && "bg-muted"
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
const aria_attributes = computed(() => ({
|
|
32
|
+
"aria-invalid": has_error.value || void 0,
|
|
33
|
+
"aria-describedby": props.value.error_id,
|
|
34
|
+
"aria-readonly": is_readonly.value || void 0
|
|
35
|
+
}));
|
|
36
|
+
const handleFocus = () => {
|
|
37
|
+
is_focused.value = true;
|
|
38
|
+
};
|
|
39
|
+
const handleBlur = () => {
|
|
40
|
+
is_focused.value = false;
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
base_classes,
|
|
44
|
+
is_focused,
|
|
45
|
+
has_error,
|
|
46
|
+
is_disabled,
|
|
47
|
+
is_readonly,
|
|
48
|
+
aria_attributes,
|
|
49
|
+
handleFocus,
|
|
50
|
+
handleBlur
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Ref, type ComputedRef } from "vue";
|
|
2
|
+
import type { PopoverSide, PopoverAlign } from "../types/popover";
|
|
3
|
+
export interface UsePopoverProps {
|
|
4
|
+
side?: PopoverSide;
|
|
5
|
+
align?: PopoverAlign;
|
|
6
|
+
modal?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface UsePopoverReturn {
|
|
9
|
+
is_open: Ref<boolean>;
|
|
10
|
+
trigger_ref: Ref<HTMLElement | null>;
|
|
11
|
+
content_ref: Ref<HTMLElement | null>;
|
|
12
|
+
popover_id: string;
|
|
13
|
+
position_styles: ComputedRef<Record<string, string>>;
|
|
14
|
+
open: () => void;
|
|
15
|
+
close: () => void;
|
|
16
|
+
toggle: () => void;
|
|
17
|
+
handleTriggerClick: () => void;
|
|
18
|
+
handleKeyDown: (event: KeyboardEvent) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function usePopover(props: Ref<UsePopoverProps>): UsePopoverReturn;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ref, computed, onMounted, onUnmounted } from "vue";
|
|
2
|
+
let popover_counter = 0;
|
|
3
|
+
export function usePopover(props) {
|
|
4
|
+
const is_open = ref(false);
|
|
5
|
+
const trigger_ref = ref(null);
|
|
6
|
+
const content_ref = ref(null);
|
|
7
|
+
const popover_id = `popover-${++popover_counter}`;
|
|
8
|
+
const open = () => {
|
|
9
|
+
is_open.value = true;
|
|
10
|
+
};
|
|
11
|
+
const close = () => {
|
|
12
|
+
is_open.value = false;
|
|
13
|
+
};
|
|
14
|
+
const toggle = () => {
|
|
15
|
+
is_open.value = !is_open.value;
|
|
16
|
+
};
|
|
17
|
+
const handleTriggerClick = () => {
|
|
18
|
+
toggle();
|
|
19
|
+
};
|
|
20
|
+
const handleKeyDown = (event) => {
|
|
21
|
+
if (event.key === "Escape" && is_open.value) {
|
|
22
|
+
close();
|
|
23
|
+
trigger_ref.value?.focus();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const handleClickOutside = (event) => {
|
|
27
|
+
if (!is_open.value) return;
|
|
28
|
+
const target = event.target;
|
|
29
|
+
const trigger = trigger_ref.value;
|
|
30
|
+
const content = content_ref.value;
|
|
31
|
+
if (trigger && trigger.contains(target)) return;
|
|
32
|
+
if (content && content.contains(target)) return;
|
|
33
|
+
close();
|
|
34
|
+
};
|
|
35
|
+
const position_styles = computed(() => {
|
|
36
|
+
if (!trigger_ref.value || !is_open.value) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
const side = props.value.side ?? "bottom";
|
|
40
|
+
const align = props.value.align ?? "center";
|
|
41
|
+
const offset = 8;
|
|
42
|
+
const styles = {
|
|
43
|
+
position: "absolute",
|
|
44
|
+
zIndex: "50"
|
|
45
|
+
};
|
|
46
|
+
switch (side) {
|
|
47
|
+
case "top":
|
|
48
|
+
styles.bottom = "100%";
|
|
49
|
+
styles.marginBottom = `${offset}px`;
|
|
50
|
+
break;
|
|
51
|
+
case "bottom":
|
|
52
|
+
styles.top = "100%";
|
|
53
|
+
styles.marginTop = `${offset}px`;
|
|
54
|
+
break;
|
|
55
|
+
case "left":
|
|
56
|
+
styles.right = "100%";
|
|
57
|
+
styles.marginRight = `${offset}px`;
|
|
58
|
+
break;
|
|
59
|
+
case "right":
|
|
60
|
+
styles.left = "100%";
|
|
61
|
+
styles.marginLeft = `${offset}px`;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (side === "top" || side === "bottom") {
|
|
65
|
+
switch (align) {
|
|
66
|
+
case "start":
|
|
67
|
+
styles.left = "0";
|
|
68
|
+
break;
|
|
69
|
+
case "center":
|
|
70
|
+
styles.left = "50%";
|
|
71
|
+
styles.transform = "translateX(-50%)";
|
|
72
|
+
break;
|
|
73
|
+
case "end":
|
|
74
|
+
styles.right = "0";
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
switch (align) {
|
|
79
|
+
case "start":
|
|
80
|
+
styles.top = "0";
|
|
81
|
+
break;
|
|
82
|
+
case "center":
|
|
83
|
+
styles.top = "50%";
|
|
84
|
+
styles.transform = "translateY(-50%)";
|
|
85
|
+
break;
|
|
86
|
+
case "end":
|
|
87
|
+
styles.bottom = "0";
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return styles;
|
|
92
|
+
});
|
|
93
|
+
onMounted(() => {
|
|
94
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
95
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
96
|
+
});
|
|
97
|
+
onUnmounted(() => {
|
|
98
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
99
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
is_open,
|
|
103
|
+
trigger_ref,
|
|
104
|
+
content_ref,
|
|
105
|
+
popover_id,
|
|
106
|
+
position_styles,
|
|
107
|
+
open,
|
|
108
|
+
close,
|
|
109
|
+
toggle,
|
|
110
|
+
handleTriggerClick,
|
|
111
|
+
handleKeyDown
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Ref } from "vue";
|
|
2
|
+
import type { RadioItemProps, RadioItemReturn } from "../types/radio";
|
|
3
|
+
/**
|
|
4
|
+
* RadioItemコンポーネントのロジックを提供するComposable
|
|
5
|
+
* size, disabled, valueに基づいてクラスとaria属性を生成
|
|
6
|
+
*/
|
|
7
|
+
export declare const useRadioItem: (props: Ref<RadioItemProps>) => RadioItemReturn;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { computed, inject } from "vue";
|
|
2
|
+
import { cn } from "../utils/cn.mjs";
|
|
3
|
+
import { RADIO_GROUP_KEY } from "../types/radio.mjs";
|
|
4
|
+
import { COMPONENT_ERRORS } from "../constants/errors.mjs";
|
|
5
|
+
export const useRadioItem = (props) => {
|
|
6
|
+
const context = inject(RADIO_GROUP_KEY, null);
|
|
7
|
+
if (!context) {
|
|
8
|
+
throw new Error(COMPONENT_ERRORS.PROVIDER_NOT_FOUND);
|
|
9
|
+
}
|
|
10
|
+
const is_disabled = computed(() => props.value.disabled ?? context.disabled.value);
|
|
11
|
+
const is_checked = computed(() => context.model_value.value === props.value.value);
|
|
12
|
+
const size_classes = computed(() => {
|
|
13
|
+
const size_map = {
|
|
14
|
+
sm: "h-4 w-4",
|
|
15
|
+
md: "h-5 w-5",
|
|
16
|
+
lg: "h-6 w-6"
|
|
17
|
+
};
|
|
18
|
+
return size_map[props.value.size ?? "md"];
|
|
19
|
+
});
|
|
20
|
+
const base_classes = computed(
|
|
21
|
+
() => cn(
|
|
22
|
+
"aspect-square rounded-full border border-primary text-primary",
|
|
23
|
+
"ring-offset-background",
|
|
24
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
25
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
26
|
+
size_classes.value
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
const indicator_size_classes = computed(() => {
|
|
30
|
+
const size_map = {
|
|
31
|
+
sm: "h-2 w-2",
|
|
32
|
+
md: "h-2.5 w-2.5",
|
|
33
|
+
lg: "h-3 w-3"
|
|
34
|
+
};
|
|
35
|
+
return size_map[props.value.size ?? "md"];
|
|
36
|
+
});
|
|
37
|
+
const indicator_classes = computed(
|
|
38
|
+
() => cn(
|
|
39
|
+
"flex items-center justify-center",
|
|
40
|
+
indicator_size_classes.value
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
const aria_attributes = computed(() => ({
|
|
44
|
+
role: "radio",
|
|
45
|
+
"aria-checked": is_checked.value,
|
|
46
|
+
"aria-disabled": is_disabled.value || void 0
|
|
47
|
+
}));
|
|
48
|
+
return {
|
|
49
|
+
base_classes,
|
|
50
|
+
indicator_classes,
|
|
51
|
+
is_checked,
|
|
52
|
+
is_disabled,
|
|
53
|
+
aria_attributes
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Ref } from "vue";
|
|
2
|
+
import type { SelectTriggerReturn, SelectContentReturn, SelectItemReturn } from "../types/select";
|
|
3
|
+
/**
|
|
4
|
+
* SelectTriggerコンポーネントのロジックを提供するComposable
|
|
5
|
+
*/
|
|
6
|
+
export declare const useSelectTrigger: () => SelectTriggerReturn;
|
|
7
|
+
/**
|
|
8
|
+
* SelectContentコンポーネントのロジックを提供するComposable
|
|
9
|
+
*/
|
|
10
|
+
export declare const useSelectContent: () => SelectContentReturn;
|
|
11
|
+
/**
|
|
12
|
+
* SelectItemコンポーネントのロジックを提供するComposable
|
|
13
|
+
*/
|
|
14
|
+
export declare const useSelectItem: (props: Ref<{
|
|
15
|
+
value: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}>) => SelectItemReturn;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { computed, inject } from "vue";
|
|
2
|
+
import { cn } from "../utils/cn.mjs";
|
|
3
|
+
import { SELECT_KEY } from "../types/select.mjs";
|
|
4
|
+
import { COMPONENT_ERRORS } from "../constants/errors.mjs";
|
|
5
|
+
export const useSelectTrigger = () => {
|
|
6
|
+
const context = inject(SELECT_KEY, null);
|
|
7
|
+
if (!context) {
|
|
8
|
+
throw new Error(COMPONENT_ERRORS.PROVIDER_NOT_FOUND);
|
|
9
|
+
}
|
|
10
|
+
const is_disabled = computed(() => context.disabled.value);
|
|
11
|
+
const size_classes = computed(() => {
|
|
12
|
+
const size_map = {
|
|
13
|
+
sm: "h-9 text-sm",
|
|
14
|
+
md: "h-10",
|
|
15
|
+
lg: "h-11 text-lg"
|
|
16
|
+
};
|
|
17
|
+
return size_map[context.size.value];
|
|
18
|
+
});
|
|
19
|
+
const base_classes = computed(
|
|
20
|
+
() => cn(
|
|
21
|
+
"flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2",
|
|
22
|
+
"text-sm ring-offset-background",
|
|
23
|
+
"placeholder:text-muted-foreground",
|
|
24
|
+
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
25
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
26
|
+
"[&>span]:line-clamp-1",
|
|
27
|
+
size_classes.value
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
return {
|
|
31
|
+
base_classes,
|
|
32
|
+
is_disabled
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export const useSelectContent = () => {
|
|
36
|
+
const base_classes = computed(
|
|
37
|
+
() => cn(
|
|
38
|
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md",
|
|
39
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
40
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
41
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
42
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
|
43
|
+
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
return {
|
|
47
|
+
base_classes
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export const useSelectItem = (props) => {
|
|
51
|
+
const context = inject(SELECT_KEY, null);
|
|
52
|
+
if (!context) {
|
|
53
|
+
throw new Error(COMPONENT_ERRORS.PROVIDER_NOT_FOUND);
|
|
54
|
+
}
|
|
55
|
+
const is_selected = computed(() => context.model_value.value === props.value.value);
|
|
56
|
+
const is_disabled = computed(() => props.value.disabled ?? false);
|
|
57
|
+
const base_classes = computed(
|
|
58
|
+
() => cn(
|
|
59
|
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2",
|
|
60
|
+
"text-sm outline-none",
|
|
61
|
+
"focus:bg-accent focus:text-accent-foreground",
|
|
62
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
63
|
+
is_selected.value && "bg-accent text-accent-foreground"
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
base_classes,
|
|
68
|
+
is_selected,
|
|
69
|
+
is_disabled
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Ref } from "vue";
|
|
2
|
+
import type { SwitchProps, SwitchReturn } from "../types/switch";
|
|
3
|
+
/**
|
|
4
|
+
* Switchコンポーネントのロジックを提供するComposable
|
|
5
|
+
* size, disabledに基づいてクラスとaria属性を生成
|
|
6
|
+
*/
|
|
7
|
+
export declare const useSwitch: (props: Ref<SwitchProps>, checked: Ref<boolean>) => SwitchReturn;
|