@devaccel-labs/comment-service 0.5.3 → 0.7.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/dist/react.js CHANGED
@@ -206,11 +206,11 @@ function overlayFor(element) {
206
206
  return { role, name: labelFor(node) || "dialog" };
207
207
  }
208
208
  function fiberNames(element) {
209
- const key = Object.keys(element).find((name) => name.startsWith("__reactFiber$") || name.startsWith("__reactInternalInstance$"));
210
- if (!key)
209
+ const key2 = Object.keys(element).find((name) => name.startsWith("__reactFiber$") || name.startsWith("__reactInternalInstance$"));
210
+ if (!key2)
211
211
  return [];
212
212
  const names2 = [];
213
- let fiber = element[key];
213
+ let fiber = element[key2];
214
214
  for (let guard = 0; fiber && guard < 40; guard++) {
215
215
  const type = fiber.type;
216
216
  if (typeof type === "function") {
@@ -267,7 +267,7 @@ function ownership(element) {
267
267
  owner: null
268
268
  };
269
269
  }
270
- function candidateFromElement(element, route, key) {
270
+ function candidateFromElement(element, route, key2) {
271
271
  const owned = ownership(element);
272
272
  const label = labelFor(element);
273
273
  const explicit = EXPLICIT.flatMap((attr) => {
@@ -275,7 +275,7 @@ function candidateFromElement(element, route, key) {
275
275
  return value && usableExplicit(attr, value) ? [{ attr, value }] : [];
276
276
  });
277
277
  return {
278
- key,
278
+ key: key2,
279
279
  route,
280
280
  visible: !obviouslyHidden(element),
281
281
  path: domPath(element),
@@ -354,8 +354,8 @@ function describeElement(element) {
354
354
  }
355
355
 
356
356
  // ../react/dist/toolbar.js
357
- import { jsx as _jsx4, jsxs as _jsxs4, Fragment as _Fragment2 } from "react/jsx-runtime";
358
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
357
+ import { jsx as _jsx6, jsxs as _jsxs6, Fragment as _Fragment3 } from "react/jsx-runtime";
358
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef3, useState as useState5 } from "react";
359
359
  import { createPortal as createPortal2 } from "react-dom";
360
360
 
361
361
  // ../react/dist/client.js
@@ -445,8 +445,14 @@ async function listMembers(session, memberToken) {
445
445
  return payload.members;
446
446
  }
447
447
  async function listThreads(session, memberToken) {
448
- const payload = await request(`${session.apiUrl}/v1/threads`, memberToken);
449
- return payload.threads;
448
+ return request(`${session.apiUrl}/v1/threads`, memberToken);
449
+ }
450
+ async function saveSettings(session, memberToken, settings) {
451
+ const payload = await request(`${session.apiUrl}/v1/settings`, memberToken, {
452
+ method: "PUT",
453
+ body: JSON.stringify({ settings })
454
+ });
455
+ return payload.settings;
450
456
  }
451
457
  async function createThread(session, memberToken, anchor, body, extras = {}) {
452
458
  return request(`${session.apiUrl}/v1/threads`, memberToken, {
@@ -467,6 +473,120 @@ async function updateThread(session, memberToken, threadId, patch) {
467
473
  });
468
474
  }
469
475
 
476
+ // ../react/dist/details.js
477
+ import { jsx as _jsx2, jsxs as _jsxs2, Fragment as _Fragment } from "react/jsx-runtime";
478
+ import { useState } from "react";
479
+
480
+ // ../react/dist/icons.js
481
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
482
+ function Icon({ children, size = 16 }) {
483
+ return _jsx("svg", { className: "cs-icon", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children });
484
+ }
485
+ var AddComment = () => _jsxs(Icon, { size: 17, children: [_jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }), _jsx("path", { d: "M12 7v6M9 10h6" })] });
486
+ var Inbox = () => _jsxs(Icon, { size: 17, children: [_jsx("polyline", { points: "22 12 16 12 14 15 10 15 8 12 2 12" }), _jsx("path", { d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" })] });
487
+ var Eye = () => _jsxs(Icon, { size: 17, children: [_jsx("path", { d: "M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" }), _jsx("circle", { cx: "12", cy: "12", r: "3" })] });
488
+ var EyeOff = () => _jsxs(Icon, { size: 17, children: [_jsx("path", { d: "M9.88 9.88a3 3 0 1 0 4.24 4.24" }), _jsx("path", { d: "M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" }), _jsx("path", { d: "M6.61 6.61A13.53 13.53 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" }), _jsx("path", { d: "M2 2l20 20" })] });
489
+ var Lock = ({ size = 15 }) => _jsxs(Icon, { size, children: [_jsx("rect", { width: "16", height: "10", x: "4", y: "11", rx: "2" }), _jsx("path", { d: "M8 11V7a4 4 0 0 1 8 0v4" })] });
490
+ var Close = () => _jsx(Icon, { size: 15, children: _jsx("path", { d: "M18 6 6 18M6 6l12 12" }) });
491
+ var Send = () => _jsx(Icon, { size: 15, children: _jsx("path", { d: "m5 12 7-7 7 7M12 19V5" }) });
492
+ var Check = () => _jsx(Icon, { size: 14, children: _jsx("polyline", { points: "20 6 9 17 4 12" }) });
493
+ var Target = () => _jsxs(Icon, { size: 14, children: [_jsx("circle", { cx: "12", cy: "12", r: "8" }), _jsx("circle", { cx: "12", cy: "12", r: "2.5" })] });
494
+ var Relink = () => _jsx(Icon, { size: 14, children: _jsx("path", { d: "M9 17H7A5 5 0 0 1 7 7h2M15 7h2a5 5 0 1 1 0 10h-2M8 12h8" }) });
495
+ var Undo = () => _jsxs(Icon, { size: 14, children: [_jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }), _jsx("path", { d: "M3 3v5h5" })] });
496
+ var Pencil = () => _jsx(Icon, { size: 13, children: _jsx("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }) });
497
+ var Cursor = () => _jsx(Icon, { size: 14, children: _jsx("path", { d: "m4 4 7.07 17 2.51-7.39L21 11.07z" }) });
498
+ var Paperclip = () => _jsx(Icon, { size: 15, children: _jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" }) });
499
+ var Camera = () => _jsxs(Icon, { size: 15, children: [_jsx("path", { d: "M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" }), _jsx("circle", { cx: "12", cy: "13", r: "3" })] });
500
+ var FileIcon = () => _jsxs(Icon, { size: 15, children: [_jsx("path", { d: "M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" }), _jsx("polyline", { points: "14 2 14 8 20 8" })] });
501
+ var Back = () => _jsx(Icon, { children: _jsx("path", { d: "m15 18-6-6 6-6" }) });
502
+ var Table = () => _jsxs(Icon, { children: [_jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }), _jsx("path", { d: "M3 10h18M3 15h18M9 10v10" })] });
503
+ var List = () => _jsx(Icon, { children: _jsx("path", { d: "M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01" }) });
504
+
505
+ // ../react/dist/details.js
506
+ var noDetails = { role: null, fields: {} };
507
+ function detailsEnabled(settings) {
508
+ if (!settings)
509
+ return false;
510
+ return settings.roles.enabled && settings.roles.options.length > 0 || settings.fields.enabled && settings.fields.items.length > 0;
511
+ }
512
+ function hasFieldValues(details) {
513
+ return Object.values(details.fields).some((value) => value.trim());
514
+ }
515
+ function detailsPayload(settings, details) {
516
+ if (!settings)
517
+ return {};
518
+ const payload = {};
519
+ if (settings.roles.enabled && details.role)
520
+ payload.role = details.role;
521
+ if (settings.fields.enabled) {
522
+ const fields = Object.fromEntries(Object.entries(details.fields).filter(([, value]) => value.trim()));
523
+ if (Object.keys(fields).length)
524
+ payload.fields = fields;
525
+ }
526
+ return payload;
527
+ }
528
+ function fieldId(label, taken) {
529
+ const base = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "field";
530
+ let id = base;
531
+ for (let n = 2; taken.some((field) => field.id === id); n += 1)
532
+ id = `${base}-${n}`;
533
+ return id;
534
+ }
535
+ function DetailsInput({ settings, value, onChange, onKeyDown }) {
536
+ if (!settings || !detailsEnabled(settings))
537
+ return null;
538
+ const roles = settings.roles.enabled ? settings.roles.options : [];
539
+ const fields = settings.fields.enabled ? settings.fields.items : [];
540
+ return _jsxs2("div", { className: "cs-details-input", "data-cs-ui": "details-input", children: [fields.map((field) => _jsxs2("label", { className: "cs-detail-field", children: [_jsx2("span", { children: field.label }), _jsx2("textarea", { "data-cs-ui": `field-${field.id}`, rows: 1, maxLength: 4e3, value: value.fields[field.id] ?? "", onChange: (event) => onChange({ ...value, fields: { ...value.fields, [field.id]: event.target.value } }), onKeyDown })] }, field.id)), roles.length ? _jsxs2("div", { className: "cs-roles", role: "radiogroup", "aria-label": "Role", children: [_jsx2("span", { className: "cs-roles-label", children: "Role" }), roles.map((role) => _jsx2("button", { type: "button", role: "radio", "data-cs-ui": "role-option", "aria-checked": value.role === role, className: value.role === role ? "cs-chip is-on" : "cs-chip", onClick: () => onChange({ ...value, role: value.role === role ? null : role }), children: role }, role))] }) : null] });
541
+ }
542
+ function RoleBadge({ role }) {
543
+ return role ? _jsx2("span", { className: "cs-role", "data-cs-ui": "role", children: role }) : null;
544
+ }
545
+ function FieldValues({ fields, clamp = false }) {
546
+ if (!fields.length)
547
+ return null;
548
+ return _jsx2("dl", { className: clamp ? "cs-detail-list is-clamped" : "cs-detail-list", "data-cs-ui": "fields", children: fields.map((field) => _jsxs2("div", { children: [_jsx2("dt", { children: field.label }), _jsx2("dd", { children: field.value })] }, field.id)) });
549
+ }
550
+ function DetailsEditor({ settings, thread, onSave, onClose }) {
551
+ const [value, setValue] = useState(() => ({
552
+ role: thread.role,
553
+ fields: Object.fromEntries(thread.fields.map((field) => [field.id, field.value]))
554
+ }));
555
+ const [busy, setBusy] = useState(false);
556
+ return _jsxs2("form", { className: "cs-details-edit", "data-cs-ui": "details-editor", onSubmit: (event) => {
557
+ event.preventDefault();
558
+ const patch = {};
559
+ if (settings.roles.enabled && value.role !== thread.role)
560
+ patch.role = value.role;
561
+ if (settings.fields.enabled) {
562
+ patch.fields = Object.fromEntries(settings.fields.items.map((field) => [field.id, value.fields[field.id] ?? ""]));
563
+ }
564
+ setBusy(true);
565
+ onSave(patch).then(onClose).finally(() => setBusy(false));
566
+ }, children: [_jsx2(DetailsInput, { settings, value, onChange: setValue }), _jsxs2("div", { className: "cs-row-end", children: [_jsx2("button", { className: "cs-button", type: "button", onClick: onClose, children: "Cancel" }), _jsx2("button", { className: "cs-button is-primary", "data-cs-ui": "details-save", type: "submit", disabled: busy, children: "Save" })] })] });
567
+ }
568
+ function AddRow({ label, hook, onAdd }) {
569
+ const [text, setText] = useState("");
570
+ return _jsxs2("form", { className: "cs-add-row", onSubmit: (event) => {
571
+ event.preventDefault();
572
+ const next = text.trim().replace(/\s+/g, " ");
573
+ if (!next)
574
+ return;
575
+ onAdd(next);
576
+ setText("");
577
+ }, children: [_jsx2("input", { "data-cs-ui": hook, "aria-label": label, placeholder: label, value: text, maxLength: 60, onChange: (event) => setText(event.target.value) }), _jsx2("button", { className: "cs-icon-button", type: "submit", "aria-label": label, disabled: !text.trim(), children: _jsx2(Check, {}) })] });
578
+ }
579
+ function SettingsPanel({ settings, onChange, onBack, error }) {
580
+ return _jsxs2("section", { className: "cs-card cs-menu cs-settings cs-hit", "data-cs-ui": "settings", "aria-label": "Comment fields", children: [_jsxs2("header", { className: "cs-settings-head", children: [_jsx2("button", { className: "cs-icon-button", type: "button", "aria-label": "Back", onClick: onBack, children: _jsx2(Back, {}) }), _jsx2("h2", { children: "Comment fields" })] }), !settings ? _jsx2("p", { className: "cs-hint", children: "Loading\u2026" }) : _jsxs2(_Fragment, { children: [_jsxs2("button", { className: "cs-item", "data-cs-ui": "roles-toggle", type: "button", role: "switch", "aria-checked": settings.roles.enabled, onClick: () => onChange({ ...settings, roles: { ...settings.roles, enabled: !settings.roles.enabled } }), children: [_jsx2("span", { children: "Role" }), _jsx2("span", { className: settings.roles.enabled ? "cs-switch is-on" : "cs-switch" })] }), settings.roles.enabled ? _jsxs2("div", { className: "cs-settings-body", children: [settings.roles.options.length ? _jsx2("div", { className: "cs-chips", children: settings.roles.options.map((role) => _jsxs2("span", { className: "cs-chip is-static", "data-cs-ui": "role-chip", children: [role, _jsx2("button", { type: "button", "aria-label": `Remove ${role}`, onClick: () => onChange({ ...settings, roles: { ...settings.roles, options: settings.roles.options.filter((item) => item !== role) } }), children: _jsx2(Close, {}) })] }, role)) }) : null, _jsx2(AddRow, { label: "Add a role", hook: "role-add", onAdd: (role) => {
581
+ if (settings.roles.options.some((item) => item.toLowerCase() === role.toLowerCase()))
582
+ return;
583
+ onChange({ ...settings, roles: { ...settings.roles, options: [...settings.roles.options, role.slice(0, 40)] } });
584
+ } })] }) : null, _jsx2("div", { className: "cs-sep" }), _jsxs2("button", { className: "cs-item", "data-cs-ui": "fields-toggle", type: "button", role: "switch", "aria-checked": settings.fields.enabled, onClick: () => onChange({ ...settings, fields: { ...settings.fields, enabled: !settings.fields.enabled } }), children: [_jsx2("span", { children: "Custom fields" }), _jsx2("span", { className: settings.fields.enabled ? "cs-switch is-on" : "cs-switch" })] }), settings.fields.enabled ? _jsxs2("div", { className: "cs-settings-body", children: [settings.fields.items.map((field) => _jsxs2("div", { className: "cs-field-row", "data-cs-ui": "field-def", children: [_jsx2("span", { children: field.label }), _jsx2("button", { className: "cs-icon-button", type: "button", "aria-label": `Remove ${field.label}`, onClick: () => onChange({ ...settings, fields: { ...settings.fields, items: settings.fields.items.filter((item) => item.id !== field.id) } }), children: _jsx2(Close, {}) })] }, field.id)), _jsx2(AddRow, { label: "Add a field", hook: "field-add", onAdd: (label) => onChange({
585
+ ...settings,
586
+ fields: { ...settings.fields, items: [...settings.fields.items, { id: fieldId(label, settings.fields.items), label }] }
587
+ }) })] }) : null, error ? _jsx2("p", { className: "cs-error cs-pad", children: error }) : null, _jsx2("p", { className: "cs-hint", children: "Shared with everyone here. Turning off or removing only affects new comments." })] })] });
588
+ }
589
+
470
590
  // ../react/dist/format.js
471
591
  var palette = ["#007aff", "#34c759", "#ff9500", "#af52de", "#ff2d55", "#5ac8fa", "#5856d6", "#a2845e"];
472
592
  function initials(name) {
@@ -537,31 +657,9 @@ function deliveryText(delivery) {
537
657
  return parts.length ? parts.join(" \xB7 ") : null;
538
658
  }
539
659
 
540
- // ../react/dist/icons.js
541
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
542
- function Icon({ children, size = 16 }) {
543
- return _jsx("svg", { className: "cs-icon", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children });
544
- }
545
- var AddComment = () => _jsxs(Icon, { size: 17, children: [_jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }), _jsx("path", { d: "M12 7v6M9 10h6" })] });
546
- var Inbox = () => _jsxs(Icon, { size: 17, children: [_jsx("polyline", { points: "22 12 16 12 14 15 10 15 8 12 2 12" }), _jsx("path", { d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" })] });
547
- var Eye = () => _jsxs(Icon, { size: 17, children: [_jsx("path", { d: "M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" }), _jsx("circle", { cx: "12", cy: "12", r: "3" })] });
548
- var EyeOff = () => _jsxs(Icon, { size: 17, children: [_jsx("path", { d: "M9.88 9.88a3 3 0 1 0 4.24 4.24" }), _jsx("path", { d: "M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" }), _jsx("path", { d: "M6.61 6.61A13.53 13.53 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" }), _jsx("path", { d: "M2 2l20 20" })] });
549
- var Lock = ({ size = 15 }) => _jsxs(Icon, { size, children: [_jsx("rect", { width: "16", height: "10", x: "4", y: "11", rx: "2" }), _jsx("path", { d: "M8 11V7a4 4 0 0 1 8 0v4" })] });
550
- var Close = () => _jsx(Icon, { size: 15, children: _jsx("path", { d: "M18 6 6 18M6 6l12 12" }) });
551
- var Send = () => _jsx(Icon, { size: 15, children: _jsx("path", { d: "m5 12 7-7 7 7M12 19V5" }) });
552
- var Check = () => _jsx(Icon, { size: 14, children: _jsx("polyline", { points: "20 6 9 17 4 12" }) });
553
- var Target = () => _jsxs(Icon, { size: 14, children: [_jsx("circle", { cx: "12", cy: "12", r: "8" }), _jsx("circle", { cx: "12", cy: "12", r: "2.5" })] });
554
- var Relink = () => _jsx(Icon, { size: 14, children: _jsx("path", { d: "M9 17H7A5 5 0 0 1 7 7h2M15 7h2a5 5 0 1 1 0 10h-2M8 12h8" }) });
555
- var Undo = () => _jsxs(Icon, { size: 14, children: [_jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }), _jsx("path", { d: "M3 3v5h5" })] });
556
- var Pencil = () => _jsx(Icon, { size: 13, children: _jsx("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }) });
557
- var Cursor = () => _jsx(Icon, { size: 14, children: _jsx("path", { d: "m4 4 7.07 17 2.51-7.39L21 11.07z" }) });
558
- var Paperclip = () => _jsx(Icon, { size: 15, children: _jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" }) });
559
- var Camera = () => _jsxs(Icon, { size: 15, children: [_jsx("path", { d: "M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" }), _jsx("circle", { cx: "12", cy: "13", r: "3" })] });
560
- var FileIcon = () => _jsxs(Icon, { size: 15, children: [_jsx("path", { d: "M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" }), _jsx("polyline", { points: "14 2 14 8 20 8" })] });
561
-
562
660
  // ../react/dist/attachments.js
563
- import { jsx as _jsx2, jsxs as _jsxs2 } from "react/jsx-runtime";
564
- import { useCallback, useEffect, useRef, useState } from "react";
661
+ import { jsx as _jsx3, jsxs as _jsxs3 } from "react/jsx-runtime";
662
+ import { useCallback, useEffect, useRef, useState as useState2 } from "react";
565
663
  var MAX_BYTES = 10 * 1024 * 1024;
566
664
  var MAX_FILES = 5;
567
665
  var ACCEPT = "image/png,image/jpeg,image/gif,image/webp,application/pdf,text/plain,text/csv,application/json,application/zip,video/mp4,video/webm,video/quicktime";
@@ -574,10 +672,10 @@ function formatSize(bytes) {
574
672
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
575
673
  }
576
674
  function useAttachments(session, memberToken, onError) {
577
- const [items, setItems] = useState([]);
675
+ const [items, setItems] = useState2([]);
578
676
  const itemsRef = useRef(items);
579
677
  itemsRef.current = items;
580
- const patch = (key, next) => setItems((current) => current.map((item) => item.key === key ? { ...item, ...next } : item));
678
+ const patch = (key2, next) => setItems((current) => current.map((item) => item.key === key2 ? { ...item, ...next } : item));
581
679
  const add = useCallback((files) => {
582
680
  if (!session || !memberToken)
583
681
  return;
@@ -593,22 +691,22 @@ function useAttachments(session, memberToken, onError) {
593
691
  onError(`${name} is over 10 MB.`);
594
692
  continue;
595
693
  }
596
- const key = crypto.randomUUID();
694
+ const key2 = crypto.randomUUID();
597
695
  const preview = blob.type.startsWith("image/") ? URL.createObjectURL(blob) : null;
598
- setItems((current) => [...current, { key, name, size: blob.size, contentType: blob.type, preview, status: "uploading" }]);
599
- uploadAttachment(session, memberToken, blob, name).then((attachment) => patch(key, { status: "ready", id: attachment.id })).catch((caught) => {
696
+ setItems((current) => [...current, { key: key2, name, size: blob.size, contentType: blob.type, preview, status: "uploading" }]);
697
+ uploadAttachment(session, memberToken, blob, name).then((attachment) => patch(key2, { status: "ready", id: attachment.id })).catch((caught) => {
600
698
  const message = caught instanceof Error ? caught.message : "Upload failed.";
601
- patch(key, { status: "failed", error: message });
699
+ patch(key2, { status: "failed", error: message });
602
700
  onError(`${name}: ${message}`);
603
701
  });
604
702
  }
605
703
  }, [memberToken, onError, session]);
606
- const remove = useCallback((key) => {
704
+ const remove = useCallback((key2) => {
607
705
  setItems((current) => {
608
- const item = current.find((candidate) => candidate.key === key);
706
+ const item = current.find((candidate) => candidate.key === key2);
609
707
  if (item?.preview)
610
708
  URL.revokeObjectURL(item.preview);
611
- return current.filter((candidate) => candidate.key !== key);
709
+ return current.filter((candidate) => candidate.key !== key2);
612
710
  });
613
711
  }, []);
614
712
  const reset = useCallback(() => {
@@ -633,26 +731,26 @@ function filesFromClipboard(event) {
633
731
  function AttachmentTray({ items, onRemove }) {
634
732
  if (!items.length)
635
733
  return null;
636
- return _jsx2("div", { className: "cs-tray", "data-cs-ui": "attachment-tray", children: items.map((item) => _jsxs2("div", { className: `cs-pending is-${item.status}`, "data-cs-ui": "pending-file", "data-status": item.status, title: item.error ?? item.name, children: [item.preview ? _jsx2("img", { src: item.preview, alt: "" }) : _jsx2(FileIcon, {}), _jsx2("span", { className: "cs-pending-name", children: item.name }), _jsx2("button", { type: "button", className: "cs-pending-remove", "aria-label": `Remove ${item.name}`, onClick: () => onRemove(item.key), children: _jsx2(Close, {}) })] }, item.key)) });
734
+ return _jsx3("div", { className: "cs-tray", "data-cs-ui": "attachment-tray", children: items.map((item) => _jsxs3("div", { className: `cs-pending is-${item.status}`, "data-cs-ui": "pending-file", "data-status": item.status, title: item.error ?? item.name, children: [item.preview ? _jsx3("img", { src: item.preview, alt: "" }) : _jsx3(FileIcon, {}), _jsx3("span", { className: "cs-pending-name", children: item.name }), _jsx3("button", { type: "button", className: "cs-pending-remove", "aria-label": `Remove ${item.name}`, onClick: () => onRemove(item.key), children: _jsx3(Close, {}) })] }, item.key)) });
637
735
  }
638
736
  function AttachButtons({ onFiles, onSnapshot, capturing }) {
639
737
  const input = useRef(null);
640
- return _jsxs2("span", { className: "cs-attach", children: [_jsx2("input", { ref: input, type: "file", multiple: true, accept: ACCEPT, hidden: true, "data-cs-ui": "file-input", onChange: (event) => {
738
+ return _jsxs3("span", { className: "cs-attach", children: [_jsx3("input", { ref: input, type: "file", multiple: true, accept: ACCEPT, hidden: true, "data-cs-ui": "file-input", onChange: (event) => {
641
739
  onFiles([...event.target.files ?? []].map((file) => ({ blob: file, name: file.name })));
642
740
  event.target.value = "";
643
- } }), _jsx2("button", { type: "button", className: "cs-icon-button", "data-cs-ui": "attach", "aria-label": "Attach files", onClick: () => input.current?.click(), children: _jsx2(Paperclip, {}) }), _jsx2("button", { type: "button", className: "cs-icon-button", "data-cs-ui": "snapshot", "aria-label": "Attach a screenshot of the page", disabled: capturing, onClick: onSnapshot, children: _jsx2(Camera, {}) })] });
741
+ } }), _jsx3("button", { type: "button", className: "cs-icon-button", "data-cs-ui": "attach", "aria-label": "Attach files", onClick: () => input.current?.click(), children: _jsx3(Paperclip, {}) }), _jsx3("button", { type: "button", className: "cs-icon-button", "data-cs-ui": "snapshot", "aria-label": "Attach a screenshot of the page", disabled: capturing, onClick: onSnapshot, children: _jsx3(Camera, {}) })] });
644
742
  }
645
743
  function MessageAttachments({ attachments }) {
646
744
  if (!attachments?.length)
647
745
  return null;
648
746
  const images = attachments.filter((file) => file.contentType.startsWith("image/"));
649
747
  const others = attachments.filter((file) => !file.contentType.startsWith("image/"));
650
- return _jsxs2("div", { className: "cs-files", "data-cs-ui": "attachments", children: [images.length ? _jsx2("div", { className: "cs-thumbs", children: images.map((file) => _jsx2("a", { href: file.url, target: "_blank", rel: "noreferrer noopener", className: "cs-thumb", title: file.name, "data-cs-ui": "attachment", children: _jsx2("img", { src: file.url, alt: file.name, loading: "lazy" }) }, file.id)) }) : null, others.map((file) => _jsxs2("a", { href: file.url, target: "_blank", rel: "noreferrer noopener", className: "cs-file", "data-cs-ui": "attachment", children: [_jsx2(FileIcon, {}), _jsx2("span", { className: "cs-file-name", children: file.name }), _jsx2("span", { className: "cs-file-size", children: formatSize(file.size) })] }, file.id))] });
748
+ return _jsxs3("div", { className: "cs-files", "data-cs-ui": "attachments", children: [images.length ? _jsx3("div", { className: "cs-thumbs", children: images.map((file) => _jsx3("a", { href: file.url, target: "_blank", rel: "noreferrer noopener", className: "cs-thumb", title: file.name, "data-cs-ui": "attachment", children: _jsx3("img", { src: file.url, alt: file.name, loading: "lazy" }) }, file.id)) }) : null, others.map((file) => _jsxs3("a", { href: file.url, target: "_blank", rel: "noreferrer noopener", className: "cs-file", "data-cs-ui": "attachment", children: [_jsx3(FileIcon, {}), _jsx3("span", { className: "cs-file-name", children: file.name }), _jsx3("span", { className: "cs-file-size", children: formatSize(file.size) })] }, file.id))] });
651
749
  }
652
750
 
653
751
  // ../react/dist/mentions.js
654
- import { Fragment as _Fragment, jsx as _jsx3, jsxs as _jsxs3 } from "react/jsx-runtime";
655
- import { useMemo, useRef as useRef2, useState as useState2 } from "react";
752
+ import { Fragment as _Fragment2, jsx as _jsx4, jsxs as _jsxs4 } from "react/jsx-runtime";
753
+ import { useMemo, useRef as useRef2, useState as useState3 } from "react";
656
754
  import { createPortal } from "react-dom";
657
755
  function queryAt(text, caret) {
658
756
  const match = text.slice(0, caret).match(/(^|\s)@([^\s@]{0,30})$/);
@@ -666,16 +764,16 @@ function mentionsIn(text, picked) {
666
764
  }
667
765
  function MessageText({ body, mentions }) {
668
766
  if (!mentions?.length)
669
- return _jsx3(_Fragment, { children: body });
767
+ return _jsx4(_Fragment2, { children: body });
670
768
  const pattern = new RegExp(`(@(?:${mentions.map((mention) => escapeRegex(mention.name)).join("|")}))`, "g");
671
- const parts = body.split(pattern).map((part, index) => index % 2 === 1 ? _jsx3("span", { className: "cs-mention", children: part }, index) : part);
672
- return _jsx3(_Fragment, { children: parts });
769
+ const parts = body.split(pattern).map((part, index) => index % 2 === 1 ? _jsx4("span", { className: "cs-mention", children: part }, index) : part);
770
+ return _jsx4(_Fragment2, { children: parts });
673
771
  }
674
772
  function MentionTextarea({ value, onValueChange, members, selfEmail, onMention, onKeyDown, ...rest }) {
675
773
  const ref = useRef2(null);
676
- const [caret, setCaret] = useState2(0);
677
- const [index, setIndex] = useState2(0);
678
- const [dismissed, setDismissed] = useState2(null);
774
+ const [caret, setCaret] = useState3(0);
775
+ const [index, setIndex] = useState3(0);
776
+ const [dismissed, setDismissed] = useState3(null);
679
777
  const active = queryAt(value, caret);
680
778
  const query = active?.query.toLowerCase() ?? null;
681
779
  const matches = useMemo(() => {
@@ -702,7 +800,7 @@ function MentionTextarea({ value, onValueChange, members, selfEmail, onMention,
702
800
  const rect = open ? ref.current?.getBoundingClientRect() : null;
703
801
  const root = open ? ref.current?.closest("#cs-root") : null;
704
802
  const below = rect ? window.innerHeight - rect.bottom > 220 : true;
705
- return _jsxs3(_Fragment, { children: [_jsx3("textarea", { ref, ...rest, value, "aria-autocomplete": "list", "aria-expanded": open, onChange: (event) => {
803
+ return _jsxs4(_Fragment2, { children: [_jsx4("textarea", { ref, ...rest, value, "aria-autocomplete": "list", "aria-expanded": open, onChange: (event) => {
706
804
  onValueChange(event.target.value);
707
805
  setCaret(event.target.selectionStart ?? event.target.value.length);
708
806
  setIndex(0);
@@ -728,13 +826,13 @@ function MentionTextarea({ value, onValueChange, members, selfEmail, onMention,
728
826
  }
729
827
  }
730
828
  onKeyDown?.(event);
731
- } }), open && rect && root ? createPortal(_jsx3("div", { className: "cs-people cs-hit", role: "listbox", "data-cs-ui": "mention-list", style: {
829
+ } }), open && rect && root ? createPortal(_jsx4("div", { className: "cs-people cs-hit", role: "listbox", "data-cs-ui": "mention-list", style: {
732
830
  left: Math.min(rect.left, window.innerWidth - 272),
733
831
  ...below ? { top: rect.bottom + 6 } : { bottom: window.innerHeight - rect.top + 6 }
734
- }, children: matches.map((member) => _jsxs3("div", { role: "option", "aria-selected": member === selected, "data-cs-ui": "mention-option", className: member === selected ? "cs-person is-on" : "cs-person", onMouseDown: (event) => {
832
+ }, children: matches.map((member) => _jsxs4("div", { role: "option", "aria-selected": member === selected, "data-cs-ui": "mention-option", className: member === selected ? "cs-person is-on" : "cs-person", onMouseDown: (event) => {
735
833
  event.preventDefault();
736
834
  insert(member);
737
- }, onMouseEnter: () => setIndex(matches.indexOf(member)), children: [_jsx3("span", { className: "cs-avatar", style: { width: 24, height: 24, fontSize: 10, background: colorFor(member.name) }, children: initials(member.name) }), _jsxs3("span", { className: "cs-person-text", children: [_jsx3("strong", { children: member.name }), _jsx3("small", { children: member.email })] })] }, member.id)) }), root) : null] });
835
+ }, onMouseEnter: () => setIndex(matches.indexOf(member)), children: [_jsx4("span", { className: "cs-avatar", style: { width: 24, height: 24, fontSize: 10, background: colorFor(member.name) }, children: initials(member.name) }), _jsxs4("span", { className: "cs-person-text", children: [_jsx4("strong", { children: member.name }), _jsx4("small", { children: member.email })] })] }, member.id)) }), root) : null] });
738
836
  }
739
837
 
740
838
  // ../../node_modules/modern-screenshot/dist/index.mjs
@@ -1050,14 +1148,14 @@ async function createContext(node, options) {
1050
1148
  drawImageCount: 0,
1051
1149
  tasks: [],
1052
1150
  features,
1053
- isEnable: (key) => {
1054
- if (key === "restoreScrollPosition") {
1055
- return typeof features === "boolean" ? false : features[key] ?? false;
1151
+ isEnable: (key2) => {
1152
+ if (key2 === "restoreScrollPosition") {
1153
+ return typeof features === "boolean" ? false : features[key2] ?? false;
1056
1154
  }
1057
1155
  if (typeof features === "boolean") {
1058
1156
  return features;
1059
1157
  }
1060
- return features[key] ?? true;
1158
+ return features[key2] ?? true;
1061
1159
  },
1062
1160
  shadowRoots: []
1063
1161
  };
@@ -1298,14 +1396,14 @@ function getDefaultStyle(node, pseudoElement, context) {
1298
1396
  const nodeName = node.nodeName.toLowerCase();
1299
1397
  const isSvgNode = isSVGElementNode(node) && nodeName !== "svg";
1300
1398
  const attributes = isSvgNode ? includedAttributes.map((name) => [name, node.getAttribute(name)]).filter(([, value]) => value !== null) : [];
1301
- const key = [
1399
+ const key2 = [
1302
1400
  isSvgNode && "svg",
1303
1401
  nodeName,
1304
1402
  attributes.map((name, value) => `${name}=${value}`).join(","),
1305
1403
  pseudoElement
1306
1404
  ].filter(Boolean).join(":");
1307
- if (defaultComputedStyles.has(key))
1308
- return defaultComputedStyles.get(key);
1405
+ if (defaultComputedStyles.has(key2))
1406
+ return defaultComputedStyles.get(key2);
1309
1407
  const sandbox = getSandBox(context);
1310
1408
  const sandboxWindow = sandbox?.contentWindow;
1311
1409
  if (!sandboxWindow)
@@ -1334,7 +1432,7 @@ function getDefaultStyle(node, pseudoElement, context) {
1334
1432
  styles.set(name, computedStyle.getPropertyValue(name));
1335
1433
  }
1336
1434
  sandboxDocument.body.removeChild(root);
1337
- defaultComputedStyles.set(key, styles);
1435
+ defaultComputedStyles.set(key2, styles);
1338
1436
  return styles;
1339
1437
  }
1340
1438
  function getDiffStyle(style, defaultStyle, includeStyleProperties) {
@@ -1382,8 +1480,8 @@ function copyCssStyles(node, cloned, isRoot, context) {
1382
1480
  const clonedStyle = cloned.style;
1383
1481
  const computedStyle = ownerWindow.getComputedStyle(node);
1384
1482
  const defaultStyle = getDefaultStyle(node, null, context);
1385
- currentParentNodeStyle?.forEach((_, key) => {
1386
- defaultStyle.delete(key);
1483
+ currentParentNodeStyle?.forEach((_, key2) => {
1484
+ defaultStyle.delete(key2);
1387
1485
  });
1388
1486
  const style = getDiffStyle(computedStyle, defaultStyle, includeStyleProperties);
1389
1487
  style.delete("transition-property");
@@ -1454,8 +1552,8 @@ function copyPseudoClass(node, cloned, copyScrollbar, context, addWordToFontFami
1454
1552
  content = content.replace(/(')|(")|(counter\(.+\))/g, "");
1455
1553
  const klasses = [uuid()];
1456
1554
  const defaultStyle = getDefaultStyle(node, pseudoClass, context);
1457
- currentNodeStyle?.forEach((_, key) => {
1458
- defaultStyle.delete(key);
1555
+ currentNodeStyle?.forEach((_, key2) => {
1556
+ defaultStyle.delete(key2);
1459
1557
  });
1460
1558
  const style = getDiffStyle(computedStyle, defaultStyle, context.includeStyleProperties);
1461
1559
  style.delete("content");
@@ -2244,6 +2342,145 @@ async function captureAround(element) {
2244
2342
  return out.toDataURL("image/jpeg", 0.82);
2245
2343
  }
2246
2344
 
2345
+ // ../react/dist/table.js
2346
+ import { jsx as _jsx5, jsxs as _jsxs5 } from "react/jsx-runtime";
2347
+ import { useMemo as useMemo2, useState as useState4 } from "react";
2348
+ var columns = [
2349
+ { key: "comment", label: "Comment" },
2350
+ { key: "role", label: "Role" },
2351
+ { key: "page", label: "Page" },
2352
+ { key: "author", label: "Author" },
2353
+ { key: "replies", label: "Replies" },
2354
+ { key: "status", label: "Status" },
2355
+ { key: "updated", label: "Updated" }
2356
+ ];
2357
+ function sortValue(thread, key2) {
2358
+ if (key2 === "comment")
2359
+ return thread.messages[0]?.body ?? "";
2360
+ if (key2 === "role")
2361
+ return thread.role ?? "";
2362
+ if (key2 === "page")
2363
+ return thread.url || thread.route;
2364
+ if (key2 === "author")
2365
+ return thread.createdBy;
2366
+ if (key2 === "replies")
2367
+ return thread.messages.length;
2368
+ if (key2 === "status")
2369
+ return thread.status;
2370
+ return thread.updatedAt;
2371
+ }
2372
+ function searchText(thread) {
2373
+ return [
2374
+ thread.createdBy,
2375
+ thread.role ?? "",
2376
+ thread.url || thread.route,
2377
+ targetName(thread.anchor),
2378
+ ...thread.messages.map((message) => message.body),
2379
+ ...thread.fields.map((field) => `${field.label} ${field.value}`)
2380
+ ].join("\n").toLowerCase();
2381
+ }
2382
+ function CommentTable({ rows, route, activeId, tabs, onSelect, onList, onClose }) {
2383
+ const [query, setQuery] = useState4("");
2384
+ const [sort, setSort] = useState4({ key: "updated", desc: true });
2385
+ const showRole = rows.some((row) => row.thread.role);
2386
+ const fieldLabels = useMemo2(() => {
2387
+ const labels = [];
2388
+ for (const { thread } of rows)
2389
+ for (const field of thread.fields)
2390
+ if (!labels.includes(field.label))
2391
+ labels.push(field.label);
2392
+ return labels;
2393
+ }, [rows]);
2394
+ const shown = useMemo2(() => {
2395
+ const needle = query.trim().toLowerCase();
2396
+ const matched = needle ? rows.filter((row) => searchText(row.thread).includes(needle)) : rows;
2397
+ return [...matched].sort((a, b) => {
2398
+ const left = sortValue(a.thread, sort.key);
2399
+ const right = sortValue(b.thread, sort.key);
2400
+ const order = typeof left === "number" && typeof right === "number" ? left - right : String(left).localeCompare(String(right));
2401
+ return sort.desc ? -order : order;
2402
+ });
2403
+ }, [query, rows, sort]);
2404
+ const visible = columns.filter((column) => column.key !== "role" || showRole);
2405
+ const header = (column) => _jsx5("th", { "aria-sort": sort.key === column.key ? sort.desc ? "descending" : "ascending" : "none", children: _jsxs5("button", { type: "button", onClick: () => setSort((current) => ({ key: column.key, desc: current.key === column.key ? !current.desc : column.key === "updated" })), children: [column.label, sort.key === column.key ? _jsx5("span", { className: "cs-sort", children: sort.desc ? "\u2193" : "\u2191" }) : null] }) }, column.key);
2406
+ return _jsxs5("section", { className: "cs-card cs-table-sheet cs-hit", "data-cs-ui": "panel", "aria-label": "Comments table", children: [_jsxs5("header", { className: "cs-sheet-head", children: [_jsx5("h2", { children: "Comments" }), _jsx5("input", { className: "cs-table-search", "data-cs-ui": "table-search", type: "search", "aria-label": "Search comments", placeholder: "Search", value: query, onChange: (event) => setQuery(event.target.value) }), _jsx5("button", { className: "cs-icon-button", "data-cs-ui": "list-view", type: "button", "aria-label": "List view", onClick: onList, children: _jsx5(List, {}) }), _jsx5("button", { className: "cs-icon-button", type: "button", "aria-label": "Close comments", onClick: onClose, children: _jsx5(Close, {}) })] }), tabs, _jsx5("div", { className: "cs-table-wrap", children: shown.length === 0 ? _jsx5("p", { className: "cs-empty", children: query ? "No comments match." : "No comments here." }) : _jsxs5("table", { className: "cs-table", "data-cs-ui": "table", children: [_jsx5("thead", { children: _jsxs5("tr", { children: [visible.slice(0, showRole ? 2 : 1).map(header), fieldLabels.map((label) => _jsx5("th", { children: _jsx5("span", { children: label }) }, `field:${label}`)), visible.slice(showRole ? 2 : 1).map(header)] }) }), _jsx5("tbody", { children: shown.map((row) => {
2407
+ const { thread } = row;
2408
+ const first = thread.messages[0];
2409
+ const note = statusNote(row.status, thread.anchor, thread.route === route);
2410
+ return _jsxs5("tr", { className: activeId === thread.id ? "is-active" : void 0, "data-cs-ui": "table-row", "data-thread-id": thread.id, tabIndex: 0, onClick: () => onSelect(row), onKeyDown: (event) => {
2411
+ if (event.key === "Enter") {
2412
+ event.preventDefault();
2413
+ onSelect(row);
2414
+ }
2415
+ }, children: [_jsxs5("td", { className: "cs-cell-comment", children: [_jsx5("span", { className: "cs-cell-clamp", children: first?.body || "\u2014" }), _jsx5("small", { children: targetName(thread.anchor) })] }), showRole ? _jsx5("td", { children: thread.role ? _jsx5("span", { className: "cs-role", children: thread.role }) : null }) : null, fieldLabels.map((label) => _jsx5("td", { children: _jsx5("span", { className: "cs-cell-clamp", children: thread.fields.find((field) => field.label === label)?.value ?? "" }) }, `field:${label}`)), _jsx5("td", { className: "cs-cell-page", title: thread.url || thread.route, children: thread.url || thread.route }), _jsx5("td", { className: "cs-cell-nowrap", children: thread.createdBy }), _jsx5("td", { className: "cs-cell-num", children: thread.messages.length - 1 || "" }), _jsxs5("td", { className: "cs-cell-nowrap", children: [_jsx5("span", { className: thread.status === "resolved" ? "cs-state is-done" : "cs-state", children: thread.status === "resolved" ? "Resolved" : "Open" }), note && thread.status === "open" ? _jsx5("small", { className: `cs-note is-${note.tone}`, children: note.text }) : null] }), _jsx5("td", { className: "cs-cell-nowrap", children: _jsx5("time", { dateTime: thread.updatedAt, children: ago(thread.updatedAt) }) })] }, thread.id);
2416
+ }) })] }) })] });
2417
+ }
2418
+
2419
+ // ../react/dist/handoff.js
2420
+ var key = "devaccel.comment.handoff";
2421
+ var ttl = 30 * 60 * 1e3;
2422
+ function readHandoff() {
2423
+ try {
2424
+ const value = JSON.parse(sessionStorage.getItem(key) ?? "null");
2425
+ return value && Date.now() - value.at < ttl ? value : null;
2426
+ } catch {
2427
+ return null;
2428
+ }
2429
+ }
2430
+ function write(value) {
2431
+ try {
2432
+ if (value.invite || value.thread)
2433
+ sessionStorage.setItem(key, JSON.stringify(value));
2434
+ else
2435
+ sessionStorage.removeItem(key);
2436
+ } catch {
2437
+ }
2438
+ }
2439
+ function linkParams() {
2440
+ const url = new URL(window.location.href);
2441
+ const hash = new URLSearchParams(url.hash.slice(1));
2442
+ return {
2443
+ url,
2444
+ hash,
2445
+ invite: url.searchParams.get("cs-invite") ?? hash.get("cs-invite"),
2446
+ thread: url.searchParams.get("cs-thread") ?? hash.get("cs-thread")
2447
+ };
2448
+ }
2449
+ function captureHandoff() {
2450
+ if (typeof window === "undefined")
2451
+ return;
2452
+ const { invite, thread } = linkParams();
2453
+ if (!invite && !thread)
2454
+ return;
2455
+ write({ ...readHandoff(), ...invite ? { invite } : {}, ...thread ? { thread } : {}, at: Date.now() });
2456
+ }
2457
+ function dropHandoff(part) {
2458
+ const current = readHandoff();
2459
+ if (!current)
2460
+ return;
2461
+ const { [part]: _dropped, ...rest } = current;
2462
+ write(rest);
2463
+ }
2464
+ function stripHandoffFromUrl() {
2465
+ const { url, hash } = linkParams();
2466
+ const inQuery = url.searchParams.has("cs-invite") || url.searchParams.has("cs-thread");
2467
+ const inHash = hash.has("cs-invite") || hash.has("cs-thread");
2468
+ if (!inQuery && !inHash)
2469
+ return;
2470
+ url.searchParams.delete("cs-invite");
2471
+ url.searchParams.delete("cs-thread");
2472
+ if (inHash)
2473
+ url.hash = "";
2474
+ history.replaceState(history.state, "", url);
2475
+ }
2476
+ function linkWith(href, name, value) {
2477
+ const url = new URL(href, window.location.origin);
2478
+ url.searchParams.delete("cs-invite");
2479
+ url.searchParams.delete("cs-thread");
2480
+ url.hash = `${name}=${encodeURIComponent(value)}`;
2481
+ return url.toString();
2482
+ }
2483
+
2247
2484
  // ../react/dist/styles.js
2248
2485
  var toolbarCss = `
2249
2486
  #cs-root {
@@ -2497,6 +2734,7 @@ var toolbarCss = `
2497
2734
  display: flex; flex-direction: column; overflow: hidden;
2498
2735
  }
2499
2736
  #cs-root .cs-sheet-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 10px 4px 16px; }
2737
+ #cs-root .cs-sheet-head .cs-push { margin-left: auto; }
2500
2738
  #cs-root .cs-sheet-head h2 { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
2501
2739
  #cs-root .cs-tabs { display: flex; gap: 2px; margin: 4px 12px 6px; padding: 2px; border-radius: 9px; background: var(--cs-fill); }
2502
2740
  #cs-root .cs-tabs button {
@@ -2568,6 +2806,88 @@ var toolbarCss = `
2568
2806
  #cs-root .cs-composer-foot { display: flex; align-items: center; gap: 6px; padding: 6px 8px 8px 40px; }
2569
2807
  #cs-root .cs-composer-foot .cs-hint { margin-left: auto; }
2570
2808
 
2809
+ /* Roles and fields */
2810
+ #cs-root .cs-details-input { display: grid; gap: 8px; padding: 2px 12px 6px 46px; }
2811
+ #cs-root .cs-details-edit .cs-details-input { padding: 0; }
2812
+ #cs-root .cs-detail-field { display: grid; gap: 3px; }
2813
+ #cs-root .cs-detail-field span, #cs-root .cs-roles-label, #cs-root .cs-detail-list dt {
2814
+ font-size: 11px; font-weight: 600; color: var(--cs-text-3); text-transform: uppercase; letter-spacing: 0.03em;
2815
+ }
2816
+ #cs-root .cs-detail-field textarea { padding: 6px 8px; min-height: 30px; max-height: 120px; field-sizing: content; }
2817
+ #cs-root .cs-thread.is-open .cs-detail-field textarea { background: var(--cs-card); }
2818
+ #cs-root .cs-roles { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
2819
+ #cs-root .cs-roles-label { margin-right: 4px; }
2820
+ #cs-root .cs-chip {
2821
+ display: inline-flex; align-items: center; gap: 2px; height: 24px; padding: 0 9px; border-radius: 999px;
2822
+ background: var(--cs-fill); font-size: 12px; font-weight: 500; color: var(--cs-text-2);
2823
+ }
2824
+ #cs-root button.cs-chip:hover { background: var(--cs-fill-2); }
2825
+ #cs-root .cs-chip.is-on, #cs-root button.cs-chip.is-on:hover { background: var(--cs-accent); color: #fff; }
2826
+ #cs-root .cs-chip.is-static { padding-right: 3px; color: var(--cs-text); }
2827
+ #cs-root .cs-chip.is-static button {
2828
+ width: 18px; height: 18px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; color: var(--cs-text-3);
2829
+ }
2830
+ #cs-root .cs-chip.is-static button:hover { background: var(--cs-fill-2); color: var(--cs-text); }
2831
+ #cs-root .cs-chip.is-static .cs-icon { width: 11px; height: 11px; }
2832
+ #cs-root .cs-chips { display: flex; flex-wrap: wrap; gap: 4px; }
2833
+ #cs-root .cs-role {
2834
+ display: inline-flex; align-items: center; height: 18px; padding: 0 7px; border-radius: 999px;
2835
+ background: var(--cs-accent-soft); color: var(--cs-accent); font-size: 11px; font-weight: 600;
2836
+ }
2837
+ #cs-root .cs-detail-list { display: grid; gap: 6px; margin: 6px 0 0; }
2838
+ #cs-root .cs-detail-list > div { padding: 6px 8px; border-radius: 8px; background: var(--cs-fill); }
2839
+ #cs-root .cs-thread.is-open .cs-detail-list > div { background: var(--cs-card); }
2840
+ #cs-root .cs-detail-list dd { margin: 1px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
2841
+ #cs-root .cs-detail-list.is-clamped dd { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
2842
+ #cs-root .cs-details-edit { display: grid; gap: 8px; }
2843
+ #cs-root .cs-details-edit .cs-row-end { margin-top: 0; }
2844
+ #cs-root .cs-settings { width: 280px; }
2845
+ #cs-root .cs-settings-head { display: flex; align-items: center; gap: 4px; padding: 4px 4px 6px; }
2846
+ #cs-root .cs-settings-head h2 { font-size: 14px; font-weight: 600; }
2847
+ #cs-root .cs-settings-body { display: grid; gap: 6px; padding: 2px 8px 8px; }
2848
+ #cs-root .cs-field-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; min-height: 28px; padding-left: 2px; }
2849
+ #cs-root .cs-add-row { display: flex; align-items: center; gap: 4px; }
2850
+ #cs-root .cs-add-row input { padding: 5px 8px; }
2851
+ #cs-root .cs-settings .cs-hint { padding: 6px 8px 4px; }
2852
+
2853
+ /* Table */
2854
+ #cs-root .cs-table-sheet {
2855
+ top: 12px; right: 12px; width: min(1100px, calc(100vw - 24px)); max-height: calc(100% - 88px);
2856
+ display: flex; flex-direction: column; overflow: hidden;
2857
+ }
2858
+ #cs-root .cs-table-sheet .cs-sheet-head { gap: 6px; }
2859
+ #cs-root .cs-table-sheet .cs-sheet-head h2 { margin-right: auto; }
2860
+ #cs-root .cs-table-search { width: 220px; padding: 5px 10px; }
2861
+ #cs-root .cs-table-sheet .cs-tabs { max-width: 420px; }
2862
+ #cs-root .cs-table-wrap { flex: 1 1 auto; min-height: 0; overflow: auto; border-top: 1px solid var(--cs-line); overscroll-behavior: contain; }
2863
+ #cs-root .cs-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
2864
+ #cs-root .cs-table th {
2865
+ position: sticky; top: 0; z-index: 1; background: var(--cs-card); text-align: left; font-weight: 600;
2866
+ font-size: 11px; color: var(--cs-text-3); text-transform: uppercase; letter-spacing: 0.03em;
2867
+ padding: 0; border-bottom: 1px solid var(--cs-line); white-space: nowrap;
2868
+ }
2869
+ #cs-root .cs-table th > button, #cs-root .cs-table th > span {
2870
+ display: inline-flex; align-items: center; gap: 4px; padding: 8px 10px; font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
2871
+ }
2872
+ #cs-root .cs-table th > button:hover { color: var(--cs-text); }
2873
+ #cs-root .cs-sort { color: var(--cs-accent); }
2874
+ #cs-root .cs-table td { padding: 8px 10px; vertical-align: top; border-bottom: 1px solid var(--cs-line); max-width: 260px; }
2875
+ #cs-root .cs-table tbody tr { cursor: pointer; }
2876
+ #cs-root .cs-table tbody tr:hover { background: var(--cs-fill); }
2877
+ #cs-root .cs-table tbody tr.is-active { background: var(--cs-accent-soft); }
2878
+ #cs-root .cs-table tbody tr:focus-visible { outline: 2px solid var(--cs-accent); outline-offset: -2px; }
2879
+ #cs-root .cs-cell-comment { min-width: 220px; }
2880
+ #cs-root .cs-cell-comment small, #cs-root .cs-table td small { display: block; margin-top: 2px; font-size: 11.5px; color: var(--cs-text-3); }
2881
+ #cs-root .cs-cell-clamp { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; white-space: pre-wrap; overflow-wrap: anywhere; }
2882
+ #cs-root .cs-cell-page { font: 11.5px ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--cs-text-2); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2883
+ #cs-root .cs-cell-nowrap { white-space: nowrap; }
2884
+ #cs-root .cs-cell-num { text-align: right; font-variant-numeric: tabular-nums; color: var(--cs-text-2); }
2885
+ #cs-root .cs-state { display: inline-flex; align-items: center; gap: 5px; font-weight: 500; }
2886
+ #cs-root .cs-state::before { content: ""; width: 6px; height: 6px; border-radius: 999px; background: #34c759; }
2887
+ #cs-root .cs-state.is-done { color: var(--cs-text-3); }
2888
+ #cs-root .cs-state.is-done::before { background: var(--cs-text-3); }
2889
+ #cs-root .cs-table .cs-note { display: flex; margin-top: 2px; font-size: 11px; }
2890
+
2571
2891
  /* Attachments */
2572
2892
  #cs-root .cs-attach { display: inline-flex; gap: 0; flex: none; }
2573
2893
  #cs-root .cs-tray { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 12px 2px 46px; }
@@ -2658,6 +2978,7 @@ var toolbarCss = `
2658
2978
  `;
2659
2979
 
2660
2980
  // ../react/dist/toolbar.js
2981
+ captureHandoff();
2661
2982
  var historyFlag = "__devaccelCommentHistory";
2662
2983
  var prefsKey = "devaccel.comment.prefs";
2663
2984
  var positions = [
@@ -2690,6 +3011,13 @@ function placeAgainst(element, host) {
2690
3011
  height: rect.height
2691
3012
  };
2692
3013
  }
3014
+ function pageHref() {
3015
+ const url = new URL(window.location.href);
3016
+ url.searchParams.delete("cs-thread");
3017
+ url.searchParams.delete("cs-invite");
3018
+ const hash = url.hash.includes("cs-thread=") || url.hash.includes("cs-invite=") ? "" : url.hash;
3019
+ return `${url.pathname}${url.search}${hash}`;
3020
+ }
2693
3021
  function isTyping(target) {
2694
3022
  if (!(target instanceof HTMLElement))
2695
3023
  return false;
@@ -2712,44 +3040,48 @@ async function loadSession(sessionPath) {
2712
3040
  return { apiUrl: payload.apiUrl, project: payload.project, environment: payload.environment, hostToken: payload.hostToken };
2713
3041
  }
2714
3042
  function Avatar({ name, size = 24 }) {
2715
- return _jsx4("span", { className: "cs-avatar", style: { width: size, height: size, fontSize: size * 0.4, background: colorFor(name) }, children: initials(name) });
2716
- }
2717
- function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs = 4e3, position = "bottom-center", theme = "light" }) {
2718
- const [phase, setPhase] = useState3("boot");
2719
- const [session, setSession] = useState3(null);
2720
- const [member, setMember] = useState3(null);
2721
- const [threads, setThreads] = useState3([]);
2722
- const [route, setRoute] = useState3("/");
2723
- const [surface, setSurface] = useState3("none");
2724
- const [tab, setTab] = useState3("page");
2725
- const [mode, setMode] = useState3("browse");
2726
- const [reattachId, setReattachId] = useState3(null);
2727
- const [activeId, setActiveId] = useState3(null);
2728
- const [peekId, setPeekId] = useState3(null);
2729
- const [draft, setDraft] = useState3(null);
2730
- const [body, setBody] = useState3("");
2731
- const [reply, setReply] = useState3("");
2732
- const [name, setName] = useState3("");
2733
- const [passphrase, setPassphrase] = useState3("");
2734
- const [email, setEmail] = useState3("");
2735
- const [members, setMembers] = useState3([]);
2736
- const [profile, setProfile] = useState3(null);
2737
- const [deliveries, setDeliveries] = useState3({});
2738
- const [invite, setInvite] = useState3(null);
2739
- const [capturing, setCapturing] = useState3(false);
2740
- const [inviteLink, setInviteLink] = useState3(null);
2741
- const [renaming, setRenaming] = useState3(null);
2742
- const [confirmReset, setConfirmReset] = useState3(false);
2743
- const [error, setError] = useState3("");
2744
- const [toast, setToast] = useState3("");
2745
- const [busy, setBusy] = useState3(false);
2746
- const [prefs, setPrefs] = useState3({});
2747
- const [portal, setPortal] = useState3(null);
2748
- const [mounted, setMounted] = useState3(false);
2749
- const [highlight, setHighlight] = useState3(null);
2750
- const [flash, setFlash] = useState3(null);
2751
- const [placed, setPlaced] = useState3([]);
2752
- const [systemDark, setSystemDark] = useState3(false);
3043
+ return _jsx6("span", { className: "cs-avatar", style: { width: size, height: size, fontSize: size * 0.4, background: colorFor(name) }, children: initials(name) });
3044
+ }
3045
+ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs = 4e3, position = "bottom-center", theme = "light", navigate }) {
3046
+ const [phase, setPhase] = useState5("boot");
3047
+ const [session, setSession] = useState5(null);
3048
+ const [member, setMember] = useState5(null);
3049
+ const [threads, setThreads] = useState5([]);
3050
+ const [route, setRoute] = useState5("/");
3051
+ const [surface, setSurface] = useState5("none");
3052
+ const [tab, setTab] = useState5("page");
3053
+ const [mode, setMode] = useState5("browse");
3054
+ const [reattachId, setReattachId] = useState5(null);
3055
+ const [activeId, setActiveId] = useState5(null);
3056
+ const [peekId, setPeekId] = useState5(null);
3057
+ const [draft, setDraft] = useState5(null);
3058
+ const [body, setBody] = useState5("");
3059
+ const [reply, setReply] = useState5("");
3060
+ const [name, setName] = useState5("");
3061
+ const [passphrase, setPassphrase] = useState5("");
3062
+ const [email, setEmail] = useState5("");
3063
+ const [members, setMembers] = useState5([]);
3064
+ const [profile, setProfile] = useState5(null);
3065
+ const [deliveries, setDeliveries] = useState5({});
3066
+ const [invite, setInvite] = useState5(null);
3067
+ const [capturing, setCapturing] = useState5(false);
3068
+ const [inviteLink, setInviteLink] = useState5(null);
3069
+ const [renaming, setRenaming] = useState5(null);
3070
+ const [confirmReset, setConfirmReset] = useState5(false);
3071
+ const [error, setError] = useState5("");
3072
+ const [toast, setToast] = useState5("");
3073
+ const [busy, setBusy] = useState5(false);
3074
+ const [prefs, setPrefs] = useState5({});
3075
+ const [portal, setPortal] = useState5(null);
3076
+ const [mounted, setMounted] = useState5(false);
3077
+ const [highlight, setHighlight] = useState5(null);
3078
+ const [flash, setFlash] = useState5(null);
3079
+ const [placed, setPlaced] = useState5([]);
3080
+ const [systemDark, setSystemDark] = useState5(false);
3081
+ const [settings, setSettings] = useState5(null);
3082
+ const [draftDetails, setDraftDetails] = useState5(noDetails);
3083
+ const [editingId, setEditingId] = useState5(null);
3084
+ const [view, setView] = useState5("list");
2753
3085
  const threadsRef = useRef3(threads);
2754
3086
  threadsRef.current = threads;
2755
3087
  const modeRef = useRef3(mode);
@@ -2759,6 +3091,7 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
2759
3091
  const draftShot = useRef3(null);
2760
3092
  const replyShot = useRef3(null);
2761
3093
  const pendingThread = useRef3(null);
3094
+ const arrival = useRef3(null);
2762
3095
  const draftFiles = useAttachments(session, member, setError);
2763
3096
  const replyFiles = useAttachments(session, member, setError);
2764
3097
  const resetReplyFiles = replyFiles.reset;
@@ -2858,7 +3191,9 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
2858
3191
  const refresh = useCallback2(async () => {
2859
3192
  if (!session || !member)
2860
3193
  return;
2861
- setThreads(await listThreads(session, member));
3194
+ const next = await listThreads(session, member);
3195
+ setThreads(next.threads);
3196
+ setSettings(next.settings);
2862
3197
  }, [member, session]);
2863
3198
  useEffect2(() => {
2864
3199
  if (!session || !member)
@@ -2866,8 +3201,10 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
2866
3201
  let stop = false;
2867
3202
  const tick = () => {
2868
3203
  listThreads(session, member).then((next) => {
2869
- if (!stop)
2870
- setThreads(next);
3204
+ if (stop)
3205
+ return;
3206
+ setThreads(next.threads);
3207
+ setSettings(next.settings);
2871
3208
  }).catch((caught) => {
2872
3209
  if (caught instanceof ApiError && caught.status === 401) {
2873
3210
  localStorage.removeItem(storageKey(session));
@@ -2900,22 +3237,23 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
2900
3237
  };
2901
3238
  }, [member, session]);
2902
3239
  useEffect2(() => {
2903
- const params = new URLSearchParams(window.location.search);
2904
- pendingThread.current = params.get("cs-thread");
2905
- const code = params.get("cs-invite");
2906
- if (code) {
2907
- setInvite(code);
2908
- const url = new URL(window.location.href);
2909
- url.searchParams.delete("cs-invite");
2910
- history.replaceState(history.state, "", url);
2911
- }
3240
+ captureHandoff();
3241
+ const handoff = readHandoff();
3242
+ pendingThread.current = handoff?.thread ?? null;
3243
+ if (handoff?.invite)
3244
+ setInvite(handoff.invite);
3245
+ stripHandoffFromUrl();
3246
+ }, []);
3247
+ const dropInvite = useCallback2(() => {
3248
+ setInvite(null);
3249
+ dropHandoff("invite");
2912
3250
  }, []);
2913
3251
  useEffect2(() => {
2914
3252
  if (phase === "ready" && !member && (pendingThread.current || invite))
2915
3253
  setSurface("unlock");
2916
3254
  if (member && invite)
2917
- setInvite(null);
2918
- }, [invite, member, phase]);
3255
+ dropInvite();
3256
+ }, [dropInvite, invite, member, phase]);
2919
3257
  useEffect2(() => {
2920
3258
  if (!mounted)
2921
3259
  return;
@@ -3003,12 +3341,13 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3003
3341
  }
3004
3342
  if (!member || event.metaKey || event.ctrlKey || event.altKey || isTyping(event.target))
3005
3343
  return;
3006
- const key = event.key.toLowerCase();
3007
- if (key === "c")
3344
+ const key2 = event.key.toLowerCase();
3345
+ if (key2 === "c")
3008
3346
  startComment();
3009
- else if (key === "i")
3347
+ else if (key2 === "i") {
3348
+ setView(prefs.view ?? "list");
3010
3349
  setSurface((current) => current === "inbox" ? "none" : "inbox");
3011
- else if (key === "h")
3350
+ } else if (key2 === "h")
3012
3351
  togglePins();
3013
3352
  else
3014
3353
  return;
@@ -3016,7 +3355,7 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3016
3355
  };
3017
3356
  document.addEventListener("keydown", onKey);
3018
3357
  return () => document.removeEventListener("keydown", onKey);
3019
- }, [draft, member, mode, startComment, surface, togglePins]);
3358
+ }, [draft, member, mode, prefs.view, startComment, surface, togglePins]);
3020
3359
  useEffect2(() => {
3021
3360
  if (mode === "browse") {
3022
3361
  setHighlight(null);
@@ -3048,7 +3387,7 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3048
3387
  const box = placeAgainst(element, target);
3049
3388
  if (modeRef.current === "reattach" && reattachId && member) {
3050
3389
  setBusy(true);
3051
- updateThread(session, member, reattachId, { anchor }).then(() => refresh()).then(() => {
3390
+ updateThread(session, member, reattachId, { anchor, url: pageHref() }).then(() => refresh()).then(() => {
3052
3391
  setMode("browse");
3053
3392
  setReattachId(null);
3054
3393
  setSurface("inbox");
@@ -3069,6 +3408,7 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3069
3408
  draftShot.current = null;
3070
3409
  draftFiles.reset();
3071
3410
  setBody("");
3411
+ setDraftDetails(noDetails);
3072
3412
  setMode("browse");
3073
3413
  setError("");
3074
3414
  };
@@ -3100,19 +3440,19 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3100
3440
  all: open.length,
3101
3441
  resolved: placed.length - open.length
3102
3442
  };
3103
- const listed = useMemo2(() => {
3443
+ const listed = useMemo3(() => {
3104
3444
  if (tab === "resolved")
3105
3445
  return placed.filter((item) => item.thread.status === "resolved");
3106
3446
  const openOnly = placed.filter((item) => item.thread.status === "open");
3107
3447
  return tab === "page" ? openOnly.filter((item) => item.thread.route === route) : openOnly;
3108
3448
  }, [placed, route, tab]);
3109
- const groups = useMemo2(() => {
3449
+ const groups = useMemo3(() => {
3110
3450
  if (tab !== "all")
3111
3451
  return [{ route: null, items: listed }];
3112
3452
  const byRoute = /* @__PURE__ */ new Map();
3113
3453
  for (const item of listed)
3114
3454
  byRoute.set(item.thread.route, [...byRoute.get(item.thread.route) ?? [], item]);
3115
- return [...byRoute.entries()].sort(([a], [b]) => a === route ? -1 : b === route ? 1 : a.localeCompare(b)).map(([key, items]) => ({ route: key, items }));
3455
+ return [...byRoute.entries()].sort(([a], [b]) => a === route ? -1 : b === route ? 1 : a.localeCompare(b)).map(([key2, items]) => ({ route: key2, items }));
3116
3456
  }, [listed, route, tab]);
3117
3457
  const showOnPage = useCallback2((thread) => {
3118
3458
  const { element } = resolveInDocument(thread.anchor, window.location.pathname);
@@ -3133,21 +3473,67 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3133
3473
  if (!item)
3134
3474
  return;
3135
3475
  pendingThread.current = null;
3136
- const url = new URL(window.location.href);
3137
- url.searchParams.delete("cs-thread");
3138
- history.replaceState(history.state, "", url);
3476
+ dropHandoff("thread");
3139
3477
  setTab(item.thread.status === "resolved" ? "resolved" : item.thread.route === route ? "page" : "all");
3140
3478
  setActiveId(id);
3141
3479
  setSurface("inbox");
3142
3480
  if (item.status === "pinned" || item.status === "drifted")
3143
3481
  showOnPage(item.thread);
3482
+ else
3483
+ arrival.current = { id, until: Date.now() + 6e3 };
3144
3484
  }, [member, placed, route, showOnPage]);
3485
+ useEffect2(() => {
3486
+ const pending = arrival.current;
3487
+ if (!pending)
3488
+ return;
3489
+ if (Date.now() > pending.until) {
3490
+ arrival.current = null;
3491
+ return;
3492
+ }
3493
+ const item = placed.find((candidate) => candidate.thread.id === pending.id);
3494
+ if (!item || item.status !== "pinned" && item.status !== "drifted")
3495
+ return;
3496
+ arrival.current = null;
3497
+ showOnPage(item.thread);
3498
+ }, [placed, showOnPage]);
3499
+ const select = useCallback2((item, keepOpen = false) => {
3500
+ const { thread } = item;
3501
+ if (!keepOpen && activeId === thread.id) {
3502
+ setActiveId(null);
3503
+ return;
3504
+ }
3505
+ setActiveId(thread.id);
3506
+ setEditingId(null);
3507
+ const onPage = item.status === "pinned" || item.status === "drifted";
3508
+ const target = thread.url || thread.route;
3509
+ const legacyHere = thread.url === thread.route && thread.route === route;
3510
+ if (target === pageHref() || onPage && legacyHere)
3511
+ return;
3512
+ if (navigate) {
3513
+ arrival.current = { id: thread.id, until: Date.now() + 6e3 };
3514
+ navigate(target);
3515
+ return;
3516
+ }
3517
+ window.location.assign(linkWith(target, "cs-thread", thread.id));
3518
+ }, [activeId, navigate, route]);
3145
3519
  if (!mounted || phase !== "ready" || !session)
3146
3520
  return null;
3147
3521
  const memberName = member ? readJwt(member)?.name ?? name : "";
3148
3522
  const host = portal ?? document.body;
3149
3523
  const inside = host !== document.body;
3150
3524
  const peek = peekId ? placed.find((item) => item.thread.id === peekId && item.top != null) : void 0;
3525
+ const canPost = Boolean(body.trim() || draftFiles.ids.length || settings?.fields.enabled && hasFieldValues(draftDetails));
3526
+ const changeSettings = (next) => {
3527
+ if (!member)
3528
+ return;
3529
+ const previous = settings;
3530
+ setSettings(next);
3531
+ setError("");
3532
+ saveSettings(session, member, next).then(setSettings).catch((caught) => {
3533
+ setSettings(previous);
3534
+ setError(caught instanceof Error ? caught.message : "Couldn\u2019t save.");
3535
+ });
3536
+ };
3151
3537
  const lock = () => {
3152
3538
  localStorage.removeItem(storageKey(session));
3153
3539
  setMember(null);
@@ -3173,22 +3559,23 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3173
3559
  setDraft(null);
3174
3560
  setSurface("unlock");
3175
3561
  };
3176
- const bar = _jsx4("div", { className: "cs-bar cs-hit", "data-cs-ui": "bar", role: "toolbar", "aria-label": "Comments toolbar", children: !member ? _jsxs4("button", { className: "cs-bar-lock", "data-cs-ui": "launcher", type: "button", "aria-label": "Comments", onClick: () => {
3562
+ const bar = _jsx6("div", { className: "cs-bar cs-hit", "data-cs-ui": "bar", role: "toolbar", "aria-label": "Comments toolbar", children: !member ? _jsxs6("button", { className: "cs-bar-lock", "data-cs-ui": "launcher", type: "button", "aria-label": "Comments", onClick: () => {
3177
3563
  setError("");
3178
3564
  setSurface(surface === "unlock" ? "none" : "unlock");
3179
- }, children: [_jsx4(Lock, {}), _jsx4("span", { children: "Comments" })] }) : _jsxs4(_Fragment2, { children: [_jsx4("button", { className: mode === "select" ? "cs-tool is-on is-accent" : "cs-tool", "data-cs-ui": "comment-mode", "data-tip": "Comment C", type: "button", "aria-label": "Add comment", "aria-pressed": mode === "select", onClick: startComment, children: _jsx4(AddComment, {}) }), _jsxs4("button", { className: surface === "inbox" ? "cs-tool is-on" : "cs-tool", "data-cs-ui": "launcher", "data-tip": "Comments I", type: "button", "aria-label": "Comments", "aria-expanded": surface === "inbox", onClick: () => {
3565
+ }, children: [_jsx6(Lock, {}), _jsx6("span", { children: "Comments" })] }) : _jsxs6(_Fragment3, { children: [_jsx6("button", { className: mode === "select" ? "cs-tool is-on is-accent" : "cs-tool", "data-cs-ui": "comment-mode", "data-tip": "Comment C", type: "button", "aria-label": "Add comment", "aria-pressed": mode === "select", onClick: startComment, children: _jsx6(AddComment, {}) }), _jsxs6("button", { className: surface === "inbox" ? "cs-tool is-on" : "cs-tool", "data-cs-ui": "launcher", "data-tip": "Comments I", type: "button", "aria-label": "Comments", "aria-expanded": surface === "inbox", onClick: () => {
3180
3566
  setError("");
3181
3567
  setMode("browse");
3568
+ setView(prefs.view ?? "list");
3182
3569
  setSurface(surface === "inbox" ? "none" : "inbox");
3183
- }, children: [_jsx4(Inbox, {}), counts.page > 0 ? _jsx4("span", { className: "cs-badge", children: counts.page }) : null] }), _jsx4("button", { className: pinsHidden ? "cs-tool is-on" : "cs-tool", "data-cs-ui": "pins-toggle", "data-tip": pinsHidden ? "Show pins H" : "Hide pins H", type: "button", "aria-label": pinsHidden ? "Show pins" : "Hide pins", "aria-pressed": pinsHidden, onClick: togglePins, children: pinsHidden ? _jsx4(EyeOff, {}) : _jsx4(Eye, {}) }), _jsx4("span", { className: "cs-divider" }), _jsx4("button", { className: surface === "menu" ? "cs-tool cs-me is-on" : "cs-tool cs-me", "data-cs-ui": "menu-button", "data-tip": memberName, type: "button", "aria-label": "You", "aria-expanded": surface === "menu", onClick: () => {
3570
+ }, children: [_jsx6(Inbox, {}), counts.page > 0 ? _jsx6("span", { className: "cs-badge", children: counts.page }) : null] }), _jsx6("button", { className: pinsHidden ? "cs-tool is-on" : "cs-tool", "data-cs-ui": "pins-toggle", "data-tip": pinsHidden ? "Show pins H" : "Hide pins H", type: "button", "aria-label": pinsHidden ? "Show pins" : "Hide pins", "aria-pressed": pinsHidden, onClick: togglePins, children: pinsHidden ? _jsx6(EyeOff, {}) : _jsx6(Eye, {}) }), _jsx6("span", { className: "cs-divider" }), _jsx6("button", { className: surface === "menu" ? "cs-tool cs-me is-on" : "cs-tool cs-me", "data-cs-ui": "menu-button", "data-tip": memberName, type: "button", "aria-label": "You", "aria-expanded": surface === "menu", onClick: () => {
3184
3571
  setConfirmReset(false);
3185
3572
  setRenaming(null);
3186
3573
  if (surface !== "menu" && member) {
3187
3574
  getProfile(session, member).then(setProfile).catch(() => void 0);
3188
3575
  }
3189
3576
  setSurface(surface === "menu" ? "none" : "menu");
3190
- }, children: _jsx4(Avatar, { name: memberName, size: 24 }) })] }) });
3191
- const unlockCard = _jsxs4("section", { className: "cs-card cs-unlock cs-hit", "data-cs-ui": "panel", "aria-label": "Unlock comments", children: [_jsx4("div", { className: "cs-unlock-icon", children: _jsx4(Lock, { size: 18 }) }), _jsx4("h2", { children: "Comments" }), _jsx4("p", { className: "cs-sub", children: invite ? `${readJwt(invite)?.name ?? "A teammate"} invited you to comment. Add your name and email to join.` : "Enter the team passphrase to see and add comments." }), _jsxs4("form", { onSubmit: (event) => {
3577
+ }, children: _jsx6(Avatar, { name: memberName, size: 24 }) })] }) });
3578
+ const unlockCard = _jsxs6("section", { className: "cs-card cs-unlock cs-hit", "data-cs-ui": "panel", "aria-label": "Unlock comments", children: [_jsx6("div", { className: "cs-unlock-icon", children: _jsx6(Lock, { size: 18 }) }), _jsx6("h2", { children: "Comments" }), _jsx6("p", { className: "cs-sub", children: invite ? `${readJwt(invite)?.name ?? "A teammate"} invited you to comment. Add your name and email to join.` : "Enter the team passphrase to see and add comments." }), _jsxs6("form", { onSubmit: (event) => {
3192
3579
  event.preventDefault();
3193
3580
  setBusy(true);
3194
3581
  setError("");
@@ -3201,16 +3588,16 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3201
3588
  localStorage.setItem(storageKey(fresh), token);
3202
3589
  setMember(token);
3203
3590
  setPassphrase("");
3204
- setInvite(null);
3591
+ dropInvite();
3205
3592
  setTab("page");
3206
3593
  setSurface("inbox");
3207
3594
  }).catch((caught) => {
3208
3595
  if (caught instanceof ApiError && caught.code === "invalid_invite")
3209
- setInvite(null);
3596
+ dropInvite();
3210
3597
  setError(caught instanceof Error ? caught.message : "Couldn\u2019t unlock.");
3211
3598
  }).finally(() => setBusy(false));
3212
- }, children: [_jsxs4("label", { className: "cs-field", children: [_jsx4("span", { children: "Your name" }), _jsx4("input", { "data-cs-ui": "unlock-name", value: name, onChange: (event) => setName(event.target.value), autoComplete: "name", required: true })] }), invite ? null : _jsxs4("label", { className: "cs-field", children: [_jsx4("span", { children: "Passphrase" }), _jsx4("input", { "data-cs-ui": "unlock-passphrase", type: "password", value: passphrase, onChange: (event) => setPassphrase(event.target.value), autoComplete: "current-password", required: true })] }), _jsxs4("label", { className: "cs-field", children: [_jsx4("span", { children: "Email" }), _jsx4("input", { "data-cs-ui": "unlock-email", type: "email", value: email, onChange: (event) => setEmail(event.target.value), autoComplete: "email", required: true }), _jsx4("small", { children: "So teammates can @mention you." })] }), error ? _jsx4("p", { className: "cs-error", "data-cs-ui": "error", children: error }) : null, _jsx4("button", { className: "cs-button is-primary is-wide", "data-cs-ui": "unlock-submit", type: "submit", disabled: busy, children: busy ? "Unlocking\u2026" : invite ? "Join" : "Unlock" })] })] });
3213
- const menuCard = _jsx4("section", { className: "cs-card cs-menu cs-hit", "data-cs-ui": "menu", "aria-label": "Your comment settings", children: confirmReset ? _jsxs4("div", { className: "cs-confirm", "data-cs-ui": "reset-confirm", children: [_jsx4("h2", { children: "Reset on this device?" }), _jsx4("p", { className: "cs-sub", children: "You\u2019ll be signed out and your name and preferences are cleared. Comments aren\u2019t affected." }), _jsxs4("div", { className: "cs-row-end", children: [_jsx4("button", { className: "cs-button", type: "button", onClick: () => setConfirmReset(false), children: "Cancel" }), _jsx4("button", { className: "cs-button is-danger", "data-cs-ui": "reset-submit", type: "button", onClick: reset, children: "Reset" })] })] }) : _jsxs4(_Fragment2, { children: [_jsxs4("div", { className: "cs-me-row", children: [_jsx4(Avatar, { name: renaming ?? memberName, size: 36 }), renaming != null ? _jsxs4("form", { className: "cs-rename", onSubmit: (event) => {
3599
+ }, children: [_jsxs6("label", { className: "cs-field", children: [_jsx6("span", { children: "Your name" }), _jsx6("input", { "data-cs-ui": "unlock-name", value: name, onChange: (event) => setName(event.target.value), autoComplete: "name", required: true })] }), invite ? null : _jsxs6("label", { className: "cs-field", children: [_jsx6("span", { children: "Passphrase" }), _jsx6("input", { "data-cs-ui": "unlock-passphrase", type: "password", value: passphrase, onChange: (event) => setPassphrase(event.target.value), autoComplete: "current-password", required: true })] }), _jsxs6("label", { className: "cs-field", children: [_jsx6("span", { children: "Email" }), _jsx6("input", { "data-cs-ui": "unlock-email", type: "email", value: email, onChange: (event) => setEmail(event.target.value), autoComplete: "email", required: true }), _jsx6("small", { children: "So teammates can @mention you." })] }), error ? _jsx6("p", { className: "cs-error", "data-cs-ui": "error", children: error }) : null, _jsx6("button", { className: "cs-button is-primary is-wide", "data-cs-ui": "unlock-submit", type: "submit", disabled: busy, children: busy ? "Unlocking\u2026" : invite ? "Join" : "Unlock" })] })] });
3600
+ const menuCard = _jsx6("section", { className: "cs-card cs-menu cs-hit", "data-cs-ui": "menu", "aria-label": "Your comment settings", children: confirmReset ? _jsxs6("div", { className: "cs-confirm", "data-cs-ui": "reset-confirm", children: [_jsx6("h2", { children: "Reset on this device?" }), _jsx6("p", { className: "cs-sub", children: "You\u2019ll be signed out and your name and preferences are cleared. Comments aren\u2019t affected." }), _jsxs6("div", { className: "cs-row-end", children: [_jsx6("button", { className: "cs-button", type: "button", onClick: () => setConfirmReset(false), children: "Cancel" }), _jsx6("button", { className: "cs-button is-danger", "data-cs-ui": "reset-submit", type: "button", onClick: reset, children: "Reset" })] })] }) : _jsxs6(_Fragment3, { children: [_jsxs6("div", { className: "cs-me-row", children: [_jsx6(Avatar, { name: renaming ?? memberName, size: 36 }), renaming != null ? _jsxs6("form", { className: "cs-rename", onSubmit: (event) => {
3214
3601
  event.preventDefault();
3215
3602
  const next = renaming.trim();
3216
3603
  if (!next || next === memberName || !member) {
@@ -3226,15 +3613,15 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3226
3613
  setRenaming(null);
3227
3614
  say("Name updated");
3228
3615
  }).catch((caught) => setError(caught instanceof Error ? caught.message : "Couldn\u2019t rename.")).finally(() => setBusy(false));
3229
- }, children: [_jsx4("input", { "data-cs-ui": "rename-input", "aria-label": "Your name", value: renaming, maxLength: 40, autoFocus: true, onChange: (event) => setRenaming(event.target.value), onKeyDown: (event) => {
3616
+ }, children: [_jsx6("input", { "data-cs-ui": "rename-input", "aria-label": "Your name", value: renaming, maxLength: 40, autoFocus: true, onChange: (event) => setRenaming(event.target.value), onKeyDown: (event) => {
3230
3617
  if (event.key === "Escape") {
3231
3618
  event.preventDefault();
3232
3619
  setRenaming(null);
3233
3620
  }
3234
- } }), _jsx4("button", { className: "cs-icon-button", type: "submit", "aria-label": "Save name", disabled: busy, children: _jsx4(Check, {}) })] }) : _jsxs4("div", { className: "cs-me-name", children: [_jsxs4("span", { className: "cs-me-lines", children: [_jsx4("strong", { children: memberName }), _jsx4("small", { children: profile?.email ?? readJwt(member ?? "")?.email ?? "" })] }), _jsx4("button", { className: "cs-icon-button", "data-cs-ui": "rename", type: "button", "aria-label": "Change name", onClick: () => {
3621
+ } }), _jsx6("button", { className: "cs-icon-button", type: "submit", "aria-label": "Save name", disabled: busy, children: _jsx6(Check, {}) })] }) : _jsxs6("div", { className: "cs-me-name", children: [_jsxs6("span", { className: "cs-me-lines", children: [_jsx6("strong", { children: memberName }), _jsx6("small", { children: profile?.email ?? readJwt(member ?? "")?.email ?? "" })] }), _jsx6("button", { className: "cs-icon-button", "data-cs-ui": "rename", type: "button", "aria-label": "Change name", onClick: () => {
3235
3622
  setError("");
3236
3623
  setRenaming(memberName);
3237
- }, children: _jsx4(Pencil, {}) })] })] }), error ? _jsx4("p", { className: "cs-error", children: error }) : null, _jsx4("div", { className: "cs-sep" }), _jsxs4("button", { className: "cs-item", type: "button", role: "switch", "aria-checked": !pinsHidden, onClick: togglePins, children: [_jsx4("span", { children: "Show pins" }), _jsx4("span", { className: pinsHidden ? "cs-switch" : "cs-switch is-on" })] }), _jsxs4("button", { className: "cs-item", "data-cs-ui": "mention-emails", type: "button", role: "switch", "aria-checked": profile?.mentionEmails ?? true, disabled: !profile, onClick: () => {
3624
+ }, children: _jsx6(Pencil, {}) })] })] }), error ? _jsx6("p", { className: "cs-error", children: error }) : null, _jsx6("div", { className: "cs-sep" }), _jsxs6("button", { className: "cs-item", type: "button", role: "switch", "aria-checked": !pinsHidden, onClick: togglePins, children: [_jsx6("span", { children: "Show pins" }), _jsx6("span", { className: pinsHidden ? "cs-switch" : "cs-switch is-on" })] }), _jsxs6("button", { className: "cs-item", "data-cs-ui": "mention-emails", type: "button", role: "switch", "aria-checked": profile?.mentionEmails ?? true, disabled: !profile, onClick: () => {
3238
3625
  if (!member || !profile)
3239
3626
  return;
3240
3627
  updateProfile(session, member, { mentionEmails: !profile.mentionEmails }).then((updated) => {
@@ -3243,36 +3630,46 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3243
3630
  setProfile(updated);
3244
3631
  say(updated.mentionEmails ? "Mention emails on" : "Mention emails off");
3245
3632
  }).catch((caught) => setError(caught instanceof Error ? caught.message : "Couldn\u2019t update."));
3246
- }, children: [_jsx4("span", { children: "Email me when mentioned" }), _jsx4("span", { className: profile?.mentionEmails ?? true ? "cs-switch is-on" : "cs-switch" })] }), _jsxs4("div", { className: "cs-item is-static", children: [_jsx4("span", { children: "Position" }), _jsx4("span", { className: "cs-segment", role: "radiogroup", "aria-label": "Toolbar position", children: positions.map((option) => _jsx4("button", { type: "button", role: "radio", "aria-checked": barPosition === option.value, className: barPosition === option.value ? "is-on" : "", onClick: () => savePrefs({ ...prefs, position: option.value }), children: option.label }, option.value)) })] }), _jsx4("div", { className: "cs-sep" }), _jsx4("button", { className: "cs-item", "data-cs-ui": "invite", type: "button", disabled: busy, onClick: () => {
3633
+ }, children: [_jsx6("span", { children: "Email me when mentioned" }), _jsx6("span", { className: profile?.mentionEmails ?? true ? "cs-switch is-on" : "cs-switch" })] }), _jsxs6("div", { className: "cs-item is-static", children: [_jsx6("span", { children: "Position" }), _jsx6("span", { className: "cs-segment", role: "radiogroup", "aria-label": "Toolbar position", children: positions.map((option) => _jsx6("button", { type: "button", role: "radio", "aria-checked": barPosition === option.value, className: barPosition === option.value ? "is-on" : "", onClick: () => savePrefs({ ...prefs, position: option.value }), children: option.label }, option.value)) })] }), _jsxs6("button", { className: "cs-item", "data-cs-ui": "settings-open", type: "button", onClick: () => {
3634
+ setError("");
3635
+ setSurface("settings");
3636
+ }, children: [_jsx6("span", { children: "Comment fields" }), _jsx6("span", { className: "cs-hint", children: detailsEnabled(settings) ? "On" : "Off" })] }), _jsx6("div", { className: "cs-sep" }), _jsx6("button", { className: "cs-item", "data-cs-ui": "invite", type: "button", disabled: busy, onClick: () => {
3247
3637
  if (!member)
3248
3638
  return;
3249
3639
  setBusy(true);
3250
3640
  createInvite(session, member).then(({ invite: code }) => {
3251
- const url = new URL(window.location.href);
3252
- url.search = "";
3253
- url.hash = "";
3254
- url.searchParams.set("cs-invite", code);
3255
- const link = url.toString();
3641
+ const link = linkWith(pageHref(), "cs-invite", code);
3256
3642
  setInviteLink(link);
3257
3643
  return navigator.clipboard.writeText(link).then(() => say("Invite link copied")).catch(() => say("Invite link ready"));
3258
3644
  }).catch((caught) => setError(caught instanceof Error ? caught.message : "Couldn\u2019t create an invite.")).finally(() => setBusy(false));
3259
- }, children: _jsx4("span", { children: "Invite someone" }) }), inviteLink ? _jsxs4("div", { className: "cs-invite", children: [_jsx4("input", { "data-cs-ui": "invite-link", "aria-label": "Invite link", readOnly: true, value: inviteLink, onFocus: (event) => event.currentTarget.select() }), _jsx4("p", { className: "cs-hint", children: "Anyone with this link can join for 7 days. Changing the passphrase cancels it." })] }) : null, _jsx4("button", { className: "cs-item", "data-cs-ui": "lock", type: "button", onClick: lock, children: _jsx4("span", { children: "Lock" }) }), _jsx4("button", { className: "cs-item is-danger", "data-cs-ui": "reset", type: "button", onClick: () => setConfirmReset(true), children: _jsx4("span", { children: "Reset on this device\u2026" }) }), _jsx4("p", { className: "cs-hint", children: "C comment \xB7 I list \xB7 H pins" })] }) });
3260
- const inbox = _jsxs4("section", { className: "cs-card cs-sheet cs-hit", "data-cs-ui": "panel", "aria-label": "Comments", children: [_jsxs4("header", { className: "cs-sheet-head", children: [_jsx4("h2", { children: "Comments" }), _jsx4("button", { className: "cs-icon-button", type: "button", "aria-label": "Close comments", onClick: () => setSurface("none"), children: _jsx4(Close, {}) })] }), _jsx4("div", { className: "cs-tabs", role: "tablist", children: [
3645
+ }, children: _jsx6("span", { children: "Invite someone" }) }), inviteLink ? _jsxs6("div", { className: "cs-invite", children: [_jsx6("input", { "data-cs-ui": "invite-link", "aria-label": "Invite link", readOnly: true, value: inviteLink, onFocus: (event) => event.currentTarget.select() }), _jsx6("p", { className: "cs-hint", children: "Anyone with this link can join for 7 days. Changing the passphrase cancels it." })] }) : null, _jsx6("button", { className: "cs-item", "data-cs-ui": "lock", type: "button", onClick: lock, children: _jsx6("span", { children: "Lock" }) }), _jsx6("button", { className: "cs-item is-danger", "data-cs-ui": "reset", type: "button", onClick: () => setConfirmReset(true), children: _jsx6("span", { children: "Reset on this device\u2026" }) }), _jsx6("p", { className: "cs-hint", children: "C comment \xB7 I list \xB7 H pins" })] }) });
3646
+ const tabsBar = _jsx6("div", { className: "cs-tabs", role: "tablist", children: [
3261
3647
  ["page", "This page", counts.page, "scope-page"],
3262
3648
  ["all", "All pages", counts.all, "scope-all"],
3263
3649
  ["resolved", "Resolved", counts.resolved, "scope-resolved"]
3264
- ].map(([value, label, count, hook]) => _jsxs4("button", { type: "button", role: "tab", "aria-selected": tab === value, "data-cs-ui": hook, className: tab === value ? "is-on" : "", onClick: () => setTab(value), children: [label, count > 0 ? _jsx4("span", { className: "cs-count", children: count }) : null] }, value)) }), error ? _jsx4("p", { className: "cs-error cs-pad", "data-cs-ui": "error", children: error }) : null, _jsxs4("div", { className: "cs-list", children: [listed.length === 0 ? _jsxs4("div", { className: "cs-empty", children: [_jsx4("p", { children: tab === "resolved" ? "Nothing resolved yet." : tab === "page" ? "No comments on this page." : "No open comments." }), tab !== "resolved" ? _jsxs4("button", { className: "cs-button", type: "button", onClick: startComment, children: [_jsx4(AddComment, {}), " Add a comment"] }) : null] }) : null, groups.map((group) => _jsxs4("div", { children: [group.route ? _jsx4("div", { className: "cs-group", children: group.route === route ? "This page" : group.route }) : null, group.items.map((item) => {
3650
+ ].map(([value, label, count, hook]) => _jsxs6("button", { type: "button", role: "tab", "aria-selected": tab === value, "data-cs-ui": hook, className: tab === value ? "is-on" : "", onClick: () => setTab(value), children: [label, count > 0 ? _jsx6("span", { className: "cs-count", children: count }) : null] }, value)) });
3651
+ const setViewPref = (next) => {
3652
+ setView(next);
3653
+ savePrefs({ ...prefs, view: next });
3654
+ };
3655
+ const table = _jsx6(CommentTable, { rows: listed, route, activeId, tabs: tabsBar, onList: () => setViewPref("list"), onClose: () => setSurface("none"), onSelect: (row) => {
3656
+ setView("list");
3657
+ const item = placed.find((candidate) => candidate.thread.id === row.thread.id);
3658
+ if (item)
3659
+ select(item, true);
3660
+ } });
3661
+ const inbox = _jsxs6("section", { className: "cs-card cs-sheet cs-hit", "data-cs-ui": "panel", "aria-label": "Comments", children: [_jsxs6("header", { className: "cs-sheet-head", children: [_jsx6("h2", { children: "Comments" }), _jsx6("button", { className: "cs-icon-button cs-push", "data-cs-ui": "table-view", type: "button", "aria-label": "Table view", onClick: () => setViewPref("table"), children: _jsx6(Table, {}) }), _jsx6("button", { className: "cs-icon-button", type: "button", "aria-label": "Close comments", onClick: () => setSurface("none"), children: _jsx6(Close, {}) })] }), tabsBar, error ? _jsx6("p", { className: "cs-error cs-pad", "data-cs-ui": "error", children: error }) : null, _jsxs6("div", { className: "cs-list", children: [listed.length === 0 ? _jsxs6("div", { className: "cs-empty", children: [_jsx6("p", { children: tab === "resolved" ? "Nothing resolved yet." : tab === "page" ? "No comments on this page." : "No open comments." }), tab !== "resolved" ? _jsxs6("button", { className: "cs-button", type: "button", onClick: startComment, children: [_jsx6(AddComment, {}), " Add a comment"] }) : null] }) : null, groups.map((group) => _jsxs6("div", { children: [group.route ? _jsx6("div", { className: "cs-group", children: group.route === route ? "This page" : group.route }) : null, group.items.map((item) => {
3265
3662
  const { thread } = item;
3266
3663
  const expanded = activeId === thread.id;
3267
3664
  const first = thread.messages[0];
3268
3665
  const note = statusNote(item.status, thread.anchor, thread.route === route);
3269
3666
  const onPage = item.status === "pinned" || item.status === "drifted";
3270
- return _jsxs4("article", { className: expanded ? "cs-thread is-open" : "cs-thread", "data-cs-ui": "thread", "data-thread-id": thread.id, "data-cs-status": item.status, children: [_jsxs4("div", { className: "cs-thread-main", role: "button", tabIndex: 0, "aria-expanded": expanded, "aria-label": `${thread.createdBy}: ${first?.body ?? ""}`, onClick: () => setActiveId(expanded ? null : thread.id), onKeyDown: (event) => {
3667
+ return _jsxs6("article", { className: expanded ? "cs-thread is-open" : "cs-thread", "data-cs-ui": "thread", "data-thread-id": thread.id, "data-cs-status": item.status, children: [_jsxs6("div", { className: "cs-thread-main", role: "button", tabIndex: 0, "aria-expanded": expanded, "aria-label": `${thread.createdBy}: ${first?.body ?? ""}`, onClick: () => select(item), onKeyDown: (event) => {
3271
3668
  if (event.key === "Enter" || event.key === " ") {
3272
3669
  event.preventDefault();
3273
- setActiveId(expanded ? null : thread.id);
3670
+ select(item);
3274
3671
  }
3275
- }, onMouseEnter: () => setPeekId(null), children: [_jsx4(Avatar, { name: thread.createdBy }), _jsxs4("div", { className: "cs-thread-body", children: [_jsxs4("div", { className: "cs-meta", children: [_jsx4("strong", { children: thread.createdBy }), _jsx4("time", { dateTime: thread.createdAt, children: ago(thread.createdAt) })] }), _jsx4("p", { className: "cs-text", children: _jsx4(MessageText, { body: first?.body ?? "", mentions: first?.mentions }) }), _jsx4(MessageAttachments, { attachments: first?.attachments }), first && deliveryText(deliveries[first.id]) ? _jsx4("div", { className: "cs-delivery", "data-cs-ui": "delivery", children: deliveryText(deliveries[first.id]) }) : null, _jsxs4("div", { className: "cs-target", children: [_jsx4("span", { title: thread.anchor.component?.file ?? void 0, children: targetName(thread.anchor) }), note ? _jsx4("span", { className: `cs-note is-${note.tone}`, children: note.text }) : null] }), !expanded && thread.messages.length > 1 ? _jsxs4("div", { className: "cs-replies", children: [thread.messages.length - 1, " ", thread.messages.length === 2 ? "reply" : "replies"] }) : null] })] }), expanded ? _jsxs4("div", { className: "cs-thread-more", children: [thread.messages.slice(1).map((message) => _jsxs4("div", { className: "cs-message", children: [_jsx4(Avatar, { name: message.author, size: 20 }), _jsxs4("div", { children: [_jsxs4("div", { className: "cs-meta", children: [_jsx4("strong", { children: message.author }), _jsx4("time", { dateTime: message.createdAt, children: ago(message.createdAt) })] }), _jsx4("p", { className: "cs-text", children: _jsx4(MessageText, { body: message.body, mentions: message.mentions }) }), _jsx4(MessageAttachments, { attachments: message.attachments }), deliveryText(deliveries[message.id]) ? _jsx4("div", { className: "cs-delivery", "data-cs-ui": "delivery", children: deliveryText(deliveries[message.id]) }) : null] })] }, message.id)), _jsxs4("form", { className: "cs-reply", onSubmit: async (event) => {
3672
+ }, onMouseEnter: () => setPeekId(null), children: [_jsx6(Avatar, { name: thread.createdBy }), _jsxs6("div", { className: "cs-thread-body", children: [_jsxs6("div", { className: "cs-meta", children: [_jsx6("strong", { children: thread.createdBy }), _jsx6("time", { dateTime: thread.createdAt, children: ago(thread.createdAt) }), _jsx6(RoleBadge, { role: thread.role })] }), first?.body ? _jsx6("p", { className: "cs-text", children: _jsx6(MessageText, { body: first.body, mentions: first.mentions }) }) : null, _jsx6(FieldValues, { fields: thread.fields, clamp: !expanded }), _jsx6(MessageAttachments, { attachments: first?.attachments }), first && deliveryText(deliveries[first.id]) ? _jsx6("div", { className: "cs-delivery", "data-cs-ui": "delivery", children: deliveryText(deliveries[first.id]) }) : null, _jsxs6("div", { className: "cs-target", children: [_jsx6("span", { title: thread.anchor.component?.file ?? void 0, children: targetName(thread.anchor) }), note ? _jsx6("span", { className: `cs-note is-${note.tone}`, children: note.text }) : null] }), !expanded && thread.messages.length > 1 ? _jsxs6("div", { className: "cs-replies", children: [thread.messages.length - 1, " ", thread.messages.length === 2 ? "reply" : "replies"] }) : null] })] }), expanded ? _jsxs6("div", { className: "cs-thread-more", children: [editingId === thread.id && settings ? _jsx6(DetailsEditor, { settings, thread, onClose: () => setEditingId(null), onSave: (patch) => updateThread(session, member, thread.id, patch).then(() => refresh()).then(() => say("Details saved")).catch((caught) => setError(caught instanceof Error ? caught.message : "Couldn\u2019t save.")) }) : null, thread.messages.slice(1).map((message) => _jsxs6("div", { className: "cs-message", children: [_jsx6(Avatar, { name: message.author, size: 20 }), _jsxs6("div", { children: [_jsxs6("div", { className: "cs-meta", children: [_jsx6("strong", { children: message.author }), _jsx6("time", { dateTime: message.createdAt, children: ago(message.createdAt) })] }), _jsx6("p", { className: "cs-text", children: _jsx6(MessageText, { body: message.body, mentions: message.mentions }) }), _jsx6(MessageAttachments, { attachments: message.attachments }), deliveryText(deliveries[message.id]) ? _jsx6("div", { className: "cs-delivery", "data-cs-ui": "delivery", children: deliveryText(deliveries[message.id]) }) : null] })] }, message.id)), _jsxs6("form", { className: "cs-reply", onSubmit: async (event) => {
3276
3673
  event.preventDefault();
3277
3674
  if (!member || !reply.trim() && !replyFiles.ids.length || replyFiles.uploading)
3278
3675
  return;
@@ -3298,7 +3695,7 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3298
3695
  }
3299
3696
  await refresh();
3300
3697
  }).catch((caught) => setError(caught instanceof Error ? caught.message : "Couldn\u2019t reply.")).finally(() => setBusy(false));
3301
- }, children: [_jsx4(MentionTextarea, { "data-cs-ui": "reply-body", "aria-label": "Reply", placeholder: "Reply\u2026 (@ to mention)", rows: 1, value: reply, onValueChange: setReply, members, selfEmail: readJwt(member ?? "")?.email, onMention: (person) => {
3698
+ }, children: [_jsx6(MentionTextarea, { "data-cs-ui": "reply-body", "aria-label": "Reply", placeholder: "Reply\u2026 (@ to mention)", rows: 1, value: reply, onValueChange: setReply, members, selfEmail: readJwt(member ?? "")?.email, onMention: (person) => {
3302
3699
  replyPicked.current.set(person.id, person);
3303
3700
  const element = thread.route === route ? resolveInDocument(thread.anchor, route).element : null;
3304
3701
  if (element && !replyShot.current)
@@ -3309,25 +3706,25 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3309
3706
  event.preventDefault();
3310
3707
  replyFiles.add(files);
3311
3708
  }
3312
- } }), _jsx4(AttachButtons, { onFiles: replyFiles.add, capturing, onSnapshot: () => snapshot(thread.route === route ? resolveInDocument(thread.anchor, route).element : null, replyFiles.add) }), _jsx4("button", { className: "cs-send", "data-cs-ui": "reply-submit", type: "submit", "aria-label": "Post reply", disabled: busy || replyFiles.uploading || !reply.trim() && !replyFiles.ids.length, children: _jsx4(Send, {}) })] }), _jsx4(AttachmentTray, { items: replyFiles.items, onRemove: replyFiles.remove }), _jsxs4("div", { className: "cs-actions", children: [_jsxs4("button", { className: "cs-button", "data-cs-ui": "resolve", type: "button", onClick: () => {
3709
+ } }), _jsx6(AttachButtons, { onFiles: replyFiles.add, capturing, onSnapshot: () => snapshot(thread.route === route ? resolveInDocument(thread.anchor, route).element : null, replyFiles.add) }), _jsx6("button", { className: "cs-send", "data-cs-ui": "reply-submit", type: "submit", "aria-label": "Post reply", disabled: busy || replyFiles.uploading || !reply.trim() && !replyFiles.ids.length, children: _jsx6(Send, {}) })] }), _jsx6(AttachmentTray, { items: replyFiles.items, onRemove: replyFiles.remove }), _jsxs6("div", { className: "cs-actions", children: [_jsxs6("button", { className: "cs-button", "data-cs-ui": "resolve", type: "button", onClick: () => {
3313
3710
  if (!member)
3314
3711
  return;
3315
3712
  const next = thread.status === "resolved" ? "open" : "resolved";
3316
3713
  updateThread(session, member, thread.id, { status: next }).then(() => refresh()).then(() => say(next === "resolved" ? "Resolved" : "Reopened")).catch((caught) => setError(caught instanceof Error ? caught.message : "Couldn\u2019t update."));
3317
- }, children: [thread.status === "resolved" ? _jsx4(Undo, {}) : _jsx4(Check, {}), thread.status === "resolved" ? "Reopen" : "Resolve"] }), onPage && thread.status === "open" ? _jsxs4("button", { className: "cs-button", type: "button", onClick: () => showOnPage(thread), children: [_jsx4(Target, {}), " Show"] }) : null, !onPage && thread.status === "open" && thread.route === route ? _jsxs4("button", { className: "cs-button", "data-cs-ui": "reattach", type: "button", onClick: () => {
3714
+ }, children: [thread.status === "resolved" ? _jsx6(Undo, {}) : _jsx6(Check, {}), thread.status === "resolved" ? "Reopen" : "Resolve"] }), onPage && thread.status === "open" ? _jsxs6("button", { className: "cs-button", type: "button", onClick: () => showOnPage(thread), children: [_jsx6(Target, {}), " Show"] }) : null, detailsEnabled(settings) && editingId !== thread.id ? _jsxs6("button", { className: "cs-button", "data-cs-ui": "details-edit", type: "button", onClick: () => setEditingId(thread.id), children: [_jsx6(Pencil, {}), " Details"] }) : null, !onPage && thread.status === "open" && thread.route === route ? _jsxs6("button", { className: "cs-button", "data-cs-ui": "reattach", type: "button", onClick: () => {
3318
3715
  setReattachId(thread.id);
3319
3716
  setMode("reattach");
3320
3717
  setSurface("none");
3321
- }, children: [_jsx4(Relink, {}), " Reattach"] }) : null] })] }) : null] }, thread.id);
3718
+ }, children: [_jsx6(Relink, {}), " Reattach"] }) : null] })] }) : null] }, thread.id);
3322
3719
  })] }, group.route ?? "all"))] })] });
3323
- const ui = _jsxs4("div", { id: "cs-root", className: [
3720
+ const ui = _jsxs6("div", { id: "cs-root", className: [
3324
3721
  inside ? "cs-inside" : "cs-fixed",
3325
3722
  `cs-at-${barPosition.replace("bottom-", "")}`,
3326
3723
  theme === "dark" || theme === "system" && systemDark ? "cs-dark" : ""
3327
- ].join(" "), children: [highlight ? _jsxs4(_Fragment2, { children: [_jsx4("div", { className: "cs-outline", style: { top: highlight.top, left: highlight.left, width: highlight.width, height: highlight.height } }), _jsx4("div", { className: "cs-outline-label", style: { top: Math.max(4, highlight.top - 26), left: highlight.left }, children: highlight.label })] }) : null, flash ? _jsx4("div", { className: "cs-outline is-flash", style: { top: flash.top, left: flash.left, width: flash.width, height: flash.height } }) : null, mode !== "browse" ? _jsxs4("div", { className: "cs-banner cs-hit", "data-cs-ui": "mode", children: [_jsx4(Cursor, {}), _jsx4("span", { children: mode === "reattach" ? "Click the element this comment belongs to" : "Click anything to comment" }), _jsx4("button", { type: "button", onClick: () => {
3724
+ ].join(" "), children: [highlight ? _jsxs6(_Fragment3, { children: [_jsx6("div", { className: "cs-outline", style: { top: highlight.top, left: highlight.left, width: highlight.width, height: highlight.height } }), _jsx6("div", { className: "cs-outline-label", style: { top: Math.max(4, highlight.top - 26), left: highlight.left }, children: highlight.label })] }) : null, flash ? _jsx6("div", { className: "cs-outline is-flash", style: { top: flash.top, left: flash.left, width: flash.width, height: flash.height } }) : null, mode !== "browse" ? _jsxs6("div", { className: "cs-banner cs-hit", "data-cs-ui": "mode", children: [_jsx6(Cursor, {}), _jsx6("span", { children: mode === "reattach" ? "Click the element this comment belongs to" : "Click anything to comment" }), _jsx6("button", { type: "button", onClick: () => {
3328
3725
  setMode("browse");
3329
3726
  setReattachId(null);
3330
- }, children: "Cancel" })] }) : null, pinsHidden ? null : placed.map((item) => item.top == null || item.left == null || item.status !== "pinned" && item.status !== "drifted" ? null : _jsxs4("button", { type: "button", className: [
3727
+ }, children: "Cancel" })] }) : null, pinsHidden ? null : placed.map((item) => item.top == null || item.left == null || item.status !== "pinned" && item.status !== "drifted" ? null : _jsxs6("button", { type: "button", className: [
3331
3728
  "cs-pin",
3332
3729
  item.status === "drifted" ? "is-drifted" : "",
3333
3730
  activeId === item.thread.id && surface === "inbox" ? "is-active" : ""
@@ -3336,9 +3733,9 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3336
3733
  setTab("page");
3337
3734
  setActiveId(item.thread.id);
3338
3735
  setSurface("inbox");
3339
- }, children: [_jsx4(Avatar, { name: item.thread.createdBy, size: 22 }), item.thread.messages.length > 1 ? _jsx4("span", { className: "cs-pin-count", children: item.thread.messages.length }) : null] }, item.thread.id)), peek && !pinsHidden && peek.top != null && peek.left != null ? _jsxs4("div", { className: "cs-peek", style: { top: peek.top + 18, left: Math.max(8, peek.left - 12) }, children: [_jsxs4("div", { className: "cs-meta", children: [_jsx4(Avatar, { name: peek.thread.createdBy, size: 18 }), _jsx4("strong", { children: peek.thread.createdBy }), _jsx4("time", { children: ago(peek.thread.createdAt) })] }), _jsx4("p", { className: "cs-text", children: _jsx4(MessageText, { body: peek.thread.messages[0]?.body ?? "", mentions: peek.thread.messages[0]?.mentions }) }), peek.thread.messages.length > 1 ? _jsxs4("div", { className: "cs-replies", children: [peek.thread.messages.length - 1, " ", peek.thread.messages.length === 2 ? "reply" : "replies"] }) : null] }) : null, draft ? _jsxs4("form", { className: "cs-card cs-composer cs-hit", "data-cs-ui": "composer", style: { top: draft.top, left: draft.left }, onSubmit: async (event) => {
3736
+ }, children: [_jsx6(Avatar, { name: item.thread.createdBy, size: 22 }), item.thread.messages.length > 1 ? _jsx6("span", { className: "cs-pin-count", children: item.thread.messages.length }) : null] }, item.thread.id)), peek && !pinsHidden && peek.top != null && peek.left != null ? _jsxs6("div", { className: "cs-peek", style: { top: peek.top + 18, left: Math.max(8, peek.left - 12) }, children: [_jsxs6("div", { className: "cs-meta", children: [_jsx6(Avatar, { name: peek.thread.createdBy, size: 18 }), _jsx6("strong", { children: peek.thread.createdBy }), _jsx6("time", { children: ago(peek.thread.createdAt) })] }), _jsx6("p", { className: "cs-text", children: _jsx6(MessageText, { body: peek.thread.messages[0]?.body ?? "", mentions: peek.thread.messages[0]?.mentions }) }), peek.thread.messages.length > 1 ? _jsxs6("div", { className: "cs-replies", children: [peek.thread.messages.length - 1, " ", peek.thread.messages.length === 2 ? "reply" : "replies"] }) : null] }) : null, draft ? _jsxs6("form", { className: "cs-card cs-composer cs-hit", "data-cs-ui": "composer", style: { top: draft.top, left: draft.left }, onSubmit: async (event) => {
3340
3737
  event.preventDefault();
3341
- if (!member || !body.trim() && !draftFiles.ids.length || draftFiles.uploading)
3738
+ if (!member || !canPost || draftFiles.uploading)
3342
3739
  return;
3343
3740
  setBusy(true);
3344
3741
  const picked = mentionsIn(body, draftPicked.current);
@@ -3346,10 +3743,13 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3346
3743
  createThread(session, member, draft.anchor, body, {
3347
3744
  mentions: picked.map((person) => person.id),
3348
3745
  screenshot,
3349
- attachments: draftFiles.ids
3746
+ attachments: draftFiles.ids,
3747
+ url: pageHref(),
3748
+ ...detailsPayload(settings, draftDetails)
3350
3749
  }).then(async ({ thread, delivery }) => {
3351
3750
  setDraft(null);
3352
3751
  setBody("");
3752
+ setDraftDetails(noDetails);
3353
3753
  draftFiles.reset();
3354
3754
  setError("");
3355
3755
  draftPicked.current = /* @__PURE__ */ new Map();
@@ -3367,7 +3767,7 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3367
3767
  setSurface("inbox");
3368
3768
  await refresh();
3369
3769
  }).catch((caught) => setError(caught instanceof Error ? caught.message : "Couldn\u2019t post.")).finally(() => setBusy(false));
3370
- }, children: [_jsxs4("div", { className: "cs-composer-target", children: [_jsx4(Target, {}), _jsx4("span", { className: "cs-composer-name", children: targetName(draft.anchor) }), draft.anchor.component?.file ? _jsx4("code", { children: draft.anchor.component.file }) : null, _jsx4("button", { className: "cs-icon-button", type: "button", "aria-label": "Discard", onClick: () => setDraft(null), children: _jsx4(Close, {}) })] }), _jsxs4("div", { className: "cs-composer-body", children: [_jsx4(Avatar, { name: memberName, size: 24 }), _jsx4(MentionTextarea, { "data-cs-ui": "composer-body", "aria-label": "Comment", placeholder: "Add a comment\u2026 (@ to mention)", value: body, autoFocus: true, onValueChange: setBody, members, selfEmail: readJwt(member ?? "")?.email, onMention: (person) => {
3770
+ }, children: [_jsxs6("div", { className: "cs-composer-target", children: [_jsx6(Target, {}), _jsx6("span", { className: "cs-composer-name", children: targetName(draft.anchor) }), draft.anchor.component?.file ? _jsx6("code", { children: draft.anchor.component.file }) : null, _jsx6("button", { className: "cs-icon-button", type: "button", "aria-label": "Discard", onClick: () => setDraft(null), children: _jsx6(Close, {}) })] }), _jsxs6("div", { className: "cs-composer-body", children: [_jsx6(Avatar, { name: memberName, size: 24 }), _jsx6(MentionTextarea, { "data-cs-ui": "composer-body", "aria-label": "Comment", placeholder: "Add a comment\u2026 (@ to mention)", value: body, autoFocus: true, onValueChange: setBody, members, selfEmail: readJwt(member ?? "")?.email, onMention: (person) => {
3371
3771
  draftPicked.current.set(person.id, person);
3372
3772
  if (!draftShot.current)
3373
3773
  draftShot.current = captureAround(draft.element);
@@ -3377,7 +3777,7 @@ function CommentToolbar({ sessionPath = "/api/comment-service/session", pollMs =
3377
3777
  event.preventDefault();
3378
3778
  draftFiles.add(files);
3379
3779
  }
3380
- } })] }), _jsx4(AttachmentTray, { items: draftFiles.items, onRemove: draftFiles.remove }), error ? _jsx4("p", { className: "cs-error cs-pad", children: error }) : null, _jsxs4("div", { className: "cs-composer-foot", children: [_jsx4(AttachButtons, { onFiles: draftFiles.add, capturing, onSnapshot: () => snapshot(draft.element, draftFiles.add) }), _jsx4("span", { className: "cs-hint", children: capturing ? "Capturing\u2026" : draftFiles.uploading ? "Uploading\u2026" : busy ? "Posting\u2026" : "\u2318\u21B5 to post" }), _jsx4("button", { className: "cs-send", "data-cs-ui": "composer-submit", type: "submit", "aria-label": "Post comment", disabled: busy || draftFiles.uploading || !body.trim() && !draftFiles.ids.length, children: _jsx4(Send, {}) })] })] }) : null, surface === "unlock" && !member ? unlockCard : null, surface === "menu" && member ? menuCard : null, surface === "inbox" && member ? inbox : null, toast ? _jsx4("div", { className: "cs-toast", role: "status", children: toast }) : null, bar] });
3780
+ } })] }), _jsx6(DetailsInput, { settings, value: draftDetails, onChange: setDraftDetails, onKeyDown: submitOnCmdEnter }), _jsx6(AttachmentTray, { items: draftFiles.items, onRemove: draftFiles.remove }), error ? _jsx6("p", { className: "cs-error cs-pad", children: error }) : null, _jsxs6("div", { className: "cs-composer-foot", children: [_jsx6(AttachButtons, { onFiles: draftFiles.add, capturing, onSnapshot: () => snapshot(draft.element, draftFiles.add) }), _jsx6("span", { className: "cs-hint", children: capturing ? "Capturing\u2026" : draftFiles.uploading ? "Uploading\u2026" : busy ? "Posting\u2026" : "\u2318\u21B5 to post" }), _jsx6("button", { className: "cs-send", "data-cs-ui": "composer-submit", type: "submit", "aria-label": "Post comment", disabled: busy || draftFiles.uploading || !canPost, children: _jsx6(Send, {}) })] })] }) : null, surface === "unlock" && !member ? unlockCard : null, surface === "menu" && member ? menuCard : null, surface === "settings" && member ? _jsx6(SettingsPanel, { settings, onChange: changeSettings, onBack: () => setSurface("menu"), error }) : null, surface === "inbox" && member ? view === "table" ? table : inbox : null, toast ? _jsx6("div", { className: "cs-toast", role: "status", children: toast }) : null, bar] });
3381
3781
  return createPortal2(ui, host);
3382
3782
  }
3383
3783
  export {