trekoon 0.2.8 → 0.3.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/README.md +14 -14
- package/docs/commands.md +9 -11
- package/docs/quickstart.md +10 -12
- package/package.json +23 -1
- package/src/board/assets/app.js +469 -1377
- package/src/board/assets/components/ClampedText.js +1 -1
- package/src/board/assets/components/Component.js +271 -0
- package/src/board/assets/components/ConfirmDialog.js +81 -0
- package/src/board/assets/components/EpicRow.js +43 -26
- package/src/board/assets/components/EpicsOverview.js +52 -11
- package/src/board/assets/components/Inspector.js +335 -0
- package/src/board/assets/components/Notice.js +87 -0
- package/src/board/assets/components/SubtaskModal.js +100 -0
- package/src/board/assets/components/TaskCard.js +82 -0
- package/src/board/assets/components/TaskModal.js +99 -0
- package/src/board/assets/components/TopBar.js +167 -0
- package/src/board/assets/components/Workspace.js +319 -0
- package/src/board/assets/components/assetMap.js +29 -14
- package/src/board/assets/components/helpers.js +261 -0
- package/src/board/assets/fonts/inter-latin.woff2 +0 -0
- package/src/board/assets/fonts/material-symbols-rounded.woff2 +0 -0
- package/src/board/assets/index.html +20 -57
- package/src/board/assets/main.js +2 -18
- package/src/board/assets/runtime/clipboard.js +34 -0
- package/src/board/assets/runtime/delegation.js +342 -0
- package/src/board/assets/state/actions.js +204 -16
- package/src/board/assets/state/api.js +201 -46
- package/src/board/assets/state/store.js +418 -117
- package/src/board/assets/state/url.js +184 -0
- package/src/board/assets/state/utils.js +222 -0
- package/src/board/assets/styles/board.css +933 -129
- package/src/board/assets/styles/fonts.css +22 -0
- package/src/board/routes.ts +15 -6
- package/src/board/server.ts +1 -0
- package/src/board/assets/components/AppShell.js +0 -17
- package/src/board/assets/components/BoardTopbar.js +0 -78
- package/src/board/assets/components/WorkspaceHeader.js +0 -70
- package/src/board/assets/utils/dom.js +0 -308
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { escapeHtml, formatDate, normalizeStatus, STATUS_ORDER } from "../state/utils.js";
|
|
2
|
+
|
|
3
|
+
export { escapeHtml, formatDate, normalizeStatus, STATUS_ORDER };
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Status labels & styles
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export const STATUS_LABELS = {
|
|
10
|
+
todo: "Todo",
|
|
11
|
+
blocked: "Blocked",
|
|
12
|
+
in_progress: "In progress",
|
|
13
|
+
done: "Done",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const STATUS_BADGE_STYLES = {
|
|
17
|
+
todo: "border-white/10 bg-white/[0.05] text-[var(--board-text-muted)]",
|
|
18
|
+
blocked: "border-amber-500/20 bg-amber-500/10 text-amber-300",
|
|
19
|
+
in_progress: "border-sky-400/20 bg-sky-400/10 text-sky-300",
|
|
20
|
+
done: "border-emerald-500/20 bg-emerald-500/10 text-emerald-300",
|
|
21
|
+
default: "border-[var(--board-border)] bg-white/[0.04] text-[var(--board-text-muted)]",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Class-name helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export function cx(...classNames) {
|
|
29
|
+
return classNames.filter(Boolean).join(" ");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function panelClasses(extra = "") {
|
|
33
|
+
return cx(
|
|
34
|
+
"rounded-[28px] border border-[var(--board-border)] bg-[var(--board-surface)] shadow-panel",
|
|
35
|
+
extra,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function secondaryPanelClasses(extra = "") {
|
|
40
|
+
return cx(
|
|
41
|
+
"rounded-[24px] border border-[var(--board-border)] bg-[var(--board-surface-2)]",
|
|
42
|
+
extra,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sectionLabelClasses() {
|
|
47
|
+
return "text-[11px] font-semibold uppercase tracking-[0.18em] text-[var(--board-text-soft)]";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function neutralChipClasses() {
|
|
51
|
+
return "inline-flex items-center gap-1 rounded-full border border-[var(--board-border)] bg-white/[0.04] px-2.5 py-1 text-[11px] font-medium text-[var(--board-text-muted)]";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buttonClasses(options = {}) {
|
|
55
|
+
const kind = options.kind ?? "secondary";
|
|
56
|
+
const iconOnly = options.iconOnly ?? false;
|
|
57
|
+
|
|
58
|
+
return cx(
|
|
59
|
+
"inline-flex items-center justify-center gap-2 rounded-2xl border text-sm font-medium transition duration-200",
|
|
60
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--board-border-strong)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--board-bg)]",
|
|
61
|
+
iconOnly ? "h-10 w-10 px-0" : "min-h-10 px-4 py-2.5",
|
|
62
|
+
kind === "primary"
|
|
63
|
+
? "border-[var(--board-accent)] bg-[var(--board-accent)] text-white hover:bg-[var(--board-accent-strong)] hover:border-[var(--board-accent-strong)]"
|
|
64
|
+
: "border-[var(--board-border)] bg-white/[0.04] text-[var(--board-text)] hover:bg-white/[0.08] hover:border-[var(--board-border-strong)]",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function fieldClasses() {
|
|
69
|
+
return cx(
|
|
70
|
+
"w-full rounded-2xl border border-[var(--board-border)] bg-[var(--board-surface-2)] px-3.5 py-3 text-sm text-[var(--board-text)] shadow-sm transition",
|
|
71
|
+
"placeholder:text-[var(--board-text-soft)] focus:border-[var(--board-border-strong)] focus:outline-none focus:ring-2 focus:ring-[var(--board-accent-soft)]",
|
|
72
|
+
"disabled:cursor-not-allowed disabled:opacity-60",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function statusBadgeClasses(status) {
|
|
77
|
+
return cx(
|
|
78
|
+
"inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em]",
|
|
79
|
+
STATUS_BADGE_STYLES[normalizeStatus(status)] ?? STATUS_BADGE_STYLES.default,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Render helpers
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
export function renderIcon(name, className = "") {
|
|
88
|
+
return `<span class="${cx("material-symbols-rounded shrink-0", className)}" aria-hidden="true">${name}</span>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function renderCopyIcon(className = "") {
|
|
92
|
+
return `
|
|
93
|
+
<svg class="${cx("board-inline-icon", className)}" aria-hidden="true" viewBox="0 0 16 16" fill="none">
|
|
94
|
+
<rect x="5" y="2.75" width="7" height="9" rx="1.5" stroke="currentColor" stroke-width="1.35"></rect>
|
|
95
|
+
<path d="M4 5.75H3.5C2.67157 5.75 2 6.42157 2 7.25V12C2 12.8284 2.67157 13.5 3.5 13.5H8.25C9.07843 13.5 9.75 12.8284 9.75 12V11.5" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
96
|
+
</svg>
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function renderCheckIcon(className = "") {
|
|
101
|
+
return `
|
|
102
|
+
<svg class="${cx("board-inline-icon", className)}" aria-hidden="true" viewBox="0 0 16 16" fill="none">
|
|
103
|
+
<path d="M3.5 8.4L6.4 11.1L12.5 4.9" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
104
|
+
</svg>
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function readStatusLabel(rawStatus) {
|
|
109
|
+
if (typeof rawStatus !== "string" || rawStatus.trim().length === 0) {
|
|
110
|
+
return "Unknown";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const normalized = normalizeStatus(rawStatus);
|
|
114
|
+
if (STATUS_LABELS[normalized]) {
|
|
115
|
+
return STATUS_LABELS[normalized];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return rawStatus.replaceAll("_", " ").replaceAll("-", " ");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function renderStatusBadge(rawStatus, label = readStatusLabel(rawStatus)) {
|
|
122
|
+
return `<span class="${statusBadgeClasses(rawStatus)}">${escapeHtml(label)}</span>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function renderStatusSelect(name, selectedStatus, disabled = false) {
|
|
126
|
+
return `
|
|
127
|
+
<select class="${fieldClasses()}" name="${escapeHtml(name)}" ${disabled ? "disabled" : ""}>
|
|
128
|
+
${STATUS_ORDER.map((status) => `
|
|
129
|
+
<option value="${escapeHtml(status)}" ${selectedStatus === status ? "selected" : ""}>${escapeHtml(STATUS_LABELS[status] ?? status)}</option>
|
|
130
|
+
`).join("")}
|
|
131
|
+
</select>
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function renderEmptyState(title, description, shortcut) {
|
|
136
|
+
return `
|
|
137
|
+
<div class="rounded-[24px] border border-dashed border-[var(--board-border-strong)] bg-[var(--board-accent-soft)]/40 px-5 py-6 text-center">
|
|
138
|
+
<strong class="block text-base font-semibold text-[var(--board-text)]">${escapeHtml(title)}</strong>
|
|
139
|
+
<p class="mt-2 text-sm leading-6 text-[var(--board-text-muted)]">${escapeHtml(description)}</p>
|
|
140
|
+
${shortcut
|
|
141
|
+
? `<p class="mt-3 text-xs text-[var(--board-text-soft)]">Try <span class="inline-flex items-center rounded-lg border border-[var(--board-border)] bg-white/[0.04] px-2 py-1 font-medium text-[var(--board-text-muted)]">${escapeHtml(shortcut)}</span></p>`
|
|
142
|
+
: ""}
|
|
143
|
+
</div>
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Description rendering helpers
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export function renderDescriptionPreview(description, className = "mt-1 text-sm leading-6 text-[var(--board-text-muted)]") {
|
|
152
|
+
if (!description || description.trim().length === 0) return "";
|
|
153
|
+
return `<p class="${escapeHtml(className)}">${escapeHtml(description)}</p>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function renderDescriptionBody(description, className = "text-sm leading-7 text-[var(--board-text-muted)]") {
|
|
157
|
+
if (!description || description.trim().length === 0) {
|
|
158
|
+
return `<p class="${escapeHtml(className)}">No description provided.</p>`;
|
|
159
|
+
}
|
|
160
|
+
return `<div class="${escapeHtml(className)}" style="white-space:pre-wrap;word-break:break-word">${escapeHtml(description)}</div>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function shouldCollapseDescription(description) {
|
|
164
|
+
if (!description) return false;
|
|
165
|
+
const trimmed = description.trim();
|
|
166
|
+
return trimmed.length > 260 || trimmed.split("\n").length > 5;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function renderDescriptionSection(title, description, options = {}) {
|
|
170
|
+
const {
|
|
171
|
+
open = false,
|
|
172
|
+
compact = false,
|
|
173
|
+
emptyText = "Add context so collaborators know what done looks like.",
|
|
174
|
+
} = options;
|
|
175
|
+
|
|
176
|
+
if (!description || description.trim().length === 0) {
|
|
177
|
+
return `
|
|
178
|
+
<section class="${secondaryPanelClasses("board-detail-card p-4")}">
|
|
179
|
+
<div class="board-section__header flex items-center justify-between gap-3">
|
|
180
|
+
<strong class="text-sm font-semibold text-[var(--board-text)]">${escapeHtml(title)}</strong>
|
|
181
|
+
<span class="${neutralChipClasses()}">Empty</span>
|
|
182
|
+
</div>
|
|
183
|
+
<p class="mt-3 text-sm leading-6 text-[var(--board-text-muted)]">${escapeHtml(emptyText)}</p>
|
|
184
|
+
</section>
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!shouldCollapseDescription(description)) {
|
|
189
|
+
return `
|
|
190
|
+
<section class="${secondaryPanelClasses("board-detail-card p-4")}">
|
|
191
|
+
<div class="board-section__header flex items-center justify-between gap-3">
|
|
192
|
+
<strong class="text-sm font-semibold text-[var(--board-text)]">${escapeHtml(title)}</strong>
|
|
193
|
+
<span class="${neutralChipClasses()}">${escapeHtml(`${description.trim().length} chars`)}</span>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="mt-3 ${compact ? "board-detail-copy board-detail-copy--compact" : "board-detail-copy"}">
|
|
196
|
+
${renderDescriptionBody(description)}
|
|
197
|
+
</div>
|
|
198
|
+
</section>
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return `
|
|
203
|
+
<details class="board-disclosure ${secondaryPanelClasses("board-detail-card p-4")}" ${open ? "open" : ""}>
|
|
204
|
+
<summary class="board-detail-summary-row cursor-pointer list-none text-sm font-semibold text-[var(--board-text)]">
|
|
205
|
+
<span>${escapeHtml(title)}</span>
|
|
206
|
+
<span class="${neutralChipClasses()}">Long</span>
|
|
207
|
+
</summary>
|
|
208
|
+
<div class="mt-3 board-detail-copy ${compact ? "board-detail-copy--compact" : ""}">
|
|
209
|
+
${renderDescriptionBody(description)}
|
|
210
|
+
</div>
|
|
211
|
+
</details>
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Misc shared helpers
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
export function readNodeLabel(kind, title) {
|
|
220
|
+
if (kind === "task") return `Task: ${title}`;
|
|
221
|
+
if (kind === "subtask") return `Subtask: ${title}`;
|
|
222
|
+
return title;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function renderEpicCountSummary(epic) {
|
|
226
|
+
const totalTasks = Array.isArray(epic.taskIds) ? epic.taskIds.length : 0;
|
|
227
|
+
const counts = epic.counts || { todo: 0, blocked: 0, in_progress: 0, done: 0 };
|
|
228
|
+
return `
|
|
229
|
+
<span class="${neutralChipClasses()}">${totalTasks} task${totalTasks === 1 ? "" : "s"}</span>
|
|
230
|
+
<span class="${neutralChipClasses()}">${counts.in_progress ?? 0} doing</span>
|
|
231
|
+
<span class="${neutralChipClasses()}">${counts.done ?? 0} done</span>
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function renderTaskMeta(task, includeStatus = false) {
|
|
236
|
+
return `
|
|
237
|
+
${includeStatus ? renderStatusBadge(task.status) : ""}
|
|
238
|
+
<span class="${neutralChipClasses()}">${task.subtasks.length} subtask${task.subtasks.length === 1 ? "" : "s"}</span>
|
|
239
|
+
${task.blockedBy.length > 0 ? `<span class="${neutralChipClasses()}">${task.blockedBy.length} blocker${task.blockedBy.length === 1 ? "" : "s"}</span>` : ""}
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function hasLongTaskTitle(title) {
|
|
244
|
+
if (!title) return false;
|
|
245
|
+
const trimmed = title.trim();
|
|
246
|
+
return trimmed.length > 72 || trimmed.split("\n").length > 2;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function isCompactViewport() {
|
|
250
|
+
return typeof window !== "undefined" && window.matchMedia?.("(max-width: 900px)")?.matches;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function shouldUseTaskModal(boardState, store) {
|
|
254
|
+
return Boolean(boardState?.selectedTask);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function lookupNode(snapshot, id) {
|
|
258
|
+
return snapshot.tasks.find((task) => task.id === id)
|
|
259
|
+
?? snapshot.subtasks.find((subtask) => subtask.id === id)
|
|
260
|
+
?? null;
|
|
261
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -3,72 +3,35 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="color-scheme" content="dark light" />
|
|
7
|
+
<meta name="theme-color" content="#0b0d12" data-board-theme-color="active" />
|
|
8
|
+
<meta name="theme-color" content="#0b0d12" media="(prefers-color-scheme: dark)" />
|
|
9
|
+
<meta name="theme-color" content="#f4f6fb" media="(prefers-color-scheme: light)" />
|
|
6
10
|
<meta
|
|
7
11
|
name="description"
|
|
8
12
|
content="Trekoon board — local-first workspace for browsing epics, tasks, and flow states."
|
|
9
13
|
/>
|
|
10
14
|
<title>Trekoon Board</title>
|
|
11
|
-
<link rel="
|
|
12
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
13
|
-
<link
|
|
14
|
-
rel="stylesheet"
|
|
15
|
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Material+Symbols+Rounded:FILL@0&display=swap"
|
|
16
|
-
/>
|
|
17
|
-
<script>
|
|
18
|
-
tailwind = window.tailwind || {};
|
|
19
|
-
tailwind.config = {
|
|
20
|
-
theme: {
|
|
21
|
-
extend: {
|
|
22
|
-
fontFamily: {
|
|
23
|
-
sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
|
|
24
|
-
},
|
|
25
|
-
boxShadow: {
|
|
26
|
-
panel: "var(--board-shadow)",
|
|
27
|
-
lift: "0 18px 52px rgba(0, 0, 0, 0.34)",
|
|
28
|
-
focus: "0 0 0 1px var(--board-border-strong), 0 16px 40px rgba(88, 28, 135, 0.2)",
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
</script>
|
|
34
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
15
|
+
<link rel="stylesheet" href="./styles/fonts.css" />
|
|
35
16
|
<link rel="stylesheet" href="./styles/board.css" />
|
|
36
|
-
<style>
|
|
37
|
-
.material-symbols-rounded {
|
|
38
|
-
font-family: "Material Symbols Rounded";
|
|
39
|
-
font-weight: 400;
|
|
40
|
-
font-style: normal;
|
|
41
|
-
font-size: 20px;
|
|
42
|
-
line-height: 1;
|
|
43
|
-
letter-spacing: normal;
|
|
44
|
-
text-transform: none;
|
|
45
|
-
display: inline-block;
|
|
46
|
-
white-space: nowrap;
|
|
47
|
-
direction: ltr;
|
|
48
|
-
font-variation-settings:
|
|
49
|
-
"FILL" 0,
|
|
50
|
-
"wght" 400,
|
|
51
|
-
"GRAD" 0,
|
|
52
|
-
"opsz" 20;
|
|
53
|
-
}
|
|
54
|
-
</style>
|
|
55
17
|
</head>
|
|
56
18
|
<body>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
19
|
+
<a href="#board-runtime-root" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:rounded-lg focus:bg-[var(--board-accent)] focus:px-4 focus:py-2 focus:text-white">
|
|
20
|
+
Skip to content
|
|
21
|
+
</a>
|
|
22
|
+
|
|
23
|
+
<main id="app">
|
|
24
|
+
<div class="board-shell-v2">
|
|
25
|
+
<section class="board-shell-v2__frame">
|
|
26
|
+
<div class="board-shell-v2__runtime-shell">
|
|
27
|
+
<div
|
|
28
|
+
id="board-runtime-root"
|
|
29
|
+
class="board-shell-v2__runtime"
|
|
30
|
+
data-board-runtime-root
|
|
31
|
+
></div>
|
|
65
32
|
</div>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
Loading epics, tasks, and saved board context from your repo-shared storage.
|
|
69
|
-
</p>
|
|
70
|
-
</div>
|
|
71
|
-
</section>
|
|
33
|
+
</section>
|
|
34
|
+
</div>
|
|
72
35
|
</main>
|
|
73
36
|
|
|
74
37
|
<script type="module" src="./main.js"></script>
|
package/src/board/assets/main.js
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
import { createApp, nextTick } from "https://unpkg.com/vue@3.5.13/dist/vue.esm-browser.js";
|
|
2
|
-
|
|
3
|
-
import { createBoardShellComponent } from "./components/AppShell.js";
|
|
4
|
-
|
|
5
1
|
window.__TREKOON_BOARD_BOOTSTRAP__ = "main";
|
|
6
2
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
if (!(appRoot instanceof HTMLElement)) {
|
|
10
|
-
throw new Error("Board shell could not find the app root.");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const shellApp = createApp(createBoardShellComponent());
|
|
14
|
-
shellApp.mount(appRoot);
|
|
15
|
-
await nextTick();
|
|
16
|
-
|
|
17
|
-
const runtimeRoot = appRoot.querySelector("[data-board-runtime-root]");
|
|
3
|
+
const runtimeRoot = document.querySelector("[data-board-runtime-root]");
|
|
18
4
|
|
|
19
5
|
if (!(runtimeRoot instanceof HTMLElement)) {
|
|
20
6
|
throw new Error("Board shell could not find the runtime mount root.");
|
|
@@ -22,6 +8,4 @@ if (!(runtimeRoot instanceof HTMLElement)) {
|
|
|
22
8
|
|
|
23
9
|
const { bootLegacyBoard } = await import("./app.js");
|
|
24
10
|
|
|
25
|
-
await bootLegacyBoard({
|
|
26
|
-
mountElement: runtimeRoot,
|
|
27
|
-
});
|
|
11
|
+
await bootLegacyBoard({ mountElement: runtimeRoot });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export async function copyTextToClipboard(value) {
|
|
2
|
+
const text = typeof value === "string" ? value : String(value ?? "");
|
|
3
|
+
|
|
4
|
+
if (!text) {
|
|
5
|
+
throw new Error("Clipboard text is empty.");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (navigator?.clipboard?.writeText) {
|
|
9
|
+
await navigator.clipboard.writeText(text);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const textarea = document.createElement("textarea");
|
|
14
|
+
textarea.value = text;
|
|
15
|
+
textarea.setAttribute("readonly", "true");
|
|
16
|
+
textarea.setAttribute("aria-hidden", "true");
|
|
17
|
+
textarea.style.position = "fixed";
|
|
18
|
+
textarea.style.top = "0";
|
|
19
|
+
textarea.style.left = "0";
|
|
20
|
+
textarea.style.opacity = "0";
|
|
21
|
+
textarea.style.pointerEvents = "none";
|
|
22
|
+
|
|
23
|
+
document.body.append(textarea);
|
|
24
|
+
textarea.focus({ preventScroll: true });
|
|
25
|
+
textarea.select();
|
|
26
|
+
textarea.setSelectionRange(0, text.length);
|
|
27
|
+
|
|
28
|
+
const didCopy = document.execCommand("copy");
|
|
29
|
+
textarea.remove();
|
|
30
|
+
|
|
31
|
+
if (!didCopy) {
|
|
32
|
+
throw new Error("Clipboard API unavailable.");
|
|
33
|
+
}
|
|
34
|
+
}
|