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/CLAUDE_MD_SNIPPET.md +66 -0
- package/INSTALL.md +261 -0
- package/LICENSE +90 -0
- package/README.md +404 -0
- package/bin/paneltir.mjs +331 -0
- package/dist/index.d.ts +1489 -0
- package/dist/index.js +3754 -0
- package/dist/style.css +1973 -0
- package/dist/style.d.ts +2 -0
- package/fingerprint.json +7 -0
- package/package.json +94 -0
- package/renovate.json.example +13 -0
- package/template/.claude/skills/panel/SKILL.md +155 -0
- package/template/api/login.ts +63 -0
- package/template/api/logout.ts +31 -0
- package/template/api/panel-state.ts +317 -0
- package/template/lib/session.ts +170 -0
- package/template/middleware.ts +175 -0
- package/template/src/data/panel-state.json +83 -0
package/dist/style.d.ts
ADDED
package/fingerprint.json
ADDED
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
|
+
}
|