@volter/world-console 3.0.24 → 3.0.26

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.
@@ -5,6 +5,7 @@
5
5
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { Background, BackgroundVariant, Controls, MiniMap, NodeResizer, NodeToolbar, Panel, Position, ReactFlow, ReactFlowProvider, useNodesState, useReactFlow, useStore, type Node, type NodeProps } from '@xyflow/react';
7
7
  import { applyTheme, storedTheme, type Theme, At, Brand, ConfirmHost, MenuDismiss, confirmAct, CopyField, counted, Dialog, DrawerButton, Empty, failure as doorWords, listed, Mark, Menu, Pending, Skeleton, Toaster, toast, useDoor as useKitDoor, vendorName, when, ago, type Load } from './kit/kit.tsx';
8
+ import { boardFrameKey } from '../../../packages/world-core/src/board-keys.ts';
8
9
  import { agentPrompt } from '../../../packages/cli/src/agent-prompt.ts';
9
10
  import { ICONS } from './brand/icons.ts';
10
11
 
@@ -1691,17 +1692,31 @@ const FRAME_W = 1280; const FRAME_H = 820; const FRAME_GAP = 160; const SECTION_
1691
1692
  const FRAME_NAME_ROOM = 72;
1692
1693
  /** A change behind a frame's count: the writes one request made, as words read them, and when. */
1693
1694
  type BoardAct = { at: string; writes: number; entries: Array<{ op: string; operation?: string; subject: { type: string; id: string }; fields: Record<string, string> }> };
1694
- type BoardFrame = { vendor: string; id: string; kind: string; title: string; section: { id: string; title: string; aka?: string[] }; url: string; /** the page it sits under at the vendor (its id there) */ parent?: string; /** the World's own */ ours?: boolean; at: { site: string } | { screen: string } | null; unseen?: 'not-drawn' | 'sites-not-routed' | 'branch-sites'; changes: number; /** when its first change not yet cut was made */ since?: string | null };
1695
+ type BoardFrame = { browser?: string; vendor: string; id: string; kind: string; title: string; section: { id: string; title: string; aka?: string[] }; url: string; /** the page it sits under at the vendor (its id there) */ parent?: string; /** the World's own */ ours?: boolean; at: { site: string } | { screen: string } | null; unseen?: 'not-drawn' | 'sites-not-routed' | 'branch-sites'; changes: number; /** when its first change not yet cut was made */ since?: string | null };
1695
1696
  /** A section of a board: its region and name, or a pack's group removed from the board (`gone`). */
1696
1697
  type SavedSection = { x: number; y: number; w: number; h: number; name?: string; gone?: true };
1697
1698
  type SavedBoard = { id: string; name: string; include?: string[]; hidden?: string[]; placed?: Record<string, { x: number; y: number }>; sections?: Record<string, SavedSection> };
1698
1699
  type BoardBranch = { name: string; origin?: string; sites?: string; at: { instant?: string; label?: string; live?: boolean } | null; expiresAt: string | null };
1699
1700
  const BRANCH_SITES_UNSEEN = "This World shows its branches' sites only on this machine.";
1700
1701
  type BoardDoor = { frames: BoardFrame[]; boards: SavedBoard[]; version: string };
