@1agh/maude 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/package.json +8 -8
- package/plugins/design/dev-server/api.ts +186 -3
- package/plugins/design/dev-server/canvas-shell.tsx +79 -5
- package/plugins/design/dev-server/client/app.jsx +53 -142
- package/plugins/design/dev-server/client/comments-overlay.css +381 -0
- package/plugins/design/dev-server/client/styles/3-shell.css +1 -10
- package/plugins/design/dev-server/client/styles/4-components.css +5 -161
- package/plugins/design/dev-server/client/styles.css +5 -160
- package/plugins/design/dev-server/comments-overlay.tsx +1156 -0
- package/plugins/design/dev-server/dist/client.bundle.js +44 -211
- package/plugins/design/dev-server/dist/styles.css +1 -218
- package/plugins/design/dev-server/http.ts +29 -0
- package/plugins/design/dev-server/input-router.tsx +21 -0
- package/plugins/design/dev-server/test/canvas-meta-api.test.ts +0 -10
- package/plugins/design/dev-server/test/comments-api.test.ts +229 -0
|
@@ -1208,64 +1208,21 @@ function Gallery({ title, items, onOpen, kind }) {
|
|
|
1208
1208
|
|
|
1209
1209
|
// ---------- Comment composer / viewer ----------
|
|
1210
1210
|
|
|
1211
|
-
function CommentBar({ activePath,
|
|
1211
|
+
function CommentBar({ activePath, comments }) {
|
|
1212
|
+
// Phase 6 — the shell-side composer + chip strip + focused-row chrome were
|
|
1213
|
+
// removed. The iframe overlay (comments-overlay.tsx) owns composer + pin
|
|
1214
|
+
// bubbles + thread popover. BottomBar shrinks to a live open-count summary
|
|
1215
|
+
// so the shell still surfaces total review activity at a glance.
|
|
1212
1216
|
if (!activePath) return null;
|
|
1213
|
-
const focused = focusedId ? comments.find(c => c.id === focusedId) : null;
|
|
1214
1217
|
const openComments = (comments || []).filter(c => c.status !== 'resolved');
|
|
1218
|
+
if (openComments.length === 0) return null;
|
|
1215
1219
|
return (
|
|
1216
1220
|
<div className="comment-bar">
|
|
1217
|
-
|
|
1218
|
-
<
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
</div>
|
|
1223
|
-
<textarea
|
|
1224
|
-
autoFocus
|
|
1225
|
-
className="composer-textarea"
|
|
1226
|
-
value={draft.text}
|
|
1227
|
-
placeholder="What should change here? (⌘↵ save · Esc cancel)"
|
|
1228
|
-
onChange={e => setDraft({ ...draft, text: e.target.value })}
|
|
1229
|
-
onKeyDown={e => {
|
|
1230
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); onSubmit(); }
|
|
1231
|
-
else if (e.key === 'Escape') { e.preventDefault(); onCancel(); }
|
|
1232
|
-
}}
|
|
1233
|
-
rows={4}
|
|
1234
|
-
/>
|
|
1235
|
-
<div className="composer-actions">
|
|
1236
|
-
<button className="cb-secondary" onClick={onCancel}>Cancel</button>
|
|
1237
|
-
<button className="cb-primary" disabled={!draft.text.trim()} onClick={onSubmit}>Save · ⌘↵</button>
|
|
1238
|
-
</div>
|
|
1239
|
-
</div>
|
|
1240
|
-
)}
|
|
1241
|
-
|
|
1242
|
-
{focused && (
|
|
1243
|
-
<div className="cb-row focused">
|
|
1244
|
-
<span className="cb-pinno">#{(comments || []).filter(c => c.selector).findIndex(c => c.id === focused.id) + 1}</span>
|
|
1245
|
-
<span className="cb-text">{focused.text}</span>
|
|
1246
|
-
<span className="cb-target" title={focused.dom_path ? focused.dom_path.join(' > ') : ''}>
|
|
1247
|
-
<code>{focused.selector || '—'}</code>
|
|
1248
|
-
</span>
|
|
1249
|
-
{focused.status === 'resolved'
|
|
1250
|
-
? <button className="cb-secondary" onClick={() => onReopen(focused.id)}>Reopen</button>
|
|
1251
|
-
: <button className="cb-primary" onClick={() => onResolve(focused.id)}>✓ Resolve</button>}
|
|
1252
|
-
<button className="cb-secondary" onClick={() => onDelete(focused.id)}>Delete</button>
|
|
1253
|
-
</div>
|
|
1254
|
-
)}
|
|
1255
|
-
|
|
1256
|
-
{!draft && !focused && openComments.length > 0 && (
|
|
1257
|
-
<div className="cb-row strip">
|
|
1258
|
-
<span className="cb-label">{openComments.length} open comment{openComments.length === 1 ? '' : 's'}</span>
|
|
1259
|
-
<div className="cb-pin-strip">
|
|
1260
|
-
{openComments.slice(0, 12).map((c, i) => (
|
|
1261
|
-
<button key={c.id} className="cb-pin-chip" title={c.text} onClick={() => onFocusPin(c.id)}>
|
|
1262
|
-
{i + 1}
|
|
1263
|
-
</button>
|
|
1264
|
-
))}
|
|
1265
|
-
{openComments.length > 12 && <span className="cb-more">+{openComments.length - 12}</span>}
|
|
1266
|
-
</div>
|
|
1267
|
-
</div>
|
|
1268
|
-
)}
|
|
1221
|
+
<div className="cb-row strip">
|
|
1222
|
+
<span className="cb-label">
|
|
1223
|
+
{openComments.length} open comment{openComments.length === 1 ? '' : 's'}
|
|
1224
|
+
</span>
|
|
1225
|
+
</div>
|
|
1269
1226
|
</div>
|
|
1270
1227
|
);
|
|
1271
1228
|
}
|
|
@@ -1278,7 +1235,7 @@ function StatusBarSlot({ label, children, className = '' }) {
|
|
|
1278
1235
|
);
|
|
1279
1236
|
}
|
|
1280
1237
|
|
|
1281
|
-
function StatusBar({ activePath, selected, wsConnected, openCount, theme, onToggleTheme, onClearSelected
|
|
1238
|
+
function StatusBar({ activePath, selected, wsConnected, openCount, theme, onToggleTheme, onClearSelected }) {
|
|
1282
1239
|
const isSystem = activePath === SYSTEM_TAB;
|
|
1283
1240
|
const text = selected && selected.selector
|
|
1284
1241
|
? selected.selector + (selected.text ? ` — "${selected.text.slice(0, 60)}"` : '')
|
|
@@ -1297,9 +1254,6 @@ function StatusBar({ activePath, selected, wsConnected, openCount, theme, onTogg
|
|
|
1297
1254
|
<StatusBarSlot label="Selected element" className="sb-selected">
|
|
1298
1255
|
<span className="sb-dot" aria-hidden="true">●</span>
|
|
1299
1256
|
<span className="sb-sel-text" title={title}>{text}</span>
|
|
1300
|
-
{!hasDraft && (
|
|
1301
|
-
<button type="button" className="sb-add-comment" onClick={onAddComment} title="Add comment on selected element (⌘⇧+click in canvas)">+ comment</button>
|
|
1302
|
-
)}
|
|
1303
1257
|
<button type="button" className="sb-clear-sel" onClick={onClearSelected} title="Clear (Esc inside iframe)" aria-label="Clear selection">×</button>
|
|
1304
1258
|
</StatusBarSlot>
|
|
1305
1259
|
)}
|
|
@@ -1454,7 +1408,8 @@ function App() {
|
|
|
1454
1408
|
return () => { cancelled = true; };
|
|
1455
1409
|
}, []);
|
|
1456
1410
|
const [commentsByFile, setCommentsByFile] = useState({}); // { file: [Comment] }
|
|
1457
|
-
|
|
1411
|
+
// Phase 6 — the in-iframe composer owns drafting; the shell no longer holds
|
|
1412
|
+
// a `draft` state. Mutations route through postMessage → WS instead.
|
|
1458
1413
|
const [focusedCommentId, setFocusedCommentId] = useState(null);
|
|
1459
1414
|
const [commentsPanelOpen, setCommentsPanelOpen] = useState(false);
|
|
1460
1415
|
const [commentsFilter, setCommentsFilter] = useState('open'); // 'all' | 'open' | 'resolved'
|
|
@@ -1609,7 +1564,6 @@ function App() {
|
|
|
1609
1564
|
});
|
|
1610
1565
|
setActivePath(path);
|
|
1611
1566
|
setFocusedCommentId(null);
|
|
1612
|
-
setDraft(null);
|
|
1613
1567
|
}, []);
|
|
1614
1568
|
|
|
1615
1569
|
const openSystem = useCallback(() => {
|
|
@@ -1672,30 +1626,6 @@ function App() {
|
|
|
1672
1626
|
try { el.contentWindow.postMessage({ dgn: 'comments-set', comments: list }, '*'); } catch {}
|
|
1673
1627
|
}, [activePath, commentsByFile]);
|
|
1674
1628
|
|
|
1675
|
-
// ----- Comment composer helpers -----
|
|
1676
|
-
// Declared BEFORE the inbound-message useEffect that references them — under
|
|
1677
|
-
// ES build (no var-style hoisting) these are real TDZ violations otherwise.
|
|
1678
|
-
const startDraftFor = useCallback((sel) => {
|
|
1679
|
-
const file = (sel && sel.file) || activePath;
|
|
1680
|
-
if (!file || file === SYSTEM_TAB) return;
|
|
1681
|
-
setDraft({
|
|
1682
|
-
file,
|
|
1683
|
-
selector: sel?.selector || '',
|
|
1684
|
-
dom_path: sel?.dom_path || [],
|
|
1685
|
-
tag: sel?.tag || '',
|
|
1686
|
-
classes: sel?.classes || '',
|
|
1687
|
-
bounds: sel?.bounds || null,
|
|
1688
|
-
html: sel?.html || '',
|
|
1689
|
-
text: '',
|
|
1690
|
-
});
|
|
1691
|
-
setFocusedCommentId(null);
|
|
1692
|
-
}, [activePath]);
|
|
1693
|
-
|
|
1694
|
-
const startDraftFromSelection = useCallback(() => {
|
|
1695
|
-
if (!selected || !selected.selector) return;
|
|
1696
|
-
startDraftFor(selected);
|
|
1697
|
-
}, [selected, startDraftFor]);
|
|
1698
|
-
|
|
1699
1629
|
// ----- Inbound messages from iframes -----
|
|
1700
1630
|
useEffect(() => {
|
|
1701
1631
|
function onMessage(e) {
|
|
@@ -1728,13 +1658,40 @@ function App() {
|
|
|
1728
1658
|
wsSend({ type: 'clear-select' });
|
|
1729
1659
|
setSelected(null);
|
|
1730
1660
|
} else if (m.dgn === 'comment-compose' && m.selection) {
|
|
1731
|
-
//
|
|
1732
|
-
//
|
|
1733
|
-
startDraftFor
|
|
1734
|
-
|
|
1735
|
-
//
|
|
1736
|
-
//
|
|
1737
|
-
|
|
1661
|
+
// Phase 6 — the iframe overlay owns the composer surface now. The
|
|
1662
|
+
// shell just mirrors `selected` so the StatusBar / sidebar still
|
|
1663
|
+
// reflect the target, and skips the legacy `startDraftFor` path that
|
|
1664
|
+
// opened the shell-side composer. Legacy `.html` mocks (no
|
|
1665
|
+
// canvas-shell mount) fall through to the same path; they lose the
|
|
1666
|
+
// shell composer in this phase. Acceptable per Phase 6 scope.
|
|
1667
|
+
setSelected(m.selection);
|
|
1668
|
+
} else if (m.dgn === 'comment-submit' && m.payload && typeof m.payload.text === 'string') {
|
|
1669
|
+
// Phase 6 — iframe overlay finished composing. Relay through the
|
|
1670
|
+
// existing WS `comments-add` channel; server-side persistence +
|
|
1671
|
+
// broadcast back are identical to the legacy shell-composer flow.
|
|
1672
|
+
const p = m.payload;
|
|
1673
|
+
const txt = String(p.text).trim();
|
|
1674
|
+
if (txt) {
|
|
1675
|
+
wsSend({
|
|
1676
|
+
type: 'comments-add',
|
|
1677
|
+
payload: {
|
|
1678
|
+
file: p.file,
|
|
1679
|
+
selector: p.selector,
|
|
1680
|
+
dom_path: p.dom_path,
|
|
1681
|
+
tag: p.tag,
|
|
1682
|
+
classes: p.classes,
|
|
1683
|
+
bounds: p.bounds,
|
|
1684
|
+
html_excerpt: p.html_excerpt,
|
|
1685
|
+
text: txt,
|
|
1686
|
+
},
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
} else if (m.dgn === 'comment-patch' && m.id && m.patch && typeof m.patch === 'object') {
|
|
1690
|
+
// Phase 6 — thread popover routes resolve / reopen through here.
|
|
1691
|
+
wsSend({ type: 'comments-patch', id: m.id, patch: m.patch });
|
|
1692
|
+
} else if (m.dgn === 'comment-delete' && m.id) {
|
|
1693
|
+
wsSend({ type: 'comments-delete', id: m.id });
|
|
1694
|
+
setFocusedCommentId(prev => (prev === m.id ? null : prev));
|
|
1738
1695
|
} else if (m.dgn === 'comment-click' && m.id) {
|
|
1739
1696
|
setFocusedCommentId(m.id);
|
|
1740
1697
|
} else if (m.dgn === 'loaded' && m.file) {
|
|
@@ -1751,7 +1708,7 @@ function App() {
|
|
|
1751
1708
|
}
|
|
1752
1709
|
window.addEventListener('message', onMessage);
|
|
1753
1710
|
return () => window.removeEventListener('message', onMessage);
|
|
1754
|
-
}, [commentsByFile, focusedCommentId
|
|
1711
|
+
}, [commentsByFile, focusedCommentId]);
|
|
1755
1712
|
|
|
1756
1713
|
// Tell the active canvas iframe to drop any persistent selection (canvas
|
|
1757
1714
|
// SelectionSet) — used when the comment composer closes via submit /
|
|
@@ -1765,27 +1722,6 @@ function App() {
|
|
|
1765
1722
|
}
|
|
1766
1723
|
}, [activePath]);
|
|
1767
1724
|
|
|
1768
|
-
const submitDraft = useCallback(() => {
|
|
1769
|
-
if (!draft || !draft.text.trim()) return;
|
|
1770
|
-
wsSend({ type: 'comments-add', payload: {
|
|
1771
|
-
file: draft.file,
|
|
1772
|
-
selector: draft.selector,
|
|
1773
|
-
dom_path: draft.dom_path,
|
|
1774
|
-
tag: draft.tag,
|
|
1775
|
-
classes: draft.classes,
|
|
1776
|
-
bounds: draft.bounds,
|
|
1777
|
-
html_excerpt: draft.html,
|
|
1778
|
-
text: draft.text.trim(),
|
|
1779
|
-
}});
|
|
1780
|
-
setDraft(null);
|
|
1781
|
-
clearActiveCanvasSelection();
|
|
1782
|
-
}, [draft, clearActiveCanvasSelection]);
|
|
1783
|
-
|
|
1784
|
-
const cancelDraft = useCallback(() => {
|
|
1785
|
-
setDraft(null);
|
|
1786
|
-
clearActiveCanvasSelection();
|
|
1787
|
-
}, [clearActiveCanvasSelection]);
|
|
1788
|
-
|
|
1789
1725
|
const resolveComment = useCallback((id) => {
|
|
1790
1726
|
wsSend({ type: 'comments-patch', id, patch: { status: 'resolved' } });
|
|
1791
1727
|
}, []);
|
|
@@ -1797,16 +1733,6 @@ function App() {
|
|
|
1797
1733
|
setFocusedCommentId(prev => (prev === id ? null : prev));
|
|
1798
1734
|
}, []);
|
|
1799
1735
|
|
|
1800
|
-
const focusPinFromBar = useCallback((id) => {
|
|
1801
|
-
setFocusedCommentId(id);
|
|
1802
|
-
if (activePath && activePath !== SYSTEM_TAB) {
|
|
1803
|
-
const el = iframesRef.current.get(activePath);
|
|
1804
|
-
if (el && el.contentWindow) {
|
|
1805
|
-
try { el.contentWindow.postMessage({ dgn: 'comment-focus', id }, '*'); } catch {}
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
}, [activePath]);
|
|
1809
|
-
|
|
1810
1736
|
// Jump from right-sidebar list to a comment: open file tab if needed, focus pin.
|
|
1811
1737
|
// The iframe may be freshly mounted; the loaded handler also re-sends focus if focusedCommentId matches.
|
|
1812
1738
|
const jumpToComment = useCallback((file, id) => {
|
|
@@ -1918,15 +1844,15 @@ function App() {
|
|
|
1918
1844
|
setHelpOpen(true);
|
|
1919
1845
|
return;
|
|
1920
1846
|
}
|
|
1921
|
-
// Esc —
|
|
1847
|
+
// Esc — clear focused pin. The in-place composer (Phase 6) and thread
|
|
1848
|
+
// popover handle their own Esc inside the iframe.
|
|
1922
1849
|
if (e.key === 'Escape') {
|
|
1923
|
-
if (draft) { setDraft(null); clearActiveCanvasSelection(); return; }
|
|
1924
1850
|
if (focusedCommentId) { setFocusedCommentId(null); return; }
|
|
1925
1851
|
}
|
|
1926
1852
|
}
|
|
1927
1853
|
window.addEventListener('keydown', onKey);
|
|
1928
1854
|
return () => window.removeEventListener('keydown', onKey);
|
|
1929
|
-
}, [reloadActive, selected, activePath,
|
|
1855
|
+
}, [reloadActive, selected, activePath, focusedCommentId, sidebarOpen, openSystem, closeTab, clearActiveCanvasSelection]);
|
|
1930
1856
|
|
|
1931
1857
|
const registerIframe = useCallback((path, el) => {
|
|
1932
1858
|
if (el) iframesRef.current.set(path, el);
|
|
@@ -1979,20 +1905,7 @@ function App() {
|
|
|
1979
1905
|
cfg={cfg}
|
|
1980
1906
|
/>
|
|
1981
1907
|
{activePath && activePath !== SYSTEM_TAB && (
|
|
1982
|
-
<CommentBar
|
|
1983
|
-
activePath={activePath}
|
|
1984
|
-
selected={selected}
|
|
1985
|
-
comments={activeFileComments}
|
|
1986
|
-
focusedId={focusedCommentId}
|
|
1987
|
-
draft={draft && draft.file === activePath ? draft : null}
|
|
1988
|
-
setDraft={setDraft}
|
|
1989
|
-
onSubmit={submitDraft}
|
|
1990
|
-
onCancel={cancelDraft}
|
|
1991
|
-
onResolve={resolveComment}
|
|
1992
|
-
onReopen={reopenComment}
|
|
1993
|
-
onDelete={deleteComment}
|
|
1994
|
-
onFocusPin={focusPinFromBar}
|
|
1995
|
-
/>
|
|
1908
|
+
<CommentBar activePath={activePath} comments={activeFileComments} />
|
|
1996
1909
|
)}
|
|
1997
1910
|
<StatusBar
|
|
1998
1911
|
activePath={activePath}
|
|
@@ -2002,8 +1915,6 @@ function App() {
|
|
|
2002
1915
|
theme={theme}
|
|
2003
1916
|
onToggleTheme={toggleTheme}
|
|
2004
1917
|
onClearSelected={clearSelected}
|
|
2005
|
-
onAddComment={startDraftFromSelection}
|
|
2006
|
-
hasDraft={!!(draft && draft.file === activePath)}
|
|
2007
1918
|
/>
|
|
2008
1919
|
</div>
|
|
2009
1920
|
{commentsPanelOpen && (
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/* Phase 6 — in-place comments overlay
|
|
2
|
+
*
|
|
3
|
+
* Lives inside the canvas iframe; portaled into `.dc-world` so CSS zoom on the
|
|
4
|
+
* world plane scales the entire layer (pins + composer + thread popover)
|
|
5
|
+
* uniformly with the artboards.
|
|
6
|
+
*
|
|
7
|
+
* DS-token only. No raw hex literals, no glow shadows, no rounded > 4px, no
|
|
8
|
+
* gradients. The design-system-guard subagent enforces this on PR — keep edits
|
|
9
|
+
* inside the token vocabulary.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
.cm-layer {
|
|
13
|
+
position: fixed;
|
|
14
|
+
inset: 0;
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
/* Pins / composer / thread sit ABOVE the SelectionHalos chrome (z-index 5
|
|
17
|
+
* in canvas-shell HALO_CSS). Rendered as a sibling of `.dc-canvas` (NOT
|
|
18
|
+
* portaled into `.dc-world`) so we share the root stacking context with
|
|
19
|
+
* the halos — z-index here directly competes with theirs. Bonus: pins stay
|
|
20
|
+
* fixed 24px regardless of canvas zoom, FigJam-style. */
|
|
21
|
+
z-index: 10;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ─── Pin badge ───────────────────────────────────────────────────────────── */
|
|
25
|
+
.cm-pin {
|
|
26
|
+
position: absolute;
|
|
27
|
+
width: 24px;
|
|
28
|
+
height: 24px;
|
|
29
|
+
padding: 0;
|
|
30
|
+
margin: 0;
|
|
31
|
+
background: var(--accent, #d63b1f);
|
|
32
|
+
color: var(--accent-fg, #faf6ef);
|
|
33
|
+
border: 1px solid var(--border-strong, #2a2520);
|
|
34
|
+
border-radius: 0;
|
|
35
|
+
font: 600 var(--type-xs, 11px) / 1 var(--font-mono, ui-monospace, monospace);
|
|
36
|
+
letter-spacing: var(--tracking-sku, 0.04em);
|
|
37
|
+
display: grid;
|
|
38
|
+
place-items: center;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
pointer-events: auto;
|
|
41
|
+
user-select: none;
|
|
42
|
+
transition: background var(--dur-flip, 120ms) var(--ease-out, ease-out);
|
|
43
|
+
will-change: transform;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.cm-pin:hover,
|
|
47
|
+
.cm-pin[aria-expanded='true'] {
|
|
48
|
+
background: var(--accent-hover, color-mix(in oklab, var(--accent, #d63b1f) 90%, white));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.cm-pin:focus-visible {
|
|
52
|
+
outline: none;
|
|
53
|
+
box-shadow: var(--shadow-focus, 0 0 0 2px var(--accent, #d63b1f));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.cm-pin[data-resolved='true'] {
|
|
57
|
+
background: var(--bg-3, #1c1916);
|
|
58
|
+
color: var(--fg-2, #6e6660);
|
|
59
|
+
opacity: 0.6;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.cm-pin[data-focused='true'] {
|
|
63
|
+
/* Subtle ring — DS hard-edges discipline: no glow, just a hairline outline. */
|
|
64
|
+
outline: 1px solid var(--fg-0, #2a2520);
|
|
65
|
+
outline-offset: 1px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ─── Composer card ───────────────────────────────────────────────────────── */
|
|
69
|
+
.cm-composer {
|
|
70
|
+
position: absolute;
|
|
71
|
+
width: 300px;
|
|
72
|
+
background: var(--bg-1, #faf6ef);
|
|
73
|
+
border: var(--rule-thin, 1px solid #2a2520);
|
|
74
|
+
border-radius: var(--radius-sm, 2px);
|
|
75
|
+
padding: var(--space-4, 12px);
|
|
76
|
+
box-shadow: none;
|
|
77
|
+
font: var(--type-sm, 13px) / var(--lh-sm, 1.45) var(--font-mono, ui-monospace, monospace);
|
|
78
|
+
color: var(--fg-0, #2a2520);
|
|
79
|
+
pointer-events: auto;
|
|
80
|
+
z-index: 4;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.cm-composer__head {
|
|
84
|
+
font-size: var(--type-xs, 11px);
|
|
85
|
+
color: var(--fg-2, #6e6660);
|
|
86
|
+
letter-spacing: var(--tracking-eyebrow, 0.08em);
|
|
87
|
+
text-transform: uppercase;
|
|
88
|
+
margin: 0 0 var(--space-2, 4px) 0;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: var(--space-2, 4px);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.cm-composer__selector {
|
|
95
|
+
color: var(--fg-1, #4a4540);
|
|
96
|
+
background: var(--mono-cell-bg, color-mix(in oklab, var(--bg-1, #faf6ef) 80%, var(--accent, #d63b1f) 6%));
|
|
97
|
+
padding: 0 var(--space-2, 4px);
|
|
98
|
+
border: 1px solid var(--mono-rule, var(--border-subtle, #c8bfb4));
|
|
99
|
+
font-size: var(--type-xs, 11px);
|
|
100
|
+
letter-spacing: 0;
|
|
101
|
+
text-transform: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.cm-composer__textarea {
|
|
105
|
+
width: 100%;
|
|
106
|
+
min-height: 72px;
|
|
107
|
+
resize: vertical;
|
|
108
|
+
background: var(--bg-0, #ffffff);
|
|
109
|
+
color: var(--fg-0, #2a2520);
|
|
110
|
+
border: var(--rule-thin, 1px solid #2a2520);
|
|
111
|
+
border-radius: 0;
|
|
112
|
+
padding: var(--space-3, 8px);
|
|
113
|
+
font: inherit;
|
|
114
|
+
box-sizing: border-box;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.cm-composer__textarea:focus-visible {
|
|
118
|
+
outline: none;
|
|
119
|
+
border-color: var(--accent, #d63b1f);
|
|
120
|
+
box-shadow: var(--shadow-focus, 0 0 0 2px var(--accent, #d63b1f));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.cm-composer__actions {
|
|
124
|
+
display: flex;
|
|
125
|
+
gap: var(--space-2, 4px);
|
|
126
|
+
justify-content: flex-end;
|
|
127
|
+
margin-top: var(--space-3, 8px);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.cm-btn {
|
|
131
|
+
font: 500 var(--type-xs, 11px) / 1.4 var(--font-mono, ui-monospace, monospace);
|
|
132
|
+
letter-spacing: var(--tracking-eyebrow, 0.08em);
|
|
133
|
+
text-transform: uppercase;
|
|
134
|
+
padding: var(--space-2, 4px) var(--space-3, 8px);
|
|
135
|
+
border: var(--rule-thin, 1px solid #2a2520);
|
|
136
|
+
border-radius: 0;
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
background: transparent;
|
|
139
|
+
color: var(--fg-0, #2a2520);
|
|
140
|
+
transition: background var(--dur-flip, 120ms) var(--ease-out, ease-out);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.cm-btn:hover {
|
|
144
|
+
background: var(--bg-2, color-mix(in oklab, var(--bg-1, #faf6ef) 80%, var(--fg-0, #2a2520) 6%));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.cm-btn--primary {
|
|
148
|
+
background: var(--accent, #d63b1f);
|
|
149
|
+
color: var(--accent-fg, #faf6ef);
|
|
150
|
+
border-color: var(--accent, #d63b1f);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.cm-btn--primary:hover {
|
|
154
|
+
background: var(--accent-hover, color-mix(in oklab, var(--accent, #d63b1f) 90%, white));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.cm-btn:disabled {
|
|
158
|
+
opacity: 0.5;
|
|
159
|
+
cursor: not-allowed;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.cm-btn:focus-visible {
|
|
163
|
+
outline: none;
|
|
164
|
+
box-shadow: var(--shadow-focus, 0 0 0 2px var(--accent, #d63b1f));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ─── Thread popover ──────────────────────────────────────────────────────── */
|
|
168
|
+
.cm-thread {
|
|
169
|
+
position: absolute;
|
|
170
|
+
width: 340px;
|
|
171
|
+
max-height: 60vh;
|
|
172
|
+
overflow: auto;
|
|
173
|
+
background: var(--bg-2, color-mix(in oklab, var(--bg-1, #faf6ef) 80%, var(--fg-0, #2a2520) 6%));
|
|
174
|
+
border: var(--rule-thin, 1px solid #2a2520);
|
|
175
|
+
border-radius: var(--radius-sm, 2px);
|
|
176
|
+
padding: var(--space-4, 12px);
|
|
177
|
+
box-shadow: none;
|
|
178
|
+
font: var(--type-sm, 13px) / var(--lh-sm, 1.45) var(--font-mono, ui-monospace, monospace);
|
|
179
|
+
color: var(--fg-0, #2a2520);
|
|
180
|
+
pointer-events: auto;
|
|
181
|
+
z-index: 5;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.cm-thread__head {
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
gap: var(--space-2, 4px);
|
|
188
|
+
padding-bottom: var(--space-3, 8px);
|
|
189
|
+
border-bottom: var(--rule-thin, 1px solid #2a2520);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.cm-thread__head-row {
|
|
193
|
+
display: flex;
|
|
194
|
+
justify-content: space-between;
|
|
195
|
+
align-items: baseline;
|
|
196
|
+
gap: var(--space-2, 4px);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.cm-thread__close {
|
|
200
|
+
background: transparent;
|
|
201
|
+
color: var(--fg-2, #6e6660);
|
|
202
|
+
border: var(--rule-thin, 1px solid #2a2520);
|
|
203
|
+
border-radius: 0;
|
|
204
|
+
padding: 0 var(--space-2, 4px);
|
|
205
|
+
font: 600 var(--type-xs, 11px) / 1.2 var(--font-mono, ui-monospace, monospace);
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
align-self: flex-start;
|
|
208
|
+
margin-left: var(--space-2, 4px);
|
|
209
|
+
transition: background var(--dur-flip, 120ms) var(--ease-out, ease-out);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.cm-thread__close:hover {
|
|
213
|
+
background: var(--bg-3, #1c1916);
|
|
214
|
+
color: var(--fg-0, #2a2520);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.cm-thread__close:focus-visible {
|
|
218
|
+
outline: none;
|
|
219
|
+
box-shadow: var(--shadow-focus, 0 0 0 2px var(--accent, #d63b1f));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.cm-thread__author {
|
|
223
|
+
color: var(--fg-0, #2a2520);
|
|
224
|
+
font-weight: 600;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.cm-thread__time {
|
|
228
|
+
color: var(--fg-2, #6e6660);
|
|
229
|
+
font-size: var(--type-xs, 11px);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.cm-thread__selector {
|
|
233
|
+
display: inline-block;
|
|
234
|
+
color: var(--fg-2, #6e6660);
|
|
235
|
+
font-size: var(--type-xs, 11px);
|
|
236
|
+
border: 1px solid var(--mono-rule, var(--border-subtle, #c8bfb4));
|
|
237
|
+
padding: 0 var(--space-2, 4px);
|
|
238
|
+
background: var(--mono-cell-bg, color-mix(in oklab, var(--bg-1, #faf6ef) 80%, var(--accent, #d63b1f) 6%));
|
|
239
|
+
align-self: flex-start;
|
|
240
|
+
max-width: 100%;
|
|
241
|
+
overflow: hidden;
|
|
242
|
+
text-overflow: ellipsis;
|
|
243
|
+
white-space: nowrap;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.cm-thread__body {
|
|
247
|
+
padding: var(--space-3, 8px) 0;
|
|
248
|
+
white-space: pre-wrap;
|
|
249
|
+
word-break: break-word;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.cm-thread__body strong,
|
|
253
|
+
.cm-thread__reply-body strong {
|
|
254
|
+
/* @mention tokens — bold, accent color. Inserted by `renderBodyWithMentions`. */
|
|
255
|
+
color: var(--accent, #d63b1f);
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.cm-thread__reply {
|
|
260
|
+
padding: var(--space-3, 8px) 0;
|
|
261
|
+
border-top: 1px solid var(--border-subtle, #c8bfb4);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.cm-thread__reply-head {
|
|
265
|
+
display: flex;
|
|
266
|
+
align-items: baseline;
|
|
267
|
+
gap: var(--space-2, 4px);
|
|
268
|
+
margin-bottom: var(--space-2, 4px);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.cm-thread__reply-author {
|
|
272
|
+
color: var(--fg-1, #4a4540);
|
|
273
|
+
font-weight: 600;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.cm-thread__reply-time {
|
|
277
|
+
color: var(--fg-3, var(--fg-2, #6e6660));
|
|
278
|
+
font-size: var(--type-xs, 11px);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.cm-thread__reply-body {
|
|
282
|
+
white-space: pre-wrap;
|
|
283
|
+
word-break: break-word;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.cm-thread__reply-form {
|
|
287
|
+
padding-top: var(--space-3, 8px);
|
|
288
|
+
border-top: var(--rule-thin, 1px solid #2a2520);
|
|
289
|
+
display: flex;
|
|
290
|
+
flex-direction: column;
|
|
291
|
+
gap: var(--space-2, 4px);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.cm-thread__reply-textarea {
|
|
295
|
+
width: 100%;
|
|
296
|
+
min-height: 56px;
|
|
297
|
+
resize: vertical;
|
|
298
|
+
background: var(--bg-0, #ffffff);
|
|
299
|
+
color: var(--fg-0, #2a2520);
|
|
300
|
+
border: var(--rule-thin, 1px solid #2a2520);
|
|
301
|
+
border-radius: 0;
|
|
302
|
+
padding: var(--space-3, 8px);
|
|
303
|
+
font: inherit;
|
|
304
|
+
box-sizing: border-box;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.cm-thread__reply-textarea:focus-visible {
|
|
308
|
+
outline: none;
|
|
309
|
+
border-color: var(--accent, #d63b1f);
|
|
310
|
+
box-shadow: var(--shadow-focus, 0 0 0 2px var(--accent, #d63b1f));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.cm-thread__reply-actions {
|
|
314
|
+
display: flex;
|
|
315
|
+
justify-content: flex-end;
|
|
316
|
+
gap: var(--space-2, 4px);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.cm-thread__actions {
|
|
320
|
+
display: flex;
|
|
321
|
+
gap: var(--space-2, 4px);
|
|
322
|
+
padding-top: var(--space-3, 8px);
|
|
323
|
+
margin-top: var(--space-3, 8px);
|
|
324
|
+
border-top: var(--rule-thin, 1px solid #2a2520);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.cm-thread__actions .cm-btn--danger {
|
|
328
|
+
margin-left: auto;
|
|
329
|
+
color: var(--status-error, #c0392b);
|
|
330
|
+
border-color: var(--status-error, #c0392b);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.cm-thread__actions .cm-btn--danger:hover {
|
|
334
|
+
background: color-mix(in oklab, var(--status-error, #c0392b) 12%, transparent);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* ─── @mention autocomplete ──────────────────────────────────────────────── */
|
|
338
|
+
.cm-mention-popup {
|
|
339
|
+
position: absolute;
|
|
340
|
+
background: var(--bg-2, color-mix(in oklab, var(--bg-1, #faf6ef) 80%, var(--fg-0, #2a2520) 6%));
|
|
341
|
+
border: var(--rule-thin, 1px solid #2a2520);
|
|
342
|
+
border-radius: 0;
|
|
343
|
+
font: var(--type-xs, 11px) / var(--lh-xs, 1.3) var(--font-mono, ui-monospace, monospace);
|
|
344
|
+
color: var(--fg-0, #2a2520);
|
|
345
|
+
max-height: 180px;
|
|
346
|
+
min-width: 160px;
|
|
347
|
+
overflow: auto;
|
|
348
|
+
box-shadow: none;
|
|
349
|
+
pointer-events: auto;
|
|
350
|
+
z-index: 6;
|
|
351
|
+
list-style: none;
|
|
352
|
+
padding: 0;
|
|
353
|
+
margin: 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.cm-mention-popup__item {
|
|
357
|
+
padding: var(--space-2, 4px) var(--space-3, 8px);
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
display: flex;
|
|
360
|
+
justify-content: space-between;
|
|
361
|
+
gap: var(--space-3, 8px);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.cm-mention-popup__item[aria-selected='true'] {
|
|
365
|
+
background: var(--accent, #d63b1f);
|
|
366
|
+
color: var(--accent-fg, #faf6ef);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.cm-mention-popup__name {
|
|
370
|
+
font-weight: 600;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.cm-mention-popup__email {
|
|
374
|
+
color: var(--fg-2, #6e6660);
|
|
375
|
+
font-size: var(--type-xs, 11px);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.cm-mention-popup__item[aria-selected='true'] .cm-mention-popup__email {
|
|
379
|
+
color: var(--accent-fg, #faf6ef);
|
|
380
|
+
opacity: 0.85;
|
|
381
|
+
}
|