paneltir 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3754 @@
1
+ // src/version.ts
2
+ var PANELTIR_VERSION = "0.6.0";
3
+ var PANELTIR_FINGERPRINT = "sha256:cb1a1df3cac0d3cc775fbaa7b9f385d6d35e22cecbe86cc0a1da4338cf43de37";
4
+ var PANELTIR_FILE_COUNT = 151;
5
+ var paneltirBuild = {
6
+ version: PANELTIR_VERSION,
7
+ fingerprint: PANELTIR_FINGERPRINT,
8
+ fileCount: PANELTIR_FILE_COUNT,
9
+ /** The tag this build came from, ready to compare or to link. */
10
+ tag: `v${PANELTIR_VERSION}`,
11
+ /** Short form, for a footer that has one line to spare. */
12
+ short: `v${PANELTIR_VERSION} \xB7 ${PANELTIR_FINGERPRINT.replace("sha256:", "").slice(0, 12)}`
13
+ };
14
+
15
+ // src/theme/ThemeProvider.tsx
16
+ import { createContext, useContext, useMemo } from "react";
17
+ import { jsx } from "react/jsx-runtime";
18
+ var ThemeContext = createContext(null);
19
+ var FormContext = createContext(null);
20
+ function put(vars, name, value) {
21
+ if (value !== void 0) vars[name] = value;
22
+ }
23
+ function DashboardThemeProvider({ tokens, form, children, className, style }) {
24
+ const cssVars = useMemo(() => {
25
+ const vars = {
26
+ "--pt-color-ground": tokens.ground,
27
+ "--pt-color-ground-raised": tokens.groundRaised,
28
+ "--pt-color-panel": tokens.panel,
29
+ "--pt-color-raised": tokens.raised,
30
+ "--pt-color-line": tokens.line,
31
+ "--pt-color-line-faint": tokens.lineFaint,
32
+ "--pt-color-ink": tokens.ink,
33
+ "--pt-color-ink-dim": tokens.inkDim,
34
+ "--pt-color-ink-faint": tokens.inkFaint,
35
+ "--pt-color-accent": tokens.accent,
36
+ "--pt-color-accent-start": tokens.accentStart ?? tokens.accent,
37
+ "--pt-color-accent-end": tokens.accentEnd ?? tokens.accent,
38
+ "--pt-color-accent-ink": tokens.accentInk ?? tokens.ground,
39
+ "--pt-color-danger": tokens.danger,
40
+ "--pt-color-warning": tokens.warning,
41
+ "--pt-color-success": tokens.success,
42
+ "--pt-color-neutral": tokens.neutral
43
+ };
44
+ put(vars, "--pt-radius-panel", form?.radiusPanel);
45
+ put(vars, "--pt-radius-card", form?.radiusCard);
46
+ put(vars, "--pt-radius-control", form?.radiusControl);
47
+ put(vars, "--pt-radius-square", form?.radiusSquare);
48
+ put(vars, "--pt-radius-pill", form?.radiusPill);
49
+ put(vars, "--pt-border-width", form?.borderWidth);
50
+ put(vars, "--pt-font-family", form?.fontFamily);
51
+ put(vars, "--pt-font-family-display", form?.fontFamilyDisplay);
52
+ put(vars, "--pt-font-size-base", form?.fontSize);
53
+ put(vars, "--pt-label-tracking", form?.labelTracking);
54
+ put(vars, "--pt-density", form?.density === void 0 ? void 0 : String(form.density));
55
+ return vars;
56
+ }, [tokens, form]);
57
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: tokens, children: /* @__PURE__ */ jsx(FormContext.Provider, { value: form ?? null, children: /* @__PURE__ */ jsx(
58
+ "div",
59
+ {
60
+ className: ["pt-theme-root", className].filter(Boolean).join(" "),
61
+ "data-pt-form": form?.shape ?? "panel",
62
+ style: { ...cssVars, ...style },
63
+ children
64
+ }
65
+ ) }) });
66
+ }
67
+ function useDashboardForm() {
68
+ return useContext(FormContext);
69
+ }
70
+ function useDashboardTheme() {
71
+ const ctx = useContext(ThemeContext);
72
+ if (!ctx) {
73
+ throw new Error("useDashboardTheme must be used inside a <DashboardThemeProvider>.");
74
+ }
75
+ return ctx;
76
+ }
77
+
78
+ // src/themes/presets.ts
79
+ var midnightTheme = {
80
+ ground: "#0b0c10",
81
+ groundRaised: "#14161c",
82
+ panel: "#181a21",
83
+ raised: "#20232b",
84
+ line: "rgba(255,255,255,0.13)",
85
+ lineFaint: "rgba(255,255,255,0.07)",
86
+ ink: "rgba(255,255,255,0.92)",
87
+ inkDim: "rgba(255,255,255,0.62)",
88
+ inkFaint: "rgba(255,255,255,0.34)",
89
+ accent: "#5b8cff",
90
+ accentStart: "#6f9bff",
91
+ accentEnd: "#4a72e0",
92
+ danger: "#ff5c5c",
93
+ warning: "#f5b74c",
94
+ success: "#3ecf8e",
95
+ neutral: "#8a8f98"
96
+ };
97
+ var oldMoneyTheme = {
98
+ ground: "#12140f",
99
+ groundRaised: "#1b1d14",
100
+ panel: "#1f2117",
101
+ raised: "#262819",
102
+ line: "rgba(232,224,196,0.14)",
103
+ lineFaint: "rgba(232,224,196,0.07)",
104
+ ink: "rgba(240,235,220,0.94)",
105
+ inkDim: "rgba(240,235,220,0.62)",
106
+ inkFaint: "rgba(240,235,220,0.34)",
107
+ accent: "#c9a227",
108
+ accentStart: "#d9b545",
109
+ accentEnd: "#8b6f3e",
110
+ danger: "#8c3b34",
111
+ warning: "#c17f3a",
112
+ success: "#5b7a52",
113
+ neutral: "#8a8574"
114
+ };
115
+ var cyberpunkTheme = {
116
+ ground: "#0b0409",
117
+ groundRaised: "#1a0711",
118
+ panel: "#210a15",
119
+ raised: "#2c0e1c",
120
+ line: "rgba(255,90,110,0.17)",
121
+ lineFaint: "rgba(255,90,110,0.08)",
122
+ ink: "rgba(255,241,236,0.95)",
123
+ inkDim: "rgba(255,226,220,0.62)",
124
+ inkFaint: "rgba(255,214,208,0.36)",
125
+ accent: "#fcee0a",
126
+ accentStart: "#fff64f",
127
+ accentEnd: "#d1c200",
128
+ accentInk: "#0b0409",
129
+ danger: "#ff003c",
130
+ warning: "#ff7a18",
131
+ success: "#00f0ff",
132
+ neutral: "#a06174"
133
+ };
134
+ var claudeTheme = {
135
+ ground: "#faf9f5",
136
+ groundRaised: "#f2efe7",
137
+ panel: "#ffffff",
138
+ raised: "#f6f3ec",
139
+ line: "rgba(61,48,38,0.16)",
140
+ lineFaint: "rgba(61,48,38,0.08)",
141
+ ink: "rgba(38,32,26,0.94)",
142
+ inkDim: "rgba(38,32,26,0.66)",
143
+ inkFaint: "rgba(38,32,26,0.42)",
144
+ accent: "#c15f3c",
145
+ accentStart: "#d4724f",
146
+ accentEnd: "#a54c2d",
147
+ accentInk: "#fffaf5",
148
+ danger: "#b3261e",
149
+ warning: "#a86a12",
150
+ success: "#3f7d52",
151
+ neutral: "#8a7f73"
152
+ };
153
+ var panelForm = {
154
+ shape: "panel"
155
+ };
156
+ var ledgerForm = {
157
+ shape: "ledger",
158
+ radiusPanel: "0px",
159
+ radiusCard: "0px",
160
+ radiusControl: "2px",
161
+ radiusSquare: "0px",
162
+ radiusPill: "2px",
163
+ borderWidth: "1px",
164
+ fontFamily: 'ui-serif, Georgia, "Times New Roman", serif',
165
+ fontFamilyDisplay: 'ui-serif, Georgia, "Times New Roman", serif',
166
+ fontSize: "15px",
167
+ labelTracking: "2.4px",
168
+ density: 1.25
169
+ };
170
+ var paperForm = {
171
+ shape: "panel",
172
+ radiusPanel: "16px",
173
+ radiusCard: "12px",
174
+ radiusControl: "8px",
175
+ radiusPill: "999px",
176
+ borderWidth: "1px",
177
+ fontFamily: 'ui-sans-serif, -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif',
178
+ fontFamilyDisplay: 'ui-serif, "Iowan Old Style", Georgia, "Times New Roman", serif',
179
+ labelTracking: "1.2px",
180
+ density: 1.1
181
+ };
182
+ var THEME_FORMS = {
183
+ panel: panelForm,
184
+ ledger: ledgerForm,
185
+ paper: paperForm
186
+ };
187
+ var THEME_PRESETS = {
188
+ midnight: midnightTheme,
189
+ oldMoney: oldMoneyTheme,
190
+ cyberpunk: cyberpunkTheme,
191
+ claude: claudeTheme
192
+ };
193
+
194
+ // src/components/Header/Header.tsx
195
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
196
+ function DashboardHeader({ wordmark, status, controls, linkLabel, linkHref, onLinkClick }) {
197
+ return /* @__PURE__ */ jsxs("header", { className: "pt-header", children: [
198
+ /* @__PURE__ */ jsx2("div", { className: "pt-header__wordmark", children: wordmark }),
199
+ /* @__PURE__ */ jsx2("div", { className: "pt-header__spacer" }),
200
+ status && /* @__PURE__ */ jsx2("div", { className: "pt-header__status", children: status }),
201
+ controls && /* @__PURE__ */ jsx2("div", { className: "pt-header__controls", children: controls }),
202
+ linkLabel && (linkHref ? /* @__PURE__ */ jsx2("a", { className: "pt-header__link", href: linkHref, children: linkLabel }) : /* @__PURE__ */ jsx2("button", { type: "button", className: "pt-header__link", onClick: onLinkClick, children: linkLabel }))
203
+ ] });
204
+ }
205
+
206
+ // src/components/AlertBanner/AlertBanner.tsx
207
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
208
+ function AlertBanner({ visible, message, actionLabel, onAction }) {
209
+ if (!visible) return null;
210
+ return /* @__PURE__ */ jsx3("div", { className: "pt-alert", role: "alert", children: /* @__PURE__ */ jsxs2("p", { className: "pt-alert__message", children: [
211
+ message,
212
+ actionLabel && /* @__PURE__ */ jsxs2(Fragment, { children: [
213
+ " ",
214
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "pt-alert__action", onClick: onAction, children: actionLabel })
215
+ ] })
216
+ ] }) });
217
+ }
218
+
219
+ // src/components/StatTiles/StatTile.tsx
220
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
221
+ function StatTile({ label, value, tone = "neutral", isZero }) {
222
+ const toneClass = isZero ? "is-calm" : `is-${tone}`;
223
+ return /* @__PURE__ */ jsxs3("div", { className: "pt-stat-tile", children: [
224
+ /* @__PURE__ */ jsx4("div", { className: `pt-stat-tile__value ${toneClass}`, children: value }),
225
+ /* @__PURE__ */ jsx4("div", { className: "pt-stat-tile__label", children: label })
226
+ ] });
227
+ }
228
+ function StatTileGrid({ children }) {
229
+ return /* @__PURE__ */ jsx4("div", { className: "pt-stat-grid", children });
230
+ }
231
+
232
+ // src/components/HealthPills/HealthPill.tsx
233
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
234
+ function HealthPill({ label, value, status }) {
235
+ return /* @__PURE__ */ jsxs4("div", { className: `pt-health-pill pt-health-pill--${status}`, children: [
236
+ /* @__PURE__ */ jsx5("span", { className: "pt-health-pill__label", children: label }),
237
+ /* @__PURE__ */ jsx5("span", { className: "pt-health-pill__value", children: value })
238
+ ] });
239
+ }
240
+ function HealthPillRow({ children }) {
241
+ return /* @__PURE__ */ jsx5("div", { className: "pt-health-row", children });
242
+ }
243
+
244
+ // src/components/FilterChips/FilterChip.tsx
245
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
246
+ function FilterChip({ label, count, active, onClick }) {
247
+ return /* @__PURE__ */ jsxs5("button", { type: "button", className: `pt-filter-chip${active ? " is-active" : ""}`, "aria-pressed": active, onClick, children: [
248
+ label,
249
+ typeof count === "number" && /* @__PURE__ */ jsx6("span", { className: "pt-filter-chip__count", children: count })
250
+ ] });
251
+ }
252
+ function FilterChipRow({ children }) {
253
+ return /* @__PURE__ */ jsx6("div", { className: "pt-filter-row", children });
254
+ }
255
+
256
+ // src/components/Board/Board.tsx
257
+ import { useRef as useRef3 } from "react";
258
+
259
+ // src/components/Board/Column.tsx
260
+ import { Fragment as Fragment2 } from "react";
261
+
262
+ // src/components/Board/BoardDndContext.tsx
263
+ import { createContext as createContext2, useContext as useContext2 } from "react";
264
+ import { jsx as jsx7 } from "react/jsx-runtime";
265
+ var BoardDndContext = createContext2(null);
266
+ function BoardDndProvider({ value, children }) {
267
+ return /* @__PURE__ */ jsx7(BoardDndContext.Provider, { value, children });
268
+ }
269
+ function useBoardDnDContext() {
270
+ const ctx = useContext2(BoardDndContext);
271
+ if (!ctx) {
272
+ throw new Error("<Card> and <Column> must be rendered inside <Board>.");
273
+ }
274
+ return ctx;
275
+ }
276
+
277
+ // src/components/Board/Column.tsx
278
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
279
+ function Column({ column, renderCard, onAddCard }) {
280
+ const dnd = useBoardDnDContext();
281
+ return /* @__PURE__ */ jsxs6("div", { className: "pt-column", children: [
282
+ /* @__PURE__ */ jsxs6("div", { className: "pt-column__header", children: [
283
+ /* @__PURE__ */ jsx8("span", { className: "pt-column__title", title: column.hint, children: column.title }),
284
+ /* @__PURE__ */ jsx8("span", { className: "pt-column__count", children: column.cards.length }),
285
+ column.hint && /* @__PURE__ */ jsx8("span", { className: "pt-sr-only", children: column.hint }),
286
+ /* @__PURE__ */ jsx8("span", { className: "pt-column__spacer" }),
287
+ onAddCard && /* @__PURE__ */ jsx8(
288
+ "button",
289
+ {
290
+ type: "button",
291
+ className: "pt-column__add",
292
+ onClick: () => onAddCard(column.id),
293
+ "aria-label": `Add to ${column.title}`,
294
+ children: "+"
295
+ }
296
+ )
297
+ ] }),
298
+ /* @__PURE__ */ jsxs6("div", { className: "pt-column__list", "data-pt-column-list": column.id, children: [
299
+ column.cards.map((card, index) => {
300
+ const showBefore = dnd.isDragging && dnd.dropTarget?.columnId === column.id && dnd.dropTarget.index === index;
301
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
302
+ showBefore && /* @__PURE__ */ jsx8("div", { className: "pt-card-placeholder" }),
303
+ renderCard(card, index, column.id)
304
+ ] }, card.id);
305
+ }),
306
+ dnd.isDragging && dnd.dropTarget?.columnId === column.id && dnd.dropTarget.index === column.cards.length && /* @__PURE__ */ jsx8("div", { className: "pt-card-placeholder" })
307
+ ] })
308
+ ] });
309
+ }
310
+
311
+ // src/components/Board/useBoardDnD.ts
312
+ import { useCallback, useEffect as useEffect2, useRef as useRef2, useState } from "react";
313
+
314
+ // src/components/Board/useAutoscroll.ts
315
+ import { useEffect, useRef } from "react";
316
+ function useAutoscroll({ active, pointerRef, scrollContainerRef, edgeSize = 56, maxSpeed = 18 }) {
317
+ const rafRef = useRef(null);
318
+ useEffect(() => {
319
+ if (!active) return;
320
+ function tick() {
321
+ const pointer = pointerRef.current;
322
+ const container = scrollContainerRef.current;
323
+ if (pointer && container) {
324
+ const rect = container.getBoundingClientRect();
325
+ let dx = 0;
326
+ const fromLeft = pointer.x - rect.left;
327
+ const fromRight = rect.right - pointer.x;
328
+ if (fromLeft >= 0 && fromLeft < edgeSize) {
329
+ dx = -maxSpeed * (1 - fromLeft / edgeSize);
330
+ } else if (fromRight >= 0 && fromRight < edgeSize) {
331
+ dx = maxSpeed * (1 - fromRight / edgeSize);
332
+ }
333
+ if (dx !== 0) container.scrollLeft += dx;
334
+ const el = document.elementFromPoint(pointer.x, pointer.y);
335
+ const columnList = el?.closest("[data-pt-column-list]");
336
+ if (columnList) {
337
+ const colRect = columnList.getBoundingClientRect();
338
+ let dy = 0;
339
+ const fromTop = pointer.y - colRect.top;
340
+ const fromBottom = colRect.bottom - pointer.y;
341
+ if (fromTop >= 0 && fromTop < edgeSize) {
342
+ dy = -maxSpeed * (1 - fromTop / edgeSize);
343
+ } else if (fromBottom >= 0 && fromBottom < edgeSize) {
344
+ dy = maxSpeed * (1 - fromBottom / edgeSize);
345
+ }
346
+ if (dy !== 0) columnList.scrollTop += dy;
347
+ }
348
+ }
349
+ rafRef.current = requestAnimationFrame(tick);
350
+ }
351
+ rafRef.current = requestAnimationFrame(tick);
352
+ return () => {
353
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
354
+ };
355
+ }, [active, pointerRef, scrollContainerRef, edgeSize, maxSpeed]);
356
+ }
357
+
358
+ // src/components/Board/useBoardDnD.ts
359
+ function useBoardDnD({ columns, onMove, scrollContainerRef }) {
360
+ const [drag, setDrag] = useState(null);
361
+ const [pointer, setPointer] = useState(null);
362
+ const [dropTarget, setDropTarget] = useState(null);
363
+ const pointerRef = useRef2(null);
364
+ const dropTargetRef = useRef2(null);
365
+ const dragRef = useRef2(null);
366
+ const cloneElRef = useRef2(null);
367
+ const cloneLayerRef = useRef2(null);
368
+ useEffect2(() => {
369
+ dropTargetRef.current = dropTarget;
370
+ }, [dropTarget]);
371
+ useAutoscroll({ active: !!drag, pointerRef, scrollContainerRef });
372
+ const startDrag = useCallback((e, cardId, columnId, index) => {
373
+ if (e.pointerType === "mouse" && e.button !== 0) return;
374
+ const cardEl = e.currentTarget.closest("[data-pt-card]");
375
+ if (!cardEl) return;
376
+ const rect = cardEl.getBoundingClientRect();
377
+ const state = {
378
+ cardId,
379
+ columnId,
380
+ index,
381
+ pointerId: e.pointerId,
382
+ offsetX: e.clientX - rect.left,
383
+ offsetY: e.clientY - rect.top
384
+ };
385
+ dragRef.current = state;
386
+ setDrag(state);
387
+ const initialTarget = { columnId, index };
388
+ dropTargetRef.current = initialTarget;
389
+ setDropTarget(initialTarget);
390
+ pointerRef.current = { x: e.clientX, y: e.clientY };
391
+ setPointer(pointerRef.current);
392
+ const clone = cardEl.cloneNode(true);
393
+ clone.classList.add("pt-card--clone");
394
+ clone.style.width = `${rect.width}px`;
395
+ clone.style.transform = `translate3d(${rect.left}px, ${rect.top}px, 0)`;
396
+ cloneElRef.current = clone;
397
+ cloneLayerRef.current?.appendChild(clone);
398
+ document.body.style.cursor = "grabbing";
399
+ e.target.setPointerCapture?.(e.pointerId);
400
+ }, []);
401
+ useEffect2(() => {
402
+ if (!drag) return;
403
+ const activeDrag = drag;
404
+ function moveClone(x, y) {
405
+ const clone = cloneElRef.current;
406
+ if (!clone || !dragRef.current) return;
407
+ clone.style.transform = `translate3d(${x - dragRef.current.offsetX}px, ${y - dragRef.current.offsetY}px, 0)`;
408
+ }
409
+ function handleMove(e) {
410
+ if (e.pointerId !== activeDrag.pointerId) return;
411
+ pointerRef.current = { x: e.clientX, y: e.clientY };
412
+ setPointer(pointerRef.current);
413
+ moveClone(e.clientX, e.clientY);
414
+ const el = document.elementFromPoint(e.clientX, e.clientY);
415
+ const listEl = el?.closest("[data-pt-column-list]");
416
+ if (!listEl) return;
417
+ const columnId = listEl.getAttribute("data-pt-column-list");
418
+ const cardEl = el?.closest("[data-pt-card]");
419
+ let index;
420
+ if (cardEl && listEl.contains(cardEl)) {
421
+ const cardRect = cardEl.getBoundingClientRect();
422
+ const after = e.clientY > cardRect.top + cardRect.height / 2;
423
+ index = Number(cardEl.getAttribute("data-pt-card-index")) + (after ? 1 : 0);
424
+ } else {
425
+ const column = columns.find((c) => c.id === columnId);
426
+ index = column ? column.cards.length : 0;
427
+ }
428
+ const next = { columnId, index };
429
+ dropTargetRef.current = next;
430
+ setDropTarget(next);
431
+ }
432
+ function swallowNextClick() {
433
+ const swallow = (event) => {
434
+ event.stopPropagation();
435
+ event.preventDefault();
436
+ };
437
+ window.addEventListener("click", swallow, { capture: true, once: true });
438
+ window.setTimeout(() => window.removeEventListener("click", swallow, true), 0);
439
+ }
440
+ function endDrag() {
441
+ swallowNextClick();
442
+ const finalTarget = dropTargetRef.current;
443
+ const source = dragRef.current;
444
+ if (finalTarget && source) {
445
+ const sameSpot = finalTarget.columnId === source.columnId && (finalTarget.index === source.index || finalTarget.index === source.index + 1);
446
+ if (!sameSpot) {
447
+ onMove({
448
+ cardId: source.cardId,
449
+ fromColumnId: source.columnId,
450
+ toColumnId: finalTarget.columnId,
451
+ toIndex: finalTarget.index
452
+ });
453
+ }
454
+ }
455
+ cloneElRef.current?.remove();
456
+ cloneElRef.current = null;
457
+ dragRef.current = null;
458
+ dropTargetRef.current = null;
459
+ document.body.style.cursor = "";
460
+ setDrag(null);
461
+ setDropTarget(null);
462
+ setPointer(null);
463
+ pointerRef.current = null;
464
+ }
465
+ function handleUp(e) {
466
+ if (e.pointerId !== activeDrag.pointerId) return;
467
+ endDrag();
468
+ }
469
+ window.addEventListener("pointermove", handleMove);
470
+ window.addEventListener("pointerup", handleUp);
471
+ window.addEventListener("pointercancel", handleUp);
472
+ return () => {
473
+ window.removeEventListener("pointermove", handleMove);
474
+ window.removeEventListener("pointerup", handleUp);
475
+ window.removeEventListener("pointercancel", handleUp);
476
+ };
477
+ }, [drag, columns, onMove]);
478
+ return {
479
+ isDragging: !!drag,
480
+ drag,
481
+ pointer,
482
+ dropTarget,
483
+ startDrag,
484
+ cloneLayerRef
485
+ };
486
+ }
487
+
488
+ // src/components/Board/Board.tsx
489
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
490
+ function Board({ columns, renderCard, onMove, onAddCard }) {
491
+ const scrollRef = useRef3(null);
492
+ const dnd = useBoardDnD({ columns, onMove, scrollContainerRef: scrollRef });
493
+ return /* @__PURE__ */ jsxs7(BoardDndProvider, { value: dnd, children: [
494
+ /* @__PURE__ */ jsx9("div", { ref: scrollRef, className: `pt-board${dnd.isDragging ? " pt-board--dragging" : ""}`, children: columns.map((column) => /* @__PURE__ */ jsx9(Column, { column, renderCard, onAddCard }, column.id)) }),
495
+ /* @__PURE__ */ jsx9("div", { className: "pt-board__clone-layer", ref: dnd.cloneLayerRef })
496
+ ] });
497
+ }
498
+
499
+ // src/components/Board/Card.tsx
500
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
501
+ function Card({ cardId, columnId, index, title, priorityColor, tags, progress, children, onClick }) {
502
+ const dnd = useBoardDnDContext();
503
+ const isDraggedSource = dnd.drag?.cardId === cardId;
504
+ return /* @__PURE__ */ jsxs8(
505
+ "div",
506
+ {
507
+ className: `pt-card${isDraggedSource ? " pt-card--dragging-source" : ""}`,
508
+ "data-pt-card": true,
509
+ "data-pt-card-index": index,
510
+ style: priorityColor ? { "--pt-card-stripe": priorityColor } : void 0,
511
+ onClick,
512
+ children: [
513
+ /* @__PURE__ */ jsxs8("div", { className: "pt-card__row", children: [
514
+ /* @__PURE__ */ jsx10(
515
+ "button",
516
+ {
517
+ type: "button",
518
+ className: "pt-card__grip",
519
+ "aria-label": "Reorder card",
520
+ onPointerDown: (e) => dnd.startDrag(e, cardId, columnId, index),
521
+ children: "\u283F"
522
+ }
523
+ ),
524
+ /* @__PURE__ */ jsx10("div", { className: "pt-card__title", children: title })
525
+ ] }),
526
+ tags && tags.length > 0 && /* @__PURE__ */ jsx10("div", { className: "pt-card__tags", children: tags.map((tag, i) => /* @__PURE__ */ jsx10("span", { className: `pt-tag pt-tag--${tag.tone ?? "neutral"}${tag.emphasize ? " pt-tag--emphasize" : ""}`, children: tag.label }, i)) }),
527
+ progress && /* @__PURE__ */ jsxs8("div", { className: `pt-card__progress${progress.done >= progress.total ? " is-complete" : ""}`, children: [
528
+ progress.done,
529
+ "/",
530
+ progress.total
531
+ ] }),
532
+ children
533
+ ]
534
+ }
535
+ );
536
+ }
537
+
538
+ // src/components/Board/moveCard.ts
539
+ function moveCardInColumns(columns, move) {
540
+ const fromColumn = columns.find((c) => c.id === move.fromColumnId);
541
+ const card = fromColumn?.cards.find((c) => c.id === move.cardId);
542
+ if (!fromColumn || !card) return columns;
543
+ return columns.map((column) => {
544
+ if (column.id === move.fromColumnId && column.id === move.toColumnId) {
545
+ const withoutCard = column.cards.filter((c) => c.id !== move.cardId);
546
+ const insertAt = Math.min(move.toIndex, withoutCard.length);
547
+ return { ...column, cards: [...withoutCard.slice(0, insertAt), card, ...withoutCard.slice(insertAt)] };
548
+ }
549
+ if (column.id === move.fromColumnId) {
550
+ return { ...column, cards: column.cards.filter((c) => c.id !== move.cardId) };
551
+ }
552
+ if (column.id === move.toColumnId) {
553
+ const insertAt = Math.min(move.toIndex, column.cards.length);
554
+ return { ...column, cards: [...column.cards.slice(0, insertAt), card, ...column.cards.slice(insertAt)] };
555
+ }
556
+ return column;
557
+ });
558
+ }
559
+
560
+ // src/components/DetailSheet/DetailSheet.tsx
561
+ import { useEffect as useEffect3 } from "react";
562
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
563
+ function DetailSheet({ open, onClose, children, ariaLabel }) {
564
+ useEffect3(() => {
565
+ if (!open) return;
566
+ function handleKey(e) {
567
+ if (e.key === "Escape") onClose();
568
+ }
569
+ window.addEventListener("keydown", handleKey);
570
+ return () => window.removeEventListener("keydown", handleKey);
571
+ }, [open, onClose]);
572
+ if (!open) return null;
573
+ return /* @__PURE__ */ jsx11("div", { className: "pt-sheet-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs9("div", { className: "pt-sheet", role: "dialog", "aria-modal": "true", "aria-label": ariaLabel, onClick: (e) => e.stopPropagation(), children: [
574
+ /* @__PURE__ */ jsx11("div", { className: "pt-sheet__handle" }),
575
+ children
576
+ ] }) });
577
+ }
578
+ function DetailSection({ title, subtitle, children }) {
579
+ return /* @__PURE__ */ jsxs9("section", { className: "pt-detail-section", children: [
580
+ /* @__PURE__ */ jsxs9("h3", { className: "pt-detail-section__title", children: [
581
+ title,
582
+ subtitle && /* @__PURE__ */ jsxs9("span", { className: "pt-detail-section__subtitle", children: [
583
+ " \u2014 ",
584
+ subtitle
585
+ ] })
586
+ ] }),
587
+ children
588
+ ] });
589
+ }
590
+
591
+ // src/components/Guide/Guide.tsx
592
+ import { useCallback as useCallback2, useEffect as useEffect4, useState as useState2 } from "react";
593
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
594
+ import { createElement } from "react";
595
+ var STORE_PREFIX = "pt-guide:";
596
+ function readDismissed(key) {
597
+ try {
598
+ return window.localStorage.getItem(STORE_PREFIX + key) === "done";
599
+ } catch {
600
+ return false;
601
+ }
602
+ }
603
+ function writeDismissed(key) {
604
+ try {
605
+ window.localStorage.setItem(STORE_PREFIX + key, "done");
606
+ } catch {
607
+ }
608
+ }
609
+ function useGuideNote(id, remember = true) {
610
+ const [dismissed, setDismissed] = useState2(null);
611
+ useEffect4(() => {
612
+ setDismissed(remember ? readDismissed(id) : false);
613
+ }, [id, remember]);
614
+ const dismiss = useCallback2(() => {
615
+ if (remember) writeDismissed(id);
616
+ setDismissed(true);
617
+ }, [id, remember]);
618
+ return { visible: dismissed === false, dismiss };
619
+ }
620
+ function resetGuide() {
621
+ try {
622
+ const keys = [];
623
+ for (let i = 0; i < window.localStorage.length; i += 1) {
624
+ const key = window.localStorage.key(i);
625
+ if (key && key.startsWith(STORE_PREFIX)) keys.push(key);
626
+ }
627
+ for (const key of keys) window.localStorage.removeItem(key);
628
+ } catch {
629
+ }
630
+ }
631
+ function GuideNote({
632
+ id,
633
+ title,
634
+ children,
635
+ dismissLabel = "Got it",
636
+ action,
637
+ step,
638
+ onDismissed,
639
+ remember = true
640
+ }) {
641
+ const { visible, dismiss } = useGuideNote(id, remember);
642
+ if (!visible) return null;
643
+ const close = () => {
644
+ dismiss();
645
+ onDismissed?.();
646
+ };
647
+ return /* @__PURE__ */ jsxs10("aside", { className: "pt-guide", role: "note", children: [
648
+ /* @__PURE__ */ jsxs10("div", { className: "pt-guide__head", children: [
649
+ /* @__PURE__ */ jsx12("p", { className: "pt-guide__title", children: title }),
650
+ step && /* @__PURE__ */ jsxs10("span", { className: "pt-guide__step", "aria-label": `Step ${step.index} of ${step.total}`, children: [
651
+ step.index,
652
+ "/",
653
+ step.total
654
+ ] })
655
+ ] }),
656
+ /* @__PURE__ */ jsx12("div", { className: "pt-guide__body", children }),
657
+ /* @__PURE__ */ jsxs10("div", { className: "pt-guide__actions", children: [
658
+ action && /* @__PURE__ */ jsx12("button", { type: "button", className: "pt-guide__do", onClick: action.onClick, children: action.label }),
659
+ /* @__PURE__ */ jsx12("button", { type: "button", className: "pt-guide__dismiss", onClick: close, children: dismissLabel })
660
+ ] })
661
+ ] });
662
+ }
663
+ function GuideTour({ notes, dismissed, onDismiss }) {
664
+ const controlled = dismissed !== void 0;
665
+ const [version, setVersion] = useState2(0);
666
+ const [ready, setReady] = useState2(false);
667
+ useEffect4(() => {
668
+ setReady(true);
669
+ }, []);
670
+ if (!ready && !controlled) return null;
671
+ void version;
672
+ const isDone = controlled ? (id) => dismissed.includes(id) : (id) => readDismissed(id);
673
+ const remaining = notes.filter((note) => !isDone(note.id));
674
+ const current = remaining[0];
675
+ if (!current) return null;
676
+ return /* @__PURE__ */ createElement(
677
+ GuideNote,
678
+ {
679
+ ...current,
680
+ key: current.id,
681
+ step: { index: notes.length - remaining.length + 1, total: notes.length },
682
+ remember: !controlled,
683
+ onDismissed: () => {
684
+ onDismiss?.(current.id);
685
+ setVersion((v) => v + 1);
686
+ }
687
+ }
688
+ );
689
+ }
690
+
691
+ // src/components/Notifications/Notifications.tsx
692
+ import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo2, useRef as useRef4, useState as useState3 } from "react";
693
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
694
+ var STORE_KEY = "pt-seen";
695
+ function readSeen() {
696
+ try {
697
+ const raw = window.localStorage.getItem(STORE_KEY);
698
+ return new Set(raw ? JSON.parse(raw) : []);
699
+ } catch {
700
+ return /* @__PURE__ */ new Set();
701
+ }
702
+ }
703
+ function writeSeen(seen) {
704
+ try {
705
+ const kept = [...seen].slice(-400);
706
+ window.localStorage.setItem(STORE_KEY, JSON.stringify(kept));
707
+ } catch {
708
+ }
709
+ }
710
+ var DEFAULT_LABELS = {
711
+ title: "What changed",
712
+ empty: "Nothing new since you last looked.",
713
+ markAll: "Mark all as seen",
714
+ unread: (n) => `${n} unread`
715
+ };
716
+ function NotificationBell({ notifications, labels, seen: given, onSeen }) {
717
+ const text2 = { ...DEFAULT_LABELS, ...labels };
718
+ const controlled = given !== void 0;
719
+ const [stored, setStored] = useState3(null);
720
+ const [open, setOpen] = useState3(false);
721
+ const root = useRef4(null);
722
+ useEffect5(() => {
723
+ if (!controlled) setStored(readSeen());
724
+ }, [controlled]);
725
+ const seen = controlled ? new Set(given) : stored;
726
+ useEffect5(() => {
727
+ if (!open) return;
728
+ const onDown = (e) => {
729
+ if (root.current && !root.current.contains(e.target)) setOpen(false);
730
+ };
731
+ const onKey = (e) => {
732
+ if (e.key === "Escape") setOpen(false);
733
+ };
734
+ document.addEventListener("mousedown", onDown);
735
+ document.addEventListener("keydown", onKey);
736
+ return () => {
737
+ document.removeEventListener("mousedown", onDown);
738
+ document.removeEventListener("keydown", onKey);
739
+ };
740
+ }, [open]);
741
+ const markSeen = useCallback3(
742
+ (ids) => {
743
+ if (controlled) {
744
+ onSeen?.(ids);
745
+ return;
746
+ }
747
+ setStored((current) => {
748
+ const next = new Set(current ?? []);
749
+ for (const id of ids) next.add(id);
750
+ writeSeen(next);
751
+ return next;
752
+ });
753
+ },
754
+ [controlled, onSeen]
755
+ );
756
+ const unread = useMemo2(
757
+ () => seen ? notifications.filter((n) => !seen.has(n.id)) : [],
758
+ [notifications, seen]
759
+ );
760
+ if (!seen) return null;
761
+ const count = unread.length;
762
+ return /* @__PURE__ */ jsxs11("div", { className: "pt-bell", ref: root, children: [
763
+ /* @__PURE__ */ jsxs11(
764
+ "button",
765
+ {
766
+ type: "button",
767
+ className: `pt-bell__button${count ? " has-unread" : ""}`,
768
+ "aria-expanded": open,
769
+ "aria-haspopup": "true",
770
+ title: count ? `${text2.title} \u2014 ${text2.unread(count)}` : text2.title,
771
+ onClick: () => setOpen((v) => !v),
772
+ children: [
773
+ /* @__PURE__ */ jsx13("span", { className: "pt-bell__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsxs11("svg", { viewBox: "0 0 16 16", width: "15", height: "15", children: [
774
+ /* @__PURE__ */ jsx13(
775
+ "path",
776
+ {
777
+ d: "M8 1.6a3.6 3.6 0 0 0-3.6 3.6v2.3L3.1 10.2h9.8l-1.3-2.7V5.2A3.6 3.6 0 0 0 8 1.6Z",
778
+ fill: "none",
779
+ stroke: "currentColor",
780
+ strokeWidth: "1.3",
781
+ strokeLinejoin: "round"
782
+ }
783
+ ),
784
+ /* @__PURE__ */ jsx13("path", { d: "M6.5 12a1.5 1.5 0 0 0 3 0", fill: "none", stroke: "currentColor", strokeWidth: "1.3" })
785
+ ] }) }),
786
+ /* @__PURE__ */ jsx13("span", { className: "pt-sr-only", children: count ? text2.unread(count) : text2.title }),
787
+ count > 0 && /* @__PURE__ */ jsx13("span", { className: "pt-bell__count", "aria-hidden": "true", children: count > 99 ? "99+" : count })
788
+ ]
789
+ }
790
+ ),
791
+ open && /* @__PURE__ */ jsxs11("div", { className: "pt-bell__panel", role: "dialog", "aria-label": text2.title, children: [
792
+ /* @__PURE__ */ jsxs11("div", { className: "pt-bell__head", children: [
793
+ /* @__PURE__ */ jsx13("p", { className: "pt-bell__title", children: text2.title }),
794
+ count > 0 && /* @__PURE__ */ jsx13(
795
+ "button",
796
+ {
797
+ type: "button",
798
+ className: "pt-bell__all",
799
+ onClick: () => markSeen(notifications.map((n) => n.id)),
800
+ children: text2.markAll
801
+ }
802
+ )
803
+ ] }),
804
+ count === 0 ? /* @__PURE__ */ jsx13("p", { className: "pt-bell__empty", children: text2.empty }) : /* @__PURE__ */ jsx13("ul", { className: "pt-bell__list", children: unread.map((item) => /* @__PURE__ */ jsx13("li", { children: /* @__PURE__ */ jsxs11(
805
+ "button",
806
+ {
807
+ type: "button",
808
+ className: `pt-bell__item is-${item.tone ?? "info"}`,
809
+ onClick: () => {
810
+ item.onOpen?.();
811
+ markSeen([item.id]);
812
+ if (item.onOpen) setOpen(false);
813
+ },
814
+ children: [
815
+ /* @__PURE__ */ jsx13("span", { className: "pt-bell__mark", "aria-hidden": "true" }),
816
+ /* @__PURE__ */ jsxs11("span", { className: "pt-bell__body", children: [
817
+ /* @__PURE__ */ jsx13("span", { className: "pt-bell__item-title", children: item.title }),
818
+ item.detail && /* @__PURE__ */ jsx13("span", { className: "pt-bell__detail", children: item.detail }),
819
+ item.at && /* @__PURE__ */ jsx13("span", { className: "pt-bell__at", children: item.at })
820
+ ] })
821
+ ]
822
+ }
823
+ ) }, item.id)) })
824
+ ] })
825
+ ] });
826
+ }
827
+
828
+ // src/components/CopyBlock/CopyBlock.tsx
829
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5, useState as useState4 } from "react";
830
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
831
+ var DEFAULT_LABELS2 = {
832
+ copy: "Copy",
833
+ copied: "Copied",
834
+ selected: "Selected \u2014 press \u2318C"
835
+ };
836
+ function CopyBlock({ text: text2, label, note, labels }) {
837
+ const words = { ...DEFAULT_LABELS2, ...labels };
838
+ const [result, setResult] = useState4("idle");
839
+ const pre = useRef5(null);
840
+ const timer = useRef5(void 0);
841
+ useEffect6(() => () => window.clearTimeout(timer.current), []);
842
+ const announce = useCallback4((next) => {
843
+ setResult(next);
844
+ window.clearTimeout(timer.current);
845
+ timer.current = window.setTimeout(() => setResult("idle"), 2400);
846
+ }, []);
847
+ const select = useCallback4(() => {
848
+ const node = pre.current;
849
+ if (!node) return false;
850
+ try {
851
+ const range = document.createRange();
852
+ range.selectNodeContents(node);
853
+ const selection = window.getSelection();
854
+ if (!selection) return false;
855
+ selection.removeAllRanges();
856
+ selection.addRange(range);
857
+ return true;
858
+ } catch {
859
+ return false;
860
+ }
861
+ }, []);
862
+ const copy = useCallback4(async () => {
863
+ try {
864
+ await navigator.clipboard.writeText(text2);
865
+ announce("copied");
866
+ return;
867
+ } catch {
868
+ }
869
+ if (select()) announce("selected");
870
+ }, [announce, select, text2]);
871
+ return /* @__PURE__ */ jsxs12("div", { className: "pt-copy", children: [
872
+ /* @__PURE__ */ jsxs12("div", { className: "pt-copy__head", children: [
873
+ /* @__PURE__ */ jsxs12("div", { className: "pt-copy__meta", children: [
874
+ label && /* @__PURE__ */ jsx14("p", { className: "pt-copy__label", children: label }),
875
+ note && /* @__PURE__ */ jsx14("p", { className: "pt-copy__note", children: note })
876
+ ] }),
877
+ /* @__PURE__ */ jsx14("button", { type: "button", className: "pt-copy__button", onClick: copy, children: result === "copied" ? words.copied : result === "selected" ? words.selected : words.copy })
878
+ ] }),
879
+ /* @__PURE__ */ jsx14("div", { className: "pt-copy__body", children: /* @__PURE__ */ jsx14("pre", { className: "pt-copy__text", ref: pre, children: text2 }) }),
880
+ /* @__PURE__ */ jsx14("p", { className: "pt-sr-only", role: "status", children: result === "copied" ? words.copied : result === "selected" ? words.selected : "" })
881
+ ] });
882
+ }
883
+
884
+ // src/components/Setup/SetupGuide.tsx
885
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
886
+ var DEFAULT_LABELS3 = {
887
+ ok: "in place",
888
+ missing: "not set",
889
+ unknown: "cannot tell",
890
+ whatToDo: "What to do"
891
+ };
892
+ function SetupGuide({ requirements, readyMessage, labels }) {
893
+ const text2 = { ...DEFAULT_LABELS3, ...labels };
894
+ const missing = requirements.filter((r) => r.status === "missing");
895
+ const allOk = requirements.length > 0 && missing.length === 0;
896
+ return /* @__PURE__ */ jsxs13("div", { className: "pt-setup", children: [
897
+ /* @__PURE__ */ jsx15("ul", { className: "pt-setup__list", children: requirements.map((requirement) => /* @__PURE__ */ jsxs13("li", { className: `pt-setup__item is-${requirement.status}`, children: [
898
+ /* @__PURE__ */ jsx15("span", { className: "pt-setup__mark", "aria-hidden": "true" }),
899
+ /* @__PURE__ */ jsxs13("div", { className: "pt-setup__main", children: [
900
+ /* @__PURE__ */ jsxs13("p", { className: "pt-setup__label", children: [
901
+ requirement.label,
902
+ /* @__PURE__ */ jsx15("span", { className: "pt-setup__state", children: text2[requirement.status] })
903
+ ] }),
904
+ requirement.variable && /* @__PURE__ */ jsx15("p", { className: "pt-setup__var", children: requirement.variable }),
905
+ requirement.enables && /* @__PURE__ */ jsx15("p", { className: "pt-setup__enables", children: requirement.enables }),
906
+ requirement.status !== "ok" && requirement.fix && /* @__PURE__ */ jsxs13("div", { className: "pt-setup__fix", children: [
907
+ /* @__PURE__ */ jsx15("p", { className: "pt-setup__fix-title", children: text2.whatToDo }),
908
+ requirement.fix
909
+ ] })
910
+ ] })
911
+ ] }, requirement.id)) }),
912
+ allOk && readyMessage && /* @__PURE__ */ jsx15("p", { className: "pt-setup__ready", children: readyMessage })
913
+ ] });
914
+ }
915
+
916
+ // src/hooks/useDelegatedClick.ts
917
+ import { useEffect as useEffect7 } from "react";
918
+ function useDelegatedClick(attribute, handler) {
919
+ useEffect7(() => {
920
+ function onClick(e) {
921
+ const target = e.target.closest(`[${attribute}]`);
922
+ if (!target) return;
923
+ const payload = {};
924
+ for (const { name, value } of Array.from(target.attributes)) {
925
+ if (name.startsWith("data-")) payload[name.replace(/^data-/, "")] = value;
926
+ }
927
+ handler(payload, e);
928
+ }
929
+ document.addEventListener("click", onClick);
930
+ return () => document.removeEventListener("click", onClick);
931
+ }, [attribute, handler]);
932
+ }
933
+
934
+ // src/hooks/useMediaQuery.ts
935
+ import { useEffect as useEffect8, useState as useState5 } from "react";
936
+ function useMediaQuery(query) {
937
+ const [matches, setMatches] = useState5(() => typeof window !== "undefined" ? window.matchMedia(query).matches : false);
938
+ useEffect8(() => {
939
+ const mql = window.matchMedia(query);
940
+ const handler = () => setMatches(mql.matches);
941
+ handler();
942
+ mql.addEventListener("change", handler);
943
+ return () => mql.removeEventListener("change", handler);
944
+ }, [query]);
945
+ return matches;
946
+ }
947
+
948
+ // src/board/text.ts
949
+ var LANGS = ["en", "es"];
950
+ var LANG_LABELS = {
951
+ en: "English",
952
+ es: "Espa\xF1ol"
953
+ };
954
+ function text(value, lang) {
955
+ if (value === void 0) return "";
956
+ if (typeof value === "string") return value;
957
+ const own = value[lang];
958
+ if (own !== void 0 && own !== "") return own;
959
+ const other = LANGS.find((code) => code !== lang && value[code]);
960
+ return other ? value[other] : "";
961
+ }
962
+ function untranslated(value) {
963
+ if (value === void 0 || typeof value === "string") return false;
964
+ const written = LANGS.filter((code) => value[code]);
965
+ return written.length === 1;
966
+ }
967
+ function writeText(lang, value) {
968
+ return { [lang]: value };
969
+ }
970
+
971
+ // src/board/model.ts
972
+ var INTENTS = ["decide", "explain", "solve", "do", "cheap", "safe", "fast", "askme", "hold"];
973
+ var INTENT_COPY = {
974
+ decide: {
975
+ label: { en: "You decide", es: "Dec\xEDdelo t\xFA" },
976
+ meaning: {
977
+ en: "Pick between the options and act. Say what you picked and why, in one line.",
978
+ es: "Elige entre las opciones y act\xFAa. Di qu\xE9 elegiste y por qu\xE9, en una l\xEDnea."
979
+ }
980
+ },
981
+ explain: {
982
+ label: { en: "Explain it first", es: "Expl\xEDcamelo antes" },
983
+ meaning: {
984
+ en: "Change nothing. Come back with the trade-off and wait.",
985
+ es: "No cambies nada. Vuelve con el compromiso que hay y espera."
986
+ }
987
+ },
988
+ solve: {
989
+ label: { en: "Solve it if it is small", es: "Resu\xE9lvelo si es peque\xF1o" },
990
+ meaning: {
991
+ en: "Fix it if it is small and reversible. If it is neither, stop and say so.",
992
+ es: "Arr\xE9glalo si es peque\xF1o y reversible. Si no es ninguna de las dos, para y dilo."
993
+ }
994
+ },
995
+ do: {
996
+ label: { en: "Do it, decide the details", es: "Hazlo y decide los detalles" },
997
+ meaning: {
998
+ en: "Build it. Every detail not written down is yours to choose.",
999
+ es: "Constr\xFAyelo. Todo detalle que no est\xE9 escrito lo eliges t\xFA."
1000
+ }
1001
+ },
1002
+ cheap: {
1003
+ label: { en: "Cheapest to maintain", es: "Lo m\xE1s barato de mantener" },
1004
+ meaning: {
1005
+ en: "Choose whatever needs the least looking after later, even if it is duller.",
1006
+ es: "Elige lo que menos haya que cuidar despu\xE9s, aunque sea m\xE1s aburrido."
1007
+ }
1008
+ },
1009
+ safe: {
1010
+ label: { en: "Safest option", es: "La opci\xF3n m\xE1s segura" },
1011
+ meaning: {
1012
+ en: "Take the conservative option. No new dependency, no new failure mode.",
1013
+ es: "Coge la opci\xF3n conservadora. Sin dependencias nuevas ni modos de fallo nuevos."
1014
+ }
1015
+ },
1016
+ fast: {
1017
+ label: { en: "Fastest that works", es: "Lo m\xE1s r\xE1pido que funcione" },
1018
+ meaning: {
1019
+ en: "Ship the smallest thing that works. Note the debt you took on.",
1020
+ es: "Entrega lo m\xEDnimo que funcione. Anota la deuda que asumes."
1021
+ }
1022
+ },
1023
+ askme: {
1024
+ label: { en: "Ask me first", es: "Preg\xFAntame antes" },
1025
+ meaning: {
1026
+ en: "Read, plan, propose. Change nothing until I answer.",
1027
+ es: "Lee, planea, prop\xF3n. No cambies nada hasta que conteste."
1028
+ }
1029
+ },
1030
+ hold: {
1031
+ label: { en: "Do not touch it yet", es: "No lo toques todav\xEDa" },
1032
+ meaning: {
1033
+ en: "Leave it alone. It is here so it is not forgotten, not so it is done.",
1034
+ es: "D\xE9jalo en paz. Est\xE1 aqu\xED para no olvidarse, no para hacerse."
1035
+ }
1036
+ }
1037
+ };
1038
+ var LEVELS = ["high", "medium", "low"];
1039
+ var OWNERS = ["claude", "you"];
1040
+ function newId() {
1041
+ return Math.random().toString(36).slice(2, 10);
1042
+ }
1043
+ function today() {
1044
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1045
+ }
1046
+ function emptyCard(column, area) {
1047
+ const stamp = today();
1048
+ return {
1049
+ id: newId(),
1050
+ column,
1051
+ title: "",
1052
+ body: "",
1053
+ area,
1054
+ priority: "medium",
1055
+ risk: "medium",
1056
+ owner: "claude",
1057
+ order: false,
1058
+ intent: null,
1059
+ checks: [],
1060
+ notes: [],
1061
+ createdAt: stamp,
1062
+ updatedAt: stamp,
1063
+ startedAt: null,
1064
+ completedAt: null
1065
+ };
1066
+ }
1067
+ function stampForColumn(card, columns) {
1068
+ const column = columns.find((candidate) => candidate.id === card.column);
1069
+ if (!column) return card;
1070
+ if (column.done) {
1071
+ return { ...card, completedAt: card.completedAt ?? today() };
1072
+ }
1073
+ const next = { ...card, completedAt: null };
1074
+ if (column.active && !next.startedAt) next.startedAt = today();
1075
+ return next;
1076
+ }
1077
+ function claimedWithoutStarting(card) {
1078
+ return Boolean(card.completedAt) && !card.startedAt;
1079
+ }
1080
+ function countCards(state) {
1081
+ const doneColumns = new Set(state.columns.filter((column) => column.done).map((column) => column.id));
1082
+ const counts = { open: 0, orders: 0, yours: 0, decisions: 0, highRisk: 0, done: 0 };
1083
+ for (const card of state.cards) {
1084
+ if (doneColumns.has(card.column)) {
1085
+ counts.done += 1;
1086
+ continue;
1087
+ }
1088
+ counts.open += 1;
1089
+ if (card.order) counts.orders += 1;
1090
+ if (card.owner === "you") counts.yours += 1;
1091
+ if (card.intent === "decide") counts.decisions += 1;
1092
+ if (card.risk === "high") counts.highRisk += 1;
1093
+ }
1094
+ return counts;
1095
+ }
1096
+ function checkProgress(card) {
1097
+ if (card.checks.length === 0) return void 0;
1098
+ return { done: card.checks.filter((check) => check.done).length, total: card.checks.length };
1099
+ }
1100
+
1101
+ // src/board/analysis.ts
1102
+ var ANALYSIS_SECTIONS = ["board", "project", "strategy", "market"];
1103
+ function decided(analysis) {
1104
+ const out = [];
1105
+ for (const section of ANALYSIS_SECTIONS) {
1106
+ for (const choice of analysis.choices?.[section] ?? []) {
1107
+ const option = choice.options.find((o) => o.id === choice.value);
1108
+ if (option) out.push({ section, choice, option });
1109
+ }
1110
+ }
1111
+ return out.sort((a, b) => (b.choice.decidedAt ?? "").localeCompare(a.choice.decidedAt ?? ""));
1112
+ }
1113
+ function undecided(analysis) {
1114
+ const out = [];
1115
+ for (const section of ANALYSIS_SECTIONS) {
1116
+ for (const choice of analysis.choices?.[section] ?? []) {
1117
+ if (!choice.options.some((o) => o.id === choice.value)) out.push({ section, choice });
1118
+ }
1119
+ }
1120
+ return out;
1121
+ }
1122
+ function boardFacts(cards, columns, today2) {
1123
+ const doneColumns = new Set(columns.filter((column) => column.done).map((column) => column.id));
1124
+ const facts = {
1125
+ total: cards.length,
1126
+ open: 0,
1127
+ byColumn: columns.map((column) => ({
1128
+ id: column.id,
1129
+ title: column.title,
1130
+ count: cards.filter((card) => card.column === column.id).length
1131
+ })),
1132
+ waitingOnOwner: 0,
1133
+ orders: 0,
1134
+ highRisk: 0,
1135
+ claimed: [],
1136
+ oldestOpenDays: null,
1137
+ oldestOpen: null
1138
+ };
1139
+ let oldest;
1140
+ for (const card of cards) {
1141
+ if (claimedWithoutStarting(card)) facts.claimed.push({ id: card.id, title: card.title });
1142
+ if (doneColumns.has(card.column)) continue;
1143
+ facts.open += 1;
1144
+ if (card.owner === "you") facts.waitingOnOwner += 1;
1145
+ if (card.order) facts.orders += 1;
1146
+ if (card.risk === "high") facts.highRisk += 1;
1147
+ if (!oldest || card.createdAt < oldest.createdAt) oldest = card;
1148
+ }
1149
+ if (oldest) {
1150
+ facts.oldestOpen = { id: oldest.id, title: oldest.title };
1151
+ const days = Math.round((Date.parse(today2) - Date.parse(oldest.createdAt)) / 864e5);
1152
+ facts.oldestOpenDays = Number.isFinite(days) ? Math.max(0, days) : null;
1153
+ }
1154
+ return facts;
1155
+ }
1156
+ function trendOf(metric) {
1157
+ const readings = metric.readings;
1158
+ const latest = readings[readings.length - 1];
1159
+ const previous = readings[readings.length - 2];
1160
+ if (!latest || !previous) return { latest, previous, delta: null, against: false };
1161
+ const delta = latest.value - previous.value;
1162
+ const against = delta === 0 ? false : metric.goal === "up" ? delta < 0 : delta > 0;
1163
+ return { latest, previous, delta, against };
1164
+ }
1165
+ function sparkPoints(readings) {
1166
+ if (readings.length === 0) return [];
1167
+ const values = readings.map((reading) => reading.value);
1168
+ const min = Math.min(...values);
1169
+ const max = Math.max(...values);
1170
+ const span = max - min;
1171
+ return readings.map((reading, index) => ({
1172
+ x: readings.length === 1 ? 0.5 : index / (readings.length - 1),
1173
+ y: span === 0 ? 0.5 : 1 - (reading.value - min) / span
1174
+ }));
1175
+ }
1176
+
1177
+ // src/board/validate.ts
1178
+ var BOARD_VERSION = 2;
1179
+ var LEVELS2 = /* @__PURE__ */ new Set(["high", "medium", "low"]);
1180
+ var OWNERS2 = /* @__PURE__ */ new Set(["claude", "you"]);
1181
+ var INTENT_SET = new Set(INTENTS);
1182
+ function isObject(value) {
1183
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1184
+ }
1185
+ function isText(value) {
1186
+ if (typeof value === "string") return true;
1187
+ if (!isObject(value)) return false;
1188
+ return Object.values(value).every((entry) => typeof entry === "string");
1189
+ }
1190
+ function validateBoard(value) {
1191
+ const problems = [];
1192
+ const say = (at, says) => problems.push({ at, says });
1193
+ if (!isObject(value)) {
1194
+ return { ok: false, reason: "shape", problems: [{ at: "", says: "the board is not a JSON object" }] };
1195
+ }
1196
+ if (value.v !== BOARD_VERSION) {
1197
+ return {
1198
+ ok: false,
1199
+ reason: "version",
1200
+ found: value.v,
1201
+ expected: BOARD_VERSION,
1202
+ problems: [
1203
+ {
1204
+ at: "v",
1205
+ says: value.v === void 0 ? `no version. This kit reads v ${BOARD_VERSION}` : `written for v ${JSON.stringify(value.v)}, and this kit reads v ${BOARD_VERSION}`
1206
+ }
1207
+ ]
1208
+ };
1209
+ }
1210
+ for (const key of ["project", "updatedAt"]) {
1211
+ if (typeof value[key] !== "string") say(key, "must be a string");
1212
+ }
1213
+ const columnIds = /* @__PURE__ */ new Set();
1214
+ if (!Array.isArray(value.columns) || value.columns.length === 0) {
1215
+ say("columns", "must be a non-empty array \u2014 a board with no columns has nowhere to put a card");
1216
+ } else {
1217
+ value.columns.forEach((column, index) => {
1218
+ const at = `columns[${index}]`;
1219
+ if (!isObject(column)) return say(at, "must be an object");
1220
+ if (typeof column.id !== "string" || column.id === "") return say(`${at}.id`, "must be a non-empty string");
1221
+ if (columnIds.has(column.id)) say(`${at}.id`, `"${column.id}" is used by more than one column`);
1222
+ columnIds.add(column.id);
1223
+ if (!isText(column.title)) say(`${at}.title`, "must be a string, or an object of language strings");
1224
+ });
1225
+ }
1226
+ const areaIds = /* @__PURE__ */ new Set();
1227
+ if (!Array.isArray(value.areas)) {
1228
+ say("areas", "must be an array");
1229
+ } else {
1230
+ value.areas.forEach((area, index) => {
1231
+ const at = `areas[${index}]`;
1232
+ if (!isObject(area) || typeof area.id !== "string") return say(`${at}.id`, "must be a non-empty string");
1233
+ areaIds.add(area.id);
1234
+ });
1235
+ }
1236
+ const cardIds = /* @__PURE__ */ new Set();
1237
+ if (!Array.isArray(value.cards)) {
1238
+ say("cards", "must be an array");
1239
+ } else {
1240
+ value.cards.forEach((card, index) => checkCard(card, `cards[${index}]`, { columnIds, areaIds, cardIds, say }));
1241
+ }
1242
+ if (!Array.isArray(value.runs)) say("runs", "must be an array");
1243
+ if (problems.length) return { ok: false, reason: "shape", problems };
1244
+ return { ok: true, board: value, problems: [] };
1245
+ }
1246
+ function checkCard(card, at, ctx) {
1247
+ const { say } = ctx;
1248
+ if (!isObject(card)) return say(at, "must be an object");
1249
+ if (typeof card.id !== "string" || card.id === "") {
1250
+ say(`${at}.id`, "must be a non-empty string");
1251
+ } else if (ctx.cardIds.has(card.id)) {
1252
+ say(`${at}.id`, `"${card.id}" is used by more than one card`);
1253
+ } else {
1254
+ ctx.cardIds.add(card.id);
1255
+ }
1256
+ if (typeof card.column !== "string") {
1257
+ say(`${at}.column`, "must be a string");
1258
+ } else if (ctx.columnIds.size && !ctx.columnIds.has(card.column)) {
1259
+ say(`${at}.column`, `"${card.column}" is not one of the board's columns, so this card is drawn nowhere`);
1260
+ }
1261
+ if (typeof card.area !== "string") {
1262
+ say(`${at}.area`, "must be a string");
1263
+ } else if (ctx.areaIds.size && !ctx.areaIds.has(card.area)) {
1264
+ say(`${at}.area`, `"${card.area}" is not one of the board's areas`);
1265
+ }
1266
+ if (!isText(card.title)) say(`${at}.title`, "must be a string, or an object of language strings");
1267
+ for (const key of ["priority", "risk"]) {
1268
+ if (card[key] !== void 0 && !LEVELS2.has(card[key])) {
1269
+ say(`${at}.${key}`, `must be high, medium or low \u2014 found ${JSON.stringify(card[key])}`);
1270
+ }
1271
+ }
1272
+ if (card.owner !== void 0 && !OWNERS2.has(card.owner)) {
1273
+ say(`${at}.owner`, `must be claude or you \u2014 found ${JSON.stringify(card.owner)}`);
1274
+ }
1275
+ if (card.intent !== void 0 && card.intent !== null && !INTENT_SET.has(card.intent)) {
1276
+ say(`${at}.intent`, `${JSON.stringify(card.intent)} is not one of: ${INTENTS.join(", ")}`);
1277
+ }
1278
+ if (card.checks !== void 0 && !Array.isArray(card.checks)) say(`${at}.checks`, "must be an array");
1279
+ if (card.notes !== void 0 && !Array.isArray(card.notes)) say(`${at}.notes`, "must be an array");
1280
+ }
1281
+ function readBoard(text2) {
1282
+ let parsed;
1283
+ try {
1284
+ parsed = JSON.parse(text2);
1285
+ } catch (error) {
1286
+ return {
1287
+ ok: false,
1288
+ reason: "shape",
1289
+ problems: [{ at: "", says: `not valid JSON \u2014 ${error.message}` }]
1290
+ };
1291
+ }
1292
+ return validateBoard(parsed);
1293
+ }
1294
+ function explainBoard(check) {
1295
+ if (check.ok) return "the board is readable";
1296
+ return check.problems.map((problem) => problem.at ? `${problem.at}: ${problem.says}` : problem.says).join("\n");
1297
+ }
1298
+ function isCard(value) {
1299
+ return isObject(value) && typeof value.id === "string" && typeof value.column === "string";
1300
+ }
1301
+
1302
+ // src/panel/PanelApp.tsx
1303
+ import React12, { useCallback as useCallback5, useEffect as useEffect9, useMemo as useMemo5, useState as useState9 } from "react";
1304
+
1305
+ // src/panel/themes.ts
1306
+ var PRESET_FORM = {
1307
+ midnight: THEME_FORMS.panel,
1308
+ oldMoney: THEME_FORMS.ledger,
1309
+ cyberpunk: THEME_FORMS.panel,
1310
+ claude: THEME_FORMS.paper
1311
+ };
1312
+ var PRESET_NOTE = {
1313
+ midnight: "the neutral preset, and the one everything else is measured against",
1314
+ oldMoney: "bottle green and muted gold, drawn as a ledger rather than as cards",
1315
+ cyberpunk: "crimson and a hot yellow, with the contrast turned up",
1316
+ claude: "warm paper and clay, and the only light one"
1317
+ };
1318
+ var PRESET_LABEL = {
1319
+ midnight: "Midnight",
1320
+ oldMoney: "Old Money",
1321
+ cyberpunk: "Cyberpunk",
1322
+ claude: "Claude"
1323
+ };
1324
+ var PRESET_PANEL_THEMES = Object.fromEntries(
1325
+ Object.keys(THEME_PRESETS).map((name) => [
1326
+ name,
1327
+ { label: PRESET_LABEL[name], tokens: THEME_PRESETS[name], form: PRESET_FORM[name], note: PRESET_NOTE[name] }
1328
+ ])
1329
+ );
1330
+
1331
+ // src/panel/AnalysisView.tsx
1332
+ import { useMemo as useMemo3, useState as useState6 } from "react";
1333
+
1334
+ // src/panel/i18n.ts
1335
+ var STORAGE_KEY = "paneltir:lang";
1336
+ function readStoredLang(fallback = "en") {
1337
+ try {
1338
+ const stored = window.localStorage.getItem(STORAGE_KEY);
1339
+ return stored === "en" || stored === "es" ? stored : fallback;
1340
+ } catch {
1341
+ return fallback;
1342
+ }
1343
+ }
1344
+ function storeLang(lang) {
1345
+ try {
1346
+ window.localStorage.setItem(STORAGE_KEY, lang);
1347
+ } catch {
1348
+ }
1349
+ }
1350
+ var UI = {
1351
+ en: {
1352
+ settings: "Settings",
1353
+ dismiss: "Dismiss",
1354
+ close: "Close",
1355
+ all: "All",
1356
+ board: "Board",
1357
+ analysis: "Analysis",
1358
+ cardDetail: "Card",
1359
+ panelSettings: "Panel settings",
1360
+ title: "Title",
1361
+ titlePlaceholder: "Name the card\u2026",
1362
+ untitled: "Untitled",
1363
+ detail: "Detail",
1364
+ detailPlaceholder: "What this is, and anything needed to act on it\u2026",
1365
+ areaLabel: "Area",
1366
+ priority: "Priority",
1367
+ risk: "Risk",
1368
+ riskHint: "what it costs if it goes wrong, not how soon it is wanted",
1369
+ levelHigh: "high",
1370
+ levelMedium: "medium",
1371
+ levelLow: "low",
1372
+ dependsOn: "Waiting on",
1373
+ ownerClaude: "Claude",
1374
+ ownerYou: "You",
1375
+ orderTitle: "Order for Claude",
1376
+ orderOn: "Do it",
1377
+ orderOff: "Noted only",
1378
+ intentTitle: "How to do it",
1379
+ intentHint: "one tap, and it beats anything Claude would otherwise decide",
1380
+ intentNone: "No instruction",
1381
+ checklist: "Checklist",
1382
+ addStep: "Add",
1383
+ addStepPlaceholder: "Add a step\u2026",
1384
+ notes: "Notes",
1385
+ notePlaceholder: "Write to Claude\u2026",
1386
+ leaveNote: "Leave a note",
1387
+ columnTitle: "Column",
1388
+ started: "Started",
1389
+ completed: "Completed",
1390
+ neverStarted: "Marked finished without ever being started.",
1391
+ deleteCard: "Delete card",
1392
+ addCard: "Add a card",
1393
+ untranslatedBadge: "ES only",
1394
+ filterForClaude: "For Claude",
1395
+ filterYours: "Waiting on you",
1396
+ filterHighRisk: "High risk",
1397
+ statOpen: "Open",
1398
+ statOrders: "Orders",
1399
+ statYours: "For you",
1400
+ statDecisions: "Decisions",
1401
+ statHighRisk: "High risk",
1402
+ statDone: "Done",
1403
+ theme: "Theme",
1404
+ themeSubtitle: "the only thing that changes between projects",
1405
+ themeNote: "The structure does not move: only the palette handed to the theme provider changes.",
1406
+ language: "Language",
1407
+ languageSubtitle: "the public pages stay in English",
1408
+ fingerprint: "Kit fingerprint",
1409
+ fingerprintSubtitle: "so nothing has to be re-read to know it changed",
1410
+ trackedFiles: (count, version) => `${count} tracked files in v${version}.`,
1411
+ notifications: "What changed",
1412
+ nothingNew: "Nothing new since you last looked.",
1413
+ markAllSeen: "Mark all as seen",
1414
+ unreadCount: (n) => `${n} unread`,
1415
+ notifRun: (by) => `${by} worked the board`,
1416
+ notifWaiting: "Waiting on you",
1417
+ notifSuggestion: (area) => `Open suggestion \xB7 ${area}`,
1418
+ help: "Help",
1419
+ helpTitle: "What this panel needs",
1420
+ setupReady: "Everything is in place. Edits made here commit to the repository.",
1421
+ setupOk: "in place",
1422
+ setupMissing: "not set",
1423
+ setupUnknown: "cannot tell",
1424
+ setupWhatToDo: "What to do",
1425
+ setupChecking: "Asking the server what is configured\u2026",
1426
+ setupWriteTarget: (repo, branch, file) => `Saves commit ${file} on ${branch} in ${repo}.`,
1427
+ needPassword: "A password on the panel",
1428
+ needPasswordEnables: "Without it the panel answers 503 rather than becoming public \u2014 so this one is already set, or you would not be reading it.",
1429
+ needToken: "A GitHub token",
1430
+ needTokenEnables: "Lets Save commit the board back to the repository. Without it everything else works and Save fails.",
1431
+ needRepo: "The repository to write to",
1432
+ needRepoEnables: "Which repository the board is committed to. There is no default on purpose: writing to the wrong one is worse than not writing.",
1433
+ fixTokenCreate: "On GitHub, make a fine-grained personal access token.",
1434
+ fixTokenScope: "Give it Contents: read and write, on this repository only \u2014 nothing else, and no other repository.",
1435
+ fixTokenPaste: "Paste it into the hosting project as GH_TOKEN. Never into the repository: a token in a commit is a token to revoke.",
1436
+ fixTokenRedeploy: "Redeploy. Environment variables are read at deploy time, so a running deployment will not see a new one.",
1437
+ fixRepo: "Set PANEL_REPO in the hosting project to this project\u2019s owner/repo, then redeploy.",
1438
+ demoTourWelcomeTitle: "A real panel, and nothing is saved yet",
1439
+ demoTourWelcomeBody: "Everything here works. Move things, open things, break things \u2014 nothing is written anywhere yet, so nothing you do can damage anything.",
1440
+ demoTourBoardTitle: "Drag a card, then open one",
1441
+ demoTourBoardBody: "The grip on the left of a card moves it between columns; it works with a finger as well as a pointer. Clicking anywhere else opens the card, where its priority, risk and checklist live.",
1442
+ demoTourThemeTitle: "Try a different identity",
1443
+ demoTourThemeBody: "Settings carries the themes. They are not repaints: one is drawn as a ledger \u2014 squared, ruled, set in a serif \u2014 where another floats cards in rounded columns. A project brings its own colour and its own shape.",
1444
+ demoTourKeepTitle: "Nothing here is kept",
1445
+ demoTourKeepBody: "Nothing is saved here, so reloading brings it all back. Wired to a repository, the panel commits every edit, which is what gives each change a diff and a history.",
1446
+ tourWelcomeTitle: "This board is the project, not a demo",
1447
+ tourWelcomeBody: "Every card here is real work on this repository. Nothing is filler, and nothing is lost when you close the tab.",
1448
+ tourBoardTitle: "Drag a card to move it",
1449
+ tourBoardBody: "Use the grip on the left of a card. A card moves to In progress when the work starts and to Done only once it is finished and checked \u2014 moving it is a claim, so only move it when it is true.",
1450
+ tourOrdersTitle: "Tell Claude what to do, and how",
1451
+ tourOrdersBody: "Open a card and set Order for Claude to ask for it. How to do it is stronger: it beats anything Claude would otherwise decide, and three of its values are refusals \u2014 explain it first, ask me first, do not touch it yet.",
1452
+ tourSaveTitle: "Nothing is saved until you press Save",
1453
+ tourSaveBody: "Edits stay in this browser until then. Save commits the board to the repository, so every change has a diff and a history. Leaving with unsaved work warns you first.",
1454
+ tourThemeTitle: "Make it yours",
1455
+ tourThemeBody: "Settings carries the theme \u2014 which is colour and shape, not just colour \u2014 and the language. Whatever you pick is what this panel looks like from now on.",
1456
+ gotIt: "Got it",
1457
+ showGuideAgain: "Show the guide again",
1458
+ guideSection: "The guide",
1459
+ session: "Session",
1460
+ sessionSubtitle: "This browser stays signed in for eight hours.",
1461
+ signOut: "Sign out",
1462
+ signingOut: "Signing out\u2026",
1463
+ signOutFailed: "Could not sign out. Check the connection and try again.",
1464
+ links: "Links",
1465
+ home: "Home",
1466
+ demo: "Demo",
1467
+ repository: "Repository",
1468
+ terms: "Terms",
1469
+ save: "Save to GitHub",
1470
+ saving: "Saving\u2026",
1471
+ savedAt: (time) => `saved ${time}`,
1472
+ savedWithCommit: "Saved. The site rebuilds on its own.",
1473
+ saveFailed: "Could not save",
1474
+ saveConflict: "The board changed in the repository since this page loaded. Reload before saving again.",
1475
+ unsaved: "unsaved changes",
1476
+ saveSection: "Board",
1477
+ saveSubtitle: "the panel writes its own state to the repository",
1478
+ saveHelp: "Saving commits the board file on main through the GitHub API, so the change has a diff, an author and a history.",
1479
+ upToDate: "No changes to save.",
1480
+ demoNotice: "Edits here stay in this browser: the demo has nowhere to save.",
1481
+ analysisEyebrow: "Analysis",
1482
+ briefTitle: "What this is for",
1483
+ stage: "Stage",
1484
+ goalsTitle: "Goals",
1485
+ constraintsTitle: "Non-negotiable",
1486
+ constraintOn: "holds",
1487
+ constraintOff: "lifted",
1488
+ weights: { primary: "primary", secondary: "secondary", off: "not a goal" },
1489
+ metricsTitle: "Metrics",
1490
+ metricsIntro: "Each one names the direction that counts as better, so a reading against it is visible rather than a matter of opinion.",
1491
+ metricWith: "with the goal",
1492
+ metricAgainst: "against the goal",
1493
+ strategiesTitle: "Strategies",
1494
+ strategyOpenHint: "No path chosen yet \u2014 that decision is yours, and the board follows it.",
1495
+ strategyChosenHint: "The chosen path is an instruction: cards that serve it are work now.",
1496
+ strategyChoose: "Choose this path",
1497
+ strategyChosen: "Chosen",
1498
+ effort: "Effort",
1499
+ riskShort: "Risk",
1500
+ upside: "Upside",
1501
+ suggestionsTitle: "Standing suggestions",
1502
+ suggestionsIntro: "Things worth doing that are not cards yet. Marking one done or dismissed is how it stops coming back.",
1503
+ severities: { high: "high", medium: "medium", low: "low" },
1504
+ suggestionStatuses: { new: "new", doing: "in progress", done: "done", dismissed: "dismissed" },
1505
+ analysisSections: { board: "Board", project: "Project", strategy: "Strategy", market: "Market" },
1506
+ analysisSectionHints: {
1507
+ board: "What the cards add up to, counted rather than declared.",
1508
+ project: "What this is for, what it must not become, and how it is measured.",
1509
+ strategy: "The paths open to it, the one chosen, and what is worth doing next.",
1510
+ market: "Who this is for, and who else is already there."
1511
+ },
1512
+ analysisAnswered: (answered, total) => `${answered} of ${total} answered`,
1513
+ analysisAllDecided: "Every question on this screen has an answer.",
1514
+ choicesTitle: "Standing answers",
1515
+ choicesIntro: "Answer once here instead of on every card. Claude reads these before deciding anything, so an answer given here outlives the conversation it would otherwise have been given in.",
1516
+ choiceOpen: "not decided",
1517
+ choiceDecidedAt: (at) => `decided ${at}`,
1518
+ choiceReopen: "Reopen",
1519
+ choicesNone: "No standing questions on this screen yet.",
1520
+ claudeReadsTitle: "What Claude reads from this",
1521
+ claudeReadsIntro: "Every answer in one place, so a session can be handed the decisions without being handed the whole panel.",
1522
+ claudeOpenTitle: "Still open",
1523
+ claudeOpenIntro: "Nobody has answered these. An unanswered question is information: Claude asks rather than guessing, and leaves the ground untouched until you say.",
1524
+ boardFactsTitle: "What the board says",
1525
+ boardFactsIntro: "Counted from the cards every time this is drawn. Nothing here is stored, so nothing here can disagree with the board.",
1526
+ factOpen: "Open",
1527
+ factWaiting: "Waiting on you",
1528
+ factOrders: "Asked for",
1529
+ factHighRisk: "High risk",
1530
+ factOldest: "Oldest open card",
1531
+ factDays: (n) => n === 1 ? "1 day" : `${n} days`,
1532
+ factColumnsTitle: "Where they sit",
1533
+ factClaimedTitle: "Finished without ever being started",
1534
+ factClaimedIntro: "A card carrying a completion with no start. Either the work happened and the start went unrecorded, or it never happened \u2014 both are worth knowing before the card is believed.",
1535
+ factClaimedNone: "None \u2014 every finished card was started first.",
1536
+ marketIntro: "Written honestly or worth nothing: a competitor with no strength is a competitor nobody looked at.",
1537
+ marketEmpty: "Nothing written down yet. Not every board is a product \u2014 leave this empty rather than inventing an audience.",
1538
+ audienceTitle: "Audience",
1539
+ positioningTitle: "Position",
1540
+ competitorsTitle: "Who else is there",
1541
+ competitorStrength: "Does better",
1542
+ competitorGap: "Leaves room",
1543
+ pricingTitle: "What it costs",
1544
+ marketNotesTitle: "Notes",
1545
+ importedTheme: "Derived from this project",
1546
+ importedFrom: (at, files) => `Claude read ${files} on ${at} and mapped what it found onto the theme tokens.`,
1547
+ importedRefresh: "Derive it again",
1548
+ importedRefreshAsked: (at) => `Asked on ${at}. The panel cannot run Claude, so this is a note for the next session rather than something happening now \u2014 it is in the bell until it is done.`,
1549
+ importedRefreshHow: "Save first: the request is part of the board, and Claude reads the board.",
1550
+ notifThemeRefresh: "Derive the project theme again",
1551
+ marketplace: "Tools",
1552
+ marketplaceEyebrow: "dfklabs",
1553
+ marketplaceTitle: "Tools",
1554
+ marketplaceIntro: "Everything dfklabs publishes, whether or not it is a Claude Code plugin. The catalogue and every version come from their own sources; what this project has turned on comes from its repository. Nothing here is stored, so nothing here can be out of date without saying so.",
1555
+ marketplaceSource: (url) => `Marketplace: ${url}`,
1556
+ refresh: "Refresh",
1557
+ refreshing: "Reading\u2026",
1558
+ readAt: (age) => `Read ${age}.`,
1559
+ catalogueStale: (age) => `The catalogue could not be reached just now, so this is the last copy that worked \u2014 read ${age}. Versions and install states may have moved since.`,
1560
+ catalogueUnavailable: "The catalogue could not be reached and there is no cached copy to fall back on. This is not an empty catalogue: it is a page with nothing to show yet. Try again in a moment.",
1561
+ catalogueFailed: "The panel could not ask its own server for the catalogue. Check the connection and try again.",
1562
+ catalogueLoading: "Reading the catalogue\u2026",
1563
+ catalogueEmpty: "Nothing matches this filter.",
1564
+ stateFrom: (repo, file) => `Install state read from ${file} in ${repo}.`,
1565
+ settingsAbsent: "that file does not exist yet",
1566
+ settingsUnreadable: "it could not be read, so nothing below is claimed",
1567
+ settingsUnparseable: "it exists but is not valid JSON",
1568
+ filterPlugins: "Plugins",
1569
+ filterActive: "Active here",
1570
+ filterStandalone: "Command only",
1571
+ installStates: {
1572
+ active: "Active",
1573
+ disabled: "Disabled",
1574
+ absent: "Not installed",
1575
+ misconfigured: "Configuration error",
1576
+ unknown: "Cannot tell"
1577
+ },
1578
+ stateMisconfiguredWhy: "The settings file names this plugin but does not register the marketplace it comes from, so there is nowhere to install it from. Copy the block under Install \u2014 it carries both halves.",
1579
+ toolCommandOnly: "Command",
1580
+ versionUnknown: "version unknown",
1581
+ runIt: "Run it",
1582
+ install: "Install",
1583
+ howToRun: "How to run it",
1584
+ learnMore: "More",
1585
+ copy: "Copy",
1586
+ copied: "Copied",
1587
+ copySelected: "Selected \u2014 press \u2318C",
1588
+ installTitle: "Install",
1589
+ installRepoTitle: "In a repository",
1590
+ installRepoHint: "works everywhere, and for everyone on the repository",
1591
+ installRepoMerge: "Merge these two keys into the file \u2014 do not replace it. It installs when the next session starts, for anyone working in this repository.",
1592
+ installRepoMergeRegistered: "The marketplace is already registered here, so only the plugin line is new. Merge these two keys into the file rather than replacing it.",
1593
+ installCliTitle: "Claude Code in a terminal or the desktop app",
1594
+ installCliHint: "this machine only",
1595
+ installStandaloneTitle: "Without Claude",
1596
+ installStandaloneHint: "the tool on its own",
1597
+ installNoStandalone: "This tool does not publish a standalone command.",
1598
+ installNoPlugin: "The catalogue does not offer this one as a plugin, so there is nothing to enable \u2014 it is a command you run.",
1599
+ historyEyebrow: "Revisions",
1600
+ historyTitle: "What each pass actually did",
1601
+ historyIntro: "One entry per session, newest first, naming the cards it touched. A card only reaches Done once its work is on main, and the board stamps when it started and when it finished \u2014 so a claim can be checked instead of believed.",
1602
+ historyEmpty: "No revisions recorded yet.",
1603
+ historyTouched: "Cards touched",
1604
+ brandEyebrow: "Brand \xB7 open decision",
1605
+ logosTitle: "Logo concepts",
1606
+ logosIntro: "Four marks drawn from the same geometry, all flat: no glow, no gradient that a favicon would flatten anyway.",
1607
+ functionsEyebrow: "Functions",
1608
+ functionsTitle: "What this gives a project, and how Claude uses it",
1609
+ functionsIntro: "Everything a consumer project gets on install, and the protocol that keeps an agent from re-reading the kit \u2014 and paying for it \u2014 on every session."
1610
+ },
1611
+ es: {
1612
+ settings: "Ajustes",
1613
+ dismiss: "Entendido",
1614
+ close: "Cerrar",
1615
+ all: "Todo",
1616
+ board: "Tablero",
1617
+ analysis: "An\xE1lisis",
1618
+ cardDetail: "Tarjeta",
1619
+ panelSettings: "Ajustes del panel",
1620
+ title: "T\xEDtulo",
1621
+ titlePlaceholder: "Ponle nombre a la tarjeta\u2026",
1622
+ untitled: "Sin t\xEDtulo",
1623
+ detail: "Detalle",
1624
+ detailPlaceholder: "Qu\xE9 es esto, y lo que haga falta para actuar\u2026",
1625
+ areaLabel: "\xC1rea",
1626
+ priority: "Prioridad",
1627
+ risk: "Riesgo",
1628
+ riskHint: "lo que cuesta si sale mal, no lo pronto que se quiere",
1629
+ levelHigh: "alto",
1630
+ levelMedium: "medio",
1631
+ levelLow: "bajo",
1632
+ dependsOn: "Depende de",
1633
+ ownerClaude: "Claude",
1634
+ ownerYou: "T\xFA",
1635
+ orderTitle: "Orden para Claude",
1636
+ orderOn: "Hazlo",
1637
+ orderOff: "Solo anotado",
1638
+ intentTitle: "C\xF3mo hacerlo",
1639
+ intentHint: "un toque, y manda sobre cualquier cosa que Claude decidiera",
1640
+ intentNone: "Sin instrucci\xF3n",
1641
+ checklist: "Checklist",
1642
+ addStep: "A\xF1adir",
1643
+ addStepPlaceholder: "A\xF1adir paso\u2026",
1644
+ notes: "Notas",
1645
+ notePlaceholder: "Escr\xEDbele a Claude\u2026",
1646
+ leaveNote: "Dejar nota",
1647
+ columnTitle: "Columna",
1648
+ started: "Empezada",
1649
+ completed: "Terminada",
1650
+ neverStarted: "Marcada como terminada sin haberse empezado nunca.",
1651
+ deleteCard: "Borrar tarjeta",
1652
+ addCard: "A\xF1adir tarjeta",
1653
+ untranslatedBadge: "solo EN",
1654
+ filterForClaude: "Para Claude",
1655
+ filterYours: "Depende de ti",
1656
+ filterHighRisk: "Riesgo alto",
1657
+ statOpen: "Abiertas",
1658
+ statOrders: "\xD3rdenes",
1659
+ statYours: "Para ti",
1660
+ statDecisions: "Decisiones",
1661
+ statHighRisk: "Riesgo alto",
1662
+ statDone: "Hecho",
1663
+ theme: "Tema",
1664
+ themeSubtitle: "lo \xFAnico que cambia entre proyectos",
1665
+ themeNote: "La estructura no se mueve: cambia solo la paleta que recibe el proveedor de tema.",
1666
+ language: "Idioma",
1667
+ languageSubtitle: "las p\xE1ginas p\xFAblicas siguen en ingl\xE9s",
1668
+ fingerprint: "Huella del kit",
1669
+ fingerprintSubtitle: "para saber si algo cambi\xF3 sin releer nada",
1670
+ trackedFiles: (count, version) => `${count} archivos versionados en la v${version}.`,
1671
+ notifications: "Qu\xE9 ha cambiado",
1672
+ nothingNew: "Nada nuevo desde la \xFAltima vez que miraste.",
1673
+ markAllSeen: "Marcar todo como visto",
1674
+ unreadCount: (n) => `${n} sin leer`,
1675
+ notifRun: (by) => `${by} ha trabajado el tablero`,
1676
+ notifWaiting: "Esperando por ti",
1677
+ notifSuggestion: (area) => `Sugerencia abierta \xB7 ${area}`,
1678
+ help: "Ayuda",
1679
+ helpTitle: "Lo que este panel necesita",
1680
+ setupReady: "Todo est\xE1 en su sitio. Lo que edites aqu\xED se confirma en el repositorio.",
1681
+ setupOk: "puesto",
1682
+ setupMissing: "sin poner",
1683
+ setupUnknown: "no se sabe",
1684
+ setupWhatToDo: "Qu\xE9 hacer",
1685
+ setupChecking: "Preguntando al servidor qu\xE9 hay configurado\u2026",
1686
+ setupWriteTarget: (repo, branch, file) => `Al guardar se confirma ${file} en ${branch}, en ${repo}.`,
1687
+ needPassword: "Una contrase\xF1a en el panel",
1688
+ needPasswordEnables: "Sin ella el panel responde 503 en vez de quedar p\xFAblico \u2014 as\xED que esta ya est\xE1 puesta, o no estar\xEDas leyendo esto.",
1689
+ needToken: "Un token de GitHub",
1690
+ needTokenEnables: "Permite que Guardar confirme el tablero en el repositorio. Sin \xE9l todo lo dem\xE1s funciona y Guardar falla.",
1691
+ needRepo: "El repositorio donde escribir",
1692
+ needRepoEnables: "En qu\xE9 repositorio se confirma el tablero. No hay valor por defecto a prop\xF3sito: escribir en el equivocado es peor que no escribir.",
1693
+ fixTokenCreate: "En GitHub, crea un token de acceso personal de tipo fine-grained.",
1694
+ fixTokenScope: "Dale Contents: read and write, solo sobre este repositorio \u2014 nada m\xE1s, y ning\xFAn otro repositorio.",
1695
+ fixTokenPaste: "P\xE9galo en el proyecto de hosting como GH_TOKEN. Nunca en el repositorio: un token en un commit es un token que hay que revocar.",
1696
+ fixTokenRedeploy: "Vuelve a desplegar. Las variables de entorno se leen al desplegar, as\xED que un despliegue en marcha no ver\xE1 una nueva.",
1697
+ fixRepo: "Pon PANEL_REPO en el proyecto de hosting con el owner/repo de este proyecto, y vuelve a desplegar.",
1698
+ demoTourWelcomeTitle: "Un panel de verdad, y a\xFAn no se guarda nada",
1699
+ demoTourWelcomeBody: "Aqu\xED funciona todo: la empresa de transporte es inventada, el panel no. Mueve cosas, abre cosas, r\xF3mpelas \u2014 no puedes estropear nada.",
1700
+ demoTourBoardTitle: "Arrastra una tarjeta, y luego abre una",
1701
+ demoTourBoardBody: "El asa de la izquierda la mueve entre columnas, y funciona con el dedo igual que con el rat\xF3n. Al pulsar en cualquier otro sitio se abre la tarjeta, donde est\xE1n su prioridad, su riesgo y su lista de pasos.",
1702
+ demoTourThemeTitle: "Prueba otra identidad",
1703
+ demoTourThemeBody: "En Ajustes est\xE1n los temas. No son repintados: uno se dibuja como un libro de cuentas \u2014 cuadrado, con filetes, en serif \u2014 mientras otro flota tarjetas en columnas redondeadas. Cada proyecto trae su color y su forma.",
1704
+ demoTourKeepTitle: "Aqu\xED no se guarda nada",
1705
+ demoTourKeepBody: "Aqu\xED no se guarda nada, as\xED que al recargar vuelve todo. Conectado a un repositorio, el panel confirma cada edici\xF3n, y eso es lo que da a cada cambio su diff y su historia.",
1706
+ tourWelcomeTitle: "Este tablero es el proyecto, no una demo",
1707
+ tourWelcomeBody: "Cada tarjeta es trabajo real sobre este repositorio. Nada es relleno, y nada se pierde al cerrar la pesta\xF1a.",
1708
+ tourBoardTitle: "Arrastra una tarjeta para moverla",
1709
+ tourBoardBody: "Usa el asa de la izquierda. Una tarjeta pasa a En curso cuando empieza el trabajo y a Hecho solo cuando est\xE1 acabado y comprobado \u2014 moverla es una afirmaci\xF3n, as\xED que mu\xE9vela solo cuando sea verdad.",
1710
+ tourOrdersTitle: "Dile a Claude qu\xE9 hacer, y c\xF3mo",
1711
+ tourOrdersBody: "Abre una tarjeta y marca Orden para Claude para pedirlo. C\xF3mo hacerlo pesa m\xE1s: gana a cualquier cosa que Claude decidir\xEDa por su cuenta, y tres de sus valores son negativas \u2014 expl\xEDcalo antes, preg\xFAntame antes, no lo toques todav\xEDa.",
1712
+ tourSaveTitle: "No se guarda nada hasta que pulsas Guardar",
1713
+ tourSaveBody: "Hasta entonces los cambios se quedan en este navegador. Guardar confirma el tablero en el repositorio, as\xED cada cambio tiene su diff y su historia. Si te vas sin guardar, se te avisa.",
1714
+ tourThemeTitle: "Ponlo a tu gusto",
1715
+ tourThemeBody: "En Ajustes est\xE1 el tema \u2014 que es color y forma, no solo color \u2014 y el idioma. Lo que elijas es como se ver\xE1 este panel a partir de ahora.",
1716
+ gotIt: "Entendido",
1717
+ showGuideAgain: "Volver a ver la gu\xEDa",
1718
+ guideSection: "La gu\xEDa",
1719
+ session: "Sesi\xF3n",
1720
+ sessionSubtitle: "Este navegador sigue con la sesi\xF3n abierta ocho horas.",
1721
+ signOut: "Cerrar sesi\xF3n",
1722
+ signingOut: "Cerrando sesi\xF3n\u2026",
1723
+ signOutFailed: "No se pudo cerrar la sesi\xF3n. Revisa la conexi\xF3n e int\xE9ntalo otra vez.",
1724
+ links: "Enlaces",
1725
+ home: "Inicio",
1726
+ demo: "Demo",
1727
+ repository: "Repositorio",
1728
+ terms: "T\xE9rminos",
1729
+ save: "Guardar en GitHub",
1730
+ saving: "Guardando\u2026",
1731
+ savedAt: (time) => `guardado ${time}`,
1732
+ savedWithCommit: "Guardado. El sitio se reconstruye solo.",
1733
+ saveFailed: "No se pudo guardar",
1734
+ saveConflict: "El tablero cambi\xF3 en el repositorio desde que se abri\xF3 esta p\xE1gina. Recarga antes de volver a guardar.",
1735
+ unsaved: "cambios sin guardar",
1736
+ saveSection: "Tablero",
1737
+ saveSubtitle: "el panel escribe su propio estado en el repositorio",
1738
+ saveHelp: "Al guardar se commitea el archivo del tablero en main a trav\xE9s de la API de GitHub, as\xED el cambio tiene diff, autor e historial.",
1739
+ upToDate: "No hay cambios que guardar.",
1740
+ demoNotice: "Lo que edites aqu\xED se queda en este navegador: la demo no tiene d\xF3nde guardar.",
1741
+ analysisEyebrow: "An\xE1lisis",
1742
+ briefTitle: "Para qu\xE9 es esto",
1743
+ stage: "Momento",
1744
+ goalsTitle: "Objetivos",
1745
+ constraintsTitle: "Innegociable",
1746
+ constraintOn: "se mantiene",
1747
+ constraintOff: "levantada",
1748
+ weights: { primary: "principal", secondary: "secundario", off: "no es objetivo" },
1749
+ metricsTitle: "M\xE9tricas",
1750
+ metricsIntro: "Cada una dice qu\xE9 direcci\xF3n cuenta como mejor, as\xED una lectura en contra se ve en vez de opinarse.",
1751
+ metricWith: "a favor del objetivo",
1752
+ metricAgainst: "en contra del objetivo",
1753
+ strategiesTitle: "Estrategias",
1754
+ strategyOpenHint: "Todav\xEDa no hay camino elegido \u2014 esa decisi\xF3n es tuya, y el tablero la sigue.",
1755
+ strategyChosenHint: "El camino elegido es una instrucci\xF3n: las tarjetas que lo sirven son trabajo de ahora.",
1756
+ strategyChoose: "Elegir este camino",
1757
+ strategyChosen: "Elegido",
1758
+ effort: "Esfuerzo",
1759
+ riskShort: "Riesgo",
1760
+ upside: "Recorrido",
1761
+ suggestionsTitle: "Sugerencias abiertas",
1762
+ suggestionsIntro: "Cosas que conviene hacer y todav\xEDa no son tarjeta. Marcarlas hechas o descartadas es lo que hace que dejen de volver.",
1763
+ severities: { high: "alta", medium: "media", low: "baja" },
1764
+ suggestionStatuses: { new: "nueva", doing: "en curso", done: "hecha", dismissed: "descartada" },
1765
+ analysisSections: { board: "Tablero", project: "Proyecto", strategy: "Estrategia", market: "Mercado" },
1766
+ analysisSectionHints: {
1767
+ board: "Lo que suman las tarjetas, contado en vez de declarado.",
1768
+ project: "Para qu\xE9 es esto, en qu\xE9 no debe convertirse, y c\xF3mo se mide.",
1769
+ strategy: "Los caminos abiertos, el elegido, y lo que merece la pena hacer ahora.",
1770
+ market: "Para qui\xE9n es esto, y qui\xE9n m\xE1s est\xE1 ya ah\xED."
1771
+ },
1772
+ analysisAnswered: (answered, total) => `${answered} de ${total} contestadas`,
1773
+ analysisAllDecided: "Todas las preguntas de esta pantalla tienen respuesta.",
1774
+ choicesTitle: "Respuestas fijas",
1775
+ choicesIntro: "Contesta aqu\xED una vez en lugar de en cada tarjeta. Claude las lee antes de decidir nada, as\xED que una respuesta dada aqu\xED sobrevive a la conversaci\xF3n en la que si no se habr\xEDa dado.",
1776
+ choiceOpen: "sin decidir",
1777
+ choiceDecidedAt: (at) => `decidido ${at}`,
1778
+ choiceReopen: "Reabrir",
1779
+ choicesNone: "Todav\xEDa no hay preguntas fijas en esta pantalla.",
1780
+ claudeReadsTitle: "Lo que Claude lee de aqu\xED",
1781
+ claudeReadsIntro: "Todas las respuestas en un sitio, para poder darle a una sesi\xF3n las decisiones sin darle el panel entero.",
1782
+ claudeOpenTitle: "Sigue abierto",
1783
+ claudeOpenIntro: "Nadie ha contestado esto. Una pregunta sin contestar es informaci\xF3n: Claude pregunta en vez de adivinar, y no toca el terreno hasta que lo digas t\xFA.",
1784
+ boardFactsTitle: "Lo que dice el tablero",
1785
+ boardFactsIntro: "Contado desde las tarjetas cada vez que se dibuja esto. Aqu\xED no se guarda nada, as\xED que nada de aqu\xED puede contradecir al tablero.",
1786
+ factOpen: "Abiertas",
1787
+ factWaiting: "Esperando por ti",
1788
+ factOrders: "Pedidas",
1789
+ factHighRisk: "Riesgo alto",
1790
+ factOldest: "Tarjeta abierta m\xE1s vieja",
1791
+ factDays: (n) => n === 1 ? "1 d\xEDa" : `${n} d\xEDas`,
1792
+ factColumnsTitle: "D\xF3nde est\xE1n",
1793
+ factClaimedTitle: "Terminadas sin haberse empezado nunca",
1794
+ factClaimedIntro: "Una tarjeta con fecha de fin y sin fecha de inicio. O el trabajo se hizo y el inicio no se apunt\xF3, o no se hizo \u2014 las dos cosas conviene saberlas antes de creerse la tarjeta.",
1795
+ factClaimedNone: "Ninguna \u2014 todas las terminadas se empezaron antes.",
1796
+ marketIntro: "Escrito con honestidad o no vale nada: un competidor sin ninguna virtud es un competidor que nadie ha mirado.",
1797
+ marketEmpty: "Todav\xEDa no hay nada escrito. No todo tablero es un producto \u2014 mejor dejarlo vac\xEDo que inventarse un p\xFAblico.",
1798
+ audienceTitle: "P\xFAblico",
1799
+ positioningTitle: "Posici\xF3n",
1800
+ competitorsTitle: "Qui\xE9n m\xE1s est\xE1 ah\xED",
1801
+ competitorStrength: "Hace mejor",
1802
+ competitorGap: "Deja hueco",
1803
+ pricingTitle: "Lo que cuesta",
1804
+ marketNotesTitle: "Notas",
1805
+ importedTheme: "Derivado de este proyecto",
1806
+ importedFrom: (at, files) => `Claude ley\xF3 ${files} el ${at} y mape\xF3 lo que encontr\xF3 a los tokens del tema.`,
1807
+ importedRefresh: "Volver a derivarlo",
1808
+ importedRefreshAsked: (at) => `Pedido el ${at}. El panel no puede ejecutar a Claude, as\xED que esto es un recado para la siguiente sesi\xF3n y no algo que est\xE9 pasando ahora \u2014 se queda en la campana hasta que se haga.`,
1809
+ importedRefreshHow: "Guarda primero: la petici\xF3n es parte del tablero, y Claude lee el tablero.",
1810
+ notifThemeRefresh: "Volver a derivar el tema del proyecto",
1811
+ marketplace: "Herramientas",
1812
+ marketplaceEyebrow: "dfklabs",
1813
+ marketplaceTitle: "Herramientas",
1814
+ marketplaceIntro: "Todo lo que publica dfklabs, sea o no un plugin de Claude Code. El cat\xE1logo y cada versi\xF3n salen de sus propias fuentes; lo que este proyecto tiene activado sale de su repositorio. Aqu\xED no se guarda nada, as\xED que nada de aqu\xED puede estar desactualizado sin decirlo.",
1815
+ marketplaceSource: (url) => `Marketplace: ${url}`,
1816
+ refresh: "Actualizar",
1817
+ refreshing: "Leyendo\u2026",
1818
+ readAt: (age) => `Le\xEDdo ${age}.`,
1819
+ catalogueStale: (age) => `Ahora mismo no se pudo llegar al cat\xE1logo, as\xED que esta es la \xFAltima copia que funcion\xF3 \u2014 le\xEDda ${age}. Las versiones y los estados pueden haber cambiado desde entonces.`,
1820
+ catalogueUnavailable: "No se pudo llegar al cat\xE1logo y no hay copia en cach\xE9 a la que recurrir. Esto no es un cat\xE1logo vac\xEDo: es una p\xE1gina que todav\xEDa no tiene nada que ense\xF1ar. Int\xE9ntalo dentro de un momento.",
1821
+ catalogueFailed: "El panel no pudo preguntarle el cat\xE1logo a su propio servidor. Revisa la conexi\xF3n e int\xE9ntalo otra vez.",
1822
+ catalogueLoading: "Leyendo el cat\xE1logo\u2026",
1823
+ catalogueEmpty: "Nada coincide con este filtro.",
1824
+ stateFrom: (repo, file) => `Estado de instalaci\xF3n le\xEDdo de ${file} en ${repo}.`,
1825
+ settingsAbsent: "ese archivo todav\xEDa no existe",
1826
+ settingsUnreadable: "no se pudo leer, as\xED que aqu\xED abajo no se afirma nada",
1827
+ settingsUnparseable: "existe pero no es JSON v\xE1lido",
1828
+ filterPlugins: "Plugins",
1829
+ filterActive: "Activos aqu\xED",
1830
+ filterStandalone: "Solo comando",
1831
+ installStates: {
1832
+ active: "Activo",
1833
+ disabled: "Desactivado",
1834
+ absent: "No instalado",
1835
+ misconfigured: "Error de configuraci\xF3n",
1836
+ unknown: "No se sabe"
1837
+ },
1838
+ stateMisconfiguredWhy: "El archivo de ajustes nombra este plugin pero no registra el marketplace del que viene, as\xED que no hay de d\xF3nde instalarlo. Copia el bloque de Instalar: lleva las dos mitades.",
1839
+ toolCommandOnly: "Comando",
1840
+ versionUnknown: "versi\xF3n desconocida",
1841
+ runIt: "Ejecutarlo",
1842
+ install: "Instalar",
1843
+ howToRun: "C\xF3mo ejecutarlo",
1844
+ learnMore: "M\xE1s",
1845
+ copy: "Copiar",
1846
+ copied: "Copiado",
1847
+ copySelected: "Seleccionado \u2014 pulsa \u2318C",
1848
+ installTitle: "Instalar",
1849
+ installRepoTitle: "En un repositorio",
1850
+ installRepoHint: "funciona en todas partes, y para todo el que trabaje en el repositorio",
1851
+ installRepoMerge: "Fusiona estas dos claves en el archivo \u2014 no lo reemplaces. Se instala al arrancar la siguiente sesi\xF3n, para cualquiera que trabaje en este repositorio.",
1852
+ installRepoMergeRegistered: "El marketplace ya est\xE1 registrado aqu\xED, as\xED que lo \xFAnico nuevo es la l\xEDnea del plugin. Fusiona estas dos claves en el archivo en vez de reemplazarlo.",
1853
+ installCliTitle: "Claude Code en terminal o en la app de escritorio",
1854
+ installCliHint: "solo en esta m\xE1quina",
1855
+ installStandaloneTitle: "Sin Claude",
1856
+ installStandaloneHint: "la herramienta por su cuenta",
1857
+ installNoStandalone: "Esta herramienta no publica un comando standalone.",
1858
+ installNoPlugin: "El cat\xE1logo no ofrece esta como plugin, as\xED que no hay nada que activar \u2014 es un comando que se ejecuta.",
1859
+ historyEyebrow: "Revisiones",
1860
+ historyTitle: "Qu\xE9 hizo de verdad cada pasada",
1861
+ historyIntro: "Una entrada por sesi\xF3n, la m\xE1s reciente primero, nombrando las tarjetas que toc\xF3. Una tarjeta solo llega a Hecho cuando su trabajo est\xE1 en main, y el tablero sella cu\xE1ndo empez\xF3 y cu\xE1ndo termin\xF3 \u2014 as\xED una afirmaci\xF3n se comprueba en vez de creerse.",
1862
+ historyEmpty: "Todav\xEDa no hay revisiones registradas.",
1863
+ historyTouched: "Tarjetas tocadas",
1864
+ brandEyebrow: "Marca \xB7 decisi\xF3n abierta",
1865
+ logosTitle: "Conceptos de logo",
1866
+ logosIntro: "Cuatro marcas dibujadas desde la misma geometr\xEDa, todas planas: sin brillo y sin degradados que un favicon aplanar\xEDa igual.",
1867
+ functionsEyebrow: "Funciones",
1868
+ functionsTitle: "Qu\xE9 le da esto a un proyecto, y c\xF3mo lo usa Claude",
1869
+ functionsIntro: "Todo lo que recibe un proyecto al instalarlo, y el protocolo que evita que un agente relea el kit \u2014y lo pague\u2014 en cada sesi\xF3n."
1870
+ }
1871
+ };
1872
+
1873
+ // src/panel/AnalysisView.tsx
1874
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
1875
+ var SUGGESTION_STATUSES = ["new", "doing", "done", "dismissed"];
1876
+ function MetricTile({ metric, lang, ui }) {
1877
+ const { latest, delta, against } = trendOf(metric);
1878
+ const points = sparkPoints(metric.readings);
1879
+ const width = 168;
1880
+ const height = 34;
1881
+ const path = points.map((point) => `${(point.x * width).toFixed(1)},${(point.y * (height - 6) + 3).toFixed(1)}`);
1882
+ const last = points[points.length - 1];
1883
+ return /* @__PURE__ */ jsxs14("article", { className: "pt-metric", children: [
1884
+ /* @__PURE__ */ jsx16("p", { className: "pt-metric__label", children: text(metric.label, lang) }),
1885
+ /* @__PURE__ */ jsxs14("p", { className: "pt-metric__value", children: [
1886
+ latest ? latest.value : "\u2014",
1887
+ /* @__PURE__ */ jsx16("span", { className: "pt-metric__unit", children: metric.unit })
1888
+ ] }),
1889
+ delta !== null && /* @__PURE__ */ jsxs14("p", { className: `pt-metric__delta${against ? " is-against" : " is-with"}`, children: [
1890
+ delta > 0 ? "\u25B2" : delta < 0 ? "\u25BC" : "\u25A0",
1891
+ " ",
1892
+ delta > 0 ? "+" : "",
1893
+ delta,
1894
+ /* @__PURE__ */ jsxs14("span", { className: "pt-metric__since", children: [
1895
+ " \xB7 ",
1896
+ against ? ui.metricAgainst : ui.metricWith
1897
+ ] })
1898
+ ] }),
1899
+ points.length > 1 && /* @__PURE__ */ jsxs14(
1900
+ "svg",
1901
+ {
1902
+ className: "pt-metric__spark",
1903
+ viewBox: `0 0 ${width} ${height}`,
1904
+ width: "100%",
1905
+ height,
1906
+ role: "img",
1907
+ "aria-label": `${text(metric.label, lang)}: ${metric.readings.map((r) => `${r.date} ${r.value}`).join(", ")}`,
1908
+ children: [
1909
+ /* @__PURE__ */ jsx16("polyline", { points: path.join(" "), fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round" }),
1910
+ last && /* @__PURE__ */ jsx16("circle", { cx: last.x * width, cy: last.y * (height - 6) + 3, r: "3.5", className: "pt-metric__point" })
1911
+ ]
1912
+ }
1913
+ ),
1914
+ latest?.note && /* @__PURE__ */ jsx16("p", { className: "pt-metric__note", children: text(latest.note, lang) })
1915
+ ] });
1916
+ }
1917
+ function ChoiceCard({
1918
+ section,
1919
+ choice,
1920
+ lang,
1921
+ ui,
1922
+ onDecide
1923
+ }) {
1924
+ const answered = choice.options.find((option) => option.id === choice.value);
1925
+ return /* @__PURE__ */ jsxs14("article", { className: `pt-choice${answered ? " is-decided" : ""}`, children: [
1926
+ /* @__PURE__ */ jsx16("p", { className: "pt-choice__state", children: answered ? ui.choiceDecidedAt(choice.decidedAt ?? "") : ui.choiceOpen }),
1927
+ /* @__PURE__ */ jsx16("p", { className: "pt-choice__question", children: text(choice.question, lang) }),
1928
+ choice.why && /* @__PURE__ */ jsx16("p", { className: "pt-choice__why", children: text(choice.why, lang) }),
1929
+ /* @__PURE__ */ jsx16("div", { className: "pt-panel-row pt-choice__options", children: choice.options.map((option) => /* @__PURE__ */ jsx16(
1930
+ FilterChip,
1931
+ {
1932
+ label: text(option.label, lang),
1933
+ active: choice.value === option.id,
1934
+ onClick: () => onDecide(section, choice.id, choice.value === option.id ? null : option.id)
1935
+ },
1936
+ option.id
1937
+ )) }),
1938
+ answered?.detail && /* @__PURE__ */ jsx16("p", { className: "pt-choice__detail", children: text(answered.detail, lang) }),
1939
+ answered && /* @__PURE__ */ jsx16(
1940
+ "button",
1941
+ {
1942
+ type: "button",
1943
+ className: "pt-choice__reopen",
1944
+ onClick: () => onDecide(section, choice.id, null),
1945
+ children: ui.choiceReopen
1946
+ }
1947
+ )
1948
+ ] });
1949
+ }
1950
+ function Choices({
1951
+ section,
1952
+ analysis,
1953
+ lang,
1954
+ ui,
1955
+ onDecide
1956
+ }) {
1957
+ const choices = analysis.choices?.[section] ?? [];
1958
+ return /* @__PURE__ */ jsxs14("section", { children: [
1959
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.choicesTitle }),
1960
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: ui.choicesIntro }),
1961
+ choices.length === 0 ? /* @__PURE__ */ jsx16("p", { className: "pt-analysis__empty", children: ui.choicesNone }) : /* @__PURE__ */ jsx16("div", { className: "pt-choice-grid", children: choices.map((choice) => /* @__PURE__ */ jsx16(
1962
+ ChoiceCard,
1963
+ {
1964
+ section,
1965
+ choice,
1966
+ lang,
1967
+ ui,
1968
+ onDecide
1969
+ },
1970
+ choice.id
1971
+ )) })
1972
+ ] });
1973
+ }
1974
+ function AnalysisView({
1975
+ analysis,
1976
+ cards,
1977
+ columns,
1978
+ today: today2,
1979
+ lang,
1980
+ ui,
1981
+ onChooseStrategy,
1982
+ onSuggestionStatus,
1983
+ onDecide,
1984
+ onOpenCard
1985
+ }) {
1986
+ const t = (value) => text(value, lang);
1987
+ const [section, setSection] = useState6("board");
1988
+ const facts = useMemo3(() => boardFacts(cards, columns, today2), [cards, columns, today2]);
1989
+ const answers = useMemo3(() => decided(analysis), [analysis]);
1990
+ const open = useMemo3(() => undecided(analysis), [analysis]);
1991
+ const total = answers.length + open.length;
1992
+ const here = analysis.choices?.[section] ?? [];
1993
+ const answeredHere = here.filter((choice) => choice.options.some((option) => option.id === choice.value)).length;
1994
+ return /* @__PURE__ */ jsxs14("div", { className: "pt-panel-page pt-panel-section pt-analysis", children: [
1995
+ /* @__PURE__ */ jsxs14("section", { children: [
1996
+ /* @__PURE__ */ jsx16("p", { className: "pt-eyebrow", children: ui.analysisEyebrow }),
1997
+ /* @__PURE__ */ jsx16("h2", { className: "pt-h2", children: ui.analysisSections[section] }),
1998
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: ui.analysisSectionHints[section] }),
1999
+ /* @__PURE__ */ jsx16("nav", { className: "pt-analysis__nav", "aria-label": ui.analysis, children: ANALYSIS_SECTIONS.map((id) => {
2000
+ const count = (analysis.choices?.[id] ?? []).filter(
2001
+ (choice) => !choice.options.some((option) => option.id === choice.value)
2002
+ ).length;
2003
+ return /* @__PURE__ */ jsxs14(
2004
+ "button",
2005
+ {
2006
+ type: "button",
2007
+ className: `pt-analysis__tab${section === id ? " is-on" : ""}`,
2008
+ "aria-current": section === id ? "page" : void 0,
2009
+ onClick: () => setSection(id),
2010
+ children: [
2011
+ ui.analysisSections[id],
2012
+ count > 0 && /* @__PURE__ */ jsx16("span", { className: "pt-analysis__open", "aria-hidden": "true", children: count })
2013
+ ]
2014
+ },
2015
+ id
2016
+ );
2017
+ }) }),
2018
+ here.length > 0 && /* @__PURE__ */ jsxs14("p", { className: "pt-analysis__stage", children: [
2019
+ ui.analysisAnswered(answeredHere, here.length),
2020
+ answeredHere === here.length && /* @__PURE__ */ jsxs14(Fragment3, { children: [
2021
+ " \xB7 ",
2022
+ ui.analysisAllDecided
2023
+ ] })
2024
+ ] })
2025
+ ] }),
2026
+ section === "board" && /* @__PURE__ */ jsxs14(Fragment3, { children: [
2027
+ /* @__PURE__ */ jsxs14("section", { children: [
2028
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.boardFactsTitle }),
2029
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: ui.boardFactsIntro }),
2030
+ /* @__PURE__ */ jsx16("div", { className: "pt-fact-grid", children: [
2031
+ [ui.factOpen, String(facts.open)],
2032
+ [ui.factWaiting, String(facts.waitingOnOwner)],
2033
+ [ui.factOrders, String(facts.orders)],
2034
+ [ui.factHighRisk, String(facts.highRisk)]
2035
+ ].map(([label, value]) => /* @__PURE__ */ jsxs14("article", { className: "pt-fact", children: [
2036
+ /* @__PURE__ */ jsx16("p", { className: "pt-fact__label", children: label }),
2037
+ /* @__PURE__ */ jsx16("p", { className: "pt-fact__value", children: value })
2038
+ ] }, label)) }),
2039
+ /* @__PURE__ */ jsx16("h4", { className: "pt-analysis__minor", children: ui.factColumnsTitle }),
2040
+ /* @__PURE__ */ jsx16("ul", { className: "pt-analysis__list", children: facts.byColumn.map((column) => /* @__PURE__ */ jsxs14("li", { className: "pt-analysis__goal", children: [
2041
+ /* @__PURE__ */ jsx16("span", { children: t(column.title) }),
2042
+ /* @__PURE__ */ jsx16("span", { className: "pt-analysis__weight", children: column.count })
2043
+ ] }, column.id)) }),
2044
+ facts.oldestOpen && facts.oldestOpenDays !== null && /* @__PURE__ */ jsxs14("p", { className: "pt-analysis__stage", children: [
2045
+ ui.factOldest,
2046
+ ": ",
2047
+ /* @__PURE__ */ jsx16("strong", { children: ui.factDays(facts.oldestOpenDays) }),
2048
+ " \xB7 ",
2049
+ t(facts.oldestOpen.title)
2050
+ ] })
2051
+ ] }),
2052
+ /* @__PURE__ */ jsxs14("section", { children: [
2053
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.factClaimedTitle }),
2054
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: ui.factClaimedIntro }),
2055
+ facts.claimed.length === 0 ? /* @__PURE__ */ jsx16("p", { className: "pt-analysis__empty", children: ui.factClaimedNone }) : /* @__PURE__ */ jsx16("ul", { className: "pt-analysis__list", children: facts.claimed.map((card) => /* @__PURE__ */ jsxs14("li", { className: "pt-analysis__goal", children: [
2056
+ /* @__PURE__ */ jsx16("button", { type: "button", className: "pt-analysis__link", onClick: () => onOpenCard?.(card.id), children: t(card.title) || ui.untitled }),
2057
+ /* @__PURE__ */ jsx16("span", { className: "pt-analysis__weight", children: ui.neverStarted })
2058
+ ] }, card.id)) })
2059
+ ] })
2060
+ ] }),
2061
+ section === "project" && /* @__PURE__ */ jsxs14(Fragment3, { children: [
2062
+ /* @__PURE__ */ jsxs14("section", { children: [
2063
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.briefTitle }),
2064
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: t(analysis.brief) }),
2065
+ /* @__PURE__ */ jsxs14("p", { className: "pt-analysis__stage", children: [
2066
+ ui.stage,
2067
+ ": ",
2068
+ /* @__PURE__ */ jsx16("strong", { children: analysis.stage })
2069
+ ] })
2070
+ ] }),
2071
+ /* @__PURE__ */ jsxs14("section", { className: "pt-analysis__split", children: [
2072
+ /* @__PURE__ */ jsxs14("div", { className: "pt-cap pt-hud", children: [
2073
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.goalsTitle }),
2074
+ /* @__PURE__ */ jsx16("ul", { className: "pt-analysis__list", children: analysis.goals.map((goal) => /* @__PURE__ */ jsxs14("li", { className: `pt-analysis__goal is-${goal.weight}`, children: [
2075
+ /* @__PURE__ */ jsx16("span", { children: t(goal.label) }),
2076
+ /* @__PURE__ */ jsx16("span", { className: "pt-analysis__weight", children: ui.weights[goal.weight] })
2077
+ ] }, goal.id)) })
2078
+ ] }),
2079
+ /* @__PURE__ */ jsxs14("div", { className: "pt-cap pt-hud", children: [
2080
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.constraintsTitle }),
2081
+ /* @__PURE__ */ jsx16("ul", { className: "pt-analysis__list", children: analysis.constraints.map((constraint) => /* @__PURE__ */ jsxs14("li", { className: `pt-analysis__goal${constraint.on ? "" : " is-off"}`, children: [
2082
+ /* @__PURE__ */ jsx16("span", { children: t(constraint.label) }),
2083
+ /* @__PURE__ */ jsx16("span", { className: "pt-analysis__weight", children: constraint.on ? ui.constraintOn : ui.constraintOff })
2084
+ ] }, constraint.id)) })
2085
+ ] })
2086
+ ] }),
2087
+ /* @__PURE__ */ jsxs14("section", { children: [
2088
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.metricsTitle }),
2089
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: ui.metricsIntro }),
2090
+ /* @__PURE__ */ jsx16("div", { className: "pt-metric-grid", children: analysis.metrics.map((metric) => /* @__PURE__ */ jsx16(MetricTile, { metric, lang, ui }, metric.id)) })
2091
+ ] })
2092
+ ] }),
2093
+ section === "strategy" && /* @__PURE__ */ jsxs14(Fragment3, { children: [
2094
+ /* @__PURE__ */ jsxs14("section", { children: [
2095
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.strategiesTitle }),
2096
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: analysis.chosen ? ui.strategyChosenHint : ui.strategyOpenHint }),
2097
+ /* @__PURE__ */ jsx16("div", { className: "pt-strategy-grid", children: analysis.strategies.map((strategy) => {
2098
+ const chosen = analysis.chosen === strategy.id;
2099
+ return /* @__PURE__ */ jsxs14("article", { className: `pt-strategy pt-hud${chosen ? " is-chosen" : ""}`, children: [
2100
+ /* @__PURE__ */ jsx16("h4", { className: "pt-strategy__title", children: t(strategy.title) }),
2101
+ /* @__PURE__ */ jsx16("p", { className: "pt-panel-text", children: t(strategy.thesis) }),
2102
+ /* @__PURE__ */ jsx16("dl", { className: "pt-strategy__meters", children: [
2103
+ [ui.effort, strategy.effort],
2104
+ [ui.riskShort, strategy.risk],
2105
+ [ui.upside, strategy.upside]
2106
+ ].map(([label, value]) => /* @__PURE__ */ jsxs14("div", { children: [
2107
+ /* @__PURE__ */ jsx16("dt", { children: label }),
2108
+ /* @__PURE__ */ jsxs14("dd", { children: [
2109
+ /* @__PURE__ */ jsx16("span", { className: "pt-strategy__track", children: /* @__PURE__ */ jsx16("span", { className: "pt-strategy__fill", style: { width: `${value * 10}%` } }) }),
2110
+ /* @__PURE__ */ jsx16("b", { children: value })
2111
+ ] })
2112
+ ] }, label)) }),
2113
+ /* @__PURE__ */ jsx16("p", { className: "pt-strategy__horizon", children: t(strategy.horizon) }),
2114
+ /* @__PURE__ */ jsx16("ul", { className: "pt-cap__list", children: strategy.moves.map((move) => /* @__PURE__ */ jsx16("li", { children: t(move) }, t(move))) }),
2115
+ /* @__PURE__ */ jsx16(
2116
+ FilterChip,
2117
+ {
2118
+ label: chosen ? ui.strategyChosen : ui.strategyChoose,
2119
+ active: chosen,
2120
+ onClick: () => onChooseStrategy(strategy.id)
2121
+ }
2122
+ )
2123
+ ] }, strategy.id);
2124
+ }) })
2125
+ ] }),
2126
+ /* @__PURE__ */ jsxs14("section", { children: [
2127
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.suggestionsTitle }),
2128
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: ui.suggestionsIntro }),
2129
+ /* @__PURE__ */ jsx16("ul", { className: "pt-analysis__suggestions", children: analysis.suggestions.map((suggestion) => /* @__PURE__ */ jsxs14("li", { className: `pt-suggestion pt-hud is-${suggestion.severity}`, children: [
2130
+ /* @__PURE__ */ jsxs14("p", { className: "pt-suggestion__meta", children: [
2131
+ suggestion.area,
2132
+ " \xB7 ",
2133
+ ui.severities[suggestion.severity],
2134
+ " \xB7 ",
2135
+ suggestion.date
2136
+ ] }),
2137
+ /* @__PURE__ */ jsx16("p", { className: "pt-panel-text", children: t(suggestion.text) }),
2138
+ /* @__PURE__ */ jsx16("div", { className: "pt-panel-row", children: SUGGESTION_STATUSES.map((status) => /* @__PURE__ */ jsx16(
2139
+ FilterChip,
2140
+ {
2141
+ label: ui.suggestionStatuses[status],
2142
+ active: suggestion.status === status,
2143
+ onClick: () => onSuggestionStatus(suggestion.id, status)
2144
+ },
2145
+ status
2146
+ )) })
2147
+ ] }, suggestion.id)) })
2148
+ ] })
2149
+ ] }),
2150
+ section === "market" && /* @__PURE__ */ jsx16("section", { children: analysis.market ? /* @__PURE__ */ jsxs14(Fragment3, { children: [
2151
+ /* @__PURE__ */ jsxs14("div", { className: "pt-analysis__split", children: [
2152
+ /* @__PURE__ */ jsxs14("div", { className: "pt-cap pt-hud", children: [
2153
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.audienceTitle }),
2154
+ /* @__PURE__ */ jsx16("p", { className: "pt-panel-text", children: t(analysis.market.audience) })
2155
+ ] }),
2156
+ /* @__PURE__ */ jsxs14("div", { className: "pt-cap pt-hud", children: [
2157
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.positioningTitle }),
2158
+ /* @__PURE__ */ jsx16("p", { className: "pt-panel-text", children: t(analysis.market.positioning) })
2159
+ ] })
2160
+ ] }),
2161
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title pt-analysis__minor", children: ui.competitorsTitle }),
2162
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: ui.marketIntro }),
2163
+ /* @__PURE__ */ jsx16("div", { className: "pt-rival-grid", children: analysis.market.competitors.map((rival) => /* @__PURE__ */ jsxs14("article", { className: "pt-rival pt-hud", children: [
2164
+ /* @__PURE__ */ jsx16("h4", { className: "pt-rival__name", children: rival.name }),
2165
+ /* @__PURE__ */ jsxs14("p", { className: "pt-rival__line", children: [
2166
+ /* @__PURE__ */ jsx16("span", { className: "pt-rival__tag", children: ui.competitorStrength }),
2167
+ t(rival.strength)
2168
+ ] }),
2169
+ /* @__PURE__ */ jsxs14("p", { className: "pt-rival__line is-gap", children: [
2170
+ /* @__PURE__ */ jsx16("span", { className: "pt-rival__tag", children: ui.competitorGap }),
2171
+ t(rival.gap)
2172
+ ] })
2173
+ ] }, rival.id)) }),
2174
+ analysis.market.pricing && /* @__PURE__ */ jsxs14(Fragment3, { children: [
2175
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title pt-analysis__minor", children: ui.pricingTitle }),
2176
+ /* @__PURE__ */ jsx16("p", { className: "pt-panel-text", children: t(analysis.market.pricing) })
2177
+ ] }),
2178
+ analysis.market.notes && analysis.market.notes.length > 0 && /* @__PURE__ */ jsxs14(Fragment3, { children: [
2179
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title pt-analysis__minor", children: ui.marketNotesTitle }),
2180
+ /* @__PURE__ */ jsx16("ul", { className: "pt-cap__list", children: analysis.market.notes.map((note) => /* @__PURE__ */ jsx16("li", { children: t(note) }, t(note))) })
2181
+ ] })
2182
+ ] }) : /* @__PURE__ */ jsx16("p", { className: "pt-analysis__empty", children: ui.marketEmpty }) }),
2183
+ /* @__PURE__ */ jsx16(Choices, { section, analysis, lang, ui, onDecide }),
2184
+ total > 0 && /* @__PURE__ */ jsxs14("section", { className: "pt-analysis__split", children: [
2185
+ /* @__PURE__ */ jsxs14("div", { className: "pt-cap pt-hud", children: [
2186
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.claudeReadsTitle }),
2187
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: ui.claudeReadsIntro }),
2188
+ /* @__PURE__ */ jsxs14("ul", { className: "pt-analysis__list", children: [
2189
+ answers.map(({ section: from, choice, option }) => /* @__PURE__ */ jsxs14("li", { className: "pt-analysis__goal is-primary", children: [
2190
+ /* @__PURE__ */ jsxs14("span", { children: [
2191
+ /* @__PURE__ */ jsx16("span", { className: "pt-analysis__from", children: ui.analysisSections[from] }),
2192
+ t(choice.question)
2193
+ ] }),
2194
+ /* @__PURE__ */ jsx16("span", { className: "pt-analysis__weight", children: t(option.label) })
2195
+ ] }, choice.id)),
2196
+ answers.length === 0 && /* @__PURE__ */ jsx16("li", { className: "pt-analysis__goal is-off", children: ui.choicesNone })
2197
+ ] })
2198
+ ] }),
2199
+ /* @__PURE__ */ jsxs14("div", { className: "pt-cap pt-hud", children: [
2200
+ /* @__PURE__ */ jsx16("h3", { className: "pt-cap__title", children: ui.claudeOpenTitle }),
2201
+ /* @__PURE__ */ jsx16("p", { className: "pt-sub", children: ui.claudeOpenIntro }),
2202
+ /* @__PURE__ */ jsxs14("ul", { className: "pt-analysis__list", children: [
2203
+ open.map(({ section: from, choice }) => /* @__PURE__ */ jsxs14("li", { className: "pt-analysis__goal", children: [
2204
+ /* @__PURE__ */ jsxs14("button", { type: "button", className: "pt-analysis__link", onClick: () => setSection(from), children: [
2205
+ /* @__PURE__ */ jsx16("span", { className: "pt-analysis__from", children: ui.analysisSections[from] }),
2206
+ t(choice.question)
2207
+ ] }),
2208
+ /* @__PURE__ */ jsx16("span", { className: "pt-analysis__weight", children: ui.choiceOpen })
2209
+ ] }, choice.id)),
2210
+ open.length === 0 && /* @__PURE__ */ jsx16("li", { className: "pt-analysis__goal is-off", children: ui.analysisAllDecided })
2211
+ ] })
2212
+ ] })
2213
+ ] })
2214
+ ] });
2215
+ }
2216
+
2217
+ // src/panel/MarketplaceView.tsx
2218
+ import { useMemo as useMemo4, useState as useState7 } from "react";
2219
+
2220
+ // src/panel/marketplace.ts
2221
+ var MARKETPLACE_NAME = "dfklabs";
2222
+ var MARKETPLACE_URL = "https://studio.dfklabs.com/marketplace.json";
2223
+ var SETTINGS_PATH = ".claude/settings.json";
2224
+ var DFKLABS = { name: MARKETPLACE_NAME, url: MARKETPLACE_URL };
2225
+ function repositorySnippet(toolId, marketplace = DFKLABS) {
2226
+ return `${JSON.stringify(
2227
+ {
2228
+ extraKnownMarketplaces: {
2229
+ [marketplace.name]: { source: { source: "url", url: marketplace.url } }
2230
+ },
2231
+ enabledPlugins: { [`${toolId}@${marketplace.name}`]: true }
2232
+ },
2233
+ null,
2234
+ 2
2235
+ )}
2236
+ `;
2237
+ }
2238
+ function commandSnippet(toolId, marketplace = DFKLABS) {
2239
+ return [
2240
+ `/plugin marketplace add ${marketplace.url}`,
2241
+ `/plugin install ${toolId}@${marketplace.name}`
2242
+ ].join("\n");
2243
+ }
2244
+ function ageInWords(seconds, lang) {
2245
+ const minutes = Math.floor(seconds / 60);
2246
+ const hours = Math.floor(minutes / 60);
2247
+ const days = Math.floor(hours / 24);
2248
+ if (lang === "es") {
2249
+ if (days >= 1) return days === 1 ? "hace 1 d\xEDa" : `hace ${days} d\xEDas`;
2250
+ if (hours >= 1) return hours === 1 ? "hace 1 hora" : `hace ${hours} horas`;
2251
+ if (minutes >= 1) return minutes === 1 ? "hace 1 minuto" : `hace ${minutes} minutos`;
2252
+ return "hace unos segundos";
2253
+ }
2254
+ if (days >= 1) return days === 1 ? "1 day ago" : `${days} days ago`;
2255
+ if (hours >= 1) return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
2256
+ if (minutes >= 1) return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
2257
+ return "seconds ago";
2258
+ }
2259
+
2260
+ // src/panel/MarketplaceView.tsx
2261
+ import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
2262
+ function ToolRow({
2263
+ tool,
2264
+ lang,
2265
+ ui,
2266
+ onInstall
2267
+ }) {
2268
+ return /* @__PURE__ */ jsxs15("article", { className: `pt-tool${tool.state ? ` is-${tool.state}` : " is-command"}`, children: [
2269
+ /* @__PURE__ */ jsxs15("div", { className: "pt-tool__head", children: [
2270
+ /* @__PURE__ */ jsx17("h3", { className: "pt-tool__name", children: tool.name }),
2271
+ /* @__PURE__ */ jsx17("span", { className: "pt-tool__state", children: tool.state ? ui.installStates[tool.state] : ui.toolCommandOnly })
2272
+ ] }),
2273
+ tool.summary && /* @__PURE__ */ jsx17("p", { className: "pt-panel-text", children: tool.summary }),
2274
+ /* @__PURE__ */ jsxs15("p", { className: "pt-tool__meta", children: [
2275
+ /* @__PURE__ */ jsx17("span", { className: "pt-tool__id", children: tool.id }),
2276
+ tool.claudePlugin && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2277
+ " \xB7 ",
2278
+ tool.version ? /* @__PURE__ */ jsxs15("span", { className: "pt-tool__version", children: [
2279
+ "v",
2280
+ tool.version
2281
+ ] }) : (
2282
+ // Never a version we made up. A blank here is the plugin's own
2283
+ // manifest failing to answer, and it says so on hover.
2284
+ /* @__PURE__ */ jsx17("span", { className: "pt-tool__version is-unknown", title: tool.versionError ?? void 0, children: ui.versionUnknown })
2285
+ )
2286
+ ] }),
2287
+ tool.repo && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2288
+ " \xB7 ",
2289
+ /* @__PURE__ */ jsx17("a", { className: "pt-tool__repo", href: `https://github.com/${tool.repo}`, target: "_blank", rel: "noreferrer", children: tool.repo })
2290
+ ] })
2291
+ ] }),
2292
+ tool.tags.length > 0 && /* @__PURE__ */ jsx17("p", { className: "pt-tool__tags", children: tool.tags.map((tag) => /* @__PURE__ */ jsx17("span", { className: "pt-tool__tag", children: tag }, tag)) }),
2293
+ !tool.claudePlugin && tool.standaloneCommand && /* @__PURE__ */ jsx17(
2294
+ CopyBlock,
2295
+ {
2296
+ text: tool.standaloneCommand,
2297
+ label: ui.runIt,
2298
+ labels: { copy: ui.copy, copied: ui.copied, selected: ui.copySelected }
2299
+ }
2300
+ ),
2301
+ /* @__PURE__ */ jsxs15("div", { className: "pt-panel-row pt-tool__actions", children: [
2302
+ /* @__PURE__ */ jsx17("button", { type: "button", className: "pt-btn pt-btn--sm", onClick: () => onInstall(tool), children: tool.claudePlugin ? ui.install : ui.howToRun }),
2303
+ tool.homepage && /* @__PURE__ */ jsx17("a", { className: "pt-btn pt-btn--sm", href: tool.homepage, target: "_blank", rel: "noreferrer", children: ui.learnMore })
2304
+ ] }),
2305
+ tool.state === "misconfigured" && /* @__PURE__ */ jsx17("p", { className: "pt-warn", children: ui.stateMisconfiguredWhy })
2306
+ ] });
2307
+ }
2308
+ function MarketplaceView({ report, loading, failure, lang, ui, onRefresh }) {
2309
+ const [filter, setFilter] = useState7("all");
2310
+ const [open, setOpen] = useState7(null);
2311
+ const tools = report?.tools ?? [];
2312
+ const shown = useMemo4(() => {
2313
+ if (filter === "plugins") return tools.filter((tool) => tool.claudePlugin);
2314
+ if (filter === "active") return tools.filter((tool) => tool.state === "active");
2315
+ if (filter === "standalone") return tools.filter((tool) => !tool.claudePlugin);
2316
+ return tools;
2317
+ }, [tools, filter]);
2318
+ const counts = useMemo4(
2319
+ () => ({
2320
+ all: tools.length,
2321
+ plugins: tools.filter((tool) => tool.claudePlugin).length,
2322
+ active: tools.filter((tool) => tool.state === "active").length,
2323
+ standalone: tools.filter((tool) => !tool.claudePlugin).length
2324
+ }),
2325
+ [tools]
2326
+ );
2327
+ return /* @__PURE__ */ jsxs15("div", { className: "pt-panel-page pt-panel-section pt-marketplace", children: [
2328
+ /* @__PURE__ */ jsxs15("section", { children: [
2329
+ /* @__PURE__ */ jsx17("p", { className: "pt-eyebrow", children: report?.marketplace.name ?? ui.marketplaceEyebrow }),
2330
+ /* @__PURE__ */ jsx17("h2", { className: "pt-h2", children: ui.marketplaceTitle }),
2331
+ /* @__PURE__ */ jsx17("p", { className: "pt-sub", children: ui.marketplaceIntro }),
2332
+ /* @__PURE__ */ jsxs15("div", { className: "pt-panel-row pt-marketplace__status", children: [
2333
+ onRefresh && /* @__PURE__ */ jsx17("button", { type: "button", className: "pt-btn pt-btn--sm", onClick: onRefresh, disabled: loading, children: loading ? ui.refreshing : ui.refresh }),
2334
+ report && onRefresh && /* @__PURE__ */ jsx17("span", { className: "pt-marketplace__age", children: ui.readAt(ageInWords(report.ageSeconds, lang)) })
2335
+ ] }),
2336
+ report?.stale && !report.unavailable && /* @__PURE__ */ jsx17("p", { className: "pt-warn", children: ui.catalogueStale(ageInWords(report.ageSeconds, lang)) }),
2337
+ report?.unavailable && /* @__PURE__ */ jsx17("p", { className: "pt-warn", children: ui.catalogueUnavailable }),
2338
+ failure && /* @__PURE__ */ jsx17("p", { className: "pt-warn", children: ui.catalogueFailed })
2339
+ ] }),
2340
+ report && /* @__PURE__ */ jsxs15("section", { children: [
2341
+ /* @__PURE__ */ jsxs15("p", { className: "pt-marketplace__project", children: [
2342
+ ui.stateFrom(report.project.repo, SETTINGS_PATH),
2343
+ report.project.settingsStatus === "absent" && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2344
+ " \xB7 ",
2345
+ ui.settingsAbsent
2346
+ ] }),
2347
+ report.project.settingsStatus === "unreadable" && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2348
+ " \xB7 ",
2349
+ ui.settingsUnreadable
2350
+ ] }),
2351
+ report.project.settingsStatus === "unparseable" && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2352
+ " \xB7 ",
2353
+ ui.settingsUnparseable
2354
+ ] })
2355
+ ] }),
2356
+ report.project.detail && /* @__PURE__ */ jsx17("p", { className: "pt-marketplace__detail", children: report.project.detail }),
2357
+ /* @__PURE__ */ jsxs15("div", { className: "pt-panel-row pt-marketplace__filters", children: [
2358
+ /* @__PURE__ */ jsx17(FilterChip, { label: ui.all, count: counts.all, active: filter === "all", onClick: () => setFilter("all") }),
2359
+ /* @__PURE__ */ jsx17(
2360
+ FilterChip,
2361
+ {
2362
+ label: ui.filterPlugins,
2363
+ count: counts.plugins,
2364
+ active: filter === "plugins",
2365
+ onClick: () => setFilter("plugins")
2366
+ }
2367
+ ),
2368
+ /* @__PURE__ */ jsx17(
2369
+ FilterChip,
2370
+ {
2371
+ label: ui.filterActive,
2372
+ count: counts.active,
2373
+ active: filter === "active",
2374
+ onClick: () => setFilter("active")
2375
+ }
2376
+ ),
2377
+ /* @__PURE__ */ jsx17(
2378
+ FilterChip,
2379
+ {
2380
+ label: ui.filterStandalone,
2381
+ count: counts.standalone,
2382
+ active: filter === "standalone",
2383
+ onClick: () => setFilter("standalone")
2384
+ }
2385
+ )
2386
+ ] }),
2387
+ loading && tools.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "pt-analysis__empty", children: ui.catalogueLoading }) : shown.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "pt-analysis__empty", children: ui.catalogueEmpty }) : /* @__PURE__ */ jsx17("div", { className: "pt-tool-grid", children: shown.map((tool) => /* @__PURE__ */ jsx17(ToolRow, { tool, lang, ui, onInstall: setOpen }, tool.id)) })
2388
+ ] }),
2389
+ open && /* @__PURE__ */ jsxs15(DetailSheet, { open: true, onClose: () => setOpen(null), ariaLabel: `${ui.installTitle} \u2014 ${open.name}`, children: [
2390
+ /* @__PURE__ */ jsx17("p", { className: "pt-eyebrow", children: ui.installTitle }),
2391
+ /* @__PURE__ */ jsx17("h2", { className: "pt-h2", children: open.name }),
2392
+ /* @__PURE__ */ jsx17(
2393
+ InstallSheet,
2394
+ {
2395
+ tool: open,
2396
+ ui,
2397
+ marketplace: report?.marketplace ?? { name: MARKETPLACE_NAME, url: MARKETPLACE_URL },
2398
+ registered: report?.project.marketplaceRegistered ?? false
2399
+ }
2400
+ ),
2401
+ /* @__PURE__ */ jsx17("button", { type: "button", className: "pt-btn pt-btn--sm", onClick: () => setOpen(null), children: ui.close })
2402
+ ] })
2403
+ ] });
2404
+ }
2405
+ function InstallSheet({
2406
+ tool,
2407
+ ui,
2408
+ marketplace,
2409
+ registered
2410
+ }) {
2411
+ const copy = { copy: ui.copy, copied: ui.copied, selected: ui.copySelected };
2412
+ return /* @__PURE__ */ jsxs15(Fragment4, { children: [
2413
+ tool.claudePlugin ? /* @__PURE__ */ jsxs15(Fragment4, { children: [
2414
+ /* @__PURE__ */ jsx17(DetailSection, { title: ui.installRepoTitle, subtitle: ui.installRepoHint, children: /* @__PURE__ */ jsx17(
2415
+ CopyBlock,
2416
+ {
2417
+ text: repositorySnippet(tool.id, marketplace),
2418
+ label: SETTINGS_PATH,
2419
+ note: registered ? ui.installRepoMergeRegistered : ui.installRepoMerge,
2420
+ labels: copy
2421
+ }
2422
+ ) }),
2423
+ /* @__PURE__ */ jsx17(DetailSection, { title: ui.installCliTitle, subtitle: ui.installCliHint, children: /* @__PURE__ */ jsx17(CopyBlock, { text: commandSnippet(tool.id, marketplace), labels: copy }) })
2424
+ ] }) : /* @__PURE__ */ jsx17("p", { className: "pt-panel-text", children: ui.installNoPlugin }),
2425
+ tool.standaloneCommand ? /* @__PURE__ */ jsx17(DetailSection, { title: ui.installStandaloneTitle, subtitle: ui.installStandaloneHint, children: /* @__PURE__ */ jsx17(CopyBlock, { text: tool.standaloneCommand, labels: copy }) }) : /* @__PURE__ */ jsx17(DetailSection, { title: ui.installStandaloneTitle, children: /* @__PURE__ */ jsx17("p", { className: "pt-panel-text", children: ui.installNoStandalone }) }),
2426
+ /* @__PURE__ */ jsx17("p", { className: "pt-marketplace__source", children: ui.marketplaceSource(marketplace.url) })
2427
+ ] });
2428
+ }
2429
+
2430
+ // src/panel/CardSheet.tsx
2431
+ import { useState as useState8 } from "react";
2432
+ import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
2433
+ var LEVEL_LABEL = (ui) => ({
2434
+ high: ui.levelHigh,
2435
+ medium: ui.levelMedium,
2436
+ low: ui.levelLow
2437
+ });
2438
+ function CardSheet({ card, state, lang, ui, onChange, onDelete, onClose }) {
2439
+ const [step, setStep] = useState8("");
2440
+ const [note, setNote] = useState8("");
2441
+ if (!card) return null;
2442
+ const patch = (changes) => onChange({ ...card, ...changes, updatedAt: today() });
2443
+ const levelLabel = LEVEL_LABEL(ui);
2444
+ const ownerLabel = { claude: ui.ownerClaude, you: ui.ownerYou };
2445
+ function addStep() {
2446
+ const value = step.trim();
2447
+ if (!value) return;
2448
+ patch({ checks: [...card.checks, { id: newId(), text: writeText(lang, value), done: false }] });
2449
+ setStep("");
2450
+ }
2451
+ function addNote() {
2452
+ const value = note.trim();
2453
+ if (!value) return;
2454
+ patch({
2455
+ notes: [...card.notes, { id: newId(), by: "you", date: today(), text: writeText(lang, value) }]
2456
+ });
2457
+ setNote("");
2458
+ }
2459
+ return /* @__PURE__ */ jsxs16(DetailSheet, { open: true, onClose, ariaLabel: ui.cardDetail, children: [
2460
+ /* @__PURE__ */ jsxs16(DetailSection, { title: ui.title, children: [
2461
+ /* @__PURE__ */ jsx18(
2462
+ "input",
2463
+ {
2464
+ className: "pt-input",
2465
+ value: text(card.title, lang),
2466
+ placeholder: ui.titlePlaceholder,
2467
+ onChange: (event) => patch({ title: writeText(lang, event.target.value) })
2468
+ }
2469
+ ),
2470
+ untranslated(card.title) && /* @__PURE__ */ jsx18("span", { className: "pt-flag", children: ui.untranslatedBadge })
2471
+ ] }),
2472
+ /* @__PURE__ */ jsx18(DetailSection, { title: ui.detail, children: /* @__PURE__ */ jsx18(
2473
+ "textarea",
2474
+ {
2475
+ className: "pt-textarea",
2476
+ rows: 4,
2477
+ value: text(card.body, lang),
2478
+ placeholder: ui.detailPlaceholder,
2479
+ onChange: (event) => patch({ body: writeText(lang, event.target.value) })
2480
+ }
2481
+ ) }),
2482
+ /* @__PURE__ */ jsx18(DetailSection, { title: ui.areaLabel, children: /* @__PURE__ */ jsx18("div", { className: "pt-panel-row", children: state.areas.map((area) => /* @__PURE__ */ jsx18(
2483
+ FilterChip,
2484
+ {
2485
+ label: text(area.label, lang),
2486
+ active: card.area === area.id,
2487
+ onClick: () => patch({ area: area.id })
2488
+ },
2489
+ area.id
2490
+ )) }) }),
2491
+ /* @__PURE__ */ jsx18(DetailSection, { title: ui.priority, children: /* @__PURE__ */ jsx18("div", { className: "pt-panel-row", children: LEVELS.map((level) => /* @__PURE__ */ jsx18(
2492
+ FilterChip,
2493
+ {
2494
+ label: levelLabel[level],
2495
+ active: card.priority === level,
2496
+ onClick: () => patch({ priority: level })
2497
+ },
2498
+ level
2499
+ )) }) }),
2500
+ /* @__PURE__ */ jsx18(DetailSection, { title: ui.risk, subtitle: ui.riskHint, children: /* @__PURE__ */ jsx18("div", { className: "pt-panel-row", children: LEVELS.map((level) => /* @__PURE__ */ jsx18(
2501
+ FilterChip,
2502
+ {
2503
+ label: levelLabel[level],
2504
+ active: card.risk === level,
2505
+ onClick: () => patch({ risk: level })
2506
+ },
2507
+ level
2508
+ )) }) }),
2509
+ /* @__PURE__ */ jsx18(DetailSection, { title: ui.dependsOn, children: /* @__PURE__ */ jsx18("div", { className: "pt-panel-row", children: OWNERS.map((owner) => /* @__PURE__ */ jsx18(
2510
+ FilterChip,
2511
+ {
2512
+ label: ownerLabel[owner],
2513
+ active: card.owner === owner,
2514
+ onClick: () => patch({ owner })
2515
+ },
2516
+ owner
2517
+ )) }) }),
2518
+ /* @__PURE__ */ jsx18(DetailSection, { title: ui.orderTitle, children: /* @__PURE__ */ jsxs16("div", { className: "pt-panel-row", children: [
2519
+ /* @__PURE__ */ jsx18(FilterChip, { label: `\u26A1 ${ui.orderOn}`, active: card.order, onClick: () => patch({ order: true }) }),
2520
+ /* @__PURE__ */ jsx18(FilterChip, { label: ui.orderOff, active: !card.order, onClick: () => patch({ order: false }) })
2521
+ ] }) }),
2522
+ /* @__PURE__ */ jsxs16(DetailSection, { title: ui.intentTitle, subtitle: ui.intentHint, children: [
2523
+ /* @__PURE__ */ jsxs16("div", { className: "pt-panel-row", children: [
2524
+ /* @__PURE__ */ jsx18(FilterChip, { label: ui.intentNone, active: card.intent === null, onClick: () => patch({ intent: null }) }),
2525
+ INTENTS.map((intent) => /* @__PURE__ */ jsx18(
2526
+ FilterChip,
2527
+ {
2528
+ label: INTENT_COPY[intent].label[lang],
2529
+ active: card.intent === intent,
2530
+ onClick: () => patch({ intent })
2531
+ },
2532
+ intent
2533
+ ))
2534
+ ] }),
2535
+ card.intent && /* @__PURE__ */ jsx18("p", { className: "pt-panel-text pt-meaning", children: INTENT_COPY[card.intent].meaning[lang] })
2536
+ ] }),
2537
+ /* @__PURE__ */ jsxs16(DetailSection, { title: ui.checklist, children: [
2538
+ card.checks.length > 0 && /* @__PURE__ */ jsx18("ul", { className: "pt-checks", children: card.checks.map((check) => /* @__PURE__ */ jsxs16("li", { children: [
2539
+ /* @__PURE__ */ jsxs16("label", { children: [
2540
+ /* @__PURE__ */ jsx18(
2541
+ "input",
2542
+ {
2543
+ type: "checkbox",
2544
+ checked: check.done,
2545
+ onChange: (event) => patch({
2546
+ checks: card.checks.map(
2547
+ (item) => item.id === check.id ? { ...item, done: event.target.checked } : item
2548
+ )
2549
+ })
2550
+ }
2551
+ ),
2552
+ /* @__PURE__ */ jsx18("span", { className: check.done ? "is-done" : void 0, children: text(check.text, lang) })
2553
+ ] }),
2554
+ /* @__PURE__ */ jsx18(
2555
+ "button",
2556
+ {
2557
+ type: "button",
2558
+ className: "pt-remove",
2559
+ "aria-label": `${ui.deleteCard}: ${text(check.text, lang)}`,
2560
+ onClick: () => patch({ checks: card.checks.filter((item) => item.id !== check.id) }),
2561
+ children: "\xD7"
2562
+ }
2563
+ )
2564
+ ] }, check.id)) }),
2565
+ /* @__PURE__ */ jsxs16("div", { className: "pt-compose", children: [
2566
+ /* @__PURE__ */ jsx18(
2567
+ "input",
2568
+ {
2569
+ className: "pt-input",
2570
+ value: step,
2571
+ placeholder: ui.addStepPlaceholder,
2572
+ onChange: (event) => setStep(event.target.value),
2573
+ onKeyDown: (event) => {
2574
+ if (event.key === "Enter") {
2575
+ event.preventDefault();
2576
+ addStep();
2577
+ }
2578
+ }
2579
+ }
2580
+ ),
2581
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "pt-btn pt-btn--sm pt-btn--primary", onClick: addStep, children: ui.addStep })
2582
+ ] })
2583
+ ] }),
2584
+ /* @__PURE__ */ jsxs16(DetailSection, { title: ui.notes, children: [
2585
+ card.notes.map((entry) => /* @__PURE__ */ jsxs16("div", { className: "pt-note", children: [
2586
+ /* @__PURE__ */ jsxs16("p", { className: "pt-note__meta", children: [
2587
+ entry.by === "claude" ? "Claude" : ui.ownerYou,
2588
+ " \xB7 ",
2589
+ entry.date
2590
+ ] }),
2591
+ /* @__PURE__ */ jsx18("p", { className: "pt-panel-text", children: text(entry.text, lang) })
2592
+ ] }, entry.id)),
2593
+ /* @__PURE__ */ jsx18(
2594
+ "textarea",
2595
+ {
2596
+ className: "pt-textarea",
2597
+ rows: 3,
2598
+ value: note,
2599
+ placeholder: ui.notePlaceholder,
2600
+ onChange: (event) => setNote(event.target.value)
2601
+ }
2602
+ ),
2603
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "pt-btn pt-btn--sm pt-btn--primary", onClick: addNote, children: ui.leaveNote })
2604
+ ] }),
2605
+ /* @__PURE__ */ jsxs16(DetailSection, { title: ui.columnTitle, children: [
2606
+ /* @__PURE__ */ jsx18("div", { className: "pt-panel-row", children: state.columns.map((column) => /* @__PURE__ */ jsx18(
2607
+ FilterChip,
2608
+ {
2609
+ label: text(column.title, lang),
2610
+ active: card.column === column.id,
2611
+ onClick: () => onChange(
2612
+ stampForColumn({ ...card, column: column.id, updatedAt: today() }, state.columns)
2613
+ )
2614
+ },
2615
+ column.id
2616
+ )) }),
2617
+ (card.startedAt || card.completedAt) && /* @__PURE__ */ jsxs16("p", { className: "pt-panel-text pt-stamps", children: [
2618
+ card.startedAt && `${ui.started}: ${card.startedAt}`,
2619
+ card.startedAt && card.completedAt && " \xB7 ",
2620
+ card.completedAt && `${ui.completed}: ${card.completedAt}`
2621
+ ] }),
2622
+ claimedWithoutStarting(card) && /* @__PURE__ */ jsx18("p", { className: "pt-warn", children: ui.neverStarted })
2623
+ ] }),
2624
+ /* @__PURE__ */ jsxs16("div", { className: "pt-sheet-actions", children: [
2625
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "pt-btn pt-btn--sm", onClick: onClose, children: ui.close }),
2626
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "pt-btn pt-btn--sm pt-danger", onClick: () => onDelete(card.id), children: ui.deleteCard })
2627
+ ] })
2628
+ ] });
2629
+ }
2630
+
2631
+ // src/panel/board.ts
2632
+ function applyMove(cards, move, visible) {
2633
+ const moving = cards.find((card) => card.id === move.cardId);
2634
+ if (!moving) return cards;
2635
+ const rest = cards.filter((card) => card.id !== move.cardId);
2636
+ const visibleTarget = visible.filter((card) => card.column === move.toColumnId && card.id !== move.cardId);
2637
+ const anchor = visibleTarget[move.toIndex];
2638
+ const moved = moving.column === move.toColumnId ? moving : { ...moving, column: move.toColumnId, updatedAt: today() };
2639
+ if (!anchor) {
2640
+ const lastIndex = rest.reduce(
2641
+ (found, card, index) => card.column === move.toColumnId ? index : found,
2642
+ -1
2643
+ );
2644
+ const at2 = lastIndex === -1 ? rest.length : lastIndex + 1;
2645
+ return [...rest.slice(0, at2), moved, ...rest.slice(at2)];
2646
+ }
2647
+ const at = rest.findIndex((card) => card.id === anchor.id);
2648
+ return [...rest.slice(0, at), moved, ...rest.slice(at)];
2649
+ }
2650
+
2651
+ // src/panel/PanelApp.tsx
2652
+ import { Fragment as Fragment5, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
2653
+ var IMPORTED = "imported";
2654
+ var PRIORITY_TONE = {
2655
+ high: "danger",
2656
+ medium: "warning",
2657
+ low: "neutral"
2658
+ };
2659
+ function PanelApp({
2660
+ state: initialState,
2661
+ themes = PRESET_PANEL_THEMES,
2662
+ defaultTheme,
2663
+ brand,
2664
+ decoration,
2665
+ className,
2666
+ footer,
2667
+ links,
2668
+ capabilities,
2669
+ extras,
2670
+ showLanguage,
2671
+ showHistory,
2672
+ saveEndpoint,
2673
+ marketplaceEndpoint,
2674
+ marketplaceReport,
2675
+ fingerprint
2676
+ }) {
2677
+ const [themeName, setThemeName] = useState9(() => {
2678
+ const saved = initialState.preferences?.theme;
2679
+ if (saved === IMPORTED && initialState.importedTheme) return IMPORTED;
2680
+ return saved && saved in themes ? saved : defaultTheme ?? Object.keys(themes)[0];
2681
+ });
2682
+ const [lang, setLang] = useState9(
2683
+ () => initialState.preferences?.lang ?? (showLanguage ? readStoredLang() : "en")
2684
+ );
2685
+ const [state, setState] = useState9(initialState);
2686
+ const [filter, setFilter] = useState9("all");
2687
+ const [openCardId, setOpenCardId] = useState9(null);
2688
+ const [settingsOpen, setSettingsOpen] = useState9(false);
2689
+ const [helpOpen, setHelpOpen] = useState9(false);
2690
+ const [setup, setSetup] = useState9(null);
2691
+ const [signOut, setSignOut] = useState9("idle");
2692
+ const [view, setView] = useState9("board");
2693
+ const [market, setMarket] = useState9(marketplaceReport ?? null);
2694
+ const [marketLoading, setMarketLoading] = useState9(false);
2695
+ const [marketFailure, setMarketFailure] = useState9(null);
2696
+ const [marketAsked, setMarketAsked] = useState9(false);
2697
+ const [noticeVisible, setNoticeVisible] = useState9(Boolean(initialState.notice));
2698
+ const [savedSnapshot, setSavedSnapshot] = useState9(() => JSON.stringify(initialState));
2699
+ const [save, setSave] = useState9({ status: "idle" });
2700
+ const imported = themeName === IMPORTED ? state.importedTheme : void 0;
2701
+ const preset = themeName === IMPORTED ? "nightCity" : themeName;
2702
+ const chosen = themes[preset] ?? Object.values(themes)[0];
2703
+ const theme = imported?.tokens ?? chosen.tokens;
2704
+ const form = imported?.form ?? chosen.form;
2705
+ const ui = UI[lang];
2706
+ const t = useCallback5((value) => text(value, lang), [lang]);
2707
+ const dirty = JSON.stringify(state) !== savedSnapshot;
2708
+ const hasMarketplace = Boolean(marketplaceEndpoint || marketplaceReport);
2709
+ const requirements = useMemo5(() => {
2710
+ const state_ = (name) => {
2711
+ if (!setup || setup.unknown) return "unknown";
2712
+ return setup.missing.includes(name) ? "missing" : "ok";
2713
+ };
2714
+ return [
2715
+ {
2716
+ id: "password",
2717
+ label: ui.needPassword,
2718
+ status: "ok",
2719
+ variable: "ADMIN_PASSWORD",
2720
+ enables: ui.needPasswordEnables
2721
+ },
2722
+ {
2723
+ id: "token",
2724
+ label: ui.needToken,
2725
+ status: state_("GH_TOKEN"),
2726
+ variable: "GH_TOKEN",
2727
+ enables: ui.needTokenEnables,
2728
+ fix: /* @__PURE__ */ jsxs17("ol", { children: [
2729
+ /* @__PURE__ */ jsx19("li", { children: ui.fixTokenCreate }),
2730
+ /* @__PURE__ */ jsx19("li", { children: ui.fixTokenScope }),
2731
+ /* @__PURE__ */ jsx19("li", { children: ui.fixTokenPaste }),
2732
+ /* @__PURE__ */ jsx19("li", { children: ui.fixTokenRedeploy })
2733
+ ] })
2734
+ },
2735
+ {
2736
+ id: "repo",
2737
+ label: ui.needRepo,
2738
+ status: state_("PANEL_REPO"),
2739
+ variable: "PANEL_REPO",
2740
+ enables: ui.needRepoEnables,
2741
+ fix: /* @__PURE__ */ jsx19("p", { children: ui.fixRepo })
2742
+ }
2743
+ ];
2744
+ }, [setup, ui]);
2745
+ useEffect9(() => {
2746
+ document.documentElement.lang = lang;
2747
+ if (showLanguage) storeLang(lang);
2748
+ }, [lang, showLanguage]);
2749
+ useEffect9(() => {
2750
+ if (!saveEndpoint || !dirty) return;
2751
+ function warn(event) {
2752
+ event.preventDefault();
2753
+ event.returnValue = "";
2754
+ }
2755
+ window.addEventListener("beforeunload", warn);
2756
+ return () => window.removeEventListener("beforeunload", warn);
2757
+ }, [dirty, saveEndpoint]);
2758
+ const counts = useMemo5(() => countCards(state), [state]);
2759
+ const doneColumns = useMemo5(
2760
+ () => new Set(state.columns.filter((column) => column.done).map((column) => column.id)),
2761
+ [state.columns]
2762
+ );
2763
+ const matches = useCallback5(
2764
+ (card) => {
2765
+ if (filter === "all") return true;
2766
+ if (filter === "orders") return card.order;
2767
+ if (filter === "yours") return card.owner === "you";
2768
+ if (filter === "risk") return card.risk === "high";
2769
+ return card.area === filter.slice("area:".length);
2770
+ },
2771
+ [filter]
2772
+ );
2773
+ const visibleCards = useMemo5(() => state.cards.filter(matches), [state.cards, matches]);
2774
+ const boardColumns = useMemo5(
2775
+ () => state.columns.map((column) => ({
2776
+ id: column.id,
2777
+ title: t(column.title),
2778
+ hint: column.hint ? t(column.hint) : void 0,
2779
+ cards: visibleCards.filter((card) => card.column === column.id)
2780
+ })),
2781
+ [state.columns, visibleCards, t]
2782
+ );
2783
+ const notifications = useMemo5(() => {
2784
+ const items = [];
2785
+ const waitingColumn = state.columns.find((column) => column.id === "yours");
2786
+ for (const card of state.cards) {
2787
+ if (card.column !== "yours") continue;
2788
+ items.push({
2789
+ id: `card:${card.id}:yours`,
2790
+ tone: "attention",
2791
+ title: t(card.title),
2792
+ detail: waitingColumn ? t(waitingColumn.title) : ui.notifWaiting,
2793
+ at: card.updatedAt,
2794
+ onOpen: () => setOpenCardId(card.id)
2795
+ });
2796
+ }
2797
+ if (state.importedTheme?.refreshRequested) {
2798
+ items.push({
2799
+ id: `theme-refresh:${state.importedTheme.refreshRequested}`,
2800
+ tone: "attention",
2801
+ title: ui.notifThemeRefresh,
2802
+ detail: t(state.importedTheme.label),
2803
+ at: state.importedTheme.refreshRequested,
2804
+ onOpen: () => setSettingsOpen(true)
2805
+ });
2806
+ }
2807
+ if (state.analysis) {
2808
+ for (const { section, choice } of undecided(state.analysis)) {
2809
+ items.push({
2810
+ id: `choice:${choice.id}`,
2811
+ tone: "attention",
2812
+ title: t(choice.question),
2813
+ detail: `${ui.analysisSections[section]} \xB7 ${ui.choiceOpen}`,
2814
+ onOpen: () => setView("analysis")
2815
+ });
2816
+ }
2817
+ }
2818
+ for (const suggestion of state.analysis?.suggestions ?? []) {
2819
+ if (suggestion.status !== "new") continue;
2820
+ items.push({
2821
+ id: `suggestion:${suggestion.id}`,
2822
+ tone: "info",
2823
+ title: t(suggestion.text),
2824
+ detail: ui.notifSuggestion(suggestion.area),
2825
+ at: suggestion.date,
2826
+ onOpen: () => setView("analysis")
2827
+ });
2828
+ }
2829
+ for (const run of [...state.runs ?? []].reverse().slice(0, 8)) {
2830
+ items.push({
2831
+ id: `run:${run.id}`,
2832
+ tone: "info",
2833
+ title: ui.notifRun(run.by === "claude" ? "Claude" : run.by),
2834
+ detail: t(run.summary),
2835
+ at: run.date
2836
+ });
2837
+ }
2838
+ return items;
2839
+ }, [state.cards, state.columns, state.analysis, state.runs, state.importedTheme, t, ui]);
2840
+ const areaCounts = useMemo5(() => {
2841
+ const totals = {};
2842
+ for (const area of state.areas) totals[area.id] = 0;
2843
+ for (const card of state.cards) {
2844
+ if (doneColumns.has(card.column)) continue;
2845
+ totals[card.area] = (totals[card.area] ?? 0) + 1;
2846
+ }
2847
+ return totals;
2848
+ }, [state.cards, state.areas, doneColumns]);
2849
+ const openCard = useMemo5(
2850
+ () => state.cards.find((card) => card.id === openCardId) ?? null,
2851
+ [state.cards, openCardId]
2852
+ );
2853
+ const updateCard = useCallback5((next) => {
2854
+ setState((current) => {
2855
+ const previous = current.cards.find((card) => card.id === next.id);
2856
+ const stamped = previous && previous.column !== next.column ? stampForColumn(next, current.columns) : next;
2857
+ return {
2858
+ ...current,
2859
+ updatedAt: today(),
2860
+ cards: current.cards.map((card) => card.id === next.id ? stamped : card)
2861
+ };
2862
+ });
2863
+ }, []);
2864
+ const deleteCard = useCallback5((id) => {
2865
+ setState((current) => ({ ...current, updatedAt: today(), cards: current.cards.filter((card) => card.id !== id) }));
2866
+ setOpenCardId(null);
2867
+ }, []);
2868
+ const addCard = useCallback5(
2869
+ (columnId) => {
2870
+ const card = emptyCard(columnId, state.areas[0]?.id ?? "general");
2871
+ setState((current) => ({ ...current, updatedAt: today(), cards: [...current.cards, card] }));
2872
+ setOpenCardId(card.id);
2873
+ },
2874
+ [state.areas]
2875
+ );
2876
+ const moveCard = useCallback5(
2877
+ (move) => {
2878
+ setState((current) => {
2879
+ const moved = applyMove(current.cards, move, current.cards.filter(matches));
2880
+ return {
2881
+ ...current,
2882
+ updatedAt: today(),
2883
+ cards: moved.map(
2884
+ (card) => card.id === move.cardId && card.column !== move.fromColumnId ? stampForColumn(card, current.columns) : card
2885
+ )
2886
+ };
2887
+ });
2888
+ },
2889
+ [matches]
2890
+ );
2891
+ const chooseStrategy = useCallback5((id) => {
2892
+ setState(
2893
+ (current) => current.analysis ? {
2894
+ ...current,
2895
+ updatedAt: today(),
2896
+ // Tapping the chosen path again clears it: a decision can be reopened.
2897
+ analysis: { ...current.analysis, chosen: current.analysis.chosen === id ? null : id }
2898
+ } : current
2899
+ );
2900
+ }, []);
2901
+ const setPreference = useCallback5((key, value) => {
2902
+ setState((current) => ({
2903
+ ...current,
2904
+ updatedAt: today(),
2905
+ preferences: { ...current.preferences, [key]: value }
2906
+ }));
2907
+ }, []);
2908
+ const chooseTheme = useCallback5(
2909
+ (name) => {
2910
+ setThemeName(name);
2911
+ setPreference("theme", name);
2912
+ },
2913
+ [setPreference]
2914
+ );
2915
+ const chooseLang = useCallback5(
2916
+ (code) => {
2917
+ setLang(code);
2918
+ storeLang(code);
2919
+ setPreference("lang", code);
2920
+ },
2921
+ [setPreference]
2922
+ );
2923
+ const markSeen = useCallback5(
2924
+ (ids) => {
2925
+ setState((current) => {
2926
+ const next = [.../* @__PURE__ */ new Set([...current.preferences?.seen ?? [], ...ids])].slice(-400);
2927
+ return { ...current, updatedAt: today(), preferences: { ...current.preferences, seen: next } };
2928
+ });
2929
+ },
2930
+ []
2931
+ );
2932
+ const dismissGuide = useCallback5((id) => {
2933
+ setState((current) => {
2934
+ const next = [.../* @__PURE__ */ new Set([...current.preferences?.guideDismissed ?? [], id])];
2935
+ return { ...current, updatedAt: today(), preferences: { ...current.preferences, guideDismissed: next } };
2936
+ });
2937
+ }, []);
2938
+ const requestThemeRefresh = useCallback5(() => {
2939
+ setState(
2940
+ (current) => current.importedTheme ? {
2941
+ ...current,
2942
+ updatedAt: today(),
2943
+ importedTheme: { ...current.importedTheme, refreshRequested: today() }
2944
+ } : current
2945
+ );
2946
+ }, []);
2947
+ const decideChoice = useCallback5((section, choiceId, optionId) => {
2948
+ setState((current) => {
2949
+ const analysis = current.analysis;
2950
+ const choices = analysis?.choices?.[section];
2951
+ if (!analysis || !choices) return current;
2952
+ return {
2953
+ ...current,
2954
+ updatedAt: today(),
2955
+ analysis: {
2956
+ ...analysis,
2957
+ choices: {
2958
+ ...analysis.choices,
2959
+ [section]: choices.map(
2960
+ (choice) => choice.id === choiceId ? optionId === null ? { ...choice, value: null, decidedAt: void 0, decidedBy: void 0 } : { ...choice, value: optionId, decidedAt: today(), decidedBy: "you" } : choice
2961
+ )
2962
+ }
2963
+ }
2964
+ };
2965
+ });
2966
+ }, []);
2967
+ const setSuggestionStatus = useCallback5((id, status2) => {
2968
+ setState(
2969
+ (current) => current.analysis ? {
2970
+ ...current,
2971
+ updatedAt: today(),
2972
+ analysis: {
2973
+ ...current.analysis,
2974
+ suggestions: current.analysis.suggestions.map(
2975
+ (suggestion) => suggestion.id === id ? { ...suggestion, status: status2 } : suggestion
2976
+ )
2977
+ }
2978
+ } : current
2979
+ );
2980
+ }, []);
2981
+ const loadMarketplace = useCallback5(
2982
+ (fresh) => {
2983
+ if (!marketplaceEndpoint) return;
2984
+ setMarketLoading(true);
2985
+ setMarketFailure(null);
2986
+ fetch(`${marketplaceEndpoint}${fresh ? "?fresh=1" : ""}`, { headers: { accept: "application/json" } }).then((response) => response.ok ? response.json() : Promise.reject(new Error(String(response.status)))).then((body) => setMarket(body)).catch((error) => setMarketFailure(String(error))).finally(() => setMarketLoading(false));
2987
+ },
2988
+ [marketplaceEndpoint]
2989
+ );
2990
+ useEffect9(() => {
2991
+ if (view !== "marketplace" || marketAsked || !marketplaceEndpoint) return;
2992
+ setMarketAsked(true);
2993
+ loadMarketplace(false);
2994
+ }, [view, marketAsked, marketplaceEndpoint, loadMarketplace]);
2995
+ useEffect9(() => {
2996
+ if (!helpOpen || !saveEndpoint || setup) return;
2997
+ let cancelled = false;
2998
+ fetch(saveEndpoint, { headers: { accept: "application/json" } }).then((response) => response.ok ? response.json() : Promise.reject(new Error(String(response.status)))).then((body) => {
2999
+ if (!cancelled) setSetup(body);
3000
+ }).catch(() => {
3001
+ if (!cancelled) setSetup({ ready: false, missing: [], repo: null, branch: "", file: "", unknown: true });
3002
+ });
3003
+ return () => {
3004
+ cancelled = true;
3005
+ };
3006
+ }, [helpOpen, saveEndpoint, setup]);
3007
+ const endSession = useCallback5(async () => {
3008
+ setSignOut("working");
3009
+ try {
3010
+ const response = await fetch("/api/logout", { method: "POST" });
3011
+ if (!response.ok) throw new Error(String(response.status));
3012
+ window.location.href = "/login";
3013
+ } catch {
3014
+ setSignOut("failed");
3015
+ }
3016
+ }, []);
3017
+ const saveBoard = useCallback5(async () => {
3018
+ if (!saveEndpoint) return;
3019
+ setSave({ status: "saving" });
3020
+ const next = { ...state, updatedAt: today() };
3021
+ try {
3022
+ const response = await fetch(saveEndpoint, {
3023
+ method: "POST",
3024
+ headers: { "content-type": "application/json" },
3025
+ credentials: "same-origin",
3026
+ body: JSON.stringify({ state: next, message: "chore(panel): update the board from the panel" })
3027
+ });
3028
+ const data = await response.json().catch(() => ({}));
3029
+ if (!response.ok) {
3030
+ const conflict = response.status === 409;
3031
+ throw Object.assign(new Error(data.error || `HTTP ${response.status}`), { conflict });
3032
+ }
3033
+ setSavedSnapshot(JSON.stringify(state));
3034
+ setSave({
3035
+ status: "saved",
3036
+ at: (/* @__PURE__ */ new Date()).toLocaleTimeString(lang === "es" ? "es-ES" : "en-GB", {
3037
+ hour: "2-digit",
3038
+ minute: "2-digit"
3039
+ }),
3040
+ url: data.url
3041
+ });
3042
+ } catch (error) {
3043
+ const conflict = Boolean(error.conflict);
3044
+ setSave({
3045
+ status: "error",
3046
+ conflict,
3047
+ message: conflict ? ui.saveConflict : error instanceof Error ? error.message : String(error)
3048
+ });
3049
+ }
3050
+ }, [lang, saveEndpoint, state, ui.saveConflict]);
3051
+ function cardTags(card) {
3052
+ const tags = [];
3053
+ if (card.order) tags.push({ label: `\u26A1 ${ui.orderOn}`, tone: "warning", emphasize: true });
3054
+ if (card.intent) tags.push({ label: INTENT_COPY[card.intent].label[lang], tone: "neutral" });
3055
+ if (card.risk === "high") tags.push({ label: ui.statHighRisk, tone: "danger" });
3056
+ if (card.owner === "you") tags.push({ label: ui.filterYours, tone: "warning" });
3057
+ const area = state.areas.find((candidate) => candidate.id === card.area);
3058
+ if (area) tags.push({ label: t(area.label), tone: "neutral" });
3059
+ if (card.notes.length > 0) tags.push({ label: `${card.notes.length} \u270E`, tone: "neutral" });
3060
+ if (claimedWithoutStarting(card)) tags.push({ label: "?", tone: "danger", emphasize: true });
3061
+ return tags;
3062
+ }
3063
+ const status = [
3064
+ t(state.environment),
3065
+ state.updatedAt,
3066
+ save.status === "saved" ? ui.savedAt(save.at) : dirty ? ui.unsaved : ""
3067
+ ].filter(Boolean).join(" \xB7 ");
3068
+ return /* @__PURE__ */ jsxs17(DashboardThemeProvider, { tokens: theme, form, className, children: [
3069
+ decoration,
3070
+ /* @__PURE__ */ jsx19(
3071
+ DashboardHeader,
3072
+ {
3073
+ wordmark: (
3074
+ /* A board belongs to a project, so its name is what goes here. A
3075
+ caller with a mark of its own passes `brand`; drawing one here
3076
+ would be the kit deciding what somebody else's panel is called. */
3077
+ brand ?? /* @__PURE__ */ jsx19("span", { className: "pt-panel-brand", children: state.project })
3078
+ ),
3079
+ status,
3080
+ controls: /* @__PURE__ */ jsxs17(Fragment5, { children: [
3081
+ (initialState.analysis || hasMarketplace) && /* @__PURE__ */ jsxs17("span", { className: "pt-views", children: [
3082
+ /* @__PURE__ */ jsx19(
3083
+ "button",
3084
+ {
3085
+ type: "button",
3086
+ className: `pt-lang${view === "board" ? " is-on" : ""}`,
3087
+ "aria-pressed": view === "board",
3088
+ onClick: () => setView("board"),
3089
+ children: ui.board
3090
+ }
3091
+ ),
3092
+ initialState.analysis && /* @__PURE__ */ jsx19(
3093
+ "button",
3094
+ {
3095
+ type: "button",
3096
+ className: `pt-lang${view === "analysis" ? " is-on" : ""}`,
3097
+ "aria-pressed": view === "analysis",
3098
+ onClick: () => setView("analysis"),
3099
+ children: ui.analysis
3100
+ }
3101
+ ),
3102
+ hasMarketplace && /* @__PURE__ */ jsx19(
3103
+ "button",
3104
+ {
3105
+ type: "button",
3106
+ className: `pt-lang${view === "marketplace" ? " is-on" : ""}`,
3107
+ "aria-pressed": view === "marketplace",
3108
+ onClick: () => setView("marketplace"),
3109
+ children: ui.marketplace
3110
+ }
3111
+ )
3112
+ ] }),
3113
+ showLanguage && LANGS.map((code) => /* @__PURE__ */ jsx19(
3114
+ "button",
3115
+ {
3116
+ type: "button",
3117
+ className: `pt-lang${lang === code ? " is-on" : ""}`,
3118
+ "aria-pressed": lang === code,
3119
+ onClick: () => chooseLang(code),
3120
+ children: code.toUpperCase()
3121
+ },
3122
+ code
3123
+ )),
3124
+ /* @__PURE__ */ jsx19(
3125
+ NotificationBell,
3126
+ {
3127
+ notifications,
3128
+ seen: state.preferences?.seen ?? [],
3129
+ onSeen: markSeen,
3130
+ labels: {
3131
+ title: ui.notifications,
3132
+ empty: ui.nothingNew,
3133
+ markAll: ui.markAllSeen,
3134
+ unread: ui.unreadCount
3135
+ }
3136
+ }
3137
+ ),
3138
+ saveEndpoint && /* @__PURE__ */ jsx19("button", { type: "button", className: "pt-lang", onClick: () => setHelpOpen(true), children: ui.help }),
3139
+ saveEndpoint && (dirty || save.status === "saving") && /* @__PURE__ */ jsx19(
3140
+ "button",
3141
+ {
3142
+ type: "button",
3143
+ className: "pt-btn pt-btn--sm pt-btn--primary",
3144
+ onClick: saveBoard,
3145
+ disabled: save.status === "saving",
3146
+ children: save.status === "saving" ? ui.saving : ui.save
3147
+ }
3148
+ )
3149
+ ] }),
3150
+ linkLabel: ui.settings,
3151
+ onLinkClick: () => setSettingsOpen(true)
3152
+ }
3153
+ ),
3154
+ state.notice && /* @__PURE__ */ jsx19(
3155
+ AlertBanner,
3156
+ {
3157
+ visible: noticeVisible,
3158
+ message: t(state.notice),
3159
+ actionLabel: ui.dismiss,
3160
+ onAction: () => setNoticeVisible(false)
3161
+ }
3162
+ ),
3163
+ save.status === "error" && /* @__PURE__ */ jsx19(AlertBanner, { visible: true, message: `${ui.saveFailed}: ${save.message}` }),
3164
+ view === "board" && !saveEndpoint && /* @__PURE__ */ jsx19(
3165
+ GuideTour,
3166
+ {
3167
+ dismissed: state.preferences?.guideDismissed,
3168
+ onDismiss: dismissGuide,
3169
+ notes: [
3170
+ {
3171
+ id: "demo-welcome",
3172
+ title: ui.demoTourWelcomeTitle,
3173
+ children: /* @__PURE__ */ jsx19("p", { children: ui.demoTourWelcomeBody }),
3174
+ dismissLabel: ui.gotIt
3175
+ },
3176
+ {
3177
+ id: "demo-board",
3178
+ title: ui.demoTourBoardTitle,
3179
+ children: /* @__PURE__ */ jsx19("p", { children: ui.demoTourBoardBody }),
3180
+ dismissLabel: ui.gotIt
3181
+ },
3182
+ {
3183
+ id: "demo-theme",
3184
+ title: ui.demoTourThemeTitle,
3185
+ children: /* @__PURE__ */ jsx19("p", { children: ui.demoTourThemeBody }),
3186
+ dismissLabel: ui.gotIt,
3187
+ action: { label: ui.settings, onClick: () => setSettingsOpen(true) }
3188
+ },
3189
+ {
3190
+ id: "demo-keep",
3191
+ title: ui.demoTourKeepTitle,
3192
+ children: /* @__PURE__ */ jsx19("p", { children: ui.demoTourKeepBody }),
3193
+ dismissLabel: ui.gotIt
3194
+ }
3195
+ ]
3196
+ }
3197
+ ),
3198
+ view === "board" && saveEndpoint && /* @__PURE__ */ jsx19(
3199
+ GuideTour,
3200
+ {
3201
+ dismissed: state.preferences?.guideDismissed,
3202
+ onDismiss: dismissGuide,
3203
+ notes: [
3204
+ {
3205
+ id: "welcome",
3206
+ title: ui.tourWelcomeTitle,
3207
+ children: /* @__PURE__ */ jsx19("p", { children: ui.tourWelcomeBody }),
3208
+ dismissLabel: ui.gotIt
3209
+ },
3210
+ {
3211
+ id: "board",
3212
+ title: ui.tourBoardTitle,
3213
+ children: /* @__PURE__ */ jsx19("p", { children: ui.tourBoardBody }),
3214
+ dismissLabel: ui.gotIt
3215
+ },
3216
+ {
3217
+ id: "orders",
3218
+ title: ui.tourOrdersTitle,
3219
+ children: /* @__PURE__ */ jsx19("p", { children: ui.tourOrdersBody }),
3220
+ dismissLabel: ui.gotIt
3221
+ },
3222
+ {
3223
+ id: "save",
3224
+ title: ui.tourSaveTitle,
3225
+ children: /* @__PURE__ */ jsx19("p", { children: ui.tourSaveBody }),
3226
+ dismissLabel: ui.gotIt
3227
+ },
3228
+ {
3229
+ id: "theme",
3230
+ title: ui.tourThemeTitle,
3231
+ children: /* @__PURE__ */ jsx19("p", { children: ui.tourThemeBody }),
3232
+ dismissLabel: ui.gotIt,
3233
+ action: { label: ui.settings, onClick: () => setSettingsOpen(true) }
3234
+ }
3235
+ ]
3236
+ }
3237
+ ),
3238
+ view === "marketplace" && hasMarketplace && /* @__PURE__ */ jsx19(
3239
+ MarketplaceView,
3240
+ {
3241
+ report: market,
3242
+ loading: marketLoading,
3243
+ failure: marketFailure,
3244
+ lang,
3245
+ ui,
3246
+ onRefresh: marketplaceEndpoint ? () => loadMarketplace(true) : void 0
3247
+ }
3248
+ ),
3249
+ view === "analysis" && state.analysis && /* @__PURE__ */ jsx19(
3250
+ AnalysisView,
3251
+ {
3252
+ analysis: state.analysis,
3253
+ cards: state.cards,
3254
+ columns: state.columns,
3255
+ today: today(),
3256
+ lang,
3257
+ ui,
3258
+ onChooseStrategy: chooseStrategy,
3259
+ onSuggestionStatus: setSuggestionStatus,
3260
+ onDecide: decideChoice,
3261
+ onOpenCard: setOpenCardId
3262
+ }
3263
+ ),
3264
+ view === "board" && /* @__PURE__ */ jsxs17(Fragment5, { children: [
3265
+ /* @__PURE__ */ jsxs17("div", { className: "pt-panel-page", children: [
3266
+ /* @__PURE__ */ jsxs17(StatTileGrid, { children: [
3267
+ /* @__PURE__ */ jsx19(StatTile, { label: ui.statOpen, value: counts.open, isZero: counts.open === 0 }),
3268
+ /* @__PURE__ */ jsx19(StatTile, { label: ui.statOrders, value: counts.orders, tone: "warning", isZero: counts.orders === 0 }),
3269
+ /* @__PURE__ */ jsx19(StatTile, { label: ui.statYours, value: counts.yours, tone: "danger", isZero: counts.yours === 0 }),
3270
+ /* @__PURE__ */ jsx19(StatTile, { label: ui.statDecisions, value: counts.decisions, tone: "warning", isZero: counts.decisions === 0 }),
3271
+ /* @__PURE__ */ jsx19(StatTile, { label: ui.statHighRisk, value: counts.highRisk, tone: "danger", isZero: counts.highRisk === 0 }),
3272
+ /* @__PURE__ */ jsx19(StatTile, { label: ui.statDone, value: counts.done, tone: "success", isZero: counts.done === 0 })
3273
+ ] }),
3274
+ /* @__PURE__ */ jsx19(HealthPillRow, { children: state.health.map((pill) => /* @__PURE__ */ jsx19(HealthPill, { label: t(pill.label), value: t(pill.value), status: pill.status }, t(pill.label))) }),
3275
+ /* @__PURE__ */ jsxs17(FilterChipRow, { children: [
3276
+ /* @__PURE__ */ jsx19(FilterChip, { label: ui.all, count: state.cards.length, active: filter === "all", onClick: () => setFilter("all") }),
3277
+ /* @__PURE__ */ jsx19(
3278
+ FilterChip,
3279
+ {
3280
+ label: `\u26A1 ${ui.filterForClaude}`,
3281
+ count: counts.orders,
3282
+ active: filter === "orders",
3283
+ onClick: () => setFilter("orders")
3284
+ }
3285
+ ),
3286
+ /* @__PURE__ */ jsx19(
3287
+ FilterChip,
3288
+ {
3289
+ label: ui.filterYours,
3290
+ count: counts.yours,
3291
+ active: filter === "yours",
3292
+ onClick: () => setFilter("yours")
3293
+ }
3294
+ ),
3295
+ /* @__PURE__ */ jsx19(
3296
+ FilterChip,
3297
+ {
3298
+ label: ui.filterHighRisk,
3299
+ count: counts.highRisk,
3300
+ active: filter === "risk",
3301
+ onClick: () => setFilter("risk")
3302
+ }
3303
+ ),
3304
+ state.areas.map((area) => /* @__PURE__ */ jsx19(
3305
+ FilterChip,
3306
+ {
3307
+ label: t(area.label),
3308
+ count: areaCounts[area.id] ?? 0,
3309
+ active: filter === `area:${area.id}`,
3310
+ onClick: () => setFilter(`area:${area.id}`)
3311
+ },
3312
+ area.id
3313
+ ))
3314
+ ] })
3315
+ ] }),
3316
+ /* @__PURE__ */ jsx19(
3317
+ Board,
3318
+ {
3319
+ columns: boardColumns,
3320
+ onMove: moveCard,
3321
+ onAddCard: addCard,
3322
+ renderCard: (card, index, columnId) => /* @__PURE__ */ jsx19(
3323
+ Card,
3324
+ {
3325
+ cardId: card.id,
3326
+ columnId,
3327
+ index,
3328
+ title: t(card.title) || ui.untitled,
3329
+ priorityColor: theme[PRIORITY_TONE[card.priority]],
3330
+ tags: cardTags(card),
3331
+ progress: checkProgress(card),
3332
+ onClick: () => setOpenCardId(card.id)
3333
+ },
3334
+ card.id
3335
+ )
3336
+ }
3337
+ ),
3338
+ showHistory && /* @__PURE__ */ jsxs17("section", { className: "pt-panel-page pt-panel-section", id: "revisions", children: [
3339
+ /* @__PURE__ */ jsxs17("div", { children: [
3340
+ /* @__PURE__ */ jsx19("p", { className: "pt-eyebrow", children: ui.historyEyebrow }),
3341
+ /* @__PURE__ */ jsx19("h2", { className: "pt-h2", children: ui.historyTitle }),
3342
+ /* @__PURE__ */ jsx19("p", { className: "pt-sub", children: ui.historyIntro })
3343
+ ] }),
3344
+ state.runs.length === 0 ? /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: ui.historyEmpty }) : /* @__PURE__ */ jsx19("ol", { className: "pt-runs", children: [...state.runs].reverse().map((run) => /* @__PURE__ */ jsxs17("li", { className: "pt-run pt-hud", children: [
3345
+ /* @__PURE__ */ jsxs17("p", { className: "pt-run__meta", children: [
3346
+ run.date,
3347
+ " \xB7 ",
3348
+ run.by === "claude" ? "Claude" : ui.ownerYou,
3349
+ run.fingerprint ? ` \xB7 ${run.fingerprint.replace("sha256:", "").slice(0, 12)}` : ""
3350
+ ] }),
3351
+ /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: t(run.summary) }),
3352
+ run.cards.length > 0 && /* @__PURE__ */ jsxs17("p", { className: "pt-run__cards", children: [
3353
+ /* @__PURE__ */ jsxs17("span", { children: [
3354
+ ui.historyTouched,
3355
+ ":"
3356
+ ] }),
3357
+ " ",
3358
+ run.cards.map((id) => {
3359
+ const card = state.cards.find((candidate) => candidate.id === id);
3360
+ return card ? t(card.title) || ui.untitled : id;
3361
+ }).join(" \xB7 ")
3362
+ ] })
3363
+ ] }, run.id)) })
3364
+ ] }),
3365
+ extras,
3366
+ capabilities && capabilities.length > 0 && /* @__PURE__ */ jsxs17("section", { className: "pt-panel-page pt-panel-section", id: "functions", children: [
3367
+ /* @__PURE__ */ jsxs17("div", { children: [
3368
+ /* @__PURE__ */ jsx19("p", { className: "pt-eyebrow", children: ui.functionsEyebrow }),
3369
+ /* @__PURE__ */ jsx19("h2", { className: "pt-h2", children: ui.functionsTitle }),
3370
+ /* @__PURE__ */ jsx19("p", { className: "pt-sub", children: ui.functionsIntro })
3371
+ ] }),
3372
+ /* @__PURE__ */ jsx19("div", { className: "pt-cap-list", children: capabilities.map((capability) => /* @__PURE__ */ jsxs17("article", { className: "pt-cap pt-hud", children: [
3373
+ /* @__PURE__ */ jsx19("h3", { className: "pt-cap__title", children: t(capability.title) }),
3374
+ /* @__PURE__ */ jsx19("p", { className: "pt-cap__summary", children: t(capability.summary) }),
3375
+ /* @__PURE__ */ jsx19("ul", { className: "pt-cap__list", children: capability.points.map((point) => /* @__PURE__ */ jsx19("li", { children: t(point) }, t(point))) }),
3376
+ capability.code && /* @__PURE__ */ jsxs17("div", { className: "pt-cap__code", children: [
3377
+ /* @__PURE__ */ jsx19("p", { className: "pt-cap__caption", children: t(capability.code.caption) }),
3378
+ /* @__PURE__ */ jsx19("pre", { children: /* @__PURE__ */ jsx19("code", { children: capability.code.body }) })
3379
+ ] })
3380
+ ] }, capability.id)) })
3381
+ ] })
3382
+ ] }),
3383
+ /* @__PURE__ */ jsx19(
3384
+ CardSheet,
3385
+ {
3386
+ card: openCard,
3387
+ state,
3388
+ lang,
3389
+ ui,
3390
+ onChange: updateCard,
3391
+ onDelete: deleteCard,
3392
+ onClose: () => setOpenCardId(null)
3393
+ }
3394
+ ),
3395
+ /* @__PURE__ */ jsxs17(DetailSheet, { open: helpOpen, onClose: () => setHelpOpen(false), ariaLabel: ui.helpTitle, children: [
3396
+ /* @__PURE__ */ jsx19(DetailSection, { title: ui.helpTitle, children: !setup ? /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: ui.setupChecking }) : /* @__PURE__ */ jsxs17(Fragment5, { children: [
3397
+ /* @__PURE__ */ jsx19(
3398
+ SetupGuide,
3399
+ {
3400
+ requirements,
3401
+ readyMessage: ui.setupReady,
3402
+ labels: {
3403
+ ok: ui.setupOk,
3404
+ missing: ui.setupMissing,
3405
+ unknown: ui.setupUnknown,
3406
+ whatToDo: ui.setupWhatToDo
3407
+ }
3408
+ }
3409
+ ),
3410
+ setup.repo && /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", style: { marginTop: 12 }, children: ui.setupWriteTarget(setup.repo, setup.branch, setup.file) })
3411
+ ] }) }),
3412
+ /* @__PURE__ */ jsx19(DetailSection, { title: ui.guideSection, children: /* @__PURE__ */ jsx19("div", { className: "pt-panel-row", children: /* @__PURE__ */ jsx19(
3413
+ "button",
3414
+ {
3415
+ type: "button",
3416
+ className: "pt-btn pt-btn--sm",
3417
+ onClick: () => {
3418
+ resetGuide();
3419
+ setState((current) => ({
3420
+ ...current,
3421
+ updatedAt: today(),
3422
+ preferences: { ...current.preferences, guideDismissed: [] }
3423
+ }));
3424
+ setHelpOpen(false);
3425
+ },
3426
+ children: ui.showGuideAgain
3427
+ }
3428
+ ) }) })
3429
+ ] }),
3430
+ /* @__PURE__ */ jsxs17(DetailSheet, { open: settingsOpen, onClose: () => setSettingsOpen(false), ariaLabel: ui.panelSettings, children: [
3431
+ saveEndpoint ? /* @__PURE__ */ jsxs17(DetailSection, { title: ui.saveSection, subtitle: ui.saveSubtitle, children: [
3432
+ /* @__PURE__ */ jsx19("div", { className: "pt-panel-row", children: /* @__PURE__ */ jsx19(
3433
+ "button",
3434
+ {
3435
+ type: "button",
3436
+ className: "pt-btn pt-btn--sm pt-btn--primary",
3437
+ onClick: saveBoard,
3438
+ disabled: !dirty || save.status === "saving",
3439
+ children: save.status === "saving" ? ui.saving : ui.save
3440
+ }
3441
+ ) }),
3442
+ /* @__PURE__ */ jsxs17("p", { className: "pt-panel-text", style: { marginTop: 10 }, children: [
3443
+ save.status === "error" ? `${ui.saveFailed}: ${save.message}` : save.status === "saved" ? ui.savedWithCommit : dirty ? ui.saveHelp : ui.upToDate,
3444
+ save.status === "saved" && save.url && /* @__PURE__ */ jsxs17(Fragment5, { children: [
3445
+ " ",
3446
+ /* @__PURE__ */ jsx19("a", { href: save.url, children: "commit" })
3447
+ ] })
3448
+ ] })
3449
+ ] }) : /* @__PURE__ */ jsx19(DetailSection, { title: ui.saveSection, subtitle: ui.saveSubtitle, children: /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: ui.demoNotice }) }),
3450
+ showLanguage && /* @__PURE__ */ jsx19(DetailSection, { title: ui.language, subtitle: ui.languageSubtitle, children: /* @__PURE__ */ jsx19("div", { className: "pt-panel-row", children: LANGS.map((code) => /* @__PURE__ */ jsx19(
3451
+ FilterChip,
3452
+ {
3453
+ label: LANG_LABELS[code],
3454
+ active: lang === code,
3455
+ onClick: () => chooseLang(code)
3456
+ },
3457
+ code
3458
+ )) }) }),
3459
+ /* @__PURE__ */ jsxs17(DetailSection, { title: ui.theme, subtitle: ui.themeSubtitle, children: [
3460
+ /* @__PURE__ */ jsxs17("div", { className: "pt-panel-row", children: [
3461
+ Object.entries(themes).map(([name, entry]) => /* @__PURE__ */ jsx19(
3462
+ FilterChip,
3463
+ {
3464
+ label: entry.label,
3465
+ active: themeName === name,
3466
+ onClick: () => chooseTheme(name)
3467
+ },
3468
+ name
3469
+ )),
3470
+ state.importedTheme && /* @__PURE__ */ jsx19(
3471
+ FilterChip,
3472
+ {
3473
+ label: t(state.importedTheme.label),
3474
+ active: themeName === IMPORTED,
3475
+ onClick: () => chooseTheme(IMPORTED)
3476
+ }
3477
+ )
3478
+ ] }),
3479
+ /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", style: { marginTop: 10 }, children: themeName === IMPORTED ? t(imported?.from.reasoning) : [chosen.note, ui.themeNote].filter(Boolean).join(". ") }),
3480
+ state.importedTheme && /* @__PURE__ */ jsxs17("div", { className: "pt-cap pt-hud", style: { marginTop: 14 }, children: [
3481
+ /* @__PURE__ */ jsx19("h3", { className: "pt-cap__title", children: ui.importedTheme }),
3482
+ /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: ui.importedFrom(state.importedTheme.importedAt, state.importedTheme.from.files.join(", ")) }),
3483
+ state.importedTheme.from.colours.length > 0 && /* @__PURE__ */ jsx19("p", { className: "pt-tool__tags", children: state.importedTheme.from.colours.map((colour) => /* @__PURE__ */ jsx19("span", { className: "pt-tool__tag", children: colour }, colour)) }),
3484
+ state.importedTheme.refreshRequested ? /* @__PURE__ */ jsx19("p", { className: "pt-warn", children: ui.importedRefreshAsked(state.importedTheme.refreshRequested) }) : /* @__PURE__ */ jsx19("div", { className: "pt-panel-row", style: { marginTop: 10 }, children: /* @__PURE__ */ jsx19("button", { type: "button", className: "pt-btn pt-btn--sm", onClick: requestThemeRefresh, children: ui.importedRefresh }) }),
3485
+ /* @__PURE__ */ jsx19("p", { className: "pt-marketplace__detail", children: ui.importedRefreshHow })
3486
+ ] })
3487
+ ] }),
3488
+ fingerprint && /* @__PURE__ */ jsxs17(DetailSection, { title: ui.fingerprint, subtitle: ui.fingerprintSubtitle, children: [
3489
+ /* @__PURE__ */ jsx19("p", { className: "pt-panel-mono", children: fingerprint.hash }),
3490
+ /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", style: { marginTop: 8 }, children: ui.trackedFiles(fingerprint.fileCount, fingerprint.version) })
3491
+ ] }),
3492
+ saveEndpoint && /* @__PURE__ */ jsxs17(DetailSection, { title: ui.session, subtitle: ui.sessionSubtitle, children: [
3493
+ /* @__PURE__ */ jsx19("div", { className: "pt-panel-row", children: /* @__PURE__ */ jsx19(
3494
+ "button",
3495
+ {
3496
+ type: "button",
3497
+ className: "pt-btn pt-btn--sm",
3498
+ onClick: endSession,
3499
+ disabled: signOut === "working",
3500
+ children: signOut === "working" ? ui.signingOut : ui.signOut
3501
+ }
3502
+ ) }),
3503
+ signOut === "failed" && /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", style: { marginTop: 10 }, children: ui.signOutFailed })
3504
+ ] }),
3505
+ links && links.length > 0 && /* @__PURE__ */ jsx19(DetailSection, { title: ui.links, children: /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: links.map((link, index) => /* @__PURE__ */ jsxs17(React12.Fragment, { children: [
3506
+ index > 0 && " \xB7 ",
3507
+ /* @__PURE__ */ jsx19("a", { href: link.href, children: link.label })
3508
+ ] }, link.href)) }) })
3509
+ ] }),
3510
+ footer
3511
+ ] });
3512
+ }
3513
+
3514
+ // src/panel/capabilities.ts
3515
+ var CAPABILITIES = [
3516
+ {
3517
+ id: "ships-today",
3518
+ title: { en: "What the panel does today", es: "Qu\xE9 hace el panel hoy" },
3519
+ summary: {
3520
+ en: "Everything below is in the package. A project supplies data and colour; nothing else has to be built.",
3521
+ es: "Todo lo de abajo viene en el paquete. Un proyecto pone datos y color; no hay que construir nada m\xE1s."
3522
+ },
3523
+ points: [
3524
+ {
3525
+ en: "Page frame: header with wordmark and status, full-width alert banner, responsive content column.",
3526
+ es: "Marco de p\xE1gina: cabecera con wordmark y estado, banner de aviso a ancho completo y columna de contenido responsive."
3527
+ },
3528
+ {
3529
+ en: "Board: columns with horizontal scroll-snap, cards dragged from a grip with pointer events, so it works on touch. Edge autoscroll runs on requestAnimationFrame and keeps moving while the finger is still.",
3530
+ es: "Tablero: columnas con scroll-snap horizontal y tarjetas que se arrastran desde el grip con Pointer Events, as\xED funciona en t\xE1ctil. El autoscroll de bordes corre en requestAnimationFrame y sigue avanzando con el dedo quieto."
3531
+ },
3532
+ {
3533
+ en: "Cards: priority stripe, tags with semantic tone, progress counter and free children for anything project-specific.",
3534
+ es: "Tarjetas: franja de prioridad, etiquetas con tono sem\xE1ntico, contador de progreso y children libres para lo propio de cada proyecto."
3535
+ },
3536
+ {
3537
+ en: "Detail sheet: a bottom sheet under 760px and a centred dialog above it, from the same markup. Closes on Escape and on backdrop click.",
3538
+ es: "Sheet de detalle: sube desde abajo por debajo de 760px y es un di\xE1logo centrado por encima, con el mismo markup. Se cierra con Escape y al tocar el fondo."
3539
+ },
3540
+ {
3541
+ en: "Metrics: stat tiles with a calm state for zero, health pills with four statuses, filter chips with counters.",
3542
+ es: "M\xE9tricas: tiles con estado calmo cuando el valor es cero, pills de salud con cuatro estados y chips de filtro con contador."
3543
+ },
3544
+ {
3545
+ en: "Theming: every colour arrives as DashboardThemeTokens and leaves as --pt-color-* variables. The kit ships no default colour on purpose, so a forgotten token is visible immediately.",
3546
+ es: "Theming: todo el color entra como DashboardThemeTokens y sale como variables --pt-color-*. El kit no trae colores por defecto a prop\xF3sito, as\xED un token olvidado se nota enseguida."
3547
+ },
3548
+ {
3549
+ en: "moveCardInColumns: the single place where a card changes column or index.",
3550
+ es: "moveCardInColumns: el \xFAnico lugar donde una tarjeta cambia de columna o de \xEDndice."
3551
+ }
3552
+ ]
3553
+ },
3554
+ {
3555
+ id: "install",
3556
+ title: { en: "Add it to a project with Claude", es: "Sumarlo a un proyecto con Claude" },
3557
+ summary: {
3558
+ en: "One instruction. INSTALL.md is written for the agent, not for a human reader.",
3559
+ es: "Una sola instrucci\xF3n. INSTALL.md est\xE1 escrito para el agente, no para que lo lea una persona."
3560
+ },
3561
+ points: [
3562
+ {
3563
+ en: "Claude reads the project palette before writing anything, and maps it to the token roles \u2014 it never invents a new palette when the project already has one.",
3564
+ es: "Claude busca la paleta del proyecto antes de escribir nada y la mapea a los roles de token: nunca inventa una paleta nueva si ya existe una."
3565
+ },
3566
+ {
3567
+ en: "If the project has no identity yet, it picks one of the four base themes and says which one it chose.",
3568
+ es: "Si el proyecto todav\xEDa no tiene identidad, elige uno de los cuatro temas de base y dice cu\xE1l eligi\xF3."
3569
+ },
3570
+ {
3571
+ en: "Pin a tag, never a branch: a branch can move under a project mid-sprint.",
3572
+ es: "Fijar un tag, nunca una rama: una rama puede moverse bajo el proyecto a mitad de sprint."
3573
+ }
3574
+ ],
3575
+ code: {
3576
+ caption: { en: "Paste this into the consumer project", es: "Peg\xE1 esto en el proyecto consumidor" },
3577
+ body: "Install paneltir following the instructions in\nnode_modules/paneltir/INSTALL.md, pinned to the tag we agreed on."
3578
+ }
3579
+ },
3580
+ {
3581
+ id: "fingerprint",
3582
+ title: { en: "Keep it cheap: the fingerprint protocol", es: "Que salga barato: el protocolo de la huella" },
3583
+ summary: {
3584
+ en: "fingerprint.json carries one SHA-256 for the whole repository plus one per file. Comparing a single string replaces re-reading the kit.",
3585
+ es: "fingerprint.json lleva un SHA-256 de todo el repositorio m\xE1s uno por archivo. Comparar una cadena reemplaza releer el kit entero."
3586
+ },
3587
+ points: [
3588
+ {
3589
+ en: "Read only the hash field of node_modules/paneltir/fingerprint.json at the start of a session.",
3590
+ es: "Al empezar la sesi\xF3n, leer solo el campo hash de node_modules/paneltir/fingerprint.json."
3591
+ },
3592
+ {
3593
+ en: "If it matches the hash recorded in the project, the kit did not change: do not open src/, README.md or INSTALL.md again.",
3594
+ es: "Si coincide con el hash anotado en el proyecto, el kit no cambi\xF3: no volver a abrir src/, README.md ni INSTALL.md."
3595
+ },
3596
+ {
3597
+ en: "If it differs, the files map names exactly which files moved \u2014 read those, and nothing else.",
3598
+ es: "Si difiere, el mapa files dice exactamente qu\xE9 archivos cambiaron: leer esos y nada m\xE1s."
3599
+ },
3600
+ {
3601
+ en: "Record the new hash in the project CLAUDE.md so the next session starts from a comparison instead of a full read.",
3602
+ es: "Anotar el hash nuevo en el CLAUDE.md del proyecto, para que la pr\xF3xima sesi\xF3n arranque comparando en vez de leyendo todo."
3603
+ },
3604
+ {
3605
+ en: "In this repository the whole protocol is a skill: .claude/skills/todo/SKILL.md turns the board into a work list and says what each intent obliges.",
3606
+ es: "En este repositorio el protocolo entero es una skill: .claude/skills/todo/SKILL.md convierte el tablero en una lista de trabajo y dice a qu\xE9 obliga cada intenci\xF3n."
3607
+ }
3608
+ ],
3609
+ code: {
3610
+ caption: {
3611
+ en: "Block for the consumer project CLAUDE.md",
3612
+ es: "Bloque para el CLAUDE.md del proyecto consumidor"
3613
+ },
3614
+ body: 'Kit fingerprint last integrated: <hash from node_modules/paneltir/fingerprint.json>.\nBefore re-reading anything under node_modules/paneltir, compare that value with\nthe current hash field. Equal means nothing changed \u2014 skip the read. Different\nmeans read only the files listed under "files" that differ, then update this hash.'
3615
+ }
3616
+ },
3617
+ {
3618
+ id: "state",
3619
+ title: { en: "The board is data, and the panel edits it", es: "El tablero es datos, y el panel lo edita" },
3620
+ summary: {
3621
+ en: "The board file holds a flat list of cards. Add, move, edit and delete them from the panel, press Save, and the change is a commit with a diff and an author.",
3622
+ es: "El archivo del tablero guarda una lista plana de tarjetas. A\xF1adilas, movelas, editalas y borralas desde el panel, puls\xE1 Guardar, y el cambio es un commit con diff y autor."
3623
+ },
3624
+ points: [
3625
+ {
3626
+ en: "Each card carries a column, a priority, a risk, who it waits on, an order flag and one of nine intents \u2014 the fields that turn a note into an instruction.",
3627
+ es: "Cada tarjeta lleva columna, prioridad, riesgo, de qui\xE9n depende, la marca de orden y una de nueve intenciones: los campos que convierten una nota en una instrucci\xF3n."
3628
+ },
3629
+ {
3630
+ en: 'order: true is the one flag that means "I am asking you to do this". It outranks position in the column.',
3631
+ es: "order: true es la \xFAnica marca que significa \xABte estoy pidiendo esto\xBB. Manda sobre la posici\xF3n en la columna."
3632
+ },
3633
+ {
3634
+ en: "intent says how to do it, and beats anything the agent would otherwise decide. Three of the nine \u2014 explain, askme, hold \u2014 are refusals to act, and honouring them exactly is the point.",
3635
+ es: "intent dice c\xF3mo hacerlo, y manda sobre cualquier cosa que el agente decidiera. Tres de las nueve \u2014explain, askme, hold\u2014 son negativas a actuar, y respetarlas al pie de la letra es justo el punto."
3636
+ },
3637
+ {
3638
+ en: "Checks are the definition of done, written in advance. Notes are the conversation, dated and attributed.",
3639
+ es: "Los checks son la definici\xF3n de terminado, escrita de antemano. Las notas son la conversaci\xF3n, con fecha y autor."
3640
+ },
3641
+ {
3642
+ en: "Any visible string can be a plain string or an { en, es } pair; typing in one language drops the other copy on purpose, because a translation of a sentence just rewritten is a lie about what the card says.",
3643
+ es: "Cualquier texto visible puede ser una cadena o un par { en, es }; escribir en un idioma borra la otra copia a prop\xF3sito, porque traducir una frase reci\xE9n reescrita miente sobre lo que dice la tarjeta."
3644
+ }
3645
+ ],
3646
+ code: {
3647
+ caption: { en: "A card", es: "Una tarjeta" },
3648
+ body: '{\n "id": "move-card-tests",\n "column": "next",\n "title": { "en": "Tests for moveCardInColumns", "es": "Tests de moveCardInColumns" },\n "area": "kit",\n "priority": "high",\n "risk": "medium",\n "owner": "claude",\n "order": true,\n "intent": "do",\n "checks": [{ "id": "k0", "text": "Edge cases covered", "done": false }],\n "notes": []\n}'
3649
+ }
3650
+ },
3651
+ {
3652
+ id: "rules",
3653
+ title: { en: "House rules for the agent", es: "Reglas de la casa para el agente" },
3654
+ summary: {
3655
+ en: "Three rules keep four projects from drifting apart invisibly.",
3656
+ es: "Tres reglas evitan que cuatro proyectos se separen sin que nadie lo vea."
3657
+ },
3658
+ points: [
3659
+ {
3660
+ en: "Never edit files inside node_modules/paneltir. An install overwrites them and the divergence becomes invisible.",
3661
+ es: "Nunca editar archivos dentro de node_modules/paneltir. Un install los pisa y la divergencia queda invisible."
3662
+ },
3663
+ {
3664
+ en: "Colour only through tokens. A hard-coded colour in a consumer project is a bug, not a shortcut.",
3665
+ es: "Color solo por tokens. Un color a mano en un proyecto consumidor es un bug, no un atajo."
3666
+ },
3667
+ {
3668
+ en: "Something structural missing? Compose first with children and renderCard; if that is not enough, the change belongs upstream in the kit, followed by a new version tag.",
3669
+ es: "\xBFFalta algo estructural? Primero componer con children y renderCard; si no alcanza, el cambio va al kit y sale con un tag de versi\xF3n nuevo."
3670
+ }
3671
+ ]
3672
+ }
3673
+ ];
3674
+ export {
3675
+ ANALYSIS_SECTIONS,
3676
+ AlertBanner,
3677
+ AnalysisView,
3678
+ BOARD_VERSION,
3679
+ Board,
3680
+ CAPABILITIES,
3681
+ Card,
3682
+ CardSheet,
3683
+ CopyBlock,
3684
+ DashboardHeader,
3685
+ DashboardThemeProvider,
3686
+ DetailSection,
3687
+ DetailSheet,
3688
+ FilterChip,
3689
+ FilterChipRow,
3690
+ GuideNote,
3691
+ GuideTour,
3692
+ HealthPill,
3693
+ HealthPillRow,
3694
+ INTENTS,
3695
+ INTENT_COPY,
3696
+ LANGS,
3697
+ LANG_LABELS,
3698
+ LEVELS,
3699
+ MARKETPLACE_NAME,
3700
+ MARKETPLACE_URL,
3701
+ MarketplaceView,
3702
+ NotificationBell,
3703
+ OWNERS,
3704
+ PANELTIR_FILE_COUNT,
3705
+ PANELTIR_FINGERPRINT,
3706
+ PANELTIR_VERSION,
3707
+ PRESET_PANEL_THEMES,
3708
+ PanelApp,
3709
+ SetupGuide,
3710
+ StatTile,
3711
+ StatTileGrid,
3712
+ THEME_FORMS,
3713
+ THEME_PRESETS,
3714
+ UI,
3715
+ applyMove,
3716
+ boardFacts,
3717
+ checkProgress,
3718
+ claimedWithoutStarting,
3719
+ claudeTheme,
3720
+ commandSnippet,
3721
+ countCards,
3722
+ cyberpunkTheme,
3723
+ decided,
3724
+ emptyCard,
3725
+ explainBoard,
3726
+ isCard,
3727
+ ledgerForm,
3728
+ midnightTheme,
3729
+ moveCardInColumns,
3730
+ newId,
3731
+ oldMoneyTheme,
3732
+ panelForm,
3733
+ paneltirBuild,
3734
+ paperForm,
3735
+ readBoard,
3736
+ readStoredLang,
3737
+ repositorySnippet,
3738
+ resetGuide,
3739
+ sparkPoints,
3740
+ stampForColumn,
3741
+ storeLang,
3742
+ text,
3743
+ today,
3744
+ trendOf,
3745
+ undecided,
3746
+ untranslated,
3747
+ useDashboardForm,
3748
+ useDashboardTheme,
3749
+ useDelegatedClick,
3750
+ useGuideNote,
3751
+ useMediaQuery,
3752
+ validateBoard,
3753
+ writeText
3754
+ };