@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.
- package/AGENT.building-cards.md +413 -0
- package/AGENT.md +50 -0
- package/AGENT.using-cards.md +1242 -0
- package/cards/box/box.types.d.ts +1 -4
- package/cards/box/box.types.js.map +1 -1
- package/cards/core-index.js +6 -6
- package/cards/fileDrop/fileDrop.component.d.ts +5 -0
- package/cards/fileDrop/fileDrop.types.d.ts +43 -0
- package/cards/fileDrop/index.d.ts +2 -0
- package/cards/icons.js +7 -3
- package/cards/icons.js.map +1 -1
- package/cards/index.d.ts +1 -0
- package/cards/stack/stack.types.d.ts +19 -22
- package/cards/stack/stack.types.js +1 -1
- package/cards/stack/stack.types.js.map +1 -1
- package/cards/tabs/index.js +4 -4
- package/cards/tabs/tabs.types.d.ts +2 -0
- package/cards/tabs/tabs.types.js +2 -2
- package/cards/tabs/tabs.types.js.map +1 -1
- package/cards/types.d.ts +4 -0
- package/cards/types.js.map +1 -1
- package/package.json +9 -1
- package/pihanga-shadcn.css +1 -1
- package/mountain-snow.svg +0 -4
- package/r/badge.json +0 -35
- package/r/box.json +0 -26
- package/r/button.json +0 -39
- package/r/checkbox.json +0 -31
- package/r/conditional.json +0 -30
- package/r/dataTable.json +0 -43
- package/r/dialog.json +0 -36
- package/r/dropDownMenu.json +0 -47
- package/r/field.json +0 -35
- package/r/flexGrid.json +0 -32
- package/r/form.json +0 -37
- package/r/framework.json +0 -28
- package/r/graphin.json +0 -42
- package/r/input.json +0 -35
- package/r/jsonViewer.json +0 -40
- package/r/list.json +0 -39
- package/r/loadingOverlay.json +0 -39
- package/r/loadingSkeleton.json +0 -30
- package/r/markdownViewer.json +0 -51
- package/r/menu.json +0 -34
- package/r/modeToggle.json +0 -36
- package/r/navbarSearch.json +0 -31
- package/r/pageWithNavbar.json +0 -46
- package/r/pasteTarget.json +0 -41
- package/r/pihanga-base.json +0 -11
- package/r/pihanga-cards-icons.json +0 -16
- package/r/pihanga-cards-types.json +0 -16
- package/r/pihanga-hook-use-is-touch-device.json +0 -15
- package/r/pihanga-lib-utils.json +0 -18
- package/r/pihanga-theme-provider.json +0 -25
- package/r/pihanga-ui-extras.json +0 -68
- package/r/registry.json +0 -171
- package/r/resizable.json +0 -35
- package/r/select.json +0 -35
- package/r/stack.json +0 -27
- package/r/stepper.json +0 -39
- package/r/switch.json +0 -36
- package/r/tabs.json +0 -33
- package/r/textField.json +0 -32
- package/r/toast.json +0 -32
- package/r/toggleGroup.json +0 -35
- 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 |
|