@nelsonlongxiang/dsh-open-a2a-net 0.6.41 → 0.6.43

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.
Files changed (72) hide show
  1. package/README.md +2 -1
  2. package/README_zh.md +2 -1
  3. package/assets/nexusDist/assets/index-CaA76j0F.js +9 -0
  4. package/assets/nexusDist/index.html +1 -1
  5. package/assets/stageDist/assets/index-Bj7S2pA1.css +1 -1
  6. package/assets/stageDist/assets/index-C-28k_o5.js +974 -974
  7. package/assets/stageDist/index.html +12 -12
  8. package/cli/a2a.mjs +24 -4
  9. package/lib/canvas-store.js +249 -17
  10. package/lib/client.js +62 -72
  11. package/lib/client.js.map +1 -1
  12. package/lib/config.js +1 -0
  13. package/lib/faces/admin.js +103 -63
  14. package/lib/faces/wire.js +3 -1
  15. package/lib/index.js +116 -22
  16. package/lib/origin-claim.js +4 -0
  17. package/lib/skill/SKILL.md +117 -117
  18. package/lib/skill/references/collaboration.md +428 -428
  19. package/lib/skill/references/discovery.md +78 -78
  20. package/lib/skill/references/intake.md +107 -107
  21. package/lib/skill/references/membership.md +73 -73
  22. package/lib/skill/references/pitfalls.md +51 -51
  23. package/lib/skill/references/router.md +96 -96
  24. package/lib/skill/references/routing.md +65 -65
  25. package/lib/types/canvas-store.d.ts +48 -1
  26. package/lib/types/config.d.ts +2 -0
  27. package/lib/types/faces/admin.d.ts +2 -0
  28. package/lib/types/origin-claim.d.ts +2 -0
  29. package/nexus-stage/package.json +21 -21
  30. package/nexus-stage/scripts/mockhost.py +261 -261
  31. package/nexus-stage/src/canvas-ops.ts +191 -191
  32. package/nexus-stage/src/canvas-wire.ts +164 -164
  33. package/nexus-stage/src/cli-home-picker.ts +42 -42
  34. package/nexus-stage/src/fault.ts +56 -56
  35. package/nexus-stage/src/federation-overlay.ts +103 -103
  36. package/nexus-stage/src/federation.ts +241 -241
  37. package/nexus-stage/src/layout-doc.ts +145 -145
  38. package/nexus-stage/src/main.ts +520 -513
  39. package/nexus-stage/src/planning-card-text.ts +73 -73
  40. package/nexus-stage/src/planning-card.ts +63 -63
  41. package/nexus-stage/src/planning-edges.ts +26 -26
  42. package/nexus-stage/src/planning-fit.ts +45 -45
  43. package/nexus-stage/src/planning-frame-hue.ts +13 -13
  44. package/nexus-stage/src/planning-legend.ts +39 -39
  45. package/nexus-stage/src/planning-member-handles.ts +61 -61
  46. package/nexus-stage/src/planning-menus.ts +422 -422
  47. package/nexus-stage/src/planning-mirror.ts +75 -75
  48. package/nexus-stage/src/planning-nodelist.ts +294 -294
  49. package/nexus-stage/src/planning-overlap.ts +159 -159
  50. package/nexus-stage/src/planning-reachability.ts +28 -28
  51. package/nexus-stage/src/planning-relayout.ts +121 -121
  52. package/nexus-stage/src/planning-roster.ts +15 -15
  53. package/nexus-stage/src/planning-search.ts +168 -168
  54. package/nexus-stage/src/planning-types.ts +115 -115
  55. package/nexus-stage/src/planning-undo.ts +68 -68
  56. package/nexus-stage/src/planning-view.ts +1499 -1499
  57. package/nexus-stage/src/planning.css +340 -340
  58. package/nexus-stage/src/seat-name-editor.ts +31 -31
  59. package/nexus-stage/src/session-drag.ts +64 -64
  60. package/nexus-stage/src/viewport.ts +106 -106
  61. package/nexus-stage/src/wb-chat.ts +684 -684
  62. package/nexus-stage/src/wb-directory.ts +237 -237
  63. package/nexus-stage/src/wb-ledger.ts +279 -279
  64. package/nexus-stage/src/wb-names.ts +43 -43
  65. package/nexus-stage/src/wb-rail.ts +147 -147
  66. package/nexus-stage/src/workbench.css +388 -388
  67. package/nexus-stage/src/workbench.ts +1294 -1272
  68. package/nexus-stage/src/world.ts +509 -509
  69. package/nexus-stage/tsconfig.json +24 -24
  70. package/nexus-stage/vite.config.ts +15 -15
  71. package/package.json +1 -1
  72. package/assets/nexusDist/assets/index-NkNSeIx6.js +0 -9
