Skip to content

Primitives Placement Analysis

Decision (2026-03-19): Option A accepted. Stock shadcn/ui components go into src/components/canary/primitives/ as a peer of atoms/. Custom components become first-class atoms under src/components/canary/atoms/.

Question: Should stock shadcn/ui components go into src/components/canary/primitives/ (peer of atoms) or src/components/canary/atoms/ (alongside first-class atoms)?

The callil consolidation introduces 20 files in src/components/ui/ that need to be relocated under the canary namespace. These fall into two categories:

Stock shadcn (13 files) — thin styled wrappers around Radix UI primitives, regenerable via shadcn CLI: collapsible, dropdown-menu, input, label, separator, sheet, sidebar, skeleton, table, tabs, textarea, toggle, tooltip

Note: sonner.tsx was originally in this list (14 files) but is excluded — it is dropped per the styles analysis decision (dead code coupling to next-themes).

Custom components (~6 files) — contain meaningful Arda-specific logic or extended variants: avatar (size variants, badge, group), badge (ghost/link variants), button (icon sizes, OKLch colors), dialog (showCloseButton), input-group (entirely original), card (CardAction)

The custom components will become first-class canary atoms (with stories, tests, StaticConfig/RuntimeConfig split). This analysis concerns only the stock ones.

Additionally, the callil branch already has canary atoms (ArdaBadge, ArdaButton, ArdaDrawer) that wrap stock shadcn components, establishing a two-tier pattern:

  • Foundation layer: stock shadcn (currently @/components/ui/*)
  • Atom layer: Arda-branded wrappers (currently @/components/canary/atoms/*)

Option A: canary/primitives/ — peer of atoms/molecules/organisms

Section titled “Option A: canary/primitives/ — peer of atoms/molecules/organisms”
src/components/canary/
primitives/ ← stock shadcn
atoms/ ← Arda atoms (may wrap primitives)
molecules/
organisms/

Storybook sidebar: Would appear as a separate top-level group if stories are added.

Option B: canary/atoms/primitives/ — subdirectory within atoms

Section titled “Option B: canary/atoms/primitives/ — subdirectory within atoms”
src/components/canary/
atoms/
primitives/ ← stock shadcn
badge/ ← Arda atoms
button/
...
molecules/
organisms/

Storybook sidebar: Canary > Atoms > Primitives > Sheet (if stories added).

Option C: Flat in canary/atoms/ — no separation

Section titled “Option C: Flat in canary/atoms/ — no separation”
src/components/canary/
atoms/
badge/ ← Arda atom
button/ ← Arda atom
sheet/ ← stock shadcn (mixed in)
tooltip/ ← stock shadcn (mixed in)
...
molecules/
organisms/

Storybook sidebar: All at same level under Atoms.

Brad Frost’s atomic design defines five levels: atoms, molecules, organisms, templates, pages. Atoms are “the smallest functional units” — basic HTML elements like buttons, inputs, labels. Below atoms are “sub-atomic particles” (design tokens: colors, spacing, typography) which are not functional on their own.

Stock shadcn components are functional — a Sheet or DropdownMenu works on its own. By Frost’s definition, they ARE atoms. The question is whether the canary design system treats them as first-class atoms or as a vendor dependency that atoms consume.

The dominant Storybook pattern organizes by atomic level with / separators in story titles: Atoms/Button, Molecules/DataGrid, etc. There is no standard “primitives” tier. Teams that add extra tiers (primitives, quarks, ions) sometimes create confusion about where new components belong.

However, mature design systems commonly separate their vendor/foundation layer. Material UI has “Base UI” (unstyled primitives). Chakra has its headless primitives. shadcn/ui itself positions as a foundation layer that teams customize on top of.

  1. Stock primitives should NOT have their own stories. They’re documented by shadcn itself. The Arda atoms that wrap them (ArdaBadge, ArdaDrawer, etc.) are where stories, tests, and documentation belong.

  2. Stock primitives should be clearly identifiable as vendor/regenerable. A developer should know at a glance that sheet.tsx is stock shadcn and can be regenerated, while button.tsx in atoms is Arda-custom.

  3. Import path clarity matters. When reading an Arda atom that imports from primitives/, the dependency direction is obvious: atom wraps primitive. When both live flat in atoms/, the relationship is opaque.

  4. The existing canary structure has no precedent. Today canary/atoms/ contains only grid cell types and detail-field. There’s no prior convention to break.

  5. Storybook stories glob (../src/components/**/*.stories.@(ts|tsx)) will pick up stories from any subdirectory, so placement doesn’t affect discoverability — it only affects sidebar organization via story title.

