openprompt-lang 0.3.0 → 0.4.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 (90) hide show
  1. package/README.md +483 -32
  2. package/bin/cli.js +232 -41
  3. package/bin/create.js +135 -0
  4. package/bin/lint.js +20 -21
  5. package/docs/COMMANDS.md +200 -127
  6. package/docs/COMMITS/INDEX.md +1 -0
  7. package/docs/PROMPT_AI_CONTEXT.md +99 -0
  8. package/docs/langs/dotnet.md +36 -0
  9. package/docs/langs/java-spring.md +45 -0
  10. package/docs/langs/python-fastapi.md +35 -0
  11. package/docs/langs/unity.md +30 -0
  12. package/docs/langs/vue-nuxt.md +36 -0
  13. package/package.json +31 -3
  14. package/scaffolds/.cursorrules +6 -0
  15. package/scaffolds/AGENTS.md +27 -0
  16. package/scaffolds/Dockerfile +11 -0
  17. package/scaffolds/capacitor.config.ts +17 -0
  18. package/scaffolds/netlify.toml +8 -0
  19. package/scaffolds/prompt-lang.json +15 -0
  20. package/scaffolds/railway.json +12 -0
  21. package/scaffolds/tailwind.config.js +8 -0
  22. package/scaffolds/tauri.conf.json +26 -0
  23. package/schemas/language-module.json +116 -0
  24. package/schemas/prompt-lang.json +38 -3
  25. package/schemas/structures.json +145 -0
  26. package/src/ai/prompt-builder.js +184 -0
  27. package/src/ai/providers.js +247 -0
  28. package/src/annotations/registry.js +39 -0
  29. package/src/annotations/tags.json +24 -0
  30. package/src/commands/ai-gen.js +161 -0
  31. package/src/commands/component.js +242 -212
  32. package/src/commands/context.js +184 -109
  33. package/src/commands/extract.js +242 -0
  34. package/src/commands/figma.js +15 -15
  35. package/src/commands/init.js +197 -93
  36. package/src/commands/integrate.js +406 -0
  37. package/src/commands/lang.js +148 -0
  38. package/src/commands/qa-gen.js +139 -0
  39. package/src/commands/scaffold.js +127 -0
  40. package/src/commands/suggest.js +24 -14
  41. package/src/commands/teach.js +110 -0
  42. package/src/commands/validate.js +143 -83
  43. package/src/commands/wizard.js +456 -0
  44. package/src/generators/figma-prompt.js +20 -12
  45. package/src/language-service/plugin.cjs +94 -0
  46. package/src/language-service/plugin.d.ts +6 -0
  47. package/src/mcp-server.js +605 -0
  48. package/src/templates/langs/react/INDEX.json +262 -0
  49. package/src/templates/langs/react/MODULE.json +166 -0
  50. package/src/templates/langs/react/templates/hooks/useAuth.template.ts +134 -0
  51. package/src/templates/langs/react/templates/hooks/useDebounce.template.ts +45 -0
  52. package/src/templates/langs/react/templates/hooks/useForm.template.ts +146 -0
  53. package/src/templates/langs/react/templates/hooks/usePagination.template.ts +108 -0
  54. package/src/templates/langs/react/templates/services/apiService.template.ts +123 -0
  55. package/src/templates/langs/react/templates/ui/Button.template.tsx +87 -0
  56. package/src/templates/langs/react/templates/ui/Card.template.tsx +85 -0
  57. package/src/templates/langs/react/templates/ui/DataTable.template.tsx +163 -0
  58. package/src/templates/langs/react/templates/ui/Input.template.tsx +96 -0
  59. package/src/templates/langs/react/templates/ui/Modal.template.tsx +133 -0
  60. package/src/templates/langs/react/templates/ui/Select.template.tsx +99 -0
  61. package/src/templates/langs/vue/INDEX.json +246 -0
  62. package/src/templates/langs/vue/MODULE.json +105 -0
  63. package/src/templates/langs/vue/templates/composables/useAuth.template.ts +106 -0
  64. package/src/templates/langs/vue/templates/composables/useDebounce.template.ts +47 -0
  65. package/src/templates/langs/vue/templates/composables/useFetch.template.ts +54 -0
  66. package/src/templates/langs/vue/templates/composables/useForm.template.ts +127 -0
  67. package/src/templates/langs/vue/templates/composables/usePagination.template.ts +98 -0
  68. package/src/templates/langs/vue/templates/services/apiService.template.ts +116 -0
  69. package/src/templates/langs/vue/templates/ui/Button.template.vue +79 -0
  70. package/src/templates/langs/vue/templates/ui/Card.template.vue +73 -0
  71. package/src/templates/langs/vue/templates/ui/DataTable.template.vue +115 -0
  72. package/src/templates/langs/vue/templates/ui/Input.template.vue +70 -0
  73. package/src/templates/langs/vue/templates/ui/Modal.template.vue +112 -0
  74. package/src/templates/langs/vue/templates/ui/Select.template.vue +77 -0
  75. package/src/templates/scripts/log-actividad.sh +32 -0
  76. package/src/templates/scripts/log-commit.sh +35 -0
  77. package/src/templates/scripts/log-error.sh +45 -0
  78. package/src/templates/scripts/validate.sh +23 -0
  79. package/src/ts-transformer/index.cjs +86 -0
  80. package/src/utils/ai.js +35 -53
  81. package/src/utils/annotations.js +260 -214
  82. package/src/utils/config.js +61 -13
  83. package/src/utils/error-learner.js +203 -0
  84. package/src/utils/file-utils.js +119 -0
  85. package/src/utils/language-loader.js +167 -0
  86. package/src/utils/template-utils.js +45 -0
  87. package/src/vite-plugin/index.js +54 -0
  88. package/vscode-extension/package.json +23 -2
  89. package/vscode-extension/snippets/promptlang.json +1 -3
  90. package/vscode-extension/syntaxes/annotations-code.tmGrammar.json +15 -0
