@anionex/dsh-pinned-sessions 0.1.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/lib/client.js ADDED
@@ -0,0 +1,794 @@
1
+ window.__ModuleLoader__.load({ id: "@anionex/dsh-pinned-sessions", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ "use strict";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/client/index.tsx
23
+ var index_exports = {};
24
+ __export(index_exports, {
25
+ PinnedSessionsBridge: () => PinnedSessionsBridge,
26
+ apply: () => apply,
27
+ inject: () => inject
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+ var import_react3 = require("react");
31
+
32
+ // node_modules/lucide-react/dist/esm/createLucideIcon.js
33
+ var import_react2 = require("react");
34
+
35
+ // node_modules/lucide-react/dist/esm/shared/src/utils.js
36
+ var toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
37
+ var mergeClasses = (...classes) => classes.filter((className, index, array) => {
38
+ return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
39
+ }).join(" ").trim();
40
+
41
+ // node_modules/lucide-react/dist/esm/Icon.js
42
+ var import_react = require("react");
43
+
44
+ // node_modules/lucide-react/dist/esm/defaultAttributes.js
45
+ var defaultAttributes = {
46
+ xmlns: "http://www.w3.org/2000/svg",
47
+ width: 24,
48
+ height: 24,
49
+ viewBox: "0 0 24 24",
50
+ fill: "none",
51
+ stroke: "currentColor",
52
+ strokeWidth: 2,
53
+ strokeLinecap: "round",
54
+ strokeLinejoin: "round"
55
+ };
56
+
57
+ // node_modules/lucide-react/dist/esm/Icon.js
58
+ var Icon = (0, import_react.forwardRef)(
59
+ ({
60
+ color = "currentColor",
61
+ size = 24,
62
+ strokeWidth = 2,
63
+ absoluteStrokeWidth,
64
+ className = "",
65
+ children,
66
+ iconNode,
67
+ ...rest
68
+ }, ref) => {
69
+ return (0, import_react.createElement)(
70
+ "svg",
71
+ {
72
+ ref,
73
+ ...defaultAttributes,
74
+ width: size,
75
+ height: size,
76
+ stroke: color,
77
+ strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
78
+ className: mergeClasses("lucide", className),
79
+ ...rest
80
+ },
81
+ [
82
+ ...iconNode.map(([tag, attrs]) => (0, import_react.createElement)(tag, attrs)),
83
+ ...Array.isArray(children) ? children : [children]
84
+ ]
85
+ );
86
+ }
87
+ );
88
+
89
+ // node_modules/lucide-react/dist/esm/createLucideIcon.js
90
+ var createLucideIcon = (iconName, iconNode) => {
91
+ const Component = (0, import_react2.forwardRef)(
92
+ ({ className, ...props }, ref) => (0, import_react2.createElement)(Icon, {
93
+ ref,
94
+ iconNode,
95
+ className: mergeClasses(`lucide-${toKebabCase(iconName)}`, className),
96
+ ...props
97
+ })
98
+ );
99
+ Component.displayName = `${iconName}`;
100
+ return Component;
101
+ };
102
+
103
+ // node_modules/lucide-react/dist/esm/icons/pin.js
104
+ var Pin = createLucideIcon("Pin", [
105
+ ["path", { d: "M12 17v5", key: "bb1du9" }],
106
+ [
107
+ "path",
108
+ {
109
+ d: "M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z",
110
+ key: "1nkz8b"
111
+ }
112
+ ]
113
+ ]);
114
+
115
+ // node_modules/lucide-react/dist/esm/icons/x.js
116
+ var X = createLucideIcon("X", [
117
+ ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
118
+ ["path", { d: "m6 6 12 12", key: "d8bk6v" }]
119
+ ]);
120
+
121
+ // src/client/index.tsx
122
+ var import_react_dom = require("react-dom");
123
+
124
+ // node_modules/lucide/dist/esm/defaultAttributes.js
125
+ var defaultAttributes2 = {
126
+ xmlns: "http://www.w3.org/2000/svg",
127
+ width: 24,
128
+ height: 24,
129
+ viewBox: "0 0 24 24",
130
+ fill: "none",
131
+ stroke: "currentColor",
132
+ "stroke-width": 2,
133
+ "stroke-linecap": "round",
134
+ "stroke-linejoin": "round"
135
+ };
136
+
137
+ // node_modules/lucide/dist/esm/icons/pin-off.js
138
+ var PinOff = [
139
+ "svg",
140
+ defaultAttributes2,
141
+ [
142
+ ["path", { d: "M12 17v5" }],
143
+ ["path", { d: "M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89" }],
144
+ ["path", { d: "m2 2 20 20" }],
145
+ ["path", { d: "M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11" }]
146
+ ]
147
+ ];
148
+
149
+ // node_modules/lucide/dist/esm/icons/pin.js
150
+ var Pin2 = [
151
+ "svg",
152
+ defaultAttributes2,
153
+ [
154
+ ["path", { d: "M12 17v5" }],
155
+ [
156
+ "path",
157
+ {
158
+ d: "M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"
159
+ }
160
+ ]
161
+ ]
162
+ ];
163
+
164
+ // src/client/dom-bridge.ts
165
+ var SIDEBAR_SLOT_SELECTOR = '[data-slot="sidebar.workspaces"]';
166
+ var PINNED_HOST_ATTRIBUTE = "data-dsh-pinned-sessions-host";
167
+ var MENU_HOST_ATTRIBUTE = "data-dsh-pinned-session-menu-host";
168
+ var MENU_OWNER_ATTRIBUTE = "data-dsh-pinned-session-menu";
169
+ function elementFromTarget(target) {
170
+ return target !== null && target.nodeType === 1 ? target : null;
171
+ }
172
+ function findSessionActionTarget(target) {
173
+ const element = elementFromTarget(target);
174
+ const button = element?.closest("button");
175
+ if (!(button instanceof HTMLButtonElement)) return null;
176
+ const row = button.closest('[role="treeitem"][aria-selected]');
177
+ if (row === null || row === button) return null;
178
+ if (row.closest(SIDEBAR_SLOT_SELECTOR) === null) return null;
179
+ if (row.closest(`[${PINNED_HOST_ATTRIBUTE}]`) !== null) return null;
180
+ return { button, row };
181
+ }
182
+ function captureSessionId(row, sessions) {
183
+ const hadOwn = Object.prototype.hasOwnProperty.call(sessions, "open");
184
+ const ownDescriptor = hadOwn ? Object.getOwnPropertyDescriptor(sessions, "open") : void 0;
185
+ const original = sessions.open;
186
+ let captured = null;
187
+ const interceptor = (sessionId) => {
188
+ if (typeof sessionId === "string" && sessionId.length > 0) captured = sessionId;
189
+ };
190
+ try {
191
+ Object.defineProperty(sessions, "open", {
192
+ configurable: true,
193
+ enumerable: ownDescriptor?.enumerable ?? false,
194
+ writable: true,
195
+ value: interceptor
196
+ });
197
+ if (sessions.open !== interceptor) return null;
198
+ row.click();
199
+ } catch {
200
+ return null;
201
+ } finally {
202
+ try {
203
+ if (hadOwn && ownDescriptor !== void 0) Object.defineProperty(sessions, "open", ownDescriptor);
204
+ else delete sessions.open;
205
+ } catch {
206
+ try {
207
+ Object.defineProperty(sessions, "open", {
208
+ configurable: true,
209
+ writable: true,
210
+ value: original
211
+ });
212
+ } catch {
213
+ }
214
+ }
215
+ }
216
+ return captured;
217
+ }
218
+ function ensurePinnedHost(doc) {
219
+ const slot = doc.querySelector(SIDEBAR_SLOT_SELECTOR);
220
+ const root = slot?.firstElementChild;
221
+ if (!(root instanceof HTMLElement)) return null;
222
+ const existing = root.querySelector(`:scope > [${PINNED_HOST_ATTRIBUTE}]`);
223
+ if (existing !== null) return existing;
224
+ const header = root.firstElementChild;
225
+ if (header === null) return null;
226
+ const host = doc.createElement("div");
227
+ host.setAttribute(PINNED_HOST_ATTRIBUTE, "");
228
+ header.insertAdjacentElement("afterend", host);
229
+ return host;
230
+ }
231
+ function listPortalMenus(doc) {
232
+ return [...doc.body.children].filter((node) => node instanceof HTMLElement && node.getAttribute("role") === "menu");
233
+ }
234
+ function belongsToTrigger(menu, trigger) {
235
+ if (trigger === void 0 || trigger.width === 0 || trigger.height === 0) return true;
236
+ const menuRect = menu.getBoundingClientRect();
237
+ if (menuRect.width === 0 || menuRect.height === 0) return true;
238
+ const horizontalGap = Math.min(
239
+ Math.abs(menuRect.left - trigger.left),
240
+ Math.abs(menuRect.right - trigger.right)
241
+ );
242
+ const verticalGap = Math.max(
243
+ 0,
244
+ menuRect.top - trigger.bottom,
245
+ trigger.top - menuRect.bottom
246
+ );
247
+ return horizontalGap <= 64 && verticalGap <= 32;
248
+ }
249
+ function findUnclaimedPortalMenu(doc, excluded = /* @__PURE__ */ new Set(), trigger) {
250
+ const menus = listPortalMenus(doc).filter((menu) => !excluded.has(menu) && !menu.hasAttribute(MENU_OWNER_ATTRIBUTE) && belongsToTrigger(menu, trigger));
251
+ return menus.at(-1) ?? null;
252
+ }
253
+ function createLucideIcon2(doc, node, name) {
254
+ const [, attributes, children = []] = node;
255
+ const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
256
+ for (const [key, value] of Object.entries(attributes)) svg.setAttribute(key, String(value));
257
+ svg.setAttribute("width", "16");
258
+ svg.setAttribute("height", "16");
259
+ svg.setAttribute("stroke-width", "1.8");
260
+ svg.setAttribute("class", `lucide lucide-${name}`);
261
+ svg.setAttribute("aria-hidden", "true");
262
+ for (const [tag, childAttributes] of children) {
263
+ const child = doc.createElementNS("http://www.w3.org/2000/svg", tag);
264
+ for (const [key, value] of Object.entries(childAttributes)) child.setAttribute(key, String(value));
265
+ svg.appendChild(child);
266
+ }
267
+ return svg;
268
+ }
269
+ function updateSessionMenuItem(target, pinned, label) {
270
+ target.button.setAttribute("aria-label", label);
271
+ target.label.textContent = label;
272
+ target.icon.replaceChildren(createLucideIcon2(
273
+ target.menu.ownerDocument,
274
+ pinned ? PinOff : Pin2,
275
+ pinned ? "pin-off" : "pin"
276
+ ));
277
+ }
278
+ function attachSessionMenuHost(menu, sessionId) {
279
+ if (menu.hasAttribute(MENU_OWNER_ATTRIBUTE)) return null;
280
+ const viewport = [...menu.children].find((node) => node instanceof HTMLElement && node.getAttribute("role") === "presentation");
281
+ if (viewport === void 0) return null;
282
+ const wrappers = [...viewport.children].filter((node) => node instanceof HTMLElement && node.querySelector(':scope > button[role="menuitem"]') instanceof HTMLButtonElement);
283
+ if (wrappers.length < 3 || wrappers.length > 4) return null;
284
+ const templateButton = wrappers[0]?.querySelector(':scope > button[role="menuitem"]');
285
+ if (templateButton === null || templateButton === void 0) return null;
286
+ const spans = [...templateButton.children].filter((node) => node instanceof HTMLSpanElement);
287
+ if (spans.length < 2) return null;
288
+ const doc = menu.ownerDocument;
289
+ const host = doc.createElement("div");
290
+ host.className = wrappers[0]?.className ?? "";
291
+ host.setAttribute(MENU_HOST_ATTRIBUTE, "");
292
+ const button = doc.createElement("button");
293
+ button.type = "button";
294
+ button.setAttribute("role", "menuitem");
295
+ button.className = `${templateButton.className} dsh-pins-menu-button`;
296
+ const icon = doc.createElement("span");
297
+ icon.className = spans[0]?.className ?? "";
298
+ icon.setAttribute("aria-hidden", "true");
299
+ const label = doc.createElement("span");
300
+ label.className = spans.at(-1)?.className ?? "";
301
+ button.append(icon, label);
302
+ host.appendChild(button);
303
+ viewport.insertBefore(host, wrappers[2] ?? null);
304
+ menu.setAttribute(MENU_OWNER_ATTRIBUTE, sessionId);
305
+ const target = { host, menu, sessionId, button, icon, label };
306
+ const view = doc.defaultView;
307
+ if (view !== null) view.dispatchEvent(new view.Event("resize"));
308
+ return target;
309
+ }
310
+ function focusAfterPinnedRemoval(doc, host, removedIndex) {
311
+ const remaining = host === null ? [] : [...host.querySelectorAll(".dsh-pins-open")];
312
+ const next = remaining[Math.min(removedIndex, remaining.length - 1)];
313
+ if (next !== void 0) {
314
+ next.focus({ preventScroll: true });
315
+ return true;
316
+ }
317
+ const nativeRow = doc.querySelector(`${SIDEBAR_SLOT_SELECTOR} [role="treeitem"][aria-selected="true"]`) ?? doc.querySelector(`${SIDEBAR_SLOT_SELECTOR} [role="treeitem"]`);
318
+ if (nativeRow === null) return false;
319
+ const hadTabIndex = nativeRow.hasAttribute("tabindex");
320
+ if (!hadTabIndex) nativeRow.tabIndex = -1;
321
+ nativeRow.focus({ preventScroll: true });
322
+ if (doc.activeElement === nativeRow) return true;
323
+ if (!hadTabIndex) nativeRow.removeAttribute("tabindex");
324
+ return false;
325
+ }
326
+ function removeBridgeArtifacts(doc) {
327
+ for (const node of doc.querySelectorAll(`[${PINNED_HOST_ATTRIBUTE}], [${MENU_HOST_ATTRIBUTE}]`)) node.remove();
328
+ for (const menu of doc.querySelectorAll(`[${MENU_OWNER_ATTRIBUTE}]`)) menu.removeAttribute(MENU_OWNER_ATTRIBUTE);
329
+ }
330
+
331
+ // src/client/pin-store.ts
332
+ var PIN_STORAGE_KEY = "dsh.pinned-sessions.v1";
333
+ var MAX_PINS = 500;
334
+ function isPinRecord(value) {
335
+ if (typeof value !== "object" || value === null) return false;
336
+ const row = value;
337
+ return typeof row.id === "string" && row.id.length > 0 && Number.isFinite(row.pinnedAt) && row.pinnedAt >= 0;
338
+ }
339
+ function decodePins(raw) {
340
+ if (raw === null) return [];
341
+ try {
342
+ const parsed = JSON.parse(raw);
343
+ if (parsed.version !== 1 || !Array.isArray(parsed.pins)) return [];
344
+ const byId = /* @__PURE__ */ new Map();
345
+ for (const candidate of parsed.pins) {
346
+ if (!isPinRecord(candidate)) continue;
347
+ const previous = byId.get(candidate.id);
348
+ if (previous === void 0 || candidate.pinnedAt > previous.pinnedAt) {
349
+ byId.set(candidate.id, { id: candidate.id, pinnedAt: candidate.pinnedAt });
350
+ }
351
+ }
352
+ return [...byId.values()].sort((left, right) => right.pinnedAt - left.pinnedAt || left.id.localeCompare(right.id)).slice(0, MAX_PINS);
353
+ } catch {
354
+ return [];
355
+ }
356
+ }
357
+ var PinStore = class {
358
+ constructor(storage) {
359
+ this.storage = storage;
360
+ this.snapshot = this.read();
361
+ }
362
+ snapshot;
363
+ listeners = /* @__PURE__ */ new Set();
364
+ getSnapshot = () => this.snapshot;
365
+ subscribe = (listener) => {
366
+ this.listeners.add(listener);
367
+ return () => {
368
+ this.listeners.delete(listener);
369
+ };
370
+ };
371
+ isPinned(id) {
372
+ return this.snapshot.some((row) => row.id === id);
373
+ }
374
+ toggle(id, now = Date.now()) {
375
+ if (id.length === 0) return;
376
+ const without = this.snapshot.filter((row) => row.id !== id);
377
+ this.publish(this.isPinned(id) ? without : [{ id, pinnedAt: now }, ...without]);
378
+ }
379
+ prune(keep) {
380
+ const next = this.snapshot.filter((row) => keep(row.id));
381
+ if (next.length !== this.snapshot.length) this.publish(next);
382
+ }
383
+ reload() {
384
+ const next = this.read();
385
+ if (samePins(this.snapshot, next)) return;
386
+ this.snapshot = next;
387
+ this.emit();
388
+ }
389
+ read() {
390
+ try {
391
+ return decodePins(this.storage?.getItem(PIN_STORAGE_KEY) ?? null);
392
+ } catch {
393
+ return [];
394
+ }
395
+ }
396
+ publish(next) {
397
+ this.snapshot = [...next].sort((left, right) => right.pinnedAt - left.pinnedAt || left.id.localeCompare(right.id)).slice(0, MAX_PINS);
398
+ try {
399
+ this.storage?.setItem(PIN_STORAGE_KEY, JSON.stringify({ version: 1, pins: this.snapshot }));
400
+ } catch {
401
+ }
402
+ this.emit();
403
+ }
404
+ emit() {
405
+ for (const listener of [...this.listeners]) listener();
406
+ }
407
+ };
408
+ function samePins(left, right) {
409
+ return left.length === right.length && left.every((row, index) => row.id === right[index]?.id && row.pinnedAt === right[index]?.pinnedAt);
410
+ }
411
+
412
+ // src/client/presentation.ts
413
+ function nonBlank(value) {
414
+ const trimmed = value?.trim();
415
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
416
+ }
417
+ function sessionDisplayTitle(session, workspaceTitle) {
418
+ const explicit = nonBlank(session.title);
419
+ if (explicit !== void 0) return explicit;
420
+ const workspace = nonBlank(workspaceTitle);
421
+ if (workspace !== void 0) return workspace;
422
+ const cwd = nonBlank(session.cwd);
423
+ if (cwd !== void 0) {
424
+ const leaf = cwd.replace(/[\\/]+$/u, "").split(/[\\/]/u).at(-1);
425
+ if (leaf !== void 0 && leaf.length > 0) return leaf;
426
+ }
427
+ return session.id;
428
+ }
429
+
430
+ // src/client/index.tsx
431
+ var import_jsx_runtime = require("react/jsx-runtime");
432
+ var NS = "pinnedSessions";
433
+ var STYLE_ID = "@anionex/dsh-pinned-sessions";
434
+ var MENU_ASSOCIATION_MS = 2e3;
435
+ var zh = {
436
+ "menu.pin": "\u7F6E\u9876\u4F1A\u8BDD",
437
+ "menu.unpin": "\u53D6\u6D88\u7F6E\u9876",
438
+ "section.label": "\u7F6E\u9876\u4F1A\u8BDD",
439
+ "session.open": "\u6253\u5F00\u7F6E\u9876\u4F1A\u8BDD\u201C{name}\u201D",
440
+ "session.unpin": "\u53D6\u6D88\u7F6E\u9876\u201C{name}\u201D",
441
+ "workspace.ungrouped": "\u672A\u5206\u7EC4"
442
+ };
443
+ var en = {
444
+ "menu.pin": "Pin session",
445
+ "menu.unpin": "Unpin session",
446
+ "section.label": "Pinned sessions",
447
+ "session.open": "Open pinned session \u201C{name}\u201D",
448
+ "session.unpin": "Unpin \u201C{name}\u201D",
449
+ "workspace.ungrouped": "Ungrouped"
450
+ };
451
+ var styles = `
452
+ [${"data-dsh-pinned-sessions-host"}]{box-sizing:border-box;min-width:0;flex:none}
453
+ .dsh-pins-section{box-sizing:border-box;min-width:0;margin:0 4px 6px;padding:0 0 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}
454
+ .dsh-pins-header{display:flex;align-items:center;gap:6px;height:28px;padding:0 8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;font-weight:500}
455
+ .dsh-pins-header svg{flex:none;color:var(--dsw-alias-state-business-primary)}
456
+ .dsh-pins-list{box-sizing:border-box;max-height:192px;min-width:0;margin:0;padding:0;overflow-y:auto;overscroll-behavior:contain;list-style:none}
457
+ .dsh-pins-row{box-sizing:border-box;display:flex;align-items:center;gap:0;height:32px;min-width:0;border-radius:6px;color:var(--dsw-alias-label-primary)}
458
+ .dsh-pins-row:hover,.dsh-pins-row[data-current="true"]{background:var(--dsw-alias-interactive-bg-hover)}
459
+ .dsh-pins-open{box-sizing:border-box;display:flex;align-items:center;gap:6px;min-width:0;height:32px;padding:0 4px 0 8px;border:0;background:transparent;color:inherit;cursor:pointer;flex:1;text-align:left}
460
+ .dsh-pins-open:focus-visible,.dsh-pins-remove:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
461
+ .dsh-pins-open>svg{flex:none;color:var(--dsw-alias-state-business-primary)}
462
+ .dsh-pins-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:20px;flex:1}
463
+ .dsh-pins-workspace{max-width:34%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px;flex:none}
464
+ .dsh-pins-remove{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;margin-right:3px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;flex:none}
465
+ .dsh-pins-row:hover .dsh-pins-remove,.dsh-pins-remove:focus-visible{opacity:1}
466
+ .dsh-pins-remove:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
467
+ .dsh-pins-menu-button{letter-spacing:0}
468
+ @media (max-width:767px){.dsh-pins-workspace{display:none}.dsh-pins-remove{opacity:1}}
469
+ `;
470
+ var inject = ["slots", "sessions", "locale"];
471
+ function apply(ctx) {
472
+ let storage;
473
+ try {
474
+ storage = window.localStorage;
475
+ } catch {
476
+ storage = void 0;
477
+ }
478
+ const store = new PinStore(storage);
479
+ ctx.effect(() => {
480
+ const onStorage = (event) => {
481
+ if (event.key === PIN_STORAGE_KEY || event.key === null) store.reload();
482
+ };
483
+ window.addEventListener("storage", onStorage);
484
+ return () => {
485
+ window.removeEventListener("storage", onStorage);
486
+ };
487
+ }, "pinned-sessions: cross-tab state");
488
+ ctx.effect(() => {
489
+ if (document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`) !== null) return () => {
490
+ };
491
+ const tag = document.createElement("style");
492
+ tag.dataset.plugin = "@anionex/dsh-pinned-sessions";
493
+ tag.dataset.pluginCss = STYLE_ID;
494
+ tag.textContent = styles;
495
+ document.head.appendChild(tag);
496
+ return () => {
497
+ tag.remove();
498
+ };
499
+ }, "pinned-sessions: styles");
500
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "pinned-sessions: dictionaries");
501
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
502
+ name: "shell.overlay",
503
+ id: "pinned-sessions-bridge",
504
+ order: 40,
505
+ locale: NS,
506
+ inject: () => ({ store, sessions: ctx.sessions })
507
+ }, PinnedSessionsBridge));
508
+ }
509
+ function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }) {
510
+ const sessionSnapshot = useSessions((snapshot) => snapshot);
511
+ const workspaceSnapshot = useWorkspaces((snapshot) => snapshot);
512
+ const pins = (0, import_react3.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
513
+ const [sidebarHost, setSidebarHost] = (0, import_react3.useState)(null);
514
+ const [sidebarWide, setSidebarWide] = (0, import_react3.useState)(true);
515
+ const pending = (0, import_react3.useRef)(null);
516
+ const activeMenu = (0, import_react3.useRef)(null);
517
+ const focusAfterUnpin = (0, import_react3.useRef)(null);
518
+ (0, import_react3.useLayoutEffect)(() => {
519
+ let active = true;
520
+ let queued = false;
521
+ let settleTimer;
522
+ let observedSidebar = null;
523
+ const sidebarObserver = new MutationObserver(() => {
524
+ queueRefresh();
525
+ });
526
+ const disposeActiveMenu = () => {
527
+ const binding = activeMenu.current;
528
+ if (binding === null) return;
529
+ activeMenu.current = null;
530
+ binding.target.button.removeEventListener("click", binding.onClick);
531
+ binding.target.host.remove();
532
+ if (binding.target.menu.getAttribute(MENU_OWNER_ATTRIBUTE) === binding.target.sessionId) {
533
+ binding.target.menu.removeAttribute(MENU_OWNER_ATTRIBUTE);
534
+ }
535
+ };
536
+ const updateActiveMenu = () => {
537
+ const binding = activeMenu.current;
538
+ if (binding === null) return;
539
+ const pinned = store.isPinned(binding.target.sessionId);
540
+ updateSessionMenuItem(binding.target, pinned, t(pinned ? "menu.unpin" : "menu.pin"));
541
+ };
542
+ const observeSidebar = () => {
543
+ const slot = document.querySelector(SIDEBAR_SLOT_SELECTOR);
544
+ if (slot === observedSidebar) return;
545
+ sidebarObserver.disconnect();
546
+ observedSidebar = slot;
547
+ if (slot !== null) {
548
+ sidebarObserver.observe(slot, {
549
+ attributes: true,
550
+ attributeFilter: ["class", "style"],
551
+ childList: true,
552
+ subtree: true
553
+ });
554
+ }
555
+ };
556
+ const refresh = () => {
557
+ if (!active) return;
558
+ const nextHost = ensurePinnedHost(document);
559
+ setSidebarHost((current) => current === nextHost ? current : nextHost);
560
+ observeSidebar();
561
+ const rootWidth = nextHost?.parentElement?.getBoundingClientRect().width;
562
+ if (rootWidth !== void 0) setSidebarWide(rootWidth >= 160);
563
+ const binding = activeMenu.current;
564
+ if (binding !== null && (!binding.target.host.isConnected || !binding.target.menu.isConnected)) {
565
+ disposeActiveMenu();
566
+ }
567
+ const request = pending.current;
568
+ if (request === null) return;
569
+ if (Date.now() - request.createdAt > MENU_ASSOCIATION_MS) {
570
+ pending.current = null;
571
+ return;
572
+ }
573
+ const menu = findUnclaimedPortalMenu(document, request.existingMenus, request.triggerRect);
574
+ if (menu === null) return;
575
+ const target = attachSessionMenuHost(menu, request.sessionId);
576
+ if (target === null) return;
577
+ const onClick = (event) => {
578
+ event.preventDefault();
579
+ event.stopPropagation();
580
+ store.toggle(target.sessionId);
581
+ disposeActiveMenu();
582
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
583
+ };
584
+ target.button.addEventListener("click", onClick);
585
+ activeMenu.current = { target, onClick };
586
+ pending.current = null;
587
+ updateActiveMenu();
588
+ };
589
+ function queueRefresh() {
590
+ if (!active) return;
591
+ if (settleTimer === void 0) {
592
+ settleTimer = window.setTimeout(() => {
593
+ settleTimer = void 0;
594
+ refresh();
595
+ }, 250);
596
+ }
597
+ if (queued) return;
598
+ queued = true;
599
+ queueMicrotask(() => {
600
+ queued = false;
601
+ refresh();
602
+ });
603
+ }
604
+ const onClickCapture = (event) => {
605
+ pending.current = null;
606
+ const target = findSessionActionTarget(event.target);
607
+ if (target === null) return;
608
+ const sessionId = captureSessionId(target.row, sessions);
609
+ if (sessionId === null) return;
610
+ const rect = target.button.getBoundingClientRect();
611
+ pending.current = {
612
+ sessionId,
613
+ createdAt: Date.now(),
614
+ existingMenus: new Set(listPortalMenus(document)),
615
+ triggerRect: {
616
+ top: rect.top,
617
+ right: rect.right,
618
+ bottom: rect.bottom,
619
+ left: rect.left,
620
+ width: rect.width,
621
+ height: rect.height
622
+ }
623
+ };
624
+ queueRefresh();
625
+ };
626
+ const onKeyDownCapture = () => {
627
+ pending.current = null;
628
+ };
629
+ refresh();
630
+ document.addEventListener("click", onClickCapture, true);
631
+ document.addEventListener("keydown", onKeyDownCapture, true);
632
+ const bodyObserver = new MutationObserver(() => {
633
+ queueRefresh();
634
+ });
635
+ bodyObserver.observe(document.body, { childList: true });
636
+ const unsubscribe = store.subscribe(updateActiveMenu);
637
+ return () => {
638
+ active = false;
639
+ pending.current = null;
640
+ if (settleTimer !== void 0) window.clearTimeout(settleTimer);
641
+ unsubscribe();
642
+ bodyObserver.disconnect();
643
+ sidebarObserver.disconnect();
644
+ document.removeEventListener("click", onClickCapture, true);
645
+ document.removeEventListener("keydown", onKeyDownCapture, true);
646
+ disposeActiveMenu();
647
+ removeBridgeArtifacts(document);
648
+ };
649
+ }, [sessions, store, t]);
650
+ (0, import_react3.useLayoutEffect)(() => {
651
+ if (sidebarHost === null) return;
652
+ const root = sidebarHost.parentElement;
653
+ if (root === null) return;
654
+ const update = () => {
655
+ setSidebarWide(root.getBoundingClientRect().width >= 160);
656
+ };
657
+ update();
658
+ if (typeof ResizeObserver === "undefined") return;
659
+ const observer = new ResizeObserver(update);
660
+ observer.observe(root);
661
+ return () => {
662
+ observer.disconnect();
663
+ };
664
+ }, [sidebarHost]);
665
+ (0, import_react3.useEffect)(() => {
666
+ if (sessionSnapshot.phase !== "ready" || workspaceSnapshot.phase !== "ready") return;
667
+ const valid = new Set(sessionSnapshot.ids);
668
+ const archived = new Set(workspaceSnapshot.archivedSessionIds);
669
+ store.prune((sessionId) => valid.has(sessionId) && !archived.has(sessionId));
670
+ }, [sessionSnapshot, store, workspaceSnapshot]);
671
+ const rows = (0, import_react3.useMemo)(() => {
672
+ const workspaceBySession = /* @__PURE__ */ new Map();
673
+ for (const workspace of workspaceSnapshot.items) {
674
+ for (const sessionId of workspace.sessionIds) {
675
+ if (!workspaceBySession.has(sessionId)) workspaceBySession.set(sessionId, workspace.title);
676
+ }
677
+ }
678
+ return pins.flatMap((pin) => {
679
+ const session = sessionSnapshot.byId[pin.id];
680
+ if (session === void 0 || session.blank === true) return [];
681
+ const workspaceTitle = workspaceBySession.get(pin.id);
682
+ const workspace = workspaceTitle ?? t("workspace.ungrouped");
683
+ return [{ session, title: sessionDisplayTitle(session, workspaceTitle), workspace }];
684
+ });
685
+ }, [pins, sessionSnapshot.byId, t, workspaceSnapshot.items]);
686
+ (0, import_react3.useLayoutEffect)(() => {
687
+ const removedIndex = focusAfterUnpin.current;
688
+ if (removedIndex === null) return;
689
+ focusAfterUnpin.current = null;
690
+ focusAfterPinnedRemoval(document, sidebarHost, removedIndex);
691
+ }, [rows, sidebarHost]);
692
+ return sidebarHost !== null && sidebarHost.isConnected && sidebarWide && rows.length > 0 ? (0, import_react_dom.createPortal)(
693
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
694
+ PinnedSessionSection,
695
+ {
696
+ currentId: sessionSnapshot.current,
697
+ rows,
698
+ open: (sessionId) => {
699
+ sessions.open(sessionId);
700
+ },
701
+ unpin: (sessionId, index) => {
702
+ focusAfterUnpin.current = index;
703
+ store.toggle(sessionId);
704
+ },
705
+ t
706
+ }
707
+ ),
708
+ sidebarHost
709
+ ) : null;
710
+ }
711
+ function PinnedSessionSection({ currentId, rows, open, unpin, t }) {
712
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-pins-section", "aria-label": t("section.label"), children: [
713
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-pins-header", children: [
714
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PinIcon, { size: 13 }),
715
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("section.label") })
716
+ ] }),
717
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "dsh-pins-list", children: rows.map(({ session, title, workspace }, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
718
+ "li",
719
+ {
720
+ className: "dsh-pins-row",
721
+ "data-current": session.id === currentId ? "true" : "false",
722
+ children: [
723
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
724
+ "button",
725
+ {
726
+ type: "button",
727
+ className: "dsh-pins-open",
728
+ "aria-current": session.id === currentId ? "page" : void 0,
729
+ "aria-label": t("session.open", { name: title }),
730
+ onClick: () => {
731
+ open(session.id);
732
+ },
733
+ children: [
734
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PinIcon, { size: 13 }),
735
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-pins-title", children: title }),
736
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-pins-workspace", children: workspace })
737
+ ]
738
+ }
739
+ ),
740
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
741
+ "button",
742
+ {
743
+ type: "button",
744
+ className: "dsh-pins-remove",
745
+ "aria-label": t("session.unpin", { name: title }),
746
+ title: t("session.unpin", { name: title }),
747
+ onClick: () => {
748
+ unpin(session.id, index);
749
+ },
750
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {})
751
+ }
752
+ )
753
+ ]
754
+ },
755
+ session.id
756
+ )) })
757
+ ] });
758
+ }
759
+ function PinIcon({ size = 16 }) {
760
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pin, { size, strokeWidth: 1.8, "aria-hidden": "true" });
761
+ }
762
+ function CloseIcon() {
763
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(X, { size: 14, strokeWidth: 2, "aria-hidden": "true" });
764
+ }
765
+ /*! Bundled license information:
766
+
767
+ lucide-react/dist/esm/shared/src/utils.js:
768
+ lucide-react/dist/esm/defaultAttributes.js:
769
+ lucide-react/dist/esm/Icon.js:
770
+ lucide-react/dist/esm/createLucideIcon.js:
771
+ lucide-react/dist/esm/icons/pin.js:
772
+ lucide-react/dist/esm/icons/x.js:
773
+ lucide-react/dist/esm/lucide-react.js:
774
+ (**
775
+ * @license lucide-react v0.468.0 - ISC
776
+ *
777
+ * This source code is licensed under the ISC license.
778
+ * See the LICENSE file in the root directory of this source tree.
779
+ *)
780
+
781
+ lucide/dist/esm/defaultAttributes.js:
782
+ lucide/dist/esm/icons/pin-off.js:
783
+ lucide/dist/esm/icons/pin.js:
784
+ lucide/dist/esm/lucide.js:
785
+ (**
786
+ * @license lucide v0.468.0 - ISC
787
+ *
788
+ * This source code is licensed under the ISC license.
789
+ * See the LICENSE file in the root directory of this source tree.
790
+ *)
791
+ */
792
+
793
+ return module.exports; } });
794
+ //# sourceMappingURL=client.js.map