@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.
Files changed (149) hide show
  1. package/dist/build.config.d.ts +2 -0
  2. package/dist/build.config.mjs +14 -0
  3. package/dist/components/ui/Alert.vue +39 -0
  4. package/dist/components/ui/AlertDescription.vue +12 -0
  5. package/dist/components/ui/AlertTitle.vue +12 -0
  6. package/dist/components/ui/Button.vue +59 -0
  7. package/dist/components/ui/Card.vue +15 -0
  8. package/dist/components/ui/CardContent.vue +12 -0
  9. package/dist/components/ui/CardDescription.vue +12 -0
  10. package/dist/components/ui/CardFooter.vue +12 -0
  11. package/dist/components/ui/CardHeader.vue +12 -0
  12. package/dist/components/ui/CardTitle.vue +12 -0
  13. package/dist/components/ui/Checkbox.vue +73 -0
  14. package/dist/components/ui/Dialog.vue +93 -0
  15. package/dist/components/ui/DialogDescription.vue +12 -0
  16. package/dist/components/ui/DialogFooter.vue +12 -0
  17. package/dist/components/ui/DialogHeader.vue +12 -0
  18. package/dist/components/ui/DialogTitle.vue +12 -0
  19. package/dist/components/ui/DropdownMenu.vue +33 -0
  20. package/dist/components/ui/DropdownMenuContent.vue +66 -0
  21. package/dist/components/ui/DropdownMenuItem.vue +77 -0
  22. package/dist/components/ui/DropdownMenuLabel.vue +20 -0
  23. package/dist/components/ui/DropdownMenuSeparator.vue +16 -0
  24. package/dist/components/ui/DropdownMenuTrigger.vue +38 -0
  25. package/dist/components/ui/FileInput.vue +153 -0
  26. package/dist/components/ui/FormError.vue +20 -0
  27. package/dist/components/ui/FormField.vue +12 -0
  28. package/dist/components/ui/FormInput.vue +46 -0
  29. package/dist/components/ui/FormLabel.vue +19 -0
  30. package/dist/components/ui/FormTextarea.vue +39 -0
  31. package/dist/components/ui/Input.vue +49 -0
  32. package/dist/components/ui/Popover.vue +36 -0
  33. package/dist/components/ui/PopoverContent.vue +62 -0
  34. package/dist/components/ui/PopoverTrigger.vue +36 -0
  35. package/dist/components/ui/RadioGroup.vue +42 -0
  36. package/dist/components/ui/RadioItem.vue +41 -0
  37. package/dist/components/ui/Select.vue +55 -0
  38. package/dist/components/ui/SelectContent.vue +29 -0
  39. package/dist/components/ui/SelectItem.vue +51 -0
  40. package/dist/components/ui/SelectTrigger.vue +38 -0
  41. package/dist/components/ui/SelectValue.vue +16 -0
  42. package/dist/components/ui/Sheet.vue +140 -0
  43. package/dist/components/ui/SheetDescription.vue +15 -0
  44. package/dist/components/ui/SheetFooter.vue +15 -0
  45. package/dist/components/ui/SheetHeader.vue +15 -0
  46. package/dist/components/ui/SheetTitle.vue +15 -0
  47. package/dist/components/ui/Switch.vue +43 -0
  48. package/dist/components/ui/Textarea.vue +50 -0
  49. package/dist/components/ui/Toast.vue +107 -0
  50. package/dist/components/ui/Toaster.vue +80 -0
  51. package/dist/components/ui/Tooltip.vue +42 -0
  52. package/dist/components/ui/TooltipContent.vue +68 -0
  53. package/dist/components/ui/TooltipTrigger.vue +39 -0
  54. package/dist/components/ui/UIProvider.vue +19 -0
  55. package/dist/components/ui/index.d.ts +52 -0
  56. package/dist/components/ui/index.mjs +52 -0
  57. package/dist/composables/index.d.ts +17 -0
  58. package/dist/composables/index.mjs +17 -0
  59. package/dist/composables/useButton.d.ts +8 -0
  60. package/dist/composables/useButton.mjs +49 -0
  61. package/dist/composables/useCard.d.ts +8 -0
  62. package/dist/composables/useCard.mjs +24 -0
  63. package/dist/composables/useCheckbox.d.ts +7 -0
  64. package/dist/composables/useCheckbox.mjs +51 -0
  65. package/dist/composables/useDialog.d.ts +6 -0
  66. package/dist/composables/useDialog.mjs +19 -0
  67. package/dist/composables/useDropdown.d.ts +24 -0
  68. package/dist/composables/useDropdown.mjs +170 -0
  69. package/dist/composables/useFileInput.d.ts +6 -0
  70. package/dist/composables/useFileInput.mjs +152 -0
  71. package/dist/composables/useForm.d.ts +7 -0
  72. package/dist/composables/useForm.mjs +159 -0
  73. package/dist/composables/useInput.d.ts +8 -0
  74. package/dist/composables/useInput.mjs +52 -0
  75. package/dist/composables/usePopover.d.ts +20 -0
  76. package/dist/composables/usePopover.mjs +113 -0
  77. package/dist/composables/useRadio.d.ts +7 -0
  78. package/dist/composables/useRadio.mjs +55 -0
  79. package/dist/composables/useSelect.d.ts +17 -0
  80. package/dist/composables/useSelect.mjs +71 -0
  81. package/dist/composables/useSwitch.d.ts +7 -0
  82. package/dist/composables/useSwitch.mjs +50 -0
  83. package/dist/composables/useTextarea.d.ts +7 -0
  84. package/dist/composables/useTextarea.mjs +50 -0
  85. package/dist/composables/useTheme.d.ts +15 -0
  86. package/dist/composables/useTheme.mjs +89 -0
  87. package/dist/composables/useToast.d.ts +11 -0
  88. package/dist/composables/useToast.mjs +64 -0
  89. package/dist/composables/useTooltip.d.ts +23 -0
  90. package/dist/composables/useTooltip.mjs +125 -0
  91. package/dist/composables/useUIConfig.d.ts +28 -0
  92. package/dist/composables/useUIConfig.mjs +36 -0
  93. package/dist/constants/errors.d.ts +22 -0
  94. package/dist/constants/errors.mjs +18 -0
  95. package/dist/constants/index.d.ts +2 -0
  96. package/dist/constants/index.mjs +2 -0
  97. package/dist/constants/logs.d.ts +17 -0
  98. package/dist/constants/logs.mjs +17 -0
  99. package/dist/index.d.ts +5 -0
  100. package/dist/index.mjs +5 -0
  101. package/dist/types/alert.d.ts +15 -0
  102. package/dist/types/alert.mjs +0 -0
  103. package/dist/types/button.d.ts +20 -0
  104. package/dist/types/button.mjs +0 -0
  105. package/dist/types/card.d.ts +23 -0
  106. package/dist/types/card.mjs +0 -0
  107. package/dist/types/checkbox.d.ts +19 -0
  108. package/dist/types/checkbox.mjs +0 -0
  109. package/dist/types/config.d.ts +30 -0
  110. package/dist/types/config.mjs +15 -0
  111. package/dist/types/dialog.d.ts +29 -0
  112. package/dist/types/dialog.mjs +0 -0
  113. package/dist/types/dropdown.d.ts +27 -0
  114. package/dist/types/dropdown.mjs +0 -0
  115. package/dist/types/file-input.d.ts +35 -0
  116. package/dist/types/file-input.mjs +0 -0
  117. package/dist/types/form.d.ts +70 -0
  118. package/dist/types/form.mjs +0 -0
  119. package/dist/types/index.d.ts +20 -0
  120. package/dist/types/index.mjs +20 -0
  121. package/dist/types/input.d.ts +27 -0
  122. package/dist/types/input.mjs +0 -0
  123. package/dist/types/popover.d.ts +15 -0
  124. package/dist/types/popover.mjs +0 -0
  125. package/dist/types/radio.d.ts +29 -0
  126. package/dist/types/radio.mjs +1 -0
  127. package/dist/types/select.d.ts +36 -0
  128. package/dist/types/select.mjs +1 -0
  129. package/dist/types/sheet.d.ts +11 -0
  130. package/dist/types/sheet.mjs +0 -0
  131. package/dist/types/switch.d.ts +17 -0
  132. package/dist/types/switch.mjs +0 -0
  133. package/dist/types/textarea.d.ts +25 -0
  134. package/dist/types/textarea.mjs +0 -0
  135. package/dist/types/theme.d.ts +43 -0
  136. package/dist/types/theme.mjs +42 -0
  137. package/dist/types/toast.d.ts +38 -0
  138. package/dist/types/toast.mjs +0 -0
  139. package/dist/types/tooltip.d.ts +25 -0
  140. package/dist/types/tooltip.mjs +0 -0
  141. package/dist/types/utils.d.ts +12 -0
  142. package/dist/types/utils.mjs +0 -0
  143. package/dist/utils/cn.d.ts +6 -0
  144. package/dist/utils/cn.mjs +5 -0
  145. package/dist/utils/deepMerge.d.ts +6 -0
  146. package/dist/utils/deepMerge.mjs +18 -0
  147. package/dist/utils/index.d.ts +2 -0
  148. package/dist/utils/index.mjs +2 -0
  149. 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,7 @@
1
+ import { z, type ZodSchema } from "zod";
2
+ import type { FormReturn } from "../types/form";
3
+ /**
4
+ * フォーム状態管理とZodバリデーションを提供するComposable
5
+ * スキーマに基づいた型安全なフォーム管理を実現
6
+ */
7
+ export declare const useForm: <T extends ZodSchema>(schema: T) => FormReturn<z.infer<T>>;
@@ -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;