@sntlr/registry-shell 1.0.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 (134) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/adapter/custom.d.ts +47 -0
  4. package/dist/adapter/custom.js +53 -0
  5. package/dist/adapter/custom.js.map +1 -0
  6. package/dist/adapter/default.d.ts +40 -0
  7. package/dist/adapter/default.js +202 -0
  8. package/dist/adapter/default.js.map +1 -0
  9. package/dist/cli/build.d.ts +1 -0
  10. package/dist/cli/build.js +31 -0
  11. package/dist/cli/build.js.map +1 -0
  12. package/dist/cli/dev.d.ts +1 -0
  13. package/dist/cli/dev.js +26 -0
  14. package/dist/cli/dev.js.map +1 -0
  15. package/dist/cli/index.d.ts +12 -0
  16. package/dist/cli/index.js +49 -0
  17. package/dist/cli/index.js.map +1 -0
  18. package/dist/cli/init.d.ts +1 -0
  19. package/dist/cli/init.js +70 -0
  20. package/dist/cli/init.js.map +1 -0
  21. package/dist/cli/shared.d.ts +33 -0
  22. package/dist/cli/shared.js +278 -0
  23. package/dist/cli/shared.js.map +1 -0
  24. package/dist/cli/start.d.ts +1 -0
  25. package/dist/cli/start.js +24 -0
  26. package/dist/cli/start.js.map +1 -0
  27. package/dist/config-loader.d.ts +49 -0
  28. package/dist/config-loader.js +140 -0
  29. package/dist/define-config.d.ts +188 -0
  30. package/dist/define-config.js +21 -0
  31. package/dist/index.d.ts +11 -0
  32. package/dist/index.js +9 -0
  33. package/package.json +124 -0
  34. package/src/adapter/custom.ts +90 -0
  35. package/src/adapter/default.ts +241 -0
  36. package/src/cli/build.ts +38 -0
  37. package/src/cli/dev.ts +38 -0
  38. package/src/cli/index.ts +52 -0
  39. package/src/cli/init.ts +76 -0
  40. package/src/cli/shared.ts +306 -0
  41. package/src/cli/start.ts +28 -0
  42. package/src/config-loader.ts +190 -0
  43. package/src/define-config.ts +206 -0
  44. package/src/index.ts +17 -0
  45. package/src/next-app/app/[...asset]/route.ts +81 -0
  46. package/src/next-app/app/_user-global.css +6 -0
  47. package/src/next-app/app/_user-sources.css +9 -0
  48. package/src/next-app/app/a11y/[name]/route.ts +19 -0
  49. package/src/next-app/app/api/search-index/route.ts +19 -0
  50. package/src/next-app/app/components/[name]/page.tsx +61 -0
  51. package/src/next-app/app/components/layout.tsx +18 -0
  52. package/src/next-app/app/docs/[slug]/page.tsx +53 -0
  53. package/src/next-app/app/docs/layout.tsx +18 -0
  54. package/src/next-app/app/globals.css +329 -0
  55. package/src/next-app/app/layout.tsx +102 -0
  56. package/src/next-app/app/page.tsx +9 -0
  57. package/src/next-app/app/preview-snapshot/[name]/page.tsx +20 -0
  58. package/src/next-app/app/preview-snapshot/layout.tsx +17 -0
  59. package/src/next-app/app/props/[name]/route.ts +19 -0
  60. package/src/next-app/app/r/[name]/route.ts +14 -0
  61. package/src/next-app/app/tests/[name]/route.ts +19 -0
  62. package/src/next-app/components/a11y-info.tsx +287 -0
  63. package/src/next-app/components/a11y-provider.tsx +39 -0
  64. package/src/next-app/components/component-breadcrumb.tsx +55 -0
  65. package/src/next-app/components/component-icon.tsx +140 -0
  66. package/src/next-app/components/component-preview.tsx +13 -0
  67. package/src/next-app/components/component-tabs.tsx +209 -0
  68. package/src/next-app/components/docs-toc.tsx +86 -0
  69. package/src/next-app/components/global-mobile-sidebar.tsx +35 -0
  70. package/src/next-app/components/header.tsx +188 -0
  71. package/src/next-app/components/heading-anchor.tsx +52 -0
  72. package/src/next-app/components/homepage-demo.tsx +180 -0
  73. package/src/next-app/components/locale-toggle.tsx +35 -0
  74. package/src/next-app/components/localized-mdx-client.tsx +14 -0
  75. package/src/next-app/components/localized-mdx.tsx +27 -0
  76. package/src/next-app/components/mobile-sidebar.tsx +22 -0
  77. package/src/next-app/components/nav-data-provider.tsx +37 -0
  78. package/src/next-app/components/navigation-progress.tsx +62 -0
  79. package/src/next-app/components/preview-canvas.tsx +368 -0
  80. package/src/next-app/components/preview-controls.tsx +94 -0
  81. package/src/next-app/components/preview-layout.tsx +218 -0
  82. package/src/next-app/components/props-table.tsx +134 -0
  83. package/src/next-app/components/resizable-preview.tsx +101 -0
  84. package/src/next-app/components/search.tsx +177 -0
  85. package/src/next-app/components/settings-modal.tsx +98 -0
  86. package/src/next-app/components/shell-ui/accordion.tsx +70 -0
  87. package/src/next-app/components/shell-ui/backdrop.tsx +29 -0
  88. package/src/next-app/components/shell-ui/badge.tsx +55 -0
  89. package/src/next-app/components/shell-ui/breadcrumb.tsx +120 -0
  90. package/src/next-app/components/shell-ui/button.tsx +64 -0
  91. package/src/next-app/components/shell-ui/card.tsx +127 -0
  92. package/src/next-app/components/shell-ui/checkbox.tsx +33 -0
  93. package/src/next-app/components/shell-ui/dialog.tsx +171 -0
  94. package/src/next-app/components/shell-ui/empty-state.tsx +66 -0
  95. package/src/next-app/components/shell-ui/input.tsx +27 -0
  96. package/src/next-app/components/shell-ui/kbd.tsx +30 -0
  97. package/src/next-app/components/shell-ui/label.tsx +25 -0
  98. package/src/next-app/components/shell-ui/select.tsx +204 -0
  99. package/src/next-app/components/shell-ui/separator.tsx +32 -0
  100. package/src/next-app/components/shell-ui/skeleton.tsx +18 -0
  101. package/src/next-app/components/shell-ui/table.tsx +124 -0
  102. package/src/next-app/components/shell-ui/tabs.tsx +102 -0
  103. package/src/next-app/components/shell-ui/toggle.tsx +56 -0
  104. package/src/next-app/components/sidebar-layout.tsx +37 -0
  105. package/src/next-app/components/sidebar-provider.tsx +75 -0
  106. package/src/next-app/components/sidebar.tsx +222 -0
  107. package/src/next-app/components/snapshot-preview.tsx +28 -0
  108. package/src/next-app/components/test-info.tsx +155 -0
  109. package/src/next-app/components/theme-provider.tsx +16 -0
  110. package/src/next-app/components/theme-toggle.tsx +21 -0
  111. package/src/next-app/components/translated-text.tsx +8 -0
  112. package/src/next-app/fallback/homepage.tsx +112 -0
  113. package/src/next-app/fallback/previews.ts +17 -0
  114. package/src/next-app/hooks/use-active-section.ts +23 -0
  115. package/src/next-app/hooks/use-controls.ts +72 -0
  116. package/src/next-app/hooks/use-mobile.ts +19 -0
  117. package/src/next-app/lib/branding.ts +52 -0
  118. package/src/next-app/lib/components-nav.ts +8 -0
  119. package/src/next-app/lib/docs.ts +16 -0
  120. package/src/next-app/lib/github.ts +38 -0
  121. package/src/next-app/lib/i18n.tsx +630 -0
  122. package/src/next-app/lib/locales.ts +17 -0
  123. package/src/next-app/lib/preview-loader.ts +7 -0
  124. package/src/next-app/lib/registry-adapter.ts +199 -0
  125. package/src/next-app/lib/utils.ts +6 -0
  126. package/src/next-app/next-env.d.ts +6 -0
  127. package/src/next-app/next.config.ts +101 -0
  128. package/src/next-app/postcss.config.mjs +7 -0
  129. package/src/next-app/public/favicon.ico +0 -0
  130. package/src/next-app/public/favicon_dark.svg +3 -0
  131. package/src/next-app/public/favicon_light.svg +3 -0
  132. package/src/next-app/registry.config.ts +50 -0
  133. package/src/next-app/tsconfig.json +29 -0
  134. package/src/next-app/user-aliases.d.ts +17 -0
