@pihanga2/shadcn 0.1.0 → 0.2.0

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