@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.
- package/client/console.tsx +56 -31
- package/dist/client/console.bundle.js +12 -12
- package/dist/client/console.tsx +56 -31
- package/dist/client/main.bundle.js +106 -106
- package/package.json +3 -3
package/dist/client/console.tsx
CHANGED
|
@@ -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 =
|
|
1702
|
-
const sectionKey = (f: BoardFrame): string =>
|
|
1703
|
-
|
|
1704
|
-
const
|
|
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(
|
|
1714
|
-
: all.filter((f) => !(b.hidden ?? []).includes(
|
|
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
|
|
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 =>
|
|
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
|
|
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?.[
|
|
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?.[
|
|
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(
|
|
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)).
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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}
|