handoff-app 1.2.2-7 → 2.0.0-1
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.
- package/Changelog.md +37 -15
- package/README.md +372 -118
- package/dist/app-builder/build.d.ts +41 -2
- package/dist/app-builder/build.js +455 -18
- package/dist/app-builder/client-config.d.ts +15 -2
- package/dist/app-builder/client-config.js +43 -4
- package/dist/app-builder/config-diff/snapshot.d.ts +1 -6
- package/dist/app-builder/config-diff/snapshot.js +2 -9
- package/dist/app-builder/index.d.ts +1 -1
- package/dist/app-builder/index.js +3 -1
- package/dist/app-builder/nav-shell.d.ts +23 -0
- package/dist/app-builder/nav-shell.js +80 -0
- package/dist/app-builder/paths.js +25 -1
- package/dist/app-builder/static-export.d.ts +17 -0
- package/dist/app-builder/static-export.js +206 -0
- package/dist/app-builder/vercel-output.d.ts +39 -0
- package/dist/app-builder/vercel-output.js +259 -0
- package/dist/app-builder/watchers/utils.d.ts +2 -0
- package/dist/app-builder/watchers.d.ts +14 -4
- package/dist/app-builder/watchers.js +123 -28
- package/dist/app-builder/websocket.js +1 -1
- package/dist/artifacts/index.d.ts +2 -0
- package/dist/artifacts/index.js +16 -0
- package/dist/artifacts/types.d.ts +136 -0
- package/dist/artifacts/types.js +24 -0
- package/dist/artifacts/url.d.ts +67 -0
- package/dist/artifacts/url.js +116 -0
- package/dist/cache/build-cache.js +48 -4
- package/dist/cli.js +0 -0
- package/dist/commands/build/app.js +1 -2
- package/dist/commands/build/index.d.ts +17 -0
- package/dist/commands/build/index.js +66 -0
- package/dist/commands/checkout/index.d.ts +24 -0
- package/dist/commands/checkout/index.js +71 -0
- package/dist/commands/db/migrate.d.ts +11 -0
- package/dist/commands/db/migrate.js +33 -0
- package/dist/commands/index.js +8 -0
- package/dist/commands/publish/index.d.ts +21 -0
- package/dist/commands/publish/index.js +68 -0
- package/dist/config/defaults.js +15 -1
- package/dist/config/loader.js +6 -1
- package/dist/config/normalizers/declaration.js +4 -1
- package/dist/config/normalizers/page.d.ts +18 -0
- package/dist/config/normalizers/page.js +34 -0
- package/dist/config/normalizers/pattern.js +7 -1
- package/dist/config/runtime.d.ts +4 -0
- package/dist/config/runtime.js +53 -5
- package/dist/index.d.ts +65 -1
- package/dist/index.js +123 -3
- package/dist/nav/index.d.ts +83 -0
- package/dist/nav/index.js +9 -0
- package/dist/nav/registry.d.ts +15 -0
- package/dist/nav/registry.js +42 -0
- package/dist/nav/resolver.d.ts +8 -0
- package/dist/nav/resolver.js +192 -0
- package/dist/nav/workspace.d.ts +18 -0
- package/dist/nav/workspace.js +103 -0
- package/dist/pipeline/styles.js +7 -35
- package/dist/pipeline/token-transformers.d.ts +45 -0
- package/dist/pipeline/token-transformers.js +71 -0
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
- package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
- package/dist/registry/asset-storage/define.d.ts +10 -0
- package/dist/registry/asset-storage/define.js +13 -0
- package/dist/registry/asset-storage/resolve.d.ts +45 -0
- package/dist/registry/asset-storage/resolve.js +36 -0
- package/dist/registry/asset-storage/types.d.ts +51 -0
- package/dist/registry/asset-storage/types.js +9 -0
- package/dist/registry/assets/layout.d.ts +22 -0
- package/dist/registry/assets/layout.js +30 -0
- package/dist/registry/assets/sets.d.ts +18 -0
- package/dist/registry/assets/sets.js +44 -0
- package/dist/registry/assets/transfer.d.ts +45 -0
- package/dist/registry/assets/transfer.js +11 -0
- package/dist/registry/checkout/assets.d.ts +16 -0
- package/dist/registry/checkout/assets.js +150 -0
- package/dist/registry/checkout/index.d.ts +33 -0
- package/dist/registry/checkout/index.js +610 -0
- package/dist/registry/checkout/tokens.d.ts +15 -0
- package/dist/registry/checkout/tokens.js +171 -0
- package/dist/registry/client.d.ts +61 -0
- package/dist/registry/client.js +211 -0
- package/dist/registry/connection.d.ts +30 -0
- package/dist/registry/connection.js +36 -0
- package/dist/registry/db/adapter.d.ts +43 -0
- package/dist/registry/db/adapter.js +59 -0
- package/dist/registry/db/client.d.ts +35 -0
- package/dist/registry/db/client.js +94 -0
- package/dist/registry/db/index.d.ts +5 -0
- package/dist/registry/db/index.js +24 -0
- package/dist/registry/db/migrate.d.ts +23 -0
- package/dist/registry/db/migrate.js +64 -0
- package/dist/registry/db/schema.d.ts +4869 -0
- package/dist/registry/db/schema.js +347 -0
- package/dist/registry/path.d.ts +4 -0
- package/dist/registry/path.js +31 -0
- package/dist/registry/publish/artifacts.d.ts +16 -0
- package/dist/registry/publish/artifacts.js +132 -0
- package/dist/registry/publish/assets.d.ts +16 -0
- package/dist/registry/publish/assets.js +129 -0
- package/dist/registry/publish/errors.d.ts +5 -0
- package/dist/registry/publish/errors.js +26 -0
- package/dist/registry/publish/index.d.ts +27 -0
- package/dist/registry/publish/index.js +126 -0
- package/dist/registry/publish/package.d.ts +27 -0
- package/dist/registry/publish/package.js +257 -0
- package/dist/registry/publish/publish-build.d.ts +12 -0
- package/dist/registry/publish/publish-build.js +43 -0
- package/dist/registry/publish/tokens.d.ts +18 -0
- package/dist/registry/publish/tokens.js +112 -0
- package/dist/registry/tokens/sets.d.ts +59 -0
- package/dist/registry/tokens/sets.js +109 -0
- package/dist/registry/tokens/transfer.d.ts +48 -0
- package/dist/registry/tokens/transfer.js +10 -0
- package/dist/registry/transfer.d.ts +75 -0
- package/dist/registry/transfer.js +11 -0
- package/dist/store/filesystem-assets.d.ts +17 -0
- package/dist/store/filesystem-assets.js +147 -0
- package/dist/store/filesystem.d.ts +76 -0
- package/dist/store/filesystem.js +208 -0
- package/dist/store/index.d.ts +20 -0
- package/dist/store/index.js +38 -0
- package/dist/store/registry.d.ts +77 -0
- package/dist/store/registry.js +350 -0
- package/dist/store/source-files.d.ts +24 -0
- package/dist/store/source-files.js +147 -0
- package/dist/store/types.d.ts +185 -0
- package/dist/store/types.js +12 -0
- package/dist/transformers/plugins/csf-render.js +17 -8
- package/dist/transformers/plugins/handlebars-previews.js +10 -3
- package/dist/transformers/plugins/ssr-render.js +116 -49
- package/dist/transformers/preview/component/api.js +3 -1
- package/dist/transformers/preview/component/artifacts.js +8 -1
- package/dist/transformers/preview/component/builder.js +13 -5
- package/dist/transformers/preview/component/css.js +9 -1
- package/dist/transformers/preview/component/javascript.d.ts +9 -3
- package/dist/transformers/preview/component/javascript.js +45 -17
- package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
- package/dist/transformers/preview/component/shared-artifacts.js +92 -0
- package/dist/transformers/preview/component.js +5 -2
- package/dist/transformers/preview/pattern/builder.js +28 -9
- package/dist/transformers/preview/pattern/html.d.ts +13 -10
- package/dist/transformers/preview/pattern/html.js +62 -27
- package/dist/transformers/preview/types.d.ts +38 -0
- package/dist/transformers/utils/handlebars.d.ts +3 -0
- package/dist/transformers/utils/handlebars.js +19 -5
- package/dist/types/config.d.ts +124 -31
- package/dist/utils/menu-shell.d.ts +60 -0
- package/dist/utils/menu-shell.js +221 -0
- package/dist/utils/stable-stringify.d.ts +2 -0
- package/dist/utils/stable-stringify.js +8 -0
- package/docs/cli.md +4 -2
- package/drizzle/0000_init_registry_storage.sql +95 -0
- package/drizzle/0001_add_review_metadata.sql +2 -0
- package/drizzle/0002_add_pages.sql +28 -0
- package/drizzle/0003_add_tokens.sql +29 -0
- package/drizzle/0004_add_assets.sql +37 -0
- package/drizzle/meta/0000_snapshot.json +642 -0
- package/drizzle/meta/0001_snapshot.json +654 -0
- package/drizzle/meta/0002_snapshot.json +835 -0
- package/drizzle/meta/0003_snapshot.json +1023 -0
- package/drizzle/meta/0004_snapshot.json +1252 -0
- package/drizzle/meta/_journal.json +41 -0
- package/drizzle.config.ts +16 -0
- package/package.json +10 -1
- package/src/app/components/Component/Preview.tsx +3 -2
- package/src/app/components/Component/RegistryActions.tsx +150 -0
- package/src/app/components/Foundations/DisplayLogo.tsx +11 -3
- package/src/app/components/Layout/Header.tsx +21 -3
- package/src/app/components/Layout/Main.tsx +11 -3
- package/src/app/components/Navigation/MainNav.tsx +4 -2
- package/src/app/components/Navigation/MobileNav.tsx +4 -2
- package/src/app/components/Navigation/SideNav.tsx +46 -11
- package/src/app/components/NotFound.tsx +24 -0
- package/src/app/components/context/NavProvider.tsx +71 -0
- package/src/app/components/old/Header.tsx +3 -3
- package/src/app/components/util/index.ts +203 -380
- package/src/app/components/util/useCollectionAssets.ts +125 -0
- package/src/app/components/util/useFoundationTokens.ts +67 -0
- package/src/app/generated/default-pages.json +1 -0
- package/src/app/generated/nav-shell.json +1 -0
- package/src/app/lib/api/query.ts +6 -0
- package/src/app/lib/api/redact.ts +46 -0
- package/src/app/lib/asset-storage.ts +81 -0
- package/src/app/lib/docs-api/artifacts.ts +116 -0
- package/src/app/lib/docs-api/backend.ts +151 -0
- package/src/app/lib/docs-api/errors.ts +52 -0
- package/src/app/lib/docs-api/handler.ts +34 -0
- package/src/app/lib/docs-api/index.ts +7 -0
- package/src/app/lib/docs-api/records.ts +189 -0
- package/src/app/lib/docs-api/registry.ts +130 -0
- package/src/app/lib/docs-api/runtime-config.ts +173 -0
- package/src/app/lib/docs-api/serve.ts +37 -0
- package/src/app/lib/docs-api/token-detail.ts +19 -0
- package/src/app/lib/registry-api/allowlist.ts +163 -0
- package/src/app/lib/registry-api/asset-transfer.ts +363 -0
- package/src/app/lib/registry-api/auth.ts +68 -0
- package/src/app/lib/registry-api/entity-routes.ts +197 -0
- package/src/app/lib/registry-api/errors.ts +51 -0
- package/src/app/lib/registry-api/files.ts +135 -0
- package/src/app/lib/registry-api/handler.ts +96 -0
- package/src/app/lib/registry-api/index.ts +4 -0
- package/src/app/lib/registry-api/meta.ts +87 -0
- package/src/app/lib/registry-api/revalidate.ts +36 -0
- package/src/app/lib/registry-api/store.ts +348 -0
- package/src/app/lib/registry-api/token-transfer.ts +231 -0
- package/src/app/lib/registry-api/transfer.ts +513 -0
- package/src/app/lib/registry-api/validation.ts +74 -0
- package/src/app/lib/registry-connection.ts +58 -0
- package/src/app/lib/utils.ts +14 -0
- package/src/app/next.config.mjs +72 -13
- package/src/app/pages/[...slug]/index.tsx +74 -20
- package/src/app/pages/_app.tsx +5 -2
- package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
- package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
- package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
- package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
- package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
- package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
- package/src/app/pages/api/docs/components/[id].ts +24 -0
- package/src/app/pages/api/docs/components.json.ts +12 -0
- package/src/app/pages/api/docs/nav.json.ts +48 -0
- package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
- package/src/app/pages/api/docs/patterns/[id].ts +24 -0
- package/src/app/pages/api/docs/patterns.json.ts +12 -0
- package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
- package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
- package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
- package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
- package/src/app/pages/api/registry/components/[id].ts +20 -0
- package/src/app/pages/api/registry/components/index.ts +19 -0
- package/src/app/pages/api/registry/health.ts +20 -0
- package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
- package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
- package/src/app/pages/api/registry/patterns/[id].ts +20 -0
- package/src/app/pages/api/registry/patterns/index.ts +19 -0
- package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
- package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
- package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
- package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
- package/src/app/pages/assets/fonts.tsx +41 -17
- package/src/app/pages/assets/index.tsx +2 -2
- package/src/app/pages/assets/logos.tsx +6 -3
- package/src/app/pages/foundations/colors.tsx +18 -8
- package/src/app/pages/foundations/effects.tsx +18 -8
- package/src/app/pages/foundations/icons/[name]/index.tsx +28 -16
- package/src/app/pages/foundations/icons/index.tsx +18 -12
- package/src/app/pages/foundations/index.tsx +1 -1
- package/src/app/pages/foundations/logo.tsx +12 -8
- package/src/app/pages/foundations/typography.tsx +19 -9
- package/src/app/pages/index.tsx +1 -1
- package/src/app/pages/system/component/[component]/index.tsx +87 -17
- package/src/app/pages/system/index.tsx +13 -4
- package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
- package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
- package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
- package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
- package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
- package/src/app/tsconfig.json +2 -3
- package/src/app-builder/build.ts +555 -11
- package/src/app-builder/client-config.ts +61 -4
- package/src/app-builder/config-diff/snapshot.ts +1 -11
- package/src/app-builder/index.ts +1 -1
- package/src/app-builder/nav-shell.ts +74 -0
- package/src/app-builder/paths.ts +27 -1
- package/src/app-builder/static-export.ts +242 -0
- package/src/app-builder/vercel-output.ts +297 -0
- package/src/app-builder/watchers/utils.ts +2 -0
- package/src/app-builder/watchers.ts +149 -25
- package/src/app-builder/websocket.ts +2 -2
- package/src/artifacts/README.md +14 -0
- package/src/artifacts/index.ts +24 -0
- package/src/artifacts/types.ts +146 -0
- package/src/artifacts/url.ts +122 -0
- package/src/cache/build-cache.ts +47 -2
- package/src/commands/build/app.ts +1 -2
- package/src/commands/build/index.ts +61 -0
- package/src/commands/checkout/index.ts +66 -0
- package/src/commands/db/migrate.ts +25 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/publish/index.ts +63 -0
- package/src/config/defaults.ts +15 -0
- package/src/config/loader.ts +10 -1
- package/src/config/normalizers/declaration.ts +3 -0
- package/src/config/normalizers/page.ts +45 -0
- package/src/config/normalizers/pattern.ts +4 -1
- package/src/config/runtime.ts +48 -2
- package/src/index.ts +126 -3
- package/src/nav/index.ts +94 -0
- package/src/nav/registry.ts +54 -0
- package/src/nav/resolver.ts +217 -0
- package/src/nav/workspace.ts +87 -0
- package/src/pipeline/styles.ts +4 -35
- package/src/pipeline/token-transformers.ts +97 -0
- package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
- package/src/registry/asset-storage/define.ts +15 -0
- package/src/registry/asset-storage/resolve.ts +72 -0
- package/src/registry/asset-storage/types.ts +46 -0
- package/src/registry/assets/layout.ts +38 -0
- package/src/registry/assets/sets.ts +48 -0
- package/src/registry/assets/transfer.ts +50 -0
- package/src/registry/checkout/assets.ts +126 -0
- package/src/registry/checkout/index.ts +596 -0
- package/src/registry/checkout/tokens.ts +152 -0
- package/src/registry/client.ts +234 -0
- package/src/registry/connection.ts +50 -0
- package/src/registry/db/adapter.ts +74 -0
- package/src/registry/db/client.ts +88 -0
- package/src/registry/db/index.ts +35 -0
- package/src/registry/db/migrate.ts +56 -0
- package/src/registry/db/schema.ts +444 -0
- package/src/registry/path.ts +26 -0
- package/src/registry/publish/artifacts.ts +147 -0
- package/src/registry/publish/assets.ts +129 -0
- package/src/registry/publish/errors.ts +34 -0
- package/src/registry/publish/index.ts +128 -0
- package/src/registry/publish/package.ts +272 -0
- package/src/registry/publish/publish-build.ts +51 -0
- package/src/registry/publish/tokens.ts +119 -0
- package/src/registry/tokens/sets.ts +132 -0
- package/src/registry/tokens/transfer.ts +53 -0
- package/src/registry/transfer.ts +82 -0
- package/src/store/filesystem-assets.ts +152 -0
- package/src/store/filesystem.ts +237 -0
- package/src/store/index.ts +62 -0
- package/src/store/registry.ts +334 -0
- package/src/store/source-files.ts +155 -0
- package/src/store/types.ts +210 -0
- package/src/transformers/plugins/csf-render.ts +31 -7
- package/src/transformers/plugins/handlebars-previews.ts +22 -3
- package/src/transformers/plugins/ssr-render.ts +145 -57
- package/src/transformers/preview/component/api.ts +2 -1
- package/src/transformers/preview/component/artifacts.ts +10 -1
- package/src/transformers/preview/component/builder.ts +9 -2
- package/src/transformers/preview/component/css.ts +10 -1
- package/src/transformers/preview/component/javascript.ts +55 -20
- package/src/transformers/preview/component/shared-artifacts.ts +146 -0
- package/src/transformers/preview/component.ts +5 -2
- package/src/transformers/preview/pattern/builder.ts +33 -7
- package/src/transformers/preview/pattern/html.ts +71 -27
- package/src/transformers/preview/types.ts +42 -0
- package/src/transformers/utils/handlebars.ts +31 -5
- package/src/types/config.ts +129 -31
- package/src/utils/menu-shell.ts +241 -0
- package/src/utils/stable-stringify.ts +7 -0
- package/templates/default/package.json.tpl +1 -1
- package/handoff.config.js +0 -90
- package/src/app/components/ComponentSearch.tsx +0 -93
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "7",
|
|
3
|
+
"dialect": "postgresql",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"idx": 0,
|
|
7
|
+
"version": "7",
|
|
8
|
+
"when": 1781979217509,
|
|
9
|
+
"tag": "0000_init_registry_storage",
|
|
10
|
+
"breakpoints": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"idx": 1,
|
|
14
|
+
"version": "7",
|
|
15
|
+
"when": 1782045350977,
|
|
16
|
+
"tag": "0001_add_review_metadata",
|
|
17
|
+
"breakpoints": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"idx": 2,
|
|
21
|
+
"version": "7",
|
|
22
|
+
"when": 1782713759115,
|
|
23
|
+
"tag": "0002_add_pages",
|
|
24
|
+
"breakpoints": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"idx": 3,
|
|
28
|
+
"version": "7",
|
|
29
|
+
"when": 1783354008332,
|
|
30
|
+
"tag": "0003_add_tokens",
|
|
31
|
+
"breakpoints": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"idx": 4,
|
|
35
|
+
"version": "7",
|
|
36
|
+
"when": 1783442253186,
|
|
37
|
+
"tag": "0004_add_assets",
|
|
38
|
+
"breakpoints": true
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'drizzle-kit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drizzle Kit config for generating the registry's package-owned migrations.
|
|
5
|
+
*
|
|
6
|
+
* Migrations are generated into `./drizzle` and shipped with the `handoff-app` package; the
|
|
7
|
+
* bundled runner (`handoff-app db:migrate`) applies them at install/deploy time. Both built-in
|
|
8
|
+
* adapters (`pg`, `neon`) target the Postgres dialect, so a single migration set serves both.
|
|
9
|
+
*
|
|
10
|
+
* Generate after schema changes with: `npx drizzle-kit generate`
|
|
11
|
+
*/
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
dialect: 'postgresql',
|
|
14
|
+
schema: './src/registry/db/schema.ts',
|
|
15
|
+
out: './drizzle',
|
|
16
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handoff-app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-1",
|
|
4
4
|
"description": "Automated documentation toolchain for building client side documentation from figma",
|
|
5
5
|
"author ": {
|
|
6
6
|
"name": "Convertiv",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@clack/prompts": "^0.7.0",
|
|
56
|
+
"@neondatabase/serverless": "^1.1.0",
|
|
56
57
|
"@radix-ui/react-accordion": "^1.2.8",
|
|
57
58
|
"@radix-ui/react-collapsible": "^1.1.2",
|
|
58
59
|
"@radix-ui/react-dialog": "^1.1.4",
|
|
@@ -78,6 +79,7 @@
|
|
|
78
79
|
"@types/react-syntax-highlighter": "^15.5.7",
|
|
79
80
|
"@types/refractor": "^3.0.2",
|
|
80
81
|
"@types/ws": "^8.18.1",
|
|
82
|
+
"@vercel/blob": "^1.1.1",
|
|
81
83
|
"@vitejs/plugin-react": "^4.5.2",
|
|
82
84
|
"archiver": "^5.3.1",
|
|
83
85
|
"chalk": "^4.1.2",
|
|
@@ -86,6 +88,7 @@
|
|
|
86
88
|
"clsx": "^2.1.1",
|
|
87
89
|
"cross-spawn": "^7.0.3",
|
|
88
90
|
"dotenv": "^16.0.3",
|
|
91
|
+
"drizzle-orm": "^0.45.2",
|
|
89
92
|
"esbuild": "^0.25.0",
|
|
90
93
|
"eslint": "^8.23.1",
|
|
91
94
|
"eslint-config-next": "^15.3.4",
|
|
@@ -101,6 +104,7 @@
|
|
|
101
104
|
"next": "^15.3.4",
|
|
102
105
|
"next-themes": "^0.4.4",
|
|
103
106
|
"node-html-parser": "^6.1.4",
|
|
107
|
+
"pg": "^8.22.0",
|
|
104
108
|
"postcss": "^8.4.16",
|
|
105
109
|
"prettier": "^3.4.2",
|
|
106
110
|
"react": "^19.1.0",
|
|
@@ -121,13 +125,18 @@
|
|
|
121
125
|
"ws": "^8.18.3",
|
|
122
126
|
"yargs": "^17.7.2"
|
|
123
127
|
},
|
|
128
|
+
"peerDependencies": {
|
|
129
|
+
"typescript": ">=5.6 <7"
|
|
130
|
+
},
|
|
124
131
|
"devDependencies": {
|
|
125
132
|
"@types/archiver": "^5.3.1",
|
|
126
133
|
"@types/cross-spawn": "^6.0.2",
|
|
127
134
|
"@types/fs-extra": "^11.0.4",
|
|
128
135
|
"@types/handlebars": "^4.1.0",
|
|
129
136
|
"@types/node": "^20.11.18",
|
|
137
|
+
"@types/pg": "^8.20.0",
|
|
130
138
|
"@types/yargs": "^17.0.33",
|
|
139
|
+
"drizzle-kit": "^0.31.10",
|
|
131
140
|
"np": "^10.2.0",
|
|
132
141
|
"prettier-plugin-tailwindcss": "^0.7.0",
|
|
133
142
|
"sass-embedded": "^1.89.0",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildArtifactUrl } from '@handoff/artifacts/url';
|
|
1
2
|
import { SlotMetadata } from '@handoff/transformers/preview/component';
|
|
2
3
|
import { PageSlice } from '@handoff/transformers/preview/types';
|
|
3
4
|
import { PreviewObject } from '@handoff/types/preview';
|
|
@@ -314,7 +315,7 @@ export const ComponentDisplay: React.FC<{
|
|
|
314
315
|
className="h-7 px-3 hover:bg-gray-300 [&_svg]:size-3"
|
|
315
316
|
onClick={() => {
|
|
316
317
|
// open in new tab
|
|
317
|
-
window.open(
|
|
318
|
+
window.open(buildArtifactUrl(`component/${previewUrl}`, process.env.HANDOFF_APP_BASE_PATH ?? ''), '_blank');
|
|
318
319
|
}}
|
|
319
320
|
variant="ghost"
|
|
320
321
|
>
|
|
@@ -344,7 +345,7 @@ export const ComponentDisplay: React.FC<{
|
|
|
344
345
|
display: 'block',
|
|
345
346
|
margin: '0 auto',
|
|
346
347
|
}}
|
|
347
|
-
src={
|
|
348
|
+
src={buildArtifactUrl(`component/${previewUrl}`, process.env.HANDOFF_APP_BASE_PATH ?? '')}
|
|
348
349
|
/>
|
|
349
350
|
</div>
|
|
350
351
|
) : (
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { CheckIcon, CopyIcon, Download } from 'lucide-react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import type { RuntimeMode } from '@handoff/types/config';
|
|
6
|
+
import { cn } from '../../lib/utils';
|
|
7
|
+
import { Button } from '../ui/button';
|
|
8
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
|
9
|
+
|
|
10
|
+
type RegistryEntityType = 'component' | 'pattern';
|
|
11
|
+
|
|
12
|
+
interface RegistryActionsProps {
|
|
13
|
+
/** Resolved runtime mode projected to the client. */
|
|
14
|
+
mode: RuntimeMode;
|
|
15
|
+
/** Whether this is a connected workspace (workspace mode + resolvable registry URL). */
|
|
16
|
+
connected: boolean;
|
|
17
|
+
/** The kind of entity the page documents. */
|
|
18
|
+
entityType: RegistryEntityType;
|
|
19
|
+
/** Stable id of the component/pattern, used in the copyable command. */
|
|
20
|
+
id: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type RegistryAction = {
|
|
24
|
+
label: string;
|
|
25
|
+
title: string;
|
|
26
|
+
description: React.ReactNode;
|
|
27
|
+
command: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Registry transfer affordance for the single component/pattern page. Surfaces a context-appropriate
|
|
32
|
+
* helper based on the runtime:
|
|
33
|
+
* - registry mode -> "Checkout" instructions (pull the entity into a connected workspace);
|
|
34
|
+
* - connected workspace (workspace mode + registry URL) -> "Publish" instructions.
|
|
35
|
+
*
|
|
36
|
+
* A static export is never a live runtime, so the publish affordance is suppressed there using the
|
|
37
|
+
* same `HANDOFF_BUILD_TARGET` gate the runtime-mode badge uses. Renders nothing when no action
|
|
38
|
+
* applies (e.g. a plain, unconnected workspace).
|
|
39
|
+
*/
|
|
40
|
+
export const RegistryActions: React.FC<RegistryActionsProps> = ({ mode, connected, entityType, id }) => {
|
|
41
|
+
// The static export is a frozen snapshot, not a live workspace, so the publish hint carries no
|
|
42
|
+
// meaning there and is omitted (mirrors the runtime-mode badge in the header).
|
|
43
|
+
const isStaticSnapshot = process.env.HANDOFF_BUILD_TARGET === 'static';
|
|
44
|
+
|
|
45
|
+
const action = resolveAction({ mode, connected, entityType, id, isStaticSnapshot });
|
|
46
|
+
if (!action) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Popover>
|
|
52
|
+
<PopoverTrigger asChild>
|
|
53
|
+
<Button variant="outline" size="sm" className="font-normal [&_svg]:size-3!">
|
|
54
|
+
<Download aria-hidden="true" />
|
|
55
|
+
{action.label}
|
|
56
|
+
</Button>
|
|
57
|
+
</PopoverTrigger>
|
|
58
|
+
<PopoverContent align="start" className="w-[360px] p-4">
|
|
59
|
+
<div className="flex flex-col gap-3">
|
|
60
|
+
<div className="flex items-center gap-2">
|
|
61
|
+
<Download className="size-4 text-gray-700 dark:text-gray-200" aria-hidden="true" />
|
|
62
|
+
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">{action.title}</p>
|
|
63
|
+
</div>
|
|
64
|
+
<p className="text-sm text-muted-foreground">{action.description}</p>
|
|
65
|
+
<div className="border-t" />
|
|
66
|
+
<div className="flex items-stretch overflow-hidden rounded-md border">
|
|
67
|
+
<code className="flex flex-1 items-center truncate px-3 py-2 font-mono text-xs text-gray-700 dark:text-gray-300">
|
|
68
|
+
{action.command}
|
|
69
|
+
</code>
|
|
70
|
+
<CopyCommandButton command={action.command} />
|
|
71
|
+
</div>
|
|
72
|
+
<p className="text-xs text-muted-foreground">Requires a workspace configured with registry access.</p>
|
|
73
|
+
</div>
|
|
74
|
+
</PopoverContent>
|
|
75
|
+
</Popover>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Copy-to-clipboard control for the command box. No tooltip — just a copy/checkmark toggle. */
|
|
80
|
+
const CopyCommandButton: React.FC<{ command: string }> = ({ command }) => {
|
|
81
|
+
const [copied, setCopied] = React.useState(false);
|
|
82
|
+
|
|
83
|
+
const handleCopy = async () => {
|
|
84
|
+
try {
|
|
85
|
+
await navigator.clipboard.writeText(command);
|
|
86
|
+
setCopied(true);
|
|
87
|
+
setTimeout(() => setCopied(false), 1500);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error('Failed to copy command: ', err);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Button
|
|
95
|
+
type="button"
|
|
96
|
+
variant="ghost"
|
|
97
|
+
size="icon"
|
|
98
|
+
onClick={handleCopy}
|
|
99
|
+
aria-label={copied ? 'Copied' : 'Copy command'}
|
|
100
|
+
className="relative h-auto w-9 self-stretch rounded-none border-0 border-l shadow-none"
|
|
101
|
+
>
|
|
102
|
+
<span className={cn('transition-all', copied ? 'scale-100 opacity-100' : 'scale-0 opacity-0')}>
|
|
103
|
+
<CheckIcon className="stroke-gray-600" size={16} aria-hidden="true" />
|
|
104
|
+
</span>
|
|
105
|
+
<span className={cn('absolute transition-all', copied ? 'scale-0 opacity-0' : 'scale-100 opacity-100')}>
|
|
106
|
+
<CopyIcon size={16} aria-hidden="true" />
|
|
107
|
+
</span>
|
|
108
|
+
</Button>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const resolveAction = ({
|
|
113
|
+
mode,
|
|
114
|
+
connected,
|
|
115
|
+
entityType,
|
|
116
|
+
id,
|
|
117
|
+
isStaticSnapshot,
|
|
118
|
+
}: RegistryActionsProps & { isStaticSnapshot: boolean }): RegistryAction | null => {
|
|
119
|
+
if (mode === 'registry') {
|
|
120
|
+
return {
|
|
121
|
+
label: 'Checkout',
|
|
122
|
+
title: `Checkout ${entityType}`,
|
|
123
|
+
description: (
|
|
124
|
+
<>
|
|
125
|
+
Run this in a connected workspace to pull <strong className="font-medium text-foreground">{id}</strong> from the
|
|
126
|
+
registry.
|
|
127
|
+
</>
|
|
128
|
+
),
|
|
129
|
+
command: `npm run checkout -- ${entityType} ${id}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (mode === 'workspace' && connected && !isStaticSnapshot) {
|
|
134
|
+
return {
|
|
135
|
+
label: 'Publish',
|
|
136
|
+
title: `Publish ${entityType}`,
|
|
137
|
+
description: (
|
|
138
|
+
<>
|
|
139
|
+
Run this in your connected workspace to publish <strong className="font-medium text-foreground">{id}</strong> to
|
|
140
|
+
the registry.
|
|
141
|
+
</>
|
|
142
|
+
),
|
|
143
|
+
command: `npm run publish -- ${entityType} ${id}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default RegistryActions;
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { Types as CoreTypes } from 'handoff-core';
|
|
2
1
|
import React from 'react';
|
|
2
|
+
import type { DisplayableAsset } from '../util/useCollectionAssets';
|
|
3
3
|
|
|
4
|
-
export const DisplayLogo: React.FC<{ logo:
|
|
4
|
+
export const DisplayLogo: React.FC<{ logo: DisplayableAsset; content?: string; correct?: boolean }> = ({ logo, content }) => {
|
|
5
|
+
// Workspace/static ships the SVG body inline in tokens.json (`data`) — render it directly so the
|
|
6
|
+
// logo has no dependency on the fetch-only `/api/assets/logos/*` files (absent on a static host).
|
|
7
|
+
// Registry has no inline body; it falls back to the individual asset content URL (`src`).
|
|
8
|
+
const src = logo.src ?? `${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/assets/logos/${logo.path}`;
|
|
5
9
|
return (
|
|
6
10
|
<div className="flex flex-col gap-5">
|
|
7
|
-
|
|
11
|
+
{logo.data ? (
|
|
12
|
+
<div className="rounded-3xl" dangerouslySetInnerHTML={{ __html: logo.data }} />
|
|
13
|
+
) : (
|
|
14
|
+
<img src={src} alt={logo.name} className="rounded-3xl" />
|
|
15
|
+
)}
|
|
8
16
|
<div className="flex flex-row gap-4">
|
|
9
17
|
<img src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/assets/images/check-circle.svg`} alt="Do" className="h-6 w-6" />
|
|
10
18
|
<p className="leading-normal text-gray-500">
|
|
@@ -4,11 +4,22 @@ import { ModeToggle } from '../../components/ModeSwitcher';
|
|
|
4
4
|
import { MainNav } from '../../components/Navigation/MainNav';
|
|
5
5
|
import { MobileNav } from '../../components/Navigation/MobileNav';
|
|
6
6
|
import { cn } from '../../lib/utils';
|
|
7
|
+
import { Badge } from '../ui/badge';
|
|
7
8
|
import { useConfigContext } from '../context/ConfigContext';
|
|
8
9
|
|
|
10
|
+
const RUNTIME_MODE_LABEL: Record<'workspace' | 'registry', string> = {
|
|
11
|
+
workspace: 'Workspace',
|
|
12
|
+
registry: 'Registry',
|
|
13
|
+
};
|
|
14
|
+
|
|
9
15
|
export function Header() {
|
|
10
16
|
const context = useConfigContext();
|
|
11
17
|
const [isScrolled, setIsScrolled] = useState(false);
|
|
18
|
+
const runtimeMode = context.config?.runtime?.mode ?? 'workspace';
|
|
19
|
+
const runtimeModeLabel = RUNTIME_MODE_LABEL[runtimeMode];
|
|
20
|
+
// The static export is a self-contained snapshot of the workspace, not a live runtime, so the
|
|
21
|
+
// runtime-mode badge carries no meaning there and is omitted.
|
|
22
|
+
const isStaticSnapshot = process.env.HANDOFF_BUILD_TARGET === 'static';
|
|
12
23
|
|
|
13
24
|
useEffect(() => {
|
|
14
25
|
const handleScroll = () => {
|
|
@@ -28,9 +39,16 @@ export function Header() {
|
|
|
28
39
|
>
|
|
29
40
|
<header className="border-grid container mx-auto w-full max-w-[1500px] bg-transparent px-8">
|
|
30
41
|
<div className="mx-auto flex items-center justify-between @container">
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
42
|
+
<div className="flex items-center gap-3">
|
|
43
|
+
<Link href="/">
|
|
44
|
+
<img className="max-h-5" src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt={context.config?.app?.title} />
|
|
45
|
+
</Link>
|
|
46
|
+
{!isStaticSnapshot && (
|
|
47
|
+
<Badge variant={runtimeMode === 'registry' ? 'info' : 'default'} aria-label={`Runtime mode: ${runtimeModeLabel}`}>
|
|
48
|
+
{runtimeModeLabel}
|
|
49
|
+
</Badge>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
34
52
|
<div className="hidden items-center gap-4 @2xl:flex">
|
|
35
53
|
<MainNav />
|
|
36
54
|
<ModeToggle />
|
|
@@ -5,6 +5,7 @@ import { Header } from '../../components/Layout/Header';
|
|
|
5
5
|
import { ThemeProvider } from '../../components/util/theme-provider';
|
|
6
6
|
import SideNav from '../Navigation/SideNav';
|
|
7
7
|
import { ConfigContextProvider } from '../context/ConfigContext';
|
|
8
|
+
import { hasRenderableNav } from '../../lib/utils';
|
|
8
9
|
import { SidebarInset, SidebarProvider } from '../ui/sidebar';
|
|
9
10
|
import { SectionLink } from '../util';
|
|
10
11
|
|
|
@@ -21,6 +22,13 @@ interface LayoutComponentProps {
|
|
|
21
22
|
fullWidthHero?: boolean;
|
|
22
23
|
}
|
|
23
24
|
export default function Layout<LayoutComponentProps>({ children, config, menu, metadata, current, fullWidthHero = false }) {
|
|
25
|
+
// In registry mode the per-page `current` is empty for lambda-rendered (fallback) pages, so mount
|
|
26
|
+
// the sidebar regardless and let SideNav resolve its section from the cached shell (and collapse
|
|
27
|
+
// itself to full width when that section has no renderable nav). Workspace/static know the section
|
|
28
|
+
// at render time, so drop the left column entirely for pages with no side-nav content (e.g. a
|
|
29
|
+
// standalone page with no children) and let the content go full width.
|
|
30
|
+
const isRegistry = config?.runtime?.mode === 'registry';
|
|
31
|
+
const showSidebar = isRegistry || hasRenderableNav(current as SectionLink);
|
|
24
32
|
return (
|
|
25
33
|
<div>
|
|
26
34
|
<ConfigContextProvider defaultConfig={config} defaultMenu={menu}>
|
|
@@ -48,10 +56,10 @@ export default function Layout<LayoutComponentProps>({ children, config, menu, m
|
|
|
48
56
|
<div className="mx-auto w-full">{children}</div>
|
|
49
57
|
</div>
|
|
50
58
|
</div>
|
|
51
|
-
) :
|
|
59
|
+
) : showSidebar ? (
|
|
52
60
|
<SidebarProvider>
|
|
53
61
|
<div className="flex w-full">
|
|
54
|
-
<SideNav
|
|
62
|
+
<SideNav />
|
|
55
63
|
<SidebarInset className="relative bg-transparent py-8 pl-8 pr-8 md:pl-8 lg:gap-10 lg:py-16 lg:pl-16">
|
|
56
64
|
<div className="mx-auto w-full">{children}</div>
|
|
57
65
|
</SidebarInset>
|
|
@@ -59,7 +67,7 @@ export default function Layout<LayoutComponentProps>({ children, config, menu, m
|
|
|
59
67
|
</SidebarProvider>
|
|
60
68
|
) : (
|
|
61
69
|
<div className="flex w-full">
|
|
62
|
-
<div className="relative bg-transparent px-16 py-8 lg:gap-10 lg:py-16
|
|
70
|
+
<div className="relative w-full bg-transparent px-16 py-8 lg:gap-10 lg:py-16">
|
|
63
71
|
<div className="mx-auto w-full">{children}</div>
|
|
64
72
|
</div>
|
|
65
73
|
</div>
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../../components/ui/navigation-menu';
|
|
11
11
|
import { cn } from '../../lib/utils';
|
|
12
12
|
import { useConfigContext } from '../context/ConfigContext';
|
|
13
|
+
import { useNav } from '../context/NavProvider';
|
|
13
14
|
|
|
14
15
|
const trimSlashes = (input: string): string => {
|
|
15
16
|
return input.replace(/^\/+|\/+$/g, '');
|
|
@@ -18,11 +19,12 @@ const trimSlashes = (input: string): string => {
|
|
|
18
19
|
export function MainNav() {
|
|
19
20
|
const context = useConfigContext();
|
|
20
21
|
const router = useRouter();
|
|
22
|
+
const { menu } = useNav();
|
|
21
23
|
return (
|
|
22
24
|
<NavigationMenu>
|
|
23
25
|
<NavigationMenuList>
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
+
{menu &&
|
|
27
|
+
menu.map((section) => {
|
|
26
28
|
const isActive = trimSlashes(router.asPath).startsWith(trimSlashes(section.path));
|
|
27
29
|
return (
|
|
28
30
|
<NavigationMenuItem key={section.title}>
|
|
@@ -7,6 +7,7 @@ import { Button } from '../../components/ui/button';
|
|
|
7
7
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../components/ui/sheet';
|
|
8
8
|
import { cn } from '../../lib/utils';
|
|
9
9
|
import { useConfigContext } from '../context/ConfigContext';
|
|
10
|
+
import { useNav } from '../context/NavProvider';
|
|
10
11
|
|
|
11
12
|
const trimSlashes = (input: string): string => {
|
|
12
13
|
return input.replace(/^\/+|\/+$/g, '');
|
|
@@ -16,6 +17,7 @@ export function MobileNav() {
|
|
|
16
17
|
const context = useConfigContext();
|
|
17
18
|
const router = useRouter();
|
|
18
19
|
const { theme, setTheme } = useTheme();
|
|
20
|
+
const { menu } = useNav();
|
|
19
21
|
|
|
20
22
|
const toggleTheme = () => {
|
|
21
23
|
if (theme === 'light') {
|
|
@@ -48,8 +50,8 @@ export function MobileNav() {
|
|
|
48
50
|
</SheetTrigger>
|
|
49
51
|
</SheetHeader>
|
|
50
52
|
<div className="mt-8 flex flex-col space-y-4">
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
+
{menu &&
|
|
54
|
+
menu.map((section) => {
|
|
53
55
|
const isActive = trimSlashes(router.asPath).startsWith(trimSlashes(section.path));
|
|
54
56
|
return (
|
|
55
57
|
<Link
|
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
} from 'lucide-react';
|
|
17
17
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../components/ui/collapsible';
|
|
18
18
|
|
|
19
|
+
import Link from 'next/link';
|
|
19
20
|
import { useRouter } from 'next/router';
|
|
20
|
-
import React from 'react';
|
|
21
|
+
import React, { useEffect, useState } from 'react';
|
|
21
22
|
import {
|
|
22
23
|
Sidebar,
|
|
23
24
|
SidebarContent,
|
|
@@ -30,8 +31,22 @@ import {
|
|
|
30
31
|
SidebarMenuSub,
|
|
31
32
|
SidebarSeparator,
|
|
32
33
|
} from '../../components/ui/sidebar';
|
|
33
|
-
import { normalizePathForMatch
|
|
34
|
-
import {
|
|
34
|
+
import { normalizePathForMatch } from '../../lib/utils';
|
|
35
|
+
import { useNav } from '../context/NavProvider';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `next/link` re-applies `basePath`, but the menu item paths already carry the base prefix (built by
|
|
39
|
+
* the server navigation resolver). Strip the leading base segment once so the link is not
|
|
40
|
+
* double-prefixed; with no base path this just returns a leading-slash route.
|
|
41
|
+
*/
|
|
42
|
+
const stripBasePath = (p: string): string => {
|
|
43
|
+
const base = (process.env.HANDOFF_APP_BASE_PATH ?? '').replace(/^\/+|\/+$/g, '');
|
|
44
|
+
let rel = p.replace(/^\/+/, '');
|
|
45
|
+
if (base && (rel === base || rel.startsWith(`${base}/`))) {
|
|
46
|
+
rel = rel.slice(base.length).replace(/^\/+/, '');
|
|
47
|
+
}
|
|
48
|
+
return `/${rel}`;
|
|
49
|
+
};
|
|
35
50
|
|
|
36
51
|
const NormalMenuItem = ({ title, icon, path }) => {
|
|
37
52
|
const router = useRouter();
|
|
@@ -39,10 +54,13 @@ const NormalMenuItem = ({ title, icon, path }) => {
|
|
|
39
54
|
return (
|
|
40
55
|
<SidebarMenuItem>
|
|
41
56
|
<SidebarMenuButton asChild isActive={isActive}>
|
|
42
|
-
|
|
57
|
+
{/* Soft (client-side) navigation so moving between entity pages reuses the already-loaded
|
|
58
|
+
cached nav with no reload/flash. Prefetch is off: each prefetch would invoke the
|
|
59
|
+
fallback:'blocking' lambda data route, so fetch the target's data on click instead. */}
|
|
60
|
+
<Link href={stripBasePath(path)} prefetch={false} className="gap-3">
|
|
43
61
|
<MenuIcon icon={icon} isActive={isActive} />
|
|
44
62
|
<span>{title}</span>
|
|
45
|
-
</
|
|
63
|
+
</Link>
|
|
46
64
|
</SidebarMenuButton>
|
|
47
65
|
</SidebarMenuItem>
|
|
48
66
|
);
|
|
@@ -53,8 +71,15 @@ const CollapsibleMenuItem = ({ title, icon, path, menu }) => {
|
|
|
53
71
|
const isActive = menu.some(
|
|
54
72
|
(item) => normalizePathForMatch(router.asPath).startsWith(normalizePathForMatch(item.path))
|
|
55
73
|
);
|
|
74
|
+
// Controlled so the active group auto-opens on soft navigation (an uncontrolled `defaultOpen` is
|
|
75
|
+
// only read on mount and would not re-open the active group when the route changes client-side),
|
|
76
|
+
// while still letting the user collapse/expand groups manually.
|
|
77
|
+
const [open, setOpen] = useState(isActive);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (isActive) setOpen(true);
|
|
80
|
+
}, [isActive]);
|
|
56
81
|
return (
|
|
57
|
-
<Collapsible
|
|
82
|
+
<Collapsible open={open} onOpenChange={setOpen} className="group/collapsible">
|
|
58
83
|
<SidebarMenuItem>
|
|
59
84
|
<CollapsibleTrigger asChild>
|
|
60
85
|
<SidebarMenuButton className="h-9 gap-3">
|
|
@@ -118,13 +143,23 @@ const MenuIcon = ({ icon, isActive = false }) => {
|
|
|
118
143
|
}
|
|
119
144
|
};
|
|
120
145
|
|
|
121
|
-
const SideNav = (
|
|
146
|
+
const SideNav = () => {
|
|
147
|
+
const { current } = useNav();
|
|
148
|
+
const sections = current?.subSections ?? [];
|
|
149
|
+
|
|
150
|
+
// Collapse the sidebar entirely (render nothing, reserve no gutter) when the resolved section has
|
|
151
|
+
// no renderable content, so the page content goes full width. A `dynamic` slot counts as
|
|
152
|
+
// renderable even before its entities load, to avoid a full-width → gutter flash in registry mode.
|
|
153
|
+
const hasContent = sections.some((section) => (section.menu && section.menu.length > 0) || !!section.dynamic);
|
|
154
|
+
if (!hasContent) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
122
158
|
return (
|
|
123
159
|
<Sidebar className="sticky left-auto">
|
|
124
160
|
<SidebarContent className="px-4 pt-5">
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
menu.subSections.map((section, index) => (
|
|
161
|
+
{sections.length > 0 &&
|
|
162
|
+
sections.map((section, index) => (
|
|
128
163
|
<React.Fragment key={index}>
|
|
129
164
|
<SidebarGroup>
|
|
130
165
|
{!section.path && <SidebarGroupLabel>{section.title}</SidebarGroupLabel>}
|
|
@@ -138,7 +173,7 @@ const SideNav = ({ menu }: { menu: SectionLink }) => {
|
|
|
138
173
|
</SidebarGroupContent>
|
|
139
174
|
)}
|
|
140
175
|
</SidebarGroup>
|
|
141
|
-
{index <
|
|
176
|
+
{index < sections.length && <SidebarSeparator className="mx-4" />}
|
|
142
177
|
</React.Fragment>
|
|
143
178
|
))}
|
|
144
179
|
</SidebarContent>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface NotFoundProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: React.ReactNode;
|
|
6
|
+
linkHref?: string;
|
|
7
|
+
linkLabel?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const NotFound: React.FC<NotFoundProps> = () => {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex flex-col items-center">
|
|
13
|
+
<div className="mb-2 text-7xl font-bold text-gray-800 dark:text-white">404</div>
|
|
14
|
+
<h1 className="mb-4 text-2xl font-semibold text-gray-700 dark:text-gray-300">Oops! Page not found.</h1>
|
|
15
|
+
<p className="mb-6 max-w-md text-center text-gray-500 dark:text-gray-400">
|
|
16
|
+
This page doesn’t exist or has been moved.
|
|
17
|
+
<br />
|
|
18
|
+
Please check the URL or return to the homepage.
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default NotFound;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { NavData, SectionLink } from '@handoff/nav';
|
|
4
|
+
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
export type { NavData, NavEntity } from '@handoff/nav';
|
|
7
|
+
|
|
8
|
+
interface INavContext {
|
|
9
|
+
nav: NavData;
|
|
10
|
+
currentSectionId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const EMPTY_NAV: NavData = { shell: [] };
|
|
14
|
+
const NavContext = createContext<INavContext>({ nav: EMPTY_NAV, currentSectionId: '' });
|
|
15
|
+
const isRegistryMode = process.env.HANDOFF_RUNTIME_MODE === 'registry';
|
|
16
|
+
|
|
17
|
+
// A module-level fulfilled cache and in-flight guard make the registry refresh a single hard-load
|
|
18
|
+
// operation, including StrictMode remounts. A browser hard reload resets both and observes publishes.
|
|
19
|
+
let cachedNav: NavData | null = null;
|
|
20
|
+
let inFlight: Promise<NavData | null> | null = null;
|
|
21
|
+
|
|
22
|
+
const loadNav = (): Promise<NavData | null> => {
|
|
23
|
+
if (cachedNav) return Promise.resolve(cachedNav);
|
|
24
|
+
if (inFlight) return inFlight;
|
|
25
|
+
inFlight = fetch(`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/docs/nav.json`)
|
|
26
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
27
|
+
.then((data: NavData | null) => {
|
|
28
|
+
if (data) cachedNav = data;
|
|
29
|
+
return cachedNav;
|
|
30
|
+
})
|
|
31
|
+
.catch(() => null)
|
|
32
|
+
.finally(() => {
|
|
33
|
+
inFlight = null;
|
|
34
|
+
});
|
|
35
|
+
return inFlight;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const NavProvider: React.FC<{
|
|
39
|
+
children: React.ReactNode;
|
|
40
|
+
initialNav?: NavData;
|
|
41
|
+
currentSectionId?: string;
|
|
42
|
+
}> = ({ children, initialNav = EMPTY_NAV, currentSectionId = '' }) => {
|
|
43
|
+
// Registry alone owns refresh state. Workspace renders the latest page prop directly, so soft
|
|
44
|
+
// navigation cannot briefly expose the preceding page's tree and triggers no provider effect.
|
|
45
|
+
const [registryNav, setRegistryNav] = useState<NavData>(() => cachedNav ?? initialNav);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!isRegistryMode || cachedNav) return;
|
|
49
|
+
let active = true;
|
|
50
|
+
loadNav().then((data) => {
|
|
51
|
+
if (active && data) setRegistryNav(data);
|
|
52
|
+
});
|
|
53
|
+
return () => {
|
|
54
|
+
active = false;
|
|
55
|
+
};
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const nav = isRegistryMode ? registryNav : initialNav;
|
|
60
|
+
return <NavContext.Provider value={{ nav, currentSectionId }}>{children}</NavContext.Provider>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** The sole view-facing navigation API: one active tree and an exact-match active section. */
|
|
64
|
+
export const useNav = (): { nav: NavData; menu: SectionLink[]; current: SectionLink | null } => {
|
|
65
|
+
const { nav, currentSectionId } = useContext(NavContext);
|
|
66
|
+
return {
|
|
67
|
+
nav,
|
|
68
|
+
menu: nav.shell,
|
|
69
|
+
current: nav.shell.find((section) => section.path === currentSectionId) ?? null,
|
|
70
|
+
};
|
|
71
|
+
};
|