@pihanga2/shadcn 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/AGENT.building-cards.md +413 -0
  2. package/AGENT.md +57 -0
  3. package/AGENT.using-cards.md +1051 -0
  4. package/cards/box/box.component.js +2 -2
  5. package/cards/box/box.component.js.map +1 -1
  6. package/cards/box/box.types.d.ts +1 -4
  7. package/cards/box/box.types.js.map +1 -1
  8. package/cards/button/button.types.d.ts +3 -0
  9. package/cards/button/button.types.js +2 -2
  10. package/cards/button/button.types.js.map +1 -1
  11. package/cards/button/index.js +5 -5
  12. package/cards/chartGraph/chartGraph.component.d.ts +4 -0
  13. package/cards/chartGraph/chartGraph.types.d.ts +102 -0
  14. package/cards/chartGraph/index.d.ts +1 -0
  15. package/cards/conditional/conditional.component.d.ts +14 -3
  16. package/cards/conditional/conditional.component.js +17 -9
  17. package/cards/conditional/conditional.component.js.map +1 -1
  18. package/cards/conditional/conditional.types.d.ts +100 -8
  19. package/cards/conditional/conditional.types.js.map +1 -1
  20. package/cards/core-index.js +31 -31
  21. package/cards/fileDrop/fileDrop.component.d.ts +5 -0
  22. package/cards/fileDrop/fileDrop.types.d.ts +43 -0
  23. package/cards/fileDrop/index.d.ts +2 -0
  24. package/cards/graphin/contextMenu.component.d.ts +28 -0
  25. package/cards/graphin/eventDispatcher.component.d.ts +8 -0
  26. package/cards/graphin/graphin.component.d.ts +2 -2
  27. package/cards/graphin/graphin.playground-cards.d.ts +1 -0
  28. package/cards/graphin/graphin.types.d.ts +444 -4
  29. package/cards/graphin/graphinOpHandler.component.d.ts +8 -0
  30. package/cards/graphin/index.d.ts +1 -0
  31. package/cards/graphin/tooltip.component.d.ts +13 -1
  32. package/cards/icons.js +7 -3
  33. package/cards/icons.js.map +1 -1
  34. package/cards/index.d.ts +1 -0
  35. package/cards/slider/index.d.ts +1 -0
  36. package/cards/slider/slider.component.d.ts +4 -0
  37. package/cards/slider/slider.types.d.ts +102 -0
  38. package/cards/sliderValue/index.d.ts +1 -0
  39. package/cards/sliderValue/sliderValue.component.d.ts +15 -0
  40. package/cards/sliderValue/sliderValue.types.d.ts +21 -0
  41. package/cards/stack/stack.types.d.ts +19 -22
  42. package/cards/stack/stack.types.js +1 -1
  43. package/cards/stack/stack.types.js.map +1 -1
  44. package/cards/tabs/index.js +4 -4
  45. package/cards/tabs/tabs.component.js +51 -25
  46. package/cards/tabs/tabs.component.js.map +1 -1
  47. package/cards/tabs/tabs.types.d.ts +12 -0
  48. package/cards/tabs/tabs.types.js +2 -2
  49. package/cards/tabs/tabs.types.js.map +1 -1
  50. package/cards/types.d.ts +4 -0
  51. package/cards/types.js.map +1 -1
  52. package/components/hooks/use-breakpoint.d.ts +49 -0
  53. package/components/hooks/use-breakpoint.js +73 -0
  54. package/components/hooks/use-breakpoint.js.map +1 -0
  55. package/package.json +9 -1
  56. package/pihanga-shadcn.css +1 -1
  57. package/mountain-snow.svg +0 -4
  58. package/r/badge.json +0 -35
  59. package/r/box.json +0 -26
  60. package/r/button.json +0 -39
  61. package/r/checkbox.json +0 -31
  62. package/r/conditional.json +0 -30
  63. package/r/dataTable.json +0 -43
  64. package/r/dialog.json +0 -36
  65. package/r/dropDownMenu.json +0 -47
  66. package/r/field.json +0 -35
  67. package/r/flexGrid.json +0 -32
  68. package/r/form.json +0 -37
  69. package/r/framework.json +0 -28
  70. package/r/graphin.json +0 -42
  71. package/r/input.json +0 -35
  72. package/r/jsonViewer.json +0 -40
  73. package/r/list.json +0 -39
  74. package/r/loadingOverlay.json +0 -39
  75. package/r/loadingSkeleton.json +0 -30
  76. package/r/markdownViewer.json +0 -51
  77. package/r/menu.json +0 -34
  78. package/r/modeToggle.json +0 -36
  79. package/r/navbarSearch.json +0 -31
  80. package/r/pageWithNavbar.json +0 -46
  81. package/r/pasteTarget.json +0 -41
  82. package/r/pihanga-base.json +0 -11
  83. package/r/pihanga-cards-icons.json +0 -16
  84. package/r/pihanga-cards-types.json +0 -16
  85. package/r/pihanga-hook-use-is-touch-device.json +0 -15
  86. package/r/pihanga-lib-utils.json +0 -18
  87. package/r/pihanga-theme-provider.json +0 -25
  88. package/r/pihanga-ui-extras.json +0 -68
  89. package/r/registry.json +0 -171
  90. package/r/resizable.json +0 -35
  91. package/r/select.json +0 -35
  92. package/r/stack.json +0 -27
  93. package/r/stepper.json +0 -39
  94. package/r/switch.json +0 -36
  95. package/r/tabs.json +0 -33
  96. package/r/textField.json +0 -32
  97. package/r/toast.json +0 -32
  98. package/r/toggleGroup.json +0 -35
  99. package/r/typography.json +0 -31
