@starlove/orbit 1.1.2 → 1.2.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/SKILL-ORBIT.md +2 -0
- package/build.mjs +11 -5
- package/dist/full/index.html +169 -20
- package/dist/full/js/app.ccc442f7.js +706 -0
- package/dist/full/js/app.ccc442f7.js.map +7 -0
- package/dist/full/styles.baccc8e3.css +1 -0
- package/dist/full/version.json +2 -2
- package/docs/ORBIT_DISPATCH.md +68 -8
- package/package.json +19 -18
- package/public/index.html +167 -18
- package/public/js/board-menu.js +16 -2
- package/public/js/card-actions.js +19 -0
- package/public/js/create-card.js +1 -1
- package/public/js/drawer.js +17 -43
- package/public/js/format.js +7 -8
- package/public/js/kanban.js +167 -101
- package/public/js/main.js +2 -1
- package/public/js/motion-preference.js +14 -3
- package/public/js/search.js +1 -1
- package/public/js/settings.js +162 -46
- package/public/js/ticket-detail.js +92 -24
- package/public/styles.css +569 -476
- package/src/cli/orbit.js +22 -0
- package/src/core/board.js +1 -0
- package/src/core/css-bundle.js +29 -0
- package/src/core/db.js +19 -0
- package/src/core/dispatch.js +176 -11
- package/src/core/http.js +10 -3
- package/src/core/registry.js +2 -2
- package/src/core/review-verdicts.js +138 -0
- package/src/core/router.js +32 -3
- package/src/core/snapshots.js +429 -9
- package/src/mcp/orbit-client.js +45 -19
- package/src/mcp-server.js +102 -27
- package/dist/full/js/app.a4f045fc.js +0 -676
- package/dist/full/js/app.a4f045fc.js.map +0 -7
- package/dist/full/styles.836872e7.css +0 -5962
package/public/index.html
CHANGED
|
@@ -15,9 +15,129 @@
|
|
|
15
15
|
sizes="180x180"
|
|
16
16
|
href="./apple-touch-icon.png"
|
|
17
17
|
/>
|
|
18
|
+
<!-- Apply the stored theme + motion preference before first paint, so
|
|
19
|
+
the app underneath the splash paints in the right theme (no flash)
|
|
20
|
+
and the splash respects an explicit reduce-motion choice. Mirrors
|
|
21
|
+
theme-preference.js (mab_theme) and motion-preference.js
|
|
22
|
+
(mab_reduce_motion → data-motion); main.js reconciles on load. -->
|
|
23
|
+
<script>
|
|
24
|
+
try {
|
|
25
|
+
var t = localStorage.getItem("mab_theme");
|
|
26
|
+
if (t === "light" || t === "dark")
|
|
27
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
28
|
+
var rm = localStorage.getItem("mab_reduce_motion");
|
|
29
|
+
if (rm === "true")
|
|
30
|
+
document.documentElement.setAttribute("data-motion", "reduce");
|
|
31
|
+
else if (rm === "false")
|
|
32
|
+
document.documentElement.setAttribute("data-motion", "full");
|
|
33
|
+
} catch (e) {}
|
|
34
|
+
</script>
|
|
35
|
+
<link rel="preload" as="image" href="./orbits-logo.png" />
|
|
18
36
|
<link rel="stylesheet" href="/styles.css" />
|
|
37
|
+
<!-- Boot splash — the canonical ss-ui loading screen (see the package's
|
|
38
|
+
components/loader.css). Inline critical CSS so it paints before
|
|
39
|
+
/js/main.js loads. The logo grows in then breathes; no spinner.
|
|
40
|
+
Keyframes namespaced `ss-loader-*`. Under reduced motion the grow-in
|
|
41
|
+
is dropped and the dot grid stops; the gentle breathe stays. -->
|
|
42
|
+
<style>
|
|
43
|
+
.ss-loader {
|
|
44
|
+
position: fixed;
|
|
45
|
+
inset: 0;
|
|
46
|
+
z-index: 99999;
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
gap: 2rem;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
background: linear-gradient(
|
|
54
|
+
135deg,
|
|
55
|
+
#1a1a2e 0%,
|
|
56
|
+
#16213e 50%,
|
|
57
|
+
#0f3460 100%
|
|
58
|
+
);
|
|
59
|
+
transition: opacity 0.4s ease;
|
|
60
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
61
|
+
Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
62
|
+
}
|
|
63
|
+
.ss-loader::before {
|
|
64
|
+
content: "";
|
|
65
|
+
position: absolute;
|
|
66
|
+
width: 200%;
|
|
67
|
+
height: 200%;
|
|
68
|
+
background-image: radial-gradient(
|
|
69
|
+
circle,
|
|
70
|
+
rgba(168, 192, 255, 0.15) 1px,
|
|
71
|
+
transparent 1px
|
|
72
|
+
);
|
|
73
|
+
background-size: 50px 50px;
|
|
74
|
+
will-change: transform;
|
|
75
|
+
animation: ss-loader-drift 20s linear infinite;
|
|
76
|
+
}
|
|
77
|
+
.ss-loader.is-hiding {
|
|
78
|
+
opacity: 0;
|
|
79
|
+
pointer-events: none;
|
|
80
|
+
}
|
|
81
|
+
.ss-loader__logo {
|
|
82
|
+
width: 120px;
|
|
83
|
+
height: 120px;
|
|
84
|
+
object-fit: contain;
|
|
85
|
+
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.3));
|
|
86
|
+
transform-origin: center;
|
|
87
|
+
will-change: transform;
|
|
88
|
+
animation:
|
|
89
|
+
ss-loader-grow 0.6s cubic-bezier(0.22, 1, 0.36, 1) both,
|
|
90
|
+
ss-loader-breathe 3s ease-in-out 0.6s infinite;
|
|
91
|
+
}
|
|
92
|
+
.ss-loader__label {
|
|
93
|
+
margin: 0;
|
|
94
|
+
color: #fff;
|
|
95
|
+
font-size: 1.25rem;
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
letter-spacing: 0.5px;
|
|
98
|
+
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
|
99
|
+
}
|
|
100
|
+
@keyframes ss-loader-drift {
|
|
101
|
+
from { transform: translate(0, 0); }
|
|
102
|
+
to { transform: translate(-50px, -50px); }
|
|
103
|
+
}
|
|
104
|
+
@keyframes ss-loader-grow {
|
|
105
|
+
from { transform: scale(0); opacity: 0; }
|
|
106
|
+
to { transform: scale(1); opacity: 1; }
|
|
107
|
+
}
|
|
108
|
+
@keyframes ss-loader-breathe {
|
|
109
|
+
0%, 100% { transform: scale(1); }
|
|
110
|
+
50% { transform: scale(1.06); }
|
|
111
|
+
}
|
|
112
|
+
@media (prefers-reduced-motion: reduce) {
|
|
113
|
+
:root:not([data-motion="full"]) .ss-loader::before {
|
|
114
|
+
animation: none;
|
|
115
|
+
}
|
|
116
|
+
:root:not([data-motion="full"]) .ss-loader__logo {
|
|
117
|
+
animation: ss-loader-breathe 3s ease-in-out infinite;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
[data-motion="reduce"] .ss-loader::before {
|
|
121
|
+
animation: none;
|
|
122
|
+
}
|
|
123
|
+
[data-motion="reduce"] .ss-loader__logo {
|
|
124
|
+
animation: ss-loader-breathe 3s ease-in-out infinite;
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
19
127
|
</head>
|
|
20
128
|
<body>
|
|
129
|
+
<!-- Boot splash - shown immediately, removed once the app is ready. -->
|
|
130
|
+
<div id="ss-loader" class="ss-loader" role="status" aria-live="polite">
|
|
131
|
+
<img
|
|
132
|
+
class="ss-loader__logo"
|
|
133
|
+
src="./orbits-logo.png"
|
|
134
|
+
alt="Orbit"
|
|
135
|
+
width="120"
|
|
136
|
+
height="120"
|
|
137
|
+
/>
|
|
138
|
+
<p class="ss-loader__label">Loading Orbit…</p>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
21
141
|
<div class="grain">
|
|
22
142
|
<div class="milky-way" aria-hidden="true"></div>
|
|
23
143
|
</div>
|
|
@@ -39,6 +159,7 @@
|
|
|
39
159
|
id="newTicketBtn"
|
|
40
160
|
type="button"
|
|
41
161
|
class="topbar-btn btn-sun topbar-ctl-shrink"
|
|
162
|
+
data-variant="cta"
|
|
42
163
|
>
|
|
43
164
|
<span class="btn-plus" aria-hidden="true">+</span>
|
|
44
165
|
<span class="btn-sun-label">New Card</span>
|
|
@@ -48,6 +169,7 @@
|
|
|
48
169
|
type="button"
|
|
49
170
|
id="boardMenuBtn"
|
|
50
171
|
class="topbar-chip topbar-ctl-shrink"
|
|
172
|
+
data-variant="board-chip"
|
|
51
173
|
aria-expanded="false"
|
|
52
174
|
aria-haspopup="true"
|
|
53
175
|
aria-controls="boardFlyout"
|
|
@@ -65,7 +187,7 @@
|
|
|
65
187
|
y="4"
|
|
66
188
|
width="6"
|
|
67
189
|
height="16"
|
|
68
|
-
rx="
|
|
190
|
+
rx="0.75"
|
|
69
191
|
stroke="currentColor"
|
|
70
192
|
stroke-width="2"
|
|
71
193
|
/>
|
|
@@ -74,12 +196,12 @@
|
|
|
74
196
|
y="4"
|
|
75
197
|
width="6"
|
|
76
198
|
height="16"
|
|
77
|
-
rx="
|
|
199
|
+
rx="0.75"
|
|
78
200
|
stroke="currentColor"
|
|
79
201
|
stroke-width="2"
|
|
80
202
|
/>
|
|
81
203
|
</svg>
|
|
82
|
-
Board
|
|
204
|
+
<span class="topbar-chip-label">Board</span>
|
|
83
205
|
</button>
|
|
84
206
|
<div
|
|
85
207
|
id="boardFlyout"
|
|
@@ -91,7 +213,8 @@
|
|
|
91
213
|
<button
|
|
92
214
|
id="themeToggle"
|
|
93
215
|
type="button"
|
|
94
|
-
class="topbar-btn
|
|
216
|
+
class="topbar-btn topbar-ctl-shrink"
|
|
217
|
+
data-variant="theme"
|
|
95
218
|
aria-label="Toggle theme"
|
|
96
219
|
>
|
|
97
220
|
<svg
|
|
@@ -115,7 +238,8 @@
|
|
|
115
238
|
<button
|
|
116
239
|
id="settingsBtn"
|
|
117
240
|
type="button"
|
|
118
|
-
class="topbar-btn
|
|
241
|
+
class="topbar-btn topbar-ctl-shrink"
|
|
242
|
+
data-variant="translucent"
|
|
119
243
|
>
|
|
120
244
|
<svg
|
|
121
245
|
class="topbar-icon"
|
|
@@ -170,19 +294,10 @@
|
|
|
170
294
|
></div>
|
|
171
295
|
<aside id="drawer" class="drawer" hidden aria-label="Panel">
|
|
172
296
|
<header class="drawer-header">
|
|
173
|
-
<div
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
hidden
|
|
178
|
-
></span>
|
|
179
|
-
<h2 id="drawerTitle" class="drawer-title-text"></h2>
|
|
180
|
-
<div
|
|
181
|
-
id="drawerSubtitle"
|
|
182
|
-
class="drawer-subtitle"
|
|
183
|
-
hidden
|
|
184
|
-
></div>
|
|
185
|
-
</div>
|
|
297
|
+
<div
|
|
298
|
+
id="drawerTitleBlock"
|
|
299
|
+
class="drawer-title-block"
|
|
300
|
+
></div>
|
|
186
301
|
<nav
|
|
187
302
|
id="drawerTabs"
|
|
188
303
|
class="drawer-tabs"
|
|
@@ -310,5 +425,39 @@
|
|
|
310
425
|
</dialog>
|
|
311
426
|
|
|
312
427
|
<script src="/js/main.js" type="module"></script>
|
|
428
|
+
<!-- Fade out the boot splash once the DOM is ready. The /js/main.js
|
|
429
|
+
module runs before DOMContentLoaded, so theme/motion + wiring are
|
|
430
|
+
already applied by the time this fires. -->
|
|
431
|
+
<script>
|
|
432
|
+
(function () {
|
|
433
|
+
function hideSplash() {
|
|
434
|
+
var splash = document.getElementById("ss-loader");
|
|
435
|
+
if (!splash) return;
|
|
436
|
+
// Keep the splash up for a minimum of 786ms (measured from
|
|
437
|
+
// navigation start) so a fast load doesn't flash it in and out.
|
|
438
|
+
var wait = Math.max(0, 786 - performance.now());
|
|
439
|
+
setTimeout(function () {
|
|
440
|
+
splash.classList.add("is-hiding");
|
|
441
|
+
splash.addEventListener(
|
|
442
|
+
"transitionend",
|
|
443
|
+
function () {
|
|
444
|
+
splash.remove();
|
|
445
|
+
},
|
|
446
|
+
{ once: true }
|
|
447
|
+
);
|
|
448
|
+
setTimeout(function () {
|
|
449
|
+
splash.remove();
|
|
450
|
+
}, 600);
|
|
451
|
+
}, wait);
|
|
452
|
+
}
|
|
453
|
+
if (document.readyState === "loading") {
|
|
454
|
+
document.addEventListener("DOMContentLoaded", hideSplash, {
|
|
455
|
+
once: true
|
|
456
|
+
});
|
|
457
|
+
} else {
|
|
458
|
+
hideSplash();
|
|
459
|
+
}
|
|
460
|
+
})();
|
|
461
|
+
</script>
|
|
313
462
|
</body>
|
|
314
463
|
</html>
|
package/public/js/board-menu.js
CHANGED
|
@@ -23,11 +23,25 @@ export function closeMenuFlyouts() {
|
|
|
23
23
|
closeBoardFlyout();
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const boardChipIcon = `
|
|
27
|
+
<svg
|
|
28
|
+
class="topbar-icon"
|
|
29
|
+
width="16"
|
|
30
|
+
height="16"
|
|
31
|
+
viewBox="0 0 24 24"
|
|
32
|
+
fill="none"
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
>
|
|
35
|
+
<rect x="4" y="4" width="6" height="16" rx="0.75" stroke="currentColor" stroke-width="2" />
|
|
36
|
+
<rect x="14" y="4" width="6" height="16" rx="0.75" stroke="currentColor" stroke-width="2" />
|
|
37
|
+
</svg>`;
|
|
38
|
+
|
|
26
39
|
export function updateTopbarChips() {
|
|
27
40
|
if (!state.data) return;
|
|
28
41
|
const board = (state.data.boards || []).find((b) => b.id === state.boardId);
|
|
29
42
|
if (boardMenuBtn) {
|
|
30
|
-
|
|
43
|
+
const label = board?.name || "Board";
|
|
44
|
+
boardMenuBtn.innerHTML = `${boardChipIcon}<span class="topbar-chip-label">${escapeHtml(label)}</span>`;
|
|
31
45
|
boardMenuBtn.title = board ? `${board.name} — slug: ${board.slug}` : "Choose board";
|
|
32
46
|
}
|
|
33
47
|
}
|
|
@@ -37,7 +51,7 @@ function renderBoardFlyoutInner() {
|
|
|
37
51
|
const rows = boards
|
|
38
52
|
.map(
|
|
39
53
|
(b) => `
|
|
40
|
-
<button type="button" class="menu-flyout-item ${b.id === state.boardId ? "is-current" : ""}" data-pick-board="${escapeHtml(b.id)}" role="menuitem">
|
|
54
|
+
<button type="button" class="menu-flyout-item ${b.id === state.boardId ? "is-current" : ""}" data-variant="menu-item" data-pick-board="${escapeHtml(b.id)}" role="menuitem">
|
|
41
55
|
<span class="menu-flyout-item-title">${escapeHtml(b.name)}</span>
|
|
42
56
|
<span class="menu-flyout-item-meta">${escapeHtml(b.slug)}</span>
|
|
43
57
|
</button>`
|
|
@@ -245,6 +245,25 @@ export function bindCardContextMenu(card, ticket, { handlers = createCardActionH
|
|
|
245
245
|
});
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// Delegated variant: a single listener on the board container resolves the card
|
|
249
|
+
// and its ticket per event. Lets renderBoard() swap innerHTML freely without
|
|
250
|
+
// re-binding a contextmenu listener onto every card on every render.
|
|
251
|
+
export function bindCardContextMenuDelegated(container, resolveTicket, { handlers = createCardActionHandlers(), onBeforeOpen = null } = {}) {
|
|
252
|
+
container.addEventListener("contextmenu", (event) => {
|
|
253
|
+
const card = event.target instanceof Element ? event.target.closest(".card") : null;
|
|
254
|
+
if (!card) return;
|
|
255
|
+
const ticket = resolveTicket(card.dataset.ticketId);
|
|
256
|
+
if (!ticket) return;
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
event.stopPropagation();
|
|
259
|
+
if (dragInFlight) {
|
|
260
|
+
closeCardActionMenu();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
openCardActionMenu({ ticket, anchor: card, x: event.clientX, y: event.clientY, handlers, onBeforeOpen });
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
248
267
|
function renderMenuItems(items) {
|
|
249
268
|
return `<ul class="card-action-menu-list" role="none">${items.map(renderMenuItem).join("")}</ul>`;
|
|
250
269
|
}
|
package/public/js/create-card.js
CHANGED
|
@@ -86,7 +86,7 @@ function renderCreateForm(preselectedLaneId) {
|
|
|
86
86
|
<label>
|
|
87
87
|
Type
|
|
88
88
|
<select id="createTicketType" name="type" class="select-chevron-field" aria-label="Ticket type">
|
|
89
|
-
<option value="epic">Epic
|
|
89
|
+
<option value="epic">Epic</option>
|
|
90
90
|
<option value="feature">Feature</option>
|
|
91
91
|
<option value="task" selected>Task</option>
|
|
92
92
|
<option value="bug">Bug</option>
|
package/public/js/drawer.js
CHANGED
|
@@ -36,19 +36,9 @@ export function closeDrawer() {
|
|
|
36
36
|
drawer.hidden = true;
|
|
37
37
|
drawerBackdrop.hidden = true;
|
|
38
38
|
drawerInner.innerHTML = "";
|
|
39
|
-
const
|
|
40
|
-
const titleEl = document.getElementById("drawerTitle");
|
|
41
|
-
const subtitleEl = document.getElementById("drawerSubtitle");
|
|
39
|
+
const titleBlockEl = document.getElementById("drawerTitleBlock");
|
|
42
40
|
const tabsEl = document.getElementById("drawerTabs");
|
|
43
|
-
if (
|
|
44
|
-
if (titleEl) {
|
|
45
|
-
for (const attr of Array.from(titleEl.attributes)) {
|
|
46
|
-
if (attr.name !== "id") titleEl.removeAttribute(attr.name);
|
|
47
|
-
}
|
|
48
|
-
titleEl.className = "drawer-title-text";
|
|
49
|
-
titleEl.textContent = "";
|
|
50
|
-
}
|
|
51
|
-
if (subtitleEl) { subtitleEl.innerHTML = ""; subtitleEl.hidden = true; }
|
|
41
|
+
if (titleBlockEl) titleBlockEl.innerHTML = "";
|
|
52
42
|
if (tabsEl) { tabsEl.innerHTML = ""; tabsEl.hidden = true; }
|
|
53
43
|
drawer.classList.remove("is-wide");
|
|
54
44
|
};
|
|
@@ -77,40 +67,24 @@ export function closeDrawer() {
|
|
|
77
67
|
export function renderDrawerShell({ eyebrow, title, titleAttrs, subtitleHtml, tabs, activeTab, onTabSelect, body, mode }) {
|
|
78
68
|
drawer.classList.toggle("is-settings", mode === "settings");
|
|
79
69
|
drawer.classList.toggle("is-wide", mode === "ticket" || mode === "settings");
|
|
80
|
-
const
|
|
81
|
-
const titleEl = document.getElementById("drawerTitle");
|
|
82
|
-
const subtitleEl = document.getElementById("drawerSubtitle");
|
|
70
|
+
const titleBlockEl = document.getElementById("drawerTitleBlock");
|
|
83
71
|
const tabsEl = document.getElementById("drawerTabs");
|
|
84
72
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
subtitleEl.innerHTML = subtitleHtml;
|
|
95
|
-
subtitleEl.hidden = false;
|
|
96
|
-
} else {
|
|
97
|
-
subtitleEl.innerHTML = "";
|
|
98
|
-
subtitleEl.hidden = true;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Reset prior edit-attrs / classes each render so different modes don't
|
|
102
|
-
// carry state from one another.
|
|
103
|
-
for (const attr of Array.from(titleEl.attributes)) {
|
|
104
|
-
if (attr.name !== "id") titleEl.removeAttribute(attr.name);
|
|
105
|
-
}
|
|
106
|
-
titleEl.className = "drawer-title-text";
|
|
107
|
-
if (titleAttrs) {
|
|
108
|
-
for (const [key, value] of Object.entries(titleAttrs)) {
|
|
109
|
-
if (key === "class") titleEl.className = `drawer-title-text ${value}`;
|
|
110
|
-
else titleEl.setAttribute(key, value);
|
|
111
|
-
}
|
|
73
|
+
// Rebuild the eyebrow / title / subtitle wholesale each render. Like
|
|
74
|
+
// drawerInner.innerHTML below, this means any listeners the caller attached
|
|
75
|
+
// last render (e.g. the inline title editor's click handler) die with the
|
|
76
|
+
// old nodes, instead of stacking on persistent shell elements.
|
|
77
|
+
const extraTitleClass = titleAttrs?.class ? ` ${titleAttrs.class}` : "";
|
|
78
|
+
let titleAttrHtml = ` class="drawer-title drawer-title-text${extraTitleClass}"`;
|
|
79
|
+
for (const [key, value] of Object.entries(titleAttrs || {})) {
|
|
80
|
+
if (key === "class") continue;
|
|
81
|
+
titleAttrHtml += ` ${key}="${escapeHtml(String(value))}"`;
|
|
112
82
|
}
|
|
113
|
-
|
|
83
|
+
const segments = [];
|
|
84
|
+
if (eyebrow) segments.push(`<span class="drawer-eyebrow">${escapeHtml(eyebrow)}</span>`);
|
|
85
|
+
segments.push(`<h2${titleAttrHtml}>${escapeHtml(title ?? "")}</h2>`);
|
|
86
|
+
if (subtitleHtml) segments.push(`<div class="drawer-subtitle">${subtitleHtml}</div>`);
|
|
87
|
+
titleBlockEl.innerHTML = segments.join("");
|
|
114
88
|
|
|
115
89
|
if (tabs && tabs.length) {
|
|
116
90
|
tabsEl.hidden = false;
|
package/public/js/format.js
CHANGED
|
@@ -11,9 +11,9 @@ export function escapeHtml(value) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// UTF-8 bytes mis-decoded as CP-437 (the typical Windows-tmux artifact).
|
|
14
|
-
// Ported from
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// Ported from a local fix-newlines utility — every entry is [mojibake sequence,
|
|
15
|
+
// what it should have been]. Long sequences first so shorter prefixes do not
|
|
16
|
+
// eat them.
|
|
17
17
|
const MOJIBAKE_REPLACEMENTS = [
|
|
18
18
|
// General Punctuation (UTF-8 E2 80 XX → CP-437 "ΓÇ" + glyph)
|
|
19
19
|
["ΓÇô", "–"],
|
|
@@ -84,8 +84,8 @@ const MOJIBAKE_REPLACEMENTS = [
|
|
|
84
84
|
["┬º", "§"],
|
|
85
85
|
["┬╖", "·"],
|
|
86
86
|
|
|
87
|
-
// Stray curly quotes that survived as proper Unicode
|
|
88
|
-
//
|
|
87
|
+
// Stray curly quotes that survived as proper Unicode; normalize to ASCII
|
|
88
|
+
// straight quotes so pasted terminal/log text stays consistent.
|
|
89
89
|
["“", '"'],
|
|
90
90
|
["”", '"'],
|
|
91
91
|
["‘", "'"],
|
|
@@ -389,11 +389,10 @@ export function typeLabel(type) {
|
|
|
389
389
|
return labels[canonicalTicketType(type)] || "Task";
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
// Long, hierarchy-aware label for dropdowns where we have the room.
|
|
393
|
-
// pairs older vocabulary so existing terminology still resonates.
|
|
392
|
+
// Long, hierarchy-aware label for dropdowns where we have the room.
|
|
394
393
|
export function typeLabelLong(type) {
|
|
395
394
|
const labels = {
|
|
396
|
-
epic: "Epic
|
|
395
|
+
epic: "Epic",
|
|
397
396
|
feature: "Feature",
|
|
398
397
|
task: "Task",
|
|
399
398
|
bug: "Bug"
|