@@ -1,15 +1,15 @@
1
- /** Roster frames use an independent layout namespace beside canvas frames. */
2
- import type { PlanningTeam } from './planning-types'
3
-
4
- export const ROSTER_PREFIX = 'roster:'
5
- export const rosterNameOf = (frame: string): string | undefined => frame.startsWith(ROSTER_PREFIX) ? frame.slice(ROSTER_PREFIX.length) : undefined
6
- export const rosterFrameName = (name: string): string => ROSTER_PREFIX + name
7
- export const rosterFrameTitle = (frame: string): string => { const name = rosterNameOf(frame); return name === undefined ? frame : `网络队 · roster · ${name}` }
8
- export function rosterFrames(rows: readonly { team: string; members: readonly string[] }[], zone: string): PlanningTeam[] {
9
- return rows.flatMap(row => {
10
- if (zone === '' || !row.team.startsWith(zone + '/') || !Array.isArray(row.members)) return []
11
- const name = row.team.slice(zone.length + 1)
12
- if (name === '' || name.length > 40 || name.includes('/') || name.startsWith(ROSTER_PREFIX) || name.includes('*')) return []
13
- return [{ name: rosterFrameName(name), team: row.team, kind: 'roster' as const, members: row.members.filter(id => typeof id === 'string' && id !== '').map(id => ({ id })) }]
14
- })
15
- }
1
+ /** Roster frames use an independent layout namespace beside canvas frames. */
2
+ import type { PlanningTeam } from './planning-types'
3
+
4
+ export const ROSTER_PREFIX = 'roster:'
5
+ export const rosterNameOf = (frame: string): string | undefined => frame.startsWith(ROSTER_PREFIX) ? frame.slice(ROSTER_PREFIX.length) : undefined
6
+ export const rosterFrameName = (name: string): string => ROSTER_PREFIX + name
7
+ export const rosterFrameTitle = (frame: string): string => { const name = rosterNameOf(frame); return name === undefined ? frame : `网络队 · roster · ${name}` }
8
+ export function rosterFrames(rows: readonly { team: string; members: readonly string[] }[], zone: string): PlanningTeam[] {
9
+ return rows.flatMap(row => {
10
+ if (zone === '' || !row.team.startsWith(zone + '/') || !Array.isArray(row.members)) return []
11
+ const name = row.team.slice(zone.length + 1)
12
+ if (name === '' || name.length > 40 || name.includes('/') || name.startsWith(ROSTER_PREFIX) || name.includes('*')) return []
13
+ return [{ name: rosterFrameName(name), team: row.team, kind: 'roster' as const, members: row.members.filter(id => typeof id === 'string' && id !== '').map(id => ({ id })) }]
14
+ })
15
+ }
@@ -1,168 +1,168 @@
1
- /**
2
- * 画板全局搜索(属主令 W1): a toolbar search box (Ctrl+K focuses) over the
3
- * board's node FACTS — display name, zone/id8 handle, and host spelling, in
4
- * one substring match (case-insensitive). Picking a hit rides the existing
5
- * fitView flight chain (the caller frames ~100% and pulses the card);
6
- * Escape clears. Pure front end: no backend, no routing, no persistence.
7
- * @module nexus-stage/planning-search
8
- */
9
- import { fitView, type LayoutViewport } from './viewport'
10
- import { NODE_W, NODE_H } from './world'
11
- /** One searchable node fact row (already display-resolved by the caller). */
12
- export interface SearchHit {
13
- readonly id: string
14
- /** Display name (member cards' person name, peer hosts' spelling). */
15
- readonly name: string
16
- /** The zone/id8 handle (n.team) and any extra host tokens. */
17
- readonly team: string
18
- /** Optional host/owner token (peer cards carry the host spelling). */
19
- readonly host?: string
20
- }
21
-
22
- /** Case-insensitive substring match across name / team / host. */
23
- export function matchHits(rows: ReadonlyArray<SearchHit>, query: string): SearchHit[] {
24
- const q = query.trim().toLowerCase()
25
- if (q === '') return []
26
- return rows.filter(r =>
27
- r.name.toLowerCase().includes(q) || r.team.toLowerCase().includes(q) || (r.host ?? '').toLowerCase().includes(q))
28
- .slice(0, 8)
29
- }
30
-
31
- /**
32
- * Build the searchable rows from the board model — the SAME display facts
33
- * the cards render (member display name / peer host spelling / its handle),
34
- * so every hit is a card the user can see.
35
- */
36
- export function hitsFromModel(model: { allNodes(): ReadonlyArray<{ id: string; name?: string; team: string; remote?: boolean }> }, displayName: (n: never) => string): SearchHit[] {
37
- return model.allNodes().map(n => {
38
- const host = n.id.slice('peer-'.length)
39
- const name = n.id.startsWith('member-')
40
- ? (n.name !== undefined && n.name !== '' ? n.name : n.team)
41
- : n.remote === true ? host : displayName(n as never)
42
- return { id: n.id, name, team: n.team, ...(n.remote === true ? { host } : {}) }
43
- })
44
- }
45
-
46
- /** W1 pulse: briefly ring the picked card (class removed on the timer). */
47
- export function pulseCard(el: HTMLElement | undefined, ms = 1200): void {
48
- if (el === undefined) return
49
- el.classList.add('pulse')
50
- setTimeout(() => el.classList.remove('pulse'), ms)
51
- }
52
-
53
- /** The view seams the node flyer needs (the view keeps owning its model). */
54
- export interface NodeFlyerSpec {
55
- readonly model: { getNode(id: string): { x: number; y: number } | undefined }
56
- readonly viewSize: () => { w: number; h: number }
57
- readonly setVp: (vp: LayoutViewport) => void
58
- readonly applyViewport: () => void
59
- readonly markDirty: () => void
60
- readonly cardEl: (id: string) => HTMLElement | undefined
61
- }
62
-
63
- /**
64
- * W1 pick flight: frame ONE node's rect at ~100% (the same fitView chain the
65
- * statusbar fly rides) and pulse its card for 1.2s.
66
- */
67
- export function createNodeFlyer(spec: NodeFlyerSpec): (id: string) => void {
68
- return id => {
69
- const n = spec.model.getNode(id)
70
- if (n === undefined) return
71
- const size = spec.viewSize()
72
- // fitView takes WorldBounds ({minX,minY,maxX,maxY}), not the card rect —
73
- // passing {x,y,w,h} silently yielded NaN → IDENTITY (flight never landed).
74
- spec.setVp(fitView({ minX: n.x, minY: n.y, maxX: n.x + NODE_W, maxY: n.y + NODE_H }, size.w, size.h, 1))
75
- spec.applyViewport()
76
- spec.markDirty()
77
- pulseCard(spec.cardEl(id))
78
- }
79
- }
80
-
81
- export interface SearchMount extends PlanningSearch {
82
- /** Rebuild the index from the model (call from the node reconcile). */
83
- refresh(): void
84
- }
85
-
86
- /**
87
- * Mount the whole search surface into the toolbar — the wiring lines the
88
- * view used to carry live here (create + append + index builder), keeping
89
- * the view under the M1 ratchet; the view keeps ONE call.
90
- */
91
- export function mountBoardSearch(
92
- toolbar: HTMLElement,
93
- model: Parameters<typeof hitsFromModel>[0],
94
- displayName: Parameters<typeof hitsFromModel>[1],
95
- onPick: (id: string) => void,
96
- ): SearchMount {
97
- const search = createPlanningSearch({ onPick })
98
- toolbar.appendChild(search.root)
99
- return { ...search, refresh: () => search.setIndex(hitsFromModel(model, displayName)) }
100
- }
101
-
102
- export interface PlanningSearch {
103
- readonly root: HTMLElement
104
- /** Refresh the searchable index (called from the node reconcile). */
105
- setIndex(rows: ReadonlyArray<SearchHit>): void
106
- /** Focus the box (Ctrl+K) and select the current text. */
107
- focus(): void
108
- /** Clear the query and the dropdown (Escape elsewhere). */
109
- clear(): void
110
- }
111
-
112
- /** Build the search box widget (input + hit list + keyboard handling). */
113
- export function createPlanningSearch(opts: { onPick: (id: string) => void }): PlanningSearch {
114
- const root = document.createElement('div')
115
- root.className = 'p-search'
116
- const input = document.createElement('input')
117
- input.type = 'search'
118
- input.className = 'p-search-input'
119
- // Short enough to read whole in the 170px box (review 2026-10-27); the
120
- // full scope rides the tooltip and the accessible name.
121
- input.placeholder = '🔍 搜索(Ctrl+K)'
122
- input.title = '搜索名字 / 句柄 / 宿主(Ctrl+K 聚焦,Esc 清空)'
123
- input.setAttribute('aria-label', '搜索画板节点:名字、句柄或宿主')
124
- const list = document.createElement('div')
125
- list.className = 'p-search-list'
126
- let rows: ReadonlyArray<SearchHit> = []
127
- let hits: SearchHit[] = []
128
- let active = 0
129
- const close = (): void => { list.style.display = 'none'; list.textContent = '' }
130
- const paint = (): void => {
131
- list.textContent = ''
132
- if (hits.length === 0) { close(); return }
133
- hits.forEach((h, i) => {
134
- const row = document.createElement('div')
135
- row.className = 'p-search-hit' + (i === active ? ' active' : '')
136
- row.dataset.id = h.id
137
- row.textContent = h.name === h.team ? h.name : `${h.name} · ${h.team}`
138
- row.addEventListener('mousedown', ev => { ev.preventDefault(); opts.onPick(h.id); clearBox() })
139
- list.appendChild(row)
140
- })
141
- list.style.display = 'block'
142
- }
143
- const clearBox = (): void => { input.value = ''; hits = []; close() }
144
- const refresh = (): void => { hits = matchHits(rows, input.value); active = 0; paint() }
145
- input.addEventListener('input', refresh)
146
- input.addEventListener('keydown', ev => {
147
- if (ev.key === 'Escape') { clearBox(); input.blur(); ev.stopPropagation(); return }
148
- if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
149
- if (hits.length === 0) return
150
- ev.preventDefault()
151
- active = (active + (ev.key === 'ArrowDown' ? 1 : hits.length - 1)) % hits.length
152
- paint()
153
- return
154
- }
155
- if (ev.key === 'Enter' && hits.length > 0) {
156
- ev.preventDefault()
157
- opts.onPick(hits[active]!.id)
158
- clearBox()
159
- }
160
- })
161
- root.append(input, list)
162
- return {
163
- root,
164
- setIndex(next) { rows = next; if (input.value !== '') refresh() },
165
- focus() { input.focus(); input.select() },
166
- clear() { clearBox() },
167
- }
168
- }
1
+ /**
2
+ * 画板全局搜索(属主令 W1): a toolbar search box (Ctrl+K focuses) over the
3
+ * board's node FACTS — display name, zone/id8 handle, and host spelling, in
4
+ * one substring match (case-insensitive). Picking a hit rides the existing
5
+ * fitView flight chain (the caller frames ~100% and pulses the card);
6
+ * Escape clears. Pure front end: no backend, no routing, no persistence.
7
+ * @module nexus-stage/planning-search
8
+ */
9
+ import { fitView, type LayoutViewport } from './viewport'
10
+ import { NODE_W, NODE_H } from './world'
11
+ /** One searchable node fact row (already display-resolved by the caller). */
12
+ export interface SearchHit {
13
+ readonly id: string
14
+ /** Display name (member cards' person name, peer hosts' spelling). */
15
+ readonly name: string
16
+ /** The zone/id8 handle (n.team) and any extra host tokens. */
17
+ readonly team: string
18
+ /** Optional host/owner token (peer cards carry the host spelling). */
19
+ readonly host?: string
20
+ }
21
+
22
+ /** Case-insensitive substring match across name / team / host. */
23
+ export function matchHits(rows: ReadonlyArray<SearchHit>, query: string): SearchHit[] {
24
+ const q = query.trim().toLowerCase()
25
+ if (q === '') return []
26
+ return rows.filter(r =>
27
+ r.name.toLowerCase().includes(q) || r.team.toLowerCase().includes(q) || (r.host ?? '').toLowerCase().includes(q))
28
+ .slice(0, 8)
29
+ }
30
+
31
+ /**
32
+ * Build the searchable rows from the board model — the SAME display facts
33
+ * the cards render (member display name / peer host spelling / its handle),
34
+ * so every hit is a card the user can see.
35
+ */
36
+ export function hitsFromModel(model: { allNodes(): ReadonlyArray<{ id: string; name?: string; team: string; remote?: boolean }> }, displayName: (n: never) => string): SearchHit[] {
37
+ return model.allNodes().map(n => {
38
+ const host = n.id.slice('peer-'.length)
39
+ const name = n.id.startsWith('member-')
40
+ ? (n.name !== undefined && n.name !== '' ? n.name : n.team)
41
+ : n.remote === true ? host : displayName(n as never)
42
+ return { id: n.id, name, team: n.team, ...(n.remote === true ? { host } : {}) }
43
+ })
44
+ }
45
+
46
+ /** W1 pulse: briefly ring the picked card (class removed on the timer). */
47
+ export function pulseCard(el: HTMLElement | undefined, ms = 1200): void {
48
+ if (el === undefined) return
49
+ el.classList.add('pulse')
50
+ setTimeout(() => el.classList.remove('pulse'), ms)
51
+ }
52
+
53
+ /** The view seams the node flyer needs (the view keeps owning its model). */
54
+ export interface NodeFlyerSpec {
55
+ readonly model: { getNode(id: string): { x: number; y: number } | undefined }
56
+ readonly viewSize: () => { w: number; h: number }
57
+ readonly setVp: (vp: LayoutViewport) => void
58
+ readonly applyViewport: () => void
59
+ readonly markDirty: () => void
60
+ readonly cardEl: (id: string) => HTMLElement | undefined
61
+ }
62
+
63
+ /**
64
+ * W1 pick flight: frame ONE node's rect at ~100% (the same fitView chain the
65
+ * statusbar fly rides) and pulse its card for 1.2s.
66
+ */
67
+ export function createNodeFlyer(spec: NodeFlyerSpec): (id: string) => void {
68
+ return id => {
69
+ const n = spec.model.getNode(id)
70
+ if (n === undefined) return
71
+ const size = spec.viewSize()
72
+ // fitView takes WorldBounds ({minX,minY,maxX,maxY}), not the card rect —
73
+ // passing {x,y,w,h} silently yielded NaN → IDENTITY (flight never landed).
74
+ spec.setVp(fitView({ minX: n.x, minY: n.y, maxX: n.x + NODE_W, maxY: n.y + NODE_H }, size.w, size.h, 1))
75
+ spec.applyViewport()
76
+ spec.markDirty()
77
+ pulseCard(spec.cardEl(id))
78
+ }
79
+ }
80
+
81
+ export interface SearchMount extends PlanningSearch {
82
+ /** Rebuild the index from the model (call from the node reconcile). */
83
+ refresh(): void
84
+ }
85
+
86
+ /**
87
+ * Mount the whole search surface into the toolbar — the wiring lines the
88
+ * view used to carry live here (create + append + index builder), keeping
89
+ * the view under the M1 ratchet; the view keeps ONE call.
90
+ */
91
+ export function mountBoardSearch(
92
+ toolbar: HTMLElement,
93
+ model: Parameters<typeof hitsFromModel>[0],
94
+ displayName: Parameters<typeof hitsFromModel>[1],
95
+ onPick: (id: string) => void,
96
+ ): SearchMount {
97
+ const search = createPlanningSearch({ onPick })
98
+ toolbar.appendChild(search.root)
99
+ return { ...search, refresh: () => search.setIndex(hitsFromModel(model, displayName)) }
100
+ }
101
+
102
+ export interface PlanningSearch {
103
+ readonly root: HTMLElement
104
+ /** Refresh the searchable index (called from the node reconcile). */
105
+ setIndex(rows: ReadonlyArray<SearchHit>): void
106
+ /** Focus the box (Ctrl+K) and select the current text. */
107
+ focus(): void
108
+ /** Clear the query and the dropdown (Escape elsewhere). */
109
+ clear(): void
110
+ }
111
+
112
+ /** Build the search box widget (input + hit list + keyboard handling). */
113
+ export function createPlanningSearch(opts: { onPick: (id: string) => void }): PlanningSearch {
114
+ const root = document.createElement('div')
115
+ root.className = 'p-search'
116
+ const input = document.createElement('input')
117
+ input.type = 'search'
118
+ input.className = 'p-search-input'
119
+ // Short enough to read whole in the 170px box (review 2026-10-27); the
120
+ // full scope rides the tooltip and the accessible name.
121
+ input.placeholder = '🔍 搜索(Ctrl+K)'
122
+ input.title = '搜索名字 / 句柄 / 宿主(Ctrl+K 聚焦,Esc 清空)'
123
+ input.setAttribute('aria-label', '搜索画板节点:名字、句柄或宿主')
124
+ const list = document.createElement('div')
125
+ list.className = 'p-search-list'
126
+ let rows: ReadonlyArray<SearchHit> = []
127
+ let hits: SearchHit[] = []
128
+ let active = 0
129
+ const close = (): void => { list.style.display = 'none'; list.textContent = '' }
130
+ const paint = (): void => {
131
+ list.textContent = ''
132
+ if (hits.length === 0) { close(); return }
133
+ hits.forEach((h, i) => {
134
+ const row = document.createElement('div')
135
+ row.className = 'p-search-hit' + (i === active ? ' active' : '')
136
+ row.dataset.id = h.id
137
+ row.textContent = h.name === h.team ? h.name : `${h.name} · ${h.team}`
138
+ row.addEventListener('mousedown', ev => { ev.preventDefault(); opts.onPick(h.id); clearBox() })
139
+ list.appendChild(row)
140
+ })
141
+ list.style.display = 'block'
142
+ }
143
+ const clearBox = (): void => { input.value = ''; hits = []; close() }
144
+ const refresh = (): void => { hits = matchHits(rows, input.value); active = 0; paint() }
145
+ input.addEventListener('input', refresh)
146
+ input.addEventListener('keydown', ev => {
147
+ if (ev.key === 'Escape') { clearBox(); input.blur(); ev.stopPropagation(); return }
148
+ if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
149
+ if (hits.length === 0) return
150
+ ev.preventDefault()
151
+ active = (active + (ev.key === 'ArrowDown' ? 1 : hits.length - 1)) % hits.length
152
+ paint()
153
+ return
154
+ }
155
+ if (ev.key === 'Enter' && hits.length > 0) {
156
+ ev.preventDefault()
157
+ opts.onPick(hits[active]!.id)
158
+ clearBox()
159
+ }
160
+ })
161
+ root.append(input, list)
162
+ return {
163
+ root,
164
+ setIndex(next) { rows = next; if (input.value !== '') refresh() },
165
+ focus() { input.focus(); input.select() },
166
+ clear() { clearBox() },
167
+ }
168
+ }
@@ -1,115 +1,115 @@
1
- /**
2
- * Public shapes of the planning board: its input, dependencies, the
3
- * handler seam tests drive, and the view surface. planning-view re-exports
4
- * all of them, so importers keep using `./planning-view`.
5
- * @module nexus-stage/planning-types
6
- */
7
- import type { CanvasAction } from './canvas-ops'
8
- import type { InFlightRow, PeerRow, RemoteTeamRow } from './federation'
9
- import type { LampState } from './layout-wire'
10
- import type { LayoutViewport } from './viewport'
11
- import type { SessionLite, TeamLite } from './world'
12
-
13
- /** Minimal session/team shapes consumed here (topology rows satisfy them). */
14
- export interface PlanningSession extends SessionLite {}
15
- export interface PlanningTeam extends TeamLite { kind?: 'roster' }
16
-
17
- export interface PlanningInput {
18
- sessions: ReadonlyArray<PlanningSession>
19
- teams: ReadonlyArray<PlanningTeam>
20
- peerCount: number
21
- /** Federation peers (PR D): rows for the badge column right of the content. */
22
- peers?: ReadonlyArray<PeerRow>
23
- /** Pending outbound routes (PR D): the accent activity edges. */
24
- inFlight?: ReadonlyArray<InFlightRow>
25
- /** Remote team directory rows (PR D v1 ruling): grouped under peer cards. */
26
- remoteTeams?: ReadonlyArray<RemoteTeamRow>
27
- /** gh#41 fleet planning: each tracked peer's published canvas org —
28
- * rendered as REMOTE team frames (read + cross-host regulation). */
29
- peerOrg?: ReadonlyArray<{ url: string; unreachable?: boolean; org: ReadonlyArray<{ name: string; team: string; members: readonly string[] }> }>
30
- }
31
-
32
- /** Screen-width class a viewport is remembered under (window width, not board width — collapsing the rail must not switch buckets). */
33
- export type ViewportBucket = 'narrow' | 'medium' | 'wide'
34
-
35
- export function viewportBucket(width: number): ViewportBucket {
36
- return width < 680 ? 'narrow' : width < 1024 ? 'medium' : 'wide'
37
- }
38
-
39
- export interface PlanningDeps {
40
- /** An edit that should persist happened (drag/nudge/pan/zoom/marquee end). */
41
- onDirty(): void
42
- /** The save lamp was clicked (error state -> retry). */
43
- onLampClick(): void
44
- /** Viewport size in CSS px (defaults to the root element's box; tests
45
- * inject fixed sizes so fit math is assertable without layout). */
46
- viewSize?(): { w: number; h: number }
47
- /**
48
- * Per-device viewport memory. The saved layout's viewport is the LAST
49
- * saver's screen, so a desktop pan landed on a phone (and any other
50
- * device's save yanked this one's view). When present, the first adopted
51
- * layout takes this screen bucket's own viewport — else, on a wide screen,
52
- * the host's (where it was most likely saved), else a fit — later
53
- * adoptions leave the viewport alone, and every persisted edit writes the
54
- * bucket back. Optional: without it the host viewport wins as before.
55
- */
56
- viewportStore?: { get(bucket: ViewportBucket): unknown; set(bucket: ViewportBucket, vp: LayoutViewport): void }
57
- /**
58
- * A team write action (optimistic state already applied by the view).
59
- * Resolves true when the host accepted it — or it was an idempotent
60
- * no-op (ok:false without an error string). Resolves false when the
61
- * write was refused/failed: the view then rolls the action's scoped
62
- * undo back itself. Rejecting the promise is also handled (treated as
63
- * false) — the channel must not leave the team's guard pinned.
64
- */
65
- onCanvasAction(a: CanvasAction): Promise<boolean>
66
- /** A plain (non-shift, no-drag) left click selected exactly one node
67
- * (0.6.13): the host may open the reading face (chat drawer) for it.
68
- * Selection itself is already applied — this is a notification, not a
69
- * command; the host decides what (if anything) to show. */
70
- onNodePick?(id: string): void
71
- /** Wall clock for the inFlight age labels (tests pin it; default Date.now). */
72
- now?(): number
73
- }
74
-
75
- /** Pointer-like event surface the handlers consume (real events satisfy it). */
76
- export interface SeamPointer {
77
- button: number
78
- shiftKey: boolean
79
- ctrlKey: boolean
80
- clientX: number
81
- clientY: number
82
- pointerId?: number
83
- target: Element | null
84
- deltaX?: number
85
- deltaY?: number
86
- deltaMode?: number
87
- preventDefault(): void
88
- }
89
-
90
- export interface SeamKey { key: string; shiftKey: boolean; altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; target: Element | null; preventDefault(): void }
91
-
92
- export interface PlanningView {
93
- root: HTMLElement
94
- reconcile(input: PlanningInput): void
95
- activate(): void
96
- deactivate(): void
97
- /** Adopt a (possibly normalized) layout document when the save loop allows. */
98
- adoptExternalLayout(layout: unknown): void
99
- /** The current document for the save loop, read at send time. */
100
- snapshotDoc(): unknown
101
- setLamp(state: LampState, savedAt?: string): void
102
- /** A transient notice (host error verbatim or an info hint). */
103
- notice(kind: 'error' | 'info', text: string): void
104
- seam: {
105
- pointerDown(ev: SeamPointer): void
106
- pointerMove(ev: SeamPointer): void
107
- pointerUp(ev: SeamPointer): void
108
- pointerCancel(ev: SeamPointer): void
109
- wheel(ev: SeamPointer): void
110
- key(ev: SeamKey): void
111
- keyUp(ev: SeamKey): void
112
- contextMenu(ev: SeamPointer): void
113
- }
114
- destroy(): void
115
- }
1
+ /**
2
+ * Public shapes of the planning board: its input, dependencies, the
3
+ * handler seam tests drive, and the view surface. planning-view re-exports
4
+ * all of them, so importers keep using `./planning-view`.
5
+ * @module nexus-stage/planning-types
6
+ */
7
+ import type { CanvasAction } from './canvas-ops'
8
+ import type { InFlightRow, PeerRow, RemoteTeamRow } from './federation'
9
+ import type { LampState } from './layout-wire'
10
+ import type { LayoutViewport } from './viewport'
11
+ import type { SessionLite, TeamLite } from './world'
12
+
13
+ /** Minimal session/team shapes consumed here (topology rows satisfy them). */
14
+ export interface PlanningSession extends SessionLite {}
15
+ export interface PlanningTeam extends TeamLite { kind?: 'roster' }
16
+
17
+ export interface PlanningInput {
18
+ sessions: ReadonlyArray<PlanningSession>
19
+ teams: ReadonlyArray<PlanningTeam>
20
+ peerCount: number
21
+ /** Federation peers (PR D): rows for the badge column right of the content. */
22
+ peers?: ReadonlyArray<PeerRow>
23
+ /** Pending outbound routes (PR D): the accent activity edges. */
24
+ inFlight?: ReadonlyArray<InFlightRow>
25
+ /** Remote team directory rows (PR D v1 ruling): grouped under peer cards. */
26
+ remoteTeams?: ReadonlyArray<RemoteTeamRow>
27
+ /** gh#41 fleet planning: each tracked peer's published canvas org —
28
+ * rendered as REMOTE team frames (read + cross-host regulation). */
29
+ peerOrg?: ReadonlyArray<{ url: string; unreachable?: boolean; org: ReadonlyArray<{ name: string; team: string; members: readonly string[] }> }>
30
+ }
31
+
32
+ /** Screen-width class a viewport is remembered under (window width, not board width — collapsing the rail must not switch buckets). */
33
+ export type ViewportBucket = 'narrow' | 'medium' | 'wide'
34
+
35
+ export function viewportBucket(width: number): ViewportBucket {
36
+ return width < 680 ? 'narrow' : width < 1024 ? 'medium' : 'wide'
37
+ }
38
+
39
+ export interface PlanningDeps {
40
+ /** An edit that should persist happened (drag/nudge/pan/zoom/marquee end). */
41
+ onDirty(): void
42
+ /** The save lamp was clicked (error state -> retry). */
43
+ onLampClick(): void
44
+ /** Viewport size in CSS px (defaults to the root element's box; tests
45
+ * inject fixed sizes so fit math is assertable without layout). */
46
+ viewSize?(): { w: number; h: number }
47
+ /**
48
+ * Per-device viewport memory. The saved layout's viewport is the LAST
49
+ * saver's screen, so a desktop pan landed on a phone (and any other
50
+ * device's save yanked this one's view). When present, the first adopted
51
+ * layout takes this screen bucket's own viewport — else, on a wide screen,
52
+ * the host's (where it was most likely saved), else a fit — later
53
+ * adoptions leave the viewport alone, and every persisted edit writes the
54
+ * bucket back. Optional: without it the host viewport wins as before.
55
+ */
56
+ viewportStore?: { get(bucket: ViewportBucket): unknown; set(bucket: ViewportBucket, vp: LayoutViewport): void }
57
+ /**
58
+ * A team write action (optimistic state already applied by the view).
59
+ * Resolves true when the host accepted it — or it was an idempotent
60
+ * no-op (ok:false without an error string). Resolves false when the
61
+ * write was refused/failed: the view then rolls the action's scoped
62
+ * undo back itself. Rejecting the promise is also handled (treated as
63
+ * false) — the channel must not leave the team's guard pinned.
64
+ */
65
+ onCanvasAction(a: CanvasAction): Promise<boolean>
66
+ /** A plain (non-shift, no-drag) left click selected exactly one node
67
+ * (0.6.13): the host may open the reading face (chat drawer) for it.
68
+ * Selection itself is already applied — this is a notification, not a
69
+ * command; the host decides what (if anything) to show. */
70
+ onNodePick?(id: string): void
71
+ /** Wall clock for the inFlight age labels (tests pin it; default Date.now). */
72
+ now?(): number
73
+ }
74
+
75
+ /** Pointer-like event surface the handlers consume (real events satisfy it). */
76
+ export interface SeamPointer {
77
+ button: number
78
+ shiftKey: boolean
79
+ ctrlKey: boolean
80
+ clientX: number
81
+ clientY: number
82
+ pointerId?: number
83
+ target: Element | null
84
+ deltaX?: number
85
+ deltaY?: number
86
+ deltaMode?: number
87
+ preventDefault(): void
88
+ }
89
+
90
+ export interface SeamKey { key: string; shiftKey: boolean; altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; target: Element | null; preventDefault(): void }
91
+
92
+ export interface PlanningView {
93
+ root: HTMLElement
94
+ reconcile(input: PlanningInput): void
95
+ activate(): void
96
+ deactivate(): void
97
+ /** Adopt a (possibly normalized) layout document when the save loop allows. */
98
+ adoptExternalLayout(layout: unknown): void
99
+ /** The current document for the save loop, read at send time. */
100
+ snapshotDoc(): unknown
101
+ setLamp(state: LampState, savedAt?: string): void
102
+ /** A transient notice (host error verbatim or an info hint). */
103
+ notice(kind: 'error' | 'info', text: string): void
104
+ seam: {
105
+ pointerDown(ev: SeamPointer): void
106
+ pointerMove(ev: SeamPointer): void
107
+ pointerUp(ev: SeamPointer): void
108
+ pointerCancel(ev: SeamPointer): void
109
+ wheel(ev: SeamPointer): void
110
+ key(ev: SeamKey): void
111
+ keyUp(ev: SeamKey): void
112
+ contextMenu(ev: SeamPointer): void
113
+ }
114
+ destroy(): void
115
+ }