@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,172 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
editor-shell — Bespoke shell-tier children (Phase 2 of ADR-0023)
|
|
3
|
+
|
|
4
|
+
The editor-* CSS-only structural children are styled by SHARING
|
|
5
|
+
the same CSS rules as their legacy raw-HTML counterparts. This
|
|
6
|
+
file maps the new bespoke tags to the existing layout.css patterns
|
|
7
|
+
without modifying the layered files themselves.
|
|
8
|
+
|
|
9
|
+
Mirrors admin-shell.bespoke.css and chat-shell.bespoke.css. Keeps
|
|
10
|
+
the editor cluster's CSS modifications isolated in one bridge so
|
|
11
|
+
Phase 3 (legacy shape removal) is a single-file delete.
|
|
12
|
+
|
|
13
|
+
Editor cluster decomposition note:
|
|
14
|
+
- <editor-toolbar> — replaces <header> chrome bar (CSS-only? no,
|
|
15
|
+
it's JS-bearing for [data-toolbar-action] click bubble + slot
|
|
16
|
+
routing, but visually it's just a chrome bar like admin-topbar)
|
|
17
|
+
- <editor-canvas> — replaces <div data-canvas> central content
|
|
18
|
+
(JS-bearing — owns [empty] + [focused] reflected, zoom API)
|
|
19
|
+
- <editor-sidebar> — wraps <pane-ui resizable> (JS-bearing — owns
|
|
20
|
+
[collapsed] + persistence, delegates drag to <pane-ui>)
|
|
21
|
+
- <editor-statusbar> — replaces <footer> chrome bar (CSS-only)
|
|
22
|
+
- <editor-canvas-empty> — empty-state slot for <editor-canvas>
|
|
23
|
+
(CSS-only, visibility via parent [empty])
|
|
24
|
+
═══════════════════════════════════════════════════════════════ */
|
|
25
|
+
|
|
26
|
+
/* ── editor-toolbar — top chrome bar ── */
|
|
27
|
+
editor-shell > editor-toolbar {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: var(--editor-toolbar-gap, var(--a-space-2));
|
|
31
|
+
padding: 0 var(--editor-toolbar-px, var(--a-space-3));
|
|
32
|
+
height: var(--editor-toolbar-height, var(--a-size-lg));
|
|
33
|
+
font-size: var(--editor-toolbar-font, var(--a-ui-size));
|
|
34
|
+
border-bottom: var(--editor-border, 1px solid var(--a-border-subtle));
|
|
35
|
+
background: var(--editor-bg, var(--a-bg));
|
|
36
|
+
flex-shrink: 0;
|
|
37
|
+
grid-area: toolbar;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Slot vocabulary inside editor-toolbar */
|
|
41
|
+
editor-toolbar > [slot="title"] {
|
|
42
|
+
font-weight: var(--a-weight-medium, 500);
|
|
43
|
+
color: var(--a-fg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
editor-toolbar > [slot="action-leading"] {
|
|
47
|
+
margin-inline-end: var(--a-space-2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
editor-toolbar > [slot="status"] {
|
|
51
|
+
margin-inline-start: var(--a-space-2);
|
|
52
|
+
color: var(--a-fg-muted);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
editor-toolbar > [slot="action"]:first-of-type {
|
|
56
|
+
margin-inline-start: auto;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Full-screen mode — toolbar can be hidden by host CSS */
|
|
60
|
+
editor-toolbar[full-screen] {
|
|
61
|
+
/* Optional: visual treatment in full-screen mode. Default: no change.
|
|
62
|
+
Authors can override via:
|
|
63
|
+
editor-shell:has(editor-toolbar[full-screen]) editor-toolbar { display: none; }
|
|
64
|
+
*/
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ── editor-canvas — central content surface ── */
|
|
68
|
+
editor-shell > editor-canvas {
|
|
69
|
+
flex: 1;
|
|
70
|
+
min-height: 0;
|
|
71
|
+
min-width: 0;
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
position: relative;
|
|
75
|
+
overflow: auto;
|
|
76
|
+
background: var(--editor-canvas-bg, var(--a-bg-subtle));
|
|
77
|
+
grid-area: canvas;
|
|
78
|
+
|
|
79
|
+
/* Apply zoom transform if the JS sets --editor-canvas-zoom */
|
|
80
|
+
/* Children inherit; the canvas itself is the scroll surface */
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
editor-canvas[focused] {
|
|
84
|
+
/* Optional: visual focus treatment. Default: no visual change. */
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
editor-canvas > * {
|
|
88
|
+
transform: scale(var(--editor-canvas-zoom, 1));
|
|
89
|
+
transform-origin: top left;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ── editor-canvas-empty — visibility via parent's [empty] ── */
|
|
93
|
+
editor-canvas:not([empty]) > editor-canvas-empty {
|
|
94
|
+
display: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
editor-canvas[empty] > editor-canvas-empty {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex: 1;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: center;
|
|
102
|
+
padding: var(--a-space-6);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── editor-sidebar — wraps <pane-ui resizable> ── */
|
|
106
|
+
:is(editor-sidebar[slot="leading"], editor-sidebar[slot="trailing"]) {
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-direction: column;
|
|
109
|
+
flex-shrink: 0;
|
|
110
|
+
min-height: 0;
|
|
111
|
+
position: relative;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
editor-sidebar[slot="leading"] {
|
|
115
|
+
grid-area: leading;
|
|
116
|
+
border-right: var(--editor-border, 1px solid var(--a-border-subtle));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
editor-sidebar[slot="trailing"] {
|
|
120
|
+
grid-area: trailing;
|
|
121
|
+
border-left: var(--editor-border, 1px solid var(--a-border-subtle));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* The inner <pane-ui> takes all available space */
|
|
125
|
+
editor-sidebar > pane-ui {
|
|
126
|
+
flex: 1;
|
|
127
|
+
min-height: 0;
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-direction: column;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* When sidebar is collapsed, hide pane content but keep the rail */
|
|
133
|
+
editor-sidebar[collapsed] > pane-ui > :not(header) {
|
|
134
|
+
display: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ── editor-statusbar — bottom chrome bar ── */
|
|
138
|
+
editor-shell > editor-statusbar {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
gap: var(--editor-statusbar-gap, var(--a-space-2));
|
|
142
|
+
padding: 0 var(--editor-statusbar-px, var(--a-space-3));
|
|
143
|
+
height: var(--editor-statusbar-height, var(--a-size-md));
|
|
144
|
+
font-size: var(--editor-statusbar-font, var(--a-ui-sm));
|
|
145
|
+
border-top: var(--editor-border, 1px solid var(--a-border-subtle));
|
|
146
|
+
background: var(--editor-bg, var(--a-bg));
|
|
147
|
+
color: var(--a-fg-muted);
|
|
148
|
+
flex-shrink: 0;
|
|
149
|
+
grid-area: statusbar;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
editor-statusbar > [slot="cursor"],
|
|
153
|
+
editor-statusbar > [slot="zoom"] {
|
|
154
|
+
font-family: var(--a-font-mono, monospace);
|
|
155
|
+
margin-inline-start: var(--a-space-2);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
editor-statusbar > [slot="action"]:first-of-type {
|
|
159
|
+
margin-inline-start: auto;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ── editor-shell layout — when bespoke children are used ── */
|
|
163
|
+
editor-shell:has(> editor-canvas) {
|
|
164
|
+
display: grid;
|
|
165
|
+
grid-template-areas:
|
|
166
|
+
"toolbar toolbar toolbar"
|
|
167
|
+
"leading canvas trailing"
|
|
168
|
+
"statusbar statusbar statusbar";
|
|
169
|
+
grid-template-rows: auto 1fr auto;
|
|
170
|
+
grid-template-columns: auto 1fr auto;
|
|
171
|
+
height: 100%;
|
|
172
|
+
}
|
|
@@ -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 };
|