Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/agent-memory/main/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions .claude/agent-memory/main/feedback_registry_deps_coupling.md
Original file line number Diff line number Diff line change
@@ -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]].
7 changes: 1 addition & 6 deletions apps/web/app/blocks/[category_id]/[block_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,7 @@ export default async function Page({ params }: { params: Params }) {
</span>
</div>
<div className="flex flex-1 items-center justify-center p-12">
<div className="text-muted-foreground flex flex-col items-center gap-2 font-mono text-xs">
<span className="text-foreground text-sm font-medium">
{block.name}
</span>
<span>rendered block placeholder</span>
</div>
<block.Demo />
</div>
</div>
</TabsContent>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/blocks/[category_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export default async function Page({ params }: { params: Params }) {
href={`/blocks/${category_id}/${item.id}`}
name={item.name}
description={item.description}
preview={<item.Block />}
preview={<item.Demo />}
className={spansTwo ? "sm:col-span-2" : undefined}
previewClassName={spansTwo ? "sm:h-60" : undefined}
/>
Expand Down
20 changes: 17 additions & 3 deletions apps/web/lib/registry/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
TextareaDemo,
} from "@workspace/registry/components/textarea"
import { meta as textareaMeta } from "@workspace/registry/components/textarea/meta"
import {
EmptyState,
EmptyStateBlockDemo,
} 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"
Expand All @@ -53,6 +58,8 @@
type BlockEntry = BlockMeta & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Block: React.ComponentType<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Demo: React.ComponentType<any>
source: string
}

Expand Down Expand Up @@ -107,7 +114,14 @@
},
]

const BLOCK_REGISTRY: BlockEntry[] = []
const BLOCK_REGISTRY: BlockEntry[] = [
{
...emptyStateMeta,
Block: EmptyState,
Demo: EmptyStateBlockDemo,
source: SOURCES.blocks["empty-state"],
},
]

