@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,206 @@
1
+ /**
2
+ * `defineConfig` — the single entry point registry builders use.
3
+ *
4
+ * ```ts
5
+ * // registry-shell.config.ts
6
+ * import { defineConfig } from "@sntlr/registry-shell"
7
+ *
8
+ * export default defineConfig({
9
+ * branding: { siteName: "My UI", shortName: "UI", ... },
10
+ * // paths/homePage/adapter are optional
11
+ * })
12
+ * ```
13
+ */
14
+
15
+ export interface GithubConfig {
16
+ /** GitHub org or user that owns the repo. */
17
+ owner: string
18
+ /** Repo name. */
19
+ repo: string
20
+ /** Button label in the header. Default: `"Github"`. */
21
+ label?: string
22
+ /**
23
+ * Show the public star count (fetched server-side, revalidated hourly).
24
+ * Default: `true`.
25
+ */
26
+ showStars?: boolean
27
+ }
28
+
29
+ export interface BrandingConfig {
30
+ /** Full product name, e.g. "My UI". Used in HTML title. */
31
+ siteName: string
32
+ /** Short breadcrumb label, e.g. "UI". */
33
+ shortName: string
34
+ /** Canonical URL of the deployed registry, e.g. "https://ui.example.com". */
35
+ siteUrl?: string
36
+ /** SEO meta description. Shown in search results + social cards. */
37
+ description?: string
38
+ /** Path/URL to a 1200×630 Open Graph image. Relative to `siteUrl` if no scheme. */
39
+ ogImage?: string
40
+ /** Twitter handle (without `@`) for Twitter card attribution. */
41
+ twitterHandle?: string
42
+ /**
43
+ * Optional. Adds a GitHub link button to the header. Omit to hide the
44
+ * button entirely (default).
45
+ */
46
+ github?: GithubConfig
47
+ /** Accessible alt text for the logo image. Default: siteName. */
48
+ logoAlt?: string
49
+ /** Public path to the dark-theme SVG favicon. */
50
+ faviconDark?: string
51
+ /** Public path to the light-theme SVG favicon. */
52
+ faviconLight?: string
53
+ /** Public path to a fallback `.ico` favicon. */
54
+ faviconIco?: string
55
+ }
56
+
57
+ /**
58
+ * Filesystem locations the default adapter scans. All paths are relative to
59
+ * the config file's directory. Any path can be omitted to use the default.
60
+ */
61
+ export interface ShellPaths {
62
+ /** Component source files. Default: "components/ui". */
63
+ components?: string
64
+ /** Block directories (each block is a folder). Default: "registry/new-york/blocks". */
65
+ blocks?: string
66
+ /** Preview index file. Default: "components/previews/index.ts" (or .tsx). */
67
+ previews?: string
68
+ /** Doc MDX files. Default: "content/docs". */
69
+ docs?: string
70
+ /** Built registry JSON files (served at /r/[name].json). Default: "public/r". */
71
+ registryJson?: string
72
+ /** A11y JSON files (served at /a11y/[name].json). Default: "public/a11y". */
73
+ a11y?: string
74
+ /** Test JSON files (served at /tests/[name].json). Default: "public/tests". */
75
+ tests?: string
76
+ /** Props JSON files (served at /props/[name].json). Default: "public/props". */
77
+ props?: string
78
+ /** Block names to omit from navigation (e.g. example blocks). */
79
+ skipBlocks?: string[]
80
+ /**
81
+ * Optional. Path to a `.css` file the shell imports AFTER its own
82
+ * globals. Use this for brand fonts (`@font-face`), token overrides
83
+ * (redefine `--primary` etc. on `:root` / `.dark`), extra `@source`
84
+ * directives, or any custom utilities.
85
+ *
86
+ * Imported at the very end of the shell's `globals.css` so your `:root`
87
+ * declarations win the cascade against the shell's defaults.
88
+ *
89
+ * Example: `globalCss: "./styles/theme.css"`.
90
+ */
91
+ globalCss?: string
92
+ /**
93
+ * Optional. Directory (relative to the config file) where
94
+ * `registry-shell build` writes Next's build output, and where
95
+ * `registry-shell start` reads it back from. Default: `.next`.
96
+ *
97
+ * Override only if `.next` collides with something else in your
98
+ * project. Most Next.js hosts (Vercel, Netlify, self-hosted)
99
+ * auto-detect `.next` — if you change this, update your host's
100
+ * "Output Directory" setting to match.
101
+ */
102
+ buildOutput?: string
103
+ }
104
+
105
+ /**
106
+ * Advanced: point at a custom adapter module. The module must default-export
107
+ * a factory `(resolved: ResolvedShellConfig) => RegistryAdapter`. When unset,
108
+ * the shell uses its built-in convention-based adapter.
109
+ */
110
+ export type CustomAdapterSpec = string
111
+
112
+ export interface ShellConfig {
113
+ /**
114
+ * Required. Displayed in shell chrome.
115
+ */
116
+ branding: BrandingConfig
117
+
118
+ /**
119
+ * When `true`, docs are organized under per-locale subfolders
120
+ * (e.g. `content/docs/en/foo.mdx`, `content/docs/fr/foo.mdx`) and each
121
+ * subfolder name is treated as a locale code. Requires `defaultLocale`.
122
+ *
123
+ * When `false` (default), docs live directly under `paths.docs` and
124
+ * locale variants use the file-extension convention `{slug}.{locale}.mdx`
125
+ * alongside the canonical `{slug}.mdx`.
126
+ */
127
+ multilocale?: boolean
128
+
129
+ /**
130
+ * Required when `multilocale` is `true`. Locale code (e.g. `"en"`) of the
131
+ * subfolder containing the canonical doc set. Other locales are optional
132
+ * translations and fall back to this one when a slug is missing.
133
+ */
134
+ defaultLocale?: string
135
+
136
+ /**
137
+ * Optional in multilocale mode. Explicit list of locale codes the shell
138
+ * should offer in its locale toggle (e.g. `["en", "fr", "ja"]`). When
139
+ * unset, the shell auto-discovers locales by scanning subfolders under
140
+ * `paths.docs`.
141
+ *
142
+ * Ignored in single-locale mode (the toggle is hidden).
143
+ */
144
+ locales?: string[]
145
+
146
+ /**
147
+ * Optional. Override filesystem layout. Defaults match the sntlr-registry
148
+ * convention.
149
+ */
150
+ paths?: ShellPaths
151
+
152
+ /**
153
+ * Optional. Path to a custom homepage module, relative to config dir.
154
+ * The module must default-export a React component accepting
155
+ * `{ firstDocSlug?: string }` props.
156
+ */
157
+ homePage?: string
158
+
159
+ /**
160
+ * Optional. Locale → key → value dictionaries merged into the shell's
161
+ * built-in i18n table. Use for marketing copy referenced by a custom
162
+ * homepage.
163
+ */
164
+ extraTranslations?: Record<string, Record<string, string>>
165
+
166
+ /**
167
+ * Optional. Path to a custom adapter module. See `CustomAdapterSpec`.
168
+ */
169
+ adapter?: CustomAdapterSpec
170
+
171
+ /**
172
+ * Optional. Pin the dev/start server to a specific port. Falls through to
173
+ * Next.js's default (3000, auto-incrementing if in use) when unset.
174
+ */
175
+ port?: number
176
+
177
+ /**
178
+ * Optional. Extra npm package names the shell's Next.js build should
179
+ * transpile. Use this when your registry depends on another workspace
180
+ * package (e.g. a shared components library) whose TSX files should be
181
+ * compiled the same way as your own.
182
+ *
183
+ * Forwarded to Next's `transpilePackages`. The shell itself
184
+ * (`@sntlr/registry-shell`) is always transpiled regardless.
185
+ */
186
+ transpilePackages?: string[]
187
+
188
+ /**
189
+ * Optional. Template the shell uses to render the install command in the
190
+ * component "Install" tab. Supported placeholders:
191
+ * - `{name}` — the component/block slug (e.g. `"button"`)
192
+ * - `{siteUrl}` — `branding.siteUrl` (trailing slash stripped)
193
+ *
194
+ * Default: `"npx shadcn@latest add {siteUrl}/r/{name}.json"`. Set to an
195
+ * empty string to hide the install line entirely.
196
+ */
197
+ installCommandTemplate?: string
198
+ }
199
+
200
+ /**
201
+ * Identity function with type inference — users call this purely for editor
202
+ * support. No runtime validation here; the shell validates at boot time.
203
+ */
204
+ export function defineConfig(config: ShellConfig): ShellConfig {
205
+ return config
206
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @sntlr/registry-shell — public API.
3
+ *
4
+ * Registry builders import `defineConfig` from here into their
5
+ * `registry-shell.config.ts`. Custom-adapter authors also import
6
+ * `ResolvedShellConfig` to type their factory's argument.
7
+ */
8
+ export { defineConfig } from "./define-config.js"
9
+ export type {
10
+ ShellConfig,
11
+ BrandingConfig,
12
+ GithubConfig,
13
+ ShellPaths,
14
+ CustomAdapterSpec,
15
+ } from "./define-config.js"
16
+ export type { ResolvedShellConfig } from "./config-loader.js"
17
+ export type { AdapterOverrides } from "./adapter/custom.js"
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Catch-all route that serves arbitrary files from the user registry's
3
+ * `public/` directory. Fires only when no other app route matches, so it
4
+ * doesn't interfere with static routes like `/`, `/docs/...`, `/r/...`,
5
+ * etc. The shell's own `public/` assets are served first by Next.js's
6
+ * built-in static handler; this route handles the user's project's assets
7
+ * since Next only serves the bundled package's `public/`.
8
+ *
9
+ * Examples of what this enables:
10
+ * - `![foo](./my-image.png)` inside user MDX resolving to
11
+ * `{userRoot}/public/my-image.png`
12
+ * - Custom favicons referenced in `branding.faviconDark` that live under
13
+ * `{userRoot}/public/`
14
+ */
15
+ import "server-only"
16
+ import fs from "node:fs"
17
+ import path from "node:path"
18
+ import { NextRequest } from "next/server"
19
+
20
+ const MIME: Record<string, string> = {
21
+ ".png": "image/png",
22
+ ".jpg": "image/jpeg",
23
+ ".jpeg": "image/jpeg",
24
+ ".gif": "image/gif",
25
+ ".webp": "image/webp",
26
+ ".avif": "image/avif",
27
+ ".svg": "image/svg+xml",
28
+ ".ico": "image/x-icon",
29
+ ".woff": "font/woff",
30
+ ".woff2": "font/woff2",
31
+ ".ttf": "font/ttf",
32
+ ".otf": "font/otf",
33
+ ".json": "application/json",
34
+ ".txt": "text/plain; charset=utf-8",
35
+ ".xml": "application/xml",
36
+ ".pdf": "application/pdf",
37
+ ".mp4": "video/mp4",
38
+ ".webm": "video/webm",
39
+ }
40
+
41
+ export async function GET(
42
+ _request: NextRequest,
43
+ { params }: { params: Promise<{ asset: string[] }> },
44
+ ) {
45
+ const { asset } = await params
46
+ const userRoot = process.env.USER_REGISTRY_ROOT
47
+ if (!userRoot || asset.length === 0) {
48
+ return new Response("Not found", { status: 404 })
49
+ }
50
+
51
+ // Reject traversal.
52
+ if (asset.some((seg) => seg.includes("..") || seg.includes("\0"))) {
53
+ return new Response("Forbidden", { status: 403 })
54
+ }
55
+
56
+ const publicDir = path.join(userRoot, "public")
57
+ const filePath = path.join(publicDir, ...asset)
58
+
59
+ // Defense-in-depth: make sure the resolved path is still under public/.
60
+ const resolved = path.resolve(filePath)
61
+ if (!resolved.startsWith(path.resolve(publicDir) + path.sep)) {
62
+ return new Response("Forbidden", { status: 403 })
63
+ }
64
+
65
+ try {
66
+ const stat = await fs.promises.stat(resolved)
67
+ if (!stat.isFile()) return new Response("Not found", { status: 404 })
68
+ const body = await fs.promises.readFile(resolved)
69
+ const type = MIME[path.extname(resolved).toLowerCase()] ?? "application/octet-stream"
70
+ return new Response(new Uint8Array(body), {
71
+ status: 200,
72
+ headers: {
73
+ "Content-Type": type,
74
+ "Content-Length": String(stat.size),
75
+ "Cache-Control": "public, max-age=0, must-revalidate",
76
+ },
77
+ })
78
+ } catch {
79
+ return new Response("Not found", { status: 404 })
80
+ }
81
+ }
@@ -0,0 +1,6 @@
1
+ /* Auto-generated by @sntlr/registry-shell. Do not edit.
2
+ *
3
+ * This file is rewritten by the CLI on every `registry-shell dev/build` boot.
4
+ * When a registry sets `paths.globalCss` in its config, the generated file
5
+ * `@import`s that user CSS. Without a config (shell-only mode), it stays a
6
+ * no-op so the `@import "./_user-global.css"` in globals.css never 404s. */
@@ -0,0 +1,9 @@
1
+ /* Auto-generated by @sntlr/registry-shell. Do not edit. */
2
+ @source "./";
3
+ @source "../components";
4
+ @source "../lib";
5
+ @source "../hooks";
6
+ @source "../fallback";
7
+ @source "../../../../sntlr-registry/components/ui";
8
+ @source "../../../../sntlr-registry/registry/new-york/blocks";
9
+ @source "../../../../sntlr-registry/components/previews";
@@ -0,0 +1,19 @@
1
+ import { NextRequest, NextResponse } from "next/server"
2
+ import { registry } from "@shell/registry.config"
3
+
4
+ /**
5
+ * Serves the user registry's `public/a11y/{name}.json` files. The shell's
6
+ * own `public/` doesn't hold these (they live in the user's project), so a
7
+ * route handler proxies the read through the adapter.
8
+ */
9
+ export async function GET(
10
+ _request: NextRequest,
11
+ { params }: { params: Promise<{ name: string }> },
12
+ ) {
13
+ const { name } = await params
14
+ const data = registry?.getA11yData ? await registry.getA11yData(name) : null
15
+ if (!data) {
16
+ return NextResponse.json({ error: "A11y data not found" }, { status: 404 })
17
+ }
18
+ return NextResponse.json(data)
19
+ }
@@ -0,0 +1,19 @@
1
+ import { NextResponse } from "next/server"
2
+ import { getAllDocs } from "@shell/lib/docs"
3
+ import { getAllComponents } from "@shell/lib/components-nav"
4
+
5
+ export function GET() {
6
+ const docs = getAllDocs().map((doc) => ({
7
+ label: doc.title,
8
+ href: `/docs/${doc.slug}`,
9
+ group: "Documentation",
10
+ }))
11
+
12
+ const components = getAllComponents().map((comp) => ({
13
+ label: comp.label,
14
+ href: `/components/${comp.name}`,
15
+ group: "Components",
16
+ }))
17
+
18
+ return NextResponse.json([...docs, ...components])
19
+ }
@@ -0,0 +1,61 @@
1
+ import { notFound } from "next/navigation"
2
+ import { getAllComponents } from "@shell/lib/components-nav"
3
+ import { registry } from "@shell/registry.config"
4
+ import { ComponentPreview } from "@shell/components/component-preview"
5
+ import { ComponentTabs } from "@shell/components/component-tabs"
6
+ import { TranslatedText } from "@shell/components/translated-text"
7
+ import { ResizablePreview } from "@shell/components/resizable-preview"
8
+ import { ComponentBreadcrumb } from "@shell/components/component-breadcrumb"
9
+
10
+ export function generateStaticParams() {
11
+ return getAllComponents().map((comp) => ({ name: comp.name }))
12
+ }
13
+
14
+ export function generateMetadata({ params }: { params: Promise<{ name: string }> }) {
15
+ return params.then(({ name }) => {
16
+ const comp = getAllComponents().find((c) => c.name === name)
17
+ if (!comp) return {}
18
+ return {
19
+ title: `${comp.label} - UI Registry`,
20
+ }
21
+ })
22
+ }
23
+
24
+ export default async function ComponentPage({
25
+ params,
26
+ }: {
27
+ params: Promise<{ name: string }>
28
+ }) {
29
+ const { name } = await params
30
+ const comp = getAllComponents().find((c) => c.name === name)
31
+
32
+ if (!comp) notFound()
33
+
34
+ const source = registry?.getComponentSource(name) ?? null
35
+
36
+ return (
37
+ <div>
38
+ {/* Component header — sticky on desktop, breadcrumb on mobile when scrolled */}
39
+ <ComponentBreadcrumb name={comp.label}>
40
+ <div className="sticky top-14 z-20 bg-background border-b border-border max-md:static max-md:border-b-0">
41
+ <div className="mx-auto px-4 md:px-8 py-4 max-md:py-3">
42
+ <h1 className="text-2xl max-md:text-xl font-bold">{comp.label}</h1>
43
+ <p className="text-sm text-muted-foreground max-md:hidden">
44
+ <TranslatedText k="component.subtitle" />
45
+ </p>
46
+ </div>
47
+ </div>
48
+ </ComponentBreadcrumb>
49
+
50
+ {/* Resizable preview area */}
51
+ <ResizablePreview>
52
+ <ComponentPreview name={name} />
53
+ </ResizablePreview>
54
+
55
+ {/* Tabs with reserved TOC column on the right. Content clamps to 900px on xl. */}
56
+ <div className="mx-auto px-4 md:px-8 pb-8 w-full">
57
+ <ComponentTabs name={name} source={source} />
58
+ </div>
59
+ </div>
60
+ )
61
+ }
@@ -0,0 +1,18 @@
1
+ import { SidebarLayout } from "@shell/components/sidebar-layout"
2
+ import { getAllDocs } from "@shell/lib/docs"
3
+ import { getAllComponents } from "@shell/lib/components-nav"
4
+
5
+ export default function ComponentsLayout({
6
+ children,
7
+ }: {
8
+ children: React.ReactNode
9
+ }) {
10
+ const docs = getAllDocs()
11
+ const components = getAllComponents()
12
+
13
+ return (
14
+ <SidebarLayout docs={docs} components={components}>
15
+ {children}
16
+ </SidebarLayout>
17
+ )
18
+ }
@@ -0,0 +1,53 @@
1
+ import { notFound } from "next/navigation"
2
+ import { getAllDocs, getDocBySlug, getDocAllLocales } from "@shell/lib/docs"
3
+ import { DocsToc } from "@shell/components/docs-toc"
4
+ import { LocalizedMdx } from "@shell/components/localized-mdx"
5
+
6
+ export function generateStaticParams() {
7
+ return getAllDocs().map((doc) => ({ slug: doc.slug }))
8
+ }
9
+
10
+ export function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
11
+ return params.then(({ slug }) => {
12
+ const doc = getDocBySlug(slug)
13
+ if (!doc) return {}
14
+ return {
15
+ title: `${doc.meta.title} - UI Registry`,
16
+ description: doc.meta.description,
17
+ }
18
+ })
19
+ }
20
+
21
+ export default async function DocPage({
22
+ params,
23
+ }: {
24
+ params: Promise<{ slug: string }>
25
+ }) {
26
+ const { slug } = await params
27
+ const doc = getDocBySlug(slug)
28
+
29
+ if (!doc) notFound()
30
+
31
+ const locales = getDocAllLocales(slug)
32
+
33
+ return (
34
+ <div className="mx-auto py-10 px-4 md:px-8 w-full max-w-300 flex justify-center gap-8">
35
+ {/* Left spacer mirrors the TOC width to keep the article column
36
+ horizontally centered under the topbar at xl+. */}
37
+ <div className="hidden xl:block w-44 shrink-0" aria-hidden="true" />
38
+ <article
39
+ data-docs-content
40
+ className="prose prose-zinc dark:prose-invert flex-1 min-w-0 xl:max-w-225"
41
+ >
42
+ <LocalizedMdx locales={locales} />
43
+ </article>
44
+ {/* TOC column — space always reserved at xl+ to avoid layout shift when
45
+ headings change; inner element hidden below xl since there's no room. */}
46
+ <div className="hidden xl:block w-44 shrink-0">
47
+ <div className="sticky top-20">
48
+ <DocsToc />
49
+ </div>
50
+ </div>
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,18 @@
1
+ import { SidebarLayout } from "@shell/components/sidebar-layout"
2
+ import { getAllDocs } from "@shell/lib/docs"
3
+ import { getAllComponents } from "@shell/lib/components-nav"
4
+
5
+ export default function DocsLayout({
6
+ children,
7
+ }: {
8
+ children: React.ReactNode
9
+ }) {
10
+ const docs = getAllDocs()
11
+ const components = getAllComponents()
12
+
13
+ return (
14
+ <SidebarLayout docs={docs} components={components}>
15
+ {children}
16
+ </SidebarLayout>
17
+ )
18
+ }