specra 0.1.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 (142) hide show
  1. package/LICENSE.MD +21 -0
  2. package/README.md +157 -0
  3. package/dist/app/api/mdx-watch/route.d.mts +12 -0
  4. package/dist/app/api/mdx-watch/route.d.ts +12 -0
  5. package/dist/app/api/mdx-watch/route.js +98 -0
  6. package/dist/app/api/mdx-watch/route.js.map +1 -0
  7. package/dist/app/api/mdx-watch/route.mjs +71 -0
  8. package/dist/app/api/mdx-watch/route.mjs.map +1 -0
  9. package/dist/app/docs-page.d.mts +32 -0
  10. package/dist/app/docs-page.d.ts +32 -0
  11. package/dist/app/docs-page.js +4072 -0
  12. package/dist/app/docs-page.js.map +1 -0
  13. package/dist/app/docs-page.mjs +14 -0
  14. package/dist/app/docs-page.mjs.map +1 -0
  15. package/dist/app/layout.css +297 -0
  16. package/dist/app/layout.css.map +1 -0
  17. package/dist/app/layout.d.mts +19 -0
  18. package/dist/app/layout.d.ts +19 -0
  19. package/dist/app/layout.js +112 -0
  20. package/dist/app/layout.js.map +1 -0
  21. package/dist/app/layout.mjs +13 -0
  22. package/dist/app/layout.mjs.map +1 -0
  23. package/dist/chunk-DR4EPLMT.mjs +1013 -0
  24. package/dist/chunk-DR4EPLMT.mjs.map +1 -0
  25. package/dist/chunk-INL2EC72.mjs +170 -0
  26. package/dist/chunk-INL2EC72.mjs.map +1 -0
  27. package/dist/chunk-IZFGEAD6.mjs +61 -0
  28. package/dist/chunk-IZFGEAD6.mjs.map +1 -0
  29. package/dist/chunk-KTRWWAGL.mjs +50 -0
  30. package/dist/chunk-KTRWWAGL.mjs.map +1 -0
  31. package/dist/chunk-MZJHJ6BV.mjs +21 -0
  32. package/dist/chunk-MZJHJ6BV.mjs.map +1 -0
  33. package/dist/chunk-NXRIAL7T.mjs +3119 -0
  34. package/dist/chunk-NXRIAL7T.mjs.map +1 -0
  35. package/dist/components/index.d.mts +822 -0
  36. package/dist/components/index.d.ts +822 -0
  37. package/dist/components/index.js +3738 -0
  38. package/dist/components/index.js.map +1 -0
  39. package/dist/components/index.mjs +3627 -0
  40. package/dist/components/index.mjs.map +1 -0
  41. package/dist/index.css +297 -0
  42. package/dist/index.css.map +1 -0
  43. package/dist/index.d.mts +545 -0
  44. package/dist/index.d.ts +545 -0
  45. package/dist/index.js +4648 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/index.mjs +347 -0
  48. package/dist/index.mjs.map +1 -0
  49. package/dist/lib/index.d.mts +798 -0
  50. package/dist/lib/index.d.ts +798 -0
  51. package/dist/lib/index.js +1301 -0
  52. package/dist/lib/index.js.map +1 -0
  53. package/dist/lib/index.mjs +89 -0
  54. package/dist/lib/index.mjs.map +1 -0
  55. package/package.json +119 -0
  56. package/src/app/api/mdx-watch/route.ts +86 -0
  57. package/src/app/docs-page.tsx +212 -0
  58. package/src/app/layout.tsx +74 -0
  59. package/src/components/docs/accordion.tsx +53 -0
  60. package/src/components/docs/api/api-endpoint.tsx +59 -0
  61. package/src/components/docs/api/api-params.tsx +43 -0
  62. package/src/components/docs/api/api-playground.tsx +233 -0
  63. package/src/components/docs/api/api-reference.tsx +291 -0
  64. package/src/components/docs/api/api-response.tsx +48 -0
  65. package/src/components/docs/api/index.ts +5 -0
  66. package/src/components/docs/badge.tsx +22 -0
  67. package/src/components/docs/breadcrumb.tsx +51 -0
  68. package/src/components/docs/callout.tsx +109 -0
  69. package/src/components/docs/card.tsx +84 -0
  70. package/src/components/docs/category-index.tsx +112 -0
  71. package/src/components/docs/code-block.tsx +129 -0
  72. package/src/components/docs/columns.tsx +45 -0
  73. package/src/components/docs/componentTextProps.ts +85 -0
  74. package/src/components/docs/dev-mode-badge.tsx +35 -0
  75. package/src/components/docs/doc-layout-wrapper.tsx +54 -0
  76. package/src/components/docs/doc-layout.tsx +111 -0
  77. package/src/components/docs/doc-loading.tsx +15 -0
  78. package/src/components/docs/doc-metadata.tsx +55 -0
  79. package/src/components/docs/doc-navigation.tsx +62 -0
  80. package/src/components/docs/doc-tags.tsx +25 -0
  81. package/src/components/docs/draft-badge.tsx +10 -0
  82. package/src/components/docs/footer.tsx +47 -0
  83. package/src/components/docs/frame.tsx +22 -0
  84. package/src/components/docs/header.tsx +122 -0
  85. package/src/components/docs/hot-reload-indicator.tsx +77 -0
  86. package/src/components/docs/icon.tsx +70 -0
  87. package/src/components/docs/image-card.tsx +95 -0
  88. package/src/components/docs/image.tsx +73 -0
  89. package/src/components/docs/index.ts +48 -0
  90. package/src/components/docs/math.tsx +46 -0
  91. package/src/components/docs/mdx-components.tsx +166 -0
  92. package/src/components/docs/mdx-hot-reload.tsx +37 -0
  93. package/src/components/docs/mermaid.tsx +77 -0
  94. package/src/components/docs/mobile-doc-layout.tsx +115 -0
  95. package/src/components/docs/not-found-content.tsx +55 -0
  96. package/src/components/docs/search-highlight.tsx +127 -0
  97. package/src/components/docs/search-modal.tsx +223 -0
  98. package/src/components/docs/sidebar-skeleton.tsx +39 -0
  99. package/src/components/docs/sidebar.tsx +323 -0
  100. package/src/components/docs/site-banner.tsx +92 -0
  101. package/src/components/docs/steps.tsx +29 -0
  102. package/src/components/docs/tab-context.tsx +28 -0
  103. package/src/components/docs/tab-groups.tsx +50 -0
  104. package/src/components/docs/table-of-contents.tsx +104 -0
  105. package/src/components/docs/tabs.tsx +63 -0
  106. package/src/components/docs/theme-toggle.tsx +39 -0
  107. package/src/components/docs/tooltip.tsx +37 -0
  108. package/src/components/docs/version-switcher.tsx +52 -0
  109. package/src/components/docs/video.tsx +80 -0
  110. package/src/components/global/index.ts +3 -0
  111. package/src/components/global/version-not-found.tsx +26 -0
  112. package/src/components/index.ts +8 -0
  113. package/src/components/theme-provider.tsx +11 -0
  114. package/src/components/ui/badge.tsx +46 -0
  115. package/src/components/ui/button.tsx +60 -0
  116. package/src/components/ui/dialog.tsx +143 -0
  117. package/src/components/ui/index.ts +6 -0
  118. package/src/components/ui/input.tsx +21 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/index.ts +41 -0
  121. package/src/lib/api-parser.types.ts +78 -0
  122. package/src/lib/api.types.ts +202 -0
  123. package/src/lib/category.ts +71 -0
  124. package/src/lib/config.server.ts +170 -0
  125. package/src/lib/config.ts +20 -0
  126. package/src/lib/config.types.ts +295 -0
  127. package/src/lib/dev-utils.ts +75 -0
  128. package/src/lib/index.ts +27 -0
  129. package/src/lib/mdx-cache.ts +200 -0
  130. package/src/lib/mdx.ts +402 -0
  131. package/src/lib/parsers/base-parser.ts +16 -0
  132. package/src/lib/parsers/index.ts +69 -0
  133. package/src/lib/parsers/openapi-parser.ts +251 -0
  134. package/src/lib/parsers/postman-parser.ts +301 -0
  135. package/src/lib/parsers/specra-parser.ts +24 -0
  136. package/src/lib/redirects.ts +40 -0
  137. package/src/lib/remark-code-meta.ts +23 -0
  138. package/src/lib/sidebar-utils.ts +188 -0
  139. package/src/lib/toc.ts +24 -0
  140. package/src/lib/utils.ts +36 -0
  141. package/src/specra.config.json +124 -0
  142. package/src/styles/globals.css +427 -0