1701
- const frameKey = (f: BoardFrame): string => `${f.vendor}:${f.id}`;
1702
- const sectionKey = (f: BoardFrame): string => `${f.vendor}:section:${f.section.id}`;
1703
- /** Every name a frame's section answers to on a board: its id, and those it was known by (an account a deploy adopted). */
1704
- const sectionKeys = (f: BoardFrame): string[] => [f.section.id, ...(f.section.aka ?? [])].map((id) => `${f.vendor}:section:${id}`);
1702
+ const frameKey = boardFrameKey;
1703
+ const sectionKey = (f: BoardFrame): string => frameKey({ ...f, id: `section:${f.section.id}` });
1704
+ type BoardAliases = { first?: string; frames: Set<string>; sections: Set<string> };
1705
+ const boardAliases = (all: BoardFrame[]): BoardAliases => ({
1706
+ first: all.flatMap(f => f.browser ? [f.browser] : []).sort()[0],
1707
+ frames: new Set(all.filter(f => !f.browser).map(frameKey)),
1708
+ sections: new Set(all.filter(f => !f.browser).flatMap(f => [f.section.id, ...(f.section.aka ?? [])].map(id => `${f.vendor}:section:${id}`))),
1709
+ });
1710
+ const frameKeys = (f: BoardFrame, aliases: BoardAliases): string[] => {
1711
+ const plain = `${f.vendor}:${f.id}`;
1712
+ return f.browser === aliases.first && !aliases.frames.has(plain) ? [frameKey(f), plain] : [frameKey(f)];
1713
+ };
1714
+ /** A section's current and adopted ids; saved plain keys apply only to the first browser copy. */
1715
+ const sectionKeys = (f: BoardFrame, aliases: BoardAliases): string[] => [...new Set([f.section.id, ...(f.section.aka ?? [])].flatMap(id => {
1716
+ const key = frameKey({ ...f, id: `section:${id}` });
1717
+ const plain = `${f.vendor}:section:${id}`;
1718
+ return f.browser === aliases.first && !aliases.sections.has(plain) ? [key, plain] : [key];
1719
+ }))];
1705
1720
  /** Where a frame opens in this World: a site at its own origin, a vendor page at the twin's place, or nowhere yet. */
1706
1721
  const frameUrl = (name: string, f: BoardFrame): string | null => (f.at && 'site' in f.at ? f.at.site : f.at ? `${WORLDS}/${name}/${f.vendor}${f.at.screen}` : null);
1707
1722
  /** A vendor page on a branch always opens through its parent's read-pass door. */
@@ -1709,14 +1724,15 @@ const branchFrameUrl = (name: string, branch: string, f: BoardFrame): string | n
1709
1724
  ? `${WORLDS}/-/${name}/branches/${branch}/pages?${new URLSearchParams({ land: `/${branch}/${f.vendor}${f.at.screen}` }).toString()}`
1710
1725
  : frameUrl(branch, f);
1711
1726
  /** The frames a board shows: everything but what it hides, or what it includes (a frame, or a section's every frame). */
