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.
- package/README.md +483 -32
- package/bin/cli.js +232 -41
- package/bin/create.js +135 -0
- package/bin/lint.js +20 -21
- package/docs/COMMANDS.md +200 -127
- package/docs/COMMITS/INDEX.md +1 -0
- package/docs/PROMPT_AI_CONTEXT.md +99 -0
- package/docs/langs/dotnet.md +36 -0
- package/docs/langs/java-spring.md +45 -0
- package/docs/langs/python-fastapi.md +35 -0
- package/docs/langs/unity.md +30 -0
- package/docs/langs/vue-nuxt.md +36 -0
- package/package.json +31 -3
- package/scaffolds/.cursorrules +6 -0
- package/scaffolds/AGENTS.md +27 -0
- package/scaffolds/Dockerfile +11 -0
- package/scaffolds/capacitor.config.ts +17 -0
- package/scaffolds/netlify.toml +8 -0
- package/scaffolds/prompt-lang.json +15 -0
- package/scaffolds/railway.json +12 -0
- package/scaffolds/tailwind.config.js +8 -0
- package/scaffolds/tauri.conf.json +26 -0
- package/schemas/language-module.json +116 -0
- package/schemas/prompt-lang.json +38 -3
- package/schemas/structures.json +145 -0
- package/src/ai/prompt-builder.js +184 -0
- package/src/ai/providers.js +247 -0
- package/src/annotations/registry.js +39 -0
- package/src/annotations/tags.json +24 -0
- package/src/commands/ai-gen.js +161 -0
- package/src/commands/component.js +242 -212
- package/src/commands/context.js +184 -109
- package/src/commands/extract.js +242 -0
- package/src/commands/figma.js +15 -15
- package/src/commands/init.js +197 -93
- package/src/commands/integrate.js +406 -0
- package/src/commands/lang.js +148 -0
- package/src/commands/qa-gen.js +139 -0
- package/src/commands/scaffold.js +127 -0
- package/src/commands/suggest.js +24 -14
- package/src/commands/teach.js +110 -0
- package/src/commands/validate.js +143 -83
- package/src/commands/wizard.js +456 -0
- package/src/generators/figma-prompt.js +20 -12
- package/src/language-service/plugin.cjs +94 -0
- package/src/language-service/plugin.d.ts +6 -0
- package/src/mcp-server.js +605 -0
- package/src/templates/langs/react/INDEX.json +262 -0
- package/src/templates/langs/react/MODULE.json +166 -0
- package/src/templates/langs/react/templates/hooks/useAuth.template.ts +134 -0
- package/src/templates/langs/react/templates/hooks/useDebounce.template.ts +45 -0
- package/src/templates/langs/react/templates/hooks/useForm.template.ts +146 -0
- package/src/templates/langs/react/templates/hooks/usePagination.template.ts +108 -0
- package/src/templates/langs/react/templates/services/apiService.template.ts +123 -0
- package/src/templates/langs/react/templates/ui/Button.template.tsx +87 -0
- package/src/templates/langs/react/templates/ui/Card.template.tsx +85 -0
- package/src/templates/langs/react/templates/ui/DataTable.template.tsx +163 -0
- package/src/templates/langs/react/templates/ui/Input.template.tsx +96 -0
- package/src/templates/langs/react/templates/ui/Modal.template.tsx +133 -0
- package/src/templates/langs/react/templates/ui/Select.template.tsx +99 -0
- package/src/templates/langs/vue/INDEX.json +246 -0
- package/src/templates/langs/vue/MODULE.json +105 -0
- package/src/templates/langs/vue/templates/composables/useAuth.template.ts +106 -0
- package/src/templates/langs/vue/templates/composables/useDebounce.template.ts +47 -0
- package/src/templates/langs/vue/templates/composables/useFetch.template.ts +54 -0
- package/src/templates/langs/vue/templates/composables/useForm.template.ts +127 -0
- package/src/templates/langs/vue/templates/composables/usePagination.template.ts +98 -0
- package/src/templates/langs/vue/templates/services/apiService.template.ts +116 -0
- package/src/templates/langs/vue/templates/ui/Button.template.vue +79 -0
- package/src/templates/langs/vue/templates/ui/Card.template.vue +73 -0
- package/src/templates/langs/vue/templates/ui/DataTable.template.vue +115 -0
- package/src/templates/langs/vue/templates/ui/Input.template.vue +70 -0
- package/src/templates/langs/vue/templates/ui/Modal.template.vue +112 -0
- package/src/templates/langs/vue/templates/ui/Select.template.vue +77 -0
- package/src/templates/scripts/log-actividad.sh +32 -0
- package/src/templates/scripts/log-commit.sh +35 -0
- package/src/templates/scripts/log-error.sh +45 -0
- package/src/templates/scripts/validate.sh +23 -0
- package/src/ts-transformer/index.cjs +86 -0
- package/src/utils/ai.js +35 -53
- package/src/utils/annotations.js +260 -214
- package/src/utils/config.js +61 -13
- package/src/utils/error-learner.js +203 -0
- package/src/utils/file-utils.js +119 -0
- package/src/utils/language-loader.js +167 -0
- package/src/utils/template-utils.js +45 -0
- package/src/vite-plugin/index.js +54 -0
- package/vscode-extension/package.json +23 -2
- package/vscode-extension/snippets/promptlang.json +1 -3
- 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
|
+
}
|