paneltir 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -451,792 +451,825 @@ interface DropTarget {
451
451
  */
452
452
  declare function dropIndex(source: DropTarget, target: DropTarget): number | null;
453
453
 
454
- interface DetailSheetProps {
455
- open: boolean;
456
- onClose: () => void;
457
- children: React.ReactNode;
458
- ariaLabel?: string;
459
- }
460
454
  /**
461
- * Below 760px this is a sheet rising from the bottom; from 760px up the same
462
- * markup becomes a centred floating dialog. The breakpoint is handled in CSS,
463
- * so there are not two different components.
455
+ * A board is read in one language and written in another.
456
+ *
457
+ * This is the mechanism only — the types and the resolution. The panel's own
458
+ * wording lives with the panel: a library that shipped its copy would be a
459
+ * library deciding what a project's board says.
464
460
  */
465
- declare function DetailSheet({ open, onClose, children, ariaLabel }: DetailSheetProps): React.JSX.Element | null;
466
- interface DetailSectionProps {
467
- title: string;
468
- /** Clarifying phrase in lowercase, printed after an em dash. */
469
- subtitle?: string;
470
- children: React.ReactNode;
471
- }
472
- declare function DetailSection({ title, subtitle, children }: DetailSectionProps): React.JSX.Element;
473
-
461
+ type Lang = 'en' | 'es';
462
+ declare const LANGS: Lang[];
463
+ declare const LANG_LABELS: Record<Lang, string>;
474
464
  /**
475
- * Whether a note has been dismissed, and how to dismiss it.
465
+ * A string, or the same string per language.
476
466
  *
477
- * Starts as "shown but not yet decided" so the first paint matches the server
478
- * and nothing flashes into place; the stored answer arrives on the first
479
- * effect.
467
+ * A plain string is a claim: this text is right in either language — true for
468
+ * a product name, a number, a file id, and almost never for prose. An object
469
+ * is the honest shape for a sentence, and one that carries a single language
470
+ * says "written, not translated yet", which is exactly what the next session
471
+ * needs to know.
480
472
  */
481
- declare function useGuideNote(id: string, remember?: boolean): {
482
- visible: boolean;
483
- dismiss: () => void;
484
- };
485
- /** Clears every dismissal, so the guidance can be asked for again. */
486
- declare function resetGuide(): void;
487
- interface GuideNoteProps {
488
- /** Stable across releases: changing it makes the note reappear for everyone. */
473
+ type Text = string | Partial<Record<Lang, string>>;
474
+ declare function text(value: Text | undefined, lang: Lang): string;
475
+ /** True when a sentence exists in one language only. */
476
+ declare function untranslated(value: Text | undefined): boolean;
477
+ /**
478
+ * Writing replaces the language being typed and drops the other copy of that
479
+ * field. Deliberate: a translation of a sentence somebody just rewrote is a
480
+ * lie about what the card says, and the gap is the panel telling the next
481
+ * session what to fill in.
482
+ */
483
+ declare function writeText(lang: Lang, value: string): Text;
484
+
485
+ type Weight = 'primary' | 'secondary' | 'off';
486
+ type Severity = 'high' | 'medium' | 'low';
487
+ type SuggestionStatus = 'new' | 'doing' | 'done' | 'dismissed';
488
+ interface Goal {
489
489
  id: string;
490
- title: React.ReactNode;
491
- children: React.ReactNode;
492
- /** Wording of the dismiss button. Say what it does, not "OK". */
493
- dismissLabel?: string;
494
- /** Shown after the note, for the one thing it wants the reader to do. */
495
- action?: {
496
- label: string;
497
- onClick: () => void;
498
- };
499
- /** Position in a sequence, so a first session reads as a tour, not a pile. */
500
- step?: {
501
- index: number;
502
- total: number;
503
- };
504
- /** Told after the note is dismissed, so a sequence can move on. */
505
- onDismissed?: () => void;
506
- /**
507
- * Whether to record the dismissal in localStorage. False when the caller
508
- * keeps that answer itself, so the two cannot disagree.
509
- */
510
- remember?: boolean;
490
+ label: Text;
491
+ weight: Weight;
511
492
  }
512
- declare function GuideNote({ id, title, children, dismissLabel, action, step, onDismissed, remember, }: GuideNoteProps): React.JSX.Element | null;
513
- interface GuideTourProps {
514
- /** Notes in order. Only the first undismissed one shows, so it reads as a tour. */
515
- notes: Array<Omit<GuideNoteProps, 'step' | 'onDismissed'>>;
516
- /**
517
- * Which notes are already dismissed, when the caller keeps that somewhere
518
- * durable. Leave it out and each reader's browser remembers, which is the
519
- * right home for a fact about one browser — and the wrong one for a panel
520
- * with a single owner, who would be told the same tour again on a new
521
- * machine. Passing it does not switch the browser off: a note dismissed
522
- * here is written to both, because a caller whose store needs saving would
523
- * otherwise lose the dismissal on reload.
524
- */
525
- dismissed?: string[];
526
- /** Told when a note is dismissed. Required for `dismissed` to mean anything. */
527
- onDismiss?: (id: string) => void;
493
+ interface Constraint {
494
+ id: string;
495
+ label: Text;
496
+ on: boolean;
497
+ }
498
+ interface Reading {
499
+ date: string;
500
+ value: number;
501
+ note?: Text;
502
+ }
503
+ interface Metric {
504
+ id: string;
505
+ label: Text;
506
+ unit: string;
507
+ /** Which direction counts as better. A reading against it is what "drifting" means. */
508
+ goal: 'up' | 'down';
509
+ readings: Reading[];
510
+ }
511
+ interface Strategy {
512
+ id: string;
513
+ title: Text;
514
+ thesis: Text;
515
+ horizon: Text;
516
+ /** 0–10, so three bars can be compared at a glance rather than read. */
517
+ effort: number;
518
+ risk: number;
519
+ upside: number;
520
+ moves: Text[];
521
+ }
522
+ interface Suggestion {
523
+ id: string;
524
+ area: string;
525
+ severity: Severity;
526
+ status: SuggestionStatus;
527
+ date: string;
528
+ text: Text;
528
529
  }
529
530
  /**
530
- * One note at a time, in order.
531
+ * A question the owner answers and Claude reads.
531
532
  *
532
- * Five notes at once is a wall nobody reads. Shown one at a time, dismissing
533
- * each reveals the next, and a reader who dismisses all of them has been
534
- * through the tour rather than having closed a box.
533
+ * This is the whole point of the analysis view. A board says what to do; a
534
+ * choice says how the project wants it done, in the owner's words, once —
535
+ * instead of being asked again on every card. Claude reads these before
536
+ * deciding anything, so answering one here is worth more than answering it
537
+ * in a message that scrolls away.
538
+ *
539
+ * `value` of null is not missing data. It means nobody has decided, which is
540
+ * information: Claude must not guess an answer to a question the owner has
541
+ * deliberately left open.
535
542
  */
536
- declare function GuideTour({ notes, dismissed, onDismiss }: GuideTourProps): React.JSX.Element | null;
537
-
538
- type NotificationTone = 'info' | 'attention';
539
- interface PanelNotification {
540
- /**
541
- * Stable for the life of the event. Two different events must never share
542
- * one, or marking the first seen silences the second; the same event must
543
- * never change it, or it comes back every time the page loads.
544
- */
543
+ interface Choice {
545
544
  id: string;
546
- title: React.ReactNode;
547
- detail?: React.ReactNode;
548
- /** Shown as given, so the caller decides the format its readers expect. */
549
- at?: string;
550
- /** `attention` for something waiting on the reader, `info` for the rest. */
551
- tone?: NotificationTone;
552
- /** Where this leads. Called before the notification is marked seen. */
553
- onOpen?: () => void;
545
+ question: Text;
546
+ /** What actually changes depending on the answer. Never "choose an option". */
547
+ why?: Text;
548
+ options: ChoiceOption[];
549
+ value: string | null;
550
+ decidedAt?: string;
551
+ decidedBy?: string;
554
552
  }
555
- interface NotificationLabels {
556
- /** Names the control for a screen reader and on hover. */
557
- title: string;
558
- empty: string;
559
- markAll: string;
560
- /** Given the count, e.g. (n) => `${n} unread`. */
561
- unread: (count: number) => string;
553
+ interface ChoiceOption {
554
+ id: string;
555
+ label: Text;
556
+ /** What picking this commits the project to. */
557
+ detail?: Text;
562
558
  }
563
- interface NotificationBellProps {
564
- notifications: PanelNotification[];
565
- labels?: Partial<NotificationLabels>;
566
- /**
567
- * What this reader has already seen, when the caller keeps it somewhere
568
- * durable. Leave it out and the bell remembers in localStorage, which is the
569
- * right home for a fact about one browser.
570
- *
571
- * A panel with one password has one owner, and there "this reader" and "this
572
- * project" are the same person — so a panel that saves its board can keep
573
- * this in it and have it survive a new browser. That is the caller's call to
574
- * make, not the component's, which is why both work.
575
- */
576
- seen?: string[];
559
+ /** Who the work is for, and against what. */
560
+ interface Market {
561
+ audience: Text;
562
+ positioning: Text;
563
+ competitors: Competitor[];
564
+ pricing?: Text;
565
+ notes?: Text[];
566
+ }
567
+ interface Competitor {
568
+ id: string;
569
+ name: string;
570
+ /** What they do better. Written honestly or it is worthless. */
571
+ strength: Text;
572
+ /** Where they leave room. */
573
+ gap: Text;
574
+ }
575
+ /** The four things the analysis is about, each its own screen. */
576
+ type AnalysisSection = 'board' | 'project' | 'strategy' | 'market';
577
+ declare const ANALYSIS_SECTIONS: AnalysisSection[];
578
+ interface Analysis {
579
+ brief: Text;
580
+ stage: string;
581
+ goals: Goal[];
582
+ constraints: Constraint[];
583
+ metrics: Metric[];
584
+ strategies: Strategy[];
585
+ /** The path the owner picked. Null is a decision waiting, and it is theirs. */
586
+ chosen: string | null;
587
+ suggestions: Suggestion[];
588
+ /** Who this is for and against what. Optional: not every board is a product. */
589
+ market?: Market;
577
590
  /**
578
- * Told which ids were just seen. Required for `seen` to mean anything —
579
- * though the bell also remembers in localStorage either way, so a caller
580
- * that has not saved yet does not show the same notifications again.
591
+ * Standing answers, grouped by the screen they belong to. Claude reads them
592
+ * before deciding anything the board does not already settle.
581
593
  */
582
- onSeen?: (ids: string[]) => void;
594
+ choices?: Partial<Record<AnalysisSection, Choice[]>>;
583
595
  }
584
- declare function NotificationBell({ notifications, labels, seen: given, onSeen }: NotificationBellProps): React.JSX.Element | null;
596
+ /** Everything the owner has actually decided, newest first. */
597
+ declare function decided(analysis: Analysis): {
598
+ section: AnalysisSection;
599
+ choice: Choice;
600
+ option: ChoiceOption;
601
+ }[];
602
+ /** Questions still open. A count of these is the honest state of the analysis. */
603
+ declare function undecided(analysis: Analysis): {
604
+ section: AnalysisSection;
605
+ choice: Choice;
606
+ }[];
607
+ /**
608
+ * What the board itself says, counted rather than declared.
609
+ *
610
+ * These are not stored: a number typed into the analysis is a number that
611
+ * disagrees with the board the moment a card moves. They are derived on every
612
+ * render from the cards, so they cannot be wrong.
613
+ */
614
+ interface BoardFacts {
615
+ total: number;
616
+ open: number;
617
+ byColumn: {
618
+ id: string;
619
+ title: Text;
620
+ count: number;
621
+ }[];
622
+ waitingOnOwner: number;
623
+ orders: number;
624
+ highRisk: number;
625
+ /** Cards claiming to be finished that were never picked up. */
626
+ claimed: Pick<Card, 'id' | 'title'>[];
627
+ /** Age of the oldest card still open. Null when nothing is open. */
628
+ oldestOpenDays: number | null;
629
+ oldestOpen: Pick<Card, 'id' | 'title'> | null;
630
+ }
631
+ /**
632
+ * Counts the board rather than believing a number somebody typed.
633
+ *
634
+ * `today` is passed in so the same cards always produce the same answer — a
635
+ * function that reads the clock cannot be checked.
636
+ */
637
+ declare function boardFacts(cards: Card[], columns: ColumnDef[], today: string): BoardFacts;
638
+ interface Trend {
639
+ latest: Reading | undefined;
640
+ previous: Reading | undefined;
641
+ delta: number | null;
642
+ /** True when the last move went against the metric's declared direction. */
643
+ against: boolean;
644
+ }
645
+ declare function trendOf(metric: Metric): Trend;
646
+ /**
647
+ * Points for a sparkline in a 0–1 box, so the caller only decides the size.
648
+ * A flat series sits on the middle line rather than collapsing to the floor.
649
+ */
650
+ declare function sparkPoints(readings: Reading[]): {
651
+ x: number;
652
+ y: number;
653
+ }[];
585
654
 
655
+ type Level = 'high' | 'medium' | 'low';
656
+ type Owner = 'claude' | 'you';
586
657
  /**
587
- * A block of text whose whole purpose is to end up somewhere else.
658
+ * What the owner wants done with a card next, in one tap.
588
659
  *
589
- * Anything a panel tells someone to paste — a settings fragment, two commands,
590
- * a shell line — is read once and copied. So the copy is the primary action
591
- * and the text is selectable underneath it, rather than the other way round.
660
+ * Free text would be clearer to write and useless to obey: "solve it if you
661
+ * can" and "solve it yourself" are the same sentence and different
662
+ * instructions. A fixed set gives each one an exact, documented meaning, so a
663
+ * tap is a contract rather than a hint. Absent is a real state: no
664
+ * instruction, use judgement.
592
665
  *
593
- * The clipboard API is not always there. It needs a secure context and can be
594
- * refused outright, and a copy button that silently does nothing is worse than
595
- * no button at all: the reader walks away believing they have the text. So the
596
- * failure falls back to selecting the block, and says which of the two
597
- * happened.
666
+ * Three of them — explain, askme, hold — are refusals to act. Honouring them
667
+ * exactly matters more than the work: they are the only way to say "not yet".
598
668
  */
599
- interface CopyBlockProps {
600
- /** Exactly what lands on the clipboard. Shown verbatim. */
601
- text: string;
602
- /** What this block is, above it. */
603
- label?: React.ReactNode;
604
- /** Why the reader wants it, and anything they must know before pasting. */
605
- note?: React.ReactNode;
606
- /** Wording, so the kit stays language-agnostic. */
607
- labels?: Partial<CopyBlockLabels>;
669
+ declare const INTENTS: readonly ["decide", "explain", "solve", "do", "cheap", "safe", "fast", "askme", "hold"];
670
+ type Intent = (typeof INTENTS)[number];
671
+ interface IntentCopy {
672
+ label: Record<Lang, string>;
673
+ meaning: Record<Lang, string>;
674
+ }
675
+ declare const INTENT_COPY: Record<Intent, IntentCopy>;
676
+ interface Check {
677
+ id: string;
678
+ text: Text;
679
+ done: boolean;
680
+ }
681
+ interface Note {
682
+ id: string;
683
+ by: Owner;
684
+ date: string;
685
+ text: Text;
686
+ }
687
+ interface Card {
688
+ id: string;
689
+ column: string;
690
+ title: Text;
691
+ body: Text;
692
+ area: string;
693
+ priority: Level;
694
+ risk: Level;
695
+ owner: Owner;
696
+ /** The one flag that means "I am asking you to do this". */
697
+ order: boolean;
698
+ intent: Intent | null;
699
+ checks: Check[];
700
+ notes: Note[];
701
+ createdAt: string;
702
+ updatedAt: string;
703
+ /**
704
+ * Stamped by the board when the card enters the working column, and when it
705
+ * reaches a done column. They are written by the move, never typed, so a
706
+ * card that carries `completedAt` with no `startedAt` is one that was
707
+ * declared finished without ever being picked up — which is exactly the
708
+ * thing worth being able to see.
709
+ */
710
+ startedAt?: string | null;
711
+ completedAt?: string | null;
608
712
  }
609
- interface CopyBlockLabels {
610
- copy: string;
611
- copied: string;
612
- /** Used when the clipboard refused and the text was selected instead. */
613
- selected: string;
713
+ interface ColumnDef {
714
+ id: string;
715
+ title: Text;
716
+ /** What the column means, shown on hover. Two words cannot say whether a
717
+ card sitting here is waiting on you or on Claude. */
718
+ hint?: Text;
719
+ /** Work in flight: entering this column stamps the card as started. */
720
+ active?: boolean;
721
+ /** Cards here are finished: they leave the open counts alone. */
722
+ done?: boolean;
614
723
  }
615
- declare function CopyBlock({ text, label, note, labels }: CopyBlockProps): React.JSX.Element;
616
-
617
724
  /**
618
- * What the panel needs in order to work, and what to do about what is missing.
619
- *
620
- * A panel with no token does not look broken — it looks fine until the moment
621
- * someone presses Save, and then it fails with something the person reading it
622
- * did not cause and cannot place. This turns that into a list they can read
623
- * before it happens, and act on without leaving the panel.
624
- *
625
- * The status comes from the server, which is the only place that can see the
626
- * environment. It reports *whether* a variable is set and never its value, so
627
- * this component has nothing worth leaking.
725
+ * One pass over the board: what a session changed, and which cards it
726
+ * touched. Written at the end of a run, so the panel keeps the account
727
+ * rather than the account living in a chat nobody can re-read.
628
728
  */
629
- type SetupStatus = 'ok' | 'missing' | 'unknown';
630
- interface SetupRequirement {
729
+ interface Run {
631
730
  id: string;
632
- /** What it is, in the reader's terms rather than the variable's. */
633
- label: React.ReactNode;
634
- status: SetupStatus;
635
- /** The environment variable behind it, when there is one to name. */
636
- variable?: string;
637
- /** What it lets the panel do — so a missing one has a visible cost. */
638
- enables?: React.ReactNode;
639
- /** Exactly what to do, shown only when it is missing. */
640
- fix?: React.ReactNode;
641
- /**
642
- * Information rather than a gate: drawn in the list, never walked as a step.
643
- *
644
- * Every other requirement here is an environment variable, and the
645
- * walkthrough's one action — re-read the environment from the server —
646
- * is what clears it. A requirement that action cannot clear parks the
647
- * walkthrough on it for ever, and because `unknown` is judged across the
648
- * whole list, one advisory row that nobody wired made every step answer a
649
- * successful check with "the panel could not ask its own server". That is
650
- * what the copied gate did the day it was added.
651
- */
652
- advisory?: boolean;
731
+ date: string;
732
+ by: Owner;
733
+ summary: Text;
734
+ /** Card ids this run touched, so a claim can be checked against the board. */
735
+ cards: string[];
736
+ /** The kit fingerprint at the time, twelve characters is enough to compare. */
737
+ fingerprint?: string;
653
738
  }
654
- interface SetupGuideProps {
655
- requirements: SetupRequirement[];
656
- /** Shown when everything is in place, instead of an empty list. */
657
- readyMessage?: React.ReactNode;
658
- /** Wording, so the kit stays language-agnostic. */
659
- labels?: Partial<{
660
- ok: string;
661
- missing: string;
662
- unknown: string;
663
- whatToDo: string;
664
- }>;
739
+ interface HealthFact {
740
+ label: Text;
741
+ value: Text;
742
+ status: 'ok' | 'warning' | 'bad' | 'unknown';
665
743
  }
666
- declare function SetupGuide({ requirements, readyMessage, labels }: SetupGuideProps): React.JSX.Element;
667
-
668
744
  /**
669
- * The setup, walked one step at a time.
670
- *
671
- * `SetupGuide` is the list: the right shape for somebody auditing a panel that
672
- * already works. This is the other reader — the one who installed it twenty
673
- * minutes ago, has four things missing and no idea which to do first. A wall
674
- * of four is not four times as useful as one; it is the thing people close.
745
+ * A theme Claude derived from the project it is installed in.
675
746
  *
676
- * Two rules hold it honest. It **checks rather than asks**: every step ends in
677
- * a button that re-reads the environment from the server, so the reader is
678
- * told whether it worked instead of ticking a box themselves. And it **never
679
- * claims what it cannot see**: a panel that could not reach its own endpoint
680
- * says the check failed, never that a variable is missing, because telling
681
- * somebody to add a token they already added is the worse of the two wrong
682
- * answers.
747
+ * Stored with where it came from, not only with its colours. "Update the
748
+ * theme" is only a meaningful request if the next answer can be compared with
749
+ * this one — otherwise a regenerated theme is a different theme for reasons
750
+ * nobody can see. `from` is what makes the difference readable.
683
751
  *
684
- * There is no copy of the values here and nothing is written: the panel cannot
685
- * set an environment variable, and a button that appeared to would be the same
686
- * fault as one that appeared to install an update.
752
+ * `refreshRequested` is a message, not a trigger: a panel cannot run Claude.
753
+ * Tapping Update writes the date here, the bell shows it, and the next session
754
+ * regenerates. Pretending the button does the work itself would leave the
755
+ * owner waiting for something that was never going to happen.
687
756
  */
688
- interface SetupWizardLabels {
689
- title: string;
690
- /** `(index, total)` — "Step 2 of 5". */
691
- progress: (index: number, total: number) => string;
692
- /** Shown above the one step in hand. */
693
- nowDo: string;
694
- /** The button that re-reads the environment. */
695
- check: string;
696
- checking: string;
697
- /** Said when a check ran and the step is still not in place. */
698
- stillMissing: string;
699
- /** Said when the panel could not ask its own server. */
700
- cannotTell: string;
701
- /** The heading over the finished steps. */
702
- doneTitle: string;
703
- /** Shown in place of a step when every requirement is in place. */
704
- readyTitle: string;
705
- readyBody: string;
706
- /** The two things to try before trusting the gate, as list items. */
707
- verify: string[];
708
- /** Closes the wizard. */
709
- close: string;
710
- /** The escape hatch: heading, explanation, and the prompt to copy. */
711
- stuckTitle: string;
712
- stuckBody: string;
713
- stuckPrompt: string;
714
- copy: string;
715
- copied: string;
716
- }
717
- interface SetupWizardProps {
718
- /** In the order they are to be done. The caller owns that order. */
719
- requirements: SetupRequirement[];
720
- labels: SetupWizardLabels;
721
- /** Re-reads the environment. Resolves when the answer has been applied. */
722
- onCheck: () => Promise<void> | void;
723
- /** Told when the reader closes it. */
724
- onClose?: () => void;
757
+ interface ImportedTheme {
758
+ /** What to call it in the theme list. */
759
+ label: Text;
760
+ tokens: Record<string, string>;
761
+ form?: Record<string, string | number>;
762
+ from: {
763
+ /** Files the colours were read out of, so the claim can be checked. */
764
+ files: string[];
765
+ /** The colours actually found, before they were mapped to tokens. */
766
+ colours: string[];
767
+ /** Why these tokens and not others. One or two sentences. */
768
+ reasoning: Text;
769
+ };
770
+ importedAt: string;
771
+ importedBy: 'claude';
772
+ /** Set by the panel when the owner asks for it to be derived again. */
773
+ refreshRequested?: string | null;
725
774
  }
726
- declare function SetupWizard({ requirements, labels, onCheck, onClose }: SetupWizardProps): React.JSX.Element;
727
-
728
775
  /**
729
- * Where a reader is in the setup, as one answer instead of a list to read.
776
+ * What the owner has chosen, kept in the board so it survives a new browser.
730
777
  *
731
- * `SetupGuide` draws every requirement at once, which is the right shape for
732
- * somebody checking a panel that already works. It is the wrong shape for
733
- * somebody who has just installed one: five things at once, four of them
734
- * missing, and no order to do them in reads as a wall rather than as a first
735
- * step. So the same requirements are walked one at a time, and this is the
736
- * function that decides which one.
778
+ * The kit's default is to remember these per browser, in localStorage, because
779
+ * for a component "which notes this reader dismissed" is a fact about a reader
780
+ * and not about a project. A panel with one password is the case where those
781
+ * two are the same person: keeping it here means the theme, the language and
782
+ * what has already been read follow the owner to a new machine instead of
783
+ * greeting them with a tour they finished months ago.
737
784
  *
738
- * The order the caller gives is the order they are done in — a token that
739
- * cannot be scoped before the repository is named is a step in the wrong
740
- * place, and that is the caller's knowledge, not this function's.
785
+ * The cost is real and worth naming: anyone who shares the password shares
786
+ * this. It holds a theme name and a list of ids — nothing worth stealing, and
787
+ * nothing anyone else would want.
741
788
  */
742
- interface WizardStep {
743
- /** The requirement to show, or null when nothing is left to do. */
744
- current: SetupRequirement | null;
745
- /** 1-based, for "step 2 of 5". Zero when there is nothing left. */
746
- index: number;
747
- /** How many steps this setup has in total. */
748
- total: number;
749
- /** Every requirement that is in place, in the caller's order. */
750
- done: SetupRequirement[];
751
- /** True when every requirement is `ok`. */
752
- ready: boolean;
789
+ interface Preferences {
790
+ theme?: string;
791
+ lang?: Lang;
792
+ /** Ids of notifications already read. Bounded when saved. */
793
+ seen?: string[];
794
+ /** Ids of guide notes already dismissed. */
795
+ guideDismissed?: string[];
753
796
  /**
754
- * True when the panel could not ask the server, so nothing is claimed.
797
+ * Whether an update card is written as an order rather than a suggestion.
755
798
  *
756
- * This is not "not ready": a panel that cannot reach its own endpoint knows
757
- * nothing about the environment, and telling somebody to add a token they
758
- * already added is worse than saying the check itself failed.
799
+ * It does not make anything automatic — the panel cannot run npm, and this
800
+ * changes `order` on the card it offers to write, nothing else. Off means
801
+ * the card asks; on means it tells.
759
802
  */
760
- unknown: boolean;
803
+ updateAsOrder?: boolean;
761
804
  }
762
- declare function wizardStep(requirements: SetupRequirement[]): WizardStep;
805
+ interface PanelState {
806
+ v: 2;
807
+ project: string;
808
+ environment: Text;
809
+ updatedAt: string;
810
+ notice?: Text;
811
+ columns: ColumnDef[];
812
+ areas: {
813
+ id: string;
814
+ label: Text;
815
+ }[];
816
+ health: HealthFact[];
817
+ cards: Card[];
818
+ /** Newest first. Capped when saving, so the file cannot grow without end. */
819
+ runs: Run[];
820
+ /** What any of the work is for: goals, constraints, metrics, strategies. */
821
+ analysis?: Analysis;
822
+ /** The owner's choices, so they survive a new browser. */
823
+ preferences?: Preferences;
824
+ /** A theme Claude derived from this project, with where it came from. */
825
+ importedTheme?: ImportedTheme;
826
+ }
827
+ declare const LEVELS: Level[];
828
+ declare const OWNERS: Owner[];
829
+ declare function newId(): string;
830
+ declare function today(): string;
831
+ /** A card is created empty and named second — the same as tapping "+" on paper. */
832
+ declare function emptyCard(column: string, area: string): Card;
833
+ /**
834
+ * How long a finished card stays on the board before it becomes history.
835
+ *
836
+ * A done column only grows. It is the one column nothing ever removes from,
837
+ * so a board that is being worked turns into a wall of things already
838
+ * finished, and the three columns that need reading get a third of the screen
839
+ * between them. A week is the window in which "what did we ship" is still a
840
+ * live question; after that it is a record, and a record does not need to be
841
+ * in the way.
842
+ */
843
+ declare const ARCHIVE_AFTER_DAYS = 7;
763
844
  /**
764
- * Whether this panel has never been worked, which is what "first install"
765
- * means here.
845
+ * Whether a finished card has been finished long enough to stop being drawn.
766
846
  *
767
- * `paneltir init` writes a board with the columns already there and nothing
768
- * on them, so a board carrying no cards and no runs is one nobody has used
769
- * yet. That is a better signal than a version on disk: a project can install
770
- * the package, never open the panel, and update three times before anyone
771
- * sees it — and it is still that reader's first run.
847
+ * Derived on every render and never written, which is the arrangement
848
+ * `boardFacts()` already uses: a flag stored on the card would be wrong the
849
+ * day after it was set, and setting it would mark the panel dirty for the
850
+ * passage of time. Here the answer simply becomes true on its own.
772
851
  *
773
- * A panel that has been worked and then loses a variable is deliberately not
774
- * this. It needs the same instructions and gets them from the same wizard,
775
- * but it is told rather than taken over: somebody mid-sentence on a board
776
- * they know does not want a walkthrough opening on top of it.
852
+ * **A card with no `completedAt` is never archived**, whatever column it sits
853
+ * in. It reached done without being stamped — a board edited by hand, or one
854
+ * from before the stamp existed — so its age is not known, and hiding a card
855
+ * whose age nobody knows is the same mistake as reporting a token missing
856
+ * because the check could not run. `unknown` is not `old`.
777
857
  */
778
- declare function isFirstRun(board: {
779
- cards?: unknown[];
780
- runs?: unknown[];
781
- }): boolean;
782
-
783
- type Handler = (payload: Record<string, string>, event: MouseEvent) => void;
858
+ declare function isArchived(card: Card, doneColumns: ReadonlySet<string>, now?: string, afterDays?: number): boolean;
784
859
  /**
785
- * A single click listener on document that dispatches by data-* attributes,
786
- * instead of one handler per element. Meant for a project's own actions; the
787
- * Board does not use it internally.
788
- *
789
- * Example: useDelegatedClick('data-action', (payload) => { if (payload.action === 'delete') ... })
790
- * with buttons such as <button data-action="delete" data-id="42">
860
+ * Applies the column's own meaning to a card that just moved into it: a card
861
+ * entering the working column is started, one reaching a done column is
862
+ * finished, and one dragged back out of done is not finished any more.
791
863
  */
792
- declare function useDelegatedClick(attribute: string, handler: Handler): void;
793
-
794
- declare function useMediaQuery(query: string): boolean;
795
-
864
+ declare function stampForColumn(card: Card, columns: ColumnDef[]): Card;
796
865
  /**
797
- * A board is read in one language and written in another.
866
+ * Where a card the panel writes on the reader's behalf goes.
798
867
  *
799
- * This is the mechanism only — the types and the resolution. The panel's own
800
- * wording lives with the panel: a library that shipped its copy would be a
801
- * library deciding what a project's board says.
868
+ * The update card and the four "ask for this again" cards used to be born in
869
+ * a column literally called `next`. The default board has one; a board whose
870
+ * owner renamed it does not, and a card in a column that does not exist is
871
+ * the exact fault `validateBoard` names — drawn nowhere, and refused by the
872
+ * write path, so one tap on a button turned a saveable board into one that
873
+ * could not be saved and said so about a column the reader had never heard of.
874
+ *
875
+ * `next` when there is one, because that is what the button means. Otherwise
876
+ * the first column that is neither in flight nor finished nor the owner's:
877
+ * queued work goes where queued work already sits. And the first column of
878
+ * all when nothing else fits, since a board with no columns cannot pass the
879
+ * validator either way.
802
880
  */
803
- type Lang = 'en' | 'es';
804
- declare const LANGS: Lang[];
805
- declare const LANG_LABELS: Record<Lang, string>;
881
+ declare function queueColumn(columns: ColumnDef[]): string;
882
+ /** A card claiming to be finished that was never picked up. */
883
+ declare function claimedWithoutStarting(card: Card): boolean;
884
+ interface Counts {
885
+ open: number;
886
+ orders: number;
887
+ yours: number;
888
+ decisions: number;
889
+ highRisk: number;
890
+ done: number;
891
+ }
892
+ declare function countCards(state: PanelState): Counts;
893
+ /** How many checklist steps are ticked, for the counter on a card. */
894
+ declare function checkProgress(card: Card): {
895
+ done: number;
896
+ total: number;
897
+ } | undefined;
898
+
806
899
  /**
807
- * A string, or the same string per language.
808
- *
809
- * A plain string is a claim: this text is right in either language — true for
810
- * a product name, a number, a file id, and almost never for prose. An object
811
- * is the honest shape for a sentence, and one that carries a single language
812
- * says "written, not translated yet", which is exactly what the next session
813
- * needs to know.
900
+ * Cards the panel writes when the owner asks for something it cannot do.
901
+ *
902
+ * The panel runs nothing. It cannot bump a version, install a plugin or
903
+ * produce a market report, and a button that appeared to would be the fault
904
+ * this project keeps refusing. What it can do is write the request down where
905
+ * work reaches Claude — an order card on the board — and open it, so the tap
906
+ * is visibly a request and the next session carries it out.
907
+ *
908
+ * Two rules live here rather than in the component, because they are the part
909
+ * with decisions in it and a component cannot be tested in this repository:
910
+ *
911
+ * - **One open request per subject.** A second tap while the first is still
912
+ * waiting opens the card already asked for. Two version bumps queued at
913
+ * once is a project two versions ahead of where anybody meant, and two
914
+ * install cards for one tool is the same edit made twice.
915
+ * - **A finished request does not block a new one.** Once the card reaches a
916
+ * done column the subject is free again: next week's bump is a new card.
814
917
  */
815
- type Text = string | Partial<Record<Lang, string>>;
816
- declare function text(value: Text | undefined, lang: Lang): string;
817
- /** True when a sentence exists in one language only. */
818
- declare function untranslated(value: Text | undefined): boolean;
918
+
919
+ /** The kinds of request the panel writes. */
920
+ type RequestKind = 'version-bump' | 'install';
921
+ /** `version-bump-2026-09-26`, `install-deploybudget-2026-09-26`. */
922
+ declare function requestId(kind: RequestKind, day: string, subject?: string): string;
819
923
  /**
820
- * Writing replaces the language being typed and drops the other copy of that
821
- * field. Deliberate: a translation of a sentence somebody just rewrote is a
822
- * lie about what the card says, and the gap is the panel telling the next
823
- * session what to fill in.
924
+ * The request of this kind, for this subject, still waiting on the board.
925
+ *
926
+ * "Waiting" is anything outside a done column — queued, in flight, or parked
927
+ * in the owner's column with a question on it. Only finishing it frees the
928
+ * subject for a new request.
824
929
  */
825
- declare function writeText(lang: Lang, value: string): Text;
930
+ declare function pendingRequest(cards: Card[], kind: RequestKind, doneColumns: ReadonlySet<string>, subject?: string): Card | undefined;
826
931
 
827
- type Weight = 'primary' | 'secondary' | 'off';
828
- type Severity = 'high' | 'medium' | 'low';
829
- type SuggestionStatus = 'new' | 'doing' | 'done' | 'dismissed';
830
- interface Goal {
831
- id: string;
832
- label: Text;
833
- weight: Weight;
834
- }
835
- interface Constraint {
836
- id: string;
837
- label: Text;
838
- on: boolean;
839
- }
840
- interface Reading {
841
- date: string;
842
- value: number;
843
- note?: Text;
844
- }
845
- interface Metric {
846
- id: string;
847
- label: Text;
848
- unit: string;
849
- /** Which direction counts as better. A reading against it is what "drifting" means. */
850
- goal: 'up' | 'down';
851
- readings: Reading[];
852
- }
853
- interface Strategy {
854
- id: string;
855
- title: Text;
856
- thesis: Text;
857
- horizon: Text;
858
- /** 0–10, so three bars can be compared at a glance rather than read. */
859
- effort: number;
860
- risk: number;
861
- upside: number;
862
- moves: Text[];
932
+ interface DetailSheetProps {
933
+ open: boolean;
934
+ onClose: () => void;
935
+ children: React.ReactNode;
936
+ ariaLabel?: string;
863
937
  }
864
- interface Suggestion {
865
- id: string;
866
- area: string;
867
- severity: Severity;
868
- status: SuggestionStatus;
869
- date: string;
870
- text: Text;
938
+ /**
939
+ * Below 760px this is a sheet rising from the bottom; from 760px up the same
940
+ * markup becomes a centred floating dialog. The breakpoint is handled in CSS,
941
+ * so there are not two different components.
942
+ */
943
+ declare function DetailSheet({ open, onClose, children, ariaLabel }: DetailSheetProps): React.JSX.Element | null;
944
+ interface DetailSectionProps {
945
+ title: string;
946
+ /** Clarifying phrase in lowercase, printed after an em dash. */
947
+ subtitle?: string;
948
+ children: React.ReactNode;
871
949
  }
950
+ declare function DetailSection({ title, subtitle, children }: DetailSectionProps): React.JSX.Element;
951
+
872
952
  /**
873
- * A question the owner answers and Claude reads.
874
- *
875
- * This is the whole point of the analysis view. A board says what to do; a
876
- * choice says how the project wants it done, in the owner's words, once —
877
- * instead of being asked again on every card. Claude reads these before
878
- * deciding anything, so answering one here is worth more than answering it
879
- * in a message that scrolls away.
953
+ * Whether a note has been dismissed, and how to dismiss it.
880
954
  *
881
- * `value` of null is not missing data. It means nobody has decided, which is
882
- * information: Claude must not guess an answer to a question the owner has
883
- * deliberately left open.
955
+ * Starts as "shown but not yet decided" so the first paint matches the server
956
+ * and nothing flashes into place; the stored answer arrives on the first
957
+ * effect.
884
958
  */
885
- interface Choice {
886
- id: string;
887
- question: Text;
888
- /** What actually changes depending on the answer. Never "choose an option". */
889
- why?: Text;
890
- options: ChoiceOption[];
891
- value: string | null;
892
- decidedAt?: string;
893
- decidedBy?: string;
894
- }
895
- interface ChoiceOption {
896
- id: string;
897
- label: Text;
898
- /** What picking this commits the project to. */
899
- detail?: Text;
900
- }
901
- /** Who the work is for, and against what. */
902
- interface Market {
903
- audience: Text;
904
- positioning: Text;
905
- competitors: Competitor[];
906
- pricing?: Text;
907
- notes?: Text[];
908
- }
909
- interface Competitor {
959
+ declare function useGuideNote(id: string, remember?: boolean): {
960
+ visible: boolean;
961
+ dismiss: () => void;
962
+ };
963
+ /** Clears every dismissal, so the guidance can be asked for again. */
964
+ declare function resetGuide(): void;
965
+ interface GuideNoteProps {
966
+ /** Stable across releases: changing it makes the note reappear for everyone. */
910
967
  id: string;
911
- name: string;
912
- /** What they do better. Written honestly or it is worthless. */
913
- strength: Text;
914
- /** Where they leave room. */
915
- gap: Text;
916
- }
917
- /** The four things the analysis is about, each its own screen. */
918
- type AnalysisSection = 'board' | 'project' | 'strategy' | 'market';
919
- declare const ANALYSIS_SECTIONS: AnalysisSection[];
920
- interface Analysis {
921
- brief: Text;
922
- stage: string;
923
- goals: Goal[];
924
- constraints: Constraint[];
925
- metrics: Metric[];
926
- strategies: Strategy[];
927
- /** The path the owner picked. Null is a decision waiting, and it is theirs. */
928
- chosen: string | null;
929
- suggestions: Suggestion[];
930
- /** Who this is for and against what. Optional: not every board is a product. */
931
- market?: Market;
968
+ title: React.ReactNode;
969
+ children: React.ReactNode;
970
+ /** Wording of the dismiss button. Say what it does, not "OK". */
971
+ dismissLabel?: string;
972
+ /** Shown after the note, for the one thing it wants the reader to do. */
973
+ action?: {
974
+ label: string;
975
+ onClick: () => void;
976
+ };
977
+ /** Position in a sequence, so a first session reads as a tour, not a pile. */
978
+ step?: {
979
+ index: number;
980
+ total: number;
981
+ };
982
+ /** Told after the note is dismissed, so a sequence can move on. */
983
+ onDismissed?: () => void;
984
+ /**
985
+ * Whether to record the dismissal in localStorage. False when the caller
986
+ * keeps that answer itself, so the two cannot disagree.
987
+ */
988
+ remember?: boolean;
989
+ }
990
+ declare function GuideNote({ id, title, children, dismissLabel, action, step, onDismissed, remember, }: GuideNoteProps): React.JSX.Element | null;
991
+ interface GuideTourProps {
992
+ /** Notes in order. Only the first undismissed one shows, so it reads as a tour. */
993
+ notes: Array<Omit<GuideNoteProps, 'step' | 'onDismissed'>>;
932
994
  /**
933
- * Standing answers, grouped by the screen they belong to. Claude reads them
934
- * before deciding anything the board does not already settle.
995
+ * Which notes are already dismissed, when the caller keeps that somewhere
996
+ * durable. Leave it out and each reader's browser remembers, which is the
997
+ * right home for a fact about one browser — and the wrong one for a panel
998
+ * with a single owner, who would be told the same tour again on a new
999
+ * machine. Passing it does not switch the browser off: a note dismissed
1000
+ * here is written to both, because a caller whose store needs saving would
1001
+ * otherwise lose the dismissal on reload.
935
1002
  */
936
- choices?: Partial<Record<AnalysisSection, Choice[]>>;
1003
+ dismissed?: string[];
1004
+ /** Told when a note is dismissed. Required for `dismissed` to mean anything. */
1005
+ onDismiss?: (id: string) => void;
937
1006
  }
938
- /** Everything the owner has actually decided, newest first. */
939
- declare function decided(analysis: Analysis): {
940
- section: AnalysisSection;
941
- choice: Choice;
942
- option: ChoiceOption;
943
- }[];
944
- /** Questions still open. A count of these is the honest state of the analysis. */
945
- declare function undecided(analysis: Analysis): {
946
- section: AnalysisSection;
947
- choice: Choice;
948
- }[];
949
1007
  /**
950
- * What the board itself says, counted rather than declared.
1008
+ * One note at a time, in order.
951
1009
  *
952
- * These are not stored: a number typed into the analysis is a number that
953
- * disagrees with the board the moment a card moves. They are derived on every
954
- * render from the cards, so they cannot be wrong.
1010
+ * Five notes at once is a wall nobody reads. Shown one at a time, dismissing
1011
+ * each reveals the next, and a reader who dismisses all of them has been
1012
+ * through the tour rather than having closed a box.
955
1013
  */
956
- interface BoardFacts {
957
- total: number;
958
- open: number;
959
- byColumn: {
960
- id: string;
961
- title: Text;
962
- count: number;
963
- }[];
964
- waitingOnOwner: number;
965
- orders: number;
966
- highRisk: number;
967
- /** Cards claiming to be finished that were never picked up. */
968
- claimed: Pick<Card, 'id' | 'title'>[];
969
- /** Age of the oldest card still open. Null when nothing is open. */
970
- oldestOpenDays: number | null;
971
- oldestOpen: Pick<Card, 'id' | 'title'> | null;
1014
+ declare function GuideTour({ notes, dismissed, onDismiss }: GuideTourProps): React.JSX.Element | null;
1015
+
1016
+ type NotificationTone = 'info' | 'attention';
1017
+ interface PanelNotification {
1018
+ /**
1019
+ * Stable for the life of the event. Two different events must never share
1020
+ * one, or marking the first seen silences the second; the same event must
1021
+ * never change it, or it comes back every time the page loads.
1022
+ */
1023
+ id: string;
1024
+ title: React.ReactNode;
1025
+ detail?: React.ReactNode;
1026
+ /** Shown as given, so the caller decides the format its readers expect. */
1027
+ at?: string;
1028
+ /** `attention` for something waiting on the reader, `info` for the rest. */
1029
+ tone?: NotificationTone;
1030
+ /** Where this leads. Called before the notification is marked seen. */
1031
+ onOpen?: () => void;
1032
+ }
1033
+ interface NotificationLabels {
1034
+ /** Names the control for a screen reader and on hover. */
1035
+ title: string;
1036
+ empty: string;
1037
+ markAll: string;
1038
+ /** Given the count, e.g. (n) => `${n} unread`. */
1039
+ unread: (count: number) => string;
1040
+ }
1041
+ interface NotificationBellProps {
1042
+ notifications: PanelNotification[];
1043
+ labels?: Partial<NotificationLabels>;
1044
+ /**
1045
+ * What this reader has already seen, when the caller keeps it somewhere
1046
+ * durable. Leave it out and the bell remembers in localStorage, which is the
1047
+ * right home for a fact about one browser.
1048
+ *
1049
+ * A panel with one password has one owner, and there "this reader" and "this
1050
+ * project" are the same person — so a panel that saves its board can keep
1051
+ * this in it and have it survive a new browser. That is the caller's call to
1052
+ * make, not the component's, which is why both work.
1053
+ */
1054
+ seen?: string[];
1055
+ /**
1056
+ * Told which ids were just seen. Required for `seen` to mean anything —
1057
+ * though the bell also remembers in localStorage either way, so a caller
1058
+ * that has not saved yet does not show the same notifications again.
1059
+ */
1060
+ onSeen?: (ids: string[]) => void;
972
1061
  }
1062
+ declare function NotificationBell({ notifications, labels, seen: given, onSeen }: NotificationBellProps): React.JSX.Element | null;
1063
+
973
1064
  /**
974
- * Counts the board rather than believing a number somebody typed.
1065
+ * A block of text whose whole purpose is to end up somewhere else.
975
1066
  *
976
- * `today` is passed in so the same cards always produce the same answer — a
977
- * function that reads the clock cannot be checked.
1067
+ * Anything a panel tells someone to paste — a settings fragment, two commands,
1068
+ * a shell line — is read once and copied. So the copy is the primary action
1069
+ * and the text is selectable underneath it, rather than the other way round.
1070
+ *
1071
+ * The clipboard API is not always there. It needs a secure context and can be
1072
+ * refused outright, and a copy button that silently does nothing is worse than
1073
+ * no button at all: the reader walks away believing they have the text. So the
1074
+ * failure falls back to selecting the block, and says which of the two
1075
+ * happened.
978
1076
  */
979
- declare function boardFacts(cards: Card[], columns: ColumnDef[], today: string): BoardFacts;
980
- interface Trend {
981
- latest: Reading | undefined;
982
- previous: Reading | undefined;
983
- delta: number | null;
984
- /** True when the last move went against the metric's declared direction. */
985
- against: boolean;
1077
+ interface CopyBlockProps {
1078
+ /** Exactly what lands on the clipboard. Shown verbatim. */
1079
+ text: string;
1080
+ /** What this block is, above it. */
1081
+ label?: React.ReactNode;
1082
+ /** Why the reader wants it, and anything they must know before pasting. */
1083
+ note?: React.ReactNode;
1084
+ /** Wording, so the kit stays language-agnostic. */
1085
+ labels?: Partial<CopyBlockLabels>;
986
1086
  }
987
- declare function trendOf(metric: Metric): Trend;
988
- /**
989
- * Points for a sparkline in a 0–1 box, so the caller only decides the size.
990
- * A flat series sits on the middle line rather than collapsing to the floor.
991
- */
992
- declare function sparkPoints(readings: Reading[]): {
993
- x: number;
994
- y: number;
995
- }[];
1087
+ interface CopyBlockLabels {
1088
+ copy: string;
1089
+ copied: string;
1090
+ /** Used when the clipboard refused and the text was selected instead. */
1091
+ selected: string;
1092
+ }
1093
+ declare function CopyBlock({ text, label, note, labels }: CopyBlockProps): React.JSX.Element;
996
1094
 
997
- type Level = 'high' | 'medium' | 'low';
998
- type Owner = 'claude' | 'you';
999
1095
  /**
1000
- * What the owner wants done with a card next, in one tap.
1096
+ * What the panel needs in order to work, and what to do about what is missing.
1001
1097
  *
1002
- * Free text would be clearer to write and useless to obey: "solve it if you
1003
- * can" and "solve it yourself" are the same sentence and different
1004
- * instructions. A fixed set gives each one an exact, documented meaning, so a
1005
- * tap is a contract rather than a hint. Absent is a real state: no
1006
- * instruction, use judgement.
1098
+ * A panel with no token does not look broken — it looks fine until the moment
1099
+ * someone presses Save, and then it fails with something the person reading it
1100
+ * did not cause and cannot place. This turns that into a list they can read
1101
+ * before it happens, and act on without leaving the panel.
1007
1102
  *
1008
- * Three of them — explain, askme, hold — are refusals to act. Honouring them
1009
- * exactly matters more than the work: they are the only way to say "not yet".
1103
+ * The status comes from the server, which is the only place that can see the
1104
+ * environment. It reports *whether* a variable is set and never its value, so
1105
+ * this component has nothing worth leaking.
1010
1106
  */
1011
- declare const INTENTS: readonly ["decide", "explain", "solve", "do", "cheap", "safe", "fast", "askme", "hold"];
1012
- type Intent = (typeof INTENTS)[number];
1013
- interface IntentCopy {
1014
- label: Record<Lang, string>;
1015
- meaning: Record<Lang, string>;
1016
- }
1017
- declare const INTENT_COPY: Record<Intent, IntentCopy>;
1018
- interface Check {
1019
- id: string;
1020
- text: Text;
1021
- done: boolean;
1022
- }
1023
- interface Note {
1024
- id: string;
1025
- by: Owner;
1026
- date: string;
1027
- text: Text;
1028
- }
1029
- interface Card {
1107
+ type SetupStatus = 'ok' | 'missing' | 'unknown';
1108
+ interface SetupRequirement {
1030
1109
  id: string;
1031
- column: string;
1032
- title: Text;
1033
- body: Text;
1034
- area: string;
1035
- priority: Level;
1036
- risk: Level;
1037
- owner: Owner;
1038
- /** The one flag that means "I am asking you to do this". */
1039
- order: boolean;
1040
- intent: Intent | null;
1041
- checks: Check[];
1042
- notes: Note[];
1043
- createdAt: string;
1044
- updatedAt: string;
1110
+ /** What it is, in the reader's terms rather than the variable's. */
1111
+ label: React.ReactNode;
1112
+ status: SetupStatus;
1113
+ /** The environment variable behind it, when there is one to name. */
1114
+ variable?: string;
1115
+ /** What it lets the panel do — so a missing one has a visible cost. */
1116
+ enables?: React.ReactNode;
1117
+ /** Exactly what to do, shown only when it is missing. */
1118
+ fix?: React.ReactNode;
1045
1119
  /**
1046
- * Stamped by the board when the card enters the working column, and when it
1047
- * reaches a done column. They are written by the move, never typed, so a
1048
- * card that carries `completedAt` with no `startedAt` is one that was
1049
- * declared finished without ever being picked up — which is exactly the
1050
- * thing worth being able to see.
1120
+ * Information rather than a gate: drawn in the list, never walked as a step.
1121
+ *
1122
+ * Every other requirement here is an environment variable, and the
1123
+ * walkthrough's one action — re-read the environment from the server —
1124
+ * is what clears it. A requirement that action cannot clear parks the
1125
+ * walkthrough on it for ever, and because `unknown` is judged across the
1126
+ * whole list, one advisory row that nobody wired made every step answer a
1127
+ * successful check with "the panel could not ask its own server". That is
1128
+ * what the copied gate did the day it was added.
1051
1129
  */
1052
- startedAt?: string | null;
1053
- completedAt?: string | null;
1054
- }
1055
- interface ColumnDef {
1056
- id: string;
1057
- title: Text;
1058
- /** What the column means, shown on hover. Two words cannot say whether a
1059
- card sitting here is waiting on you or on Claude. */
1060
- hint?: Text;
1061
- /** Work in flight: entering this column stamps the card as started. */
1062
- active?: boolean;
1063
- /** Cards here are finished: they leave the open counts alone. */
1064
- done?: boolean;
1065
- }
1066
- /**
1067
- * One pass over the board: what a session changed, and which cards it
1068
- * touched. Written at the end of a run, so the panel keeps the account
1069
- * rather than the account living in a chat nobody can re-read.
1070
- */
1071
- interface Run {
1072
- id: string;
1073
- date: string;
1074
- by: Owner;
1075
- summary: Text;
1076
- /** Card ids this run touched, so a claim can be checked against the board. */
1077
- cards: string[];
1078
- /** The kit fingerprint at the time, twelve characters is enough to compare. */
1079
- fingerprint?: string;
1130
+ advisory?: boolean;
1080
1131
  }
1081
- interface HealthFact {
1082
- label: Text;
1083
- value: Text;
1084
- status: 'ok' | 'warning' | 'bad' | 'unknown';
1132
+ interface SetupGuideProps {
1133
+ requirements: SetupRequirement[];
1134
+ /** Shown when everything is in place, instead of an empty list. */
1135
+ readyMessage?: React.ReactNode;
1136
+ /** Wording, so the kit stays language-agnostic. */
1137
+ labels?: Partial<{
1138
+ ok: string;
1139
+ missing: string;
1140
+ unknown: string;
1141
+ whatToDo: string;
1142
+ }>;
1085
1143
  }
1144
+ declare function SetupGuide({ requirements, readyMessage, labels }: SetupGuideProps): React.JSX.Element;
1145
+
1086
1146
  /**
1087
- * A theme Claude derived from the project it is installed in.
1147
+ * The setup, walked one step at a time.
1088
1148
  *
1089
- * Stored with where it came from, not only with its colours. "Update the
1090
- * theme" is only a meaningful request if the next answer can be compared with
1091
- * this one — otherwise a regenerated theme is a different theme for reasons
1092
- * nobody can see. `from` is what makes the difference readable.
1149
+ * `SetupGuide` is the list: the right shape for somebody auditing a panel that
1150
+ * already works. This is the other reader — the one who installed it twenty
1151
+ * minutes ago, has four things missing and no idea which to do first. A wall
1152
+ * of four is not four times as useful as one; it is the thing people close.
1093
1153
  *
1094
- * `refreshRequested` is a message, not a trigger: a panel cannot run Claude.
1095
- * Tapping Update writes the date here, the bell shows it, and the next session
1096
- * regenerates. Pretending the button does the work itself would leave the
1097
- * owner waiting for something that was never going to happen.
1154
+ * Two rules hold it honest. It **checks rather than asks**: every step ends in
1155
+ * a button that re-reads the environment from the server, so the reader is
1156
+ * told whether it worked instead of ticking a box themselves. And it **never
1157
+ * claims what it cannot see**: a panel that could not reach its own endpoint
1158
+ * says the check failed, never that a variable is missing, because telling
1159
+ * somebody to add a token they already added is the worse of the two wrong
1160
+ * answers.
1161
+ *
1162
+ * There is no copy of the values here and nothing is written: the panel cannot
1163
+ * set an environment variable, and a button that appeared to would be the same
1164
+ * fault as one that appeared to install an update.
1098
1165
  */
1099
- interface ImportedTheme {
1100
- /** What to call it in the theme list. */
1101
- label: Text;
1102
- tokens: Record<string, string>;
1103
- form?: Record<string, string | number>;
1104
- from: {
1105
- /** Files the colours were read out of, so the claim can be checked. */
1106
- files: string[];
1107
- /** The colours actually found, before they were mapped to tokens. */
1108
- colours: string[];
1109
- /** Why these tokens and not others. One or two sentences. */
1110
- reasoning: Text;
1111
- };
1112
- importedAt: string;
1113
- importedBy: 'claude';
1114
- /** Set by the panel when the owner asks for it to be derived again. */
1115
- refreshRequested?: string | null;
1166
+ interface SetupWizardLabels {
1167
+ title: string;
1168
+ /** `(index, total)` — "Step 2 of 5". */
1169
+ progress: (index: number, total: number) => string;
1170
+ /** Shown above the one step in hand. */
1171
+ nowDo: string;
1172
+ /** The button that re-reads the environment. */
1173
+ check: string;
1174
+ checking: string;
1175
+ /** Said when a check ran and the step is still not in place. */
1176
+ stillMissing: string;
1177
+ /** Said when the panel could not ask its own server. */
1178
+ cannotTell: string;
1179
+ /** The heading over the finished steps. */
1180
+ doneTitle: string;
1181
+ /** Shown in place of a step when every requirement is in place. */
1182
+ readyTitle: string;
1183
+ readyBody: string;
1184
+ /** The two things to try before trusting the gate, as list items. */
1185
+ verify: string[];
1186
+ /** Closes the wizard. */
1187
+ close: string;
1188
+ /** The escape hatch: heading, explanation, and the prompt to copy. */
1189
+ stuckTitle: string;
1190
+ stuckBody: string;
1191
+ stuckPrompt: string;
1192
+ copy: string;
1193
+ copied: string;
1194
+ }
1195
+ interface SetupWizardProps {
1196
+ /** In the order they are to be done. The caller owns that order. */
1197
+ requirements: SetupRequirement[];
1198
+ labels: SetupWizardLabels;
1199
+ /** Re-reads the environment. Resolves when the answer has been applied. */
1200
+ onCheck: () => Promise<void> | void;
1201
+ /** Told when the reader closes it. */
1202
+ onClose?: () => void;
1116
1203
  }
1204
+ declare function SetupWizard({ requirements, labels, onCheck, onClose }: SetupWizardProps): React.JSX.Element;
1205
+
1117
1206
  /**
1118
- * What the owner has chosen, kept in the board so it survives a new browser.
1207
+ * Where a reader is in the setup, as one answer instead of a list to read.
1119
1208
  *
1120
- * The kit's default is to remember these per browser, in localStorage, because
1121
- * for a component "which notes this reader dismissed" is a fact about a reader
1122
- * and not about a project. A panel with one password is the case where those
1123
- * two are the same person: keeping it here means the theme, the language and
1124
- * what has already been read follow the owner to a new machine instead of
1125
- * greeting them with a tour they finished months ago.
1209
+ * `SetupGuide` draws every requirement at once, which is the right shape for
1210
+ * somebody checking a panel that already works. It is the wrong shape for
1211
+ * somebody who has just installed one: five things at once, four of them
1212
+ * missing, and no order to do them in reads as a wall rather than as a first
1213
+ * step. So the same requirements are walked one at a time, and this is the
1214
+ * function that decides which one.
1126
1215
  *
1127
- * The cost is real and worth naming: anyone who shares the password shares
1128
- * this. It holds a theme name and a list of ids — nothing worth stealing, and
1129
- * nothing anyone else would want.
1216
+ * The order the caller gives is the order they are done in — a token that
1217
+ * cannot be scoped before the repository is named is a step in the wrong
1218
+ * place, and that is the caller's knowledge, not this function's.
1130
1219
  */
1131
- interface Preferences {
1132
- theme?: string;
1133
- lang?: Lang;
1134
- /** Ids of notifications already read. Bounded when saved. */
1135
- seen?: string[];
1136
- /** Ids of guide notes already dismissed. */
1137
- guideDismissed?: string[];
1220
+ interface WizardStep {
1221
+ /** The requirement to show, or null when nothing is left to do. */
1222
+ current: SetupRequirement | null;
1223
+ /** 1-based, for "step 2 of 5". Zero when there is nothing left. */
1224
+ index: number;
1225
+ /** How many steps this setup has in total. */
1226
+ total: number;
1227
+ /** Every requirement that is in place, in the caller's order. */
1228
+ done: SetupRequirement[];
1229
+ /** True when every requirement is `ok`. */
1230
+ ready: boolean;
1138
1231
  /**
1139
- * Whether an update card is written as an order rather than a suggestion.
1232
+ * True when the panel could not ask the server, so nothing is claimed.
1140
1233
  *
1141
- * It does not make anything automatic — the panel cannot run npm, and this
1142
- * changes `order` on the card it offers to write, nothing else. Off means
1143
- * the card asks; on means it tells.
1234
+ * This is not "not ready": a panel that cannot reach its own endpoint knows
1235
+ * nothing about the environment, and telling somebody to add a token they
1236
+ * already added is worse than saying the check itself failed.
1144
1237
  */
1145
- updateAsOrder?: boolean;
1146
- }
1147
- interface PanelState {
1148
- v: 2;
1149
- project: string;
1150
- environment: Text;
1151
- updatedAt: string;
1152
- notice?: Text;
1153
- columns: ColumnDef[];
1154
- areas: {
1155
- id: string;
1156
- label: Text;
1157
- }[];
1158
- health: HealthFact[];
1159
- cards: Card[];
1160
- /** Newest first. Capped when saving, so the file cannot grow without end. */
1161
- runs: Run[];
1162
- /** What any of the work is for: goals, constraints, metrics, strategies. */
1163
- analysis?: Analysis;
1164
- /** The owner's choices, so they survive a new browser. */
1165
- preferences?: Preferences;
1166
- /** A theme Claude derived from this project, with where it came from. */
1167
- importedTheme?: ImportedTheme;
1238
+ unknown: boolean;
1168
1239
  }
1169
- declare const LEVELS: Level[];
1170
- declare const OWNERS: Owner[];
1171
- declare function newId(): string;
1172
- declare function today(): string;
1173
- /** A card is created empty and named second — the same as tapping "+" on paper. */
1174
- declare function emptyCard(column: string, area: string): Card;
1175
- /**
1176
- * How long a finished card stays on the board before it becomes history.
1177
- *
1178
- * A done column only grows. It is the one column nothing ever removes from,
1179
- * so a board that is being worked turns into a wall of things already
1180
- * finished, and the three columns that need reading get a third of the screen
1181
- * between them. A week is the window in which "what did we ship" is still a
1182
- * live question; after that it is a record, and a record does not need to be
1183
- * in the way.
1184
- */
1185
- declare const ARCHIVE_AFTER_DAYS = 7;
1240
+ declare function wizardStep(requirements: SetupRequirement[]): WizardStep;
1186
1241
  /**
1187
- * Whether a finished card has been finished long enough to stop being drawn.
1242
+ * Whether this panel has never been worked, which is what "first install"
1243
+ * means here.
1188
1244
  *
1189
- * Derived on every render and never written, which is the arrangement
1190
- * `boardFacts()` already uses: a flag stored on the card would be wrong the
1191
- * day after it was set, and setting it would mark the panel dirty for the
1192
- * passage of time. Here the answer simply becomes true on its own.
1245
+ * `paneltir init` writes a board with the columns already there and nothing
1246
+ * on them, so a board carrying no cards and no runs is one nobody has used
1247
+ * yet. That is a better signal than a version on disk: a project can install
1248
+ * the package, never open the panel, and update three times before anyone
1249
+ * sees it — and it is still that reader's first run.
1193
1250
  *
1194
- * **A card with no `completedAt` is never archived**, whatever column it sits
1195
- * in. It reached done without being stamped — a board edited by hand, or one
1196
- * from before the stamp existed — so its age is not known, and hiding a card
1197
- * whose age nobody knows is the same mistake as reporting a token missing
1198
- * because the check could not run. `unknown` is not `old`.
1199
- */
1200
- declare function isArchived(card: Card, doneColumns: ReadonlySet<string>, now?: string, afterDays?: number): boolean;
1201
- /**
1202
- * Applies the column's own meaning to a card that just moved into it: a card
1203
- * entering the working column is started, one reaching a done column is
1204
- * finished, and one dragged back out of done is not finished any more.
1251
+ * A panel that has been worked and then loses a variable is deliberately not
1252
+ * this. It needs the same instructions and gets them from the same wizard,
1253
+ * but it is told rather than taken over: somebody mid-sentence on a board
1254
+ * they know does not want a walkthrough opening on top of it.
1205
1255
  */
1206
- declare function stampForColumn(card: Card, columns: ColumnDef[]): Card;
1256
+ declare function isFirstRun(board: {
1257
+ cards?: unknown[];
1258
+ runs?: unknown[];
1259
+ }): boolean;
1260
+
1261
+ type Handler = (payload: Record<string, string>, event: MouseEvent) => void;
1207
1262
  /**
1208
- * Where a card the panel writes on the reader's behalf goes.
1209
- *
1210
- * The update card and the four "ask for this again" cards used to be born in
1211
- * a column literally called `next`. The default board has one; a board whose
1212
- * owner renamed it does not, and a card in a column that does not exist is
1213
- * the exact fault `validateBoard` names — drawn nowhere, and refused by the
1214
- * write path, so one tap on a button turned a saveable board into one that
1215
- * could not be saved and said so about a column the reader had never heard of.
1263
+ * A single click listener on document that dispatches by data-* attributes,
1264
+ * instead of one handler per element. Meant for a project's own actions; the
1265
+ * Board does not use it internally.
1216
1266
  *
1217
- * `next` when there is one, because that is what the button means. Otherwise
1218
- * the first column that is neither in flight nor finished nor the owner's:
1219
- * queued work goes where queued work already sits. And the first column of
1220
- * all when nothing else fits, since a board with no columns cannot pass the
1221
- * validator either way.
1267
+ * Example: useDelegatedClick('data-action', (payload) => { if (payload.action === 'delete') ... })
1268
+ * with buttons such as <button data-action="delete" data-id="42">
1222
1269
  */
1223
- declare function queueColumn(columns: ColumnDef[]): string;
1224
- /** A card claiming to be finished that was never picked up. */
1225
- declare function claimedWithoutStarting(card: Card): boolean;
1226
- interface Counts {
1227
- open: number;
1228
- orders: number;
1229
- yours: number;
1230
- decisions: number;
1231
- highRisk: number;
1232
- done: number;
1233
- }
1234
- declare function countCards(state: PanelState): Counts;
1235
- /** How many checklist steps are ticked, for the counter on a card. */
1236
- declare function checkProgress(card: Card): {
1237
- done: number;
1238
- total: number;
1239
- } | undefined;
1270
+ declare function useDelegatedClick(attribute: string, handler: Handler): void;
1271
+
1272
+ declare function useMediaQuery(query: string): boolean;
1240
1273
 
1241
1274
  /**
1242
1275
  * Reads a board and says what is wrong with it, in words.
@@ -1471,6 +1504,18 @@ interface UiStrings {
1471
1504
  releaseNone: string;
1472
1505
  updateCardTitle: (version: string) => string;
1473
1506
  updateCardBody: (version: string) => string;
1507
+ versionBumpAsk: string;
1508
+ versionBumpOpen: string;
1509
+ versionBumpHow: string;
1510
+ versionBumpTitle: string;
1511
+ versionBumpBody: string;
1512
+ versionBumpChecks: string[];
1513
+ installAsk: string;
1514
+ installAskOpen: string;
1515
+ installAskHow: string;
1516
+ installCardTitle: (tool: string) => string;
1517
+ installCardBody: (tool: string, file: string, snippet: string) => string;
1518
+ installCardChecks: string[];
1474
1519
  cardDetail: string;
1475
1520
  panelSettings: string;
1476
1521
  title: string;
@@ -2004,8 +2049,15 @@ interface MarketplaceViewProps {
2004
2049
  ui: UiStrings;
2005
2050
  /** Absent where there is no server to ask, so no dead button is drawn. */
2006
2051
  onRefresh?: () => void;
2052
+ /**
2053
+ * Writes a card asking Claude to install a plugin. Absent where the board
2054
+ * cannot be saved, so the button is not drawn where the card would be lost.
2055
+ */
2056
+ onRequestInstall?: (tool: MarketplaceTool, marketplace: Marketplace) => void;
2057
+ /** Whether an install card for this tool is already waiting on the board. */
2058
+ installRequested?: (toolId: string) => boolean;
2007
2059
  }
2008
- declare function MarketplaceView({ report, loading, failure, lang, ui, onRefresh }: MarketplaceViewProps): React.JSX.Element;
2060
+ declare function MarketplaceView({ report, loading, failure, lang, ui, onRefresh, onRequestInstall, installRequested, }: MarketplaceViewProps): React.JSX.Element;
2009
2061
 
2010
2062
  interface CardSheetProps {
2011
2063
  card: Card | null;
@@ -2018,4 +2070,4 @@ interface CardSheetProps {
2018
2070
  }
2019
2071
  declare function CardSheet({ card, state, lang, ui, onChange, onDelete, onClose }: CardSheetProps): React.JSX.Element | null;
2020
2072
 
2021
- export { ANALYSIS_SECTIONS, ARCHIVE_AFTER_DAYS, AlertBanner, type AlertBannerProps, type Analysis, type AnalysisSection, AnalysisView, BOARD_VERSION, Board, type BoardCardData, type BoardCheck, type BoardColumnData, type BoardFacts, type BoardMoveResult, type BoardProblem, type BoardProps, type BoardTag, CAPABILITIES, type Capability, Card$1 as Card, type CardProps, CardSheet, type Check, type Choice, type ChoiceOption, type ColumnDef, type Competitor, type Constraint, CopyBlock, type CopyBlockLabels, type CopyBlockProps, type Counts, DashboardHeader, type DashboardHeaderProps, type DashboardThemeForm, DashboardThemeProvider, type DashboardThemeTokens, DetailSection, type DetailSectionProps, DetailSheet, type DetailSheetProps, FilterChip, type FilterChipProps, FilterChipRow, type GateReading, type GateStamp, type GateVerdict, type Goal, GuideNote, type GuideNoteProps, GuideTour, type GuideTourProps, type HealthFact, HealthPill, type HealthPillProps, HealthPillRow, type HealthStatus, INTENTS, INTENT_COPY, type ImportedTheme, type InstallState, type Intent, type IntentCopy, LANGS, LANG_LABELS, LEVELS, type Lang, type Level, MARKETPLACE_NAME, MARKETPLACE_URL, type Market, type MarketplaceReport, type MarketplaceTool, MarketplaceView, type Metric, type Note, NotificationBell, type NotificationBellProps, type NotificationLabels, type NotificationTone, OWNERS, type Owner, PANELTIR_FILE_COUNT, PANELTIR_FINGERPRINT, PANELTIR_TEMPLATE_HASH, PANELTIR_VERSION, PRESET_PANEL_THEMES, PanelApp, type PanelAppProps, type Card as PanelCard, type PanelNotification, type PanelState, type PanelTheme, type PanelThemes, type Preferences, type Reading, type Run, SetupGuide, type SetupGuideProps, type SetupRequirement, type SetupStatus, SetupWizard, type SetupWizardLabels, type SetupWizardProps, type Severity, type SituationItem, SituationRow, type SituationRowProps, type SituationTone, StatTile, StatTileGrid, type StatTileProps, type Strategy, type Suggestion, type SuggestionStatus, THEME_FORMS, THEME_PRESETS, type Text, type ThemeFormName, type ThemePresetName, type Trend, UI, type UiStrings, type Weight, type WizardStep, applyMove, boardFacts, checkProgress, claimedWithoutStarting, claudeTheme, commandSnippet, countCards, cyberpunkTheme, decided, dropIndex, emptyCard, explainBoard, gateStatus, isArchived, isCard, isFirstRun, ledgerForm, midnightTheme, moveCardInColumns, newId, offeredThemes, oldMoneyTheme, panelForm, paneltirBuild, paperForm, queueColumn, readBoard, readStoredLang, repositorySnippet, resetGuide, sparkPoints, stampForColumn, storeLang, text, today, trendOf, undecided, untranslated, useDashboardForm, useDashboardTheme, useDelegatedClick, useGuideNote, useMediaQuery, validateBoard, warRoomForm, wizardStep, writeText };
2073
+ export { ANALYSIS_SECTIONS, ARCHIVE_AFTER_DAYS, AlertBanner, type AlertBannerProps, type Analysis, type AnalysisSection, AnalysisView, BOARD_VERSION, Board, type BoardCardData, type BoardCheck, type BoardColumnData, type BoardFacts, type BoardMoveResult, type BoardProblem, type BoardProps, type BoardTag, CAPABILITIES, type Capability, Card$1 as Card, type CardProps, CardSheet, type Check, type Choice, type ChoiceOption, type ColumnDef, type Competitor, type Constraint, CopyBlock, type CopyBlockLabels, type CopyBlockProps, type Counts, DashboardHeader, type DashboardHeaderProps, type DashboardThemeForm, DashboardThemeProvider, type DashboardThemeTokens, DetailSection, type DetailSectionProps, DetailSheet, type DetailSheetProps, FilterChip, type FilterChipProps, FilterChipRow, type GateReading, type GateStamp, type GateVerdict, type Goal, GuideNote, type GuideNoteProps, GuideTour, type GuideTourProps, type HealthFact, HealthPill, type HealthPillProps, HealthPillRow, type HealthStatus, INTENTS, INTENT_COPY, type ImportedTheme, type InstallState, type Intent, type IntentCopy, LANGS, LANG_LABELS, LEVELS, type Lang, type Level, MARKETPLACE_NAME, MARKETPLACE_URL, type Market, type MarketplaceReport, type MarketplaceTool, MarketplaceView, type Metric, type Note, NotificationBell, type NotificationBellProps, type NotificationLabels, type NotificationTone, OWNERS, type Owner, PANELTIR_FILE_COUNT, PANELTIR_FINGERPRINT, PANELTIR_TEMPLATE_HASH, PANELTIR_VERSION, PRESET_PANEL_THEMES, PanelApp, type PanelAppProps, type Card as PanelCard, type PanelNotification, type PanelState, type PanelTheme, type PanelThemes, type Preferences, type Reading, type RequestKind, type Run, SetupGuide, type SetupGuideProps, type SetupRequirement, type SetupStatus, SetupWizard, type SetupWizardLabels, type SetupWizardProps, type Severity, type SituationItem, SituationRow, type SituationRowProps, type SituationTone, StatTile, StatTileGrid, type StatTileProps, type Strategy, type Suggestion, type SuggestionStatus, THEME_FORMS, THEME_PRESETS, type Text, type ThemeFormName, type ThemePresetName, type Trend, UI, type UiStrings, type Weight, type WizardStep, applyMove, boardFacts, checkProgress, claimedWithoutStarting, claudeTheme, commandSnippet, countCards, cyberpunkTheme, decided, dropIndex, emptyCard, explainBoard, gateStatus, isArchived, isCard, isFirstRun, ledgerForm, midnightTheme, moveCardInColumns, newId, offeredThemes, oldMoneyTheme, panelForm, paneltirBuild, paperForm, pendingRequest, queueColumn, readBoard, readStoredLang, repositorySnippet, requestId, resetGuide, sparkPoints, stampForColumn, storeLang, text, today, trendOf, undecided, untranslated, useDashboardForm, useDashboardTheme, useDelegatedClick, useGuideNote, useMediaQuery, validateBoard, warRoomForm, wizardStep, writeText };