paneltir 0.6.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/README.md ADDED
@@ -0,0 +1,404 @@
1
+ # Paneltir
2
+
3
+ **Paneltir** is the DFKlabs UI kit for administration panels. It owns
4
+ **structure and behaviour** โ€” page frame, a board with touch-ready drag and
5
+ drop, detail sheets, theming โ€” and never colour or brand. Each project hands
6
+ it a palette and that is the whole customisation.
7
+
8
+ > โš ๏ธ **Proprietary software.** This repository is private and is not open
9
+ > source. Access is granted only to authorised projects; copying,
10
+ > redistribution or modification without written permission is prohibited.
11
+ > See [`LICENSE`](./LICENSE).
12
+
13
+ ๐ŸŒ **Site:** [paneltir.dfklabs.com](https://paneltir.dfklabs.com) โ€”
14
+ [live demo](https://paneltir.dfklabs.com/demo) ยท
15
+ [project panel](https://paneltir.dfklabs.com/admin) (password protected)
16
+
17
+ ## Repository contents
18
+
19
+ | Path | What it is |
20
+ | --- | --- |
21
+ | `src/` | The kit: React + TypeScript components with a stylesheet per component. |
22
+ | `src/themes/presets.ts` | Base palettes: `cyberpunkTheme` (Night City), `midnightTheme`, `oldMoneyTheme`. |
23
+ | `site/` | The website: landing, live demo, the project's own panel and the legal pages. |
24
+ | `site/src/data/panel-state.json` | The board behind `/admin` โ€” panel content as version-controlled data. |
25
+ | `middleware.ts` | Vercel edge middleware that password-protects `/admin` and the panel API. |
26
+ | `api/panel-state.ts` | Serverless function that commits the board back to this repository. |
27
+ | `.claude/skills/todo/SKILL.md` | `/todo` โ€” how Claude reads the board and what each intent obliges. |
28
+ | `examples/demo/` | A minimal Vite app kept as a starting template for a consumer project. |
29
+ | `INSTALL.md` | Install instructions, written for Claude rather than for a human reader. |
30
+ | `CLAUDE_MD_SNIPPET.md` | The block to paste into each consumer project's `CLAUDE.md`. |
31
+ | `fingerprint.json` | SHA-256 of the whole repository and of every file (see below). |
32
+ | `renovate.json.example` | Renovate config so consumers get a pull request on every new tag. |
33
+
34
+ ## Components
35
+
36
+ | Export | Purpose |
37
+ | --- | --- |
38
+ | `DashboardThemeProvider` / `useDashboardTheme` | Turns the project palette into `--pt-color-*` variables. |
39
+ | `DashboardHeader` | Header with wordmark, status line and an action link. |
40
+ | `AlertBanner` | Full-width alert with an optional retry action. |
41
+ | `StatTile` / `StatTileGrid` | Large metrics with a semantic tone and a calm state for zero. |
42
+ | `HealthPill` / `HealthPillRow` | System facts: ok, warning, bad, unknown. |
43
+ | `FilterChip` / `FilterChipRow` | Filters with counters. |
44
+ | `Board` / `Card` / `moveCardInColumns` | Kanban board with pointer-event drag and drop and edge autoscroll. |
45
+ | `DetailSheet` / `DetailSection` | Bottom sheet under 760px, centred dialog above it, from one markup. |
46
+ | `useDelegatedClick` / `useMediaQuery` | Supporting hooks. |
47
+
48
+ ## Installation
49
+
50
+ The kit ships as a Git dependency โ€” no npm registry involved. Always pin a
51
+ tag rather than a branch, so an unfinished push cannot reach a project:
52
+
53
+ ```bash
54
+ npm install paneltir
55
+
56
+ # or, without npm access, straight from the repository:
57
+ # npm install github:daifukus/paneltir#v0.6.0
58
+ ```
59
+
60
+ Installing from Git runs the `prepare` script, which builds `dist/`, so the
61
+ repository does not need to carry build output.
62
+
63
+ ### Private repository
64
+
65
+ Whoever runs `npm install` needs Git read access to this repository:
66
+
67
+ - **Locally**, an existing SSH key for GitHub is enough โ€” `npm install
68
+ github:...` reuses it.
69
+ - **In CI or on a deploy platform**, that machine needs its own access: a
70
+ read-only deploy key, or a fine-grained GitHub token scoped to this
71
+ repository and stored as a pipeline secret. **Never put a token in the
72
+ dependency URL** โ€” it would be committed in plain text in every consumer
73
+ repository. Configure it in the machine's `.npmrc` or credential helper
74
+ instead.
75
+
76
+ ### Installing it with Claude
77
+
78
+ Paste this into the project where the kit should go:
79
+
80
+ > Install `paneltir` following the instructions in
81
+ > `node_modules/paneltir/INSTALL.md`, pinned to the tag we agreed on.
82
+
83
+ `INSTALL.md` is written as an instruction for the agent: it looks for the
84
+ palette the project already has before inventing one, maps it to the token
85
+ roles and wires up `DashboardThemeProvider`, so the result always looks like
86
+ *that* project.
87
+
88
+ `INSTALL.md` is read once, at install time. To keep the "never edit
89
+ `node_modules/paneltir`" rule alive across day-to-day requests, also paste the
90
+ block from [`CLAUDE_MD_SNIPPET.md`](./CLAUDE_MD_SNIPPET.md) into the project's
91
+ own `CLAUDE.md`, which is reloaded every session.
92
+
93
+ ## Base themes
94
+
95
+ For a project without a visual identity yet (see `INSTALL.md`, step 3), the
96
+ package ships three palettes: `cyberpunkTheme` (crimson surfaces and Night
97
+ City yellow โ€” the site's default), `midnightTheme` (neutral) and
98
+ `oldMoneyTheme` (bottle green, muted gold). All three follow the same rules โ€”
99
+ a single accent, four semantic colours kept apart from it โ€” and differ only in
100
+ the choice of colour:
101
+
102
+ ```tsx
103
+ import { DashboardThemeProvider, cyberpunkTheme } from 'paneltir'
104
+
105
+ <DashboardThemeProvider tokens={cyberpunkTheme}>...</DashboardThemeProvider>
106
+ ```
107
+
108
+ They are a starting point, not an identity: replace them as soon as the
109
+ project has its own.
110
+
111
+ ## The website
112
+
113
+ `site/` is what gets published to
114
+ [paneltir.dfklabs.com](https://paneltir.dfklabs.com):
115
+
116
+ - **`/`** โ€” the landing: what the kit solves, its public surface, the base
117
+ themes and how it installs.
118
+ - **`/demo`** โ€” a public panel with fictional data, so the drag and drop, the
119
+ filters, the detail sheet and the theme switcher can be tried without an
120
+ account.
121
+ - **`/admin`** โ€” the project's own panel, password protected, and fully
122
+ editable: add a card with the column's **+**, drag it by its grip, open it to
123
+ set priority, risk, who it waits on, an **order for Claude** and one of nine
124
+ **intents**, tick checklist steps and leave notes. **Save to GitHub** commits
125
+ `site/src/data/panel-state.json`, so every change to the board is a diff in
126
+ version control. It also carries a **Functions** section describing what the
127
+ kit gives a project and how Claude should work with it.
128
+ - **`/terms`, `/license`, `/copyright`** โ€” the legal pages.
129
+
130
+ ```bash
131
+ npm install # builds dist/, which the site consumes as file:..
132
+ npm run site:dev # landing, demo and panel on http://localhost:5173
133
+ ```
134
+
135
+ ### Panel access
136
+
137
+ Opening `/admin` without a session lands on `/login`, a sign-in screen on the
138
+ site rather than the browser's own credential dialog. The password is posted to
139
+ `api/login.ts`, which answers with a signed, HttpOnly session cookie;
140
+ `middleware.ts` then verifies it before serving `/admin`, the panel's own
141
+ JavaScript bundle or the panel API. Sessions last eight hours, and **Sign out**
142
+ lives in the panel's settings sheet.
143
+
144
+ Set these in the Vercel project (Settings โ†’ Environment Variables):
145
+
146
+ | Variable | Value |
147
+ | --- | --- |
148
+ | `ADMIN_PASSWORD` | The panel password. Required โ€” without it `/admin` answers 503 instead of becoming public. |
149
+ | `SESSION_SECRET` | Optional key for signing sessions. Defaults to `ADMIN_PASSWORD`, so changing the password ends every session already issued. |
150
+ | `GH_TOKEN` | Fine-grained token with *contents: read and write* on this repository only. Lets the panel commit its board. |
151
+ | `PANEL_REPO` | Optional `owner/repo`, `daifukus/paneltir` by default. |
152
+ | `PANEL_BRANCH` | Optional branch, `main` by default. |
153
+
154
+ None of these live in the repository, and `GH_TOKEN` never reaches the browser:
155
+ it is read inside the serverless function only.
156
+
157
+ ### The board is a queue
158
+
159
+ A card is not a note, it is an instruction. Beyond a title and a body it
160
+ carries a **column** (`ideas`, `next`, `doing`, `yours`, `done`), a
161
+ **priority**, a **risk** โ€” what it costs if it goes wrong, not how soon it is
162
+ wanted โ€” **who it waits on**, an **order** flag meaning "I am asking you to do
163
+ this", and an **intent**: one of nine values that says *how* to do it and beats
164
+ anything the agent would otherwise decide. Three of them โ€” `explain`, `askme`,
165
+ `hold` โ€” are refusals to act, which is the only way to say "not yet" in a way
166
+ that survives a session boundary.
167
+
168
+ `.claude/skills/todo/SKILL.md` is the other half: it turns those fields into a
169
+ work list, in order, and says what each one obliges. Run `/todo` and the panel
170
+ stops being a status page and becomes the interface.
171
+
172
+ ### The panel writes its own state
173
+
174
+ Moving a card in `/admin` marks the board as changed and a **Save to GitHub**
175
+ button appears in the header. Saving POSTs the state to
176
+ `/admin/api/panel-state`, which is `api/panel-state.ts`: it re-checks the same
177
+ credentials, validates the payload, and commits
178
+ `site/src/data/panel-state.json` on `main` through the GitHub API using
179
+ `GH_TOKEN`. So an edit made from the board is an ordinary commit with a diff,
180
+ an author and a history โ€” and the deploy that follows is what makes it visible
181
+ on the site.
182
+
183
+ The write sends the file's current blob SHA, so if the file changed in the
184
+ repository meanwhile the save is rejected with a conflict instead of silently
185
+ overwriting that change.
186
+
187
+ ## Basic usage
188
+
189
+ ```tsx
190
+ import {
191
+ DashboardThemeProvider,
192
+ DashboardHeader,
193
+ StatTileGrid,
194
+ StatTile,
195
+ Board,
196
+ Card,
197
+ DetailSheet,
198
+ DetailSection,
199
+ moveCardInColumns,
200
+ type DashboardThemeTokens,
201
+ type BoardColumnData,
202
+ } from 'paneltir'
203
+ import 'paneltir/style.css'
204
+
205
+ // The project palette โ€” the only thing that changes between projects.
206
+ const theme: DashboardThemeTokens = {
207
+ ground: '#0b0c10',
208
+ groundRaised: '#14161c',
209
+ panel: '#181a21',
210
+ raised: '#20232b',
211
+ line: 'rgba(255,255,255,0.13)',
212
+ lineFaint: 'rgba(255,255,255,0.07)',
213
+ ink: 'rgba(255,255,255,0.92)',
214
+ inkDim: 'rgba(255,255,255,0.62)',
215
+ inkFaint: 'rgba(255,255,255,0.34)',
216
+ accent: '#5b8cff',
217
+ danger: '#ff5c5c',
218
+ warning: '#f5b74c',
219
+ success: '#3ecf8e',
220
+ neutral: '#8a8f98',
221
+ }
222
+
223
+ function AdminDashboard() {
224
+ const [columns, setColumns] = React.useState<BoardColumnData[]>(initialColumns)
225
+
226
+ return (
227
+ <DashboardThemeProvider tokens={theme}>
228
+ <DashboardHeader wordmark="My project" status="3 pending" linkLabel="Sign out" />
229
+
230
+ <StatTileGrid>
231
+ <StatTile label="Open" value={12} tone="warning" />
232
+ <StatTile label="Overdue" value={0} isZero />
233
+ </StatTileGrid>
234
+
235
+ <Board
236
+ columns={columns}
237
+ onMove={(move) => setColumns((current) => moveCardInColumns(current, move))}
238
+ renderCard={(card, index, columnId) => (
239
+ <Card
240
+ cardId={card.id}
241
+ columnId={columnId}
242
+ index={index}
243
+ title={card.title as string}
244
+ priorityColor={theme.danger}
245
+ tags={[{ label: 'blocked', tone: 'danger', emphasize: true }]}
246
+ progress={{ done: 2, total: 5 }}
247
+ onClick={() => openDetail(card.id)}
248
+ />
249
+ )}
250
+ />
251
+
252
+ <DetailSheet open={detailOpen} onClose={closeDetail} ariaLabel="Card detail">
253
+ <DetailSection title="Assigned to" subtitle="who is working on it">
254
+ ...
255
+ </DetailSection>
256
+ </DetailSheet>
257
+ </DashboardThemeProvider>
258
+ )
259
+ }
260
+ ```
261
+
262
+ ## What the library actually solves
263
+
264
+ - **Pointer-event drag and drop**, not the HTML5 drag API, which never fires
265
+ on iOS touch.
266
+ - **Edge autoscroll on requestAnimationFrame**, reading the pointer from a
267
+ ref rather than from `pointermove`, which stops firing while a finger is
268
+ held still against the edge.
269
+ - **Scroll-snap disabled during a drag**, so the autoscroll does not fight the
270
+ board's snap.
271
+ - **A real floating clone** (a DOM clone, not a redrawn approximation) that
272
+ follows the pointer.
273
+ - **A single `moveCardInColumns`** โ€” moving an item between arrays by index is
274
+ exactly where an off-by-one hides when four projects each rewrite it.
275
+
276
+ ## Theming
277
+
278
+ All colour enters through `DashboardThemeTokens` and leaves as `--pt-color-*`
279
+ variables inside `.pt-theme-root`. The library ships no defaults on purpose:
280
+ a forgotten token renders without colour, which is noticed immediately,
281
+ instead of silently inheriting something generic. Every role is documented in
282
+ `src/theme/tokens.ts` and in the published `.d.ts`.
283
+
284
+ ## Change fingerprint (`fingerprint.json`)
285
+
286
+ `fingerprint.json` holds a SHA-256 computed over every tracked file, plus a
287
+ hash per file. It ships with the package, so in a consumer project it sits at
288
+ `node_modules/paneltir/fingerprint.json`.
289
+
290
+ It exists so Claude โ€” or any script โ€” can tell in a single read whether the
291
+ kit changed since it was last reviewed, instead of spending tokens re-reading
292
+ code and documentation that did not move. The block in `CLAUDE_MD_SNIPPET.md`
293
+ records the last integrated hash in each project; when it matches, there is
294
+ nothing to read. When it differs, the `files` map names exactly which files
295
+ moved.
296
+
297
+ ```bash
298
+ npm run fingerprint # regenerate it (before every commit)
299
+ npm run fingerprint:check # fail if stale (this is what CI runs)
300
+ node scripts/fingerprint.mjs --print # print just the hash
301
+ ```
302
+
303
+ It is only rewritten when the global hash changes, so it never produces empty
304
+ diffs.
305
+
306
+ ## Deployment (Vercel)
307
+
308
+ The site is served from Vercel. The configuration lives in `vercel.json`; in
309
+ the Vercel project leave **Root Directory empty** (the repository root) and
310
+ Framework Preset as "Other": the install builds the kit (`dist/`) first and
311
+ the site second, and the output is `site/dist`. Every merge to `main` deploys.
312
+ Remember to set `ADMIN_PASSWORD` before the first deploy.
313
+
314
+ Changing a Vercel setting only affects new deployments: redeploy, or merge a
315
+ pull request, for it to take effect. Keep the repository connected to a single
316
+ Vercel project โ€” two projects on the same repository build twice and only one
317
+ can hold the domain.
318
+
319
+ ### Only `main` deploys
320
+
321
+ `vercel.json` sets `git.deploymentEnabled` so Vercel builds `main` and nothing
322
+ else. Branch previews were spending the free plan's allowance of 100
323
+ deployments a day โ€” once it runs out, *nothing* deploys for 24 hours,
324
+ production included. Pull requests lose their preview URL, which costs little:
325
+ CI already builds the kit, the site and the starter template on every pull
326
+ request.
327
+
328
+ If previews are wanted back, delete that `git` block, and keep an eye on the
329
+ daily count. The same switch exists in the dashboard under Settings โ†’ Git,
330
+ and the dashboard is authoritative if the two ever disagree.
331
+
332
+ ### If production never deploys
333
+
334
+ Vercel refuses to build a commit whose author GitHub cannot verify, and it
335
+ says so on the pull request ("GitHub couldn't verify an account for commit").
336
+ Merge commits created from the GitHub UI or API carry the account's primary
337
+ email, so if that address is a private relay the production build is blocked
338
+ while branch previews still work.
339
+
340
+ Two fixes, and doing both is fine:
341
+
342
+ 1. **At the source.** GitHub โ†’ Settings โ†’ Emails โ†’ enable *Keep my email
343
+ address private*. From then on web and API commits are authored as
344
+ `<id>+<user>@users.noreply.github.com`, which is always verifiable.
345
+ 2. **Independently of Git.** `.github/workflows/deploy.yml` pings a Vercel
346
+ deploy hook on every push to `main`. Create the hook in Vercel โ†’ Settings โ†’
347
+ Git โ†’ Deploy Hooks (branch `main`) and store its URL as the
348
+ `VERCEL_DEPLOY_HOOK_URL` secret in this repository. Without the secret the
349
+ job is a no-op, so nothing breaks if it is not configured.
350
+
351
+ ### Does the build need a GitHub token?
352
+
353
+ The **build** does not: the site consumes the kit as `file:..`, so Vercel never
354
+ fetches this repository over the network, and its GitHub integration already
355
+ has read access to the source.
356
+
357
+ The **panel** does, to write: `GH_TOKEN` is what lets `api/panel-state.ts`
358
+ commit the board. Keep it fine-grained, scoped to this repository, with
359
+ *contents: read and write* and nothing else. It is only ever read server-side.
360
+
361
+ ## Updating consumer projects
362
+
363
+ ### By hand
364
+
365
+ 1. Make the change here with the version bump in `package.json`, merge it to
366
+ `main`, and `tag-release.yml` creates the `vX.Y.Z` tag automatically.
367
+ 2. In each consumer project: `npm install paneltir@X.Y.Z`, or
368
+ `npm install github:daifukus/paneltir#vX.Y.Z` without npm access.
369
+ 3. Claude can do that bump and run the dashboard smoke test in each
370
+ repository โ€” it is mechanical, not a reinterpretation.
371
+
372
+ ### With automatic pull requests
373
+
374
+ Install the [Renovate app](https://github.com/apps/renovate) on each consumer
375
+ project and copy [`renovate.json.example`](./renovate.json.example) as
376
+ `renovate.json` at its root. From then on, every new tag opens a pull request
377
+ with the bump already applied.
378
+
379
+ ## Local development
380
+
381
+ ```bash
382
+ npm install
383
+ npm run dev # tsup --watch
384
+ npm run typecheck # tsc --noEmit
385
+ npm run build # produces dist/
386
+ npm run verify # typecheck + build + fingerprint:check (what CI runs)
387
+ ```
388
+
389
+ To try it against a project before tagging, `npm link` (or a temporary
390
+ `file:../paneltir` dependency) avoids tagging on every iteration.
391
+
392
+ ## Roadmap
393
+
394
+ - Type sizes as `--pt-font-size-*` tokens instead of per-component values.
395
+ - Drop placeholder matching the dragged card's real height.
396
+ - Tests for `moveCardInColumns` (same index, end of column, empty columns).
397
+ - Validate the token system against light surfaces.
398
+
399
+ ## License
400
+
401
+ Copyright ยฉ 2026 DFKlabs. All rights reserved. Use is restricted to the
402
+ projects the holder authorises โ€” see [`LICENSE`](./LICENSE) for the full
403
+ terms, and the [License page](https://paneltir.dfklabs.com/license) for a
404
+ plain-English summary.