beadcyte 0.4.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.
Files changed (108) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +661 -0
  3. package/README.md +386 -0
  4. package/THIRD_PARTY_NOTICES.md +56 -0
  5. package/bin/beadcyte.mjs +60 -0
  6. package/package.json +77 -0
  7. package/src/changelog-cli.mjs +105 -0
  8. package/src/changelog.mjs +196 -0
  9. package/src/cli.mjs +577 -0
  10. package/src/estimator.mjs +314 -0
  11. package/src/format.mjs +22 -0
  12. package/src/history-walk.mjs +170 -0
  13. package/src/index.mjs +5 -0
  14. package/src/mutate.mjs +193 -0
  15. package/src/projects.mjs +120 -0
  16. package/src/provenance.mjs +75 -0
  17. package/src/review-hours.mjs +117 -0
  18. package/src/roster-path.mjs +24 -0
  19. package/src/scheduler.mjs +424 -0
  20. package/src/serve.mjs +411 -0
  21. package/src/server-state.mjs +105 -0
  22. package/src/ship.mjs +178 -0
  23. package/src/stage-waits.mjs +69 -0
  24. package/src/start.mjs +111 -0
  25. package/src/stop.mjs +66 -0
  26. package/src/velocity.mjs +209 -0
  27. package/src/web/App.vue +691 -0
  28. package/src/web/app.css +54 -0
  29. package/src/web/assets/favicon.svg +12 -0
  30. package/src/web/avatar.ts +53 -0
  31. package/src/web/bead-detail.ts +240 -0
  32. package/src/web/changelog-view.ts +41 -0
  33. package/src/web/components/BeadDrawer.vue +1883 -0
  34. package/src/web/components/BeadSubGraph.vue +326 -0
  35. package/src/web/components/BeadSubGraphOverlay.vue +192 -0
  36. package/src/web/components/BeadTooltip.vue +516 -0
  37. package/src/web/components/BeadcyteMark.vue +64 -0
  38. package/src/web/components/BeadsGantt.vue +2125 -0
  39. package/src/web/components/BeadsGrid.vue +468 -0
  40. package/src/web/components/BeadsIncytes.vue +567 -0
  41. package/src/web/components/BeadsMine.vue +325 -0
  42. package/src/web/components/BeadsTable.vue +335 -0
  43. package/src/web/components/ChangelogOverlay.vue +198 -0
  44. package/src/web/components/ContextMenu.vue +386 -0
  45. package/src/web/components/ControlsPanel.vue +476 -0
  46. package/src/web/components/CostTrend.vue +206 -0
  47. package/src/web/components/FilterPopover.vue +245 -0
  48. package/src/web/components/GroupProgress.vue +274 -0
  49. package/src/web/components/LoadMeter.vue +144 -0
  50. package/src/web/components/MineRow.vue +28 -0
  51. package/src/web/components/OptionsMenu.vue +825 -0
  52. package/src/web/components/PriorityChip.vue +105 -0
  53. package/src/web/components/ScoreStrip.vue +131 -0
  54. package/src/web/components/SearchPalette.vue +210 -0
  55. package/src/web/components/ShipTrend.vue +510 -0
  56. package/src/web/components/ShortcutsOverlay.vue +164 -0
  57. package/src/web/components/Term.vue +177 -0
  58. package/src/web/components/Toast.vue +50 -0
  59. package/src/web/components/TriageMeters.vue +426 -0
  60. package/src/web/components/TypeChip.vue +96 -0
  61. package/src/web/components/Walkthrough.vue +209 -0
  62. package/src/web/components/WhatIfPanel.vue +206 -0
  63. package/src/web/components/WipBullets.vue +191 -0
  64. package/src/web/components/filter-option.ts +9 -0
  65. package/src/web/composables/url-codec.ts +136 -0
  66. package/src/web/composables/useBeadTooltip.ts +148 -0
  67. package/src/web/composables/useKeyboard.ts +97 -0
  68. package/src/web/composables/useLiveRefresh.ts +69 -0
  69. package/src/web/composables/useTheme.ts +125 -0
  70. package/src/web/composables/useUrlState.ts +208 -0
  71. package/src/web/controls-scope.ts +83 -0
  72. package/src/web/cost.ts +251 -0
  73. package/src/web/dep-headings.ts +62 -0
  74. package/src/web/economics.ts +440 -0
  75. package/src/web/env.d.ts +85 -0
  76. package/src/web/frontier.ts +208 -0
  77. package/src/web/gantt-viewport.ts +99 -0
  78. package/src/web/highlights.ts +124 -0
  79. package/src/web/index.html +46 -0
  80. package/src/web/insights.ts +107 -0
  81. package/src/web/keybindings.ts +200 -0
  82. package/src/web/load-meter.ts +72 -0
  83. package/src/web/main.ts +20 -0
  84. package/src/web/markdown.ts +14 -0
  85. package/src/web/mine.ts +137 -0
  86. package/src/web/mutations.ts +21 -0
  87. package/src/web/person.ts +102 -0
  88. package/src/web/projects-text.ts +15 -0
  89. package/src/web/projects.ts +188 -0
  90. package/src/web/refresh.ts +47 -0
  91. package/src/web/search.ts +50 -0
  92. package/src/web/shortcuts.ts +113 -0
  93. package/src/web/status-filter.ts +48 -0
  94. package/src/web/store.ts +1378 -0
  95. package/src/web/style-audit.mjs +346 -0
  96. package/src/web/styles-alt.css +111 -0
  97. package/src/web/styles-ported.css +270 -0
  98. package/src/web/subgraph.ts +362 -0
  99. package/src/web/table.ts +201 -0
  100. package/src/web/theme.ts +88 -0
  101. package/src/web/tokens.css +168 -0
  102. package/src/web/triage.ts +914 -0
  103. package/src/web/view-model.ts +717 -0
  104. package/src/web/walkthrough.ts +133 -0
  105. package/src/web/watchlist.ts +47 -0
  106. package/src/web/whatif.ts +291 -0
  107. package/src/web/window.ts +73 -0
  108. package/src/web/wip.ts +83 -0
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ // TypeChip.vue — issue-type icon+label chip. Same visual footprint as
3
+ // PriorityChip so they line up cleanly side-by-side in dossiers.
4
+ //
5
+ // task → CheckSquare
6
+ // bug → Bug
7
+ // feature → Sparkles
8
+ // epic → Layers
9
+ // chore → Wrench
10
+ // docs → FileText
11
+ // question → HelpCircle
12
+ // * → Square (fallback)
13
+
14
+ import { computed } from "vue";
15
+ import {
16
+ CheckSquare,
17
+ Bug,
18
+ Sparkles,
19
+ Layers,
20
+ Wrench,
21
+ FileText,
22
+ HelpCircle,
23
+ Square,
24
+ } from "@lucide/vue";
25
+
26
+ const props = defineProps<{
27
+ type: string;
28
+ compact?: boolean;
29
+ }>();
30
+
31
+ const icon = computed(() => {
32
+ switch ((props.type ?? "").toLowerCase()) {
33
+ case "task":
34
+ return CheckSquare;
35
+ case "bug":
36
+ return Bug;
37
+ case "feature":
38
+ return Sparkles;
39
+ case "epic":
40
+ return Layers;
41
+ case "chore":
42
+ return Wrench;
43
+ case "docs":
44
+ case "documentation":
45
+ return FileText;
46
+ case "question":
47
+ return HelpCircle;
48
+ default:
49
+ return Square;
50
+ }
51
+ });
52
+ </script>
53
+
54
+ <template>
55
+ <span class="type-chip" :class="`type-${type}`" :title="type">
56
+ <component :is="icon" class="icon type-icon" />
57
+ <span v-if="!compact" class="type-text">{{ type }}</span>
58
+ </span>
59
+ </template>
60
+
61
+ <style>
62
+ .type-chip {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ gap: 3px;
66
+ padding: 2px 6px 2px 5px;
67
+ border-radius: 3px;
68
+ font-size: 10px;
69
+ font-weight: 500;
70
+ line-height: 1;
71
+ white-space: nowrap;
72
+ border: 1px solid var(--c-border);
73
+ color: var(--c-dim);
74
+ text-transform: lowercase;
75
+ }
76
+
77
+ .type-chip .type-icon {
78
+ flex-shrink: 0;
79
+ }
80
+
81
+ .type-chip.type-bug {
82
+ color: var(--c-danger-bright);
83
+ border-color: oklch(from var(--c-today) l c h / 30%);
84
+ }
85
+ .type-chip.type-feature {
86
+ color: var(--c-shipped-bright);
87
+ border-color: oklch(from var(--c-shipped) l c h / 30%);
88
+ }
89
+ .type-chip.type-epic {
90
+ color: var(--c-epic);
91
+ border-color: oklch(from var(--c-epic) l c h / 35%);
92
+ }
93
+ .type-chip.type-chore {
94
+ color: var(--c-dim-bright);
95
+ }
96
+ </style>
@@ -0,0 +1,209 @@
1
+ <script setup lang="ts">
2
+ // Walkthrough.vue — the first-run tour (bp-67g.48). A small fixed card in
3
+ // the bottom-right corner, NOT a modal: no backdrop, no aria-modal, focus is
4
+ // never moved or trapped, so the app behind it stays usable while it is up.
5
+ // Skip or Done marks it seen; it never appears again unasked, and the options
6
+ // menu re-opens it. Escape closes it too, last in the registry's close order,
7
+ // so it never steals an Escape meant for the drawer or an overlay.
8
+ //
9
+ // Content and memory live in walkthrough.ts; this file only lays them out.
10
+
11
+ import { computed, ref, watch } from "vue";
12
+ import { X } from "@lucide/vue";
13
+ import { useBeadsStore } from "../store";
14
+ import { WALKTHROUGH_STEPS, barKindNotes, markWalkthroughSeen } from "../walkthrough";
15
+
16
+ const store = useBeadsStore();
17
+ const i = ref(0);
18
+ const step = computed(() => WALKTHROUGH_STEPS[i.value]!);
19
+ const last = computed(() => i.value === WALKTHROUGH_STEPS.length - 1);
20
+ const kinds = barKindNotes();
21
+
22
+ // Start from the top each time it is opened.
23
+ watch(
24
+ () => store.walkthroughOpen,
25
+ (open) => {
26
+ if (open) i.value = 0;
27
+ },
28
+ );
29
+
30
+ function finish() {
31
+ markWalkthroughSeen();
32
+ store.closeWalkthrough();
33
+ }
34
+
35
+ function next() {
36
+ if (last.value) finish();
37
+ else i.value++;
38
+ }
39
+
40
+ const showMe = computed(() => step.value.view && step.value.view !== store.view);
41
+ </script>
42
+
43
+ <template>
44
+ <Teleport to="body">
45
+ <aside
46
+ v-if="store.walkthroughOpen"
47
+ class="walkthrough"
48
+ role="complementary"
49
+ aria-label="Walkthrough"
50
+ >
51
+ <header>
52
+ <span class="count">{{ i + 1 }} / {{ WALKTHROUGH_STEPS.length }}</span>
53
+ <h2>{{ step.title }}</h2>
54
+ <button type="button" class="close" title="Skip the walkthrough (Esc)" aria-label="Skip the walkthrough" @click="finish">
55
+ <X class="icon" />
56
+ </button>
57
+ </header>
58
+ <p class="body">{{ step.body }}</p>
59
+ <dl v-if="step.kinds" class="kinds">
60
+ <template v-for="k in kinds" :key="k.kind">
61
+ <dt><code>{{ k.kind }}</code></dt>
62
+ <dd>{{ k.note }}</dd>
63
+ </template>
64
+ </dl>
65
+ <p class="also">Also in: {{ step.alsoIn }}</p>
66
+ <footer>
67
+ <button v-if="showMe" type="button" class="btn subtle" @click="store.setView(step.view!)">show me</button>
68
+ <span class="spacer" />
69
+ <button type="button" class="btn subtle" :disabled="i === 0" @click="i--">back</button>
70
+ <button type="button" class="btn" @click="next">{{ last ? "done" : "next" }}</button>
71
+ </footer>
72
+ </aside>
73
+ </Teleport>
74
+ </template>
75
+
76
+ <style>
77
+ .walkthrough {
78
+ position: fixed;
79
+ right: 20px;
80
+ bottom: 20px;
81
+ z-index: 800; /* under the overlays (900+), over the page */
82
+ width: min(420px, calc(100vw - 40px));
83
+ max-height: min(70vh, 560px);
84
+ overflow: auto;
85
+ background: var(--c-panel);
86
+ color: var(--c-text);
87
+ border: 1px solid var(--c-border-strong);
88
+ border-radius: 8px;
89
+ box-shadow: var(--shadow-pop);
90
+ padding: 12px 14px 12px;
91
+ font-size: 12px;
92
+ line-height: 1.5;
93
+ }
94
+
95
+ .walkthrough header {
96
+ display: flex;
97
+ align-items: baseline;
98
+ gap: 10px;
99
+ margin-bottom: 6px;
100
+ }
101
+
102
+ .walkthrough .count {
103
+ font-family: ui-monospace, monospace;
104
+ font-size: 10px;
105
+ color: var(--c-dim);
106
+ white-space: nowrap;
107
+ }
108
+
109
+ .walkthrough h2 {
110
+ flex: 1;
111
+ margin: 0;
112
+ font-size: 13px;
113
+ font-weight: 600;
114
+ }
115
+
116
+ .walkthrough .close {
117
+ all: unset;
118
+ cursor: pointer;
119
+ color: var(--c-dim);
120
+ display: inline-flex;
121
+ padding: 2px;
122
+ border-radius: 4px;
123
+ align-self: center;
124
+ }
125
+
126
+ .walkthrough .close:hover,
127
+ .walkthrough .close:focus-visible {
128
+ color: var(--c-text);
129
+ background: var(--c-lift);
130
+ }
131
+
132
+ .walkthrough .close .icon {
133
+ width: 14px;
134
+ height: 14px;
135
+ }
136
+
137
+ .walkthrough .body {
138
+ margin: 0;
139
+ }
140
+
141
+ .walkthrough .kinds {
142
+ display: grid;
143
+ grid-template-columns: max-content 1fr;
144
+ gap: 2px 10px;
145
+ margin: 8px 0 0;
146
+ font-size: 11px;
147
+ }
148
+
149
+ .walkthrough .kinds dt code {
150
+ font-family: ui-monospace, monospace;
151
+ font-size: 11px;
152
+ color: var(--c-accent);
153
+ }
154
+
155
+ .walkthrough .kinds dd {
156
+ margin: 0;
157
+ color: var(--c-text);
158
+ }
159
+
160
+ .walkthrough .also {
161
+ margin: 8px 0 0;
162
+ font-size: 11px;
163
+ color: var(--c-dim);
164
+ font-style: italic;
165
+ }
166
+
167
+ .walkthrough footer {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 6px;
171
+ margin-top: 10px;
172
+ }
173
+
174
+ .walkthrough .spacer {
175
+ flex: 1;
176
+ }
177
+
178
+ .walkthrough .btn {
179
+ all: unset;
180
+ cursor: pointer;
181
+ padding: 4px 10px;
182
+ border-radius: 4px;
183
+ font-size: 11px;
184
+ font-weight: 600;
185
+ background: var(--c-accent);
186
+ color: var(--c-bg);
187
+ }
188
+
189
+ .walkthrough .btn.subtle {
190
+ background: transparent;
191
+ color: var(--c-dim);
192
+ border: 1px solid var(--c-border);
193
+ font-weight: 400;
194
+ }
195
+
196
+ .walkthrough .btn.subtle:hover:not(:disabled) {
197
+ color: var(--c-text);
198
+ }
199
+
200
+ .walkthrough .btn:disabled {
201
+ opacity: 0.4;
202
+ cursor: not-allowed;
203
+ }
204
+
205
+ .walkthrough .btn:focus-visible {
206
+ outline: 2px solid var(--c-accent);
207
+ outline-offset: 1px;
208
+ }
209
+ </style>
@@ -0,0 +1,206 @@
1
+ <script setup lang="ts">
2
+ // WhatIfPanel.vue — the what-if plan's card (bp-q56): the change set, what
3
+ // moved and by how much, the frontier per person before and after, an idea
4
+ // form, discard, and confirm. Confirm hands the change set to the apply
5
+ // path (bp-2fq); until that lands it shows the bd commands to paste, which
6
+ // is also the honest answer to "what would this do to my tracker".
7
+ //
8
+ // Confirm is two steps (bp-2fq): the commands are shown first, then "run
9
+ // them" writes each through the same whitelisted endpoint the quick actions
10
+ // use, and the report says what happened to every change.
11
+ //
12
+ // Not a modal. Sits bottom-left so it does not fight the walkthrough's
13
+ // corner, and stays while the views underneath show the what-if plan.
14
+
15
+ import { computed, ref } from "vue";
16
+ import { X, Plus } from "@lucide/vue";
17
+ import { useBeadsStore } from "../store";
18
+ import { describeChange, describeDelta, whatIfCommands, IDEA_PREFIX } from "../whatif";
19
+
20
+ const store = useBeadsStore();
21
+ const diff = computed(() => store.whatIfDiff);
22
+ const titleOf = (id: string) => store.beadById(id)?.title;
23
+ const showCommands = ref(false);
24
+ const commands = computed(() => whatIfCommands(store.whatIf));
25
+
26
+ // The idea form.
27
+ const ideaOpen = ref(false);
28
+ const ideaTitle = ref("");
29
+ const ideaType = ref("feature");
30
+ const ideaPriority = ref(2);
31
+ const ideaHours = ref(4);
32
+ const ideaAssignee = ref<string>("");
33
+ const handles = computed(() => (store.calibratedRoster ?? store.roster)?.humans.map((h) => h.handle) ?? []);
34
+ let ideaSeq = 1;
35
+ function addIdea() {
36
+ const title = ideaTitle.value.trim();
37
+ if (!title) return;
38
+ store.addWhatIf({
39
+ kind: "idea",
40
+ id: `${IDEA_PREFIX}${ideaSeq++}`,
41
+ title,
42
+ type: ideaType.value,
43
+ priority: ideaPriority.value,
44
+ hours: Math.max(0.25, ideaHours.value),
45
+ assignee: ideaAssignee.value || null,
46
+ });
47
+ ideaTitle.value = "";
48
+ ideaOpen.value = false;
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <Teleport to="body">
54
+ <aside v-if="store.whatIfActive || store.whatIfPanelOpen || store.whatIfReport" class="whatif" role="complementary" aria-label="What-if plan">
55
+ <header>
56
+ <h2>What-if <span class="count">{{ store.whatIf.length }} change{{ store.whatIf.length === 1 ? "" : "s" }}</span></h2>
57
+ <button type="button" class="close" title="Close the panel (changes stay)" aria-label="Close the panel" @click="store.closeWhatIfPanel()"><X class="icon" /></button>
58
+ </header>
59
+ <ul v-if="store.whatIfReport" class="report">
60
+ <li v-for="(r, i) in store.whatIfReport" :key="i" :class="{ ok: r.ok, failed: !r.ok }">
61
+ <span class="mark">{{ r.ok ? "✓" : "✗" }}</span>
62
+ <span class="text">{{ r.text }}</span>
63
+ <span v-if="r.detail" class="detail">{{ r.detail }}</span>
64
+ </li>
65
+ <li class="report-actions"><button type="button" class="btn subtle" @click="store.whatIfReport = null">dismiss</button></li>
66
+ </ul>
67
+ <p class="lede">
68
+ Every view shows the plan as it would be. Nothing is written until you confirm.
69
+ </p>
70
+
71
+ <ul v-if="store.whatIf.length" class="changes">
72
+ <li v-for="(c, i) in store.whatIf" :key="i">
73
+ <span class="change">{{ describeChange(c, titleOf) }}</span>
74
+ <button type="button" class="remove" :aria-label="`Remove change ${i + 1}`" @click="store.removeWhatIf(i)"><X class="icon" /></button>
75
+ </li>
76
+ </ul>
77
+ <p v-else class="hint">No changes yet. Reassign, reprioritise or mark spec:ready from a bead's drawer, change a cap from a person's roster line on incytes, or add an idea below.</p>
78
+
79
+ <button v-if="!ideaOpen" type="button" class="btn subtle" @click="ideaOpen = true"><Plus class="icon" /> add an idea</button>
80
+ <form v-else class="idea" @submit.prevent="addIdea">
81
+ <input v-model="ideaTitle" type="text" placeholder="the idea, as a title" required />
82
+ <div class="row">
83
+ <select v-model="ideaType" aria-label="type"><option>feature</option><option>task</option><option>bug</option><option>chore</option></select>
84
+ <select v-model.number="ideaPriority" aria-label="priority"><option v-for="p in [0, 1, 2, 3, 4]" :key="p" :value="p">P{{ p }}</option></select>
85
+ <label class="hours">~<input v-model.number="ideaHours" type="number" min="0.25" step="0.25" aria-label="hours of work" />h</label>
86
+ <select v-model="ideaAssignee" aria-label="assignee"><option value="">let the scheduler route it</option><option v-for="h in handles" :key="h" :value="h">{{ h }}</option></select>
87
+ </div>
88
+ <div class="row">
89
+ <button type="button" class="btn subtle" @click="ideaOpen = false">cancel</button>
90
+ <button type="submit" class="btn">add to the what-if</button>
91
+ </div>
92
+ </form>
93
+
94
+ <template v-if="diff">
95
+ <h3>What moves <span class="count">{{ diff.moved.length }}</span></h3>
96
+ <ul v-if="diff.moved.length" class="moves">
97
+ <li v-for="m in diff.moved.slice(0, 12)" :key="m.id">
98
+ <code>{{ m.id }}</code>
99
+ <span class="title">{{ m.title }}</span>
100
+ <span class="delta" :class="{ later: m.deltaDays > 0.25, earlier: m.deltaDays < -0.25 }">{{ describeDelta(m.deltaDays) }}</span>
101
+ <span v-if="m.assigneeBefore !== m.assigneeAfter" class="hands">{{ m.assigneeBefore ?? "—" }} → {{ m.assigneeAfter ?? "—" }}</span>
102
+ </li>
103
+ <li v-if="diff.moved.length > 12" class="more">and {{ diff.moved.length - 12 }} more</li>
104
+ </ul>
105
+ <p v-else class="hint">No landing moves by more than a quarter day{{ diff.unchanged ? ` (${diff.unchanged} unchanged)` : "" }}.</p>
106
+
107
+ <template v-if="diff.added.length">
108
+ <h3>Ideas land</h3>
109
+ <ul class="moves">
110
+ <li v-for="a in diff.added" :key="a.id">
111
+ <code>{{ a.id }}</code><span class="title">{{ a.title }}</span>
112
+ <span class="delta">{{ a.land === null ? "not within the window" : `day ${a.land.toFixed(1)}` }}</span>
113
+ <span class="hands">{{ a.assignee ?? "unrouted" }}</span>
114
+ </li>
115
+ </ul>
116
+ </template>
117
+
118
+ <h3>Frontier <span class="count">next {{ diff.frontierDays }} days</span></h3>
119
+ <table class="frontier">
120
+ <tbody>
121
+ <tr v-for="f in diff.frontier" :key="f.handle">
122
+ <td><code>{{ f.handle }}</code></td>
123
+ <td class="num">{{ f.before }}</td>
124
+ <td class="arrow">→</td>
125
+ <td class="num" :class="{ later: f.after < f.before, earlier: f.after > f.before }">{{ f.after }}</td>
126
+ <td class="lands">land</td>
127
+ </tr>
128
+ </tbody>
129
+ </table>
130
+ </template>
131
+
132
+ <footer>
133
+ <button type="button" class="btn subtle" :disabled="!store.whatIf.length || store.whatIfApplying" @click="store.discardWhatIf(); store.whatIfReport = null">discard</button>
134
+ <span class="spacer" />
135
+ <button type="button" class="btn" :disabled="!store.whatIf.length || store.whatIfApplying" @click="showCommands = !showCommands">{{ showCommands ? "back" : "confirm…" }}</button>
136
+ </footer>
137
+ <div v-if="showCommands && store.whatIf.length" class="commands">
138
+ <p class="hint">Confirming runs these through bd, one after another, as the quick actions do. Caps and unassigns are not bd facts and are reported instead.</p>
139
+ <pre>{{ commands.join("\n") }}</pre>
140
+ <button type="button" class="btn danger" :disabled="store.whatIfApplying" @click="store.confirmWhatIf(); showCommands = false">{{ store.whatIfApplying ? "running…" : `run ${commands.length} command${commands.length === 1 ? "" : "s"}` }}</button>
141
+ </div>
142
+ </aside>
143
+ </Teleport>
144
+ </template>
145
+
146
+ <style>
147
+ .whatif {
148
+ position: fixed;
149
+ left: 20px;
150
+ bottom: 20px;
151
+ z-index: 800;
152
+ width: min(460px, calc(100vw - 40px));
153
+ max-height: min(72vh, 640px);
154
+ overflow: auto;
155
+ background: var(--c-panel);
156
+ color: var(--c-text);
157
+ border: 1px solid var(--c-accent);
158
+ border-radius: 8px;
159
+ box-shadow: var(--shadow-pop);
160
+ padding: 12px 14px;
161
+ font-size: 12px;
162
+ line-height: 1.5;
163
+ }
164
+
165
+ .whatif header { display: flex; align-items: center; gap: 8px; }
166
+ .whatif h2 { flex: 1; margin: 0; font-size: 13px; font-weight: 600; }
167
+ .whatif h3 { margin: 12px 0 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--c-dim); }
168
+ .whatif .count { font-weight: 400; color: var(--c-dim); font-size: 11px; margin-left: 6px; text-transform: none; letter-spacing: normal; }
169
+ .whatif .close, .whatif .remove { all: unset; cursor: pointer; color: var(--c-dim); display: inline-flex; padding: 2px; border-radius: 4px; }
170
+ .whatif .close:hover, .whatif .remove:hover { color: var(--c-text); background: var(--c-lift); }
171
+ .whatif .icon { width: 14px; height: 14px; }
172
+ .whatif .lede, .whatif .hint { margin: 4px 0 0; color: var(--c-dim); font-size: 11px; }
173
+ .whatif ul { list-style: none; margin: 6px 0 0; padding: 0; }
174
+ .whatif .changes li { display: flex; align-items: center; gap: 6px; padding: 2px 0; }
175
+ .whatif .change { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
176
+ .whatif .moves li { display: grid; grid-template-columns: max-content 1fr max-content max-content; gap: 0 8px; align-items: baseline; padding: 2px 0; }
177
+ .whatif .moves .title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--c-dim); }
178
+ .whatif code { font-family: ui-monospace, monospace; font-size: 11px; color: var(--c-accent); }
179
+ .whatif .delta { font-variant-numeric: tabular-nums; }
180
+ .whatif .delta.later, .whatif .num.later { color: var(--c-danger); }
181
+ .whatif .delta.earlier, .whatif .num.earlier { color: var(--c-shipped); }
182
+ .whatif .hands { font-size: 11px; color: var(--c-dim); }
183
+ .whatif .more { color: var(--c-dim); }
184
+ .whatif .frontier { border-collapse: collapse; margin-top: 4px; }
185
+ .whatif .frontier td { padding: 1px 6px 1px 0; }
186
+ .whatif .frontier .num { text-align: right; font-variant-numeric: tabular-nums; }
187
+ .whatif .frontier .arrow, .whatif .frontier .lands { color: var(--c-dim); }
188
+ .whatif footer { display: flex; align-items: center; gap: 6px; margin-top: 12px; }
189
+ .whatif .spacer { flex: 1; }
190
+ .whatif .btn { all: unset; cursor: pointer; padding: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--c-accent); color: var(--c-bg); display: inline-flex; align-items: center; gap: 4px; }
191
+ .whatif .btn.subtle { background: transparent; color: var(--c-dim); border: 1px solid var(--c-border); font-weight: 400; }
192
+ .whatif .btn:disabled { opacity: 0.4; cursor: not-allowed; }
193
+ .whatif .idea { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
194
+ .whatif .idea .row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
195
+ .whatif input, .whatif select { font: inherit; padding: 4px 8px; border: 1px solid var(--c-border-strong); border-radius: 4px; background: var(--c-bg); color: var(--c-text); }
196
+ .whatif input[type="text"] { width: 100%; box-sizing: border-box; }
197
+ .whatif .hours input { width: 56px; }
198
+ .whatif .commands pre { margin: 6px 0 0; padding: 8px; background: var(--c-bg); border: 1px solid var(--c-border); border-radius: 4px; font-size: 11px; white-space: pre-wrap; }
199
+ .whatif .commands .btn { margin-top: 8px; }
200
+ .whatif .btn.danger { background: var(--c-danger); }
201
+ .whatif .report li { display: grid; grid-template-columns: max-content 1fr; gap: 0 8px; padding: 2px 0; }
202
+ .whatif .report .detail { grid-column: 2; font-size: 11px; color: var(--c-dim); }
203
+ .whatif .report .ok .mark { color: var(--c-shipped); }
204
+ .whatif .report .failed .mark { color: var(--c-danger); }
205
+ .whatif .report .report-actions { display: block; padding-top: 4px; }
206
+ </style>