@adia-ai/web-modules 0.3.4 → 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 +50 -0
- package/chat/chat-composer/chat-composer.a2ui.json +94 -0
- package/chat/chat-composer/chat-composer.examples.html +28 -0
- package/chat/chat-composer/chat-composer.html +43 -0
- package/chat/chat-composer/chat-composer.js +107 -0
- package/chat/chat-composer/chat-composer.test.js +112 -0
- package/chat/chat-composer/chat-composer.yaml +91 -0
- package/chat/chat-empty/chat-empty.a2ui.json +68 -0
- package/chat/chat-empty/chat-empty.examples.html +34 -0
- package/chat/chat-empty/chat-empty.html +42 -0
- package/chat/chat-empty/chat-empty.yaml +58 -0
- package/chat/chat-header/chat-header.a2ui.json +77 -0
- package/chat/chat-header/chat-header.examples.html +30 -0
- package/chat/chat-header/chat-header.html +42 -0
- package/chat/chat-header/chat-header.yaml +68 -0
- package/chat/chat-shell/chat-shell.css +1 -0
- package/chat/chat-shell/chat-shell.examples.html +43 -2
- package/chat/chat-shell/chat-shell.js +35 -7
- package/chat/chat-shell/css/chat-shell.bespoke.css +196 -0
- package/chat/chat-sidebar/chat-sidebar.a2ui.json +136 -0
- package/chat/chat-sidebar/chat-sidebar.examples.html +36 -0
- package/chat/chat-sidebar/chat-sidebar.html +43 -0
- package/chat/chat-sidebar/chat-sidebar.js +227 -0
- package/chat/chat-sidebar/chat-sidebar.test.js +110 -0
- package/chat/chat-sidebar/chat-sidebar.yaml +140 -0
- package/chat/chat-status/chat-status.a2ui.json +63 -0
- package/chat/chat-status/chat-status.examples.html +29 -0
- package/chat/chat-status/chat-status.html +42 -0
- package/chat/chat-status/chat-status.yaml +52 -0
- package/chat/chat-thread/chat-thread.a2ui.json +91 -0
- package/chat/chat-thread/chat-thread.examples.html +36 -0
- package/chat/chat-thread/chat-thread.html +43 -0
- package/chat/chat-thread/chat-thread.js +106 -0
- package/chat/chat-thread/chat-thread.test.js +82 -0
- package/chat/chat-thread/chat-thread.yaml +89 -0
- package/chat/index.js +3 -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
|
@@ -1,47 +1,102 @@
|
|
|
1
1
|
import { UIElement } from '../../../web-components/core/element.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* <editor-shell
|
|
4
|
+
* <editor-shell focus-mode?>
|
|
5
|
+
* <editor-toolbar>...</editor-toolbar> (or legacy <header>)
|
|
6
|
+
* <editor-sidebar slot="leading"> (or legacy <pane-ui data-left>)
|
|
7
|
+
* <pane-ui resizable>...</pane-ui>
|
|
8
|
+
* </editor-sidebar>
|
|
9
|
+
* <editor-canvas>...</editor-canvas> (or legacy <div data-canvas>)
|
|
10
|
+
* <editor-sidebar slot="trailing"> (or legacy <pane-ui data-right>)
|
|
11
|
+
* <pane-ui resizable>...</pane-ui>
|
|
12
|
+
* </editor-sidebar>
|
|
13
|
+
* <editor-statusbar>...</editor-statusbar> (or legacy <footer>)
|
|
14
|
+
* </editor-shell>
|
|
5
15
|
*
|
|
6
|
-
* Behavior-only orchestrator for design
|
|
16
|
+
* Behavior-only orchestrator for design-tool UIs:
|
|
7
17
|
* topbar, navigator pane, center canvas, inspector pane, bottombar.
|
|
8
18
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* </pane-ui>
|
|
29
|
-
* </div>
|
|
30
|
-
* <footer>
|
|
31
|
-
* <span>Ready</span>
|
|
32
|
-
* <span data-spacer></span>
|
|
33
|
-
* <span>100%</span>
|
|
34
|
-
* </footer>
|
|
35
|
-
* </editor-shell>
|
|
19
|
+
* Per ADR-0023, the bespoke shape uses cluster-namespaced custom
|
|
20
|
+
* elements. Both legacy and bespoke shapes coexist via :is() reads
|
|
21
|
+
* in the host. Editor cluster has the smallest bespoke family of
|
|
22
|
+
* the three (admin, chat, editor) — only 3 JS-bearing children +
|
|
23
|
+
* 2 CSS-only structural stubs — because <pane-ui> already owns
|
|
24
|
+
* resize and the editor doesn't need a command palette.
|
|
25
|
+
*
|
|
26
|
+
* Reflected attributes:
|
|
27
|
+
* [focus-mode] — distraction-free / canvas-focus mode; consumers
|
|
28
|
+
* can hide toolbar/sidebars/statusbar via CSS:
|
|
29
|
+
* editor-shell[focus-mode] editor-toolbar { display: none; }
|
|
30
|
+
*
|
|
31
|
+
* Events:
|
|
32
|
+
* editor-mode-change — fires when [focus-mode] is toggled
|
|
33
|
+
*
|
|
34
|
+
* Public methods:
|
|
35
|
+
* .toggleFocusMode() — flip [focus-mode]; also propagates to
|
|
36
|
+
* child <editor-toolbar>'s [full-screen]
|
|
37
|
+
* and <editor-canvas>'s [focused] attributes
|
|
36
38
|
*/
|
|
37
39
|
class EditorShell extends UIElement {
|
|
40
|
+
static properties = {
|
|
41
|
+
focusMode: { type: Boolean, default: false, reflect: true, attribute: 'focus-mode' },
|
|
42
|
+
};
|
|
43
|
+
|
|
38
44
|
static template = () => null;
|
|
39
45
|
|
|
46
|
+
#toolbarEl = null;
|
|
47
|
+
#canvasEl = null;
|
|
48
|
+
#onToolbarAction = null;
|
|
49
|
+
|
|
40
50
|
connected() {
|
|
41
|
-
//
|
|
51
|
+
// Per ADR-0023 — read BOTH legacy + bespoke shapes
|
|
52
|
+
this.#toolbarEl = this.querySelector('editor-toolbar')
|
|
53
|
+
|| this.querySelector('header');
|
|
54
|
+
this.#canvasEl = this.querySelector('editor-canvas')
|
|
55
|
+
|| this.querySelector('[data-canvas]');
|
|
56
|
+
|
|
57
|
+
// Wire select options (legacy concern, kept for backwards compat)
|
|
42
58
|
this.#wireSelects();
|
|
59
|
+
|
|
60
|
+
// Listen for toolbar-action bubbling from <editor-toolbar>
|
|
61
|
+
this.#onToolbarAction = (e) => {
|
|
62
|
+
const name = e?.detail?.name;
|
|
63
|
+
if (!name) return;
|
|
64
|
+
if (name === 'toggle-focus' || name === 'full-screen') {
|
|
65
|
+
this.toggleFocusMode();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
this.addEventListener('toolbar-action', this.#onToolbarAction);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
disconnected() {
|
|
72
|
+
if (this.#onToolbarAction) {
|
|
73
|
+
this.removeEventListener('toolbar-action', this.#onToolbarAction);
|
|
74
|
+
}
|
|
75
|
+
this.#onToolbarAction = null;
|
|
43
76
|
}
|
|
44
77
|
|
|
78
|
+
// ── Public API ──
|
|
79
|
+
|
|
80
|
+
toggleFocusMode() {
|
|
81
|
+
this.focusMode = !this.focusMode;
|
|
82
|
+
|
|
83
|
+
// Propagate to bespoke children if present
|
|
84
|
+
if (this.#toolbarEl?.tagName?.toLowerCase() === 'editor-toolbar') {
|
|
85
|
+
this.#toolbarEl.fullScreen = this.focusMode;
|
|
86
|
+
}
|
|
87
|
+
if (this.#canvasEl?.tagName?.toLowerCase() === 'editor-canvas') {
|
|
88
|
+
if (this.focusMode) this.#canvasEl.focus?.();
|
|
89
|
+
else this.#canvasEl.blur?.();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.dispatchEvent(new CustomEvent('editor-mode-change', {
|
|
93
|
+
bubbles: true,
|
|
94
|
+
detail: { focusMode: this.focusMode },
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Internal ──
|
|
99
|
+
|
|
45
100
|
#wireSelects() {
|
|
46
101
|
for (const sel of this.querySelectorAll('select-ui[data-options]')) {
|
|
47
102
|
try {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/EditorSidebar.json",
|
|
4
|
+
"title": "EditorSidebar",
|
|
5
|
+
"description": "Module-tier editor-cluster sidebar — wraps <pane-ui resizable>\n(the primitive that owns drag) and adds [collapsed] reflected\nstate, localStorage persistence (adia-editor-sidebar-{name}),\nand .toggle() / .collapse() / .expand() public API.\n\nSits inside <editor-shell> as slot=\"leading\" (navigator pane) or\nslot=\"trailing\" (inspector pane). Mirrors the cluster-namespace\nshape from <admin-sidebar> and <chat-sidebar>, but DELEGATES to\n<pane-ui> for resize rather than reimplementing it (per\nbespoke-shell-children skill §When NOT to promote — pane primitive\nalready owns this concern).\n\nThis is the FIRST bespoke shell child that delegates rather than\nduplicates a primitive's behavior. Pattern is: cluster-namespace +\nstate-as-attribute + persistence at the bespoke tier; physical\ndrag at the primitive tier.\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
|
+
"collapsed": {
|
|
17
|
+
"description": "Reflected — set when inner <pane-ui> width is at or below 96px snap threshold.",
|
|
18
|
+
"type": "boolean",
|
|
19
|
+
"default": false
|
|
20
|
+
},
|
|
21
|
+
"component": {
|
|
22
|
+
"const": "EditorSidebar"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": [
|
|
26
|
+
"component"
|
|
27
|
+
],
|
|
28
|
+
"unevaluatedProperties": false,
|
|
29
|
+
"x-adiaui": {
|
|
30
|
+
"anti_patterns": [],
|
|
31
|
+
"category": "layout",
|
|
32
|
+
"events": {
|
|
33
|
+
"sidebar-toggle": {
|
|
34
|
+
"description": "Bubbles when .toggle() / .collapse() / .expand() is called.",
|
|
35
|
+
"detail": {
|
|
36
|
+
"expanded": "boolean",
|
|
37
|
+
"name": "string"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"examples": [],
|
|
42
|
+
"keywords": [
|
|
43
|
+
"editor-sidebar",
|
|
44
|
+
"navigator-pane",
|
|
45
|
+
"inspector-pane",
|
|
46
|
+
"editor-rail",
|
|
47
|
+
"sidebar",
|
|
48
|
+
"leading",
|
|
49
|
+
"trailing"
|
|
50
|
+
],
|
|
51
|
+
"name": "EditorSidebar",
|
|
52
|
+
"related": [
|
|
53
|
+
"EditorShell",
|
|
54
|
+
"EditorCanvas",
|
|
55
|
+
"Pane",
|
|
56
|
+
"AdminSidebar",
|
|
57
|
+
"ChatSidebar"
|
|
58
|
+
],
|
|
59
|
+
"slots": {
|
|
60
|
+
"default": {
|
|
61
|
+
"description": "Default — the inner <pane-ui resizable> wrapper. Authors compose header / section / footer inside the pane as usual."
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"states": [
|
|
65
|
+
{
|
|
66
|
+
"description": "Default expanded state.",
|
|
67
|
+
"name": "idle"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"description": "Inner pane is at or below the snap threshold.",
|
|
71
|
+
"attribute": "collapsed",
|
|
72
|
+
"name": "collapsed"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"synonyms": {
|
|
76
|
+
"editor-sidebar": [
|
|
77
|
+
"navigator",
|
|
78
|
+
"inspector",
|
|
79
|
+
"editor-rail",
|
|
80
|
+
"side-panel"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"tag": "editor-sidebar",
|
|
84
|
+
"tokens": {},
|
|
85
|
+
"traits": [],
|
|
86
|
+
"version": 1
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <editor-sidebar slot="leading|trailing" collapsible>
|
|
3
|
+
* <pane-ui resizable>
|
|
4
|
+
* <header>Navigator</header>
|
|
5
|
+
* <section>...content...</section>
|
|
6
|
+
* <footer>...actions...</footer>
|
|
7
|
+
* </pane-ui>
|
|
8
|
+
* </editor-sidebar>
|
|
9
|
+
*
|
|
10
|
+
* Module-tier editor-cluster sidebar — wraps <pane-ui resizable>
|
|
11
|
+
* (the primitive that owns drag) and adds:
|
|
12
|
+
*
|
|
13
|
+
* - [collapsed] reflected attribute (read from inner <pane-ui>
|
|
14
|
+
* width via ResizeObserver; threshold = SNAP_THRESHOLD = 96px)
|
|
15
|
+
* - localStorage persistence (adia-editor-sidebar-{name}) — preserves
|
|
16
|
+
* pane width across reloads, distinct from admin's adia-sidebar-*
|
|
17
|
+
* and chat's adia-chat-sidebar-* prefixes per cluster-namespace
|
|
18
|
+
* convention
|
|
19
|
+
* - .toggle() / .collapse() / .expand() public API (delegates to
|
|
20
|
+
* inner <pane-ui> by setting its style.width)
|
|
21
|
+
* - Slot routing — pass-through; whatever the author puts inside
|
|
22
|
+
* the inner <pane-ui> is preserved
|
|
23
|
+
*
|
|
24
|
+
* Author-supplied attributes (read once at connect):
|
|
25
|
+
* [slot="leading"|"trailing"] — required, drives localStorage
|
|
26
|
+
* namespacing
|
|
27
|
+
* [collapsible] — opts in to programmatic collapse
|
|
28
|
+
* [name="<id>"] — optional override for the
|
|
29
|
+
* localStorage key (defaults to slot)
|
|
30
|
+
*
|
|
31
|
+
* Reflected attributes:
|
|
32
|
+
* [collapsed] — set when inner pane width ≤ SNAP_THRESHOLD
|
|
33
|
+
*
|
|
34
|
+
* Events:
|
|
35
|
+
* sidebar-toggle — bubbles. detail: { name, expanded }
|
|
36
|
+
*
|
|
37
|
+
* The host (<editor-shell>) reads either <editor-sidebar> or
|
|
38
|
+
* <pane-ui data-left|data-right> via :is() selector for backwards compat.
|
|
39
|
+
*
|
|
40
|
+
* Note — this is the FIRST bespoke shell child that DELEGATES rather
|
|
41
|
+
* than DUPLICATES a primitive's behavior. Per skill principle 4, since
|
|
42
|
+
* <pane-ui resizable> already owns drag, we compose it rather than
|
|
43
|
+
* reimplement. The cluster-namespace + state-as-attribute + persistence
|
|
44
|
+
* concerns still get owned at this tier (because they're cluster-
|
|
45
|
+
* specific, not primitive-specific). See bespoke-shell-children skill
|
|
46
|
+
* §When NOT to promote for the rationale.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { UIElement } from '../../../web-components/core/element.js';
|
|
50
|
+
|
|
51
|
+
const SNAP_THRESHOLD = 96; // px — below this, sidebar is "collapsed"
|
|
52
|
+
const STORAGE_PREFIX = 'adia-editor-sidebar-';
|
|
53
|
+
|
|
54
|
+
class EditorSidebar extends UIElement {
|
|
55
|
+
static properties = {
|
|
56
|
+
collapsed: { type: Boolean, default: false, reflect: true },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
static template = () => null;
|
|
60
|
+
|
|
61
|
+
#pane = null;
|
|
62
|
+
#ro = null;
|
|
63
|
+
#storageKey = null;
|
|
64
|
+
|
|
65
|
+
connected() {
|
|
66
|
+
this.#pane = this.querySelector('pane-ui');
|
|
67
|
+
if (!this.#pane) return;
|
|
68
|
+
|
|
69
|
+
// Resolve storage key
|
|
70
|
+
const slot = this.getAttribute('slot') || 'leading';
|
|
71
|
+
const name = this.getAttribute('name') || slot;
|
|
72
|
+
this.#storageKey = `${STORAGE_PREFIX}${name}`;
|
|
73
|
+
|
|
74
|
+
// Restore persisted width
|
|
75
|
+
this.#restoreWidth();
|
|
76
|
+
|
|
77
|
+
// Observe inner pane width to maintain [collapsed] reflected
|
|
78
|
+
this.#ro = new ResizeObserver(() => this.#syncCollapsed());
|
|
79
|
+
this.#ro.observe(this.#pane);
|
|
80
|
+
|
|
81
|
+
// Persist on resize-end (via pointerup on the document)
|
|
82
|
+
this._onPointerUp = () => this.#persistWidth();
|
|
83
|
+
document.addEventListener('pointerup', this._onPointerUp);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
disconnected() {
|
|
87
|
+
this.#ro?.disconnect();
|
|
88
|
+
this.#ro = null;
|
|
89
|
+
if (this._onPointerUp) {
|
|
90
|
+
document.removeEventListener('pointerup', this._onPointerUp);
|
|
91
|
+
this._onPointerUp = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Public API ──
|
|
96
|
+
|
|
97
|
+
toggle() {
|
|
98
|
+
if (this.collapsed) this.expand();
|
|
99
|
+
else this.collapse();
|
|
100
|
+
return this.collapsed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
collapse() {
|
|
104
|
+
if (!this.#pane) return;
|
|
105
|
+
// Preserve current expanded width before snapping so expand() can restore
|
|
106
|
+
if (!this.collapsed) {
|
|
107
|
+
this.#persistWidth();
|
|
108
|
+
}
|
|
109
|
+
this.#pane.style.width = `${SNAP_THRESHOLD}px`;
|
|
110
|
+
this.collapsed = true;
|
|
111
|
+
this.#emitToggle();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
expand() {
|
|
115
|
+
if (!this.#pane) return;
|
|
116
|
+
// Restore from storage, falling back to a usable default
|
|
117
|
+
const stored = this.#readStoredWidth();
|
|
118
|
+
const restored = stored && stored > SNAP_THRESHOLD ? stored : 240;
|
|
119
|
+
this.#pane.style.width = `${restored}px`;
|
|
120
|
+
this.collapsed = false;
|
|
121
|
+
this.#persistWidth();
|
|
122
|
+
this.#emitToggle();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Internal ──
|
|
126
|
+
|
|
127
|
+
#syncCollapsed() {
|
|
128
|
+
if (!this.#pane) return;
|
|
129
|
+
const rect = this.#pane.getBoundingClientRect();
|
|
130
|
+
this.collapsed = rect.width <= SNAP_THRESHOLD;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#emitToggle() {
|
|
134
|
+
const slot = this.getAttribute('slot') || 'leading';
|
|
135
|
+
const name = this.getAttribute('name') || slot;
|
|
136
|
+
this.dispatchEvent(new CustomEvent('sidebar-toggle', {
|
|
137
|
+
bubbles: true,
|
|
138
|
+
detail: { name, expanded: !this.collapsed },
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#persistWidth() {
|
|
143
|
+
if (!this.#pane || !this.#storageKey) return;
|
|
144
|
+
try {
|
|
145
|
+
const w = this.#pane.style.width || '';
|
|
146
|
+
if (w) localStorage.setItem(this.#storageKey, w);
|
|
147
|
+
} catch { /* private mode etc. */ }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#readStoredWidth() {
|
|
151
|
+
if (!this.#storageKey) return null;
|
|
152
|
+
try {
|
|
153
|
+
const raw = localStorage.getItem(this.#storageKey);
|
|
154
|
+
if (!raw) return null;
|
|
155
|
+
const n = parseFloat(raw);
|
|
156
|
+
return isFinite(n) ? n : null;
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#restoreWidth() {
|
|
163
|
+
if (!this.#pane) return;
|
|
164
|
+
const stored = this.#readStoredWidth();
|
|
165
|
+
if (stored && stored > 0) {
|
|
166
|
+
this.#pane.style.width = `${stored}px`;
|
|
167
|
+
this.collapsed = stored <= SNAP_THRESHOLD;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
customElements.define('editor-sidebar', EditorSidebar);
|
|
173
|
+
export { EditorSidebar };
|
|
@@ -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
|
+
}
|