1712
- const shownOn = (b: SavedBoard, all: BoardFrame[]): BoardFrame[] => b.include
1713
- ? all.filter((f) => b.include!.includes(frameKey(f)) || sectionKeys(f).some((k) => b.include!.includes(k)))
1714
- : all.filter((f) => !(b.hidden ?? []).includes(frameKey(f)));
1715
- type Screen = { world: string; branch: boolean; frame: BoardFrame; url: string | null; active: boolean; picked: boolean; boards: SavedBoard[]; addTo: (board: string, key: string) => void; remove: { label: string; act: (key: string) => void } | null; compare: (key: string) => void; /** its name moves it, as a canvas app's frame name does */ grab: (e: React.PointerEvent) => void };
1727
+ const shownOn = (b: SavedBoard, all: BoardFrame[], aliases: BoardAliases): BoardFrame[] => b.include
1728
+ ? all.filter((f) => frameKeys(f, aliases).some(k => b.include!.includes(k)) || sectionKeys(f, aliases).some((k) => b.include!.includes(k)))
1729
+ : all.filter((f) => !frameKeys(f, aliases).some(k => (b.hidden ?? []).includes(k)));
1730
+ type Screen = { aliases: BoardAliases; world: string; branch: boolean; frame: BoardFrame; url: string | null; active: boolean; picked: boolean; boards: SavedBoard[]; addTo: (board: string, key: string) => void; remove: { label: string; act: (key: string) => void } | null; compare: (key: string) => void; /** its name moves it, as a canvas app's frame name does */ grab: (e: React.PointerEvent) => void };
1716
1731
  function ScreenFrame({ data }: NodeProps<Node<Screen>>): React.JSX.Element {
1717
- const { frame, url, active, picked, boards, addTo, remove, compare, grab, branch } = data;
1732
+ const { aliases, frame, url, active, picked, boards, addTo, remove, compare, grab, branch } = data;
1718
1733
  const key = frameKey(frame);
1719
- const others = boards.filter((b) => b.include && !b.include.includes(key) && !sectionKeys(frame).some((k) => b.include!.includes(k)));
1734
+ const iframe = useRef<HTMLIFrameElement>(null);
1735
+ const others = boards.filter((b) => b.include && !frameKeys(frame, aliases).some(k => b.include!.includes(k)) && !sectionKeys(frame, aliases).some((k) => b.include!.includes(k)));
1720
1736
  // its name is drawn the same size at every zoom while the head above it shrinks: zoomed out past the point the name fits
1721
1737
  // there, it is left out, as a canvas app leaves out a frame name that does not fit; selected or in use, it is always shown
1722
1738
  const roomy = useStore((st) => st.transform[2] * (SECTION_PAD + SECTION_HEAD) >= FRAME_NAME_ROOM);
@@ -1732,13 +1748,13 @@ function ScreenFrame({ data }: NodeProps<Node<Screen>>): React.JSX.Element {
1732
1748
  </NodeToolbar>
1733
1749
  <div className={active ? 'frame on' : picked ? 'frame picked' : 'frame'} style={{ width: FRAME_W, height: FRAME_H }}>
1734
1750
  <div className="frame-bar">
1735
- <Mark vendor={frame.vendor} /><strong>{vendorName(frame.vendor)} · {frame.kind}</strong>
1751
+ <Mark vendor={frame.vendor} /><strong>{frame.browser ? `${frame.browser} · ` : ''}{vendorName(frame.vendor)} · {frame.kind}</strong>
1736
1752
  <span className="frame-url">{frame.url.replace(/^https?:\/\//, '')}</span>
1737
1753
  {url ? <a className="nodrag" href={url} target="_blank" rel="noopener noreferrer" aria-label={`Open ${frame.title} in a new tab`}>↗</a> : null}
1738
1754
  </div>
1739
1755
  <div className={active ? 'frame-body nodrag nopan nowheel' : 'frame-body'}>
1740
1756
  {url
1741
- ? <iframe title={frame.title} src={url} loading="lazy" style={{ pointerEvents: active ? 'auto' : 'none' }} />
1757
+ ? <iframe ref={iframe} title={frame.title} src={url} loading="lazy" style={{ pointerEvents: active ? 'auto' : 'none' }} />
1742
1758
  : <div className="frame-none"><p><strong>{frame.unseen === 'branch-sites' ? BRANCH_SITES_UNSEEN : frame.unseen === 'sites-not-routed' ? `This World shows ${frame.title} only at its own local address.` : `${vendorName(frame.vendor)}'s twin draws no page for this ${frame.kind} yet.`}</strong></p><p className="mode">{frame.url}</p></div>}
1743
1759
  </div>
1744
1760
  </div>
@@ -1963,7 +1979,8 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
1963
1979
  }, [token, name, viewing]);
1964
1980
  // read now and every ten seconds while the page is shown: another person's arrangement arrives as the World's changes do
1965
1981
  useEffect(() => { void readBoard(); const id = setInterval(() => { if (!document.hidden) void readBoard(); }, 10000); return () => clearInterval(id); }, [readBoard]);
1966
- const all = loaded.state === 'ok' ? loaded.data.frames.map((f): BoardFrame => viewing && f.at && 'site' in f.at && !/^https?:\/\/\{host\}\./.test(branch?.sites ?? '') ? { ...f, at: null, unseen: 'branch-sites' } : f) : [];
1982
+ const all = loaded.state === 'ok' ? loaded.data.frames.map((f): BoardFrame => viewing && (f.browser || (f.at && 'site' in f.at && !/^https?:\/\/\{host\}\./.test(branch?.sites ?? ''))) ? { ...f, at: null, unseen: 'branch-sites' } : f) : [];
1983
+ const aliases = boardAliases(all);
1967
1984
  const boards = held.current?.boards ?? [{ id: 'everything', name: 'Everything' }];
1968
1985
  // what is shown (architecture, "The board"): the pages whose records changed, everything, or a board people keep.
1969
1986
  // Changes is no board of its own: it is the first board's places and sections, holding only the pages that changed
@@ -2010,13 +2027,13 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
2010
2027
  return run;
2011
2028
  }, [token, name, readBoard, editable]);
2012
2029
  const change = (id: string, edit: (b: SavedBoard) => SavedBoard): void => { void save((list) => list.map((b) => (b.id === id ? edit(b) : b))); };
2013
- const parentKey = (f: BoardFrame): string | null => (f.parent ? `${f.vendor}:${f.parent}` : null);
2030
+ const parentKey = (f: BoardFrame): string | null => f.parent ? frameKey({ ...f, id: f.parent }) : null;
2014
2031
  const keys = new Set(all.map(frameKey));
