paneltir 0.12.0 → 0.12.1
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 +9 -0
- package/INSTALL.md +2 -2
- package/README.md +1 -1
- package/bin/paneltir.mjs +18 -1
- package/dist/index.d.ts +46 -1
- package/dist/index.js +57 -26
- package/fingerprint.json +4 -4
- package/package.json +34 -20
- package/template/.claude/skills/panel/SKILL.md +26 -0
package/CLAUDE_MD_SNIPPET.md
CHANGED
|
@@ -68,6 +68,15 @@ library shared with other projects. Rules:
|
|
|
68
68
|
orders waiting on Claude, each as a count that filters the board or opens the
|
|
69
69
|
question when tapped — beside the health pills the project supplies. Nothing
|
|
70
70
|
to do there draws one sentence instead of a row of noughts.
|
|
71
|
+
- **A second problem found mid-card is fixed or filed, never a reason to
|
|
72
|
+
stop.** Small, reversible and inside what was already being touched: fixed in
|
|
73
|
+
the same pass. Anything larger becomes a card in `ideas` and the original work
|
|
74
|
+
carries on. And a detail a card does not specify is Claude's to choose — that
|
|
75
|
+
is the normal state of a card rather than a gap in it. Only `askme`, `explain`
|
|
76
|
+
and `hold` mean stop, and an undecided question holds back only the part of
|
|
77
|
+
the work that depends on it. A question that needs the owner is asked on the
|
|
78
|
+
board as a choice with `value: null`, where it outlives the conversation,
|
|
79
|
+
never as a session that stalls waiting for a reply.
|
|
71
80
|
- **The panel explains itself once.** New readers get a short guided tour the
|
|
72
81
|
first time they open the board; each note is dismissed and stays dismissed.
|
|
73
82
|
**Help** in the header reports what the panel needs — the password, the
|
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.12.
|
|
22
|
+
# npm install github:daifukus/paneltir#v0.12.1
|
|
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.12.
|
|
240
|
+
npx paneltir check 0.12.1 # 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.12.
|
|
57
|
+
# npm install github:daifukus/paneltir#v0.12.1
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Installing from Git runs the `prepare` script, which builds `dist/`, so the
|
package/bin/paneltir.mjs
CHANGED
|
@@ -614,8 +614,25 @@ async function run(args) {
|
|
|
614
614
|
// rendered at the bottom of the revisions log and fell outside the bell's
|
|
615
615
|
// newest-first window — a record of work that read as the first thing ever
|
|
616
616
|
// done here.
|
|
617
|
+
/*
|
|
618
|
+
* An id the board does not already have. The id used to be `run-<date>-<n>`
|
|
619
|
+
* with `n` the list's length, which is unique only while the list only ever
|
|
620
|
+
* grows: once it reaches the cap below and is trimmed, the length stops
|
|
621
|
+
* moving and every pass on the same day gets the same id. Two runs sharing
|
|
622
|
+
* an id is two React keys colliding in the revisions log, and one bell entry
|
|
623
|
+
* standing for both — mark the first seen and the second is silenced too,
|
|
624
|
+
* which is the fault `PanelNotification.id` warns about by name.
|
|
625
|
+
*/
|
|
626
|
+
const used = new Set(board.runs.map((run) => run?.id))
|
|
627
|
+
let serial = board.runs.length + 1
|
|
628
|
+
let id = `run-${date}-${String(serial).padStart(3, '0')}`
|
|
629
|
+
while (used.has(id)) {
|
|
630
|
+
serial += 1
|
|
631
|
+
id = `run-${date}-${String(serial).padStart(3, '0')}`
|
|
632
|
+
}
|
|
633
|
+
|
|
617
634
|
board.runs.push({
|
|
618
|
-
id
|
|
635
|
+
id,
|
|
619
636
|
date,
|
|
620
637
|
by,
|
|
621
638
|
summary,
|
package/dist/index.d.ts
CHANGED
|
@@ -378,6 +378,12 @@ interface BoardMoveResult {
|
|
|
378
378
|
cardId: string;
|
|
379
379
|
fromColumnId: string;
|
|
380
380
|
toColumnId: string;
|
|
381
|
+
/**
|
|
382
|
+
* Where to insert the card in the destination column *once it has been
|
|
383
|
+
* taken out of wherever it was*. So a reducer removes the card and then
|
|
384
|
+
* inserts at exactly this index, with no arithmetic of its own — the
|
|
385
|
+
* adjustment for a card moving down its own column is already made.
|
|
386
|
+
*/
|
|
381
387
|
toIndex: number;
|
|
382
388
|
}
|
|
383
389
|
|
|
@@ -423,6 +429,28 @@ declare function Card$1({ cardId, columnId, index, title, priorityColor, tags, p
|
|
|
423
429
|
*/
|
|
424
430
|
declare function moveCardInColumns<TCard extends BoardCardData>(columns: BoardColumnData<TCard>[], move: BoardMoveResult): BoardColumnData<TCard>[];
|
|
425
431
|
|
|
432
|
+
interface DropTarget {
|
|
433
|
+
columnId: string;
|
|
434
|
+
index: number;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* What `onMove` is told, given where the drag began and where it ended.
|
|
438
|
+
*
|
|
439
|
+
* Both are rendered positions: the source card stays in its slot at reduced
|
|
440
|
+
* opacity while it is dragged, so `data-pt-card-index` counts it, and the
|
|
441
|
+
* placeholder is drawn *before* the card at `target.index`. `toIndex` is a
|
|
442
|
+
* different thing — the position to insert at once the card is taken out —
|
|
443
|
+
* and the two differ by exactly one whenever a card moves down its own
|
|
444
|
+
* column. Neither reducer allowed for that, so a card dropped before the
|
|
445
|
+
* fourth card landed after it: one slot lower than the placeholder had
|
|
446
|
+
* promised, every time, in both copies of the arithmetic. Adjusted here, once,
|
|
447
|
+
* where the numbers are still known to be rendered ones.
|
|
448
|
+
*
|
|
449
|
+
* `null` means the drop is where the card already was — its own slot, or the
|
|
450
|
+
* slot immediately after it, which is the same place once it is removed.
|
|
451
|
+
*/
|
|
452
|
+
declare function dropIndex(source: DropTarget, target: DropTarget): number | null;
|
|
453
|
+
|
|
426
454
|
interface DetailSheetProps {
|
|
427
455
|
open: boolean;
|
|
428
456
|
onClose: () => void;
|
|
@@ -1176,6 +1204,23 @@ declare function isArchived(card: Card, doneColumns: ReadonlySet<string>, now?:
|
|
|
1176
1204
|
* finished, and one dragged back out of done is not finished any more.
|
|
1177
1205
|
*/
|
|
1178
1206
|
declare function stampForColumn(card: Card, columns: ColumnDef[]): Card;
|
|
1207
|
+
/**
|
|
1208
|
+
* Where a card the panel writes on the reader's behalf goes.
|
|
1209
|
+
*
|
|
1210
|
+
* The update card and the four "ask for this again" cards used to be born in
|
|
1211
|
+
* a column literally called `next`. The default board has one; a board whose
|
|
1212
|
+
* owner renamed it does not, and a card in a column that does not exist is
|
|
1213
|
+
* the exact fault `validateBoard` names — drawn nowhere, and refused by the
|
|
1214
|
+
* write path, so one tap on a button turned a saveable board into one that
|
|
1215
|
+
* could not be saved and said so about a column the reader had never heard of.
|
|
1216
|
+
*
|
|
1217
|
+
* `next` when there is one, because that is what the button means. Otherwise
|
|
1218
|
+
* the first column that is neither in flight nor finished nor the owner's:
|
|
1219
|
+
* queued work goes where queued work already sits. And the first column of
|
|
1220
|
+
* all when nothing else fits, since a board with no columns cannot pass the
|
|
1221
|
+
* validator either way.
|
|
1222
|
+
*/
|
|
1223
|
+
declare function queueColumn(columns: ColumnDef[]): string;
|
|
1179
1224
|
/** A card claiming to be finished that was never picked up. */
|
|
1180
1225
|
declare function claimedWithoutStarting(card: Card): boolean;
|
|
1181
1226
|
interface Counts {
|
|
@@ -1973,4 +2018,4 @@ interface CardSheetProps {
|
|
|
1973
2018
|
}
|
|
1974
2019
|
declare function CardSheet({ card, state, lang, ui, onChange, onDelete, onClose }: CardSheetProps): React.JSX.Element | null;
|
|
1975
2020
|
|
|
1976
|
-
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, type SituationItem, SituationRow, type SituationRowProps, type SituationTone, 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, warRoomForm, wizardStep, writeText };
|
|
2021
|
+
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, type SituationItem, SituationRow, type SituationRowProps, type SituationTone, 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, dropIndex, emptyCard, explainBoard, gateStatus, isArchived, isCard, isFirstRun, ledgerForm, midnightTheme, moveCardInColumns, newId, offeredThemes, oldMoneyTheme, panelForm, paneltirBuild, paperForm, queueColumn, readBoard, readStoredLang, repositorySnippet, resetGuide, sparkPoints, stampForColumn, storeLang, text, today, trendOf, undecided, untranslated, useDashboardForm, useDashboardTheme, useDelegatedClick, useGuideNote, useMediaQuery, validateBoard, warRoomForm, wizardStep, writeText };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var PANELTIR_VERSION = "0.12.
|
|
3
|
-
var PANELTIR_FINGERPRINT = "sha256:
|
|
2
|
+
var PANELTIR_VERSION = "0.12.1";
|
|
3
|
+
var PANELTIR_FINGERPRINT = "sha256:3e958b3c4fa8a7c32fb49568d6189c9a0dea22e65263265177ba6d404ffa13ca";
|
|
4
4
|
var PANELTIR_FILE_COUNT = 166;
|
|
5
|
-
var PANELTIR_TEMPLATE_HASH = "sha256:
|
|
5
|
+
var PANELTIR_TEMPLATE_HASH = "sha256:9d9168001acb58c6ac0c7766515cb0fa95c4fc6d16e6fb17efd59d88e4a3c382";
|
|
6
6
|
var paneltirBuild = {
|
|
7
7
|
version: PANELTIR_VERSION,
|
|
8
8
|
fingerprint: PANELTIR_FINGERPRINT,
|
|
@@ -348,11 +348,11 @@ function Column({ column, renderCard, onAddCard }) {
|
|
|
348
348
|
column.cards.map((card, index) => {
|
|
349
349
|
const showBefore = dnd.isDragging && dnd.dropTarget?.columnId === column.id && dnd.dropTarget.index === index;
|
|
350
350
|
return /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
351
|
-
showBefore && /* @__PURE__ */ jsx9("div", { className: "pt-card-placeholder" }),
|
|
351
|
+
showBefore && /* @__PURE__ */ jsx9("div", { className: "pt-card-placeholder", "data-pt-placeholder": true }),
|
|
352
352
|
renderCard(card, index, column.id)
|
|
353
353
|
] }, card.id);
|
|
354
354
|
}),
|
|
355
|
-
dnd.isDragging && dnd.dropTarget?.columnId === column.id && dnd.dropTarget.index === column.cards.length && /* @__PURE__ */ jsx9("div", { className: "pt-card-placeholder" }),
|
|
355
|
+
dnd.isDragging && dnd.dropTarget?.columnId === column.id && dnd.dropTarget.index === column.cards.length && /* @__PURE__ */ jsx9("div", { className: "pt-card-placeholder", "data-pt-placeholder": true }),
|
|
356
356
|
column.footer ? /* @__PURE__ */ jsx9("div", { className: "pt-column__footer", children: column.footer }) : null
|
|
357
357
|
] })
|
|
358
358
|
] });
|
|
@@ -406,6 +406,11 @@ function useAutoscroll({ active, pointerRef, scrollContainerRef, edgeSize = 56,
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
// src/components/Board/useBoardDnD.ts
|
|
409
|
+
function dropIndex(source, target) {
|
|
410
|
+
if (source.columnId !== target.columnId) return target.index;
|
|
411
|
+
if (target.index === source.index || target.index === source.index + 1) return null;
|
|
412
|
+
return target.index > source.index ? target.index - 1 : target.index;
|
|
413
|
+
}
|
|
409
414
|
function useBoardDnD({ columns, onMove, scrollContainerRef }) {
|
|
410
415
|
const [drag, setDrag] = useState(null);
|
|
411
416
|
const [pointer, setPointer] = useState(null);
|
|
@@ -466,6 +471,7 @@ function useBoardDnD({ columns, onMove, scrollContainerRef }) {
|
|
|
466
471
|
if (!listEl) return;
|
|
467
472
|
const columnId = listEl.getAttribute("data-pt-column-list");
|
|
468
473
|
const cardEl = el?.closest("[data-pt-card]");
|
|
474
|
+
if (el?.closest("[data-pt-placeholder]")) return;
|
|
469
475
|
let index;
|
|
470
476
|
if (cardEl && listEl.contains(cardEl)) {
|
|
471
477
|
const cardRect = cardEl.getBoundingClientRect();
|
|
@@ -492,13 +498,13 @@ function useBoardDnD({ columns, onMove, scrollContainerRef }) {
|
|
|
492
498
|
const finalTarget = dropTargetRef.current;
|
|
493
499
|
const source = dragRef.current;
|
|
494
500
|
if (finalTarget && source) {
|
|
495
|
-
const
|
|
496
|
-
if (
|
|
501
|
+
const toIndex = dropIndex(source, finalTarget);
|
|
502
|
+
if (toIndex !== null) {
|
|
497
503
|
onMove({
|
|
498
504
|
cardId: source.cardId,
|
|
499
505
|
fromColumnId: source.columnId,
|
|
500
506
|
toColumnId: finalTarget.columnId,
|
|
501
|
-
toIndex
|
|
507
|
+
toIndex
|
|
502
508
|
});
|
|
503
509
|
}
|
|
504
510
|
}
|
|
@@ -608,9 +614,10 @@ function moveCardInColumns(columns, move) {
|
|
|
608
614
|
}
|
|
609
615
|
|
|
610
616
|
// src/components/DetailSheet/DetailSheet.tsx
|
|
611
|
-
import { useEffect as useEffect3 } from "react";
|
|
617
|
+
import { useEffect as useEffect3, useRef as useRef4 } from "react";
|
|
612
618
|
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
613
619
|
function DetailSheet({ open, onClose, children, ariaLabel }) {
|
|
620
|
+
const pressedBackdrop = useRef4(false);
|
|
614
621
|
useEffect3(() => {
|
|
615
622
|
if (!open) return;
|
|
616
623
|
function handleKey(e) {
|
|
@@ -620,10 +627,24 @@ function DetailSheet({ open, onClose, children, ariaLabel }) {
|
|
|
620
627
|
return () => window.removeEventListener("keydown", handleKey);
|
|
621
628
|
}, [open, onClose]);
|
|
622
629
|
if (!open) return null;
|
|
623
|
-
return /* @__PURE__ */ jsx12(
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
630
|
+
return /* @__PURE__ */ jsx12(
|
|
631
|
+
"div",
|
|
632
|
+
{
|
|
633
|
+
className: "pt-sheet-backdrop",
|
|
634
|
+
onPointerDown: (e) => {
|
|
635
|
+
pressedBackdrop.current = e.target === e.currentTarget;
|
|
636
|
+
},
|
|
637
|
+
onClick: (e) => {
|
|
638
|
+
const began = pressedBackdrop.current;
|
|
639
|
+
pressedBackdrop.current = false;
|
|
640
|
+
if (began && e.target === e.currentTarget) onClose();
|
|
641
|
+
},
|
|
642
|
+
children: /* @__PURE__ */ jsxs10("div", { className: "pt-sheet", role: "dialog", "aria-modal": "true", "aria-label": ariaLabel, onClick: (e) => e.stopPropagation(), children: [
|
|
643
|
+
/* @__PURE__ */ jsx12("div", { className: "pt-sheet__handle" }),
|
|
644
|
+
children
|
|
645
|
+
] })
|
|
646
|
+
}
|
|
647
|
+
);
|
|
627
648
|
}
|
|
628
649
|
function DetailSection({ title, subtitle, children }) {
|
|
629
650
|
return /* @__PURE__ */ jsxs10("section", { className: "pt-detail-section", children: [
|
|
@@ -739,7 +760,7 @@ function GuideTour({ notes, dismissed, onDismiss }) {
|
|
|
739
760
|
}
|
|
740
761
|
|
|
741
762
|
// src/components/Notifications/Notifications.tsx
|
|
742
|
-
import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo2, useRef as
|
|
763
|
+
import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo2, useRef as useRef5, useState as useState3 } from "react";
|
|
743
764
|
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
744
765
|
var STORE_KEY = "pt-seen";
|
|
745
766
|
function readSeen() {
|
|
@@ -768,7 +789,7 @@ function NotificationBell({ notifications, labels, seen: given, onSeen }) {
|
|
|
768
789
|
const controlled = given !== void 0;
|
|
769
790
|
const [stored, setStored] = useState3(null);
|
|
770
791
|
const [open, setOpen] = useState3(false);
|
|
771
|
-
const root =
|
|
792
|
+
const root = useRef5(null);
|
|
772
793
|
useEffect5(() => {
|
|
773
794
|
setStored(readSeen());
|
|
774
795
|
}, []);
|
|
@@ -873,7 +894,7 @@ function NotificationBell({ notifications, labels, seen: given, onSeen }) {
|
|
|
873
894
|
}
|
|
874
895
|
|
|
875
896
|
// src/components/CopyBlock/CopyBlock.tsx
|
|
876
|
-
import { useCallback as useCallback4, useEffect as useEffect6, useRef as
|
|
897
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef6, useState as useState4 } from "react";
|
|
877
898
|
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
878
899
|
var DEFAULT_LABELS2 = {
|
|
879
900
|
copy: "Copy",
|
|
@@ -883,8 +904,8 @@ var DEFAULT_LABELS2 = {
|
|
|
883
904
|
function CopyBlock({ text: text2, label, note, labels }) {
|
|
884
905
|
const words = { ...DEFAULT_LABELS2, ...labels };
|
|
885
906
|
const [result, setResult] = useState4("idle");
|
|
886
|
-
const pre =
|
|
887
|
-
const timer =
|
|
907
|
+
const pre = useRef6(null);
|
|
908
|
+
const timer = useRef6(void 0);
|
|
888
909
|
useEffect6(() => () => window.clearTimeout(timer.current), []);
|
|
889
910
|
const announce = useCallback4((next) => {
|
|
890
911
|
setResult(next);
|
|
@@ -1217,6 +1238,12 @@ function stampForColumn(card, columns) {
|
|
|
1217
1238
|
if (column.active && !next.startedAt) next.startedAt = today();
|
|
1218
1239
|
return next;
|
|
1219
1240
|
}
|
|
1241
|
+
function queueColumn(columns) {
|
|
1242
|
+
const next = columns.find((column) => column.id === "next");
|
|
1243
|
+
if (next) return next.id;
|
|
1244
|
+
const queued = columns.find((column) => !column.active && !column.done && column.id !== "yours");
|
|
1245
|
+
return (queued ?? columns[0])?.id ?? "next";
|
|
1246
|
+
}
|
|
1220
1247
|
function claimedWithoutStarting(card) {
|
|
1221
1248
|
return Boolean(card.completedAt) && !card.startedAt;
|
|
1222
1249
|
}
|
|
@@ -3350,18 +3377,18 @@ function PanelApp({
|
|
|
3350
3377
|
}, []);
|
|
3351
3378
|
const addCard = useCallback5(
|
|
3352
3379
|
(columnId) => {
|
|
3353
|
-
const card = emptyCard(columnId, state.areas[0]?.id ?? "general");
|
|
3380
|
+
const card = stampForColumn(emptyCard(columnId, state.areas[0]?.id ?? "general"), state.columns);
|
|
3354
3381
|
setState((current) => ({ ...current, updatedAt: today(), cards: [...current.cards, card] }));
|
|
3355
3382
|
setOpenCardId(card.id);
|
|
3356
3383
|
},
|
|
3357
|
-
[state.areas]
|
|
3384
|
+
[state.areas, state.columns]
|
|
3358
3385
|
);
|
|
3359
3386
|
const addUpdateCard = useCallback5(() => {
|
|
3360
3387
|
if (!update?.behind) return;
|
|
3361
3388
|
const id = updateCardId(update.latest);
|
|
3362
3389
|
if (state.cards.some((card2) => card2.id === id)) return;
|
|
3363
3390
|
const card = {
|
|
3364
|
-
...emptyCard(
|
|
3391
|
+
...emptyCard(queueColumn(state.columns), state.areas[0]?.id ?? "general"),
|
|
3365
3392
|
id,
|
|
3366
3393
|
title: { en: UI.en.updateCardTitle(update.latest), es: UI.es.updateCardTitle(update.latest) },
|
|
3367
3394
|
body: { en: UI.en.updateCardBody(update.latest), es: UI.es.updateCardBody(update.latest) },
|
|
@@ -3372,7 +3399,7 @@ function PanelApp({
|
|
|
3372
3399
|
};
|
|
3373
3400
|
setState((current) => ({ ...current, updatedAt: today(), cards: [...current.cards, card] }));
|
|
3374
3401
|
setOpenCardId(id);
|
|
3375
|
-
}, [update, state.cards, state.areas, state.preferences?.updateAsOrder]);
|
|
3402
|
+
}, [update, state.cards, state.areas, state.columns, state.preferences?.updateAsOrder]);
|
|
3376
3403
|
const requestReport = useCallback5(
|
|
3377
3404
|
(section) => {
|
|
3378
3405
|
const id = `report-${section}-${today()}`;
|
|
@@ -3381,7 +3408,7 @@ function PanelApp({
|
|
|
3381
3408
|
return;
|
|
3382
3409
|
}
|
|
3383
3410
|
const card = {
|
|
3384
|
-
...emptyCard(
|
|
3411
|
+
...emptyCard(queueColumn(state.columns), state.areas[0]?.id ?? "general"),
|
|
3385
3412
|
id,
|
|
3386
3413
|
title: { en: UI.en.analysisRedoTitle[section], es: UI.es.analysisRedoTitle[section] },
|
|
3387
3414
|
body: { en: UI.en.analysisRedoBody[section], es: UI.es.analysisRedoBody[section] },
|
|
@@ -3394,12 +3421,14 @@ function PanelApp({
|
|
|
3394
3421
|
setState((current) => ({ ...current, updatedAt: today(), cards: [...current.cards, card] }));
|
|
3395
3422
|
setOpenCardId(id);
|
|
3396
3423
|
},
|
|
3397
|
-
[state.cards, state.areas]
|
|
3424
|
+
[state.cards, state.areas, state.columns]
|
|
3398
3425
|
);
|
|
3399
3426
|
const moveCard = useCallback5(
|
|
3400
3427
|
(move) => {
|
|
3401
3428
|
setState((current) => {
|
|
3402
|
-
const
|
|
3429
|
+
const done = new Set(current.columns.filter((column) => column.done).map((column) => column.id));
|
|
3430
|
+
const drawn = current.cards.filter((card) => matches(card) && !isArchived(card, done));
|
|
3431
|
+
const moved = applyMove(current.cards, move, drawn);
|
|
3403
3432
|
return {
|
|
3404
3433
|
...current,
|
|
3405
3434
|
updatedAt: today(),
|
|
@@ -3587,7 +3616,7 @@ function PanelApp({
|
|
|
3587
3616
|
throw Object.assign(new Error(saveError(response.status, data, body)), { conflict });
|
|
3588
3617
|
}
|
|
3589
3618
|
setSavedSnapshot(JSON.stringify(next));
|
|
3590
|
-
setState(next);
|
|
3619
|
+
setState((current) => current === state ? next : current);
|
|
3591
3620
|
setSave({
|
|
3592
3621
|
status: "saved",
|
|
3593
3622
|
at: (/* @__PURE__ */ new Date()).toLocaleTimeString(lang === "es" ? "es-ES" : "en-GB", {
|
|
@@ -4459,6 +4488,7 @@ export {
|
|
|
4459
4488
|
countCards,
|
|
4460
4489
|
cyberpunkTheme,
|
|
4461
4490
|
decided,
|
|
4491
|
+
dropIndex,
|
|
4462
4492
|
emptyCard,
|
|
4463
4493
|
explainBoard,
|
|
4464
4494
|
gateStatus,
|
|
@@ -4474,6 +4504,7 @@ export {
|
|
|
4474
4504
|
panelForm,
|
|
4475
4505
|
paneltirBuild,
|
|
4476
4506
|
paperForm,
|
|
4507
|
+
queueColumn,
|
|
4477
4508
|
readBoard,
|
|
4478
4509
|
readStoredLang,
|
|
4479
4510
|
repositorySnippet,
|
package/fingerprint.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paneltir",
|
|
3
|
-
"version": "0.12.
|
|
4
|
-
"hash": "sha256:
|
|
3
|
+
"version": "0.12.1",
|
|
4
|
+
"hash": "sha256:3e958b3c4fa8a7c32fb49568d6189c9a0dea22e65263265177ba6d404ffa13ca",
|
|
5
5
|
"fileCount": 166,
|
|
6
|
-
"templateHash": "sha256:
|
|
7
|
-
"generatedAt": "2026-09-
|
|
6
|
+
"templateHash": "sha256:9d9168001acb58c6ac0c7766515cb0fa95c4fc6d16e6fb17efd59d88e4a3c382",
|
|
7
|
+
"generatedAt": "2026-09-18T06:35:32.449Z"
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paneltir",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
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,49 +99,63 @@
|
|
|
99
99
|
"node": ">=20"
|
|
100
100
|
},
|
|
101
101
|
"paneltirRelease": {
|
|
102
|
-
"version": "0.12.
|
|
103
|
-
"date": "2026-09-
|
|
102
|
+
"version": "0.12.1",
|
|
103
|
+
"date": "2026-09-18",
|
|
104
104
|
"entries": [
|
|
105
105
|
{
|
|
106
|
-
"kind": "
|
|
106
|
+
"kind": "fix",
|
|
107
107
|
"text": {
|
|
108
|
-
"en": "
|
|
109
|
-
"es": "
|
|
108
|
+
"en": "A card dragged down its own column landed one slot below the placeholder. The index the drag emitted counted the card being dragged, and neither reducer allowed for it once the card was taken out; it is adjusted once now, where the numbers are still rendered ones, and `toIndex` says what it means.",
|
|
109
|
+
"es": "Una tarjeta arrastrada hacia abajo en su propia columna caía un hueco por debajo del marcador. El índice que emitía el arrastre contaba la propia tarjeta arrastrada y ninguno de los dos reductores lo descontaba al sacarla; ahora se ajusta una sola vez, donde los números aún son los dibujados, y `toIndex` dice lo que significa."
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
|
-
"kind": "
|
|
113
|
+
"kind": "fix",
|
|
114
114
|
"text": {
|
|
115
|
-
"en": "
|
|
116
|
-
"es": "
|
|
115
|
+
"en": "Hovering the drop placeholder no longer sends the target to the bottom of the column. It appeared under the pointer the moment a slot was chosen and read as \"not a card, so the end\", which moved it, put the card back under the pointer, and chose again.",
|
|
116
|
+
"es": "Pasar por encima del marcador de soltado ya no manda el destino al final de la columna. Aparecía bajo el puntero en cuanto se elegía un hueco y se leía como «no es una tarjeta, así que el final», lo que lo movía, devolvía la tarjeta bajo el puntero y volvía a elegir."
|
|
117
117
|
}
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
|
-
"kind": "
|
|
120
|
+
"kind": "fix",
|
|
121
121
|
"text": {
|
|
122
|
-
"en": "A
|
|
123
|
-
"es": "
|
|
122
|
+
"en": "A drop in a done column that holds archived cards lands where it was dropped. The move was anchored against a list that still carried the archived cards, so the slot on screen and the slot in the file disagreed.",
|
|
123
|
+
"es": "Soltar en una columna Hecho con tarjetas archivadas cae donde se soltó. El movimiento se anclaba contra una lista que aún llevaba las archivadas, así que el hueco en pantalla y el hueco en el archivo no coincidían."
|
|
124
124
|
}
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
|
-
"kind": "
|
|
127
|
+
"kind": "fix",
|
|
128
128
|
"text": {
|
|
129
|
-
"en": "
|
|
130
|
-
"es": "
|
|
129
|
+
"en": "A save in flight no longer discards what was typed while it ran. The board was put back to the moment Save was pressed when the commit came back; edited meanwhile, it now keeps the edits and reads as unsaved again, which is what they are.",
|
|
130
|
+
"es": "Un guardado en curso ya no descarta lo que se escribió mientras corría. El tablero volvía al momento de pulsar Guardar cuando regresaba el commit; si se editó entre medias, ahora conserva los cambios y vuelve a leerse como sin guardar, que es lo que son."
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
"kind": "fix",
|
|
135
135
|
"text": {
|
|
136
|
-
"en": "The
|
|
137
|
-
"es": "
|
|
136
|
+
"en": "The update card and the four \"ask for this again\" cards no longer assume a column called `next`. On a board whose owner renamed it they landed in a column that does not exist, which the write path refuses — one tap made the board unsaveable. `queueColumn` picks where queued work already sits.",
|
|
137
|
+
"es": "La tarjeta de actualización y las cuatro de «pídelo otra vez» ya no dan por hecho una columna llamada `next`. En un tablero cuyo dueño la renombró caían en una columna que no existe, cosa que la ruta de escritura rechaza: un toque dejaba el tablero sin poder guardarse. `queueColumn` elige donde ya está el trabajo en cola."
|
|
138
138
|
}
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
|
-
"kind": "
|
|
141
|
+
"kind": "fix",
|
|
142
|
+
"text": {
|
|
143
|
+
"en": "A card added with \"+\" straight into the working column is stamped as started, as a card moved there would be. It carried no `startedAt`, so the day it reached done the panel flagged it as never picked up.",
|
|
144
|
+
"es": "Una tarjeta añadida con «+» directamente en la columna de trabajo se sella como empezada, igual que si se hubiera movido ahí. No llevaba `startedAt`, así que el día que llegaba a Hecho el panel la marcaba como nunca recogida."
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"kind": "fix",
|
|
149
|
+
"text": {
|
|
150
|
+
"en": "`paneltir run` never mints an id the board already has. At the cap the list stops growing and the length-based id repeated, so two runs shared a key in the revisions log and one bell entry stood for both.",
|
|
151
|
+
"es": "`paneltir run` nunca acuña un id que el tablero ya tiene. En el tope la lista deja de crecer y el id basado en la longitud se repetía, así que dos pasadas compartían clave en el registro y una entrada de la campana valía por las dos."
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"kind": "fix",
|
|
142
156
|
"text": {
|
|
143
|
-
"en": "
|
|
144
|
-
"es": "
|
|
157
|
+
"en": "Selecting text inside a sheet and letting go over the backdrop no longer closes it. Only a press that begins and ends on the backdrop is somebody asking to leave.",
|
|
158
|
+
"es": "Seleccionar texto dentro de una hoja y soltar sobre el fondo ya no la cierra. Solo una pulsación que empieza y termina en el fondo es alguien pidiendo salir."
|
|
145
159
|
}
|
|
146
160
|
}
|
|
147
161
|
]
|
|
@@ -59,6 +59,32 @@ lie in a file the owner reads on a phone.
|
|
|
59
59
|
**`risk`** — what it costs if it goes wrong, not how soon it is wanted. A
|
|
60
60
|
high-risk card gets the safer approach even when its intent says `fast`.
|
|
61
61
|
|
|
62
|
+
## A second problem, and a detail nobody wrote down
|
|
63
|
+
|
|
64
|
+
These two come up on almost every card, and neither is a reason to stop.
|
|
65
|
+
|
|
66
|
+
**A second problem found while doing a card.** Fix it in the same pass when it
|
|
67
|
+
is small, reversible, and inside what you were already touching. Anything
|
|
68
|
+
larger becomes a card: write it in `ideas` with a title, what you saw and why
|
|
69
|
+
it matters, then carry on with the card you were on and name the new card in
|
|
70
|
+
your reply. It is never a reason to stop and wait, and never something to fix
|
|
71
|
+
quietly inside an unrelated change — a queue stops being trusted when work
|
|
72
|
+
arrives in it that nobody asked for, and a finding stops existing when it was
|
|
73
|
+
only mentioned in a conversation that ended.
|
|
74
|
+
|
|
75
|
+
**A detail the card does not specify.** Decide it. That is the normal state of
|
|
76
|
+
a card rather than an omission: a card says what to build and rarely how, which
|
|
77
|
+
is why `do` says outright that *every detail not written down is yours to
|
|
78
|
+
choose* and an absent intent says to use judgement. Prefer the smaller, more
|
|
79
|
+
reversible option and say in one line what you chose and why.
|
|
80
|
+
|
|
81
|
+
Only `askme`, `explain` and `hold` mean stop, and `value: null` on a choice
|
|
82
|
+
holds back only the part of the work that actually depends on that answer —
|
|
83
|
+
never the whole card. Asking is for a question that outlives the conversation,
|
|
84
|
+
and it is asked on the board as a choice with `value: null`, which the bell
|
|
85
|
+
surfaces. Stopping to ask something you could have chosen costs the owner a
|
|
86
|
+
session; choosing it and saying so costs them one line.
|
|
87
|
+
|
|
62
88
|
## Read the analysis before deciding anything
|
|
63
89
|
|
|
64
90
|
`analysis.choices` holds the questions the owner has already answered, grouped
|