create-pmos 0.67.1 → 0.68.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.
@@ -32,6 +32,12 @@
32
32
  * 4-second session poll included — took a half-read document back to the top. `scrollPane` keeps one
33
33
  * element per tab and mode; see the block comment above it.
34
34
  *
35
+ * ── In live mode, a markdown file the PM can edit is edited in place (workspace-tabs-declutter) ─────────────
36
+ * The four modes are the prototype's, and the fixture keeps them. In live mode a markdown file the PM can write is
37
+ * drawn by doc-editor.js: read and edited in one surface, block by block, in the layout it is read in. `edit` stays as
38
+ * the whole file as markdown (the Markdown toggle) and `diff` as the Changes toggle; `raw` is the Source toggle of a
39
+ * file that cannot be edited. Beside the document, doc-rail.js holds its outline and its Connections.
40
+ *
35
41
  * ── What `view` renders it through ──────────────────────────────────────────────────────────
36
42
  * `markdown.js`, not `fixture.js`'s `mdParse`. The latter is a transcription of the design
37
43
  * prototype's four-branch stand-in (`# `/`## `/`- `/paragraph) pinned byte-for-byte by
@@ -43,7 +49,10 @@ import { state, setState, FIXTURE } from './state.js';
43
49
  import * as FX from './fixture.js';
44
50
  import { mdRender } from './markdown.js';
45
51
  import { openDoc } from './panel-initiatives.js';
46
- import { button } from './primitives.js';
52
+ import { button, iconButton, menu } from './primitives.js';
53
+ import { editorNode, disposeEditor, headsOf, goToBlock, isEditing } from './doc-editor.js';
54
+ import { railNode, disposeRail, refreshGraph } from './doc-rail.js';
55
+ import { copyChip, refreshCopies } from './doc-remote.js';
47
56
 
48
57
  /** `{{ expr }}` — one interpolated value in its own span, as the prototype's runtime emits it. */
49
58
  const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
@@ -165,6 +174,8 @@ function save(tab, text) {
165
174
  const final = out && typeof out.text === 'string' ? out.text : text;
166
175
  disk.set(path, final);
167
176
  claims.delete(wsOf(tab)); // the consistency record may move with the file; read it again
177
+ refreshCopies(wsOf(tab)); // …and so may the copy's state, and the file's ties
178
+ refreshGraph(wsOf(tab), tab.path);
168
179
  commit(final);
169
180
  },
170
181
  (e) => { saveErr.set(path, (e && e.message) || 'could not reach the workspace server'); setState({}); },