2015
2032
  // a page under one that is not on the list stands at the top
2016
2033
  const top = (f: BoardFrame): boolean => { const p = parentKey(f); return !p || !keys.has(p); };
2017
2034
  const shown = lens === 'changes' ? all.filter((f) => f.changes)
2018
- : board.include ? shownOn(board, all)
2019
- : shownOn(board, all).filter((f) => top(f) || opened.has(parentKey(f)!));
2035
+ : board.include ? shownOn(board, all, aliases)
2036
+ : shownOn(board, all, aliases).filter((f) => top(f) || opened.has(parentKey(f)!));
2020
2037
  const elsewhere = all.filter((f) => f.changes && !shown.includes(f)).reduce((n, f) => n + f.changes, 0);
2021
2038
  // a frame in use takes the pointer (double-click, as a canvas app enters a frame); one selected shows what can be done to it
2022
2039
  const [active, setActive] = useState<string | null>(null);
@@ -2026,7 +2043,15 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
2026
2043
  const flow = useReactFlow();
2027
2044
  const others = boards.filter((b) => b.include);
2028
2045
  const addTo = (id: string, key: string): void => change(id, (b) => ({ ...b, include: [...(b.include ?? []), key] }));
2029
- const removeFrame = { label: board.include ? 'Take it off this board' : 'Hide it', act: (key: string): void => { setPicked(null); change(board.id, (b) => (b.include ? { ...b, include: b.include.filter((k) => k !== key) } : { ...b, hidden: [...(b.hidden ?? []), key] })); } };
2046
+ const savedFrameKey = (key: string, held: string[]): string => {
2047
+ const f = all.find(frame => frameKey(frame) === key);
2048
+ return f ? frameKeys(f, aliases).find(candidate => held.includes(candidate)) ?? key : key;
2049
+ };
2050
+ const removeFrame = { label: board.include ? 'Take it off this board' : 'Hide it', act: (key: string): void => {
2051
+ setPicked(null);
2052
+ change(board.id, b => b.include ? { ...b, include: b.include.filter(k => k !== savedFrameKey(key, b.include!)) }
2053
+ : { ...b, hidden: [...(b.hidden ?? []), key] });
2054
+ } };
2030
2055
  const rect = (n: Node): { x: number; y: number; w: number; h: number } => ({ x: n.position.x, y: n.position.y, w: n.width ?? n.measured?.width ?? FRAME_W, h: n.height ?? n.measured?.height ?? FRAME_H });
2031
2056
  /** The section a frame sits in, as on a canvas: the smallest whose region holds the frame's middle, the newest of equals
2032
2057
  * (a section drawn around a frame takes it). */
@@ -2040,7 +2065,7 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
2040
2065
  * sections people drew where they drew them. */