@@ -0,0 +1,246 @@
1
+ {
2
+ "language": "vue",
3
+ "version": "1.0.0",
4
+ "updated": "2026-05-14",
5
+ "categories": {
6
+ "ui/button": {
7
+ "name": "Buttons",
8
+ "description": "Button components with variants and states"
9
+ },
10
+ "ui/modal": {
11
+ "name": "Modals",
12
+ "description": "Dialogs, confirmations, and popovers"
13
+ },
14
+ "ui/form": {
15
+ "name": "Forms",
16
+ "description": "Inputs, selects, and form utilities"
17
+ },
18
+ "ui/data-display": {
19
+ "name": "Data Display",
20
+ "description": "Tables, lists, cards, and data visualizers"
21
+ },
22
+ "auth": {
23
+ "name": "Authentication",
24
+ "description": "Auth composables, guards, and middleware"
25
+ },
26
+ "composable/general": {
27
+ "name": "General Composables",
28
+ "description": "Reusable Vue composables"
29
+ },
30
+ "composable/data": {
31
+ "name": "Data Composables",
32
+ "description": "Data fetching and state management composables"
33
+ },
34
+ "service/api": {
35
+ "name": "API Services",
36
+ "description": "HTTP clients and API abstractions"
37
+ }
38
+ },
39
+ "templates": [
40
+ {
41
+ "id": "button-shadcn-vue",
42
+ "name": "Button",
43
+ "description": "Versatile button with variants, loading state, and dark mode",
44
+ "category": "ui/button",
45
+ "purposes": ["primary-action", "confirmation", "danger", "navigation"],
46
+ "file": "templates/ui/Button.template.vue",
47
+ "testFile": null,
48
+ "variants": ["primary", "secondary", "danger", "ghost", "outline"],
49
+ "sizes": ["sm", "default", "lg", "icon"],
50
+ "darkMode": true,
51
+ "responsive": true,
52
+ "dependencies": [],
53
+ "tags": ["button", "action", "form", "shadcn-vue", "loading"],
54
+ "hasTeachMe": true,
55
+ "fixHistory": []
56
+ },
57
+ {
58
+ "id": "input-shadcn-vue",
59
+ "name": "Input",
60
+ "description": "Form input with label, error state, helper text, and v-model",
61
+ "category": "ui/form",
62
+ "purposes": ["text-input", "search", "email", "password", "number"],
63
+ "file": "templates/ui/Input.template.vue",
64
+ "testFile": null,
65
+ "variants": ["default", "error", "flushed"],
66
+ "sizes": ["sm", "default", "lg"],
67
+ "darkMode": true,
68
+ "responsive": true,
69
+ "dependencies": [],
70
+ "tags": ["input", "form", "label", "error", "shadcn-vue"],
71
+ "hasTeachMe": true,
72
+ "fixHistory": []
73
+ },
74
+ {
75
+ "id": "select-shadcn-vue",
76
+ "name": "Select",
77
+ "description": "Custom styled select dropdown with label, error state, and placeholder",
78
+ "category": "ui/form",
79
+ "purposes": ["dropdown", "option-picker", "multi-select"],
80
+ "file": "templates/ui/Select.template.vue",
81
+ "testFile": null,
82
+ "variants": ["default", "error", "multiple"],
83
+ "sizes": ["sm", "default", "lg"],
84
+ "darkMode": true,
85
+ "responsive": true,
86
+ "dependencies": [],
87
+ "tags": ["select", "dropdown", "form", "shadcn-vue"],
88
+ "hasTeachMe": true,
89
+ "fixHistory": []
90
+ },
91
+ {
92
+ "id": "card-shadcn-vue",
93
+ "name": "Card",
94
+ "description": "Container card with title, subtitle, footer, and interactive variant",
95
+ "category": "ui/data-display",
96
+ "purposes": ["container", "summary", "feature-card", "dashboard-card"],
97
+ "file": "templates/ui/Card.template.vue",
98
+ "testFile": null,
99
+ "variants": ["default", "bordered", "elevated", "interactive"],
100
+ "sizes": ["sm", "default", "full"],
101
+ "darkMode": true,
102
+ "responsive": true,
103
+ "dependencies": [],
104
+ "tags": ["card", "container", "dashboard", "shadcn-vue"],
105
+ "hasTeachMe": true,
106
+ "fixHistory": []
107
+ },
108
+ {
109
+ "id": "modal-dialog-vue",
110
+ "name": "Modal",
111
+ "description": "Accessible modal dialog with Teleport, focus trap, and escape-to-close",
112
+ "category": "ui/modal",
113
+ "purposes": ["confirm-action", "form-dialog", "alert", "fullscreen"],
114
+ "file": "templates/ui/Modal.template.vue",
115
+ "testFile": null,
116
+ "variants": ["dialog", "confirm-destructive", "fullscreen"],
117
+ "sizes": ["sm", "default", "lg", "xl", "fullscreen"],
118
+ "darkMode": true,
119
+ "responsive": true,
120
+ "dependencies": [],
121
+ "tags": ["modal", "dialog", "overlay", "teleport", "a11y"],
122
+ "hasTeachMe": true,
123
+ "fixHistory": []
124
+ },
125
+ {
126
+ "id": "datatable-shadcn-vue",
127
+ "name": "DataTable",
128
+ "description": "Generic typed data table with sorting, loading skeleton, and empty state",
129
+ "category": "ui/data-display",
130
+ "purposes": ["data-table", "sortable-table", "paginated-list"],
131
+ "file": "templates/ui/DataTable.template.vue",
132
+ "testFile": null,
133
+ "variants": ["default", "compact", "striped", "sortable"],
134
+ "sizes": ["default", "compact"],
135
+ "darkMode": true,
136
+ "responsive": true,
137
+ "dependencies": [],
138
+ "tags": ["table", "data", "sortable", "shadcn-vue"],
139
+ "hasTeachMe": true,
140
+ "fixHistory": []
141
+ },
142
+ {
143
+ "id": "composable-useFetch",
144
+ "name": "useFetch",
145
+ "description": "Typed data fetching composable with loading/error states",
146
+ "category": "composable/data",
147
+ "purposes": ["data-fetching", "api-calls", "loading-states"],
148
+ "file": "templates/composables/useFetch.template.ts",
149
+ "testFile": null,
150
+ "variants": [],
151
+ "sizes": [],
152
+ "darkMode": false,
153
+ "responsive": false,
154
+ "dependencies": ["vue"],
155
+ "tags": ["fetch", "composable", "data", "api", "loading"],
156
+ "hasTeachMe": false,
157
+ "fixHistory": []
158
+ },
159
+ {
160
+ "id": "composable-useAuth",
161
+ "name": "useAuth",
162
+ "description": "Authentication composable with Supabase and session management",
163
+ "category": "auth",
164
+ "purposes": ["login", "register", "session", "logout", "protected-routes"],
165
+ "file": "templates/composables/useAuth.template.ts",
166
+ "testFile": null,
167
+ "variants": [],
168
+ "sizes": [],
169
+ "darkMode": false,
170
+ "responsive": false,
171
+ "dependencies": ["@supabase/supabase-js", "vue"],
172
+ "tags": ["auth", "supabase", "session", "composable"],
173
+ "hasTeachMe": true,
174
+ "fixHistory": []
175
+ },
176
+ {
177
+ "id": "composable-useDebounce",
178
+ "name": "useDebounce",
179
+ "description": "Debounce composable for input values, search delays, and resize throttling",
180
+ "category": "composable/general",
181
+ "purposes": ["input-debounce", "search-delay", "resize-throttle"],
182
+ "file": "templates/composables/useDebounce.template.ts",
183
+ "testFile": null,
184
+ "variants": ["default", "leading", "trailing"],
185
+ "sizes": [],
186
+ "darkMode": false,
187
+ "responsive": false,
188
+ "dependencies": [],
189
+ "tags": ["debounce", "composable", "search", "throttle"],
190
+ "hasTeachMe": true,
191
+ "fixHistory": []
192
+ },
193
+ {
194
+ "id": "composable-usePagination",
195
+ "name": "usePagination",
196
+ "description": "Pagination composable with ellipsis logic, page buttons, and server-side support",
197
+ "category": "composable/general",
198
+ "purposes": ["paginated-list", "table-pagination", "infinite-scroll"],
199
+ "file": "templates/composables/usePagination.template.ts",
200
+ "testFile": null,
201
+ "variants": ["client-side", "server-side"],
202
+ "sizes": [],
203
+ "darkMode": false,
204
+ "responsive": true,
205
+ "dependencies": [],
206
+ "tags": ["pagination", "composable", "table", "navigation"],
207
+ "hasTeachMe": true,
208
+ "fixHistory": []
209
+ },
210
+ {
211
+ "id": "composable-useForm",
212
+ "name": "useForm",
213
+ "description": "Lightweight form state manager with validation, async submit, and touched tracking",
214
+ "category": "ui/form",
215
+ "purposes": ["form-state", "validation", "async-submit"],
216
+ "file": "templates/composables/useForm.template.ts",
217
+ "testFile": null,
218
+ "variants": ["controlled", "uncontrolled"],
219
+ "sizes": [],
220
+ "darkMode": false,
221
+ "responsive": false,
222
+ "dependencies": [],
223
+ "tags": ["form", "validation", "composable", "state"],
224
+ "hasTeachMe": true,
225
+ "fixHistory": []
226
+ },
227
+ {
228
+ "id": "service-api-vue",
229
+ "name": "ApiClient",
230
+ "description": "Generic REST API client with typed responses, auth, timeout, and error handling",
231
+ "category": "service/api",
232
+ "purposes": ["crud", "rest-api", "http-client", "data-fetching"],
233
+ "file": "templates/services/apiService.template.ts",
234
+ "testFile": null,
235
+ "variants": ["rest", "supabase"],
236
+ "sizes": [],
237
+ "darkMode": false,
238
+ "responsive": false,
239
+ "dependencies": [],
240
+ "tags": ["api", "rest", "http", "client", "service"],
241
+ "hasTeachMe": true,
242
+ "fixHistory": []
243
+ }
244
+ ],
245
+ "errorsLearned": []
246
+ }
@@ -0,0 +1,105 @@
1
+ {
2
+ "id": "vue",
3
+ "name": "Vue 3 + Nuxt",
4
+ "version": "1.0.0",
5
+ "tags": {
6
+ "kind": {
7
+ "component": {
8
+ "extends": "base",
9
+ "limit": 120,
10
+ "suggests": ["props"],
11
+ "description": "Vue SFC component"
12
+ },
13
+ "composable": {
14
+ "extends": "base",
15
+ "limit": 80,
16
+ "requires": ["contract"],
17
+ "description": "Vue composable function"
18
+ },
19
+ "page": {
20
+ "extends": "base",
21
+ "limit": 200,
22
+ "requires": ["compose"],
23
+ "description": "Nuxt page"
24
+ },
25
+ "server": {
26
+ "extends": "base",
27
+ "limit": 100,
28
+ "requires": ["contract"],
29
+ "description": "Nuxt server API route"
30
+ },
31
+ "store": {
32
+ "extends": "base",
33
+ "limit": 100,
34
+ "suggests": ["deps"],
35
+ "description": "Pinia store"
36
+ },
37
+ "middleware": { "extends": "base", "limit": 60, "description": "Nuxt route middleware" },
38
+ "util": {
39
+ "extends": "base",
40
+ "limit": 100,
41
+ "forbids": ["state"],
42
+ "description": "Utility function"
43
+ }
44
+ },
45
+ "platforms": ["web", "mobile"],
46
+ "forbidden": ["any", "vuex"]
47
+ },
48
+ "structure": {
49
+ "folders": [
50
+ "composables",
51
+ "components/ui",
52
+ "components/layout",
53
+ "pages",
54
+ "server/api",
55
+ "server/utils",
56
+ "stores",
57
+ "types",
58
+ "middleware",
59
+ "docs/COMMITS",
60
+ "docs/LOGS/ERRORES",
61
+ "docs/BACKLOG"
62
+ ],
63
+ "scaffolds": ["prompt-lang.json", "AGENTS.md", ".gitignore"],
64
+ "deps": {
65
+ "base": { "dev": ["nuxt", "vue", "typescript", "tailwindcss", "vitest"] },
66
+ "supabase": { "prod": ["@supabase/supabase-js"] },
67
+ "prisma": { "dev": ["prisma"], "prod": ["@prisma/client"] },
68
+ "ionic": {
69
+ "prod": ["@ionic/vue", "@ionic/vue-router", "@capacitor/core"],
70
+ "dev": ["@capacitor/cli"]
71
+ }
72
+ }
73
+ },
74
+ "projectTypes": {
75
+ "saas": {
76
+ "name": "SaaS / Dashboard",
77
+ "ui": true,
78
+ "description": "Multi-tenant SaaS with dashboard"
79
+ },
80
+ "web": { "name": "Corporate Website", "ui": true, "description": "Company site with CMS" },
81
+ "landing": { "name": "Landing Page", "ui": true, "description": "Single product page" },
82
+ "api": { "name": "API / Microservice", "ui": false, "description": "Nuxt server-only API" }
83
+ },
84
+ "uiStyles": ["shadcn-vue", "primevue", "vuetify", "none"],
85
+ "extensions": [
86
+ {
87
+ "name": "Supabase (auth + DB)",
88
+ "value": "supabase",
89
+ "deps": ["@supabase/supabase-js"],
90
+ "description": "Backend as a service"
91
+ },
92
+ {
93
+ "name": "Prisma ORM",
94
+ "value": "prisma",
95
+ "deps": ["prisma", "@prisma/client"],
96
+ "description": "Type-safe database ORM"
97
+ },
98
+ {
99
+ "name": "Pinia (state)",
100
+ "value": "pinia",
101
+ "deps": ["pinia"],
102
+ "description": "State management"
103
+ }
104
+ ]
105
+ }
@@ -0,0 +1,106 @@
1
+ // @template
2
+ // @id: composable-useAuth
3
+ // @category: auth
4
+ // @purpose: login, register, session, logout, protected-routes
5
+ // @framework: vue
6
+ // @style: none
7
+ // @darkMode: false
8
+ // @responsive: false
9
+ // @deps: [vue, @supabase/supabase-js]
10
+ // @states: [idle, loading, authenticated, unauthenticated, error]
11
+ // @example: const { user, login, logout } = useAuth()
12
+ // @teachMe
13
+ //
14
+ // @goodPractice: Use provide/inject for auth state across app
15
+ // @goodPractice: Persist session in localStorage for refresh resilience
16
+ // @goodPractice: Type user data for type-safe access throughout app
17
+ // @badPractice: Don't store tokens in plain state — use httpOnly cookies or localStorage with guards
18
+
19
+ import { ref, computed, type Ref } from "vue"
20
+ import { createClient, type SupabaseClient, type User } from "@supabase/supabase-js"
21
+
22
+ interface AuthState {
23
+ user: Ref<User | null>
24
+ loading: Ref<boolean>
25
+ error: Ref<string | null>
26
+ isAuthenticated: Ref<boolean>
27
+ }
28
+
29
+ const supabase: SupabaseClient = createClient(
30
+ import.meta.env.VITE_SUPABASE_URL || "",
31
+ import.meta.env.VITE_SUPABASE_ANON_KEY || ""
32
+ )
33
+
34
+ const user = ref<User | null>(null)
35
+ const loading = ref(true)
36
+ const error = ref<string | null>(null)
37
+
38
+ export function useAuth(): AuthState & {
39
+ login: (email: string, password: string) => Promise<void>
40
+ register: (email: string, password: string) => Promise<void>
41
+ logout: () => Promise<void>
42
+ refreshSession: () => Promise<void>
43
+ } {
44
+ async function refreshSession() {
45
+ loading.value = true
46
+ error.value = null
47
+ try {
48
+ const { data: { session } } = await supabase.auth.getSession()
49
+ user.value = session?.user ?? null
50
+ } catch (e) {
51
+ error.value = e instanceof Error ? e.message : "Session error"
52
+ user.value = null
53
+ } finally {
54
+ loading.value = false
55
+ }
56
+ }
57
+
58
+ async function login(email: string, password: string) {
59
+ loading.value = true
60
+ error.value = null
61
+ try {
62
+ const { data, error: authError } = await supabase.auth.signInWithPassword({ email, password })
63
+ if (authError) throw authError
64
+ user.value = data.user
65
+ } catch (e) {
66
+ error.value = e instanceof Error ? e.message : "Login failed"
67
+ throw e
68
+ } finally {
69
+ loading.value = false
70
+ }
71
+ }
72
+
73
+ async function register(email: string, password: string) {
74
+ loading.value = true
75
+ error.value = null
76
+ try {
77
+ const { data, error: authError } = await supabase.auth.signUp({ email, password })
78
+ if (authError) throw authError
79
+ user.value = data.user
80
+ } catch (e) {
81
+ error.value = e instanceof Error ? e.message : "Registration failed"
82
+ throw e
83
+ } finally {
84
+ loading.value = false
85
+ }
86
+ }
87
+
88
+ async function logout() {
89
+ loading.value = true
90
+ try {
91
+ await supabase.auth.signOut()
92
+ user.value = null
93
+ } catch (e) {
94
+ error.value = e instanceof Error ? e.message : "Logout failed"
95
+ } finally {
96
+ loading.value = false
97
+ }
98
+ }
99
+
100
+ const isAuthenticated = computed(() => user.value !== null)
101
+
102
+ // Initialize session
103
+ refreshSession()
104
+
105
+ return { user, loading, error, isAuthenticated, login, register, logout, refreshSession }
106
+ }
@@ -0,0 +1,47 @@
1
+ // @template
2
+ // @id: composable-useDebounce
3
+ // @category: composable/general
4
+ // @purpose: input-debounce, search-delay, resize-throttle
5
+ // @framework: vue
6
+ // @style: none
7
+ // @darkMode: false
8
+ // @responsive: false
9
+ // @deps: [vue]
10
+ // @states: [active, idle]
11
+ // @example: const debouncedValue = useDebounce(inputValue, 300)
12
+ // @teachMe
13
+ //
14
+ // @goodPractice: Clean up timeout on unmount via onUnmounted
15
+ // @goodPractice: Use computed.ref for seamless reactivity integration
16
+ // @goodPractice: Allow leading/trailing invocation modes
17
+
18
+ import { ref, watch, onUnmounted, type Ref } from "vue"
19
+
20
+ export function useDebounce<T>(
21
+ value: Ref<T> | (() => T),
22
+ delay: number = 300
23
+ ): Ref<T> {
24
+ const debouncedValue = ref<T>(
25
+ typeof value === "function" ? value() : value.value
26
+ ) as Ref<T>
27
+ let timeoutId: ReturnType<typeof setTimeout> | null = null
28
+
29
+ const sourceValue = typeof value === "function" ? value : () => value.value
30
+
31
+ watch(
32
+ sourceValue,
33
+ (newVal) => {
34
+ if (timeoutId) clearTimeout(timeoutId)
35
+ timeoutId = setTimeout(() => {
36
+ debouncedValue.value = newVal as T
37
+ }, delay)
38
+ },
39
+ { immediate: false }
40
+ )
41
+
42
+ onUnmounted(() => {
43
+ if (timeoutId) clearTimeout(timeoutId)
44
+ })
45
+
46
+ return debouncedValue
47
+ }
@@ -0,0 +1,54 @@
1
+ // @template
2
+ // @id: composable-useFetch
3
+ // @category: composable/data
4
+ // @purpose: data-fetching, api-calls
5
+ // @framework: vue
6
+ // @style: none
7
+ // @darkMode: false
8
+ // @responsive: false
9
+ // @deps: [vue]
10
+ // @states: [idle, loading, success, error]
11
+ // @example: const { data, loading, error } = await useFetch<User[]>('/api/users')
12
+ //
13
+ // @goodPractice: Use AbortController to cancel requests on unmount
14
+ // @goodPractice: Provide type generics for return data
15
+
16
+ import { ref, watchEffect, type Ref } from "vue"
17
+
18
+ interface UseFetchResult<T> {
19
+ data: Ref<T | null>
20
+ loading: Ref<boolean>
21
+ error: Ref<string | null>
22
+ refresh: () => void
23
+ }
24
+
25
+ export function useFetch<T = unknown>(
26
+ url: string | Ref<string>,
27
+ options?: RequestInit
28
+ ): UseFetchResult<T> {
29
+ const data = ref<T | null>(null) as Ref<T | null>
30
+ const loading = ref(true)
31
+ const error = ref<string | null>(null)
32
+
33
+ const fetchData = async () => {
34
+ loading.value = true
35
+ error.value = null
36
+
37
+ try {
38
+ const resolvedUrl = typeof url === "string" ? url : url.value
39
+ const res = await fetch(resolvedUrl, options)
40
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
41
+ data.value = await res.json()
42
+ } catch (e) {
43
+ error.value = e instanceof Error ? e.message : "Unknown error"
44
+ } finally {
45
+ loading.value = false
46
+ }
47
+ }
48
+
49
+ watchEffect(() => {
50
+ fetchData()
51
+ })
52
+
53
+ return { data, loading, error, refresh: fetchData }
54
+ }
@@ -0,0 +1,127 @@
1
+ // @template
2
+ // @id: composable-useForm
3
+ // @category: ui/form
4
+ // @purpose: form-state, validation, async-submit
5
+ // @framework: vue
6
+ // @style: none
7
+ // @darkMode: false
8
+ // @responsive: false
9
+ // @deps: [vue]
10
+ // @states: [idle, validating, submitting, error, success]
11
+ // @example: const { values, errors, handleSubmit, isSubmitting } = useForm({ email: '' })
12
+ // @teachMe
13
+ //
14
+ // @goodPractice: Track touched fields for selective error display
15
+ // @goodPractice: Support async validation for server-side checks
16
+ // @goodPractice: Use TypeScript generics for type-safe form values
17
+ // @badPractice: Don't validate on every keystroke for large forms — debounce first
18
+
19
+ import { ref, reactive, computed, type Ref, type UnwrapRef } from "vue"
20
+
21
+ type ValidationRule<T> = {
22
+ key: keyof T
23
+ validate: (value: T[keyof T], values: T) => string | null
24
+ async?: (value: T[keyof T], values: T) => Promise<string | null>
25
+ debounce?: number
26
+ }
27
+
28
+ interface UseFormOptions<T extends Record<string, unknown>> {
29
+ initialValues: T
30
+ rules?: ValidationRule<T>[]
31
+ onSubmit?: (values: T) => Promise<void> | void
32
+ }
33
+
34
+ export function useForm<T extends Record<string, unknown>>(options: UseFormOptions<T>) {
35
+ const { initialValues, rules = [], onSubmit } = options
36
+
37
+ const values = reactive<T>({ ...initialValues }) as UnwrapRef<T>
38
+ const errors = ref<Partial<Record<keyof T, string>>>({})
39
+ const touched = ref<Set<keyof T>>(new Set())
40
+ const isSubmitting = ref(false)
41
+ const isDirty = ref(false)
42
+ const submitCount = ref(0)
43
+
44
+ const isValid = computed(() => Object.keys(errors.value).length === 0)
45
+
46
+ function validateField(key: keyof T) {
47
+ const fieldRules = rules.filter((r) => r.key === key)
48
+ for (const rule of fieldRules) {
49
+ const error = rule.validate(values[key], values)
50
+ if (error) {
51
+ errors.value[key] = error
52
+ return
53
+ }
54
+ }
55
+ delete errors.value[key]
56
+ }
57
+
58
+ function validateAll(): boolean {
59
+ const newErrors: Partial<Record<keyof T, string>> = {}
60
+ for (const rule of rules) {
61
+ const error = rule.validate(values[rule.key], values)
62
+ if (error) newErrors[rule.key] = error
63
+ }
64
+ errors.value = newErrors as Partial<Record<keyof T, string>>
65
+ return Object.keys(newErrors).length === 0
66
+ }
67
+
68
+ function setFieldValue(key: keyof T, value: T[keyof T]) {
69
+ values[key] = value
70
+ isDirty.value = true
71
+ if (touched.value.has(key)) {
72
+ validateField(key)
73
+ }
74
+ }
75
+
76
+ function setFieldTouched(key: keyof T) {
77
+ touched.value.add(key)
78
+ validateField(key)
79
+ }
80
+
81
+ async function handleSubmit() {
82
+ submitCount.value++
83
+ isSubmitting.value = true
84
+ touched.value = new Set(Object.keys(values)) as Set<keyof T>
85
+
86
+ const valid = validateAll()
87
+ if (!valid) {
88
+ isSubmitting.value = false
89
+ return false
90
+ }
91
+
92
+ try {
93
+ if (onSubmit) await onSubmit(values)
94
+ return true
95
+ } catch (e) {
96
+ const message = e instanceof Error ? e.message : "Submission failed"
97
+ errors.value.submit = message as any
98
+ return false
99
+ } finally {
100
+ isSubmitting.value = false
101
+ }
102
+ }
103
+
104
+ function resetForm() {
105
+ Object.assign(values, { ...initialValues })
106
+ errors.value = {} as Partial<Record<keyof T, string>>
107
+ touched.value = new Set()
108
+ isDirty.value = false
109
+ isSubmitting.value = false
110
+ }
111
+
112
+ return {
113
+ values,
114
+ errors,
115
+ touched,
116
+ isSubmitting,
117
+ isDirty,
118
+ isValid,
119
+ submitCount,
120
+ setFieldValue,
121
+ setFieldTouched,
122
+ validateField,
123
+ validateAll,
124
+ handleSubmit,
125
+ resetForm,
126
+ }
127
+ }