@adia-ai/web-components 0.5.3 → 0.5.5
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/components/accordion/accordion-item.a2ui.json +50 -0
- package/components/accordion/accordion-item.yaml +27 -0
- package/components/action-list/action-item.a2ui.json +63 -0
- package/components/action-list/action-item.yaml +37 -0
- package/components/avatar/avatar-group.a2ui.json +50 -0
- package/components/avatar/avatar-group.yaml +26 -0
- package/components/avatar/avatar.a2ui.json +4 -1
- package/components/avatar/avatar.yaml +7 -0
- package/components/button/class.js +39 -0
- package/components/calendar-picker/class.js +1 -0
- package/components/chart/chart.a2ui.json +4 -2
- package/components/chat-thread/chat-input.a2ui.json +158 -0
- package/components/chat-thread/chat-input.yaml +251 -0
- package/components/check/class.js +1 -0
- package/components/feed/feed-item.a2ui.json +86 -0
- package/components/list/list-item.a2ui.json +53 -0
- package/components/list/list-item.yaml +29 -0
- package/components/radio/class.js +1 -0
- package/components/select/class.js +15 -0
- package/components/select/select.a2ui.json +5 -0
- package/components/select/select.css +10 -0
- package/components/select/select.yaml +5 -0
- package/components/slider/class.js +58 -0
- package/components/slider/slider.a2ui.json +10 -0
- package/components/slider/slider.css +13 -0
- package/components/slider/slider.yaml +10 -0
- package/components/switch/class.js +19 -4
- package/components/switch/switch.css +10 -0
- package/components/tabs/tab.a2ui.json +58 -0
- package/components/tabs/tab.yaml +33 -0
- package/components/textarea/class.js +1 -0
- package/components/timeline/timeline-item.a2ui.json +76 -0
- package/components/timeline/timeline-item.yaml +47 -0
- package/components/tree/class.js +91 -0
- package/components/tree/tree-item.a2ui.json +65 -0
- package/components/tree/tree-item.yaml +41 -0
- package/components/tree/tree.a2ui.json +15 -0
- package/components/tree/tree.css +18 -0
- package/components/tree/tree.yaml +10 -0
- package/components/upload/class.js +1 -0
- package/core/icons.d.ts +148 -0
- package/core/template.js +21 -3
- package/package.json +2 -2
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
#
|
|
3
|
+
# §172 (v0.5.4): authored to close the chat-input redundant-send-button
|
|
4
|
+
# regression diagnosed 2026-05-14. Pre-§172 the runtime registry declared
|
|
5
|
+
# `ChatInput → chat-input-ui` but the component had no yaml/sidecar/catalog
|
|
6
|
+
# entry — `<chat-input-ui>` lived as a sibling file inside `chat-thread/`
|
|
7
|
+
# rather than its own folder. The build scanner only looked for canonical
|
|
8
|
+
# `<name>/<name>.yaml`, so this component was invisible to the catalog +
|
|
9
|
+
# system prompt + audit pipeline.
|
|
10
|
+
#
|
|
11
|
+
# Result: the LLM saw `ChatInput` in the type registry but had no schema
|
|
12
|
+
# describing what the element stamps. When users asked for "chat interface
|
|
13
|
+
# with chat-input", the LLM defensively added a sibling Button (primary
|
|
14
|
+
# variant) for "send" — duplicating the built-in send button the
|
|
15
|
+
# component already stamps. The user's ticket
|
|
16
|
+
# (20260514045025-chatinput-training-data-unclea) flagged this.
|
|
17
|
+
#
|
|
18
|
+
# The scanner was extended in §172 to pick up sibling yamls in the same
|
|
19
|
+
# component dir. This file now flows through the build to the v0.9 catalog,
|
|
20
|
+
# the LLM system prompt's CORPUS CONTEXT block, and the audit-script family.
|
|
21
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
22
|
+
name: UIChatInput
|
|
23
|
+
tag: chat-input-ui
|
|
24
|
+
component: ChatInput
|
|
25
|
+
category: agent
|
|
26
|
+
version: 1
|
|
27
|
+
description: |
|
|
28
|
+
Composable chat input bar — a self-contained chat-message composer
|
|
29
|
+
that stamps its OWN inner structure (textarea + model picker + send
|
|
30
|
+
button) when authored as a bare `<chat-input-ui>` tag.
|
|
31
|
+
|
|
32
|
+
IMPORTANT (closes the 2026-05-14 redundant-send-button class): the
|
|
33
|
+
component stamps a built-in send button (paper-plane-right icon,
|
|
34
|
+
primary variant). DO NOT add a separate Button sibling for "send" —
|
|
35
|
+
the user gets two send buttons. The submit event fires on Enter or
|
|
36
|
+
send-button click; `detail` is `{ text, model }`.
|
|
37
|
+
|
|
38
|
+
Inner stamped structure (default):
|
|
39
|
+
<chat-input-ui>
|
|
40
|
+
<textarea-ui placeholder="Type a message..." rows="1"></textarea-ui>
|
|
41
|
+
<div slot="toolbar">
|
|
42
|
+
<select-ui slot="model" placeholder="Model">...</select-ui>
|
|
43
|
+
<button-ui icon="paper-plane-right" variant="primary" slot="send"></button-ui>
|
|
44
|
+
</div>
|
|
45
|
+
</chat-input-ui>
|
|
46
|
+
|
|
47
|
+
Layout:
|
|
48
|
+
┌──────────────────────────────────┐
|
|
49
|
+
│ textarea (grows vertically) │
|
|
50
|
+
├──────────────────────────────────┤
|
|
51
|
+
│ [model ▾] [⏎ send] │ ← toolbar (model picker + built-in send)
|
|
52
|
+
└──────────────────────────────────┘
|
|
53
|
+
|
|
54
|
+
Composite wrapper, not a form field itself. The inner textarea-ui
|
|
55
|
+
is form-associated via UIFormElement and submits through the parent
|
|
56
|
+
form. `chat-input-ui`'s `disabled` / `placeholder` props propagate
|
|
57
|
+
to the inner textarea.
|
|
58
|
+
|
|
59
|
+
For module-tier composer surfaces (slot vocabulary for file attach,
|
|
60
|
+
autocomplete, trailing/leading controls), wrap inside
|
|
61
|
+
`<chat-composer>` — see ChatComposer for the module-tier shape.
|
|
62
|
+
|
|
63
|
+
props:
|
|
64
|
+
disabled:
|
|
65
|
+
description: |
|
|
66
|
+
Disable the entire input. Textarea becomes contenteditable=false;
|
|
67
|
+
send button disabled.
|
|
68
|
+
type: boolean
|
|
69
|
+
default: false
|
|
70
|
+
reflect: true
|
|
71
|
+
loading:
|
|
72
|
+
description: |
|
|
73
|
+
In-flight / streaming state. Send button disabled, submit events
|
|
74
|
+
suppressed, but textarea stays editable so the user can draft a
|
|
75
|
+
follow-up while the model is still responding.
|
|
76
|
+
type: boolean
|
|
77
|
+
default: false
|
|
78
|
+
reflect: true
|
|
79
|
+
placeholder:
|
|
80
|
+
description: Textarea placeholder. Defaults to "Type a message...".
|
|
81
|
+
type: string
|
|
82
|
+
default: "Type a message..."
|
|
83
|
+
reflect: true
|
|
84
|
+
model:
|
|
85
|
+
description: |
|
|
86
|
+
Currently selected model value (reflected, two-way with inner
|
|
87
|
+
`<select-ui slot="model">`). Empty when no model picker is shown.
|
|
88
|
+
type: string
|
|
89
|
+
default: ""
|
|
90
|
+
reflect: true
|
|
91
|
+
models:
|
|
92
|
+
description: |
|
|
93
|
+
JSON array of model options for the inner model picker —
|
|
94
|
+
[{value, label}] or [{label, options: [...]}] groups. Empty array
|
|
95
|
+
hides the model picker.
|
|
96
|
+
type: array
|
|
97
|
+
default: []
|
|
98
|
+
|
|
99
|
+
events:
|
|
100
|
+
submit:
|
|
101
|
+
description: >-
|
|
102
|
+
Fires when the user presses Enter (without Shift) in the textarea
|
|
103
|
+
OR clicks the built-in send button. The composer suppresses
|
|
104
|
+
submission while `[loading]` is set.
|
|
105
|
+
detail:
|
|
106
|
+
text:
|
|
107
|
+
type: string
|
|
108
|
+
description: Submitted message text from the inner textarea.
|
|
109
|
+
model:
|
|
110
|
+
type: string
|
|
111
|
+
description: Currently selected model value (empty if no model picker).
|
|
112
|
+
|
|
113
|
+
slots:
|
|
114
|
+
toolbar:
|
|
115
|
+
description: >-
|
|
116
|
+
Override slot for the entire toolbar row (model picker + send
|
|
117
|
+
button). Most authors should NOT override this — the built-in
|
|
118
|
+
toolbar handles model selection + send. Use this only when
|
|
119
|
+
custom toolbar layout is required.
|
|
120
|
+
send:
|
|
121
|
+
description: >-
|
|
122
|
+
Override slot for the send button only. The default stamped send
|
|
123
|
+
button has `[icon="paper-plane-right"] [variant="primary"]`. Most
|
|
124
|
+
authors should NOT override this — the built-in send button IS
|
|
125
|
+
the send control. Adding a separate Button sibling for "send"
|
|
126
|
+
duplicates this functionality.
|
|
127
|
+
model:
|
|
128
|
+
description: >-
|
|
129
|
+
Override slot for the model picker. Most authors should NOT
|
|
130
|
+
override this — set the `models` prop instead.
|
|
131
|
+
|
|
132
|
+
states:
|
|
133
|
+
- name: idle
|
|
134
|
+
description: Default, accepting input. Send button enabled when textarea has content.
|
|
135
|
+
- name: disabled
|
|
136
|
+
attribute: disabled
|
|
137
|
+
description: Input fully disabled (typically during initial form setup).
|
|
138
|
+
- name: loading
|
|
139
|
+
attribute: loading
|
|
140
|
+
description: >-
|
|
141
|
+
LLM is responding. Send button disabled + submit events suppressed,
|
|
142
|
+
but textarea stays editable for follow-up drafts.
|
|
143
|
+
|
|
144
|
+
traits: []
|
|
145
|
+
|
|
146
|
+
a2ui:
|
|
147
|
+
rules:
|
|
148
|
+
- >-
|
|
149
|
+
ChatInput is a self-contained composer — it stamps its own
|
|
150
|
+
textarea + model picker + send button. DO NOT add a separate
|
|
151
|
+
Button sibling for "send" inside the same parent. The user
|
|
152
|
+
gets two send buttons (one built-in, one redundant).
|
|
153
|
+
- >-
|
|
154
|
+
For chat interfaces emit ChatInput as the sole input
|
|
155
|
+
component inside the chat shell. The submit event fires on
|
|
156
|
+
Enter or send-button click; `detail` is `{ text, model }`.
|
|
157
|
+
- >-
|
|
158
|
+
To customize models, set the `models` prop with an array of
|
|
159
|
+
{value, label} option objects. Do not stamp a separate Select
|
|
160
|
+
next to ChatInput for model selection — the built-in model
|
|
161
|
+
picker handles it.
|
|
162
|
+
|
|
163
|
+
anti_patterns:
|
|
164
|
+
- description: >-
|
|
165
|
+
Adding a separate Button(primary) sibling next to ChatInput for
|
|
166
|
+
"send". The component stamps its own send button (paper-plane-right
|
|
167
|
+
icon). Two send buttons render side-by-side, confusing users.
|
|
168
|
+
- description: >-
|
|
169
|
+
Stamping a Select next to ChatInput for model picker. The component
|
|
170
|
+
has a built-in model picker driven by the `models` prop. Set
|
|
171
|
+
`models=[{value, label}, ...]` instead of stamping a separate Select.
|
|
172
|
+
- description: >-
|
|
173
|
+
Wrapping ChatInput inside <field-ui label="..."> for label
|
|
174
|
+
association. ChatInput is a composite container, not a form
|
|
175
|
+
field. For a labeled composer surface, use <chat-composer> +
|
|
176
|
+
slot label markup.
|
|
177
|
+
|
|
178
|
+
examples:
|
|
179
|
+
- name: basic-chat-input
|
|
180
|
+
description: >-
|
|
181
|
+
Minimal chat input — placeholder, no model picker. Sits inside
|
|
182
|
+
a chat shell footer.
|
|
183
|
+
a2ui: |-
|
|
184
|
+
[
|
|
185
|
+
{"id": "root", "component": "ChatInput", "placeholder": "Ask me anything..."}
|
|
186
|
+
]
|
|
187
|
+
- name: chat-input-with-models
|
|
188
|
+
description: >-
|
|
189
|
+
Chat input with model selection. The `models` prop drives the
|
|
190
|
+
built-in model picker — DO NOT add a separate Select sibling.
|
|
191
|
+
a2ui: |-
|
|
192
|
+
[
|
|
193
|
+
{
|
|
194
|
+
"id": "root",
|
|
195
|
+
"component": "ChatInput",
|
|
196
|
+
"placeholder": "Type a message...",
|
|
197
|
+
"model": "claude-opus-4-7",
|
|
198
|
+
"models": [
|
|
199
|
+
{"value": "claude-opus-4-7", "label": "Claude Opus 4.7"},
|
|
200
|
+
{"value": "claude-haiku-4-5", "label": "Claude Haiku 4.5"},
|
|
201
|
+
{"value": "claude-sonnet-4-5", "label": "Claude Sonnet 4.5"}
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
- name: chat-input-loading-state
|
|
206
|
+
description: >-
|
|
207
|
+
Streaming response state. `[loading]` reflects on the host;
|
|
208
|
+
send button disables; textarea stays editable so the user can
|
|
209
|
+
draft a follow-up while the LLM is responding.
|
|
210
|
+
a2ui: |-
|
|
211
|
+
[
|
|
212
|
+
{
|
|
213
|
+
"id": "root",
|
|
214
|
+
"component": "ChatInput",
|
|
215
|
+
"placeholder": "Drafting follow-up while streaming...",
|
|
216
|
+
"loading": true
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
- name: chat-shell-with-chat-input
|
|
220
|
+
description: >-
|
|
221
|
+
Full chat shell — header + thread + ChatInput in the footer.
|
|
222
|
+
Note: ChatInput is the sole footer child; no separate send
|
|
223
|
+
button.
|
|
224
|
+
a2ui: |-
|
|
225
|
+
[
|
|
226
|
+
{"id": "root", "component": "ChatShell", "children": ["header", "thread", "input"]},
|
|
227
|
+
{"id": "header", "component": "ChatHeader", "title": "Chat"},
|
|
228
|
+
{"id": "thread", "component": "ChatThread"},
|
|
229
|
+
{"id": "input", "component": "ChatInput", "placeholder": "Send a message..."}
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
keywords:
|
|
233
|
+
- chat-input
|
|
234
|
+
- chat
|
|
235
|
+
- message-input
|
|
236
|
+
- composer
|
|
237
|
+
- send-message
|
|
238
|
+
- conversation
|
|
239
|
+
- prompt
|
|
240
|
+
- submit
|
|
241
|
+
|
|
242
|
+
synonyms:
|
|
243
|
+
message-input: [conversation-input, prompt-input, send-bar]
|
|
244
|
+
|
|
245
|
+
related:
|
|
246
|
+
- ChatShell
|
|
247
|
+
- ChatThread
|
|
248
|
+
- ChatComposer
|
|
249
|
+
- ChatHeader
|
|
250
|
+
- TextArea
|
|
251
|
+
- Input
|
|
@@ -20,6 +20,7 @@ import { UIFormElement } from '../../core/form.js';
|
|
|
20
20
|
import { html } from '../../core/element.js';
|
|
21
21
|
|
|
22
22
|
export class UICheck extends UIFormElement {
|
|
23
|
+
static labelDeprecated = false; // §170 (v0.5.4): label is first-class per check.yaml
|
|
23
24
|
static properties = {
|
|
24
25
|
...UIFormElement.properties,
|
|
25
26
|
checked: { type: Boolean, default: false, reflect: true },
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/FeedItem.json",
|
|
4
|
+
"title": "FeedItem",
|
|
5
|
+
"description": "Atomic feed entry inside a `<feed-ui>` lane. Three dismiss policies are inferred from the prop shape — auto-fade (duration > 0 + no action), sticky-dismissible (duration null/0), action-required (duration null/0 + action; future phase). Posted via `UIFeed.post()` rather than authored directly.",
|
|
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": "FeedItem"
|
|
18
|
+
},
|
|
19
|
+
"dismissible": {
|
|
20
|
+
"description": "Render an x close button (default true for sticky, false for auto-fade)",
|
|
21
|
+
"type": "boolean",
|
|
22
|
+
"default": false
|
|
23
|
+
},
|
|
24
|
+
"duration": {
|
|
25
|
+
"description": "Auto-fade timer in ms; null/0 = sticky (requires user input)",
|
|
26
|
+
"type": "number",
|
|
27
|
+
"default": 4000
|
|
28
|
+
},
|
|
29
|
+
"heading": {
|
|
30
|
+
"description": "Optional emphasis line above text",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"default": ""
|
|
33
|
+
},
|
|
34
|
+
"icon": {
|
|
35
|
+
"description": "Optional leading icon name",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"default": ""
|
|
38
|
+
},
|
|
39
|
+
"text": {
|
|
40
|
+
"description": "Body copy",
|
|
41
|
+
"type": "string",
|
|
42
|
+
"default": ""
|
|
43
|
+
},
|
|
44
|
+
"variant": {
|
|
45
|
+
"description": "Semantic variant",
|
|
46
|
+
"type": "string",
|
|
47
|
+
"enum": [
|
|
48
|
+
"default",
|
|
49
|
+
"info",
|
|
50
|
+
"success",
|
|
51
|
+
"warning",
|
|
52
|
+
"danger"
|
|
53
|
+
],
|
|
54
|
+
"default": "default"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"required": [
|
|
58
|
+
"component"
|
|
59
|
+
],
|
|
60
|
+
"unevaluatedProperties": false,
|
|
61
|
+
"x-adiaui": {
|
|
62
|
+
"anti_patterns": [],
|
|
63
|
+
"category": "feedback",
|
|
64
|
+
"composes": [],
|
|
65
|
+
"events": {
|
|
66
|
+
"close": {
|
|
67
|
+
"description": "Fired after the item finishes its exit animation"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"examples": [],
|
|
71
|
+
"keywords": [],
|
|
72
|
+
"name": "UIFeedItem",
|
|
73
|
+
"related": [],
|
|
74
|
+
"slots": {
|
|
75
|
+
"body": {
|
|
76
|
+
"description": "Default content slot (also accepts the `text` / `heading` props)"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"states": {},
|
|
80
|
+
"synonyms": {},
|
|
81
|
+
"tag": "feed-item-ui",
|
|
82
|
+
"tokens": {},
|
|
83
|
+
"traits": [],
|
|
84
|
+
"version": 1
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/ListItem.json",
|
|
4
|
+
"title": "ListItem",
|
|
5
|
+
"description": "Child of <list-ui>. One list item with optional icon + text + description, plus arbitrary slotted content (cards, rows). Use inside <list-ui> only.",
|
|
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
|
+
"description": {
|
|
17
|
+
"description": "Secondary line below the primary text. Subtle color.",
|
|
18
|
+
"type": "string"
|
|
19
|
+
},
|
|
20
|
+
"component": {
|
|
21
|
+
"const": "ListItem"
|
|
22
|
+
},
|
|
23
|
+
"icon": {
|
|
24
|
+
"description": "Optional leading icon name (Phosphor).",
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
"text": {
|
|
28
|
+
"description": "Primary text. Renders as semibold inline body.",
|
|
29
|
+
"type": "string"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"required": [
|
|
33
|
+
"component"
|
|
34
|
+
],
|
|
35
|
+
"unevaluatedProperties": false,
|
|
36
|
+
"x-adiaui": {
|
|
37
|
+
"anti_patterns": [],
|
|
38
|
+
"category": "layout",
|
|
39
|
+
"composes": [],
|
|
40
|
+
"events": {},
|
|
41
|
+
"examples": [],
|
|
42
|
+
"keywords": [],
|
|
43
|
+
"name": "UIListItem",
|
|
44
|
+
"related": [],
|
|
45
|
+
"slots": {},
|
|
46
|
+
"states": [],
|
|
47
|
+
"synonyms": {},
|
|
48
|
+
"tag": "list-item-ui",
|
|
49
|
+
"tokens": {},
|
|
50
|
+
"traits": [],
|
|
51
|
+
"version": 1
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
#
|
|
3
|
+
# §176 (v0.5.5): authored to close the §175 baseline-orphan class. The
|
|
4
|
+
# component already existed as a sibling class in the parent's class.js
|
|
5
|
+
# + was registered alongside the parent (e.g. UIList + UIListItem both
|
|
6
|
+
# from list/class.js). The catalog just lacked its own entry. With the
|
|
7
|
+
# §172 sibling-yaml scanner, this file gets picked up next to the parent
|
|
8
|
+
# yaml.
|
|
9
|
+
|
|
10
|
+
# Child component of <list-ui>. Surface only inside that parent.
|
|
11
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
12
|
+
name: UIListItem
|
|
13
|
+
tag: list-item-ui
|
|
14
|
+
component: ListItem
|
|
15
|
+
category: layout
|
|
16
|
+
version: 1
|
|
17
|
+
description: |-
|
|
18
|
+
Child of <list-ui>. One list item with optional icon + text + description, plus arbitrary slotted content (cards, rows). Use inside <list-ui> only.
|
|
19
|
+
|
|
20
|
+
props:
|
|
21
|
+
icon:
|
|
22
|
+
description: Optional leading icon name (Phosphor).
|
|
23
|
+
type: string
|
|
24
|
+
text:
|
|
25
|
+
description: Primary text. Renders as semibold inline body.
|
|
26
|
+
type: string
|
|
27
|
+
description:
|
|
28
|
+
description: Secondary line below the primary text. Subtle color.
|
|
29
|
+
type: string
|
|
@@ -20,6 +20,7 @@ import { UIFormElement } from '../../core/form.js';
|
|
|
20
20
|
import { html } from '../../core/element.js';
|
|
21
21
|
|
|
22
22
|
export class UIRadio extends UIFormElement {
|
|
23
|
+
static labelDeprecated = false; // §170 (v0.5.4): label is first-class per radio.yaml
|
|
23
24
|
static properties = {
|
|
24
25
|
...UIFormElement.properties,
|
|
25
26
|
checked: { type: Boolean, default: false, reflect: true },
|
|
@@ -19,6 +19,7 @@ function escapeHTML(s) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export class UISelect extends UIFormElement {
|
|
22
|
+
static labelDeprecated = false; // §170 (v0.5.4): label is first-class per select.yaml
|
|
22
23
|
// §154 (v0.5.3): Phosphor icons this primitive auto-stamps (without
|
|
23
24
|
// consumer markup). Aggregated by installIconLoadersForRegistered()
|
|
24
25
|
// across all defined elements. Audited by check-required-icons.mjs
|
|
@@ -36,8 +37,14 @@ export class UISelect extends UIFormElement {
|
|
|
36
37
|
searchable: { type: Boolean, default: false, reflect: true },
|
|
37
38
|
freeText: { type: Boolean, default: false, reflect: true, attribute: 'free-text' },
|
|
38
39
|
divider: { type: Boolean, default: false, reflect: true },
|
|
40
|
+
// §184 (v0.5.5, FEEDBACK-08 §7): optional caption beneath the
|
|
41
|
+
// trigger, wired to aria-describedby on the host.
|
|
42
|
+
hint: { type: String, default: '', reflect: true },
|
|
39
43
|
};
|
|
40
44
|
|
|
45
|
+
// §184: per-instance hint id counter for aria-describedby wiring.
|
|
46
|
+
static #hintSeq = 0;
|
|
47
|
+
|
|
41
48
|
static template = () => null;
|
|
42
49
|
|
|
43
50
|
#options = [];
|
|
@@ -96,13 +103,21 @@ export class UISelect extends UIFormElement {
|
|
|
96
103
|
const displayMarkup = this.searchable
|
|
97
104
|
? `<input slot="display" type="text" role="combobox" aria-autocomplete="list" autocomplete="off" placeholder="${escapeHTML(this.placeholder || '')}" value="${escapeHTML(this.#displayText() === this.placeholder ? '' : this.#displayText())}" />`
|
|
98
105
|
: `<span slot="display">${escapeHTML(this.#displayText())}</span>`;
|
|
106
|
+
// §184 (v0.5.5, FEEDBACK-08 §7): optional hint slot beneath the
|
|
107
|
+
// trigger. Mirrors slider-ui's hint pattern + matches the
|
|
108
|
+
// schema-declared `hint` prop on switch-ui (which had the spec
|
|
109
|
+
// but not the rendering until this arc).
|
|
110
|
+
const hintId = this.hint ? `select-hint-${++UISelect.#hintSeq}` : '';
|
|
111
|
+
const hintMarkup = this.hint ? `<span slot="hint" id="${hintId}">${escapeHTML(this.hint)}</span>` : '';
|
|
99
112
|
this.innerHTML = `
|
|
100
113
|
<span slot="trigger">
|
|
101
114
|
${leading}
|
|
102
115
|
${displayMarkup}
|
|
103
116
|
<icon-ui name="caret-up-down" slot="caret"></icon-ui>
|
|
104
117
|
</span>
|
|
118
|
+
${hintMarkup}
|
|
105
119
|
`;
|
|
120
|
+
if (this.hint) this.setAttribute('aria-describedby', hintId);
|
|
106
121
|
|
|
107
122
|
if (this.searchable) {
|
|
108
123
|
// Detach from previous search input if any
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
"type": "boolean",
|
|
47
47
|
"default": false
|
|
48
48
|
},
|
|
49
|
+
"hint": {
|
|
50
|
+
"description": "§184 (v0.5.5, FEEDBACK-08 §7): small caption rendered beneath the select. Sets `aria-describedby` on the host so screen readers announce it as a description (distinct from `aria-label`, which comes from `label`). Does not conflict with the in-component `label`.",
|
|
51
|
+
"type": "string",
|
|
52
|
+
"default": ""
|
|
53
|
+
},
|
|
49
54
|
"icon": {
|
|
50
55
|
"description": "Leading icon name rendered inside the trigger",
|
|
51
56
|
"type": "string",
|
|
@@ -308,3 +308,13 @@ select-ui[divider] [role="group"] + [role="group"] {
|
|
|
308
308
|
margin-top: var(--a-space-1);
|
|
309
309
|
padding-top: var(--a-space-1);
|
|
310
310
|
}
|
|
311
|
+
|
|
312
|
+
/* ── Hint (§184, v0.5.5, FEEDBACK-08 §7) ──
|
|
313
|
+
Caption beneath the trigger; wired to aria-describedby on the host. */
|
|
314
|
+
select-ui > [slot="hint"] {
|
|
315
|
+
display: block;
|
|
316
|
+
margin-top: var(--select-hint-mt, var(--a-space-1));
|
|
317
|
+
font-size: var(--select-hint-size, var(--a-fine-size));
|
|
318
|
+
color: var(--select-hint-fg, var(--a-fg-muted));
|
|
319
|
+
line-height: var(--select-hint-lh, 1.4);
|
|
320
|
+
}
|
|
@@ -67,6 +67,11 @@ props:
|
|
|
67
67
|
description: Label text above the trigger
|
|
68
68
|
type: string
|
|
69
69
|
default: ""
|
|
70
|
+
hint:
|
|
71
|
+
description: |-
|
|
72
|
+
§184 (v0.5.5, FEEDBACK-08 §7): small caption rendered beneath the select. Sets `aria-describedby` on the host so screen readers announce it as a description (distinct from `aria-label`, which comes from `label`). Does not conflict with the in-component `label`.
|
|
73
|
+
type: string
|
|
74
|
+
default: ""
|
|
70
75
|
maxlength:
|
|
71
76
|
description: Maximum character length for validation
|
|
72
77
|
type: number
|
|
@@ -42,13 +42,31 @@ export class UISlider extends UIFormElement {
|
|
|
42
42
|
step: { type: Number, default: 1, reflect: true },
|
|
43
43
|
label: { type: String, default: '', reflect: true },
|
|
44
44
|
suffix: { type: String, default: '', reflect: true },
|
|
45
|
+
// §184 (v0.5.5, FEEDBACK-08 §4): declarative debounce for the
|
|
46
|
+
// `input` event when driving expensive computation (palette regen,
|
|
47
|
+
// shader compile, large list reflow). When > 0, value updates +
|
|
48
|
+
// visual feedback are immediate but `input` event emission is
|
|
49
|
+
// debounced — only the FINAL value in the throttle window dispatches.
|
|
50
|
+
// `change` fires unthrottled on pointerup / track click / keyboard;
|
|
51
|
+
// any pending `input` flushes BEFORE `change` so consumers always
|
|
52
|
+
// see input→input→…→input→change ordering. throttle="0" (default)
|
|
53
|
+
// preserves the pre-§184 every-pointer-move-fires-input behavior.
|
|
54
|
+
throttle: { type: Number, default: 0, reflect: true },
|
|
45
55
|
};
|
|
46
56
|
|
|
47
57
|
static template = () => null;
|
|
48
58
|
|
|
59
|
+
// §184: per-instance hint id counter for aria-describedby wiring.
|
|
60
|
+
static #hintSeq = 0;
|
|
61
|
+
|
|
49
62
|
#trackEl = null;
|
|
50
63
|
#thumbEl = null;
|
|
51
64
|
#dragging = false;
|
|
65
|
+
// §184 (v0.5.5, FEEDBACK-08 §4): debounce timer for the `input`
|
|
66
|
+
// event. When `throttle > 0`, #setValue stores a pending dispatch
|
|
67
|
+
// here + restarts the timer on every value change. Flushed before
|
|
68
|
+
// any `change` event so input always precedes change.
|
|
69
|
+
#inputTimer = null;
|
|
52
70
|
|
|
53
71
|
get #pct() {
|
|
54
72
|
const range = this.max - this.min;
|
|
@@ -69,6 +87,9 @@ export class UISlider extends UIFormElement {
|
|
|
69
87
|
if (this.label) this.setAttribute('aria-label', this.label);
|
|
70
88
|
|
|
71
89
|
if (!this.querySelector('[slot="track"]')) {
|
|
90
|
+
// §184 (v0.5.5, FEEDBACK-08 §7): hint slot stamped underneath
|
|
91
|
+
// the track when [hint] is set. Wired to aria-describedby below.
|
|
92
|
+
const hintId = this.hint ? `slider-hint-${++UISlider.#hintSeq}` : '';
|
|
72
93
|
this.innerHTML = `
|
|
73
94
|
<div slot="header">
|
|
74
95
|
${this.label ? `<span slot="label">${this.label}</span>` : ''}
|
|
@@ -81,7 +102,9 @@ export class UISlider extends UIFormElement {
|
|
|
81
102
|
<div slot="fill"></div>
|
|
82
103
|
<div slot="thumb" tabindex="0"></div>
|
|
83
104
|
</div>
|
|
105
|
+
${this.hint ? `<span slot="hint" id="${hintId}">${this.hint}</span>` : ''}
|
|
84
106
|
`;
|
|
107
|
+
if (this.hint) this.setAttribute('aria-describedby', hintId);
|
|
85
108
|
}
|
|
86
109
|
|
|
87
110
|
this.#trackEl = this.querySelector('[slot="track"]');
|
|
@@ -148,9 +171,36 @@ export class UISlider extends UIFormElement {
|
|
|
148
171
|
#setValue(v) {
|
|
149
172
|
if (v === this.value) return;
|
|
150
173
|
this.value = v;
|
|
174
|
+
// §184: when throttle > 0, debounce the `input` dispatch.
|
|
175
|
+
// The value update + UI is still immediate; only event emission is
|
|
176
|
+
// accumulated. Same value can dispatch input multiple times across
|
|
177
|
+
// pointer moves, but the throttle collapses them to one trailing
|
|
178
|
+
// emission at quiet+throttle ms.
|
|
179
|
+
const t = Number(this.throttle) || 0;
|
|
180
|
+
if (t > 0) {
|
|
181
|
+
if (this.#inputTimer != null) clearTimeout(this.#inputTimer);
|
|
182
|
+
this.#inputTimer = setTimeout(() => {
|
|
183
|
+
this.#inputTimer = null;
|
|
184
|
+
this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
|
|
185
|
+
}, t);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
151
188
|
this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
|
|
152
189
|
}
|
|
153
190
|
|
|
191
|
+
/**
|
|
192
|
+
* §184: flush any pending throttled `input` dispatch synchronously.
|
|
193
|
+
* Called before `change` so consumers see the trailing input event
|
|
194
|
+
* BEFORE the change commit. No-op when no timer is pending.
|
|
195
|
+
*/
|
|
196
|
+
#flushInput() {
|
|
197
|
+
if (this.#inputTimer != null) {
|
|
198
|
+
clearTimeout(this.#inputTimer);
|
|
199
|
+
this.#inputTimer = null;
|
|
200
|
+
this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
154
204
|
#onPointerDown = (e) => {
|
|
155
205
|
if (this.disabled) return;
|
|
156
206
|
e.preventDefault();
|
|
@@ -170,12 +220,14 @@ export class UISlider extends UIFormElement {
|
|
|
170
220
|
this.#thumbEl.releasePointerCapture(e.pointerId);
|
|
171
221
|
this.#thumbEl.removeEventListener('pointermove', this.#onPointerMove);
|
|
172
222
|
this.#thumbEl.removeEventListener('pointerup', this.#onPointerUp);
|
|
223
|
+
this.#flushInput(); // §184: pending throttled input fires before change
|
|
173
224
|
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
|
|
174
225
|
};
|
|
175
226
|
|
|
176
227
|
#onTrackClick = (e) => {
|
|
177
228
|
if (this.disabled || e.target === this.#thumbEl) return;
|
|
178
229
|
this.#setValue(this.#valueFromX(e.clientX));
|
|
230
|
+
this.#flushInput(); // §184: ensure trailing input precedes change
|
|
179
231
|
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
|
|
180
232
|
};
|
|
181
233
|
|
|
@@ -193,6 +245,7 @@ export class UISlider extends UIFormElement {
|
|
|
193
245
|
}
|
|
194
246
|
e.preventDefault();
|
|
195
247
|
this.#setValue(this.#snap(v));
|
|
248
|
+
this.#flushInput(); // §184: trailing input fires before change
|
|
196
249
|
this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
|
|
197
250
|
};
|
|
198
251
|
|
|
@@ -203,6 +256,11 @@ export class UISlider extends UIFormElement {
|
|
|
203
256
|
this.#thumbEl?.removeEventListener('pointermove', this.#onPointerMove);
|
|
204
257
|
this.#thumbEl?.removeEventListener('pointerup', this.#onPointerUp);
|
|
205
258
|
this.removeEventListener('keydown', this.#onKey);
|
|
259
|
+
// §184: drop any pending throttle timer (no flush — element is gone)
|
|
260
|
+
if (this.#inputTimer != null) {
|
|
261
|
+
clearTimeout(this.#inputTimer);
|
|
262
|
+
this.#inputTimer = null;
|
|
263
|
+
}
|
|
206
264
|
this.#trackEl = null;
|
|
207
265
|
this.#thumbEl = null;
|
|
208
266
|
}
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"type": "string",
|
|
32
32
|
"default": ""
|
|
33
33
|
},
|
|
34
|
+
"hint": {
|
|
35
|
+
"description": "§184 (v0.5.5, FEEDBACK-08 §7): small caption rendered beneath the slider track. Sets `aria-describedby` on the host so screen readers announce it as a description (distinct from `aria-label`, which comes from `label`). Does not conflict with the in-component `label`. Use for semantic clarifications a `<field-ui>` wrapper would be overkill for.",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"default": ""
|
|
38
|
+
},
|
|
34
39
|
"label": {
|
|
35
40
|
"description": "Label text above the slider",
|
|
36
41
|
"type": "string",
|
|
@@ -61,6 +66,11 @@
|
|
|
61
66
|
"type": "string",
|
|
62
67
|
"default": ""
|
|
63
68
|
},
|
|
69
|
+
"throttle": {
|
|
70
|
+
"description": "§184 (v0.5.5, FEEDBACK-08 §4): when > 0, debounce the `input` event by this many milliseconds. Value updates + visual feedback remain immediate; only event dispatch accumulates. Pending input flushes BEFORE `change` so consumers always see input→…→input→change ordering. throttle=\"0\" (default) preserves the pre-§184 every-pointer-move-fires-input behavior. Common values: 50-100ms for palette regen / shader compile / large list reflow.",
|
|
71
|
+
"type": "number",
|
|
72
|
+
"default": 0
|
|
73
|
+
},
|
|
64
74
|
"value": {
|
|
65
75
|
"description": "Current slider value",
|
|
66
76
|
"type": "number",
|