@@ -388,35 +399,26 @@ function editBox(tab) {
388
399
  /** Release what a closed doc tab held. Called by panel-tabs.js when the tab goes away. */
389
400
  export function disposeDoc(id) {
390
401
  boxes.delete(id);
402
+ docBars.delete(id);
391
403
  normNote.delete(id);
392
404
  frames.delete(id);
393
405
  scrollers.delete(id);
394
406
  running.delete(id); // the grant belonged to this open document, and it is closed
395
- alignOpen.delete(id);
396
- alignSkip.delete(id);
397
- alignNodes.delete(id);
407
+ disposeEditor(id);
408
+ disposeRail(id);
409
+ places.delete(id);
410
+ railHidden.delete(id);
398
411
  }
399
412
 
400
413
 
401
- /* ── the Alignment drawer (workspace-cockpit-nav Run 3, ViewAlignment.dc.html) ──────────────────
414
+ /* ── the findings beside a document (was the Alignment drawer) ─────────────────────────────────────────────────────
402
415
  *
403
- * The board shows a drawer beside a document: the other documents with a CONCRETE tie to this one,
404
- * and what to do about each. Everything in it here is read from the consistency record the Validate
405
- * view already reads (`/api/claims`): which files a document is compared with, and which findings
406
- * name a source. Nothing is guessed from similarity, and nothing is drafted:
407
- * · a tie is drawn only from `compared_with` and a finding's `source` — never from wording;
408
- * · the board's "Apply" (a proposed edit to the other file, with its diff) has no backing — PMOS
409
- * drafts no edit to another file — so it is not drawn, and the drawer says so;
410
- * · a file the last check did not read is said to be unread, not shown as clean.
411
- * The drawer is closed by default, and closed it adds nothing to the pane: the pane is exactly
412
- * [mode bar, body] as before. Open, it is a third child laid over the right of the pane, and the
413
- * mode bar and body give it room with a margin. No node is re-parented, so a textarea keeps its
414
- * caret, a scroller its offset and a sandboxed frame its document. */
415
- const alignOpen = new Set(); // tab ids whose drawer is open
416
- const alignSkip = new Map(); // tab id -> Set of rels the PM skipped for this tab
416
+ * The Alignment drawer (workspace-cockpit-nav Run 3) listed the documents with a concrete tie to this one, from the
417
+ * consistency record the Validate view reads (`/api/claims`). The Connections rail (doc-rail.js) now draws every stated
418
+ * tie, so what the record adds is its FINDINGS: the ones in this file, and the ones in another file that name this
419
+ * file as their source. A tie is still never drawn from wording, and a file the last check did not read is not shown as
420
+ * clean: it has no findings to show, and the rail says only what was read. */
417
421
  const claims = new Map(); // ws -> { state: 'loading' | 'ok' | 'error', data?, error? }
418
- const ALIGN_W = 'min(420px, 50%)';
419
- const alignNodes = new Map(); // tab id -> the drawer element, kept so its scroll survives a repaint
420
422
 
421
423
  function readClaims(ws) {
422
424
  let c = claims.get(ws);
@@ -430,132 +432,24 @@ function readClaims(ws) {
430
432
  return c;
431
433
  }
432
434
 
433
- /** Whether a tab can have the drawer at all: a file in the checked-out tree, in real mode. */
434
- const canAlign = (tab) => !FIXTURE && !tab.guide && !tab.ref && !isReference(tab.path);
435
-
436
- const A_LBL = 'font-family:var(--mono);font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
437
- const A_NOTE = 'font-size:12px;line-height:1.5;color:var(--muted-foreground)';
438
- const A_BADGE = 'font-family:var(--sans);font-size:11px;font-weight:500;border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;white-space:nowrap;color:var(--foreground)';
439
- const A_CARD = 'border:1px solid var(--border);background:var(--background);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:8px';
440
- const A_BTN = 'font-family:var(--sans);font-size:12.5px;font-weight:500;padding:0 12px;min-height:30px;border-radius:8px;background:transparent;border:1px solid var(--input);color:var(--foreground)';
441
- const STATUS_TONE = { contradicted: 'var(--destructive)', stale: 'var(--warning)', suspected: 'var(--muted-foreground)', undecided: 'var(--muted-foreground)' };
442
- const STATUS_WORD = { contradicted: 'contradicted', stale: 'stale', suspected: 'suspected by Jev', undecided: 'worth a look' };
443
-
444
- const statusBadge = (st) => el('span', {
445
- style: A_BADGE + ';border-color:transparent;color:' + (STATUS_TONE[st] || 'var(--muted-foreground)')
446
- + ';background:color-mix(in oklab, ' + (STATUS_TONE[st] || 'var(--muted-foreground)') + ' 14%, transparent)',
447
- }, iv(STATUS_WORD[st] || st));
448
-
449
- /**
450
- * The documents tied to `rel`, each with the ties that put it there. Three kinds, all read:
451
- * · this file is compared with it (this file's `compared_with`)
452
- * · it is compared with this file (its `compared_with` names this file)
453
- * · a finding in it names this file (a finding whose `source` is this file)
454
- */
455
- function tiesFor(rel, docs) {
456
- const out = new Map();
457
- const add = (r, tie, finding) => {
458
- if (!r || r === rel) return;
459
- let c = out.get(r);
460
- if (!c) { c = { rel: r, ties: new Set(), findings: [] }; out.set(r, c); }
461
- c.ties.add(tie);
462
- if (finding) c.findings.push(finding);
463
- };
464
- const self = docs.find((x) => x.rel === rel) || null;
465
- if (self) for (const r of self.compared_with || []) add(r, 'this file checks against it');
435
+ /** The findings that concern `rel`: its own, then each other file's finding whose source is `rel` (with that file). */
436
+ function findingsFor(ws, rel) {
437
+ const c = readClaims(ws);
438
+ if (c.state !== 'ok') return [];
439
+ const docs = Array.isArray(c.data && c.data.docs) ? c.data.docs : [];
440
+ const out = [];
441
+ const self = docs.find((x) => x.rel === rel);
442
+ if (self) for (const f of self.findings || []) out.push({ status: f.status, sentence: f.sentence });
466
443
  for (const x of docs) {
467
444
  if (x.rel === rel) continue;
468
- if ((x.compared_with || []).includes(rel)) add(x.rel, 'checks against this file');
469
- for (const f of x.findings || []) if (f && f.source === rel) add(x.rel, 'a finding names this file', f);
470
- }
471
- return { self, list: [...out.values()] };
472
- }
473
-
474
- function alignDrawer(tab) {
475
- const ws = wsOf(tab);
476
- const rel = String(tab.path || '');
477
- const close = () => { alignOpen.delete(tab.id); setState({}); };
478
- const kids = [
479
- el('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:8px' },
480
- el('span', { style: 'font-weight:600;font-size:15px;font-family:var(--sans)', 'data-r29': 'align-title' }, 'Alignment'),
481
- el('button', { onClick: close, 'data-r29': 'align-close', 'aria-label': 'close alignment', title: 'close the drawer',
482
- style: 'background:transparent;border:0;color:var(--muted-foreground);font-size:15px;padding:2px 6px', hover: 'color:var(--foreground)' }, '⟩')),
483
- el('div', { style: A_NOTE, 'data-r29': 'align-rule' }, 'Only documents with a concrete tie to this file are listed. No similarity guesses.'),
484
- ];
485
- const c = readClaims(ws);
486
- if (c.state === 'loading') {
487
- kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-status', 'data-state': 'loading' }, 'Reading the consistency record…'));
488
- } else if (c.state === 'error') {
489
- kids.push(el('div', { style: A_NOTE + ';color:var(--destructive)', 'data-r29': 'align-status', 'data-state': 'error' },
490
- iv('The consistency record could not be read: ' + c.error + '. Nothing is listed.')));
491
- } else {
492
- const d = c.data || {};
493
- const docs = Array.isArray(d.docs) ? d.docs : [];
494
- const openValidate = () => import('./panel-cockpit.js').then((m) => m.openView('validate')).catch(() => {});
495
- if (d.verdict === 'not-checked' || !docs.length) {
496
- kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-status', 'data-state': 'not-checked' },
497
- 'This repository has not been checked yet, so no tie is known. Open Validate and press “check now”.'));
498
- kids.push(el('div', null, el('button', { style: A_BTN, hover: 'border-color:var(--ring)', onClick: openValidate, 'data-r29': 'align-validate' }, 'open Validate')));
499
- } else {
500
- const { self, list } = tiesFor(rel, docs);
501
- const skipped = alignSkip.get(tab.id) || new Set();
502
- const shown = list.filter((x) => !skipped.has(x.rel));
503
- kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-status', 'data-state': self ? 'read' : 'unread' },
504
- iv(self
505
- ? 'This file: ' + (self.classified ? self.type : 'not classified') + ' · ' + self.verified + ' statement(s) verified'
506
- + (self.findings.length ? ' · ' + self.findings.length + ' finding(s)' : ' · no open findings')
507
- : 'The last check did not read this file, so its own findings are unknown. Ties from other files are still listed.')));
508
- if (self && self.findings.length) {
509
- kids.push(el('div', { style: A_LBL }, 'findings in this file'));
510
- kids.push(el('div', { style: 'display:flex;flex-direction:column;gap:6px', 'data-r29': 'align-self' },
511
- self.findings.map((f) => el('div', { 'data-r29': 'align-finding', 'data-status': f.status, style: 'display:flex;flex-direction:column;gap:3px' },
512
- el('span', null, statusBadge(f.status)),
513
- el('span', { style: 'font-size:12px;line-height:1.5;color:var(--foreground)' }, iv(f.sentence))))));
514
- }
515
- if (state.savedDocs[docKey(tab)] != null && list.length) {
516
- kids.push(el('div', { style: A_NOTE + ';color:var(--warning)', 'data-r29': 'align-saved' },
517
- iv('Saved. ' + list.length + ' other document' + (list.length === 1 ? '' : 's') + ' may now disagree with this change.')));
518
- }
519
- kids.push(el('div', { style: A_LBL }, iv('tied documents · ' + list.length)));
520
- if (!list.length) kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-none' }, 'No other document has a concrete tie to this file.'));
521
- for (const x of shown) {
522
- kids.push(el('div', { style: A_CARD, 'data-r29': 'align-cand', 'data-rel': x.rel },
523
- el('b', { style: 'font-family:var(--mono);font-size:12px;font-weight:600;overflow-wrap:anywhere' }, iv(x.rel)),
524
- el('div', { style: 'display:flex;gap:6px;flex-wrap:wrap' }, [...x.ties].map((t) => el('span', { style: A_BADGE, 'data-r29': 'align-tie' }, iv(t)))),
525
- x.findings.map((f) => el('div', { style: 'display:flex;flex-direction:column;gap:3px' },
526
- el('span', null, statusBadge(f.status)),
527
- el('span', { style: A_NOTE }, iv(f.sentence)))),
528
- el('div', { style: 'display:flex;gap:6px' },
529
- el('button', { style: A_BTN, hover: 'border-color:var(--ring)', 'data-r29': 'align-open', onClick: () => openDoc(tab.slug, x.rel, 'FILE', tab.ws ? { ws: tab.ws, branch: tab.branch || null, linked: true } : undefined) }, 'Open to edit'),
530
- el('button', { style: A_BTN, hover: 'border-color:var(--ring)', 'data-r29': 'align-skip',
531
- onClick: () => { const s2 = new Set(skipped); s2.add(x.rel); alignSkip.set(tab.id, s2); setState({}); } }, 'Skip'))));
532
- }
533
- if (skipped.size) {
534
- kids.push(el('button', { style: A_BTN + ';align-self:flex-start', hover: 'border-color:var(--ring)', 'data-r29': 'align-unskip',
535
- onClick: () => { alignSkip.delete(tab.id); setState({}); } }, iv('show ' + skipped.size + ' skipped')));
536
- }
537
- }
538
- kids.push(el('div', { style: 'display:flex;gap:6px' },
539
- el('button', { style: A_BTN, hover: 'border-color:var(--ring)', 'data-r29': 'align-reread', onClick: () => { claims.delete(ws); setState({}); } }, 're-read')));
445
+ for (const f of x.findings || []) if (f && f.source === rel) out.push({ status: f.status, sentence: f.sentence, rel: x.rel });
540
446
  }