@@ -0,0 +1,291 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import type { SpecraApiSpec } from "@/lib/api-parser.types"
5
+ import { parseApiSpec, type ParserType } from "@/lib/parsers"
6
+ import { Accordion, AccordionItem } from "../accordion"
7
+ import { ApiEndpoint } from "./api-endpoint"
8
+ import { ApiParams } from "./api-params"
9
+ import { ApiResponse } from "./api-response"
10
+ import { ApiPlayground } from "./api-playground"
11
+ import { CodeBlock } from "../code-block"
12
+ import { Loader2 } from "lucide-react"
13
+
14
+ interface ApiReferenceProps {
15
+ /**
16
+ * Path to the API spec JSON file (relative to /public)
17
+ * Example: "/api-specs/my-api.json"
18
+ */
19
+ spec: string
20
+
21
+ /**
22
+ * Parser type - auto-detect by default
23
+ * - "auto": Auto-detect format (Specra, OpenAPI, or Postman)
24
+ * - "specra": Native Specra format
25
+ * - "openapi": OpenAPI 3.x / Swagger
26
+ * - "postman": Postman Collection v2.x
27
+ */
28
+ parser?: ParserType
29
+
30
+ /**
31
+ * Show API playground for testing
32
+ */
33
+ showPlayground?: boolean
34
+ }
35
+
36
+ export function ApiReference({ spec, parser = "auto", showPlayground = true }: ApiReferenceProps) {
37
+ const [apiSpec, setApiSpec] = useState<SpecraApiSpec | null>(null)
38
+ const [loading, setLoading] = useState(true)
39
+ const [error, setError] = useState<string | null>(null)
40
+
41
+ useEffect(() => {
42
+ async function loadSpec() {
43
+ try {
44
+ const response = await fetch(spec)
45
+ if (!response.ok) {
46
+ throw new Error(`Failed to load API spec: ${response.statusText}`)
47
+ }
48
+ const data = await response.json()
49
+
50
+ // Parse using the appropriate parser
51
+ const parsedSpec = parseApiSpec(data, parser)
52
+ setApiSpec(parsedSpec)
53
+ } catch (err) {
54
+ setError(err instanceof Error ? err.message : "Failed to load API spec")
55
+ } finally {
56
+ setLoading(false)
57
+ }
58
+ }
59
+
60
+ loadSpec()
61
+ }, [spec, parser])
62
+
63
+ // Replace environment variables in text
64
+ const interpolateEnv = (text: string, env?: Record<string, string>): string => {
65
+ if (!env) return text
66
+ return text.replace(/\{(\w+)\}/g, (match, key) => {
67
+ return env[key] || match
68
+ })
69
+ }
70
+
71
+ if (loading) {
72
+ return (
73
+ <div className="flex items-center justify-center py-12">
74
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
75
+ <span className="ml-2 text-muted-foreground">Loading API specification...</span>
76
+ </div>
77
+ )
78
+ }
79
+
80
+ if (error) {
81
+ return (
82
+ <div className="rounded-lg border border-red-500/20 bg-red-500/10 p-4">
83
+ <p className="text-sm text-red-600 dark:text-red-400">Error: {error}</p>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ if (!apiSpec) {
89
+ return null
90
+ }
91
+
92
+ return (
93
+ <div className="space-y-6">
94
+ {/* API Info */}
95
+ {(apiSpec.title || apiSpec.description) && (
96
+ <div className="mb-8">
97
+ {apiSpec.title && (
98
+ <h2 className="text-2xl font-semibold mb-2 text-foreground">{apiSpec.title}</h2>
99
+ )}
100
+ {apiSpec.description && (
101
+ <p className="text-muted-foreground">{apiSpec.description}</p>
102
+ )}
103
+ {apiSpec.baseUrl && (
104
+ <div className="mt-4">
105
+ <p className="text-sm font-semibold text-muted-foreground mb-1">Base URL</p>
106
+ <code className="text-sm px-2 py-1 bg-muted rounded">{apiSpec.baseUrl}</code>
107
+ </div>
108
+ )}
109
+ </div>
110
+ )}
111
+
112
+ {/* Authentication */}
113
+ {apiSpec.auth && (
114
+ <div className="rounded-lg border border-border bg-card/30 p-4 mb-6">
115
+ <h3 className="text-lg font-semibold mb-2 text-foreground">Authentication</h3>
116
+ <p className="text-sm text-muted-foreground mb-2">
117
+ {apiSpec.auth.description || `This API uses ${apiSpec.auth.type} authentication.`}
118
+ </p>
119
+ {apiSpec.auth.type === "bearer" && (
120
+ <CodeBlock
121
+ code={`Authorization: ${apiSpec.auth.tokenPrefix || "Bearer"} {YOUR_TOKEN}`}
122
+ language="bash"
123
+ />
124
+ )}
125
+ {apiSpec.auth.type === "apiKey" && (
126
+ <CodeBlock
127
+ code={`${apiSpec.auth.headerName || "X-API-Key"}: {YOUR_API_KEY}`}
128
+ language="bash"
129
+ />
130
+ )}
131
+ </div>
132
+ )}
133
+
134
+ {/* Endpoints as Accordion */}
135
+ <Accordion type="single" collapsible className="space-y-4">
136
+ {apiSpec.endpoints.map((endpoint, index) => {
137
+ // Merge global and endpoint-specific headers
138
+ const allHeaders = [
139
+ ...(apiSpec.globalHeaders || []),
140
+ ...(endpoint.headers || []),
141
+ ].map((header) => ({
142
+ ...header,
143
+ value: interpolateEnv(header.value, apiSpec.env),
144
+ }))
145
+
146
+ return (
147
+ <AccordionItem
148
+ key={index}
149
+ value={`endpoint-${index}`}
150
+ title={
151
+ <div className="flex items-center gap-3">
152
+ <span
153
+ className={`text-xs font-semibold px-2 py-0.5 rounded ${
154
+ endpoint.method === "GET"
155
+ ? "bg-blue-500/10 text-blue-600 dark:text-blue-400"
156
+ : endpoint.method === "POST"
157
+ ? "bg-green-500/10 text-green-600 dark:text-green-400"
158
+ : endpoint.method === "PUT"
159
+ ? "bg-orange-500/10 text-orange-600 dark:text-orange-400"
160
+ : endpoint.method === "PATCH"
161
+ ? "bg-purple-500/10 text-purple-600 dark:text-purple-400"
162
+ : "bg-red-500/10 text-red-600 dark:text-red-400"
163
+ }`}
164
+ >
165
+ {endpoint.method}
166
+ </span>
167
+ <code className="text-sm font-mono">{endpoint.path}</code>
168
+ <span className="text-sm text-muted-foreground ml-auto">{endpoint.title}</span>
169
+ </div>
170
+ }
171
+ >
172
+ <div className="space-y-6 pt-4">
173
+ {/* Description */}
174
+ {endpoint.description && (
175
+ <p className="text-sm text-muted-foreground">{endpoint.description}</p>
176
+ )}
177
+
178
+ {/* Path Parameters */}
179
+ {endpoint.pathParams && endpoint.pathParams.length > 0 && (
180
+ <ApiParams title="Path Parameters" params={endpoint.pathParams} />
181
+ )}
182
+
183
+ {/* Query Parameters */}
184
+ {endpoint.queryParams && endpoint.queryParams.length > 0 && (
185
+ <ApiParams title="Query Parameters" params={endpoint.queryParams} />
186
+ )}
187
+
188
+ {/* Headers */}
189
+ {allHeaders.length > 0 && (
190
+ <div>
191
+ <h4 className="text-sm font-semibold text-foreground mb-3">Headers</h4>
192
+ <div className="space-y-2">
193
+ {allHeaders.map((header, idx) => (
194
+ <div key={idx} className="flex flex-col gap-1">
195
+ <div className="flex items-center gap-2">
196
+ <code className="text-sm font-mono text-foreground">{header.name}</code>
197
+ <span className="text-xs text-muted-foreground">{header.value}</span>
198
+ </div>
199
+ {header.description && (
200
+ <p className="text-sm text-muted-foreground">{header.description}</p>
201
+ )}
202
+ </div>
203
+ ))}
204
+ </div>
205
+ </div>
206
+ )}
207
+
208
+ {/* Request Body */}
209
+ {endpoint.body && (
210
+ <div>
211
+ <h4 className="text-sm font-semibold text-foreground mb-3">Request Body</h4>
212
+ {endpoint.body.description && (
213
+ <p className="text-sm text-muted-foreground mb-2">
214
+ {endpoint.body.description}
215
+ </p>
216
+ )}
217
+ {endpoint.body.example && (
218
+ <CodeBlock
219
+ code={
220
+ typeof endpoint.body.example === "string"
221
+ ? endpoint.body.example
222
+ : JSON.stringify(endpoint.body.example, null, 2)
223
+ }
224
+ language="json"
225
+ />
226
+ )}
227
+ </div>
228
+ )}
229
+
230
+ {/* Responses */}
231
+ <div>
232
+ <h4 className="text-sm font-semibold text-foreground mb-3">Responses</h4>
233
+ {endpoint.successResponse && (
234
+ <ApiResponse
235
+ status={endpoint.successResponse.status}
236
+ description={endpoint.successResponse.description}
237
+ example={endpoint.successResponse.example}
238
+ schema={endpoint.successResponse.schema}
239
+ />
240
+ )}
241
+ {endpoint.errorResponses?.map((response, idx) => (
242
+ <ApiResponse
243
+ key={idx}
244
+ status={response.status}
245
+ description={response.description}
246
+ example={response.example}
247
+ schema={response.schema}
248
+ />
249
+ ))}
250
+ </div>
251
+
252
+ {/* Code Examples */}
253
+ {endpoint.examples && endpoint.examples.length > 0 && (
254
+ <div>
255
+ <h4 className="text-sm font-semibold text-foreground mb-3">Examples</h4>
256
+ {endpoint.examples.map((example, idx) => (
257
+ <div key={idx} className="mb-3">
258
+ <p className="text-xs font-semibold text-muted-foreground mb-2">
259
+ {example.title}
260
+ </p>
261
+ <CodeBlock code={example.code} language={example.language} />
262
+ </div>
263
+ ))}
264
+ </div>
265
+ )}
266
+
267
+ {/* API Playground */}
268
+ {showPlayground && (
269
+ <ApiPlayground
270
+ method={endpoint.method}
271
+ path={endpoint.path}
272
+ baseUrl={apiSpec.baseUrl}
273
+ headers={Object.fromEntries(allHeaders.map((h) => [h.name, h.value]))}
274
+ pathParams={endpoint.pathParams}
275
+ defaultBody={
276
+ endpoint.body?.example
277
+ ? typeof endpoint.body.example === "string"
278
+ ? endpoint.body.example
279
+ : JSON.stringify(endpoint.body.example, null, 2)
280
+ : undefined
281
+ }
282
+ />
283
+ )}
284
+ </div>
285
+ </AccordionItem>
286
+ )
287
+ })}
288
+ </Accordion>
289
+ </div>
290
+ )
291
+ }
@@ -0,0 +1,48 @@
1
+ import { CodeBlock } from "../code-block"
2
+
3
+ interface ApiResponseProps {
4
+ status: number
5
+ description?: string
6
+ example?: any
7
+ schema?: any
8
+ }
9
+
10
+ const statusColors: Record<string, string> = {
11
+ "2": "text-green-600 dark:text-green-400",
12
+ "3": "text-blue-600 dark:text-blue-400",
13
+ "4": "text-orange-600 dark:text-orange-400",
14
+ "5": "text-red-600 dark:text-red-400",
15
+ }
16
+
17
+ export function ApiResponse({ status, description, example, schema }: ApiResponseProps) {
18
+ const statusClass = statusColors[String(status)[0]] || "text-muted-foreground"
19
+
20
+ return (
21
+ <div className="mb-4">
22
+ <div className="flex items-center gap-2 mb-2">
23
+ <span className={`text-sm font-semibold ${statusClass}`}>{status}</span>
24
+ {description && <span className="text-sm text-muted-foreground">{description}</span>}
25
+ </div>
26
+
27
+ {example && (
28
+ <div className="mb-3">
29
+ <p className="text-xs font-semibold text-muted-foreground mb-2">Example Response</p>
30
+ <CodeBlock
31
+ code={typeof example === "string" ? example : JSON.stringify(example, null, 2)}
32
+ language="json"
33
+ />
34
+ </div>
35
+ )}
36
+
37
+ {schema && (
38
+ <div>
39
+ <p className="text-xs font-semibold text-muted-foreground mb-2">Schema</p>
40
+ <CodeBlock
41
+ code={typeof schema === "string" ? schema : JSON.stringify(schema, null, 2)}
42
+ language="json"
43
+ />
44
+ </div>
45
+ )}
46
+ </div>
47
+ )
48
+ }
@@ -0,0 +1,5 @@
1
+ export { ApiEndpoint } from "./api-endpoint"
2
+ export { ApiParams } from "./api-params"
3
+ export { ApiResponse as ApiResponseDisplay, ApiResponse } from "./api-response"
4
+ export { ApiPlayground } from "./api-playground"
5
+ export { ApiReference } from "./api-reference"
@@ -0,0 +1,22 @@
1
+ interface BadgeProps {
2
+ children: React.ReactNode
3
+ variant?: "default" | "success" | "warning" | "error" | "info"
4
+ }
5
+
6
+ export function Badge({ children, variant = "default" }: BadgeProps) {
7
+ const variants = {
8
+ default: "bg-muted text-foreground border-border",
9
+ success: "bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20",
10
+ warning: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/20",
11
+ error: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20",
12
+ info: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20",
13
+ }
14
+
15
+ return (
16
+ <span
17
+ className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border ${variants[variant]}`}
18
+ >
19
+ {children}
20
+ </span>
21
+ )
22
+ }
@@ -0,0 +1,51 @@
1
+ import Link from "next/link"
2
+ import { ChevronRight } from "lucide-react"
3
+
4
+ interface BreadcrumbProps {
5
+ version: string
6
+ slug: string
7
+ title: string
8
+ }
9
+
10
+ export function Breadcrumb({ version, slug, title }: BreadcrumbProps) {
11
+ const parts = slug.split("/")
12
+ const breadcrumbs = [
13
+ { label: "Docs", href: `/docs/${version}` },
14
+ ]
15
+
16
+ // Build breadcrumb path
17
+ let currentPath = ""
18
+ for (let i = 0; i < parts.length - 1; i++) {
19
+ currentPath += (currentPath ? "/" : "") + parts[i]
20
+ breadcrumbs.push({
21
+ label: parts[i].replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
22
+ href: `/docs/${version}/${currentPath}`,
23
+ })
24
+ }
25
+
26
+ // Add current page
27
+ breadcrumbs.push({
28
+ label: title,
29
+ href: `/docs/${version}/${slug}`,
30
+ })
31
+
32
+ return (
33
+ <nav className="flex items-center gap-2 text-sm text-muted-foreground mb-4" aria-label="Breadcrumb">
34
+ {breadcrumbs.map((crumb, index) => (
35
+ <div key={crumb.href} className="flex items-center gap-2">
36
+ {index > 0 && <ChevronRight className="h-4 w-4" />}
37
+ {index === breadcrumbs.length - 1 ? (
38
+ <span className="text-foreground font-medium">{crumb.label}</span>
39
+ ) : (
40
+ <Link
41
+ href={crumb.href}
42
+ className="hover:text-foreground transition-colors"
43
+ >
44
+ {crumb.label}
45
+ </Link>
46
+ )}
47
+ </div>
48
+ ))}
49
+ </nav>
50
+ )
51
+ }
@@ -0,0 +1,109 @@
1
+ "use client"
2
+
3
+ import type { ReactNode } from "react"
4
+ import { Info, AlertTriangle, CheckCircle2, XCircle, Lightbulb } from "lucide-react"
5
+
6
+ interface CalloutProps {
7
+ children: ReactNode
8
+ type?: "info" | "warning" | "success" | "error" | "tip" | "note" | "danger"
9
+ title?: string
10
+ }
11
+
12
+ export function Callout({ children, type = "info", title }: CalloutProps) {
13
+ const configs = {
14
+ info: {
15
+ icon: Info,
16
+ className: "bg-blue-500/10 border-blue-500/30 text-blue-900 dark:bg-blue-400/5 dark:border-blue-500/20 dark:text-blue-400",
17
+ iconClassName: "text-blue-600 dark:text-blue-400",
18
+ titleClassName: "text-blue-700 dark:text-blue-300",
19
+ defaultTitle: "Info",
20
+ },
21
+ note: {
22
+ icon: Info,
23
+ className: "bg-blue-500/10 border-blue-500/30 text-blue-900 dark:bg-blue-400/5 dark:border-blue-500/20 dark:text-blue-400",
24
+ iconClassName: "text-blue-600 dark:text-blue-400",
25
+ titleClassName: "text-blue-700 dark:text-blue-300",
26
+ defaultTitle: "Note",
27
+ },
28
+ warning: {
29
+ icon: AlertTriangle,
30
+ className: "bg-yellow-500/10 border-yellow-500/30 text-yellow-900 dark:bg-yellow-400/5 dark:border-yellow-500/20 dark:text-yellow-400",
31
+ iconClassName: "text-yellow-600 dark:text-yellow-400",
32
+ titleClassName: "text-yellow-700 dark:text-yellow-300",
33
+ defaultTitle: "Warning",
34
+ },
35
+ success: {
36
+ icon: CheckCircle2,
37
+ className: "bg-green-500/10 border-green-500/30 text-green-900 dark:bg-green-400/5 dark:border-green-500/20 dark:text-green-400",
38
+ iconClassName: "text-green-600 dark:text-green-400",
39
+ titleClassName: "text-green-700 dark:text-green-300",
40
+ defaultTitle: "Success",
41
+ },
42
+ error: {
43
+ icon: XCircle,
44
+ className: "bg-red-500/10 border-red-500/30 text-red-900 dark:bg-red-400/5 dark:border-red-500/20 dark:text-red-400",
45
+ iconClassName: "text-red-600 dark:text-red-400",
46
+ titleClassName: "text-red-700 dark:text-red-300",
47
+ defaultTitle: "Error",
48
+ },
49
+ danger: {
50
+ icon: XCircle,
51
+ className: "bg-red-500/10 border-red-500/30 text-red-900 dark:bg-red-400/5 dark:border-red-500/20 dark:text-red-400",
52
+ iconClassName: "text-red-600 dark:text-red-400",
53
+ titleClassName: "text-red-700 dark:text-red-300",
54
+ defaultTitle: "Danger",
55
+ },
56
+ tip: {
57
+ icon: Lightbulb,
58
+ className: "bg-purple-500/10 border-purple-500/30 text-purple-900 dark:bg-purple-400/5 dark:border-purple-500/20 dark:text-purple-400",
59
+ iconClassName: "text-purple-600 dark:text-purple-400",
60
+ titleClassName: "text-purple-700 dark:text-purple-300",
61
+ defaultTitle: "Tip",
62
+ },
63
+ }
64
+
65
+ const config = configs[type]
66
+ const Icon = config.icon
67
+
68
+ // Extract title from strong/bold text if present, but ONLY if no explicit title prop was provided
69
+ let _title = title || config.defaultTitle
70
+ let content = children
71
+
72
+ if (!title && children && typeof children === "object") {
73
+ const childArray = Array.isArray(children) ? children : [children]
74
+ const firstElement = childArray[0]
75
+
76
+ // Check if first child is a paragraph with a strong element
77
+ if (firstElement && typeof firstElement === "object" && "props" in firstElement) {
78
+ const props = (firstElement as any).props
79
+ if (props.children && Array.isArray(props.children)) {
80
+ const strongChild = props.children.find(
81
+ (child: any) => child && typeof child === "object" && child.type === "strong",
82
+ )
83
+ if (strongChild) {
84
+ _title = strongChild.props.children
85
+ // Remove the title from content
86
+ content = childArray.map((child, idx) => {
87
+ if (idx === 0 && typeof child === "object" && "props" in child) {
88
+ const newChildren = (child as any).props.children.filter((c: any) => c !== strongChild)
89
+ return { ...child, props: { ...(child as any).props, children: newChildren } }
90
+ }
91
+ return child
92
+ })
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ return (
99
+ <div className={`flex gap-3 p-4 rounded-xl border my-2 ${config.className}`}>
100
+ <div className="flex-shrink-0 mt-0.5">
101
+ <Icon className={`h-5 w-5 ${config.iconClassName}`} />
102
+ </div>
103
+ <div className="flex-1 space-y-0">
104
+ <div className={`font-semibold text-sm ${config.titleClassName}`}>{_title}</div>
105
+ <div className="text-sm leading-relaxed [&>p]:mb-0 [&>p]:text-current">{content}</div>
106
+ </div>
107
+ </div>
108
+ )
109
+ }
@@ -0,0 +1,84 @@
1
+ import { ArrowRight, ExternalLink } from "lucide-react"
2
+ import Link from "next/link"
3
+ import { Icon } from "./icon"
4
+
5
+ interface CardProps {
6
+ title: string
7
+ description?: string
8
+ href?: string
9
+ icon?: string | React.ReactNode
10
+ children?: React.ReactNode
11
+ external?: boolean
12
+ }
13
+
14
+ export function Card({ title, description, href, icon, children, external = false }: CardProps) {
15
+ const content = (
16
+ <>
17
+ <div className="flex items-center gap-3">
18
+ {icon && (
19
+ <div className="shrink-0 w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary">
20
+ {typeof icon === "string" ? <Icon icon={icon} size={20} /> : icon}
21
+ </div>
22
+ )}
23
+ <div className="flex-1 min-w-0">
24
+ <h3 className={`font-semibold text-foreground mb-1 no-underline ${href ? 'group-hover:text-primary transition-colors' : ''}`}>
25
+ {title}
26
+ </h3>
27
+ {description && (
28
+ <p className="text-sm text-muted-foreground line-clamp-2 no-underline">{description}</p>
29
+ )}
30
+ {children && (
31
+ <div className="mt-2 text-sm text-muted-foreground no-underline">{children}</div>
32
+ )}
33
+ </div>
34
+ {href && (
35
+ <div className="shrink-0 self-start mt-1">
36
+ {external ? (
37
+ <ExternalLink className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
38
+ ) : (
39
+ <ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
40
+ )}
41
+ </div>
42
+ )}
43
+ </div>
44
+ </>
45
+ )
46
+
47
+ if (href) {
48
+ const Component = external ? "a" : Link
49
+ return (
50
+ <Component
51
+ href={href}
52
+ className="card-link group block p-4 rounded-xl border border-border hover:border-primary/50 hover:bg-muted/50 transition-all"
53
+ {...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
54
+ >
55
+ {content}
56
+ </Component>
57
+ )
58
+ }
59
+
60
+ return (
61
+ <div className="p-4 rounded-xl border border-border bg-muted/30 no-underline">
62
+ {content}
63
+ </div>
64
+ )
65
+ }
66
+
67
+ interface CardGridProps {
68
+ children: React.ReactNode
69
+ cols?: 1 | 2 | 3
70
+ }
71
+
72
+ export function CardGrid({ children, cols = 2 }: CardGridProps) {
73
+ const gridCols = {
74
+ 1: "grid-cols-1",
75
+ 2: "grid-cols-1 md:grid-cols-2",
76
+ 3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
77
+ }
78
+
79
+ return (
80
+ <div className={`grid ${gridCols[cols]} gap-4 my-6`}>
81
+ {children}
82
+ </div>
83
+ )
84
+ }