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,105 @@
1
+ <script setup lang="ts">
2
+ // PriorityChip.vue — first-class priority indicator. Priority is one of
3
+ // the most-scanned attributes on a bead, so it gets its own chip with a
4
+ // distinct icon per level, color-coded background, and tight uniform
5
+ // sizing so chips line up cleanly in tooltips + drawers + row labels.
6
+ //
7
+ // Levels:
8
+ // P0 → flame (drop everything, red)
9
+ // P1 → chevrons-up (urgent, orange)
10
+ // P2 → chevron-up (normal, amber)
11
+ // P3 → chevron-down (low, dim blue)
12
+ // P4 → moon (deferred / backlog, grey)
13
+
14
+ import { computed } from "vue";
15
+ import { Flame, ChevronsUp, ChevronUp, ChevronDown, Moon } from "@lucide/vue";
16
+
17
+ const props = defineProps<{
18
+ priority: number | null | undefined;
19
+ /** compact = icon only, no text */
20
+ compact?: boolean;
21
+ }>();
22
+
23
+ const level = computed(() => {
24
+ const p = props.priority ?? 4;
25
+ if (p < 0) return 0;
26
+ if (p > 4) return 4;
27
+ return p;
28
+ });
29
+
30
+ const icon = computed(() => {
31
+ switch (level.value) {
32
+ case 0:
33
+ return Flame;
34
+ case 1:
35
+ return ChevronsUp;
36
+ case 2:
37
+ return ChevronUp;
38
+ case 3:
39
+ return ChevronDown;
40
+ case 4:
41
+ return Moon;
42
+ default:
43
+ return ChevronUp;
44
+ }
45
+ });
46
+ </script>
47
+
48
+ <template>
49
+ <span class="prio-chip" :class="`prio-p${level}`" :title="`Priority P${level}`">
50
+ <component :is="icon" class="icon prio-icon" />
51
+ <span v-if="!compact" class="prio-text">P{{ level }}</span>
52
+ </span>
53
+ </template>
54
+
55
+ <style>
56
+ .prio-chip {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ gap: 3px;
60
+ padding: 2px 6px 2px 5px;
61
+ border-radius: 3px;
62
+ font-size: 10px;
63
+ font-weight: 600;
64
+ font-variant-numeric: tabular-nums;
65
+ line-height: 1;
66
+ white-space: nowrap;
67
+ border: 1px solid transparent;
68
+ }
69
+
70
+ .prio-chip .prio-icon {
71
+ flex-shrink: 0;
72
+ }
73
+
74
+ .prio-chip.prio-p0 {
75
+ background: oklch(from var(--c-today) l c h / 15%);
76
+ color: var(--c-danger-bright);
77
+ border-color: oklch(from var(--c-today) l c h / 40%);
78
+ }
79
+
80
+ .prio-chip.prio-p1 {
81
+ background: oklch(from var(--c-warning-strong) l c h / 13%);
82
+ /* warning-strong, not warning-bright: the bright variant sat 0.06 OKLab
83
+ from P2's --c-warning in dark mode, under the audit floor (bp-jak). */
84
+ color: var(--c-warning-strong);
85
+ border-color: oklch(from var(--c-warning-strong) l c h / 35%);
86
+ }
87
+
88
+ .prio-chip.prio-p2 {
89
+ background: oklch(from var(--c-warning) l c h / 12%);
90
+ color: var(--c-warning);
91
+ border-color: oklch(from var(--c-warning) l c h / 30%);
92
+ }
93
+
94
+ .prio-chip.prio-p3 {
95
+ background: oklch(from var(--c-in-progress) l c h / 10%);
96
+ color: var(--c-in-progress-bright);
97
+ border-color: oklch(from var(--c-in-progress) l c h / 25%);
98
+ }
99
+
100
+ .prio-chip.prio-p4 {
101
+ background: oklch(from var(--c-dim) l c h / 8%);
102
+ color: var(--c-dim);
103
+ border-color: oklch(from var(--c-dim) l c h / 25%);
104
+ }
105
+ </style>
@@ -0,0 +1,131 @@
1
+ <script setup lang="ts">
2
+ // ScoreStrip.vue — the composite triage score placed in its population
3
+ // (bp-67g.28): a percentile line and a 40-bin histogram of the open beads'
4
+ // scores, with quartile ticks and one saturated marker where this bead sits.
5
+ //
6
+ // Presentational. The population comes from the store's lazy getter so the
7
+ // drawer stays the only reader of triage; the bins are --c-dim, the marker
8
+ // is the one accent mark, and nothing here introduces a new semantic colour.
9
+
10
+ import { computed } from "vue";
11
+ import { describeRank, type ScorePopulation } from "../triage";
12
+
13
+ const props = defineProps<{
14
+ population: ScorePopulation;
15
+ score: number;
16
+ }>();
17
+
18
+ const rank = computed(() => describeRank(props.population, props.score));
19
+ const maxBin = computed(() => Math.max(1, ...props.population.histogram));
20
+ const span = computed(() => props.population.max - props.population.min || 1);
21
+ const posOf = (v: number) =>
22
+ Math.min(100, Math.max(0, ((v - props.population.min) / span.value) * 100));
23
+ const fmtPct = (v: number) => `${(v * 100).toFixed(1)}%`;
24
+ </script>
25
+
26
+ <template>
27
+ <div class="score-strip" v-if="population.n > 0">
28
+ <p class="rank">{{ rank }}</p>
29
+ <div
30
+ class="strip"
31
+ role="img"
32
+ :aria-label="`Distribution of ${population.n} open beads' triage scores from ${fmtPct(population.min)} to ${fmtPct(population.max)}; this bead at ${fmtPct(score)}, ${rank}`"
33
+ >
34
+ <div class="bins">
35
+ <i
36
+ v-for="(count, i) in population.histogram"
37
+ :key="i"
38
+ class="bin"
39
+ :style="{ height: count ? `${Math.max(6, (count / maxBin) * 100)}%` : '0%' }"
40
+ />
41
+ </div>
42
+ <i class="q" :style="{ left: `${posOf(population.p25)}%` }" />
43
+ <i class="q is-median" :style="{ left: `${posOf(population.p50)}%` }" />
44
+ <i class="q" :style="{ left: `${posOf(population.p75)}%` }" />
45
+ <i class="marker" :style="{ left: `${posOf(score)}%` }" />
46
+ </div>
47
+ <div class="axis">
48
+ <span>{{ fmtPct(population.min) }}</span>
49
+ <span class="mid">median {{ fmtPct(population.p50) }}</span>
50
+ <span>{{ fmtPct(population.max) }}</span>
51
+ </div>
52
+ </div>
53
+ </template>
54
+
55
+ <style>
56
+ .score-strip {
57
+ margin-top: 8px;
58
+ }
59
+
60
+ .score-strip .rank {
61
+ margin: 0 0 6px;
62
+ font-size: 11px;
63
+ color: var(--c-dim);
64
+ }
65
+
66
+ .score-strip .strip {
67
+ position: relative;
68
+ height: 44px;
69
+ border-bottom: 1px solid var(--c-border);
70
+ }
71
+
72
+ .score-strip .bins {
73
+ position: absolute;
74
+ inset: 0;
75
+ display: flex;
76
+ align-items: flex-end;
77
+ gap: 1px;
78
+ }
79
+
80
+ .score-strip .bin {
81
+ flex: 1 1 0;
82
+ min-width: 0;
83
+ background: var(--c-dim);
84
+ opacity: 0.55;
85
+ border-radius: 1px 1px 0 0;
86
+ }
87
+
88
+ /* Quartile ticks read as a distribution's landmarks, not as data. */
89
+ .score-strip .q {
90
+ position: absolute;
91
+ top: 0;
92
+ bottom: -1px;
93
+ width: 1px;
94
+ background: var(--c-border-strong);
95
+ transform: translateX(-0.5px);
96
+ }
97
+
98
+ .score-strip .q.is-median {
99
+ background: var(--c-dim-bright);
100
+ }
101
+
102
+ /* The one saturated mark: a line down the strip with a dot at its foot. */
103
+ .score-strip .marker {
104
+ position: absolute;
105
+ top: -4px;
106
+ bottom: -5px;
107
+ width: 2px;
108
+ background: var(--c-accent);
109
+ transform: translateX(-1px);
110
+ }
111
+
112
+ .score-strip .marker::after {
113
+ content: "";
114
+ position: absolute;
115
+ left: -3px;
116
+ bottom: -2px;
117
+ width: 8px;
118
+ height: 8px;
119
+ border-radius: 50%;
120
+ background: var(--c-accent);
121
+ }
122
+
123
+ .score-strip .axis {
124
+ display: flex;
125
+ justify-content: space-between;
126
+ margin-top: 6px;
127
+ font-size: 10px;
128
+ color: var(--c-dim);
129
+ font-variant-numeric: tabular-nums;
130
+ }
131
+ </style>
@@ -0,0 +1,210 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, onUnmounted, watch, computed, nextTick } from "vue";
3
+ import { useBeadsStore } from "../store";
4
+ import { rankBeads } from "../search";
5
+
6
+ const store = useBeadsStore();
7
+ const isOpen = ref(false);
8
+ const searchQuery = ref("");
9
+ const selectedIndex = ref(0);
10
+ const showPalette = ref(false);
11
+
12
+ // Search results — ranking lives in ../search.ts so it can be tested
13
+ // without mounting this component.
14
+ const searchResults = computed(() => rankBeads(store.beads, searchQuery.value));
15
+
16
+ function openPalette() {
17
+ isOpen.value = true;
18
+ searchQuery.value = "";
19
+ selectedIndex.value = 0;
20
+ showPalette.value = true;
21
+ }
22
+
23
+ function closePalette() {
24
+ isOpen.value = false;
25
+ showPalette.value = false;
26
+ searchQuery.value = "";
27
+ }
28
+
29
+ function selectItem(index: number) {
30
+ if (index >= 0 && index < searchResults.value.length) {
31
+ const selectedBead = searchResults.value[index].bead;
32
+ // Open the bead detail drawer instead of just focusing it
33
+ store.openBead(selectedBead.id);
34
+ closePalette();
35
+ }
36
+ }
37
+
38
+ function handleKeydown(e: KeyboardEvent) {
39
+ if (!isOpen.value) return;
40
+
41
+ switch (e.key) {
42
+ case "Escape":
43
+ e.preventDefault();
44
+ closePalette();
45
+ break;
46
+ case "ArrowDown":
47
+ e.preventDefault();
48
+ selectedIndex.value = Math.min(selectedIndex.value + 1, searchResults.value.length - 1);
49
+ break;
50
+ case "ArrowUp":
51
+ e.preventDefault();
52
+ selectedIndex.value = Math.max(selectedIndex.value - 1, 0);
53
+ break;
54
+ case "Enter":
55
+ e.preventDefault();
56
+ selectItem(selectedIndex.value);
57
+ break;
58
+ }
59
+ }
60
+
61
+ // ⌘K / Ctrl+K and / arrive through keybindings.ts → useKeyboard, which bumps
62
+ // store.searchRequests (bp-67g.45); the palette no longer listens for the
63
+ // chord itself, so the ? overlay is the one list of what opens it.
64
+ watch(() => store.searchRequests, () => openPalette());
65
+
66
+ // Setup event listeners
67
+ onMounted(() => {
68
+ document.addEventListener("keydown", handleKeydown);
69
+ });
70
+
71
+ onUnmounted(() => {
72
+ document.removeEventListener("keydown", handleKeydown);
73
+ });
74
+
75
+ // Watch for changes to selected index to make sure it's visible
76
+ watch(selectedIndex, async (newIndex) => {
77
+ if (newIndex >= 0 && showPalette.value) {
78
+ await nextTick();
79
+ const resultElements = document.querySelectorAll('.search-result');
80
+ if (resultElements[newIndex]) {
81
+ // Scroll the selected item into view
82
+ (resultElements[newIndex] as HTMLElement).scrollIntoView({
83
+ block: 'nearest',
84
+ behavior: 'smooth'
85
+ });
86
+ }
87
+ }
88
+ });
89
+
90
+ // Watch for changes to open state to manage focus
91
+ watch(isOpen, (newVal) => {
92
+ if (newVal) {
93
+ // Give the search input focus when opening
94
+ setTimeout(() => {
95
+ const input = document.querySelector('.search-input') as HTMLInputElement;
96
+ if (input) {
97
+ input.focus();
98
+ }
99
+ }, 0);
100
+ }
101
+ });
102
+ </script>
103
+
104
+ <template>
105
+ <div v-if="showPalette" class="search-palette-overlay">
106
+ <div class="search-palette">
107
+ <input
108
+ v-model="searchQuery"
109
+ class="search-input"
110
+ type="text"
111
+ placeholder="Search beads by ID or title..."
112
+ autocomplete="off"
113
+ />
114
+ <div class="search-results">
115
+ <div
116
+ v-for="(item, index) in searchResults"
117
+ :key="item.bead.id"
118
+ class="search-result"
119
+ :class="{ 'selected': index === selectedIndex }"
120
+ @click="selectItem(index)"
121
+ >
122
+ <div class="result-id">{{ item.bead.id }}</div>
123
+ <div class="result-title">{{ item.bead.title }}</div>
124
+ </div>
125
+ <div v-if="searchQuery && searchResults.length === 0" class="no-results">
126
+ No matching beads found
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </template>
132
+
133
+ <style scoped>
134
+ .search-palette-overlay {
135
+ position: fixed;
136
+ top: 0;
137
+ left: 0;
138
+ width: 100%;
139
+ height: 100%;
140
+ background: var(--c-scrim);
141
+ z-index: 1000;
142
+ display: flex;
143
+ justify-content: center;
144
+ align-items: flex-start;
145
+ padding-top: 50px;
146
+ }
147
+
148
+ .search-palette {
149
+ background: var(--c-panel);
150
+ border: 1px solid var(--c-border);
151
+ border-radius: 4px;
152
+ width: 500px;
153
+ max-width: 90vw;
154
+ box-shadow: var(--shadow-panel);
155
+ }
156
+
157
+ .search-input {
158
+ width: 100%;
159
+ padding: 12px;
160
+ border: none;
161
+ border-bottom: 1px solid var(--c-border);
162
+ background: var(--c-bg);
163
+ color: var(--c-text);
164
+ font-size: 14px;
165
+ }
166
+
167
+ .search-input:focus {
168
+ outline: none;
169
+ }
170
+
171
+ .search-results {
172
+ max-height: 400px;
173
+ overflow-y: auto;
174
+ }
175
+
176
+ .search-result {
177
+ padding: 10px 12px;
178
+ cursor: pointer;
179
+ border-bottom: 1px solid var(--c-border);
180
+ display: flex;
181
+ flex-direction: column;
182
+ }
183
+
184
+ .search-result:last-child {
185
+ border-bottom: none;
186
+ }
187
+
188
+ .search-result:hover,
189
+ .search-result.selected {
190
+ background: var(--c-accent);
191
+ color: var(--c-bg);
192
+ }
193
+
194
+ .result-id {
195
+ font-family: monospace;
196
+ font-size: 12px;
197
+ opacity: 0.8;
198
+ margin-bottom: 2px;
199
+ }
200
+
201
+ .result-title {
202
+ font-size: 14px;
203
+ }
204
+
205
+ .no-results {
206
+ padding: 12px;
207
+ text-align: center;
208
+ color: var(--c-dim);
209
+ }
210
+ </style>