@adia-ai/web-modules 0.8.25 → 0.8.26
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 +12 -0
- package/agent-admin/admin-roster/admin-roster.a2ui.json +171 -0
- package/agent-admin/admin-roster/admin-roster.css +112 -0
- package/agent-admin/admin-roster/admin-roster.d.ts +44 -0
- package/agent-admin/admin-roster/admin-roster.js +202 -0
- package/agent-admin/admin-roster/admin-roster.yaml +167 -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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: UIAdminRoster
|
|
4
|
+
tag: admin-roster-ui
|
|
5
|
+
status: stable
|
|
6
|
+
component: AdminRoster
|
|
7
|
+
category: container
|
|
8
|
+
version: 1
|
|
9
|
+
description: |
|
|
10
|
+
L4 persona roster (CHAT-HARNESS.md §Components row L4, gh#600 / WCH-5) —
|
|
11
|
+
one row per persona with exactly-one-active menu semantics
|
|
12
|
+
(`role="menu"` of `role="menuitemradio"` rows, `aria-checked` mirroring
|
|
13
|
+
`[active-id]`). Clicking a row fires `persona-change` with the clicked
|
|
14
|
+
persona's id; clicking the already-active row is a no-op. Import/export
|
|
15
|
+
buttons ride `@adia-ai/persona`'s persona-file functions verbatim —
|
|
16
|
+
export downloads the active persona as JSON, import reads a chosen file
|
|
17
|
+
and re-emits the minted persona as `persona-import` for the host to
|
|
18
|
+
append and persist.
|
|
19
|
+
|
|
20
|
+
Lives in `packages/web-modules/agent-admin/` alongside
|
|
21
|
+
`<admin-settings-ui>` and `<agent-admin-ui>` — the L4 admin composite
|
|
22
|
+
family over `@adia-ai/persona`'s L2 store contract.
|
|
23
|
+
|
|
24
|
+
composes:
|
|
25
|
+
- button-ui
|
|
26
|
+
- icon-ui
|
|
27
|
+
|
|
28
|
+
props:
|
|
29
|
+
personas:
|
|
30
|
+
description: |
|
|
31
|
+
Array of `{ id, label, category? }` persona summaries. Empty array
|
|
32
|
+
renders "No personas yet."
|
|
33
|
+
type: array
|
|
34
|
+
default: []
|
|
35
|
+
required: true
|
|
36
|
+
dynamic: true
|
|
37
|
+
active-id:
|
|
38
|
+
description: |
|
|
39
|
+
The currently-active persona's id. Reflects so a host can deep-link;
|
|
40
|
+
drives `aria-checked` + the visible check glyph on the matching row.
|
|
41
|
+
type: string
|
|
42
|
+
default: ""
|
|
43
|
+
reflect: true
|
|
44
|
+
dynamic: true
|
|
45
|
+
|
|
46
|
+
events:
|
|
47
|
+
persona-change:
|
|
48
|
+
description: A non-active row was clicked.
|
|
49
|
+
detail:
|
|
50
|
+
personaId:
|
|
51
|
+
type: string
|
|
52
|
+
description: The id of the persona the user switched to.
|
|
53
|
+
persona-import:
|
|
54
|
+
description: A persona file was chosen and parsed successfully.
|
|
55
|
+
detail:
|
|
56
|
+
persona:
|
|
57
|
+
type: object
|
|
58
|
+
description: The minted persona (fresh, collision-safe id) from `importPersona`.
|
|
59
|
+
persona-import-error:
|
|
60
|
+
description: A chosen file failed to parse or validate as a persona file.
|
|
61
|
+
detail:
|
|
62
|
+
error:
|
|
63
|
+
type: string
|
|
64
|
+
description: The error message from `importPersona`.
|
|
65
|
+
|
|
66
|
+
slots: {}
|
|
67
|
+
|
|
68
|
+
states:
|
|
69
|
+
- name: idle
|
|
70
|
+
description: Default; renders the persona list.
|
|
71
|
+
- name: empty
|
|
72
|
+
description: "`personas` is an empty array."
|
|
73
|
+
|
|
74
|
+
traits: []
|
|
75
|
+
|
|
76
|
+
tokens:
|
|
77
|
+
--admin-roster-bg:
|
|
78
|
+
description: Panel background.
|
|
79
|
+
default: var(--a-bg)
|
|
80
|
+
--admin-roster-fg:
|
|
81
|
+
description: Primary foreground.
|
|
82
|
+
default: var(--a-fg)
|
|
83
|
+
--admin-roster-fg-muted:
|
|
84
|
+
description: Secondary foreground (category text, empty state).
|
|
85
|
+
default: var(--a-fg-muted)
|
|
86
|
+
--admin-roster-px:
|
|
87
|
+
description: Horizontal padding.
|
|
88
|
+
default: var(--a-space-4)
|
|
89
|
+
--admin-roster-py:
|
|
90
|
+
description: Vertical padding.
|
|
91
|
+
default: var(--a-space-4)
|
|
92
|
+
--admin-roster-gap:
|
|
93
|
+
description: Vertical gap between header / menu / toolbar.
|
|
94
|
+
default: var(--a-space-3)
|
|
95
|
+
--admin-roster-row-gap:
|
|
96
|
+
description: Gap between rows.
|
|
97
|
+
default: var(--a-space-1)
|
|
98
|
+
--admin-roster-row-radius:
|
|
99
|
+
description: Row border radius.
|
|
100
|
+
default: var(--a-radius-md)
|
|
101
|
+
--admin-roster-active-bg:
|
|
102
|
+
description: Active/hover row background.
|
|
103
|
+
default: var(--a-bg-muted)
|
|
104
|
+
--admin-roster-active-fg:
|
|
105
|
+
description: Active row foreground.
|
|
106
|
+
default: var(--a-fg-strong)
|
|
107
|
+
|
|
108
|
+
a2ui:
|
|
109
|
+
rules:
|
|
110
|
+
- rule: |
|
|
111
|
+
Use <admin-roster-ui> for the canonical persona-switch list in an
|
|
112
|
+
agent-admin surface. Supply the `personas` array; the composite
|
|
113
|
+
owns menuitemradio semantics and the active-row check glyph.
|
|
114
|
+
reason: One composite replaces a hand-rolled persona list.
|
|
115
|
+
- rule: |
|
|
116
|
+
Listen for `persona-change` to swap the bound `SettingsStore`
|
|
117
|
+
(CHAT-HARNESS §Interfaces 5) — a genuine store reference change
|
|
118
|
+
resets the conversation + open surfaces; this composite never
|
|
119
|
+
touches the store itself.
|
|
120
|
+
reason: Store-swap is the host composition's job, not the roster's.
|
|
121
|
+
|
|
122
|
+
anti_patterns:
|
|
123
|
+
- wrong: |
|
|
124
|
+
{"component": "AdminRoster", "personas": [{"id": "a", "label": "A"}], "children": [
|
|
125
|
+
{"component": "Button", "text": "Switch"}
|
|
126
|
+
]}
|
|
127
|
+
why: Rows are generated from the `personas` prop — slotted children are ignored.
|
|
128
|
+
fix: |
|
|
129
|
+
{"component": "AdminRoster", "personas": [{"id": "a", "label": "A"}]}
|
|
130
|
+
|
|
131
|
+
examples:
|
|
132
|
+
- name: two-personas
|
|
133
|
+
description: Roster with one active persona.
|
|
134
|
+
a2ui: |
|
|
135
|
+
[{"id": "roster", "component": "AdminRoster",
|
|
136
|
+
"active-id": "support",
|
|
137
|
+
"personas": [
|
|
138
|
+
{"id": "support", "label": "Support agent", "category": "Customer"},
|
|
139
|
+
{"id": "writer", "label": "Writer", "category": "Content"}
|
|
140
|
+
]}]
|
|
141
|
+
- name: empty
|
|
142
|
+
description: No personas yet.
|
|
143
|
+
a2ui: |
|
|
144
|
+
[{"id": "roster", "component": "AdminRoster", "personas": []}]
|
|
145
|
+
|
|
146
|
+
keywords:
|
|
147
|
+
- admin
|
|
148
|
+
- persona
|
|
149
|
+
- roster
|
|
150
|
+
- agent
|
|
151
|
+
- switch
|
|
152
|
+
- menu
|
|
153
|
+
|
|
154
|
+
synonyms:
|
|
155
|
+
roster:
|
|
156
|
+
- list
|
|
157
|
+
- personas
|
|
158
|
+
- agents
|
|
159
|
+
persona:
|
|
160
|
+
- agent
|
|
161
|
+
- config
|
|
162
|
+
- profile
|
|
163
|
+
|
|
164
|
+
related:
|
|
165
|
+
- AdminSettings
|
|
166
|
+
- AgentAdmin
|
|
167
|
+
- Menu
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/AdminSettings.json",
|
|
4
|
+
"title": "AdminSettings",
|
|
5
|
+
"description": "L4 settings pane over ONE bound per-persona store (CHAT-HARNESS.md\n§Components row L4 + §Interfaces 5, gh#600 / WCH-5). Sections group by\n`@adia-ai/persona`'s `KNOWN_KINDS` — DISPLAY grouping only; this\ncomposite never switches on `entry.kind` for logic (law 7: Entry is one\ngeneric primitive). Each Entry renders through the same row: label +\ndescription, an enabled toggle, and a collapsible content textarea\n(authoring). Both write through `store.set(...)` immediately — the\nstore is the state, persisted per persona.\n\n`persona` and `store` are plain JS properties (no attribute path);\n`store` is an opaque `SettingsStore` object from\n`@adia-ai/persona`'s `personaStore`.\n\nLives in `packages/web-modules/agent-admin/` alongside\n`<admin-roster-ui>` and `<agent-admin-ui>`.\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
|
+
"component": {
|
|
17
|
+
"const": "AdminSettings"
|
|
18
|
+
},
|
|
19
|
+
"persona": {
|
|
20
|
+
"description": "The `Persona` (from `@adia-ai/persona`) whose entries this pane\nrenders, grouped by `KNOWN_KINDS`. JS property only — no attribute.\n",
|
|
21
|
+
"type": "object"
|
|
22
|
+
},
|
|
23
|
+
"store": {
|
|
24
|
+
"description": "The `SettingsStore` bound to `persona` (typically\n`personaStore(persona)`). Toggle + content edits write through it\nimmediately; external `store.set()` calls (e.g. a live-apply probe)\nre-render this pane via `store.subscribe`. JS property only.\n",
|
|
25
|
+
"type": "object"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"required": [
|
|
29
|
+
"component",
|
|
30
|
+
"persona",
|
|
31
|
+
"store"
|
|
32
|
+
],
|
|
33
|
+
"unevaluatedProperties": false,
|
|
34
|
+
"x-adiaui": {
|
|
35
|
+
"anti_patterns": [
|
|
36
|
+
{
|
|
37
|
+
"fix": "const el = document.querySelector('admin-settings-ui');\nel.persona = myPersona;\nel.store = personaStore(myPersona);\n",
|
|
38
|
+
"why": "Renders \"No persona selected.\" — `persona` and `store` must be set as JS properties before use.",
|
|
39
|
+
"wrong": "{\"component\": \"AdminSettings\"}\n"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"category": "container",
|
|
43
|
+
"composes": [
|
|
44
|
+
"switch-ui",
|
|
45
|
+
"textarea-ui",
|
|
46
|
+
"text-ui",
|
|
47
|
+
"icon-ui"
|
|
48
|
+
],
|
|
49
|
+
"events": {
|
|
50
|
+
"entry-change": {
|
|
51
|
+
"description": "An entry's `enabled` or `content` field was written through the store.",
|
|
52
|
+
"detail": {
|
|
53
|
+
"entryId": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
},
|
|
56
|
+
"field": {
|
|
57
|
+
"description": "'enabled' or 'content'.",
|
|
58
|
+
"type": "string"
|
|
59
|
+
},
|
|
60
|
+
"value": {
|
|
61
|
+
"description": "The new value (boolean for enabled, string for content).",
|
|
62
|
+
"type": "string"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"examples": [
|
|
68
|
+
{
|
|
69
|
+
"description": "Settings pane bound to a persona + its store (JS-driven; not expressible as static a2ui JSON since `store` is an opaque object).",
|
|
70
|
+
"a2ui": "[{\"id\": \"settings\", \"component\": \"AdminSettings\"}]\n",
|
|
71
|
+
"name": "bound-to-a-persona"
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"keywords": [
|
|
75
|
+
"admin",
|
|
76
|
+
"settings",
|
|
77
|
+
"persona",
|
|
78
|
+
"entry",
|
|
79
|
+
"store",
|
|
80
|
+
"agent"
|
|
81
|
+
],
|
|
82
|
+
"name": "UIAdminSettings",
|
|
83
|
+
"related": [
|
|
84
|
+
"AdminRoster",
|
|
85
|
+
"AgentAdmin"
|
|
86
|
+
],
|
|
87
|
+
"slots": {},
|
|
88
|
+
"states": [
|
|
89
|
+
{
|
|
90
|
+
"description": "Default; renders `persona`'s entries grouped by kind.",
|
|
91
|
+
"name": "idle"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"description": "No `persona` set.",
|
|
95
|
+
"name": "empty"
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
"status": "stable",
|
|
99
|
+
"synonyms": {
|
|
100
|
+
"persona": [
|
|
101
|
+
"agent",
|
|
102
|
+
"profile"
|
|
103
|
+
],
|
|
104
|
+
"settings": [
|
|
105
|
+
"config",
|
|
106
|
+
"preferences",
|
|
107
|
+
"entries"
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"tag": "admin-settings-ui",
|
|
111
|
+
"tokens": {
|
|
112
|
+
"--admin-settings-bg": {
|
|
113
|
+
"description": "Panel background.",
|
|
114
|
+
"default": "var(--a-bg)"
|
|
115
|
+
},
|
|
116
|
+
"--admin-settings-border": {
|
|
117
|
+
"description": "Entry card border.",
|
|
118
|
+
"default": "var(--a-border-subtle)"
|
|
119
|
+
},
|
|
120
|
+
"--admin-settings-entry-gap": {
|
|
121
|
+
"description": "Gap between sibling entries in a section.",
|
|
122
|
+
"default": "var(--a-space-1)"
|
|
123
|
+
},
|
|
124
|
+
"--admin-settings-fg": {
|
|
125
|
+
"description": "Primary foreground.",
|
|
126
|
+
"default": "var(--a-fg)"
|
|
127
|
+
},
|
|
128
|
+
"--admin-settings-fg-muted": {
|
|
129
|
+
"description": "Secondary foreground (description, empty state).",
|
|
130
|
+
"default": "var(--a-fg-muted)"
|
|
131
|
+
},
|
|
132
|
+
"--admin-settings-gap": {
|
|
133
|
+
"description": "Vertical gap between header and sections.",
|
|
134
|
+
"default": "var(--a-space-5)"
|
|
135
|
+
},
|
|
136
|
+
"--admin-settings-px": {
|
|
137
|
+
"description": "Horizontal padding.",
|
|
138
|
+
"default": "var(--a-space-4)"
|
|
139
|
+
},
|
|
140
|
+
"--admin-settings-py": {
|
|
141
|
+
"description": "Vertical padding.",
|
|
142
|
+
"default": "var(--a-space-4)"
|
|
143
|
+
},
|
|
144
|
+
"--admin-settings-radius": {
|
|
145
|
+
"description": "Entry card border radius.",
|
|
146
|
+
"default": "var(--a-radius-md)"
|
|
147
|
+
},
|
|
148
|
+
"--admin-settings-section-gap": {
|
|
149
|
+
"description": "Gap between a section's heading and its entries.",
|
|
150
|
+
"default": "var(--a-space-2)"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"traits": [],
|
|
154
|
+
"version": 1
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
admin-settings-ui — Module CSS
|
|
3
|
+
|
|
4
|
+
Two-block @scope:
|
|
5
|
+
Block 1 — TOKENS (:where(:scope))
|
|
6
|
+
Block 2 — BASE (:scope + descendant rules)
|
|
7
|
+
|
|
8
|
+
Composition note: layout + spacing only; child primitives (switch-ui,
|
|
9
|
+
textarea-ui, text-ui, icon-ui) carry their own chrome.
|
|
10
|
+
═══════════════════════════════════════════════════════════════ */
|
|
11
|
+
|
|
12
|
+
@scope (admin-settings-ui) {
|
|
13
|
+
|
|
14
|
+
:where(:scope) {
|
|
15
|
+
--admin-settings-bg: var(--a-bg);
|
|
16
|
+
--admin-settings-fg: var(--a-fg);
|
|
17
|
+
--admin-settings-fg-muted: var(--a-fg-muted);
|
|
18
|
+
--admin-settings-px: var(--a-space-4);
|
|
19
|
+
--admin-settings-py: var(--a-space-4);
|
|
20
|
+
--admin-settings-gap: var(--a-space-5);
|
|
21
|
+
--admin-settings-section-gap: var(--a-space-2);
|
|
22
|
+
--admin-settings-entry-gap: var(--a-space-1);
|
|
23
|
+
--admin-settings-border: var(--a-border-subtle);
|
|
24
|
+
--admin-settings-radius: var(--a-radius-md);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
:scope {
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
display: grid;
|
|
30
|
+
gap: var(--admin-settings-gap);
|
|
31
|
+
padding: var(--admin-settings-py) var(--admin-settings-px);
|
|
32
|
+
background: var(--admin-settings-bg);
|
|
33
|
+
color: var(--admin-settings-fg);
|
|
34
|
+
min-width: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:scope > [data-part="header"] > [data-part="title"] {
|
|
38
|
+
margin: 0;
|
|
39
|
+
font-size: var(--a-text-lg);
|
|
40
|
+
font-weight: var(--a-weight-medium);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:scope [data-part="sections"] {
|
|
44
|
+
display: grid;
|
|
45
|
+
gap: var(--a-space-6);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:scope [data-part="section"] {
|
|
49
|
+
display: grid;
|
|
50
|
+
gap: var(--admin-settings-section-gap);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:scope [data-part="entries"] {
|
|
54
|
+
display: grid;
|
|
55
|
+
gap: var(--admin-settings-entry-gap);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:scope [data-part="entry"] {
|
|
59
|
+
border: 1px solid var(--admin-settings-border);
|
|
60
|
+
border-radius: var(--admin-settings-radius);
|
|
61
|
+
padding: var(--a-space-2) var(--a-space-3);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:scope [data-part="entry-row"] {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: var(--a-space-2);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:scope [data-part="entry-meta"] {
|
|
71
|
+
flex: 1 1 auto;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
display: grid;
|
|
74
|
+
gap: 2px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
:scope [data-part="entry-label"] {
|
|
78
|
+
font-weight: var(--a-weight-medium);
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
text-overflow: ellipsis;
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
:scope [data-part="entry-description"] {
|
|
85
|
+
font-size: var(--a-ui-sm);
|
|
86
|
+
color: var(--admin-settings-fg-muted);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:scope [data-action="collapse-toggle"] {
|
|
90
|
+
flex: 0 0 auto;
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: center;
|
|
94
|
+
border: none;
|
|
95
|
+
background: transparent;
|
|
96
|
+
color: var(--admin-settings-fg-muted);
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
padding: var(--a-space-1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
:scope [data-part="entry-enabled"] {
|
|
102
|
+
flex: 0 0 auto;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
:scope [data-part="entry-content"] {
|
|
106
|
+
margin-top: var(--a-space-2);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
:scope [data-part="empty"] {
|
|
110
|
+
padding: var(--a-space-6);
|
|
111
|
+
text-align: center;
|
|
112
|
+
color: var(--admin-settings-fg-muted);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<admin-settings-ui>` — L4 settings pane over ONE bound per-persona
|
|
3
|
+
* store (CHAT-HARNESS.md §Components row L4 + §Interfaces 5, gh#600 /
|
|
4
|
+
* WCH-5).
|
|
5
|
+
*
|
|
6
|
+
* Hand-authored alongside `<name>.yaml`; regenerate when the props /
|
|
7
|
+
* events shape changes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { UIElement } from '../../../web-components/core/element.js';
|
|
11
|
+
import type { Persona, SettingsStore } from '@adia-ai/persona';
|
|
12
|
+
|
|
13
|
+
export interface AdminSettingsEntryChangeDetail {
|
|
14
|
+
entryId: string;
|
|
15
|
+
field: 'enabled' | 'content';
|
|
16
|
+
value: boolean | string;
|
|
17
|
+
}
|
|
18
|
+
export type AdminSettingsEntryChangeEvent = CustomEvent<AdminSettingsEntryChangeDetail>;
|
|
19
|
+
|
|
20
|
+
export class UIAdminSettings extends UIElement {
|
|
21
|
+
persona: Persona | null;
|
|
22
|
+
store: SettingsStore | null;
|
|
23
|
+
|
|
24
|
+
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
25
|
+
type: K,
|
|
26
|
+
listener: (this: UIAdminSettings, ev: HTMLElementEventMap[K]) => unknown,
|
|
27
|
+
options?: boolean | AddEventListenerOptions,
|
|
28
|
+
): void;
|
|
29
|
+
addEventListener(type: 'entry-change', listener: (ev: AdminSettingsEntryChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <admin-settings-ui> — L4 settings pane over ONE bound per-persona store
|
|
3
|
+
* (CHAT-HARNESS.md §Components row L4 + §Interfaces 5, gh#600 / WCH-5).
|
|
4
|
+
*
|
|
5
|
+
* <admin-settings-ui id="settings"></admin-settings-ui>
|
|
6
|
+
* <script>
|
|
7
|
+
* const el = document.querySelector('#settings');
|
|
8
|
+
* el.persona = myPersona; // @adia-ai/persona Persona
|
|
9
|
+
* el.store = personaStore(myPersona); // @adia-ai/persona SettingsStore
|
|
10
|
+
* </script>
|
|
11
|
+
*
|
|
12
|
+
* Sections are grouped by `KNOWN_KINDS` (display grouping ONLY — this file
|
|
13
|
+
* never switches on `entry.kind` for logic, per CHAT-HARNESS law 7: Entry
|
|
14
|
+
* is ONE generic primitive, six kinds are seed data). Each Entry renders
|
|
15
|
+
* through the SAME markup: a label + description, an enabled toggle, and a
|
|
16
|
+
* collapsible content textarea (authoring, M-scope 2). Both the toggle and
|
|
17
|
+
* the textarea write through `store.set(...)` IMMEDIATELY on change — this
|
|
18
|
+
* composite holds no local edit buffer; the store IS the state, persisted
|
|
19
|
+
* per persona (`@adia-ai/persona`'s `personaStore`).
|
|
20
|
+
*
|
|
21
|
+
* `persona` / `store` are plain JS properties (no attribute path for
|
|
22
|
+
* either; `store` is an opaque `SettingsStore` object, never
|
|
23
|
+
* attribute-serializable) that force a re-render through `_v`, an
|
|
24
|
+
* internal reflected counter declared in `static properties` — the same
|
|
25
|
+
* "reassign a declared reactive prop to force a repaint" trick
|
|
26
|
+
* `<integrations-page-ui>` uses for its own non-attribute state.
|
|
27
|
+
*
|
|
28
|
+
* Per ADR-0027 — composites that programmatically create primitives in
|
|
29
|
+
* render() do NOT auto-import those primitives; a consumer imports
|
|
30
|
+
* <switch-ui>, <textarea-ui>, <text-ui>, <icon-ui>, <button-ui> alongside this module.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { UIElement, html } from '../../../web-components/core/element.js';
|
|
34
|
+
import { KNOWN_KINDS } from '@adia-ai/persona';
|
|
35
|
+
import { entryStoreKey } from '../live-persona.js';
|
|
36
|
+
|
|
37
|
+
const GROUP_LABEL = {
|
|
38
|
+
'prompt-section': 'Prompt sections',
|
|
39
|
+
skill: 'Skills',
|
|
40
|
+
workflow: 'Workflows',
|
|
41
|
+
resource: 'Resources',
|
|
42
|
+
tool: 'Tools',
|
|
43
|
+
'pattern-source': 'Pattern sources',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
class UIAdminSettings extends UIElement {
|
|
47
|
+
// `_v` is an internal repaint counter — not a public prop, never meant
|
|
48
|
+
// to be read or set by a consumer. Bumping it (an ordinary declared-
|
|
49
|
+
// property write) is how persona/store/collapse-state changes reach the
|
|
50
|
+
// framework's own render effect, without inventing a second reactivity
|
|
51
|
+
// mechanism alongside it (that duplication is what caused a signals
|
|
52
|
+
// drain-loop warning in an earlier draft of this file).
|
|
53
|
+
static properties = {
|
|
54
|
+
_v: { type: Number, default: 0 },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
static template = (host) => host._renderTemplate();
|
|
58
|
+
|
|
59
|
+
#headerId = `admin-settings-h-${Math.random().toString(36).slice(2, 8)}`;
|
|
60
|
+
#persona = null;
|
|
61
|
+
#store = null;
|
|
62
|
+
#expanded = new Set();
|
|
63
|
+
#unsubscribe = null;
|
|
64
|
+
|
|
65
|
+
connected() {
|
|
66
|
+
this.setAttribute('role', 'region');
|
|
67
|
+
this.setAttribute('aria-labelledby', this.#headerId);
|
|
68
|
+
this.addEventListener('change', this.#onChange);
|
|
69
|
+
this.addEventListener('input', this.#onContentInput);
|
|
70
|
+
this.addEventListener('press', this.#onToggleCollapse);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
disconnected() {
|
|
74
|
+
this.removeEventListener('change', this.#onChange);
|
|
75
|
+
this.removeEventListener('input', this.#onContentInput);
|
|
76
|
+
this.removeEventListener('press', this.#onToggleCollapse);
|
|
77
|
+
if (this.#unsubscribe) { this.#unsubscribe(); this.#unsubscribe = null; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get persona() { return this.#persona; }
|
|
81
|
+
set persona(value) {
|
|
82
|
+
this.#persona = value;
|
|
83
|
+
this._v++;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get store() { return this.#store; }
|
|
87
|
+
set store(value) {
|
|
88
|
+
if (this.#unsubscribe) { this.#unsubscribe(); this.#unsubscribe = null; }
|
|
89
|
+
this.#store = value;
|
|
90
|
+
if (value?.subscribe) {
|
|
91
|
+
this.#unsubscribe = value.subscribe(() => { this._v++; });
|
|
92
|
+
}
|
|
93
|
+
this._v++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_renderTemplate() {
|
|
97
|
+
// Touch `_v` unconditionally so this render subscribes to it even on
|
|
98
|
+
// the empty-state branch below.
|
|
99
|
+
void this._v;
|
|
100
|
+
const persona = this.#persona;
|
|
101
|
+
const store = this.#store;
|
|
102
|
+
|
|
103
|
+
if (!persona) {
|
|
104
|
+
return html`<div data-part="empty" role="status">No persona selected.</div>`;
|
|
105
|
+
}
|
|
106
|
+
const groups = this.#groupedEntries(persona);
|
|
107
|
+
return html`
|
|
108
|
+
<header data-part="header">
|
|
109
|
+
<h2 data-part="title" id=${this.#headerId}>${persona.label ?? persona.id} — Settings</h2>
|
|
110
|
+
</header>
|
|
111
|
+
<div data-part="sections">
|
|
112
|
+
${groups.map((g) => this.#renderSection(g, store))}
|
|
113
|
+
</div>
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#groupedEntries(persona) {
|
|
118
|
+
const byKind = new Map();
|
|
119
|
+
for (const entry of persona.entries) {
|
|
120
|
+
const list = byKind.get(entry.kind);
|
|
121
|
+
if (list) list.push(entry);
|
|
122
|
+
else byKind.set(entry.kind, [entry]);
|
|
123
|
+
}
|
|
124
|
+
return KNOWN_KINDS
|
|
125
|
+
.filter((kind) => byKind.has(kind))
|
|
126
|
+
.map((kind) => ({ kind, label: GROUP_LABEL[kind] ?? kind, entries: byKind.get(kind).sort((a, b) => a.order - b.order) }));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#renderSection(group, store) {
|
|
130
|
+
return html`
|
|
131
|
+
<section data-part="section" data-kind=${group.kind} aria-label=${group.label}>
|
|
132
|
+
<text-ui data-part="section-heading" variant="kicker">${group.label}</text-ui>
|
|
133
|
+
<div data-part="entries">
|
|
134
|
+
${group.entries.map((entry) => this.#renderEntry(entry, store))}
|
|
135
|
+
</div>
|
|
136
|
+
</section>
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#renderEntry(entry, store) {
|
|
141
|
+
const enabled = this.#readEnabled(entry, store);
|
|
142
|
+
const content = this.#readContent(entry, store);
|
|
143
|
+
const isOpen = this.#expanded.has(entry.id);
|
|
144
|
+
return html`
|
|
145
|
+
<div data-part="entry" data-entry-id=${entry.id}>
|
|
146
|
+
<div data-part="entry-row">
|
|
147
|
+
<div data-part="entry-meta">
|
|
148
|
+
<span data-part="entry-label">${entry.label}</span>
|
|
149
|
+
${entry.description ? html`<span data-part="entry-description">${entry.description}</span>` : null}
|
|
150
|
+
</div>
|
|
151
|
+
<button-ui data-action="collapse-toggle" data-entry-id=${entry.id}
|
|
152
|
+
variant="ghost" size="sm" icon=${isOpen ? 'caret-up' : 'caret-down'}
|
|
153
|
+
aria-expanded=${String(isOpen)} aria-label=${`Edit ${entry.label} content`}></button-ui>
|
|
154
|
+
<switch-ui data-part="entry-enabled" data-entry-id=${entry.id} .checked=${enabled}
|
|
155
|
+
aria-label=${`Enable ${entry.label}`}></switch-ui>
|
|
156
|
+
</div>
|
|
157
|
+
${isOpen ? html`
|
|
158
|
+
<div data-part="entry-content">
|
|
159
|
+
<textarea-ui data-part="entry-content-input" data-entry-id=${entry.id}
|
|
160
|
+
.value=${content} rows="4"></textarea-ui>
|
|
161
|
+
</div>
|
|
162
|
+
` : null}
|
|
163
|
+
</div>
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#readEnabled(entry, store) {
|
|
168
|
+
const stored = store?.get(entryStoreKey(entry.id, 'enabled'));
|
|
169
|
+
return typeof stored === 'boolean' ? stored : entry.enabled;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#readContent(entry, store) {
|
|
173
|
+
const stored = store?.get(entryStoreKey(entry.id, 'content'));
|
|
174
|
+
return typeof stored === 'string' ? stored : entry.content;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── event delegation ────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
#onChange = (e) => {
|
|
180
|
+
const sw = e.target.closest?.('[data-part="entry-enabled"]');
|
|
181
|
+
if (sw && this.contains(sw)) {
|
|
182
|
+
const entryId = sw.getAttribute('data-entry-id');
|
|
183
|
+
const checked = !!e.detail?.checked;
|
|
184
|
+
this.store?.set(entryStoreKey(entryId, 'enabled'), checked);
|
|
185
|
+
this.dispatchEvent(new CustomEvent('entry-change', {
|
|
186
|
+
bubbles: true,
|
|
187
|
+
detail: { entryId, field: 'enabled', value: checked },
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
#onContentInput = (e) => {
|
|
193
|
+
const ta = e.target.closest?.('[data-part="entry-content-input"]');
|
|
194
|
+
if (ta && this.contains(ta)) {
|
|
195
|
+
const entryId = ta.getAttribute('data-entry-id');
|
|
196
|
+
const value = e.detail?.value ?? ta.value ?? '';
|
|
197
|
+
this.store?.set(entryStoreKey(entryId, 'content'), value);
|
|
198
|
+
this.dispatchEvent(new CustomEvent('entry-change', {
|
|
199
|
+
bubbles: true,
|
|
200
|
+
detail: { entryId, field: 'content', value },
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
#onToggleCollapse = (e) => {
|
|
206
|
+
const btn = e.target.closest?.('[data-action="collapse-toggle"]');
|
|
207
|
+
if (!btn || !this.contains(btn)) return;
|
|
208
|
+
const entryId = btn.getAttribute('data-entry-id');
|
|
209
|
+
if (this.#expanded.has(entryId)) this.#expanded.delete(entryId);
|
|
210
|
+
else this.#expanded.add(entryId);
|
|
211
|
+
this._v++;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!customElements.get('admin-settings-ui')) {
|
|
216
|
+
customElements.define('admin-settings-ui', UIAdminSettings);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export { UIAdminSettings };
|