@adia-ai/web-components 0.6.37 → 0.6.38
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 +20 -0
- package/components/accordion/accordion-item.a2ui.json +3 -0
- package/components/accordion/accordion-item.yaml +5 -0
- package/components/action-list/action-item.a2ui.json +5 -1
- package/components/action-list/action-item.yaml +7 -0
- package/components/card/card.a2ui.json +17 -1
- package/components/card/card.yaml +24 -1
- package/components/empty-state/empty-state.a2ui.json +9 -0
- package/components/empty-state/empty-state.yaml +15 -0
- package/components/feed/feed-item.a2ui.json +5 -0
- package/components/feed/feed-item.yaml +10 -0
- package/components/field/field.a2ui.json +6 -0
- package/components/field/field.yaml +10 -0
- package/components/index.js +2 -0
- package/components/inline-edit/inline-edit.a2ui.json +159 -0
- package/components/inline-edit/inline-edit.class.js +184 -0
- package/components/inline-edit/inline-edit.css +62 -0
- package/components/inline-edit/inline-edit.d.ts +52 -0
- package/components/inline-edit/inline-edit.js +12 -0
- package/components/inline-edit/inline-edit.yaml +125 -0
- package/components/list/list-item.a2ui.json +8 -1
- package/components/list/list-item.yaml +12 -0
- package/components/list/list.css +36 -6
- package/components/mark/mark.a2ui.json +109 -0
- package/components/mark/mark.class.js +22 -0
- package/components/mark/mark.css +39 -0
- package/components/mark/mark.d.ts +27 -0
- package/components/mark/mark.js +12 -0
- package/components/mark/mark.yaml +87 -0
- package/components/modal/modal.a2ui.json +9 -0
- package/components/modal/modal.yaml +14 -0
- package/components/nav-group/nav-group.a2ui.json +3 -0
- package/components/nav-group/nav-group.yaml +5 -0
- package/components/nav-item/nav-item.a2ui.json +3 -0
- package/components/nav-item/nav-item.yaml +5 -0
- package/components/segmented/segmented.class.js +10 -2
- package/components/select/select.a2ui.json +3 -0
- package/components/select/select.yaml +5 -0
- package/components/slider/slider.a2ui.json +6 -0
- package/components/slider/slider.yaml +10 -0
- package/components/stat/stat.css +18 -14
- package/components/stepper/stepper-item.a2ui.json +3 -0
- package/components/stepper/stepper-item.yaml +5 -0
- package/components/timeline/timeline-item.a2ui.json +8 -1
- package/components/timeline/timeline-item.yaml +12 -0
- package/components/tree/tree-item.a2ui.json +5 -1
- package/components/tree/tree-item.yaml +7 -0
- package/components/tree/tree.a2ui.json +3 -0
- package/components/tree/tree.yaml +5 -0
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +74 -74
- package/package.json +1 -1
- package/styles/components.css +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
+
## [0.6.38] — 2026-05-26
|
|
4
|
+
|
|
5
|
+
### Added — Wave 5e cohort: 3 new primitives + slot-vocab-vs-css backlog closed
|
|
6
|
+
|
|
7
|
+
- **`<mark-ui>`** — inline highlighted text primitive (#357)
|
|
8
|
+
- **`<inline-edit-ui>`** — click-to-edit text in place (#212)
|
|
9
|
+
- **Notification deep-link demo + Mark All Read verify** for `<feed-ui>` (#344/#343)
|
|
10
|
+
|
|
11
|
+
### Fixed — substrate polish across the v0.6.38 window
|
|
12
|
+
|
|
13
|
+
- **`<list-item-ui>`**: slot selectors changed from direct-child to descendant — description row now lands on row 2 as intended
|
|
14
|
+
- **`<segmented-ui>`**: stale indicator stripping — duplicate pills on re-render eliminated
|
|
15
|
+
- **`<stat-ui>`**: chart slot bottom-aligns to value baseline; aspect 4:3
|
|
16
|
+
- **`<toolbar-ui>`** mixed-controls demo: `<segment-ui>` (not `<button-ui>`) inside `<segmented-ui>`
|
|
17
|
+
|
|
18
|
+
### Maintenance — slot-vocab-vs-css audit reached 0 critical findings
|
|
19
|
+
|
|
20
|
+
- `audit-slot-vocab-vs-css` substrate backlog closed → gate now enforced. Multi-yaml + @scope-aware pairing landed in the audit script.
|
|
21
|
+
- 5 Wave 5a/b/c/d gap-analysis flips landed (substrate-verified as covered; no new code).
|
|
22
|
+
|
|
3
23
|
## [0.6.37] — 2026-05-25
|
|
4
24
|
|
|
5
25
|
### Changed — `--a-warning-bg` redirected from `-strong` to `-20-tint` (bright caution-tape amber, scheme-independent)
|
|
@@ -63,6 +63,9 @@
|
|
|
63
63
|
"action": {
|
|
64
64
|
"description": "§206 (v0.5.7): action buttons inside a custom header (e.g. Copy /\nDownload / settings). Children placed at `[slot=\"action\"]` (or\n`[slot=\"actions\"]`, or marked `[data-no-toggle]`) are excluded from\nthe toggle-on-click cascade — clicking them fires their own handler\nwithout also toggling the section."
|
|
65
65
|
},
|
|
66
|
+
"body": {
|
|
67
|
+
"description": "The section's collapsible content. Renders below the header; hidden when `[open]` is unset. Author-fills with prose, lists, embedded forms, or any rich markup the panel needs."
|
|
68
|
+
},
|
|
66
69
|
"header": {
|
|
67
70
|
"description": "Custom header content. By default `[text]` renders as a plain header\nlabel, but a `[slot=\"header\"]` override lets consumers author rich\nheaders (icon + title + action buttons + caret)."
|
|
68
71
|
}
|
|
@@ -39,6 +39,11 @@ slots:
|
|
|
39
39
|
`[slot="actions"]`, or marked `[data-no-toggle]`) are excluded from
|
|
40
40
|
the toggle-on-click cascade — clicking them fires their own handler
|
|
41
41
|
without also toggling the section.
|
|
42
|
+
body:
|
|
43
|
+
description: >-
|
|
44
|
+
The section's collapsible content. Renders below the header; hidden
|
|
45
|
+
when `[open]` is unset. Author-fills with prose, lists, embedded
|
|
46
|
+
forms, or any rich markup the panel needs.
|
|
42
47
|
events:
|
|
43
48
|
toggle:
|
|
44
49
|
description: Fired when the section opens or closes.
|
|
@@ -62,7 +62,11 @@
|
|
|
62
62
|
"MenuItem",
|
|
63
63
|
"Button"
|
|
64
64
|
],
|
|
65
|
-
"slots": {
|
|
65
|
+
"slots": {
|
|
66
|
+
"icon": {
|
|
67
|
+
"description": "Override the [icon] glyph with a custom slotted element (custom icon-ui, image, avatar). Mutually exclusive with the [icon] attribute — slot child wins."
|
|
68
|
+
}
|
|
69
|
+
},
|
|
66
70
|
"states": [],
|
|
67
71
|
"status": "stable",
|
|
68
72
|
"synonyms": {
|
|
@@ -37,6 +37,13 @@ props:
|
|
|
37
37
|
type: boolean
|
|
38
38
|
default: false
|
|
39
39
|
|
|
40
|
+
slots:
|
|
41
|
+
icon:
|
|
42
|
+
description: >-
|
|
43
|
+
Override the [icon] glyph with a custom slotted element (custom
|
|
44
|
+
icon-ui, image, avatar). Mutually exclusive with the [icon]
|
|
45
|
+
attribute — slot child wins.
|
|
46
|
+
|
|
40
47
|
keywords:
|
|
41
48
|
- action-item
|
|
42
49
|
- command-row
|
|
@@ -113,7 +113,23 @@
|
|
|
113
113
|
"alert",
|
|
114
114
|
"skeleton"
|
|
115
115
|
],
|
|
116
|
-
"slots": {
|
|
116
|
+
"slots": {
|
|
117
|
+
"description": {
|
|
118
|
+
"description": "Optional descriptive text beneath the heading. Renders in the header slot at body-subtle typography. Use for short metadata lines (timestamp, author, status sentence)."
|
|
119
|
+
},
|
|
120
|
+
"action": {
|
|
121
|
+
"description": "Trailing action cluster in the header (e.g. icon-buttons, menu trigger, more-options). Aligns to the header's flex-end edge."
|
|
122
|
+
},
|
|
123
|
+
"action-leading": {
|
|
124
|
+
"description": "Leading action cluster in the header (e.g. back button, switcher, breadcrumb-context). Aligns to the header's flex-start edge, before the icon/heading column."
|
|
125
|
+
},
|
|
126
|
+
"heading": {
|
|
127
|
+
"description": "Card title. Renders in the header slot with title typography. Typically a short noun phrase or document/object name."
|
|
128
|
+
},
|
|
129
|
+
"icon": {
|
|
130
|
+
"description": "Optional leading icon for the card header (status / brand / type marker). Renders next to the heading. Use `<icon-ui name=\"…\">` or any inline icon element."
|
|
131
|
+
}
|
|
132
|
+
},
|
|
117
133
|
"states": [
|
|
118
134
|
{
|
|
119
135
|
"description": "Default, ready for interaction.",
|
|
@@ -58,7 +58,30 @@ props:
|
|
|
58
58
|
- soft
|
|
59
59
|
- primary
|
|
60
60
|
events: {}
|
|
61
|
-
slots:
|
|
61
|
+
slots:
|
|
62
|
+
icon:
|
|
63
|
+
description: >-
|
|
64
|
+
Optional leading icon for the card header (status / brand / type
|
|
65
|
+
marker). Renders next to the heading. Use `<icon-ui name="…">` or
|
|
66
|
+
any inline icon element.
|
|
67
|
+
heading:
|
|
68
|
+
description: >-
|
|
69
|
+
Card title. Renders in the header slot with title typography.
|
|
70
|
+
Typically a short noun phrase or document/object name.
|
|
71
|
+
description:
|
|
72
|
+
description: >-
|
|
73
|
+
Optional descriptive text beneath the heading. Renders in the
|
|
74
|
+
header slot at body-subtle typography. Use for short metadata
|
|
75
|
+
lines (timestamp, author, status sentence).
|
|
76
|
+
action:
|
|
77
|
+
description: >-
|
|
78
|
+
Trailing action cluster in the header (e.g. icon-buttons, menu
|
|
79
|
+
trigger, more-options). Aligns to the header's flex-end edge.
|
|
80
|
+
action-leading:
|
|
81
|
+
description: >-
|
|
82
|
+
Leading action cluster in the header (e.g. back button, switcher,
|
|
83
|
+
breadcrumb-context). Aligns to the header's flex-start edge,
|
|
84
|
+
before the icon/heading column.
|
|
62
85
|
states:
|
|
63
86
|
- name: idle
|
|
64
87
|
description: Default, ready for interaction.
|
|
@@ -85,8 +85,17 @@
|
|
|
85
85
|
"select"
|
|
86
86
|
],
|
|
87
87
|
"slots": {
|
|
88
|
+
"description": {
|
|
89
|
+
"description": "Override slot for the description text. Use for multi-paragraph or richly-marked-up explanatory copy."
|
|
90
|
+
},
|
|
88
91
|
"action": {
|
|
89
92
|
"description": "User-provided action element (e.g. button) displayed below the description"
|
|
93
|
+
},
|
|
94
|
+
"heading": {
|
|
95
|
+
"description": "Override slot for the heading text. Use when richer markup than the plain [heading] attribute is needed (line breaks, inline links, bold emphasis)."
|
|
96
|
+
},
|
|
97
|
+
"icon": {
|
|
98
|
+
"description": "Override slot for the [icon] glyph. Use when the attribute-driven Phosphor icon isn't enough (e.g., custom SVG, illustration, or a styled icon-ui with non-default weight). Mutually exclusive with [icon]."
|
|
90
99
|
}
|
|
91
100
|
},
|
|
92
101
|
"states": [
|
|
@@ -53,6 +53,21 @@ props:
|
|
|
53
53
|
reflect: true
|
|
54
54
|
events: {}
|
|
55
55
|
slots:
|
|
56
|
+
icon:
|
|
57
|
+
description: >-
|
|
58
|
+
Override slot for the [icon] glyph. Use when the attribute-driven
|
|
59
|
+
Phosphor icon isn't enough (e.g., custom SVG, illustration, or
|
|
60
|
+
a styled icon-ui with non-default weight). Mutually exclusive
|
|
61
|
+
with [icon].
|
|
62
|
+
heading:
|
|
63
|
+
description: >-
|
|
64
|
+
Override slot for the heading text. Use when richer markup than
|
|
65
|
+
the plain [heading] attribute is needed (line breaks, inline
|
|
66
|
+
links, bold emphasis).
|
|
67
|
+
description:
|
|
68
|
+
description: >-
|
|
69
|
+
Override slot for the description text. Use for multi-paragraph
|
|
70
|
+
or richly-marked-up explanatory copy.
|
|
56
71
|
action:
|
|
57
72
|
description: User-provided action element (e.g. button) displayed below the description
|
|
58
73
|
states:
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"properties": {
|
|
16
|
+
"action": {
|
|
17
|
+
"description": "Label of an inline action button (the \"deep-link\" / \"undo\" pattern).\nPair with `onAction` callback in `UIFeed.post()` — click invokes the\ncallback then auto-dismisses. Empty string renders no action button.\n",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"default": ""
|
|
20
|
+
},
|
|
16
21
|
"component": {
|
|
17
22
|
"const": "FeedItem"
|
|
18
23
|
},
|
|
@@ -42,6 +42,14 @@ props:
|
|
|
42
42
|
description: Render an x close button (default true for sticky, false for auto-fade)
|
|
43
43
|
type: boolean
|
|
44
44
|
default: false
|
|
45
|
+
action:
|
|
46
|
+
description: |
|
|
47
|
+
Label of an inline action button (the "deep-link" / "undo" pattern).
|
|
48
|
+
Pair with `onAction` callback in `UIFeed.post()` — click invokes the
|
|
49
|
+
callback then auto-dismisses. Empty string renders no action button.
|
|
50
|
+
type: string
|
|
51
|
+
default: ""
|
|
52
|
+
reflect: true
|
|
45
53
|
events:
|
|
46
54
|
close:
|
|
47
55
|
description: Fired after the item finishes its exit animation
|
|
@@ -74,3 +82,5 @@ a2ui:
|
|
|
74
82
|
reason: 'Imperative ownership.'
|
|
75
83
|
- rule: 'Different from <alert-ui> (inline persistent) and <toast-ui> (standalone ephemeral); feed-item is feed-scoped.'
|
|
76
84
|
reason: 'Surface boundary.'
|
|
85
|
+
- rule: 'For "notification deep-link" pattern: post with action + onAction. Click navigates and auto-dismisses (router.push() / location.href / api.markRead()).'
|
|
86
|
+
reason: 'Deep-link pattern lives in the onAction callback, not in markup.'
|
|
@@ -103,6 +103,12 @@
|
|
|
103
103
|
"action": {
|
|
104
104
|
"description": "Button adjacent to the control for inline actions (clear, reset, help popover)."
|
|
105
105
|
},
|
|
106
|
+
"error": {
|
|
107
|
+
"description": "Override slot for error markup richer than the plain [error] attribute string. Rendered below the control with danger-text styling. Mutually exclusive with [error]."
|
|
108
|
+
},
|
|
109
|
+
"hint": {
|
|
110
|
+
"description": "Override slot for hint markup richer than the plain [hint] attribute string (inline links, code spans, abbreviations). Rendered below the control at body-subtle typography. Mutually exclusive with [hint]."
|
|
111
|
+
},
|
|
106
112
|
"trailing": {
|
|
107
113
|
"description": "Secondary text or badge aligned with the label in the stacked layout (right-aligned) or between label and control in the inline layout."
|
|
108
114
|
}
|
|
@@ -89,6 +89,16 @@ slots:
|
|
|
89
89
|
description: >-
|
|
90
90
|
Button adjacent to the control for inline actions (clear,
|
|
91
91
|
reset, help popover).
|
|
92
|
+
hint:
|
|
93
|
+
description: >-
|
|
94
|
+
Override slot for hint markup richer than the plain [hint] attribute
|
|
95
|
+
string (inline links, code spans, abbreviations). Rendered below
|
|
96
|
+
the control at body-subtle typography. Mutually exclusive with [hint].
|
|
97
|
+
error:
|
|
98
|
+
description: >-
|
|
99
|
+
Override slot for error markup richer than the plain [error] attribute
|
|
100
|
+
string. Rendered below the control with danger-text styling. Mutually
|
|
101
|
+
exclusive with [error].
|
|
92
102
|
states:
|
|
93
103
|
- name: idle
|
|
94
104
|
description: Default, ready for interaction.
|
package/components/index.js
CHANGED
|
@@ -93,6 +93,8 @@ export { UIMenu, UIMenuItem, UIMenuDivider } from './menu/menu.js';
|
|
|
93
93
|
export { UIContextMenu } from './context-menu/context-menu.js';
|
|
94
94
|
export { UIVisuallyHidden } from './visually-hidden/visually-hidden.js';
|
|
95
95
|
export { UISkipNav } from './skip-nav/skip-nav.js';
|
|
96
|
+
export { UIMark } from './mark/mark.js';
|
|
97
|
+
export { UIInlineEdit } from './inline-edit/inline-edit.js';
|
|
96
98
|
export { UIToolbar, UIToolbarGroup } from './toolbar/toolbar.js';
|
|
97
99
|
export { UINav } from './nav/nav.js';
|
|
98
100
|
export { UINavGroup } from './nav-group/nav-group.js';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/InlineEdit.json",
|
|
4
|
+
"title": "InlineEdit",
|
|
5
|
+
"description": "Click-to-edit text in place. Renders as static text until clicked /\nfocused + Enter, then becomes editable. Enter or blur commits; Escape\ncancels and restores the original value. Form-participating.\n\nUse for editable titles, breadcrumb labels, table-cell text fields,\ndraft document names — any case where a user expects to edit text\nwithout opening a separate dialog or input. Distinct from `<input-ui>`\n(always-editable chrome) and from `<field-ui>` (stacked label + input\ncomposition). inline-edit reads as text in static state.\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
|
+
"commit": {
|
|
17
|
+
"description": "When to commit pending edits. `blur` (default) — saves on focusout\nor Enter. `enter` — Enter saves, blur cancels (returns to original).\n`manual` — only programmatic `commitEdit()` saves.\n",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"enum": [
|
|
20
|
+
"blur",
|
|
21
|
+
"enter",
|
|
22
|
+
"manual"
|
|
23
|
+
],
|
|
24
|
+
"default": "blur"
|
|
25
|
+
},
|
|
26
|
+
"component": {
|
|
27
|
+
"const": "InlineEdit"
|
|
28
|
+
},
|
|
29
|
+
"editing": {
|
|
30
|
+
"description": "Reflected state — `true` when the element is actively being edited.\nToggles automatically on click / Enter / blur / Escape; rarely set\nby consumers. Listen to `edit-start` / `edit-end` events instead.\n",
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"default": false
|
|
33
|
+
},
|
|
34
|
+
"placeholder": {
|
|
35
|
+
"description": "Hint text shown when the value is empty (inline-edit reads this in the static state).",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"default": "Click to edit"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"required": [
|
|
41
|
+
"component"
|
|
42
|
+
],
|
|
43
|
+
"unevaluatedProperties": false,
|
|
44
|
+
"x-adiaui": {
|
|
45
|
+
"anti_patterns": [
|
|
46
|
+
{
|
|
47
|
+
"fix": "<inline-edit-ui value=\"Title\" name=\"title\"> — reads as text in static state; chrome only appears while editing.",
|
|
48
|
+
"why": "Ghost variant still renders input chrome (border on focus). Looks like a control, not text.",
|
|
49
|
+
"wrong": "<input-ui value=\"Title\" name=\"title\" variant=\"ghost\">"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"fix": "<inline-edit-ui value=\"Title\">",
|
|
53
|
+
"why": "No state machine — no commit/cancel semantics, no form participation, no a11y wiring.",
|
|
54
|
+
"wrong": "<text-ui contenteditable>Title</text-ui>"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"category": "form",
|
|
58
|
+
"composes": [],
|
|
59
|
+
"events": {
|
|
60
|
+
"cancel": {
|
|
61
|
+
"description": "Fired when Escape (or blur with commit=enter) restores the original. Bubbles."
|
|
62
|
+
},
|
|
63
|
+
"change": {
|
|
64
|
+
"description": "Fired after a successful commit. detail = { value, oldValue }. Bubbles."
|
|
65
|
+
},
|
|
66
|
+
"edit-end": {
|
|
67
|
+
"description": "Fired when leaving edit mode. detail = { committed: boolean }. Bubbles."
|
|
68
|
+
},
|
|
69
|
+
"edit-start": {
|
|
70
|
+
"description": "Fired when entering edit mode. Bubbles."
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"examples": [
|
|
74
|
+
{
|
|
75
|
+
"description": "A draft document title that becomes editable on click.",
|
|
76
|
+
"a2ui": "[{ \"id\": \"title\", \"component\": \"InlineEdit\", \"value\": \"Untitled draft\" }]\n",
|
|
77
|
+
"name": "editable-title"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"keywords": [
|
|
81
|
+
"inline-edit",
|
|
82
|
+
"editable",
|
|
83
|
+
"click-to-edit",
|
|
84
|
+
"rename",
|
|
85
|
+
"edit-in-place"
|
|
86
|
+
],
|
|
87
|
+
"name": "UIInlineEdit",
|
|
88
|
+
"related": [
|
|
89
|
+
"input",
|
|
90
|
+
"field",
|
|
91
|
+
"text"
|
|
92
|
+
],
|
|
93
|
+
"slots": {},
|
|
94
|
+
"states": [
|
|
95
|
+
{
|
|
96
|
+
"description": "Static — text reads as plain text with a hover hint.",
|
|
97
|
+
"name": "idle"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"description": "Hover affordance — subtle background tint signals editability.",
|
|
101
|
+
"name": "hover"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"description": "contenteditable — host is the input surface; outline + caret.",
|
|
105
|
+
"name": "editing"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"description": "Value is empty and not editing — placeholder text renders.",
|
|
109
|
+
"name": "empty"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"description": "Locked; click + keyboard activation are no-ops.",
|
|
113
|
+
"name": "disabled"
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"status": "stable",
|
|
117
|
+
"synonyms": {
|
|
118
|
+
"inline-edit": [
|
|
119
|
+
"editable",
|
|
120
|
+
"click-to-edit",
|
|
121
|
+
"edit-in-place",
|
|
122
|
+
"rename-in-place"
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
"tag": "inline-edit-ui",
|
|
126
|
+
"tokens": {
|
|
127
|
+
"--inline-edit-bg-edit": {
|
|
128
|
+
"description": "Background while editing.",
|
|
129
|
+
"default": "var(--a-bg)"
|
|
130
|
+
},
|
|
131
|
+
"--inline-edit-bg-hover": {
|
|
132
|
+
"description": "Background tint on hover (idle state).",
|
|
133
|
+
"default": "var(--a-bg-muted)"
|
|
134
|
+
},
|
|
135
|
+
"--inline-edit-outline": {
|
|
136
|
+
"description": "Outline color in the editing state.",
|
|
137
|
+
"default": "var(--a-accent-strong)"
|
|
138
|
+
},
|
|
139
|
+
"--inline-edit-placeholder": {
|
|
140
|
+
"description": "Placeholder text color in empty-static state.",
|
|
141
|
+
"default": "var(--a-fg-subtle)"
|
|
142
|
+
},
|
|
143
|
+
"--inline-edit-px": {
|
|
144
|
+
"description": "Horizontal padding (inner gutter so hover tint reads as a pill, not a flush block).",
|
|
145
|
+
"default": "var(--a-space-1)"
|
|
146
|
+
},
|
|
147
|
+
"--inline-edit-py": {
|
|
148
|
+
"description": "Vertical padding.",
|
|
149
|
+
"default": "var(--a-space-0-5)"
|
|
150
|
+
},
|
|
151
|
+
"--inline-edit-radius": {
|
|
152
|
+
"description": "Border-radius for the hover / editing chrome.",
|
|
153
|
+
"default": "var(--a-radius-sm)"
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"traits": [],
|
|
157
|
+
"version": 1
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<inline-edit-ui>` — click-to-edit text in place.
|
|
3
|
+
*
|
|
4
|
+
* Static state: textContent reads as plain text with a hover affordance.
|
|
5
|
+
* Editing state: host becomes `contenteditable=plaintext-only`. Enter or
|
|
6
|
+
* blur commits (configurable via `commit`); Escape always cancels.
|
|
7
|
+
*
|
|
8
|
+
* Form-participating via UIFormElement (name/value/required/disabled/
|
|
9
|
+
* readonly inherited). Pair with a `<form>` + `name=` to submit edits
|
|
10
|
+
* as a field.
|
|
11
|
+
*
|
|
12
|
+
* Architecture: no template (stamp-nothing). The element manages its
|
|
13
|
+
* own DOM imperatively — textContent IS the value, contenteditable
|
|
14
|
+
* attribute IS the editing state. This keeps the element trivial and
|
|
15
|
+
* lets the inherited UIFormElement value sync stay authoritative.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { UIFormElement } from '../../core/form.js';
|
|
19
|
+
|
|
20
|
+
export class UIInlineEdit extends UIFormElement {
|
|
21
|
+
static properties = {
|
|
22
|
+
...UIFormElement.properties,
|
|
23
|
+
placeholder: { type: String, default: 'Click to edit', reflect: true },
|
|
24
|
+
editing: { type: Boolean, default: false, reflect: true },
|
|
25
|
+
commit: { type: String, default: 'blur', reflect: true }, // blur | enter | manual
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
static template = () => null;
|
|
29
|
+
|
|
30
|
+
#originalValue = '';
|
|
31
|
+
#bound = false;
|
|
32
|
+
#suppressBlur = false;
|
|
33
|
+
|
|
34
|
+
connected() {
|
|
35
|
+
super.connected();
|
|
36
|
+
if (!this.hasAttribute('role')) this.setAttribute('role', 'textbox');
|
|
37
|
+
if (!this.hasAttribute('tabindex') && !this.disabled) {
|
|
38
|
+
this.setAttribute('tabindex', '0');
|
|
39
|
+
}
|
|
40
|
+
// Reflect the initial value to textContent (the source of truth for
|
|
41
|
+
// display while not editing). Setting via property triggers the
|
|
42
|
+
// reflection but not the textContent sync — do it explicitly.
|
|
43
|
+
if (this.value && this.textContent.trim() === '') {
|
|
44
|
+
this.textContent = this.value;
|
|
45
|
+
} else if (!this.value && this.textContent.trim()) {
|
|
46
|
+
// Author supplied text in the slot — treat it as the initial value.
|
|
47
|
+
this.value = this.textContent.trim();
|
|
48
|
+
this.syncValue();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!this.#bound) {
|
|
52
|
+
this.#bound = true;
|
|
53
|
+
this.addEventListener('click', this.#onClick);
|
|
54
|
+
this.addEventListener('keydown', this.#onKeydown);
|
|
55
|
+
this.addEventListener('focus', this.#onFocus);
|
|
56
|
+
this.addEventListener('blur', this.#onBlur);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
disconnected() {
|
|
61
|
+
super.disconnected();
|
|
62
|
+
this.removeEventListener('click', this.#onClick);
|
|
63
|
+
this.removeEventListener('keydown', this.#onKeydown);
|
|
64
|
+
this.removeEventListener('focus', this.#onFocus);
|
|
65
|
+
this.removeEventListener('blur', this.#onBlur);
|
|
66
|
+
this.#bound = false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ── Public API ──────────────────────────────────────────────────── */
|
|
70
|
+
|
|
71
|
+
startEdit() {
|
|
72
|
+
if (this.editing || this.disabled || this.readonly) return;
|
|
73
|
+
this.#originalValue = this.value || '';
|
|
74
|
+
this.editing = true;
|
|
75
|
+
this.setAttribute('contenteditable', 'plaintext-only');
|
|
76
|
+
this.dispatchEvent(new CustomEvent('edit-start', { bubbles: true }));
|
|
77
|
+
// Defer focus/selection — setting contenteditable in the same tick
|
|
78
|
+
// as a click can race with the browser's own caret placement.
|
|
79
|
+
queueMicrotask(() => {
|
|
80
|
+
this.focus();
|
|
81
|
+
this.#selectAll();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
commitEdit() {
|
|
86
|
+
if (!this.editing) return;
|
|
87
|
+
const newValue = (this.textContent || '').trim();
|
|
88
|
+
const oldValue = this.#originalValue;
|
|
89
|
+
this.editing = false;
|
|
90
|
+
this.removeAttribute('contenteditable');
|
|
91
|
+
// Keep the visible text in sync with the canonical value (normalizes
|
|
92
|
+
// trailing whitespace + restores the source-of-truth string).
|
|
93
|
+
if (this.textContent !== newValue) this.textContent = newValue;
|
|
94
|
+
if (newValue !== oldValue) {
|
|
95
|
+
this.value = newValue;
|
|
96
|
+
this.syncValue(newValue);
|
|
97
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
98
|
+
bubbles: true,
|
|
99
|
+
detail: { value: newValue, oldValue },
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
this.dispatchEvent(new CustomEvent('edit-end', {
|
|
103
|
+
bubbles: true,
|
|
104
|
+
detail: { committed: true },
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
cancelEdit() {
|
|
109
|
+
if (!this.editing) return;
|
|
110
|
+
this.editing = false;
|
|
111
|
+
this.removeAttribute('contenteditable');
|
|
112
|
+
this.textContent = this.#originalValue;
|
|
113
|
+
this.dispatchEvent(new CustomEvent('cancel', { bubbles: true }));
|
|
114
|
+
this.dispatchEvent(new CustomEvent('edit-end', {
|
|
115
|
+
bubbles: true,
|
|
116
|
+
detail: { committed: false },
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ── Event handlers ──────────────────────────────────────────────── */
|
|
121
|
+
|
|
122
|
+
#onClick = (e) => {
|
|
123
|
+
if (this.disabled || this.readonly) return;
|
|
124
|
+
if (this.editing) return; // already editing, let caret behavior pass through
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
this.startEdit();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
#onKeydown = (e) => {
|
|
130
|
+
if (this.editing) {
|
|
131
|
+
if (e.key === 'Enter') {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
this.#suppressBlur = true; // commitEdit blurs us; don't recurse
|
|
134
|
+
this.commitEdit();
|
|
135
|
+
this.blur();
|
|
136
|
+
} else if (e.key === 'Escape') {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
this.#suppressBlur = true;
|
|
139
|
+
this.cancelEdit();
|
|
140
|
+
this.blur();
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
// Activate edit from keyboard (Enter or Space)
|
|
144
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
145
|
+
if (this.disabled || this.readonly) return;
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
this.startEdit();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
#onFocus = () => {
|
|
153
|
+
// No-op — keyboard activation handled in #onKeydown to distinguish
|
|
154
|
+
// focus-via-Tab (don't edit) from focus-via-click (also don't edit
|
|
155
|
+
// here; the click handler does that). Focus alone never starts an
|
|
156
|
+
// edit — that would surprise tab-traversers.
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
#onBlur = () => {
|
|
160
|
+
if (!this.editing) return;
|
|
161
|
+
if (this.#suppressBlur) {
|
|
162
|
+
this.#suppressBlur = false;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (this.commit === 'blur') {
|
|
166
|
+
this.commitEdit();
|
|
167
|
+
} else if (this.commit === 'enter') {
|
|
168
|
+
// blur cancels under `commit=enter` semantics
|
|
169
|
+
this.cancelEdit();
|
|
170
|
+
}
|
|
171
|
+
// commit=manual: do nothing on blur; consumer must call commitEdit()
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/* ── Helpers ─────────────────────────────────────────────────────── */
|
|
175
|
+
|
|
176
|
+
#selectAll() {
|
|
177
|
+
const range = document.createRange();
|
|
178
|
+
range.selectNodeContents(this);
|
|
179
|
+
const sel = window.getSelection();
|
|
180
|
+
if (!sel) return;
|
|
181
|
+
sel.removeAllRanges();
|
|
182
|
+
sel.addRange(range);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
@scope (inline-edit-ui) {
|
|
2
|
+
:where(:scope) {
|
|
3
|
+
--inline-edit-bg-hover-default: var(--a-bg-muted);
|
|
4
|
+
--inline-edit-bg-edit-default: var(--a-bg);
|
|
5
|
+
--inline-edit-outline-default: var(--a-accent-strong);
|
|
6
|
+
--inline-edit-placeholder-default: var(--a-fg-subtle);
|
|
7
|
+
--inline-edit-px-default: var(--a-space-1);
|
|
8
|
+
--inline-edit-py-default: var(--a-space-0-5);
|
|
9
|
+
--inline-edit-radius-default: var(--a-radius-sm);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
:scope {
|
|
13
|
+
/* Inline-block so padding works without breaking text flow when
|
|
14
|
+
embedded inside a paragraph or heading. Falls back to inline for
|
|
15
|
+
table cells (where the cell already contains the box). */
|
|
16
|
+
display: inline-block;
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
padding-inline: var(--inline-edit-px, var(--inline-edit-px-default));
|
|
19
|
+
padding-block: var(--inline-edit-py, var(--inline-edit-py-default));
|
|
20
|
+
margin-inline: calc(-1 * var(--inline-edit-px, var(--inline-edit-px-default)));
|
|
21
|
+
margin-block: calc(-1 * var(--inline-edit-py, var(--inline-edit-py-default)));
|
|
22
|
+
border-radius: var(--inline-edit-radius, var(--inline-edit-radius-default));
|
|
23
|
+
cursor: text;
|
|
24
|
+
color: inherit;
|
|
25
|
+
font: inherit;
|
|
26
|
+
transition: background-color var(--a-duration-fast) var(--a-easing-out);
|
|
27
|
+
/* Make sure focus-visible outline can land on us (we set tabindex=0) */
|
|
28
|
+
outline-offset: 2px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Hover affordance — subtle bg tint so the user sees this IS editable.
|
|
32
|
+
Suppressed when disabled / readonly / already editing. */
|
|
33
|
+
:scope:not([editing]):not([disabled]):not([readonly]):hover {
|
|
34
|
+
background: var(--inline-edit-bg-hover, var(--inline-edit-bg-hover-default));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Editing state — flat background + accent outline for focus clarity */
|
|
38
|
+
:scope[editing] {
|
|
39
|
+
background: var(--inline-edit-bg-edit, var(--inline-edit-bg-edit-default));
|
|
40
|
+
outline: 2px solid var(--inline-edit-outline, var(--inline-edit-outline-default));
|
|
41
|
+
outline-offset: 0;
|
|
42
|
+
cursor: text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Empty-value placeholder — show the [placeholder] attr as faint text
|
|
46
|
+
when the host has no text content + isn't being edited. */
|
|
47
|
+
:scope:empty:not([editing])::before {
|
|
48
|
+
content: attr(placeholder);
|
|
49
|
+
color: var(--inline-edit-placeholder, var(--inline-edit-placeholder-default));
|
|
50
|
+
font-style: italic;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:scope[disabled],
|
|
54
|
+
:scope[readonly] {
|
|
55
|
+
cursor: default;
|
|
56
|
+
opacity: 0.6;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
:scope[disabled] {
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
}
|
|
62
|
+
}
|