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,198 @@
1
+ <script setup lang="ts">
2
+ // ChangelogOverlay.vue — the in-app changelog (bp-67g.46). Opens from the
3
+ // header's version tag and the options menu's About; closes on Escape
4
+ // (through keybindings.ts), the close button or a click on the backdrop.
5
+ //
6
+ // The markdown is CHANGELOG.md itself, inlined by Vite at build time, so
7
+ // reading it needs no network and a published install has its own copy. The
8
+ // link out is configuration — BEADCYTE_CHANGELOG_URL, reflected by serve.mjs
9
+ // onto import.meta.env — and absent when unset, because a hardcoded URL would
10
+ // point a private deployment at a repo it may not be able to see.
11
+
12
+ import { computed } from "vue";
13
+ import { ExternalLink, X } from "@lucide/vue";
14
+ import { useBeadsStore } from "../store";
15
+ import { renderMarkdown } from "../markdown";
16
+ import { changelogBody, describeChangelog } from "../changelog-view";
17
+ import changelogMd from "../../../CHANGELOG.md?raw";
18
+
19
+ const store = useBeadsStore();
20
+ const version: string = import.meta.env.VITE_BEADCYTE_VERSION || "unknown";
21
+ const externalUrl: string | null = import.meta.env.VITE_BEADCYTE_CHANGELOG_URL || null;
22
+
23
+ const summary = computed(() => describeChangelog(changelogMd, version));
24
+ const html = computed(() => renderMarkdown(changelogBody(changelogMd)));
25
+ </script>
26
+
27
+ <template>
28
+ <Teleport to="body">
29
+ <div
30
+ v-if="store.changelogOpen"
31
+ class="changelog-overlay"
32
+ role="dialog"
33
+ aria-modal="true"
34
+ aria-label="What changed"
35
+ @click.self="store.closeChangelog()"
36
+ >
37
+ <div class="changelog-panel">
38
+ <header>
39
+ <h2>What changed <span class="running">· running v{{ version }}</span></h2>
40
+ <a
41
+ v-if="externalUrl"
42
+ class="external"
43
+ :href="externalUrl"
44
+ target="_blank"
45
+ rel="noopener"
46
+ title="The same file on the repo"
47
+ >
48
+ on the repo <ExternalLink class="icon" />
49
+ </a>
50
+ <button type="button" class="close" title="Close (Esc)" aria-label="Close" @click="store.closeChangelog()">
51
+ <X class="icon" />
52
+ </button>
53
+ </header>
54
+ <p class="note">{{ summary.note }}</p>
55
+ <!-- Generated from our own tracker and shipped in the package: a
56
+ trusted source, rendered without a sanitizer (see markdown.ts). -->
57
+ <div class="changelog-body" v-html="html" />
58
+ </div>
59
+ </div>
60
+ </Teleport>
61
+ </template>
62
+
63
+ <style>
64
+ .changelog-overlay {
65
+ position: fixed;
66
+ inset: 0;
67
+ z-index: 950;
68
+ display: flex;
69
+ align-items: flex-start;
70
+ justify-content: center;
71
+ padding: 6vh 16px;
72
+ background: color-mix(in oklab, var(--c-bg) 70%, transparent);
73
+ }
74
+
75
+ .changelog-panel {
76
+ width: min(760px, 100%);
77
+ max-height: 88vh;
78
+ display: flex;
79
+ flex-direction: column;
80
+ background: var(--c-panel);
81
+ color: var(--c-text);
82
+ border: 1px solid var(--c-border-strong);
83
+ border-radius: 8px;
84
+ box-shadow: var(--shadow-pop);
85
+ padding: 16px 20px 20px;
86
+ }
87
+
88
+ .changelog-panel header {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 12px;
92
+ margin-bottom: 4px;
93
+ }
94
+
95
+ .changelog-panel h2 {
96
+ margin: 0;
97
+ flex: 1;
98
+ font-size: 14px;
99
+ font-weight: 600;
100
+ }
101
+
102
+ .changelog-panel .running {
103
+ color: var(--c-dim);
104
+ font-weight: 400;
105
+ font-family: ui-monospace, monospace;
106
+ font-size: 12px;
107
+ }
108
+
109
+ .changelog-panel .external {
110
+ display: inline-flex;
111
+ align-items: center;
112
+ gap: 4px;
113
+ font-size: 12px;
114
+ color: var(--c-accent);
115
+ text-decoration: none;
116
+ }
117
+
118
+ .changelog-panel .external:hover {
119
+ text-decoration: underline;
120
+ }
121
+
122
+ .changelog-panel .external .icon,
123
+ .changelog-panel .close .icon {
124
+ width: 14px;
125
+ height: 14px;
126
+ }
127
+
128
+ .changelog-panel .close {
129
+ all: unset;
130
+ cursor: pointer;
131
+ color: var(--c-dim);
132
+ display: inline-flex;
133
+ padding: 4px;
134
+ border-radius: 4px;
135
+ }
136
+
137
+ .changelog-panel .close:hover {
138
+ color: var(--c-text);
139
+ background: var(--c-lift);
140
+ }
141
+
142
+ .changelog-panel .note {
143
+ margin: 0 0 12px;
144
+ font-size: 12px;
145
+ color: var(--c-dim);
146
+ line-height: 1.5;
147
+ }
148
+
149
+ .changelog-body {
150
+ overflow: auto;
151
+ min-height: 0;
152
+ font-size: 13px;
153
+ line-height: 1.55;
154
+ padding-right: 6px;
155
+ }
156
+
157
+ .changelog-body h2 {
158
+ font-size: 13px;
159
+ font-weight: 600;
160
+ margin: 14px 0 4px;
161
+ padding-top: 10px;
162
+ border-top: 1px solid var(--c-border);
163
+ }
164
+
165
+ .changelog-body h2:first-child {
166
+ border-top: 0;
167
+ padding-top: 0;
168
+ margin-top: 0;
169
+ }
170
+
171
+ .changelog-body h3 {
172
+ font-size: 10px;
173
+ font-weight: 600;
174
+ text-transform: uppercase;
175
+ letter-spacing: 0.05em;
176
+ color: var(--c-dim);
177
+ margin: 10px 0 4px;
178
+ }
179
+
180
+ .changelog-body ul {
181
+ margin: 0;
182
+ padding-left: 18px;
183
+ }
184
+
185
+ .changelog-body li {
186
+ margin: 2px 0;
187
+ }
188
+
189
+ .changelog-body code {
190
+ font-family: ui-monospace, monospace;
191
+ font-size: 11px;
192
+ color: var(--c-accent);
193
+ }
194
+
195
+ .changelog-body a {
196
+ color: var(--c-accent);
197
+ }
198
+ </style>
@@ -0,0 +1,386 @@
1
+ <script setup lang="ts">
2
+ // ContextMenu.vue — the quick actions on one bead (bp-ocs): claim, assign
3
+ // to a roster member, mark or unmark spec:ready, add a label, defer. Opens on right-click over a bar, a
4
+ // card, a table row or a mine row, from the drawer's Actions button, and on
5
+ // the a key for the open bead, so every path has a keyboard route.
6
+ //
7
+ // It is a real menu: focus moves to the first item on open, arrows move
8
+ // between items, Escape closes (through the key registry, ahead of the
9
+ // drawer), and a click anywhere else closes. Defer asks twice — pick a
10
+ // horizon, then confirm — because a wrong click writes to the real tracker.
11
+ // The menu speaks in outcomes; the exact bd command behind each action, and
12
+ // how to reverse it by hand, is documented in docs/bd-usage.md. There is no
13
+ // client-side undo by design.
14
+
15
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
16
+ import { useBeadsStore } from "../store";
17
+ import { DEFER_CHOICES, READY_LABEL, type Mutation } from "../mutations";
18
+
19
+ const store = useBeadsStore();
20
+ const menuEl = ref<HTMLElement | null>(null);
21
+ const bead = computed(() => (store.contextMenu ? store.beadById(store.contextMenu.id) : null));
22
+ const step = ref<"root" | "label" | "defer" | "assign">("root");
23
+ const labelText = ref("");
24
+ const deferUntil = ref<string>(DEFER_CHOICES[2].until);
25
+
26
+ // The roster is the list of people to assign to; a handle that is only on
27
+ // beads (a derived roster) is still a person, so it is on the list too.
28
+ const handles = computed(() => {
29
+ const roster = (store.calibratedRoster ?? store.roster)?.humans ?? [];
30
+ const seen = new Set<string>(roster.map((h) => h.handle));
31
+ for (const b of store.beads) if (b.assignee) seen.add(b.assignee);
32
+ return [...seen].sort();
33
+ });
34
+ const assignee = ref<string>("");
35
+
36
+ // Type-ahead for labels: every label already on some bead, most used first,
37
+ // minus the ones this bead has. A native datalist, so filtering as you type
38
+ // and picking with the keyboard come from the browser.
39
+ const labelSuggestions = computed(() => {
40
+ const counts = new Map<string, number>();
41
+ for (const b of store.beads) for (const l of b.labels ?? []) counts.set(l, (counts.get(l) ?? 0) + 1);
42
+ const mine = new Set(bead.value?.labels ?? []);
43
+ return [...counts.entries()]
44
+ .filter(([l]) => !mine.has(l))
45
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
46
+ .map(([l, n]) => ({ label: l, n }));
47
+ });
48
+ const assignMutation = computed<Mutation | null>(() =>
49
+ bead.value && assignee.value && assignee.value !== bead.value.assignee
50
+ ? { action: "assign", id: bead.value.id, assignee: assignee.value }
51
+ : null,
52
+ );
53
+
54
+ const isReady = computed(() => !!bead.value?.labels?.includes(READY_LABEL));
55
+ const claimable = computed(() => !!bead.value && bead.value.status !== "closed" && bead.value.status !== "in_progress");
56
+ const deferrable = computed(() => !!bead.value && bead.value.status !== "closed" && bead.value.status !== "deferred");
57
+
58
+ const pos = computed(() => {
59
+ const m = store.contextMenu;
60
+ if (!m) return { left: "0px", top: "0px" };
61
+ const w = 280;
62
+ const h = 260;
63
+ const left = Math.min(Math.max(8, m.x), window.innerWidth - w - 8);
64
+ const top = Math.min(Math.max(8, m.y), window.innerHeight - h - 8);
65
+ return { left: `${left}px`, top: `${top}px` };
66
+ });
67
+
68
+ function close() {
69
+ store.closeContextMenu();
70
+ }
71
+
72
+ function run(m: Mutation) {
73
+ close();
74
+ void store.mutate(m);
75
+ }
76
+
77
+ const deferMutation = computed<Mutation | null>(() =>
78
+ bead.value ? { action: "defer", id: bead.value.id, until: deferUntil.value } : null,
79
+ );
80
+ const labelMutation = computed<Mutation | null>(() =>
81
+ bead.value && /^[a-z0-9][a-z0-9:_./-]{0,63}$/i.test(labelText.value.trim())
82
+ ? { action: "add-label", id: bead.value.id, label: labelText.value.trim() }
83
+ : null,
84
+ );
85
+
86
+ function items(): HTMLElement[] {
87
+ return [...(menuEl.value?.querySelectorAll<HTMLElement>('[role="menuitem"], input, select') ?? [])].filter((el) => !(el as HTMLButtonElement).disabled);
88
+ }
89
+
90
+ function onKeydown(e: KeyboardEvent) {
91
+ const list = items();
92
+ const i = list.indexOf(document.activeElement as HTMLElement);
93
+ if (e.key === "ArrowDown") {
94
+ e.preventDefault();
95
+ list[(i + 1) % list.length]?.focus();
96
+ } else if (e.key === "ArrowUp") {
97
+ e.preventDefault();
98
+ list[(i - 1 + list.length) % list.length]?.focus();
99
+ } else if (e.key === "Home") {
100
+ e.preventDefault();
101
+ list[0]?.focus();
102
+ } else if (e.key === "End") {
103
+ e.preventDefault();
104
+ list[list.length - 1]?.focus();
105
+ }
106
+ // Escape is the key registry's (close order: this menu first).
107
+ }
108
+
109
+ // Closing rules. The listeners are permanent and check state, rather than
110
+ // being attached when the menu opens: a component re-mounted by hot reload
111
+ // while a menu was up would otherwise have none until the next open.
112
+ function onOutside(e: Event) {
113
+ if (!store.contextMenu) return;
114
+ if (menuEl.value && !menuEl.value.contains(e.target as Node)) close();
115
+ }
116
+ function onScroll() {
117
+ if (store.contextMenu) close();
118
+ }
119
+ onMounted(() => {
120
+ document.addEventListener("pointerdown", onOutside, true);
121
+ document.addEventListener("scroll", onScroll, { capture: true, passive: true });
122
+ });
123
+ onBeforeUnmount(() => {
124
+ document.removeEventListener("pointerdown", onOutside, true);
125
+ document.removeEventListener("scroll", onScroll, { capture: true });
126
+ });
127
+ // The menu belongs to a bead on a surface: when the drawer closes or moves
128
+ // to another bead, or the view changes, it is about nothing and goes.
129
+ watch(
130
+ () => [store.openBeadId, store.view],
131
+ () => {
132
+ if (store.contextMenu) close();
133
+ },
134
+ );
135
+
136
+ watch(
137
+ () => store.contextMenu,
138
+ async (m) => {
139
+ step.value = "root";
140
+ labelText.value = "";
141
+ if (m) {
142
+ const b = store.beadById(m.id);
143
+ assignee.value = b?.assignee ?? handles.value[0] ?? "";
144
+ await nextTick();
145
+ items()[0]?.focus();
146
+ }
147
+ },
148
+ );
149
+ // Each step is a small form: focus its first field so typing goes there.
150
+ watch(step, async () => {
151
+ await nextTick();
152
+ items()[0]?.focus();
153
+ });
154
+
155
+ </script>
156
+
157
+ <template>
158
+ <Teleport to="body">
159
+ <div
160
+ v-if="store.contextMenu && bead"
161
+ ref="menuEl"
162
+ class="bead-menu"
163
+ role="menu"
164
+ :aria-label="`Actions for ${bead.id}`"
165
+ :style="pos"
166
+ @keydown="onKeydown"
167
+ >
168
+ <header class="menu-head"><code>{{ bead.id }}</code> <span class="menu-title">{{ bead.title }}</span></header>
169
+
170
+ <template v-if="step === 'root'">
171
+ <button type="button" role="menuitem" class="item" :disabled="!claimable" @click="run({ action: 'claim', id: bead.id })">
172
+ claim <span class="cmd">{{ bead.status === "in_progress" ? "already in progress" : "start it, assigned to you" }}</span>
173
+ </button>
174
+ <button type="button" role="menuitem" class="item" :disabled="bead.status === 'closed' || !handles.length" @click="step = 'assign'">
175
+ assign… <span class="cmd">{{ bead.assignee ? `now ${bead.assignee}` : "unassigned" }}</span>
176
+ </button>
177
+ <button type="button" role="menuitem" class="item" :disabled="bead.status === 'closed'" @click="run({ action: isReady ? 'unspec-ready' : 'spec-ready', id: bead.id })">
178
+ {{ isReady ? "remove spec:ready" : "mark spec:ready" }}
179
+ </button>
180
+ <button type="button" role="menuitem" class="item" :disabled="bead.status === 'closed'" @click="step = 'label'">add label…</button>
181
+ <button type="button" role="menuitem" class="item" :disabled="!deferrable" @click="step = 'defer'">defer…</button>
182
+ <p class="menu-note">Writes to your tracker through bd. No undo here; every action and how to reverse it is in docs/bd-usage.md.</p>
183
+ </template>
184
+
185
+ <template v-else-if="step === 'assign'">
186
+ <label class="field">
187
+ <span>assign to</span>
188
+ <select v-model="assignee">
189
+ <option v-for="h in handles" :key="h" :value="h">{{ h }}{{ h === bead.assignee ? " (current)" : "" }}</option>
190
+ </select>
191
+ </label>
192
+ <p class="menu-note">Sets the assignee only; the status stays. Claim is what starts work.<template v-if="!assignMutation"> That is already the assignee.</template></p>
193
+ <div class="row">
194
+ <button type="button" role="menuitem" class="item subtle" @click="step = 'root'">back</button>
195
+ <button type="button" role="menuitem" class="item primary" :disabled="!assignMutation" @click="assignMutation && run(assignMutation)">assign</button>
196
+ </div>
197
+ </template>
198
+
199
+ <template v-else-if="step === 'label'">
200
+ <label class="field">
201
+ <span>label</span>
202
+ <input
203
+ v-model="labelText"
204
+ type="text"
205
+ list="bead-menu-labels"
206
+ autocomplete="off"
207
+ placeholder="type to search existing labels, or a new one"
208
+ spellcheck="false"
209
+ @keydown.enter.prevent="labelMutation && run(labelMutation)"
210
+ />
211
+ <datalist id="bead-menu-labels">
212
+ <option v-for="o in labelSuggestions" :key="o.label" :value="o.label">{{ o.n }} bead{{ o.n === 1 ? "" : "s" }}</option>
213
+ </datalist>
214
+ </label>
215
+ <p class="menu-note">
216
+ {{ labelSuggestions.length }} existing label{{ labelSuggestions.length === 1 ? "" : "s" }} to pick from; a new one is fine too.
217
+ Letters, digits, <code>: _ . / -</code>, up to 64.
218
+ </p>
219
+ <div class="row">
220
+ <button type="button" role="menuitem" class="item subtle" @click="step = 'root'">back</button>
221
+ <button type="button" role="menuitem" class="item primary" :disabled="!labelMutation" @click="labelMutation && run(labelMutation)">
222
+ add
223
+ </button>
224
+ </div>
225
+ </template>
226
+
227
+ <template v-else>
228
+ <label class="field">
229
+ <span>defer for</span>
230
+ <select v-model="deferUntil">
231
+ <option v-for="c in DEFER_CHOICES" :key="c.until" :value="c.until">{{ c.label }} ({{ c.until }})</option>
232
+ </select>
233
+ </label>
234
+ <p class="menu-note confirm">
235
+ Deferring hides <code>{{ bead.id }}</code> from ready work until then. Undo later with <code>bd undefer</code>.
236
+ </p>
237
+ <div class="row">
238
+ <button type="button" role="menuitem" class="item subtle" @click="step = 'root'">back</button>
239
+ <button type="button" role="menuitem" class="item primary danger" @click="deferMutation && run(deferMutation)">confirm defer</button>
240
+ </div>
241
+ </template>
242
+ </div>
243
+ </Teleport>
244
+ </template>
245
+
246
+ <style>
247
+ .bead-menu {
248
+ position: fixed;
249
+ z-index: 10000; /* above the bead tooltip (9999): a menu is the thing being used */
250
+ width: 280px;
251
+ padding: 8px;
252
+ background: var(--c-panel);
253
+ color: var(--c-text);
254
+ border: 1px solid var(--c-border-strong);
255
+ border-radius: 6px;
256
+ box-shadow: var(--shadow-pop);
257
+ font-size: 12px;
258
+ display: flex;
259
+ flex-direction: column;
260
+ gap: 2px;
261
+ }
262
+
263
+ .bead-menu .menu-head {
264
+ display: flex;
265
+ gap: 6px;
266
+ align-items: baseline;
267
+ padding: 2px 6px 8px;
268
+ border-bottom: 1px solid var(--c-border);
269
+ margin-bottom: 4px;
270
+ }
271
+
272
+ .bead-menu .menu-head code {
273
+ font-family: ui-monospace, monospace;
274
+ font-size: 11px;
275
+ color: var(--c-accent);
276
+ white-space: nowrap; /* the id never wraps; the title is what truncates */
277
+ }
278
+
279
+ .bead-menu .menu-title {
280
+ color: var(--c-dim);
281
+ overflow: hidden;
282
+ text-overflow: ellipsis;
283
+ white-space: nowrap;
284
+ }
285
+
286
+ .bead-menu .item {
287
+ all: unset;
288
+ display: flex;
289
+ justify-content: space-between;
290
+ gap: 10px;
291
+ align-items: baseline;
292
+ cursor: pointer;
293
+ padding: 6px 8px;
294
+ border-radius: 4px;
295
+ color: var(--c-text);
296
+ }
297
+
298
+ .bead-menu .item:hover,
299
+ .bead-menu .item:focus-visible {
300
+ background: var(--c-lift);
301
+ outline: none;
302
+ }
303
+
304
+ .bead-menu .item:focus-visible {
305
+ box-shadow: inset 0 0 0 2px var(--c-accent);
306
+ }
307
+
308
+ .bead-menu .item:disabled {
309
+ opacity: 0.4;
310
+ cursor: not-allowed;
311
+ }
312
+
313
+ .bead-menu .item .cmd {
314
+ font-family: ui-monospace, monospace;
315
+ font-size: 10px;
316
+ color: var(--c-dim);
317
+ }
318
+
319
+ .bead-menu .item.primary {
320
+ justify-content: center;
321
+ background: var(--c-accent);
322
+ color: var(--c-bg);
323
+ font-weight: 600;
324
+ }
325
+
326
+ .bead-menu .item.primary.danger {
327
+ background: var(--c-danger);
328
+ }
329
+
330
+ .bead-menu .item.subtle {
331
+ justify-content: center;
332
+ color: var(--c-dim);
333
+ border: 1px solid var(--c-border);
334
+ }
335
+
336
+ .bead-menu .row {
337
+ display: flex;
338
+ gap: 6px;
339
+ margin-top: 4px;
340
+ }
341
+
342
+ .bead-menu .row .item {
343
+ flex: 1;
344
+ }
345
+
346
+ .bead-menu .field {
347
+ display: flex;
348
+ flex-direction: column;
349
+ gap: 4px;
350
+ padding: 4px 6px;
351
+ }
352
+
353
+ .bead-menu .field span {
354
+ font-size: 10px;
355
+ text-transform: uppercase;
356
+ letter-spacing: 0.05em;
357
+ color: var(--c-dim);
358
+ }
359
+
360
+ .bead-menu input,
361
+ .bead-menu select {
362
+ font: inherit;
363
+ padding: 5px 8px;
364
+ border: 1px solid var(--c-border-strong);
365
+ border-radius: 4px;
366
+ background: var(--c-bg);
367
+ color: var(--c-text);
368
+ }
369
+
370
+ .bead-menu .menu-note {
371
+ margin: 4px 6px 2px;
372
+ font-size: 11px;
373
+ color: var(--c-dim);
374
+ line-height: 1.45;
375
+ }
376
+
377
+ .bead-menu .menu-note code {
378
+ font-family: ui-monospace, monospace;
379
+ font-size: 10px;
380
+ color: var(--c-text);
381
+ }
382
+
383
+ .bead-menu .menu-note.confirm {
384
+ color: var(--c-text);
385
+ }
386
+ </style>