RiskOption A (peer)Option B (subdirectory)Option C (flat)
Breaks atomic design conventionModerate — adds a 4th tierLow — stays within atomsNone
Vendor code mixed with customNoneLowHigh — hard to distinguish
Import path confusionNoneNoneHigh — atoms/sheet looks like Arda code
Storybook sidebar clutterNone (no stories)None (no stories)Moderate — 14 undocumented components
Future shadcn CLI regenerationEasy — clear directoryEasy — clear directoryHard — mixed with custom code
Developer cognitive loadLow — clear separationLow — clear nestingModerate — must know which are stock

Option A: canary/primitives/ as a peer of atoms/, with one adjustment.

  1. Clear vendor boundary. The primitives/ directory is an explicit declaration: “these are stock shadcn, regenerable, no custom logic.” This is the most important property for maintainability.

  2. No stories needed. Primitives don’t appear in the Storybook sidebar at all. They’re internal building blocks consumed by atoms. This avoids sidebar clutter and the question of “where do I document Sheet vs ArdaDrawer?”

  3. Natural dependency direction. Atoms import from primitives (never the reverse). Molecules import from atoms and primitives. This is clean and auditable.

  4. shadcn CLI compatibility. A dedicated directory makes it trivial to point the shadcn CLI at canary/primitives/ for regeneration or updates, without risking overwrites of custom code.

  5. Atomic design alignment. Brad Frost explicitly acknowledges sub-atomic particles (design tokens). Primitives occupy a similar conceptual space — they’re the styled foundation that atoms are composed from. The analogy holds: tokens are to CSS what primitives are to React components.

  6. Pragmatism over dogma. The 2025 community consensus (Mykola Aleksandrov, Qt blog) explicitly warns against forcing every component into a rigid “chemistry box.” The model is a guide, not a religion. A primitives/ tier that serves a clear purpose (vendor isolation) is justified.

Option B (subdirectory) is a close second. The main disadvantage: atoms/primitives/sheet implies Sheet is a kind of atom. It’s technically true, but muddies the distinction between “vendor foundation” and “Arda-designed atom.” When a developer sees atoms/button/ and atoms/primitives/sheet/ side by side, the relationship isn’t immediately clear. Option A makes the separation explicit at the directory level.

Option C creates a maintenance burden. Without a directory boundary, developers must know which components are stock shadcn and which are custom. The shadcn CLI can’t safely regenerate components mixed with custom code. And the Storybook sidebar would show 14 undocumented primitives alongside documented Arda atoms.

src/components/canary/
primitives/ ← Stock shadcn (no stories, no tests) — 13 files
collapsible.tsx
dropdown-menu.tsx
input.tsx
label.tsx
separator.tsx
sheet.tsx
sidebar.tsx
skeleton.tsx
table.tsx
tabs.tsx
textarea.tsx
toggle.tsx
tooltip.tsx
atoms/ ← Arda first-class atoms (stories + tests)
avatar/ ← Promoted from ui/ (custom: size variants, badge, group)
badge/ ← Promoted from ui/ (custom: ghost/link variants)
button/ ← Promoted from ui/ (custom: icon sizes, OKLch)
card/ ← Promoted from ui/ (custom: CardAction)
dialog/ ← Promoted from ui/ (custom: showCloseButton)
drawer/ ← Existing callil atom (wraps primitives/sheet)
input-group/ ← Promoted from ui/ (entirely original)
brand-logo/ ← Existing callil atom
icon-button/ ← Existing callil atom
icon-label/ ← Existing callil atom
read-only-field/ ← Existing callil atom
search-input/ ← Existing callil atom
detail-field/ ← Existing jmpicnic atom
grid/ ← Existing jmpicnic atom group
molecules/
organisms/
// Primitives — internal use only, not exported from package
import { Sheet, SheetContent } from '@/components/canary/primitives/sheet';
// Atoms — public API, exported from canary.ts barrel
import { ArdaDrawer } from '@/components/canary/atoms/drawer';

