@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.
@@ -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
+ }
@@ -4,3 +4,4 @@
4
4
 
5
5
  @import "./css/editor-shell.tokens.css";
6
6
  @import "./css/editor-shell.layout.css";
7
+ @import "./css/editor-shell.bespoke.css";
@@ -1,47 +1,102 @@
1
1
  import { UIElement } from '../../../web-components/core/element.js';
2
2
 
3
3
  /**
4
- * <editor-shell> — Editor layout pattern.
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 tool UIs:
16
+ * Behavior-only orchestrator for design-tool UIs:
7
17
  * topbar, navigator pane, center canvas, inspector pane, bottombar.
8
18
  *
9
- * Structure:
10
- * <editor-shell>
11
- * <header>
12
- * <span data-title>Editor</span>
13
- * <span data-spacer></span>
14
- * <button-ui icon="gear" variant="ghost" size="sm"></button-ui>
15
- * </header>
16
- * <div data-editor-body>
17
- * <pane-ui data-left resizable>
18
- * <header>Navigator</header>
19
- * <section>...tree, layers...</section>
20
- * </pane-ui>
21
- * <div data-canvas>
22
- * ...canvas content...
23
- * </div>
24
- * <pane-ui data-right resizable>
25
- * <header>Inspector</header>
26
- * <section>...form fields...</section>
27
- * <footer>...actions...</footer>
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
- // Wire select options if present
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 };