paneltir 0.10.0 → 0.11.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/INSTALL.md +2 -2
- package/README.md +1 -1
- package/bin/paneltir.mjs +81 -8
- package/dist/index.d.ts +50 -1
- package/dist/index.js +61 -17
- package/dist/style.css +22 -0
- package/fingerprint.json +4 -4
- package/package.json +9 -23
- package/template/api/panel-state.ts +23 -3
package/INSTALL.md
CHANGED
|
@@ -19,7 +19,7 @@ identity** — never another project's.
|
|
|
19
19
|
npm install paneltir
|
|
20
20
|
|
|
21
21
|
# or, without npm access, straight from the repository:
|
|
22
|
-
# npm install github:daifukus/paneltir#v0.
|
|
22
|
+
# npm install github:daifukus/paneltir#v0.11.0
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
(Use the tag you were given; never install without pinning a version.)
|
|
@@ -237,7 +237,7 @@ identity** — never another project's.
|
|
|
237
237
|
|
|
238
238
|
```bash
|
|
239
239
|
npx paneltir version # version, fingerprint, where it came from
|
|
240
|
-
npx paneltir check 0.
|
|
240
|
+
npx paneltir check 0.11.0 # exits non-zero if that is not what is installed
|
|
241
241
|
npx paneltir board # read the board and say what is wrong with it
|
|
242
242
|
```
|
|
243
243
|
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ tag rather than a branch, so an unfinished push cannot reach a project:
|
|
|
54
54
|
npm install paneltir
|
|
55
55
|
|
|
56
56
|
# or, without npm access, straight from the repository:
|
|
57
|
-
# npm install github:daifukus/paneltir#v0.
|
|
57
|
+
# npm install github:daifukus/paneltir#v0.11.0
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Installing from Git runs the `prepare` script, which builds `dist/`, so the
|
package/bin/paneltir.mjs
CHANGED
|
@@ -130,10 +130,35 @@ function writeStamp(target, templateFiles, written) {
|
|
|
130
130
|
const files = { ...(previous?.files ?? {}) }
|
|
131
131
|
for (const file of written) files[file] = sha256(join(target, file))
|
|
132
132
|
|
|
133
|
+
/*
|
|
134
|
+
* The headline only advances when this run actually rewrote the whole gate.
|
|
135
|
+
*
|
|
136
|
+
* It used to advance on every run, which quietly defeated the entire point.
|
|
137
|
+
* A project on v0.9.0, a template changed in v0.10.0, `npm update`, then a
|
|
138
|
+
* re-run of `init` — which writes nothing, because the files are already
|
|
139
|
+
* there — and the stamp said the gate came from v0.10.0 while the files on
|
|
140
|
+
* disk were still v0.9.0's. `doctor` then printed "The template has not
|
|
141
|
+
* changed since" directly above a row reading BEHIND, and the panel, which
|
|
142
|
+
* has only this hash to go on, said the gate was current. Reproduced before
|
|
143
|
+
* fixing.
|
|
144
|
+
*
|
|
145
|
+
* The board is excluded because it is never rewritten: requiring it would
|
|
146
|
+
* mean the headline never advanced at all after the first install.
|
|
147
|
+
*
|
|
148
|
+
* When the gate is mixed — some files written, some left — the previous
|
|
149
|
+
* answer stands. That can over-report staleness for a partly updated gate,
|
|
150
|
+
* and it is the safe direction: `doctor`'s per-file rows carry the truth,
|
|
151
|
+
* and over-reporting sends somebody to read a diff while under-reporting
|
|
152
|
+
* sends them nowhere at all.
|
|
153
|
+
*/
|
|
154
|
+
const gateFiles = templateFiles.filter((file) => file !== BOARD_SEED)
|
|
155
|
+
const wholeGate = gateFiles.length > 0 && gateFiles.every((file) => written.includes(file))
|
|
156
|
+
|
|
133
157
|
const stamp = {
|
|
134
|
-
version: pkg.version,
|
|
135
|
-
templateHash: fingerprint.templateHash ?? '',
|
|
136
|
-
|
|
158
|
+
version: wholeGate ? pkg.version : (previous?.version ?? pkg.version),
|
|
159
|
+
templateHash: wholeGate ? (fingerprint.templateHash ?? '') : (previous?.templateHash ?? ''),
|
|
160
|
+
// When this gate was last written, not when this command last ran.
|
|
161
|
+
copiedAt: written.length ? new Date().toISOString() : (previous?.copiedAt ?? new Date().toISOString()),
|
|
137
162
|
files,
|
|
138
163
|
}
|
|
139
164
|
try {
|
|
@@ -513,9 +538,33 @@ function doctor() {
|
|
|
513
538
|
* behalf would be the second thing in this project writing to their
|
|
514
539
|
* repository.
|
|
515
540
|
*/
|
|
541
|
+
/**
|
|
542
|
+
* One flag, in either form, because the usage line prints the space one.
|
|
543
|
+
*
|
|
544
|
+
* It only read `--cards=a,b`, while its own error message printed
|
|
545
|
+
* `[--cards a,b]` — so the documented form took `a,b` as the board path and
|
|
546
|
+
* failed with "no board at a,b", and `--by you` was dropped silently, which is
|
|
547
|
+
* worse: the run was attributed to Claude when somebody said it was theirs.
|
|
548
|
+
*/
|
|
549
|
+
function flag(args, name) {
|
|
550
|
+
const joined = args.find((a) => a.startsWith(`--${name}=`))
|
|
551
|
+
if (joined) return joined.slice(name.length + 3)
|
|
552
|
+
const at = args.indexOf(`--${name}`)
|
|
553
|
+
if (at !== -1 && typeof args[at + 1] === 'string' && !args[at + 1].startsWith('--')) return args[at + 1]
|
|
554
|
+
return undefined
|
|
555
|
+
}
|
|
556
|
+
|
|
516
557
|
async function run(args) {
|
|
517
|
-
|
|
518
|
-
const
|
|
558
|
+
// A flag's value is not a positional argument, whichever form it came in.
|
|
559
|
+
const taken = new Set()
|
|
560
|
+
for (const name of ['cards', 'by']) {
|
|
561
|
+
const at = args.indexOf(`--${name}`)
|
|
562
|
+
if (at !== -1) {
|
|
563
|
+
taken.add(at)
|
|
564
|
+
if (typeof args[at + 1] === 'string' && !args[at + 1].startsWith('--')) taken.add(at + 1)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const plain = args.filter((a, i) => !a.startsWith('--') && !taken.has(i))
|
|
519
568
|
const summary = plain[0]
|
|
520
569
|
const file = plain[1] ?? BOARD_SEED
|
|
521
570
|
|
|
@@ -543,7 +592,7 @@ async function run(args) {
|
|
|
543
592
|
}
|
|
544
593
|
|
|
545
594
|
const board = check.board
|
|
546
|
-
const cards = (
|
|
595
|
+
const cards = (flag(args, 'cards') ?? '')
|
|
547
596
|
.split(',')
|
|
548
597
|
.map((id) => id.trim())
|
|
549
598
|
.filter(Boolean)
|
|
@@ -557,10 +606,15 @@ async function run(args) {
|
|
|
557
606
|
process.exit(1)
|
|
558
607
|
}
|
|
559
608
|
|
|
560
|
-
const by =
|
|
609
|
+
const by = flag(args, 'by') === 'you' ? 'you' : 'claude'
|
|
561
610
|
const date = new Date().toISOString().slice(0, 10)
|
|
562
611
|
board.runs = Array.isArray(board.runs) ? board.runs : []
|
|
563
|
-
|
|
612
|
+
// Appended, not prepended: `runs` is stored oldest-first and every reader
|
|
613
|
+
// reverses it. `unshift` put a new pass at the *oldest* position, so it
|
|
614
|
+
// rendered at the bottom of the revisions log and fell outside the bell's
|
|
615
|
+
// newest-first window — a record of work that read as the first thing ever
|
|
616
|
+
// done here.
|
|
617
|
+
board.runs.push({
|
|
564
618
|
id: `run-${date}-${String(board.runs.length + 1).padStart(3, '0')}`,
|
|
565
619
|
date,
|
|
566
620
|
by,
|
|
@@ -569,8 +623,27 @@ async function run(args) {
|
|
|
569
623
|
fingerprint: shortHash,
|
|
570
624
|
})
|
|
571
625
|
|
|
626
|
+
/*
|
|
627
|
+
* Bounded here, because here is where it grows.
|
|
628
|
+
*
|
|
629
|
+
* The validators used to refuse a board with more than 200 runs while
|
|
630
|
+
* nothing anywhere trimmed them, so the only reachable outcome was a Save
|
|
631
|
+
* that broke permanently on the two-hundredth pass and said "Too many runs".
|
|
632
|
+
* A bound belongs where the list is appended, not where it is read: trimming
|
|
633
|
+
* on the write path would commit a board shorter than the one the panel is
|
|
634
|
+
* holding, and the panel would read as saved while the repository disagreed.
|
|
635
|
+
*
|
|
636
|
+
* The oldest go, since `runs` is oldest-first. Three hundred is about a
|
|
637
|
+
* decade of daily passes, and the diff of a list that only grows grows with
|
|
638
|
+
* it.
|
|
639
|
+
*/
|
|
640
|
+
const KEEP_RUNS = 300
|
|
641
|
+
const dropped = Math.max(0, board.runs.length - KEEP_RUNS)
|
|
642
|
+
if (dropped) board.runs = board.runs.slice(dropped)
|
|
643
|
+
|
|
572
644
|
writeFileSync(path, JSON.stringify(board, null, 2) + '\n')
|
|
573
645
|
console.log(`Appended to ${file}: ${summary}`)
|
|
646
|
+
if (dropped) console.log(` dropped the ${dropped} oldest run(s), keeping the last ${KEEP_RUNS}`)
|
|
574
647
|
console.log(` ${cards.length ? `cards ${cards.join(', ')}` : 'no cards named'} · by ${by} · ${shortHash}`)
|
|
575
648
|
console.log('\nThe file is changed and not committed. The panel commits it on Save,')
|
|
576
649
|
console.log('or commit it yourself — this writes nothing to your repository.')
|
package/dist/index.d.ts
CHANGED
|
@@ -284,6 +284,14 @@ interface BoardColumnData<TCard extends BoardCardData = BoardCardData> {
|
|
|
284
284
|
*/
|
|
285
285
|
hint?: string;
|
|
286
286
|
cards: TCard[];
|
|
287
|
+
/**
|
|
288
|
+
* Drawn under the cards, inside the column's own scroll.
|
|
289
|
+
*
|
|
290
|
+
* It exists for what a column has to say about cards it is *not* showing —
|
|
291
|
+
* a done column hiding what was finished last month still owes the reader a
|
|
292
|
+
* line saying so, or the cards have not been hidden, they have been lost.
|
|
293
|
+
*/
|
|
294
|
+
footer?: React.ReactNode;
|
|
287
295
|
}
|
|
288
296
|
interface BoardMoveResult {
|
|
289
297
|
cardId: string;
|
|
@@ -521,6 +529,18 @@ interface SetupRequirement {
|
|
|
521
529
|
enables?: React.ReactNode;
|
|
522
530
|
/** Exactly what to do, shown only when it is missing. */
|
|
523
531
|
fix?: React.ReactNode;
|
|
532
|
+
/**
|
|
533
|
+
* Information rather than a gate: drawn in the list, never walked as a step.
|
|
534
|
+
*
|
|
535
|
+
* Every other requirement here is an environment variable, and the
|
|
536
|
+
* walkthrough's one action — re-read the environment from the server —
|
|
537
|
+
* is what clears it. A requirement that action cannot clear parks the
|
|
538
|
+
* walkthrough on it for ever, and because `unknown` is judged across the
|
|
539
|
+
* whole list, one advisory row that nobody wired made every step answer a
|
|
540
|
+
* successful check with "the panel could not ask its own server". That is
|
|
541
|
+
* what the copied gate did the day it was added.
|
|
542
|
+
*/
|
|
543
|
+
advisory?: boolean;
|
|
524
544
|
}
|
|
525
545
|
interface SetupGuideProps {
|
|
526
546
|
requirements: SetupRequirement[];
|
|
@@ -1043,6 +1063,32 @@ declare function newId(): string;
|
|
|
1043
1063
|
declare function today(): string;
|
|
1044
1064
|
/** A card is created empty and named second — the same as tapping "+" on paper. */
|
|
1045
1065
|
declare function emptyCard(column: string, area: string): Card;
|
|
1066
|
+
/**
|
|
1067
|
+
* How long a finished card stays on the board before it becomes history.
|
|
1068
|
+
*
|
|
1069
|
+
* A done column only grows. It is the one column nothing ever removes from,
|
|
1070
|
+
* so a board that is being worked turns into a wall of things already
|
|
1071
|
+
* finished, and the three columns that need reading get a third of the screen
|
|
1072
|
+
* between them. A week is the window in which "what did we ship" is still a
|
|
1073
|
+
* live question; after that it is a record, and a record does not need to be
|
|
1074
|
+
* in the way.
|
|
1075
|
+
*/
|
|
1076
|
+
declare const ARCHIVE_AFTER_DAYS = 7;
|
|
1077
|
+
/**
|
|
1078
|
+
* Whether a finished card has been finished long enough to stop being drawn.
|
|
1079
|
+
*
|
|
1080
|
+
* Derived on every render and never written, which is the arrangement
|
|
1081
|
+
* `boardFacts()` already uses: a flag stored on the card would be wrong the
|
|
1082
|
+
* day after it was set, and setting it would mark the panel dirty for the
|
|
1083
|
+
* passage of time. Here the answer simply becomes true on its own.
|
|
1084
|
+
*
|
|
1085
|
+
* **A card with no `completedAt` is never archived**, whatever column it sits
|
|
1086
|
+
* in. It reached done without being stamped — a board edited by hand, or one
|
|
1087
|
+
* from before the stamp existed — so its age is not known, and hiding a card
|
|
1088
|
+
* whose age nobody knows is the same mistake as reporting a token missing
|
|
1089
|
+
* because the check could not run. `unknown` is not `old`.
|
|
1090
|
+
*/
|
|
1091
|
+
declare function isArchived(card: Card, doneColumns: ReadonlySet<string>, now?: string, afterDays?: number): boolean;
|
|
1046
1092
|
/**
|
|
1047
1093
|
* Applies the column's own meaning to a card that just moved into it: a card
|
|
1048
1094
|
* entering the working column is started, one reaching a done column is
|
|
@@ -1374,6 +1420,9 @@ interface UiStrings {
|
|
|
1374
1420
|
needTokenEnables: string;
|
|
1375
1421
|
needRepo: string;
|
|
1376
1422
|
needRepoEnables: string;
|
|
1423
|
+
archivedOne: string;
|
|
1424
|
+
archivedMany: string;
|
|
1425
|
+
archivedHide: string;
|
|
1377
1426
|
needGate: string;
|
|
1378
1427
|
needGateEnables: string;
|
|
1379
1428
|
fixGate: string;
|
|
@@ -1835,4 +1884,4 @@ interface CardSheetProps {
|
|
|
1835
1884
|
}
|
|
1836
1885
|
declare function CardSheet({ card, state, lang, ui, onChange, onDelete, onClose }: CardSheetProps): React.JSX.Element | null;
|
|
1837
1886
|
|
|
1838
|
-
export { ANALYSIS_SECTIONS, AlertBanner, type AlertBannerProps, type Analysis, type AnalysisSection, AnalysisView, BOARD_VERSION, Board, type BoardCardData, type BoardCheck, type BoardColumnData, type BoardFacts, type BoardMoveResult, type BoardProblem, type BoardProps, type BoardTag, CAPABILITIES, type Capability, Card$1 as Card, type CardProps, CardSheet, type Check, type Choice, type ChoiceOption, type ColumnDef, type Competitor, type Constraint, CopyBlock, type CopyBlockLabels, type CopyBlockProps, type Counts, DashboardHeader, type DashboardHeaderProps, type DashboardThemeForm, DashboardThemeProvider, type DashboardThemeTokens, DetailSection, type DetailSectionProps, DetailSheet, type DetailSheetProps, FilterChip, type FilterChipProps, FilterChipRow, type GateReading, type GateStamp, type GateVerdict, type Goal, GuideNote, type GuideNoteProps, GuideTour, type GuideTourProps, type HealthFact, HealthPill, type HealthPillProps, HealthPillRow, type HealthStatus, INTENTS, INTENT_COPY, type ImportedTheme, type InstallState, type Intent, type IntentCopy, LANGS, LANG_LABELS, LEVELS, type Lang, type Level, MARKETPLACE_NAME, MARKETPLACE_URL, type Market, type MarketplaceReport, type MarketplaceTool, MarketplaceView, type Metric, type Note, NotificationBell, type NotificationBellProps, type NotificationLabels, type NotificationTone, OWNERS, type Owner, PANELTIR_FILE_COUNT, PANELTIR_FINGERPRINT, PANELTIR_TEMPLATE_HASH, PANELTIR_VERSION, PRESET_PANEL_THEMES, PanelApp, type PanelAppProps, type Card as PanelCard, type PanelNotification, type PanelState, type PanelTheme, type PanelThemes, type Preferences, type Reading, type Run, SetupGuide, type SetupGuideProps, type SetupRequirement, type SetupStatus, SetupWizard, type SetupWizardLabels, type SetupWizardProps, type Severity, StatTile, StatTileGrid, type StatTileProps, type Strategy, type Suggestion, type SuggestionStatus, THEME_FORMS, THEME_PRESETS, type Text, type ThemeFormName, type ThemePresetName, type Trend, UI, type UiStrings, type Weight, type WizardStep, applyMove, boardFacts, checkProgress, claimedWithoutStarting, claudeTheme, commandSnippet, countCards, cyberpunkTheme, decided, emptyCard, explainBoard, gateStatus, isCard, isFirstRun, ledgerForm, midnightTheme, moveCardInColumns, newId, offeredThemes, oldMoneyTheme, panelForm, paneltirBuild, paperForm, readBoard, readStoredLang, repositorySnippet, resetGuide, sparkPoints, stampForColumn, storeLang, text, today, trendOf, undecided, untranslated, useDashboardForm, useDashboardTheme, useDelegatedClick, useGuideNote, useMediaQuery, validateBoard, wizardStep, writeText };
|
|
1887
|
+
export { ANALYSIS_SECTIONS, ARCHIVE_AFTER_DAYS, AlertBanner, type AlertBannerProps, type Analysis, type AnalysisSection, AnalysisView, BOARD_VERSION, Board, type BoardCardData, type BoardCheck, type BoardColumnData, type BoardFacts, type BoardMoveResult, type BoardProblem, type BoardProps, type BoardTag, CAPABILITIES, type Capability, Card$1 as Card, type CardProps, CardSheet, type Check, type Choice, type ChoiceOption, type ColumnDef, type Competitor, type Constraint, CopyBlock, type CopyBlockLabels, type CopyBlockProps, type Counts, DashboardHeader, type DashboardHeaderProps, type DashboardThemeForm, DashboardThemeProvider, type DashboardThemeTokens, DetailSection, type DetailSectionProps, DetailSheet, type DetailSheetProps, FilterChip, type FilterChipProps, FilterChipRow, type GateReading, type GateStamp, type GateVerdict, type Goal, GuideNote, type GuideNoteProps, GuideTour, type GuideTourProps, type HealthFact, HealthPill, type HealthPillProps, HealthPillRow, type HealthStatus, INTENTS, INTENT_COPY, type ImportedTheme, type InstallState, type Intent, type IntentCopy, LANGS, LANG_LABELS, LEVELS, type Lang, type Level, MARKETPLACE_NAME, MARKETPLACE_URL, type Market, type MarketplaceReport, type MarketplaceTool, MarketplaceView, type Metric, type Note, NotificationBell, type NotificationBellProps, type NotificationLabels, type NotificationTone, OWNERS, type Owner, PANELTIR_FILE_COUNT, PANELTIR_FINGERPRINT, PANELTIR_TEMPLATE_HASH, PANELTIR_VERSION, PRESET_PANEL_THEMES, PanelApp, type PanelAppProps, type Card as PanelCard, type PanelNotification, type PanelState, type PanelTheme, type PanelThemes, type Preferences, type Reading, type Run, SetupGuide, type SetupGuideProps, type SetupRequirement, type SetupStatus, SetupWizard, type SetupWizardLabels, type SetupWizardProps, type Severity, StatTile, StatTileGrid, type StatTileProps, type Strategy, type Suggestion, type SuggestionStatus, THEME_FORMS, THEME_PRESETS, type Text, type ThemeFormName, type ThemePresetName, type Trend, UI, type UiStrings, type Weight, type WizardStep, applyMove, boardFacts, checkProgress, claimedWithoutStarting, claudeTheme, commandSnippet, countCards, cyberpunkTheme, decided, emptyCard, explainBoard, gateStatus, isArchived, isCard, isFirstRun, ledgerForm, midnightTheme, moveCardInColumns, newId, offeredThemes, oldMoneyTheme, panelForm, paneltirBuild, paperForm, readBoard, readStoredLang, repositorySnippet, resetGuide, sparkPoints, stampForColumn, storeLang, text, today, trendOf, undecided, untranslated, useDashboardForm, useDashboardTheme, useDelegatedClick, useGuideNote, useMediaQuery, validateBoard, wizardStep, writeText };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var PANELTIR_VERSION = "0.
|
|
3
|
-
var PANELTIR_FINGERPRINT = "sha256:
|
|
2
|
+
var PANELTIR_VERSION = "0.11.0";
|
|
3
|
+
var PANELTIR_FINGERPRINT = "sha256:2739462e763939de1d1aeb8ab97779149c2ddb0ec2d2fb45644c4c45ef9dc9fa";
|
|
4
4
|
var PANELTIR_FILE_COUNT = 163;
|
|
5
|
-
var PANELTIR_TEMPLATE_HASH = "sha256:
|
|
5
|
+
var PANELTIR_TEMPLATE_HASH = "sha256:034ba27266408af4f2ba5793d9cefbe448b168570d6a0b87dfe698dfe639feb1";
|
|
6
6
|
var paneltirBuild = {
|
|
7
7
|
version: PANELTIR_VERSION,
|
|
8
8
|
fingerprint: PANELTIR_FINGERPRINT,
|
|
@@ -306,7 +306,8 @@ function Column({ column, renderCard, onAddCard }) {
|
|
|
306
306
|
renderCard(card, index, column.id)
|
|
307
307
|
] }, card.id);
|
|
308
308
|
}),
|
|
309
|
-
dnd.isDragging && dnd.dropTarget?.columnId === column.id && dnd.dropTarget.index === column.cards.length && /* @__PURE__ */ jsx8("div", { className: "pt-card-placeholder" })
|
|
309
|
+
dnd.isDragging && dnd.dropTarget?.columnId === column.id && dnd.dropTarget.index === column.cards.length && /* @__PURE__ */ jsx8("div", { className: "pt-card-placeholder" }),
|
|
310
|
+
column.footer ? /* @__PURE__ */ jsx8("div", { className: "pt-column__footer", children: column.footer }) : null
|
|
310
311
|
] })
|
|
311
312
|
] });
|
|
312
313
|
}
|
|
@@ -918,15 +919,16 @@ import { useState as useState5 } from "react";
|
|
|
918
919
|
|
|
919
920
|
// src/components/Setup/wizard.ts
|
|
920
921
|
function wizardStep(requirements) {
|
|
921
|
-
const
|
|
922
|
-
const
|
|
923
|
-
const
|
|
924
|
-
const
|
|
922
|
+
const steps = requirements.filter((requirement) => !requirement.advisory);
|
|
923
|
+
const total = steps.length;
|
|
924
|
+
const done = steps.filter((requirement) => requirement.status === "ok");
|
|
925
|
+
const unknown = steps.some((requirement) => requirement.status === "unknown");
|
|
926
|
+
const at = steps.findIndex((requirement) => requirement.status !== "ok");
|
|
925
927
|
if (at === -1) {
|
|
926
928
|
return { current: null, index: 0, total, done, ready: total > 0, unknown: false };
|
|
927
929
|
}
|
|
928
930
|
return {
|
|
929
|
-
current:
|
|
931
|
+
current: steps[at],
|
|
930
932
|
index: at + 1,
|
|
931
933
|
total,
|
|
932
934
|
done,
|
|
@@ -1147,6 +1149,18 @@ function emptyCard(column, area) {
|
|
|
1147
1149
|
completedAt: null
|
|
1148
1150
|
};
|
|
1149
1151
|
}
|
|
1152
|
+
var ARCHIVE_AFTER_DAYS = 7;
|
|
1153
|
+
function daysBefore(from, days) {
|
|
1154
|
+
const at = /* @__PURE__ */ new Date(`${from}T00:00:00Z`);
|
|
1155
|
+
at.setUTCDate(at.getUTCDate() - days);
|
|
1156
|
+
return at.toISOString().slice(0, 10);
|
|
1157
|
+
}
|
|
1158
|
+
function isArchived(card, doneColumns, now = today(), afterDays = ARCHIVE_AFTER_DAYS) {
|
|
1159
|
+
if (!doneColumns.has(card.column)) return false;
|
|
1160
|
+
const finished = card.completedAt;
|
|
1161
|
+
if (typeof finished !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(finished)) return false;
|
|
1162
|
+
return finished < daysBefore(now, afterDays);
|
|
1163
|
+
}
|
|
1150
1164
|
function stampForColumn(card, columns) {
|
|
1151
1165
|
const column = columns.find((candidate) => candidate.id === card.column);
|
|
1152
1166
|
if (!column) return card;
|
|
@@ -1614,6 +1628,9 @@ var UI = {
|
|
|
1614
1628
|
needTokenEnables: "Lets Save commit the board back to the repository. Without it everything else works and Save fails.",
|
|
1615
1629
|
needRepo: "The repository to write to",
|
|
1616
1630
|
needRepoEnables: "Which repository the board is committed to. There is no default on purpose: writing to the wrong one is worse than not writing.",
|
|
1631
|
+
archivedOne: "archived \u2014 finished over a week ago",
|
|
1632
|
+
archivedMany: "archived \u2014 finished over a week ago",
|
|
1633
|
+
archivedHide: "Hide them again",
|
|
1617
1634
|
needGate: "The gate this project copied",
|
|
1618
1635
|
needGateEnables: "The sign-in, the session and the write-back are files this project owns, copied once and never updated with the library. When one of them is older than the package, the panel does not look misconfigured \u2014 it looks broken.",
|
|
1619
1636
|
fixGate: "The gate is older than the installed kit. See what changed before replacing anything:",
|
|
@@ -1938,6 +1955,9 @@ var UI = {
|
|
|
1938
1955
|
needTokenEnables: "Permite que Guardar confirme el tablero en el repositorio. Sin \xE9l todo lo dem\xE1s funciona y Guardar falla.",
|
|
1939
1956
|
needRepo: "El repositorio donde escribir",
|
|
1940
1957
|
needRepoEnables: "En qu\xE9 repositorio se confirma el tablero. No hay valor por defecto a prop\xF3sito: escribir en el equivocado es peor que no escribir.",
|
|
1958
|
+
archivedOne: "archivada \u2014 terminada hace m\xE1s de una semana",
|
|
1959
|
+
archivedMany: "archivadas \u2014 terminadas hace m\xE1s de una semana",
|
|
1960
|
+
archivedHide: "Volver a esconderlas",
|
|
1941
1961
|
needGate: "La puerta que copi\xF3 este proyecto",
|
|
1942
1962
|
needGateEnables: "El acceso, la sesi\xF3n y la escritura de vuelta son archivos de este proyecto, copiados una vez y que no se actualizan con la librer\xEDa. Cuando uno se queda m\xE1s viejo que el paquete, el panel no parece mal configurado: parece roto.",
|
|
1943
1963
|
fixGate: "La puerta es m\xE1s vieja que el kit instalado. Mira qu\xE9 cambi\xF3 antes de reemplazar nada:",
|
|
@@ -2939,7 +2959,7 @@ function applyMove(cards, move, visible) {
|
|
|
2939
2959
|
function resolveTheme(saved, stored, themes, hasImported, fallback) {
|
|
2940
2960
|
for (const name of [saved, stored]) {
|
|
2941
2961
|
if (name === "imported" && hasImported) return "imported";
|
|
2942
|
-
if (name && name
|
|
2962
|
+
if (name && Object.prototype.hasOwnProperty.call(themes, name)) return name;
|
|
2943
2963
|
}
|
|
2944
2964
|
return fallback ?? Object.keys(themes)[0];
|
|
2945
2965
|
}
|
|
@@ -3031,6 +3051,7 @@ function PanelApp({
|
|
|
3031
3051
|
);
|
|
3032
3052
|
const [state, setState] = useState10(initialState);
|
|
3033
3053
|
const [filter, setFilter] = useState10("all");
|
|
3054
|
+
const [showArchived, setShowArchived] = useState10(() => /* @__PURE__ */ new Set());
|
|
3034
3055
|
const [openCardId, setOpenCardId] = useState10(null);
|
|
3035
3056
|
const [settingsOpen, setSettingsOpen] = useState10(false);
|
|
3036
3057
|
const [update, setUpdate] = useState10(null);
|
|
@@ -3108,6 +3129,9 @@ function PanelApp({
|
|
|
3108
3129
|
// `SetupWizard` already refuses to make.
|
|
3109
3130
|
...gate === void 0 ? [] : [{
|
|
3110
3131
|
id: "gate",
|
|
3132
|
+
// Information, not a gate: the walkthrough's only action re-reads the
|
|
3133
|
+
// environment, and this is files on disk. See SetupRequirement.
|
|
3134
|
+
advisory: true,
|
|
3111
3135
|
label: ui.needGate,
|
|
3112
3136
|
status: gateReading.verdict === "current" ? "ok" : gateReading.verdict === "behind" ? "missing" : "unknown",
|
|
3113
3137
|
enables: ui.needGateEnables,
|
|
@@ -3151,13 +3175,31 @@ function PanelApp({
|
|
|
3151
3175
|
);
|
|
3152
3176
|
const visibleCards = useMemo5(() => state.cards.filter(matches), [state.cards, matches]);
|
|
3153
3177
|
const boardColumns = useMemo5(
|
|
3154
|
-
() => state.columns.map((column) =>
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3178
|
+
() => state.columns.map((column) => {
|
|
3179
|
+
const mine = visibleCards.filter((card) => card.column === column.id);
|
|
3180
|
+
const archived = mine.filter((card) => isArchived(card, doneColumns));
|
|
3181
|
+
const showing = showArchived.has(column.id) ? mine : mine.filter((card) => !archived.includes(card));
|
|
3182
|
+
return {
|
|
3183
|
+
id: column.id,
|
|
3184
|
+
title: t(column.title),
|
|
3185
|
+
hint: column.hint ? t(column.hint) : void 0,
|
|
3186
|
+
cards: showing,
|
|
3187
|
+
footer: archived.length ? /* @__PURE__ */ jsx20(
|
|
3188
|
+
"button",
|
|
3189
|
+
{
|
|
3190
|
+
type: "button",
|
|
3191
|
+
onClick: () => setShowArchived((current) => {
|
|
3192
|
+
const next = new Set(current);
|
|
3193
|
+
if (next.has(column.id)) next.delete(column.id);
|
|
3194
|
+
else next.add(column.id);
|
|
3195
|
+
return next;
|
|
3196
|
+
}),
|
|
3197
|
+
children: showArchived.has(column.id) ? ui.archivedHide : `${archived.length} ${archived.length === 1 ? ui.archivedOne : ui.archivedMany}`
|
|
3198
|
+
}
|
|
3199
|
+
) : void 0
|
|
3200
|
+
};
|
|
3201
|
+
}),
|
|
3202
|
+
[state.columns, visibleCards, doneColumns, showArchived, t, ui]
|
|
3161
3203
|
);
|
|
3162
3204
|
const notifications = useMemo5(() => {
|
|
3163
3205
|
const items = [];
|
|
@@ -4259,6 +4301,7 @@ var CAPABILITIES = [
|
|
|
4259
4301
|
];
|
|
4260
4302
|
export {
|
|
4261
4303
|
ANALYSIS_SECTIONS,
|
|
4304
|
+
ARCHIVE_AFTER_DAYS,
|
|
4262
4305
|
AlertBanner,
|
|
4263
4306
|
AnalysisView,
|
|
4264
4307
|
BOARD_VERSION,
|
|
@@ -4312,6 +4355,7 @@ export {
|
|
|
4312
4355
|
emptyCard,
|
|
4313
4356
|
explainBoard,
|
|
4314
4357
|
gateStatus,
|
|
4358
|
+
isArchived,
|
|
4315
4359
|
isCard,
|
|
4316
4360
|
isFirstRun,
|
|
4317
4361
|
ledgerForm,
|
package/dist/style.css
CHANGED
|
@@ -594,6 +594,28 @@
|
|
|
594
594
|
background: var(--pt-color-panel);
|
|
595
595
|
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45);
|
|
596
596
|
}
|
|
597
|
+
.pt-column__footer {
|
|
598
|
+
padding-top: var(--pt-space-sm);
|
|
599
|
+
border-top: var(--pt-border-width) solid var(--pt-color-line-faint);
|
|
600
|
+
font-size: 11.5px;
|
|
601
|
+
color: var(--pt-color-ink-faint);
|
|
602
|
+
}
|
|
603
|
+
.pt-column__footer button {
|
|
604
|
+
border: none;
|
|
605
|
+
background: none;
|
|
606
|
+
padding: 0;
|
|
607
|
+
font: inherit;
|
|
608
|
+
color: inherit;
|
|
609
|
+
cursor: pointer;
|
|
610
|
+
text-decoration: underline;
|
|
611
|
+
text-underline-offset: 2px;
|
|
612
|
+
}
|
|
613
|
+
.pt-column__footer button:hover {
|
|
614
|
+
color: var(--pt-color-ink-dim);
|
|
615
|
+
}
|
|
616
|
+
.pt-column__list > * {
|
|
617
|
+
flex-shrink: 0;
|
|
618
|
+
}
|
|
597
619
|
|
|
598
620
|
/* src/components/DetailSheet/DetailSheet.css */
|
|
599
621
|
.pt-sheet-backdrop {
|
package/fingerprint.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paneltir",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"hash": "sha256:
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"hash": "sha256:2739462e763939de1d1aeb8ab97779149c2ddb0ec2d2fb45644c4c45ef9dc9fa",
|
|
5
5
|
"fileCount": 163,
|
|
6
|
-
"templateHash": "sha256:
|
|
7
|
-
"generatedAt": "2026-09-
|
|
6
|
+
"templateHash": "sha256:034ba27266408af4f2ba5793d9cefbe448b168570d6a0b87dfe698dfe639feb1",
|
|
7
|
+
"generatedAt": "2026-09-13T08:39:02.027Z"
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paneltir",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
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
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "DFKlabs (https://DFKlabs.com)",
|
|
@@ -99,42 +99,28 @@
|
|
|
99
99
|
"node": ">=20"
|
|
100
100
|
},
|
|
101
101
|
"paneltirRelease": {
|
|
102
|
-
"version": "0.
|
|
103
|
-
"date": "2026-09-
|
|
102
|
+
"version": "0.11.0",
|
|
103
|
+
"date": "2026-09-13",
|
|
104
104
|
"entries": [
|
|
105
105
|
{
|
|
106
106
|
"kind": "feature",
|
|
107
107
|
"text": {
|
|
108
|
-
"en": "
|
|
109
|
-
"es": "
|
|
108
|
+
"en": "A card finished more than a week ago stops being drawn in its column. The column says how many it is holding and one tap shows them — nothing is hidden without being accounted for.",
|
|
109
|
+
"es": "Una tarjeta terminada hace más de una semana deja de dibujarse en su columna. La columna dice cuántas guarda y un toque las enseña — nada se esconde sin quedar contado."
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"kind": "feature",
|
|
114
114
|
"text": {
|
|
115
|
-
"en": "
|
|
116
|
-
"es": "
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
"kind": "feature",
|
|
121
|
-
"text": {
|
|
122
|
-
"en": "`paneltir run \"what this pass did\"` appends to the board's revisions, so the record does not depend on anyone remembering at the end of a session.",
|
|
123
|
-
"es": "`paneltir run \"qué hizo esta pasada\"` añade una entrada a las revisiones del tablero, así el registro no depende de que alguien se acuerde al final de la sesión."
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
"kind": "fix",
|
|
128
|
-
"text": {
|
|
129
|
-
"en": "`paneltir init` and `paneltir doctor` no longer need React installed. They loaded the component bundle at startup, so they failed in a project that had not installed it yet — which is exactly when `init` is run.",
|
|
130
|
-
"es": "`paneltir init` y `paneltir doctor` ya no necesitan React instalado. Cargaban el bundle de componentes al arrancar, así que fallaban en un proyecto que aún no lo tenía — que es justo cuando se ejecuta `init`."
|
|
115
|
+
"en": "A board column can carry a footer, drawn inside its own scroll so it takes no height from the cards.",
|
|
116
|
+
"es": "Una columna del tablero puede llevar un pie, dibujado dentro de su propio scroll para que no le quite alto a las tarjetas."
|
|
131
117
|
}
|
|
132
118
|
},
|
|
133
119
|
{
|
|
134
120
|
"kind": "chore",
|
|
135
121
|
"text": {
|
|
136
|
-
"en": "
|
|
137
|
-
"es": "
|
|
122
|
+
"en": "A card in a scrolling column can never shrink below its own content, so a title can never be cut in half.",
|
|
123
|
+
"es": "Una tarjeta en una columna con scroll no puede encogerse por debajo de su contenido, así que un título no puede quedar cortado por la mitad."
|
|
138
124
|
}
|
|
139
125
|
}
|
|
140
126
|
]
|
|
@@ -121,8 +121,22 @@ function readCookie(header: string | null | undefined, name: string): string | u
|
|
|
121
121
|
* Where the board lives in this repository. Set PANEL_FILE to override; the
|
|
122
122
|
* default is only a guess at a sensible place, and a wrong path here fails
|
|
123
123
|
* loudly on the first save rather than quietly writing somewhere else.
|
|
124
|
+
*
|
|
125
|
+
* A function, and not a constant, for the reason `env()` above is wrapped: a
|
|
126
|
+
* bare `process.env.X` is a ReferenceError wherever `process` is undefined,
|
|
127
|
+
* and at module scope it runs *before* the handler's `try` — so it cannot be
|
|
128
|
+
* caught and the platform answers `FUNCTION_INVOCATION_FAILED`, a code that
|
|
129
|
+
* names the shape of the fault and never the fault. That is the exact failure
|
|
130
|
+
* this file has already had twice, from an import and then from a throw ahead
|
|
131
|
+
* of the wrapper. This was the last module-scope read left in any gate file.
|
|
124
132
|
*/
|
|
125
|
-
|
|
133
|
+
function filePath(): string {
|
|
134
|
+
try {
|
|
135
|
+
return process.env.PANEL_FILE || 'src/data/panel-state.json'
|
|
136
|
+
} catch {
|
|
137
|
+
return 'src/data/panel-state.json'
|
|
138
|
+
}
|
|
139
|
+
}
|
|
126
140
|
const MAX_BODY_BYTES = 512 * 1024
|
|
127
141
|
|
|
128
142
|
interface VercelRequest {
|
|
@@ -277,6 +291,12 @@ function invalidState(state: unknown): string | null {
|
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
if (!Array.isArray(state.runs)) say('runs', 'must be an array')
|
|
294
|
+
// Deliberately no cap. The shape is checked here; how long a log may get is
|
|
295
|
+
// not a shape, and refusing a board for having too much history is a panel
|
|
296
|
+
// that stops saving one day and never says why. This copy used to refuse
|
|
297
|
+
// over 200 runs while nothing anywhere trimmed them, so the only outcome was
|
|
298
|
+
// a Save that broke permanently on the two-hundredth pass. Bounded on write
|
|
299
|
+
// instead, where a bound can be applied rather than only enforced.
|
|
280
300
|
|
|
281
301
|
return problems.length ? problems.join('; ') : null
|
|
282
302
|
}
|
|
@@ -302,7 +322,7 @@ function configuration() {
|
|
|
302
322
|
// is how a misconfigured target gets noticed before it is written to.
|
|
303
323
|
repo: repo ?? null,
|
|
304
324
|
branch: process.env.PANEL_BRANCH || 'main',
|
|
305
|
-
file:
|
|
325
|
+
file: filePath(),
|
|
306
326
|
}
|
|
307
327
|
}
|
|
308
328
|
|
|
@@ -373,7 +393,7 @@ async function handleRequest(request: VercelRequest, response: VercelResponse) {
|
|
|
373
393
|
? payload.message.trim().slice(0, 120)
|
|
374
394
|
: 'chore(panel): update the board from the panel'
|
|
375
395
|
|
|
376
|
-
const api = `https://api.github.com/repos/${repo}/contents/${
|
|
396
|
+
const api = `https://api.github.com/repos/${repo}/contents/${filePath()}`
|
|
377
397
|
const githubHeaders = {
|
|
378
398
|
authorization: `Bearer ${token}`,
|
|
379
399
|
accept: 'application/vnd.github+json',
|