zz-meridian 0.0.0-stage → 0.2.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.
- package/README.md +49 -2
- package/dist/adopt.js +296 -0
- package/dist/cli.js +76 -0
- package/dist/create.js +62 -0
- package/dist/files.js +144 -0
- package/package.json +32 -4
- package/payload/.env.example +14 -0
- package/payload/.gitignore +39 -0
- package/payload/AGENTS.md +20 -0
- package/payload/CHANGELOG.md +78 -0
- package/payload/CLAUDE.md +1 -0
- package/payload/CONTRIBUTING.md +107 -0
- package/payload/README.md +109 -0
- package/payload/app/(dashboard)/README.md +48 -0
- package/payload/app/(dashboard)/analytics/README.md +52 -0
- package/payload/app/(dashboard)/analytics/page.tsx +32 -0
- package/payload/app/(dashboard)/customers/README.md +42 -0
- package/payload/app/(dashboard)/customers/page.tsx +13 -0
- package/payload/app/(dashboard)/error.tsx +33 -0
- package/payload/app/(dashboard)/health/README.md +58 -0
- package/payload/app/(dashboard)/health/page.tsx +23 -0
- package/payload/app/(dashboard)/keys/README.md +47 -0
- package/payload/app/(dashboard)/keys/actions.ts +24 -0
- package/payload/app/(dashboard)/keys/page.tsx +12 -0
- package/payload/app/(dashboard)/layout.tsx +20 -0
- package/payload/app/(dashboard)/loading.tsx +40 -0
- package/payload/app/(dashboard)/members/README.md +49 -0
- package/payload/app/(dashboard)/members/actions.ts +37 -0
- package/payload/app/(dashboard)/members/page.tsx +12 -0
- package/payload/app/(dashboard)/not-found.tsx +8 -0
- package/payload/app/(dashboard)/page.tsx +33 -0
- package/payload/app/(dashboard)/requests/README.md +56 -0
- package/payload/app/(dashboard)/requests/[id]/README.md +51 -0
- package/payload/app/(dashboard)/requests/[id]/page.tsx +23 -0
- package/payload/app/(dashboard)/requests/page.tsx +15 -0
- package/payload/app/(dashboard)/settings/README.md +51 -0
- package/payload/app/(dashboard)/settings/page.tsx +17 -0
- package/payload/app/api/assistant/route.ts +31 -0
- package/payload/app/embed/health/README.md +36 -0
- package/payload/app/embed/health/page.tsx +18 -0
- package/payload/app/embed/health/view.tsx +49 -0
- package/payload/app/embed/layout.tsx +9 -0
- package/payload/app/embed/overview/README.md +26 -0
- package/payload/app/embed/overview/page.tsx +22 -0
- package/payload/app/embed/overview/view.tsx +74 -0
- package/payload/app/embed/proposal/README.md +43 -0
- package/payload/app/embed/proposal/page.tsx +11 -0
- package/payload/app/embed/proposal/view.tsx +45 -0
- package/payload/app/embed/requests/README.md +33 -0
- package/payload/app/embed/requests/page.tsx +18 -0
- package/payload/app/embed/requests/view.tsx +112 -0
- package/payload/app/globals.css +10 -0
- package/payload/app/icon.ts +27 -0
- package/payload/app/layout.tsx +36 -0
- package/payload/app/not-found/README.md +44 -0
- package/payload/app/not-found.tsx +19 -0
- package/payload/app/sign-in/README.md +50 -0
- package/payload/app/sign-in/page.tsx +35 -0
- package/payload/app/sign-in/panel.tsx +65 -0
- package/payload/app/system/(atlas)/[section]/[id]/page.tsx +82 -0
- package/payload/app/system/(atlas)/layout.tsx +14 -0
- package/payload/app/system/(atlas)/page.tsx +163 -0
- package/payload/app/system/preview/[section]/[card]/page.tsx +11 -0
- package/payload/app/system/preview/[section]/[card]/stage.tsx +27 -0
- package/payload/app/system/states/error/page.tsx +9 -0
- package/payload/app/system/states/layout.tsx +18 -0
- package/payload/app/system/states/loading/page.tsx +7 -0
- package/payload/decisions/0001-react-and-the-repository.md +20 -0
- package/payload/decisions/0002-zandro-register.md +20 -0
- package/payload/decisions/0003-accent-as-two-numbers.md +19 -0
- package/payload/decisions/0004-three-surfaces-two-operators.md +19 -0
- package/payload/decisions/0005-the-meridian.md +16 -0
- package/payload/decisions/0006-quiet-light.md +21 -0
- package/payload/decisions/0007-the-name.md +18 -0
- package/payload/decisions/0008-an-assistant-in-the-console.md +22 -0
- package/payload/decisions/0009-a-package-that-copies.md +38 -0
- package/payload/docs/agents.md +70 -0
- package/payload/docs/assistant.md +132 -0
- package/payload/docs/benchmark.md +74 -0
- package/payload/docs/data-display.md +57 -0
- package/payload/docs/distribution.md +157 -0
- package/payload/docs/start-a-dashboard.md +73 -0
- package/payload/docs/surfaces.md +160 -0
- package/payload/docs/voice.md +35 -0
- package/payload/eslint.config.ts +35 -0
- package/payload/next.config.ts +10 -0
- package/payload/package.json +63 -0
- package/payload/pnpm-lock.yaml +7982 -0
- package/payload/pnpm-workspace.yaml +4 -0
- package/payload/postcss.config.mjs +3 -0
- package/payload/scripts/assistant.ts +276 -0
- package/payload/scripts/audit.ts +166 -0
- package/payload/scripts/brand.ts +257 -0
- package/payload/scripts/check.ts +209 -0
- package/payload/scripts/contrast.ts +136 -0
- package/payload/scripts/fake-llm.ts +165 -0
- package/payload/scripts/gate.ts +38 -0
- package/payload/scripts/interactions.ts +109 -0
- package/payload/scripts/keyboard.ts +78 -0
- package/payload/scripts/lib/bin.ts +8 -0
- package/payload/scripts/lib/chrome.ts +97 -0
- package/payload/scripts/lib/routes.ts +27 -0
- package/payload/scripts/registry.ts +64 -0
- package/payload/scripts/shot.ts +32 -0
- package/payload/scripts/tokens.ts +235 -0
- package/payload/scripts/verify.config.ts +41 -0
- package/payload/scripts/verify.ts +179 -0
- package/payload/scripts/vitals.ts +90 -0
- package/payload/skills/zz-meridian/SKILL.md +202 -0
- package/payload/skills/zz-meridian/references/customize.md +134 -0
- package/payload/skills/zz-meridian/references/existing-project.md +85 -0
- package/payload/skills/zz-meridian/references/validation.md +63 -0
- package/payload/src/app.config.ts +60 -0
- package/payload/src/components/base/app-mark/README.md +50 -0
- package/payload/src/components/base/app-mark/index.tsx +23 -0
- package/payload/src/components/base/app-mark/preview.tsx +37 -0
- package/payload/src/components/base/icons/README.md +47 -0
- package/payload/src/components/base/icons/preview.tsx +35 -0
- package/payload/src/components/base/motion/README.md +59 -0
- package/payload/src/components/base/motion/preview.tsx +76 -0
- package/payload/src/components/base/planes/README.md +58 -0
- package/payload/src/components/base/planes/preview.tsx +60 -0
- package/payload/src/components/base/providers.tsx +60 -0
- package/payload/src/components/base/shell/README.md +77 -0
- package/payload/src/components/base/shell/index.tsx +235 -0
- package/payload/src/components/base/shell/preview.tsx +60 -0
- package/payload/src/components/base/surface/README.md +77 -0
- package/payload/src/components/base/surface/index.tsx +125 -0
- package/payload/src/components/base/surface/preview.tsx +75 -0
- package/payload/src/components/base/text-roles/README.md +64 -0
- package/payload/src/components/base/text-roles/preview.tsx +38 -0
- package/payload/src/components/base/use-share-view.ts +19 -0
- package/payload/src/components/charts/bar-list/README.md +69 -0
- package/payload/src/components/charts/bar-list/index.tsx +58 -0
- package/payload/src/components/charts/bar-list/preview.tsx +45 -0
- package/payload/src/components/charts/column-chart/README.md +67 -0
- package/payload/src/components/charts/column-chart/index.tsx +99 -0
- package/payload/src/components/charts/column-chart/preview.tsx +54 -0
- package/payload/src/components/charts/composition-bar/README.md +64 -0
- package/payload/src/components/charts/composition-bar/index.tsx +48 -0
- package/payload/src/components/charts/composition-bar/preview.tsx +37 -0
- package/payload/src/components/charts/heatmap/README.md +67 -0
- package/payload/src/components/charts/heatmap/index.tsx +125 -0
- package/payload/src/components/charts/heatmap/preview.tsx +25 -0
- package/payload/src/components/charts/meridian/README.md +79 -0
- package/payload/src/components/charts/meridian/index.tsx +30 -0
- package/payload/src/components/charts/meridian/preview.tsx +51 -0
- package/payload/src/components/charts/scale.ts +30 -0
- package/payload/src/components/charts/sparkline/README.md +64 -0
- package/payload/src/components/charts/sparkline/index.tsx +48 -0
- package/payload/src/components/charts/sparkline/preview.tsx +42 -0
- package/payload/src/components/charts/timeline/README.md +72 -0
- package/payload/src/components/charts/timeline/index.tsx +140 -0
- package/payload/src/components/charts/timeline/preview.tsx +27 -0
- package/payload/src/components/charts/trend-chart/README.md +97 -0
- package/payload/src/components/charts/trend-chart/index.tsx +288 -0
- package/payload/src/components/charts/trend-chart/preview.tsx +68 -0
- package/payload/src/components/charts/uptime-bars/README.md +67 -0
- package/payload/src/components/charts/uptime-bars/index.tsx +101 -0
- package/payload/src/components/charts/uptime-bars/preview.tsx +25 -0
- package/payload/src/components/charts/use-size.ts +17 -0
- package/payload/src/components/patterns/activity-feed/README.md +59 -0
- package/payload/src/components/patterns/activity-feed/index.tsx +35 -0
- package/payload/src/components/patterns/activity-feed/preview.tsx +31 -0
- package/payload/src/components/patterns/appearance-menu/README.md +63 -0
- package/payload/src/components/patterns/appearance-menu/index.tsx +53 -0
- package/payload/src/components/patterns/appearance-menu/preview.tsx +40 -0
- package/payload/src/components/patterns/ask-about/README.md +64 -0
- package/payload/src/components/patterns/ask-about/index.tsx +28 -0
- package/payload/src/components/patterns/ask-about/preview.tsx +31 -0
- package/payload/src/components/patterns/assistant/README.md +95 -0
- package/payload/src/components/patterns/assistant/index.tsx +271 -0
- package/payload/src/components/patterns/assistant/preview.tsx +49 -0
- package/payload/src/components/patterns/assistant/thread.ts +79 -0
- package/payload/src/components/patterns/command-palette/README.md +73 -0
- package/payload/src/components/patterns/command-palette/index.tsx +190 -0
- package/payload/src/components/patterns/command-palette/preview.tsx +51 -0
- package/payload/src/components/patterns/data-table/README.md +105 -0
- package/payload/src/components/patterns/data-table/index.tsx +310 -0
- package/payload/src/components/patterns/data-table/preview.tsx +55 -0
- package/payload/src/components/patterns/data-table/use-query-state.ts +39 -0
- package/payload/src/components/patterns/detail-head/README.md +85 -0
- package/payload/src/components/patterns/detail-head/index.tsx +109 -0
- package/payload/src/components/patterns/detail-head/preview.tsx +45 -0
- package/payload/src/components/patterns/embed-frame/README.md +75 -0
- package/payload/src/components/patterns/embed-frame/index.tsx +83 -0
- package/payload/src/components/patterns/embed-frame/preview.tsx +48 -0
- package/payload/src/components/patterns/export-button/README.md +71 -0
- package/payload/src/components/patterns/export-button/index.tsx +38 -0
- package/payload/src/components/patterns/export-button/preview.tsx +18 -0
- package/payload/src/components/patterns/featured-metric/README.md +65 -0
- package/payload/src/components/patterns/featured-metric/index.tsx +87 -0
- package/payload/src/components/patterns/featured-metric/preview.tsx +58 -0
- package/payload/src/components/patterns/filter-bar/README.md +81 -0
- package/payload/src/components/patterns/filter-bar/index.tsx +128 -0
- package/payload/src/components/patterns/filter-bar/preview.tsx +47 -0
- package/payload/src/components/patterns/form-section/README.md +96 -0
- package/payload/src/components/patterns/form-section/index.tsx +107 -0
- package/payload/src/components/patterns/form-section/preview.tsx +46 -0
- package/payload/src/components/patterns/freshness/README.md +62 -0
- package/payload/src/components/patterns/freshness/index.tsx +41 -0
- package/payload/src/components/patterns/freshness/preview.tsx +24 -0
- package/payload/src/components/patterns/incident-card/README.md +92 -0
- package/payload/src/components/patterns/incident-card/index.tsx +81 -0
- package/payload/src/components/patterns/incident-card/preview.tsx +28 -0
- package/payload/src/components/patterns/metric-tile/README.md +72 -0
- package/payload/src/components/patterns/metric-tile/index.tsx +119 -0
- package/payload/src/components/patterns/metric-tile/preview.tsx +53 -0
- package/payload/src/components/patterns/period-select/README.md +55 -0
- package/payload/src/components/patterns/period-select/index.tsx +53 -0
- package/payload/src/components/patterns/period-select/preview.tsx +28 -0
- package/payload/src/components/patterns/proposal/README.md +80 -0
- package/payload/src/components/patterns/proposal/index.tsx +111 -0
- package/payload/src/components/patterns/proposal/preview.tsx +42 -0
- package/payload/src/components/patterns/prose/README.md +71 -0
- package/payload/src/components/patterns/prose/index.tsx +62 -0
- package/payload/src/components/patterns/prose/preview.tsx +63 -0
- package/payload/src/components/patterns/rail/README.md +88 -0
- package/payload/src/components/patterns/rail/index.tsx +150 -0
- package/payload/src/components/patterns/rail/preview.tsx +41 -0
- package/payload/src/components/patterns/shell-tools/README.md +68 -0
- package/payload/src/components/patterns/shell-tools/index.tsx +89 -0
- package/payload/src/components/patterns/shell-tools/preview.tsx +33 -0
- package/payload/src/components/patterns/status-list/README.md +104 -0
- package/payload/src/components/patterns/status-list/index.tsx +91 -0
- package/payload/src/components/patterns/status-list/preview.tsx +29 -0
- package/payload/src/components/patterns/status-list/summarise.ts +13 -0
- package/payload/src/components/ui/agent-mark/README.md +63 -0
- package/payload/src/components/ui/agent-mark/index.tsx +16 -0
- package/payload/src/components/ui/agent-mark/preview.tsx +30 -0
- package/payload/src/components/ui/avatar/README.md +71 -0
- package/payload/src/components/ui/avatar/index.tsx +47 -0
- package/payload/src/components/ui/avatar/preview.tsx +33 -0
- package/payload/src/components/ui/badge/README.md +67 -0
- package/payload/src/components/ui/badge/index.tsx +26 -0
- package/payload/src/components/ui/badge/preview.tsx +30 -0
- package/payload/src/components/ui/banner/README.md +75 -0
- package/payload/src/components/ui/banner/index.tsx +61 -0
- package/payload/src/components/ui/banner/preview.tsx +22 -0
- package/payload/src/components/ui/breadcrumb/README.md +68 -0
- package/payload/src/components/ui/breadcrumb/index.tsx +60 -0
- package/payload/src/components/ui/breadcrumb/preview.tsx +25 -0
- package/payload/src/components/ui/button/README.md +89 -0
- package/payload/src/components/ui/button/index.tsx +69 -0
- package/payload/src/components/ui/button/preview.tsx +38 -0
- package/payload/src/components/ui/card/README.md +89 -0
- package/payload/src/components/ui/card/index.tsx +55 -0
- package/payload/src/components/ui/card/preview.tsx +75 -0
- package/payload/src/components/ui/checkbox/README.md +69 -0
- package/payload/src/components/ui/checkbox/index.tsx +54 -0
- package/payload/src/components/ui/checkbox/preview.tsx +26 -0
- package/payload/src/components/ui/copy-field/README.md +71 -0
- package/payload/src/components/ui/copy-field/index.tsx +61 -0
- package/payload/src/components/ui/copy-field/preview.tsx +18 -0
- package/payload/src/components/ui/delta/README.md +67 -0
- package/payload/src/components/ui/delta/index.tsx +39 -0
- package/payload/src/components/ui/delta/preview.tsx +27 -0
- package/payload/src/components/ui/dialog/README.md +88 -0
- package/payload/src/components/ui/dialog/index.tsx +59 -0
- package/payload/src/components/ui/dialog/preview.tsx +62 -0
- package/payload/src/components/ui/empty-state/README.md +73 -0
- package/payload/src/components/ui/empty-state/index.tsx +79 -0
- package/payload/src/components/ui/empty-state/preview.tsx +52 -0
- package/payload/src/components/ui/field/README.md +71 -0
- package/payload/src/components/ui/field/index.tsx +68 -0
- package/payload/src/components/ui/field/preview.tsx +59 -0
- package/payload/src/components/ui/icon-button/README.md +80 -0
- package/payload/src/components/ui/icon-button/index.tsx +54 -0
- package/payload/src/components/ui/icon-button/preview.tsx +29 -0
- package/payload/src/components/ui/input/README.md +80 -0
- package/payload/src/components/ui/input/index.tsx +68 -0
- package/payload/src/components/ui/input/preview.tsx +40 -0
- package/payload/src/components/ui/kbd/README.md +50 -0
- package/payload/src/components/ui/kbd/index.tsx +15 -0
- package/payload/src/components/ui/kbd/preview.tsx +27 -0
- package/payload/src/components/ui/key-value/README.md +65 -0
- package/payload/src/components/ui/key-value/index.tsx +29 -0
- package/payload/src/components/ui/key-value/preview.tsx +36 -0
- package/payload/src/components/ui/menu/README.md +85 -0
- package/payload/src/components/ui/menu/index.tsx +68 -0
- package/payload/src/components/ui/menu/preview.tsx +68 -0
- package/payload/src/components/ui/pagination/README.md +80 -0
- package/payload/src/components/ui/pagination/index.tsx +131 -0
- package/payload/src/components/ui/pagination/preview.tsx +45 -0
- package/payload/src/components/ui/popover/README.md +71 -0
- package/payload/src/components/ui/popover/index.tsx +25 -0
- package/payload/src/components/ui/popover/preview.tsx +65 -0
- package/payload/src/components/ui/progress/README.md +80 -0
- package/payload/src/components/ui/progress/index.tsx +83 -0
- package/payload/src/components/ui/progress/preview.tsx +25 -0
- package/payload/src/components/ui/radio-group/README.md +69 -0
- package/payload/src/components/ui/radio-group/index.tsx +69 -0
- package/payload/src/components/ui/radio-group/preview.tsx +44 -0
- package/payload/src/components/ui/search-input/README.md +67 -0
- package/payload/src/components/ui/search-input/index.tsx +69 -0
- package/payload/src/components/ui/search-input/preview.tsx +26 -0
- package/payload/src/components/ui/segmented/README.md +70 -0
- package/payload/src/components/ui/segmented/index.tsx +74 -0
- package/payload/src/components/ui/segmented/preview.tsx +34 -0
- package/payload/src/components/ui/select/README.md +73 -0
- package/payload/src/components/ui/select/index.tsx +97 -0
- package/payload/src/components/ui/select/preview.tsx +57 -0
- package/payload/src/components/ui/sheet/README.md +67 -0
- package/payload/src/components/ui/sheet/index.tsx +66 -0
- package/payload/src/components/ui/sheet/preview.tsx +59 -0
- package/payload/src/components/ui/skeleton/README.md +70 -0
- package/payload/src/components/ui/skeleton/index.tsx +6 -0
- package/payload/src/components/ui/skeleton/preview.tsx +37 -0
- package/payload/src/components/ui/spinner/README.md +66 -0
- package/payload/src/components/ui/spinner/index.tsx +15 -0
- package/payload/src/components/ui/spinner/preview.tsx +20 -0
- package/payload/src/components/ui/status-dot/README.md +68 -0
- package/payload/src/components/ui/status-dot/index.tsx +9 -0
- package/payload/src/components/ui/status-dot/preview.tsx +22 -0
- package/payload/src/components/ui/switch/README.md +70 -0
- package/payload/src/components/ui/switch/index.tsx +53 -0
- package/payload/src/components/ui/switch/preview.tsx +29 -0
- package/payload/src/components/ui/table/README.md +85 -0
- package/payload/src/components/ui/table/index.tsx +148 -0
- package/payload/src/components/ui/table/preview.tsx +75 -0
- package/payload/src/components/ui/tabs/README.md +82 -0
- package/payload/src/components/ui/tabs/index.tsx +116 -0
- package/payload/src/components/ui/tabs/preview.tsx +60 -0
- package/payload/src/components/ui/textarea/README.md +58 -0
- package/payload/src/components/ui/textarea/index.tsx +31 -0
- package/payload/src/components/ui/textarea/preview.tsx +27 -0
- package/payload/src/components/ui/toast/README.md +76 -0
- package/payload/src/components/ui/toast/index.tsx +92 -0
- package/payload/src/components/ui/toast/preview.tsx +29 -0
- package/payload/src/components/ui/tooltip/README.md +71 -0
- package/payload/src/components/ui/tooltip/index.tsx +48 -0
- package/payload/src/components/ui/tooltip/preview.tsx +32 -0
- package/payload/src/data/README.md +11 -0
- package/payload/src/data/collections.ts +77 -0
- package/payload/src/lib/assistant/config.ts +32 -0
- package/payload/src/lib/assistant/prompt.ts +32 -0
- package/payload/src/lib/assistant/respond.ts +43 -0
- package/payload/src/lib/assistant/tools.ts +108 -0
- package/payload/src/lib/cn.ts +17 -0
- package/payload/src/lib/collection.ts +172 -0
- package/payload/src/lib/color.ts +71 -0
- package/payload/src/lib/csv.ts +23 -0
- package/payload/src/lib/format-date.ts +66 -0
- package/payload/src/lib/format.ts +130 -0
- package/payload/src/lib/host.ts +102 -0
- package/payload/src/lib/period.ts +32 -0
- package/payload/src/lib/preferences.ts +18 -0
- package/payload/src/lib/safe-markdown.ts +26 -0
- package/payload/src/styles/base.css +174 -0
- package/payload/src/styles/motion.css +75 -0
- package/payload/src/styles/theme.css +115 -0
- package/payload/src/styles/tokens.css +552 -0
- package/payload/src/system/atlas-shell.tsx +143 -0
- package/payload/src/system/card-stage.tsx +81 -0
- package/payload/src/system/content.ts +157 -0
- package/payload/src/system/fixtures/sample-members.ts +52 -0
- package/payload/src/system/fixtures/sample-ops.ts +57 -0
- package/payload/src/system/fixtures/sample-records.ts +116 -0
- package/payload/src/system/fixtures/sample.ts +225 -0
- package/payload/src/system/hero.tsx +155 -0
- package/payload/src/system/markdown.tsx +55 -0
- package/payload/src/system/page-stage.tsx +193 -0
- package/payload/src/system/registry.ts +80 -0
- package/payload/src/system/sample-cells.tsx +70 -0
- package/payload/src/system/specimen.tsx +34 -0
- package/payload/src/system/strata.tsx +87 -0
- package/payload/src/system/token-view.tsx +322 -0
- package/payload/src/system/tokens-data.ts +41 -0
- package/payload/src/views/analytics.tsx +139 -0
- package/payload/src/views/console-chrome.tsx +18 -0
- package/payload/src/views/customers.tsx +141 -0
- package/payload/src/views/health.tsx +94 -0
- package/payload/src/views/keys.tsx +174 -0
- package/payload/src/views/members.tsx +171 -0
- package/payload/src/views/missing-page.tsx +23 -0
- package/payload/src/views/not-found-address.tsx +64 -0
- package/payload/src/views/not-found.tsx +34 -0
- package/payload/src/views/overview.tsx +157 -0
- package/payload/src/views/request.tsx +162 -0
- package/payload/src/views/requests.tsx +113 -0
- package/payload/src/views/sample-footer.tsx +23 -0
- package/payload/src/views/settings.tsx +238 -0
- package/payload/src/views/standalone.tsx +51 -0
- package/payload/tests/agents.test.tsx +51 -0
- package/payload/tests/assistant-config.test.ts +53 -0
- package/payload/tests/assistant-errors.test.ts +37 -0
- package/payload/tests/assistant-panel.test.tsx +41 -0
- package/payload/tests/assistant-proposal.test.tsx +68 -0
- package/payload/tests/assistant-respond.test.ts +55 -0
- package/payload/tests/assistant-route.test.ts +28 -0
- package/payload/tests/assistant-safety.test.ts +136 -0
- package/payload/tests/assistant-settings.test.tsx +61 -0
- package/payload/tests/assistant-thread.test.tsx +122 -0
- package/payload/tests/assistant-tools.test.ts +158 -0
- package/payload/tests/collection.test.ts +100 -0
- package/payload/tests/color.test.ts +20 -0
- package/payload/tests/fake-llm.test.ts +19 -0
- package/payload/tests/format.test.ts +47 -0
- package/payload/tests/forms.test.tsx +19 -0
- package/payload/tests/layers.test.tsx +30 -0
- package/payload/tests/meridian.test.tsx +28 -0
- package/payload/tests/not-found.test.ts +33 -0
- package/payload/tests/paged.test.tsx +40 -0
- package/payload/tests/prose.test.tsx +42 -0
- package/payload/tests/sample.test.ts +43 -0
- package/payload/tests/setup.ts +40 -0
- package/payload/tests/trend-chart.test.tsx +26 -0
- package/payload/tokens/accent.cobalt.tokens.json +17 -0
- package/payload/tokens/accent.graphite.tokens.json +38 -0
- package/payload/tokens/accent.indigo.tokens.json +17 -0
- package/payload/tokens/accent.jade.tokens.json +27 -0
- package/payload/tokens/core.tokens.json +688 -0
- package/payload/tokens/density.compact.tokens.json +55 -0
- package/payload/tokens/palette.tokens.json +601 -0
- package/payload/tokens/theme.dark.tokens.json +916 -0
- package/payload/tokens/theme.light.tokens.json +992 -0
- package/payload/tokens/zz-meridian.resolver.json +95 -0
- package/payload/tsconfig.json +43 -0
- package/payload/vitest.config.ts +9 -0
package/README.md
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
|
-
#
|
|
1
|
+
# zz-meridian
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Bring [ZZ Meridian](https://github.com/zhixuan312/zz-meridian), a dashboard design system for Next.js, into your
|
|
4
|
+
project, or start a new dashboard on it. The package copies the files in and installs the agent skill; nothing in your
|
|
5
|
+
dashboard depends on it afterwards.
|
|
6
|
+
|
|
7
|
+
## The one sentence
|
|
8
|
+
|
|
9
|
+
Give your coding agent (Codex, Claude Code, or any agent that can run a shell) this, from your frontend's folder:
|
|
10
|
+
|
|
11
|
+
> Run `npx zz-meridian@latest adopt` here, then follow the zz-meridian skill it installs: keep our data layer and
|
|
12
|
+
> routes, restyle every page with Meridian's components and tokens, and run pnpm verify until it passes.
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx zz-meridian@latest adopt [brand flags] # bring Meridian into this Next.js App Router project
|
|
18
|
+
npx zz-meridian@latest create <dir> [brand flags] # start a new dashboard
|
|
19
|
+
npx zz-meridian@latest skill [--global] # install only the agent skill
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Brand flags: `--name "Acme Ops"`, `--hex '#2E6BE4'` (or `--accent indigo|cobalt|jade|graphite`), `--workspace`,
|
|
23
|
+
`--timezone`, `--currency`, `--user`, `--role`.
|
|
24
|
+
|
|
25
|
+
**adopt** copies Meridian's tokens, styles, components, gates and scripts into `src/`, `tokens/` and `scripts/`;
|
|
26
|
+
merges the dependencies and scripts it needs into your `package.json`; replaces your global stylesheet (yours is kept
|
|
27
|
+
beside it as `*.before.css`); brands it; appends Meridian's rules to your `AGENTS.md`; installs the skill into
|
|
28
|
+
`.agents/skills/` (Codex) and `.claude/skills/` (Claude Code); records every copied file in `.meridian/manifest.json`;
|
|
29
|
+
installs and type checks. Your routes, your data layer and your own components are not touched. Meridian's files
|
|
30
|
+
import each other by relative path, so a `components/ui/button` of your own is never confused with Meridian's; your
|
|
31
|
+
code imports Meridian as `@meridian/…`.
|
|
32
|
+
|
|
33
|
+
It refuses, and writes nothing, when the git tree has uncommitted changes (so its change is one reviewable diff), when
|
|
34
|
+
the project is not Next.js with the App Router, or when a file it would copy already exists with other content.
|
|
35
|
+
|
|
36
|
+
**create** copies the template into a new folder, branded as your product, with the Design Atlas and the design
|
|
37
|
+
system's own documents left out.
|
|
38
|
+
|
|
39
|
+
Requires Node 22.18 or newer. The checks (`pnpm verify`) also need Google Chrome.
|
|
40
|
+
|
|
41
|
+
## What this package does not do
|
|
42
|
+
|
|
43
|
+
It has no dependencies and no install scripts: running it executes only its own code, and only when you call it. It
|
|
44
|
+
sends nothing anywhere; the only network access is your package manager's install, which you can skip with
|
|
45
|
+
`--no-install`. Every release is built and published by GitHub Actions with npm provenance, so the registry shows the
|
|
46
|
+
commit and workflow each version came from.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/dist/adopt.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zz-meridian adopt`: bring Meridian into an existing Next.js App Router project, in place, keeping its routes and
|
|
3
|
+
* its data layer. Everything here is the settled part of the skill's Route A; rebuilding each page on Meridian's
|
|
4
|
+
* components is the agent's work, and the printed next step hands it over.
|
|
5
|
+
*
|
|
6
|
+
* Nothing is written until every check has passed: a dirty tree, a project that is not Next.js with the App Router,
|
|
7
|
+
* or a file of the same name with other content stops it before the first write.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { PAYLOAD, VERSION, brandArgs, inside, installSkill, packageManager, payloadFiles, readJsonc, readPayload, relativeImports, run, runTool, sha256, writeIn, writeManifest, } from './files.js';
|
|
12
|
+
/** The library modules Meridian's components and gates import; the rest of src/lib belongs to the template's pages. */
|
|
13
|
+
const LIB = ['cn', 'format', 'format-date', 'period', 'color', 'host', 'preferences', 'csv', 'safe-markdown'].map((n) => `src/lib/${n}.ts`);
|
|
14
|
+
/** What adopt copies from the template, as payload paths. The fixture build in CI is what keeps this list complete. */
|
|
15
|
+
export function adoptSet() {
|
|
16
|
+
const own = (f) => !/(^|\/)(README\.md|preview\.tsx)$/.test(f);
|
|
17
|
+
return [
|
|
18
|
+
...payloadFiles('tokens'),
|
|
19
|
+
...payloadFiles('src/styles'),
|
|
20
|
+
...payloadFiles('src/components').filter(own),
|
|
21
|
+
...payloadFiles('scripts').filter((f) => f !== 'scripts/verify.config.ts'),
|
|
22
|
+
...LIB,
|
|
23
|
+
'src/lib/assistant/prompt.ts',
|
|
24
|
+
'src/views/console-chrome.tsx',
|
|
25
|
+
'tests/setup.ts',
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/** The template's dependencies a dashboard built from these files needs. */
|
|
29
|
+
const DEPS = ['next', 'react', 'react-dom', 'radix-ui', 'lucide-react', 'clsx', 'tailwind-merge', 'react-markdown', 'remark-gfm', 'ai', '@ai-sdk/react'];
|
|
30
|
+
const DEV_DEPS = ['typescript', '@types/node', '@types/react', '@types/react-dom', 'tailwindcss', '@tailwindcss/postcss', 'eslint', 'eslint-config-next', 'vitest', '@vitejs/plugin-react', 'jsdom', '@testing-library/react', '@testing-library/jest-dom'];
|
|
31
|
+
const SCRIPTS = ['typecheck', 'test', 'tokens', 'check', 'contrast', 'gate', 'audit', 'verify', 'brand', 'shot', 'interactions', 'keyboard', 'vitals'];
|
|
32
|
+
const major = (spec) => Number(/(\d+)/.exec(spec)?.[1] ?? NaN);
|
|
33
|
+
const TS = /\.(ts|tsx)$/;
|
|
34
|
+
/** A specifier inside a copied file, pinned to the exact module so a team file of a similar name can never win. */
|
|
35
|
+
function exactImports(rel, source, known) {
|
|
36
|
+
const pin = (spec) => {
|
|
37
|
+
if (!spec.startsWith('.'))
|
|
38
|
+
return spec;
|
|
39
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(rel), spec));
|
|
40
|
+
if (['.ts', '.tsx'].some((x) => known.has(base + x)) || /\.(ts|tsx|css|json)$/.test(spec))
|
|
41
|
+
return spec;
|
|
42
|
+
if (['.ts', '.tsx'].some((x) => known.has(`${base}/index${x}`)))
|
|
43
|
+
return spec === '.' ? './index' : `${spec.replace(/\/$/, '')}/index`;
|
|
44
|
+
return spec;
|
|
45
|
+
};
|
|
46
|
+
return relativeImports(rel, source).replace(/(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])([\w@./()[\]-]+)\2/g, (_m, lead, q, spec) => `${lead}${q}${pin(spec)}${q}`);
|
|
47
|
+
}
|
|
48
|
+
function appDir(root) {
|
|
49
|
+
for (const d of ['app', 'src/app'])
|
|
50
|
+
if (['tsx', 'jsx', 'ts', 'js'].some((x) => fs.existsSync(path.join(root, d, `layout.${x}`))))
|
|
51
|
+
return d;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/** The static pages under the app folder, as routes: the first navigation, which the agent then edits. */
|
|
55
|
+
function routes(root, dir) {
|
|
56
|
+
const out = [];
|
|
57
|
+
const walk = (abs, route) => {
|
|
58
|
+
for (const e of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
59
|
+
if (e.isDirectory()) {
|
|
60
|
+
if (e.name.startsWith('[') || e.name.startsWith('_') || e.name.startsWith('@') || e.name === 'api' || e.name === 'node_modules')
|
|
61
|
+
continue;
|
|
62
|
+
walk(path.join(abs, e.name), /^\(.*\)$/.test(e.name) ? route : [...route, e.name]);
|
|
63
|
+
}
|
|
64
|
+
else if (/^page\.(tsx|jsx|ts|js)$/.test(e.name))
|
|
65
|
+
out.push('/' + route.join('/'));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
walk(path.join(root, dir), []);
|
|
69
|
+
// Only a plain path goes into generated code; anything else is left for the agent to add by hand.
|
|
70
|
+
return [...new Set(out)].filter((r) => /^\/[\w/.-]*$/.test(r)).sort((a, b) => (a === '/' ? -1 : b === '/' ? 1 : a.localeCompare(b)));
|
|
71
|
+
}
|
|
72
|
+
const label = (route) => (route === '/' ? 'Home' : route.split('/').pop().replace(/[-_]+/g, ' ').replace(/^./, (c) => c.toUpperCase()));
|
|
73
|
+
function appConfig(pages) {
|
|
74
|
+
const template = readPayload('src/app.config.ts');
|
|
75
|
+
const items = pages.map((r) => ` { href: ${JSON.stringify(r)}, label: ${JSON.stringify(label(r))}, icon: LayoutGrid },`).join('\n');
|
|
76
|
+
return template
|
|
77
|
+
.replace(/^import \{[\s\S]*?\} from 'lucide-react';/m, "import { LayoutGrid, type LucideIcon } from 'lucide-react';")
|
|
78
|
+
.replace(/export const nav: NavGroup\[\] = \[[\s\S]*?\n\];/, `/** Written by zz-meridian adopt from the project's pages: give each its own icon and group them. */\nexport const nav: NavGroup[] = [\n {\n items: [\n${items}\n ],\n },\n];`);
|
|
79
|
+
}
|
|
80
|
+
function verifyConfig() {
|
|
81
|
+
return readPayload('scripts/verify.config.ts').replace(/const config: VerifyConfig = \{[\s\S]*?\n\};/, `const config: VerifyConfig = {
|
|
82
|
+
// One detail page per state worth seeing (a normal record, a failed one, a missing one), with ids from your data.
|
|
83
|
+
detailRoutes: [],
|
|
84
|
+
};`);
|
|
85
|
+
}
|
|
86
|
+
/** The template's stylesheet, with its paths made relative to where the project keeps its own. */
|
|
87
|
+
function globals(cssRel, dir) {
|
|
88
|
+
const from = path.posix.dirname(cssRel);
|
|
89
|
+
const rel = (to) => { const r = path.posix.relative(from, to) || '.'; return r.startsWith('.') ? r : `./${r}`; };
|
|
90
|
+
return readPayload('app/globals.css')
|
|
91
|
+
.replace(/"\.\.\/src\/styles\/([^"]+)"/g, (_m, f) => `"${rel(`src/styles/${f}`)}"`)
|
|
92
|
+
.replace(/@source "\.\.\/app";/, `@source "${rel(dir)}";`)
|
|
93
|
+
.replace(/@source "\.\.\/src";/, `@source "${rel('src')}";`);
|
|
94
|
+
}
|
|
95
|
+
function vitestConfig(alias) {
|
|
96
|
+
const aliases = [`'@meridian': path.resolve(import.meta.dirname, 'src')`, ...(alias !== null ? [`'@': path.resolve(import.meta.dirname, ${JSON.stringify(alias)})`] : [])];
|
|
97
|
+
return `import { defineConfig } from 'vitest/config';
|
|
98
|
+
import react from '@vitejs/plugin-react';
|
|
99
|
+
import path from 'node:path';
|
|
100
|
+
|
|
101
|
+
// Written by zz-meridian adopt: the gate runs these tests; tests/setup.ts stands in for what jsdom lacks.
|
|
102
|
+
export default defineConfig({
|
|
103
|
+
plugins: [react()],
|
|
104
|
+
resolve: { alias: { ${aliases.join(', ')} } },
|
|
105
|
+
test: { environment: 'jsdom', setupFiles: ['./tests/setup.ts'], include: ['tests/**/*.test.{ts,tsx}'], passWithNoTests: true },
|
|
106
|
+
});
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
export function adopt(o) {
|
|
110
|
+
const { root } = o;
|
|
111
|
+
const say = (s) => console.log(s);
|
|
112
|
+
const pkgPath = path.join(root, 'package.json');
|
|
113
|
+
// ── Checks: nothing is written until all of them pass ─────────────────────────────────────────────────
|
|
114
|
+
if (!fs.existsSync(pkgPath))
|
|
115
|
+
return fail(`no package.json in ${root}: run adopt in the project's folder`);
|
|
116
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
117
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
118
|
+
const dir = appDir(root);
|
|
119
|
+
if (!deps.next || !dir) {
|
|
120
|
+
return fail(`this is not a Next.js App Router project (no "next" dependency, or no app/layout or src/app/layout).
|
|
121
|
+
Meridian's components are React; for another stack, read Route B or C in
|
|
122
|
+
https://github.com/zhixuan312/zz-meridian/blob/master/skills/zz-meridian/references/existing-project.md`);
|
|
123
|
+
}
|
|
124
|
+
if (!o.allowDirty) {
|
|
125
|
+
const git = run('git', ['status', '--porcelain'], root, true);
|
|
126
|
+
if (git.status !== 0)
|
|
127
|
+
return fail('this folder is not a git repository, so the change could not be reviewed or undone as one diff. Commit it to git first, or pass --allow-dirty.');
|
|
128
|
+
if (git.stdout.trim())
|
|
129
|
+
return fail('the git tree has uncommitted changes. Commit or stash them first, so adopt\'s change is one reviewable diff (or pass --allow-dirty).');
|
|
130
|
+
}
|
|
131
|
+
const set = adoptSet();
|
|
132
|
+
const known = new Set(set);
|
|
133
|
+
const writes = new Map();
|
|
134
|
+
const owned = [];
|
|
135
|
+
const conflicts = [];
|
|
136
|
+
const notes = [];
|
|
137
|
+
const own = (rel, content) => {
|
|
138
|
+
const abs = path.join(root, rel);
|
|
139
|
+
if (fs.existsSync(abs)) {
|
|
140
|
+
if (fs.readFileSync(abs).equals(Buffer.from(content))) {
|
|
141
|
+
owned.push(rel);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
conflicts.push(rel);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
writes.set(rel, content);
|
|
148
|
+
owned.push(rel);
|
|
149
|
+
};
|
|
150
|
+
for (const rel of set) {
|
|
151
|
+
const raw = fs.readFileSync(path.join(PAYLOAD, rel));
|
|
152
|
+
own(rel, TS.test(rel) ? exactImports(rel, raw.toString('utf8'), known) : raw);
|
|
153
|
+
}
|
|
154
|
+
own('src/app.config.ts', exactImports('src/app.config.ts', appConfig(routes(root, dir)), known));
|
|
155
|
+
own('scripts/verify.config.ts', verifyConfig());
|
|
156
|
+
// Meridian's scripts are ES modules; a project whose package.json is not "type": "module" gets that for scripts/ only.
|
|
157
|
+
if (pkg.type !== 'module')
|
|
158
|
+
own('scripts/package.json', '{\n "type": "module"\n}\n');
|
|
159
|
+
if (conflicts.length) {
|
|
160
|
+
return fail(`these files already exist with other content, so nothing was written:\n${conflicts.map((c) => ` ${c}`).join('\n')}
|
|
161
|
+
Rename or move them, then run adopt again.`);
|
|
162
|
+
}
|
|
163
|
+
// ── Merged files: the project's own, changed only where Meridian needs it ────────────────────────────
|
|
164
|
+
const tpl = JSON.parse(readPayload('package.json'));
|
|
165
|
+
const merge = (field, names) => {
|
|
166
|
+
pkg[field] ??= {};
|
|
167
|
+
for (const n of names) {
|
|
168
|
+
const want = tpl.dependencies?.[n] ?? tpl.devDependencies?.[n];
|
|
169
|
+
if (!want)
|
|
170
|
+
continue;
|
|
171
|
+
const have = pkg.dependencies?.[n] ?? pkg.devDependencies?.[n];
|
|
172
|
+
if (!have) {
|
|
173
|
+
pkg[field][n] = want;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (major(have) < major(want)) {
|
|
177
|
+
const at = pkg.dependencies?.[n] ? 'dependencies' : 'devDependencies';
|
|
178
|
+
pkg[at][n] = want;
|
|
179
|
+
notes.push(`raised ${n} from ${have} to ${want} (Meridian needs the newer major; read its upgrade guide)`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
merge('dependencies', DEPS);
|
|
184
|
+
merge('devDependencies', DEV_DEPS);
|
|
185
|
+
pkg.scripts ??= {};
|
|
186
|
+
for (const s of SCRIPTS) {
|
|
187
|
+
const want = tpl.scripts[s];
|
|
188
|
+
if (!pkg.scripts[s])
|
|
189
|
+
pkg.scripts[s] = want;
|
|
190
|
+
else if (pkg.scripts[s] !== want)
|
|
191
|
+
notes.push(`kept your "${s}" script; Meridian's is "${want}"`);
|
|
192
|
+
}
|
|
193
|
+
writes.set('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
194
|
+
// tsconfig: Meridian's scripts import with .ts extensions, and its code needs ES2022; @meridian/* reaches its files.
|
|
195
|
+
const tsPath = path.join(root, 'tsconfig.json');
|
|
196
|
+
let atAlias = null;
|
|
197
|
+
if (fs.existsSync(tsPath)) {
|
|
198
|
+
const ts = readJsonc(tsPath);
|
|
199
|
+
ts.compilerOptions ??= {};
|
|
200
|
+
ts.compilerOptions.allowImportingTsExtensions = true;
|
|
201
|
+
ts.compilerOptions.noEmit = true;
|
|
202
|
+
if (!/^es20(2[2-9]|[3-9]\d)|^esnext$/i.test(ts.compilerOptions.target ?? '')) {
|
|
203
|
+
notes.push(`raised tsconfig target from ${ts.compilerOptions.target ?? 'its default'} to ES2022`);
|
|
204
|
+
ts.compilerOptions.target = 'ES2022';
|
|
205
|
+
}
|
|
206
|
+
ts.compilerOptions.paths ??= {};
|
|
207
|
+
ts.compilerOptions.paths['@meridian/*'] = ['./src/*'];
|
|
208
|
+
const at = ts.compilerOptions.paths['@/*']?.[0];
|
|
209
|
+
if (typeof at === 'string')
|
|
210
|
+
atAlias = at.replace(/\/\*$/, '').replace(/^\.\//, '') || '.';
|
|
211
|
+
writes.set('tsconfig.json', JSON.stringify(ts, null, 2) + '\n');
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
return fail('no tsconfig.json: Meridian is TypeScript. Add one (next dev writes it), then run adopt again.');
|
|
215
|
+
}
|
|
216
|
+
// The global stylesheet: Meridian's, at the place the root layout already imports; theirs kept beside it to port from.
|
|
217
|
+
const layout = ['tsx', 'jsx', 'ts', 'js'].map((x) => `${dir}/layout.${x}`).find((f) => fs.existsSync(path.join(root, f)));
|
|
218
|
+
const layoutSrc = fs.readFileSync(path.join(root, layout), 'utf8');
|
|
219
|
+
const cssImport = /import\s+['"](\.{1,2}\/[^'"]+\.css)['"]/.exec(layoutSrc)?.[1];
|
|
220
|
+
const cssRel = cssImport ? path.posix.normalize(path.posix.join(path.posix.dirname(layout), cssImport)) : `${dir}/globals.css`;
|
|
221
|
+
const cssAbs = path.join(root, cssRel);
|
|
222
|
+
if (fs.existsSync(cssAbs)) {
|
|
223
|
+
const before = cssRel.replace(/\.css$/, '.before.css');
|
|
224
|
+
writes.set(before, fs.readFileSync(cssAbs));
|
|
225
|
+
notes.push(`your stylesheet is now Meridian's; the old one is ${before}, to port anything still needed from it as tokens`);
|
|
226
|
+
}
|
|
227
|
+
writes.set(cssRel, globals(cssRel, dir));
|
|
228
|
+
if (!cssImport) {
|
|
229
|
+
writes.set(layout, `import './globals.css';\n${layoutSrc}`);
|
|
230
|
+
notes.push(`${layout} now imports ./globals.css`);
|
|
231
|
+
}
|
|
232
|
+
const postcss = ['postcss.config.mjs', 'postcss.config.js', 'postcss.config.cjs'].find((f) => fs.existsSync(path.join(root, f)));
|
|
233
|
+
if (!postcss)
|
|
234
|
+
writes.set('postcss.config.mjs', readPayload('postcss.config.mjs'));
|
|
235
|
+
else if (!fs.readFileSync(path.join(root, postcss), 'utf8').includes('@tailwindcss/postcss')) {
|
|
236
|
+
writes.set(postcss.replace(/(\.[cm]?js)$/, '.before$1'), fs.readFileSync(path.join(root, postcss)));
|
|
237
|
+
writes.set(postcss, readPayload('postcss.config.mjs'));
|
|
238
|
+
notes.push(`${postcss} now runs Tailwind v4; the old one is kept beside it`);
|
|
239
|
+
}
|
|
240
|
+
if (!['ts', 'mts', 'js', 'mjs'].some((x) => fs.existsSync(path.join(root, `vitest.config.${x}`))))
|
|
241
|
+
writes.set('vitest.config.ts', vitestConfig(atAlias));
|
|
242
|
+
else
|
|
243
|
+
notes.push('kept your vitest config: add tests/setup.ts to its setupFiles and passWithNoTests for the gate');
|
|
244
|
+
if (!fs.readdirSync(path.join(root, dir)).some((f) => /^(icon|favicon)\./.test(f)))
|
|
245
|
+
writes.set(`${dir}/icon.ts`, exactImports(`${dir}/icon.ts`, readPayload('app/icon.ts'), known));
|
|
246
|
+
const ignore = path.join(root, '.gitignore');
|
|
247
|
+
const ignored = fs.existsSync(ignore) ? fs.readFileSync(ignore, 'utf8') : '';
|
|
248
|
+
if (!/^\/?out\/?$/m.test(ignored))
|
|
249
|
+
writes.set('.gitignore', `${ignored.trimEnd()}\n\n# zz-meridian: verify's report and the screenshots\n/out/\n`);
|
|
250
|
+
// ── Write: every target checked first, so a bad one stops it before anything changes ─────────────────
|
|
251
|
+
try {
|
|
252
|
+
for (const rel of writes.keys())
|
|
253
|
+
inside(root, rel);
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
return fail(e.message);
|
|
257
|
+
}
|
|
258
|
+
for (const [rel, content] of writes)
|
|
259
|
+
writeIn(root, rel, content);
|
|
260
|
+
const name = o.brand.name ?? String(pkg.name ?? path.basename(root)).replace(/^@[^/]+\//, '').replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
261
|
+
const brand = { ...o.brand, name };
|
|
262
|
+
const b = run(process.execPath, ['scripts/brand.ts', ...brandArgs(brand), '--existing'], root);
|
|
263
|
+
if (b.status !== 0)
|
|
264
|
+
return fail('scripts/brand.ts failed (above); the files are copied, so fix the cause and run it again with the same flags');
|
|
265
|
+
const files = {};
|
|
266
|
+
for (const rel of owned)
|
|
267
|
+
files[rel] = sha256(fs.readFileSync(inside(root, rel)));
|
|
268
|
+
installSkill(root, files);
|
|
269
|
+
writeManifest(root, { version: VERSION, route: 'adopt', brand, files });
|
|
270
|
+
// ── Install and prove it compiles ─────────────────────────────────────────────────────────────────────
|
|
271
|
+
const pm = packageManager(root);
|
|
272
|
+
let typed = 'not run (--no-install)';
|
|
273
|
+
if (o.install) {
|
|
274
|
+
say(`\nInstalling with ${pm}…`);
|
|
275
|
+
if (run(pm, ['install'], root).status !== 0)
|
|
276
|
+
return fail(`${pm} install failed (above). Fix it, then run ${pm} install and ${pm} run typecheck.`);
|
|
277
|
+
// Route types first (LayoutProps, PageProps), as the gate does.
|
|
278
|
+
runTool(root, 'next/dist/bin/next', ['typegen']);
|
|
279
|
+
const tsc = runTool(root, 'typescript/bin/tsc', ['--noEmit']);
|
|
280
|
+
typed = tsc.status === 0 ? 'passes' : `fails:\n${(tsc.stdout + tsc.stderr).trim().split('\n').slice(0, 30).join('\n')}`;
|
|
281
|
+
}
|
|
282
|
+
say(`
|
|
283
|
+
Meridian ${VERSION} is in ${root}.
|
|
284
|
+
copied: ${owned.length} files (recorded in .meridian/manifest.json)
|
|
285
|
+
skill: .agents/skills/zz-meridian (Codex) and .claude/skills/zz-meridian (Claude Code)
|
|
286
|
+
types: ${typed}${notes.length ? `\n notes:\n${notes.map((n) => ` - ${n}`).join('\n')}` : ''}
|
|
287
|
+
|
|
288
|
+
Next: follow .agents/skills/zz-meridian/references/existing-project.md, Route A, from step 2 (step 1 was this). The
|
|
289
|
+
template to read from is https://github.com/zhixuan312/zz-meridian/tree/v${VERSION}. Import Meridian's components as
|
|
290
|
+
'@meridian/components/…'. Before the first ${pm} run verify, read step 5: verify presses every control, Delete included.`);
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
function fail(msg) {
|
|
294
|
+
console.error(`zz-meridian adopt: ${msg}`);
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* zz-meridian: bring ZZ Meridian into a Next.js project, or start a new dashboard on it. The package copies files in
|
|
4
|
+
* and installs the agent skill; nothing in the dashboard depends on it afterwards.
|
|
5
|
+
*/
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { adopt } from './adopt.js';
|
|
10
|
+
import { create } from './create.js';
|
|
11
|
+
import { BRAND_FLAGS, VERSION, installSkill } from './files.js';
|
|
12
|
+
const HELP = `zz-meridian ${VERSION}
|
|
13
|
+
|
|
14
|
+
npx zz-meridian@latest adopt [brand flags] [--allow-dirty] [--no-install]
|
|
15
|
+
Bring Meridian into the Next.js App Router project in this folder, keeping its routes and data layer.
|
|
16
|
+
|
|
17
|
+
npx zz-meridian@latest create <dir> [brand flags] [--no-install]
|
|
18
|
+
Start a new dashboard on Meridian in <dir>.
|
|
19
|
+
|
|
20
|
+
npx zz-meridian@latest skill [--global]
|
|
21
|
+
Install only the agent skill: into this project, or for every project (~/.agents/skills, ~/.claude/skills).
|
|
22
|
+
|
|
23
|
+
Brand flags: --name "Acme Ops" --workspace Production --timezone Europe/London --currency EUR
|
|
24
|
+
--user "Ada Park" --role Admin --accent indigo|cobalt|jade|graphite | --hex '#2E6BE4' | --hue 250 --chroma 0.14
|
|
25
|
+
|
|
26
|
+
Then give your coding agent the skill: .agents/skills/zz-meridian/SKILL.md.`;
|
|
27
|
+
const { values, positionals } = parseArgs({
|
|
28
|
+
allowPositionals: true,
|
|
29
|
+
options: {
|
|
30
|
+
...Object.fromEntries(BRAND_FLAGS.map((f) => [f, { type: 'string' }])),
|
|
31
|
+
'allow-dirty': { type: 'boolean' },
|
|
32
|
+
'no-install': { type: 'boolean' },
|
|
33
|
+
global: { type: 'boolean' },
|
|
34
|
+
help: { type: 'boolean', short: 'h' },
|
|
35
|
+
version: { type: 'boolean', short: 'v' },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const flags = values;
|
|
39
|
+
const brand = Object.fromEntries(BRAND_FLAGS.flatMap((f) => (typeof flags[f] === 'string' ? [[f, flags[f]]] : [])));
|
|
40
|
+
const install = !values['no-install'];
|
|
41
|
+
const [command, arg] = positionals;
|
|
42
|
+
function main() {
|
|
43
|
+
if (values.version) {
|
|
44
|
+
console.log(VERSION);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
// A brand value is written into TypeScript and JSON; refuse what could end its string before anything is copied.
|
|
48
|
+
const unsafe = Object.entries(brand).find(([, v]) => /[\\\r\n\u2028\u2029]/.test(v));
|
|
49
|
+
if (unsafe) {
|
|
50
|
+
console.error(`zz-meridian: --${unsafe[0]} may not contain a backslash or a line break`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
if (values.help || !command) {
|
|
54
|
+
console.log(HELP);
|
|
55
|
+
return command || values.help ? 0 : 1;
|
|
56
|
+
}
|
|
57
|
+
if (command === 'adopt')
|
|
58
|
+
return adopt({ root: process.cwd(), brand, allowDirty: Boolean(values['allow-dirty']), install });
|
|
59
|
+
if (command === 'create') {
|
|
60
|
+
if (!arg) {
|
|
61
|
+
console.error('zz-meridian create: name the folder for the new dashboard');
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
return create({ dir: arg, brand, install });
|
|
65
|
+
}
|
|
66
|
+
if (command === 'skill') {
|
|
67
|
+
const roots = values.global ? [os.homedir()] : [process.cwd()];
|
|
68
|
+
for (const r of roots)
|
|
69
|
+
installSkill(r);
|
|
70
|
+
console.log(`The zz-meridian skill is in ${values.global ? path.join(os.homedir(), '.agents/skills') + ' and ' + path.join(os.homedir(), '.claude/skills') : '.agents/skills and .claude/skills'}. Codex may need a restart to list it.`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
console.error(`zz-meridian: unknown command "${command}"\n\n${HELP}`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
process.exitCode = main();
|
package/dist/create.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zz-meridian create <dir>`: a new dashboard from the template, branded as the person's product (scripts/brand.ts
|
|
3
|
+
* --product removes the Design Atlas, the specifications and the design system's own docs), with the agent skill in
|
|
4
|
+
* the project and every file recorded in the manifest.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { PAYLOAD, VERSION, brandArgs, hasCommand, inside, installSkill, payloadFiles, run, sha256, writeIn, writeManifest } from './files.js';
|
|
9
|
+
export function create(o) {
|
|
10
|
+
const root = path.resolve(o.dir);
|
|
11
|
+
if (fs.existsSync(root) && fs.readdirSync(root).length)
|
|
12
|
+
return fail(`${root} exists and is not empty: name a new folder`);
|
|
13
|
+
for (const rel of payloadFiles()) {
|
|
14
|
+
if (rel.startsWith('skills/'))
|
|
15
|
+
continue;
|
|
16
|
+
writeIn(root, rel, fs.readFileSync(path.join(PAYLOAD, rel)));
|
|
17
|
+
}
|
|
18
|
+
const name = o.brand.name ?? path.basename(root).replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
19
|
+
const brand = { ...o.brand, name };
|
|
20
|
+
if (run(process.execPath, ['scripts/brand.ts', ...brandArgs(brand), '--product'], root).status !== 0) {
|
|
21
|
+
return fail('scripts/brand.ts failed (above); the template is copied, so fix the cause and run it again with the same flags');
|
|
22
|
+
}
|
|
23
|
+
// pnpm installs from the template's lockfile, with install scripts off (pnpm-workspace.yaml). Without pnpm, corepack
|
|
24
|
+
// (bundled with Node 22) runs it; only without both does npm install, from the version ranges, with scripts off too.
|
|
25
|
+
const install = hasCommand('pnpm') ? ['pnpm', ['install']] : hasCommand('corepack') ? ['corepack', ['pnpm', 'install']] : ['npm', ['install', '--ignore-scripts']];
|
|
26
|
+
const pm = install[0] === 'npm' ? 'npm' : 'pnpm';
|
|
27
|
+
if (pm === 'npm') {
|
|
28
|
+
for (const f of ['pnpm-lock.yaml', 'pnpm-workspace.yaml'])
|
|
29
|
+
fs.rmSync(path.join(root, f), { force: true });
|
|
30
|
+
console.log('pnpm is not available: installing with npm from the version ranges, without a lockfile.');
|
|
31
|
+
}
|
|
32
|
+
const files = {};
|
|
33
|
+
const walk = (rel) => {
|
|
34
|
+
for (const e of fs.readdirSync(inside(root, rel || '.'), { withFileTypes: true })) {
|
|
35
|
+
const p = rel ? `${rel}/${e.name}` : e.name;
|
|
36
|
+
if (['node_modules', '.git', '.meridian', '.next'].includes(p))
|
|
37
|
+
continue;
|
|
38
|
+
if (e.isDirectory())
|
|
39
|
+
walk(p);
|
|
40
|
+
else if (e.isFile())
|
|
41
|
+
files[p] = sha256(fs.readFileSync(inside(root, p)));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
walk('');
|
|
45
|
+
installSkill(root, files);
|
|
46
|
+
writeManifest(root, { version: VERSION, route: 'create', brand, files });
|
|
47
|
+
if (hasCommand('git'))
|
|
48
|
+
run('git', ['init', '-q'], root);
|
|
49
|
+
if (o.install && run(install[0], install[1], root).status !== 0)
|
|
50
|
+
return fail(`${install.join(' ')} failed (above); run it again in ${root}`);
|
|
51
|
+
console.log(`
|
|
52
|
+
${name} is a new Meridian ${VERSION} dashboard in ${root}.
|
|
53
|
+
skill: .agents/skills/zz-meridian (Codex) and .claude/skills/zz-meridian (Claude Code)
|
|
54
|
+
|
|
55
|
+
Next: follow .agents/skills/zz-meridian/SKILL.md from step 5 (the template is fetched and branded): data first, then
|
|
56
|
+
the pages, then ${pm} run verify until it passes.`);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
function fail(msg) {
|
|
60
|
+
console.error(`zz-meridian create: ${msg}`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file work every command shares: where the payload is, reading and writing inside one project without ever
|
|
3
|
+
* leaving it, hashing for the manifest, and running a tool without a shell.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
/** The template snapshot this package carries, built from `git ls-files` at the release commit. */
|
|
11
|
+
export const PAYLOAD = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'payload');
|
|
12
|
+
export const VERSION = JSON.parse(fs.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8')).version;
|
|
13
|
+
/** Every file under a payload folder, as posix paths relative to the payload. Symbolic links are never followed. */
|
|
14
|
+
export function payloadFiles(dir = '') {
|
|
15
|
+
const out = [];
|
|
16
|
+
const walk = (rel) => {
|
|
17
|
+
for (const e of fs.readdirSync(path.join(PAYLOAD, rel), { withFileTypes: true })) {
|
|
18
|
+
const p = rel ? `${rel}/${e.name}` : e.name;
|
|
19
|
+
if (e.isSymbolicLink())
|
|
20
|
+
continue;
|
|
21
|
+
if (e.isDirectory())
|
|
22
|
+
walk(p);
|
|
23
|
+
else if (e.isFile())
|
|
24
|
+
out.push(p);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
if (fs.existsSync(path.join(PAYLOAD, dir)))
|
|
28
|
+
walk(dir);
|
|
29
|
+
return out.sort();
|
|
30
|
+
}
|
|
31
|
+
export const readPayload = (rel) => fs.readFileSync(path.join(PAYLOAD, rel), 'utf8');
|
|
32
|
+
/**
|
|
33
|
+
* A path inside `root`; anything that would resolve outside it is refused: `..`, an absolute path, or a symbolic link
|
|
34
|
+
* on the way (a linked folder or file could carry a write somewhere else).
|
|
35
|
+
*/
|
|
36
|
+
export function inside(root, rel) {
|
|
37
|
+
const abs = path.resolve(root, rel);
|
|
38
|
+
if (abs !== root && !abs.startsWith(root + path.sep))
|
|
39
|
+
throw new Error(`refusing to touch ${rel}: it is outside ${root}`);
|
|
40
|
+
for (let p = abs; p !== root && p.startsWith(root + path.sep); p = path.dirname(p)) {
|
|
41
|
+
if (fs.lstatSync(p, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
42
|
+
throw new Error(`refusing to touch ${rel}: ${path.relative(root, p)} is a symbolic link`);
|
|
43
|
+
}
|
|
44
|
+
return abs;
|
|
45
|
+
}
|
|
46
|
+
export function writeIn(root, rel, content) {
|
|
47
|
+
const abs = inside(root, rel);
|
|
48
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
49
|
+
fs.writeFileSync(abs, content);
|
|
50
|
+
}
|
|
51
|
+
export const sha256 = (content) => `sha256-${createHash('sha256').update(content).digest('hex')}`;
|
|
52
|
+
/**
|
|
53
|
+
* A command with its arguments, without a shell, so nothing in a name or a path is ever interpreted. The one exception
|
|
54
|
+
* is a Windows .cmd shim (npm, pnpm, tsc), which Node runs only through a shell: those calls pass fixed arguments
|
|
55
|
+
* only, never a brand flag or a path the person typed; node itself (the brand script) never goes through a shell.
|
|
56
|
+
*/
|
|
57
|
+
export function run(cmd, args, cwd, quiet = false) {
|
|
58
|
+
const shim = process.platform === 'win32' && cmd !== process.execPath;
|
|
59
|
+
return spawnSync(shim && cmd.includes(' ') ? `"${cmd}"` : cmd, args, { cwd, encoding: 'utf8', stdio: quiet ? 'pipe' : 'inherit', shell: shim });
|
|
60
|
+
}
|
|
61
|
+
/** The package manager a project already uses, from its lockfile; npm when it has none. */
|
|
62
|
+
export function packageManager(root) {
|
|
63
|
+
if (fs.existsSync(path.join(root, 'pnpm-lock.yaml')))
|
|
64
|
+
return 'pnpm';
|
|
65
|
+
if (fs.existsSync(path.join(root, 'yarn.lock')))
|
|
66
|
+
return 'yarn';
|
|
67
|
+
if (fs.existsSync(path.join(root, 'bun.lock')) || fs.existsSync(path.join(root, 'bun.lockb')))
|
|
68
|
+
return 'bun';
|
|
69
|
+
return 'npm';
|
|
70
|
+
}
|
|
71
|
+
/** A tool in the project's node_modules, run as `node <its script>`: no shell and no .cmd shim on any platform. */
|
|
72
|
+
export function runTool(root, entry, args, quiet = true) {
|
|
73
|
+
return spawnSync(process.execPath, [path.join(root, 'node_modules', entry), ...args], { cwd: root, encoding: 'utf8', stdio: quiet ? 'pipe' : 'inherit' });
|
|
74
|
+
}
|
|
75
|
+
export const hasCommand = (cmd) => spawnSync(cmd, ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' }).status === 0;
|
|
76
|
+
/** JSON with the comments and trailing commas a tsconfig may carry. */
|
|
77
|
+
export function readJsonc(file) {
|
|
78
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
79
|
+
let out = '';
|
|
80
|
+
for (let i = 0, str = false; i < text.length; i++) {
|
|
81
|
+
const c = text[i];
|
|
82
|
+
if (str) {
|
|
83
|
+
out += c;
|
|
84
|
+
if (c === '\\')
|
|
85
|
+
out += text[++i];
|
|
86
|
+
else if (c === '"')
|
|
87
|
+
str = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (c === '"') {
|
|
91
|
+
str = true;
|
|
92
|
+
out += c;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
96
|
+
while (i < text.length && text[i] !== '\n')
|
|
97
|
+
i++;
|
|
98
|
+
out += '\n';
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (c === '/' && text[i + 1] === '*') {
|
|
102
|
+
i += 2;
|
|
103
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
|
|
104
|
+
i++;
|
|
105
|
+
i++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
out += c;
|
|
109
|
+
}
|
|
110
|
+
return JSON.parse(out.replace(/,(\s*[}\]])/g, '$1'));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Meridian's own `@/` imports, made relative to the file. An adopting project may map `@/` somewhere else, or already
|
|
114
|
+
* have a `components/ui/button`; a relative import can never resolve to the team's file instead of Meridian's.
|
|
115
|
+
*/
|
|
116
|
+
export function relativeImports(rel, source) {
|
|
117
|
+
// A literal specifier only: a template string such as `import('@/${dir}')` is code that builds a path, not an import.
|
|
118
|
+
return source.replace(/(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])@\/([\w./()[\]-]+)\2/g, (_m, lead, q, spec) => {
|
|
119
|
+
let to = path.posix.relative(path.posix.dirname(rel), `src/${spec}`);
|
|
120
|
+
if (!to.startsWith('.'))
|
|
121
|
+
to = `./${to}`;
|
|
122
|
+
return `${lead}${q}${to}${q}`;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export function writeManifest(root, m) {
|
|
126
|
+
const sorted = Object.fromEntries(Object.entries(m.files).sort(([a], [b]) => a.localeCompare(b)));
|
|
127
|
+
writeIn(root, '.meridian/manifest.json', JSON.stringify({ ...m, files: sorted }, null, 2) + '\n');
|
|
128
|
+
}
|
|
129
|
+
/** The agent skill, into the folders each agent reads in a repository: Codex (.agents) and Claude Code (.claude). */
|
|
130
|
+
export function installSkill(root, record) {
|
|
131
|
+
const files = payloadFiles('skills/zz-meridian');
|
|
132
|
+
for (const base of ['.agents/skills/zz-meridian', '.claude/skills/zz-meridian']) {
|
|
133
|
+
for (const f of files) {
|
|
134
|
+
const rel = `${base}/${f.slice('skills/zz-meridian/'.length)}`;
|
|
135
|
+
const content = fs.readFileSync(path.join(PAYLOAD, f));
|
|
136
|
+
writeIn(root, rel, content);
|
|
137
|
+
if (record)
|
|
138
|
+
record[rel] = sha256(content);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Brand flags passed through to scripts/brand.ts, in the order it reads them. */
|
|
143
|
+
export const BRAND_FLAGS = ['name', 'workspace', 'timezone', 'currency', 'user', 'role', 'accent', 'hex', 'hue', 'chroma'];
|
|
144
|
+
export const brandArgs = (brand) => BRAND_FLAGS.flatMap((k) => (brand[k] ? [`--${k}`, brand[k]] : []));
|