@adia-ai/web-modules 0.3.5 → 0.3.6
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 +26 -0
- package/editor/editor-canvas/editor-canvas.a2ui.json +87 -0
- package/editor/editor-canvas/editor-canvas.js +103 -0
- package/editor/editor-canvas/editor-canvas.test.js +100 -0
- package/editor/editor-canvas/editor-canvas.yaml +88 -0
- package/editor/editor-canvas-empty/editor-canvas-empty.a2ui.json +69 -0
- package/editor/editor-canvas-empty/editor-canvas-empty.yaml +56 -0
- package/editor/editor-shell/css/editor-shell.bespoke.css +172 -0
- package/editor/editor-shell/editor-shell.css +1 -0
- package/editor/editor-shell/editor-shell.js +85 -30
- package/editor/editor-sidebar/editor-sidebar.a2ui.json +88 -0
- package/editor/editor-sidebar/editor-sidebar.js +173 -0
- package/editor/editor-sidebar/editor-sidebar.test.js +126 -0
- package/editor/editor-sidebar/editor-sidebar.yaml +83 -0
- package/editor/editor-statusbar/editor-statusbar.a2ui.json +76 -0
- package/editor/editor-statusbar/editor-statusbar.yaml +57 -0
- package/editor/editor-toolbar/editor-toolbar.a2ui.json +96 -0
- package/editor/editor-toolbar/editor-toolbar.js +58 -0
- package/editor/editor-toolbar/editor-toolbar.test.js +99 -0
- package/editor/editor-toolbar/editor-toolbar.yaml +81 -0
- package/editor/index.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import '../../../web-components/core/element.js';
|
|
3
|
+
import './editor-sidebar.js';
|
|
4
|
+
|
|
5
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
+
|
|
7
|
+
function mount(html) {
|
|
8
|
+
const wrap = document.createElement('div');
|
|
9
|
+
wrap.innerHTML = html;
|
|
10
|
+
document.body.appendChild(wrap);
|
|
11
|
+
return wrap.firstElementChild;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let originalRect;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
document.body.innerHTML = '';
|
|
17
|
+
try { localStorage.clear(); } catch {}
|
|
18
|
+
globalThis.ResizeObserver = class {
|
|
19
|
+
observe() {} unobserve() {} disconnect() {}
|
|
20
|
+
};
|
|
21
|
+
// happy-dom: stub getBoundingClientRect to derive from style.width
|
|
22
|
+
originalRect = HTMLElement.prototype.getBoundingClientRect;
|
|
23
|
+
HTMLElement.prototype.getBoundingClientRect = function () {
|
|
24
|
+
const inline = this.style?.width || '';
|
|
25
|
+
const w = parseFloat(inline) || 240;
|
|
26
|
+
return { width: w, height: 600, top: 0, left: 0, right: w, bottom: 600, x: 0, y: 0 };
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (originalRect) HTMLElement.prototype.getBoundingClientRect = originalRect;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('editor-sidebar', () => {
|
|
35
|
+
it('registers editor-sidebar as a custom element', () => {
|
|
36
|
+
expect(customElements.get('editor-sidebar')).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('defaults to collapsed=false on connect', () => {
|
|
40
|
+
const sb = mount('<editor-sidebar slot="leading"><pane-ui></pane-ui></editor-sidebar>');
|
|
41
|
+
expect(sb.collapsed).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('reflects [collapsed] via property assignment', async () => {
|
|
45
|
+
const sb = mount('<editor-sidebar slot="leading"><pane-ui></pane-ui></editor-sidebar>');
|
|
46
|
+
sb.collapsed = true;
|
|
47
|
+
await tick();
|
|
48
|
+
expect(sb.hasAttribute('collapsed')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('exposes .toggle() / .collapse() / .expand() public methods', () => {
|
|
52
|
+
const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
|
|
53
|
+
expect(typeof sb.toggle).toBe('function');
|
|
54
|
+
expect(typeof sb.collapse).toBe('function');
|
|
55
|
+
expect(typeof sb.expand).toBe('function');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('persists pane width to editor-namespaced localStorage key (cluster-distinct)', () => {
|
|
59
|
+
const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
|
|
60
|
+
const pane = sb.querySelector('pane-ui');
|
|
61
|
+
pane.style.width = '240px';
|
|
62
|
+
sb.collapse();
|
|
63
|
+
expect(sb.collapsed).toBe(true);
|
|
64
|
+
// editor-namespaced key
|
|
65
|
+
expect(localStorage.getItem('adia-editor-sidebar-leading')).not.toBeNull();
|
|
66
|
+
// Should NOT collide with admin or chat namespaces
|
|
67
|
+
expect(localStorage.getItem('adia-sidebar-leading')).toBeNull();
|
|
68
|
+
expect(localStorage.getItem('adia-chat-sidebar-leading')).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('uses [name] override for the localStorage key', () => {
|
|
72
|
+
const sb = mount('<editor-sidebar slot="leading" name="navigator" collapsible><pane-ui></pane-ui></editor-sidebar>');
|
|
73
|
+
const pane = sb.querySelector('pane-ui');
|
|
74
|
+
pane.style.width = '200px';
|
|
75
|
+
sb.collapse();
|
|
76
|
+
expect(localStorage.getItem('adia-editor-sidebar-navigator')).not.toBeNull();
|
|
77
|
+
expect(localStorage.getItem('adia-editor-sidebar-leading')).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('toggle() returns the new collapsed value', () => {
|
|
81
|
+
const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
|
|
82
|
+
const pane = sb.querySelector('pane-ui');
|
|
83
|
+
pane.style.width = '200px';
|
|
84
|
+
const result = sb.toggle();
|
|
85
|
+
expect(result).toBe(sb.collapsed);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('dispatches sidebar-toggle event on toggle()', () => {
|
|
89
|
+
const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
|
|
90
|
+
const pane = sb.querySelector('pane-ui');
|
|
91
|
+
pane.style.width = '200px';
|
|
92
|
+
const onToggle = vi.fn();
|
|
93
|
+
sb.addEventListener('sidebar-toggle', onToggle);
|
|
94
|
+
sb.toggle();
|
|
95
|
+
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(onToggle.mock.calls[0][0].detail).toEqual(
|
|
97
|
+
expect.objectContaining({ name: expect.any(String), expanded: expect.any(Boolean) })
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('expand() restores from stored width or defaults to 240', () => {
|
|
102
|
+
// Pre-populate stored width
|
|
103
|
+
localStorage.setItem('adia-editor-sidebar-leading', '320');
|
|
104
|
+
const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
|
|
105
|
+
sb.collapse();
|
|
106
|
+
sb.expand();
|
|
107
|
+
const pane = sb.querySelector('pane-ui');
|
|
108
|
+
expect(pane.style.width).toBe('320px');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('cleanup on disconnect — removes pointerup listener', () => {
|
|
112
|
+
const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
|
|
113
|
+
const removeSpy = vi.spyOn(document, 'removeEventListener');
|
|
114
|
+
sb.remove();
|
|
115
|
+
const removedTypes = removeSpy.mock.calls.map((args) => args[0]);
|
|
116
|
+
expect(removedTypes).toContain('pointerup');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('handles missing inner <pane-ui> gracefully (no crash)', () => {
|
|
120
|
+
const sb = mount('<editor-sidebar slot="leading"></editor-sidebar>');
|
|
121
|
+
// Should not throw
|
|
122
|
+
expect(() => sb.toggle()).not.toThrow();
|
|
123
|
+
expect(() => sb.collapse()).not.toThrow();
|
|
124
|
+
expect(() => sb.expand()).not.toThrow();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: EditorSidebar
|
|
4
|
+
tag: editor-sidebar
|
|
5
|
+
component: EditorSidebar
|
|
6
|
+
category: layout
|
|
7
|
+
version: 1
|
|
8
|
+
description: |
|
|
9
|
+
Module-tier editor-cluster sidebar — wraps <pane-ui resizable>
|
|
10
|
+
(the primitive that owns drag) and adds [collapsed] reflected
|
|
11
|
+
state, localStorage persistence (adia-editor-sidebar-{name}),
|
|
12
|
+
and .toggle() / .collapse() / .expand() public API.
|
|
13
|
+
|
|
14
|
+
Sits inside <editor-shell> as slot="leading" (navigator pane) or
|
|
15
|
+
slot="trailing" (inspector pane). Mirrors the cluster-namespace
|
|
16
|
+
shape from <admin-sidebar> and <chat-sidebar>, but DELEGATES to
|
|
17
|
+
<pane-ui> for resize rather than reimplementing it (per
|
|
18
|
+
bespoke-shell-children skill §When NOT to promote — pane primitive
|
|
19
|
+
already owns this concern).
|
|
20
|
+
|
|
21
|
+
This is the FIRST bespoke shell child that delegates rather than
|
|
22
|
+
duplicates a primitive's behavior. Pattern is: cluster-namespace +
|
|
23
|
+
state-as-attribute + persistence at the bespoke tier; physical
|
|
24
|
+
drag at the primitive tier.
|
|
25
|
+
|
|
26
|
+
props:
|
|
27
|
+
collapsed:
|
|
28
|
+
description: Reflected — set when inner <pane-ui> width is at or below 96px snap threshold.
|
|
29
|
+
type: boolean
|
|
30
|
+
default: false
|
|
31
|
+
reflect: true
|
|
32
|
+
|
|
33
|
+
events:
|
|
34
|
+
sidebar-toggle:
|
|
35
|
+
description: Bubbles when .toggle() / .collapse() / .expand() is called.
|
|
36
|
+
detail:
|
|
37
|
+
name: string
|
|
38
|
+
expanded: boolean
|
|
39
|
+
|
|
40
|
+
slots:
|
|
41
|
+
default:
|
|
42
|
+
description: >-
|
|
43
|
+
Default — the inner <pane-ui resizable> wrapper. Authors compose
|
|
44
|
+
header / section / footer inside the pane as usual.
|
|
45
|
+
|
|
46
|
+
states:
|
|
47
|
+
- name: idle
|
|
48
|
+
description: Default expanded state.
|
|
49
|
+
- name: collapsed
|
|
50
|
+
attribute: collapsed
|
|
51
|
+
description: Inner pane is at or below the snap threshold.
|
|
52
|
+
|
|
53
|
+
traits: []
|
|
54
|
+
|
|
55
|
+
a2ui:
|
|
56
|
+
rules:
|
|
57
|
+
- >-
|
|
58
|
+
editor-sidebar wraps <pane-ui resizable> rather than implementing
|
|
59
|
+
drag itself. Place a <pane-ui resizable> as the only structural
|
|
60
|
+
child; fill the pane with header / section / footer slots.
|
|
61
|
+
- >-
|
|
62
|
+
The cluster-distinct localStorage prefix (adia-editor-sidebar-*)
|
|
63
|
+
keeps editor sidebars from colliding with admin (adia-sidebar-*)
|
|
64
|
+
and chat (adia-chat-sidebar-*) sidebars on the same domain.
|
|
65
|
+
|
|
66
|
+
keywords:
|
|
67
|
+
- editor-sidebar
|
|
68
|
+
- navigator-pane
|
|
69
|
+
- inspector-pane
|
|
70
|
+
- editor-rail
|
|
71
|
+
- sidebar
|
|
72
|
+
- leading
|
|
73
|
+
- trailing
|
|
74
|
+
|
|
75
|
+
synonyms:
|
|
76
|
+
editor-sidebar: [navigator, inspector, editor-rail, side-panel]
|
|
77
|
+
|
|
78
|
+
related:
|
|
79
|
+
- EditorShell
|
|
80
|
+
- EditorCanvas
|
|
81
|
+
- Pane
|
|
82
|
+
- AdminSidebar
|
|
83
|
+
- ChatSidebar
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/EditorStatusbar.json",
|
|
4
|
+
"title": "EditorStatusbar",
|
|
5
|
+
"description": "Module-tier editor statusbar — bottom chrome bar inside\n<editor-shell>. CSS-only, no behavior, no JS. Holds save/sync\nstate, zoom indicator, cursor position, and tertiary actions\nvia slot vocabulary.\n\nReplaces the legacy <footer> with positional content per ADR-0023.\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"component": {
|
|
17
|
+
"const": "EditorStatusbar"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"required": [
|
|
21
|
+
"component"
|
|
22
|
+
],
|
|
23
|
+
"unevaluatedProperties": false,
|
|
24
|
+
"x-adiaui": {
|
|
25
|
+
"anti_patterns": [],
|
|
26
|
+
"category": "layout",
|
|
27
|
+
"events": {},
|
|
28
|
+
"examples": [],
|
|
29
|
+
"keywords": [
|
|
30
|
+
"editor-statusbar",
|
|
31
|
+
"statusbar",
|
|
32
|
+
"bottom-bar",
|
|
33
|
+
"editor-footer"
|
|
34
|
+
],
|
|
35
|
+
"name": "EditorStatusbar",
|
|
36
|
+
"related": [
|
|
37
|
+
"EditorShell",
|
|
38
|
+
"EditorToolbar",
|
|
39
|
+
"EditorCanvas"
|
|
40
|
+
],
|
|
41
|
+
"slots": {
|
|
42
|
+
"default": {
|
|
43
|
+
"description": "Default — ad-hoc inline content (legacy positional layout)."
|
|
44
|
+
},
|
|
45
|
+
"action": {
|
|
46
|
+
"description": "Trailing action cluster (e.g., toggle full-screen)."
|
|
47
|
+
},
|
|
48
|
+
"cursor": {
|
|
49
|
+
"description": "Cursor position / selection indicator."
|
|
50
|
+
},
|
|
51
|
+
"status": {
|
|
52
|
+
"description": "Save/sync state indicator."
|
|
53
|
+
},
|
|
54
|
+
"zoom": {
|
|
55
|
+
"description": "Zoom level indicator."
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"states": [
|
|
59
|
+
{
|
|
60
|
+
"description": "Default, the only state.",
|
|
61
|
+
"name": "idle"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"synonyms": {
|
|
65
|
+
"editor-statusbar": [
|
|
66
|
+
"statusbar",
|
|
67
|
+
"footer-bar",
|
|
68
|
+
"bottom-bar"
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
"tag": "editor-statusbar",
|
|
72
|
+
"tokens": {},
|
|
73
|
+
"traits": [],
|
|
74
|
+
"version": 1
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: EditorStatusbar
|
|
4
|
+
tag: editor-statusbar
|
|
5
|
+
component: EditorStatusbar
|
|
6
|
+
category: layout
|
|
7
|
+
version: 1
|
|
8
|
+
description: |
|
|
9
|
+
Module-tier editor statusbar — bottom chrome bar inside
|
|
10
|
+
<editor-shell>. CSS-only, no behavior, no JS. Holds save/sync
|
|
11
|
+
state, zoom indicator, cursor position, and tertiary actions
|
|
12
|
+
via slot vocabulary.
|
|
13
|
+
|
|
14
|
+
Replaces the legacy <footer> with positional content per ADR-0023.
|
|
15
|
+
|
|
16
|
+
props: {}
|
|
17
|
+
|
|
18
|
+
events: {}
|
|
19
|
+
|
|
20
|
+
slots:
|
|
21
|
+
default:
|
|
22
|
+
description: Default — ad-hoc inline content (legacy positional layout).
|
|
23
|
+
status:
|
|
24
|
+
description: Save/sync state indicator.
|
|
25
|
+
cursor:
|
|
26
|
+
description: Cursor position / selection indicator.
|
|
27
|
+
zoom:
|
|
28
|
+
description: Zoom level indicator.
|
|
29
|
+
action:
|
|
30
|
+
description: Trailing action cluster (e.g., toggle full-screen).
|
|
31
|
+
|
|
32
|
+
states:
|
|
33
|
+
- name: idle
|
|
34
|
+
description: Default, the only state.
|
|
35
|
+
|
|
36
|
+
traits: []
|
|
37
|
+
|
|
38
|
+
a2ui:
|
|
39
|
+
rules:
|
|
40
|
+
- >-
|
|
41
|
+
editor-statusbar replaces legacy <footer> chrome bar inside
|
|
42
|
+
<editor-shell>. Use named slots for canonical clusters; ad-hoc
|
|
43
|
+
content goes in the default slot.
|
|
44
|
+
|
|
45
|
+
keywords:
|
|
46
|
+
- editor-statusbar
|
|
47
|
+
- statusbar
|
|
48
|
+
- bottom-bar
|
|
49
|
+
- editor-footer
|
|
50
|
+
|
|
51
|
+
synonyms:
|
|
52
|
+
editor-statusbar: [statusbar, footer-bar, bottom-bar]
|
|
53
|
+
|
|
54
|
+
related:
|
|
55
|
+
- EditorShell
|
|
56
|
+
- EditorToolbar
|
|
57
|
+
- EditorCanvas
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/EditorToolbar.json",
|
|
4
|
+
"title": "EditorToolbar",
|
|
5
|
+
"description": "Module-tier editor toolbar — replaces legacy <header> chrome bar\ninside <editor-shell> per ADR-0023. Owns the [full-screen] reflected\nattribute (set when host enters focus mode), click-bubble for\n[data-toolbar-action] buttons, and slot vocabulary routing.\n\nSits at the top of <editor-shell>. Authors compose actions + status\nvia slot vocabulary. The host (<editor-shell>) reads either\n<editor-toolbar> or <header> via :is() selector for backwards compat.\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"component": {
|
|
17
|
+
"const": "EditorToolbar"
|
|
18
|
+
},
|
|
19
|
+
"fullScreen": {
|
|
20
|
+
"description": "Reflected — set when editor is in distraction-free / focus mode.",
|
|
21
|
+
"type": "boolean",
|
|
22
|
+
"default": false
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": [
|
|
26
|
+
"component"
|
|
27
|
+
],
|
|
28
|
+
"unevaluatedProperties": false,
|
|
29
|
+
"x-adiaui": {
|
|
30
|
+
"anti_patterns": [],
|
|
31
|
+
"category": "layout",
|
|
32
|
+
"events": {
|
|
33
|
+
"toolbar-action": {
|
|
34
|
+
"description": "Bubbles when a child element with [data-toolbar-action] is clicked. Detail carries the action name from the clicked element's attribute.",
|
|
35
|
+
"detail": {
|
|
36
|
+
"name": "string"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"examples": [],
|
|
41
|
+
"keywords": [
|
|
42
|
+
"editor-toolbar",
|
|
43
|
+
"editor-titlebar",
|
|
44
|
+
"editor-chrome",
|
|
45
|
+
"app-header",
|
|
46
|
+
"editor-actions"
|
|
47
|
+
],
|
|
48
|
+
"name": "EditorToolbar",
|
|
49
|
+
"related": [
|
|
50
|
+
"EditorShell",
|
|
51
|
+
"EditorCanvas",
|
|
52
|
+
"EditorSidebar",
|
|
53
|
+
"EditorStatusbar"
|
|
54
|
+
],
|
|
55
|
+
"slots": {
|
|
56
|
+
"title": {
|
|
57
|
+
"description": "Editor / document title cluster."
|
|
58
|
+
},
|
|
59
|
+
"default": {
|
|
60
|
+
"description": "Default — ad-hoc inline content (legacy positional layout)."
|
|
61
|
+
},
|
|
62
|
+
"action": {
|
|
63
|
+
"description": "Trailing action cluster (settings, share, more)."
|
|
64
|
+
},
|
|
65
|
+
"action-leading": {
|
|
66
|
+
"description": "Leading action cluster (back, switcher, undo/redo)."
|
|
67
|
+
},
|
|
68
|
+
"status": {
|
|
69
|
+
"description": "Status indicator (saving, dirty, synced, etc.)."
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"states": [
|
|
73
|
+
{
|
|
74
|
+
"description": "Default editor mode.",
|
|
75
|
+
"name": "idle"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"description": "Distraction-free / focus mode active.",
|
|
79
|
+
"attribute": "full-screen",
|
|
80
|
+
"name": "full-screen"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"synonyms": {
|
|
84
|
+
"editor-toolbar": [
|
|
85
|
+
"editor-header",
|
|
86
|
+
"app-header",
|
|
87
|
+
"navbar",
|
|
88
|
+
"titlebar"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"tag": "editor-toolbar",
|
|
92
|
+
"tokens": {},
|
|
93
|
+
"traits": [],
|
|
94
|
+
"version": 1
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <editor-toolbar full-screen?>
|
|
3
|
+
* <span slot="title">Editor</span>
|
|
4
|
+
* <button-ui slot="action" icon="gear"></button-ui>
|
|
5
|
+
* </editor-toolbar>
|
|
6
|
+
*
|
|
7
|
+
* Module-tier editor toolbar — replaces legacy <header> with [data-title]
|
|
8
|
+
* + [data-spacer] + ad-hoc action children inside <editor-shell>. Owns:
|
|
9
|
+
*
|
|
10
|
+
* - [full-screen] reflected attribute (set when host enters focus mode)
|
|
11
|
+
* - Slot vocabulary routing (title / status / action / action-leading)
|
|
12
|
+
* - Click-bubble for [data-toolbar-action="<name>"] buttons → fires
|
|
13
|
+
* 'toolbar-action' event with the action name
|
|
14
|
+
*
|
|
15
|
+
* Reflected attributes:
|
|
16
|
+
* [full-screen] — true when editor is in distraction-free / focus mode
|
|
17
|
+
*
|
|
18
|
+
* Events:
|
|
19
|
+
* toolbar-action — bubbles. detail: { name } from clicked button's
|
|
20
|
+
* [data-toolbar-action] attribute
|
|
21
|
+
*
|
|
22
|
+
* The host (<editor-shell>) reads either <editor-toolbar> or <header>
|
|
23
|
+
* via :is() selector for backwards compat. Default-slot children fall
|
|
24
|
+
* back to legacy positional layout if no named slots are used.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { UIElement } from '../../../web-components/core/element.js';
|
|
28
|
+
|
|
29
|
+
class EditorToolbar extends UIElement {
|
|
30
|
+
static properties = {
|
|
31
|
+
fullScreen: { type: Boolean, default: false, reflect: true, attribute: 'full-screen' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
static template = () => null;
|
|
35
|
+
|
|
36
|
+
#onClick = null;
|
|
37
|
+
|
|
38
|
+
connected() {
|
|
39
|
+
this.#onClick = (e) => {
|
|
40
|
+
const btn = e.target.closest('[data-toolbar-action]');
|
|
41
|
+
if (!btn || !this.contains(btn)) return;
|
|
42
|
+
const name = btn.getAttribute('data-toolbar-action');
|
|
43
|
+
this.dispatchEvent(new CustomEvent('toolbar-action', {
|
|
44
|
+
bubbles: true,
|
|
45
|
+
detail: { name },
|
|
46
|
+
}));
|
|
47
|
+
};
|
|
48
|
+
this.addEventListener('click', this.#onClick);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
disconnected() {
|
|
52
|
+
if (this.#onClick) this.removeEventListener('click', this.#onClick);
|
|
53
|
+
this.#onClick = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
customElements.define('editor-toolbar', EditorToolbar);
|
|
58
|
+
export { EditorToolbar };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import '../../../web-components/core/element.js';
|
|
3
|
+
import './editor-toolbar.js';
|
|
4
|
+
|
|
5
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
+
|
|
7
|
+
function mount(html) {
|
|
8
|
+
const wrap = document.createElement('div');
|
|
9
|
+
wrap.innerHTML = html;
|
|
10
|
+
document.body.appendChild(wrap);
|
|
11
|
+
return wrap.firstElementChild;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
document.body.innerHTML = '';
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('editor-toolbar', () => {
|
|
19
|
+
it('registers editor-toolbar as a custom element', () => {
|
|
20
|
+
expect(customElements.get('editor-toolbar')).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('defaults to fullScreen=false', () => {
|
|
24
|
+
const t = mount('<editor-toolbar></editor-toolbar>');
|
|
25
|
+
expect(t.fullScreen).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('reflects [full-screen] via property assignment', async () => {
|
|
29
|
+
const t = mount('<editor-toolbar></editor-toolbar>');
|
|
30
|
+
t.fullScreen = true;
|
|
31
|
+
await tick();
|
|
32
|
+
expect(t.hasAttribute('full-screen')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('honors initial [full-screen] attribute on connect', () => {
|
|
36
|
+
const t = mount('<editor-toolbar full-screen></editor-toolbar>');
|
|
37
|
+
expect(t.fullScreen).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('bubbles toolbar-action event from [data-toolbar-action] children', async () => {
|
|
41
|
+
const wrap = document.createElement('div');
|
|
42
|
+
wrap.innerHTML = `<editor-toolbar>
|
|
43
|
+
<button data-toolbar-action="save">Save</button>
|
|
44
|
+
<button data-toolbar-action="undo">Undo</button>
|
|
45
|
+
</editor-toolbar>`;
|
|
46
|
+
document.body.appendChild(wrap);
|
|
47
|
+
const toolbar = wrap.firstElementChild;
|
|
48
|
+
const saveBtn = toolbar.querySelector('[data-toolbar-action="save"]');
|
|
49
|
+
|
|
50
|
+
const onAction = vi.fn();
|
|
51
|
+
toolbar.addEventListener('toolbar-action', onAction);
|
|
52
|
+
|
|
53
|
+
saveBtn.click();
|
|
54
|
+
await tick();
|
|
55
|
+
|
|
56
|
+
expect(onAction).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(onAction.mock.calls[0][0].detail).toEqual({ name: 'save' });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does not fire toolbar-action for clicks outside [data-toolbar-action]', async () => {
|
|
61
|
+
const wrap = document.createElement('div');
|
|
62
|
+
wrap.innerHTML = `<editor-toolbar>
|
|
63
|
+
<span>Title</span>
|
|
64
|
+
<button>Plain button</button>
|
|
65
|
+
</editor-toolbar>`;
|
|
66
|
+
document.body.appendChild(wrap);
|
|
67
|
+
const toolbar = wrap.firstElementChild;
|
|
68
|
+
const plainBtn = toolbar.querySelector('button');
|
|
69
|
+
|
|
70
|
+
const onAction = vi.fn();
|
|
71
|
+
toolbar.addEventListener('toolbar-action', onAction);
|
|
72
|
+
|
|
73
|
+
plainBtn.click();
|
|
74
|
+
await tick();
|
|
75
|
+
|
|
76
|
+
expect(onAction).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('cleanup on disconnect — removes click listener', () => {
|
|
80
|
+
const t = mount('<editor-toolbar></editor-toolbar>');
|
|
81
|
+
const removeSpy = vi.spyOn(t, 'removeEventListener');
|
|
82
|
+
t.remove();
|
|
83
|
+
const removedTypes = removeSpy.mock.calls.map((args) => args[0]);
|
|
84
|
+
expect(removedTypes).toContain('click');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('event bubbles up beyond the toolbar', async () => {
|
|
88
|
+
const outer = document.createElement('div');
|
|
89
|
+
document.body.appendChild(outer);
|
|
90
|
+
outer.innerHTML = `<editor-toolbar>
|
|
91
|
+
<button data-toolbar-action="settings"></button>
|
|
92
|
+
</editor-toolbar>`;
|
|
93
|
+
const onOuterAction = vi.fn();
|
|
94
|
+
outer.addEventListener('toolbar-action', onOuterAction);
|
|
95
|
+
outer.querySelector('button').click();
|
|
96
|
+
await tick();
|
|
97
|
+
expect(onOuterAction).toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: EditorToolbar
|
|
4
|
+
tag: editor-toolbar
|
|
5
|
+
component: EditorToolbar
|
|
6
|
+
category: layout
|
|
7
|
+
version: 1
|
|
8
|
+
description: |
|
|
9
|
+
Module-tier editor toolbar — replaces legacy <header> chrome bar
|
|
10
|
+
inside <editor-shell> per ADR-0023. Owns the [full-screen] reflected
|
|
11
|
+
attribute (set when host enters focus mode), click-bubble for
|
|
12
|
+
[data-toolbar-action] buttons, and slot vocabulary routing.
|
|
13
|
+
|
|
14
|
+
Sits at the top of <editor-shell>. Authors compose actions + status
|
|
15
|
+
via slot vocabulary. The host (<editor-shell>) reads either
|
|
16
|
+
<editor-toolbar> or <header> via :is() selector for backwards compat.
|
|
17
|
+
|
|
18
|
+
props:
|
|
19
|
+
fullScreen:
|
|
20
|
+
description: Reflected — set when editor is in distraction-free / focus mode.
|
|
21
|
+
type: boolean
|
|
22
|
+
default: false
|
|
23
|
+
reflect: true
|
|
24
|
+
attribute: full-screen
|
|
25
|
+
|
|
26
|
+
events:
|
|
27
|
+
toolbar-action:
|
|
28
|
+
description: >-
|
|
29
|
+
Bubbles when a child element with [data-toolbar-action] is clicked.
|
|
30
|
+
Detail carries the action name from the clicked element's attribute.
|
|
31
|
+
detail:
|
|
32
|
+
name: string
|
|
33
|
+
|
|
34
|
+
slots:
|
|
35
|
+
default:
|
|
36
|
+
description: Default — ad-hoc inline content (legacy positional layout).
|
|
37
|
+
title:
|
|
38
|
+
description: Editor / document title cluster.
|
|
39
|
+
status:
|
|
40
|
+
description: Status indicator (saving, dirty, synced, etc.).
|
|
41
|
+
action:
|
|
42
|
+
description: Trailing action cluster (settings, share, more).
|
|
43
|
+
action-leading:
|
|
44
|
+
description: Leading action cluster (back, switcher, undo/redo).
|
|
45
|
+
|
|
46
|
+
states:
|
|
47
|
+
- name: idle
|
|
48
|
+
description: Default editor mode.
|
|
49
|
+
- name: full-screen
|
|
50
|
+
attribute: full-screen
|
|
51
|
+
description: Distraction-free / focus mode active.
|
|
52
|
+
|
|
53
|
+
traits: []
|
|
54
|
+
|
|
55
|
+
a2ui:
|
|
56
|
+
rules:
|
|
57
|
+
- >-
|
|
58
|
+
editor-toolbar replaces legacy <header> chrome bar inside
|
|
59
|
+
<editor-shell>. Use named slots (title / status / action /
|
|
60
|
+
action-leading) for canonical clusters; ad-hoc inline content
|
|
61
|
+
goes in the default slot.
|
|
62
|
+
- >-
|
|
63
|
+
Buttons that should trigger named actions get
|
|
64
|
+
[data-toolbar-action="<name>"]. The toolbar bubbles a single
|
|
65
|
+
'toolbar-action' event up to the host with the name in detail.
|
|
66
|
+
|
|
67
|
+
keywords:
|
|
68
|
+
- editor-toolbar
|
|
69
|
+
- editor-titlebar
|
|
70
|
+
- editor-chrome
|
|
71
|
+
- app-header
|
|
72
|
+
- editor-actions
|
|
73
|
+
|
|
74
|
+
synonyms:
|
|
75
|
+
editor-toolbar: [editor-header, app-header, navbar, titlebar]
|
|
76
|
+
|
|
77
|
+
related:
|
|
78
|
+
- EditorShell
|
|
79
|
+
- EditorCanvas
|
|
80
|
+
- EditorSidebar
|
|
81
|
+
- EditorStatusbar
|
package/editor/index.js
CHANGED