541
- kids.push(el('div', { style: A_NOTE, 'data-r29': 'align-foot' },
542
- 'Ties come from the last consistency check. PMOS drafts no edit to another file: open it to change it. Git keeps the history.'));
543
- /* One node per tab, kept like the scrollers above, so a repaint does not take the drawer's own
544
- * scroll back to the top. */
545
- let node = alignNodes.get(tab.id);
546
- if (!node) { node = makeDrawerNode(); alignNodes.set(tab.id, node); }
547
- setChildren(node, kids);
548
- return node;
447
+ return out;
549
448
  }
550
449
 
551
- function makeDrawerNode() {
552
- return el('div', {
553
- 'data-r29': 'align-drawer', 'data-board': 'alignment',
554
- style: 'position:absolute;top:0;right:0;bottom:0;width:' + ALIGN_W + ';box-sizing:border-box;overflow-y:auto;'
555
- + 'border-left:1px solid var(--border);background:var(--sidebar);color:var(--foreground);padding:16px;'
556
- + 'display:flex;flex-direction:column;gap:12px;font-family:var(--sans);font-size:13px;line-height:1.5',
557
- });
558
- }
450
+ /** Whether a tab can have the rail at all: a file in the checked-out tree, in real mode. */
451
+ const canRail = (tab) => !FIXTURE && !tab.guide && !tab.ref && !isReference(tab.path);
452
+ const railHidden = new Set(); // tab ids whose outline and connections the PM put away
559
453
 
560
454
  /* ── mount ─────────────────────────────────────────────────────────────────────────────────── */
561
455
 
