paneltir 0.12.0 → 0.13.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/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";
4
- var PANELTIR_FILE_COUNT = 166;
5
- var PANELTIR_TEMPLATE_HASH = "sha256:c0c1a1678d860212ff82d55c7e7c0ee3eaf8be623621cadbb6ca5bf51b31e2ba";
2
+ var PANELTIR_VERSION = "0.13.0";
3
+ var PANELTIR_FINGERPRINT = "sha256:f3b5664bddf68f9560bd368b01184f97c733b9a5a3706507ae2219c6a41d9a1c";
4
+ var PANELTIR_FILE_COUNT = 167;
5
+ var PANELTIR_TEMPLATE_HASH = "sha256:7c204bad5c7a8b329ad53ac51b5856438fe84735cea47543eb36b976edbb27da";
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
  }
@@ -607,10 +613,27 @@ function moveCardInColumns(columns, move) {
607
613
  });
608
614
  }
609
615
 
616
+ // src/panel/requests.ts
617
+ function slug(subject) {
618
+ return subject.replace(/[^A-Za-z0-9._]/g, "-");
619
+ }
620
+ function requestId(kind, day, subject) {
621
+ return subject === void 0 ? `${kind}-${day}` : `${kind}-${slug(subject)}-${day}`;
622
+ }
623
+ function requestKey(id) {
624
+ const match = /^(.+)-\d{4}-\d{2}-\d{2}$/.exec(id);
625
+ return match ? match[1] : null;
626
+ }
627
+ function pendingRequest(cards, kind, doneColumns, subject) {
628
+ const wanted = subject === void 0 ? kind : `${kind}-${slug(subject)}`;
629
+ return cards.find((card) => !doneColumns.has(card.column) && requestKey(card.id) === wanted);
630
+ }
631
+
610
632
  // src/components/DetailSheet/DetailSheet.tsx
611
- import { useEffect as useEffect3 } from "react";
633
+ import { useEffect as useEffect3, useRef as useRef4 } from "react";
612
634
  import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
613
635
  function DetailSheet({ open, onClose, children, ariaLabel }) {
636
+ const pressedBackdrop = useRef4(false);
614
637
  useEffect3(() => {
615
638
  if (!open) return;
616
639
  function handleKey(e) {
@@ -620,10 +643,24 @@ function DetailSheet({ open, onClose, children, ariaLabel }) {
620
643
  return () => window.removeEventListener("keydown", handleKey);
621
644
  }, [open, onClose]);
622
645
  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
- ] }) });
646
+ return /* @__PURE__ */ jsx12(
647
+ "div",
648
+ {
649
+ className: "pt-sheet-backdrop",
650
+ onPointerDown: (e) => {
651
+ pressedBackdrop.current = e.target === e.currentTarget;
652
+ },
653
+ onClick: (e) => {
654
+ const began = pressedBackdrop.current;
655
+ pressedBackdrop.current = false;
656
+ if (began && e.target === e.currentTarget) onClose();
657
+ },
658
+ children: /* @__PURE__ */ jsxs10("div", { className: "pt-sheet", role: "dialog", "aria-modal": "true", "aria-label": ariaLabel, onClick: (e) => e.stopPropagation(), children: [
659
+ /* @__PURE__ */ jsx12("div", { className: "pt-sheet__handle" }),
660
+ children
661
+ ] })
662
+ }
663
+ );
627
664
  }
