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.
- package/CHANGELOG.md +73 -0
- package/LICENSE +661 -0
- package/README.md +386 -0
- package/THIRD_PARTY_NOTICES.md +56 -0
- package/bin/beadcyte.mjs +60 -0
- package/package.json +77 -0
- package/src/changelog-cli.mjs +105 -0
- package/src/changelog.mjs +196 -0
- package/src/cli.mjs +577 -0
- package/src/estimator.mjs +314 -0
- package/src/format.mjs +22 -0
- package/src/history-walk.mjs +170 -0
- package/src/index.mjs +5 -0
- package/src/mutate.mjs +193 -0
- package/src/projects.mjs +120 -0
- package/src/provenance.mjs +75 -0
- package/src/review-hours.mjs +117 -0
- package/src/roster-path.mjs +24 -0
- package/src/scheduler.mjs +424 -0
- package/src/serve.mjs +411 -0
- package/src/server-state.mjs +105 -0
- package/src/ship.mjs +178 -0
- package/src/stage-waits.mjs +69 -0
- package/src/start.mjs +111 -0
- package/src/stop.mjs +66 -0
- package/src/velocity.mjs +209 -0
- package/src/web/App.vue +691 -0
- package/src/web/app.css +54 -0
- package/src/web/assets/favicon.svg +12 -0
- package/src/web/avatar.ts +53 -0
- package/src/web/bead-detail.ts +240 -0
- package/src/web/changelog-view.ts +41 -0
- package/src/web/components/BeadDrawer.vue +1883 -0
- package/src/web/components/BeadSubGraph.vue +326 -0
- package/src/web/components/BeadSubGraphOverlay.vue +192 -0
- package/src/web/components/BeadTooltip.vue +516 -0
- package/src/web/components/BeadcyteMark.vue +64 -0
- package/src/web/components/BeadsGantt.vue +2125 -0
- package/src/web/components/BeadsGrid.vue +468 -0
- package/src/web/components/BeadsIncytes.vue +567 -0
- package/src/web/components/BeadsMine.vue +325 -0
- package/src/web/components/BeadsTable.vue +335 -0
- package/src/web/components/ChangelogOverlay.vue +198 -0
- package/src/web/components/ContextMenu.vue +386 -0
- package/src/web/components/ControlsPanel.vue +476 -0
- package/src/web/components/CostTrend.vue +206 -0
- package/src/web/components/FilterPopover.vue +245 -0
- package/src/web/components/GroupProgress.vue +274 -0
- package/src/web/components/LoadMeter.vue +144 -0
- package/src/web/components/MineRow.vue +28 -0
- package/src/web/components/OptionsMenu.vue +825 -0
- package/src/web/components/PriorityChip.vue +105 -0
- package/src/web/components/ScoreStrip.vue +131 -0
- package/src/web/components/SearchPalette.vue +210 -0
- package/src/web/components/ShipTrend.vue +510 -0
- package/src/web/components/ShortcutsOverlay.vue +164 -0
- package/src/web/components/Term.vue +177 -0
- package/src/web/components/Toast.vue +50 -0
- package/src/web/components/TriageMeters.vue +426 -0
- package/src/web/components/TypeChip.vue +96 -0
- package/src/web/components/Walkthrough.vue +209 -0
- package/src/web/components/WhatIfPanel.vue +206 -0
- package/src/web/components/WipBullets.vue +191 -0
- package/src/web/components/filter-option.ts +9 -0
- package/src/web/composables/url-codec.ts +136 -0
- package/src/web/composables/useBeadTooltip.ts +148 -0
- package/src/web/composables/useKeyboard.ts +97 -0
- package/src/web/composables/useLiveRefresh.ts +69 -0
- package/src/web/composables/useTheme.ts +125 -0
- package/src/web/composables/useUrlState.ts +208 -0
- package/src/web/controls-scope.ts +83 -0
- package/src/web/cost.ts +251 -0
- package/src/web/dep-headings.ts +62 -0
- package/src/web/economics.ts +440 -0
- package/src/web/env.d.ts +85 -0
- package/src/web/frontier.ts +208 -0
- package/src/web/gantt-viewport.ts +99 -0
- package/src/web/highlights.ts +124 -0
- package/src/web/index.html +46 -0
- package/src/web/insights.ts +107 -0
- package/src/web/keybindings.ts +200 -0
- package/src/web/load-meter.ts +72 -0
- package/src/web/main.ts +20 -0
- package/src/web/markdown.ts +14 -0
- package/src/web/mine.ts +137 -0
- package/src/web/mutations.ts +21 -0
- package/src/web/person.ts +102 -0
- package/src/web/projects-text.ts +15 -0
- package/src/web/projects.ts +188 -0
- package/src/web/refresh.ts +47 -0
- package/src/web/search.ts +50 -0
- package/src/web/shortcuts.ts +113 -0
- package/src/web/status-filter.ts +48 -0
- package/src/web/store.ts +1378 -0
- package/src/web/style-audit.mjs +346 -0
- package/src/web/styles-alt.css +111 -0
- package/src/web/styles-ported.css +270 -0
- package/src/web/subgraph.ts +362 -0
- package/src/web/table.ts +201 -0
- package/src/web/theme.ts +88 -0
- package/src/web/tokens.css +168 -0
- package/src/web/triage.ts +914 -0
- package/src/web/view-model.ts +717 -0
- package/src/web/walkthrough.ts +133 -0
- package/src/web/watchlist.ts +47 -0
- package/src/web/whatif.ts +291 -0
- package/src/web/window.ts +73 -0
- package/src/web/wip.ts +83 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// ShipTrend.vue — ships per week over the velocity lookback window.
|
|
3
|
+
//
|
|
4
|
+
// The one thing the summary blocks could not say. The Allocation block
|
|
5
|
+
// reports a flat closes-per-week average; whether that rate is rising or
|
|
6
|
+
// falling is invisible in an average and is the whole question anyone asks
|
|
7
|
+
// of a velocity number. computeVelocity already visits every close in the
|
|
8
|
+
// window, so the series costs nothing beyond keeping it (bp-67g.30).
|
|
9
|
+
//
|
|
10
|
+
// Assembled from the same DDV primitives as BeadsGantt — <DDV> for the
|
|
11
|
+
// wrapper, <CartesianScaler> for the projection, <Axis> for both axes — and
|
|
12
|
+
// a hand-written path for the line. LinePlot is deliberately not used: it
|
|
13
|
+
// strokes at a fixed 1px and defaults to Catmull-Rom, and a curve through
|
|
14
|
+
// integer bucket counts draws ships that did not happen.
|
|
15
|
+
|
|
16
|
+
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
|
17
|
+
import { Axis, CartesianScaler, DDV, fromRange } from "@incytestudios/ddv";
|
|
18
|
+
import type { Layout, MouseState, Value } from "@incytestudios/ddv";
|
|
19
|
+
import "@incytestudios/ddv/style.css";
|
|
20
|
+
import type { ShipTrend } from "../insights";
|
|
21
|
+
|
|
22
|
+
const props = defineProps<{ trend: ShipTrend }>();
|
|
23
|
+
|
|
24
|
+
// left holds <Axis>'s self-reserved label width — plotMarginPx +
|
|
25
|
+
// tickLengthPx + 4 + the widest tick label — with slack for a four-digit
|
|
26
|
+
// count; right holds the endpoint's direct label; bottom holds the date row.
|
|
27
|
+
const PAD = { top: 14, right: 40, bottom: 24, left: 42 };
|
|
28
|
+
const HEIGHT = 168;
|
|
29
|
+
|
|
30
|
+
// Below this the plot is narrower than its own axis labels, so the chart is
|
|
31
|
+
// dropped for the table instead of rendering a smear.
|
|
32
|
+
const MIN_PLOT_WIDTH = 140;
|
|
33
|
+
|
|
34
|
+
const hostEl = ref<HTMLElement | null>(null);
|
|
35
|
+
const elementWidth = ref(0);
|
|
36
|
+
|
|
37
|
+
let ro: ResizeObserver | null = null;
|
|
38
|
+
onMounted(() => {
|
|
39
|
+
if (!hostEl.value) return;
|
|
40
|
+
const measure = () => {
|
|
41
|
+
if (hostEl.value) {
|
|
42
|
+
elementWidth.value = hostEl.value.getBoundingClientRect().width;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
measure();
|
|
46
|
+
ro = new ResizeObserver(measure);
|
|
47
|
+
ro.observe(hostEl.value);
|
|
48
|
+
});
|
|
49
|
+
onBeforeUnmount(() => ro?.disconnect());
|
|
50
|
+
|
|
51
|
+
const weeks = computed(() => props.trend.weeks);
|
|
52
|
+
|
|
53
|
+
const layout = computed<Layout>(() => ({
|
|
54
|
+
plotWidth: Math.max(0, elementWidth.value - PAD.left - PAD.right),
|
|
55
|
+
plotHeight: HEIGHT - PAD.top - PAD.bottom,
|
|
56
|
+
elementWidth: elementWidth.value,
|
|
57
|
+
elementHeight: HEIGHT,
|
|
58
|
+
computedHeight: HEIGHT,
|
|
59
|
+
margins: { ...PAD },
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A repo with no ship records at all would plot a flat line along zero,
|
|
64
|
+
* which looks like a measurement rather than an absence — and it is the
|
|
65
|
+
* common case, because a ship here means recorded evidence — an MR URL or a
|
|
66
|
+
* commit SHA — and plenty of projects close beads carrying neither. Say so
|
|
67
|
+
* instead.
|
|
68
|
+
*/
|
|
69
|
+
const hasShips = computed(() => props.trend.total > 0);
|
|
70
|
+
|
|
71
|
+
const canDraw = computed(
|
|
72
|
+
() =>
|
|
73
|
+
hasShips.value &&
|
|
74
|
+
weeks.value.length > 1 &&
|
|
75
|
+
layout.value.plotWidth >= MIN_PLOT_WIDTH,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// DDV's primitives read flags off a MouseState for styling; the scaler and
|
|
79
|
+
// the axes don't need live coordinates, and the hover below is our own.
|
|
80
|
+
const mouseState = reactive<MouseState>({
|
|
81
|
+
mouse: { x: 0, y: 0 },
|
|
82
|
+
normalizedMouse: { x: 0, y: 0 },
|
|
83
|
+
globalMouse: { x: 0, y: 0 },
|
|
84
|
+
isHovered: false,
|
|
85
|
+
isPlotHovered: false,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Week INDEX on x, not the date. A Date domain makes DDV pick its own tick
|
|
90
|
+
* unit, which lands on days for an eight-week window and crowds the axis.
|
|
91
|
+
* The points are weekly buckets, so the index is the natural unit and the
|
|
92
|
+
* formatter puts the date back.
|
|
93
|
+
*/
|
|
94
|
+
const xDomain = computed(() => ({
|
|
95
|
+
min: 0,
|
|
96
|
+
max: Math.max(1, weeks.value.length - 1),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
/** Headroom above the peak so the line isn't welded to the top edge. */
|
|
100
|
+
const yDomain = computed(() => {
|
|
101
|
+
const peak = Math.max(1, ...weeks.value.map((w) => w.count));
|
|
102
|
+
return { min: 0, max: Math.ceil(peak * 1.15) };
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const xScreen = computed(() => fromRange(layout.value.plotWidth || 1));
|
|
106
|
+
const yScreen = computed(() => fromRange(layout.value.plotHeight || 1));
|
|
107
|
+
|
|
108
|
+
const fmtWeek = (d: Date) =>
|
|
109
|
+
d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Blank for a fractional tick. <Axis> solves ticks over a continuous domain,
|
|
113
|
+
* so a week-index axis gets values like 3.5 once the plot is wide enough to
|
|
114
|
+
* fit more ticks than there are weeks — and rounding those to the nearest
|
|
115
|
+
* week prints the same date twice in a row. There is no discrete tick mode
|
|
116
|
+
* to ask for instead (filed upstream as bp-67g.22), so the label is dropped
|
|
117
|
+
* and the tick mark left standing.
|
|
118
|
+
*/
|
|
119
|
+
function weekLabel(i: number): string {
|
|
120
|
+
if (!Number.isInteger(i)) return "";
|
|
121
|
+
const w = weeks.value[i];
|
|
122
|
+
return w ? fmtWeek(w.weekStart) : "";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const deltaLabel = computed(() => {
|
|
126
|
+
const d = props.trend.vsMean;
|
|
127
|
+
// One shipping week is not a baseline, so say what IS true — how much of
|
|
128
|
+
// the window has any history in it — rather than a percentage against a
|
|
129
|
+
// mean derived from the single week being described.
|
|
130
|
+
if (d === null) {
|
|
131
|
+
const a = props.trend.activeWeeks;
|
|
132
|
+
if (a === 0) return null;
|
|
133
|
+
return `first ${a === 1 ? "week" : `${a} weeks`} of history — no baseline yet`;
|
|
134
|
+
}
|
|
135
|
+
const pct = Math.round(d * 100);
|
|
136
|
+
if (pct === 0) return "on its average";
|
|
137
|
+
return `${pct > 0 ? "+" : ""}${pct}% vs average`;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── hover ─────────────────────────────────────────────────────────────────
|
|
141
|
+
// The hit area is the whole plot, so reading a middle week doesn't require
|
|
142
|
+
// landing on a 3px point.
|
|
143
|
+
|
|
144
|
+
const hoverIndex = ref<number | null>(null);
|
|
145
|
+
|
|
146
|
+
function onMove(e: MouseEvent) {
|
|
147
|
+
const rect = (e.currentTarget as SVGRectElement).getBoundingClientRect();
|
|
148
|
+
const frac = (e.clientX - rect.left) / Math.max(1, rect.width);
|
|
149
|
+
hoverIndex.value = Math.max(
|
|
150
|
+
0,
|
|
151
|
+
Math.min(weeks.value.length - 1, Math.round(frac * (weeks.value.length - 1))),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
<template>
|
|
157
|
+
<section class="stat-block trend-block">
|
|
158
|
+
<h2>
|
|
159
|
+
Ships per week
|
|
160
|
+
<span class="hint">last {{ weeks.length }} weeks</span>
|
|
161
|
+
</h2>
|
|
162
|
+
|
|
163
|
+
<div v-if="hasShips" class="trend-head">
|
|
164
|
+
<span class="trend-value">{{ trend.total }}</span>
|
|
165
|
+
<span class="trend-sub">
|
|
166
|
+
{{ trend.perWeek.toFixed(1) }}/wk average ·
|
|
167
|
+
{{ trend.last }} this week<template v-if="deltaLabel">,
|
|
168
|
+
{{ deltaLabel }}</template>
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div ref="hostEl" class="trend-host">
|
|
173
|
+
<DDV
|
|
174
|
+
v-if="canDraw"
|
|
175
|
+
:layout="layout"
|
|
176
|
+
:mouse-state="mouseState"
|
|
177
|
+
class="trend-ddv"
|
|
178
|
+
>
|
|
179
|
+
<template #default>
|
|
180
|
+
<!-- reverse-y-axis, because SVG's y grows DOWNWARD. Without it the
|
|
181
|
+
scaler maps count 0 to y 0 (the top) and the whole plot renders
|
|
182
|
+
upside down: the axis reads 0 at the top and the peak week sits
|
|
183
|
+
at the bottom. Easy to miss, because a monotonically falling
|
|
184
|
+
line is a perfectly plausible chart. BeadsGantt does NOT set
|
|
185
|
+
this and is right not to — its y axis is row index, where 0
|
|
186
|
+
belongs at the top. -->
|
|
187
|
+
<CartesianScaler
|
|
188
|
+
:x-source="xDomain"
|
|
189
|
+
:y-source="yDomain"
|
|
190
|
+
:x-target="xScreen"
|
|
191
|
+
:y-target="yScreen"
|
|
192
|
+
reverse-y-axis
|
|
193
|
+
>
|
|
194
|
+
<template #default="{ xScaler, yScaler }">
|
|
195
|
+
<!-- ONE translated group for the axes and the marks alike.
|
|
196
|
+
<Axis> positions itself relative to wherever it is
|
|
197
|
+
mounted: a vertical axis draws at `-reservedWidth` and a
|
|
198
|
+
horizontal one at `+plotHeight`, expecting to sit inside a
|
|
199
|
+
coordinate system already shifted by the plot margins. Left
|
|
200
|
+
at the SVG origin it puts its y labels at negative x, where
|
|
201
|
+
svg.outer's overflow:hidden clips them away entirely, and
|
|
202
|
+
shifts the x labels out of line with the data. -->
|
|
203
|
+
<g
|
|
204
|
+
v-if="xScaler && yScaler"
|
|
205
|
+
:transform="`translate(${PAD.left} ${PAD.top})`"
|
|
206
|
+
>
|
|
207
|
+
<Axis
|
|
208
|
+
:layout="layout"
|
|
209
|
+
:scaler="yScaler"
|
|
210
|
+
:domain="yDomain"
|
|
211
|
+
is-vertical
|
|
212
|
+
draw-grid-lines
|
|
213
|
+
tick-frequency="36px"
|
|
214
|
+
:tick-length-px="3"
|
|
215
|
+
:plot-margin-px="0"
|
|
216
|
+
/>
|
|
217
|
+
<Axis
|
|
218
|
+
:layout="layout"
|
|
219
|
+
:scaler="xScaler"
|
|
220
|
+
:domain="xDomain"
|
|
221
|
+
tick-frequency="88px"
|
|
222
|
+
:tick-length-px="3"
|
|
223
|
+
:plot-margin-px="0"
|
|
224
|
+
:axis-tick-formatter="(v: Value) => weekLabel(Number(v))"
|
|
225
|
+
/>
|
|
226
|
+
|
|
227
|
+
<g>
|
|
228
|
+
<!-- A single series reads as a quantity, so it gets a
|
|
229
|
+
wash under it. 10% keeps it a wash, not a block. -->
|
|
230
|
+
<path
|
|
231
|
+
class="trend-area"
|
|
232
|
+
:d="
|
|
233
|
+
`M ${xScaler.scale(0)} ${yScaler.scale(0)} ` +
|
|
234
|
+
weeks
|
|
235
|
+
.map((w, i) => `L ${xScaler.scale(i)} ${yScaler.scale(w.count)}`)
|
|
236
|
+
.join(' ') +
|
|
237
|
+
` L ${xScaler.scale(weeks.length - 1)} ${yScaler.scale(0)} Z`
|
|
238
|
+
"
|
|
239
|
+
/>
|
|
240
|
+
<path
|
|
241
|
+
class="trend-line"
|
|
242
|
+
:d="
|
|
243
|
+
weeks
|
|
244
|
+
.map(
|
|
245
|
+
(w, i) =>
|
|
246
|
+
`${i ? 'L' : 'M'} ${xScaler.scale(i)} ${yScaler.scale(w.count)}`,
|
|
247
|
+
)
|
|
248
|
+
.join(' ')
|
|
249
|
+
"
|
|
250
|
+
/>
|
|
251
|
+
|
|
252
|
+
<!-- Only the endpoint is direct-labelled; the axis and the
|
|
253
|
+
hover carry the rest. -->
|
|
254
|
+
<circle
|
|
255
|
+
class="trend-end"
|
|
256
|
+
:cx="xScaler.scale(weeks.length - 1)"
|
|
257
|
+
:cy="yScaler.scale(trend.last)"
|
|
258
|
+
r="3.5"
|
|
259
|
+
/>
|
|
260
|
+
<text
|
|
261
|
+
class="trend-end-label"
|
|
262
|
+
:x="xScaler.scale(weeks.length - 1) + 7"
|
|
263
|
+
:y="yScaler.scale(trend.last) + 4"
|
|
264
|
+
>{{ trend.last }}</text>
|
|
265
|
+
|
|
266
|
+
<g v-if="hoverIndex !== null" class="trend-hover">
|
|
267
|
+
<line
|
|
268
|
+
:x1="xScaler.scale(hoverIndex)"
|
|
269
|
+
:x2="xScaler.scale(hoverIndex)"
|
|
270
|
+
:y1="0"
|
|
271
|
+
:y2="layout.plotHeight"
|
|
272
|
+
/>
|
|
273
|
+
<circle
|
|
274
|
+
:cx="xScaler.scale(hoverIndex)"
|
|
275
|
+
:cy="yScaler.scale(weeks[hoverIndex].count)"
|
|
276
|
+
r="3.5"
|
|
277
|
+
/>
|
|
278
|
+
<text
|
|
279
|
+
:x="xScaler.scale(hoverIndex)"
|
|
280
|
+
:y="-3"
|
|
281
|
+
:text-anchor="
|
|
282
|
+
hoverIndex > (weeks.length - 1) / 2 ? 'end' : 'start'
|
|
283
|
+
"
|
|
284
|
+
>
|
|
285
|
+
{{ fmtWeek(weeks[hoverIndex].weekStart) }} ·
|
|
286
|
+
{{ weeks[hoverIndex].count }}
|
|
287
|
+
</text>
|
|
288
|
+
</g>
|
|
289
|
+
<rect
|
|
290
|
+
class="trend-hit"
|
|
291
|
+
:width="layout.plotWidth"
|
|
292
|
+
:height="layout.plotHeight"
|
|
293
|
+
@mousemove="onMove"
|
|
294
|
+
@mouseleave="hoverIndex = null"
|
|
295
|
+
/>
|
|
296
|
+
</g>
|
|
297
|
+
</g>
|
|
298
|
+
</template>
|
|
299
|
+
</CartesianScaler>
|
|
300
|
+
</template>
|
|
301
|
+
</DDV>
|
|
302
|
+
<p v-if="!hasShips" class="trend-empty">
|
|
303
|
+
No ships recorded in the last {{ weeks.length }} weeks. A ship is a
|
|
304
|
+
closed bead carrying <code>metadata.ship.mr_url</code> or
|
|
305
|
+
<code>metadata.ship.commit</code>; closes with neither aren't counted,
|
|
306
|
+
here or in the calibrated caps.
|
|
307
|
+
</p>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<!-- Every value reachable without hovering. -->
|
|
311
|
+
<details v-if="hasShips" class="table-view">
|
|
312
|
+
<summary>Table</summary>
|
|
313
|
+
<table>
|
|
314
|
+
<thead>
|
|
315
|
+
<tr>
|
|
316
|
+
<th>week of</th>
|
|
317
|
+
<th class="num">ships</th>
|
|
318
|
+
</tr>
|
|
319
|
+
</thead>
|
|
320
|
+
<tbody>
|
|
321
|
+
<tr v-for="w in weeks" :key="w.weekStart.toISOString()">
|
|
322
|
+
<td>{{ fmtWeek(w.weekStart) }}</td>
|
|
323
|
+
<td class="num">{{ w.count }}</td>
|
|
324
|
+
</tr>
|
|
325
|
+
</tbody>
|
|
326
|
+
</table>
|
|
327
|
+
</details>
|
|
328
|
+
</section>
|
|
329
|
+
</template>
|
|
330
|
+
|
|
331
|
+
<style scoped>
|
|
332
|
+
.trend-block h2 .hint {
|
|
333
|
+
text-transform: none;
|
|
334
|
+
letter-spacing: 0;
|
|
335
|
+
font-weight: 400;
|
|
336
|
+
color: var(--c-dim);
|
|
337
|
+
margin-left: 6px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.trend-head {
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: baseline;
|
|
343
|
+
gap: 8px;
|
|
344
|
+
flex-wrap: wrap;
|
|
345
|
+
margin-bottom: 6px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* Proportional figures, not tabular: equal-width digits read loose at this
|
|
349
|
+
size. tabular-nums belongs in the table below, where they align. */
|
|
350
|
+
.trend-value {
|
|
351
|
+
font-size: 24px;
|
|
352
|
+
font-weight: 600;
|
|
353
|
+
line-height: 1.1;
|
|
354
|
+
color: var(--c-text);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.trend-sub {
|
|
358
|
+
font-size: 11px;
|
|
359
|
+
color: var(--c-dim);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.trend-host {
|
|
363
|
+
/* DDV's inner svg.outer caps at min-height 250px unless --minHeight says
|
|
364
|
+
otherwise, and --aspectRatio must be auto or it resizes to whatever
|
|
365
|
+
ratio the wrapper happened to start at. */
|
|
366
|
+
--minHeight: 168px;
|
|
367
|
+
--aspectRatio: auto;
|
|
368
|
+
width: 100%;
|
|
369
|
+
min-height: 168px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.trend-empty {
|
|
373
|
+
color: var(--c-dim);
|
|
374
|
+
font-size: 12px;
|
|
375
|
+
line-height: 1.5;
|
|
376
|
+
margin: 8px 0 0;
|
|
377
|
+
max-width: 62ch;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.trend-empty code {
|
|
381
|
+
font-family: var(--font-mono);
|
|
382
|
+
font-size: 0.92em;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* No chart, no reserved chart height. */
|
|
386
|
+
.trend-block:has(.trend-empty) .trend-host {
|
|
387
|
+
--minHeight: 0;
|
|
388
|
+
min-height: 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.table-view {
|
|
392
|
+
margin-top: 10px;
|
|
393
|
+
font-size: 11px;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.table-view summary {
|
|
397
|
+
cursor: pointer;
|
|
398
|
+
color: var(--c-dim);
|
|
399
|
+
padding: 2px 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.table-view table {
|
|
403
|
+
border-collapse: collapse;
|
|
404
|
+
margin-top: 6px;
|
|
405
|
+
width: 100%;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.table-view th,
|
|
409
|
+
.table-view td {
|
|
410
|
+
text-align: left;
|
|
411
|
+
padding: 2px 8px 2px 0;
|
|
412
|
+
border-bottom: 1px solid var(--c-border);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.table-view thead th {
|
|
416
|
+
color: var(--c-dim);
|
|
417
|
+
font-size: 10px;
|
|
418
|
+
font-weight: 600;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.table-view .num {
|
|
422
|
+
text-align: right;
|
|
423
|
+
font-variant-numeric: tabular-nums;
|
|
424
|
+
}
|
|
425
|
+
</style>
|
|
426
|
+
|
|
427
|
+
<style>
|
|
428
|
+
/* Unscoped: these marks live inside DDV's own SVG subtree, which a scoped
|
|
429
|
+
style does not reach.
|
|
430
|
+
`shipped` is the encoding token for a real ship, so a trend OF ships
|
|
431
|
+
wears it rather than inventing a colour — nothing new to register in
|
|
432
|
+
style-audit. */
|
|
433
|
+
.trend-ddv {
|
|
434
|
+
display: block;
|
|
435
|
+
width: 100%;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* DDV's axis and grid defaults are authored for a light theme and vanish on
|
|
439
|
+
our dark ground. Same overrides BeadsGantt applies. */
|
|
440
|
+
.trend-block .trend-host {
|
|
441
|
+
--axisTickFontColor: var(--c-dim);
|
|
442
|
+
--axisLabelColor: var(--c-dim);
|
|
443
|
+
--axisTickFontFamily: var(--font-body);
|
|
444
|
+
--axisTickFontSize: 10px;
|
|
445
|
+
--axisTickFontWeight: 400;
|
|
446
|
+
--boundaryStroke: var(--c-border-strong);
|
|
447
|
+
--boundaryStrokeWidth: 1;
|
|
448
|
+
--tickStroke: var(--c-border-strong);
|
|
449
|
+
--tickStrokeWidth: 1;
|
|
450
|
+
--subTickStroke: var(--c-border);
|
|
451
|
+
--subTickStrokeWidth: 0.5;
|
|
452
|
+
--gridStroke: var(--c-border);
|
|
453
|
+
--gridStrokeWidth: 0.5;
|
|
454
|
+
--subGridStroke: var(--c-border);
|
|
455
|
+
--subGridStrokeWidth: 0.5;
|
|
456
|
+
--gridStrokeDashArray: none;
|
|
457
|
+
--plotSurface: transparent;
|
|
458
|
+
--chartSurface: transparent;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.trend-ddv .trend-line {
|
|
462
|
+
fill: none;
|
|
463
|
+
stroke: var(--c-shipped);
|
|
464
|
+
stroke-width: 2;
|
|
465
|
+
stroke-linejoin: round;
|
|
466
|
+
stroke-linecap: round;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.trend-ddv .trend-area {
|
|
470
|
+
fill: var(--c-shipped);
|
|
471
|
+
fill-opacity: 0.1;
|
|
472
|
+
stroke: none;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* A 2px ring in the surface colour keeps the marker legible where it
|
|
476
|
+
crosses the line or the axis. */
|
|
477
|
+
.trend-ddv .trend-end {
|
|
478
|
+
fill: var(--c-shipped);
|
|
479
|
+
stroke: var(--c-panel);
|
|
480
|
+
stroke-width: 2;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.trend-ddv .trend-end-label {
|
|
484
|
+
fill: var(--c-text);
|
|
485
|
+
font-size: 11px;
|
|
486
|
+
font-weight: 600;
|
|
487
|
+
font-variant-numeric: tabular-nums;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.trend-ddv .trend-hit {
|
|
491
|
+
fill: transparent;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.trend-ddv .trend-hover line {
|
|
495
|
+
stroke: var(--c-border-strong);
|
|
496
|
+
stroke-width: 1;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.trend-ddv .trend-hover circle {
|
|
500
|
+
fill: var(--c-shipped);
|
|
501
|
+
stroke: var(--c-panel);
|
|
502
|
+
stroke-width: 2;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.trend-ddv .trend-hover text {
|
|
506
|
+
fill: var(--c-text);
|
|
507
|
+
font-size: 10px;
|
|
508
|
+
font-variant-numeric: tabular-nums;
|
|
509
|
+
}
|
|
510
|
+
</style>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// ShortcutsOverlay.vue — the ? overlay (bp-67g.45), generated from
|
|
3
|
+
// keybindings.ts so it cannot drift from what the keys do. Opens on ? and
|
|
4
|
+
// from the options menu; closes on Escape (through the same table), ?,
|
|
5
|
+
// the close button or a click on the backdrop.
|
|
6
|
+
|
|
7
|
+
import { computed } from "vue";
|
|
8
|
+
import { X } from "@lucide/vue";
|
|
9
|
+
import { useBeadsStore } from "../store";
|
|
10
|
+
import { bindingsByGroup, chordsToShow, formatChord, KEY_CONVENTION } from "../keybindings";
|
|
11
|
+
|
|
12
|
+
const store = useBeadsStore();
|
|
13
|
+
const mac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
|
|
14
|
+
const groups = computed(() => bindingsByGroup());
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<Teleport to="body">
|
|
19
|
+
<div
|
|
20
|
+
v-if="store.helpOpen"
|
|
21
|
+
class="shortcuts-overlay"
|
|
22
|
+
role="dialog"
|
|
23
|
+
aria-modal="true"
|
|
24
|
+
aria-label="Keyboard shortcuts"
|
|
25
|
+
@click.self="store.closeHelp()"
|
|
26
|
+
>
|
|
27
|
+
<div class="shortcuts-panel">
|
|
28
|
+
<header>
|
|
29
|
+
<h2>Keyboard shortcuts</h2>
|
|
30
|
+
<button type="button" class="close" title="Close (Esc)" aria-label="Close" @click="store.closeHelp()">
|
|
31
|
+
<X class="icon" />
|
|
32
|
+
</button>
|
|
33
|
+
</header>
|
|
34
|
+
<p class="convention">{{ KEY_CONVENTION }}</p>
|
|
35
|
+
<div class="groups">
|
|
36
|
+
<section v-for="g in groups" :key="g.group">
|
|
37
|
+
<h3>{{ g.group }}</h3>
|
|
38
|
+
<dl>
|
|
39
|
+
<template v-for="b in g.bindings" :key="b.action">
|
|
40
|
+
<dt>
|
|
41
|
+
<kbd v-for="c in chordsToShow(b, mac)" :key="c.key + (c.ctrl ? 'c' : '') + (c.meta ? 'm' : '')">{{ formatChord(c, mac) }}</kbd>
|
|
42
|
+
</dt>
|
|
43
|
+
<dd>{{ b.label }}</dd>
|
|
44
|
+
</template>
|
|
45
|
+
</dl>
|
|
46
|
+
</section>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</Teleport>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<style>
|
|
54
|
+
.shortcuts-overlay {
|
|
55
|
+
position: fixed;
|
|
56
|
+
inset: 0;
|
|
57
|
+
z-index: 950;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: flex-start;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
padding: 8vh 16px;
|
|
62
|
+
background: color-mix(in oklab, var(--c-bg) 70%, transparent);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.shortcuts-panel {
|
|
66
|
+
width: min(880px, 100%);
|
|
67
|
+
max-height: 84vh;
|
|
68
|
+
overflow: auto;
|
|
69
|
+
background: var(--c-panel);
|
|
70
|
+
color: var(--c-text);
|
|
71
|
+
border: 1px solid var(--c-border-strong);
|
|
72
|
+
border-radius: 8px;
|
|
73
|
+
box-shadow: var(--shadow-pop);
|
|
74
|
+
padding: 16px 20px 20px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.shortcuts-panel header {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: space-between;
|
|
81
|
+
margin-bottom: 6px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.shortcuts-panel h2 {
|
|
85
|
+
margin: 0;
|
|
86
|
+
font-size: 14px;
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.shortcuts-panel .close {
|
|
91
|
+
all: unset;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
color: var(--c-dim);
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
padding: 4px;
|
|
96
|
+
border-radius: 4px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.shortcuts-panel .close:hover {
|
|
100
|
+
color: var(--c-text);
|
|
101
|
+
background: var(--c-lift);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.shortcuts-panel .close .icon {
|
|
105
|
+
width: 16px;
|
|
106
|
+
height: 16px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.shortcuts-panel .convention {
|
|
110
|
+
margin: 0 0 14px;
|
|
111
|
+
font-size: 12px;
|
|
112
|
+
color: var(--c-dim);
|
|
113
|
+
line-height: 1.5;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.shortcuts-panel .groups {
|
|
117
|
+
display: grid;
|
|
118
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
119
|
+
gap: 14px 28px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.shortcuts-panel h3 {
|
|
123
|
+
margin: 0 0 6px;
|
|
124
|
+
font-size: 10px;
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
text-transform: uppercase;
|
|
127
|
+
letter-spacing: 0.05em;
|
|
128
|
+
color: var(--c-dim);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.shortcuts-panel dl {
|
|
132
|
+
display: grid;
|
|
133
|
+
grid-template-columns: max-content 1fr;
|
|
134
|
+
gap: 4px 12px;
|
|
135
|
+
margin: 0;
|
|
136
|
+
font-size: 12px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.shortcuts-panel dt {
|
|
140
|
+
display: flex;
|
|
141
|
+
gap: 4px;
|
|
142
|
+
align-items: center;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.shortcuts-panel dd {
|
|
146
|
+
margin: 0;
|
|
147
|
+
color: var(--c-text);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.shortcuts-panel kbd {
|
|
151
|
+
display: inline-block;
|
|
152
|
+
min-width: 18px;
|
|
153
|
+
padding: 1px 6px;
|
|
154
|
+
border: 1px solid var(--c-border-strong);
|
|
155
|
+
border-bottom-width: 2px;
|
|
156
|
+
border-radius: 4px;
|
|
157
|
+
background: var(--c-lift);
|
|
158
|
+
color: var(--c-text);
|
|
159
|
+
font-family: ui-monospace, monospace;
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
line-height: 1.5;
|
|
162
|
+
text-align: center;
|
|
163
|
+
}
|
|
164
|
+
</style>
|