const CATEGORY_LABELS: Record<string, string> = {
buttons: "Buttons",
Expand Down Expand Up @@ -191,13 +205,13 @@
function deriveComponentPreview(items: ComponentEntry[]): React.ComponentType {
const First = items[0]!
const Demo = First.Demo
return () => <Demo />

Check warning on line 208 in apps/web/lib/registry/index.tsx

View workflow job for this annotation

GitHub Actions / Lint

Component definition is missing display name
}

function deriveBlockPreview(items: BlockEntry[]): React.ComponentType {
const First = items[0]!
const Block = First.Block
return () => <Block />
const Demo = First.Demo
return () => <Demo />

Check warning on line 214 in apps/web/lib/registry/index.tsx

View workflow job for this annotation

GitHub Actions / Lint

Component definition is missing display name
}

export function getEnrichedComponentCategories(): EnrichedComponentCategory[] {
Expand Down
14 changes: 8 additions & 6 deletions apps/web/lib/registry/sources.generated.ts

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions apps/web/public/r/ds-block-empty-state.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "ds-block-empty-state",
"type": "registry:block",
"title": "Empty State",
"description": "DeesseJS composable empty-state block — bundles ds-empty + ds-button + ds-colored-badge. Optional icon, status badge, description, and CTA. First block, single-file, validates the registry:block pipeline.",
"dependencies": [
"clsx",
"tailwind-merge"
],
"registryDependencies": [
"ds-empty",
"ds-button",
"ds-colored-badge"
],
"files": [
{
"path": "registry/base-nova/ds-block-empty-state/ds-block-empty-state.tsx",
"type": "registry:component",
"target": "@/components/empty-state.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport {\n Empty,\n EmptyHeader,\n EmptyMedia,\n EmptyTitle,\n EmptyDescription,\n EmptyContent,\n} from \"@/components/ui/ds-empty\"\nimport { DsButton } from \"@/components/ui/ds-button\"\nimport {\n DsColoredBadge,\n type DsColoredBadgeColor,\n} from \"@/components/ui/ds-colored-badge\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface DsEmptyStateProps {\n title?: string\n description?: string\n icon?: React.ReactNode\n badge?: { label: string; color: DsColoredBadgeColor }\n action?: { label: string; onClick?: () => void }\n className?: string\n}\n\nexport function DsEmptyState({\n title = \"Nothing here yet\",\n description,\n icon,\n badge,\n action,\n className,\n}: DsEmptyStateProps) {\n const hasHeader = icon !== undefined || badge !== undefined\n return (\n <Empty className={cn(className)}>\n {hasHeader ? (\n <EmptyHeader>\n {icon && <EmptyMedia variant=\"icon\">{icon}</EmptyMedia>}\n {badge && (\n <DsColoredBadge color={badge.color}>{badge.label}</DsColoredBadge>\n )}\n <EmptyTitle>{title}</EmptyTitle>\n {description && <EmptyDescription>{description}</EmptyDescription>}\n </EmptyHeader>\n ) : (\n <>\n <EmptyTitle>{title}</EmptyTitle>\n {description && <EmptyDescription>{description}</EmptyDescription>}\n </>\n )}\n {action && (\n <EmptyContent>\n <DsButton onClick={action.onClick}>{action.label}</DsButton>\n </EmptyContent>\n )}\n </Empty>\n )\n}"
}
]
}
2 changes: 1 addition & 1 deletion apps/web/public/r/ds-breadcrumb.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"path": "registry/base-nova/ds-breadcrumb/ds-breadcrumb.tsx",
"type": "registry:ui",
"target": "@ui/ds-breadcrumb.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { mergeProps } from \"@base-ui/react/merge-props\"\nimport { useRender } from \"@base-ui/react/use-render\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ChevronRightIcon, MoreHorizontalIcon } from \"lucide-react\"\n\nfunction Breadcrumb({ className, ...props }: React.ComponentProps<\"nav\">) {\n return (\n <nav\n aria-label=\"breadcrumb\"\n data-slot=\"breadcrumb\"\n className={cn(className)}\n {...props}\n />\n )\n}\n\nfunction BreadcrumbList({ className, ...props }: React.ComponentProps<\"ol\">) {\n return (\n <ol\n data-slot=\"breadcrumb-list\"\n className={cn(\n \"flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction BreadcrumbItem({ className, ...props }: React.ComponentProps<\"li\">) {\n return (\n <li\n data-slot=\"breadcrumb-item\"\n className={cn(\"inline-flex items-center gap-1\", className)}\n {...props}\n />\n )\n}\n\nfunction BreadcrumbLink({\n className,\n render,\n ...props\n}: useRender.ComponentProps<\"a\">) {\n return useRender({\n defaultTagName: \"a\",\n props: mergeProps<\"a\">(\n {\n className: cn(\"transition-colors hover:text-foreground\", className),\n },\n props\n ),\n render,\n state: {\n slot: \"breadcrumb-link\",\n },\n })\n}\n\nfunction BreadcrumbPage({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"breadcrumb-page\"\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn(\"font-normal text-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction BreadcrumbSeparator({\n children,\n className,\n ...props\n}: React.ComponentProps<\"li\">) {\n return (\n <li\n data-slot=\"breadcrumb-separator\"\n role=\"presentation\"\n aria-hidden=\"true\"\n className={cn(\"[&>svg]:size-3.5\", className)}\n {...props}\n >\n {children ?? <ChevronRightIcon />}\n </li>\n )\n}\n\nfunction BreadcrumbEllipsis({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"breadcrumb-ellipsis\"\n role=\"presentation\"\n aria-hidden=\"true\"\n className={cn(\n \"flex size-5 items-center justify-center [&>svg]:size-4\",\n className\n )}\n {...props}\n >\n <MoreHorizontalIcon />\n <span className=\"sr-only\">More</span>\n </span>\n )\n}\n\nexport {\n Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n}\n"
"content": "\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport { mergeProps } from \"@base-ui/react/merge-props\"\r\nimport { useRender } from \"@base-ui/react/use-render\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\nimport { ChevronRightIcon, MoreHorizontalIcon } from \"lucide-react\"\r\n\r\nfunction Breadcrumb({ className, ...props }: React.ComponentProps<\"nav\">) {\r\n return (\r\n <nav\r\n aria-label=\"breadcrumb\"\r\n data-slot=\"breadcrumb\"\r\n className={cn(className)}\r\n {...props}\r\n />\r\n )\r\n}\r\n\r\nfunction BreadcrumbList({ className, ...props }: React.ComponentProps<\"ol\">) {\r\n return (\r\n <ol\r\n data-slot=\"breadcrumb-list\"\r\n className={cn(\r\n \"flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground\",\r\n className\r\n )}\r\n {...props}\r\n />\r\n )\r\n}\r\n\r\nfunction BreadcrumbItem({ className, ...props }: React.ComponentProps<\"li\">) {\r\n return (\r\n <li\r\n data-slot=\"breadcrumb-item\"\r\n className={cn(\"inline-flex items-center gap-1\", className)}\r\n {...props}\r\n />\r\n )\r\n}\r\n\r\nfunction BreadcrumbLink({\r\n className,\r\n render,\r\n ...props\r\n}: useRender.ComponentProps<\"a\">) {\r\n return useRender({\r\n defaultTagName: \"a\",\r\n props: mergeProps<\"a\">(\r\n {\r\n className: cn(\"transition-colors hover:text-foreground\", className),\r\n },\r\n props\r\n ),\r\n render,\r\n state: {\r\n slot: \"breadcrumb-link\",\r\n },\r\n })\r\n}\r\n\r\nfunction BreadcrumbPage({ className, ...props }: React.ComponentProps<\"span\">) {\r\n return (\r\n <span\r\n data-slot=\"breadcrumb-page\"\r\n role=\"link\"\r\n aria-disabled=\"true\"\r\n aria-current=\"page\"\r\n className={cn(\"font-normal text-foreground\", className)}\r\n {...props}\r\n />\r\n )\r\n}\r\n\r\nfunction BreadcrumbSeparator({\r\n children,\r\n className,\r\n ...props\r\n}: React.ComponentProps<\"li\">) {\r\n return (\r\n <li\r\n data-slot=\"breadcrumb-separator\"\r\n role=\"presentation\"\r\n aria-hidden=\"true\"\r\n className={cn(\"[&>svg]:size-3.5\", className)}\r\n {...props}\r\n >\r\n {children ?? <ChevronRightIcon />}\r\n </li>\r\n )\r\n}\r\n\r\nfunction BreadcrumbEllipsis({\r\n className,\r\n ...props\r\n}: React.ComponentProps<\"span\">) {\r\n return (\r\n <span\r\n data-slot=\"breadcrumb-ellipsis\"\r\n role=\"presentation\"\r\n aria-hidden=\"true\"\r\n className={cn(\r\n \"flex size-5 items-center justify-center [&>svg]:size-4\",\r\n className\r\n )}\r\n {...props}\r\n >\r\n <MoreHorizontalIcon />\r\n <span className=\"sr-only\">More</span>\r\n </span>\r\n )\r\n}\r\n\r\nexport {\r\n Breadcrumb,\r\n BreadcrumbList,\r\n BreadcrumbItem,\r\n BreadcrumbLink,\r\n BreadcrumbPage,\r\n BreadcrumbSeparator,\r\n BreadcrumbEllipsis,\r\n}\r\n"
}
]
}
2 changes: 1 addition & 1 deletion apps/web/public/r/ds-empty.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"path": "registry/base-nova/ds-empty/ds-empty.tsx",
"type": "registry:ui",
"target": "@ui/ds-empty.tsx",
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Empty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty\"\n className={cn(\n \"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction EmptyHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty-header\"\n className={cn(\"flex max-w-sm flex-col items-center gap-2\", className)}\n {...props}\n />\n )\n}\n\nconst emptyMediaVariants = cva(\n \"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n icon: \"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction EmptyMedia({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof emptyMediaVariants>) {\n return (\n <div\n data-slot=\"empty-icon\"\n data-variant={variant}\n className={cn(emptyMediaVariants({ variant, className }))}\n {...props}\n />\n )\n}\n\nfunction EmptyTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty-title\"\n className={cn(\n \"font-heading text-sm font-medium tracking-tight\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction EmptyDescription({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty-description\"\n className={cn(\n \"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction EmptyContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty-content\"\n className={cn(\n \"flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n Empty,\n EmptyHeader,\n EmptyTitle,\n EmptyDescription,\n EmptyContent,\n EmptyMedia,\n}\n"
"content": "import * as React from \"react\"\r\nimport { cva, type VariantProps } from \"class-variance-authority\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\n\r\nfunction Empty({ className, ...props }: React.ComponentProps<\"div\">) {\r\n return (\r\n <div\r\n data-slot=\"empty\"\r\n className={cn(\r\n \"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance\",\r\n className\r\n )}\r\n {...props}\r\n />\r\n )\r\n}\r\n\r\nfunction EmptyHeader({ className, ...props }: React.ComponentProps<\"div\">) {\r\n return (\r\n <div\r\n data-slot=\"empty-header\"\r\n className={cn(\"flex max-w-sm flex-col items-center gap-2\", className)}\r\n {...props}\r\n />\r\n )\r\n}\r\n\r\nconst emptyMediaVariants = cva(\r\n \"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0\",\r\n {\r\n variants: {\r\n variant: {\r\n default: \"bg-transparent\",\r\n icon: \"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4\",\r\n },\r\n },\r\n defaultVariants: {\r\n variant: \"default\",\r\n },\r\n }\r\n)\r\n\r\nfunction EmptyMedia({\r\n className,\r\n variant = \"default\",\r\n ...props\r\n}: React.ComponentProps<\"div\"> & VariantProps<typeof emptyMediaVariants>) {\r\n return (\r\n <div\r\n data-slot=\"empty-icon\"\r\n data-variant={variant}\r\n className={cn(emptyMediaVariants({ variant, className }))}\r\n {...props}\r\n />\r\n )\r\n}\r\n\r\nfunction EmptyTitle({ className, ...props }: React.ComponentProps<\"div\">) {\r\n return (\r\n <div\r\n data-slot=\"empty-title\"\r\n className={cn(\r\n \"font-heading text-sm font-medium tracking-tight\",\r\n className\r\n )}\r\n {...props}\r\n />\r\n )\r\n}\r\n\r\nfunction EmptyDescription({\r\n className,\r\n ...props\r\n}: React.ComponentProps<\"div\">) {\r\n return (\r\n <div\r\n data-slot=\"empty-description\"\r\n className={cn(\r\n \"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary\",\r\n className\r\n )}\r\n {...props}\r\n />\r\n )\r\n}\r\n\r\nfunction EmptyContent({ className, ...props }: React.ComponentProps<\"div\">) {\r\n return (\r\n <div\r\n data-slot=\"empty-content\"\r\n className={cn(\r\n \"flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance\",\r\n className\r\n )}\r\n {...props}\r\n />\r\n )\r\n}\r\n\r\nexport {\r\n Empty,\r\n EmptyHeader,\r\n EmptyTitle,\r\n EmptyDescription,\r\n EmptyContent,\r\n EmptyMedia,\r\n}\r\n"
}
]
}
Loading
Loading