@nelsonlongxiang/dsh-open-a2a-net 0.6.40 → 0.6.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/README_zh.md +2 -1
- package/assets/stageDist/assets/index-Bj7S2pA1.css +1 -1
- package/assets/stageDist/assets/index-C-28k_o5.js +974 -974
- package/assets/stageDist/index.html +12 -12
- package/lib/a2a-client.js +3 -2
- package/lib/client.js +64 -64
- package/lib/client.js.map +1 -1
- package/lib/config.js +1 -0
- package/lib/faces/files.js +2 -2
- package/lib/faces/session-log.js +3 -3
- package/lib/faces/wire.js +11 -16
- package/lib/index.js +102 -18
- package/lib/link-keys.js +29 -0
- package/lib/origin-claim.js +4 -0
- package/lib/skill/SKILL.md +117 -117
- package/lib/skill/references/collaboration.md +428 -428
- package/lib/skill/references/discovery.md +78 -78
- package/lib/skill/references/intake.md +107 -107
- package/lib/skill/references/membership.md +73 -73
- package/lib/skill/references/pitfalls.md +51 -51
- package/lib/skill/references/router.md +96 -96
- package/lib/skill/references/routing.md +65 -65
- package/lib/skill/references/setup.md +5 -1
- package/lib/tools/files.js +3 -1
- package/lib/types/a2a-client.d.ts +3 -1
- package/lib/types/config.d.ts +6 -0
- package/lib/types/faces/files.d.ts +2 -1
- package/lib/types/faces/session-log.d.ts +2 -0
- package/lib/types/faces/wire.d.ts +2 -1
- package/lib/types/link-keys.d.ts +13 -0
- package/lib/types/origin-claim.d.ts +2 -0
- package/lib/types/tools/files.d.ts +1 -1
- package/nexus-stage/package.json +21 -21
- package/nexus-stage/scripts/mockhost.py +261 -261
- package/nexus-stage/src/canvas-ops.ts +191 -191
- package/nexus-stage/src/canvas-wire.ts +164 -164
- package/nexus-stage/src/cli-home-picker.ts +42 -42
- package/nexus-stage/src/fault.ts +56 -56
- package/nexus-stage/src/federation-overlay.ts +103 -103
- package/nexus-stage/src/federation.ts +241 -241
- package/nexus-stage/src/layout-doc.ts +145 -145
- package/nexus-stage/src/main.ts +513 -513
- package/nexus-stage/src/planning-card-text.ts +73 -73
- package/nexus-stage/src/planning-card.ts +63 -63
- package/nexus-stage/src/planning-edges.ts +26 -26
- package/nexus-stage/src/planning-fit.ts +45 -45
- package/nexus-stage/src/planning-frame-hue.ts +13 -13
- package/nexus-stage/src/planning-legend.ts +39 -39
- package/nexus-stage/src/planning-member-handles.ts +61 -61
- package/nexus-stage/src/planning-menus.ts +422 -422
- package/nexus-stage/src/planning-mirror.ts +75 -75
- package/nexus-stage/src/planning-nodelist.ts +294 -294
- package/nexus-stage/src/planning-overlap.ts +159 -159
- package/nexus-stage/src/planning-reachability.ts +28 -28
- package/nexus-stage/src/planning-relayout.ts +121 -121
- package/nexus-stage/src/planning-roster.ts +15 -15
- package/nexus-stage/src/planning-search.ts +168 -168
- package/nexus-stage/src/planning-types.ts +115 -115
- package/nexus-stage/src/planning-undo.ts +68 -68
- package/nexus-stage/src/planning-view.ts +1499 -1499
- package/nexus-stage/src/planning.css +340 -340
- package/nexus-stage/src/seat-name-editor.ts +31 -31
- package/nexus-stage/src/session-drag.ts +64 -64
- package/nexus-stage/src/viewport.ts +106 -106
- package/nexus-stage/src/wb-chat.ts +684 -684
- package/nexus-stage/src/wb-directory.ts +237 -237
- package/nexus-stage/src/wb-ledger.ts +279 -279
- package/nexus-stage/src/wb-names.ts +43 -43
- package/nexus-stage/src/wb-rail.ts +147 -147
- package/nexus-stage/src/workbench.css +388 -388
- package/nexus-stage/src/workbench.ts +1272 -1272
- package/nexus-stage/src/world.ts +509 -509
- package/nexus-stage/tsconfig.json +24 -24
- package/nexus-stage/vite.config.ts +15 -15
- package/package.json +1 -1
|
@@ -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
|
+
}
|