@adia-ai/web-modules 0.3.3 → 0.3.5
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 +60 -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 +126 -0
- package/chat/chat-shell/chat-shell.html +42 -0
- 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-shell/editor-shell.examples.html +71 -0
- package/editor/editor-shell/editor-shell.html +42 -0
- package/package.json +1 -1
- package/shell/admin-command/admin-command.a2ui.json +102 -0
- package/shell/admin-command/admin-command.examples.html +83 -0
- package/shell/admin-command/admin-command.html +42 -0
- package/shell/admin-command/admin-command.js +161 -0
- package/shell/admin-command/admin-command.test.js +115 -0
- package/shell/admin-command/admin-command.yaml +102 -0
- package/shell/admin-content/admin-content.a2ui.json +73 -0
- package/shell/admin-content/admin-content.examples.html +33 -0
- package/shell/admin-content/admin-content.html +42 -0
- package/shell/admin-content/admin-content.yaml +63 -0
- package/shell/admin-page/admin-page.a2ui.json +74 -0
- package/shell/admin-page/admin-page.examples.html +37 -0
- package/shell/admin-page/admin-page.html +42 -0
- package/shell/admin-page/admin-page.yaml +61 -0
- package/shell/admin-page-body/admin-page-body.a2ui.json +62 -0
- package/shell/admin-page-body/admin-page-body.examples.html +34 -0
- package/shell/admin-page-body/admin-page-body.html +42 -0
- package/shell/admin-page-body/admin-page-body.yaml +49 -0
- package/shell/admin-page-header/admin-page-header.a2ui.json +62 -0
- package/shell/admin-page-header/admin-page-header.examples.html +34 -0
- package/shell/admin-page-header/admin-page-header.html +42 -0
- package/shell/admin-page-header/admin-page-header.yaml +47 -0
- package/shell/admin-scroll/admin-scroll.a2ui.json +62 -0
- package/shell/admin-scroll/admin-scroll.examples.html +31 -0
- package/shell/admin-scroll/admin-scroll.html +42 -0
- package/shell/admin-scroll/admin-scroll.yaml +51 -0
- package/shell/admin-shell/admin-shell.a2ui.json +0 -10
- package/shell/admin-shell/admin-shell.css +1 -0
- package/shell/admin-shell/admin-shell.examples.html +61 -5
- package/shell/admin-shell/admin-shell.js +165 -121
- package/shell/admin-shell/admin-shell.yaml +6 -6
- package/shell/admin-shell/css/admin-shell.bespoke.css +198 -0
- package/shell/admin-shell/css/admin-shell.tokens.css +10 -0
- package/shell/admin-sidebar/admin-sidebar.a2ui.json +138 -0
- package/shell/admin-sidebar/admin-sidebar.examples.html +76 -0
- package/shell/admin-sidebar/admin-sidebar.html +47 -0
- package/shell/admin-sidebar/admin-sidebar.js +227 -0
- package/shell/admin-sidebar/admin-sidebar.test.js +123 -0
- package/shell/admin-sidebar/admin-sidebar.yaml +140 -0
- package/shell/admin-statusbar/admin-statusbar.a2ui.json +81 -0
- package/shell/admin-statusbar/admin-statusbar.examples.html +29 -0
- package/shell/admin-statusbar/admin-statusbar.html +42 -0
- package/shell/admin-statusbar/admin-statusbar.yaml +68 -0
- package/shell/admin-topbar/admin-topbar.a2ui.json +83 -0
- package/shell/admin-topbar/admin-topbar.examples.html +31 -0
- package/shell/admin-topbar/admin-topbar.html +42 -0
- package/shell/admin-topbar/admin-topbar.yaml +75 -0
- package/shell/index.js +2 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <admin-sidebar slot="leading|trailing" resizable collapsible>
|
|
3
|
+
* <admin-topbar slot="header">…</admin-topbar> (or any chrome bar)
|
|
4
|
+
* …default content (nav, list, etc.)…
|
|
5
|
+
* <admin-statusbar slot="footer">…</admin-statusbar>
|
|
6
|
+
* </admin-sidebar>
|
|
7
|
+
*
|
|
8
|
+
* Module-tier sidebar — owns resize, snap-to-collapsed, persistence,
|
|
9
|
+
* and the [collapsed] reflected attribute. Sits inside <admin-shell>
|
|
10
|
+
* but doesn't reach into it; the shell coordinates without orchestrating
|
|
11
|
+
* child internals.
|
|
12
|
+
*
|
|
13
|
+
* Reflected attributes (the consumer-queryable state):
|
|
14
|
+
* [collapsed] — set when sidebar width is at or below SNAP_THRESHOLD
|
|
15
|
+
* [resizing] — set during an active pointer-drag
|
|
16
|
+
*
|
|
17
|
+
* Author-supplied attributes (read once at connect, never overwritten):
|
|
18
|
+
* [slot="leading"|"trailing"] — required, drives drag direction +
|
|
19
|
+
* localStorage namespacing
|
|
20
|
+
* [resizable] — opts in to drag handle wiring
|
|
21
|
+
* [collapsible] — opts in to programmatic collapse
|
|
22
|
+
* (toggle button + window.toggle())
|
|
23
|
+
* [name="<id>"] — optional override for the localStorage
|
|
24
|
+
* key. Defaults to slot value.
|
|
25
|
+
* [min-width="48px"] — optional override for the snap-floor
|
|
26
|
+
* width (otherwise reads CSS min-width)
|
|
27
|
+
*
|
|
28
|
+
* Events:
|
|
29
|
+
* sidebar-toggle — bubbles. detail: { name, expanded }
|
|
30
|
+
* sidebar-resize — bubbles. detail: { name, width }
|
|
31
|
+
*
|
|
32
|
+
* The drag handle is conventional: a child `[data-resize]` element.
|
|
33
|
+
* Authors may provide a custom one, or omit (no resize affordance).
|
|
34
|
+
*
|
|
35
|
+
* Backwards compat: <admin-shell> still recognizes the legacy
|
|
36
|
+
* `<aside data-sidebar="leading">` shape via :is() selector. New
|
|
37
|
+
* code should prefer <admin-sidebar>.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { UIElement } from '../../../web-components/core/element.js';
|
|
41
|
+
|
|
42
|
+
const SNAP_THRESHOLD = 96;
|
|
43
|
+
const SNAP_MIN_USABLE = 160;
|
|
44
|
+
|
|
45
|
+
class AdminSidebar extends UIElement {
|
|
46
|
+
static properties = {
|
|
47
|
+
collapsed: { type: Boolean, default: false, reflect: true },
|
|
48
|
+
resizing: { type: Boolean, default: false, reflect: true },
|
|
49
|
+
resizable: { type: Boolean, default: false, reflect: true },
|
|
50
|
+
collapsible: { type: Boolean, default: false, reflect: true },
|
|
51
|
+
name: { type: String, default: '', reflect: true },
|
|
52
|
+
minWidth: { type: String, default: '', reflect: true, attribute: 'min-width' },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
static template = () => null;
|
|
56
|
+
|
|
57
|
+
// The width the sidebar had before being collapsed — used for restore.
|
|
58
|
+
// Map keyed by sidebar name allows multiple sidebars on one host.
|
|
59
|
+
#previousExpandedWidth = '';
|
|
60
|
+
#resizeCleanups = [];
|
|
61
|
+
#childRO = null;
|
|
62
|
+
|
|
63
|
+
connected() {
|
|
64
|
+
this.#restoreFromStorage();
|
|
65
|
+
if (this.resizable) this.#setupResizeHandle();
|
|
66
|
+
this.#setupChildResizeObserver();
|
|
67
|
+
this.#syncCollapsedFromWidth();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
disconnected() {
|
|
71
|
+
for (const cleanup of this.#resizeCleanups) cleanup();
|
|
72
|
+
this.#resizeCleanups = [];
|
|
73
|
+
this.#childRO?.disconnect();
|
|
74
|
+
this.#childRO = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Public API (callable from <admin-shell> or external code) ──
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Toggle collapsed state. Collapses if expanded, restores if collapsed.
|
|
81
|
+
* Returns the new collapsed value.
|
|
82
|
+
*/
|
|
83
|
+
toggle() {
|
|
84
|
+
if (this.collapsed) {
|
|
85
|
+
this.expand();
|
|
86
|
+
} else {
|
|
87
|
+
this.collapse();
|
|
88
|
+
}
|
|
89
|
+
return this.collapsed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Collapse to the snap-floor width. Persists to localStorage. */
|
|
93
|
+
collapse() {
|
|
94
|
+
if (this.collapsed) return;
|
|
95
|
+
// Remember current expanded width before collapsing
|
|
96
|
+
const currentWidth = this.style.width || getComputedStyle(this).width;
|
|
97
|
+
if (parseFloat(currentWidth) > SNAP_THRESHOLD) {
|
|
98
|
+
this.#previousExpandedWidth = currentWidth;
|
|
99
|
+
}
|
|
100
|
+
const minW = this.minWidth || getComputedStyle(this).minWidth;
|
|
101
|
+
this.style.width = minW;
|
|
102
|
+
this.#persist(minW);
|
|
103
|
+
this.collapsed = true;
|
|
104
|
+
this.#dispatchToggle(false);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Restore to the previous expanded width (or default if none). */
|
|
108
|
+
expand() {
|
|
109
|
+
if (!this.collapsed) return;
|
|
110
|
+
const restoreWidth = this.#previousExpandedWidth || '';
|
|
111
|
+
this.style.width = restoreWidth;
|
|
112
|
+
this.#persist(restoreWidth);
|
|
113
|
+
this.collapsed = false;
|
|
114
|
+
this.#dispatchToggle(true);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Persistence ──
|
|
118
|
+
|
|
119
|
+
#storageKey() {
|
|
120
|
+
const id = this.name || this.getAttribute('slot') || 'default';
|
|
121
|
+
return `adia-sidebar-${id}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#persist(width) {
|
|
125
|
+
try { localStorage.setItem(this.#storageKey(), width); } catch {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#restoreFromStorage() {
|
|
129
|
+
try {
|
|
130
|
+
const saved = localStorage.getItem(this.#storageKey());
|
|
131
|
+
if (!saved) return;
|
|
132
|
+
this.style.width = saved;
|
|
133
|
+
// Only treat as "previous expanded" if actually expanded
|
|
134
|
+
const w = parseFloat(saved);
|
|
135
|
+
if (!isNaN(w) && w > SNAP_THRESHOLD) {
|
|
136
|
+
this.#previousExpandedWidth = saved;
|
|
137
|
+
}
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Reflect [collapsed] from current measured width ──
|
|
142
|
+
|
|
143
|
+
#syncCollapsedFromWidth() {
|
|
144
|
+
const w = this.getBoundingClientRect().width;
|
|
145
|
+
this.collapsed = w <= SNAP_THRESHOLD;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Resize drag handle ──
|
|
149
|
+
|
|
150
|
+
#setupResizeHandle() {
|
|
151
|
+
const handle = this.querySelector(':scope > [data-resize]');
|
|
152
|
+
if (!handle) return;
|
|
153
|
+
|
|
154
|
+
const slot = this.getAttribute('slot');
|
|
155
|
+
const isLeading = slot === 'leading';
|
|
156
|
+
|
|
157
|
+
const onPointerDown = (e) => {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
handle.setPointerCapture(e.pointerId);
|
|
160
|
+
const startX = e.clientX;
|
|
161
|
+
const startW = this.getBoundingClientRect().width;
|
|
162
|
+
this.resizing = true;
|
|
163
|
+
document.documentElement.style.cursor = 'col-resize';
|
|
164
|
+
|
|
165
|
+
const onMove = (e) => {
|
|
166
|
+
const dx = e.clientX - startX;
|
|
167
|
+
const max = parseInt(getComputedStyle(this).getPropertyValue('max-width')) || 480;
|
|
168
|
+
const w = Math.max(48, Math.min(max, startW + (isLeading ? dx : -dx)));
|
|
169
|
+
this.style.width = `${w}px`;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const onUp = () => {
|
|
173
|
+
this.resizing = false;
|
|
174
|
+
document.documentElement.style.cursor = '';
|
|
175
|
+
handle.removeEventListener('pointermove', onMove);
|
|
176
|
+
handle.removeEventListener('pointerup', onUp);
|
|
177
|
+
|
|
178
|
+
// Snap logic
|
|
179
|
+
const w = this.getBoundingClientRect().width;
|
|
180
|
+
if (w <= SNAP_THRESHOLD) {
|
|
181
|
+
this.style.width = this.minWidth || getComputedStyle(this).minWidth;
|
|
182
|
+
} else if (w < SNAP_MIN_USABLE) {
|
|
183
|
+
this.style.width = `${SNAP_MIN_USABLE}px`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.#persist(this.style.width);
|
|
187
|
+
this.#syncCollapsedFromWidth();
|
|
188
|
+
this.dispatchEvent(new CustomEvent('sidebar-resize', {
|
|
189
|
+
bubbles: true,
|
|
190
|
+
detail: { name: this.name || slot, width: this.getBoundingClientRect().width },
|
|
191
|
+
}));
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
handle.addEventListener('pointermove', onMove);
|
|
195
|
+
handle.addEventListener('pointerup', onUp);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
handle.addEventListener('pointerdown', onPointerDown);
|
|
199
|
+
this.#resizeCleanups.push(() => handle.removeEventListener('pointerdown', onPointerDown));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Child ResizeObserver — flips select-ui placement in narrow mode ──
|
|
203
|
+
|
|
204
|
+
#setupChildResizeObserver() {
|
|
205
|
+
this.#childRO = new ResizeObserver((entries) => {
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
const narrow = entry.contentBoxSize[0].inlineSize <= SNAP_THRESHOLD;
|
|
208
|
+
for (const sel of this.querySelectorAll('select-ui')) {
|
|
209
|
+
sel.setAttribute('placement', narrow ? 'right' : 'bottom-start');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
this.#childRO.observe(this);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Internal — dispatch the toggle event in a consistent shape ──
|
|
217
|
+
|
|
218
|
+
#dispatchToggle(expanded) {
|
|
219
|
+
this.dispatchEvent(new CustomEvent('sidebar-toggle', {
|
|
220
|
+
bubbles: true,
|
|
221
|
+
detail: { name: this.name || this.getAttribute('slot') || 'default', expanded },
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
customElements.define('admin-sidebar', AdminSidebar);
|
|
227
|
+
export { AdminSidebar };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import '../../../web-components/core/element.js';
|
|
3
|
+
import './admin-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
|
+
// happy-dom doesn't ship a working ResizeObserver out-of-the-box, and
|
|
15
|
+
// returns zero from getBoundingClientRect (no layout engine). Patch both
|
|
16
|
+
// in beforeEach so connected() doesn't immediately mark every sidebar as
|
|
17
|
+
// collapsed (width <= 96).
|
|
18
|
+
let originalRect;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
document.body.innerHTML = '';
|
|
21
|
+
try { localStorage.clear(); } catch {}
|
|
22
|
+
globalThis.ResizeObserver = class {
|
|
23
|
+
observe() {}
|
|
24
|
+
unobserve() {}
|
|
25
|
+
disconnect() {}
|
|
26
|
+
};
|
|
27
|
+
// Mock getBoundingClientRect to derive width from inline style (or 240 default)
|
|
28
|
+
originalRect = HTMLElement.prototype.getBoundingClientRect;
|
|
29
|
+
HTMLElement.prototype.getBoundingClientRect = function () {
|
|
30
|
+
const inline = this.style?.width || '';
|
|
31
|
+
const w = parseFloat(inline) || 240;
|
|
32
|
+
return { width: w, height: 600, top: 0, left: 0, right: w, bottom: 600, x: 0, y: 0 };
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
if (originalRect) HTMLElement.prototype.getBoundingClientRect = originalRect;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('admin-sidebar', () => {
|
|
41
|
+
it('registers admin-sidebar as a custom element', () => {
|
|
42
|
+
expect(customElements.get('admin-sidebar')).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('defaults to collapsed=false on connect', () => {
|
|
46
|
+
const sb = mount('<admin-sidebar slot="leading"></admin-sidebar>');
|
|
47
|
+
expect(sb.collapsed).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('reflects [collapsed] via property assignment', async () => {
|
|
51
|
+
const sb = mount('<admin-sidebar slot="leading"></admin-sidebar>');
|
|
52
|
+
sb.collapsed = true;
|
|
53
|
+
await tick();
|
|
54
|
+
expect(sb.hasAttribute('collapsed')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('reflects [resizing] via property assignment', async () => {
|
|
58
|
+
const sb = mount('<admin-sidebar slot="leading"></admin-sidebar>');
|
|
59
|
+
sb.resizing = true;
|
|
60
|
+
await tick();
|
|
61
|
+
expect(sb.hasAttribute('resizing')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('exposes .toggle() / .collapse() / .expand() public methods', () => {
|
|
65
|
+
const sb = mount('<admin-sidebar slot="leading" collapsible></admin-sidebar>');
|
|
66
|
+
expect(typeof sb.toggle).toBe('function');
|
|
67
|
+
expect(typeof sb.collapse).toBe('function');
|
|
68
|
+
expect(typeof sb.expand).toBe('function');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('persists width to localStorage on collapse, restores on connect', async () => {
|
|
72
|
+
// First mount: set a width, collapse, persisted to localStorage
|
|
73
|
+
const sb1 = mount('<admin-sidebar slot="leading" collapsible></admin-sidebar>');
|
|
74
|
+
sb1.style.width = '240px';
|
|
75
|
+
// happy-dom doesn't compute layout; mock getBoundingClientRect to return our set width
|
|
76
|
+
sb1.getBoundingClientRect = () => ({ width: 240 });
|
|
77
|
+
sb1.collapse();
|
|
78
|
+
expect(sb1.collapsed).toBe(true);
|
|
79
|
+
// localStorage should hold either the floor or the previous width
|
|
80
|
+
const stored = localStorage.getItem('adia-sidebar-leading');
|
|
81
|
+
expect(stored).not.toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('uses [name] override for the localStorage key', () => {
|
|
85
|
+
const sb = mount('<admin-sidebar slot="leading" name="custom-id" collapsible></admin-sidebar>');
|
|
86
|
+
sb.getBoundingClientRect = () => ({ width: 200 });
|
|
87
|
+
sb.style.width = '200px';
|
|
88
|
+
sb.collapse();
|
|
89
|
+
expect(localStorage.getItem('adia-sidebar-custom-id')).not.toBeNull();
|
|
90
|
+
expect(localStorage.getItem('adia-sidebar-leading')).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('toggle() returns the new collapsed value', () => {
|
|
94
|
+
const sb = mount('<admin-sidebar slot="leading" collapsible></admin-sidebar>');
|
|
95
|
+
sb.getBoundingClientRect = () => ({ width: 200 });
|
|
96
|
+
sb.style.width = '200px';
|
|
97
|
+
const result = sb.toggle();
|
|
98
|
+
expect(result).toBe(sb.collapsed);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('dispatches sidebar-toggle event on toggle()', () => {
|
|
102
|
+
const sb = mount('<admin-sidebar slot="leading" collapsible></admin-sidebar>');
|
|
103
|
+
sb.getBoundingClientRect = () => ({ width: 200 });
|
|
104
|
+
sb.style.width = '200px';
|
|
105
|
+
const onToggle = vi.fn();
|
|
106
|
+
sb.addEventListener('sidebar-toggle', onToggle);
|
|
107
|
+
sb.toggle();
|
|
108
|
+
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(onToggle.mock.calls[0][0].detail).toEqual(
|
|
110
|
+
expect.objectContaining({ name: expect.any(String), expanded: expect.any(Boolean) })
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('cleans up resize handlers on disconnect (no zombie listeners)', () => {
|
|
115
|
+
const sb = mount('<admin-sidebar slot="leading" resizable><div data-resize></div></admin-sidebar>');
|
|
116
|
+
const handle = sb.querySelector('[data-resize]');
|
|
117
|
+
const removeSpy = vi.spyOn(handle, 'removeEventListener');
|
|
118
|
+
sb.remove();
|
|
119
|
+
// disconnected() runs cleanup; pointerdown listener should have been removed
|
|
120
|
+
const removedTypes = removeSpy.mock.calls.map((args) => args[0]);
|
|
121
|
+
expect(removedTypes).toContain('pointerdown');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: AdminSidebar
|
|
4
|
+
tag: admin-sidebar
|
|
5
|
+
component: AdminSidebar
|
|
6
|
+
category: layout
|
|
7
|
+
version: 1
|
|
8
|
+
description: |
|
|
9
|
+
Module-tier shell sidebar — owns resize, snap-to-collapsed, persistence,
|
|
10
|
+
and the [collapsed] reflected attribute. Sits inside <admin-shell> as
|
|
11
|
+
slot="leading" or slot="trailing". Authors compose chrome bars + content
|
|
12
|
+
inside via slot vocabulary.
|
|
13
|
+
|
|
14
|
+
This is the bespoke web-component replacement for the legacy
|
|
15
|
+
<aside data-sidebar> shape. <admin-shell> still recognizes the legacy
|
|
16
|
+
shape via :is() selector for backwards compat. New code should prefer
|
|
17
|
+
<admin-sidebar>.
|
|
18
|
+
|
|
19
|
+
props:
|
|
20
|
+
collapsed:
|
|
21
|
+
description: |
|
|
22
|
+
Reflected — set when the sidebar's measured width is at or below
|
|
23
|
+
96px. Consumers query this to style "collapsed mode" without
|
|
24
|
+
duplicating threshold math.
|
|
25
|
+
type: boolean
|
|
26
|
+
default: false
|
|
27
|
+
reflect: true
|
|
28
|
+
|
|
29
|
+
resizing:
|
|
30
|
+
description: |
|
|
31
|
+
Reflected — set during an active pointer-drag on the resize handle.
|
|
32
|
+
Useful for suppressing transitions while dragging.
|
|
33
|
+
type: boolean
|
|
34
|
+
default: false
|
|
35
|
+
reflect: true
|
|
36
|
+
|
|
37
|
+
resizable:
|
|
38
|
+
description: |
|
|
39
|
+
Opts in to drag-to-resize behavior. Author supplies a child
|
|
40
|
+
[data-resize] element as the drag handle.
|
|
41
|
+
type: boolean
|
|
42
|
+
default: false
|
|
43
|
+
reflect: true
|
|
44
|
+
|
|
45
|
+
collapsible:
|
|
46
|
+
description: |
|
|
47
|
+
Opts in to programmatic collapse — toggle button wiring + the
|
|
48
|
+
.toggle() / .collapse() / .expand() public methods.
|
|
49
|
+
type: boolean
|
|
50
|
+
default: false
|
|
51
|
+
reflect: true
|
|
52
|
+
|
|
53
|
+
name:
|
|
54
|
+
description: |
|
|
55
|
+
Identifier for localStorage namespacing. Defaults to slot value
|
|
56
|
+
("leading" or "trailing"). Override when running multiple sidebars
|
|
57
|
+
with the same slot.
|
|
58
|
+
type: string
|
|
59
|
+
default: ""
|
|
60
|
+
reflect: true
|
|
61
|
+
|
|
62
|
+
min-width:
|
|
63
|
+
description: |
|
|
64
|
+
Optional override for the snap-floor width. Defaults to reading
|
|
65
|
+
CSS min-width via getComputedStyle.
|
|
66
|
+
type: string
|
|
67
|
+
default: ""
|
|
68
|
+
reflect: true
|
|
69
|
+
|
|
70
|
+
events:
|
|
71
|
+
sidebar-toggle:
|
|
72
|
+
description: Bubbles when sidebar collapses or expands.
|
|
73
|
+
detail:
|
|
74
|
+
name: string
|
|
75
|
+
expanded: boolean
|
|
76
|
+
sidebar-resize:
|
|
77
|
+
description: Bubbles when an active pointer-drag releases.
|
|
78
|
+
detail:
|
|
79
|
+
name: string
|
|
80
|
+
width: number
|
|
81
|
+
|
|
82
|
+
slots:
|
|
83
|
+
default:
|
|
84
|
+
description: >-
|
|
85
|
+
Default content — typically a <nav-ui> for navigation, or a list
|
|
86
|
+
/ tree for the trailing inspector pattern.
|
|
87
|
+
header:
|
|
88
|
+
description: >-
|
|
89
|
+
Top chrome bar (workspace select, breadcrumb, etc.).
|
|
90
|
+
footer:
|
|
91
|
+
description: >-
|
|
92
|
+
Bottom chrome bar (user select, status indicator, etc.).
|
|
93
|
+
|
|
94
|
+
states:
|
|
95
|
+
- name: idle
|
|
96
|
+
description: Default, expanded.
|
|
97
|
+
- name: collapsed
|
|
98
|
+
attribute: collapsed
|
|
99
|
+
description: Sidebar width is at or below the snap threshold; CSS
|
|
100
|
+
container queries flip child layout to icon-only mode.
|
|
101
|
+
- name: resizing
|
|
102
|
+
attribute: resizing
|
|
103
|
+
description: Active pointer-drag in progress.
|
|
104
|
+
|
|
105
|
+
traits: []
|
|
106
|
+
|
|
107
|
+
a2ui:
|
|
108
|
+
rules:
|
|
109
|
+
- >-
|
|
110
|
+
admin-sidebar is the bespoke replacement for legacy
|
|
111
|
+
<aside data-sidebar>. Use slot="leading" or slot="trailing"
|
|
112
|
+
to position. Add resizable + collapsible attributes to opt in
|
|
113
|
+
to interactive behaviors.
|
|
114
|
+
- >-
|
|
115
|
+
For chrome bars inside the sidebar, prefer <admin-topbar
|
|
116
|
+
slot="header"> and <admin-statusbar slot="footer"> over raw
|
|
117
|
+
<header-ui> / <footer-ui> when authoring shell-tier markup.
|
|
118
|
+
|
|
119
|
+
keywords:
|
|
120
|
+
- admin-sidebar
|
|
121
|
+
- sidebar
|
|
122
|
+
- aside
|
|
123
|
+
- rail
|
|
124
|
+
- panel
|
|
125
|
+
- leading
|
|
126
|
+
- trailing
|
|
127
|
+
- inspector
|
|
128
|
+
- nav
|
|
129
|
+
|
|
130
|
+
synonyms:
|
|
131
|
+
sidebar: [aside, rail, panel]
|
|
132
|
+
collapsed: [minimized, narrow, icon-only]
|
|
133
|
+
|
|
134
|
+
related:
|
|
135
|
+
- AdminShell
|
|
136
|
+
- AdminCommand
|
|
137
|
+
- Aside
|
|
138
|
+
- Nav
|
|
139
|
+
- NavGroup
|
|
140
|
+
- NavItem
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/AdminStatusbar.json",
|
|
4
|
+
"title": "AdminStatusbar",
|
|
5
|
+
"description": "Module-tier shell bottom chrome bar. CSS-only — no behavior, no JS.\nSame shape as <admin-topbar> (icon + heading + description + action\nclusters via slot vocabulary), conventionally used for read-only\nstatus — connection state, sync indicator, footer metadata.\n\nReplaces legacy <footer-ui> usage at shell-tier. Both still work per\nADR-0023 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": "AdminStatusbar"
|
|
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
|
+
"admin-statusbar",
|
|
31
|
+
"statusbar",
|
|
32
|
+
"footer-bar",
|
|
33
|
+
"app-footer",
|
|
34
|
+
"status-line"
|
|
35
|
+
],
|
|
36
|
+
"name": "AdminStatusbar",
|
|
37
|
+
"related": [
|
|
38
|
+
"AdminShell",
|
|
39
|
+
"AdminContent",
|
|
40
|
+
"AdminTopbar",
|
|
41
|
+
"Footer"
|
|
42
|
+
],
|
|
43
|
+
"slots": {
|
|
44
|
+
"description": {
|
|
45
|
+
"description": "Secondary metadata."
|
|
46
|
+
},
|
|
47
|
+
"default": {
|
|
48
|
+
"description": "Default content — status text or inline children."
|
|
49
|
+
},
|
|
50
|
+
"action": {
|
|
51
|
+
"description": "Trailing control cluster."
|
|
52
|
+
},
|
|
53
|
+
"action-leading": {
|
|
54
|
+
"description": "Leading control cluster."
|
|
55
|
+
},
|
|
56
|
+
"heading": {
|
|
57
|
+
"description": "Primary label."
|
|
58
|
+
},
|
|
59
|
+
"icon": {
|
|
60
|
+
"description": "Leading glyph (icon-ui or img)."
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"states": [
|
|
64
|
+
{
|
|
65
|
+
"description": "Default, the only state.",
|
|
66
|
+
"name": "idle"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"synonyms": {
|
|
70
|
+
"statusbar": [
|
|
71
|
+
"status-line",
|
|
72
|
+
"footer-bar",
|
|
73
|
+
"app-footer"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"tag": "admin-statusbar",
|
|
77
|
+
"tokens": {},
|
|
78
|
+
"traits": [],
|
|
79
|
+
"version": 1
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<header>
|
|
2
|
+
<div>
|
|
3
|
+
<h1>Admin Statusbar</h1>
|
|
4
|
+
<div data-actions>
|
|
5
|
+
<tag-ui size="sm">admin-statusbar</tag-ui>
|
|
6
|
+
<tag-ui size="sm" variant="ghost">CSS-only</tag-ui>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
<p>Module-tier shell bottom chrome bar. Same shape as admin-topbar; conventionally read-only status.</p>
|
|
10
|
+
</header>
|
|
11
|
+
|
|
12
|
+
<section data-section>
|
|
13
|
+
<h2 variant="section">Role</h2>
|
|
14
|
+
<p>This is a CSS-only structural stub — no JavaScript, no behavior. The shell host (<code><admin-shell></code>) styles it via tag-presence. Authors compose it with sibling bespoke children to express semantic shell-tier structure.</p>
|
|
15
|
+
</section>
|
|
16
|
+
|
|
17
|
+
<section data-section>
|
|
18
|
+
<h2 variant="section">Composition</h2>
|
|
19
|
+
<p>Typical placement inside <code><admin-shell></code>:</p>
|
|
20
|
+
<code-ui language="html"><admin-statusbar>
|
|
21
|
+
<icon-ui slot="icon" name="check-circle"></icon-ui>
|
|
22
|
+
<span>Synced just now · 12 changes</span>
|
|
23
|
+
</admin-statusbar></code-ui>
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
<section data-section>
|
|
27
|
+
<h2 variant="section">Slot vocabulary</h2>
|
|
28
|
+
<p>See the <a href="../admin-shell/admin-shell.html"><code>admin-shell</code></a> demo for the full composition pattern. CSS-only stubs declare slot intent; the parent shell handles layout.</p>
|
|
29
|
+
</section>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="auto">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Admin Statusbar — AdiaUI</title>
|
|
7
|
+
|
|
8
|
+
<link rel="stylesheet" href="../../../web-components/styles/resets.css">
|
|
9
|
+
<link rel="stylesheet" href="../../../web-components/styles/tokens.css">
|
|
10
|
+
<link rel="stylesheet" href="../admin-shell/admin-shell.css">
|
|
11
|
+
<link rel="stylesheet" href="../../../web-components/components/code/code.css">
|
|
12
|
+
<link rel="stylesheet" href="../../../web-components/components/tag/tag.css">
|
|
13
|
+
|
|
14
|
+
<script type="module" src="../admin-shell/admin-shell.js"></script>
|
|
15
|
+
<script type="module" src="../../../web-components/components/code/code.js"></script>
|
|
16
|
+
<script type="module" src="../../../web-components/components/tag/tag.js"></script>
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
:where(html, body) { margin: 0; min-height: 100vh; background: var(--a-bg); color: var(--a-fg); font-family: var(--a-font); }
|
|
20
|
+
main { max-width: 960px; margin-inline: auto; padding: var(--a-space-6) var(--a-space-5); }
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
|
|
25
|
+
<main id="demo-root">
|
|
26
|
+
<p>Loading examples…</p>
|
|
27
|
+
</main>
|
|
28
|
+
|
|
29
|
+
<script type="module">
|
|
30
|
+
const root = document.getElementById('demo-root');
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch('./admin-statusbar.examples.html');
|
|
33
|
+
if (!res.ok) throw new Error(`fetch failed (${res.status})`);
|
|
34
|
+
root.innerHTML = await res.text();
|
|
35
|
+
} catch (err) {
|
|
36
|
+
root.innerHTML = `<p style="color:var(--a-danger-strong);">Failed to load admin-statusbar.examples.html — ${err.message}</p>`;
|
|
37
|
+
console.error('[admin-statusbar.html]', err);
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|