@@ -0,0 +1,287 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useState } from "react"
4
+ import { PartyPopper } from "lucide-react"
5
+ import { Button } from "@shell/components/shell-ui/button"
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from "@shell/components/shell-ui/table"
14
+ import { Badge } from "@shell/components/shell-ui/badge"
15
+ import { Checkbox } from "@shell/components/shell-ui/checkbox"
16
+ import { EmptyState, EmptyStateDescription } from "@shell/components/shell-ui/empty-state"
17
+ import { Kbd } from "@shell/components/shell-ui/kbd"
18
+ import { Separator } from "@shell/components/shell-ui/separator"
19
+ import { useTranslations } from "@shell/lib/i18n"
20
+
21
+ interface A11yData {
22
+ component: string
23
+ wcag: string
24
+ standard: string
25
+ element: string
26
+ role: string
27
+ delegatesTo: string | null
28
+ description: string
29
+ keyboard: { key: string; action: string }[]
30
+ focus: {
31
+ visible: boolean
32
+ trapped: boolean
33
+ notes: string
34
+ }
35
+ screenReader: {
36
+ announcements: string[]
37
+ notes: string | null
38
+ }
39
+ contrast: {
40
+ text: string
41
+ ui: string
42
+ }
43
+ motion: {
44
+ reducedMotion: string
45
+ }
46
+ consumerResponsibilities: string[]
47
+ }
48
+
49
+ function formatKey(key: string): string {
50
+ return key
51
+ .replace(/Arrow Left/g, "⇦")
52
+ .replace(/Arrow Right/g, "⇨")
53
+ .replace(/Arrow Up/g, "⇧")
54
+ .replace(/Arrow Down/g, "⇩")
55
+ .replace(/Arrow /g, "")
56
+ .replace(/\bLeft\b/g, "⇦")
57
+ .replace(/\bRight\b/g, "⇨")
58
+ .replace(/\bUp\b/g, "⇧")
59
+ .replace(/\bDown\b/g, "⇩")
60
+ }
61
+
62
+ /** Render a key string as one or more Kbd elements, splitting on / and + */
63
+ function renderKeys(key: string) {
64
+ const formatted = formatKey(key)
65
+ // Split compound keys like "Enter/Space" or "Shift+Arrow"
66
+ const parts = formatted.split(/\s*[/]\s*/)
67
+ return parts.map((part, i) => (
68
+ <span key={i} className="inline-flex items-center gap-1">
69
+ {i > 0 && <span className="text-muted-foreground mx-0.5">/</span>}
70
+ {part.split(/\s*\+\s*/).map((k, j) => (
71
+ <span key={j} className="inline-flex items-center gap-0.5">
72
+ {j > 0 && <span className="text-muted-foreground">+</span>}
73
+ <Kbd>{k.trim()}</Kbd>
74
+ </span>
75
+ ))}
76
+ </span>
77
+ ))
78
+ }
79
+
80
+ export function A11yInfo({ name }: { name: string }) {
81
+ const [data, setData] = useState<A11yData | null>(null)
82
+ const [error, setError] = useState(false)
83
+ const [checked, setChecked] = useState<Set<number>>(new Set())
84
+ const [dismissed, setDismissed] = useState(false)
85
+ const t = useTranslations()
86
+
87
+ const toggleCheck = useCallback((i: number) => {
88
+ setChecked((prev) => {
89
+ const next = new Set(prev)
90
+ if (next.has(i)) next.delete(i)
91
+ else next.add(i)
92
+ return next
93
+ })
94
+ setDismissed(false)
95
+ }, [])
96
+
97
+ const allChecked = data ? data.consumerResponsibilities.length > 0 && checked.size === data.consumerResponsibilities.length : false
98
+ const [fading, setFading] = useState(false)
99
+
100
+ // Auto-dismiss celebration after 2 seconds
101
+ useEffect(() => {
102
+ if (allChecked && !dismissed) {
103
+ const fadeTimer = setTimeout(() => setFading(true), 1500)
104
+ const dismissTimer = setTimeout(() => { setDismissed(true); setFading(false) }, 2000)
105
+ return () => { clearTimeout(fadeTimer); clearTimeout(dismissTimer) }
106
+ }
107
+ }, [allChecked, dismissed])
108
+
109
+ useEffect(() => {
110
+ fetch(`/a11y/${name}.json`)
111
+ .then((r) => {
112
+ if (!r.ok) throw new Error()
113
+ return r.json()
114
+ })
115
+ .then(setData)
116
+ .catch(() => setError(true))
117
+ }, [name])
118
+
119
+ if (error) {
120
+ return (
121
+ <p className="text-sm text-muted-foreground">
122
+ {t("component.a11yPlaceholder")}
123
+ </p>
124
+ )
125
+ }
126
+
127
+ if (!data) {
128
+ return (
129
+ <p className="text-sm text-muted-foreground" role="status" aria-live="polite">
130
+ {t("a11y.loading")}
131
+ </p>
132
+ )
133
+ }
134
+
135
+ return (
136
+ <div className="space-y-8">
137
+ {/* Overview */}
138
+ <div className="space-y-3">
139
+ <div className="flex flex-wrap items-center gap-2">
140
+ <Badge variant="outline">WCAG {data.wcag}</Badge>
141
+ <Badge variant="outline">{data.standard}</Badge>
142
+ {data.delegatesTo && (
143
+ <Badge variant="secondary">{data.delegatesTo}</Badge>
144
+ )}
145
+ </div>
146
+ <p className="text-sm text-muted-foreground">{data.description}</p>
147
+ <div className="flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground">
148
+ <span>
149
+ {t("a11y.element")} <code className="bg-muted px-1 rounded">&lt;{data.element}&gt;</code>
150
+ </span>
151
+ <span>
152
+ {t("a11y.role")} <code className="bg-muted px-1 rounded">{data.role}</code>
153
+ </span>
154
+ </div>
155
+ </div>
156
+
157
+ <Separator />
158
+
159
+ {/* Developer checklist — actionable, shown first */}
160
+ <div className="space-y-3">
161
+ <h4 id="a11y-consumer" className="text-base font-semibold">
162
+ {t("a11y.devChecklist")}
163
+ </h4>
164
+ {allChecked && !dismissed ? (
165
+ <div className={`rounded-lg border border-primary/30 bg-primary/5 p-6 text-center space-y-3 transition-opacity duration-500 ${fading ? "opacity-0" : "opacity-100"}`}>
166
+ <PartyPopper className="size-8 text-primary mx-auto" />
167
+ <p className="text-sm font-semibold text-foreground">{t("a11y.checklistComplete")}</p>
168
+ <p className="text-sm text-muted-foreground">{t("a11y.checklistCompleteDesc")}</p>
169
+ <Button variant="outline" size="sm" onClick={() => setDismissed(true)}>
170
+ {t("a11y.checklistDismiss")}
171
+ </Button>
172
+ </div>
173
+ ) : (
174
+ <div className="grid gap-2">
175
+ {data.consumerResponsibilities.map((r, i) => (
176
+ <div key={i} className="flex items-start gap-3 rounded-lg border bg-muted/30 p-3">
177
+ <Checkbox
178
+ id={`a11y-check-${i}`}
179
+ className="shrink-0 mt-0.5"
180
+ checked={checked.has(i)}
181
+ onCheckedChange={() => toggleCheck(i)}
182
+ />
183
+ <label htmlFor={`a11y-check-${i}`} className="text-sm cursor-pointer leading-snug">{r}</label>
184
+ </div>
185
+ ))}
186
+ </div>
187
+ )}
188
+ </div>
189
+
190
+ <Separator />
191
+
192
+ {/* Keyboard interactions */}
193
+ <div className="space-y-3">
194
+ <h4 id="a11y-keyboard" className="text-base font-semibold">
195
+ {t("a11y.keyboard")}
196
+ </h4>
197
+ {data.keyboard.length === 0 || (data.keyboard.length === 1 && data.keyboard[0].key === "N/A") ? (
198
+ <EmptyState>
199
+ <EmptyStateDescription>
200
+ {data.keyboard.length === 1 ? data.keyboard[0].action : t("a11y.noKeyboard")}
201
+ </EmptyStateDescription>
202
+ </EmptyState>
203
+ ) : (
204
+ <div className="rounded-lg border overflow-x-auto">
205
+ <Table>
206
+ <TableHeader>
207
+ <TableRow>
208
+ <TableHead className="whitespace-nowrap">{t("a11y.key")}</TableHead>
209
+ <TableHead>{t("a11y.action")}</TableHead>
210
+ </TableRow>
211
+ </TableHeader>
212
+ <TableBody>
213
+ {data.keyboard.map((k, i) => (
214
+ <TableRow key={i}>
215
+ <TableCell className="whitespace-nowrap align-top">
216
+ <span className="inline-flex items-center gap-1">
217
+ {renderKeys(k.key)}
218
+ </span>
219
+ </TableCell>
220
+ <TableCell className="text-sm">{k.action}</TableCell>
221
+ </TableRow>
222
+ ))}
223
+ </TableBody>
224
+ </Table>
225
+ </div>
226
+ )}
227
+ </div>
228
+
229
+ <Separator />
230
+
231
+ {/* Focus management */}
232
+ <div className="space-y-3">
233
+ <h4 id="a11y-focus" className="text-base font-semibold">
234
+ {t("a11y.focus")}
235
+ </h4>
236
+ <div className="flex gap-3">
237
+ <Badge variant={data.focus.visible ? "default" : "destructive"}>
238
+ {data.focus.visible ? t("a11y.focusVisible") : t("a11y.noFocusVisible")}
239
+ </Badge>
240
+ <Badge variant={data.focus.trapped ? "default" : "outline"}>
241
+ {data.focus.trapped ? t("a11y.focusTrapped") : t("a11y.noFocusTrap")}
242
+ </Badge>
243
+ </div>
244
+ <p className="text-sm text-muted-foreground">{data.focus.notes}</p>
245
+ </div>
246
+
247
+ <Separator />
248
+
249
+ {/* Screen reader */}
250
+ <div className="space-y-3">
251
+ <h4 id="a11y-screenreader" className="text-base font-semibold">
252
+ {t("a11y.screenReader")}
253
+ </h4>
254
+ <ul className="space-y-1.5 list-disc list-inside">
255
+ {data.screenReader.announcements.map((a, i) => (
256
+ <li key={i} className="text-sm text-muted-foreground">
257
+ {a}
258
+ </li>
259
+ ))}
260
+ </ul>
261
+ {data.screenReader.notes && (
262
+ <p className="text-sm text-muted-foreground italic">
263
+ {data.screenReader.notes}
264
+ </p>
265
+ )}
266
+ </div>
267
+
268
+ <Separator />
269
+
270
+ {/* Contrast & Motion */}
271
+ <div className="space-y-3">
272
+ <h4 id="a11y-contrast" className="text-base font-semibold">
273
+ {t("a11y.colorMotion")}
274
+ </h4>
275
+ <div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
276
+ <span className="font-medium">{t("a11y.text")}</span>
277
+ <span className="text-muted-foreground">{data.contrast.text}</span>
278
+ <span className="font-medium">{t("a11y.ui")}</span>
279
+ <span className="text-muted-foreground">{data.contrast.ui}</span>
280
+ <span className="font-medium">{t("a11y.motionLabel")}</span>
281
+ <span className="text-muted-foreground">{data.motion.reducedMotion}</span>
282
+ </div>
283
+ </div>
284
+
285
+ </div>
286
+ )
287
+ }
@@ -0,0 +1,39 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef, useState } from "react"
4
+ import { useLocale, useTranslations } from "@shell/lib/i18n"
5
+
6
+ export function A11yProvider() {
7
+ const { locale } = useLocale()
8
+ const t = useTranslations()
9
+ const [announcement, setAnnouncement] = useState("")
10
+ const prevLocale = useRef(locale)
11
+
12
+ // Sync lang attribute with locale
13
+ useEffect(() => {
14
+ document.documentElement.lang = locale
15
+ }, [locale])
16
+
17
+ // Announce locale change
18
+ useEffect(() => {
19
+ if (prevLocale.current !== locale) {
20
+ prevLocale.current = locale
21
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- announce locale change to screen readers
22
+ setAnnouncement(locale === "fr" ? "Langue changée en français" : "Language changed to English")
23
+ const timer = setTimeout(() => setAnnouncement(""), 3000)
24
+ return () => clearTimeout(timer)
25
+ }
26
+ }, [locale])
27
+
28
+ return (
29
+ <>
30
+ <a href="#main-content" className="skip-nav">
31
+ {t("a11y.skipToContent")}
32
+ </a>
33
+ {/* Live region for announcements */}
34
+ <div aria-live="polite" aria-atomic="true" className="sr-only">
35
+ {announcement}
36
+ </div>
37
+ </>
38
+ )
39
+ }
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef, useState } from "react"
4
+ import { createPortal } from "react-dom"
5
+ import {
6
+ BreadcrumbItem,
7
+ BreadcrumbPage,
8
+ BreadcrumbSeparator,
9
+ } from "@shell/components/shell-ui/breadcrumb"
10
+
11
+ export function ComponentBreadcrumb({
12
+ name,
13
+ children,
14
+ }: {
15
+ name: string
16
+ children: React.ReactNode
17
+ }) {
18
+ const titleRef = useRef<HTMLDivElement>(null)
19
+ const [scrolledPast, setScrolledPast] = useState(false)
20
+ const [target, setTarget] = useState<HTMLElement | null>(null)
21
+
22
+ useEffect(() => {
23
+ setTarget(document.getElementById("header-breadcrumb")) // eslint-disable-line react-hooks/set-state-in-effect -- mount detection for portal target
24
+ }, [])
25
+
26
+ useEffect(() => {
27
+ const el = titleRef.current
28
+ if (!el) return
29
+ const observer = new IntersectionObserver(
30
+ ([entry]) => {
31
+ setScrolledPast(!entry.isIntersecting)
32
+ },
33
+ { threshold: 0, rootMargin: "-56px 0px 0px 0px" }
34
+ )
35
+ observer.observe(el)
36
+ return () => observer.disconnect()
37
+ }, [])
38
+
39
+ return (
40
+ <>
41
+ <div ref={titleRef}>{children}</div>
42
+ {target && createPortal(
43
+ <span className={`inline-flex items-center transition-opacity duration-200 ${scrolledPast ? "opacity-100" : "opacity-0 pointer-events-none"}`}>
44
+ <BreadcrumbSeparator />
45
+ <BreadcrumbItem>
46
+ <BreadcrumbPage className="text-sm max-md:text-xs font-medium">
47
+ {name}
48
+ </BreadcrumbPage>
49
+ </BreadcrumbItem>
50
+ </span>,
51
+ target
52
+ )}
53
+ </>
54
+ )
55
+ }
@@ -0,0 +1,140 @@
1
+ "use client"
2
+
3
+ import {
4
+ AlertTriangle,
5
+ ChevronsDown,
6
+ Terminal,
7
+ Hash,
8
+ BookMarked,
9
+ RectangleHorizontal,
10
+ BadgeCheck,
11
+ ChevronRight,
12
+ CalendarDays,
13
+ CreditCard,
14
+ CheckSquare,
15
+ ChevronsUpDown,
16
+ MousePointerClick,
17
+ Inbox,
18
+ Keyboard,
19
+ PanelTop,
20
+ ChevronDown,
21
+ TextCursorInput,
22
+ Tag,
23
+ Layers,
24
+ CircleDot,
25
+ ListFilter,
26
+ SeparatorHorizontal,
27
+ PanelRight,
28
+ LayoutDashboard,
29
+ Square,
30
+ Bell,
31
+ Table2,
32
+ Columns3,
33
+ AlignLeft,
34
+ ToggleLeft,
35
+ MessageSquare,
36
+ Pipette,
37
+ Palette,
38
+ TableProperties,
39
+ FunctionSquare,
40
+ Upload,
41
+ SplitSquareHorizontal,
42
+ TextCursor,
43
+ MousePointer2,
44
+ KeyRound,
45
+ ArrowLeftRight,
46
+ ShieldAlert,
47
+ SquarePen,
48
+ Filter,
49
+ Users,
50
+ Link2,
51
+ LogIn,
52
+ UserPlus,
53
+ Smartphone,
54
+ Mail,
55
+ Lock,
56
+ Shield,
57
+ UserCog,
58
+ Building2,
59
+ Users2,
60
+ ShieldCheck,
61
+ Component,
62
+ type LucideIcon,
63
+ } from "lucide-react"
64
+
65
+ const iconMap: Record<string, LucideIcon> = {
66
+ accordion: ChevronsDown,
67
+ alert: AlertTriangle,
68
+ avatar: CircleDot,
69
+ badge: BadgeCheck,
70
+ breadcrumb: ChevronRight,
71
+ button: RectangleHorizontal,
72
+ calendar: CalendarDays,
73
+ card: CreditCard,
74
+ checkbox: CheckSquare,
75
+ collapsible: ChevronsUpDown,
76
+ command: Terminal,
77
+ "confirm-dialog": ShieldAlert,
78
+ "context-menu": MousePointerClick,
79
+ "color-picker": Pipette,
80
+ "color-swatch": Palette,
81
+ "data-table": TableProperties,
82
+ "empty-state": Inbox,
83
+ "file-upload": Upload,
84
+ "form-section": SquarePen,
85
+ kbd: Keyboard,
86
+ "formula-editor": FunctionSquare,
87
+ "app-switcher": ArrowLeftRight,
88
+ dialog: PanelTop,
89
+ "dropdown-menu": ChevronDown,
90
+ input: TextCursorInput,
91
+ "input-otp": Hash,
92
+ label: Tag,
93
+ "live-caret": TextCursor,
94
+ "live-cursor": MousePointer2,
95
+ pagination: BookMarked,
96
+ popover: Layers,
97
+ "radio-group": CircleDot,
98
+ "search-filter-bar": Filter,
99
+ select: ListFilter,
100
+ separator: SeparatorHorizontal,
101
+ "password-input": KeyRound,
102
+ sheet: PanelRight,
103
+ sidebar: LayoutDashboard,
104
+ skeleton: Square,
105
+ "split-button": SplitSquareHorizontal,
106
+ sonner: Bell,
107
+ table: Table2,
108
+ tabs: Columns3,
109
+ textarea: AlignLeft,
110
+ toggle: ToggleLeft,
111
+ tooltip: MessageSquare,
112
+ "social-links": Link2,
113
+ "user-status": Users,
114
+ "auth-login": LogIn,
115
+ "auth-register": UserPlus,
116
+ "authorized-devices": Smartphone,
117
+ "email-update-form": Mail,
118
+ "password-reset-form": Lock,
119
+ "mfa-form": Shield,
120
+ "profile-form": UserCog,
121
+ "account-deletion-form": AlertTriangle,
122
+ "org-settings-form": Building2,
123
+ "org-roles-form": ShieldCheck,
124
+ "org-members-form": Users2,
125
+ }
126
+
127
+ export function ComponentHeroIcon({ name }: { name: string }) {
128
+ const Icon = iconMap[name] ?? Component
129
+
130
+ return (
131
+ <Icon
132
+ className="absolute right-0 top-1/2 -translate-y-1/2 size-28 text-foreground/4 -rotate-12 pointer-events-none"
133
+ strokeWidth={1.5}
134
+ />
135
+ )
136
+ }
137
+
138
+ export function getComponentIcon(name: string): LucideIcon {
139
+ return iconMap[name] ?? Component
140
+ }
@@ -0,0 +1,13 @@
1
+ import { previewLoader } from "@shell/lib/preview-loader"
2
+
3
+ const { Preview } = previewLoader
4
+
5
+ const fallback = (
6
+ <div className="border border-l-0 border-r-0 border-dashed border-border p-8 text-center text-sm text-muted-foreground">
7
+ No preview available
8
+ </div>
9
+ )
10
+
11
+ export function ComponentPreview({ name }: { name: string }) {
12
+ return <Preview name={name} fallback={fallback} />
13
+ }