From 878664816583574d21937ae6b247f27a833d062d Mon Sep 17 00:00:00 2001 From: martyy-code Date: Wed, 29 Jul 2026 17:41:43 +0200 Subject: [PATCH 1/4] feat(blocks): first ds-block-empty-state + contract-test target resolution Three concerns bundled in one commit because they are tightly coupled: 1. New block: ds-block-empty-state (category: onboarding) - Single-file block that composes ds-empty + ds-button + ds-colored-badge via registryDependencies (per the shadcn-registry-adoption plan's Blocks extension point convention). - First entry in BLOCK_REGISTRY. Showcase aggregator wires EmptyState and pulls source from SOURCES.blocks. - 4-surface pattern (showcase tree, consumer tree, registry.json, audit drift entry) same as components. - API: title, description, icon, badge, action - all optional, sensible defaults so the showcase preview renders with no props. 2. fix(contract-test): simulate shadcn CLI file rename via registry.json targets - The contract test previously did a flat cpSync(SOURCE_DIR, components/ui) which kept the registry/base-nova/ds-X/ds-X.tsx layout. Imports like @/components/ui/ds-empty would resolve to components/ui/ds-empty.tsx (the rename shadcn CLI applies at install time), not to components/ui/ds-empty/ds-empty.tsx. - The old layout happened to work because no consumer file imported other ds-* components. ds-block-empty-state breaks that - it composes three ds-* items. - Fix: read each item's target field from registry.json and copy to the resolved consumer-relative path. Handles @ui/, @components/, @lib/, @hooks/, @/ prefixes. - This makes the contract test accurately simulate what shadcn CLI does at install time, not just what the source tree looks like. 3. memory: project-level learnings - New file feedback_registry_deps_coupling.md captures the lesson from the ds-breadcrumb lucide-react incident (install list in contract-test.mjs must mirror registry.json dependencies). - MEMORY.md index updated. Validation (all green): - npm run build -w @workspace/registry - node apps/web/scripts/build-sources.mjs (8 components, 1 block) - node apps/web/scripts/build-registry.mjs (catalog + 9 items) - node apps/web/scripts/check-registry-drift.mjs - cd apps/web && npx tsc --noEmit - next build (7/7 pages) - node apps/web/scripts/contract-test.mjs Co-Authored-By: Claude --- .claude/agent-memory/main/MEMORY.md | 1 + .../main/feedback_registry_deps_coupling.md | 18 ++++++ apps/web/lib/registry/index.tsx | 10 ++- apps/web/lib/registry/sources.generated.ts | 14 +++-- apps/web/public/r/ds-block-empty-state.json | 23 +++++++ apps/web/public/r/ds-breadcrumb.json | 2 +- apps/web/public/r/ds-empty.json | 2 +- apps/web/public/r/ds-input.json | 2 +- apps/web/public/r/ds-tabs.json | 2 +- apps/web/public/r/ds-textarea.json | 2 +- apps/web/public/r/registry.json | 22 +++++++ apps/web/scripts/contract-test.mjs | 28 ++++++++- docs/registry/audit-2026-07-29.json | 20 ++++++ .../registry/src/blocks/empty-state/index.tsx | 61 ++++++++++++++++++ .../registry/src/blocks/empty-state/meta.ts | 9 +++ registry.json | 22 +++++++ .../ds-block-empty-state.tsx | 62 +++++++++++++++++++ 17 files changed, 286 insertions(+), 14 deletions(-) create mode 100644 .claude/agent-memory/main/feedback_registry_deps_coupling.md create mode 100644 apps/web/public/r/ds-block-empty-state.json create mode 100644 packages/registry/src/blocks/empty-state/index.tsx create mode 100644 packages/registry/src/blocks/empty-state/meta.ts create mode 100644 registry/base-nova/ds-block-empty-state/ds-block-empty-state.tsx diff --git a/.claude/agent-memory/main/MEMORY.md b/.claude/agent-memory/main/MEMORY.md index b09b505..8ea551e 100644 --- a/.claude/agent-memory/main/MEMORY.md +++ b/.claude/agent-memory/main/MEMORY.md @@ -2,5 +2,6 @@ - [User profile](user_profile.md) — French-speaking, shadcn/Tailwind v4 practitioner fighting agent-generated "slop UI" - [Concrete over theory](feedback_concrete_over_theory.md) — anchor design talk in current, verifiable tooling; label opinion as opinion +- [Registry deps coupling](feedback_registry_deps_coupling.md) — new registry item with new peer dep requires updating contract-test.mjs install list in lockstep - [Design learnings repo](project_design_learnings.md) — knowledge base + working monorepo for deessejs/ui registry at ui.deessejs.com (hosted on Vercel) - [Phase 4 validated](project_phase4_validated.md) — external install end-to-end confirmed 2026-07-29; Phase 6 (official shadcn index submission) gate is now lifted diff --git a/.claude/agent-memory/main/feedback_registry_deps_coupling.md b/.claude/agent-memory/main/feedback_registry_deps_coupling.md new file mode 100644 index 0000000..c7625f3 --- /dev/null +++ b/.claude/agent-memory/main/feedback_registry_deps_coupling.md @@ -0,0 +1,18 @@ +--- +name: feedback-registry-deps-coupling +description: Adding a new registry item with a new peer dep requires updating contract-test.mjs install list in lockstep — refactor target is to derive deps from registry.json +metadata: + type: feedback +--- + +When adding a new registry item whose `dependencies[]` introduces a peer dep the contract test hasn't installed, the CI `contract` job fails with TypeScript resolution errors (`Cannot find module 'X'`). Fix: add the new dep to the hardcoded install list in `apps/web/scripts/contract-test.mjs`. + +**Why:** during the 3→8 catalog expansion in this session, `ds-breadcrumb` introduced `lucide-react` (for `ChevronRightIcon` and `MoreHorizontalIcon`). The contract test passed locally because `lucide-react` happened to be present in the workspace `node_modules` from another package, but CI on a clean checkout failed. Cost: one fix-commit (`17d5f3d`) + one CI re-run. + +**How to apply:** +- Short term: when adding any registry item, audit its `dependencies[]` against the install list in `apps/web/scripts/contract-test.mjs` (lines ~84-99) and add any new ones before pushing. +- Long term: refactor `contract-test.mjs` to read `registry.json`, aggregate the union of `dependencies[]` across all items, and install that. Single source of truth = the catalog. Removes the coupling entirely. + +The drift script (`check-registry-drift.mjs`) has the same shape of coupling to `registry.json` but in the opposite direction (reads, doesn't install) — that one is fine as-is. + +Related: [[project-design-learnings]], [[project-phase4-validated]]. diff --git a/apps/web/lib/registry/index.tsx b/apps/web/lib/registry/index.tsx index 2f65f9a..7245619 100644 --- a/apps/web/lib/registry/index.tsx +++ b/apps/web/lib/registry/index.tsx @@ -38,6 +38,8 @@ import { TextareaDemo, } from "@workspace/registry/components/textarea" import { meta as textareaMeta } from "@workspace/registry/components/textarea/meta" +import { EmptyState } from "@workspace/registry/blocks/empty-state" +import { meta as emptyStateMeta } from "@workspace/registry/blocks/empty-state/meta" import { SOURCES } from "./sources" import type { ComponentMeta, BlockMeta } from "./types" @@ -107,7 +109,13 @@ const COMPONENT_REGISTRY: ComponentEntry[] = [ }, ] -const BLOCK_REGISTRY: BlockEntry[] = [] +const BLOCK_REGISTRY: BlockEntry[] = [ + { + ...emptyStateMeta, + Block: EmptyState, + source: SOURCES.blocks["empty-state"], + }, +] const CATEGORY_LABELS: Record = { buttons: "Buttons", diff --git a/apps/web/lib/registry/sources.generated.ts b/apps/web/lib/registry/sources.generated.ts index 87f29f8..4a453da 100644 --- a/apps/web/lib/registry/sources.generated.ts +++ b/apps/web/lib/registry/sources.generated.ts @@ -4,14 +4,16 @@ export const SOURCES = { components: { - "breadcrumb": "\"use client\"\n\nimport {\n Breadcrumb as ShadcnBreadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n} from \"@workspace/ui/components/breadcrumb\"\n\nexport {\n ShadcnBreadcrumb as Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n}\n\nexport function BreadcrumbDemo() {\n return (\n \n \n \n Home\n \n \n \n Components\n \n \n \n Breadcrumb\n \n \n \n )\n}\n", + "breadcrumb": "\"use client\"\r\n\r\nimport {\r\n Breadcrumb as ShadcnBreadcrumb,\r\n BreadcrumbList,\r\n BreadcrumbItem,\r\n BreadcrumbLink,\r\n BreadcrumbPage,\r\n BreadcrumbSeparator,\r\n BreadcrumbEllipsis,\r\n} from \"@workspace/ui/components/breadcrumb\"\r\n\r\nexport {\r\n ShadcnBreadcrumb as Breadcrumb,\r\n BreadcrumbList,\r\n BreadcrumbItem,\r\n BreadcrumbLink,\r\n BreadcrumbPage,\r\n BreadcrumbSeparator,\r\n BreadcrumbEllipsis,\r\n}\r\n\r\nexport function BreadcrumbDemo() {\r\n return (\r\n \r\n \r\n \r\n Home\r\n \r\n \r\n \r\n Components\r\n \r\n \r\n \r\n Breadcrumb\r\n \r\n \r\n \r\n )\r\n}\r\n", "button": "\"use client\"\n\nimport { Button as ShadcnButton } from \"@workspace/ui/components/button\"\n\nexport type ButtonProps = React.ComponentProps\n\nexport { ShadcnButton as Button }\n\nexport function ButtonDemo() {\n return (\n
\n Default\n Secondary\n Outline\n Ghost\n Destructive\n
\n )\n}", "colored-badge": "\"use client\"\r\n\r\nimport { Badge } from \"@workspace/ui/components/badge\"\r\nimport { cn } from \"@workspace/ui/lib/utils\"\r\n\r\nexport type ColoredBadgeColor =\r\n | \"blue\"\r\n | \"green\"\r\n | \"red\"\r\n | \"yellow\"\r\n | \"orange\"\r\n | \"purple\"\r\n | \"pink\"\r\n | \"gray\"\r\n\r\nconst COLOR_CLASSES: Record = {\r\n blue: \"bg-blue-600/10 text-blue-500 border-blue-500/20\",\r\n green: \"bg-green-500/10 text-green-500 border-green-500/20\",\r\n red: \"bg-red-500/10 text-red-500 border-red-500/20\",\r\n yellow: \"bg-yellow-500/10 text-yellow-500 border-yellow-500/20\",\r\n orange: \"bg-orange-500/10 text-orange-500 border-orange-500/20\",\r\n purple: \"bg-purple-500/10 text-purple-500 border-purple-500/20\",\r\n pink: \"bg-pink-500/10 text-pink-500 border-pink-500/20\",\r\n gray: \"bg-gray-500/10 text-gray-500 border-gray-500/20\",\r\n}\r\n\r\nexport interface ColoredBadgeProps {\r\n color: ColoredBadgeColor\r\n children: React.ReactNode\r\n}\r\n\r\nexport function ColoredBadge({ color, children }: ColoredBadgeProps) {\r\n return (\r\n \r\n {children}\r\n \r\n )\r\n}\r\n\r\nexport function ColoredBadgeDemo() {\r\n return (\r\n
\r\n Blue\r\n Green\r\n Red\r\n Yellow\r\n Orange\r\n Purple\r\n Pink\r\n Gray\r\n
\r\n )\r\n}", - "empty": "\"use client\"\n\nimport {\n Empty as ShadcnEmpty,\n EmptyHeader,\n EmptyTitle,\n EmptyDescription,\n EmptyContent,\n EmptyMedia,\n} from \"@workspace/ui/components/empty\"\n\nexport {\n ShadcnEmpty as Empty,\n EmptyHeader,\n EmptyTitle,\n EmptyDescription,\n EmptyContent,\n EmptyMedia,\n}\n\nexport function EmptyDemo() {\n return (\n \n \n ⊙\n No projects yet\n \n Create your first project to start tracking work.\n \n \n \n \n Create project\n \n \n \n )\n}\n", + "empty": "\"use client\"\r\n\r\nimport {\r\n Empty as ShadcnEmpty,\r\n EmptyHeader,\r\n EmptyTitle,\r\n EmptyDescription,\r\n EmptyContent,\r\n EmptyMedia,\r\n} from \"@workspace/ui/components/empty\"\r\n\r\nexport {\r\n ShadcnEmpty as Empty,\r\n EmptyHeader,\r\n EmptyTitle,\r\n EmptyDescription,\r\n EmptyContent,\r\n EmptyMedia,\r\n}\r\n\r\nexport function EmptyDemo() {\r\n return (\r\n \r\n \r\n ⊙\r\n No projects yet\r\n \r\n Create your first project to start tracking work.\r\n \r\n \r\n \r\n \r\n Create project\r\n \r\n \r\n \r\n )\r\n}\r\n", "icon-button": "\"use client\"\n\nimport { Button as ShadcnButton } from \"@workspace/ui/components/button\"\nimport { cn } from \"@workspace/ui/lib/utils\"\n\nexport interface IconButtonProps\n extends Omit, \"children\" | \"size\"> {\n \"aria-label\": string\n children: React.ReactNode\n size?: \"sm\" | \"md\" | \"lg\"\n}\n\nconst SIZE_CLASSES: Record, string> = {\n sm: \"size-8\",\n md: \"size-10\",\n lg: \"size-12\",\n}\n\nexport function IconButton({\n className,\n size = \"md\",\n type = \"button\",\n ...props\n}: IconButtonProps) {\n return (\n \n )\n}\n\nexport function IconButtonDemo() {\n return (\n
\n \n +\n \n \n ✎\n \n \n ×\n \n
\n )\n}", - "input": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@workspace/ui/lib/utils\"\n\nexport interface InputProps\n extends React.InputHTMLAttributes {}\n\nexport const Input = React.forwardRef(\n ({ className, type, ...props }, ref) => (\n \n )\n)\nInput.displayName = \"Input\"\n\nexport function InputDemo() {\n return (\n
\n \n \n \n \n \n
\n )\n}\n", - "tabs": "\"use client\"\n\nimport {\n Tabs as ShadcnTabs,\n TabsList,\n TabsTrigger,\n TabsContent,\n} from \"@workspace/ui/components/tabs\"\n\nexport {\n ShadcnTabs as Tabs,\n TabsList,\n TabsTrigger,\n TabsContent,\n}\n\nexport function TabsDemo() {\n return (\n \n \n Overview\n Analytics\n Settings\n \n Overview panel content.\n Analytics panel content.\n Settings panel content.\n \n )\n}\n", - "textarea": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@workspace/ui/lib/utils\"\n\nexport interface TextareaProps\n extends React.TextareaHTMLAttributes {}\n\nexport const Textarea = React.forwardRef(\n ({ className, ...props }, ref) => (\n \n )\n)\nTextarea.displayName = \"Textarea\"\n\nexport function TextareaDemo() {\n return (\n
\n