@@ -724,7 +618,10 @@ export function mountDoc(host, tab) {
724
618
  const raw = docText(tab);
725
619
  const text = state.docEdits[docKey(tab)] ?? raw;
726
620
  const nonMd = !FIXTURE && isSourcePath(tab.path); // file-viewer-any-text: read-only, shown as source
727
- const dm = ((tab.readonly || nonMd) && (state.docModes[tab.id] === 'edit' || state.docModes[tab.id] === 'diff')) ? 'view' : (state.docModes[tab.id] ?? 'view');
621
+ const writable = canWrite(tab, nonMd);
622
+ let dm = ((tab.readonly || nonMd) && (state.docModes[tab.id] === 'edit' || state.docModes[tab.id] === 'diff')) ? 'view' : (state.docModes[tab.id] ?? 'view');
623
+ if (writable && dm === 'raw') dm = 'view'; // in place, the file you can write IS its source: no read-only copy of it
624
+ if (!FIXTURE && !writable && (dm === 'edit' || dm === 'diff')) dm = 'view'; // only a markdown file can be saved
728
625
  const dirty = text !== raw;
729
626
  const err = saveErr.get(docKey(tab));
730
627
 
@@ -780,14 +677,7 @@ export function mountDoc(host, tab) {
780
677
  const canNormalize = !!(norm && typeof norm.normalize === 'function')
781
678
  && !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(docKey(tab)) && !nonMd;
782
679
  const normBtn = canNormalize ? el('button', {
783
- onClick: () => {
784
- const r = norm.normalize(text);
785
- normNote.set(tab.id, r.changed
786
- ? r.joins + ' line(s) joined' + (r.blanks ? ', ' + r.blanks + ' blank inserted' : '')
787
- : 'already normalized — every paragraph is one line');
788
- if (!r.changed) { setState({}); return; }
789
- setState((s) => ({ docEdits: { ...s.docEdits, [docKey(tab)]: r.text } }));
790
- },
680
+ onClick: () => normalizeBuffer(tab, norm),
791
681
  style: 'margin-left:8px;font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:var(--secondary);border:1px solid var(--border);color:var(--muted-foreground)',
792
682
  hover: 'border-color:var(--ring)',
793
683
  title: 'Join hard-wrapped paragraphs into one line each — what a Jira issue, a Google Doc or any'
@@ -812,21 +702,19 @@ export function mountDoc(host, tab) {
812
702
  hover: 'color:var(--foreground)', 'data-r1': 'measure',
813
703
  title: narrowOf(tab) ? 'use the full width of the tab' : 'limit lines to a 72-character reading measure',
814
704
  }, iv(narrowOf(tab) ? '↔ full width' : '⇥ 72ch')),
815
- canAlign(tab) ? el('button', {
816
- onClick: () => {
817
- if (alignOpen.has(tab.id)) alignOpen.delete(tab.id);
818
- else { alignOpen.add(tab.id); claims.delete(wsOf(tab)); } // opening reads the record afresh
819
- setState({});
820
- },
821
- style: 'margin-left:8px;font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:'
822
- + (alignOpen.has(tab.id) ? 'var(--secondary)' : 'transparent') + ';border:1px solid '
823
- + (alignOpen.has(tab.id) ? 'var(--brand)' : 'var(--border)') + ';color:var(--foreground)',
824
- hover: 'border-color:var(--ring)', 'data-r29': 'align-toggle', 'data-open': alignOpen.has(tab.id) ? 'yes' : 'no',
825
- title: 'Show the documents with a concrete tie to this one, from the last consistency check.',
826
- }, 'alignment') : null,
827
705
  el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:' + dirtyColor }, iv(dirtyLabel)),
828
706
  );
829
707
 
708
+ /* Live mode draws the bar from the design system (workspace-tabs-declutter): the four modes as one segmented control,
709
+ * the save state beside them, save and revert, and one ⋯ menu for what is used now and then (the reading width,
710
+ * normalize, alignment). Run scripts stays on the bar while a page can run: it grants a privilege, so its state is
711
+ * never one click away. The fixture keeps the prototype's bar above, which the parity suites pin. */
712
+ const bar = FIXTURE ? modeBar : docBar(tab, {
713
+ dm, readonly, writable, dirty, err, text, asPage, live, canNormalize, norm,
714
+ statusText: dirtyLabel,
715
+ tone: err ? 'error' : (dirty ? 'dirty' : 'clean'),
716
+ });
717
+
830
718
  let body;
831
719
  if (dm === 'raw') body = rawBody(tab, text);
832
720
  else if (dm === 'diff') body = diffBody(tab, raw, text, dirty);
@@ -839,16 +727,133 @@ export function mountDoc(host, tab) {
839
727
  if (body.defaultValue !== text) body.defaultValue = text;
840
728
  if (body.value !== text) body.value = text;
841
729
  } else if (!FIXTURE && (extOfPath(tab.path) === 'csv' || extOfPath(tab.path) === 'tsv')) body = gridBody(tab, text);
730
+ else if (writable) body = inPlace(tab, text, raw);
842
731
  else body = asPage ? htmlFrame(tab, text, live) : (nonMd && !unread.has(docKey(tab)) ? sourceBody(tab, text) : viewBody(tab, text));
843
732
 
844
- /* The drawer, when open, lies over the right of the pane; the bar and the body give it room. The
845
- * margin is written on every paint so a closed drawer leaves no gap behind on a kept node. */
846
- const drawerOn = canAlign(tab) && alignOpen.has(tab.id);
847
- host.style.position = drawerOn ? 'relative' : '';
848
- modeBar.style.marginRight = drawerOn ? ALIGN_W : '';
849
- body.style.marginRight = drawerOn ? ALIGN_W : '';
850
- setChildren(host, drawerOn ? [modeBar, body, alignDrawer(tab)] : [modeBar, body]);
733
+ setChildren(host, [bar, body]);
851
734
  return host;
852
735
  }
853
736
 
737
+ /* A markdown file the PM can write, in live mode: edited in place. A guide page ships with the app, a file from git is
738
+ * evidence, a reference names someone else's record, and a file that could not be read is a stub: none of those. */
739
+ function canWrite(tab, nonMd) {
740
+ if (FIXTURE || tab.readonly || tab.guide || tab.ref || nonMd || isReference(tab.path) || isHtmlDoc(tab.path)) return false;
741
+ const PK = typeof window !== 'undefined' ? window.PreviewKind : null;
742
+ if (!PK || typeof PK.previewKind !== 'function' || PK.previewKind(String(tab.path || '')) !== 'markdown') return false;
743
+ const k = docKey(tab);
744
+ return state.savedDocs[k] != null || (disk.has(k) && !unread.has(k));
745
+ }
746
+
747
+ /* The in-place surface: the editor and, beside it, the rail. One scrolling node per tab, kept like the scrollers above,
748
+ * so neither a repaint nor a keystroke moves the page. */
749
+ const places = new Map(); // tab id -> { node, grid }
750
+ function inPlace(tab, text, raw) {
751
+ let pl = places.get(tab.id);
752
+ if (!pl) {
753
+ const grid = el('div', { class: 'fr-ed__grid' });
754
+ pl = { node: el('div', { class: 'fr-ed', 'data-ed': 'scroll' }, grid), grid };
755
+ places.set(tab.id, pl);
756
+ }
757
+ const ws = wsOf(tab);
758
+ const main = editorNode(tab, {
759
+ text, ctx: linkCtx(tab),
760
+ onChange: (t) => { saveErr.delete(docKey(tab)); setState((s) => { const de = { ...s.docEdits }; if (t === raw) delete de[docKey(tab)]; else de[docKey(tab)] = t; return { docEdits: de }; }); },
761
+ onSave: () => { const cur = state.docEdits[docKey(tab)]; if (cur != null && cur !== raw) save(tab, cur); },
762
+ });
763
+ const showRail = canRail(tab) && !railHidden.has(tab.id);
764
+ const rail = showRail ? railNode(tab, {
765
+ ws, rel: String(tab.path || ''), heads: headsOf(tab.id), findings: findingsFor(ws, String(tab.path || '')),
766
+ onGoto: (i) => goToBlock(tab.id, i),
767
+ onOpen: (rel) => openDoc(tab.slug, rel, 'FILE', tab.ws ? { ws: tab.ws, branch: tab.branch || null, linked: true } : undefined),
768
+ }) : null;
769
+ pl.grid.setAttribute('data-measure', narrowOf(tab) ? '72ch' : 'full');
770
+ pl.grid.toggleAttribute('data-rail', !!rail);
771
+ setChildren(pl.grid, [main, rail]);
772
+ if (isEditing(tab.id)) pl.node.setAttribute('data-editing', ''); else pl.node.removeAttribute('data-editing');
773
+ return pl.node;
774
+ }
775
+
776
+ /** normalize: the buffer's paragraphs joined into one line each. The buffer, never the file; a clean document says so. */
777
+ function normalizeBuffer(tab, norm) {
778
+ const raw = docText(tab);
779
+ const text = state.docEdits[docKey(tab)] ?? raw;
780
+ const r = norm.normalize(text);
781
+ normNote.set(tab.id, r.changed
782
+ ? r.joins + ' line(s) joined' + (r.blanks ? ', ' + r.blanks + ' blank inserted' : '')
783
+ : 'already normalized — every paragraph is one line');
784
+ if (!r.changed) { setState({}); return; }
785
+ setState((s) => ({ docEdits: { ...s.docEdits, [docKey(tab)]: r.text } }));
786
+ }
787
+
788
+ /* ── the document bar in live mode ──────────────────────────────────────────────────────────────────────────────────
789
+ * One bar per tab, kept so the ⋯ control keeps its focus across a repaint; its children are rebuilt. The bar says whether
790
+ * the file is saved and what its copy holds, and carries what changes the view: Changes while there is something to
791
+ * compare, the whole file as markdown (or, for a file that cannot be edited, its source), and one ⋯ menu for what is used
792
+ * now and then. Run scripts stays on the bar while a page can run: it grants a privilege, so its state is never one
793
+ * click away. Every action is the prototype's: the same state keys, the same buffer, the same save path. */
794
+ const docBars = new Map(); // tab id -> { node, more }
795
+
796
+ function docBar(tab, o) {
797
+ let kept = docBars.get(tab.id);
798
+ if (!kept) {
799
+ const more = iconButton({ icon: 'ellipsis', ariaLabel: 'Document options', size: 'sm', attrs: { 'data-doc': 'more', class: 'fr-doc-bar__more' },
800
+ onClick: (e) => menu({ anchor: e.currentTarget, ariaLabel: 'Document options', items: docMenu(tab) }) });
801
+ kept = { node: el('div', { class: 'fr-doc-bar', 'data-doc': 'bar' }), more };
802
+ docBars.set(tab.id, kept);
803
+ }
804
+ const setMode = (m) => setState((s) => ({ docModes: { ...s.docModes, [tab.id]: m } }));
805
+ const status = el('span', { class: 'fr-doc-bar__status', 'data-doc': 'status', 'data-tone': o.tone }, o.statusText);
806
+ const copy = canRail(tab) && !o.readonly ? copyChip(wsOf(tab), String(tab.path || '')) : null;
807
+ const inDiff = o.dm === 'diff';
808
+ const changes = o.writable && (o.dirty || inDiff) ? button({
809
+ label: 'changes', variant: inDiff ? 'outline' : 'ghost', size: 'sm', icon: 'git-compare',
810
+ attrs: { 'data-doc': 'changes', 'aria-pressed': inDiff ? 'true' : 'false', title: 'What saving would change on disk, line by line' },
811
+ onClick: () => setMode(inDiff ? 'view' : 'diff'),
812
+ }) : null;
813
+ const srcMode = o.writable ? 'edit' : 'raw';
814
+ const inSrc = o.dm === srcMode;
815
+ const source = button({
816
+ label: o.writable ? 'markdown' : 'source', variant: inSrc ? 'outline' : 'ghost', size: 'sm', icon: 'file-code',
817
+ attrs: { 'data-doc': 'source', 'aria-pressed': inSrc ? 'true' : 'false',
818
+ title: o.writable ? 'Edit the whole file as markdown' : 'Show the file as it is written' },
819
+ onClick: () => setMode(inSrc ? 'view' : srcMode),
820
+ });
821
+ const saveB = o.dirty ? button({ label: 'save', variant: 'primary', size: 'sm', attrs: { 'data-doc': 'save', title: 'Save (Ctrl or ⌘ S)' }, onClick: () => save(tab, o.text) }) : null;
822
+ const revertB = o.dirty ? button({ label: 'revert', variant: 'ghost', size: 'sm', attrs: { 'data-doc': 'revert' }, onClick: () => revert(tab) }) : null;
823
+ const run = o.asPage && o.dm === 'view' ? button({
824
+ label: o.live ? 'scripts running — stop' : 'run scripts', variant: o.live ? 'danger' : 'outline', size: 'sm',
825
+ onClick: () => { if (o.live) running.delete(tab.id); else running.add(tab.id); setState({}); },
826
+ attrs: { 'data-doc': 'run', 'aria-pressed': o.live ? 'true' : 'false',
827
+ title: o.live
828
+ ? 'This document is running its own scripts, in a frame with no origin and no network. Stop discards it.'
829
+ : "Run this document's own scripts. It stays sandboxed: no origin, no network, nothing loaded from anywhere." },
830
+ }) : null;
831
+ const hasMenu = docMenu(tab).length > 0;
832
+ setChildren(kept.node, [status, copy, el('span', { class: 'fr-doc-bar__end' }, run, changes, revertB, saveB, source, hasMenu ? kept.more : null)]);
833
+ return kept.node;
834
+ }
835
+
836
+ /** The ⋯ menu's items, read afresh when it opens, so each one says what choosing it will do now. */
837
+ function docMenu(tab) {
838
+ const items = [];
839
+ const dm = state.docModes[tab.id] ?? 'view';
840
+ if (dm === 'view' || dm === 'raw') {
841
+ const narrow = narrowOf(tab);
842
+ items.push({ label: narrow ? 'Use the full width' : 'Limit lines to 72 characters', icon: 'arrow-left-right', attrs: { 'data-r1': 'measure' },
843
+ onSelect: () => setState((s) => ({ docNarrow: { ...(s.docNarrow || {}), [tab.id]: !narrowOf(tab) } })) });
844
+ }
845
+ const norm = typeof window !== 'undefined' ? window.MdNormalize : null;
846
+ const nonMd = isSourcePath(tab.path);
847
+ if (norm && typeof norm.normalize === 'function' && !tab.readonly && !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(docKey(tab)) && !nonMd) {
848
+ items.push({ label: 'Normalize line breaks', icon: 'refresh-cw', attrs: { 'data-doc': 'normalize', title: 'Join hard-wrapped paragraphs into one line each. Writes the buffer, not the file: read it in Changes, then save.' },
849
+ onSelect: () => normalizeBuffer(tab, norm) });
850
+ }
851
+ if (canRail(tab) && canWrite(tab, nonMd)) {
852
+ const hidden = railHidden.has(tab.id);
853
+ items.push({ label: hidden ? 'Show the outline and connections' : 'Hide the outline and connections', icon: 'share-2', attrs: { 'data-doc': 'rail' },
854
+ onSelect: () => { if (hidden) railHidden.delete(tab.id); else railHidden.add(tab.id); setState({}); } });
855
+ }
856
+ return items;
857
+ }
858
+
854
859
  export default mountDoc;
@@ -1,5 +1,5 @@
1
1
  /* lucide-sprite.js — GENERATED by workspace/third-party/build-lucide-sprite.js from lucide-static 1.52.0.
2
- * Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock.
2
+ * Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock, bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud, share-2.
3
3
  *
4
4
  * ISC License
5
5
  *
@@ -81,5 +81,18 @@ export const SPRITE = '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true
81
81
  + '<symbol id="lucide-minimize-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14 10 7-7" /><path d="M20 10h-6V4" /><path d="m3 21 7-7" /><path d="M4 14h6v6" /></symbol>'
82
82
  + '<symbol id="lucide-git-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6a9 9 0 0 0-9 9V3" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /></symbol>'
83
83
  + '<symbol id="lucide-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></symbol>'
84
+ + '<symbol id="lucide-bold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" /></symbol>'
85
+ + '<symbol id="lucide-italic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" x2="10" y1="4" y2="4" /><line x1="14" x2="5" y1="20" y2="20" /><line x1="15" x2="9" y1="4" y2="20" /></symbol>'
86
+ + '<symbol id="lucide-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6" /><path d="m8 6-6 6 6 6" /></symbol>'
87
+ + '<symbol id="lucide-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>'
88
+ + '<symbol id="lucide-type" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16" /><path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" /><path d="M9 20h6" /></symbol>'
89
+ + '<symbol id="lucide-heading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h12" /><path d="M6 20V4" /><path d="M18 20V4" /></symbol>'
90
+ + '<symbol id="lucide-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5h.01" /><path d="M3 12h.01" /><path d="M3 19h.01" /><path d="M8 5h13" /><path d="M8 12h13" /><path d="M8 19h13" /></symbol>'
91
+ + '<symbol id="lucide-list-checks" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 5h8" /><path d="M13 12h8" /><path d="M13 19h8" /><path d="m3 17 2 2 4-4" /><path d="m3 7 2 2 4-4" /></symbol>'
92
+ + '<symbol id="lucide-text-quote" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 5H3" /><path d="M21 12H8" /><path d="M21 19H8" /><path d="M3 12v7" /></symbol>'
93
+ + '<symbol id="lucide-file-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /><path d="M14 2v5a1 1 0 0 0 1 1h5" /><path d="M10 12.5 8 15l2 2.5" /><path d="m14 12.5 2 2.5-2 2.5" /></symbol>'
94
+ + '<symbol id="lucide-git-compare" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" /><path d="M13 6h3a2 2 0 0 1 2 2v7" /><path d="M11 18H8a2 2 0 0 1-2-2V9" /></symbol>'
95
+ + '<symbol id="lucide-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>'
96
+ + '<symbol id="lucide-share-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /><line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>'
84
97
  + '</svg>';
85
- export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock"];
98
+ export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock","bold","italic","code","link","type","heading","list","list-checks","text-quote","file-code","git-compare","cloud","share-2"];
@@ -80,6 +80,49 @@
80
80
  "no-network: the server process opens no HTTP client outside the one TypeSafe module (D79)",
81
81
  "no-network: lib/typesafe.js is the ONLY file that opens an HTTP client"
82
82
  ]
83
+ },
84
+ {
85
+ "id": "STD-R18",
86
+ "file": "workspace/pm-r18-test.js",
87
+ "run": "workspace-tabs-declutter",
88
+ "ref": "planning/initiatives/workspace-tabs-declutter.md (status, amended 2026-10-07 after the PM approved the clickable mockup)",
89
+ "reason": "A file that cannot be edited now has one Source toggle instead of the view and raw modes. The assertion keeps its name and still checks that a git file offers no edit, diff or save; it reads the new control names (source, markdown, changes).",
90
+ "names": [
91
+ "R5: a git file offers no edit, diff or save"
92
+ ]
93
+ },
94
+ {
95
+ "id": "STD-R21",
96
+ "file": "workspace/pm-r21-test.js",
97
+ "run": "workspace-tabs-declutter",
98
+ "ref": "planning/initiatives/workspace-tabs-declutter.md (status, amended 2026-10-07 after the PM approved the clickable mockup)",
99
+ "reason": "The four document modes became toggles: a markdown file is edited in place with a Markdown toggle, and a read-only file has a Source toggle. F7 now checks that the source is one toggle away; F10 checks that a markdown tab is edited in place and offers its markdown.",
100
+ "names": [
101
+ "F7: view and raw are still offered",
102
+ "F10: a markdown tab still offers edit"
103
+ ]
104
+ },
105
+ {
106
+ "id": "STD-TABS",
107
+ "file": "workspace/pm-tabs-test.js",
108
+ "run": "workspace-tabs-declutter",
109
+ "ref": "planning/initiatives/workspace-tabs-declutter.md (status, amended 2026-10-07 after the PM approved the clickable mockup)",
110
+ "reason": "A markdown file has no raw mode any more: its source is the Markdown toggle, a textarea that scrolls itself. The scroll-position assertion now holds the Markdown source's position. pm-doc-inplace-test.js asserts the in-place editor.",
111
+ "names": [
112
+ "raw mode holds its position too — the fix is the pane's, not one mode's"
113
+ ]
114
+ },
115
+ {
116
+ "id": "STD-UI",
117
+ "file": "workspace/ui-test.js",
118
+ "run": "workspace-tabs-declutter",
119
+ "ref": "planning/initiatives/workspace-tabs-declutter.md (status, amended 2026-10-07 after the PM approved the clickable mockup)",
120
+ "reason": "The document round trip now goes through the in-place editor: DOC1 checks the tab is edited in place with a Markdown toggle (not four modes), DOC2 counts the Properties inputs (one per frontmatter key) instead of the old card's spans, and DOC4 reads the whole file from the Markdown toggle's textarea instead of raw. DOC5 and DOC6 keep their names and still type, diff and save the real file.",
121
+ "names": [
122
+ "DOC1: a document opens as a doc tab offering view · raw · edit · diff",
123
+ "DOC4: raw shows the file as written, frontmatter fence and all",
124
+ "DOC2: view renders the FILE ON DISK as markdown — a heading, its frontmatter card, its list"
125
+ ]
83
126
  }
84
127
  ],
85
128
  "entries": [
@@ -119,6 +119,8 @@ const startPolicy = require('./lib/start-policy'); // ONE stated rule for wh
119
119
  const treeShare = require('./lib/tree-share'); // who shares a working tree, and who is DECLARED to be driving it
120
120
  const tmuxControl = require('./lib/tmux-control'); // tmux -CC protocol: the pane's raw stream, so xterm.js keeps its scrollback
121
121
  const relatedDocs = require('./lib/related-docs'); // which documents a change puts out of step (R5/R6)
122
+ const Kg = require('./lib/kg'); // the stated ties between files (pmos kg)
123
+ const kgCli = require('./lib/kg-cli'); // …and its cached build for one repository
122
124
  const applyChange = require('./lib/apply-change'); // anchored + whole-document saves, refuse-on-stale
123
125
  const sessionState = require('./lib/session-state'); // the cadence×turn join — validated on BOTH sides
124
126
  // Bundle 2 (planning/design/bundle-2-wiring-contract.md). Four already-built pure modules. The
@@ -138,6 +140,7 @@ const docCheckSessions = new Map();
138
140
  // The session each Use … version press started (workspace id|path|kind → { session, use }), so the line says "Updating…"
139
141
  // before the session has opened its sync, and a second press on the same copy refuses.
140
142
  const docSyncSessions = new Map();
143
+ const docGraphs = new Map(); // root -> { at, graph }: the document tab's Connections, held 15 s
141
144
  const jiraTickets = require('./lib/jira-tickets.js'); // jira-wbs — the ticket draft, the PM's selection, the push plan
142
145
  const jiraStatus = require('./lib/jira-status.js'); // …and the status snapshot an agent's read writes
143
146
  // The session each Push or Check now press started (workspace id|initiative → { session, at }), so a second press
@@ -8248,6 +8251,33 @@ async function handleRequest(req, res) {
8248
8251
  }
8249
8252
  return json(res, { ok: true, wrote: rel });
8250
8253
  }
8254
+ if (p === '/api/doc/graph') {
8255
+ // The document tab's Connections (workspace-tabs-declutter): the files this file is tied to, read from the same
8256
+ // derived graph `pmos kg neighbors` prints. Only stated ties: a link, a frontmatter path, an initiative, an anchor
8257
+ // or a decision id. Nothing is guessed. The graph is built from git's tracked Markdown and cached by HEAD and the
8258
+ // dirty .md files, so a second read is a file read; a short in-memory hold keeps a repaint from asking git again.
8259
+ const w = getWs(url.searchParams.get('ws'));
8260
+ if (!w) return json(res, { ok: false, error: 'no workspace' }, 404);
8261
+ const rel = String(url.searchParams.get('path') || '');
8262
+ if (!rel || !insideRoot(w.root, path.resolve(w.root, rel))) return json(res, { ok: false, error: 'outside workspace' }, 404);
8263
+ let held = docGraphs.get(w.root);
8264
+ // A folder that is not a git repository, or one with no commit yet, tracks no file, so no tie is known.
8265
+ if (!held && spawnSync('git', ['rev-parse', '--verify', '-q', 'HEAD'], { cwd: w.root, stdio: 'ignore' }).status !== 0) {
8266
+ return json(res, { ok: true, tracked: false, ties: [] });
8267
+ }
8268
+ if (!held || Date.now() - held.at > 15000) {
8269
+ try { held = { at: Date.now(), graph: kgCli.graphFor(w.root).graph }; docGraphs.set(w.root, held); }
8270
+ catch (e) { return json(res, { ok: false, error: 'the graph could not be built: ' + ((e && e.message) || e).toString().split('\n')[0] }, 500); }
8271
+ }
8272
+ const g = held.graph;
8273
+ const me = Kg.nodeById(g, rel);
8274
+ if (!me) return json(res, { ok: true, tracked: false, ties: [] });
8275
+ const ties = (Kg.neighbors(g, rel, { depth: 1 }) || []).slice(0, 40).map((r) => {
8276
+ const n = Kg.nodeById(g, r.id) || {};
8277
+ return { path: r.id, title: n.title || r.id, type: n.type || null, via: r.via, basis: r.basis, direction: r.direction };
8278
+ });
8279
+ return json(res, { ok: true, tracked: true, title: me.title, ties });
8280
+ }
8251
8281
  if (p === '/api/doc/changes') {
8252
8282
  // The change log, read back (R8 — this is what an agent reads before working here).
8253
8283
  const w = getWs(url.searchParams.get('ws'));
@@ -0,0 +1,13 @@
1
+ {
2
+ "_comment": "workspace-tabs-declutter: the document tab edited in place (block model, browser).",
3
+ "modules": [
4
+ "doc-editor.test.mjs"
5
+ ],
6
+ "seconds": {
7
+ "doc-editor.test.mjs": 1,
8
+ "pm-doc-inplace-test.js": 25
9
+ },
10
+ "panels": [
11
+ "pm-doc-inplace-test.js"
12
+ ]
13
+ }
@@ -12,7 +12,7 @@ The workspace runs with no network (D62). Two third-party browser files are comm
12
12
  | Package | Version | Licence | Tarball | Tarball sha256 | Shipped file |
13
13
  |---|---|---|---|---|---|
14
14
  | sortablejs | 1.15.7 | MIT | https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz | `66cbfc8fa18860783584fe0c346baf6cbf3399da72d23d21053425ba232abed4` | `public/vendor/Sortable.min.js`: an unmodified copy of `package/Sortable.min.js` (45 478 bytes, sha256 `bf4241bc73fef7f11c59a283a69fe8051cdd31c6d8ff5a2b9ba219e7831fcf76`; npm integrity `sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==`) |
15
- | lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (13 525 bytes, sha256 `3bb7ec3369206ff1bc8c6ae5dfd91c5d99e5ec89c4357d18b744b594ea366447`). It holds 35 icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, then the eight the frame's chrome draws (Run 3): settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, then globe, columns-2, arrow-up-down and arrow-left-right for the dock strip's web, split and orientation chips (Run 3, C6), then panel-top, panel-left, maximize-2, minimize-2, git-branch and clock for the dock's icon controls and session rows (0.54.2) |
15
+ | lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (17 352 bytes, sha256 `7f9e952399a3c1c91659a527bc1363a008b531ed6e296c2b3c819554ffb0a56f`). It holds 48 icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, then the eight the frame's chrome draws (Run 3): settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, then globe, columns-2, arrow-up-down and arrow-left-right for the dock strip's web, split and orientation chips (Run 3, C6), then panel-top, panel-left, maximize-2, minimize-2, git-branch and clock for the dock's icon controls and session rows (0.54.2), then bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud and share-2 for the document tab's in-place editor, its Confluence copy and its connections |
16
16
 
17
17
  The licences are in this folder: [sortablejs-1.15.7.LICENSE](sortablejs-1.15.7.LICENSE) and [lucide-static-1.52.0.LICENSE](lucide-static-1.52.0.LICENSE). The sprite also carries the lucide licence text as its first comment.
18
18