2041
2066
  const layout = useCallback((): Node[] => {
2042
2067
  const at = new Map<string, { x: number; y: number }>();
2043
- for (const f of shown) { const p = board.placed?.[frameKey(f)]; if (p) at.set(frameKey(f), p); }
2068
+ for (const f of shown) { const p = frameKeys(f, aliases).map(k => board.placed?.[k]).find(Boolean); if (p) at.set(frameKey(f), p); }
2044
2069
  // a new group goes under the frames placed so far; a section someone draws never moves what is already laid out
2045
2070
  let bottom = Math.max(-FRAME_GAP, ...[...at.values()].map((p) => p.y + FRAME_H));
2046
2071
  const groups = [...new Set(shown.map(sectionKey))];
@@ -2058,7 +2083,7 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
2058
2083
  for (const s of groups) {
2059
2084
  const f = shown.find((x) => sectionKey(x) === s)!;
2060
2085
  // a pack's group as people left it, under whichever id it was known by when they did (an account adopted since)
2061
- const keyed = sectionKeys(f).find((k) => kept[k]) ?? s;
2086
+ const keyed = sectionKeys(f, aliases).find((k) => kept[k]) ?? s;
2062
2087
  const own = kept[keyed];
2063
2088
  if (own?.gone) continue;
2064
2089
  const ps = shown.filter((x) => sectionKey(x) === s).map((x) => at.get(frameKey(x))!);
@@ -2066,16 +2091,16 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
2066
2091
  const around = { x: Math.min(...ps.map((p) => p.x)) - SECTION_PAD, y: Math.min(...ps.map((p) => p.y)) - SECTION_PAD - SECTION_HEAD, w: 0, h: 0 };
2067
2092
  around.w = Math.max(...ps.map((p) => p.x)) + FRAME_W + SECTION_PAD - around.x; around.h = Math.max(...ps.map((p) => p.y)) + FRAME_H + SECTION_PAD - around.y;
2068
2093
  // a group people arranged still takes its new frames: its region reaches over the ones placed after it
2069
- const unplacedOf = shown.filter((x) => sectionKey(x) === s && !board.placed?.[frameKey(x)]).map((x) => at.get(frameKey(x))!);
2094
+ const unplacedOf = shown.filter((x) => sectionKey(x) === s && !frameKeys(x, aliases).some(key => board.placed?.[key])).map((x) => at.get(frameKey(x))!);
2070
2095
  const r = own ? { ...own } : around;
2071
2096
  if (own) for (const p of unplacedOf) { r.w = Math.max(r.w, p.x + FRAME_W + SECTION_PAD - r.x); r.h = Math.max(r.h, p.y + FRAME_H + SECTION_PAD - r.y); }
2072
- sections.push(section(keyed, r, own?.name ?? `${vendorName(f.vendor)} · ${f.section.title}`, f.vendor, Boolean(own?.name)));
2097
+ sections.push(section(s, r, own?.name ?? `${f.browser ? `${f.browser} · ` : ''}${vendorName(f.vendor)} · ${f.section.title}`, f.vendor, Boolean(own?.name)));
2073
2098
  }
2074
2099
  for (const [k, r] of Object.entries(kept)) if (k.startsWith('user:') && !r.gone) sections.push(section(k, r, r.name ?? 'Section', null, true));
2075
2100
  // frames above the sections they sit in; a section's name tag above the board, where it can be clicked
2076
2101
  // a frame's size given, never left to be measured again: a board laid out anew keeps showing what did not change size.
2077
2102
  // A frame moves from anywhere on it, as a canvas app's does, until it is in use (its page then takes the pointer)
2078
- const frames: Node[] = shown.map((f) => ({ id: frameKey(f), type: 'screen', zIndex: 1, width: FRAME_W, height: FRAME_H, draggable: editable, position: at.get(frameKey(f))!, data: { world: name, branch: Boolean(viewing), frame: f, url: viewing ? branchFrameUrl(name, viewing, f) : frameUrl(name, f) } }));
2103
+ const frames: Node[] = shown.map((f) => ({ id: frameKey(f), type: 'screen', zIndex: 1, width: FRAME_W, height: FRAME_H, draggable: editable, position: at.get(frameKey(f))!, data: { aliases, world: name, branch: Boolean(viewing), frame: f, url: viewing ? branchFrameUrl(name, viewing, f) : frameUrl(name, f) } }));
2079
2104
  return [...sections, ...frames];
2080
2105
  // biome-ignore lint/correctness/useExhaustiveDependencies: the frames, the board and its places are what change it
2081
2106
  }, [JSON.stringify(shown), JSON.stringify(board), name, editable, viewing]);
@@ -2130,9 +2155,9 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
2130
2155
  const frames = new Map(moved.filter((m) => !m.id.startsWith('sec:')).map((m) => [m.id, { x: m.x, y: m.y }]));
2131
2156
  const sections = new Set([...(node.type === 'section' ? [(node.data as { key: string }).key] : []), ...moved.filter((m) => m.id.startsWith('sec:')).map((m) => m.id.slice(4))]);
2132
2157
  // a group by every id its section was known by (its kept settings may be under an adopted account's earlier one)
2133
- const groups = new Set(ns.filter((n) => frames.has(n.id)).flatMap((n) => sectionKeys((n.data as Screen).frame)));
2158
+ const groups = new Set(ns.filter((n) => frames.has(n.id)).map((n) => sectionKey((n.data as Screen).frame)));
2134
2159
  for (const n of ns) {
2135
- if (n.type === 'screen' && sectionKeys((n.data as Screen).frame).some((k) => groups.has(k)) && !frames.has(n.id)) frames.set(n.id, { x: n.position.x, y: n.position.y });
2160
+ if (n.type === 'screen' && sectionKeys((n.data as Screen).frame, aliases).some((k) => groups.has(k)) && !frames.has(n.id)) frames.set(n.id, { x: n.position.x, y: n.position.y });
2136
2161
  if (n.type === 'section' && groups.has((n.data as SectionData).key)) sections.add((n.data as SectionData).key);
2137
2162
  }