@@ -0,0 +1,1051 @@
1
+ # AGENT.using-cards.md — consuming pihanga-shadcn cards
2
+
3
+ > **Scope:** installing, wiring, composing, and navigating with existing cards.
4
+ > Read [`AGENT.md`](./AGENT.md) first for orientation and universal rules.
5
+ > If you need to *create* a new card type, switch to
6
+ > [`AGENT.building-cards.md`](./AGENT.building-cards.md).
7
+ >
8
+ > **Starting from scratch?** Follow [`AGENTS.getting-started.md`](./AGENTS.getting-started.md)
9
+ > first to scaffold your Vite project, run `shadcn init`, install `@pihanga2/core`,
10
+ > and wire up the initial file layout. Return here once the scaffold is in place.
11
+
12
+ > 💬 **Encountered a bug, a missing card, or an agent-unfriendly doc section?**
13
+ > Please open an issue at **https://github.com/ivcap-works/pihanga-shadcn/issues**
14
+ > — card suggestions and AI agent experience reports are especially welcome.
15
+
16
+ ---
17
+
18
+ > ### ⚠️ Check before you code — always search for an existing card first
19
+ >
20
+ > Before writing a new card component, **thoroughly check whether a suitable
21
+ > card already exists** in both the registry (34 cards) and the npm package
22
+ > (30 cards). Use the [Available cards](#available-cards) table below, and
23
+ > look carefully at non-obvious names — for example, the controlled text input
24
+ > is `pi/input` (`PiInput`), not `pi/text-input`; the graph visualiser is
25
+ > `shad/graphin`; layout cards include `pi/flex-grid`, `pi/stack`, and
26
+ > `pi/resizable`.
27
+ >
28
+ > **Why this matters:** writing a new component is quick, but a locally-owned
29
+ > card creates an ongoing maintenance burden — it must be kept in sync with
30
+ > shadcn/ui and Radix UI updates, it won't appear in the playground
31
+ > automatically, and it won't benefit from upstream bug fixes.
32
+ >
33
+ > **If no suitable card exists:**
34
+ > - Confirm the need is genuine and not already covered by composing existing
35
+ > cards (`Conditional`, `FlexGrid`, `Stack`, `Box`).
36
+ > - Unless the card would expose confidential business logic, please
37
+ > **open an issue** at
38
+ > `https://github.com/ivcap-works/pihanga-shadcn/issues` describing what you
39
+ > need. This helps the maintainers prioritise new cards and prevents the
40
+ > same gap from being worked around independently by multiple teams.
41
+ > - Only proceed to implement a local card after the above checks.
42
+
43
+ ---
44
+
45
+ ## Table of Contents
46
+
47
+ - [Distribution channels — choose one](#distribution-channels--choose-one)
48
+ - [Channel 1 — shadcn registry (one-time project setup)](#channel-1--shadcn-registry-one-time-project-setup)
49
+ - [Channel 2 — npm package `@pihanga2/shadcn`](#channel-2--npm-package-pihanga2shadcn)
50
+ - [Migrating a card from npm to a local copy](#migrating-a-card-from-npm-to-a-local-customised-copy)
51
+ - [Vite configuration (both channels)](#vite-configuration-both-channels)
52
+ - [`@pihanga2/cards` — deprecated, do not use](#pihanga2cards--deprecated-do-not-use)
53
+ - [Type-only import gotcha](#type-only-import-gotcha--picardref-store-etc)
54
+ - [Transitive card dependencies](#transitive-card-dependencies)
55
+ - [Adding individual cards](#adding-individual-cards)
56
+ - [Notes for AI agents](#notes-for-ai-agents)
57
+ - [Using cards in your app](#using-cards-in-your-app)
58
+ - [Version pinning](#version-pinning)
59
+ - [Bootstrapping a pihanga app (init pattern)](#bootstrapping-a-pihanga-app-init-pattern)
60
+ - [`memo()` — reactive state-driven props](#memo--reactive-state-driven-props)
61
+ - [Multi-page navigation with `PageWithNavbar`](#multi-page-navigation-with-pagewithnavbar)
62
+ - [`MarkdownViewer` — inline source vs. fetched path](#markdownviewer--inline-source-vs-fetched-path)
63
+ - [`registerFramework` — only one active at a time](#registerframework--only-one-active-at-a-time)
64
+ - [Card API quick reference — common naming gotchas](#card-api-quick-reference--common-naming-gotchas)
65
+ - [`pi/button` — theming the `brand` variant](#pibutton--theming-the-brand-variant)
66
+ - [Known gaps identified during AI agent evaluations](#known-gaps-identified-during-ai-agent-evaluations)
67
+
68
+ ---
69
+
70
+ ## Distribution channels — choose one
71
+
72
+ | | Registry | npm package |
73
+ |---|---|---|
74
+ | **Install** | `npx shadcn@latest add <url>` (or `yarn dlx shadcn@latest add <url>`) | `npm install @pihanga2/shadcn` |
75
+ | **Cards available** | All 34 | 30 core (no graphin / jsonViewer / markdownViewer / resizable) |
76
+ | **Requires `shadcn init`** | Yes | No |
77
+ | **Files land in your project** | Yes — editable source | No — compiled bundle |
78
+ | **Tailwind** | Consumer's own Tailwind config | Add `@source` pointing at `node_modules/@pihanga2/shadcn/dist-lib` |
79
+
80
+ Use the **registry** for projects already on shadcn/ui or when you want to
81
+ customise card source. Use the **npm package** for monorepos, CI, or anywhere
82
+ a clean `npm install` workflow is preferred.
83
+
84
+ ---
85
+
86
+ ## Channel 1 — shadcn registry (one-time project setup)
87
+
88
+ > **New project?** The full one-time setup (Vite scaffold, shadcn init, Tailwind
89
+ > theme, Vite aliases, pihanga-core install) is documented step-by-step in
90
+ > **[`AGENTS.getting-started.md`](./AGENTS.getting-started.md)**. Follow that
91
+ > guide first, then return here to install and wire individual cards.
92
+
93
+ Key things the setup guide covers:
94
+
95
+ - **⚠️ npx + npm 11 + Node 24 incompatibility** — use `nvm use 22` or `yarn dlx`
96
+ as a drop-in (see getting-started § *npx compatibility*)
97
+ - `npx shadcn@latest init` / `yarn dlx shadcn@latest init` — creates
98
+ `components.json`, patches `tsconfig.json` with `@/*` alias, installs Tailwind
99
+ - `src/index.css` — requires a full `@theme inline` block; without it `bg-card`,
100
+ `bg-background` etc. render transparent and dialogs appear invisible
101
+ - `src/components/lib/utils.ts` — must create the `cn()` helper at this path
102
+ - `vite.config.ts` aliases — `@/lib`, `@/registry`, `@/components`, `@/cards`, `@`
103
+
104
+ ---
105
+
106
+ ## Channel 2 — npm package `@pihanga2/shadcn`
107
+
108
+ No shadcn CLI or `components.json` required.
109
+
110
+ ```sh
111
+ npm install @pihanga2/shadcn
112
+ # or: yarn add @pihanga2/shadcn / pnpm add @pihanga2/shadcn
113
+ ```
114
+
115
+ Activate cards in the app entry point:
116
+
117
+ ```ts
118
+ // Activate all 30 core cards at once (side-effecting import)
119
+ import "@pihanga2/shadcn";
120
+
121
+ // OR activate only what you need (tree-shakeable):
122
+ import "@pihanga2/shadcn/cards/button";
123
+ import "@pihanga2/shadcn/cards/form";
124
+ import "@pihanga2/shadcn/cards/framework";
125
+ ```
126
+
127
+ Each import calls `registerCardComponent(...)` — no further activation needed.
128
+ Then use `registerCard(...)` in your init function exactly as with the registry.
129
+
130
+ **Cards NOT in the npm package** (registry-only due to heavy deps):
131
+ `graphin`, `jsonViewer`, `markdownViewer`, `resizable`.
132
+
133
+ **Tailwind:** add `@source` to your CSS so Tailwind can scan the package:
134
+ ```css
135
+ /* src/index.css — Tailwind v4 */
136
+ @import "tailwindcss";
137
+ @source "../../node_modules/@pihanga2/shadcn/dist-lib";
138
+ ```
139
+
140
+ ### Migrating a card from npm to a local customised copy
141
+
142
+ When you need to modify a card that came from the npm package (custom styling,
143
+ extra props, different behaviour), follow this three-step pattern:
144
+
145
+ **Step 1 — Switch from the all-at-once import to per-card imports**
146
+
147
+ ```ts
148
+ // Before — no control over individual cards:
149
+ import "@pihanga2/shadcn";
150
+
151
+ // After — omit the card you want to customise:
152
+ import "@pihanga2/shadcn/cards/badge";
153
+ import "@pihanga2/shadcn/cards/form";
154
+ // import "@pihanga2/shadcn/cards/button"; ← omit
155
+ import "@pihanga2/shadcn/cards/dialog";
156
+ // … rest of your cards
157
+ ```
158
+
159
+ **Step 2 — Install the card from the registry (copies source into your project)**
160
+
161
+ ```sh
162
+ npx shadcn@latest add https://ivcap-works.github.io/pihanga-shadcn/r/button.json
163
+ # → creates src/cards/button/ and installs its npm deps
164
+ ```
165
+
166
+ **Step 3 — Import your local copy**
167
+
168
+ ```ts
169
+ import "./cards/button"; // registers with the SAME card ID as the npm version
170
+ ```
171
+
172
+ The local `index.ts` calls `registerCardComponent({name: "shad/button", …})` —
173
+ the **same card ID** as the npm build — so all existing `registerCard()` calls
174
+ in your app continue to work with zero changes.
175
+
176
+ > **No import-order tricks required.** There is exactly one registration as
177
+ > long as you omitted the npm sub-path import (step 1) and added the local
178
+ > import (step 3).
179
+
180
+ **Alternative — keep both and rename the local card**
181
+
182
+ If you want the npm version and your custom version to coexist side-by-side,
183
+ give the local card a distinct ID:
184
+
185
+ ```ts
186
+ // src/cards/button/button.types.ts
187
+ // Before: export const BUTTON_CARD = "shad/button";
188
+ export const BUTTON_CARD = "myapp/custom-button"; // ← unique ID
189
+ ```
190
+
191
+ Update every `registerCard` call that should use your version. Calls that
192
+ reference `"shad/button"` still resolve to the npm build; calls referencing
193
+ `"myapp/custom-button"` resolve to your local copy. No ambiguity, no
194
+ import-order dependency.
195
+
196
+ ---
197
+
198
+ ## Vite configuration (both channels)
199
+
200
+ > **Vite project setup** (aliases, `src/components/lib/utils.ts`, `src/index.css`)
201
+ > is documented in full in **[`AGENTS.getting-started.md`](./AGENTS.getting-started.md)
202
+ > — Step 5 & Step 2**. The sub-sections below cover **runtime-only gotchas** that
203
+ > apply regardless of when the project was scaffolded.
204
+
205
+ ### `@pihanga2/cards` — deprecated, do not use
206
+
207
+ > ⚠️ **`@pihanga2/cards` is deprecated and should no longer be used.**
208
+ >
209
+ > All types previously imported from that package (`BoxProps`, `StackProps`, etc.)
210
+ > have been migrated into each card's own local `*.types.ts` file within this
211
+ > library (`src/cards/box/box.types.ts`, `src/cards/stack/stack.types.ts`, …).
212
+ >
213
+ > **Do not install `@pihanga2/cards` and do not add `import … from "@pihanga2/cards"`
214
+ > anywhere in this codebase or in consumer projects.**
215
+
216
+ ### Type-only import gotcha — `PiCardRef`, `Store`, etc.
217
+
218
+ > ⚠️ **This is the most common runtime crash when bootstrapping from scratch.**
219
+
220
+ Several symbols exported by `@pihanga2/core` and `@reduxjs/toolkit` are
221
+ **TypeScript types with no runtime JS value** — they live only in `.d.ts` files.
222
+ If you import them without the `type` keyword, Vite/esbuild compiles them without
223
+ error, but the **browser crashes** at runtime with:
224
+
225
+ ```
226
+ SyntaxError: The requested module '@pihanga2_core.js' does not
227
+ provide an export named 'PiCardRef'
228
+ ```
229
+
230
+ **Always use `import type` for these symbols:**
231
+
232
+ ```ts
233
+ // ❌ Crashes at runtime — esbuild does not enforce verbatimModuleSyntax
234
+ import {PiCardRef} from "@pihanga2/core";
235
+ import {Store} from "@reduxjs/toolkit";
236
+
237
+ // ✅ Correct — type imports are erased before the browser sees the bundle
238
+ import type {PiCardRef} from "@pihanga2/core";
239
+ import type {Store} from "@reduxjs/toolkit";
240
+
241
+ // ✅ Also correct — inline type modifier
242
+ import {type PiCardRef, createCardDeclaration} from "@pihanga2/core";
243
+ ```
244
+
245
+ Common type-only symbols in `@pihanga2/core`: `PiCardRef`, `ReduxState`,
246
+ `WindowProps`, `PiCardDef`, `PiMapProps`.
247
+
248
+ Common type-only symbols in `@reduxjs/toolkit`: `Store`.
249
+
250
+ ### Transitive card dependencies
251
+
252
+ Some cards import internal helpers from *other* cards. When you add a card to
253
+ `src/cards/` you must also add all its transitive dependencies — even if you
254
+ never reference those cards in `app.pihanga.ts`.
255
+
256
+ | Card you add | Also requires (import in `main.ts`) |
257
+ |---|---|
258
+ | `button` | `dropDownMenu` (imports `dropdown-context`) |
259
+ | `pageWithNavbar` | `modeToggle`, `navbarSearch`, `toast` |
260
+
261
+ Example `main.ts` import block when using `pageWithNavbar` and `button`:
262
+
263
+ ```ts
264
+ // src/main.ts
265
+ import "@/cards/framework";
266
+ import "@/cards/pageWithNavbar";
267
+ import "@/cards/modeToggle"; // required by pageWithNavbar
268
+ import "@/cards/navbarSearch"; // required by pageWithNavbar
269
+ import "@/cards/toast"; // required by pageWithNavbar
270
+ import "@/cards/button";
271
+ import "@/cards/dropDownMenu"; // required by button
272
+ import "@/cards/stack";
273
+ import "@/cards/typography";
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Adding individual cards
279
+
280
+ ### Registry channel
281
+
282
+ After the one-time setup (Channel 1 prerequisites above), add any card with a
283
+ single command. `@pihanga2/core` and all card-specific npm packages are
284
+ installed automatically.
285
+
286
+ > ⚠️ **npm 11 + Node 24:** `npx` fails on this combination — use Node 22 via
287
+ > `nvm use 22`, or use `yarn dlx` instead (see the
288
+ > [compatibility note above](#channel-1--shadcn-registry-prerequisites)).
289
+
290
+ ```sh
291
+ # npm / npx (Node 22 recommended)
292
+ npx shadcn@latest add https://ivcap-works.github.io/pihanga-shadcn/r/button.json
293
+
294
+ # yarn (works on all supported Node versions)
295
+ yarn dlx shadcn@latest add https://ivcap-works.github.io/pihanga-shadcn/r/button.json
296
+
297
+ # Add multiple cards at once (npx)
298
+ npx shadcn@latest add \
299
+ https://ivcap-works.github.io/pihanga-shadcn/r/button.json \
300
+ https://ivcap-works.github.io/pihanga-shadcn/r/form.json \
301
+ https://ivcap-works.github.io/pihanga-shadcn/r/dataTable.json
302
+
303
+ # Add multiple cards at once (yarn dlx)
304
+ yarn dlx shadcn@latest add \
305
+ https://ivcap-works.github.io/pihanga-shadcn/r/button.json \
306
+ https://ivcap-works.github.io/pihanga-shadcn/r/form.json \
307
+ https://ivcap-works.github.io/pihanga-shadcn/r/dataTable.json
308
+ ```
309
+
310
+ ### npm channel
311
+
312
+ ```ts
313
+ import "@pihanga2/shadcn/cards/button"; // activates shad/button
314
+ import "@pihanga2/shadcn/cards/form"; // activates pi/form
315
+ ```
316
+
317
+ Or activate everything at once: `import "@pihanga2/shadcn"`.
318
+
319
+ ### Available cards
320
+
321
+ | Card | Registry URL | In npm pkg | Notes |
322
+ |------|-------------|---|---|
323
+ | badge | `/r/badge` | ✅ | |
324
+ | box | `/r/box` | ✅ | |
325
+ | button | `/r/button` | ✅ | |
326
+ | checkbox | `/r/checkbox` | ✅ | |
327
+ | conditional | `/r/conditional` | ✅ | |
328
+ | dataTable | `/r/dataTable` | ✅ | |
329
+ | dialog | `/r/dialog` | ✅ | |
330
+ | dropDownMenu | `/r/dropDownMenu` | ✅ | |
331
+ | field | `/r/field` | ✅ | |
332
+ | flexGrid | `/r/flexGrid` | ✅ | |
333
+ | form | `/r/form` | ✅ | |
334
+ | framework | `/r/framework` | ✅ | App root |
335
+ | graphin | `/r/graphin` | ✗ | ⚠️ Heavy AntV deps |
336
+ | input | `/r/input` | ✅ | |
337
+ | jsonViewer | `/r/jsonViewer` | ✗ | optional viewer |
338
+ | list | `/r/list` | ✅ | |
339
+ | loadingOverlay | `/r/loadingOverlay` | ✅ | |
340
+ | loadingSkeleton | `/r/loadingSkeleton` | ✅ | |
341
+ | markdownViewer | `/r/markdownViewer` | ✗ | ⚠️ Heavy markdown deps |
342
+ | menu | `/r/menu` | ✅ | |
343
+ | modeToggle | `/r/modeToggle` | ✅ | |
344
+ | navbarSearch | `/r/navbarSearch` | ✅ | |
345
+ | pageWithNavbar | `/r/pageWithNavbar` | ✅ | |
346
+ | pasteTarget | `/r/pasteTarget` | ✅ | |
347
+ | resizable | `/r/resizable` | ✗ | optional layout |
348
+ | select | `/r/select` | ✅ | |
349
+ | stack | `/r/stack` | ✅ | |
350
+ | stepper | `/r/stepper` | ✅ | |
351
+ | switch | `/r/switch` | ✅ | |
352
+ | tabs | `/r/tabs` | ✅ | |
353
+ | textField | `/r/textField` | ✅ | |
354
+ | toast | `/r/toast` | ✅ | |
355
+ | toggleGroup | `/r/toggleGroup` | ✅ | |
356
+ | typography | `/r/typography` | ✅ | |
357
+
358
+ Full registry index: `https://ivcap-works.github.io/pihanga-shadcn/r/registry.json`
359
+
360
+ ---
361
+
362
+ ## Notes for AI agents
363
+
364
+ **Registry channel:**
365
+ - `npx shadcn@latest add <url>` automatically installs `@pihanga2/core` and all
366
+ card-specific npm packages — no separate `npm install` needed.
367
+ - **`npx` fails on npm 11 + Node 24** — use `nvm use 22` or replace every
368
+ `npx shadcn@latest` with `yarn dlx shadcn@latest` (identical output).
369
+ - Cards land at `src/cards/<card-name>/` in the consumer's project.
370
+ - The `framework` card is the Pihanga app root — add it first for new apps.
371
+ - `graphin` has heavy AntV dependencies (~5 MB) — only add if graph
372
+ visualisation is explicitly required.
373
+
374
+ **npm channel:**
375
+ - `npm install @pihanga2/shadcn` is a single command — no `shadcn init` needed.
376
+ - Activate cards with `import "@pihanga2/shadcn"` (all) or per-card sub-paths.
377
+ - `graphin`, `jsonViewer`, `markdownViewer`, `resizable` are not in the npm
378
+ package — use the registry for those.
379
+ - Point Tailwind at `node_modules/@pihanga2/shadcn/dist-lib` to scan classes.
380
+
381
+ **Both channels:**
382
+ - After activation, app-wiring APIs (`registerCard`, `registerFramework`,
383
+ `register`, `memo`) are identical.
384
+ - `@pihanga2/core` must be importable — it is installed automatically by either
385
+ channel (`peerDependency` for npm; auto-installed by shadcn CLI for registry).
386
+
387
+ ---
388
+
389
+ ## Using cards in your app
390
+
391
+ After installation, each card is registered automatically when imported.
392
+ Bootstrap Pihanga in your app entry point:
393
+
394
+ ```ts
395
+ // ── Registry channel (cards copied to src/cards/) ────────────────────────
396
+ // src/main.ts
397
+ import "@pihanga2/core";
398
+ import "./cards/button"; // activates shad/button
399
+ import "./cards/form"; // activates pi/form
400
+ ```
401
+
402
+ ```ts
403
+ // ── npm channel ───────────────────────────────────────────────────────────
404
+ // src/main.ts
405
+ import "@pihanga2/shadcn"; // all 30 core cards at once
406
+ // OR selectively:
407
+ import "@pihanga2/shadcn/cards/button";
408
+ import "@pihanga2/shadcn/cards/form";
409
+ ```
410
+
411
+ Then wire up cards identically regardless of channel:
412
+
413
+ ```ts
414
+ // src/app.pihanga.ts
415
+ import {Button} from "@/cards/button"; // registry: local path
416
+ // import {Button} from "@pihanga2/shadcn/cards/button"; // npm: package path
417
+
418
+ registerCard("myApp/save", Button({
419
+ id: "save",
420
+ label: "Save",
421
+ opts: {variant: "default"},
422
+ }));
423
+ ```
424
+
425
+ ---
426
+
427
+ ## Version pinning
428
+
429
+ Use a git tag in the URL to pin to a specific release:
430
+
431
+ ```sh
432
+ npx shadcn@latest add \
433
+ https://raw.githubusercontent.com/ivcap-works/pihanga-shadcn/v1.0.0/public/r/button.json
434
+ ```
435
+
436
+ Or with GitHub Pages versioned sub-paths (once published):
437
+
438
+ ```sh
439
+ npx shadcn@latest add \
440
+ https://ivcap-works.github.io/pihanga-shadcn/v1.0.0/r/button
441
+ ```
442
+
443
+ ---
444
+
445
+ ## Bootstrapping a pihanga app (init pattern)
446
+
447
+ All app configuration lives in an `*init*` function (conventionally `appPiInit`)
448
+ that is called by `start()` in `src/main.ts`. The function calls three core APIs:
449
+
450
+ | API | Purpose |
451
+ |-----|---------|
452
+ | `registerFramework(card)` | Sets the single root framework card. Call **once**. |
453
+ | `registerCard(id, card)` | Registers a named card so other cards can reference it by id. |
454
+ | `register(r => { … })` | Registers global event handlers / reducers. |
455
+
456
+ ```ts
457
+ // src/main.ts
458
+ import {start, DEFAULT_REDUX_STATE} from "@pihanga2/core";
459
+ import {appPiInit} from "./app.pihanga";
460
+
461
+ start({...DEFAULT_REDUX_STATE}, [appPiInit], {
462
+ rootComponent: RootComponent,
463
+ });
464
+ ```
465
+
466
+ ```ts
467
+ // src/app.pihanga.ts
468
+ import {registerFramework, registerCard, register} from "@pihanga2/core";
469
+ import {SdFramework} from "./cards/framework";
470
+
471
+ export function appPiInit(): void {
472
+ registerFramework(SdFramework({page: "app/main", theme: "light"}));
473
+ registerCard("app/main", /* … card def … */);
474
+ }
475
+ ```
476
+
477
+ ---
478
+
479
+ ## `memo()` — reactive state-driven props
480
+
481
+ `memo(selector, mapper)` makes any card prop reactive. The selector extracts a
482
+ slice of state; the mapper converts that slice into the final prop value.
483
+ Pihanga re-renders only when the selector's return value changes (shallow equal).
484
+
485
+ ```ts
486
+ import {memo} from "@pihanga2/core";
487
+ import type {AppState} from "@/app.state";
488
+
489
+ // Switch the active card dynamically based on state.currentPage
490
+ main: memo(
491
+ (s: AppState) => s.currentPage ?? "home",
492
+ (page) => `app/page/${page}`,
493
+ ),
494
+ ```
495
+
496
+ `memo` can also produce arrays, objects, or any serialisable value:
497
+
498
+ ```ts
499
+ items: memo(
500
+ (s: AppState) => s.selectedId,
501
+ (selectedId) => myList.map(item => ({...item, isSelected: item.id === selectedId})),
502
+ ),
503
+ ```
504
+
505
+ ---
506
+
507
+ ## Multi-page navigation with `PageWithNavbar`
508
+
509
+ The standard two-page (or N-page) pattern:
510
+
511
+ 1. Add `navLinks` to `PageWithNavbar`.
512
+ 2. Register an `onPageWithNavbarNavigateTo` handler that stores the clicked id
513
+ in state.
514
+ 3. Pass a `memo`-driven string to `main` so the rendered card changes with state.
515
+
516
+ ```ts
517
+ import {
518
+ PageWithNavbar,
519
+ onPageWithNavbarNavigateTo,
520
+ } from "@/cards/pageWithNavbar";
521
+ import {memo, register, registerCard, registerFramework} from "@pihanga2/core";
522
+ import {SdFramework} from "@/cards/framework";
523
+ import type {AppState} from "@/app.state";
524
+
525
+ export function appPiInit(): void {
526
+ registerFramework(SdFramework({page: "app/main", theme: "light"}));
527
+
528
+ register((r) => {
529
+ onPageWithNavbarNavigateTo(r, (state: AppState, {id}) => {
530
+ state.currentPage = id; // store active page in state
531
+ });
532
+ });
533
+
534
+ registerCard("app/main", PageWithNavbar({
535
+ title: "My App",
536
+ navLinks: [
537
+ {id: "home", title: "Home"},
538
+ {id: "settings", title: "Settings"},
539
+ ],
540
+ main: memo(
541
+ (s: AppState) => s.currentPage ?? "home",
542
+ (page) => `app/page/${page}`, // resolves to "app/page/home" etc.
543
+ ),
544
+ }));
545
+
546
+ registerCard("app/page/home", /* … */);
547
+ registerCard("app/page/settings", /* … */);
548
+ }
549
+ ```
550
+
551
+ Add `currentPage?: string` to your `AppState` type:
552
+
553
+ ```ts
554
+ // src/app.state.ts
555
+ export type AppState = ReduxState & {
556
+ currentPage?: string;
557
+ // … other fields
558
+ };
559
+ ```
560
+
561
+ ---
562
+
563
+ ## `MarkdownViewer` — inline source vs. fetched path
564
+
565
+ The `markdownViewer` card accepts either an inline string or a URL:
566
+
567
+ ```ts
568
+ // Inline markdown string
569
+ MarkdownViewer({source: "# Hello\nSome **markdown**."})
570
+
571
+ // Fetch from a URL (file must be accessible via HTTP)
572
+ MarkdownViewer({path: "/AGENT.md"})
573
+ ```
574
+
575
+ When using `path`, the file must be reachable from the browser at that URL.
576
+ The recommended approach for project-root files (e.g. `AGENT.md`) is a small
577
+ **inline Vite plugin** in `vite.config.ts` — this avoids a stale copy in
578
+ `public/` and keeps a single source of truth:
579
+
580
+ ```ts
581
+ // vite.config.ts
582
+ import {readFileSync} from "fs";
583
+ import type {Plugin} from "vite";
584
+
585
+ function rootFilePlugin(filenames: string[]): Plugin {
586
+ return {
587
+ name: "root-file-serve",
588
+ // Dev: serve the file directly from the project root
589
+ configureServer(server) {
590
+ server.middlewares.use((req, res, next) => {
591
+ const name = filenames.find((f) => req.url === `/${f}`);
592
+ if (name) {
593
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
594
+ res.end(readFileSync(path.resolve(__dirname, name), "utf-8"));
595
+ return;
596
+ }
597
+ next();
598
+ });
599
+ },
600
+ // Build: emit the file into dist/ via Rollup
601
+ generateBundle() {
602
+ for (const name of filenames) {
603
+ this.emitFile({
604
+ type: "asset",
605
+ fileName: name,
606
+ source: readFileSync(path.resolve(__dirname, name), "utf-8"),
607
+ });
608
+ }
609
+ },
610
+ };
611
+ }
612
+
613
+ export default defineConfig({
614
+ plugins: [react(), tailwindcss(), rootFilePlugin(["AGENT.md"])],
615
+ // …
616
+ });
617
+ ```
618
+
619
+ Do **not** copy the file to `public/` manually — the plugin handles both dev
620
+ and production in one place.
621
+
622
+ ---
623
+
624
+ ## `registerFramework` — only one active at a time
625
+
626
+ Only one `registerFramework()` call may be active in a given app boot. If you
627
+ compose multiple init functions (e.g. `appPiInit` calls `playgroundPiInit`),
628
+ ensure that only **one** of them calls `registerFramework`.
629
+
630
+ Remove or guard any `registerFramework` call in sub-inits before composing them:
631
+
632
+ ```ts
633
+ // ❌ Both call registerFramework — second one silently wins (or errors)
634
+ const inits = [appPiInit, playgroundPiInit];
635
+
636
+ // ✅ appPiInit calls playgroundPiInit() internally after removing its
637
+ // registerFramework call from playgroundPiInit.
638
+ export function appPiInit(): void {
639
+ playgroundPiInit(); // no longer calls registerFramework
640
+ registerFramework(SdFramework({page: "app/main", theme: "light"}));
641
+ // …
642
+ }
643
+ ```
644
+
645
+ ---
646
+
647
+ ## Card API quick reference — common naming gotchas
648
+
649
+ Several cards have prop or export names that differ from what you might intuit.
650
+ This table is a quick-lookup to avoid "card not found" or type-error surprises.
651
+
652
+ ### `pi/button` — theming the `brand` variant
653
+
654
+ `variant="brand"` is intended for a visually prominent call-to-action button
655
+ that carries your app's brand colour. Its out-of-the-box default is the
656
+ primary colour family — an intentional baseline; unlike other variants, it is
657
+ designed to be **rethemed with CSS only**, without touching any TypeScript.
658
+
659
+ The button's appearance is driven by three CSS tokens declared in the
660
+ `@theme inline` block of `src/index.css`:
661
+
662
+ | Token | Default | Controls |
663
+ |---|---|---|
664
+ | `--color-btn-brand` | `var(--primary)` | Background colour |
665
+ | `--color-btn-brand-foreground` | `var(--primary-foreground)` | Text / icon colour |
666
+ | `--radius-btn-brand` | `var(--radius-md)` | Border radius |
667
+
668
+ To apply your app's brand colour, add an `@theme inline` override **after**
669
+ the registry tokens in your `src/index.css`. Tailwind v4 processes later
670
+ `@theme inline` blocks last, so your values silently win:
671
+
672
+ ```css
673
+ /* src/index.css — app-level override */
674
+ @theme inline {
675
+ --color-btn-brand: oklch(0.78 0.18 85); /* your brand colour */
676
+ --color-btn-brand-foreground: oklch(0.15 0 0); /* high-contrast foreground */
677
+ --radius-btn-brand: 9999px; /* pill shape */
678
+ }
679
+ ```
680
+
681
+ Usage in `app.pihanga.ts` is unchanged:
682
+
683
+ ```ts
684
+ import {Button} from "@/cards/button";
685
+
686
+ registerCard("myApp/cta", Button({
687
+ label: "Get started",
688
+ opts: {variant: "brand"},
689
+ }));
690
+ ```
691
+
692
+ > **Why token indirection?** Hard-coding a brand colour directly into the CVA
693
+ > string would couple every consumer of the registry to a specific palette.
694
+ > Routing through `--color-btn-brand` means any app can retheme the `brand`
695
+ > variant in CSS alone. The pattern can be extended to any other variant that
696
+ > needs per-app theming.
697
+
698
+ ### `pi/button` — rendering as an anchor / link
699
+
700
+ > ⚠️ **Do NOT write a separate local link-button card.** The `pi/button` card
701
+ > already renders as an `<a>` element when `href` is provided.
702
+
703
+ Pass `href` (and optionally `target`) directly to `Button(…)`:
704
+
705
+ ```ts
706
+ registerCard("myApp/docsLink", Button({
707
+ label: "Documentation",
708
+ href: "https://example.com/docs",
709
+ target: "_blank", // open in new tab
710
+ opts: {variant: "outline"},
711
+ }));
712
+ ```
713
+
714
+ When `href` is set the card renders an `<a>` tag styled identically to the
715
+ `<button>` variant. The `onClicked` event still fires (via `e.preventDefault()`
716
+ internally), so Pihanga event handlers work as usual if you also need to react
717
+ to the click in Redux.
718
+
719
+ | Prop | Type | Purpose |
720
+ |---|---|---|
721
+ | `href` | `string` | Destination URL; presence switches element to `<a>` |
722
+ | `target` | `string` | e.g. `"_blank"` for new tab; passed straight to `<a target>` |
723
+
724
+ ### `shad/tabs` — import `SdTabs`, not `Tabs`
725
+
726
+ ```ts
727
+ // ✅ Correct
728
+ import {SdTabs, onTabsTabChanged} from "@/cards/tabs";
729
+ import type {TabsProps} from "@/cards/tabs";
730
+
731
+ registerCard("myApp/tabs", SdTabs({
732
+ value: memo((s: AppState) => s.activeTab), // ← "value", NOT "activeTab"
733
+ tabs: [
734
+ {id: "a", title: "Panel A", contentCard: "myApp/panelA"}, // ← "title" + "contentCard"
735
+ {id: "b", title: "Panel B", contentCard: "myApp/panelB"},
736
+ ],
737
+ }));
738
+ ```
739
+
740
+ | What you might write | Actual prop / export |
741
+ |---|---|
742
+ | `Tabs({…})` | `SdTabs({…})` |
743
+ | `activeTab: …` | `value: …` |
744
+ | `tab.label` | `tab.title` |
745
+ | `tab.content` | `tab.contentCard` |
746
+
747
+ The `shad/tabs` card also supports `selfManaged: true` for cases where you do
748
+ **not** want to store the active tab in Redux — the component manages its own
749
+ state internally, but still dispatches `onTabChanged` so reducers can observe.
750
+
751
+ ```ts
752
+ // Self-managed — no reducer needed:
753
+ registerCard("myApp/tabs", SdTabs({
754
+ selfManaged: true,
755
+ tabs: [{id: "a", title: "A", contentCard: "myApp/panelA"}],
756
+ }));
757
+ ```
758
+
759
+ ### `shad/tabs` — `tab.title` strings are always rendered as text, not card IDs
760
+
761
+ When `tab.title` is a plain `string`, it is **always rendered as literal text**
762
+ on the tab trigger — even if the string happens to be a registered card name.
763
+ To mount a card inside the tab trigger, use an **object-form card declaration**:
764
+
765
+ ```ts
766
+ import {SdBadge} from "@/cards/badge";
767
+
768
+ registerCard("myApp/tabs", SdTabs({
769
+ tabs: [
770
+ // ✅ String label — rendered as tab text
771
+ {id: "home", title: "Home", contentCard: "myApp/home"},
772
+
773
+ // ✅ Object card declaration — card is mounted inside the tab trigger
774
+ {id: "alerts", title: SdBadge({label: "3", variant: "destructive"}),
775
+ contentCard: "myApp/alerts"},
776
+
777
+ // ❌ String card ID — NOT mounted; "myApp/badgeCard" appears as literal text
778
+ {id: "settings", title: "myApp/badgeCard", contentCard: "myApp/settings"},
779
+ ],
780
+ }));
781
+ ```
782
+
783
+ **Why?** The component uses `typeof tab.title !== "string"` to distinguish plain
784
+ labels from object card declarations. `isCardRef()` from `@pihanga2/core` does
785
+ not recognise plain-string card IDs — only object-form `PiCardDef` values — so
786
+ the `typeof` guard is the correct approach. A string `title` is therefore
787
+ unambiguously a label, regardless of whether it looks like a card path.
788
+
789
+ ### `pi/input` — the labeled, **controlled** standalone text input
790
+
791
+ **`pi/input`** in this library is a fully-controlled labeled text input that
792
+ can bind to Redux state, mask passwords, and fire per-keystroke or commit events.
793
+ It supersedes the uncontrolled `Input` from the deprecated `@pihanga2/cards`
794
+ package, which had no `value` prop and no `type` prop.
795
+
796
+ | Feature | `pi/input` (this library) |
797
+ |---|---|
798
+ | `value` prop (Redux binding) | ✓ |
799
+ | `type` prop (`password`, `email`, …) | ✓ |
800
+ | `onChanged` (per-keystroke) | ✓ |
801
+ | `onCommitted` (blur / Enter) | ✓ |
802
+ | `label` + `description` | ✓ |
803
+ | Works inside `pi/form` / `pi/field` | ✓ |
804
+
805
+ The card you want for a freestanding labeled input (e.g. a JWT token field, a
806
+ search box, a settings field) is **`pi/input`** — *not* `pi/text-input`, which
807
+ does not exist. You do **not** need to write your own local card.
808
+
809
+ ```ts
810
+ import {PiInput, onPiInputChanged} from "@/cards/input";
811
+ import type {AppState} from "@/app.state";
812
+
813
+ register((r) => {
814
+ onPiInputChanged(r, (state: AppState, {value}) => {
815
+ state.jwtToken = value;
816
+ });
817
+ });
818
+
819
+ registerCard("myApp/jwtField", PiInput({
820
+ label: "JWT token",
821
+ value: memo((s: AppState) => s.jwtToken), // ← bound to Redux state
822
+ placeholder: "Paste your bearer token here…",
823
+ type: "password", // ← masks as ••••
824
+ className: "flex-1",
825
+ }));
826
+ ```
827
+
828
+ `pi/input` also fires `onCommitted` (blur / Enter) for cases where you only
829
+ want to react once per editing session rather than on every keystroke.
830
+
831
+ > **`pi/text-field`** (`TextField`) is a *different* card — it is designed to
832
+ > live *inside* a `pi/field` + `pi/form` composition and reads its value from
833
+ > form context. Use `pi/input` for standalone labeled inputs.
834
+
835
+ ### `shad/loading-skeleton` — prefer named presets over raw Tailwind
836
+
837
+ The card has built-in named presets so you rarely need raw Tailwind classes:
838
+
839
+ ```ts
840
+ import {LoadingSkeleton} from "@/cards/loadingSkeleton";
841
+
842
+ registerCard("myApp/area", LoadingSkeleton({
843
+ loading: memo((s: AppState) => s.dataLoading),
844
+ rows: 4,
845
+ rowSize: "lg", // ← xs | sm | md (default) | lg | xl
846
+ spacing: "lg", // ← sm | md (default) | lg
847
+ content: "myApp/dataList",
848
+ }));
849
+ ```
850
+
851
+ Raw `rowClassName` / `className` overrides are available as escape hatches for
852
+ custom layouts, but the presets handle the common cases without any Tailwind
853
+ knowledge.
854
+
855
+ ### `shad/conditional` — mount/unmount a card based on state
856
+
857
+ ```ts
858
+ import {Conditional} from "@/cards/conditional";
859
+
860
+ registerCard("myApp/hint", Conditional({
861
+ show: memo((s: AppState) => s.items.length === 0 && !s.isLoading),
862
+ content: "myApp/emptyStateHint",
863
+ }));
864
+ ```
865
+
866
+ This is a transparent pass-through — no extra DOM wrapper is added. Prefer
867
+ it over `className: (s) => s.x ? "" : "hidden"` workarounds.
868
+
869
+ ### `shad/framework` — default theme is `"dark"`
870
+
871
+ > ⚠️ **Do NOT assume the default theme is `"light"` or `"system"`.** The
872
+ > `framework` card defaults to `theme: "dark"` when no `theme` prop is provided.
873
+
874
+ The `theme` prop is passed to `ThemeProvider` and persisted in `localStorage`
875
+ under the key `"shadcn-ui-theme"`. Override it explicitly if your app needs
876
+ a different default:
877
+
878
+ ```ts
879
+ // Dark (default — no explicit prop needed):
880
+ registerFramework(SdFramework({page: "app/main"}));
881
+
882
+ // Light on first load:
883
+ registerFramework(SdFramework({page: "app/main", theme: "light"}));
884
+
885
+ // Follow the OS preference:
886
+ registerFramework(SdFramework({page: "app/main", theme: "system"}));
887
+ ```
888
+
889
+ The `modeToggle` card switches between `"light"` and `"dark"` at runtime and
890
+ persists the new preference. Once the user has toggled, the stored
891
+ `localStorage` value takes over from the `theme` default.
892
+
893
+ ### `shad/list` — icon decorators require registered icons
894
+
895
+ The `shad/list` card supports an `"icon"` decorator type on list items. When
896
+ a list item specifies `decorator: {type: "icon", name: "someIcon"}`, the
897
+ component calls `getIcon(name)` from `src/cards/icons.ts` to resolve the icon.
898
+
899
+ > ⚠️ **Icons must be registered before they can appear in list item decorators.**
900
+ > This dependency is NOT visible from the card's type declarations — an
901
+ > unregistered icon name silently renders nothing.
902
+
903
+ Register icons in `src/cards/icons.ts` (or your app's icon registry file):
904
+
905
+ ```ts
906
+ // src/cards/icons.ts (or your project's equivalent)
907
+ import {registerIcon} from "@/cards/icons";
908
+ import {CheckCircle, AlertCircle, Info} from "lucide-react";
909
+
910
+ registerIcon("check-circle", CheckCircle);
911
+ registerIcon("alert", AlertCircle);
912
+ registerIcon("info", Info);
913
+ ```
914
+
915
+ Then use the registered name in list item decorators:
916
+
917
+ ```ts
918
+ import {ShadList} from "@/cards/list";
919
+
920
+ registerCard("myApp/statusList", ShadList({
921
+ items: [
922
+ {
923
+ id: "1",
924
+ title: "Job complete",
925
+ decorator: {type: "icon", name: "check-circle"}, // ← registered name
926
+ },
927
+ ],
928
+ }));
929
+ ```
930
+
931
+ If the icon name is not registered, the decorator slot renders as empty with no
932
+ error in the console — making the omission easy to miss.
933
+
934
+ ---
935
+
936
+ ## Known gaps identified during AI agent evaluations
937
+
938
+ ### 2026-04 evaluation — multi-page app task
939
+
940
+ The following patterns were **not** documented in the original AGENT.md but were
941
+ required to complete a multi-page app task. They have been added above.
942
+
943
+ | Gap | Section now added |
944
+ |-----|-------------------|
945
+ | App bootstrap pattern (`registerFramework` / `start`) | *Bootstrapping a pihanga app* |
946
+ | `memo()` for reactive state-driven props | *`memo()` — reactive state-driven props* |
947
+ | Multi-page navigation with `PageWithNavbar` + `onPageWithNavbarNavigateTo` | *Multi-page navigation with `PageWithNavbar`* |
948
+ | `MarkdownViewer` `path` prop requires HTTP access / `public/` | *`MarkdownViewer` — inline source vs. fetched path* |
949
+ | `registerFramework` uniqueness constraint when composing inits | *`registerFramework` — only one active at a time* |
950
+ | `AppState` must be extended for new state fields | *Multi-page navigation* (see `currentPage` example) |
951
+
952
+ ### 2026-06 evaluation — card-composition data-fetch app (`@pihanga/ivcap`)
953
+
954
+ An agent built a pure-card-composition data-fetch app and reported several cards
955
+ as "missing". Post-mortem: the cards existed but had non-obvious API names or
956
+ were absent from the available-cards table. The following fixes were applied:
957
+
958
+ | Reported gap | Reality | Fix applied |
959
+ |---|---|---|
960
+ | `shad/loading-skeleton` not found | Card exists; uses `rowSize`/`spacing` presets, not raw `rowClassName` | Added to available-cards table; added to *Card API quick reference* |
961
+ | `shad/conditional` not found | Card exists and is straightforward | Added to available-cards table; added to *Card API quick reference* |
962
+ | `pi/text-input` card missing | Card exists as **`pi/input`** (`PiInput` export) | Added `pi/input` guidance to *Card API quick reference* |
963
+ | `pi/tabs` card missing | Card exists as **`shad/tabs`** (`SdTabs` export); `value` not `activeTab`; `contentCard` not `content`; `title` not `label` | Added tabs guidance to *Card API quick reference* |
964
+ | Dialog invisible on dark theme | `bg-background` made modal panel near-black on dark themes | Fixed `dialog.tsx`: `bg-card text-card-foreground border border-border shadow-xl` |
965
+
966
+ ### 2026-06 developer report — app team wrote a local `pi/text-input` card
967
+
968
+ A developer building an app on top of this library wrote and kept their own
969
+ local `pi/text-input` card, reasoning that the deprecated `@pihanga2/cards`
970
+ Input is uncontrolled (no `value` prop, no `type` prop) and therefore
971
+ unsuitable for Redux binding or password masking.
972
+
973
+ **Reality:** `pi/input` in *this* library (`pihanga-shadcn`) is already a fully
974
+ controlled replacement. It has `value`, `type`, `onChanged`, `onCommitted`,
975
+ label, description, and `pi/form` integration. The developer did not need a
976
+ local card.
977
+
978
+ | Root cause | Fix applied |
979
+ |---|---|
980
+ | `pi/input` section did not explicitly contrast itself with `@pihanga2/cards`' uncontrolled `Input` | Added feature-comparison table and "you do **not** need to write your own local card" callout to *Card API quick reference → `pi/input`* |
981
+
982
+ ### 2026-06 developer report — CSS setup pitfalls with Tailwind v4 + Dialog
983
+
984
+ Two CSS issues were observed when integrating the library into a Tailwind v4
985
+ project that was bootstrapped without the reference `index.css`:
986
+
987
+ | Symptom | Root cause | Fix |
988
+ |---|---|---|
989
+ | Dialog panel, cards, popovers appear **invisible** (no background colour) | Tailwind v4 does not auto-generate colour utilities from raw CSS variables — the `@theme inline` block bridging `--card` → `--color-card` etc. was absent | Add the full `@theme inline` block to `src/index.css` (see *Prerequisites → Configure `src/index.css`*) |
990
+ | Page content **reflows** (text re-wraps) when a dialog opens | Radix UI's `@radix-ui/react-remove-scroll` applies `overflow: hidden` to `<body>` on dialog open, removing the scrollbar and widening the layout by ~15 px | Add `scrollbar-gutter: stable` to `body` in `src/index.css` (see *Prerequisites → Configure `src/index.css`*) |
991
+
992
+ Both fixes are now included in the reference `src/index.css` template in
993
+ *Prerequisites step 3*.
994
+
995
+ ### 2026-06 agent evaluation — ViteJS app from scratch (BuhlOS-2)
996
+
997
+ An agent built a new ViteJS + React + TypeScript app from scratch using
998
+ `yarn create vite` and then manually copied pihanga-shadcn cards. The
999
+ following blockers were hit that were not covered by any existing doc section.
1000
+ All fixes have been added to the new *[Vite configuration (both channels)](#vite-configuration-both-channels)* section.
1001
+
1002
+ | Blocker | Root cause | Fix |
1003
+ |---|---|---|
1004
+ | `"Failed to resolve import '@/registry/ui/button'"` | The `@/registry` → `src/components` Vite alias was not documented | Added required `vite.config.ts` alias table |
1005
+ | `"Failed to resolve import '@/components/lib/utils'"` | shadcn UI components import `cn()` from `@/components/lib/utils`, not `@/lib/utils`; the file must exist at both paths | Added `src/components/lib/utils.ts` creation step |
1006
+ | `SyntaxError: does not provide an export named 'PiCardRef'` | Card source files (button.types.ts, toast.types.ts, typography.types.ts) imported `PiCardRef` without the `type` keyword; esbuild doesn't enforce `verbatimModuleSyntax` | Added **Type-only import gotcha** section with before/after examples |
1007
+ | `SyntaxError: does not provide an export named 'Store'` | `app.root.tsx` imported `Store` from `@reduxjs/toolkit` as a value | Covered by same type-only import section |
1008
+ | Vite `"Failed to resolve import '@/cards/dropDownMenu/dropdown-context'"` | `button` card has a transitive dependency on `dropDownMenu` that isn't obvious | Added **Transitive card dependencies** table |
1009
+ | `pageWithNavbar` missing internal helpers at runtime | `pageWithNavbar` has transitive dependencies on `modeToggle`, `navbarSearch`, and `toast` | Added to transitive dependencies table |
1010
+ | `@pihanga2/cards` not in package.json | `shad/stack` previously imported `StackProps` from `@pihanga2/cards` | **`@pihanga2/cards` is now deprecated.** `BoxProps` and `StackProps` are defined locally in `box.types.ts` / `stack.types.ts`. Do **not** install `@pihanga2/cards`. |
1011
+
1012
+ ### 2026-06 app developer wrote a local link-button card
1013
+
1014
+ A developer building an app with `pi/button` wrote a separate local card for
1015
+ anchor-style buttons, believing that `pi/button` only rendered `<button>`
1016
+ elements and had no link capability.
1017
+
1018
+ **Reality:** `pi/button` already renders as an `<a>` element when the `href`
1019
+ prop is provided (see *Card API quick reference → `pi/button` — rendering as
1020
+ an anchor / link*). No local card was needed.
1021
+
1022
+ | Root cause | Fix applied |
1023
+ |---|---|
1024
+ | `href` / `target` props on `pi/button` existed in `button.types.ts` but were never documented in any guide or quick-reference section | Added `pi/button — rendering as an anchor / link` section to *Card API quick reference* in `AGENT.using-cards.md` and a matching `### Button as an anchor link` subsection to `USER_GUIDE.md` |
1025
+
1026
+ ### 2026-06 app developer findings — hidden contracts and misunderstood defaults
1027
+
1028
+ An app team building on pihanga-shadcn documented several card behaviours that
1029
+ they had to discover by reading source, along with two cases where they made
1030
+ local modifications to the registry code for features that already existed.
1031
+
1032
+ | Reported issue | Reality | Fix applied |
1033
+ |---|---|---|
1034
+ | `shad/framework` defaults to `"light"` or `"system"` — team patched it to `"dark"` | The registry already defaults to `theme: "dark"`; no patch was needed | Added *`shad/framework` — default theme is `"dark"`* to *Card API quick reference* |
1035
+ | `shad/tabs` with a string card ID as `tab.title` rendered the raw ID string instead of mounting the card | This is intended behaviour: strings are always text labels; only object-form declarations mount as cards; `isCardRef()` cannot reliably identify string card IDs | Added *`shad/tabs` — `tab.title` strings are always rendered as text* to *Card API quick reference* with correct object-form pattern |
1036
+ | `shad/list` icon decorators silently rendered nothing when icon names were unregistered | The `shad/list` card calls `getIcon(name)` which requires icons to be pre-registered via `registerIcon()` in `src/cards/icons.ts`; this contract is not visible in the type declarations | Added *`shad/list` — icon decorators require registered icons* to *Card API quick reference* |
1037
+ | `flexGrid.component.tsx` failed to compile under `verbatimModuleSyntax` / `noImplicitAny` strict TypeScript | Registry source mixed value and type imports; had implicit `any` in `.map()` callback; `Object.entries` lacked an explicit cast; `_style` lacked widened type for `gridTemplateAreas` | Fixed directly in `flexGrid.component.tsx`: split `import type`, added explicit `row: string[]` annotation, added `as [string, PiCardRef][]` cast, widened `_style` type |
1038
+ | `stack.component.tsx` contained commented-out Joy UI dead code | Leftover from a previous MUI Joy UI implementation; the current Tailwind implementation is complete and the comment block serves no purpose | Removed the dead code block |
1039
+
1040
+ ### 2026-06 user feedback — `npx shadcn@latest add` fails on npm 11 + Node 24
1041
+
1042
+ Users reported that every `npx shadcn@latest` command failed with
1043
+ `npm error could not determine executable to run` when running Node 24 (which
1044
+ bundles npm 11). The docs only showed `npx` examples with no mention of a
1045
+ yarn alternative or Node version constraint.
1046
+
1047
+ | Reported gap | Fix applied |
1048
+ |---|---|
1049
+ | No `yarn dlx` alternative documented | Added `yarn dlx shadcn@latest` as the yarn-compatible alternative in the distribution channels table, Channel 1 init step, and "Adding individual cards" commands |
1050
+ | No warning about npm 11 + Node 24 incompatibility | Added a prominent compatibility callout at the top of *Channel 1 — shadcn registry prerequisites*, with the `nvm use 22` workaround and the `yarn dlx` alternative |
1051
+ | "Notes for AI agents" section did not mention the `npx` breakage | Added a bullet noting that `npx` fails on npm 11 + Node 24 and that `yarn dlx shadcn@latest` is a drop-in replacement |