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,468 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// BeadsGrid.vue — DOM card grid view of beads, grouped by the same
|
|
3
|
+
// rules as the Gantt chart. Each bead renders as a card with id,
|
|
4
|
+
// title, priority, status, and assignee. Group headers show the
|
|
5
|
+
// same progress chip as the Gantt.
|
|
6
|
+
//
|
|
7
|
+
// Uses the same ViewModel prop as BeadsGantt so the grouping,
|
|
8
|
+
// filtering, and truncation are consistent.
|
|
9
|
+
|
|
10
|
+
import { computed } from "vue";
|
|
11
|
+
import type { ViewModel, GroupBy } from "../view-model";
|
|
12
|
+
import { useBeadsStore } from "../store";
|
|
13
|
+
import PriorityChip from "./PriorityChip.vue";
|
|
14
|
+
import TypeChip from "./TypeChip.vue";
|
|
15
|
+
import { initialsOf, colorsFor } from "../avatar";
|
|
16
|
+
import { Bot, CircleHelp, Pin } from "@lucide/vue";
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
vm: ViewModel | null;
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
const store = useBeadsStore();
|
|
23
|
+
|
|
24
|
+
const groupBy = computed<GroupBy>(() => store.groupBy);
|
|
25
|
+
|
|
26
|
+
// "none" is one synthetic bucket with nothing to label: the header, its
|
|
27
|
+
// progress chip and the group border all go, leaving a flat card list.
|
|
28
|
+
const isUngrouped = computed(() => store.groupBy === "none");
|
|
29
|
+
|
|
30
|
+
const blockedIds = computed(() => store.blockedBeadIds);
|
|
31
|
+
const readyNowIds = computed(() => store.readyNowBeadIds);
|
|
32
|
+
const overCapAssignees = computed(() => store.overCapAssignees);
|
|
33
|
+
|
|
34
|
+
function isGroupOverCap(gid: string): boolean {
|
|
35
|
+
if (!store.highlights["over-cap"]) return false;
|
|
36
|
+
if (store.groupBy !== "assignee") return false;
|
|
37
|
+
return overCapAssignees.value.has(gid);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function statusClass(status: string): string {
|
|
41
|
+
return `status-${status.replace(/_/g, "-")}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function priorityColor(priority: number): string {
|
|
45
|
+
switch (priority) {
|
|
46
|
+
case 0:
|
|
47
|
+
return "var(--c-danger)";
|
|
48
|
+
case 1:
|
|
49
|
+
return "var(--c-warning-strong)";
|
|
50
|
+
case 2:
|
|
51
|
+
return "var(--c-warning)";
|
|
52
|
+
case 3:
|
|
53
|
+
return "var(--c-accent)";
|
|
54
|
+
default:
|
|
55
|
+
return "var(--c-closed-noship)";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const watched = computed(() => store.watchedSet);
|
|
60
|
+
const isWatched = (id: string) => watched.value.has(id);
|
|
61
|
+
|
|
62
|
+
// When filtering to a person, beads that are only *inferred* to be theirs
|
|
63
|
+
// still belong in the result — that's what the chart shows — but they're a
|
|
64
|
+
// guess, so they recede rather than sitting level with recorded work.
|
|
65
|
+
const assigneeFilterActive = computed(
|
|
66
|
+
() => (store.filters.assignees?.length ?? 0) > 0,
|
|
67
|
+
);
|
|
68
|
+
const isInferredMatch = (row: { syntheticAssignee: boolean }) =>
|
|
69
|
+
assigneeFilterActive.value && row.syntheticAssignee;
|
|
70
|
+
|
|
71
|
+
/** The bead behind a group, when the group *is* a bead — epic grouping
|
|
72
|
+
* only, and never a "__" placeholder bucket. */
|
|
73
|
+
function groupBeadId(gid: string): string | null {
|
|
74
|
+
if (store.groupBy !== "epic") return null;
|
|
75
|
+
if (gid.startsWith("__")) return null;
|
|
76
|
+
return gid;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** An assignee lane's header is a handle: it opens that person (bp-67g.43). */
|
|
80
|
+
function groupPerson(gid: string): string | null {
|
|
81
|
+
if (store.groupBy !== "assignee") return null;
|
|
82
|
+
if (gid.startsWith("__")) return null;
|
|
83
|
+
return gid;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const isGroupOpenable = (gid: string) => !!groupBeadId(gid) || !!groupPerson(gid);
|
|
87
|
+
|
|
88
|
+
function openGroupBead(gid: string) {
|
|
89
|
+
const id = groupBeadId(gid);
|
|
90
|
+
if (id) return store.openBead(id);
|
|
91
|
+
const who = groupPerson(gid);
|
|
92
|
+
if (who) store.openPerson(who);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function onBeadClick(beadId: string) {
|
|
96
|
+
store.openBead(beadId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const progressFraction = (p: { closed: number; total: number }): number =>
|
|
100
|
+
p.total > 0 ? p.closed / p.total : 0;
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<template>
|
|
104
|
+
<div class="beads-grid" v-if="vm">
|
|
105
|
+
<div
|
|
106
|
+
v-for="g in vm.groups"
|
|
107
|
+
:key="g.id"
|
|
108
|
+
class="grid-group"
|
|
109
|
+
:class="{
|
|
110
|
+
'is-ungrouped': isUngrouped,
|
|
111
|
+
'is-over-cap': isGroupOverCap(g.id),
|
|
112
|
+
}"
|
|
113
|
+
>
|
|
114
|
+
<div v-if="!isUngrouped" class="group-header">
|
|
115
|
+
<span
|
|
116
|
+
class="group-label"
|
|
117
|
+
:class="{ 'is-openable': isGroupOpenable(g.id) }"
|
|
118
|
+
:title="groupBeadId(g.id) ? `Open ${g.id}` : groupPerson(g.id) ? `Show ${g.id}` : g.label"
|
|
119
|
+
@click.stop="openGroupBead(g.id)"
|
|
120
|
+
>{{ g.label }}</span>
|
|
121
|
+
<Pin v-if="isWatched(g.id)" class="icon watch-pin" />
|
|
122
|
+
<span v-if="isGroupOverCap(g.id)" class="over-cap-badge">⚠ over cap</span>
|
|
123
|
+
<span v-if="g.progress.total > 0" class="progress-chip">
|
|
124
|
+
{{ g.progress.closed }}/{{ g.progress.total }}
|
|
125
|
+
</span>
|
|
126
|
+
<span class="group-count">{{ g.rowCount }} bead{{ g.rowCount === 1 ? "" : "s" }}</span>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="grid-cards">
|
|
130
|
+
<div
|
|
131
|
+
v-for="row in vm.rows"
|
|
132
|
+
:key="row.beadId"
|
|
133
|
+
v-show="row.groupId === g.id"
|
|
134
|
+
class="grid-card"
|
|
135
|
+
:class="{
|
|
136
|
+
'is-inferred-match': isInferredMatch(row),
|
|
137
|
+
'hl-blocked': blockedIds.has(row.beadId),
|
|
138
|
+
'hl-ready-now': readyNowIds.has(row.beadId),
|
|
139
|
+
'is-open': store.openBeadId === row.beadId,
|
|
140
|
+
}"
|
|
141
|
+
@click="onBeadClick(row.beadId)"
|
|
142
|
+
@contextmenu.prevent="store.openContextMenu(row.beadId, $event.clientX, $event.clientY)"
|
|
143
|
+
>
|
|
144
|
+
<div class="card-head">
|
|
145
|
+
<code class="card-id">{{ row.beadId }}</code>
|
|
146
|
+
<PriorityChip :priority="row.priority" />
|
|
147
|
+
<span class="pill" :class="statusClass(row.status)">
|
|
148
|
+
{{ row.status.replace(/_/g, " ") }}
|
|
149
|
+
</span>
|
|
150
|
+
<Pin v-if="isWatched(row.beadId)" class="icon watch-pin" />
|
|
151
|
+
</div>
|
|
152
|
+
<div class="card-title" :title="row.title">{{ row.title }}</div>
|
|
153
|
+
<div class="card-meta">
|
|
154
|
+
<TypeChip :type="row.issueType" />
|
|
155
|
+
<!-- Three states, each with its own glyph. A recorded assignee
|
|
156
|
+
gets their colour and initials; an inferred one gets a bot
|
|
157
|
+
and no colour, because we don't actually know it's them. -->
|
|
158
|
+
<!-- The handle leads to that person's picture on incytes
|
|
159
|
+
(bp-67g.43); stopped so the card does not also open. -->
|
|
160
|
+
<span
|
|
161
|
+
v-if="row.assignee && !row.syntheticAssignee"
|
|
162
|
+
class="assignee is-link"
|
|
163
|
+
:title="`Show ${row.assignee}`"
|
|
164
|
+
@click.stop="store.openPerson(row.assignee)"
|
|
165
|
+
>
|
|
166
|
+
<span
|
|
167
|
+
class="assignee-chip"
|
|
168
|
+
:style="{
|
|
169
|
+
background: colorsFor(row.assignee).bg,
|
|
170
|
+
color: colorsFor(row.assignee).fg,
|
|
171
|
+
}"
|
|
172
|
+
>{{ initialsOf(row.assignee) }}</span>
|
|
173
|
+
<span class="assignee-handle">{{ row.assignee }}</span>
|
|
174
|
+
</span>
|
|
175
|
+
<span
|
|
176
|
+
v-else-if="row.assignee"
|
|
177
|
+
class="assignee is-inferred is-link"
|
|
178
|
+
:title="`${row.assignee} — inferred by the scheduler, not recorded on the bead. Show ${row.assignee}`"
|
|
179
|
+
@click.stop="store.openPerson(row.assignee)"
|
|
180
|
+
>
|
|
181
|
+
<span class="assignee-chip"><Bot class="icon" /></span>
|
|
182
|
+
<span class="assignee-handle">{{ row.assignee }}</span>
|
|
183
|
+
</span>
|
|
184
|
+
<span v-else class="assignee is-unassigned" title="No assignee">
|
|
185
|
+
<span class="assignee-chip"><CircleHelp class="icon" /></span>
|
|
186
|
+
<span class="assignee-handle">unassigned</span>
|
|
187
|
+
</span>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<p v-if="vm.rows.length === 0" class="empty-state">
|
|
194
|
+
No beads match the current filters and window.
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
198
|
+
|
|
199
|
+
<style>
|
|
200
|
+
.beads-grid {
|
|
201
|
+
display: flex;
|
|
202
|
+
flex-direction: column;
|
|
203
|
+
gap: 12px;
|
|
204
|
+
padding: 16px 20px;
|
|
205
|
+
background: var(--c-bg);
|
|
206
|
+
border: 1px solid var(--c-border);
|
|
207
|
+
border-radius: 4px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* flex: none is load-bearing. A flex item's automatic minimum size is
|
|
211
|
+
content-based ONLY while its overflow is visible; `overflow: hidden` here
|
|
212
|
+
(which rounds the corners over the header) drops that floor to zero, so
|
|
213
|
+
every group would shrink toward nothing and spill its header and cards
|
|
214
|
+
over the group below. Invisible until the grid holds more than fits. */
|
|
215
|
+
.grid-group {
|
|
216
|
+
flex: none;
|
|
217
|
+
border: 1px solid var(--c-border);
|
|
218
|
+
border-radius: 4px;
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
transition: opacity 0.12s ease-out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Ungrouped: no header, so the frame around a single bucket is just a box
|
|
224
|
+
drawn round the whole list. Drop it and let the cards stand alone. */
|
|
225
|
+
.grid-group.is-ungrouped {
|
|
226
|
+
border: none;
|
|
227
|
+
border-radius: 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.grid-group.is-over-cap {
|
|
231
|
+
border-color: var(--c-danger);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.beads-grid .watch-pin {
|
|
235
|
+
color: var(--c-accent);
|
|
236
|
+
flex: none;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Only a group backed by a real bead opens; a facet value must not look
|
|
240
|
+
like a link. */
|
|
241
|
+
.beads-grid .group-label.is-openable {
|
|
242
|
+
cursor: pointer;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.beads-grid .group-label.is-openable:hover {
|
|
246
|
+
text-decoration: underline;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.beads-grid .group-header {
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
gap: 8px;
|
|
253
|
+
padding: 8px 12px;
|
|
254
|
+
background: var(--c-border);
|
|
255
|
+
background: oklch(from var(--c-border) l c h / 60%);
|
|
256
|
+
user-select: none;
|
|
257
|
+
flex-wrap: wrap;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.beads-grid .group-header:hover {
|
|
261
|
+
background: oklch(from var(--c-border) l c h / 90%);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Wraps rather than ellipsising: the header is a flex row that already
|
|
265
|
+
wraps, and an epic's full title is worth the second line. */
|
|
266
|
+
.beads-grid .group-label {
|
|
267
|
+
font-size: 12px;
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
color: var(--c-epic);
|
|
270
|
+
overflow-wrap: anywhere;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.beads-grid .over-cap-badge {
|
|
274
|
+
font-size: 10px;
|
|
275
|
+
font-weight: 600;
|
|
276
|
+
color: var(--c-danger);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.beads-grid .progress-chip {
|
|
280
|
+
font-size: 10px;
|
|
281
|
+
font-family: ui-monospace, monospace;
|
|
282
|
+
color: var(--c-dim);
|
|
283
|
+
padding: 1px 6px;
|
|
284
|
+
border: 1px solid var(--c-border);
|
|
285
|
+
border-radius: 2px;
|
|
286
|
+
background: var(--c-bg);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.beads-grid .group-count {
|
|
290
|
+
font-size: 10px;
|
|
291
|
+
color: var(--c-dim);
|
|
292
|
+
margin-left: auto;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.grid-cards {
|
|
296
|
+
display: grid;
|
|
297
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
298
|
+
gap: 8px;
|
|
299
|
+
padding: 8px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* The open bead is the keyboard cursor (bp-67g.45): marked in the view too. */
|
|
303
|
+
.grid-card.is-open {
|
|
304
|
+
outline: 1px solid var(--c-accent);
|
|
305
|
+
outline-offset: -1px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.grid-card {
|
|
309
|
+
background: var(--c-panel);
|
|
310
|
+
border: 1px solid var(--c-border);
|
|
311
|
+
border-radius: 4px;
|
|
312
|
+
padding: 10px 12px;
|
|
313
|
+
cursor: pointer;
|
|
314
|
+
transition: border-color 0.08s ease-out, background 0.08s ease-out;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Matched only by an inferred assignment: present, but visibly a guess. */
|
|
318
|
+
.grid-card.is-inferred-match {
|
|
319
|
+
opacity: var(--o-inferred);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.grid-card.is-inferred-match:hover {
|
|
323
|
+
opacity: 1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.grid-card:hover {
|
|
327
|
+
border-color: var(--c-accent);
|
|
328
|
+
background: var(--c-accent-wash);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.grid-card.hl-blocked {
|
|
332
|
+
border-left: 3px solid var(--c-danger);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.grid-card.hl-ready-now {
|
|
336
|
+
border-left: 3px solid var(--c-accent);
|
|
337
|
+
box-shadow: 0 0 4px oklch(from var(--c-accent) l c h / 30%);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.beads-grid .card-head {
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
gap: 6px;
|
|
344
|
+
flex-wrap: wrap;
|
|
345
|
+
margin-bottom: 4px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.beads-grid .card-id {
|
|
349
|
+
color: var(--c-accent);
|
|
350
|
+
font-family: ui-monospace, monospace;
|
|
351
|
+
font-size: 11px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Wrap to at most three lines. Unclamped, one long title would stretch its
|
|
355
|
+
whole grid row; clamped, cards stay comparable in height and the ellipsis
|
|
356
|
+
still signals there's more. Unlike a character cap this responds to the
|
|
357
|
+
actual column width. */
|
|
358
|
+
.beads-grid .card-title {
|
|
359
|
+
font-size: 12px;
|
|
360
|
+
color: var(--c-text);
|
|
361
|
+
margin-bottom: 6px;
|
|
362
|
+
display: -webkit-box;
|
|
363
|
+
-webkit-box-orient: vertical;
|
|
364
|
+
-webkit-line-clamp: 3;
|
|
365
|
+
line-clamp: 3;
|
|
366
|
+
overflow: hidden;
|
|
367
|
+
overflow-wrap: anywhere;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* Assignee. A recorded assignee carries their own hashed colour; the two
|
|
371
|
+
uncertain states drop the colour entirely and mute down, so a guess never
|
|
372
|
+
competes with a fact for attention. The glyph says which kind it is: a bot
|
|
373
|
+
for a scheduler inference, a question mark for nobody at all. */
|
|
374
|
+
.beads-grid .assignee {
|
|
375
|
+
display: inline-flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
gap: 5px;
|
|
378
|
+
font-size: 11px;
|
|
379
|
+
color: var(--c-dim);
|
|
380
|
+
min-width: 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.beads-grid .assignee-chip {
|
|
384
|
+
display: inline-flex;
|
|
385
|
+
align-items: center;
|
|
386
|
+
justify-content: center;
|
|
387
|
+
width: 16px;
|
|
388
|
+
height: 16px;
|
|
389
|
+
border-radius: 3px;
|
|
390
|
+
font-size: 9px;
|
|
391
|
+
font-weight: 700;
|
|
392
|
+
letter-spacing: 0.02em;
|
|
393
|
+
flex: none;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.beads-grid .assignee.is-link {
|
|
397
|
+
cursor: pointer;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.beads-grid .assignee.is-link:hover .assignee-handle {
|
|
401
|
+
color: var(--c-accent);
|
|
402
|
+
text-decoration: underline dotted;
|
|
403
|
+
text-underline-offset: 2px;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.beads-grid .assignee-handle {
|
|
407
|
+
overflow: hidden;
|
|
408
|
+
text-overflow: ellipsis;
|
|
409
|
+
white-space: nowrap;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.beads-grid .assignee.is-inferred,
|
|
413
|
+
.beads-grid .assignee.is-unassigned {
|
|
414
|
+
opacity: 0.6;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.beads-grid .assignee.is-inferred .assignee-chip,
|
|
418
|
+
.beads-grid .assignee.is-unassigned .assignee-chip {
|
|
419
|
+
background: transparent;
|
|
420
|
+
color: var(--c-dim);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* The chip is a 16px box; the glyph wants to fill it rather than sit at the
|
|
424
|
+
inherited text size. */
|
|
425
|
+
.beads-grid .assignee-chip .icon {
|
|
426
|
+
width: 13px;
|
|
427
|
+
height: 13px;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.beads-grid .card-meta {
|
|
431
|
+
display: flex;
|
|
432
|
+
gap: 6px;
|
|
433
|
+
align-items: center;
|
|
434
|
+
flex-wrap: wrap;
|
|
435
|
+
min-width: 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.beads-grid .pill {
|
|
439
|
+
font-size: 10px;
|
|
440
|
+
padding: 1px 6px;
|
|
441
|
+
border-radius: 2px;
|
|
442
|
+
border: 1px solid var(--c-border);
|
|
443
|
+
color: var(--c-dim);
|
|
444
|
+
text-transform: lowercase;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.beads-grid .pill.status-closed {
|
|
448
|
+
color: var(--c-shipped);
|
|
449
|
+
border-color: var(--c-shipped-stroke);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.beads-grid .pill.status-in-progress {
|
|
453
|
+
color: var(--c-in-progress);
|
|
454
|
+
border-color: var(--c-in-progress);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.beads-grid .pill.status-deferred {
|
|
458
|
+
color: var(--c-dim);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.beads-grid .empty-state {
|
|
462
|
+
color: var(--c-dim);
|
|
463
|
+
font-size: 12px;
|
|
464
|
+
text-align: center;
|
|
465
|
+
padding: 24px;
|
|
466
|
+
margin: 0;
|
|
467
|
+
}
|
|
468
|
+
</style>
|