@adia-ai/web-components 0.0.23 → 0.0.25
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/components/app-shell/app-shell.a2ui.json +136 -0
- package/components/app-shell/app-shell.css +16 -0
- package/components/app-shell/app-shell.js +202 -0
- package/components/app-shell/app-shell.yaml +183 -0
- package/components/aside/aside.a2ui.json +84 -0
- package/components/aside/aside.yaml +100 -0
- package/components/button/button.css +7 -5
- package/components/check/check.css +24 -27
- package/components/drawer/drawer.css +356 -349
- package/components/drawer/drawer.js +44 -11
- package/components/footer/footer.a2ui.json +1 -1
- package/components/footer/footer.yaml +1 -1
- package/components/header/header.a2ui.json +2 -2
- package/components/header/header.yaml +2 -2
- package/components/index.js +2 -0
- package/components/input/input.css +13 -11
- package/components/kbd/kbd.css +1 -1
- package/components/modal/modal.js +12 -11
- package/components/option-card/option-card.css +28 -36
- package/components/page/page.a2ui.json +107 -0
- package/components/page/page.css +68 -0
- package/components/page/page.js +88 -0
- package/components/page/page.yaml +148 -0
- package/components/radio/radio.css +13 -14
- package/components/range/range.css +8 -4
- package/components/section/section.a2ui.json +1 -1
- package/components/section/section.yaml +1 -1
- package/components/segment/segment.css +7 -7
- package/components/switch/switch.css +13 -11
- package/components/textarea/textarea.css +10 -5
- package/components/toast/toast.css +27 -26
- package/components/toggle-group/toggle-group.css +4 -3
- package/components/tree/tree.css +21 -9
- package/package.json +1 -1
- package/patterns/app-nav-item/app-nav-item.css +24 -24
- package/patterns/app-shell/app-shell.css +12 -0
- package/patterns/section-nav-item/section-nav-item.css +23 -24
- package/styles/components.css +2 -0
|
@@ -36,7 +36,6 @@ class AdiaDrawer extends AdiaElement {
|
|
|
36
36
|
#closing = false;
|
|
37
37
|
#previousFocus = null;
|
|
38
38
|
#closeTimer = null;
|
|
39
|
-
#openRaf = null;
|
|
40
39
|
#dialogRef = null;
|
|
41
40
|
|
|
42
41
|
static properties = {
|
|
@@ -60,6 +59,28 @@ class AdiaDrawer extends AdiaElement {
|
|
|
60
59
|
// html`` result would trigger stamp() → replaceChildren(), wiping authored
|
|
61
60
|
// [slot=header|body|footer] before render() can migrate them into the panel.
|
|
62
61
|
|
|
62
|
+
constructor() {
|
|
63
|
+
super();
|
|
64
|
+
// Safari requires <dialog>.showModal() to be invoked synchronously inside
|
|
65
|
+
// the click handler. The reactive system schedules render() in a microtask
|
|
66
|
+
// after the property change, which Safari treats as outside the user-gesture
|
|
67
|
+
// window and silently no-ops the showModal. Wrap the auto-installed `open`
|
|
68
|
+
// setter so dialog state syncs in the same synchronous frame as the
|
|
69
|
+
// assignment. See docs/BROWSER-COMPAT.md §3a (Flavor C).
|
|
70
|
+
const desc = Object.getOwnPropertyDescriptor(this, 'open');
|
|
71
|
+
if (desc?.set) {
|
|
72
|
+
const origSet = desc.set;
|
|
73
|
+
Object.defineProperty(this, 'open', {
|
|
74
|
+
get: desc.get,
|
|
75
|
+
set: (v) => {
|
|
76
|
+
origSet.call(this, v);
|
|
77
|
+
this.#syncDialog();
|
|
78
|
+
},
|
|
79
|
+
configurable: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
#onPress = (e) => {
|
|
64
85
|
if (e.target.closest('[slot="close"]')) this.open = false;
|
|
65
86
|
};
|
|
@@ -101,10 +122,6 @@ class AdiaDrawer extends AdiaElement {
|
|
|
101
122
|
this.#dialogRef.removeEventListener('close', this.#onDialogClose);
|
|
102
123
|
this.#dialogRef.removeEventListener('click', this.#onDialogClick);
|
|
103
124
|
}
|
|
104
|
-
if (this.#openRaf != null) {
|
|
105
|
-
cancelAnimationFrame(this.#openRaf);
|
|
106
|
-
this.#openRaf = null;
|
|
107
|
-
}
|
|
108
125
|
if (this.#closeTimer != null) {
|
|
109
126
|
clearTimeout(this.#closeTimer);
|
|
110
127
|
this.#closeTimer = null;
|
|
@@ -228,15 +245,26 @@ class AdiaDrawer extends AdiaElement {
|
|
|
228
245
|
if (userFooter.parentElement !== panel) panel.appendChild(userFooter);
|
|
229
246
|
}
|
|
230
247
|
|
|
231
|
-
// Sync open state
|
|
248
|
+
// Sync open state — also syncs synchronously from the `open` setter
|
|
249
|
+
// (see constructor) so showModal() runs in the click handler's gesture
|
|
250
|
+
// frame on Safari.
|
|
251
|
+
this.#syncDialog();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#syncDialog() {
|
|
255
|
+
const dialog = this.#dialogRef;
|
|
256
|
+
if (!dialog) return;
|
|
232
257
|
if (this.open && !dialog.open) {
|
|
233
258
|
this.#closing = false;
|
|
234
259
|
this.#previousFocus = document.activeElement;
|
|
235
260
|
dialog.showModal();
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
261
|
+
// Synchronous reflow instead of rAF — Safari throttles
|
|
262
|
+
// requestAnimationFrame when a top-layer dialog is open, sometimes
|
|
263
|
+
// delaying [data-open] (and the slide-in transition) by tens of
|
|
264
|
+
// seconds. Forcing a reflow keeps the animation start in the same
|
|
265
|
+
// synchronous frame. See docs/BROWSER-COMPAT.md §3a (Flavor C).
|
|
266
|
+
void dialog.offsetHeight;
|
|
267
|
+
dialog.setAttribute('data-open', '');
|
|
240
268
|
} else if (!this.open && dialog.open && !this.#closing) {
|
|
241
269
|
this.#animateClose(dialog);
|
|
242
270
|
}
|
|
@@ -244,8 +272,13 @@ class AdiaDrawer extends AdiaElement {
|
|
|
244
272
|
|
|
245
273
|
#animateClose(dialog) {
|
|
246
274
|
this.#closing = true;
|
|
247
|
-
|
|
275
|
+
// Set [data-closing] FIRST (carries the transition spec), force a
|
|
276
|
+
// reflow, THEN remove [data-open]. If both attribute changes batch
|
|
277
|
+
// into a single style update, Safari can skip the slide-out animation
|
|
278
|
+
// entirely.
|
|
248
279
|
dialog.setAttribute('data-closing', '');
|
|
280
|
+
void dialog.offsetHeight;
|
|
281
|
+
dialog.removeAttribute('data-open');
|
|
249
282
|
|
|
250
283
|
this.#closeTimer = setTimeout(() => {
|
|
251
284
|
this.#closeTimer = null;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://adiaui.dev/a2ui/v0_9/components/Footer.json",
|
|
4
4
|
"title": "Footer",
|
|
5
|
-
"description": "Card
|
|
5
|
+
"description": "Footer — styled by closest container parent (Card / Drawer / Modal / Page / AppShell). Contains actions, pagination, or summary. Typically holds Buttons.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"allOf": [
|
|
8
8
|
{
|
|
@@ -6,7 +6,7 @@ tag: footer-ui
|
|
|
6
6
|
component: Footer
|
|
7
7
|
category: container
|
|
8
8
|
version: 1
|
|
9
|
-
description: Card
|
|
9
|
+
description: Footer — styled by closest container parent (Card / Drawer / Modal / Page / AppShell). Contains actions, pagination, or summary. Typically holds Buttons.
|
|
10
10
|
props:
|
|
11
11
|
justify:
|
|
12
12
|
description: Horizontal alignment of children
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://adiaui.dev/a2ui/v0_9/components/Header.json",
|
|
4
4
|
"title": "Header",
|
|
5
|
-
"description": "Card
|
|
5
|
+
"description": "Header — styled by closest container parent (Card / Drawer / Modal / Page / AppShell). Contains heading text and optional action slot.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"allOf": [
|
|
8
8
|
{
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"const": "Header"
|
|
18
18
|
},
|
|
19
19
|
"padding": {
|
|
20
|
-
"description": "Bare attribute — enables default header padding.
|
|
20
|
+
"description": "Bare attribute — enables default header padding. The container parent's own `padding` prop sets the scale.",
|
|
21
21
|
"type": "boolean",
|
|
22
22
|
"default": false
|
|
23
23
|
}
|
|
@@ -6,10 +6,10 @@ tag: header-ui
|
|
|
6
6
|
component: Header
|
|
7
7
|
category: container
|
|
8
8
|
version: 1
|
|
9
|
-
description: Card
|
|
9
|
+
description: Header — styled by closest container parent (Card / Drawer / Modal / Page / AppShell). Contains heading text and optional action slot.
|
|
10
10
|
props:
|
|
11
11
|
padding:
|
|
12
|
-
description: Bare attribute — enables default header padding.
|
|
12
|
+
description: Bare attribute — enables default header padding. The container parent's own `padding` prop sets the scale.
|
|
13
13
|
type: boolean
|
|
14
14
|
default: false
|
|
15
15
|
reflect: true
|
package/components/index.js
CHANGED
|
@@ -24,6 +24,8 @@ export { AdiaSegmented } from './segmented/segmented.js';
|
|
|
24
24
|
export { AdiaRange } from './range/range.js';
|
|
25
25
|
export { AdiaTree, AdiaTreeItem } from './tree/tree.js';
|
|
26
26
|
export { AdiaPane } from './pane/pane.js';
|
|
27
|
+
export { AdiaAppShell } from './app-shell/app-shell.js';
|
|
28
|
+
export { AdiaPage } from './page/page.js';
|
|
27
29
|
export { AdiaChatInput } from './chat/chat-input.js';
|
|
28
30
|
export { AdiaChat } from './chat/chat.js';
|
|
29
31
|
export { AdiaDrawer } from './drawer/drawer.js';
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
/* Safari 17.x bug: `:scope[attr]:hover`
|
|
2
|
-
scope root. Plain
|
|
1
|
+
/* Safari 17.x bug: `:scope[attr]:hover` and `:scope:not(...) [descendant]:hover`
|
|
2
|
+
inside `@scope` don't match the scope root. Plain selectors outside work.
|
|
3
|
+
See docs/BROWSER-COMPAT.md §3a. */
|
|
3
4
|
input-ui[variant="ghost"]:hover {
|
|
4
5
|
--input-bg: var(--a-bg-muted);
|
|
5
6
|
}
|
|
7
|
+
input-ui:not([disabled]) [slot="field"]:hover {
|
|
8
|
+
background: var(--input-bg-hover);
|
|
9
|
+
border-color: var(--input-border-hover);
|
|
10
|
+
color: var(--input-fg-hover);
|
|
11
|
+
}
|
|
12
|
+
input-ui:not([disabled]) [slot="field"]:hover [slot="prefix"],
|
|
13
|
+
input-ui:not([disabled]) [slot="field"]:hover [slot="suffix"] {
|
|
14
|
+
color: var(--input-affix-fg-hover);
|
|
15
|
+
}
|
|
6
16
|
|
|
7
17
|
@scope (input-ui) {
|
|
8
18
|
:where(:scope) {
|
|
@@ -83,15 +93,7 @@ input-ui[variant="ghost"]:hover {
|
|
|
83
93
|
color var(--input-duration) var(--input-easing),
|
|
84
94
|
box-shadow var(--input-duration) var(--input-easing);
|
|
85
95
|
}
|
|
86
|
-
|
|
87
|
-
background: var(--input-bg-hover);
|
|
88
|
-
border-color: var(--input-border-hover);
|
|
89
|
-
color: var(--input-fg-hover);
|
|
90
|
-
}
|
|
91
|
-
:scope:not([disabled]) [slot="field"]:hover [slot="prefix"],
|
|
92
|
-
:scope:not([disabled]) [slot="field"]:hover [slot="suffix"] {
|
|
93
|
-
color: var(--input-affix-fg-hover);
|
|
94
|
-
}
|
|
96
|
+
/* hover rules moved outside @scope — see Safari 17.x bug note at top. */
|
|
95
97
|
:scope:not([disabled]):focus-within [slot="field"] {
|
|
96
98
|
/* Canonical ring — consumes the L3 --input-focus-ring token
|
|
97
99
|
which aliases --a-focus-ring. Border stays stable; the ring
|
package/components/kbd/kbd.css
CHANGED
|
@@ -36,7 +36,6 @@ class AdiaModal extends AdiaElement {
|
|
|
36
36
|
#closing = false;
|
|
37
37
|
#previousFocus = null;
|
|
38
38
|
#closeTimer = null;
|
|
39
|
-
#openRaf = null;
|
|
40
39
|
#dialogRef = null;
|
|
41
40
|
|
|
42
41
|
static properties = {
|
|
@@ -101,10 +100,6 @@ class AdiaModal extends AdiaElement {
|
|
|
101
100
|
this.#dialogRef.removeEventListener('close', this.#onDialogClose);
|
|
102
101
|
this.#dialogRef.removeEventListener('click', this.#onDialogClick);
|
|
103
102
|
}
|
|
104
|
-
if (this.#openRaf != null) {
|
|
105
|
-
cancelAnimationFrame(this.#openRaf);
|
|
106
|
-
this.#openRaf = null;
|
|
107
|
-
}
|
|
108
103
|
if (this.#closeTimer != null) {
|
|
109
104
|
clearTimeout(this.#closeTimer);
|
|
110
105
|
this.#closeTimer = null;
|
|
@@ -185,15 +180,16 @@ class AdiaModal extends AdiaElement {
|
|
|
185
180
|
if (footer.parentElement !== panel) panel.appendChild(footer);
|
|
186
181
|
}
|
|
187
182
|
|
|
188
|
-
// Sync open state
|
|
183
|
+
// Sync open state. Synchronous reflow instead of rAF — Safari throttles
|
|
184
|
+
// requestAnimationFrame when a top-layer dialog is open, sometimes
|
|
185
|
+
// delaying [data-open] (and the scale-in transition) by tens of seconds.
|
|
186
|
+
// See docs/BROWSER-COMPAT.md §3a (Flavor C).
|
|
189
187
|
if (this.open && !dialog.open) {
|
|
190
188
|
this.#closing = false;
|
|
191
189
|
this.#previousFocus = document.activeElement;
|
|
192
190
|
dialog.showModal();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
dialog.setAttribute('data-open', '');
|
|
196
|
-
});
|
|
191
|
+
void dialog.offsetHeight;
|
|
192
|
+
dialog.setAttribute('data-open', '');
|
|
197
193
|
} else if (!this.open && dialog.open && !this.#closing) {
|
|
198
194
|
this.#animateClose(dialog);
|
|
199
195
|
}
|
|
@@ -201,8 +197,13 @@ class AdiaModal extends AdiaElement {
|
|
|
201
197
|
|
|
202
198
|
#animateClose(dialog) {
|
|
203
199
|
this.#closing = true;
|
|
204
|
-
|
|
200
|
+
// Set [data-closing] FIRST (carries the transition spec), force a
|
|
201
|
+
// reflow, THEN remove [data-open]. If both attribute changes batch
|
|
202
|
+
// into a single style update, Safari can skip the fade-out animation
|
|
203
|
+
// entirely.
|
|
205
204
|
dialog.setAttribute('data-closing', '');
|
|
205
|
+
void dialog.offsetHeight;
|
|
206
|
+
dialog.removeAttribute('data-open');
|
|
206
207
|
|
|
207
208
|
this.#closeTimer = setTimeout(() => {
|
|
208
209
|
this.#closeTimer = null;
|
|
@@ -1,10 +1,32 @@
|
|
|
1
|
-
/* Safari 17.x bug: `:scope:not(...):hover`
|
|
2
|
-
|
|
3
|
-
docs/BROWSER-COMPAT.md §3a. */
|
|
1
|
+
/* Safari 17.x bug: `:scope:not(...):hover` (Flavor A) and `:scope[checked]`
|
|
2
|
+
(Flavor B — attribute-removal restyle) both fail inside `@scope`.
|
|
3
|
+
Selectors moved out. See docs/BROWSER-COMPAT.md §3a. */
|
|
4
4
|
option-card-ui:not([checked]):not([disabled]):hover {
|
|
5
5
|
background: var(--option-card-bg-hover);
|
|
6
6
|
border-color: var(--option-card-border-hover);
|
|
7
7
|
}
|
|
8
|
+
option-card-ui[checked] > :not([slot]) {
|
|
9
|
+
display: block;
|
|
10
|
+
}
|
|
11
|
+
option-card-ui[checked] {
|
|
12
|
+
background: var(--option-card-bg-checked);
|
|
13
|
+
border-color: var(--option-card-border-checked);
|
|
14
|
+
}
|
|
15
|
+
option-card-ui[checked]::before {
|
|
16
|
+
border-color: var(--option-card-radio-fill);
|
|
17
|
+
background:
|
|
18
|
+
radial-gradient(
|
|
19
|
+
circle,
|
|
20
|
+
var(--option-card-radio-dot) 0 30%,
|
|
21
|
+
var(--option-card-radio-fill) 30% 100%
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
option-card-ui[checked] > [slot="heading"] {
|
|
25
|
+
color: var(--option-card-heading-color-checked);
|
|
26
|
+
}
|
|
27
|
+
option-card-ui[checked] > [slot="icon"] {
|
|
28
|
+
color: var(--option-card-icon-color-checked);
|
|
29
|
+
}
|
|
8
30
|
|
|
9
31
|
@scope (option-card-ui) {
|
|
10
32
|
:where(:scope) {
|
|
@@ -142,39 +164,9 @@ option-card-ui:not([checked]):not([disabled]):hover {
|
|
|
142
164
|
:scope:has(> [slot="icon"]) > :not([slot]) {
|
|
143
165
|
grid-column: 3 / -1;
|
|
144
166
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
/* hover rule moved outside @scope — see Safari 17.x bug note at top. */
|
|
150
|
-
|
|
151
|
-
/* ── State: checked — accent border + tinted bg + filled radio.
|
|
152
|
-
The indicator becomes an accent disc with a centered dot of
|
|
153
|
-
--option-card-radio-dot at 60% of the size, mirroring
|
|
154
|
-
radio-ui's recipe (radio.css:75-78). Done with a radial
|
|
155
|
-
gradient so a single pseudo-element carries both layers. */
|
|
156
|
-
:scope[checked] {
|
|
157
|
-
background: var(--option-card-bg-checked);
|
|
158
|
-
border-color: var(--option-card-border-checked);
|
|
159
|
-
}
|
|
160
|
-
:scope[checked]::before {
|
|
161
|
-
border-color: var(--option-card-radio-fill);
|
|
162
|
-
background:
|
|
163
|
-
radial-gradient(
|
|
164
|
-
circle,
|
|
165
|
-
var(--option-card-radio-dot) 0 30%,
|
|
166
|
-
var(--option-card-radio-fill) 30% 100%
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
/* Heading + icon shift to a strong color when checked — gives the
|
|
170
|
-
selected card a clear text-level emphasis on top of the bg/border
|
|
171
|
-
state, so picking is unambiguous beyond the radio dot alone. */
|
|
172
|
-
:scope[checked] > [slot="heading"] {
|
|
173
|
-
color: var(--option-card-heading-color-checked);
|
|
174
|
-
}
|
|
175
|
-
:scope[checked] > [slot="icon"] {
|
|
176
|
-
color: var(--option-card-icon-color-checked);
|
|
177
|
-
}
|
|
167
|
+
/* hover + [checked] state rules moved outside @scope — see Safari 17.x bug note at top.
|
|
168
|
+
The :scope[checked]::before recipe lives at top-of-file: an accent disc
|
|
169
|
+
with a centered dot via radial-gradient, mirroring radio-ui's recipe. */
|
|
178
170
|
|
|
179
171
|
/* ── Layout: tile — icon top-left, indicator top-right, heading +
|
|
180
172
|
description below, all left-aligned. Used for hero pickers
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/Page.json",
|
|
4
|
+
"title": "Page",
|
|
5
|
+
"description": "Page container. Holds page-level chrome — header / content / footer —\nand manages max-width clamps, padding scale, optional scroll-container,\nand an optional sticky-header sentinel. Compose with the slot\nprimitives (`<header-ui>`, `<section-ui>`, `<footer-ui>`); the page's\n@scope rules style them. Drop in directly, or nest inside an\n`<app-shell-ui>`'s main column.\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": "Page"
|
|
18
|
+
},
|
|
19
|
+
"maxWidth": {
|
|
20
|
+
"description": "Token-bound max-width clamp. `prose` (65ch) for reading pages,\n`narrow` (80ch) for tight forms, `wide` (1080px) for data-rich\npages, `full` for unconstrained. Empty defers to parent / 100%.\nCentered horizontally via `margin-inline: auto`.\n",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"enum": [
|
|
23
|
+
"",
|
|
24
|
+
"prose",
|
|
25
|
+
"narrow",
|
|
26
|
+
"wide",
|
|
27
|
+
"full"
|
|
28
|
+
],
|
|
29
|
+
"default": ""
|
|
30
|
+
},
|
|
31
|
+
"padding": {
|
|
32
|
+
"description": "Page-padding scale from the spacing system. Accepts `0`–`8`\n(mapped to `--a-space-N`). Empty (no value) applies the\n`--page-padding-default` token; `0` removes padding.\n",
|
|
33
|
+
"type": "string",
|
|
34
|
+
"default": ""
|
|
35
|
+
},
|
|
36
|
+
"scroll": {
|
|
37
|
+
"description": "Sets the page as a scroll container. `overflow-y: auto`, full\nheight, contained overscroll. Use when the page IS the scroll\nsurface (standalone pages); leave off when nested inside a parent\nthat already manages scroll (e.g. inside an `<app-shell-ui>`'s\nmain `<section>`).\n",
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"default": false
|
|
40
|
+
},
|
|
41
|
+
"stickyHeader": {
|
|
42
|
+
"description": "Installs an IntersectionObserver sentinel before the first\n`<header>` / `<header-ui>` child. When the sentinel scrolls out\nof view the page gains `[data-header-stuck]`, which the CSS\nuses to add a border + shadow to the header. No-op when no\nheader is present.\n",
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"default": false
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"required": [
|
|
48
|
+
"component"
|
|
49
|
+
],
|
|
50
|
+
"unevaluatedProperties": false,
|
|
51
|
+
"x-adiaui": {
|
|
52
|
+
"anti_patterns": [],
|
|
53
|
+
"category": "container",
|
|
54
|
+
"events": {},
|
|
55
|
+
"examples": [
|
|
56
|
+
{
|
|
57
|
+
"description": "Reading page with sticky header, 65ch column, padding scale 6.",
|
|
58
|
+
"a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Page\",\n \"stickyHeader\": true,\n \"maxWidth\": \"prose\",\n \"padding\": \"6\",\n \"children\": [\"hdr\", \"body\"]\n },\n {\n \"id\": \"hdr\",\n \"component\": \"Header\",\n \"children\": [\"title\"]\n },\n {\n \"id\": \"title\",\n \"component\": \"Text\",\n \"variant\": \"display\",\n \"textContent\": \"Reading Page\"\n },\n {\n \"id\": \"body\",\n \"component\": \"Section\",\n \"children\": []\n }\n]",
|
|
59
|
+
"name": "prose-page"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"description": "Wide-clamp dashboard page acting as a scroll container.",
|
|
63
|
+
"a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"Page\",\n \"scroll\": true,\n \"maxWidth\": \"wide\",\n \"padding\": \"4\",\n \"children\": [\"hdr\", \"body\"]\n },\n {\n \"id\": \"hdr\",\n \"component\": \"Header\",\n \"children\": []\n },\n {\n \"id\": \"body\",\n \"component\": \"Section\",\n \"children\": []\n }\n]",
|
|
64
|
+
"name": "dashboard-page"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"keywords": [
|
|
68
|
+
"page",
|
|
69
|
+
"layout",
|
|
70
|
+
"container",
|
|
71
|
+
"scroll",
|
|
72
|
+
"sticky-header",
|
|
73
|
+
"max-width",
|
|
74
|
+
"padding",
|
|
75
|
+
"prose-page",
|
|
76
|
+
"dashboard-page"
|
|
77
|
+
],
|
|
78
|
+
"name": "AdiaPage",
|
|
79
|
+
"related": [
|
|
80
|
+
"app-shell",
|
|
81
|
+
"card",
|
|
82
|
+
"section",
|
|
83
|
+
"header",
|
|
84
|
+
"footer"
|
|
85
|
+
],
|
|
86
|
+
"slots": {
|
|
87
|
+
"default": {
|
|
88
|
+
"description": "Composes from the slot primitives — `<header-ui>` (page header),\n`<section-ui>` (main content), optional `<footer-ui>`. Native\n`<header>` / `<section>` / `<footer>` also work; the @scope rules\ntarget both via `:where(header, header-ui)`.\n"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"states": [
|
|
92
|
+
{
|
|
93
|
+
"description": "Default, ready for interaction.",
|
|
94
|
+
"name": "idle"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"description": "Header has scrolled past the sentinel; visual cue applied.",
|
|
98
|
+
"name": "header-stuck"
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"synonyms": {},
|
|
102
|
+
"tag": "page-ui",
|
|
103
|
+
"tokens": {},
|
|
104
|
+
"traits": [],
|
|
105
|
+
"version": 1
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
@scope (page-ui) {
|
|
2
|
+
:where(:scope) {
|
|
3
|
+
/* ── Max-width clamps ── */
|
|
4
|
+
--page-max-width-prose: 65ch;
|
|
5
|
+
--page-max-width-narrow: 80ch;
|
|
6
|
+
--page-max-width-wide: 1080px;
|
|
7
|
+
--page-max-width-full: 100%;
|
|
8
|
+
|
|
9
|
+
/* ── Padding default (when [padding] is set without a value) ── */
|
|
10
|
+
--page-padding-default: var(--a-space-6);
|
|
11
|
+
|
|
12
|
+
/* ── Surfaces ── */
|
|
13
|
+
--page-bg: var(--a-canvas-0);
|
|
14
|
+
--page-fg: var(--a-fg);
|
|
15
|
+
|
|
16
|
+
/* ── Sticky-header chrome (when [data-header-stuck]) ── */
|
|
17
|
+
--page-sticky-bg: var(--a-canvas-0);
|
|
18
|
+
--page-sticky-border: 1px solid var(--a-border-subtle);
|
|
19
|
+
--page-sticky-shadow: var(--a-shadow-sm);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
:scope {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
display: block;
|
|
25
|
+
width: 100%;
|
|
26
|
+
background: var(--page-bg);
|
|
27
|
+
color: var(--page-fg);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ── max-width clamps ── */
|
|
31
|
+
:scope[max-width="prose"] { max-width: var(--page-max-width-prose); margin-inline: auto; }
|
|
32
|
+
:scope[max-width="narrow"] { max-width: var(--page-max-width-narrow); margin-inline: auto; }
|
|
33
|
+
:scope[max-width="wide"] { max-width: var(--page-max-width-wide); margin-inline: auto; }
|
|
34
|
+
:scope[max-width="full"] { max-width: var(--page-max-width-full); }
|
|
35
|
+
|
|
36
|
+
/* ── Padding scale (mirrors --a-space-N) ── */
|
|
37
|
+
:scope[padding=""] { padding: var(--page-padding-default); }
|
|
38
|
+
:scope[padding="0"] { padding: 0; }
|
|
39
|
+
:scope[padding="1"] { padding: var(--a-space-1); }
|
|
40
|
+
:scope[padding="2"] { padding: var(--a-space-2); }
|
|
41
|
+
:scope[padding="3"] { padding: var(--a-space-3); }
|
|
42
|
+
:scope[padding="4"] { padding: var(--a-space-4); }
|
|
43
|
+
:scope[padding="5"] { padding: var(--a-space-5); }
|
|
44
|
+
:scope[padding="6"] { padding: var(--a-space-6); }
|
|
45
|
+
:scope[padding="7"] { padding: var(--a-space-7); }
|
|
46
|
+
:scope[padding="8"] { padding: var(--a-space-8); }
|
|
47
|
+
|
|
48
|
+
/* ── Scroll container ── */
|
|
49
|
+
:scope[scroll] {
|
|
50
|
+
overflow-y: auto;
|
|
51
|
+
height: 100%;
|
|
52
|
+
overscroll-behavior: contain;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ── Sticky-header support ── */
|
|
56
|
+
:scope[sticky-header] > :where(header, header-ui) {
|
|
57
|
+
position: sticky;
|
|
58
|
+
top: 0;
|
|
59
|
+
z-index: 1;
|
|
60
|
+
background: var(--page-sticky-bg);
|
|
61
|
+
transition: border-color 150ms ease, box-shadow 150ms ease;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:scope[data-header-stuck] > :where(header, header-ui) {
|
|
65
|
+
border-block-end: var(--page-sticky-border);
|
|
66
|
+
box-shadow: var(--page-sticky-shadow);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <page-ui> — Page container.
|
|
3
|
+
*
|
|
4
|
+
* Holds page-level chrome — header / content / footer — and manages
|
|
5
|
+
* max-width clamps, padding scale, optional scroll-container, and an
|
|
6
|
+
* optional sticky-header sentinel. Compose with the slot primitives
|
|
7
|
+
* (`<header-ui>`, `<section-ui>`, `<footer-ui>`); the page's @scope
|
|
8
|
+
* rules style them.
|
|
9
|
+
*
|
|
10
|
+
* Authoring:
|
|
11
|
+
* <page-ui sticky-header max-width="prose" padding="6">
|
|
12
|
+
* <header-ui>...</header-ui> <!-- page title + actions -->
|
|
13
|
+
* <section-ui>...</section-ui> <!-- main content -->
|
|
14
|
+
* <footer-ui>...</footer-ui> <!-- optional -->
|
|
15
|
+
* </page-ui>
|
|
16
|
+
*
|
|
17
|
+
* Attributes:
|
|
18
|
+
* scroll — boolean. Page is the scroll container (overflow-y: auto).
|
|
19
|
+
* max-width — '' | 'prose' (65ch) | 'narrow' (80ch) | 'wide' (1080px) | 'full' (100%).
|
|
20
|
+
* padding — '' | '0'..'8' (mapped to --a-space-N).
|
|
21
|
+
* sticky-header — boolean. Installs IntersectionObserver sentinel before the
|
|
22
|
+
* first <header> / <header-ui> child; sets [data-header-stuck]
|
|
23
|
+
* on the page when the sentinel scrolls out of view.
|
|
24
|
+
*
|
|
25
|
+
* ADR: .brain/adrs/0009-promote-app-shell-and-page-to-components.md.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { AdiaElement } from '../../core/element.js';
|
|
29
|
+
|
|
30
|
+
class AdiaPage extends AdiaElement {
|
|
31
|
+
static properties = {
|
|
32
|
+
scroll: { type: Boolean, default: false, reflect: true },
|
|
33
|
+
maxWidth: { type: String, default: '', attribute: 'max-width', reflect: true },
|
|
34
|
+
padding: { type: String, default: '', reflect: true },
|
|
35
|
+
stickyHeader: { type: Boolean, default: false, attribute: 'sticky-header', reflect: true },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
static template = () => null;
|
|
39
|
+
|
|
40
|
+
#sentinel = null;
|
|
41
|
+
#observer = null;
|
|
42
|
+
|
|
43
|
+
connected() {
|
|
44
|
+
if (this.stickyHeader) this.#installSticky();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
disconnected() {
|
|
48
|
+
this.#teardownSticky();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render() {
|
|
52
|
+
// Sticky-header attribute changes between renders → install / tear down.
|
|
53
|
+
if (this.stickyHeader && !this.#sentinel) this.#installSticky();
|
|
54
|
+
if (!this.stickyHeader && this.#sentinel) this.#teardownSticky();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#installSticky() {
|
|
58
|
+
const header = this.querySelector(':scope > :is(header, header-ui)');
|
|
59
|
+
if (!header) return;
|
|
60
|
+
|
|
61
|
+
if (!this.#sentinel) {
|
|
62
|
+
this.#sentinel = document.createElement('div');
|
|
63
|
+
this.#sentinel.setAttribute('data-page-sentinel', '');
|
|
64
|
+
this.#sentinel.style.cssText = 'height: 0; width: 0; pointer-events: none;';
|
|
65
|
+
header.insertAdjacentElement('beforebegin', this.#sentinel);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!this.#observer) {
|
|
69
|
+
this.#observer = new IntersectionObserver((entries) => {
|
|
70
|
+
this.toggleAttribute('data-header-stuck', !entries[0].isIntersecting);
|
|
71
|
+
}, { threshold: 0 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.#observer.observe(this.#sentinel);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#teardownSticky() {
|
|
78
|
+
this.#observer?.disconnect();
|
|
79
|
+
this.#observer = null;
|
|
80
|
+
this.#sentinel?.remove();
|
|
81
|
+
this.#sentinel = null;
|
|
82
|
+
this.removeAttribute('data-header-stuck');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
customElements.define('page-ui', AdiaPage);
|
|
87
|
+
|
|
88
|
+
export { AdiaPage };
|