@adia-ai/web-components 0.6.36 → 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 +48 -1
- 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/badge/badge.a2ui.json +10 -0
- package/components/badge/badge.css +70 -0
- package/components/badge/badge.yaml +20 -0
- package/components/blockquote/blockquote.a2ui.json +121 -0
- package/components/blockquote/blockquote.class.js +68 -0
- package/components/blockquote/blockquote.css +46 -0
- package/components/blockquote/blockquote.d.ts +31 -0
- package/components/blockquote/blockquote.js +17 -0
- package/components/blockquote/blockquote.yaml +124 -0
- package/components/button/button.css +11 -3
- package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
- package/components/calendar-picker/calendar-picker.class.js +7 -1
- package/components/calendar-picker/calendar-picker.yaml +14 -0
- package/components/card/card.a2ui.json +17 -1
- package/components/card/card.yaml +24 -1
- package/components/color-input/color-input.a2ui.json +2 -2
- package/components/color-input/color-input.class.js +9 -2
- package/components/color-input/color-input.yaml +2 -2
- package/components/combobox/combobox.class.js +4 -0
- package/components/context-menu/context-menu.a2ui.json +159 -0
- package/components/context-menu/context-menu.class.js +275 -0
- package/components/context-menu/context-menu.css +56 -0
- package/components/context-menu/context-menu.d.ts +70 -0
- package/components/context-menu/context-menu.js +17 -0
- package/components/context-menu/context-menu.yaml +136 -0
- package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
- package/components/date-range-picker/date-range-picker.class.js +2 -0
- package/components/date-range-picker/date-range-picker.yaml +14 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
- package/components/datetime-picker/datetime-picker.class.js +3 -1
- package/components/datetime-picker/datetime-picker.d.ts +2 -0
- package/components/datetime-picker/datetime-picker.yaml +14 -0
- package/components/empty-state/empty-state.a2ui.json +9 -0
- package/components/empty-state/empty-state.class.js +2 -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/feed/feed.class.js +13 -5
- package/components/feed/feed.css +14 -0
- package/components/field/field.a2ui.json +6 -0
- package/components/field/field.yaml +10 -0
- package/components/index.js +11 -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/integration-card/integration-card.class.js +9 -0
- package/components/integration-card/integration-card.test.js +4 -3
- 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.css +7 -1
- 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/number-format/number-format.a2ui.json +180 -0
- package/components/number-format/number-format.class.js +96 -0
- package/components/number-format/number-format.css +18 -0
- package/components/number-format/number-format.d.ts +68 -0
- package/components/number-format/number-format.js +17 -0
- package/components/number-format/number-format.yaml +204 -0
- package/components/pagination/pagination.a2ui.json +19 -2
- package/components/pagination/pagination.class.js +90 -37
- package/components/pagination/pagination.css +32 -127
- package/components/pagination/pagination.d.ts +8 -2
- package/components/pagination/pagination.test.js +195 -0
- package/components/pagination/pagination.yaml +22 -1
- package/components/password-strength/password-strength.a2ui.json +152 -0
- package/components/password-strength/password-strength.class.js +157 -0
- package/components/password-strength/password-strength.css +80 -0
- package/components/password-strength/password-strength.d.ts +59 -0
- package/components/password-strength/password-strength.js +17 -0
- package/components/password-strength/password-strength.yaml +153 -0
- package/components/popover/popover.css +43 -23
- package/components/popover/popover.yaml +8 -4
- package/components/qr-code/QR-TEST.svg +4 -0
- package/components/qr-code/qr-code.a2ui.json +154 -0
- package/components/qr-code/qr-code.class.js +129 -0
- package/components/qr-code/qr-code.css +41 -0
- package/components/qr-code/qr-code.d.ts +83 -0
- package/components/qr-code/qr-code.js +17 -0
- package/components/qr-code/qr-code.yaml +203 -0
- package/components/qr-code/qr-encoder.js +633 -0
- package/components/relative-time/relative-time.a2ui.json +120 -0
- package/components/relative-time/relative-time.class.js +136 -0
- package/components/relative-time/relative-time.css +22 -0
- package/components/relative-time/relative-time.d.ts +51 -0
- package/components/relative-time/relative-time.js +17 -0
- package/components/relative-time/relative-time.yaml +133 -0
- package/components/segmented/segmented.class.js +15 -3
- package/components/select/select.a2ui.json +3 -0
- package/components/select/select.class.js +4 -0
- package/components/select/select.yaml +5 -0
- package/components/skip-nav/skip-nav.a2ui.json +92 -0
- package/components/skip-nav/skip-nav.class.js +45 -0
- package/components/skip-nav/skip-nav.css +54 -0
- package/components/skip-nav/skip-nav.d.ts +27 -0
- package/components/skip-nav/skip-nav.js +12 -0
- package/components/skip-nav/skip-nav.yaml +68 -0
- package/components/slider/slider.a2ui.json +22 -1
- package/components/slider/slider.class.js +264 -122
- package/components/slider/slider.css +82 -2
- package/components/slider/slider.d.ts +19 -3
- package/components/slider/slider.test.js +55 -0
- package/components/slider/slider.yaml +38 -6
- 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/table/table.class.js +29 -6
- package/components/table/table.css +31 -4
- package/components/table-toolbar/table-toolbar.class.js +3 -1
- package/components/tag/tag.a2ui.json +3 -2
- package/components/tag/tag.css +35 -11
- package/components/tag/tag.d.ts +14 -0
- package/components/tag/tag.test.js +35 -11
- package/components/tag/tag.yaml +13 -7
- package/components/timeline/timeline-item.a2ui.json +8 -1
- package/components/timeline/timeline-item.yaml +12 -0
- package/components/toast/toast.class.js +12 -4
- package/components/toc/toc.a2ui.json +159 -0
- package/components/toc/toc.class.js +222 -0
- package/components/toc/toc.css +92 -0
- package/components/toc/toc.d.ts +61 -0
- package/components/toc/toc.js +17 -0
- package/components/toc/toc.yaml +180 -0
- package/components/toolbar/toolbar.class.js +3 -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/components/visually-hidden/visually-hidden.a2ui.json +71 -0
- package/components/visually-hidden/visually-hidden.class.js +14 -0
- package/components/visually-hidden/visually-hidden.css +25 -0
- package/components/visually-hidden/visually-hidden.d.ts +26 -0
- package/components/visually-hidden/visually-hidden.js +12 -0
- package/components/visually-hidden/visually-hidden.yaml +54 -0
- package/core/anchor.js +19 -3
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +100 -89
- package/package.json +1 -1
- package/styles/colors/semantics.css +11 -2
- package/styles/components.css +11 -0
- package/styles/resets.css +10 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<inline-edit-ui>` — Click-to-edit text in place. Renders as static text until clicked /
|
|
3
|
+
focused + Enter, then becomes editable. Enter or blur commits; Escape
|
|
4
|
+
cancels and restores the original value. Form-participating.
|
|
5
|
+
|
|
6
|
+
Use for editable titles, breadcrumb labels, table-cell text fields,
|
|
7
|
+
draft document names — any case where a user expects to edit text
|
|
8
|
+
without opening a separate dialog or input. Distinct from `<input-ui>`
|
|
9
|
+
(always-editable chrome) and from `<field-ui>` (stacked label + input
|
|
10
|
+
composition). inline-edit reads as text in static state.
|
|
11
|
+
|
|
12
|
+
*
|
|
13
|
+
* @see https://ui-kit.exe.xyz/site/components/inline-edit
|
|
14
|
+
*
|
|
15
|
+
* Type declarations generated by scripts/build/dts-codegen.mjs from
|
|
16
|
+
* the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
|
|
17
|
+
* run `npm run build:components`, then `npm run codegen:dts` to
|
|
18
|
+
* regenerate; or hand-author this file fully if rich event types are
|
|
19
|
+
* needed beyond what the yaml `events:` block can express.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { UIElement } from '../../core/element.js';
|
|
23
|
+
|
|
24
|
+
export type InlineEditCancelEvent = CustomEvent<unknown>;
|
|
25
|
+
export type InlineEditChangeEvent = CustomEvent<unknown>;
|
|
26
|
+
export type InlineEditEditEndEvent = CustomEvent<unknown>;
|
|
27
|
+
export type InlineEditEditStartEvent = CustomEvent<unknown>;
|
|
28
|
+
|
|
29
|
+
export class UIInlineEdit extends UIElement {
|
|
30
|
+
/** When to commit pending edits. `blur` (default) — saves on focusout
|
|
31
|
+
or Enter. `enter` — Enter saves, blur cancels (returns to original).
|
|
32
|
+
`manual` — only programmatic `commitEdit()` saves.
|
|
33
|
+
*/
|
|
34
|
+
commit: 'blur' | 'enter' | 'manual';
|
|
35
|
+
/** Reflected state — `true` when the element is actively being edited.
|
|
36
|
+
Toggles automatically on click / Enter / blur / Escape; rarely set
|
|
37
|
+
by consumers. Listen to `edit-start` / `edit-end` events instead.
|
|
38
|
+
*/
|
|
39
|
+
editing: boolean;
|
|
40
|
+
/** Hint text shown when the value is empty (inline-edit reads this in the static state). */
|
|
41
|
+
placeholder: string;
|
|
42
|
+
|
|
43
|
+
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
44
|
+
type: K,
|
|
45
|
+
listener: (this: UIInlineEdit, ev: HTMLElementEventMap[K]) => unknown,
|
|
46
|
+
options?: boolean | AddEventListenerOptions,
|
|
47
|
+
): void;
|
|
48
|
+
addEventListener(type: 'cancel', listener: (ev: InlineEditCancelEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
49
|
+
addEventListener(type: 'change', listener: (ev: InlineEditChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
50
|
+
addEventListener(type: 'edit-end', listener: (ev: InlineEditEditEndEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
51
|
+
addEventListener(type: 'edit-start', listener: (ev: InlineEditEditStartEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<inline-edit-ui>` — auto-registers the tag on import.
|
|
3
|
+
*
|
|
4
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { defineIfFree } from '../../core/register.js';
|
|
8
|
+
import { UIInlineEdit } from './inline-edit.class.js';
|
|
9
|
+
|
|
10
|
+
defineIfFree('inline-edit-ui', UIInlineEdit);
|
|
11
|
+
|
|
12
|
+
export { UIInlineEdit };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
2
|
+
name: UIInlineEdit
|
|
3
|
+
tag: inline-edit-ui
|
|
4
|
+
status: stable
|
|
5
|
+
component: InlineEdit
|
|
6
|
+
category: form
|
|
7
|
+
version: 1
|
|
8
|
+
description: |
|
|
9
|
+
Click-to-edit text in place. Renders as static text until clicked /
|
|
10
|
+
focused + Enter, then becomes editable. Enter or blur commits; Escape
|
|
11
|
+
cancels and restores the original value. Form-participating.
|
|
12
|
+
|
|
13
|
+
Use for editable titles, breadcrumb labels, table-cell text fields,
|
|
14
|
+
draft document names — any case where a user expects to edit text
|
|
15
|
+
without opening a separate dialog or input. Distinct from `<input-ui>`
|
|
16
|
+
(always-editable chrome) and from `<field-ui>` (stacked label + input
|
|
17
|
+
composition). inline-edit reads as text in static state.
|
|
18
|
+
props:
|
|
19
|
+
placeholder:
|
|
20
|
+
description: Hint text shown when the value is empty (inline-edit reads this in the static state).
|
|
21
|
+
type: string
|
|
22
|
+
default: Click to edit
|
|
23
|
+
reflect: true
|
|
24
|
+
editing:
|
|
25
|
+
description: |
|
|
26
|
+
Reflected state — `true` when the element is actively being edited.
|
|
27
|
+
Toggles automatically on click / Enter / blur / Escape; rarely set
|
|
28
|
+
by consumers. Listen to `edit-start` / `edit-end` events instead.
|
|
29
|
+
type: boolean
|
|
30
|
+
default: false
|
|
31
|
+
reflect: true
|
|
32
|
+
commit:
|
|
33
|
+
description: |
|
|
34
|
+
When to commit pending edits. `blur` (default) — saves on focusout
|
|
35
|
+
or Enter. `enter` — Enter saves, blur cancels (returns to original).
|
|
36
|
+
`manual` — only programmatic `commitEdit()` saves.
|
|
37
|
+
type: string
|
|
38
|
+
default: blur
|
|
39
|
+
enum:
|
|
40
|
+
- blur
|
|
41
|
+
- enter
|
|
42
|
+
- manual
|
|
43
|
+
reflect: true
|
|
44
|
+
events:
|
|
45
|
+
change:
|
|
46
|
+
description: 'Fired after a successful commit. detail = { value, oldValue }. Bubbles.'
|
|
47
|
+
cancel:
|
|
48
|
+
description: 'Fired when Escape (or blur with commit=enter) restores the original. Bubbles.'
|
|
49
|
+
edit-start:
|
|
50
|
+
description: 'Fired when entering edit mode. Bubbles.'
|
|
51
|
+
edit-end:
|
|
52
|
+
description: 'Fired when leaving edit mode. detail = { committed: boolean }. Bubbles.'
|
|
53
|
+
slots: {}
|
|
54
|
+
states:
|
|
55
|
+
- name: idle
|
|
56
|
+
description: Static — text reads as plain text with a hover hint.
|
|
57
|
+
- name: hover
|
|
58
|
+
description: Hover affordance — subtle background tint signals editability.
|
|
59
|
+
- name: editing
|
|
60
|
+
description: contenteditable — host is the input surface; outline + caret.
|
|
61
|
+
- name: empty
|
|
62
|
+
description: Value is empty and not editing — placeholder text renders.
|
|
63
|
+
- name: disabled
|
|
64
|
+
description: Locked; click + keyboard activation are no-ops.
|
|
65
|
+
traits: []
|
|
66
|
+
tokens:
|
|
67
|
+
--inline-edit-bg-hover:
|
|
68
|
+
description: Background tint on hover (idle state).
|
|
69
|
+
default: var(--a-bg-muted)
|
|
70
|
+
--inline-edit-bg-edit:
|
|
71
|
+
description: Background while editing.
|
|
72
|
+
default: var(--a-bg)
|
|
73
|
+
--inline-edit-outline:
|
|
74
|
+
description: Outline color in the editing state.
|
|
75
|
+
default: var(--a-accent-strong)
|
|
76
|
+
--inline-edit-placeholder:
|
|
77
|
+
description: Placeholder text color in empty-static state.
|
|
78
|
+
default: var(--a-fg-subtle)
|
|
79
|
+
--inline-edit-px:
|
|
80
|
+
description: Horizontal padding (inner gutter so hover tint reads as a pill, not a flush block).
|
|
81
|
+
default: var(--a-space-1)
|
|
82
|
+
--inline-edit-py:
|
|
83
|
+
description: Vertical padding.
|
|
84
|
+
default: var(--a-space-0-5)
|
|
85
|
+
--inline-edit-radius:
|
|
86
|
+
description: Border-radius for the hover / editing chrome.
|
|
87
|
+
default: var(--a-radius-sm)
|
|
88
|
+
a2ui:
|
|
89
|
+
rules:
|
|
90
|
+
- rule: 'Use inline-edit-ui for click-to-edit titles, draft names, table-cell text, breadcrumb labels — anywhere the user expects to edit text without opening a dialog.'
|
|
91
|
+
reason: 'Primary use case — inline rename/edit.'
|
|
92
|
+
- rule: 'inline-edit-ui IS form-participating (extends UIFormElement). Pair with a hidden <form> + name="..." to submit edits as a field.'
|
|
93
|
+
reason: 'Form participation contract.'
|
|
94
|
+
- rule: 'Distinct from <input-ui> (always shows input chrome) and from <field-ui> (stacked label + input composition). inline-edit reads as text in the static state.'
|
|
95
|
+
reason: 'Sibling-component boundary.'
|
|
96
|
+
- rule: 'Listen to `change` event (detail.value, detail.oldValue) for commit; `cancel` for Escape. Default commit=blur saves on focusout + Enter.'
|
|
97
|
+
reason: 'Event-handling contract.'
|
|
98
|
+
anti_patterns:
|
|
99
|
+
- wrong: '<input-ui value="Title" name="title" variant="ghost">'
|
|
100
|
+
why: 'Ghost variant still renders input chrome (border on focus). Looks like a control, not text.'
|
|
101
|
+
fix: '<inline-edit-ui value="Title" name="title"> — reads as text in static state; chrome only appears while editing.'
|
|
102
|
+
- wrong: '<text-ui contenteditable>Title</text-ui>'
|
|
103
|
+
why: 'No state machine — no commit/cancel semantics, no form participation, no a11y wiring.'
|
|
104
|
+
fix: '<inline-edit-ui value="Title">'
|
|
105
|
+
examples:
|
|
106
|
+
- name: editable-title
|
|
107
|
+
description: A draft document title that becomes editable on click.
|
|
108
|
+
a2ui: |
|
|
109
|
+
[{ "id": "title", "component": "InlineEdit", "value": "Untitled draft" }]
|
|
110
|
+
keywords:
|
|
111
|
+
- inline-edit
|
|
112
|
+
- editable
|
|
113
|
+
- click-to-edit
|
|
114
|
+
- rename
|
|
115
|
+
- edit-in-place
|
|
116
|
+
synonyms:
|
|
117
|
+
inline-edit:
|
|
118
|
+
- editable
|
|
119
|
+
- click-to-edit
|
|
120
|
+
- edit-in-place
|
|
121
|
+
- rename-in-place
|
|
122
|
+
related:
|
|
123
|
+
- input
|
|
124
|
+
- field
|
|
125
|
+
- text
|
|
@@ -229,6 +229,15 @@ export class UIIntegrationCard extends UIElement {
|
|
|
229
229
|
if (!this.#logoEl) return;
|
|
230
230
|
const logo = (this.logo || '').trim();
|
|
231
231
|
|
|
232
|
+
// Guard against unresolved AdiaUI template binding descriptors
|
|
233
|
+
// ({{p:N}} placeholders). The integration-card's connected() + render()
|
|
234
|
+
// fires synchronously when the parent template stamps the element, BEFORE
|
|
235
|
+
// the parent's reconciliation pass replaces {{p:N}} with the real value.
|
|
236
|
+
// Skipping here lets the second render (triggered by the reconciliation's
|
|
237
|
+
// property set) render the real logo. Without this guard, icon-ui fires
|
|
238
|
+
// a "not found" warn for every {{p:4}} it receives on first connect.
|
|
239
|
+
if (logo.startsWith('{{p:')) return;
|
|
240
|
+
|
|
232
241
|
// No logo → strip any prior content and hide.
|
|
233
242
|
if (!logo) {
|
|
234
243
|
this.#logoEl.replaceChildren();
|
|
@@ -275,9 +275,10 @@ describe('integration-card-ui — CSS contract (source-grep)', () => {
|
|
|
275
275
|
|
|
276
276
|
expect(CSS).toMatch(/@scope\s*\(\s*integration-card-ui\s*\)/);
|
|
277
277
|
expect(CSS).toMatch(/:where\(:scope\)\s*\{/);
|
|
278
|
-
|
|
279
|
-
expect(CSS).toMatch(/--integration-card-
|
|
280
|
-
expect(CSS).toMatch(/--integration-card-
|
|
278
|
+
// OD-5 sweep: token declarations use -default suffix per component-token-contract.md
|
|
279
|
+
expect(CSS).toMatch(/--integration-card-bg-default:/);
|
|
280
|
+
expect(CSS).toMatch(/--integration-card-border-default:/);
|
|
281
|
+
expect(CSS).toMatch(/--integration-card-radius-default:/);
|
|
281
282
|
});
|
|
282
283
|
|
|
283
284
|
it('keeps status-driven border tint (status="connected") inside @scope', async () => {
|
|
@@ -53,7 +53,14 @@
|
|
|
53
53
|
"MenuItem",
|
|
54
54
|
"TreeItem"
|
|
55
55
|
],
|
|
56
|
-
"slots": {
|
|
56
|
+
"slots": {
|
|
57
|
+
"description": {
|
|
58
|
+
"description": "Override slot for richer description markup than the plain [description] attribute string (inline links, code spans, multiple lines). Renders beneath the primary text at body-subtle typography."
|
|
59
|
+
},
|
|
60
|
+
"icon": {
|
|
61
|
+
"description": "Override the [icon] glyph with a custom slotted element (e.g. a colored <icon-ui>, an image, or an avatar-ui). Mutually exclusive with the [icon] attribute — slot child wins if both are present."
|
|
62
|
+
}
|
|
63
|
+
},
|
|
57
64
|
"states": [],
|
|
58
65
|
"status": "stable",
|
|
59
66
|
"synonyms": {
|
|
@@ -29,6 +29,18 @@ props:
|
|
|
29
29
|
description: Secondary line below the primary text. Subtle color.
|
|
30
30
|
type: string
|
|
31
31
|
|
|
32
|
+
slots:
|
|
33
|
+
icon:
|
|
34
|
+
description: >-
|
|
35
|
+
Override the [icon] glyph with a custom slotted element (e.g. a colored
|
|
36
|
+
<icon-ui>, an image, or an avatar-ui). Mutually exclusive with the
|
|
37
|
+
[icon] attribute — slot child wins if both are present.
|
|
38
|
+
description:
|
|
39
|
+
description: >-
|
|
40
|
+
Override slot for richer description markup than the plain [description]
|
|
41
|
+
attribute string (inline links, code spans, multiple lines). Renders
|
|
42
|
+
beneath the primary text at body-subtle typography.
|
|
43
|
+
|
|
32
44
|
keywords:
|
|
33
45
|
- list-item
|
|
34
46
|
- list-row
|