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.
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "paneltir",
3
+ "version": "0.6.0",
4
+ "hash": "sha256:cb1a1df3cac0d3cc775fbaa7b9f385d6d35e22cecbe86cc0a1da4338cf43de37",
5
+ "fileCount": 151,
6
+ "generatedAt": "2026-09-06T10:08:28.981Z"
7
+ }
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "paneltir",
3
+ "version": "0.6.0",
4
+ "description": "A React UI kit for admin panels: page frame, a board with touch-ready drag and drop, detail sheets, and a whole panel in one component. Structure and behaviour are fixed; colour and brand belong to each project.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "author": "DFKlabs (https://DFKlabs.com)",
7
+ "homepage": "https://paneltir.dfklabs.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/daifukus/paneltir.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/daifukus/paneltir/issues"
14
+ },
15
+ "keywords": [
16
+ "react",
17
+ "dashboard",
18
+ "admin-panel",
19
+ "kanban",
20
+ "drag-and-drop",
21
+ "theming",
22
+ "ui-kit"
23
+ ],
24
+ "type": "module",
25
+ "main": "./dist/index.js",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./style.css": "./dist/style.css"
34
+ },
35
+ "bin": {
36
+ "paneltir": "./bin/paneltir.mjs"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "bin",
41
+ "template",
42
+ "README.md",
43
+ "INSTALL.md",
44
+ "CLAUDE_MD_SNIPPET.md",
45
+ "renovate.json.example",
46
+ "LICENSE",
47
+ "fingerprint.json"
48
+ ],
49
+ "sideEffects": [
50
+ "**/*.css"
51
+ ],
52
+ "scripts": {
53
+ "build": "tsup",
54
+ "dev": "tsup --watch",
55
+ "prepare": "npm run build",
56
+ "typecheck": "tsc --noEmit",
57
+ "fingerprint": "node scripts/fingerprint.mjs",
58
+ "fingerprint:check": "node scripts/fingerprint.mjs --check",
59
+ "icons": "node scripts/icons.mjs",
60
+ "icons:check": "node scripts/icons.mjs --check",
61
+ "seo": "node scripts/seo.mjs",
62
+ "seo:check": "node scripts/seo.mjs --check",
63
+ "test:auth": "node scripts/test-auth.mjs",
64
+ "check:boards": "node scripts/check-boards.mjs",
65
+ "demo:install": "npm --prefix examples/demo ci",
66
+ "demo:build": "npm --prefix examples/demo run build",
67
+ "verify": "npm run typecheck && npm run build && npm run icons:check && npm run seo:check && npm run test:auth && npm run test:marketplace && npm run test:init && npm run test:board && npm run check:boards && npm run check:docs && npm run check:package && npm run fingerprint:check",
68
+ "site:install": "npm --prefix site ci",
69
+ "site:dev": "npm --prefix site run dev",
70
+ "site:build": "npm --prefix site run build",
71
+ "test:marketplace": "node scripts/test-marketplace.mjs",
72
+ "check:docs": "node scripts/check-docs.mjs",
73
+ "check:package": "node scripts/check-package.mjs",
74
+ "test:init": "node scripts/test-init.mjs",
75
+ "test:board": "node scripts/test-board.mjs"
76
+ },
77
+ "peerDependencies": {
78
+ "react": ">=18",
79
+ "react-dom": ">=18"
80
+ },
81
+ "devDependencies": {
82
+ "@types/node": "^20.14.0",
83
+ "@types/react": "^18.3.0",
84
+ "@types/react-dom": "^18.3.0",
85
+ "esbuild": "0.27.7",
86
+ "react": "^18.3.0",
87
+ "react-dom": "^18.3.0",
88
+ "tsup": "^8.0.0",
89
+ "typescript": "^5.5.0"
90
+ },
91
+ "engines": {
92
+ "node": ">=20"
93
+ }
94
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": ["config:recommended"],
4
+ "packageRules": [
5
+ {
6
+ "matchPackageNames": ["paneltir"],
7
+ "groupName": "paneltir",
8
+ "labels": ["paneltir"],
9
+ "prPriority": 5,
10
+ "schedule": ["before 9am on monday"]
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: panel
3
+ description: Work this project's Paneltir board. Reads src/data/panel-state.json, builds the list of what the owner has already asked for through the panel — orders, intents, unticked steps, notes — and works it in order, writing the board back to say what happened. Use when the user says /panel, "what's on the board", "do what's pending", "qué queda por hacer", "vacía el tablero", or asks what the panel says.
4
+ ---
5
+
6
+ # The board is the queue
7
+
8
+ This project has a Paneltir panel. The board lives at
9
+ `src/data/panel-state.json` (or wherever `PANEL_FILE` points) and the owner
10
+ edits it from a browser, often from a phone. They do not write instructions in
11
+ prose — they tap. This skill reads those taps.
12
+
13
+ So `/panel` means *carry out what I have already told you through the panel*,
14
+ not *guess what needs doing*.
15
+
16
+ ## Reading a card
17
+
18
+ Six fields decide whether a card is yours and what you may do with it.
19
+
20
+ **`column`** — where it sits. The board defines its own columns; two of them
21
+ carry meaning in the data rather than in their name: the column marked
22
+ `active` is work in flight (entering it stamps `startedAt`), and the one
23
+ marked `done` is finished (entering it stamps `completedAt`). A column named
24
+ for the owner — `yours` in the default board — **is never yours to complete.**
25
+ You may still help it: sharpen its checklist, or leave a note answering a
26
+ question in its body.
27
+
28
+ **`owner`** — `you` means the person, `claude` means you. Combined with the
29
+ column: a `claude` card in a queued or active column is work; a `you` card is
30
+ not, whatever column it is in.
31
+
32
+ **`order: true`** — the one flag that means *I am asking you to do this*. It
33
+ is the strongest signal on the board and outranks position in the column.
34
+
35
+ **`intent`** — *how* to do it, and it **beats anything you would otherwise
36
+ decide**:
37
+
38
+ | `intent` | What it obliges you to do |
39
+ | --- | --- |
40
+ | `decide` | Pick between the options and act. Say what you picked and why, in one line. |
41
+ | `explain` | **Change nothing.** Come back with the trade-off and wait. |
42
+ | `solve` | Fix it if it is small and reversible. If it is neither, stop and say so. |
43
+ | `do` | Build it. Every detail not written down is yours to choose. |
44
+ | `cheap` | Choose whatever needs the least looking after later, even if it is duller. |
45
+ | `safe` | Take the conservative option. No new dependency, no new failure mode. |
46
+ | `fast` | Ship the smallest thing that works. Note the debt you took on. |
47
+ | `askme` | Read, plan, propose. **Change nothing** until they answer. |
48
+ | `hold` | **Leave it alone.** It is on the board so it is not forgotten, not so it is done. |
49
+ | absent | No instruction. Use judgement, and prefer the smaller move. |
50
+
51
+ `explain`, `askme` and `hold` are refusals to act. Honour them exactly: doing
52
+ the work anyway is worse than doing nothing, because it spends the owner's
53
+ trust in the mechanism.
54
+
55
+ **`checks`** — the definition of done, written in advance. Tick a step only
56
+ when you have actually done it and can say how. A tick you cannot defend is a
57
+ lie in a file the owner reads on a phone.
58
+
59
+ **`risk`** — what it costs if it goes wrong, not how soon it is wanted. A
60
+ high-risk card gets the safer approach even when its intent says `fast`.
61
+
62
+ ## Read the analysis before deciding anything
63
+
64
+ `analysis.choices` holds the questions the owner has already answered, grouped
65
+ by the four screens of the Analysis view — `board`, `project`, `strategy`,
66
+ `market`. Each carries its options and a `value` naming the one chosen.
67
+
68
+ **An answer there beats your judgement**, the same way a card's `intent` does.
69
+ It was given once, deliberately, so it would not have to be given again on
70
+ every card. Read them before picking an approach, and say which one you were
71
+ following when it decided something.
72
+
73
+ **`value: null` is not missing data.** It means the owner has deliberately not
74
+ decided. Do not answer it for them, and do not act on the half of the work
75
+ that depends on it: do everything that does not, and say plainly which
76
+ question is holding the rest.
77
+
78
+ **Adding a choice with `value: null` is how you ask a question** that outlives
79
+ the conversation — the panel's bell surfaces every open one. Writing the
80
+ question is yours to do. Answering it is not: never write `value`, `decidedAt`
81
+ or `decidedBy` yourself. Those are the panel's stamps for a tap the owner
82
+ made, and a decision you recorded on their behalf is a decision nobody made.
83
+
84
+ ## Moving a card is a claim
85
+
86
+ **Into the active column when you start it, not when you plan to.** The board
87
+ stamps `startedAt` the moment it lands there, and that stamp is the evidence
88
+ the work was picked up.
89
+
90
+ **Into the done column only when the work is finished and you have checked it
91
+ does what the card says.** Merged is not finished if the thing it enables was
92
+ never exercised once. In that case the card stays where it is with a note
93
+ saying exactly what is shipped and what is unverified — or the verification
94
+ becomes a card in the owner's column, when only they can do it.
95
+
96
+ A card carrying `completedAt` with no `startedAt` was declared finished
97
+ without ever being picked up, and the panel marks it. Never create that shape.
98
+
99
+ ## Writing back
100
+
101
+ The board is a conversation. Leave it saying what happened:
102
+
103
+ - Tick the `checks` you completed.
104
+ - Move a finished card to done; move one you started to the active column.
105
+ - **Clear `order` when the order is carried out** — leaving it set means the
106
+ next session does it again.
107
+ - Add a note with `by: "claude"` and the date.
108
+ - Never delete a card. An empty untitled card is a stray tap; leave it.
109
+ - **Append one entry to `runs`**: the date, `by: "claude"`, a summary of what
110
+ the pass actually did, and the ids of the cards it touched. That list is the
111
+ account the owner reads instead of taking the claim on trust, so name only
112
+ cards you really changed.
113
+
114
+ Then commit the board with the rest of the change, so the card that moved to
115
+ done sits in the same diff as the work that finished it.
116
+
117
+ ## The theme the panel may ask you for
118
+
119
+ If the board carries `importedTheme`, it is a theme derived from *this*
120
+ project, stored with where it came from: `from.files`, `from.colours` and
121
+ `from.reasoning`. That provenance is not decoration — it is what makes
122
+ deriving it again a comparison rather than a coin toss.
123
+
124
+ `refreshRequested` is a date the owner tapped. It is a message to you, because
125
+ the panel cannot run anything: read the files it names again, map what you find
126
+ onto the tokens, write the new `tokens`, `form` and `from`, set `importedAt`
127
+ to today, and clear `refreshRequested` back to `null`. Say in `from.reasoning`
128
+ what changed and why, in a sentence or two. Leaving the request set means the
129
+ next session does it all over again.
130
+
131
+ Never invent a palette. If the project has no colours of its own, say so and
132
+ leave the theme alone — the kit ships presets for exactly that case, and one
133
+ of them is a light theme.
134
+
135
+ ## When the board will not open
136
+
137
+ Run `npx paneltir board`. It reads the file and names every problem by
138
+ location — `cards[3].column: "in-progress" is not one of the board's columns`
139
+ — which is the difference between a fix and a hunt. The same check runs on
140
+ every save, so a broken board almost always got that way from a hand edit
141
+ rather than from the panel.
142
+
143
+ Nothing repairs it for you, deliberately: a board quietly corrected is a board
144
+ whose owner never learns what changed. The file is in git, so `git diff` shows
145
+ it, and `git revert` undoes it.
146
+
147
+ ## Two things never to do
148
+
149
+ - **Never edit anything inside `node_modules/paneltir`.** Not even when asked
150
+ to "change the dashboard": that phrasing almost always means the content or
151
+ the data, not the library. A change to the kit itself belongs upstream and
152
+ arrives here as a new version.
153
+ - **Never put a secret in the board.** Passwords and tokens live in the
154
+ hosting environment. The board is a file in the repository, with a diff and
155
+ a history, and everything written into it stays there.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Exchanges this project's panel password for a session cookie.
3
+ *
4
+ * COPIED FROM PANELTIR, AND NOW YOURS.
5
+ *
6
+ * This is the only route that accepts the password, and it is reachable
7
+ * without a session — signing in cannot require being signed in. It answers
8
+ * with the same message and the same shape whether the password was wrong or
9
+ * the account name was, so a caller learns only "not this".
10
+ */
11
+ import { env, issueToken, safeEqual, sessionCookie } from '../lib/session'
12
+
13
+ export const config = { runtime: 'edge' }
14
+
15
+ /** Always the same wording, whatever was wrong about the attempt. */
16
+ function rejected(): Response {
17
+ return new Response(JSON.stringify({ error: 'That password is not right.' }), {
18
+ status: 401,
19
+ headers: { 'content-type': 'application/json', 'Cache-Control': 'no-store' },
20
+ })
21
+ }
22
+
23
+ export default async function handler(request: Request): Promise<Response> {
24
+ if (request.method !== 'POST') {
25
+ return new Response(JSON.stringify({ error: 'Use POST.' }), {
26
+ status: 405,
27
+ headers: { 'content-type': 'application/json', Allow: 'POST', 'Cache-Control': 'no-store' },
28
+ })
29
+ }
30
+
31
+ const expected = env('ADMIN_PASSWORD')
32
+ if (!expected) {
33
+ return new Response(JSON.stringify({ error: 'The panel is not configured yet.' }), {
34
+ status: 503,
35
+ headers: { 'content-type': 'application/json', 'Cache-Control': 'no-store' },
36
+ })
37
+ }
38
+
39
+ let password = ''
40
+ try {
41
+ const body = (await request.json()) as { password?: unknown }
42
+ if (typeof body?.password === 'string') password = body.password
43
+ } catch {
44
+ return rejected()
45
+ }
46
+
47
+ // Runs even for an empty password, so the timing does not separate
48
+ // "nothing sent" from "wrong password".
49
+ if (!safeEqual(password, expected)) return rejected()
50
+
51
+ const token = await issueToken()
52
+ if (!token) return rejected()
53
+
54
+ const secure = new URL(request.url).protocol === 'https:'
55
+ return new Response(JSON.stringify({ ok: true }), {
56
+ status: 200,
57
+ headers: {
58
+ 'content-type': 'application/json',
59
+ 'set-cookie': sessionCookie(token, { secure }),
60
+ 'Cache-Control': 'no-store',
61
+ },
62
+ })
63
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Ends the session by expiring the cookie.
3
+ *
4
+ * COPIED FROM PANELTIR, AND NOW YOURS.
5
+ *
6
+ * It answers the same way whether there was a session or not: there is
7
+ * nothing to report, and nothing to protect, in being told you were already
8
+ * signed out.
9
+ */
10
+ import { clearedCookie } from '../lib/session'
11
+
12
+ export const config = { runtime: 'edge' }
13
+
14
+ export default async function handler(request: Request): Promise<Response> {
15
+ if (request.method !== 'POST') {
16
+ return new Response(JSON.stringify({ error: 'Use POST.' }), {
17
+ status: 405,
18
+ headers: { 'content-type': 'application/json', Allow: 'POST', 'Cache-Control': 'no-store' },
19
+ })
20
+ }
21
+
22
+ const secure = new URL(request.url).protocol === 'https:'
23
+ return new Response(JSON.stringify({ ok: true }), {
24
+ status: 200,
25
+ headers: {
26
+ 'content-type': 'application/json',
27
+ 'set-cookie': clearedCookie({ secure }),
28
+ 'Cache-Control': 'no-store',
29
+ },
30
+ })
31
+ }