@unbrained/pm-web 2026.8.10 → 2026.8.17
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 +22 -0
- package/dist/app.d.ts +16 -0
- package/dist/app.js +16 -0
- package/dist/app.js.map +1 -1
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js.map +1 -1
- package/dist/board.d.ts +53 -0
- package/dist/board.js +48 -0
- package/dist/board.js.map +1 -1
- package/dist/crypto.d.ts +23 -0
- package/dist/crypto.js +32 -0
- package/dist/crypto.js.map +1 -1
- package/dist/db.d.ts +19 -0
- package/dist/db.js +19 -0
- package/dist/db.js.map +1 -1
- package/dist/ical.d.ts +72 -0
- package/dist/ical.js +73 -0
- package/dist/ical.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +52 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +8 -0
- package/dist/middleware/auth.js.map +1 -1
- package/dist/oidc.d.ts +87 -0
- package/dist/oidc.js +113 -0
- package/dist/oidc.js.map +1 -1
- package/dist/routes/admin.js +15 -0
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/extensions.js +10 -0
- package/dist/routes/extensions.js.map +1 -1
- package/dist/routes/github.js +13 -0
- package/dist/routes/github.js.map +1 -1
- package/dist/routes/oidc.d.ts +10 -0
- package/dist/routes/oidc.js +51 -0
- package/dist/routes/oidc.js.map +1 -1
- package/dist/routes/pm.js +132 -1
- package/dist/routes/pm.js.map +1 -1
- package/dist/routes/sharing.js +8 -0
- package/dist/routes/sharing.js.map +1 -1
- package/dist/services/mutation-event-watcher.js +13 -0
- package/dist/services/mutation-event-watcher.js.map +1 -1
- package/dist/services/package-catalog.d.ts +24 -0
- package/dist/services/package-catalog.js +22 -2
- package/dist/services/package-catalog.js.map +1 -1
- package/dist/services/pm-runner.d.ts +82 -0
- package/dist/services/pm-runner.js +121 -0
- package/dist/services/pm-runner.js.map +1 -1
- package/dist/services/project-watcher.d.ts +22 -0
- package/dist/services/project-watcher.js +51 -0
- package/dist/services/project-watcher.js.map +1 -1
- package/dist/services/realtime-bus.d.ts +46 -0
- package/dist/services/realtime-bus.js +59 -0
- package/dist/services/realtime-bus.js.map +1 -1
- package/dist/services/sse.d.ts +134 -0
- package/dist/services/sse.js +145 -0
- package/dist/services/sse.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +5 -3
- package/public/src/app.js +44 -1
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +44 -1
- package/public/src/components/modals.js +11 -0
- package/public/src/components/modals.js.map +1 -1
- package/public/src/components/modals.ts +11 -0
- package/public/src/components/toast.js +2 -0
- package/public/src/components/toast.js.map +1 -1
- package/public/src/components/toast.ts +2 -0
- package/public/src/constants.js +21 -4
- package/public/src/constants.js.map +1 -1
- package/public/src/constants.ts +21 -4
- package/public/src/filters.js +10 -7
- package/public/src/filters.js.map +1 -1
- package/public/src/filters.ts +10 -7
- package/public/src/i18n/de.json +3 -1
- package/public/src/i18n/en.json +3 -1
- package/public/src/i18n/es.json +3 -1
- package/public/src/i18n/zh.json +3 -1
- package/public/src/i18n.js +11 -1
- package/public/src/i18n.js.map +1 -1
- package/public/src/i18n.ts +13 -1
- package/public/src/state.js +3 -0
- package/public/src/state.js.map +1 -1
- package/public/src/state.ts +3 -0
- package/public/src/sw.ts +71 -7
- package/public/src/theme.js +13 -1
- package/public/src/theme.js.map +1 -1
- package/public/src/theme.ts +15 -1
- package/public/src/types.ts +2 -0
- package/public/src/utils.js +26 -1
- package/public/src/utils.js.map +1 -1
- package/public/src/utils.ts +26 -1
- package/public/src/views/activity.js +1 -0
- package/public/src/views/activity.js.map +1 -1
- package/public/src/views/activity.ts +1 -0
- package/public/src/views/admin.js +134 -0
- package/public/src/views/admin.js.map +1 -1
- package/public/src/views/admin.ts +134 -0
- package/public/src/views/auth.js +31 -0
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +31 -0
- package/public/src/views/calendar.js +3 -0
- package/public/src/views/calendar.js.map +1 -1
- package/public/src/views/calendar.ts +3 -0
- package/public/src/views/comments-audit.js +1 -0
- package/public/src/views/comments-audit.js.map +1 -1
- package/public/src/views/comments-audit.ts +1 -0
- package/public/src/views/config.js +67 -0
- package/public/src/views/config.js.map +1 -1
- package/public/src/views/config.ts +67 -0
- package/public/src/views/context.js +2 -0
- package/public/src/views/context.js.map +1 -1
- package/public/src/views/context.ts +2 -0
- package/public/src/views/create.js +3 -0
- package/public/src/views/create.js.map +1 -1
- package/public/src/views/create.ts +3 -0
- package/public/src/views/dedupe.js +1 -0
- package/public/src/views/dedupe.js.map +1 -1
- package/public/src/views/dedupe.ts +1 -0
- package/public/src/views/export.js +6 -0
- package/public/src/views/export.js.map +1 -1
- package/public/src/views/export.ts +6 -0
- package/public/src/views/github.js +58 -0
- package/public/src/views/github.js.map +1 -1
- package/public/src/views/github.ts +58 -0
- package/public/src/views/graph-canvas.js +18 -0
- package/public/src/views/graph-canvas.js.map +1 -1
- package/public/src/views/graph-canvas.ts +20 -0
- package/public/src/views/graph.js +41 -0
- package/public/src/views/graph.js.map +1 -1
- package/public/src/views/graph.ts +42 -1
- package/public/src/views/groups.js +45 -0
- package/public/src/views/groups.js.map +1 -1
- package/public/src/views/groups.ts +45 -0
- package/public/src/views/guide.js +3 -0
- package/public/src/views/guide.js.map +1 -1
- package/public/src/views/guide.ts +3 -0
- package/public/src/views/health.js +3 -0
- package/public/src/views/health.js.map +1 -1
- package/public/src/views/health.ts +3 -0
- package/public/src/views/items.js +196 -6
- package/public/src/views/items.js.map +1 -1
- package/public/src/views/items.ts +196 -6
- package/public/src/views/normalize.js +2 -0
- package/public/src/views/normalize.js.map +1 -1
- package/public/src/views/normalize.ts +2 -0
- package/public/src/views/packages.js +67 -5
- package/public/src/views/packages.js.map +1 -1
- package/public/src/views/packages.ts +74 -5
- package/public/src/views/plan-execution.js +6 -0
- package/public/src/views/plan-execution.js.map +1 -1
- package/public/src/views/plan-execution.ts +8 -0
- package/public/src/views/plan.js +145 -0
- package/public/src/views/plan.js.map +1 -1
- package/public/src/views/plan.ts +146 -1
- package/public/src/views/projects.js +60 -0
- package/public/src/views/projects.js.map +1 -1
- package/public/src/views/projects.ts +60 -0
- package/public/src/views/router.js +43 -0
- package/public/src/views/router.js.map +1 -1
- package/public/src/views/router.ts +43 -0
- package/public/src/views/search.js +5 -0
- package/public/src/views/search.js.map +1 -1
- package/public/src/views/search.ts +5 -0
- package/public/src/views/settings.js +36 -0
- package/public/src/views/settings.js.map +1 -1
- package/public/src/views/settings.ts +36 -0
- package/public/src/views/shared.js +1 -0
- package/public/src/views/shared.js.map +1 -1
- package/public/src/views/shared.ts +1 -0
- package/public/src/views/sharing.js +32 -0
- package/public/src/views/sharing.js.map +1 -1
- package/public/src/views/sharing.ts +32 -0
- package/public/src/views/stats.js +1 -0
- package/public/src/views/stats.js.map +1 -1
- package/public/src/views/stats.ts +1 -0
- package/public/src/views/templates.js +4 -0
- package/public/src/views/templates.js.map +1 -1
- package/public/src/views/templates.ts +4 -0
- package/public/src/views/validate.js +1 -0
- package/public/src/views/validate.js.map +1 -1
- package/public/src/views/validate.ts +1 -0
- package/public/sw.js +60 -5
package/public/src/i18n.ts
CHANGED
|
@@ -24,6 +24,8 @@ export const LOCALE_STORAGE_KEY = 'pmLocale';
|
|
|
24
24
|
|
|
25
25
|
/** Locales shipped by this package. The first entry is the default/fallback. */
|
|
26
26
|
export const SUPPORTED_LOCALES = ['en', 'de', 'es', 'zh'] as const;
|
|
27
|
+
/** String literal union of every locale code this package ships a catalog
|
|
28
|
+
* for, derived from `SUPPORTED_LOCALES`. */
|
|
27
29
|
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
|
28
30
|
|
|
29
31
|
const DEFAULT_LOCALE: SupportedLocale = 'en';
|
|
@@ -166,7 +168,8 @@ export async function initI18n(): Promise<void> {
|
|
|
166
168
|
applyTranslations();
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
/**
|
|
171
|
+
/** Return the locale currently selected for the session, set by `initI18n` or
|
|
172
|
+
* `setLocale`. Defaults to the English fallback before initialization. */
|
|
170
173
|
export function getLocale(): SupportedLocale {
|
|
171
174
|
return currentLocale;
|
|
172
175
|
}
|
|
@@ -232,6 +235,10 @@ export function applyTranslations(root?: ParentNode): void {
|
|
|
232
235
|
bindAttr(scope, '[data-i18n-aria]', 'data-i18n-aria', (el, v) => { el.setAttribute('aria-label', v); });
|
|
233
236
|
}
|
|
234
237
|
|
|
238
|
+
/** For every element in `scope` matching `selector`, read its `attr` data
|
|
239
|
+
* key, resolve the translation (with optional `data-i18n-params`
|
|
240
|
+
* substitutions), and pass the result to `setter`. Used to bind each
|
|
241
|
+
* `data-i18n*` flavor. */
|
|
235
242
|
function bindAttr(
|
|
236
243
|
scope: ParentNode,
|
|
237
244
|
selector: string,
|
|
@@ -247,6 +254,8 @@ function bindAttr(
|
|
|
247
254
|
});
|
|
248
255
|
}
|
|
249
256
|
|
|
257
|
+
/** Parse a `data-i18n-params` JSON string into a parameter object, returning
|
|
258
|
+
* `undefined` when the JSON is missing, malformed, or not a plain object. */
|
|
250
259
|
function safeParseParams(json: string): Record<string, string | number> | undefined {
|
|
251
260
|
try {
|
|
252
261
|
const parsed = JSON.parse(json) as unknown;
|
|
@@ -274,6 +283,9 @@ export function translateError(message: string): string {
|
|
|
274
283
|
// Lazy reverse map: English error text → catalog key. Built from enCatalog
|
|
275
284
|
// so it tracks the catalog without a hand-maintained second table.
|
|
276
285
|
let cachedErrorReverse: Map<string, string> | null = null;
|
|
286
|
+
/** Build and cache a reverse lookup from English error text to its catalog
|
|
287
|
+
* key (covering only `error.*` entries), so server error strings can be
|
|
288
|
+
* mapped back to translatable keys at the display layer. */
|
|
277
289
|
function errorReverseMap(): Map<string, string> {
|
|
278
290
|
if (cachedErrorReverse) return cachedErrorReverse;
|
|
279
291
|
const map = new Map<string, string>();
|
package/public/src/state.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** Mutable singleton holding the SPA's global application state: the signed-in
|
|
2
|
+
* user, loaded projects, the active project and view, item list and filters,
|
|
3
|
+
* and search state. Views read and mutate this directly. */
|
|
1
4
|
export const state = {
|
|
2
5
|
user: null,
|
|
3
6
|
projects: [],
|
package/public/src/state.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.js","sourceRoot":"","sources":["state.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,KAAK,GAAa;IAC7B,IAAI,EAAE,IAAI;IACV,QAAQ,EAAE,EAAE;IACZ,cAAc,EAAE,IAAI;IACpB,WAAW,EAAE,UAAU;IACvB,OAAO,EAAE,OAAO;IAChB,KAAK,EAAE,EAAE;IACT,WAAW,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IACnG,WAAW,EAAE,EAAE;IACf,aAAa,EAAE,EAAE;IACjB,UAAU,EAAE,QAAQ;IACpB,SAAS,EAAE,CAAC;IACZ,MAAM,EAAE,IAAI;CACb,CAAC"}
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["state.ts"],"names":[],"mappings":"AAKA;;4DAE4D;AAC5D,MAAM,CAAC,MAAM,KAAK,GAAa;IAC7B,IAAI,EAAE,IAAI;IACV,QAAQ,EAAE,EAAE;IACZ,cAAc,EAAE,IAAI;IACpB,WAAW,EAAE,UAAU;IACvB,OAAO,EAAE,OAAO;IAChB,KAAK,EAAE,EAAE;IACT,WAAW,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IACnG,WAAW,EAAE,EAAE;IACf,aAAa,EAAE,EAAE;IACjB,UAAU,EAAE,QAAQ;IACpB,SAAS,EAAE,CAAC;IACZ,MAAM,EAAE,IAAI;CACb,CAAC"}
|
package/public/src/state.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════
|
|
4
4
|
import type { AppState } from './types.js';
|
|
5
5
|
|
|
6
|
+
/** Mutable singleton holding the SPA's global application state: the signed-in
|
|
7
|
+
* user, loaded projects, the active project and view, item list and filters,
|
|
8
|
+
* and search state. Views read and mutate this directly. */
|
|
6
9
|
export const state: AppState = {
|
|
7
10
|
user: null,
|
|
8
11
|
projects: [],
|
package/public/src/sw.ts
CHANGED
|
@@ -127,6 +127,9 @@ interface SyncEvent extends ExtendableEvent {
|
|
|
127
127
|
readonly tag: string;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** Open (creating on first run) the IndexedDB database backing the offline
|
|
131
|
+
* mutation queue, ensuring the `mutations` object store and its timestamp
|
|
132
|
+
* index exist. Resolves with the ready database handle. */
|
|
130
133
|
function openMutationDB(): Promise<IDBDatabase> {
|
|
131
134
|
return new Promise((resolve, reject) => {
|
|
132
135
|
const request = indexedDB.open(MUTATION_DB, 1);
|
|
@@ -185,18 +188,32 @@ async function queueMutation(method: string, path: string, body: unknown): Promi
|
|
|
185
188
|
}
|
|
186
189
|
}
|
|
187
190
|
|
|
188
|
-
|
|
191
|
+
/** The outcome of reading the mutation queue: either the mutations were read
|
|
192
|
+
* successfully, or the read failed and the queue state is unknown. The
|
|
193
|
+
* discriminator lets callers distinguish an empty queue from an unreadable
|
|
194
|
+
* one instead of collapsing both into `[]`. */
|
|
195
|
+
type QueueReadResult =
|
|
196
|
+
| { ok: true; mutations: QueuedMutation[] }
|
|
197
|
+
| { ok: false; error: unknown };
|
|
198
|
+
|
|
199
|
+
/** Read every queued mutation out of IndexedDB in insertion order. Returns
|
|
200
|
+
* `{ ok: true, mutations }` on success (including an empty array when the
|
|
201
|
+
* queue is genuinely empty) or `{ ok: false, error }` on storage failure so
|
|
202
|
+
* callers can distinguish an empty queue from an unreadable one instead of
|
|
203
|
+
* treating both as drained. */
|
|
204
|
+
async function getQueuedMutations(): Promise<QueueReadResult> {
|
|
189
205
|
try {
|
|
190
206
|
const db = await openMutationDB();
|
|
191
207
|
const tx = db.transaction(MUTATION_STORE, 'readonly');
|
|
192
208
|
const store = tx.objectStore(MUTATION_STORE);
|
|
193
|
-
|
|
209
|
+
const mutations = await new Promise<QueuedMutation[]>((resolve, reject) => {
|
|
194
210
|
const request = store.getAll();
|
|
195
211
|
request.onsuccess = () => resolve(request.result as QueuedMutation[]);
|
|
196
212
|
request.onerror = () => reject(request.error);
|
|
197
213
|
});
|
|
198
|
-
|
|
199
|
-
|
|
214
|
+
return { ok: true, mutations };
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return { ok: false, error };
|
|
200
217
|
}
|
|
201
218
|
}
|
|
202
219
|
|
|
@@ -218,10 +235,22 @@ async function clearMutation(id: number): Promise<boolean> {
|
|
|
218
235
|
}
|
|
219
236
|
}
|
|
220
237
|
|
|
238
|
+
/** Replay queued mutations to the API in order, removing each on success and
|
|
239
|
+
* stopping at the first failure so it can retry later, then post-message the
|
|
240
|
+
* connected clients with how many were replayed or remain. A storage read
|
|
241
|
+
* failure is never treated as a drained queue: the initial read failure
|
|
242
|
+
* aborts the flush, and a final read failure reports a partial result rather
|
|
243
|
+
* than claiming all mutations were replayed. */
|
|
221
244
|
async function flushMutationQueue(): Promise<void> {
|
|
222
|
-
const
|
|
245
|
+
const read = await getQueuedMutations();
|
|
246
|
+
if (!read.ok) {
|
|
247
|
+
console.warn('Failed to read mutation queue:', read.error);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const mutations = read.mutations;
|
|
223
251
|
if (mutations.length === 0) return;
|
|
224
252
|
|
|
253
|
+
let replayed = 0;
|
|
225
254
|
for (const mut of mutations) {
|
|
226
255
|
try {
|
|
227
256
|
const opts: RequestInit = {
|
|
@@ -238,6 +267,7 @@ async function flushMutationQueue(): Promise<void> {
|
|
|
238
267
|
// avoid duplicate replays on the next flush; it will retry later.
|
|
239
268
|
break;
|
|
240
269
|
}
|
|
270
|
+
replayed++;
|
|
241
271
|
} else {
|
|
242
272
|
console.warn('Offline mutation failed:', mut.method, mut.path, res.status);
|
|
243
273
|
// Stop processing on first failure — try again later
|
|
@@ -250,8 +280,25 @@ async function flushMutationQueue(): Promise<void> {
|
|
|
250
280
|
}
|
|
251
281
|
|
|
252
282
|
// Notify clients about replayed mutations
|
|
253
|
-
const
|
|
283
|
+
const remainingRead = await getQueuedMutations();
|
|
254
284
|
const clients = await sw.clients.matchAll();
|
|
285
|
+
if (!remainingRead.ok) {
|
|
286
|
+
// Could not re-read the queue — do NOT claim all mutations were replayed.
|
|
287
|
+
// Report a partial result with the known replayed count so unreplayed
|
|
288
|
+
// mutations are not treated as drained.
|
|
289
|
+
console.warn('Failed to re-read mutation queue after replay:', remainingRead.error);
|
|
290
|
+
if (mutations.length > 0) {
|
|
291
|
+
clients.forEach((client) => {
|
|
292
|
+
client.postMessage({
|
|
293
|
+
type: 'MUTATIONS_PARTIAL',
|
|
294
|
+
replayed,
|
|
295
|
+
remaining: mutations.length - replayed,
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const remaining = remainingRead.mutations;
|
|
255
302
|
if (remaining.length === 0 && mutations.length > 0) {
|
|
256
303
|
clients.forEach((client) => {
|
|
257
304
|
client.postMessage({ type: 'MUTATIONS_REPLAYED', count: mutations.length });
|
|
@@ -445,4 +492,21 @@ sw.addEventListener('sync', (event: Event) => {
|
|
|
445
492
|
// ── Online event: flush queue when connectivity returns ──
|
|
446
493
|
sw.addEventListener('online', () => {
|
|
447
494
|
void flushMutationQueue();
|
|
448
|
-
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// ── Test-only: expose queue internals ──
|
|
498
|
+
// app.ts registers this file as a CLASSIC worker (`register('/sw.js', ...)`
|
|
499
|
+
// with no `type: 'module'`), so it cannot use `export` — a module-mode script
|
|
500
|
+
// fails to load under a classic registration, which would take offline support
|
|
501
|
+
// down. That leaves a global as the only seam a test can reach.
|
|
502
|
+
//
|
|
503
|
+
// It is inert in the browser because a classic service worker is the only
|
|
504
|
+
// script that ever runs in its own global scope: no page script, extension or
|
|
505
|
+
// import shares it, so nothing exists that could set the flag before this line
|
|
506
|
+
// executes. That invariant is the registration mode, not the flag name — if
|
|
507
|
+
// app.ts ever registers with `type: 'module'`, replace this with real exports
|
|
508
|
+
// rather than keeping both.
|
|
509
|
+
const __testGlobals = globalThis as unknown as Record<string, unknown>;
|
|
510
|
+
if (__testGlobals.__swTestHarness) {
|
|
511
|
+
__testGlobals.__swInternals = { getQueuedMutations, flushMutationQueue, queueMutation, clearMutation };
|
|
512
|
+
}
|
package/public/src/theme.js
CHANGED
|
@@ -20,6 +20,9 @@ const LABEL = {
|
|
|
20
20
|
light: 'Theme: light',
|
|
21
21
|
dark: 'Theme: dark',
|
|
22
22
|
};
|
|
23
|
+
/** Read the persisted theme choice from localStorage, defaulting to `auto`
|
|
24
|
+
* when no preference is stored or when localStorage is unavailable (e.g.
|
|
25
|
+
* private browsing). */
|
|
23
26
|
export function getStoredTheme() {
|
|
24
27
|
try {
|
|
25
28
|
const v = localStorage.getItem(STORAGE_KEY);
|
|
@@ -29,6 +32,9 @@ export function getStoredTheme() {
|
|
|
29
32
|
catch { /* localStorage may be unavailable (private mode) */ }
|
|
30
33
|
return 'auto';
|
|
31
34
|
}
|
|
35
|
+
/** Apply a theme to the page: set `data-theme` on `<html>`, update the toggle
|
|
36
|
+
* button's glyph/title, and sync the `<meta name="theme-color">` tag so the
|
|
37
|
+
* mobile browser chrome matches the chosen palette. */
|
|
32
38
|
export function applyTheme(theme) {
|
|
33
39
|
document.documentElement.setAttribute('data-theme', theme);
|
|
34
40
|
const btn = document.getElementById('theme-toggle');
|
|
@@ -50,6 +56,8 @@ export function applyTheme(theme) {
|
|
|
50
56
|
meta.setAttribute('content', color);
|
|
51
57
|
}
|
|
52
58
|
}
|
|
59
|
+
/** Persist a theme choice to localStorage and immediately apply it to the
|
|
60
|
+
* document. */
|
|
53
61
|
export function setTheme(theme) {
|
|
54
62
|
try {
|
|
55
63
|
localStorage.setItem(STORAGE_KEY, theme);
|
|
@@ -57,12 +65,16 @@ export function setTheme(theme) {
|
|
|
57
65
|
catch { /* ignore */ }
|
|
58
66
|
applyTheme(theme);
|
|
59
67
|
}
|
|
68
|
+
/** Advance to the next theme in the auto → light → dark order (wrapping
|
|
69
|
+
* around) and persist plus apply the new selection. */
|
|
60
70
|
export function cycleTheme() {
|
|
61
71
|
const current = getStoredTheme();
|
|
62
72
|
const next = ORDER[(ORDER.indexOf(current) + 1) % ORDER.length];
|
|
63
73
|
setTheme(next);
|
|
64
74
|
}
|
|
65
|
-
|
|
75
|
+
/** Apply the persisted (or default) theme early to avoid a flash of the wrong
|
|
76
|
+
* palette, then register a listener so `auto` mode tracks live OS theme
|
|
77
|
+
* changes. */
|
|
66
78
|
export function initTheme() {
|
|
67
79
|
applyTheme(getStoredTheme());
|
|
68
80
|
// When in auto mode, react to live OS theme changes.
|
package/public/src/theme.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","sourceRoot":"","sources":["theme.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,yDAAyD;AACzD,kEAAkE;AAClE,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,yEAAyE;AACzE,8EAA8E;AAC9E,yDAAyD;
|
|
1
|
+
{"version":3,"file":"theme.js","sourceRoot":"","sources":["theme.ts"],"names":[],"mappings":"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"}
|
package/public/src/theme.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// preference) is "auto", matching the data-theme="auto" attribute in the HTML
|
|
9
9
|
// shell so there is no flash of the wrong theme on load.
|
|
10
10
|
|
|
11
|
+
/** The user-selectable theme modes: an explicit `dark` or `light`, or `auto`
|
|
12
|
+
* which follows the OS `prefers-color-scheme` setting. */
|
|
11
13
|
export type Theme = 'dark' | 'light' | 'auto';
|
|
12
14
|
|
|
13
15
|
const STORAGE_KEY = 'pm-web-theme';
|
|
@@ -26,6 +28,9 @@ const LABEL: Record<Theme, string> = {
|
|
|
26
28
|
dark: 'Theme: dark',
|
|
27
29
|
};
|
|
28
30
|
|
|
31
|
+
/** Read the persisted theme choice from localStorage, defaulting to `auto`
|
|
32
|
+
* when no preference is stored or when localStorage is unavailable (e.g.
|
|
33
|
+
* private browsing). */
|
|
29
34
|
export function getStoredTheme(): Theme {
|
|
30
35
|
try {
|
|
31
36
|
const v = localStorage.getItem(STORAGE_KEY);
|
|
@@ -34,6 +39,9 @@ export function getStoredTheme(): Theme {
|
|
|
34
39
|
return 'auto';
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
/** Apply a theme to the page: set `data-theme` on `<html>`, update the toggle
|
|
43
|
+
* button's glyph/title, and sync the `<meta name="theme-color">` tag so the
|
|
44
|
+
* mobile browser chrome matches the chosen palette. */
|
|
37
45
|
export function applyTheme(theme: Theme): void {
|
|
38
46
|
document.documentElement.setAttribute('data-theme', theme);
|
|
39
47
|
const btn = document.getElementById('theme-toggle');
|
|
@@ -54,18 +62,24 @@ export function applyTheme(theme: Theme): void {
|
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
/** Persist a theme choice to localStorage and immediately apply it to the
|
|
66
|
+
* document. */
|
|
57
67
|
export function setTheme(theme: Theme): void {
|
|
58
68
|
try { localStorage.setItem(STORAGE_KEY, theme); } catch { /* ignore */ }
|
|
59
69
|
applyTheme(theme);
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
/** Advance to the next theme in the auto → light → dark order (wrapping
|
|
73
|
+
* around) and persist plus apply the new selection. */
|
|
62
74
|
export function cycleTheme(): void {
|
|
63
75
|
const current = getStoredTheme();
|
|
64
76
|
const next = ORDER[(ORDER.indexOf(current) + 1) % ORDER.length];
|
|
65
77
|
setTheme(next);
|
|
66
78
|
}
|
|
67
79
|
|
|
68
|
-
|
|
80
|
+
/** Apply the persisted (or default) theme early to avoid a flash of the wrong
|
|
81
|
+
* palette, then register a listener so `auto` mode tracks live OS theme
|
|
82
|
+
* changes. */
|
|
69
83
|
export function initTheme(): void {
|
|
70
84
|
applyTheme(getStoredTheme());
|
|
71
85
|
// When in auto mode, react to live OS theme changes.
|
package/public/src/types.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// TYPES
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════
|
|
4
4
|
|
|
5
|
+
/** The signed-in application user, as returned by the auth/session endpoints. */
|
|
5
6
|
export interface User {
|
|
6
7
|
id: string;
|
|
7
8
|
email: string;
|
|
@@ -255,6 +256,7 @@ export interface AdminGroup {
|
|
|
255
256
|
|
|
256
257
|
export type ToastType = 'info' | 'success' | 'error';
|
|
257
258
|
|
|
259
|
+
/** Union of every navigation view key the SPA router can display. */
|
|
258
260
|
export type ViewName =
|
|
259
261
|
| 'projects' | 'items' | 'create' | 'activity' | 'search'
|
|
260
262
|
| 'stats' | 'calendar' | 'context' | 'graph' | 'sharing' | 'groups'
|
package/public/src/utils.js
CHANGED
|
@@ -2,23 +2,41 @@
|
|
|
2
2
|
// UTILITIES
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════
|
|
4
4
|
import { PRIORITY_LABELS, TYPE_ICONS } from './constants.js';
|
|
5
|
+
/** Escape `&`, `<`, `>` and `"` so a value can be inserted into HTML text or a
|
|
6
|
+
* double-quoted attribute. Falsy input yields an empty string.
|
|
7
|
+
*
|
|
8
|
+
* NOT sufficient for a JavaScript context. It does not escape `'`, and even if
|
|
9
|
+
* it did, the browser HTML-decodes an attribute value before the JS parser sees
|
|
10
|
+
* it, so an entity cannot keep a quote from terminating a string literal. A
|
|
11
|
+
* value interpolated into an inline handler — `onclick="fn('${escHtml(x)}')"` —
|
|
12
|
+
* is therefore still injectable. Pass such values through a `data-*` attribute
|
|
13
|
+
* and bind with `addEventListener` instead. */
|
|
5
14
|
export function escHtml(s) {
|
|
6
15
|
if (!s)
|
|
7
16
|
return '';
|
|
8
17
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
9
18
|
}
|
|
19
|
+
/** Build a colored status-badge span for an item status, mapping the
|
|
20
|
+
* `in_progress` code to "In Progress" and CSS-classifying the rest by slug. */
|
|
10
21
|
export function statusBadge(s) {
|
|
11
22
|
const labels = { in_progress: 'In Progress' };
|
|
12
23
|
const status = String(s || 'unknown');
|
|
13
24
|
const classSuffix = status.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
|
|
14
25
|
return `<span class="status-badge status-${classSuffix}">${escHtml(labels[status] || status)}</span>`;
|
|
15
26
|
}
|
|
27
|
+
/** Render a priority indicator span (P0–P4) whose color class and tooltip
|
|
28
|
+
* label come from the PRIORITY_LABELS lookup table. */
|
|
16
29
|
export function priorityDot(p) {
|
|
17
30
|
return `<span class="priority-dot priority-${p}" title="P${p}: ${PRIORITY_LABELS[p] || ''}"></span>`;
|
|
18
31
|
}
|
|
32
|
+
/** Render an item-type glyph span using the TYPE_ICONS map, falling back to
|
|
33
|
+
* the middle-dot character when the type has no assigned icon. */
|
|
19
34
|
export function typeIcon(t) {
|
|
20
|
-
return `<span class="item-type-icon" title="${t}">${TYPE_ICONS[t] || '·'}</span>`;
|
|
35
|
+
return `<span class="item-type-icon" title="${escHtml(t)}">${TYPE_ICONS[t] || '·'}</span>`;
|
|
21
36
|
}
|
|
37
|
+
/** Format a timestamp as a short relative phrase ("just now", "5m ago",
|
|
38
|
+
* "3h ago", "2d ago") or, past a week, a localized calendar date. Falsy or
|
|
39
|
+
* unparseable input returns an empty string. */
|
|
22
40
|
export function relTime(ts) {
|
|
23
41
|
if (!ts)
|
|
24
42
|
return '';
|
|
@@ -35,11 +53,16 @@ export function relTime(ts) {
|
|
|
35
53
|
return `${Math.floor(s / 86400)}d ago`;
|
|
36
54
|
return d.toLocaleDateString();
|
|
37
55
|
}
|
|
56
|
+
/** Format a timestamp as a US-style short date (e.g. "Jan 5, 2026"). Falsy
|
|
57
|
+
* input returns an empty string. */
|
|
38
58
|
export function fmtDate(ts) {
|
|
39
59
|
if (!ts)
|
|
40
60
|
return '';
|
|
41
61
|
return new Date(ts).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
42
62
|
}
|
|
63
|
+
/** Toggle a button's disabled state and, when `text` is supplied, update the
|
|
64
|
+
* button label — replacing the first inner span when present, otherwise the
|
|
65
|
+
* whole text content. */
|
|
43
66
|
export function setLoading(btn, yes, text) {
|
|
44
67
|
btn.disabled = yes;
|
|
45
68
|
if (text) {
|
|
@@ -50,9 +73,11 @@ export function setLoading(btn, yes, text) {
|
|
|
50
73
|
btn.textContent = text;
|
|
51
74
|
}
|
|
52
75
|
}
|
|
76
|
+
/** Build an HTML string of `n` placeholder skeleton rows for loading states. */
|
|
53
77
|
export function skeletonRows(n = 5) {
|
|
54
78
|
return Array.from({ length: n }, () => '<div class="skeleton skeleton-row"></div>').join('');
|
|
55
79
|
}
|
|
80
|
+
/** Build an HTML string of `n` placeholder skeleton cards for loading states. */
|
|
56
81
|
export function skeletonCards(n = 3) {
|
|
57
82
|
return Array.from({ length: n }, () => '<div class="skeleton skeleton-card"></div>').join('');
|
|
58
83
|
}
|
package/public/src/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["utils.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,YAAY;AACZ,kEAAkE;AAClE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE7D,MAAM,UAAU,OAAO,CAAC,CAA4B;IAClD,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAClB,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAC,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAC,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAC,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAC,QAAQ,CAAC,CAAC;AAC1G,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAS;IACnC,MAAM,MAAM,GAA2B,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;IACtE,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC;IACtC,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,SAAS,CAAC;IAC5G,OAAO,oCAAoC,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC;AACxG,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAS;IACnC,OAAO,sCAAsC,CAAC,aAAa,CAAC,KAAK,eAAe,CAAC,CAAC,CAAC,IAAE,EAAE,WAAW,CAAC;AACrG,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,CAAS;IAChC,OAAO,uCAAuC,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,IAAE,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["utils.ts"],"names":[],"mappings":"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"}
|
package/public/src/utils.ts
CHANGED
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════
|
|
4
4
|
import { PRIORITY_LABELS, TYPE_ICONS } from './constants.js';
|
|
5
5
|
|
|
6
|
+
/** Escape `&`, `<`, `>` and `"` so a value can be inserted into HTML text or a
|
|
7
|
+
* double-quoted attribute. Falsy input yields an empty string.
|
|
8
|
+
*
|
|
9
|
+
* NOT sufficient for a JavaScript context. It does not escape `'`, and even if
|
|
10
|
+
* it did, the browser HTML-decodes an attribute value before the JS parser sees
|
|
11
|
+
* it, so an entity cannot keep a quote from terminating a string literal. A
|
|
12
|
+
* value interpolated into an inline handler — `onclick="fn('${escHtml(x)}')"` —
|
|
13
|
+
* is therefore still injectable. Pass such values through a `data-*` attribute
|
|
14
|
+
* and bind with `addEventListener` instead. */
|
|
6
15
|
export function escHtml(s: string | undefined | null): string {
|
|
7
16
|
if (!s) return '';
|
|
8
17
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
9
18
|
}
|
|
10
19
|
|
|
20
|
+
/** Build a colored status-badge span for an item status, mapping the
|
|
21
|
+
* `in_progress` code to "In Progress" and CSS-classifying the rest by slug. */
|
|
11
22
|
export function statusBadge(s: string): string {
|
|
12
23
|
const labels: Record<string, string> = { in_progress: 'In Progress' };
|
|
13
24
|
const status = String(s || 'unknown');
|
|
@@ -15,14 +26,21 @@ export function statusBadge(s: string): string {
|
|
|
15
26
|
return `<span class="status-badge status-${classSuffix}">${escHtml(labels[status] || status)}</span>`;
|
|
16
27
|
}
|
|
17
28
|
|
|
29
|
+
/** Render a priority indicator span (P0–P4) whose color class and tooltip
|
|
30
|
+
* label come from the PRIORITY_LABELS lookup table. */
|
|
18
31
|
export function priorityDot(p: number): string {
|
|
19
32
|
return `<span class="priority-dot priority-${p}" title="P${p}: ${PRIORITY_LABELS[p]||''}"></span>`;
|
|
20
33
|
}
|
|
21
34
|
|
|
35
|
+
/** Render an item-type glyph span using the TYPE_ICONS map, falling back to
|
|
36
|
+
* the middle-dot character when the type has no assigned icon. */
|
|
22
37
|
export function typeIcon(t: string): string {
|
|
23
|
-
return `<span class="item-type-icon" title="${t}">${TYPE_ICONS[t]||'·'}</span>`;
|
|
38
|
+
return `<span class="item-type-icon" title="${escHtml(t)}">${TYPE_ICONS[t]||'·'}</span>`;
|
|
24
39
|
}
|
|
25
40
|
|
|
41
|
+
/** Format a timestamp as a short relative phrase ("just now", "5m ago",
|
|
42
|
+
* "3h ago", "2d ago") or, past a week, a localized calendar date. Falsy or
|
|
43
|
+
* unparseable input returns an empty string. */
|
|
26
44
|
export function relTime(ts: string | undefined | null): string {
|
|
27
45
|
if (!ts) return '';
|
|
28
46
|
const d = new Date(ts);
|
|
@@ -35,11 +53,16 @@ export function relTime(ts: string | undefined | null): string {
|
|
|
35
53
|
return d.toLocaleDateString();
|
|
36
54
|
}
|
|
37
55
|
|
|
56
|
+
/** Format a timestamp as a US-style short date (e.g. "Jan 5, 2026"). Falsy
|
|
57
|
+
* input returns an empty string. */
|
|
38
58
|
export function fmtDate(ts: string | undefined | null): string {
|
|
39
59
|
if (!ts) return '';
|
|
40
60
|
return new Date(ts).toLocaleDateString('en-US', { year:'numeric', month:'short', day:'numeric' });
|
|
41
61
|
}
|
|
42
62
|
|
|
63
|
+
/** Toggle a button's disabled state and, when `text` is supplied, update the
|
|
64
|
+
* button label — replacing the first inner span when present, otherwise the
|
|
65
|
+
* whole text content. */
|
|
43
66
|
export function setLoading(btn: HTMLButtonElement, yes: boolean, text?: string): void {
|
|
44
67
|
btn.disabled = yes;
|
|
45
68
|
if (text) {
|
|
@@ -49,10 +72,12 @@ export function setLoading(btn: HTMLButtonElement, yes: boolean, text?: string):
|
|
|
49
72
|
}
|
|
50
73
|
}
|
|
51
74
|
|
|
75
|
+
/** Build an HTML string of `n` placeholder skeleton rows for loading states. */
|
|
52
76
|
export function skeletonRows(n = 5): string {
|
|
53
77
|
return Array.from({length: n}, () => '<div class="skeleton skeleton-row"></div>').join('');
|
|
54
78
|
}
|
|
55
79
|
|
|
80
|
+
/** Build an HTML string of `n` placeholder skeleton cards for loading states. */
|
|
56
81
|
export function skeletonCards(n = 3): string {
|
|
57
82
|
return Array.from({length: n}, () => '<div class="skeleton skeleton-card"></div>').join('');
|
|
58
83
|
}
|
|
@@ -5,6 +5,7 @@ import { state } from '../state.js';
|
|
|
5
5
|
import { api } from '../api.js';
|
|
6
6
|
import { escHtml, relTime } from '../utils.js';
|
|
7
7
|
import { TYPE_ICONS } from '../constants.js';
|
|
8
|
+
/** Renders the activity feed for the current project, listing recent changes with their type icon, a description or fallback, and a relative timestamp. */
|
|
8
9
|
export async function renderActivityView() {
|
|
9
10
|
const el = document.getElementById('content-activity');
|
|
10
11
|
if (!el)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity.js","sourceRoot":"","sources":["activity.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,gBAAgB;AAChB,kEAAkE;AAClE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7C,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,EAAE,GAAG,QAAQ,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE;QAAE,OAAO;IAChB,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;QAAC,EAAE,CAAC,SAAS,GAAG,wFAAwF,CAAC;QAAC,OAAO;IAAC,CAAC;IAC/I,EAAE,CAAC,SAAS,GAAG;;gGAE+E,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC;;;iJAGe,CAAC;IAEhJ,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAmB,KAAK,EAAC,aAAa,KAAK,CAAC,cAAc,CAAC,EAAE,uBAAuB,CAAC,CAAC;QAC5G,MAAM,KAAK,GAAoB,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QACjE,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,SAAS,GAAG,yHAAyH,CAAC;YAC7I,OAAO;QACT,CAAC;QACD,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAC,EAAE,CAAA;;qCAED,UAAU,CAAC,CAAC,CAAC,IAAI,IAAE,EAAE,CAAC,IAAE,GAAG;;uCAEzB,OAAO,CAAC,CAAC,CAAC,OAAO,IAAE,CAAC,CAAC,KAAK,IAAE,CAAC,CAAC,MAAM,IAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;uCACxD,OAAO,CAAC,CAAC,CAAC,SAAS,IAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,CAAA,CAAC,CAAA,+CAA+C,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAA,CAAC,CAAA,EAAE;;aAE7I,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;IAAC,OAAM,GAAY,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QACxD,IAAI,MAAM;YAAE,MAAM,CAAC,SAAS,GAAG,iEAAiE,OAAO,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC;IAC1K,CAAC;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"activity.js","sourceRoot":"","sources":["activity.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,gBAAgB;AAChB,kEAAkE;AAClE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7C,2JAA2J;AAC3J,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,EAAE,GAAG,QAAQ,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE;QAAE,OAAO;IAChB,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;QAAC,EAAE,CAAC,SAAS,GAAG,wFAAwF,CAAC;QAAC,OAAO;IAAC,CAAC;IAC/I,EAAE,CAAC,SAAS,GAAG;;gGAE+E,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC;;;iJAGe,CAAC;IAEhJ,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAmB,KAAK,EAAC,aAAa,KAAK,CAAC,cAAc,CAAC,EAAE,uBAAuB,CAAC,CAAC;QAC5G,MAAM,KAAK,GAAoB,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QACjE,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,SAAS,GAAG,yHAAyH,CAAC;YAC7I,OAAO;QACT,CAAC;QACD,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAC,EAAE,CAAA;;qCAED,UAAU,CAAC,CAAC,CAAC,IAAI,IAAE,EAAE,CAAC,IAAE,GAAG;;uCAEzB,OAAO,CAAC,CAAC,CAAC,OAAO,IAAE,CAAC,CAAC,KAAK,IAAE,CAAC,CAAC,MAAM,IAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;uCACxD,OAAO,CAAC,CAAC,CAAC,SAAS,IAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,CAAA,CAAC,CAAA,+CAA+C,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAA,CAAC,CAAA,EAAE;;aAE7I,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;IAAC,OAAM,GAAY,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QACxD,IAAI,MAAM;YAAE,MAAM,CAAC,SAAS,GAAG,iEAAiE,OAAO,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC;IAC1K,CAAC;AACH,CAAC"}
|
|
@@ -7,6 +7,7 @@ import { escHtml, relTime } from '../utils.js';
|
|
|
7
7
|
import { TYPE_ICONS } from '../constants.js';
|
|
8
8
|
import type { ActivityEntry, ActivityResponse } from '../api-types.js';
|
|
9
9
|
|
|
10
|
+
/** Renders the activity feed for the current project, listing recent changes with their type icon, a description or fallback, and a relative timestamp. */
|
|
10
11
|
export async function renderActivityView(): Promise<void> {
|
|
11
12
|
const el = document.getElementById('content-activity');
|
|
12
13
|
if (!el) return;
|