2138
2163
  keep(Object.fromEntries(frames), [...sections]);
@@ -2140,7 +2165,7 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
2140
2165
  };
2141
2166
  /** A pack's group's frames where they now stand: kept with its section when the section is arranged, so they stay in it. */
2142
2167
  const groupFrames = (key: string): Record<string, { x: number; y: number }> =>
2143
- Object.fromEntries(flow.getNodes().filter((n) => n.type === 'screen' && sectionKeys((n.data as Screen).frame).includes(key)).map((n) => [n.id, { x: n.position.x, y: n.position.y }]));
2168
+ Object.fromEntries(flow.getNodes().filter((n) => n.type === 'screen' && sectionKeys((n.data as Screen).frame, aliases).includes(key)).map((n) => [n.id, { x: n.position.x, y: n.position.y }]));
2144
2169
  // the name being edited where it is shown: a section's (its key) or this board's (`board:<id>`)
2145
2170
  const [renaming, setRenaming] = useState<string | null>(null);
2146
2171
  const renameSection = (key: string): void => { setPicked(`sec:${key}`); setRenaming(key); };
@@ -2195,14 +2220,14 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
2195
2220
  // what each node shows: in use, selected, and what can be done to it
2196
2221
  const shownNodes = nodes.map((n) => (n.type === 'section'
2197
2222
  ? { ...n, data: { ...n.data, grab: grab(n.id), picked: picked === n.id, editable, renaming: editable && renaming === (n.data as { key: string }).key, pick: (k: string) => setPicked(`sec:${k}`), rename: renameSection, renamed: renamedSection, remove: removeSection, resized: () => requestAnimationFrame(() => { const k = (n.data as { key: string }).key; keep(groupFrames(k), [k]); }) } }
2198
- : { ...n, data: { ...n.data, grab: grab(n.id), active: active === n.id, picked: picked === n.id, boards: editable ? others : [], addTo, remove: !editable || (board.include && !board.include.includes(n.id)) ? null : removeFrame, compare: (k: string) => { setPicked(k); setComparing(k); } } }));
2223
+ : { ...n, data: { ...n.data, grab: grab(n.id), active: active === n.id, picked: picked === n.id, boards: editable ? others : [], addTo, remove: !editable || (board.include && !frameKeys((n.data as Screen).frame, aliases).some(k => board.include!.includes(k))) ? null : removeFrame, compare: (k: string) => { setPicked(k); setComparing(k); } } }));
2199
2224
  // the keys a canvas app answers: Esc leaves a frame, then the selection; Delete removes what is selected; ⇧S draws a section
