@adia-ai/web-modules 0.8.25 → 0.8.27
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/CHANGELOG.md +23 -0
- package/agent-admin/admin-roster/admin-roster.a2ui.json +172 -0
- package/agent-admin/admin-roster/admin-roster.css +93 -0
- package/agent-admin/admin-roster/admin-roster.d.ts +44 -0
- package/agent-admin/admin-roster/admin-roster.js +217 -0
- package/agent-admin/admin-roster/admin-roster.yaml +168 -0
- package/agent-admin/admin-settings/admin-settings.a2ui.json +156 -0
- package/agent-admin/admin-settings/admin-settings.css +114 -0
- package/agent-admin/admin-settings/admin-settings.d.ts +30 -0
- package/agent-admin/admin-settings/admin-settings.js +219 -0
- package/agent-admin/admin-settings/admin-settings.yaml +153 -0
- package/agent-admin/agent-admin/agent-admin.a2ui.json +135 -0
- package/agent-admin/agent-admin/agent-admin.css +49 -0
- package/agent-admin/agent-admin/agent-admin.d.ts +45 -0
- package/agent-admin/agent-admin/agent-admin.js +257 -0
- package/agent-admin/agent-admin/agent-admin.yaml +162 -0
- package/agent-admin/demo-seeds.js +95 -0
- package/agent-admin/index.js +13 -0
- package/agent-admin/live-persona.js +53 -0
- package/chat/chat-surfaces/chat-surfaces.d.ts +56 -0
- package/chat/chat-surfaces/chat-surfaces.js +242 -0
- package/chat/index.js +1 -0
- package/dist/everything.min.js +179 -108
- package/index.js +1 -0
- package/package.json +26 -5
- package/theme/theme-panel/theme-panel.js +31 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-modules
|
|
2
2
|
|
|
3
|
+
## [0.8.27] — 2026-08-05
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **`admin-roster-ui` migrated off native `<button>` rows** onto the extended `menu-item-ui` — the stamped-native-buttons sanction is removed; the audit is clean with zero exceptions in this package (gh#616, PR #623).
|
|
7
|
+
|
|
8
|
+
### Maintenance
|
|
9
|
+
- agent-admin test family deflaked properly: `settled()`/`vi.waitFor` condition-polling replaces fixed timer ticks across the suite (gh#614, PR #622).
|
|
10
|
+
- **`dist/` bundles rebuilt** in this cut's window (1 file(s)) — regenerated from the source changes described above, not independent edits.
|
|
11
|
+
- **`onboarding/` touched in this release window** (1 file(s), e.g. `onboarding-checklist/onboarding-checklist.test.js`) — carried by the entries above.
|
|
12
|
+
- **`settings/` touched in this release window** (1 file(s), e.g. `integrations-page/integrations-page.test.js`) — carried by the entries above.
|
|
13
|
+
|
|
14
|
+
## [0.8.26] — 2026-08-05
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`chat-surfaces` (WCH-4, gh#599)** — the L3 surface-host registry: `createSurfaceRegistry` (per-surfaceId hosts with persistent bubble identity across turns; protocol-level close with a visible non-removable annotation) + `wireAgentEvents` (folds an `AgentEvent` stream into chat-shell's external-drive API with a code-owned progress-label table). Transport-blind; the renderer is dependency-injected.
|
|
18
|
+
- **`agent-admin` composite family (WCH-5, gh#600)** — `admin-roster-ui` (exactly-one-active persona roster, import/export), `admin-settings-ui` (settings over one bound store, KNOWN_KINDS display grouping, zero per-kind logic), `agent-admin-ui` (roster + settings + chat; the store-swap law: a genuine store-reference change resets conversation/surfaces/session, a same-store reconnect resets nothing; live-apply by fresh read). Peer-depends on `@adia-ai/persona` + `@adia-ai/agent` (exact-pinned).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **theme-panel sliders trust applied state** (gh#578, PR #609) — theme clicks no longer round-trip `getComputedStyle` (which showed the theme's baked shape values); the one legitimate initial read is isolated in `#readInitialShape` with its rationale.
|
|
22
|
+
|
|
23
|
+
### Maintenance
|
|
24
|
+
- **`dist/` bundles rebuilt** in this cut's window (1 file(s)) — regenerated from the source changes described above, not independent edits.
|
|
25
|
+
|
|
3
26
|
## [0.8.25] — 2026-07-31
|
|
4
27
|
|
|
5
28
|
### Fixed
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/AdminRoster.json",
|
|
4
|
+
"title": "AdminRoster",
|
|
5
|
+
"description": "L4 persona roster (CHAT-HARNESS.md §Components row L4, gh#600 / WCH-5) —\none row per persona with exactly-one-active menu semantics\n(`role=\"menu\"` of `role=\"menuitemradio\"` rows, `aria-checked` mirroring\n`[active-id]`). Clicking a row fires `persona-change` with the clicked\npersona's id; clicking the already-active row is a no-op. Import/export\nbuttons ride `@adia-ai/persona`'s persona-file functions verbatim —\nexport downloads the active persona as JSON, import reads a chosen file\nand re-emits the minted persona as `persona-import` for the host to\nappend and persist.\n\nLives in `packages/web-modules/agent-admin/` alongside\n`<admin-settings-ui>` and `<agent-admin-ui>` — the L4 admin composite\nfamily over `@adia-ai/persona`'s L2 store contract.\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"active-id": {
|
|
17
|
+
"description": "The currently-active persona's id. Reflects so a host can deep-link;\ndrives `aria-checked` + the visible check glyph on the matching row.\n",
|
|
18
|
+
"$ref": "common_types.json#/$defs/DynamicString"
|
|
19
|
+
},
|
|
20
|
+
"component": {
|
|
21
|
+
"const": "AdminRoster"
|
|
22
|
+
},
|
|
23
|
+
"personas": {
|
|
24
|
+
"description": "Array of `{ id, label, category? }` persona summaries. Empty array\nrenders \"No personas yet.\"\n",
|
|
25
|
+
"$ref": "common_types.json#/$defs/DynamicStringList"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"required": [
|
|
29
|
+
"component",
|
|
30
|
+
"personas"
|
|
31
|
+
],
|
|
32
|
+
"unevaluatedProperties": false,
|
|
33
|
+
"x-adiaui": {
|
|
34
|
+
"anti_patterns": [
|
|
35
|
+
{
|
|
36
|
+
"fix": "{\"component\": \"AdminRoster\", \"personas\": [{\"id\": \"a\", \"label\": \"A\"}]}\n",
|
|
37
|
+
"why": "Rows are generated from the `personas` prop — slotted children are ignored.",
|
|
38
|
+
"wrong": "{\"component\": \"AdminRoster\", \"personas\": [{\"id\": \"a\", \"label\": \"A\"}], \"children\": [\n {\"component\": \"Button\", \"text\": \"Switch\"}\n]}\n"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"category": "container",
|
|
42
|
+
"composes": [
|
|
43
|
+
"button-ui",
|
|
44
|
+
"icon-ui",
|
|
45
|
+
"menu-item-ui"
|
|
46
|
+
],
|
|
47
|
+
"events": {
|
|
48
|
+
"persona-change": {
|
|
49
|
+
"description": "A non-active row was clicked.",
|
|
50
|
+
"detail": {
|
|
51
|
+
"personaId": {
|
|
52
|
+
"description": "The id of the persona the user switched to.",
|
|
53
|
+
"type": "string"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"persona-import": {
|
|
58
|
+
"description": "A persona file was chosen and parsed successfully.",
|
|
59
|
+
"detail": {
|
|
60
|
+
"persona": {
|
|
61
|
+
"description": "The minted persona (fresh, collision-safe id) from `importPersona`.",
|
|
62
|
+
"type": "object"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"persona-import-error": {
|
|
67
|
+
"description": "A chosen file failed to parse or validate as a persona file.",
|
|
68
|
+
"detail": {
|
|
69
|
+
"error": {
|
|
70
|
+
"description": "The error message from `importPersona`.",
|
|
71
|
+
"type": "string"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"examples": [
|
|
77
|
+
{
|
|
78
|
+
"description": "Roster with one active persona.",
|
|
79
|
+
"a2ui": "[{\"id\": \"roster\", \"component\": \"AdminRoster\",\n \"active-id\": \"support\",\n \"personas\": [\n {\"id\": \"support\", \"label\": \"Support agent\", \"category\": \"Customer\"},\n {\"id\": \"writer\", \"label\": \"Writer\", \"category\": \"Content\"}\n ]}]\n",
|
|
80
|
+
"name": "two-personas"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"description": "No personas yet.",
|
|
84
|
+
"a2ui": "[{\"id\": \"roster\", \"component\": \"AdminRoster\", \"personas\": []}]\n",
|
|
85
|
+
"name": "empty"
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"keywords": [
|
|
89
|
+
"admin",
|
|
90
|
+
"persona",
|
|
91
|
+
"roster",
|
|
92
|
+
"agent",
|
|
93
|
+
"switch",
|
|
94
|
+
"menu"
|
|
95
|
+
],
|
|
96
|
+
"name": "UIAdminRoster",
|
|
97
|
+
"related": [
|
|
98
|
+
"AdminSettings",
|
|
99
|
+
"AgentAdmin",
|
|
100
|
+
"Menu"
|
|
101
|
+
],
|
|
102
|
+
"slots": {},
|
|
103
|
+
"states": [
|
|
104
|
+
{
|
|
105
|
+
"description": "Default; renders the persona list.",
|
|
106
|
+
"name": "idle"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"description": "`personas` is an empty array.",
|
|
110
|
+
"name": "empty"
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"status": "stable",
|
|
114
|
+
"synonyms": {
|
|
115
|
+
"persona": [
|
|
116
|
+
"agent",
|
|
117
|
+
"config",
|
|
118
|
+
"profile"
|
|
119
|
+
],
|
|
120
|
+
"roster": [
|
|
121
|
+
"list",
|
|
122
|
+
"personas",
|
|
123
|
+
"agents"
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
"tag": "admin-roster-ui",
|
|
127
|
+
"tokens": {
|
|
128
|
+
"--admin-roster-active-bg": {
|
|
129
|
+
"description": "Active/hover row background.",
|
|
130
|
+
"default": "var(--a-bg-muted)"
|
|
131
|
+
},
|
|
132
|
+
"--admin-roster-active-fg": {
|
|
133
|
+
"description": "Active row foreground.",
|
|
134
|
+
"default": "var(--a-fg-strong)"
|
|
135
|
+
},
|
|
136
|
+
"--admin-roster-bg": {
|
|
137
|
+
"description": "Panel background.",
|
|
138
|
+
"default": "var(--a-bg)"
|
|
139
|
+
},
|
|
140
|
+
"--admin-roster-fg": {
|
|
141
|
+
"description": "Primary foreground.",
|
|
142
|
+
"default": "var(--a-fg)"
|
|
143
|
+
},
|
|
144
|
+
"--admin-roster-fg-muted": {
|
|
145
|
+
"description": "Secondary foreground (category text, empty state).",
|
|
146
|
+
"default": "var(--a-fg-muted)"
|
|
147
|
+
},
|
|
148
|
+
"--admin-roster-gap": {
|
|
149
|
+
"description": "Vertical gap between header / menu / toolbar.",
|
|
150
|
+
"default": "var(--a-space-3)"
|
|
151
|
+
},
|
|
152
|
+
"--admin-roster-px": {
|
|
153
|
+
"description": "Horizontal padding.",
|
|
154
|
+
"default": "var(--a-space-4)"
|
|
155
|
+
},
|
|
156
|
+
"--admin-roster-py": {
|
|
157
|
+
"description": "Vertical padding.",
|
|
158
|
+
"default": "var(--a-space-4)"
|
|
159
|
+
},
|
|
160
|
+
"--admin-roster-row-gap": {
|
|
161
|
+
"description": "Gap between rows.",
|
|
162
|
+
"default": "var(--a-space-1)"
|
|
163
|
+
},
|
|
164
|
+
"--admin-roster-row-radius": {
|
|
165
|
+
"description": "Row border radius.",
|
|
166
|
+
"default": "var(--a-radius-md)"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"traits": [],
|
|
170
|
+
"version": 1
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
admin-roster-ui — Module CSS
|
|
3
|
+
|
|
4
|
+
Two-block @scope:
|
|
5
|
+
Block 1 — TOKENS (:where(:scope))
|
|
6
|
+
Block 2 — BASE (:scope + descendant rules)
|
|
7
|
+
|
|
8
|
+
Tokens consumed (substrate):
|
|
9
|
+
--a-bg, --a-fg, --a-fg-muted, --a-fg-strong, --a-border-subtle,
|
|
10
|
+
--a-bg-muted, --a-radius-md, --a-space-1..6, --a-ui-sm, --a-text-lg,
|
|
11
|
+
--a-weight-medium, --a-accent, --a-accent-contrast
|
|
12
|
+
═══════════════════════════════════════════════════════════════ */
|
|
13
|
+
|
|
14
|
+
@scope (admin-roster-ui) {
|
|
15
|
+
|
|
16
|
+
:where(:scope) {
|
|
17
|
+
--admin-roster-bg: var(--a-bg);
|
|
18
|
+
--admin-roster-fg: var(--a-fg);
|
|
19
|
+
--admin-roster-fg-muted: var(--a-fg-muted);
|
|
20
|
+
--admin-roster-px: var(--a-space-4);
|
|
21
|
+
--admin-roster-py: var(--a-space-4);
|
|
22
|
+
--admin-roster-gap: var(--a-space-3);
|
|
23
|
+
--admin-roster-row-gap: var(--a-space-1);
|
|
24
|
+
--admin-roster-row-radius: var(--a-radius-md);
|
|
25
|
+
--admin-roster-active-bg: var(--a-bg-muted);
|
|
26
|
+
--admin-roster-active-fg: var(--a-fg-strong);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
:scope {
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
display: grid;
|
|
32
|
+
gap: var(--admin-roster-gap);
|
|
33
|
+
padding: var(--admin-roster-py) var(--admin-roster-px);
|
|
34
|
+
background: var(--admin-roster-bg);
|
|
35
|
+
color: var(--admin-roster-fg);
|
|
36
|
+
min-width: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:scope > [data-part="header"] > [data-part="title"] {
|
|
40
|
+
margin: 0;
|
|
41
|
+
font-size: var(--a-ui-sm);
|
|
42
|
+
font-weight: var(--a-weight-medium);
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 0.04em;
|
|
45
|
+
color: var(--admin-roster-fg-muted);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:scope [data-part="menu"] {
|
|
49
|
+
display: grid;
|
|
50
|
+
gap: var(--admin-roster-row-gap);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Rows are <menu-item-ui> (gh#616). Its own @scope wins on proximity, so
|
|
54
|
+
row overrides route through the menu-item token surface; only
|
|
55
|
+
properties menu-item never declares (width, font-weight) are set
|
|
56
|
+
directly. */
|
|
57
|
+
:scope [data-part="row"] {
|
|
58
|
+
--menu-item-px: var(--a-space-3);
|
|
59
|
+
--menu-item-py: var(--a-space-2);
|
|
60
|
+
--menu-item-radius: var(--admin-roster-row-radius);
|
|
61
|
+
--menu-item-fg: var(--admin-roster-fg);
|
|
62
|
+
--menu-item-bg-hover: var(--admin-roster-active-bg);
|
|
63
|
+
width: 100%;
|
|
64
|
+
box-sizing: border-box;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:scope [data-part="row"][data-active="true"] {
|
|
68
|
+
--menu-item-bg: var(--admin-roster-active-bg);
|
|
69
|
+
--menu-item-fg: var(--admin-roster-active-fg);
|
|
70
|
+
--menu-item-fg-hover: var(--admin-roster-active-fg);
|
|
71
|
+
font-weight: var(--a-weight-medium);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:scope [data-part="row-check"] {
|
|
75
|
+
flex: 0 0 auto;
|
|
76
|
+
visibility: hidden;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
:scope [data-part="row"][data-active="true"] [data-part="row-check"] {
|
|
80
|
+
visibility: visible;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
:scope [data-part="empty"] {
|
|
84
|
+
padding: var(--a-space-4);
|
|
85
|
+
color: var(--admin-roster-fg-muted);
|
|
86
|
+
text-align: center;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:scope [data-part="toolbar"] {
|
|
90
|
+
display: flex;
|
|
91
|
+
gap: var(--a-space-2);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<admin-roster-ui>` — L4 persona roster (CHAT-HARNESS.md §Components row
|
|
3
|
+
* L4, gh#600 / WCH-5).
|
|
4
|
+
*
|
|
5
|
+
* Hand-authored alongside `<name>.yaml`; regenerate when the props /
|
|
6
|
+
* events shape changes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { UIElement } from '../../../web-components/core/element.js';
|
|
10
|
+
|
|
11
|
+
export interface AdminRosterPersonaSummary {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
category?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AdminRosterPersonaChangeDetail {
|
|
18
|
+
personaId: string;
|
|
19
|
+
}
|
|
20
|
+
export type AdminRosterPersonaChangeEvent = CustomEvent<AdminRosterPersonaChangeDetail>;
|
|
21
|
+
|
|
22
|
+
export interface AdminRosterPersonaImportDetail {
|
|
23
|
+
persona: import('@adia-ai/persona').Persona;
|
|
24
|
+
}
|
|
25
|
+
export type AdminRosterPersonaImportEvent = CustomEvent<AdminRosterPersonaImportDetail>;
|
|
26
|
+
|
|
27
|
+
export interface AdminRosterPersonaImportErrorDetail {
|
|
28
|
+
error: string;
|
|
29
|
+
}
|
|
30
|
+
export type AdminRosterPersonaImportErrorEvent = CustomEvent<AdminRosterPersonaImportErrorDetail>;
|
|
31
|
+
|
|
32
|
+
export class UIAdminRoster extends UIElement {
|
|
33
|
+
personas: AdminRosterPersonaSummary[];
|
|
34
|
+
'active-id': string;
|
|
35
|
+
|
|
36
|
+
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
37
|
+
type: K,
|
|
38
|
+
listener: (this: UIAdminRoster, ev: HTMLElementEventMap[K]) => unknown,
|
|
39
|
+
options?: boolean | AddEventListenerOptions,
|
|
40
|
+
): void;
|
|
41
|
+
addEventListener(type: 'persona-change', listener: (ev: AdminRosterPersonaChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
42
|
+
addEventListener(type: 'persona-import', listener: (ev: AdminRosterPersonaImportEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
43
|
+
addEventListener(type: 'persona-import-error', listener: (ev: AdminRosterPersonaImportErrorEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
44
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <admin-roster-ui> — L4 persona roster (CHAT-HARNESS.md §Components row L4,
|
|
3
|
+
* gh#600 / WCH-5).
|
|
4
|
+
*
|
|
5
|
+
* <admin-roster-ui id="roster"></admin-roster-ui>
|
|
6
|
+
* <script>
|
|
7
|
+
* const roster = document.querySelector('#roster');
|
|
8
|
+
* roster.personas = [
|
|
9
|
+
* { id: 'support', label: 'Support agent', category: 'Customer' },
|
|
10
|
+
* { id: 'writer', label: 'Writer', category: 'Content' },
|
|
11
|
+
* ];
|
|
12
|
+
* roster['active-id'] = 'support';
|
|
13
|
+
* roster.addEventListener('persona-change', (e) => { ... e.detail.personaId });
|
|
14
|
+
* </script>
|
|
15
|
+
*
|
|
16
|
+
* One row per persona, exactly-one-active menu semantics: WAI-ARIA
|
|
17
|
+
* `role="menu"` of `role="menuitemradio"` rows, `aria-checked` mirrors
|
|
18
|
+
* `[active-id]`. Clicking the already-active row is a no-op (radio
|
|
19
|
+
* semantics — a radio group never fires a change back to its own value).
|
|
20
|
+
*
|
|
21
|
+
* Import/export rides `@adia-ai/persona`'s persona-file functions
|
|
22
|
+
* verbatim — this composite never re-implements the file format. Export
|
|
23
|
+
* downloads a `.json` via a throwaway object URL; import reads a chosen
|
|
24
|
+
* file through `input[type=file]`, calls `importPersona` (which always
|
|
25
|
+
* MINTS a new persona — never overwrites an existing one, even on an id
|
|
26
|
+
* collision) and re-emits it upward as `persona-import` so a host can
|
|
27
|
+
* append it to its own persona list and persist it.
|
|
28
|
+
*
|
|
29
|
+
* Per ADR-0027 — composites that programmatically create primitives in
|
|
30
|
+
* render() do NOT auto-import those primitives; a consumer imports
|
|
31
|
+
* <button-ui> + <icon-ui> + <menu-item-ui> (menu.js) alongside this module.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { UIElement, html } from '../../../web-components/core/element.js';
|
|
35
|
+
import { exportPersona, importPersona } from '@adia-ai/persona';
|
|
36
|
+
|
|
37
|
+
class UIAdminRoster extends UIElement {
|
|
38
|
+
static properties = {
|
|
39
|
+
personas: { type: Array, default: [] },
|
|
40
|
+
'active-id': { type: String, default: '', reflect: true },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
static template = (host) => host._renderTemplate();
|
|
44
|
+
|
|
45
|
+
#headerId = `admin-roster-h-${Math.random().toString(36).slice(2, 8)}`;
|
|
46
|
+
#fileInput = null;
|
|
47
|
+
|
|
48
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
49
|
+
if (name === 'personas') {
|
|
50
|
+
if (newVal == null) {
|
|
51
|
+
this.personas = [];
|
|
52
|
+
} else {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(newVal);
|
|
55
|
+
this.personas = Array.isArray(parsed) ? parsed : [];
|
|
56
|
+
} catch {
|
|
57
|
+
this.personas = [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
super.attributeChangedCallback(name, oldVal, newVal);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
connected() {
|
|
66
|
+
this.setAttribute('role', 'region');
|
|
67
|
+
this.setAttribute('aria-labelledby', this.#headerId);
|
|
68
|
+
if (this.hasAttribute('personas') && !Array.isArray(this.personas)) {
|
|
69
|
+
try { this.personas = JSON.parse(this.getAttribute('personas')); }
|
|
70
|
+
catch { this.personas = []; }
|
|
71
|
+
}
|
|
72
|
+
this.addEventListener('click', this.#onClick);
|
|
73
|
+
this.addEventListener('keydown', this.#onKeydown);
|
|
74
|
+
this.addEventListener('change', this.#onImportFileChange);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
disconnected() {
|
|
78
|
+
this.removeEventListener('click', this.#onClick);
|
|
79
|
+
this.removeEventListener('keydown', this.#onKeydown);
|
|
80
|
+
this.removeEventListener('change', this.#onImportFileChange);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_renderTemplate() {
|
|
84
|
+
const list = Array.isArray(this.personas) ? this.personas : [];
|
|
85
|
+
const activeId = this['active-id'] || '';
|
|
86
|
+
return html`
|
|
87
|
+
<header data-part="header">
|
|
88
|
+
<h2 data-part="title" id=${this.#headerId}>Personas</h2>
|
|
89
|
+
</header>
|
|
90
|
+
<div data-part="menu" role="menu" aria-label="Personas">
|
|
91
|
+
${list.length === 0 ? this.#renderEmpty() : list.map((p) => this.#renderRow(p, activeId))}
|
|
92
|
+
</div>
|
|
93
|
+
<div data-part="toolbar">
|
|
94
|
+
<button-ui data-action="export" text="Export" icon="download-simple" variant="ghost" size="sm"
|
|
95
|
+
.disabled=${!activeId}></button-ui>
|
|
96
|
+
<button-ui data-action="import" text="Import" icon="upload-simple" variant="ghost" size="sm"></button-ui>
|
|
97
|
+
</div>
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#renderRow(persona, activeId) {
|
|
102
|
+
const isActive = persona.id === activeId;
|
|
103
|
+
// menu-item-ui with a consumer-owned role (gh#616): the primitive leaves
|
|
104
|
+
// role/tabindex/aria-checked to us — radio semantics stay this module's.
|
|
105
|
+
return html`
|
|
106
|
+
<menu-item-ui
|
|
107
|
+
data-part="row"
|
|
108
|
+
data-persona-id=${persona.id}
|
|
109
|
+
role="menuitemradio"
|
|
110
|
+
tabindex="0"
|
|
111
|
+
aria-checked=${String(isActive)}
|
|
112
|
+
data-active=${isActive}
|
|
113
|
+
text=${persona.label ?? persona.id}
|
|
114
|
+
subtitle=${persona.category ?? ''}
|
|
115
|
+
>
|
|
116
|
+
<icon-ui slot="trailing" data-part="row-check" name="check" aria-hidden="true"></icon-ui>
|
|
117
|
+
</menu-item-ui>
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#renderEmpty() {
|
|
122
|
+
return html`<div data-part="empty" role="status">No personas yet.</div>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── event delegation ────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
// The rows were native <button>s before gh#616; menu-item-ui rows need
|
|
128
|
+
// explicit Enter/Space activation to keep keyboard parity.
|
|
129
|
+
#onKeydown = (e) => {
|
|
130
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
131
|
+
const row = e.target.closest?.('[data-part="row"]');
|
|
132
|
+
if (row && this.contains(row)) {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
row.click();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
#onClick = (e) => {
|
|
139
|
+
const row = e.target.closest?.('[data-part="row"]');
|
|
140
|
+
if (row && this.contains(row)) {
|
|
141
|
+
const personaId = row.getAttribute('data-persona-id');
|
|
142
|
+
if (personaId && personaId !== this['active-id']) {
|
|
143
|
+
this['active-id'] = personaId;
|
|
144
|
+
this.dispatchEvent(new CustomEvent('persona-change', {
|
|
145
|
+
bubbles: true,
|
|
146
|
+
detail: { personaId },
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const exportBtn = e.target.closest?.('[data-action="export"]');
|
|
153
|
+
if (exportBtn && this.contains(exportBtn)) {
|
|
154
|
+
this.#exportActive();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const importBtn = e.target.closest?.('[data-action="import"]');
|
|
159
|
+
if (importBtn && this.contains(importBtn)) {
|
|
160
|
+
this.#triggerImport();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
#exportActive() {
|
|
165
|
+
const activeId = this['active-id'];
|
|
166
|
+
const persona = (Array.isArray(this.personas) ? this.personas : []).find((p) => p.id === activeId);
|
|
167
|
+
if (!persona) return;
|
|
168
|
+
const json = exportPersona(persona);
|
|
169
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
170
|
+
const url = URL.createObjectURL(blob);
|
|
171
|
+
const a = this.ownerDocument.createElement('a');
|
|
172
|
+
a.href = url;
|
|
173
|
+
a.download = `${persona.id}.persona.json`;
|
|
174
|
+
a.click();
|
|
175
|
+
URL.revokeObjectURL(url);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#triggerImport() {
|
|
179
|
+
if (!this.#fileInput) {
|
|
180
|
+
this.#fileInput = this.ownerDocument.createElement('input');
|
|
181
|
+
this.#fileInput.type = 'file';
|
|
182
|
+
this.#fileInput.accept = 'application/json';
|
|
183
|
+
this.#fileInput.setAttribute('data-part', 'import-file');
|
|
184
|
+
this.#fileInput.style.display = 'none';
|
|
185
|
+
this.appendChild(this.#fileInput);
|
|
186
|
+
}
|
|
187
|
+
this.#fileInput.value = '';
|
|
188
|
+
this.#fileInput.click();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#onImportFileChange = async (e) => {
|
|
192
|
+
const input = e.target;
|
|
193
|
+
if (!input || input !== this.#fileInput) return;
|
|
194
|
+
const file = input.files?.[0];
|
|
195
|
+
if (!file) return;
|
|
196
|
+
try {
|
|
197
|
+
const text = await file.text();
|
|
198
|
+
const existingIds = (Array.isArray(this.personas) ? this.personas : []).map((p) => p.id);
|
|
199
|
+
const persona = importPersona(text, existingIds);
|
|
200
|
+
this.dispatchEvent(new CustomEvent('persona-import', {
|
|
201
|
+
bubbles: true,
|
|
202
|
+
detail: { persona },
|
|
203
|
+
}));
|
|
204
|
+
} catch (err) {
|
|
205
|
+
this.dispatchEvent(new CustomEvent('persona-import-error', {
|
|
206
|
+
bubbles: true,
|
|
207
|
+
detail: { error: err instanceof Error ? err.message : String(err) },
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!customElements.get('admin-roster-ui')) {
|
|
214
|
+
customElements.define('admin-roster-ui', UIAdminRoster);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { UIAdminRoster };
|