This section maps every @/components/ui/* import in the callil organisms and molecules to its target location after consolidation (primitive or atom).

MarkerMeaning
primitives/Stock shadcn — moves to src/components/canary/primitives/
atoms/Custom component — moves to src/components/canary/atoms/

Current importExported namesTarget
@/components/ui/sidebarSidebarProvider, Sidebar, SidebarRailprimitives/sidebar
Current importExported namesTarget
@/components/ui/buttonButtonatoms/button
@/components/ui/separatorSeparatorprimitives/separator

(Also imports atoms/search-input and atoms/icon-button — already canary atoms, no change.)

No direct @/components/ui/* imports. Composes via canary atoms and molecules exclusively: atoms/drawer, atoms/button, molecules/item-details/*, molecules/field-list/*, molecules/action-toolbar.

Current importExported namesTarget
@/components/ui/inputInputprimitives/input

Current importExported namesTarget
@/components/ui/sidebarSidebarMenuItem, SidebarMenuButtonprimitives/sidebar

(Also imports atoms/badge — already a canary atom.)

Current importExported namesTarget
@/components/ui/avatarAvatar, AvatarFallback, AvatarImageatoms/avatar
@/components/ui/dropdown-menuDropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTriggerprimitives/dropdown-menu
@/components/ui/sidebarSidebarFooter, SidebarMenu, SidebarMenuItem, SidebarMenuButtonprimitives/sidebar
Current importExported namesTarget
@/components/ui/sidebarSidebarContent, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenuprimitives/sidebar
Current importExported namesTarget
@/components/ui/collapsibleCollapsible, CollapsibleContent, CollapsibleTriggerprimitives/collapsible
@/components/ui/sidebarSidebarMenuItem, SidebarMenuButton, SidebarMenuSubprimitives/sidebar
Current importExported namesTarget
@/components/ui/sidebarSidebarHeader, SidebarMenu, SidebarMenuItem, SidebarMenuButtonprimitives/sidebar
@/components/ui/dropdown-menuDropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTriggerprimitives/dropdown-menu

(Also imports atoms/brand-logo — already a canary atom.)

molecules/item-details/item-details-header.tsx

Section titled “molecules/item-details/item-details-header.tsx”
Current importExported namesTarget
@/components/ui/tabsTabs, TabsList, TabsTriggerprimitives/tabs
@/components/ui/dropdown-menuDropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTriggerprimitives/dropdown-menu

molecules/action-toolbar/action-toolbar.tsx

Section titled “molecules/action-toolbar/action-toolbar.tsx”
Current importExported namesTarget
@/components/ui/buttonButtonatoms/button
@/components/ui/dropdown-menuDropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTriggerprimitives/dropdown-menu

molecules/overflow-toolbar/overflow-toolbar.tsx

Section titled “molecules/overflow-toolbar/overflow-toolbar.tsx”
Current importExported namesTarget
@/components/ui/buttonButtonatoms/button
@/components/ui/dropdown-menuDropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTriggerprimitives/dropdown-menu

Summary — unique @/components/ui/* modules referenced across all organisms and molecules

Section titled “Summary — unique @/components/ui/* modules referenced across all organisms and molecules”
ui/ moduleTarget tierFiles that import it
sidebarprimitives/sidebarorganism/sidebar, mol/sidebar-nav-item, mol/sidebar-user-menu, mol/sidebar-nav, mol/sidebar-nav-group, mol/sidebar-header
dropdown-menuprimitives/dropdown-menumol/sidebar-user-menu, mol/sidebar-header, mol/item-details-header, mol/action-toolbar, mol/overflow-toolbar
buttonatoms/buttonorganism/app-header, mol/action-toolbar, mol/overflow-toolbar
separatorprimitives/separatororganism/app-header
inputprimitives/inputorganism/item-grid
tabsprimitives/tabsmol/item-details-header
collapsibleprimitives/collapsiblemol/sidebar-nav-group
avataratoms/avatarmol/sidebar-user-menu

The four organisms below exist in the callil branch (callil-consolidation-worktree) but have not yet been moved into the main clone’s src/components/canary/organisms/ directory.

OrganismCallil source path
ArdaSidebarorganisms/sidebar/sidebar.tsx
ArdaAppHeaderorganisms/app-header/app-header.tsx
ArdaItemDetailsorganisms/item-details/item-details.tsx
ItemGridorganisms/item-grid/item-grid.tsx

Each organism must be moved (not copied) as part of the consolidation work. The migration steps for each file are:

  1. Copy the file into src/components/canary/organisms/<name>/ in the main clone.
  2. Rewrite all @/components/ui/* imports to their target locations per the mapping table above:
    • @/components/ui/sidebar@/components/canary/primitives/sidebar
    • @/components/ui/dropdown-menu@/components/canary/primitives/dropdown-menu
    • @/components/ui/button@/components/canary/atoms/button
    • @/components/ui/separator@/components/canary/primitives/separator
    • @/components/ui/input@/components/canary/primitives/input
    • @/components/ui/tabs@/components/canary/primitives/tabs
    • @/components/ui/collapsible@/components/canary/primitives/collapsible
    • @/components/ui/avatar@/components/canary/atoms/avatar
  3. Rewrite relative imports of sibling canary atoms/molecules to the correct relative paths in the main clone’s directory tree.
  4. Add/update the barrel export in src/components/canary/organisms/index.ts.
  5. Verify TypeScript compiles (npx tsc --noEmit) before opening a PR.

The same import-rewrite obligation applies to all molecule files that accompany these organisms (the full molecules/sidebar/ set plus molecules/item-details/item-details-header.tsx, molecules/action-toolbar/action-toolbar.tsx, and molecules/overflow-toolbar/overflow-toolbar.tsx).