@starlove/orbit 1.3.4 → 1.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/Dockerfile +2 -1
- package/README.md +10 -9
- package/SKILL-ORBIT.md +41 -17
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/OFL.txt +93 -0
- package/dist/full/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/dist/full/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/dist/full/fonts/Inter/OFL.txt +93 -0
- package/dist/full/fonts/Inter/README.txt +118 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/JetBrains_Mono/OFL.txt +93 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +79 -0
- package/dist/full/fonts/OpenRing-Regular.ttf +0 -0
- package/dist/full/fonts/Open_Sans/OFL.txt +93 -0
- package/dist/full/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/dist/full/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/dist/full/fonts/Open_Sans/README.txt +100 -0
- package/dist/full/fonts/Orbitron/OFL.txt +93 -0
- package/dist/full/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/Orbitron/README.txt +68 -0
- package/dist/full/fonts/Rajdhani/OFL.txt +93 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/dist/full/fonts/runescape-uf.ttf +0 -0
- package/dist/full/index.html +137 -27
- package/dist/full/js/app.62905fcb.js +952 -0
- package/dist/full/js/app.62905fcb.js.map +7 -0
- package/dist/full/styles.4e60d824.css +1 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +20 -13
- package/docs/ORBIT_DISPATCH.md +73 -38
- package/docs/UPDATING.md +114 -0
- package/package.json +5 -7
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +93 -0
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +93 -0
- package/public/fonts/Inter/README.txt +118 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/OFL.txt +93 -0
- package/public/fonts/JetBrains_Mono/README.txt +79 -0
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OFL.txt +93 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +100 -0
- package/public/fonts/Orbitron/OFL.txt +93 -0
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +68 -0
- package/public/fonts/Rajdhani/OFL.txt +93 -0
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +135 -25
- package/public/js/artifact-drawer.js +79 -0
- package/public/js/board-export.js +161 -0
- package/public/js/board-menu.js +219 -17
- package/public/js/card-actions.js +1 -1
- package/public/js/create-card.js +60 -23
- package/public/js/drawer.js +164 -4
- package/public/js/font-preference.js +229 -0
- package/public/js/format.js +0 -10
- package/public/js/icons.js +16 -0
- package/public/js/kanban.js +123 -1
- package/public/js/local-backend.js +31 -4
- package/public/js/main.js +7 -1
- package/public/js/modal.js +3 -1
- package/public/js/panel.js +58 -0
- package/public/js/settings-tabs.js +1 -1
- package/public/js/settings.js +606 -275
- package/public/js/sse.js +3 -0
- package/public/js/state.js +1 -1
- package/public/js/text-editing.js +128 -0
- package/public/js/ticket-detail.js +377 -148
- package/public/js/ticket-typeahead.js +49 -0
- package/public/js/toast.js +19 -4
- package/public/js/topbar-notes.js +3 -3
- package/public/js/typeahead.js +206 -0
- package/public/js/update-check.js +58 -0
- package/public/styles.css +5455 -3538
- package/src/cli/orbit.js +9 -5
- package/src/core/agent.js +12 -4
- package/src/core/agents-md.js +3 -3
- package/src/core/artifacts.js +149 -0
- package/src/core/board.js +3 -2
- package/src/core/boards.js +0 -1
- package/src/core/css-bundle.js +1 -0
- package/src/core/db.js +6 -0
- package/src/core/dispatch.js +344 -34
- package/src/core/http.js +9 -2
- package/src/core/paths.js +26 -2
- package/src/core/router.js +33 -0
- package/src/core/search.js +3 -2
- package/src/core/snapshots.js +10 -5
- package/src/core/tickets.js +34 -3
- package/src/core/util.js +3 -2
- package/src/core/version-check.js +154 -0
- package/src/mcp/orbit-client.js +18 -2
- package/src/mcp-server.js +19 -14
- package/src/server.js +28 -3
- package/dist/full/js/app.fb7767dc.js +0 -742
- package/dist/full/js/app.fb7767dc.js.map +0 -7
- package/dist/full/styles.bbc542f5.css +0 -1
package/public/js/drawer.js
CHANGED
|
@@ -10,6 +10,158 @@ import { escapeHtml } from "./format.js";
|
|
|
10
10
|
/** Bumps on each open/close so a stale `transitionend` from an old close cannot clear a newly opened drawer. */
|
|
11
11
|
let drawerToken = 0;
|
|
12
12
|
|
|
13
|
+
/** Aborts scroll/resize observers when tabs are re-rendered or the drawer closes. */
|
|
14
|
+
let drawerTabsScrollAbort = null;
|
|
15
|
+
|
|
16
|
+
/** Toggle edge-fade classes on the tab shell when the strip can scroll further left/right. */
|
|
17
|
+
function syncDrawerTabsScrollState(tabsEl) {
|
|
18
|
+
const shellEl = document.getElementById("drawerTabsShell");
|
|
19
|
+
if (!tabsEl || !shellEl) return;
|
|
20
|
+
|
|
21
|
+
const maxScroll = tabsEl.scrollWidth - tabsEl.clientWidth;
|
|
22
|
+
if (maxScroll <= 1) {
|
|
23
|
+
shellEl.classList.remove("can-scroll-left", "can-scroll-right");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
shellEl.classList.toggle("can-scroll-left", tabsEl.scrollLeft > 1);
|
|
28
|
+
shellEl.classList.toggle("can-scroll-right", tabsEl.scrollLeft < maxScroll - 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Keep the active settings tab visible when the strip overflows horizontally. */
|
|
32
|
+
function scrollActiveDrawerTabIntoView(tabsEl) {
|
|
33
|
+
tabsEl
|
|
34
|
+
?.querySelector(".drawer-tab.is-active")
|
|
35
|
+
?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function teardownDrawerTabsScroll() {
|
|
39
|
+
drawerTabsScrollAbort?.abort();
|
|
40
|
+
drawerTabsScrollAbort = null;
|
|
41
|
+
const shellEl = document.getElementById("drawerTabsShell");
|
|
42
|
+
const tabsEl = document.getElementById("drawerTabs");
|
|
43
|
+
shellEl?.classList.remove("can-scroll-left", "can-scroll-right");
|
|
44
|
+
tabsEl?.classList.remove("is-grabbing");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function bindDrawerTabsScroll(tabsEl) {
|
|
48
|
+
teardownDrawerTabsScroll();
|
|
49
|
+
if (!tabsEl) return;
|
|
50
|
+
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
drawerTabsScrollAbort = controller;
|
|
53
|
+
const { signal } = controller;
|
|
54
|
+
|
|
55
|
+
const onScrollChange = () => syncDrawerTabsScrollState(tabsEl);
|
|
56
|
+
tabsEl.addEventListener("scroll", onScrollChange, { passive: true, signal });
|
|
57
|
+
|
|
58
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
59
|
+
const observer = new ResizeObserver(onScrollChange);
|
|
60
|
+
observer.observe(tabsEl);
|
|
61
|
+
signal.addEventListener("abort", () => observer.disconnect(), { once: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Vertical wheel over the tab strip pans horizontally (no Shift required).
|
|
65
|
+
const LINE_SCROLL_PX = 16;
|
|
66
|
+
const wheelPixels = (value, mode) => {
|
|
67
|
+
if (mode === WheelEvent.DOM_DELTA_LINE) return value * LINE_SCROLL_PX;
|
|
68
|
+
if (mode === WheelEvent.DOM_DELTA_PAGE) return value * Math.max(tabsEl.clientWidth, 1);
|
|
69
|
+
return value;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
tabsEl.addEventListener(
|
|
73
|
+
"wheel",
|
|
74
|
+
(event) => {
|
|
75
|
+
if (event.defaultPrevented || event.ctrlKey) return;
|
|
76
|
+
if (tabsEl.scrollWidth <= tabsEl.clientWidth) return;
|
|
77
|
+
|
|
78
|
+
const horizontalDelta = event.deltaX || event.deltaY;
|
|
79
|
+
if (!horizontalDelta) return;
|
|
80
|
+
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
tabsEl.scrollLeft += wheelPixels(horizontalDelta, event.deltaMode);
|
|
83
|
+
},
|
|
84
|
+
{ passive: false, signal }
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Click-and-drag horizontal panning; threshold keeps tab clicks intact.
|
|
88
|
+
const DRAG_THRESHOLD = 4;
|
|
89
|
+
let dragActive = false;
|
|
90
|
+
let dragged = false;
|
|
91
|
+
let startX = 0;
|
|
92
|
+
let startScroll = 0;
|
|
93
|
+
let pointerId = 0;
|
|
94
|
+
|
|
95
|
+
tabsEl.addEventListener(
|
|
96
|
+
"pointerdown",
|
|
97
|
+
(event) => {
|
|
98
|
+
if (event.button !== 0) return;
|
|
99
|
+
if (tabsEl.scrollWidth <= tabsEl.clientWidth) return;
|
|
100
|
+
dragged = false;
|
|
101
|
+
dragActive = true;
|
|
102
|
+
pointerId = event.pointerId;
|
|
103
|
+
startX = event.clientX;
|
|
104
|
+
startScroll = tabsEl.scrollLeft;
|
|
105
|
+
},
|
|
106
|
+
{ signal }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
tabsEl.addEventListener(
|
|
110
|
+
"pointermove",
|
|
111
|
+
(event) => {
|
|
112
|
+
if (!dragActive || event.pointerId !== pointerId) return;
|
|
113
|
+
const delta = event.clientX - startX;
|
|
114
|
+
if (!dragged && Math.abs(delta) > DRAG_THRESHOLD) {
|
|
115
|
+
dragged = true;
|
|
116
|
+
try {
|
|
117
|
+
tabsEl.setPointerCapture(pointerId);
|
|
118
|
+
} catch {
|
|
119
|
+
/* no-op: some browsers can't capture here */
|
|
120
|
+
}
|
|
121
|
+
tabsEl.classList.add("is-grabbing");
|
|
122
|
+
}
|
|
123
|
+
if (dragged) {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
tabsEl.scrollLeft = startScroll - delta;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{ signal }
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const endDrag = (event) => {
|
|
132
|
+
if (!dragActive || (event && event.pointerId !== pointerId)) return;
|
|
133
|
+
dragActive = false;
|
|
134
|
+
tabsEl.classList.remove("is-grabbing");
|
|
135
|
+
try {
|
|
136
|
+
tabsEl.releasePointerCapture(pointerId);
|
|
137
|
+
} catch {
|
|
138
|
+
/* no-op */
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
tabsEl.addEventListener("pointerup", endDrag, { signal });
|
|
143
|
+
tabsEl.addEventListener("pointercancel", endDrag, { signal });
|
|
144
|
+
tabsEl.addEventListener("pointerleave", endDrag, { signal });
|
|
145
|
+
|
|
146
|
+
tabsEl.addEventListener(
|
|
147
|
+
"click",
|
|
148
|
+
(event) => {
|
|
149
|
+
if (dragged) {
|
|
150
|
+
event.stopPropagation();
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
dragged = false;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{ capture: true, signal }
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
requestAnimationFrame(() => {
|
|
159
|
+
if (signal.aborted) return;
|
|
160
|
+
onScrollChange();
|
|
161
|
+
scrollActiveDrawerTabIntoView(tabsEl);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
13
165
|
/** Slide the right drawer in (ticket detail, project settings, or create form). */
|
|
14
166
|
export function openDrawer() {
|
|
15
167
|
drawerToken += 1;
|
|
@@ -38,8 +190,11 @@ export function closeDrawer() {
|
|
|
38
190
|
drawerInner.innerHTML = "";
|
|
39
191
|
const titleBlockEl = document.getElementById("drawerTitleBlock");
|
|
40
192
|
const tabsEl = document.getElementById("drawerTabs");
|
|
193
|
+
const tabsShellEl = document.getElementById("drawerTabsShell");
|
|
41
194
|
if (titleBlockEl) titleBlockEl.innerHTML = "";
|
|
42
|
-
if (tabsEl)
|
|
195
|
+
if (tabsEl) tabsEl.innerHTML = "";
|
|
196
|
+
if (tabsShellEl) tabsShellEl.hidden = true;
|
|
197
|
+
teardownDrawerTabsScroll();
|
|
43
198
|
drawer.classList.remove("is-wide");
|
|
44
199
|
};
|
|
45
200
|
|
|
@@ -69,6 +224,7 @@ export function renderDrawerShell({ eyebrow, title, titleAttrs, subtitleHtml, ta
|
|
|
69
224
|
drawer.classList.toggle("is-wide", mode === "ticket" || mode === "settings");
|
|
70
225
|
const titleBlockEl = document.getElementById("drawerTitleBlock");
|
|
71
226
|
const tabsEl = document.getElementById("drawerTabs");
|
|
227
|
+
const tabsShellEl = document.getElementById("drawerTabsShell");
|
|
72
228
|
|
|
73
229
|
// Rebuild the eyebrow / title / subtitle wholesale each render. Like
|
|
74
230
|
// drawerInner.innerHTML below, this means any listeners the caller attached
|
|
@@ -82,12 +238,14 @@ export function renderDrawerShell({ eyebrow, title, titleAttrs, subtitleHtml, ta
|
|
|
82
238
|
}
|
|
83
239
|
const segments = [];
|
|
84
240
|
if (eyebrow) segments.push(`<span class="drawer-eyebrow">${escapeHtml(eyebrow)}</span>`);
|
|
85
|
-
|
|
241
|
+
if (title != null && String(title).trim() !== "") {
|
|
242
|
+
segments.push(`<h2${titleAttrHtml}>${escapeHtml(title)}</h2>`);
|
|
243
|
+
}
|
|
86
244
|
if (subtitleHtml) segments.push(`<div class="drawer-subtitle">${subtitleHtml}</div>`);
|
|
87
245
|
titleBlockEl.innerHTML = segments.join("");
|
|
88
246
|
|
|
89
247
|
if (tabs && tabs.length) {
|
|
90
|
-
|
|
248
|
+
if (tabsShellEl) tabsShellEl.hidden = false;
|
|
91
249
|
tabsEl.innerHTML = tabs
|
|
92
250
|
.map(
|
|
93
251
|
(t) =>
|
|
@@ -101,9 +259,11 @@ export function renderDrawerShell({ eyebrow, title, titleAttrs, subtitleHtml, ta
|
|
|
101
259
|
btn.addEventListener("click", () => onTabSelect(btn.dataset.drawerTab));
|
|
102
260
|
});
|
|
103
261
|
}
|
|
262
|
+
bindDrawerTabsScroll(tabsEl);
|
|
104
263
|
} else {
|
|
105
|
-
|
|
264
|
+
if (tabsShellEl) tabsShellEl.hidden = true;
|
|
106
265
|
tabsEl.innerHTML = "";
|
|
266
|
+
teardownDrawerTabsScroll();
|
|
107
267
|
}
|
|
108
268
|
|
|
109
269
|
const archiveBtn = document.getElementById("drawerArchiveBtn");
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
export const FONT_STORAGE_KEY = "orbit_font_preferences";
|
|
2
|
+
export const FONT_CHANGE_EVENT = "orbit:font-preference-change";
|
|
3
|
+
|
|
4
|
+
// Inter is the default site font and for most categories; badges default to
|
|
5
|
+
// Orbitron. The site font drives global font metrics such as size, spacing,
|
|
6
|
+
// and line-height.
|
|
7
|
+
export const DEFAULT_SITE_FONT = "inter";
|
|
8
|
+
export const DEFAULT_BADGE_FONT = "orbitron";
|
|
9
|
+
export const DEFAULT_USER_SCALE = 1;
|
|
10
|
+
export const USER_SCALE_MIN = 0.85;
|
|
11
|
+
export const USER_SCALE_MAX = 1.3;
|
|
12
|
+
export const USER_SCALE_STEP = 0.05;
|
|
13
|
+
|
|
14
|
+
export const SITE_FONT_OPTIONS = Object.freeze([
|
|
15
|
+
"inter",
|
|
16
|
+
"openSans",
|
|
17
|
+
"jetBrainsMono",
|
|
18
|
+
"chakraPetch",
|
|
19
|
+
"orbitron",
|
|
20
|
+
"rajdhani",
|
|
21
|
+
"runescapeUF",
|
|
22
|
+
"openRing"
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const FONT_OPTIONS = Object.freeze({
|
|
26
|
+
inter: {
|
|
27
|
+
label: "Inter",
|
|
28
|
+
stack: "\"Orbit Inter\", \"Inter\", \"Segoe UI Variable\", \"Segoe UI\", Arial, sans-serif",
|
|
29
|
+
className: "font-inter"
|
|
30
|
+
},
|
|
31
|
+
openSans: {
|
|
32
|
+
label: "Open Sans",
|
|
33
|
+
stack: "\"Orbit Open Sans\", \"Open Sans\", \"Segoe UI Variable\", \"Segoe UI\", Arial, sans-serif",
|
|
34
|
+
className: "font-open-sans"
|
|
35
|
+
},
|
|
36
|
+
jetBrainsMono: {
|
|
37
|
+
label: "JetBrains Mono",
|
|
38
|
+
stack: "\"Orbit JetBrains Mono\", \"JetBrains Mono\", \"Cascadia Mono\", \"SFMono-Regular\", Consolas, monospace",
|
|
39
|
+
className: "font-jetbrains-mono"
|
|
40
|
+
},
|
|
41
|
+
chakraPetch: {
|
|
42
|
+
label: "Chakra Petch",
|
|
43
|
+
stack: "\"Orbit Chakra Petch\", \"Chakra Petch\", \"Segoe UI Variable\", \"Segoe UI\", Arial, sans-serif",
|
|
44
|
+
className: "font-chakra-petch"
|
|
45
|
+
},
|
|
46
|
+
orbitron: {
|
|
47
|
+
label: "Orbitron",
|
|
48
|
+
stack: "\"Orbit Orbitron\", \"Orbitron\", \"Segoe UI Variable\", \"Segoe UI\", Arial, sans-serif",
|
|
49
|
+
className: "font-orbitron"
|
|
50
|
+
},
|
|
51
|
+
rajdhani: {
|
|
52
|
+
label: "Rajdhani",
|
|
53
|
+
stack: "\"Orbit Rajdhani\", \"Rajdhani\", \"Segoe UI Variable\", \"Segoe UI\", Arial, sans-serif",
|
|
54
|
+
className: "font-rajdhani"
|
|
55
|
+
},
|
|
56
|
+
runescapeUF: {
|
|
57
|
+
label: "Runescape UF",
|
|
58
|
+
stack: "\"Orbit Runescape UF\", \"Cascadia Mono\", monospace",
|
|
59
|
+
className: "font-runescape-uf"
|
|
60
|
+
},
|
|
61
|
+
openRing: {
|
|
62
|
+
label: "Open Ring",
|
|
63
|
+
stack: "\"Orbit Open Ring\", \"Segoe UI Variable\", \"Segoe UI\", Arial, sans-serif",
|
|
64
|
+
className: "font-open-ring"
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Per-category overrides shown in the Appearance grid. Body text is not a
|
|
69
|
+
// target: it always follows the primary font (`site`), so it drives
|
|
70
|
+
// --orbit-font-body directly in applyFontPreferences instead.
|
|
71
|
+
export const FONT_TARGETS = Object.freeze({
|
|
72
|
+
heading: {
|
|
73
|
+
label: "Headings",
|
|
74
|
+
cssVar: "--orbit-font-heading",
|
|
75
|
+
dataAttr: "fontHeading"
|
|
76
|
+
},
|
|
77
|
+
control: {
|
|
78
|
+
label: "Controls",
|
|
79
|
+
cssVar: "--orbit-font-control",
|
|
80
|
+
dataAttr: "fontControl"
|
|
81
|
+
},
|
|
82
|
+
label: {
|
|
83
|
+
label: "Labels",
|
|
84
|
+
cssVar: "--orbit-font-label",
|
|
85
|
+
dataAttr: "fontLabel"
|
|
86
|
+
},
|
|
87
|
+
badge: {
|
|
88
|
+
label: "Badges",
|
|
89
|
+
cssVar: "--orbit-font-badge",
|
|
90
|
+
dataAttr: "fontBadge"
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const LEGACY_FONT_TARGET_ALIASES = Object.freeze({
|
|
95
|
+
ui: "control"
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const DEFAULT_FONT_TARGET_PREFERENCES = Object.freeze({
|
|
99
|
+
control: DEFAULT_SITE_FONT,
|
|
100
|
+
heading: DEFAULT_SITE_FONT,
|
|
101
|
+
label: DEFAULT_SITE_FONT,
|
|
102
|
+
badge: DEFAULT_BADGE_FONT
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const DEFAULT_FONT_PREFERENCES = Object.freeze({
|
|
106
|
+
...DEFAULT_FONT_TARGET_PREFERENCES,
|
|
107
|
+
// Primary font: drives body text and the default for every override category.
|
|
108
|
+
site: DEFAULT_SITE_FONT,
|
|
109
|
+
// User size slider multiplier (drives --type-user-scale).
|
|
110
|
+
userScale: DEFAULT_USER_SCALE
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function hasOwn(object, key) {
|
|
114
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function clampUserScale(value) {
|
|
118
|
+
const number = Number(value);
|
|
119
|
+
if (!Number.isFinite(number)) return DEFAULT_USER_SCALE;
|
|
120
|
+
return Math.min(USER_SCALE_MAX, Math.max(USER_SCALE_MIN, number));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeFontPreferences(value) {
|
|
124
|
+
const input = value && typeof value === "object" ? value : {};
|
|
125
|
+
const normalized = { ...DEFAULT_FONT_PREFERENCES };
|
|
126
|
+
const explicitTargets = new Set();
|
|
127
|
+
for (const key of Object.keys(FONT_TARGETS)) {
|
|
128
|
+
if (typeof input[key] === "string" && hasOwn(FONT_OPTIONS, input[key])) {
|
|
129
|
+
normalized[key] = input[key];
|
|
130
|
+
explicitTargets.add(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const [legacyKey, target] of Object.entries(LEGACY_FONT_TARGET_ALIASES)) {
|
|
134
|
+
if (!explicitTargets.has(target) && typeof input[legacyKey] === "string" && hasOwn(FONT_OPTIONS, input[legacyKey])) {
|
|
135
|
+
normalized[target] = input[legacyKey];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (typeof input.site === "string" && hasOwn(FONT_OPTIONS, input.site)) {
|
|
139
|
+
normalized.site = input.site;
|
|
140
|
+
}
|
|
141
|
+
if (input.userScale !== undefined) {
|
|
142
|
+
normalized.userScale = clampUserScale(input.userScale);
|
|
143
|
+
}
|
|
144
|
+
// Before the split, one "label" target drove both categories. Preserve a
|
|
145
|
+
// customized label choice for badges unless badge was saved separately.
|
|
146
|
+
if (!hasOwn(input, "badge") && hasOwn(input, "label") && input.label !== DEFAULT_SITE_FONT) {
|
|
147
|
+
normalized.badge = normalized.label;
|
|
148
|
+
}
|
|
149
|
+
return normalized;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function storedFontPreferences(storage = globalThis.localStorage) {
|
|
153
|
+
if (!storage) return { ...DEFAULT_FONT_PREFERENCES };
|
|
154
|
+
try {
|
|
155
|
+
const raw = storage.getItem(FONT_STORAGE_KEY);
|
|
156
|
+
if (!raw) return { ...DEFAULT_FONT_PREFERENCES };
|
|
157
|
+
return normalizeFontPreferences(JSON.parse(raw));
|
|
158
|
+
} catch {
|
|
159
|
+
return { ...DEFAULT_FONT_PREFERENCES };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function fontStackFor(optionId) {
|
|
164
|
+
return FONT_OPTIONS[optionId]?.stack || FONT_OPTIONS[DEFAULT_SITE_FONT].stack;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function applyFontPreferences(preferences = storedFontPreferences(), { doc = globalThis.document } = {}) {
|
|
168
|
+
const normalized = normalizeFontPreferences(preferences);
|
|
169
|
+
const root = doc?.documentElement;
|
|
170
|
+
if (!root) return normalized;
|
|
171
|
+
|
|
172
|
+
for (const [target, config] of Object.entries(FONT_TARGETS)) {
|
|
173
|
+
const optionId = normalized[target];
|
|
174
|
+
root.style?.setProperty?.(config.cssVar, fontStackFor(optionId));
|
|
175
|
+
root.setAttribute?.(`data-${config.dataAttr.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}`, optionId);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Body text (paragraphs, card titles, long-form prose, editors) always tracks
|
|
179
|
+
// the primary font rather than being an independent category.
|
|
180
|
+
root.style?.setProperty?.("--orbit-font-body", fontStackFor(normalized.site));
|
|
181
|
+
// The only size lever is the Appearance slider; font family no longer changes size.
|
|
182
|
+
root.style?.setProperty?.("--type-user-scale", String(normalized.userScale));
|
|
183
|
+
root.setAttribute?.("data-font-site", normalized.site);
|
|
184
|
+
return normalized;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function setFontPreferences(preferences, {
|
|
188
|
+
doc = globalThis.document,
|
|
189
|
+
storage = globalThis.localStorage,
|
|
190
|
+
eventTarget = globalThis.window
|
|
191
|
+
} = {}) {
|
|
192
|
+
const normalized = normalizeFontPreferences(preferences);
|
|
193
|
+
storage?.setItem?.(FONT_STORAGE_KEY, JSON.stringify(normalized));
|
|
194
|
+
applyFontPreferences(normalized, { doc });
|
|
195
|
+
eventTarget?.dispatchEvent?.(new CustomEvent(FONT_CHANGE_EVENT, { detail: { preferences: normalized } }));
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function resetFontPreferences(options = {}) {
|
|
200
|
+
options.storage?.removeItem?.(FONT_STORAGE_KEY);
|
|
201
|
+
return setFontPreferences(DEFAULT_FONT_PREFERENCES, options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function setFontPreference(target, optionId, options = {}) {
|
|
205
|
+
if (!hasOwn(FONT_TARGETS, target)) throw new Error(`Unknown font target: ${target}`);
|
|
206
|
+
if (!hasOwn(FONT_OPTIONS, optionId)) throw new Error(`Unknown font option: ${optionId}`);
|
|
207
|
+
return setFontPreferences({ ...storedFontPreferences(options.storage), [target]: optionId }, options);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Swap the whole site to a primary font: sets every target plus the `site`
|
|
211
|
+
// driver. Per-target overrides can be applied afterward via setFontPreference.
|
|
212
|
+
export function setSiteFont(optionId, options = {}) {
|
|
213
|
+
if (!hasOwn(FONT_OPTIONS, optionId)) throw new Error(`Unknown font option: ${optionId}`);
|
|
214
|
+
const next = { ...storedFontPreferences(options.storage) };
|
|
215
|
+
for (const target of Object.keys(FONT_TARGETS)) next[target] = optionId;
|
|
216
|
+
next.site = optionId;
|
|
217
|
+
return setFontPreferences(next, options);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function setUserScale(value, options = {}) {
|
|
221
|
+
return setFontPreferences(
|
|
222
|
+
{ ...storedFontPreferences(options.storage), userScale: clampUserScale(value) },
|
|
223
|
+
options
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function applyStoredFontPreferences(options = {}) {
|
|
228
|
+
return applyFontPreferences(storedFontPreferences(options.storage), options);
|
|
229
|
+
}
|
package/public/js/format.js
CHANGED
|
@@ -431,13 +431,3 @@ export function renderLabels(labels = []) {
|
|
|
431
431
|
</div>
|
|
432
432
|
`;
|
|
433
433
|
}
|
|
434
|
-
|
|
435
|
-
export function repoLabelFromUrl(repoUrl) {
|
|
436
|
-
const raw = String(repoUrl || "").trim();
|
|
437
|
-
if (!raw) return "Not set";
|
|
438
|
-
const githubHttps = raw.match(/github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i);
|
|
439
|
-
if (githubHttps) return `${githubHttps[1]}/${githubHttps[2]}`;
|
|
440
|
-
const githubSsh = raw.match(/github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i);
|
|
441
|
-
if (githubSsh) return `${githubSsh[1]}/${githubSsh[2]}`;
|
|
442
|
-
return raw;
|
|
443
|
-
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const closeIconSvg = `
|
|
2
|
+
<svg class="ui-icon ui-icon-x" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
3
|
+
<path d="M6 6l12 12M18 6 6 18" />
|
|
4
|
+
</svg>
|
|
5
|
+
`;
|
|
6
|
+
|
|
7
|
+
/** Trash bin — archive / permanent delete actions (drawer header, card archive controls, toasts). */
|
|
8
|
+
export const trashIconSvg = `
|
|
9
|
+
<svg class="ui-icon ui-icon-trash" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
10
|
+
<path d="M3 6h18" />
|
|
11
|
+
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
12
|
+
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
|
13
|
+
<path d="M10 11v6" />
|
|
14
|
+
<path d="M14 11v6" />
|
|
15
|
+
</svg>
|
|
16
|
+
`;
|
package/public/js/kanban.js
CHANGED
|
@@ -513,10 +513,13 @@ export function enableKanbanDragScroll() {
|
|
|
513
513
|
|
|
514
514
|
kanban.addEventListener("pointerdown", (event) => {
|
|
515
515
|
if (event.button !== 0) return;
|
|
516
|
+
// Clear stale drag-click suppression on any fresh primary press — even over
|
|
517
|
+
// a card — so a both-button / two-finger pan that emitted no trailing click
|
|
518
|
+
// can't swallow the next genuine click.
|
|
519
|
+
dragged = false;
|
|
516
520
|
if (shouldIgnore(event.target)) return;
|
|
517
521
|
|
|
518
522
|
active = true;
|
|
519
|
-
dragged = false;
|
|
520
523
|
pointerId = event.pointerId;
|
|
521
524
|
startX = event.clientX;
|
|
522
525
|
startScroll = kanban.scrollLeft;
|
|
@@ -569,4 +572,123 @@ export function enableKanbanDragScroll() {
|
|
|
569
572
|
},
|
|
570
573
|
true
|
|
571
574
|
);
|
|
575
|
+
|
|
576
|
+
// --- Both-button mouse drag + two-finger touch drag -----------------------
|
|
577
|
+
// A second panning affordance that works ANYWHERE on the board, including
|
|
578
|
+
// directly over cards. A plain left-click / single-finger tap is left to the
|
|
579
|
+
// cards (so they still open + drag); holding *both* mouse buttons, or dragging
|
|
580
|
+
// with *two fingers*, pans the board horizontally without engaging a card.
|
|
581
|
+
let bothBtnPanning = false;
|
|
582
|
+
let bothBtnLastX = 0;
|
|
583
|
+
let suppressContextMenu = false;
|
|
584
|
+
|
|
585
|
+
// Pressing the second mouse button (buttons bitmask 1|2 === 3) arms the pan
|
|
586
|
+
// and cancels any single-button drag already in flight (see `active` above).
|
|
587
|
+
kanban.addEventListener("pointerdown", (event) => {
|
|
588
|
+
if (event.pointerType !== "mouse" || (event.buttons & 3) !== 3) return;
|
|
589
|
+
bothBtnPanning = true;
|
|
590
|
+
bothBtnLastX = event.clientX;
|
|
591
|
+
suppressContextMenu = true;
|
|
592
|
+
active = false; // hand control off from the left-button drag
|
|
593
|
+
kanban.classList.add("is-grabbing");
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
kanban.addEventListener("pointermove", (event) => {
|
|
597
|
+
if (event.pointerType !== "mouse") return;
|
|
598
|
+
const bothDown = (event.buttons & 3) === 3;
|
|
599
|
+
if (bothDown) {
|
|
600
|
+
event.preventDefault();
|
|
601
|
+
if (!bothBtnPanning) {
|
|
602
|
+
bothBtnPanning = true;
|
|
603
|
+
bothBtnLastX = event.clientX;
|
|
604
|
+
suppressContextMenu = true;
|
|
605
|
+
active = false;
|
|
606
|
+
kanban.classList.add("is-grabbing");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const delta = event.clientX - bothBtnLastX;
|
|
610
|
+
if (delta) {
|
|
611
|
+
kanban.scrollLeft -= delta;
|
|
612
|
+
bothBtnLastX = event.clientX;
|
|
613
|
+
dragged = true; // eat the trailing synthetic click
|
|
614
|
+
}
|
|
615
|
+
} else if (bothBtnPanning) {
|
|
616
|
+
bothBtnPanning = false;
|
|
617
|
+
kanban.classList.remove("is-grabbing");
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
kanban.addEventListener("pointerup", (event) => {
|
|
622
|
+
if (event.pointerType === "mouse" && bothBtnPanning && (event.buttons & 3) !== 3) {
|
|
623
|
+
bothBtnPanning = false;
|
|
624
|
+
kanban.classList.remove("is-grabbing");
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Right-click handling on the board:
|
|
629
|
+
// - after a both-button pan, swallow the trailing context menu;
|
|
630
|
+
// - otherwise suppress the native browser menu everywhere EXCEPT over a card,
|
|
631
|
+
// which owns its own context menu (see bindCardContextMenuDelegated).
|
|
632
|
+
kanban.addEventListener("contextmenu", (event) => {
|
|
633
|
+
if (suppressContextMenu) {
|
|
634
|
+
event.preventDefault();
|
|
635
|
+
event.stopPropagation();
|
|
636
|
+
suppressContextMenu = false;
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const onCard = event.target instanceof Element && event.target.closest(".card");
|
|
640
|
+
if (!onCard) {
|
|
641
|
+
event.preventDefault();
|
|
642
|
+
event.stopPropagation();
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Two-finger touch pan. Touch events (not pointer events) are used here so we
|
|
647
|
+
// can reliably preventDefault the browser's native two-finger scroll/zoom.
|
|
648
|
+
let touchPanning = false;
|
|
649
|
+
let touchPanLastX = 0;
|
|
650
|
+
const twoFingerCentroidX = (touches) => (touches[0].clientX + touches[1].clientX) / 2;
|
|
651
|
+
|
|
652
|
+
kanban.addEventListener(
|
|
653
|
+
"touchstart",
|
|
654
|
+
(event) => {
|
|
655
|
+
// Fresh gesture — clear any leftover drag-click suppression so a real tap
|
|
656
|
+
// is never swallowed when the prior two-finger pan emitted no click.
|
|
657
|
+
dragged = false;
|
|
658
|
+
if (event.touches.length === 2) {
|
|
659
|
+
touchPanning = true;
|
|
660
|
+
touchPanLastX = twoFingerCentroidX(event.touches);
|
|
661
|
+
active = false; // cancel any single-finger drag that started
|
|
662
|
+
kanban.classList.add("is-grabbing");
|
|
663
|
+
} else {
|
|
664
|
+
touchPanning = false;
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
{ passive: true }
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
kanban.addEventListener(
|
|
671
|
+
"touchmove",
|
|
672
|
+
(event) => {
|
|
673
|
+
if (!touchPanning || event.touches.length !== 2) return;
|
|
674
|
+
const x = twoFingerCentroidX(event.touches);
|
|
675
|
+
const delta = x - touchPanLastX;
|
|
676
|
+
if (delta) {
|
|
677
|
+
event.preventDefault();
|
|
678
|
+
kanban.scrollLeft -= delta;
|
|
679
|
+
touchPanLastX = x;
|
|
680
|
+
dragged = true; // eat the trailing tap so a card doesn't open
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
{ passive: false }
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
const endTouchPan = (event) => {
|
|
687
|
+
if (event.touches.length < 2 && touchPanning) {
|
|
688
|
+
touchPanning = false;
|
|
689
|
+
kanban.classList.remove("is-grabbing");
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
kanban.addEventListener("touchend", endTouchPan, { passive: true });
|
|
693
|
+
kanban.addEventListener("touchcancel", endTouchPan, { passive: true });
|
|
572
694
|
}
|