2200
2225
  useEffect(() => {
2201
2226
  const on = (e: KeyboardEvent): void => {
2202
2227
  if ((e.target as HTMLElement | null)?.closest('input, textarea, select, [contenteditable]')) return;
2203
2228
  if (e.key === 'Escape') { if (active) setActive(null); else setPicked(null); return; }
2204
2229
  if (!editable || active) return;
2205
- if ((e.key === 'Delete' || e.key === 'Backspace') && picked) { e.preventDefault(); if (picked.startsWith('sec:')) removeSection(picked.slice(4)); else if (!board.include || board.include.includes(picked)) removeFrame.act(picked); else toast('It is on this board with its whole group: take the group off in the list to remove it', 'error'); return; }
2230
+ if ((e.key === 'Delete' || e.key === 'Backspace') && picked) { e.preventDefault(); if (picked.startsWith('sec:')) removeSection(picked.slice(4)); else if (!board.include || all.some(f => frameKey(f) === picked && frameKeys(f, aliases).some(k => board.include!.includes(k)))) removeFrame.act(picked); else toast('It is on this board with its whole group: take the group off in the list to remove it', 'error'); return; }
2206
2231
  if (e.key === 'S' && e.shiftKey && !e.metaKey && !e.ctrlKey) { e.preventDefault(); addSection(); }
2207
2232
  };
2208
2233
  document.addEventListener('keydown', on); return () => document.removeEventListener('keydown', on);
@@ -2222,8 +2247,8 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
2222
2247
  const rename = (): void => setRenaming(`board:${board.id}`);
2223
2248
  const drop = (): void => { const gone = board.id; setBoardId(null); void save((list) => list.filter((b) => b.id !== gone)); };
2224
2249
  const groups = [...new Map(all.map((f) => [sectionKey(f), f])).values()];
2225
- const missing = board.include ? [...groups.filter((f) => !sectionKeys(f).some((k) => board.include!.includes(k))).map((f) => ({ key: sectionKey(f), label: `${vendorName(f.vendor)} · ${f.section.title} (all)` })), ...all.filter((f) => !shown.includes(f)).map((f) => ({ key: frameKey(f), label: f.title }))] : [];
2226
- const hidden = board.include ? [] : all.filter((f) => (board.hidden ?? []).includes(frameKey(f)));
2250
+ const missing = board.include ? [...groups.filter((f) => !sectionKeys(f, aliases).some((k) => board.include!.includes(k))).map((f) => ({ key: sectionKey(f), label: `${f.browser ? `${f.browser} · ` : ''}${vendorName(f.vendor)} · ${f.section.title} (all)` })), ...all.filter((f) => !shown.includes(f)).map((f) => ({ key: frameKey(f), label: `${f.browser ? `${f.browser} · ` : ''}${f.title}` }))] : [];
2251
+ const hidden = board.include ? [] : all.filter((f) => frameKeys(f, aliases).some(k => (board.hidden ?? []).includes(k)));
2227
2252
  // the layers as the canvas has them: each section with the frames in it, then the frames in none
2228
2253
  const sectionNodes = nodes.filter((n) => n.type === 'section');
2229
2254
  const frameNodes = nodes.filter((n) => n.type === 'screen');
@@ -2277,7 +2302,7 @@ function CanvasSurface({ name, token, session, retry, snapshot, back, canWrite,
2277
2302
  </ul>
2278
2303
  {board.include && !shown.length ? <p className="mode pad-s">An empty board: add frames from Everything, or below.</p> : null}
2279
2304
  {missing.length && editable ? <select className="board-add" value="" aria-label="Add to this board" onChange={(e) => { const k = e.target.value; if (k) change(board.id, (b) => ({ ...b, include: [...(b.include ?? []), k] })); }}><option value="">Add to this board…</option>{missing.map((m) => <option key={m.key} value={m.key}>{m.label}</option>)}</select> : null}
2280
- {hidden.length ? <details className="board-hidden"><summary>Hidden ({hidden.length})</summary><ul className="layer-list">{hidden.map((f) => <li key={frameKey(f)}>{editable ? <button type="button" onClick={() => change(board.id, (b) => ({ ...b, hidden: (b.hidden ?? []).filter((k) => k !== frameKey(f)) }))}><span>{f.title}</span><span className="mode">Show</span></button> : <span className="pad-s">{f.title}</span>}</li>)}</ul></details> : null}
2305
+ {hidden.length ? <details className="board-hidden"><summary>Hidden ({hidden.length})</summary><ul className="layer-list">{hidden.map((f) => <li key={frameKey(f)}>{editable ? <button type="button" onClick={() => change(board.id, (b) => ({ ...b, hidden: (b.hidden ?? []).filter(k => k !== savedFrameKey(frameKey(f), b.hidden ?? [])) }))}><span>{f.browser ? `${f.browser} · ` : ''}{f.title}</span><span className="mode">Show</span></button> : <span className="pad-s">{f.browser ? `${f.browser} · ` : ''}{f.title}</span>}</li>)}</ul></details> : null}
2281
2306
  {elsewhere ? <button type="button" className="board-elsewhere" onClick={() => setBoardId('changes')}><Icon name="square-dot" size={14} />{counted(elsewhere, 'change')} not on this board</button> : null}
2282
2307
  <Pending load={loaded} what="the board" />
2283
2308
  {stale && loaded.state === 'ok' ? <p role="alert" className="pad-s">The board could not be read again ({stale}): it shows what was last read.</p> : null}