@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 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 };