@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,222 @@
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import { usePathname } from "next/navigation"
5
+ import { useEffect, useRef } from "react"
6
+ import { useIsMobile } from "@shell/hooks/use-mobile"
7
+ import { BookOpen, Component, Blocks } from "lucide-react"
8
+ import type { DocMeta } from "@shell/lib/docs"
9
+ import type { ComponentMeta } from "@shell/lib/components-nav"
10
+ import { useTranslations, useLocale } from "@shell/lib/i18n"
11
+ import { Backdrop } from "@shell/components/shell-ui/backdrop"
12
+
13
+ import type { ActiveSection } from "@shell/hooks/use-active-section"
14
+
15
+ interface SidebarProps {
16
+ docs: DocMeta[]
17
+ components: ComponentMeta[]
18
+ open?: boolean
19
+ onClose?: () => void
20
+ collapsed?: boolean
21
+ /** When set, desktop views show only this section. Mobile always shows all three. */
22
+ activeSection?: ActiveSection
23
+ /**
24
+ * Which viewport variants of the sidebar to render.
25
+ * - `"all"` (default): mobile floating card + desktop inline + desktop floating (current behavior).
26
+ * - `"mobile"`: only the mobile floating card + backdrop. Used by the root layout so the
27
+ * hamburger menu works on every page (including the homepage), without injecting a
28
+ * desktop sidebar on pages that don't have one.
29
+ * - `"desktop"`: only the desktop inline + desktop floating card. Used by `SidebarLayout`
30
+ * so the per-section docs/components pages still get their desktop nav, while the
31
+ * root layout owns the mobile variant.
32
+ */
33
+ display?: "all" | "mobile" | "desktop"
34
+ }
35
+
36
+ export function Sidebar({ docs, components, open, onClose, collapsed, activeSection, display = "all" }: SidebarProps) {
37
+ const pathname = usePathname()
38
+ const t = useTranslations()
39
+ const { locale } = useLocale()
40
+
41
+ const isMobile = useIsMobile()
42
+ const uiComponents = components.filter((c) => c.kind === "component")
43
+ const blocks = components.filter((c) => c.kind === "block")
44
+
45
+ // Close floating nav on mobile navigation only
46
+ const isMobileRef = useRef(isMobile)
47
+ isMobileRef.current = isMobile
48
+ const mountedRef = useRef(false)
49
+ useEffect(() => {
50
+ if (!mountedRef.current) { mountedRef.current = true; return }
51
+ if (isMobileRef.current) onClose?.()
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- only trigger on pathname change
53
+ }, [pathname])
54
+
55
+ const docsSection = (
56
+ <SidebarSection icon={BookOpen} title={t("sidebar.documentation")}>
57
+ {docs.map((doc) => (
58
+ <SidebarLink
59
+ key={doc.slug}
60
+ href={`/docs/${doc.slug}`}
61
+ active={pathname === `/docs/${doc.slug}`}
62
+ >
63
+ {doc.titles?.[locale] ?? doc.title}
64
+ </SidebarLink>
65
+ ))}
66
+ </SidebarSection>
67
+ )
68
+
69
+ const componentsSection = (
70
+ <SidebarSection icon={Component} title={t("sidebar.components")}>
71
+ {uiComponents.map((comp) => (
72
+ <SidebarLink
73
+ key={comp.name}
74
+ href={`/components/${comp.name}`}
75
+ active={pathname === `/components/${comp.name}`}
76
+ >
77
+ {comp.label}
78
+ </SidebarLink>
79
+ ))}
80
+ </SidebarSection>
81
+ )
82
+
83
+ const blocksSection = blocks.length > 0 ? (
84
+ <SidebarSection icon={Blocks} title={t("sidebar.blocks")}>
85
+ {blocks.map((comp) => (
86
+ <SidebarLink
87
+ key={comp.name}
88
+ href={`/components/${comp.name}`}
89
+ active={pathname === `/components/${comp.name}`}
90
+ >
91
+ {comp.label}
92
+ </SidebarLink>
93
+ ))}
94
+ </SidebarSection>
95
+ ) : null
96
+
97
+ // Mobile: always show all three sections (no topbar tabs on mobile)
98
+ const mobileNavContent = (
99
+ <nav aria-label="Main navigation" className="p-4 space-y-4">
100
+ {docsSection}
101
+ {componentsSection}
102
+ {blocksSection}
103
+ </nav>
104
+ )
105
+
106
+ // Desktop: show only the active section (topbar tabs handle section switching)
107
+ const desktopNavContent = (
108
+ <nav aria-label="Main navigation" className="p-4 space-y-4">
109
+ {activeSection === "docs" && docsSection}
110
+ {activeSection === "components" && componentsSection}
111
+ {activeSection === "blocks" && blocksSection}
112
+ {/* Fallback: when we can't determine a section, show all */}
113
+ {activeSection === null && (
114
+ <>
115
+ {docsSection}
116
+ {componentsSection}
117
+ {blocksSection}
118
+ </>
119
+ )}
120
+ </nav>
121
+ )
122
+
123
+ const showMobile = display === "all" || display === "mobile"
124
+ const showDesktop = display === "all" || display === "desktop"
125
+
126
+ return (
127
+ <>
128
+ {/* Backdrop — mobile nav only (no backdrop in desktop fullscreen) */}
129
+ {showMobile && open && !collapsed && (
130
+ <Backdrop
131
+ belowHeader
132
+ className="md:hidden"
133
+ onClick={onClose}
134
+ />
135
+ )}
136
+
137
+ {/* Mobile floating card — mobile only */}
138
+ {showMobile && (
139
+ <aside
140
+ className={`
141
+ md:hidden fixed top-18 left-4 z-50 w-64 rounded-lg border border-border bg-background shadow-xl overflow-y-auto overflow-x-hidden transition-all duration-300 ease-in-out motion-reduce:transition-none
142
+ ${open ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-2 pointer-events-none"}
143
+ `}
144
+ style={{ maxHeight: "calc(100vh - 6rem)" }}
145
+ >
146
+ {mobileNavContent}
147
+ </aside>
148
+ )}
149
+
150
+ {/* Desktop floating card — only meaningful on component pages where the
151
+ user can enter fullscreen preview mode. On other sections the
152
+ fullscreen state is moot and the floating card just looks out of
153
+ place; skip rendering it entirely. */}
154
+ {showDesktop &&
155
+ (activeSection === "components" || activeSection === "blocks") && (
156
+ <aside
157
+ className={`
158
+ hidden md:block fixed top-18 left-4 z-50 w-64 rounded-lg border border-border bg-background shadow-xl overflow-y-auto overflow-x-hidden transition-all duration-300 ease-in-out motion-reduce:transition-none
159
+ ${open && collapsed ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-2 pointer-events-none"}
160
+ `}
161
+ style={{ maxHeight: "calc(100vh - 6rem)" }}
162
+ >
163
+ {desktopNavContent}
164
+ </aside>
165
+ )}
166
+
167
+ {/* Desktop inline sidebar — only when not collapsed */}
168
+ {showDesktop && !collapsed && (
169
+ <aside
170
+ className="hidden md:block w-64 border-r border-border bg-background h-[calc(100vh-3.5rem)] overflow-y-auto overflow-x-hidden sticky top-14 shrink-0"
171
+ >
172
+ {desktopNavContent}
173
+ </aside>
174
+ )}
175
+ </>
176
+ )
177
+ }
178
+
179
+ function SidebarSection({
180
+ icon: Icon,
181
+ title,
182
+ children,
183
+ }: {
184
+ icon: React.ComponentType<{ className?: string }>
185
+ title: string
186
+ children: React.ReactNode
187
+ }) {
188
+ return (
189
+ <div>
190
+ <div className="flex items-center gap-2 text-sm font-semibold text-foreground mb-2">
191
+ <Icon className="size-4" />
192
+ <span className="flex-1 text-left">{title}</span>
193
+ </div>
194
+ <ul className="space-y-1">{children}</ul>
195
+ </div>
196
+ )
197
+ }
198
+
199
+ function SidebarLink({
200
+ href,
201
+ active,
202
+ children,
203
+ }: {
204
+ href: string
205
+ active: boolean
206
+ children: React.ReactNode
207
+ }) {
208
+ return (
209
+ <li>
210
+ <Link
211
+ href={href}
212
+ className={`block text-sm px-2 py-1.5 rounded-md transition-colors ${
213
+ active
214
+ ? "bg-accent text-accent-foreground font-medium"
215
+ : "text-muted-foreground hover:text-foreground hover:bg-accent/50"
216
+ }`}
217
+ >
218
+ {children}
219
+ </Link>
220
+ </li>
221
+ )
222
+ }
@@ -0,0 +1,28 @@
1
+ "use client"
2
+
3
+ import { SnapshotModeProvider } from "@shell/components/preview-layout"
4
+ import { previewLoader } from "@shell/lib/preview-loader"
5
+
6
+ const { Preview } = previewLoader
7
+
8
+ /**
9
+ * Renders a component preview without the PreviewCanvas chrome.
10
+ * The preview components still use PreviewLayout internally, but this page
11
+ * provides a clean, minimal container for Playwright screenshots.
12
+ *
13
+ * Usage: /preview-snapshot/{component-name}
14
+ */
15
+ export function SnapshotPreview({ name }: { name: string }) {
16
+ return (
17
+ <SnapshotModeProvider>
18
+ <div data-snapshot-target className="inline-block">
19
+ <Preview
20
+ name={name}
21
+ fallback={
22
+ <p className="text-sm text-muted-foreground">No preview for {name}</p>
23
+ }
24
+ />
25
+ </div>
26
+ </SnapshotModeProvider>
27
+ )
28
+ }
@@ -0,0 +1,155 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import { CheckCircle2, XCircle, FlaskConical, Eye, Keyboard, Gauge, Shield } from "lucide-react"
5
+ import { Badge } from "@shell/components/shell-ui/badge"
6
+ import {
7
+ Accordion,
8
+ AccordionContent,
9
+ AccordionItem,
10
+ AccordionTrigger,
11
+ } from "@shell/components/shell-ui/accordion"
12
+ import {
13
+ EmptyState,
14
+ EmptyStateIcon,
15
+ EmptyStateDescription,
16
+ } from "@shell/components/shell-ui/empty-state"
17
+ import { useTranslations } from "@shell/lib/i18n"
18
+
19
+ interface TestFile {
20
+ file: string
21
+ type: string
22
+ tests: string[]
23
+ }
24
+
25
+ interface TestReport {
26
+ component: string
27
+ hasUnitTests: boolean
28
+ hasInteractionTests: boolean
29
+ hasVisualTests: boolean
30
+ hasA11yTests: boolean
31
+ hasPerformanceTests: boolean
32
+ hasProps: boolean
33
+ hasA11yDocs: boolean
34
+ hasPreview: boolean
35
+ testFiles: TestFile[]
36
+ totalTests: number
37
+ }
38
+
39
+ const typeIcons: Record<string, React.ComponentType<{ className?: string }>> = {
40
+ unit: FlaskConical,
41
+ interaction: Keyboard,
42
+ visual: Eye,
43
+ a11y: Shield,
44
+ performance: Gauge,
45
+ }
46
+
47
+ function CheckItem({ passed, label }: { passed: boolean; label: string }) {
48
+ return (
49
+ <div className="flex items-center gap-2 text-sm">
50
+ {passed ? (
51
+ <CheckCircle2 className="size-4 text-green-500 shrink-0" />
52
+ ) : (
53
+ <XCircle className="size-4 text-muted-foreground/40 shrink-0" />
54
+ )}
55
+ <span className={passed ? "text-foreground" : "text-muted-foreground"}>
56
+ {label}
57
+ </span>
58
+ </div>
59
+ )
60
+ }
61
+
62
+ export function TestInfo({ name }: { name: string }) {
63
+ const [report, setReport] = useState<TestReport | null>(null)
64
+ const [error, setError] = useState(false)
65
+ const t = useTranslations()
66
+
67
+ useEffect(() => {
68
+ fetch(`/tests/${name}.json`)
69
+ .then((r) => {
70
+ if (!r.ok) throw new Error()
71
+ return r.json()
72
+ })
73
+ .then(setReport)
74
+ .catch(() => setError(true))
75
+ }, [name])
76
+
77
+ if (error) {
78
+ return (
79
+ <EmptyState>
80
+ <EmptyStateIcon><FlaskConical /></EmptyStateIcon>
81
+ <EmptyStateDescription>{t("component.testsPlaceholder")}</EmptyStateDescription>
82
+ </EmptyState>
83
+ )
84
+ }
85
+
86
+ if (!report) {
87
+ return <p className="text-sm text-muted-foreground" role="status">{t("a11y.loading")}</p>
88
+ }
89
+
90
+ const hasAnyTests = report.totalTests > 0
91
+
92
+ return (
93
+ <div className="space-y-6">
94
+ {/* Health overview */}
95
+ <div className="space-y-3">
96
+ <h4 id="test-health" className="text-base font-semibold">{t("tests.health")}</h4>
97
+ <div className="grid grid-cols-2 gap-2">
98
+ <CheckItem passed={report.hasProps} label={t("tests.propsDocs")} />
99
+ <CheckItem passed={report.hasA11yDocs} label={t("tests.a11yDocs")} />
100
+ <CheckItem passed={report.hasPreview} label={t("tests.preview")} />
101
+ <CheckItem passed={report.hasUnitTests} label={t("tests.unit")} />
102
+ <CheckItem passed={report.hasInteractionTests} label={t("tests.interaction")} />
103
+ <CheckItem passed={report.hasVisualTests} label={t("tests.visual")} />
104
+ <CheckItem passed={report.hasA11yTests} label={t("tests.accessibility")} />
105
+ <CheckItem passed={report.hasPerformanceTests} label={t("tests.performance")} />
106
+ </div>
107
+ </div>
108
+
109
+ {hasAnyTests && (
110
+ <div className="space-y-3">
111
+ <h4 id="test-coverage" className="text-base font-semibold">
112
+ {t("tests.tests")}
113
+ <Badge variant="secondary" className="ml-2">{report.totalTests}</Badge>
114
+ </h4>
115
+
116
+ <Accordion type="multiple" defaultValue={report.testFiles.map((f) => f.type)}>
117
+ {report.testFiles.map((tf) => {
118
+ const Icon = typeIcons[tf.type] ?? FlaskConical
119
+
120
+ return (
121
+ <AccordionItem key={tf.file} value={tf.type}>
122
+ <AccordionTrigger>
123
+ <div className="flex items-center gap-2">
124
+ <Icon className="size-4 text-muted-foreground" />
125
+ <span>{t(`tests.${tf.type}` as string)}</span>
126
+ <Badge variant="outline" className="text-[10px]">{tf.tests.length}</Badge>
127
+ </div>
128
+ </AccordionTrigger>
129
+ <AccordionContent>
130
+ <p className="text-xs text-muted-foreground font-mono mb-2">{tf.file}</p>
131
+ <ul className="space-y-1">
132
+ {tf.tests.map((test, i) => (
133
+ <li key={i} className="text-sm text-muted-foreground flex items-center gap-2">
134
+ <CheckCircle2 className="size-3 text-green-500 shrink-0" />
135
+ {test}
136
+ </li>
137
+ ))}
138
+ </ul>
139
+ </AccordionContent>
140
+ </AccordionItem>
141
+ )
142
+ })}
143
+ </Accordion>
144
+ </div>
145
+ )}
146
+
147
+ {!hasAnyTests && (
148
+ <EmptyState>
149
+ <EmptyStateIcon><FlaskConical /></EmptyStateIcon>
150
+ <EmptyStateDescription>{t("component.testsPlaceholder")}</EmptyStateDescription>
151
+ </EmptyState>
152
+ )}
153
+ </div>
154
+ )
155
+ }
@@ -0,0 +1,16 @@
1
+ "use client"
2
+
3
+ import { ThemeProvider as NextThemesProvider } from "next-themes"
4
+
5
+ export function ThemeProvider({ children }: { children: React.ReactNode }) {
6
+ return (
7
+ <NextThemesProvider
8
+ attribute="class"
9
+ defaultTheme="system"
10
+ enableSystem
11
+ disableTransitionOnChange
12
+ >
13
+ {children}
14
+ </NextThemesProvider>
15
+ )
16
+ }
@@ -0,0 +1,21 @@
1
+ "use client"
2
+
3
+ import { useTheme } from "next-themes"
4
+ import { Moon, Sun } from "lucide-react"
5
+ import { Button } from "@shell/components/shell-ui/button"
6
+
7
+ export function ThemeToggle() {
8
+ const { setTheme, resolvedTheme } = useTheme()
9
+
10
+ return (
11
+ <Button
12
+ variant="ghost"
13
+ size="icon"
14
+ onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
15
+ aria-label="Toggle theme"
16
+ >
17
+ <Sun className="size-4 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
18
+ <Moon className="absolute size-4 scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
19
+ </Button>
20
+ )
21
+ }
@@ -0,0 +1,8 @@
1
+ "use client"
2
+
3
+ import { useTranslations } from "@shell/lib/i18n"
4
+
5
+ export function TranslatedText({ k }: { k: Parameters<ReturnType<typeof useTranslations>>[0] }) {
6
+ const t = useTranslations()
7
+ return <>{t(k)}</>
8
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Fallback homepage rendered at `/` when the user's config has no custom
3
+ * `homePage`. Two states:
4
+ * - Registry has content → generic index listing components/blocks/docs.
5
+ * - Registry empty / absent → terse "no registry wired" placeholder
6
+ * pointing at the shell's documentation site.
7
+ */
8
+ import Link from "next/link"
9
+ import { getAllComponents } from "@shell/lib/components-nav"
10
+ import { getAllDocs } from "@shell/lib/docs"
11
+ import type { HomePageProps } from "@shell/lib/registry-adapter"
12
+
13
+ export default function FallbackHomePage({ firstDocSlug }: HomePageProps) {
14
+ const items = getAllComponents()
15
+ const docs = getAllDocs()
16
+
17
+ if (items.length === 0 && docs.length === 0) return <NoRegistryPlaceholder />
18
+
19
+ const components = items.filter((c) => c.kind === "component")
20
+ const blocks = items.filter((c) => c.kind === "block")
21
+
22
+ return (
23
+ <main
24
+ id="main-content"
25
+ tabIndex={-1}
26
+ className="outline-none max-w-5xl mx-auto px-4 md:px-8 py-12"
27
+ >
28
+ <section className="mb-12">
29
+ <h1 className="text-3xl font-bold">Registry</h1>
30
+ <p className="mt-2 text-muted-foreground">
31
+ {components.length} component{components.length === 1 ? "" : "s"},{" "}
32
+ {blocks.length} block{blocks.length === 1 ? "" : "s"}, {docs.length} doc
33
+ {docs.length === 1 ? "" : "s"}
34
+ </p>
35
+ {firstDocSlug && (
36
+ <Link
37
+ href={`/docs/${firstDocSlug}`}
38
+ className="mt-4 inline-block text-sm underline underline-offset-4"
39
+ >
40
+ Browse documentation →
41
+ </Link>
42
+ )}
43
+ </section>
44
+
45
+ {components.length > 0 && (
46
+ <section className="mb-12">
47
+ <h2 className="text-xl font-semibold mb-4">Components</h2>
48
+ <ul className="grid grid-cols-2 md:grid-cols-3 gap-2 text-sm">
49
+ {components.map((c) => (
50
+ <li key={c.name}>
51
+ <Link className="hover:underline" href={`/components/${c.name}`}>
52
+ {c.label}
53
+ </Link>
54
+ </li>
55
+ ))}
56
+ </ul>
57
+ </section>
58
+ )}
59
+
60
+ {blocks.length > 0 && (
61
+ <section>
62
+ <h2 className="text-xl font-semibold mb-4">Blocks</h2>
63
+ <ul className="grid grid-cols-2 md:grid-cols-3 gap-2 text-sm">
64
+ {blocks.map((b) => (
65
+ <li key={b.name}>
66
+ <Link className="hover:underline" href={`/components/${b.name}`}>
67
+ {b.label}
68
+ </Link>
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ </section>
73
+ )}
74
+ </main>
75
+ )
76
+ }
77
+
78
+ /**
79
+ * Shown when the shell boots with no config AND no content. Zero external
80
+ * dependencies (no fetches, no dynamic content) — if a consumer ever sees
81
+ * this, they're either running `registry-shell dev` in a blank directory
82
+ * or they're mid-setup.
83
+ */
84
+ function NoRegistryPlaceholder() {
85
+ return (
86
+ <main
87
+ id="main-content"
88
+ tabIndex={-1}
89
+ className="outline-none max-w-xl mx-auto px-6 py-24 text-center"
90
+ >
91
+ <h1 className="text-2xl font-semibold">No registry wired</h1>
92
+ <p className="mt-3 text-sm text-muted-foreground">
93
+ Add a{" "}
94
+ <code className="bg-muted rounded px-1.5 py-0.5 text-xs">
95
+ registry-shell.config.ts
96
+ </code>{" "}
97
+ to this project and restart the dev server.
98
+ </p>
99
+ <p className="mt-8 text-xs text-muted-foreground">
100
+ Setup guide:{" "}
101
+ <a
102
+ href="https://github.com/scintillar-com/registry-shell"
103
+ className="underline underline-offset-4 hover:text-foreground"
104
+ target="_blank"
105
+ rel="noopener noreferrer"
106
+ >
107
+ scintillar-com/registry-shell
108
+ </a>
109
+ </p>
110
+ </main>
111
+ )
112
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Fallback previewLoader used when the user's registry has no
3
+ * `components/previews/index.ts`. Renders the caller's `fallback` prop (or
4
+ * nothing) for every name.
5
+ */
6
+ import { createElement, Fragment, type ReactNode } from "react"
7
+ import type { PreviewLoader } from "@shell/lib/registry-adapter"
8
+
9
+ function EmptyPreview({ fallback = null }: { name: string; fallback?: ReactNode }) {
10
+ return createElement(Fragment, null, fallback)
11
+ }
12
+
13
+ export const previewLoader: PreviewLoader = {
14
+ Preview: EmptyPreview,
15
+ load: () => null,
16
+ names: () => [],
17
+ }
@@ -0,0 +1,23 @@
1
+ "use client"
2
+
3
+ import { usePathname } from "next/navigation"
4
+ import type { ComponentMeta } from "@shell/lib/components-nav"
5
+
6
+ export type ActiveSection = "docs" | "components" | "blocks" | null
7
+
8
+ /**
9
+ * Returns the current navigation section based on the URL and component list.
10
+ * - `/docs/*` → "docs"
11
+ * - `/components/{name}` → "components" or "blocks" depending on the item's kind
12
+ * - anything else → null
13
+ */
14
+ export function useActiveSection(components: ComponentMeta[]): ActiveSection {
15
+ const pathname = usePathname()
16
+ if (pathname.startsWith("/docs/")) return "docs"
17
+ if (pathname.startsWith("/components/")) {
18
+ const name = pathname.split("/")[2]
19
+ const item = components.find((c) => c.name === name)
20
+ return item?.kind === "block" ? "blocks" : "components"
21
+ }
22
+ return null
23
+ }
@@ -0,0 +1,72 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+
5
+ interface SelectControl {
6
+ type: "select"
7
+ options: string[]
8
+ default: string
9
+ }
10
+
11
+ interface BooleanControl {
12
+ type: "boolean"
13
+ default: boolean
14
+ }
15
+
16
+ interface TextControl {
17
+ type: "text"
18
+ default: string
19
+ }
20
+
21
+ interface NumberControl {
22
+ type: "number"
23
+ default: number
24
+ min?: number
25
+ max?: number
26
+ }
27
+
28
+ type ControlDef = SelectControl | BooleanControl | TextControl | NumberControl
29
+
30
+ export type ControlDefs = Record<string, ControlDef>
31
+
32
+ type ControlValues<T extends ControlDefs> = {
33
+ [K in keyof T]: T[K] extends SelectControl
34
+ ? string
35
+ : T[K] extends BooleanControl
36
+ ? boolean
37
+ : T[K] extends TextControl
38
+ ? string
39
+ : T[K] extends NumberControl
40
+ ? number
41
+ : never
42
+ }
43
+
44
+ export interface ControlEntry {
45
+ name: string
46
+ def: ControlDef
47
+ value: unknown
48
+ onChange: (value: unknown) => void
49
+ }
50
+
51
+ export function useControls<T extends ControlDefs>(defs: T) {
52
+ const [values, setValues] = useState<Record<string, unknown>>(() => {
53
+ const initial: Record<string, unknown> = {}
54
+ for (const [key, def] of Object.entries(defs)) {
55
+ initial[key] = def.default
56
+ }
57
+ return initial
58
+ })
59
+
60
+ const entries: ControlEntry[] = Object.entries(defs).map(([name, def]) => ({
61
+ name,
62
+ def,
63
+ value: values[name],
64
+ onChange: (value: unknown) =>
65
+ setValues((prev) => ({ ...prev, [name]: value })),
66
+ }))
67
+
68
+ return {
69
+ values: values as ControlValues<T>,
70
+ entries,
71
+ }
72
+ }