628
665
  function DetailSection({ title, subtitle, children }) {
629
666
  return /* @__PURE__ */ jsxs10("section", { className: "pt-detail-section", children: [
@@ -739,7 +776,7 @@ function GuideTour({ notes, dismissed, onDismiss }) {
739
776
  }
740
777
 
741
778
  // src/components/Notifications/Notifications.tsx
742
- import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo2, useRef as useRef4, useState as useState3 } from "react";
779
+ import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo2, useRef as useRef5, useState as useState3 } from "react";
743
780
  import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
744
781
  var STORE_KEY = "pt-seen";
745
782
  function readSeen() {
@@ -768,7 +805,7 @@ function NotificationBell({ notifications, labels, seen: given, onSeen }) {
768
805
  const controlled = given !== void 0;
769
806
  const [stored, setStored] = useState3(null);
770
807
  const [open, setOpen] = useState3(false);
771
- const root = useRef4(null);
808
+ const root = useRef5(null);
772
809
  useEffect5(() => {
773
810
  setStored(readSeen());
774
811
  }, []);
@@ -873,7 +910,7 @@ function NotificationBell({ notifications, labels, seen: given, onSeen }) {
873
910
  }
874
911
 
875
912
  // src/components/CopyBlock/CopyBlock.tsx
876
- import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5, useState as useState4 } from "react";
913
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef6, useState as useState4 } from "react";
877
914
  import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
878
915
  var DEFAULT_LABELS2 = {
879
916
  copy: "Copy",
@@ -883,8 +920,8 @@ var DEFAULT_LABELS2 = {
883
920
  function CopyBlock({ text: text2, label, note, labels }) {
884
921
  const words = { ...DEFAULT_LABELS2, ...labels };
885
922
  const [result, setResult] = useState4("idle");
886
- const pre = useRef5(null);
887
- const timer = useRef5(void 0);
923
+ const pre = useRef6(null);
924
+ const timer = useRef6(void 0);
888
925
  useEffect6(() => () => window.clearTimeout(timer.current), []);
889
926
  const announce = useCallback4((next) => {
890
927
  setResult(next);
@@ -1217,6 +1254,12 @@ function stampForColumn(card, columns) {
1217
1254
  if (column.active && !next.startedAt) next.startedAt = today();
1218
1255
  return next;
1219
1256
  }
1257
+ function queueColumn(columns) {
1258
+ const next = columns.find((column) => column.id === "next");
1259
+ if (next) return next.id;
1260
+ const queued = columns.find((column) => !column.active && !column.done && column.id !== "yours");
1261
+ return (queued ?? columns[0])?.id ?? "next";
1262
+ }
1220
1263
  function claimedWithoutStarting(card) {
1221
1264
  return Boolean(card.completedAt) && !card.startedAt;
1222
1265
  }
@@ -1599,6 +1642,28 @@ var UI = {
1599
1642
  releaseNone: "This version was published without notes.",
1600
1643
  updateCardTitle: (version) => `Update Paneltir to v${version}`,
1601
1644
  updateCardBody: (version) => `Run \`npm install paneltir@${version}\` in this project, then check the panel still builds and renders. The version in use is reported by \`paneltirBuild\`, so it can be confirmed rather than assumed.`,
1645
+ versionBumpAsk: "Ask for a version bump",
1646
+ versionBumpOpen: "A version bump is already asked for \u2014 open it",
1647
+ versionBumpHow: "Writes an order card: every target up one version and one build, kept in step. Nothing changes until a session does it, and the card is dirty until Save like any edit.",
1648
+ versionBumpTitle: "Bump the version by one, on every target",
1649
+ versionBumpBody: "Asked from the panel. Move the version up one step and the build number up one on every target this project ships \u2014 a web app\u2019s package.json, an iOS app\u2019s MARKETING_VERSION and CURRENT_PROJECT_VERSION, an Android app\u2019s versionName and versionCode, and anything else that carries one \u2014 so every target ends on the same version and the same build. For example: build 18, version 1.0.18 becomes build 19, version 1.0.19. A target with no build number moves its version only. If the targets already disagree, bring them all to the highest plus one and say which were behind. Follow this project\u2019s own release rules \u2014 changelog, tag, notes \u2014 if it has any.",
1650
+ versionBumpChecks: [
1651
+ "Found every target that carries a version or a build number",
1652
+ "Moved each one: version up one step, build up one",
1653
+ "Every target ends on the same version and the same build"
1654
+ ],
1655
+ installAsk: "Add a card asking Claude to install it",
1656
+ installAskOpen: "Already asked for \u2014 open the card",
1657
+ installAskHow: "The card carries the repository snippet above, so the session that picks it up installs exactly this. It is dirty until Save, like any edit.",
1658
+ installCardTitle: (tool) => `Install ${tool} in this repository`,
1659
+ installCardBody: (tool, file, snippet) => `Asked from the Tools screen. Merge these two keys into ${file} \u2014 merge, never replace: the file holds other settings. Then commit it, so ${tool} installs for anyone who starts a session in this repository.
1660
+
1661
+ ${snippet}`,
1662
+ installCardChecks: [
1663
+ "Merged into the settings file without replacing anything already there",
1664
+ "Committed to the repository",
1665
+ "The Tools screen shows it as Active"
1666
+ ],
1602
1667
  cardDetail: "Card",
1603
1668
  panelSettings: "Panel settings",
1604
1669
  title: "Title",
@@ -1934,6 +1999,28 @@ var UI = {
1934
1999
  releaseNone: "Esta versi\xF3n se public\xF3 sin notas.",
1935
2000
  updateCardTitle: (version) => `Actualizar Paneltir a la v${version}`,
1936
2001
  updateCardBody: (version) => `Ejecuta \`npm install paneltir@${version}\` en este proyecto y comprueba que el panel sigue construy\xE9ndose y dibuj\xE1ndose. La versi\xF3n en uso la reporta \`paneltirBuild\`, as\xED que se puede confirmar en vez de darla por hecha.`,
2002
+ versionBumpAsk: "Pedir subir versi\xF3n",
2003
+ versionBumpOpen: "Ya hay una subida de versi\xF3n pedida \u2014 abrirla",
2004
+ versionBumpHow: "Escribe una tarjeta de orden: todos los targets suben una versi\xF3n y un build, a la par. No cambia nada hasta que una sesi\xF3n lo haga, y la tarjeta queda sin guardar hasta Guardar, como cualquier edici\xF3n.",
2005
+ versionBumpTitle: "Subir la versi\xF3n en uno, en todos los targets",
2006
+ versionBumpBody: "Pedido desde el panel. Sube la versi\xF3n un paso y el n\xFAmero de build en uno en todos los targets que publica este proyecto \u2014 el package.json de una web, MARKETING_VERSION y CURRENT_PROJECT_VERSION de una app de iOS, versionName y versionCode de una de Android, y cualquier otro que lleve uno \u2014 para que todos acaben en la misma versi\xF3n y el mismo build. Por ejemplo: build 18, versi\xF3n 1.0.18 pasa a build 19, versi\xF3n 1.0.19. Un target sin n\xFAmero de build mueve solo la versi\xF3n. Si los targets ya no coinciden, ll\xE9valos todos al m\xE1s alto m\xE1s uno y di cu\xE1les iban por detr\xE1s. Sigue las reglas de release del propio proyecto \u2014 changelog, tag, notas \u2014 si las tiene.",
2007
+ versionBumpChecks: [
2008
+ "Encontrados todos los targets que llevan versi\xF3n o n\xFAmero de build",
2009
+ "Movido cada uno: versi\xF3n un paso arriba, build uno arriba",
2010
+ "Todos los targets acaban en la misma versi\xF3n y el mismo build"
2011
+ ],
2012
+ installAsk: "A\xF1adir una tarjeta para que Claude lo instale",
2013
+ installAskOpen: "Ya pedido \u2014 abrir la tarjeta",
2014
+ installAskHow: "La tarjeta lleva el fragmento del repositorio de arriba, as\xED que la sesi\xF3n que la recoja instala exactamente esto. Queda sin guardar hasta Guardar, como cualquier edici\xF3n.",
2015
+ installCardTitle: (tool) => `Instalar ${tool} en este repositorio`,
2016
+ installCardBody: (tool, file, snippet) => `Pedido desde la pantalla de Herramientas. Fusiona estas dos claves en ${file} \u2014 fusiona, nunca reemplaces: el archivo lleva otros ajustes. Despu\xE9s haz commit, para que ${tool} se instale para cualquiera que abra una sesi\xF3n en este repositorio.
2017
+
2018
+ ${snippet}`,
2019
+ installCardChecks: [
2020
+ "Fusionado en el archivo de ajustes sin reemplazar nada de lo que ya hab\xEDa",
2021
+ "Commiteado en el repositorio",
2022
+ "La pantalla de Herramientas lo muestra como Activo"
2023
+ ],
1937
2024
  cardDetail: "Tarjeta",
1938
2025
  panelSettings: "Ajustes del panel",
1939
2026
  title: "T\xEDtulo",
@@ -2676,7 +2763,16 @@ function ToolRow({
2676
2763
  tool.state === "misconfigured" && /* @__PURE__ */ jsx19("p", { className: "pt-warn", children: ui.stateMisconfiguredWhy })
2677
2764
  ] });
2678
2765
  }
2679
- function MarketplaceView({ report, loading, failure, lang, ui, onRefresh }) {
2766
+ function MarketplaceView({
2767
+ report,
2768
+ loading,
2769
+ failure,
2770
+ lang,
2771
+ ui,
2772
+ onRefresh,
2773
+ onRequestInstall,
2774
+ installRequested
2775
+ }) {
2680
2776
  const [filter, setFilter] = useState8("all");
2681
2777
  const [open, setOpen] = useState8(null);
2682
2778
  const tools = report?.tools ?? [];
@@ -2766,7 +2862,12 @@ function MarketplaceView({ report, loading, failure, lang, ui, onRefresh }) {
2766
2862
  tool: open,
2767
2863
  ui,
2768
2864
  marketplace: report?.marketplace ?? { name: MARKETPLACE_NAME, url: MARKETPLACE_URL },
2769
- registered: report?.project.marketplaceRegistered ?? false
2865
+ registered: report?.project.marketplaceRegistered ?? false,
2866
+ requested: installRequested?.(open.id) ?? false,
2867
+ onRequest: onRequestInstall ? (marketplace) => {
2868
+ onRequestInstall(open, marketplace);
2869
+ setOpen(null);
2870
+ } : void 0
2770
2871
  }
2771
2872
  ),
2772
2873
  /* @__PURE__ */ jsx19("button", { type: "button", className: "pt-btn pt-btn--sm", onClick: () => setOpen(null), children: ui.close })
@@ -2777,20 +2878,28 @@ function InstallSheet({
2777
2878
  tool,
2778
2879
  ui,
2779
2880
  marketplace,
2780
- registered
2881
+ registered,
2882
+ requested,
2883
+ onRequest
2781
2884
  }) {
2782
2885
  const copy = { copy: ui.copy, copied: ui.copied, selected: ui.copySelected };
2783
2886
  return /* @__PURE__ */ jsxs17(Fragment5, { children: [
2784
2887
  tool.claudePlugin ? /* @__PURE__ */ jsxs17(Fragment5, { children: [
2785
- /* @__PURE__ */ jsx19(DetailSection, { title: ui.installRepoTitle, subtitle: ui.installRepoHint, children: /* @__PURE__ */ jsx19(
2786
- CopyBlock,
2787
- {
2788
- text: repositorySnippet(tool.id, marketplace),
2789
- label: SETTINGS_PATH,
2790
- note: registered ? ui.installRepoMergeRegistered : ui.installRepoMerge,
2791
- labels: copy
2792
- }
2793
- ) }),
2888
+ /* @__PURE__ */ jsxs17(DetailSection, { title: ui.installRepoTitle, subtitle: ui.installRepoHint, children: [
2889
+ /* @__PURE__ */ jsx19(
2890
+ CopyBlock,
2891
+ {
2892
+ text: repositorySnippet(tool.id, marketplace),
2893
+ label: SETTINGS_PATH,
2894
+ note: registered ? ui.installRepoMergeRegistered : ui.installRepoMerge,
2895
+ labels: copy
2896
+ }
2897
+ ),
2898
+ onRequest && tool.state !== "active" && /* @__PURE__ */ jsxs17("div", { className: "pt-panel-row", style: { marginTop: 10 }, children: [
2899
+ /* @__PURE__ */ jsx19("button", { type: "button", className: "pt-btn pt-btn--sm pt-btn--primary", onClick: () => onRequest(marketplace), children: requested ? ui.installAskOpen : ui.installAsk }),
2900
+ /* @__PURE__ */ jsx19("p", { className: "pt-marketplace__detail", children: ui.installAskHow })
2901
+ ] })
2902
+ ] }),
2794
2903
  /* @__PURE__ */ jsx19(DetailSection, { title: ui.installCliTitle, subtitle: ui.installCliHint, children: /* @__PURE__ */ jsx19(CopyBlock, { text: commandSnippet(tool.id, marketplace), labels: copy }) })
2795
2904
  ] }) : /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: ui.installNoPlugin }),
2796
2905
  tool.standaloneCommand ? /* @__PURE__ */ jsx19(DetailSection, { title: ui.installStandaloneTitle, subtitle: ui.installStandaloneHint, children: /* @__PURE__ */ jsx19(CopyBlock, { text: tool.standaloneCommand, labels: copy }) }) : /* @__PURE__ */ jsx19(DetailSection, { title: ui.installStandaloneTitle, children: /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: ui.installNoStandalone }) }),
@@ -3350,18 +3459,18 @@ function PanelApp({
3350
3459
  }, []);
3351
3460
  const addCard = useCallback5(
3352
3461
  (columnId) => {
3353
- const card = emptyCard(columnId, state.areas[0]?.id ?? "general");
3462
+ const card = stampForColumn(emptyCard(columnId, state.areas[0]?.id ?? "general"), state.columns);
3354
3463
  setState((current) => ({ ...current, updatedAt: today(), cards: [...current.cards, card] }));
3355
3464
  setOpenCardId(card.id);
3356
3465
  },
3357
- [state.areas]
3466
+ [state.areas, state.columns]
3358
3467
  );
3359
3468
  const addUpdateCard = useCallback5(() => {
3360
3469
  if (!update?.behind) return;
3361
3470
  const id = updateCardId(update.latest);
3362
3471
  if (state.cards.some((card2) => card2.id === id)) return;
3363
3472
  const card = {
3364
- ...emptyCard("next", state.areas[0]?.id ?? "general"),
3473
+ ...emptyCard(queueColumn(state.columns), state.areas[0]?.id ?? "general"),
3365
3474
  id,
3366
3475
  title: { en: UI.en.updateCardTitle(update.latest), es: UI.es.updateCardTitle(update.latest) },
3367
3476
  body: { en: UI.en.updateCardBody(update.latest), es: UI.es.updateCardBody(update.latest) },
@@ -3372,7 +3481,7 @@ function PanelApp({
3372
3481
  };
3373
3482
  setState((current) => ({ ...current, updatedAt: today(), cards: [...current.cards, card] }));
3374
3483
  setOpenCardId(id);
3375
- }, [update, state.cards, state.areas, state.preferences?.updateAsOrder]);
3484
+ }, [update, state.cards, state.areas, state.columns, state.preferences?.updateAsOrder]);
3376
3485
  const requestReport = useCallback5(
3377
3486
  (section) => {
3378
3487
  const id = `report-${section}-${today()}`;
@@ -3381,7 +3490,7 @@ function PanelApp({
3381
3490
  return;
3382
3491
  }
3383
3492
  const card = {
3384
- ...emptyCard("next", state.areas[0]?.id ?? "general"),
3493
+ ...emptyCard(queueColumn(state.columns), state.areas[0]?.id ?? "general"),
3385
3494
  id,
3386
3495
  title: { en: UI.en.analysisRedoTitle[section], es: UI.es.analysisRedoTitle[section] },
3387
3496
  body: { en: UI.en.analysisRedoBody[section], es: UI.es.analysisRedoBody[section] },
@@ -3394,12 +3503,63 @@ function PanelApp({
3394
3503
  setState((current) => ({ ...current, updatedAt: today(), cards: [...current.cards, card] }));
3395
3504
  setOpenCardId(id);
3396
3505
  },
3397
- [state.cards, state.areas]
3506
+ [state.cards, state.areas, state.columns]
3507
+ );
3508
+ const requestCard = useCallback5(
3509
+ (kind, subject, content) => {
3510
+ const waiting = pendingRequest(state.cards, kind, doneColumns, subject);
3511
+ if (waiting) {
3512
+ setOpenCardId(waiting.id);
3513
+ return;
3514
+ }
3515
+ const card = {
3516
+ ...emptyCard(queueColumn(state.columns), state.areas[0]?.id ?? "general"),
3517
+ id: requestId(kind, today(), subject),
3518
+ title: content.title,
3519
+ body: content.body,
3520
+ priority: "medium",
3521
+ risk: "low",
3522
+ owner: "claude",
3523
+ order: true,
3524
+ intent: "do",
3525
+ checks: content.checks.map((text2) => ({ id: newId(), text: text2, done: false }))
3526
+ };
3527
+ setState((current) => ({ ...current, updatedAt: today(), cards: [...current.cards, card] }));
3528
+ setOpenCardId(card.id);
3529
+ },
3530
+ [state.cards, state.columns, state.areas, doneColumns]
3531
+ );
3532
+ const requestVersionBump = useCallback5(() => {
3533
+ requestCard("version-bump", void 0, {
3534
+ title: { en: UI.en.versionBumpTitle, es: UI.es.versionBumpTitle },
3535
+ body: { en: UI.en.versionBumpBody, es: UI.es.versionBumpBody },
3536
+ checks: UI.en.versionBumpChecks.map((en, i) => ({ en, es: UI.es.versionBumpChecks[i] }))
3537
+ });
3538
+ }, [requestCard]);
3539
+ const requestInstall = useCallback5(
3540
+ (tool, marketplace) => {
3541
+ const snippet = repositorySnippet(tool.id, marketplace).trimEnd();
3542
+ requestCard("install", tool.id, {
3543
+ title: { en: UI.en.installCardTitle(tool.name), es: UI.es.installCardTitle(tool.name) },
3544
+ body: {
3545
+ en: UI.en.installCardBody(tool.name, SETTINGS_PATH, snippet),
3546
+ es: UI.es.installCardBody(tool.name, SETTINGS_PATH, snippet)
3547
+ },
3548
+ checks: UI.en.installCardChecks.map((en, i) => ({ en, es: UI.es.installCardChecks[i] }))
3549
+ });
3550
+ },
3551
+ [requestCard]
3552
+ );
3553
+ const versionBumpWaiting = useMemo5(
3554
+ () => pendingRequest(state.cards, "version-bump", doneColumns),
3555
+ [state.cards, doneColumns]
3398
3556
  );
3399
3557
  const moveCard = useCallback5(
3400
3558
  (move) => {
3401
3559
  setState((current) => {
3402
- const moved = applyMove(current.cards, move, current.cards.filter(matches));
3560
+ const done = new Set(current.columns.filter((column) => column.done).map((column) => column.id));
3561
+ const drawn = current.cards.filter((card) => matches(card) && !isArchived(card, done));
3562
+ const moved = applyMove(current.cards, move, drawn);
3403
3563
  return {
3404
3564
  ...current,
3405
3565
  updatedAt: today(),
@@ -3587,7 +3747,7 @@ function PanelApp({
3587
3747
  throw Object.assign(new Error(saveError(response.status, data, body)), { conflict });
3588
3748
  }
3589
3749
  setSavedSnapshot(JSON.stringify(next));
3590
- setState(next);
3750
+ setState((current) => current === state ? next : current);
3591
3751
  setSave({
3592
3752
  status: "saved",
3593
3753
  at: (/* @__PURE__ */ new Date()).toLocaleTimeString(lang === "es" ? "es-ES" : "en-GB", {
@@ -3819,7 +3979,9 @@ function PanelApp({
3819
3979
  failure: marketFailure,
3820
3980
  lang,
3821
3981
  ui,
3822
- onRefresh: marketplaceEndpoint ? () => loadMarketplace(true) : void 0
3982
+ onRefresh: marketplaceEndpoint ? () => loadMarketplace(true) : void 0,
3983
+ onRequestInstall: saveEndpoint ? requestInstall : void 0,
3984
+ installRequested: (toolId) => Boolean(pendingRequest(state.cards, "install", doneColumns, toolId))
3823
3985
  }
3824
3986
  ),
3825
3987
  view === "analysis" && state.analysis && /* @__PURE__ */ jsx21(
@@ -3920,7 +4082,17 @@ function PanelApp({
3920
4082
  },
3921
4083
  area.id
3922
4084
  ))
3923
- ] })
4085
+ ] }),
4086
+ saveEndpoint && /* @__PURE__ */ jsx21("div", { className: "pt-panel-row pt-requests", children: /* @__PURE__ */ jsx21(
4087
+ "button",
4088
+ {
4089
+ type: "button",
4090
+ className: "pt-btn pt-btn--sm",
4091
+ title: ui.versionBumpHow,
4092
+ onClick: requestVersionBump,
4093
+ children: versionBumpWaiting ? ui.versionBumpOpen : `\u2B06 ${ui.versionBumpAsk}`
4094
+ }
4095
+ ) })
3924
4096
  ] }),
3925
4097
  /* @__PURE__ */ jsx21(
3926
4098
  Board,
@@ -4459,6 +4631,7 @@ export {
4459
4631
  countCards,
4460
4632
  cyberpunkTheme,
4461
4633
  decided,
4634
+ dropIndex,
4462
4635
  emptyCard,
4463
4636
  explainBoard,
4464
4637
  gateStatus,
@@ -4474,9 +4647,12 @@ export {
4474
4647
  panelForm,
4475
4648
  paneltirBuild,
4476
4649
  paperForm,
4650
+ pendingRequest,
4651
+ queueColumn,
4477
4652
  readBoard,
4478
4653
  readStoredLang,
4479
4654
  repositorySnippet,
4655
+ requestId,
4480
4656
  resetGuide,
4481
4657
  sparkPoints,
4482
4658
  stampForColumn,
package/fingerprint.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "paneltir",
3
- "version": "0.12.0",
4
- "hash": "sha256:ea981bdb1de721f4d2afd279244a7298b025773fb4a5abf8ffa33868c0d73f7d",
5
- "fileCount": 166,
6
- "templateHash": "sha256:c0c1a1678d860212ff82d55c7e7c0ee3eaf8be623621cadbb6ca5bf51b31e2ba",
7
- "generatedAt": "2026-09-14T04:42:15.291Z"
3
+ "version": "0.13.0",
4
+ "hash": "sha256:f3b5664bddf68f9560bd368b01184f97c733b9a5a3706507ae2219c6a41d9a1c",
5
+ "fileCount": 167,
6
+ "templateHash": "sha256:7c204bad5c7a8b329ad53ac51b5856438fe84735cea47543eb36b976edbb27da",
7
+ "generatedAt": "2026-09-26T09:08:52.803Z"
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paneltir",
3
- "version": "0.12.0",
3
+ "version": "0.13.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,49 +99,35 @@
99
99
  "node": ">=20"
100
100
  },
101
101
  "paneltirRelease": {
102
- "version": "0.12.0",
103
- "date": "2026-09-14",
102
+ "version": "0.13.0",
103
+ "date": "2026-09-26",
104
104
  "entries": [
105
105
  {
106
106
  "kind": "feature",
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": "The board can ask for a version bump. \"Ask for a version bump\", under the filter row, writes an order card: every target the project ships moves up one version and one build, and they all end on the same pair — build 18, 1.0.18 becomes build 19, 1.0.19. The panel still runs nothing; the card is the request, and only one is open at a time.",
109
+ "es": "El tablero puede pedir una subida de versión. «Pedir subir versión», bajo la fila de filtros, escribe una tarjeta de orden: todos los targets del proyecto suben una versión y un build, y acaban en el mismo par — build 18, 1.0.18 pasa a build 19, 1.0.19. El panel sigue sin ejecutar nada; la tarjeta es la petición, y solo hay una abierta a la vez."
110
110
  }
111
111
  },
112
112
  {
113
113
  "kind": "feature",
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": "Installing a tool can be handed to Claude. The install sheet gains \"Add a card asking Claude to install it\", which writes an order carrying the exact settings snippet, and whose last step is the Tools screen reading Active. Offered for plugins not already active, one request per tool.",
116
+ "es": "Instalar una herramienta se le puede pasar a Claude. La hoja de instalar gana «Añadir una tarjeta para que Claude lo instale», que escribe una orden con el fragmento de ajustes exacto y cuyo último paso es que la pantalla de Herramientas diga Activo. Para plugins que no estén ya activos, una petición por herramienta."
117
117
  }
118
118
  },
119
119
  {
120
120
  "kind": "feature",
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."
124
- }
125
- },
126
- {
127
- "kind": "feature",
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."
131
- }
132
- },
133
- {
134
- "kind": "fix",
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."
122
+ "en": "`pendingRequest` and `requestId` are exported, so a project building its own screen can write the same requests with the same one-open-at-a-time rule.",
123
+ "es": "`pendingRequest` y `requestId` se exportan, así que un proyecto que construye su propia pantalla puede escribir las mismas peticiones con la misma regla de una abierta a la vez."
138
124
  }
139
125
  },
140
126
  {
141
127
  "kind": "chore",
142
128
  "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."
129
+ "en": "The skill a project keeps says how to carry out both cards, and `check:docs` fails if the version-bump rule stops reaching it.",
130
+ "es": "La skill que guarda cada proyecto explica cómo llevar a cabo las dos tarjetas, y `check:docs` falla si la regla de subir versión deja de llegarle."
145
131
  }
146
132
  }
147
133
  ]
@@ -59,6 +59,56 @@ 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
+
88
+ ## Cards the panel wrote for the owner
89
+
90
+ Some cards were not typed: a button wrote them, because the panel runs nothing
91
+ and can only record the ask. Their id says which button, and each is an order
92
+ with `intent: do` — carry it out like any other.
93
+
94
+ **`version-bump-<date>` — bump the version by one, on every target.** Find
95
+ every place this project carries a version or a build number: a web app's
96
+ `package.json`, an iOS app's `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`,
97
+ an Android app's `versionName` and `versionCode`, and anything else. Move the
98
+ version up one step and the build up one on each, so every target ends on the
99
+ same pair — build 18, version 1.0.18 becomes build 19, version 1.0.19. A target
100
+ with no build number moves its version only. If the targets already disagree,
101
+ bring them all to the highest plus one and say which were behind. Follow the
102
+ project's own release rules if it has any. Only one is ever open at a time:
103
+ the button opens the waiting card instead of writing a second, so two bumps in
104
+ a row are two separate asks, not a duplicate.
105
+
106
+ **`install-<tool>-<date>` — install a plugin in this repository.** The body
107
+ carries the exact snippet. Merge its two keys into `.claude/settings.json` —
108
+ merge, never replace: the file holds other settings — and commit. The last
109
+ step is the panel's Tools screen reading *Active*, which is how the install is
110
+ checked rather than assumed.
111
+
62
112
  ## Read the analysis before deciding anything
63
113
 
64
114
  `analysis.choices` holds the questions the owner has already answered, grouped