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,98 @@
1
+ // @template
2
+ // @id: composable-usePagination
3
+ // @category: composable/general
4
+ // @purpose: paginated-list, table-pagination, infinite-scroll
5
+ // @framework: vue
6
+ // @style: none
7
+ // @darkMode: false
8
+ // @responsive: true
9
+ // @deps: [vue]
10
+ // @states: [first-page, middle-page, last-page, single-page]
11
+ // @example: const { page, pages, goTo, hasNext } = usePagination({ total: 100, perPage: 10 })
12
+ // @teachMe
13
+ //
14
+ // @goodPractice: Show ellipsis for large page ranges
15
+ // @goodPractice: Keep siblingCount configurable for UX tuning
16
+ // @goodPractice: Support both client-side and server-side modes
17
+
18
+ import { ref, computed } from "vue"
19
+
20
+ interface PaginationOptions {
21
+ total: number
22
+ perPage?: number
23
+ initialPage?: number
24
+ siblingCount?: number
25
+ }
26
+
27
+ export function usePagination(options: PaginationOptions) {
28
+ const { total, perPage = 10, initialPage = 1, siblingCount = 1 } = options
29
+
30
+ const currentPage = ref(initialPage)
31
+ const totalPages = Math.max(1, Math.ceil(total / perPage))
32
+
33
+ const hasPrev = computed(() => currentPage.value > 1)
34
+ const hasNext = computed(() => currentPage.value < totalPages)
35
+
36
+ const pages = computed(() => {
37
+ const totalPageNumbers = siblingCount * 2 + 5
38
+ if (totalPages <= totalPageNumbers) {
39
+ return Array.from({ length: totalPages }, (_, i) => i + 1)
40
+ }
41
+
42
+ const leftSiblingIndex = Math.max(currentPage.value - siblingCount, 1)
43
+ const rightSiblingIndex = Math.min(currentPage.value + siblingCount, totalPages)
44
+
45
+ const showLeftEllipsis = leftSiblingIndex > 2
46
+ const showRightEllipsis = rightSiblingIndex < totalPages - 1
47
+
48
+ if (!showLeftEllipsis && showRightEllipsis) {
49
+ const leftItemCount = 3 + 2 * siblingCount
50
+ const leftRange = Array.from({ length: leftItemCount }, (_, i) => i + 1)
51
+ return [...leftRange, "...", totalPages]
52
+ }
53
+
54
+ if (showLeftEllipsis && !showRightEllipsis) {
55
+ const rightItemCount = 3 + 2 * siblingCount
56
+ const rightRange = Array.from({ length: rightItemCount }, (_, i) => totalPages - rightItemCount + i + 1)
57
+ return [1, "...", ...rightRange]
58
+ }
59
+
60
+ const middleRange = Array.from(
61
+ { length: rightSiblingIndex - leftSiblingIndex + 1 },
62
+ (_, i) => leftSiblingIndex + i
63
+ )
64
+ return [1, "...", ...middleRange, "...", totalPages]
65
+ })
66
+
67
+ const startItem = computed(() => (currentPage.value - 1) * perPage + 1)
68
+ const endItem = computed(() => Math.min(currentPage.value * perPage, total))
69
+
70
+ function goTo(page: number) {
71
+ if (page >= 1 && page <= totalPages) {
72
+ currentPage.value = page
73
+ }
74
+ }
75
+
76
+ function next() {
77
+ if (hasNext.value) goTo(currentPage.value + 1)
78
+ }
79
+
80
+ function prev() {
81
+ if (hasPrev.value) goTo(currentPage.value - 1)
82
+ }
83
+
84
+ return {
85
+ currentPage,
86
+ totalPages,
87
+ pages,
88
+ hasPrev,
89
+ hasNext,
90
+ startItem,
91
+ endItem,
92
+ total,
93
+ perPage,
94
+ goTo,
95
+ next,
96
+ prev,
97
+ }
98
+ }
@@ -0,0 +1,116 @@
1
+ // @template
2
+ // @id: service-api-vue
3
+ // @category: service/api
4
+ // @purpose: crud, rest-api, http-client, data-fetching
5
+ // @framework: vue
6
+ // @style: none
7
+ // @darkMode: false
8
+ // @responsive: false
9
+ // @deps: [vue]
10
+ // @states: [idle, loading, error, success]
11
+ // @example: const { get, post, put, del } = createApi('/api/users')
12
+ // @teachMe
13
+ //
14
+ // @goodPractice: Centralize API config (base URL, headers, timeout) in one factory
15
+ // @goodPractice: Use typed generics for request/response types
16
+ // @goodPractice: Handle token refresh transparently with interceptors
17
+ // @badPractice: Don't hardcode API URLs in components — use this service
18
+
19
+ interface ApiConfig {
20
+ baseUrl: string
21
+ headers?: Record<string, string>
22
+ timeout?: number
23
+ getToken?: () => string | null
24
+ }
25
+
26
+ interface ApiResponse<T> {
27
+ data: T | null
28
+ error: string | null
29
+ status: number
30
+ }
31
+
32
+ class ApiError extends Error {
33
+ constructor(
34
+ message: string,
35
+ public status: number,
36
+ public data?: unknown
37
+ ) {
38
+ super(message)
39
+ this.name = "ApiError"
40
+ }
41
+ }
42
+
43
+ export function createApi<T = unknown>(config: ApiConfig) {
44
+ const { baseUrl, timeout = 10000, getToken } = config
45
+
46
+ async function request<TResponse>(
47
+ endpoint: string,
48
+ options: RequestInit = {}
49
+ ): Promise<ApiResponse<TResponse>> {
50
+ const controller = new AbortController()
51
+ const timeoutId = setTimeout(() => controller.abort(), timeout)
52
+
53
+ const headers: Record<string, string> = {
54
+ "Content-Type": "application/json",
55
+ ...config.headers,
56
+ }
57
+
58
+ const token = getToken?.()
59
+ if (token) {
60
+ headers["Authorization"] = `Bearer ${token}`
61
+ }
62
+
63
+ try {
64
+ const response = await fetch(`${baseUrl}${endpoint}`, {
65
+ ...options,
66
+ headers: { ...headers, ...options.headers },
67
+ signal: controller.signal,
68
+ })
69
+
70
+ clearTimeout(timeoutId)
71
+
72
+ if (!response.ok) {
73
+ const errorData = await response.json().catch(() => null)
74
+ throw new ApiError(
75
+ `HTTP ${response.status}: ${response.statusText}`,
76
+ response.status,
77
+ errorData
78
+ )
79
+ }
80
+
81
+ const data = await response.json()
82
+ return { data: data as TResponse, error: null, status: response.status }
83
+ } catch (e) {
84
+ clearTimeout(timeoutId)
85
+ if (e instanceof ApiError) {
86
+ return { data: null, error: e.message, status: e.status }
87
+ }
88
+ const message = e instanceof Error ? e.message : "Unknown error"
89
+ return { data: null, error: message, status: 0 }
90
+ }
91
+ }
92
+
93
+ function get<TResponse>(endpoint: string) {
94
+ return request<TResponse>(endpoint, { method: "GET" })
95
+ }
96
+
97
+ function post<TBody, TResponse>(endpoint: string, body: TBody) {
98
+ return request<TResponse>(endpoint, {
99
+ method: "POST",
100
+ body: JSON.stringify(body),
101
+ })
102
+ }
103
+
104
+ function put<TBody, TResponse>(endpoint: string, body: TBody) {
105
+ return request<TResponse>(endpoint, {
106
+ method: "PUT",
107
+ body: JSON.stringify(body),
108
+ })
109
+ }
110
+
111
+ function del<TResponse>(endpoint: string) {
112
+ return request<TResponse>(endpoint, { method: "DELETE" })
113
+ }
114
+
115
+ return { get, post, put, del, request }
116
+ }
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ // @template
3
+ // @id: button-shadcn-vue
4
+ // @category: ui/button
5
+ // @purpose: primary-action, confirmation, danger
6
+ // @framework: vue
7
+ // @style: shadcn-vue
8
+ // @variants: [primary, secondary, danger, ghost, outline]
9
+ // @sizes: [sm, default, lg, icon]
10
+ // @darkMode: true
11
+ // @responsive: true
12
+ // @deps: [vue]
13
+ // @props: { variant: string, size: string, disabled?: boolean, loading?: boolean }
14
+ // @states: [idle, loading, disabled, hover, active, focus-visible]
15
+ // @example: <Button variant="primary" size="default" :loading="true">Save</Button>
16
+ // @teachMe
17
+ //
18
+ // ## Anatomy
19
+ // Vue 3 button using Composition API with Tailwind CSS variant classes.
20
+ // Loading state disables interaction and shows a spinner.
21
+ // Dark mode uses Tailwind's `dark:` modifier.
22
+ //
23
+ // @goodPractice: Use defineProps with TypeScript for type-safe props
24
+ // @goodPractice: Use type="button" to prevent form submission
25
+ // @goodPractice: Emit typed events with defineEmits
26
+ // @badPractice: Don't use v-html with user content — XSS risk
27
+ // @badPractice: Don't omit type="button" — causes form submission
28
+
29
+ import { computed } from "vue"
30
+
31
+ const props = withDefaults(defineProps<{
32
+ variant?: "primary" | "secondary" | "danger" | "ghost" | "outline"
33
+ size?: "sm" | "default" | "lg" | "icon"
34
+ disabled?: boolean
35
+ loading?: boolean
36
+ }>(), {
37
+ variant: "primary",
38
+ size: "default",
39
+ disabled: false,
40
+ loading: false,
41
+ })
42
+
43
+ const emit = defineEmits<{
44
+ click: [event: MouseEvent]
45
+ }>()
46
+
47
+ const classes = computed(() => {
48
+ const base = "inline-flex items-center justify-center gap-2 rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
49
+
50
+ const variants: Record<string, string> = {
51
+ primary: "bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600",
52
+ secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700",
53
+ danger: "bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600",
54
+ ghost: "hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-100",
55
+ outline: "border border-gray-300 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800",
56
+ }
57
+
58
+ const sizes: Record<string, string> = {
59
+ sm: "h-8 px-3 text-xs",
60
+ default: "h-10 px-4 py-2",
61
+ lg: "h-12 px-6 text-base",
62
+ icon: "h-10 w-10",
63
+ }
64
+
65
+ return [base, variants[props.variant], sizes[props.size]].join(" ")
66
+ })
67
+ </script>
68
+
69
+ <template>
70
+ <button
71
+ type="button"
72
+ :class="classes"
73
+ :disabled="disabled || loading"
74
+ @click="emit('click', $event)"
75
+ >
76
+ <span v-if="loading" class="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
77
+ <slot />
78
+ </button>
79
+ </template>
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ // @template
3
+ // @id: card-shadcn-vue
4
+ // @category: ui/data-display
5
+ // @purpose: container, summary, feature-card, dashboard-card
6
+ // @framework: vue
7
+ // @style: shadcn-vue
8
+ // @variants: [default, bordered, elevated, interactive]
9
+ // @sizes: [sm, default, full]
10
+ // @darkMode: true
11
+ // @responsive: true
12
+ // @deps: [vue]
13
+ // @props: { title?: string, subtitle?: string, variant: string }
14
+ // @states: [idle, hover (interactive)]
15
+ // @example: <Card title="Welcome" subtitle="Get started" variant="elevated">...</Card>
16
+ // @teachMe
17
+ //
18
+ // ## Anatomy
19
+ // Flexible card container with header, body, and footer slots.
20
+ // Supports multiple visual variants via Tailwind classes.
21
+ //
22
+ // @goodPractice: Use named slots for customizable layout
23
+ // @goodPractice: Use scoped styles or Tailwind for consistent theming
24
+ // @badPractice: Don't nest cards inside cards — creates visual noise
25
+
26
+ import { computed } from "vue"
27
+
28
+ const props = withDefaults(defineProps<{
29
+ title?: string
30
+ subtitle?: string
31
+ variant?: "default" | "bordered" | "elevated" | "interactive"
32
+ size?: "sm" | "default" | "full"
33
+ }>(), {
34
+ variant: "default",
35
+ size: "default",
36
+ })
37
+
38
+ const cardClasses = computed(() => {
39
+ const base = "rounded-xl bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
40
+
41
+ const variants: Record<string, string> = {
42
+ default: "shadow-sm",
43
+ bordered: "border border-gray-200 dark:border-gray-700",
44
+ elevated: "shadow-lg",
45
+ interactive: "shadow-sm hover:shadow-md transition-shadow cursor-pointer border border-gray-200 dark:border-gray-700",
46
+ }
47
+
48
+ const sizes: Record<string, string> = {
49
+ sm: "max-w-sm",
50
+ default: "max-w-md",
51
+ full: "w-full",
52
+ }
53
+
54
+ return [base, variants[props.variant], sizes[props.size]].join(" ")
55
+ })
56
+ </script>
57
+
58
+ <template>
59
+ <div :class="cardClasses">
60
+ <div v-if="title || subtitle || $slots.header" class="flex flex-col gap-1 p-6 pb-0">
61
+ <slot name="header">
62
+ <h3 v-if="title" class="text-lg font-semibold">{{ title }}</h3>
63
+ <p v-if="subtitle" class="text-sm text-gray-500 dark:text-gray-400">{{ subtitle }}</p>
64
+ </slot>
65
+ </div>
66
+ <div class="p-6">
67
+ <slot />
68
+ </div>
69
+ <div v-if="$slots.footer" class="border-t border-gray-200 p-6 pt-4 dark:border-gray-700">
70
+ <slot name="footer" />
71
+ </div>
72
+ </div>
73
+ </template>
@@ -0,0 +1,115 @@
1
+ <script setup lang="ts">
2
+ // @template
3
+ // @id: datatable-shadcn-vue
4
+ // @category: ui/data-display
5
+ // @purpose: data-table, sortable-table, paginated-list
6
+ // @framework: vue
7
+ // @style: shadcn-vue
8
+ // @variants: [default, compact, striped, sortable]
9
+ // @sizes: [default, compact]
10
+ // @darkMode: true
11
+ // @responsive: true
12
+ // @deps: [vue]
13
+ // @props: { columns: Column[], data: T[], loading?: boolean, sortable?: boolean }
14
+ // @states: [loading, empty, populated, error]
15
+ // @example: <DataTable :columns="cols" :data="users" :loading="loading" />
16
+ // @teachMe
17
+ //
18
+ // ## Anatomy
19
+ // Generic typed data table with sorting, loading skeleton,
20
+ // empty state, and responsive scroll.
21
+ //
22
+ // @goodPractice: Use generic type parameter for type-safe columns and data
23
+ // @goodPractice: Show skeleton loader during loading for perceived performance
24
+ // @goodPractice: Use sticky header for long tables
25
+
26
+ import { computed, ref } from "vue"
27
+
28
+ export interface Column<T = Record<string, unknown>> {
29
+ key: keyof T & string
30
+ label: string
31
+ sortable?: boolean
32
+ width?: string
33
+ }
34
+
35
+ const props = withDefaults(defineProps<{
36
+ columns: Column[]
37
+ data: Record<string, unknown>[]
38
+ loading?: boolean
39
+ variant?: "default" | "compact" | "striped" | "sortable"
40
+ emptyMessage?: string
41
+ }>(), {
42
+ loading: false,
43
+ variant: "default",
44
+ emptyMessage: "No data available",
45
+ })
46
+
47
+ const sortKey = ref<string | null>(null)
48
+ const sortDir = ref<"asc" | "desc">("asc")
49
+
50
+ const sortedData = computed(() => {
51
+ if (!sortKey.value) return props.data
52
+ return [...props.data].sort((a, b) => {
53
+ const aVal = a[sortKey.value!]
54
+ const bVal = b[sortKey.value!]
55
+ if (aVal < bVal) return sortDir.value === "asc" ? -1 : 1
56
+ if (aVal > bVal) return sortDir.value === "asc" ? 1 : -1
57
+ return 0
58
+ })
59
+ })
60
+
61
+ function toggleSort(key: string) {
62
+ if (sortKey.value === key) {
63
+ sortDir.value = sortDir.value === "asc" ? "desc" : "asc"
64
+ } else {
65
+ sortKey.value = key
66
+ sortDir.value = "asc"
67
+ }
68
+ }
69
+ </script>
70
+
71
+ <template>
72
+ <div class="w-full overflow-x-auto rounded-xl border border-gray-200 dark:border-gray-700">
73
+ <table class="w-full text-left text-sm">
74
+ <thead class="bg-gray-50 dark:bg-gray-800">
75
+ <tr>
76
+ <th
77
+ v-for="col in columns"
78
+ :key="col.key"
79
+ :style="{ width: col.width }"
80
+ class="px-4 py-3 font-medium text-gray-500 dark:text-gray-400"
81
+ :class="{ 'cursor-pointer select-none hover:text-gray-700 dark:hover:text-gray-200': col.sortable }"
82
+ @click="col.sortable && toggleSort(col.key)"
83
+ >
84
+ {{ col.label }}
85
+ <span v-if="col.sortable && sortKey === col.key" class="ml-1">
86
+ {{ sortDir === "asc" ? "↑" : "↓" }}
87
+ </span>
88
+ </th>
89
+ </tr>
90
+ </thead>
91
+ <tbody>
92
+ <tr v-if="loading">
93
+ <td :colspan="columns.length" class="p-4">
94
+ <div v-for="i in 3" :key="i" class="mb-2 h-4 animate-pulse rounded bg-gray-200 dark:bg-gray-700" style="width: 80%" />
95
+ </td>
96
+ </tr>
97
+ <tr v-else-if="sortedData.length === 0">
98
+ <td :colspan="columns.length" class="p-8 text-center text-gray-400">
99
+ {{ emptyMessage }}
100
+ </td>
101
+ </tr>
102
+ <tr
103
+ v-for="(row, i) in sortedData"
104
+ :key="i"
105
+ class="border-t border-gray-200 dark:border-gray-700"
106
+ :class="{ 'bg-gray-50 dark:bg-gray-800/50': variant === 'striped' && i % 2 === 0 }"
107
+ >
108
+ <td v-for="col in columns" :key="col.key" class="px-4 py-3">
109
+ {{ row[col.key] }}
110
+ </td>
111
+ </tr>
112
+ </tbody>
113
+ </table>
114
+ </div>
115
+ </template>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ // @template
3
+ // @id: input-shadcn-vue
4
+ // @category: ui/form
5
+ // @purpose: text-input, search, email, password, number
6
+ // @framework: vue
7
+ // @style: shadcn-vue
8
+ // @variants: [default, error, flushed]
9
+ // @sizes: [sm, default, lg]
10
+ // @darkMode: true
11
+ // @responsive: true
12
+ // @deps: [vue]
13
+ // @props: { label: string, error?: string, helperText?: string, icon?: Component }
14
+ // @states: [idle, focused, filled, error, disabled]
15
+ // @example: <Input label="Email" v-model="email" error="Required" />
16
+ // @teachMe
17
+ //
18
+ // ## Anatomy
19
+ // Wraps native input with label, error message, helper text, and optional icon.
20
+ // Uses v-model for two-way binding with Vue 3 Composition API.
21
+ //
22
+ // @goodPractice: Use v-model with defineModel for two-way binding
23
+ // @goodPractice: Forward $attrs via useAttrs for native HTML attribute support
24
+ // @goodPractice: Always associate label with input via :id
25
+ // @badPractice: Don't use v-model on the same element twice
26
+
27
+ import { computed, useAttrs } from "vue"
28
+
29
+ const model = defineModel<string>({ default: "" })
30
+
31
+ const props = withDefaults(defineProps<{
32
+ label?: string
33
+ error?: string
34
+ helperText?: string
35
+ placeholder?: string
36
+ type?: string
37
+ disabled?: boolean
38
+ }>(), {
39
+ type: "text",
40
+ disabled: false,
41
+ })
42
+
43
+ const attrs = useAttrs()
44
+
45
+ const inputClasses = computed(() => {
46
+ const base = "flex h-10 w-full rounded-lg border bg-white px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-950 dark:text-gray-100"
47
+ const borderColor = props.error
48
+ ? "border-red-500 focus-visible:ring-red-500"
49
+ : "border-gray-300 focus-visible:ring-blue-500 dark:border-gray-600"
50
+ return [base, borderColor].join(" ")
51
+ })
52
+ </script>
53
+
54
+ <template>
55
+ <div class="flex flex-col gap-1.5">
56
+ <label v-if="label" :for="attrs.id as string" class="text-sm font-medium text-gray-700 dark:text-gray-300">
57
+ {{ label }}
58
+ </label>
59
+ <input
60
+ v-model="model"
61
+ :type="type"
62
+ :placeholder="placeholder"
63
+ :disabled="disabled"
64
+ :class="inputClasses"
65
+ v-bind="attrs"
66
+ />
67
+ <p v-if="error" class="text-sm text-red-500">{{ error }}</p>
68
+ <p v-else-if="helperText" class="text-sm text-gray-400">{{ helperText }}</p>
69
+ </div>
70
+ </template>
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ // @template
3
+ // @id: modal-dialog-vue
4
+ // @category: ui/modal
5
+ // @purpose: confirm-action, form-dialog, alert, fullscreen
6
+ // @framework: vue
7
+ // @style: shadcn-vue
8
+ // @variants: [dialog, confirm-destructive, fullscreen]
9
+ // @sizes: [sm, default, lg, xl, fullscreen]
10
+ // @darkMode: true
11
+ // @responsive: true
12
+ // @deps: [vue]
13
+ // @props: { open: boolean, title?: string }
14
+ // @states: [closed, open, closing]
15
+ // @example: <Modal v-model:open="showModal" title="Confirm">...</Modal>
16
+ // @teachMe
17
+ //
18
+ // ## Anatomy
19
+ // Modal dialog with overlay, close-on-escape, close-on-overlay-click,
20
+ // focus trap, and portal rendering via Teleport.
21
+ // Uses v-model:open for controlled visibility.
22
+ //
23
+ // @goodPractice: Use Teleport to render modal outside app root
24
+ // @goodPractice: Trap focus inside modal for keyboard accessibility
25
+ // @goodPractice: Close on Escape key and overlay click for UX
26
+ // @badPractice: Don't render modal inline — z-index stacking issues
27
+
28
+ import { watch, onMounted, onUnmounted } from "vue"
29
+
30
+ const props = withDefaults(defineProps<{
31
+ open?: boolean
32
+ title?: string
33
+ size?: "sm" | "default" | "lg" | "xl" | "fullscreen"
34
+ closeOnOverlay?: boolean
35
+ }>(), {
36
+ open: false,
37
+ size: "default",
38
+ closeOnOverlay: true,
39
+ })
40
+
41
+ const emit = defineEmits<{
42
+ close: []
43
+ "update:open": [value: boolean]
44
+ }>()
45
+
46
+ const sizeClasses: Record<string, string> = {
47
+ sm: "max-w-sm",
48
+ default: "max-w-lg",
49
+ lg: "max-w-2xl",
50
+ xl: "max-w-4xl",
51
+ fullscreen: "max-w-[95vw] h-[95vh]",
52
+ }
53
+
54
+ function handleEscape(e: KeyboardEvent) {
55
+ if (e.key === "Escape" && props.open) {
56
+ emit("close")
57
+ emit("update:open", false)
58
+ }
59
+ }
60
+
61
+ onMounted(() => document.addEventListener("keydown", handleEscape))
62
+ onUnmounted(() => document.removeEventListener("keydown", handleEscape))
63
+
64
+ function onOverlayClick() {
65
+ if (props.closeOnOverlay) {
66
+ emit("close")
67
+ emit("update:open", false)
68
+ }
69
+ }
70
+ </script>
71
+
72
+ <template>
73
+ <Teleport to="body">
74
+ <Transition name="modal">
75
+ <div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center">
76
+ <div class="fixed inset-0 bg-black/50" @click="onOverlayClick" />
77
+ <div
78
+ :class="['relative z-10 w-full rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900', sizeClasses[size]]"
79
+ role="dialog"
80
+ aria-modal="true"
81
+ >
82
+ <div class="mb-4 flex items-center justify-between">
83
+ <h2 v-if="title" class="text-lg font-semibold">{{ title }}</h2>
84
+ <button
85
+ type="button"
86
+ class="rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800"
87
+ @click="emit('close'); emit('update:open', false)"
88
+ >
89
+ <span class="sr-only">Close</span>
90
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
91
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
92
+ </svg>
93
+ </button>
94
+ </div>
95
+ <slot />
96
+ </div>
97
+ </div>
98
+ </Transition>
99
+ </Teleport>
100
+ </template>
101
+
102
+ <style scoped>
103
+ .modal-enter-active, .modal-leave-active {
104
+ transition: opacity 0.2s ease;
105
+ }
106
+ .modal-enter-active > div:last-child, .modal-leave-active > div:last-child {
107
+ transition: transform 0.2s ease;
108
+ }
109
+ .modal-enter-from, .modal-leave-to { opacity: 0; }
110
+ .modal-enter-from > div:last-child { transform: scale(0.95); }
111
+ .modal-leave-to > div:last-child { transform: scale(0.95); }
112
+ </style>