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.
@@ -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.0
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.0 # exits non-zero if that is not what is installed
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.0
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: `run-${date}-${String(board.runs.length + 1).padStart(3, '0')}`,
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.0";
3
- var PANELTIR_FINGERPRINT = "sha256:ea981bdb1de721f4d2afd279244a7298b025773fb4a5abf8ffa33868c0d73f7d";
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:c0c1a1678d860212ff82d55c7e7c0ee3eaf8be623621cadbb6ca5bf51b31e2ba";
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 sameSpot = finalTarget.columnId === source.columnId && (finalTarget.index === source.index || finalTarget.index === source.index + 1);
496
- if (!sameSpot) {
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: finalTarget.index
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("div", { className: "pt-sheet-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs10("div", { className: "pt-sheet", role: "dialog", "aria-modal": "true", "aria-label": ariaLabel, onClick: (e) => e.stopPropagation(), children: [
624
- /* @__PURE__ */ jsx12("div", { className: "pt-sheet__handle" }),
625
- children
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 useRef4, useState as useState3 } from "react";
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 = useRef4(null);
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 useRef5, useState as useState4 } from "react";
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 = useRef5(null);
887
- const timer = useRef5(void 0);
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("next", state.areas[0]?.id ?? "general"),
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("next", state.areas[0]?.id ?? "general"),
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 moved = applyMove(current.cards, move, current.cards.filter(matches));
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.0",
4
- "hash": "sha256:ea981bdb1de721f4d2afd279244a7298b025773fb4a5abf8ffa33868c0d73f7d",
3
+ "version": "0.12.1",
4
+ "hash": "sha256:3e958b3c4fa8a7c32fb49568d6189c9a0dea22e65263265177ba6d404ffa13ca",
5
5
  "fileCount": 166,
6
- "templateHash": "sha256:c0c1a1678d860212ff82d55c7e7c0ee3eaf8be623621cadbb6ca5bf51b31e2ba",
7
- "generatedAt": "2026-09-14T04:42:15.291Z"
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.0",
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.0",
103
- "date": "2026-09-14",
102
+ "version": "0.12.1",
103
+ "date": "2026-09-18",
104
104
  "entries": [
105
105
  {
106
- "kind": "feature",
106
+ "kind": "fix",
107
107
  "text": {
108
- "en": "Archived cards leave the board and get a place of their own. A card finished more than a week ago is no longer drawn in its column at all — it is listed in the record view, under what each pass did, with the date it was finished. The count in its column was still the board carrying it: one tap put fifteen finished cards back in the way.",
109
- "es": "Las tarjetas archivadas salen del tablero y tienen su propio sitio. Una tarjeta terminada hace más de una semana ya no se dibuja en su columna: aparece en la vista de revisiones, debajo de lo que hizo cada pasada, con la fecha en que se terminó. El contador en la columna seguía siendo el tablero cargando con ellas: un toque devolvía quince tarjetas terminadas a en medio."
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": "feature",
113
+ "kind": "fix",
114
114
  "text": {
115
- "en": "A new strip at the top of the board says what is waiting on you and whether the system is working. The two questions a board is opened to answer were on the screen and neither was prominent — three of six identical tiles, and a line of small grey pills under them. The counts are now large and are controls: tapping one filters the board or opens the question.",
116
- "es": "Una franja nueva encima del tablero dice qué te está esperando y si el sistema funciona. Las dos preguntas por las que se abre un tablero estaban en pantalla y ninguna destacaba: tres de seis casillas iguales, y una línea de pastillas grises debajo. Ahora los números son grandes y son controles: tocar uno filtra el tablero o abre la pregunta."
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": "feature",
120
+ "kind": "fix",
121
121
  "text": {
122
- "en": "A third board shape, `warRoom`: square, hairline-ruled, tight, priority carried as a full-height bar down the left edge of every row and counts set in the monospace face. It is the only form that tightens rather than loosens the spacing — a board somebody is watching wants as many rows on the screen as will fit. Ask for it with `THEME_FORMS.warRoom` on a theme's `form`.",
123
- "es": "Una tercera forma de tablero, `warRoom`: cuadrada, reglada a un pelo, apretada, con la prioridad como una barra de alto completo en el borde izquierdo de cada fila y los números en la tipografía monoespaciada. Es la única forma que aprieta el espaciado en vez de soltarlo: un tablero que se vigila quiere tantas filas en pantalla como quepan. Se pide con `THEME_FORMS.warRoom` en el `form` de un tema."
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": "feature",
127
+ "kind": "fix",
128
128
  "text": {
129
- "en": "`SituationRow` is exported, so a project building its own screen out of the components can draw the same strip.",
130
- "es": "`SituationRow` se exporta, así que un proyecto que construye su propia pantalla con los componentes puede dibujar la misma franja."
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 open decisions counter counts questions nobody has answered, not cards about deciding. Those are different numbers and the one that holds work up is the first.",
137
- "es": "El contador de decisiones abiertas cuenta preguntas que nadie ha contestado, no tarjetas sobre decidir. Son números distintos y el que frena el trabajo es el primero."
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": "chore",
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": "A shape declared with no drawing now fails the checks. A form whose CSS is missing renders exactly as the default, which does not look like a missing stylesheet — it looks like the `form` prop being ignored.",
144
- "es": "Una forma declarada sin dibujo ahora falla las comprobaciones. Una forma a la que le falta el CSS se dibuja igual que la de por defecto, y eso no parece una hoja de estilos que falta: parece que la prop `form` se está ignorando."
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