@veristone/nuxt-v-app 0.1.1 → 0.2.1

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.
@@ -0,0 +1,149 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * VACrudCreate - Veristone CRUD Create Modal
4
+ *
5
+ * Provides a reusable modal for creating new records via API.
6
+ * Handles form submission, loading states, and error handling.
7
+ *
8
+ * @example
9
+ * <VACrudCreate
10
+ * endpoint="/api/loans"
11
+ * friendly-name="Loan"
12
+ * @created="onLoanCreated"
13
+ * >
14
+ * <template #default="{ submit, loading, error }">
15
+ * <form @submit.prevent="submit({ name: 'New Loan' })">
16
+ * <UInput v-model="formData.name" placeholder="Loan name" />
17
+ * <UButton type="submit" :loading="loading">Create</UButton>
18
+ * </form>
19
+ * </template>
20
+ * </VACrudCreate>
21
+ */
22
+
23
+ interface Props {
24
+ endpoint: string;
25
+ friendlyName: string;
26
+ triggerLabel?: string;
27
+ triggerIcon?: string;
28
+ triggerColor?:
29
+ | "error"
30
+ | "primary"
31
+ | "secondary"
32
+ | "success"
33
+ | "info"
34
+ | "warning"
35
+ | "neutral";
36
+ triggerVariant?: "link" | "solid" | "outline" | "soft" | "subtle" | "ghost";
37
+ modalTitle?: string;
38
+ modalSize?: "sm" | "md" | "lg" | "xl";
39
+ initialData?: Record<string, any>;
40
+ open?: boolean;
41
+ }
42
+
43
+ interface Emits {
44
+ (e: "created", data: any): void;
45
+ (e: "error", error: any): void;
46
+ (e: "opened"): void;
47
+ (e: "closed"): void;
48
+ (e: "update:open", value: boolean): void;
49
+ }
50
+
51
+ const props = withDefaults(defineProps<Props>(), {
52
+ triggerLabel: undefined,
53
+ triggerIcon: "i-lucide-plus",
54
+ triggerColor: "primary",
55
+ triggerVariant: "solid",
56
+ modalTitle: undefined,
57
+ modalSize: "md",
58
+ initialData: () => ({}),
59
+ open: undefined,
60
+ });
61
+
62
+ const emit = defineEmits<Emits>();
63
+
64
+ const { create, loading, errorState } = useVCrud(props.endpoint);
65
+ const internalOpen = ref(false);
66
+
67
+ // Hybrid internal/external state via computed getter/setter
68
+ const isOpen = computed({
69
+ get: () => props.open ?? internalOpen.value,
70
+ set: (val) => {
71
+ emit("update:open", val);
72
+ internalOpen.value = val;
73
+ },
74
+ });
75
+
76
+ // Watch for open state changes to emit lifecycle events
77
+ watch(isOpen, (val) => {
78
+ if (val) {
79
+ emit("opened");
80
+ } else {
81
+ emit("closed");
82
+ }
83
+ });
84
+
85
+ // Computed defaults for labels
86
+ const computedTriggerLabel = computed(() => {
87
+ return props.triggerLabel ?? `Create ${props.friendlyName}`;
88
+ });
89
+
90
+ const computedModalTitle = computed(() => {
91
+ return props.modalTitle ?? `Create ${props.friendlyName}`;
92
+ });
93
+
94
+ // Submit handler
95
+ const handleSubmit = async (data: Record<string, any>) => {
96
+ try {
97
+ const result = await create(data);
98
+ emit("created", result);
99
+ isOpen.value = false;
100
+ } catch (err) {
101
+ emit("error", err);
102
+ }
103
+ };
104
+ </script>
105
+
106
+ <template>
107
+ <UModal v-model:open="isOpen">
108
+ <UButton
109
+ :label="computedTriggerLabel"
110
+ :icon="triggerIcon"
111
+ :color="triggerColor"
112
+ :variant="triggerVariant"
113
+ />
114
+
115
+ <template #header>
116
+ <div class="flex items-center justify-between w-full">
117
+ <h3 class="text-base font-semibold text-gray-900 dark:text-white">
118
+ {{ computedModalTitle }}
119
+ </h3>
120
+ <UButton
121
+ icon="i-lucide-x"
122
+ color="neutral"
123
+ variant="ghost"
124
+ size="sm"
125
+ @click="isOpen = false"
126
+ />
127
+ </div>
128
+ </template>
129
+
130
+ <template #body>
131
+ <div class="p-4 space-y-4">
132
+ <slot :submit="handleSubmit" :loading="loading" :error="errorState">
133
+ <div class="space-y-4">
134
+ <p class="text-gray-500 italic">
135
+ Provide form content via default slot
136
+ </p>
137
+ </div>
138
+ </slot>
139
+
140
+ <div
141
+ v-if="errorState"
142
+ class="p-3 rounded bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm"
143
+ >
144
+ {{ errorState }}
145
+ </div>
146
+ </div>
147
+ </template>
148
+ </UModal>
149
+ </template>
@@ -0,0 +1,147 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * VACrudDelete - Delete confirmation component with CRUD integration
4
+ *
5
+ * Provides a delete button with confirmation modal that integrates with useVCrud.
6
+ * Handles API deletion, loading states, and error handling.
7
+ *
8
+ * @example
9
+ * <VACrudDelete
10
+ * endpoint="/api/users"
11
+ * :record-id="userId"
12
+ * item-name="User"
13
+ * @deleted="onUserDeleted"
14
+ * />
15
+ */
16
+
17
+ const props = withDefaults(
18
+ defineProps<{
19
+ endpoint: string;
20
+ recordId: string | number;
21
+ itemName?: string;
22
+ triggerLabel?: string;
23
+ triggerIcon?: string;
24
+ triggerColor?:
25
+ | "error"
26
+ | "primary"
27
+ | "secondary"
28
+ | "success"
29
+ | "info"
30
+ | "warning"
31
+ | "neutral";
32
+ triggerVariant?: "link" | "solid" | "outline" | "soft" | "subtle" | "ghost";
33
+ triggerSize?: "xs" | "sm" | "md" | "lg" | "xl";
34
+ confirmTitle?: string;
35
+ confirmMessage?: string;
36
+ open?: boolean;
37
+ }>(),
38
+ {
39
+ itemName: "this item",
40
+ triggerIcon: "i-lucide-trash-2",
41
+ triggerColor: "error",
42
+ triggerVariant: "solid",
43
+ triggerSize: "xs",
44
+ confirmTitle: "Delete Item",
45
+ open: undefined,
46
+ },
47
+ );
48
+
49
+ const emit = defineEmits<{
50
+ deleted: [id: string | number];
51
+ error: [error: any];
52
+ opened: [];
53
+ closed: [];
54
+ "update:open": [value: boolean];
55
+ }>();
56
+
57
+ const { remove, loading } = useVCrud(props.endpoint);
58
+
59
+ const isOpen = computed({
60
+ get: () => props.open,
61
+ set: (val) => {
62
+ emit("update:open", val ?? false);
63
+ if (val) {
64
+ emit("opened");
65
+ } else {
66
+ emit("closed");
67
+ }
68
+ },
69
+ });
70
+
71
+ const confirmMessage = computed(() => {
72
+ return (
73
+ props.confirmMessage ||
74
+ `Are you sure you want to delete ${props.itemName}? This cannot be undone.`
75
+ );
76
+ });
77
+
78
+ const handleDelete = async () => {
79
+ try {
80
+ await remove(props.recordId);
81
+ emit("deleted", props.recordId);
82
+ isOpen.value = false;
83
+ } catch (error) {
84
+ emit("error", error);
85
+ }
86
+ };
87
+ </script>
88
+
89
+ <template>
90
+ <UModal v-model:open="isOpen">
91
+ <UButton
92
+ :icon="triggerIcon"
93
+ :color="triggerColor as any"
94
+ :variant="triggerVariant as any"
95
+ :size="triggerSize as any"
96
+ :label="triggerLabel"
97
+ />
98
+
99
+ <template #header>
100
+ <div class="flex items-center justify-between w-full">
101
+ <h3 class="text-base font-semibold text-gray-900 dark:text-white">
102
+ {{ confirmTitle }}
103
+ </h3>
104
+ <UButton
105
+ icon="i-lucide-x"
106
+ color="neutral"
107
+ variant="ghost"
108
+ size="sm"
109
+ @click="isOpen = false"
110
+ />
111
+ </div>
112
+ </template>
113
+
114
+ <template #body>
115
+ <div class="p-4 space-y-4">
116
+ <div class="flex items-start gap-4">
117
+ <div
118
+ class="shrink-0 p-2 rounded-full bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400"
119
+ >
120
+ <UIcon name="i-lucide-alert-triangle" class="w-6 h-6" />
121
+ </div>
122
+ <div class="flex-1">
123
+ <p class="text-sm text-gray-500 dark:text-gray-400">
124
+ {{ confirmMessage }}
125
+ </p>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="flex justify-end gap-3">
130
+ <UButton
131
+ label="Cancel"
132
+ color="neutral"
133
+ variant="ghost"
134
+ @click="isOpen = false"
135
+ />
136
+ <UButton
137
+ label="Delete"
138
+ color="error"
139
+ variant="solid"
140
+ :loading="loading"
141
+ @click="handleDelete"
142
+ />
143
+ </div>
144
+ </div>
145
+ </template>
146
+ </UModal>
147
+ </template>
@@ -0,0 +1,171 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * VACrudUpdate - Veristone CRUD Update Modal
4
+ *
5
+ * Provides a reusable modal for editing records via API.
6
+ * Handles data fetching, form state management, and submission.
7
+ *
8
+ * @example
9
+ * ```vue
10
+ * <VACrudUpdate
11
+ * endpoint="/api/users"
12
+ * :record-id="userId"
13
+ * friendly-name="User"
14
+ * @updated="onUserUpdated"
15
+ * >
16
+ * <template #default="{ submit, loading, error, data, isDirty }">
17
+ * <form @submit.prevent="submit(data)">
18
+ * <UFormGroup label="Name">
19
+ * <UInput v-model="data.name" />
20
+ * </UFormGroup>
21
+ * <UButton type="submit" :loading="loading">Save</UButton>
22
+ * </form>
23
+ * </template>
24
+ * </VACrudUpdate>
25
+ * ```
26
+ */
27
+
28
+ interface Props {
29
+ endpoint: string;
30
+ recordId: string | number;
31
+ friendlyName?: string;
32
+ triggerLabel?: string;
33
+ triggerIcon?: string;
34
+ triggerColor?:
35
+ | "neutral"
36
+ | "error"
37
+ | "primary"
38
+ | "secondary"
39
+ | "success"
40
+ | "info"
41
+ | "warning";
42
+ triggerVariant?: "ghost" | "link" | "solid" | "outline" | "soft" | "subtle";
43
+ modalTitle?: string;
44
+ modalSize?: "sm" | "md" | "lg" | "xl";
45
+ open?: boolean;
46
+ }
47
+
48
+ const props = withDefaults(defineProps<Props>(), {
49
+ friendlyName: "Record",
50
+ triggerLabel: "Edit",
51
+ triggerIcon: "i-lucide-pencil",
52
+ triggerColor: "neutral",
53
+ triggerVariant: "ghost",
54
+ modalSize: "md",
55
+ open: undefined,
56
+ });
57
+
58
+ const emit = defineEmits<{
59
+ updated: [data: any];
60
+ error: [error: any];
61
+ opened: [];
62
+ closed: [];
63
+ "update:open": [value: boolean];
64
+ }>();
65
+
66
+ // Composables
67
+ const { findOne, update, loading, errorState, formData, isFormDirty } =
68
+ useVCrud(props.endpoint);
69
+
70
+ // State
71
+ const internalOpen = ref(false);
72
+ const isFetching = ref(false);
73
+
74
+ // Computed modal title
75
+ const computedModalTitle = computed(() => {
76
+ return props.modalTitle || `Edit ${props.friendlyName}`;
77
+ });
78
+
79
+ // Hybrid open state (internal + external v-model)
80
+ const isOpen = computed({
81
+ get: () => (props.open !== undefined ? props.open : internalOpen.value),
82
+ set: (val) => {
83
+ emit("update:open", val);
84
+ internalOpen.value = val;
85
+ },
86
+ });
87
+
88
+ // Watch for modal open to fetch data
89
+ watch(isOpen, async (val) => {
90
+ if (val) {
91
+ emit("opened");
92
+ if (props.recordId) {
93
+ isFetching.value = true;
94
+ try {
95
+ await findOne(props.recordId);
96
+ } catch (e) {
97
+ // Error handled by composable
98
+ } finally {
99
+ isFetching.value = false;
100
+ }
101
+ }
102
+ } else {
103
+ emit("closed");
104
+ }
105
+ });
106
+
107
+ // Submit handler
108
+ const submit = async (data: Record<string, any>) => {
109
+ try {
110
+ const result = await update(props.recordId, data);
111
+ emit("updated", result);
112
+ isOpen.value = false;
113
+ } catch (e) {
114
+ emit("error", e);
115
+ }
116
+ };
117
+ </script>
118
+
119
+ <template>
120
+ <UModal v-model:open="isOpen">
121
+ <UButton
122
+ :label="triggerLabel"
123
+ :icon="triggerIcon"
124
+ :color="triggerColor"
125
+ :variant="triggerVariant"
126
+ />
127
+
128
+ <template #header>
129
+ <div class="flex items-center justify-between w-full">
130
+ <h3 class="text-base font-semibold text-gray-900 dark:text-white">
131
+ {{ computedModalTitle }}
132
+ </h3>
133
+ <UButton
134
+ icon="i-lucide-x"
135
+ color="neutral"
136
+ variant="ghost"
137
+ size="sm"
138
+ @click="isOpen = false"
139
+ />
140
+ </div>
141
+ </template>
142
+
143
+ <template #body>
144
+ <div class="p-4 space-y-4">
145
+ <div v-if="isFetching" class="space-y-4">
146
+ <USkeleton class="h-4 w-full" />
147
+ <USkeleton class="h-4 w-3/4" />
148
+ <USkeleton class="h-4 w-1/2" />
149
+ </div>
150
+
151
+ <div v-else>
152
+ <slot
153
+ :submit="submit"
154
+ :loading="loading"
155
+ :error="errorState"
156
+ :data="formData"
157
+ :isDirty="isFormDirty"
158
+ :isFetching="isFetching"
159
+ />
160
+ </div>
161
+
162
+ <div
163
+ v-if="errorState"
164
+ class="rounded bg-red-50 p-3 text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400"
165
+ >
166
+ {{ errorState }}
167
+ </div>
168
+ </div>
169
+ </template>
170
+ </UModal>
171
+ </template>