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,146 @@
1
+ // @template
2
+ // @id: hook-useForm
3
+ // @category: ui/form
4
+ // @purpose: form-state, validation, submission, async-submit
5
+ // @framework: react
6
+ // @style: generic
7
+ // @variants: [controlled, uncontrolled]
8
+ // @sizes: []
9
+ // @darkMode: false
10
+ // @responsive: false
11
+ // @deps: []
12
+ // @props: { initialValues: T, validate?: (values: T) => Partial<Record<keyof T, string>>, onSubmit: (values: T) => Promise<void> }
13
+ // @states: [idle, validating, submitting, success, error]
14
+ // @example: const { values, errors, handleChange, handleSubmit, isSubmitting } = useForm({ initialValues: { email: '' }, onSubmit: handleSave })
15
+ // @teachMe
16
+ //
17
+ // ## Anatomy
18
+ // Lightweight form state manager without external dependencies.
19
+ // Handles field values, validation errors, touched state, and async submission.
20
+ // Use `handleChange` for input onChange, `handleSubmit` for form onSubmit.
21
+ // Validate function receives current values and returns error map.
22
+ //
23
+ // @goodPractice: Validate on blur (touched) + on submit — not on every keystroke
24
+ // @goodPractice: Disable submit button while isSubmitting to prevent double-submit
25
+ // @goodPractice: Set focus to first error field after validation for UX
26
+ // @goodPractice: Use generic <T> for type-safe field names and values
27
+ // @badPractice: Don't store entire form state in a single useState — use useReducer for complex forms
28
+ // @badPractice: Don't call setValues for every keystroke on large forms — batch updates
29
+ // @badPractice: Don't forget to reset form after successful submission
30
+
31
+ import { useState, useCallback, type FormEvent, type ChangeEvent } from "react"
32
+
33
+ type ValidationErrors<T> = Partial<Record<keyof T, string>>
34
+
35
+ interface UseFormOptions<T extends Record<string, unknown>> {
36
+ initialValues: T
37
+ validate?: (values: T) => ValidationErrors<T>
38
+ onSubmit: (values: T) => Promise<void> | void
39
+ }
40
+
41
+ interface UseFormReturn<T> {
42
+ values: T
43
+ errors: ValidationErrors<T>
44
+ touched: Partial<Record<keyof T, boolean>>
45
+ isSubmitting: boolean
46
+ isDirty: boolean
47
+ handleChange: (field: keyof T) => (e: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => void
48
+ handleSubmit: (e: FormEvent) => Promise<void>
49
+ setFieldValue: (field: keyof T, value: unknown) => void
50
+ setFieldTouched: (field: keyof T) => void
51
+ resetForm: (newValues?: T) => void
52
+ setErrors: (errors: ValidationErrors<T>) => void
53
+ }
54
+
55
+ function useForm<T extends Record<string, unknown>>({
56
+ initialValues,
57
+ validate,
58
+ onSubmit,
59
+ }: UseFormOptions<T>): UseFormReturn<T> {
60
+ const [values, setValues] = useState<T>(initialValues)
61
+ const [errors, setErrors] = useState<ValidationErrors<T>>({})
62
+ const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({})
63
+ const [isSubmitting, setIsSubmitting] = useState(false)
64
+
65
+ const isDirty = Object.keys(touched).length > 0
66
+
67
+ const handleChange = useCallback(
68
+ (field: keyof T) =>
69
+ (e: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
70
+ const { value, type, checked } = e.target
71
+ setValues((prev) => ({
72
+ ...prev,
73
+ [field]: type === "checkbox" ? checked : value,
74
+ }))
75
+ },
76
+ []
77
+ )
78
+
79
+ const handleSubmit = useCallback(
80
+ async (e: FormEvent) => {
81
+ e.preventDefault()
82
+
83
+ if (validate) {
84
+ const validationErrors = validate(values)
85
+ setErrors(validationErrors)
86
+ const allTouched = Object.keys(values).reduce(
87
+ (acc, key) => ({ ...acc, [key]: true }),
88
+ {} as Partial<Record<keyof T, boolean>>
89
+ )
90
+ setTouched(allTouched)
91
+
92
+ if (Object.keys(validationErrors).length > 0) {
93
+ const firstErrorField = Object.keys(validationErrors)[0] as keyof T
94
+ document.querySelector<HTMLElement>(`[name="${String(firstErrorField)}"]`)?.focus()
95
+ return
96
+ }
97
+ }
98
+
99
+ setIsSubmitting(true)
100
+ try {
101
+ await onSubmit(values)
102
+ } finally {
103
+ setIsSubmitting(false)
104
+ }
105
+ },
106
+ [values, validate, onSubmit]
107
+ )
108
+
109
+ const setFieldValue = useCallback((field: keyof T, value: unknown) => {
110
+ setValues((prev) => ({ ...prev, [field]: value as T[keyof T] }))
111
+ }, [])
112
+
113
+ const setFieldTouched = useCallback((field: keyof T) => {
114
+ setTouched((prev) => ({ ...prev, [field]: true }))
115
+ if (validate) {
116
+ setValues((current) => {
117
+ const validationErrors = validate(current)
118
+ setErrors((prev) => ({ ...prev, ...validationErrors }))
119
+ return current
120
+ })
121
+ }
122
+ }, [validate])
123
+
124
+ const resetForm = useCallback((newValues?: T) => {
125
+ setValues(newValues || initialValues)
126
+ setErrors({})
127
+ setTouched({})
128
+ setIsSubmitting(false)
129
+ }, [initialValues])
130
+
131
+ return {
132
+ values,
133
+ errors,
134
+ touched,
135
+ isSubmitting,
136
+ isDirty,
137
+ handleChange,
138
+ handleSubmit,
139
+ setFieldValue,
140
+ setFieldTouched,
141
+ resetForm,
142
+ setErrors,
143
+ }
144
+ }
145
+
146
+ export { useForm }
@@ -0,0 +1,108 @@
1
+ // @template
2
+ // @id: hook-usePagination
3
+ // @category: hook/general
4
+ // @purpose: paginated-list, table-pagination, infinite-scroll
5
+ // @framework: react
6
+ // @style: generic
7
+ // @variants: [client-side, server-side]
8
+ // @sizes: []
9
+ // @darkMode: false
10
+ // @responsive: true
11
+ // @deps: []
12
+ // @props: { totalItems: number, pageSize?: number, initialPage?: number }
13
+ // @states: [first-page, middle-page, last-page, single-page, loading]
14
+ // @example: const { page, setPage, totalPages, pageButtons } = usePagination({ totalItems: 100 })
15
+ // @teachMe
16
+ //
17
+ // ## Anatomy
18
+ // Calculates pagination state given total items and page size. Returns page number, total pages,
19
+ // and a `pageButtons` array for rendering pagination UI (with ellipsis for large page counts).
20
+ // Server-side variant assumes totalItems is known from API response.
21
+ //
22
+ // @goodPractice: Show first + last page always, use ellipsis for gaps (1 ... 4 5 6 ... 20)
23
+ // @goodPractice: Keep pageSize in URL search params for shareable URLs
24
+ // @goodPractice: Disable prev on first page, next on last page
25
+ // @badPractice: Don't render all page buttons for 100+ pages — use ellipsis
26
+ // @badPractice: Don't reset page to 1 on every data change — breaks UX
27
+ // @badPractice: Don't use client-side pagination for > 10k rows — use server-side
28
+
29
+ import { useState, useMemo } from "react"
30
+
31
+ interface UsePaginationOptions {
32
+ totalItems: number
33
+ pageSize?: number
34
+ initialPage?: number
35
+ siblingCount?: number
36
+ }
37
+
38
+ interface UsePaginationReturn {
39
+ page: number
40
+ setPage: (page: number) => void
41
+ totalPages: number
42
+ pageSize: number
43
+ hasPrev: boolean
44
+ hasNext: boolean
45
+ pageButtons: (number | "ellipsis")[]
46
+ from: number
47
+ to: number
48
+ nextPage: () => void
49
+ prevPage: () => void
50
+ }
51
+
52
+ function usePagination({
53
+ totalItems,
54
+ pageSize = 20,
55
+ initialPage = 1,
56
+ siblingCount = 1,
57
+ }: UsePaginationOptions): UsePaginationReturn {
58
+ const [page, setPage] = useState(initialPage)
59
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize))
60
+
61
+ const safePage = Math.min(page, totalPages)
62
+
63
+ const pageButtons = useMemo<(number | "ellipsis")[]>(() => {
64
+ const totalPageNumbers = siblingCount + 5
65
+ if (totalPages <= totalPageNumbers) {
66
+ return Array.from({ length: totalPages }, (_, i) => i + 1)
67
+ }
68
+
69
+ const leftSiblingIndex = Math.max(safePage - siblingCount, 1)
70
+ const rightSiblingIndex = Math.min(safePage + siblingCount, totalPages)
71
+ const showLeftEllipsis = leftSiblingIndex > 2
72
+ const showRightEllipsis = rightSiblingIndex < totalPages - 1
73
+
74
+ if (!showLeftEllipsis && showRightEllipsis) {
75
+ const leftCount = 3 + 2 * siblingCount
76
+ const leftRange = Array.from({ length: leftCount }, (_, i) => i + 1)
77
+ return [...leftRange, "ellipsis", totalPages]
78
+ }
79
+
80
+ if (showLeftEllipsis && !showRightEllipsis) {
81
+ const rightCount = 3 + 2 * siblingCount
82
+ const rightRange = Array.from({ length: rightCount }, (_, i) => totalPages - rightCount + i + 1)
83
+ return [1, "ellipsis", ...rightRange]
84
+ }
85
+
86
+ const middleRange = Array.from(
87
+ { length: rightSiblingIndex - leftSiblingIndex + 1 },
88
+ (_, i) => leftSiblingIndex + i
89
+ )
90
+ return [1, "ellipsis", ...middleRange, "ellipsis", totalPages]
91
+ }, [totalPages, safePage, siblingCount])
92
+
93
+ return {
94
+ page: safePage,
95
+ setPage: (p: number) => setPage(Math.max(1, Math.min(p, totalPages))),
96
+ totalPages,
97
+ pageSize,
98
+ hasPrev: safePage > 1,
99
+ hasNext: safePage < totalPages,
100
+ pageButtons,
101
+ from: (safePage - 1) * pageSize + 1,
102
+ to: Math.min(safePage * pageSize, totalItems),
103
+ nextPage: () => setPage((p) => Math.min(p + 1, totalPages)),
104
+ prevPage: () => setPage((p) => Math.max(p - 1, 1)),
105
+ }
106
+ }
107
+
108
+ export { usePagination }
@@ -0,0 +1,123 @@
1
+ // @template
2
+ // @id: service-api
3
+ // @category: service/api
4
+ // @purpose: crud, rest-api, http-client, data-fetching
5
+ // @framework: react
6
+ // @style: generic
7
+ // @variants: [rest, supabase]
8
+ // @sizes: []
9
+ // @darkMode: false
10
+ // @responsive: false
11
+ // @deps: []
12
+ // @props: { baseUrl: string, headers?: Record<string, string> }
13
+ // @states: [idle, loading, success, error]
14
+ // @example: const api = createApiClient('/api/users'); const users = await api.getAll()
15
+ // @teachMe
16
+ //
17
+ // ## Anatomy
18
+ // Generic API client for RESTful services. Supports CRUD operations with typed responses.
19
+ // Built on fetch() with automatic JSON parsing, error handling, and auth header injection.
20
+ // Request interceptor for adding tokens, response interceptor for 401 handling.
21
+ // Use with services/domain files that wrap specific endpoints.
22
+ //
23
+ // @goodPractice: Centralize fetch configuration — base URL, timeout, headers
24
+ // @goodPractice: Handle 401 globally — redirect to login, don't retry
25
+ // @goodPractice: Type all responses — never use `any` for API data
26
+ // @goodPractice: Use AbortController for cancelling requests on unmount
27
+ // @badPractice: Don't catch errors silently — always surface to UI or error boundary
28
+ // @badPractice: Don't store tokens in localStorage without httpOnly cookie fallback
29
+ // @badPractice: Don't retry 4xx errors — only retry 5xx (server) errors
30
+ // @fixHistory: AUTH-002 — removed debug console.log exposing auth tokens
31
+
32
+ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
33
+
34
+ interface ApiResponse<T> {
35
+ data: T | null
36
+ error: ApiError | null
37
+ status: number
38
+ }
39
+
40
+ interface ApiError {
41
+ message: string
42
+ status: number
43
+ code?: string
44
+ }
45
+
46
+ interface ApiClientConfig {
47
+ baseUrl: string
48
+ headers?: Record<string, string>
49
+ getToken?: () => string | null
50
+ onUnauthorized?: () => void
51
+ timeout?: number
52
+ }
53
+
54
+ function createApiClient(config: ApiClientConfig) {
55
+ const { baseUrl, headers = {}, getToken, onUnauthorized, timeout = 15000 } = config
56
+
57
+ async function request<T>(method: HttpMethod, path: string, body?: unknown): Promise<ApiResponse<T>> {
58
+ const controller = new AbortController()
59
+ const timer = setTimeout(() => controller.abort(), timeout)
60
+
61
+ try {
62
+ const token = getToken?.()
63
+ const response = await fetch(`${baseUrl}${path}`, {
64
+ method,
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
68
+ ...headers,
69
+ },
70
+ body: body ? JSON.stringify(body) : undefined,
71
+ signal: controller.signal,
72
+ })
73
+
74
+ clearTimeout(timer)
75
+
76
+ if (response.status === 401) {
77
+ onUnauthorized?.()
78
+ return {
79
+ data: null,
80
+ error: { message: "Unauthorized", status: 401, code: "UNAUTHORIZED" },
81
+ status: 401,
82
+ }
83
+ }
84
+
85
+ const json = await response.json().catch(() => null)
86
+
87
+ if (!response.ok) {
88
+ return {
89
+ data: null,
90
+ error: {
91
+ message: json?.message || json?.error || `Request failed with status ${response.status}`,
92
+ status: response.status,
93
+ code: json?.code,
94
+ },
95
+ status: response.status,
96
+ }
97
+ }
98
+
99
+ return { data: json as T, error: null, status: response.status }
100
+ } catch (err) {
101
+ clearTimeout(timer)
102
+ if (err instanceof DOMException && err.name === "AbortError") {
103
+ return { data: null, error: { message: "Request timed out", status: 408, code: "TIMEOUT" }, status: 408 }
104
+ }
105
+ return {
106
+ data: null,
107
+ error: { message: err instanceof Error ? err.message : "Network error", status: 0, code: "NETWORK_ERROR" },
108
+ status: 0,
109
+ }
110
+ }
111
+ }
112
+
113
+ return {
114
+ get: <T>(path: string) => request<T>("GET", path),
115
+ post: <T>(path: string, body: unknown) => request<T>("POST", path, body),
116
+ put: <T>(path: string, body: unknown) => request<T>("PUT", path, body),
117
+ patch: <T>(path: string, body: unknown) => request<T>("PATCH", path, body),
118
+ del: <T>(path: string) => request<T>("DELETE", path),
119
+ }
120
+ }
121
+
122
+ export { createApiClient }
123
+ export type { ApiResponse, ApiError, ApiClientConfig }
@@ -0,0 +1,87 @@
1
+ // @template
2
+ // @id: button-shadcn
3
+ // @category: ui/button
4
+ // @purpose: primary-action, confirmation, danger
5
+ // @framework: react
6
+ // @style: shadcn
7
+ // @variants: [primary, secondary, danger, ghost, outline]
8
+ // @sizes: [sm, default, lg, icon]
9
+ // @darkMode: true
10
+ // @responsive: true
11
+ // @deps: [class-variance-authority, lucide-react]
12
+ // @props: { variant: string, size: string, disabled?: boolean, loading?: boolean, children: ReactNode }
13
+ // @states: [idle, loading, disabled, hover, active, focus-visible]
14
+ // @example: <Button variant="primary" size="default" loading>Save</Button>
15
+ // @teachMe
16
+ //
17
+ // ## Anatomy
18
+ // This Button uses `cva` (class-variance-authority) for type-safe variant management.
19
+ // The loading state renders a spinner icon and disables interaction.
20
+ // Dark mode is handled via Tailwind's `dark:` modifier on the variant classes.
21
+ //
22
+ // @goodPractice: Use cva for variant props — gives autocomplete and runtime safety
23
+ // @goodPractice: Spread remaining props via ...rest for HTML attribute passthrough
24
+ // @goodPractice: Use pointer-events-none during loading to prevent double-clicks
25
+ // @badPractice: Don't omit type="button" — causes unintended form submissions (see BUTTON-001)
26
+ // @badPractice: Don't inline variant styles — breaks dark mode consistency
27
+ // @badPractice: Don't use isLoading boolean alone — use a state machine for complex flows
28
+ // @fixHistory: BUTTON-001 — added type="button" default to prevent form submit
29
+
30
+ import { cva, type VariantProps } from "class-variance-authority"
31
+ import { Loader2 } from "lucide-react"
32
+ import { memo } from "react"
33
+
34
+ const buttonVariants = cva(
35
+ "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",
36
+ {
37
+ variants: {
38
+ variant: {
39
+ primary:
40
+ "bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600",
41
+ secondary:
42
+ "bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700",
43
+ danger:
44
+ "bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600",
45
+ ghost:
46
+ "hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-100",
47
+ outline:
48
+ "border border-gray-300 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800",
49
+ },
50
+ size: {
51
+ sm: "h-8 px-3 text-xs",
52
+ default: "h-10 px-4 py-2",
53
+ lg: "h-12 px-6 text-base",
54
+ icon: "h-10 w-10",
55
+ },
56
+ },
57
+ defaultVariants: {
58
+ variant: "primary",
59
+ size: "default",
60
+ },
61
+ }
62
+ )
63
+
64
+ interface ButtonProps
65
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
66
+ VariantProps<typeof buttonVariants> {
67
+ loading?: boolean
68
+ }
69
+
70
+ const Button = memo<ButtonProps>(
71
+ ({ className, variant, size, loading = false, disabled, children, ...rest }) => {
72
+ return (
73
+ <button
74
+ type="button"
75
+ className={buttonVariants({ variant, size, className })}
76
+ disabled={disabled || loading}
77
+ {...rest}
78
+ >
79
+ {loading && <Loader2 className="h-4 w-4 animate-spin" />}
80
+ {children}
81
+ </button>
82
+ )
83
+ }
84
+ )
85
+ Button.displayName = "Button"
86
+
87
+ export { Button, buttonVariants }
@@ -0,0 +1,85 @@
1
+ // @template
2
+ // @id: card-shadcn
3
+ // @category: ui/data-display
4
+ // @purpose: container, summary, feature-card, dashboard-card
5
+ // @framework: react
6
+ // @style: shadcn
7
+ // @variants: [default, bordered, elevated, interactive]
8
+ // @sizes: [sm, default, full]
9
+ // @darkMode: true
10
+ // @responsive: true
11
+ // @deps: []
12
+ // @props: { title?: string, subtitle?: string, footer?: ReactNode, padding?: boolean }
13
+ // @states: [default, hover, loading]
14
+ // @example: <Card title="Revenue" subtitle="Last 30 days"><p>$12,400</p></Card>
15
+ // @teachMe
16
+ //
17
+ // ## Anatomy
18
+ // Card is a container component with optional header (title + subtitle) and footer.
19
+ // `interactive` variant adds hover elevation and cursor pointer for clickable cards.
20
+ // Can show a Skeleton placeholder when loading.
21
+ //
22
+ // @goodPractice: Use semantic padding scale (p-4, p-6) — never fixed widths
23
+ // @goodPractice: Interactive cards need role="button" + onKeyDown for keyboard a11y
24
+ // @goodPractice: Keep card content self-contained — no external margin dependencies
25
+ // @badPractice: Don't nest cards more than 2 levels deep — reduces readability
26
+ // @badPractice: Don't hard-code min-height on cards — let content determine size
27
+ // @badPractice: Don't use cards for simple text — they add visual noise
28
+
29
+ import { type ReactNode } from "react"
30
+
31
+ interface CardProps {
32
+ title?: string
33
+ subtitle?: string
34
+ footer?: ReactNode
35
+ children: ReactNode
36
+ variant?: "default" | "bordered" | "elevated" | "interactive"
37
+ padding?: boolean
38
+ className?: string
39
+ onClick?: () => void
40
+ }
41
+
42
+ const variantStyles: Record<string, string> = {
43
+ default: "bg-white dark:bg-gray-900 shadow-sm",
44
+ bordered: "border border-gray-200 dark:border-gray-700",
45
+ elevated: "bg-white dark:bg-gray-900 shadow-md",
46
+ interactive: "bg-white dark:bg-gray-900 shadow-sm hover:shadow-md cursor-pointer transition-shadow",
47
+ }
48
+
49
+ function Card({
50
+ title,
51
+ subtitle,
52
+ footer,
53
+ children,
54
+ variant = "default",
55
+ padding = true,
56
+ className = "",
57
+ onClick,
58
+ }: CardProps) {
59
+ const Tag = onClick ? "button" : "div"
60
+
61
+ return (
62
+ <Tag
63
+ className={`rounded-xl ${variantStyles[variant]} ${padding ? "p-6" : ""} ${className}`}
64
+ onClick={onClick}
65
+ {...(onClick ? { type: "button" as const } : {})}
66
+ >
67
+ {(title || subtitle) && (
68
+ <div className="mb-4">
69
+ {title && (
70
+ <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h3>
71
+ )}
72
+ {subtitle && (
73
+ <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
74
+ )}
75
+ </div>
76
+ )}
77
+ <div>{children}</div>
78
+ {footer && (
79
+ <div className="mt-4 border-t border-gray-100 pt-4 dark:border-gray-800">{footer}</div>
80
+ )}
81
+ </Tag>
82
+ )
83
+ }
84
+
85
+ export { Card }