@nelsonlongxiang/dsh-open-a2a-net 0.6.41 → 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 +1 -1
- package/README_zh.md +1 -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/client.js +64 -64
- package/lib/client.js.map +1 -1
- package/lib/index.js +57 -9
- 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/types/origin-claim.d.ts +2 -0
- 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,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Readable names for routing team ids, shared by the activity and plan
|
|
3
|
-
* panes and the 台账 tab (review 2026-10-27: rows read bare `dsh/1111…` ids;
|
|
4
|
-
* the ledger kept doing so after #72 fixed the other two panes).
|
|
5
|
-
* Names are attacker-shaped data: textContent only.
|
|
6
|
-
* @module nexus-stage/wb-names
|
|
7
|
-
*/
|
|
8
|
-
import type { StateBody } from './workbench'
|
|
9
|
-
import { displayName } from './stage-name'
|
|
10
|
-
|
|
11
|
-
/** The name behind a team id — a session, a remote node or a canvas team —
|
|
12
|
-
* or undefined when none matches. An empty name counts as no match:
|
|
13
|
-
* displayName's `??` keeps '', and a peer can publish one. `remote` marks a
|
|
14
|
-
* name the PEER chose — it can collide with a local session's, so callers
|
|
15
|
-
* must show where it is from. */
|
|
16
|
-
export function teamName(state: StateBody, team: string): { name: string; remote: boolean } | undefined {
|
|
17
|
-
const named = (name: string | undefined, remote: boolean) => name !== undefined && name.trim() !== '' ? { name, remote } : undefined
|
|
18
|
-
const s = (state.sessions ?? []).find(r => r.team === team)
|
|
19
|
-
if (s !== undefined) return named(displayName(s), false)
|
|
20
|
-
const r = (state.remote ?? []).find(x => x.team === team)
|
|
21
|
-
if (r !== undefined) return named(displayName(r), true)
|
|
22
|
-
return named((state.canvas?.teams ?? []).find(t => t.team === team)?.name, false)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** A name for plain text: a peer-published one says it is remote. */
|
|
26
|
-
export function teamLabel(state: StateBody, team: string): string {
|
|
27
|
-
const n = teamName(state, team)
|
|
28
|
-
return n === undefined ? team : n.remote ? `${n.name}(远端)` : n.name
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Fill `target` with `prefix` + the name (the id rides the tooltip; an
|
|
32
|
-
* unknown team stays its id); a peer-published name gets a visible 远端 chip. */
|
|
33
|
-
export function nameInto(target: HTMLElement, state: StateBody, team: string, prefix = ''): void {
|
|
34
|
-
const n = teamName(state, team)
|
|
35
|
-
target.textContent = `${prefix}${n?.name ?? team}`
|
|
36
|
-
target.title = team
|
|
37
|
-
if (n?.remote === true) {
|
|
38
|
-
const chip = target.appendChild(document.createElement('span'))
|
|
39
|
-
chip.className = 'wb-row-remote'
|
|
40
|
-
chip.textContent = '远端'
|
|
41
|
-
chip.title = '名字由对端自行发布'
|
|
42
|
-
}
|
|
43
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Readable names for routing team ids, shared by the activity and plan
|
|
3
|
+
* panes and the 台账 tab (review 2026-10-27: rows read bare `dsh/1111…` ids;
|
|
4
|
+
* the ledger kept doing so after #72 fixed the other two panes).
|
|
5
|
+
* Names are attacker-shaped data: textContent only.
|
|
6
|
+
* @module nexus-stage/wb-names
|
|
7
|
+
*/
|
|
8
|
+
import type { StateBody } from './workbench'
|
|
9
|
+
import { displayName } from './stage-name'
|
|
10
|
+
|
|
11
|
+
/** The name behind a team id — a session, a remote node or a canvas team —
|
|
12
|
+
* or undefined when none matches. An empty name counts as no match:
|
|
13
|
+
* displayName's `??` keeps '', and a peer can publish one. `remote` marks a
|
|
14
|
+
* name the PEER chose — it can collide with a local session's, so callers
|
|
15
|
+
* must show where it is from. */
|
|
16
|
+
export function teamName(state: StateBody, team: string): { name: string; remote: boolean } | undefined {
|
|
17
|
+
const named = (name: string | undefined, remote: boolean) => name !== undefined && name.trim() !== '' ? { name, remote } : undefined
|
|
18
|
+
const s = (state.sessions ?? []).find(r => r.team === team)
|
|
19
|
+
if (s !== undefined) return named(displayName(s), false)
|
|
20
|
+
const r = (state.remote ?? []).find(x => x.team === team)
|
|
21
|
+
if (r !== undefined) return named(displayName(r), true)
|
|
22
|
+
return named((state.canvas?.teams ?? []).find(t => t.team === team)?.name, false)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A name for plain text: a peer-published one says it is remote. */
|
|
26
|
+
export function teamLabel(state: StateBody, team: string): string {
|
|
27
|
+
const n = teamName(state, team)
|
|
28
|
+
return n === undefined ? team : n.remote ? `${n.name}(远端)` : n.name
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Fill `target` with `prefix` + the name (the id rides the tooltip; an
|
|
32
|
+
* unknown team stays its id); a peer-published name gets a visible 远端 chip. */
|
|
33
|
+
export function nameInto(target: HTMLElement, state: StateBody, team: string, prefix = ''): void {
|
|
34
|
+
const n = teamName(state, team)
|
|
35
|
+
target.textContent = `${prefix}${n?.name ?? team}`
|
|
36
|
+
target.title = team
|
|
37
|
+
if (n?.remote === true) {
|
|
38
|
+
const chip = target.appendChild(document.createElement('span'))
|
|
39
|
+
chip.className = 'wb-row-remote'
|
|
40
|
+
chip.textContent = '远端'
|
|
41
|
+
chip.title = '名字由对端自行发布'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The workbench's left rail as tabs (2026-10-23 rewrite): one tab per pane
|
|
3
|
-
* — 目录 · 主机 · 台账 · 活动 · 规划 — each panel full-height with its own
|
|
4
|
-
* scroll, replacing the single accordion scroll where the host rail sat
|
|
5
|
-
* last and the ledger pushed everything below the fold.
|
|
6
|
-
*
|
|
7
|
-
* WAI-ARIA tabs: role=tablist / tab / tabpanel, roving tabindex, ←/→ and
|
|
8
|
-
* Home/End move between tabs and activate them. Badges carry each pane's
|
|
9
|
-
* count, colored only when there is something to act on (danger / warn).
|
|
10
|
-
* The selected tab is a per-viewer convenience kept in localStorage (every
|
|
11
|
-
* access guarded; the rail works the same without it).
|
|
12
|
-
* @module nexus-stage/wb-rail
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export type RailTone = 'danger' | 'warn' | 'accent'
|
|
16
|
-
|
|
17
|
-
export interface RailTabDef<K extends string> {
|
|
18
|
-
readonly key: K
|
|
19
|
-
readonly label: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface RailTabs<K extends string> {
|
|
23
|
-
readonly root: HTMLElement
|
|
24
|
-
/** The panel a pane mounts into for tab `key`. */
|
|
25
|
-
panel(key: K): HTMLElement
|
|
26
|
-
select(key: K, opts?: { focus?: boolean }): void
|
|
27
|
-
selected(): K
|
|
28
|
-
setBadge(key: K, text: string, tone?: RailTone): void
|
|
29
|
-
/** Hide a tab (data mode moves its pane to the stage); a hidden selected tab falls back to the first visible one. */
|
|
30
|
-
setHidden(key: K, hidden: boolean): void
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const STORE_KEY = 'dsh-a2a.rail.tab'
|
|
34
|
-
|
|
35
|
-
function readStored(): string | null {
|
|
36
|
-
try {
|
|
37
|
-
return window.localStorage.getItem(STORE_KEY)
|
|
38
|
-
} catch {
|
|
39
|
-
return null
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function writeStored(key: string): void {
|
|
44
|
-
try {
|
|
45
|
-
window.localStorage.setItem(STORE_KEY, key)
|
|
46
|
-
} catch { /* per-viewer convenience only */ }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function createRailTabs<K extends string>(defs: ReadonlyArray<RailTabDef<K>>, opts: { onChange?(key: K): void } = {}): RailTabs<K> {
|
|
50
|
-
const root = document.createElement('aside')
|
|
51
|
-
root.className = 'wb-rail'
|
|
52
|
-
const list = document.createElement('div')
|
|
53
|
-
list.className = 'wb-tabs'
|
|
54
|
-
list.setAttribute('role', 'tablist')
|
|
55
|
-
list.setAttribute('aria-label', '工作台侧栏')
|
|
56
|
-
const panels = document.createElement('div')
|
|
57
|
-
panels.className = 'wb-tabpanels'
|
|
58
|
-
root.append(list, panels)
|
|
59
|
-
|
|
60
|
-
const tabs = new Map<K, HTMLButtonElement>()
|
|
61
|
-
const panelEls = new Map<K, HTMLElement>()
|
|
62
|
-
const badges = new Map<K, HTMLElement>()
|
|
63
|
-
const hidden = new Set<K>()
|
|
64
|
-
for (const def of defs) {
|
|
65
|
-
const tab = document.createElement('button')
|
|
66
|
-
tab.type = 'button'
|
|
67
|
-
tab.className = 'wb-tab'
|
|
68
|
-
tab.id = `wb-tab-${def.key}`
|
|
69
|
-
tab.dataset.sec = def.key
|
|
70
|
-
tab.setAttribute('role', 'tab')
|
|
71
|
-
tab.setAttribute('aria-controls', `wb-panel-${def.key}`)
|
|
72
|
-
const label = document.createElement('span')
|
|
73
|
-
label.className = 'wb-tab-label'
|
|
74
|
-
label.textContent = def.label
|
|
75
|
-
const badge = document.createElement('span')
|
|
76
|
-
badge.className = 'wb-tab-badge'
|
|
77
|
-
tab.append(label, badge)
|
|
78
|
-
tab.addEventListener('click', () => select(def.key))
|
|
79
|
-
list.appendChild(tab)
|
|
80
|
-
const panel = document.createElement('section')
|
|
81
|
-
panel.className = 'wb-tabpanel'
|
|
82
|
-
panel.id = `wb-panel-${def.key}`
|
|
83
|
-
panel.dataset.sec = def.key
|
|
84
|
-
panel.setAttribute('role', 'tabpanel')
|
|
85
|
-
panel.setAttribute('aria-labelledby', tab.id)
|
|
86
|
-
panel.tabIndex = -1
|
|
87
|
-
panels.appendChild(panel)
|
|
88
|
-
tabs.set(def.key, tab)
|
|
89
|
-
panelEls.set(def.key, panel)
|
|
90
|
-
badges.set(def.key, badge)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const visible = (): K[] => defs.map(d => d.key).filter(k => !hidden.has(k))
|
|
94
|
-
const stored = readStored()
|
|
95
|
-
let current: K = defs.find(d => d.key === stored)?.key ?? defs[0]!.key
|
|
96
|
-
|
|
97
|
-
list.addEventListener('keydown', ev => {
|
|
98
|
-
const keys = visible()
|
|
99
|
-
const at = keys.indexOf(current)
|
|
100
|
-
let next: K | undefined
|
|
101
|
-
if (ev.key === 'ArrowRight') next = keys[(at + 1) % keys.length]
|
|
102
|
-
else if (ev.key === 'ArrowLeft') next = keys[(at - 1 + keys.length) % keys.length]
|
|
103
|
-
else if (ev.key === 'Home') next = keys[0]
|
|
104
|
-
else if (ev.key === 'End') next = keys[keys.length - 1]
|
|
105
|
-
if (next === undefined) return
|
|
106
|
-
ev.preventDefault()
|
|
107
|
-
select(next, { focus: true })
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
function paint(): void {
|
|
111
|
-
for (const def of defs) {
|
|
112
|
-
const on = def.key === current
|
|
113
|
-
const tab = tabs.get(def.key)!
|
|
114
|
-
tab.setAttribute('aria-selected', String(on))
|
|
115
|
-
tab.tabIndex = on ? 0 : -1
|
|
116
|
-
tab.classList.toggle('is-active', on)
|
|
117
|
-
tab.hidden = hidden.has(def.key)
|
|
118
|
-
panelEls.get(def.key)!.hidden = !on
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function select(key: K, o: { focus?: boolean } = {}): void {
|
|
123
|
-
if (hidden.has(key)) return
|
|
124
|
-
const changed = key !== current
|
|
125
|
-
current = key
|
|
126
|
-
writeStored(key)
|
|
127
|
-
paint()
|
|
128
|
-
if (o.focus === true) tabs.get(key)!.focus()
|
|
129
|
-
if (changed) opts.onChange?.(key)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function setBadge(key: K, text: string, tone?: RailTone): void {
|
|
133
|
-
const b = badges.get(key)!
|
|
134
|
-
b.textContent = text
|
|
135
|
-
b.className = `wb-tab-badge${tone !== undefined ? ` is-${tone}` : ''}`
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function setHidden(key: K, h: boolean): void {
|
|
139
|
-
if (h) hidden.add(key)
|
|
140
|
-
else hidden.delete(key)
|
|
141
|
-
if (hidden.has(current)) current = visible()[0] ?? defs[0]!.key
|
|
142
|
-
paint()
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
paint()
|
|
146
|
-
return { root, panel: (k) => panelEls.get(k)!, select, selected: () => current, setBadge, setHidden }
|
|
147
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The workbench's left rail as tabs (2026-10-23 rewrite): one tab per pane
|
|
3
|
+
* — 目录 · 主机 · 台账 · 活动 · 规划 — each panel full-height with its own
|
|
4
|
+
* scroll, replacing the single accordion scroll where the host rail sat
|
|
5
|
+
* last and the ledger pushed everything below the fold.
|
|
6
|
+
*
|
|
7
|
+
* WAI-ARIA tabs: role=tablist / tab / tabpanel, roving tabindex, ←/→ and
|
|
8
|
+
* Home/End move between tabs and activate them. Badges carry each pane's
|
|
9
|
+
* count, colored only when there is something to act on (danger / warn).
|
|
10
|
+
* The selected tab is a per-viewer convenience kept in localStorage (every
|
|
11
|
+
* access guarded; the rail works the same without it).
|
|
12
|
+
* @module nexus-stage/wb-rail
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type RailTone = 'danger' | 'warn' | 'accent'
|
|
16
|
+
|
|
17
|
+
export interface RailTabDef<K extends string> {
|
|
18
|
+
readonly key: K
|
|
19
|
+
readonly label: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RailTabs<K extends string> {
|
|
23
|
+
readonly root: HTMLElement
|
|
24
|
+
/** The panel a pane mounts into for tab `key`. */
|
|
25
|
+
panel(key: K): HTMLElement
|
|
26
|
+
select(key: K, opts?: { focus?: boolean }): void
|
|
27
|
+
selected(): K
|
|
28
|
+
setBadge(key: K, text: string, tone?: RailTone): void
|
|
29
|
+
/** Hide a tab (data mode moves its pane to the stage); a hidden selected tab falls back to the first visible one. */
|
|
30
|
+
setHidden(key: K, hidden: boolean): void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const STORE_KEY = 'dsh-a2a.rail.tab'
|
|
34
|
+
|
|
35
|
+
function readStored(): string | null {
|
|
36
|
+
try {
|
|
37
|
+
return window.localStorage.getItem(STORE_KEY)
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeStored(key: string): void {
|
|
44
|
+
try {
|
|
45
|
+
window.localStorage.setItem(STORE_KEY, key)
|
|
46
|
+
} catch { /* per-viewer convenience only */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createRailTabs<K extends string>(defs: ReadonlyArray<RailTabDef<K>>, opts: { onChange?(key: K): void } = {}): RailTabs<K> {
|
|
50
|
+
const root = document.createElement('aside')
|
|
51
|
+
root.className = 'wb-rail'
|
|
52
|
+
const list = document.createElement('div')
|
|
53
|
+
list.className = 'wb-tabs'
|
|
54
|
+
list.setAttribute('role', 'tablist')
|
|
55
|
+
list.setAttribute('aria-label', '工作台侧栏')
|
|
56
|
+
const panels = document.createElement('div')
|
|
57
|
+
panels.className = 'wb-tabpanels'
|
|
58
|
+
root.append(list, panels)
|
|
59
|
+
|
|
60
|
+
const tabs = new Map<K, HTMLButtonElement>()
|
|
61
|
+
const panelEls = new Map<K, HTMLElement>()
|
|
62
|
+
const badges = new Map<K, HTMLElement>()
|
|
63
|
+
const hidden = new Set<K>()
|
|
64
|
+
for (const def of defs) {
|
|
65
|
+
const tab = document.createElement('button')
|
|
66
|
+
tab.type = 'button'
|
|
67
|
+
tab.className = 'wb-tab'
|
|
68
|
+
tab.id = `wb-tab-${def.key}`
|
|
69
|
+
tab.dataset.sec = def.key
|
|
70
|
+
tab.setAttribute('role', 'tab')
|
|
71
|
+
tab.setAttribute('aria-controls', `wb-panel-${def.key}`)
|
|
72
|
+
const label = document.createElement('span')
|
|
73
|
+
label.className = 'wb-tab-label'
|
|
74
|
+
label.textContent = def.label
|
|
75
|
+
const badge = document.createElement('span')
|
|
76
|
+
badge.className = 'wb-tab-badge'
|
|
77
|
+
tab.append(label, badge)
|
|
78
|
+
tab.addEventListener('click', () => select(def.key))
|
|
79
|
+
list.appendChild(tab)
|
|
80
|
+
const panel = document.createElement('section')
|
|
81
|
+
panel.className = 'wb-tabpanel'
|
|
82
|
+
panel.id = `wb-panel-${def.key}`
|
|
83
|
+
panel.dataset.sec = def.key
|
|
84
|
+
panel.setAttribute('role', 'tabpanel')
|
|
85
|
+
panel.setAttribute('aria-labelledby', tab.id)
|
|
86
|
+
panel.tabIndex = -1
|
|
87
|
+
panels.appendChild(panel)
|
|
88
|
+
tabs.set(def.key, tab)
|
|
89
|
+
panelEls.set(def.key, panel)
|
|
90
|
+
badges.set(def.key, badge)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const visible = (): K[] => defs.map(d => d.key).filter(k => !hidden.has(k))
|
|
94
|
+
const stored = readStored()
|
|
95
|
+
let current: K = defs.find(d => d.key === stored)?.key ?? defs[0]!.key
|
|
96
|
+
|
|
97
|
+
list.addEventListener('keydown', ev => {
|
|
98
|
+
const keys = visible()
|
|
99
|
+
const at = keys.indexOf(current)
|
|
100
|
+
let next: K | undefined
|
|
101
|
+
if (ev.key === 'ArrowRight') next = keys[(at + 1) % keys.length]
|
|
102
|
+
else if (ev.key === 'ArrowLeft') next = keys[(at - 1 + keys.length) % keys.length]
|
|
103
|
+
else if (ev.key === 'Home') next = keys[0]
|
|
104
|
+
else if (ev.key === 'End') next = keys[keys.length - 1]
|
|
105
|
+
if (next === undefined) return
|
|
106
|
+
ev.preventDefault()
|
|
107
|
+
select(next, { focus: true })
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
function paint(): void {
|
|
111
|
+
for (const def of defs) {
|
|
112
|
+
const on = def.key === current
|
|
113
|
+
const tab = tabs.get(def.key)!
|
|
114
|
+
tab.setAttribute('aria-selected', String(on))
|
|
115
|
+
tab.tabIndex = on ? 0 : -1
|
|
116
|
+
tab.classList.toggle('is-active', on)
|
|
117
|
+
tab.hidden = hidden.has(def.key)
|
|
118
|
+
panelEls.get(def.key)!.hidden = !on
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function select(key: K, o: { focus?: boolean } = {}): void {
|
|
123
|
+
if (hidden.has(key)) return
|
|
124
|
+
const changed = key !== current
|
|
125
|
+
current = key
|
|
126
|
+
writeStored(key)
|
|
127
|
+
paint()
|
|
128
|
+
if (o.focus === true) tabs.get(key)!.focus()
|
|
129
|
+
if (changed) opts.onChange?.(key)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setBadge(key: K, text: string, tone?: RailTone): void {
|
|
133
|
+
const b = badges.get(key)!
|
|
134
|
+
b.textContent = text
|
|
135
|
+
b.className = `wb-tab-badge${tone !== undefined ? ` is-${tone}` : ''}`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function setHidden(key: K, h: boolean): void {
|
|
139
|
+
if (h) hidden.add(key)
|
|
140
|
+
else hidden.delete(key)
|
|
141
|
+
if (hidden.has(current)) current = visible()[0] ?? defs[0]!.key
|
|
142
|
+
paint()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
paint()
|
|
146
|
+
return { root, panel: (k) => panelEls.get(k)!, select, selected: () => current, setBadge, setHidden }
|
|
147
|
+
}
|