@adia-ai/web-components 0.0.5 → 0.0.7
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/card/card.css +9 -0
- package/components/chat/chat-input.css +9 -3
- package/components/chat/chat-input.js +9 -2
- package/components/drawer/drawer.a2ui.json +4 -1
- package/components/drawer/drawer.css +9 -0
- package/components/drawer/drawer.yaml +9 -0
- package/components/modal/modal.js +5 -2
- package/core/icons.js +5 -2
- package/package.json +1 -1
- package/patterns/a2ui-root/a2ui-root.a2ui.json +7 -0
- package/patterns/a2ui-root/a2ui-root.js +27 -0
- package/patterns/a2ui-root/a2ui-root.yaml +11 -0
- package/patterns/adia-editor/adia-editor.a2ui.json +3 -0
- package/patterns/adia-editor/adia-editor.yaml +6 -0
- package/patterns/adia-editor/css/adia-editor.layout.css +18 -0
- package/patterns/app-shell/app-shell.a2ui.json +3 -0
- package/patterns/app-shell/app-shell.yaml +6 -0
- package/patterns/app-shell/css/app-shell.main.css +16 -0
- package/patterns/app-shell/css/app-shell.sidebar.css +9 -0
package/components/card/card.css
CHANGED
|
@@ -335,6 +335,15 @@
|
|
|
335
335
|
margin-inline-start: 0;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/* Dual-cluster footer: leading action (e.g. Delete) on the inline-start
|
|
339
|
+
edge, trailing action cluster on the inline-end. margin-inline-end:
|
|
340
|
+
auto fills the gap between the two groups. */
|
|
341
|
+
> footer > [slot="action-leading"] {
|
|
342
|
+
margin-inline-end: auto;
|
|
343
|
+
display: flex;
|
|
344
|
+
gap: var(--card-footer-gap);
|
|
345
|
+
}
|
|
346
|
+
|
|
338
347
|
/* ═══════ Images ═══════ */
|
|
339
348
|
|
|
340
349
|
> img,
|
|
@@ -49,14 +49,20 @@
|
|
|
49
49
|
border-color: var(--chat-input-border-focus);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/* Textarea: no border/bg of its own — container handles it
|
|
53
|
-
|
|
52
|
+
/* Textarea: no border/bg of its own — container handles it. The
|
|
53
|
+
second selector keeps the transparent bg even when the host
|
|
54
|
+
textarea-ui carries [disabled] (streaming / submit lock) —
|
|
55
|
+
otherwise textarea.css's `:scope[disabled] [slot="text"]` rule
|
|
56
|
+
(specificity 0,3,0) paints --a-ui-bg-disabled over the container
|
|
57
|
+
bg. The `:scope` prefix boosts specificity to 0,3,1 so our rule
|
|
58
|
+
wins. */
|
|
59
|
+
textarea-ui [slot="text"],
|
|
60
|
+
:scope textarea-ui[disabled] [slot="text"] {
|
|
54
61
|
border: none;
|
|
55
62
|
background: transparent;
|
|
56
63
|
border-radius: 0;
|
|
57
64
|
box-shadow: none;
|
|
58
65
|
max-height: 8rem;
|
|
59
|
-
/*min-height: var(--a-size-md);*/
|
|
60
66
|
padding: var(--chat-input-textarea-pt) var(--chat-input-textarea-px) 0;
|
|
61
67
|
}
|
|
62
68
|
|
|
@@ -23,7 +23,10 @@ import { AdiaElement } from '../../core/element.js';
|
|
|
23
23
|
* models — JSON array of model options: [{value, label}] or [{label, options: [...]}]
|
|
24
24
|
* model — currently selected model value (reflected, two-way with select)
|
|
25
25
|
* placeholder — textarea placeholder
|
|
26
|
-
* disabled — disable entire input
|
|
26
|
+
* disabled — disable entire input (textarea becomes contenteditable=false)
|
|
27
|
+
* busy — in-flight / streaming state: send button disabled, submit
|
|
28
|
+
* events suppressed, but textarea stays editable so the user
|
|
29
|
+
* can draft a follow-up while the model is still responding.
|
|
27
30
|
*
|
|
28
31
|
* Events:
|
|
29
32
|
* submit — user pressed Enter or clicked send (detail: { text, model })
|
|
@@ -37,6 +40,7 @@ import { AdiaElement } from '../../core/element.js';
|
|
|
37
40
|
class AdiaChatInput extends AdiaElement {
|
|
38
41
|
static properties = {
|
|
39
42
|
disabled: { type: Boolean, default: false, reflect: true },
|
|
43
|
+
busy: { type: Boolean, default: false, reflect: true },
|
|
40
44
|
placeholder: { type: String, default: 'Type a message...', reflect: true },
|
|
41
45
|
model: { type: String, default: '', reflect: true },
|
|
42
46
|
};
|
|
@@ -138,6 +142,9 @@ class AdiaChatInput extends AdiaElement {
|
|
|
138
142
|
this.#textareaEl.disabled = this.disabled;
|
|
139
143
|
this.#textareaEl.placeholder = this.placeholder;
|
|
140
144
|
}
|
|
145
|
+
if (this.#sendEl) {
|
|
146
|
+
this.#sendEl.disabled = this.disabled || this.busy;
|
|
147
|
+
}
|
|
141
148
|
// Sync model value to select (handles late upgrades)
|
|
142
149
|
if (this.#modelEl && this.model && this.#modelEl.value !== this.model) {
|
|
143
150
|
this.#modelEl.value = this.model;
|
|
@@ -154,7 +161,7 @@ class AdiaChatInput extends AdiaElement {
|
|
|
154
161
|
};
|
|
155
162
|
|
|
156
163
|
#onSubmit = () => {
|
|
157
|
-
if (this.disabled) return;
|
|
164
|
+
if (this.disabled || this.busy) return;
|
|
158
165
|
const text = this.value;
|
|
159
166
|
if (!text && !this.#attachments.length) return;
|
|
160
167
|
this.dispatchEvent(new CustomEvent('submit', {
|
|
@@ -97,7 +97,10 @@
|
|
|
97
97
|
"description": "Direct child of <header> — grid row 2, spans the heading + action columns. Also accepts bare <p> / <small> tags."
|
|
98
98
|
},
|
|
99
99
|
"action": {
|
|
100
|
-
"description": "Direct child of <header> — placed in the grid's last column alongside the stamped close button. Flex container for badge + button combinations."
|
|
100
|
+
"description": "Direct child of <header> — placed in the grid's last column alongside the stamped close button. Flex container for badge + button combinations. In <footer>, self-aligns to the trailing (inline-end) edge; pair with `action-leading` for dual-cluster footers."
|
|
101
|
+
},
|
|
102
|
+
"action-leading": {
|
|
103
|
+
"description": "Direct child of <footer> — leading (inline-start) action cluster. Used for dual-cluster footers where a destructive or secondary action sits on the opposite edge from the primary trailing cluster (e.g. Delete ↔ Cancel/Save, Back ↔ Cancel/Next). Replaces the legacy <span data-spacer> hack."
|
|
101
104
|
},
|
|
102
105
|
"backdrop": {
|
|
103
106
|
"description": "Scrim overlay behind the drawer (stamped by the component)."
|
|
@@ -339,4 +339,13 @@
|
|
|
339
339
|
[slot="panel"] > [slot="footer"] > [slot="action"] ~ [slot="action"] {
|
|
340
340
|
margin-inline-start: 0;
|
|
341
341
|
}
|
|
342
|
+
|
|
343
|
+
/* Dual-cluster footer: leading action (e.g. Delete) on the inline-start
|
|
344
|
+
edge, trailing action cluster on the inline-end. The margin-inline-end:
|
|
345
|
+
auto on the leading slot fills the gap between the two groups. */
|
|
346
|
+
[slot="panel"] > [slot="footer"] > [slot="action-leading"] {
|
|
347
|
+
margin-inline-end: auto;
|
|
348
|
+
display: flex;
|
|
349
|
+
gap: var(--drawer-footer-gap);
|
|
350
|
+
}
|
|
342
351
|
}
|
|
@@ -97,6 +97,15 @@ slots:
|
|
|
97
97
|
description: >-
|
|
98
98
|
Direct child of <header> — placed in the grid's last column alongside the
|
|
99
99
|
stamped close button. Flex container for badge + button combinations.
|
|
100
|
+
In <footer>, self-aligns to the trailing (inline-end) edge; pair with
|
|
101
|
+
`action-leading` for dual-cluster footers.
|
|
102
|
+
action-leading:
|
|
103
|
+
description: >-
|
|
104
|
+
Direct child of <footer> — leading (inline-start) action cluster. Used
|
|
105
|
+
for dual-cluster footers where a destructive or secondary action sits on
|
|
106
|
+
the opposite edge from the primary trailing cluster (e.g. Delete ↔
|
|
107
|
+
Cancel/Save, Back ↔ Cancel/Next). Replaces the legacy <span data-spacer>
|
|
108
|
+
hack.
|
|
100
109
|
states:
|
|
101
110
|
- name: idle
|
|
102
111
|
description: Default, ready for interaction.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* close — fired after the modal finishes closing
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { AdiaElement
|
|
32
|
+
import { AdiaElement } from '../../core/element.js';
|
|
33
33
|
|
|
34
34
|
class AdiaModal extends AdiaElement {
|
|
35
35
|
#bound = false;
|
|
@@ -55,7 +55,10 @@ class AdiaModal extends AdiaElement {
|
|
|
55
55
|
footer: '<footer slot="footer"></footer>',
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
// No template — modal composes from authored light-DOM children. An empty
|
|
59
|
+
// html`` result would trigger stamp() → replaceChildren(), wiping authored
|
|
60
|
+
// [slot=body|footer] before render() can migrate them into the panel.
|
|
61
|
+
// (Same rationale as drawer.js — parallel pattern.)
|
|
59
62
|
|
|
60
63
|
#onPress = (e) => {
|
|
61
64
|
if (e.target.closest('[slot="close"]')) this.open = false;
|
package/core/icons.js
CHANGED
|
@@ -53,14 +53,17 @@ export function listIcons() {
|
|
|
53
53
|
|
|
54
54
|
// Vite resolves each glob at build time into a map of path → module.
|
|
55
55
|
// One glob per weight so the bundler can tree-shake what isn't referenced.
|
|
56
|
-
|
|
56
|
+
// In non-Vite environments (e.g. plain static serving) `import.meta.glob`
|
|
57
|
+
// is undefined — fall back to empty maps so consumers using registerIcon()
|
|
58
|
+
// still work.
|
|
59
|
+
const weightModules = typeof import.meta.glob === 'function' ? {
|
|
57
60
|
regular: import.meta.glob('/node_modules/@phosphor-icons/core/assets/regular/*.svg', { query: '?raw', import: 'default' }),
|
|
58
61
|
thin: import.meta.glob('/node_modules/@phosphor-icons/core/assets/thin/*.svg', { query: '?raw', import: 'default' }),
|
|
59
62
|
light: import.meta.glob('/node_modules/@phosphor-icons/core/assets/light/*.svg', { query: '?raw', import: 'default' }),
|
|
60
63
|
bold: import.meta.glob('/node_modules/@phosphor-icons/core/assets/bold/*.svg', { query: '?raw', import: 'default' }),
|
|
61
64
|
fill: import.meta.glob('/node_modules/@phosphor-icons/core/assets/fill/*.svg', { query: '?raw', import: 'default' }),
|
|
62
65
|
duotone: import.meta.glob('/node_modules/@phosphor-icons/core/assets/duotone/*.svg', { query: '?raw', import: 'default' }),
|
|
63
|
-
};
|
|
66
|
+
} : { regular: {}, thin: {}, light: {}, bold: {}, fill: {}, duotone: {} };
|
|
64
67
|
|
|
65
68
|
/**
|
|
66
69
|
* Phosphor filename convention: `star.svg` for regular, `star-fill.svg` for
|
package/package.json
CHANGED
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"component": {
|
|
27
27
|
"const": "A2UIRoot"
|
|
28
28
|
},
|
|
29
|
+
"doc": {
|
|
30
|
+
"description": "Author-driven mode — set to an array of A2UI messages and the renderer resets + replays them. No network/transport involvement. Setting to a new array triggers a full re-render. Use this for editors, previews, tests, and any static-doc authoring loop. When both `src` and `doc` are set, `doc` wins (the stream is not opened). Pass as a JS property; not reflected to an attribute.",
|
|
31
|
+
"type": "array"
|
|
32
|
+
},
|
|
29
33
|
"loading": {
|
|
30
34
|
"description": "True while the stream is connecting.",
|
|
31
35
|
"type": "boolean",
|
|
@@ -71,6 +75,9 @@
|
|
|
71
75
|
},
|
|
72
76
|
"a2ui-message": {
|
|
73
77
|
"description": "Fired for each A2UI message received. detail: { message }"
|
|
78
|
+
},
|
|
79
|
+
"doc-replaced": {
|
|
80
|
+
"description": "Fired after a full doc reset + replay in author-driven mode. detail: { count }"
|
|
74
81
|
}
|
|
75
82
|
},
|
|
76
83
|
"examples": [],
|
|
@@ -5,12 +5,24 @@
|
|
|
5
5
|
* <a2ui-root src="/api/agent" transport="sse"></a2ui-root>
|
|
6
6
|
* <a2ui-root src="ws://localhost:8080" transport="ws"></a2ui-root>
|
|
7
7
|
*
|
|
8
|
+
* Static / author-driven mode — set the `doc` property (array of A2UI messages)
|
|
9
|
+
* and the renderer resets + replays them. Editors and previews can drive the
|
|
10
|
+
* surface without opening a transport. Setting `doc` to a new array re-renders
|
|
11
|
+
* from scratch (reset() + processAll()).
|
|
12
|
+
*
|
|
13
|
+
* const root = document.querySelector('a2ui-root');
|
|
14
|
+
* root.doc = [
|
|
15
|
+
* { type: 'createSurface', surfaceId: 'root', root: 'c-1' },
|
|
16
|
+
* { type: 'updateComponents', components: [{ id: 'c-1', component: 'Heading', text: 'Hi' }] },
|
|
17
|
+
* ];
|
|
18
|
+
*
|
|
8
19
|
* Events:
|
|
9
20
|
* a2ui-connected — stream connected
|
|
10
21
|
* a2ui-message — each message received (detail: { message })
|
|
11
22
|
* a2ui-error — stream error (detail: { error })
|
|
12
23
|
* a2ui-closed — stream ended
|
|
13
24
|
* a2ui-action — user interaction (detail: { name, sourceComponentId, context })
|
|
25
|
+
* doc-replaced — fired after a full doc reset + replay (author-driven mode)
|
|
14
26
|
*/
|
|
15
27
|
|
|
16
28
|
import { AdiaElement } from '../../core/element.js';
|
|
@@ -36,6 +48,7 @@ class AdiaA2UIRoot extends AdiaElement {
|
|
|
36
48
|
|
|
37
49
|
#renderer = null;
|
|
38
50
|
#abortCtrl = null;
|
|
51
|
+
#doc = null;
|
|
39
52
|
|
|
40
53
|
connected() {
|
|
41
54
|
this.#renderer = new A2UIRenderer(this, registry, { batch: this.batch });
|
|
@@ -136,6 +149,20 @@ class AdiaA2UIRoot extends AdiaElement {
|
|
|
136
149
|
for (const msg of messages) this.process(msg);
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
get doc() { return this.#doc; }
|
|
153
|
+
set doc(messages) {
|
|
154
|
+
this.#doc = Array.isArray(messages) ? messages : [];
|
|
155
|
+
if (!this.#renderer) {
|
|
156
|
+
this.#renderer = new A2UIRenderer(this, registry);
|
|
157
|
+
}
|
|
158
|
+
this.#renderer.reset();
|
|
159
|
+
for (const msg of this.#doc) this.#renderer.process(msg);
|
|
160
|
+
this.dispatchEvent(new CustomEvent('doc-replaced', {
|
|
161
|
+
bubbles: true,
|
|
162
|
+
detail: { count: this.#doc.length },
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
|
|
139
166
|
reset() {
|
|
140
167
|
this.#renderer?.reset();
|
|
141
168
|
}
|
|
@@ -35,6 +35,15 @@ props:
|
|
|
35
35
|
description: Batch renderer updates via requestAnimationFrame for large fan-in.
|
|
36
36
|
type: boolean
|
|
37
37
|
default: false
|
|
38
|
+
doc:
|
|
39
|
+
description: >-
|
|
40
|
+
Author-driven mode — set to an array of A2UI messages and the renderer
|
|
41
|
+
resets + replays them. No network/transport involvement. Setting to a
|
|
42
|
+
new array triggers a full re-render. Use this for editors, previews,
|
|
43
|
+
tests, and any static-doc authoring loop. When both `src` and `doc` are
|
|
44
|
+
set, `doc` wins (the stream is not opened). Pass as a JS property; not
|
|
45
|
+
reflected to an attribute.
|
|
46
|
+
type: array
|
|
38
47
|
events:
|
|
39
48
|
a2ui-connected:
|
|
40
49
|
description: Fired when the stream is established.
|
|
@@ -46,6 +55,8 @@ events:
|
|
|
46
55
|
description: "Fired when the stream errors. detail: { error }"
|
|
47
56
|
a2ui-closed:
|
|
48
57
|
description: Fired when the stream ends.
|
|
58
|
+
doc-replaced:
|
|
59
|
+
description: "Fired after a full doc reset + replay in author-driven mode. detail: { count }"
|
|
49
60
|
slots:
|
|
50
61
|
default:
|
|
51
62
|
description: The rendered surface. Children are stamped by the A2UI renderer.
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
"action": {
|
|
49
49
|
"description": "Trailing control cluster inside <header> or <footer>. The first [slot=\"action\"] child pushes itself (and siblings) to the end of the bar; subsequent [slot=\"action\"] siblings flow with gap."
|
|
50
50
|
},
|
|
51
|
+
"action-leading": {
|
|
52
|
+
"description": "Leading (inline-start) control cluster inside <header> or <footer>. Pairs with [slot=\"action\"] to produce a dual-cluster bar with space-between alignment (e.g. Back ↔ Cancel/Next, Discard ↔ Publish). Replaces the legacy <span data-spacer> hack."
|
|
53
|
+
},
|
|
51
54
|
"heading": {
|
|
52
55
|
"description": "Primary label inside <header> or <footer>. Rendered with --editor-title-weight + the strong foreground token."
|
|
53
56
|
},
|
|
@@ -37,6 +37,12 @@ slots:
|
|
|
37
37
|
Trailing control cluster inside <header> or <footer>. The first
|
|
38
38
|
[slot="action"] child pushes itself (and siblings) to the end of
|
|
39
39
|
the bar; subsequent [slot="action"] siblings flow with gap.
|
|
40
|
+
action-leading:
|
|
41
|
+
description: >-
|
|
42
|
+
Leading (inline-start) control cluster inside <header> or <footer>.
|
|
43
|
+
Pairs with [slot="action"] to produce a dual-cluster bar with
|
|
44
|
+
space-between alignment (e.g. Back ↔ Cancel/Next, Discard ↔
|
|
45
|
+
Publish). Replaces the legacy <span data-spacer> hack.
|
|
40
46
|
|
|
41
47
|
states:
|
|
42
48
|
- name: idle
|
|
@@ -72,6 +72,15 @@ adia-editor-ui > header > [slot="action"] ~ [slot="action"] {
|
|
|
72
72
|
margin-inline-start: 0;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/* Dual-cluster: leading group on inline-start, trailing cluster on inline-end. */
|
|
76
|
+
adia-editor-ui > header > [slot="action-leading"] {
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: var(--editor-bar-gap);
|
|
80
|
+
flex-shrink: 0;
|
|
81
|
+
margin-inline-end: auto;
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
/* ── Body: pane | canvas | pane ── */
|
|
76
85
|
adia-editor-ui > [data-editor-body] {
|
|
77
86
|
display: flex;
|
|
@@ -151,3 +160,12 @@ adia-editor-ui > footer > [slot="action"] {
|
|
|
151
160
|
adia-editor-ui > footer > [slot="action"] ~ [slot="action"] {
|
|
152
161
|
margin-inline-start: 0;
|
|
153
162
|
}
|
|
163
|
+
|
|
164
|
+
/* Dual-cluster: leading group on inline-start, trailing cluster on inline-end. */
|
|
165
|
+
adia-editor-ui > footer > [slot="action-leading"] {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: var(--editor-bar-gap);
|
|
169
|
+
flex-shrink: 0;
|
|
170
|
+
margin-inline-end: auto;
|
|
171
|
+
}
|
|
@@ -83,6 +83,9 @@
|
|
|
83
83
|
"action": {
|
|
84
84
|
"description": "Trailing control cluster inside any chrome bar. The first [slot=\"action\"] child pushes itself (and siblings) to the end; subsequent siblings flow with gap. Coexists with legacy <span data-spacer> / <div data-actions> hooks for one release — new code should prefer slots."
|
|
85
85
|
},
|
|
86
|
+
"action-leading": {
|
|
87
|
+
"description": "Leading (inline-start) control cluster inside any chrome bar. Pairs with [slot=\"action\"] for dual-cluster chrome (e.g. back button + breadcrumb on the left, primary actions on the right). Replaces the legacy <span data-spacer> hack."
|
|
88
|
+
},
|
|
86
89
|
"heading": {
|
|
87
90
|
"description": "Primary label inside any chrome bar. Medium-weight + strong fg."
|
|
88
91
|
},
|
|
@@ -56,6 +56,12 @@ slots:
|
|
|
56
56
|
subsequent siblings flow with gap. Coexists with legacy
|
|
57
57
|
<span data-spacer> / <div data-actions> hooks for one release —
|
|
58
58
|
new code should prefer slots.
|
|
59
|
+
action-leading:
|
|
60
|
+
description: >-
|
|
61
|
+
Leading (inline-start) control cluster inside any chrome bar.
|
|
62
|
+
Pairs with [slot="action"] for dual-cluster chrome (e.g. back
|
|
63
|
+
button + breadcrumb on the left, primary actions on the right).
|
|
64
|
+
Replaces the legacy <span data-spacer> hack.
|
|
59
65
|
|
|
60
66
|
states:
|
|
61
67
|
- name: idle
|
|
@@ -61,6 +61,14 @@ app-shell-ui > main > header > [slot="action"] {
|
|
|
61
61
|
app-shell-ui > main > header > [slot="action"] ~ [slot="action"] {
|
|
62
62
|
margin-inline-start: 0;
|
|
63
63
|
}
|
|
64
|
+
/* Dual-cluster: leading group on inline-start, trailing cluster on inline-end. */
|
|
65
|
+
app-shell-ui > main > header > [slot="action-leading"] {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: var(--page-actions-gap);
|
|
69
|
+
flex-shrink: 0;
|
|
70
|
+
margin-inline-end: auto;
|
|
71
|
+
}
|
|
64
72
|
|
|
65
73
|
/* ── Main > section (scroll container) ──
|
|
66
74
|
Wraps [data-content-root]. Scrolls vertically, hides scrollbar. */
|
|
@@ -125,3 +133,11 @@ app-shell-ui > main > footer > [slot="action"] {
|
|
|
125
133
|
app-shell-ui > main > footer > [slot="action"] ~ [slot="action"] {
|
|
126
134
|
margin-inline-start: 0;
|
|
127
135
|
}
|
|
136
|
+
/* Dual-cluster: leading group on inline-start, trailing cluster on inline-end. */
|
|
137
|
+
app-shell-ui > main > footer > [slot="action-leading"] {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: var(--page-actions-gap);
|
|
141
|
+
flex-shrink: 0;
|
|
142
|
+
margin-inline-end: auto;
|
|
143
|
+
}
|
|
@@ -118,6 +118,15 @@
|
|
|
118
118
|
[data-sidebar] > footer > [slot="action"] ~ [slot="action"] {
|
|
119
119
|
margin-inline-start: 0;
|
|
120
120
|
}
|
|
121
|
+
/* Dual-cluster: leading group on inline-start, trailing cluster on inline-end. */
|
|
122
|
+
[data-sidebar] > header > [slot="action-leading"],
|
|
123
|
+
[data-sidebar] > footer > [slot="action-leading"] {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: var(--page-actions-gap);
|
|
127
|
+
flex-shrink: 0;
|
|
128
|
+
margin-inline-end: auto;
|
|
129
|
+
}
|
|
121
130
|
|
|
122
131
|
/* ── Sidebar section (scrollable body) ── */
|
|
123
132
|
[data-sidebar] > section {
|