@taprootio/espalier 2.1.2 → 2.2.0

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,393 @@
1
+ import { type PropertyValues } from "lit";
2
+ import { EspalierElementBase } from "../shared/esp-element-base.js";
3
+ import type { FlyoutCloseReason } from "../shared/flyout-events.js";
4
+ /**
5
+ * A transient panel for help, more-info, and preview content that
6
+ * claims designed spare width before ever covering the page.
7
+ *
8
+ * Place one `esp-flyout` in an `esp-page`'s `flyout` slot. The panel
9
+ * lives on the canvas, outside the content surface. The page decides
10
+ * where it goes, from widest viewport to narrowest:
11
+ *
12
+ * 1. **Gutter** — the panel occupies the right canvas gutter. When the
13
+ * gutter already has room, it claims existing spare canvas and the
14
+ * main content does not move at all, on any alignment.
15
+ * 2. **Shift** — when the gutter is not quite wide enough, the surface
16
+ * slides toward `start` by exactly the shortfall (no more) to free
17
+ * the difference.
18
+ * 3. **Docked sidebar** — once the gutter is gone, the panel competes
19
+ * for width with the main well; content narrows but is never
20
+ * covered.
21
+ * 4. **Overlay drawer** — below the mobile threshold (`50em`, the
22
+ * same one the `esp-menu` drawer uses) the panel becomes a fixed
23
+ * drawer over a vellum backdrop. Set `mode="overlay"` to get this
24
+ * presentation at every width.
25
+ *
26
+ * The panel is styled as a tear-off piece: a dotted perforation on
27
+ * its leading edge, rounded trailing corners, and no shadow — all
28
+ * overridable through the `--esp-flyout-*` tokens below.
29
+ *
30
+ * A11y follows the presentation. In the in-grid modes (1–3) the
31
+ * flyout is a non-modal `complementary` landmark named by its
32
+ * `heading`, and opening it never steals focus. In the overlay-drawer
33
+ * mode (4) it is a true modal: `role="dialog"` + `aria-modal`, the
34
+ * background goes inert, scroll is locked, focus moves into the drawer
35
+ * and is trapped, and focus is restored on close. `Escape` closes it
36
+ * in every mode.
37
+ *
38
+ * ```html
39
+ * <style>
40
+ * esp-page.flyout-demo {
41
+ * --esp-page-max-width: 420px;
42
+ * --esp-page-canvas-background: var(--esp-color-layer-1);
43
+ * &::part(wrapper) {
44
+ * min-height: 320px;
45
+ * height: 320px;
46
+ * }
47
+ * > div {
48
+ * padding: var(--esp-size-padding);
49
+ * }
50
+ * }
51
+ * </style>
52
+ * <esp-page class="flyout-demo" align="center">
53
+ * <esp-flyout slot="flyout" heading="More info" id="demo-flyout" standalone>
54
+ * <p>Spare width claimed; the well shifted only enough to make room.</p>
55
+ * </esp-flyout>
56
+ * <div>
57
+ * <esp-button id="toggle-flyout" label="Toggle flyout" collapsed></esp-button>
58
+ * </div>
59
+ * </esp-page>
60
+ * <script>
61
+ * const flyout = findById("demo-flyout");
62
+ * findById("toggle-flyout").addEventListener("clicked", () => flyout.toggle());
63
+ * </script>
64
+ * ```
65
+ *
66
+ * By default the panel is a tear-off — a dotted perforation on its
67
+ * leading edge, rounded trailing corners, no shadow. Add
68
+ * `match-surface` to lift the trailing edge with the page's surface
69
+ * edge shadow, so the flyout reads as a raised peer of the content it
70
+ * flies out from while staying attached at the leading perforation:
71
+ *
72
+ * ```html
73
+ * <style>
74
+ * esp-page.flyout-match-demo {
75
+ * --esp-page-max-width: 420px;
76
+ * &::part(wrapper) {
77
+ * min-height: 320px;
78
+ * height: 320px;
79
+ * }
80
+ * > div {
81
+ * padding: var(--esp-size-padding);
82
+ * }
83
+ * }
84
+ * </style>
85
+ * <esp-page class="flyout-match-demo" align="center">
86
+ * <esp-flyout slot="flyout" heading="More info" id="match-flyout" match-surface standalone>
87
+ * <p>Attached at the perforation, trailing edge raised like the surface.</p>
88
+ * </esp-flyout>
89
+ * <div>
90
+ * <esp-button id="toggle-match" label="Toggle flyout" collapsed></esp-button>
91
+ * </div>
92
+ * </esp-page>
93
+ * <script>
94
+ * const matchFlyout = findById("match-flyout");
95
+ * findById("toggle-match").addEventListener("clicked", () => matchFlyout.toggle());
96
+ * </script>
97
+ * ```
98
+ *
99
+ * When the page has no spare width to claim — a `kind="full"` page,
100
+ * or a capped page on a viewport at the cap — the open flyout docks
101
+ * as a right sidebar and the content makes room:
102
+ *
103
+ * ```html
104
+ * <style>
105
+ * esp-page.flyout-docked-demo {
106
+ * &::part(wrapper) {
107
+ * min-height: 320px;
108
+ * height: 320px;
109
+ * }
110
+ * > div {
111
+ * padding: var(--esp-size-padding);
112
+ * }
113
+ * }
114
+ * </style>
115
+ * <esp-page class="flyout-docked-demo" kind="full">
116
+ * <esp-flyout slot="flyout" heading="Docked" id="docked-flyout" standalone>
117
+ * <p>No gutter to claim, so the content narrows instead.</p>
118
+ * </esp-flyout>
119
+ * <div>
120
+ * <p>A <code>kind="full"</code> page has no canvas gutters at any
121
+ * width — the flyout squeezes the main well and never covers it.</p>
122
+ * <esp-button id="toggle-docked" label="Toggle flyout" collapsed></esp-button>
123
+ * </div>
124
+ * </esp-page>
125
+ * <script>
126
+ * const dockedFlyout = findById("docked-flyout");
127
+ * findById("toggle-docked").addEventListener("clicked", () => dockedFlyout.toggle());
128
+ * </script>
129
+ * ```
130
+ *
131
+ * Below the `50em` viewport threshold every flyout becomes a fixed
132
+ * overlay drawer over a vellum — there is no room for a side-by-side
133
+ * split on a phone. `mode="overlay"` forces that presentation at any
134
+ * width (try it here; `Escape` or the vellum closes it):
135
+ *
136
+ * ```html
137
+ * <style>
138
+ * esp-page.flyout-overlay-demo {
139
+ * &::part(wrapper) {
140
+ * min-height: 160px;
141
+ * height: 160px;
142
+ * }
143
+ * > div {
144
+ * padding: var(--esp-size-padding);
145
+ * }
146
+ * }
147
+ * </style>
148
+ * <esp-page class="flyout-overlay-demo">
149
+ * <esp-flyout slot="flyout" heading="Overlay drawer" mode="overlay" id="overlay-flyout" standalone>
150
+ * <p>A fixed drawer over a vellum — what every flyout becomes on
151
+ * small viewports.</p>
152
+ * </esp-flyout>
153
+ * <div>
154
+ * <esp-button id="open-overlay" label="Open overlay flyout" collapsed></esp-button>
155
+ * </div>
156
+ * </esp-page>
157
+ * <script>
158
+ * const overlayFlyout = findById("overlay-flyout");
159
+ * findById("open-overlay").addEventListener("clicked", () => overlayFlyout.toggle());
160
+ * </script>
161
+ * ```
162
+ *
163
+ * Components that cannot know where the flyout lives can request it
164
+ * over the bus with `showFlyout()` — the same pattern `showToast()`
165
+ * uses. A second request swaps the content in place; it never
166
+ * stacks. Content passed as a string renders as plain text; pass a
167
+ * `Node` for rich content. Pass the triggering element as `anchor` to
168
+ * align the in-grid panel with that control and keep both in the same
169
+ * scroll flow. If the panel would cross the visible viewport's bottom,
170
+ * it shifts up only far enough to fit and returns to its natural trigger
171
+ * alignment as the page scrolls. Content taller than the viewport
172
+ * scrolls inside the panel. Overlay drawers stay viewport-fixed.
173
+ *
174
+ * The bus is a global broadcast, so every listening flyout on the
175
+ * page answers a `showFlyout()` call. The demos above are each driven
176
+ * by their own toggle button, so they are marked `standalone` to opt
177
+ * out of the bus — leaving this one flyout as the page's shared
178
+ * surface. In a real app you typically have a single flyout and need
179
+ * no attribute at all.
180
+ *
181
+ * ```html
182
+ * <style>
183
+ * esp-page.flyout-bus-demo {
184
+ * --esp-page-max-width: 420px;
185
+ * &::part(wrapper) {
186
+ * min-height: 320px;
187
+ * }
188
+ * > div {
189
+ * padding: var(--esp-size-padding);
190
+ * }
191
+ * .flyout-demo-copy {
192
+ * max-inline-size: 42ch;
193
+ * }
194
+ * }
195
+ * </style>
196
+ * <esp-page class="flyout-bus-demo">
197
+ * <esp-flyout slot="flyout"></esp-flyout>
198
+ * <div>
199
+ * <esp-button id="show-apples" label="Apples" collapsed></esp-button>
200
+ * <div class="flyout-demo-copy">
201
+ * <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
202
+ * tempor incididunt ut labore et dolore magna aliqua.</p>
203
+ * <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
204
+ * ut aliquip ex ea commodo consequat.</p>
205
+ * <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
206
+ * dolore eu fugiat nulla pariatur.</p>
207
+ * </div>
208
+ * <esp-button id="show-pears" label="Pears" collapsed></esp-button>
209
+ * <template id="pears-description">
210
+ * <p>Ripen pears at room temperature until the neck yields gently to
211
+ * pressure, then refrigerate them to slow further ripening.</p>
212
+ * <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer
213
+ * nec odio. Praesent libero. Sed cursus ante dapibus diam.</p>
214
+ * <p>Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis
215
+ * sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper
216
+ * porta.</p>
217
+ * <p>Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent
218
+ * taciti sociosqu ad litora torquent per conubia nostra.</p>
219
+ * <p>Curabitur sodales ligula in libero. Sed dignissim lacinia nunc.
220
+ * Curabitur tortor. Pellentesque nibh. Aenean quam.</p>
221
+ * <p>In scelerisque sem at dolor. Maecenas mattis. Sed convallis
222
+ * tristique sem. Proin ut ligula vel nunc egestas porttitor.</p>
223
+ * <p>Morbi lectus risus, iaculis vel, suscipit quis, luctus non,
224
+ * massa. Fusce ac turpis quis ligula lacinia aliquet.</p>
225
+ * <p>Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed,
226
+ * euismod in, nibh. Quisque volutpat condimentum velit.</p>
227
+ * </template>
228
+ * <div class="flyout-demo-copy">
229
+ * <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
230
+ * deserunt mollit anim id est laborum.</p>
231
+ * <p>Curabitur pretium tincidunt lacus. Nulla gravida orci a odio, nullam
232
+ * varius, turpis et commodo pharetra.</p>
233
+ * </div>
234
+ * </div>
235
+ * </esp-page>
236
+ * <script>
237
+ * const applesButton = findById("show-apples");
238
+ * applesButton.addEventListener("clicked", () => {
239
+ * showFlyout({ heading: "Apples", content: "Crisp. Store cold.", anchor: applesButton });
240
+ * });
241
+ * const pearsButton = findById("show-pears");
242
+ * const pearsDescription = findById("pears-description");
243
+ * pearsButton.addEventListener("clicked", () => {
244
+ * showFlyout({
245
+ * heading: "Pears",
246
+ * content: pearsDescription.content.cloneNode(true),
247
+ * anchor: pearsButton,
248
+ * });
249
+ * });
250
+ * </script>
251
+ * ```
252
+ *
253
+ * @slot - The flyout's content.
254
+ *
255
+ * @event {CustomEvent<{}>} flyout-opened - Fired by `show()` when the
256
+ * flyout opens.
257
+ * @event {CustomEvent<{reason: FlyoutCloseReason}>} flyout-closed -
258
+ * Fired by `close()` when the flyout closes. `reason` is `"escape"`,
259
+ * `"vellum"`, `"button"`, or `"programmatic"`.
260
+ * @event {CustomEvent<{}>} flyout-state-changed - Fired on every
261
+ * `open`, `mode`, or `anchor` change, including direct property
262
+ * assignment (not just `show()`/`close()`). `esp-page` uses it to keep
263
+ * its layout in sync; most consumers want
264
+ * `flyout-opened`/`flyout-closed` instead.
265
+ *
266
+ * @csspart panel - The panel surface.
267
+ * @csspart header - The heading/close-button row.
268
+ * @csspart content - The scrollable content region wrapping the slot.
269
+ *
270
+ * @cssprop --esp-flyout-background - The background color of the
271
+ * panel. Defaults to `var(--esp-color-background)`.
272
+ * @cssprop --esp-flyout-border - The border on the panel's leading
273
+ * edge. Defaults to the tear-off perforation,
274
+ * `1px dotted var(--esp-color-border)`.
275
+ * @cssprop --esp-flyout-radius - The radius of the panel's trailing
276
+ * corners. Defaults to `var(--esp-size-border-radius)`; the overlay
277
+ * drawer squares them.
278
+ * @cssprop --esp-flyout-shadow - The panel's shadow. Defaults to
279
+ * `none` in the gutter/docked modes (the perforation does the
280
+ * separating) and to a drawer shadow in overlay mode.
281
+ * @cssprop --esp-flyout-padding - Padding inside the panel's header
282
+ * and content regions. Defaults to `var(--esp-size-padding)`.
283
+ * @cssprop --esp-flyout-z-index - Stack order of the overlay-drawer
284
+ * mode. Defaults to `3000` — above page chrome, below the dialog
285
+ * drop zone (4000). Set it on the `esp-page` (or an ancestor), not on
286
+ * the flyout itself: the page reads the same token to hoist the
287
+ * drawer's aside above its header.
288
+ *
289
+ * @docPageTitle Flyout
290
+ * @docUrl /components/flyout
291
+ * @menuGroup Structure
292
+ * @menuOrder 3
293
+ * @menuLabel Flyout
294
+ * @menuIcon layout
295
+ *
296
+ */
297
+ export declare class EspalierFlyout extends EspalierElementBase {
298
+ /**
299
+ * Whether the flyout is open. Reflected so the initial state can
300
+ * be declared in markup; `esp-page` mirrors it (via the lifecycle
301
+ * events and the slot) onto its own `flyout-open` attribute to
302
+ * drive the grid.
303
+ */
304
+ open: boolean;
305
+ /**
306
+ * Heading text for the panel's header row. Also becomes the
307
+ * flyout's accessible name unless the author set an `aria-label`.
308
+ * An open overlay without either uses the fallback name "Flyout" so
309
+ * its dialog is never exposed without an accessible name.
310
+ */
311
+ heading: string;
312
+ /**
313
+ * Placement behavior.
314
+ *
315
+ * - `"auto"` (default) — the page's placement ladder: gutter,
316
+ * docked sidebar, then overlay drawer below the `50em` viewport
317
+ * threshold.
318
+ * - `"overlay"` — always the fixed overlay drawer, at any width.
319
+ * The page never reserves a grid track for an overlay-mode
320
+ * flyout.
321
+ */
322
+ mode: "auto" | "overlay";
323
+ /**
324
+ * Triggering element whose block-start edge the in-grid panel should
325
+ * align with. Anchored panels stay in normal page flow, shift upward
326
+ * only while needed to fit the visible scrollport, and return to their
327
+ * natural alignment as the page scrolls. Oversized content scrolls
328
+ * inside the panel. Overlay drawers ignore this geometry and remain
329
+ * fixed to the viewport.
330
+ */
331
+ anchor: HTMLElement | undefined;
332
+ /**
333
+ * When set on a flyout slotted into an `esp-page`, the panel lifts
334
+ * its **trailing** edge with the page's surface edge shadow, so it
335
+ * reads as a raised peer of the content surface it flies out from
336
+ * rather than the shadowless tear-off. The **leading** edge keeps
337
+ * its dotted perforation and casts no shadow, so the panel stays
338
+ * attached at the tear line instead of floating over the content.
339
+ * The trailing-corner radius is still `--esp-flyout-radius`. No
340
+ * effect on a standalone flyout — there is no page surface to match.
341
+ */
342
+ matchSurface: boolean;
343
+ /**
344
+ * Whether this flyout ignores the shared `showFlyout()` /
345
+ * `closeFlyout()` bus and is driven only through its own
346
+ * `show()` / `close()` / `toggle()` API.
347
+ *
348
+ * By default a flyout services the bus, so a single flyout in a
349
+ * page "just works" as the target of `showFlyout()` — the same
350
+ * zero-config model as `<esp-toaster>` and `showToast()`. Because
351
+ * the bus is a global broadcast, **every** listening flyout on the
352
+ * page answers a `showFlyout()` call; set `standalone` on any
353
+ * flyout you drive directly so only your one designated help
354
+ * surface responds.
355
+ */
356
+ standalone: boolean;
357
+ /**
358
+ * Element to return focus to when the flyout closes. In the in-grid
359
+ * modes it is focused only if focus is inside the flyout at close;
360
+ * in the overlay-drawer mode it overrides the modal treatment's
361
+ * automatic restore-to-opener. Usually supplied through
362
+ * `showFlyout({ returnFocusTo })`.
363
+ */
364
+ returnFocusTo: HTMLElement | undefined;
365
+ connectedCallback(): void;
366
+ disconnectedCallback(): void;
367
+ /**
368
+ * Open the flyout with its current content. In the in-grid modes it
369
+ * does not move focus; in the overlay-drawer mode it moves focus
370
+ * into the drawer as a modal.
371
+ */
372
+ show(): void;
373
+ /**
374
+ * Close the flyout. In the in-grid modes, if focus is inside the
375
+ * flyout it is returned to `returnFocusTo` when one was provided; in
376
+ * the overlay-drawer mode the modal treatment restores focus to
377
+ * wherever it was when the drawer opened, unless `returnFocusTo`
378
+ * names an explicit target — the explicit target wins.
379
+ */
380
+ close(reason?: FlyoutCloseReason): void;
381
+ /**
382
+ * Toggle the flyout between open and closed.
383
+ */
384
+ toggle(): void;
385
+ protected updated(changed: PropertyValues): void;
386
+ protected render(): import("lit-html").TemplateResult<1>;
387
+ static styles: import("lit").CSSResult[];
388
+ }
389
+ declare global {
390
+ interface HTMLElementTagNameMap {
391
+ "esp-flyout": EspalierFlyout;
392
+ }
393
+ }
@@ -0,0 +1,153 @@
1
+ var h=function(c,e,t,i){var o=arguments.length,s=o<3?e:i===null?i=Object.getOwnPropertyDescriptor(e,t):i,n;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")s=Reflect.decorate(c,e,t,i);else for(var a=c.length-1;a>=0;a--)(n=c[a])&&(s=(o<3?n(s):o>3?n(e,t,s):n(e,t))||s);return o>3&&s&&Object.defineProperty(e,t,s),s};import{css as p,html as y,nothing as R}from"lit";import{customElement as k,property as l}from"lit/decorators.js";import{createRef as x,ref as C}from"lit/directives/ref.js";import{EspalierElementBase as v}from"../shared/esp-element-base.js";import{getEspBus as f}from"../shared/bus-events.js";import{OverlayController as T}from"../shared/overlay-controller.js";import{cancelSVG as E}from"../shared/svgs/cancel.js";import{traverseToClosest as b}from"../shared/utilities.js";const P="(max-width: 50em)",O="Flyout",m=p`
2
+ position: fixed;
3
+ inset: 0;
4
+ margin-block-start: 0;
5
+ width: auto;
6
+
7
+ height: 100vh;
8
+ height: 100dvh;
9
+ z-index: var(--esp-flyout-z-index, 3000);
10
+ `,w=p`
11
+ display: block;
12
+ position: absolute;
13
+ inset: 0;
14
+ background-color: var(--esp-vellum-background, var(--esp-color-layer-3));
15
+ opacity: var(--esp-vellum-opacity, 0.85);
16
+ `,g=p`
17
+ position: absolute;
18
+ inset-block: 0;
19
+ inset-inline-end: 0;
20
+ width: min(var(--esp-page-flyout-width, 20rem), 85vw);
21
+ overflow-y: auto;
22
+
23
+ border-start-end-radius: 0;
24
+ border-end-end-radius: 0;
25
+ box-shadow: var(--esp-flyout-shadow, -0.75rem 0 1.5rem -0.75rem var(--esp-color-shadow));
26
+ translate: 0 0;
27
+ transition: translate 0.25s ease;
28
+
29
+ @starting-style {
30
+ translate: 100% 0;
31
+ }
32
+ `;let r=class extends v{constructor(){super(...arguments),this.open=!1,this.heading="",this.mode="auto",this.matchSurface=!1,this.standalone=!1,this.lastGeneratedAriaLabel=null,this.boundKeydown=e=>this.handleKeydown(e),this.boundShowRequest=e=>this.handleShowRequest(e),this.boundCloseRequest=()=>this.close(),this.boundOverlayMediaChange=()=>{this.syncOverlayModal(),this.syncAnchorTracking(),this.syncAnchorGeometry()},this.boundAnchorViewportChange=()=>this.scheduleAnchorGeometrySync(),this.anchorPositionRaf=0,this.trackingAnchorPosition=!1,this.busSubscribed=!1,this.panelRef=x(),this.overlay=new T({host:this,getFocusTrapContainer:()=>this.panelRef.value??null,getFocusFallback:()=>this.panelRef.value??null,promote:!1}),this.overlayActive=!1,this.baseRole="complementary"}get effectiveOverlay(){return this.mode==="overlay"||(this.overlayMediaQuery?.matches??!1)}connectedCallback(){super.connectedCallback(),this.hasAttribute("role")||this.setAttribute("role","complementary"),this.baseRole=this.getAttribute("role")??"complementary",!this.overlayMediaQuery&&typeof window<"u"&&"matchMedia"in window&&(this.overlayMediaQuery=window.matchMedia(P)),this.overlayMediaQuery?.addEventListener("change",this.boundOverlayMediaChange),this.syncBusSubscription(),document.addEventListener("keydown",this.boundKeydown),this.syncOverlayModal(),this.syncAnchorTracking(),this.syncAnchorGeometry()}disconnectedCallback(){super.disconnectedCallback(),this.unsubscribeBus(),this.overlayMediaQuery?.removeEventListener("change",this.boundOverlayMediaChange),document.removeEventListener("keydown",this.boundKeydown),this.stopAnchorTracking(),this.overlayActive&&(this.overlayActive=!1,this.setAttribute("role",this.baseRole),this.removeAttribute("aria-modal"),this.updateGeneratedAriaLabel()),this.overlayReturnFocusTo=void 0}syncBusSubscription(){if(this.standalone||!this.isConnected){this.unsubscribeBus();return}this.busSubscribed||(f().subscribe("show-flyout",this.boundShowRequest),f().subscribe("close-flyout",this.boundCloseRequest),this.busSubscribed=!0)}unsubscribeBus(){this.busSubscribed&&(f().unsubscribe("show-flyout",this.boundShowRequest),f().unsubscribe("close-flyout",this.boundCloseRequest),this.busSubscribed=!1)}show(){this.open||(this.open=!0,this.dispatchEvent(new CustomEvent("flyout-opened",{detail:{},bubbles:!0,composed:!0})))}close(e="programmatic"){if(this.open){if(this.open=!1,this.overlayActive)this.overlayReturnFocusTo=this.returnFocusTo;else{const t=this.focusIsInside();this.overlay.close(),t&&this.returnFocusTo?.focus()}this.returnFocusTo=void 0,this.dispatchEvent(new CustomEvent("flyout-closed",{detail:{reason:e},bubbles:!0,composed:!0}))}}toggle(){this.open?this.close():this.show()}handleShowRequest(e){e.heading!==void 0&&(this.heading=e.heading),e.content!==void 0&&this.replaceChildren(typeof e.content=="string"?document.createTextNode(e.content):e.content),this.anchor=e.anchor,this.returnFocusTo=e.returnFocusTo,this.show()}handleKeydown(e){if(this.open){if(this.overlayActive&&e.key==="Tab"){this.overlay.trapFocus(e);return}e.key!=="Escape"||e.defaultPrevented||(e.preventDefault(),this.close("escape"))}}focusIsInside(){const e=document.activeElement;return e&&this.contains(e)?!0:this.shadowRoot?.activeElement!=null}updated(e){super.updated(e),e.has("standalone")&&this.syncBusSubscription(),e.has("heading")&&this.updateGeneratedAriaLabel(),(e.has("anchor")||e.has("open")||e.has("mode"))&&(this.syncAnchorTracking(),this.syncAnchorGeometry()),(e.has("open")||e.has("mode"))&&this.syncOverlayModal(),(e.has("open")||e.has("mode")||e.has("anchor"))&&this.dispatchEvent(new CustomEvent("flyout-state-changed",{detail:{},bubbles:!0,composed:!0}))}syncAnchorGeometry(){const e=b(this,"esp-page"),t=this.anchor?b(this.anchor,"esp-page"):null,i=e?.shadowRoot?.querySelector(".esp-page-main"),o=this.panelRef.value;if(!this.open||this.effectiveOverlay||!this.anchor||!e||t!==e||!i||!o){this.clearAnchorGeometry();return}const s=this.anchor.getBoundingClientRect(),n=Math.max(0,s.top-i.getBoundingClientRect().top),a=this.getVisibleBlockBounds(),d=Math.max(0,a.bottom-a.top);this.setGeometryProperty("--_esp-flyout-anchor-offset",`${n}px`),this.setGeometryProperty("--_esp-flyout-max-block-size",`${d}px`);const u=o.getBoundingClientRect().height,A=Math.max(0,s.top+u-a.bottom);this.setGeometryProperty("--_esp-flyout-viewport-shift",`${A}px`)}getVisibleBlockBounds(){const e=window.visualViewport;let t=e?.offsetTop??0,i=t+(e?.height??window.innerHeight);for(let o=this.composedParent(this);o;o=this.composedParent(o)){const s=getComputedStyle(o).overflowY;if(s!=="auto"&&s!=="scroll")continue;const n=o.getBoundingClientRect(),a=o instanceof HTMLElement?o.clientTop:0,d=o instanceof HTMLElement?o.clientHeight:n.height;if(d<=0)continue;const u=n.top+a;t=Math.max(t,u),i=Math.min(i,u+d)}return{top:t,bottom:Math.max(t,i)}}composedParent(e){if(e.parentElement)return e.parentElement;const t=e.getRootNode();return t instanceof ShadowRoot?t.host:null}setGeometryProperty(e,t){this.style.getPropertyValue(e)!==t&&this.style.setProperty(e,t)}clearAnchorGeometry(){this.style.removeProperty("--_esp-flyout-anchor-offset"),this.style.removeProperty("--_esp-flyout-viewport-shift"),this.style.removeProperty("--_esp-flyout-max-block-size")}scheduleAnchorGeometrySync(){!this.isConnected||this.anchorPositionRaf||(this.anchorPositionRaf=requestAnimationFrame(()=>{this.anchorPositionRaf=0,this.syncAnchorGeometry()}))}syncAnchorTracking(){if(!(this.isConnected&&this.open&&!!this.anchor&&!this.effectiveOverlay)){this.stopAnchorTracking();return}this.trackingAnchorPosition||(this.trackingAnchorPosition=!0,window.addEventListener("scroll",this.boundAnchorViewportChange,{capture:!0,passive:!0}),window.addEventListener("resize",this.boundAnchorViewportChange,{passive:!0}));const t=this.panelRef.value;t&&t!==this.observedAnchorPanel&&typeof ResizeObserver<"u"&&(this.anchorResizeObserver??=new ResizeObserver(this.boundAnchorViewportChange),this.anchorResizeObserver.disconnect(),this.anchorResizeObserver.observe(t),this.observedAnchorPanel=t)}stopAnchorTracking(){this.trackingAnchorPosition&&(window.removeEventListener("scroll",this.boundAnchorViewportChange,{capture:!0}),window.removeEventListener("resize",this.boundAnchorViewportChange),this.trackingAnchorPosition=!1),this.anchorPositionRaf&&(cancelAnimationFrame(this.anchorPositionRaf),this.anchorPositionRaf=0),this.anchorResizeObserver?.disconnect(),this.observedAnchorPanel=void 0}syncOverlayModal(){const e=this.isConnected&&this.open&&this.effectiveOverlay;if(e===this.overlayActive){!e&&!this.open&&this.overlay.close();return}e?(this.overlayActive=!0,this.setAttribute("role","dialog"),this.setAttribute("aria-modal","true"),this.updateGeneratedAriaLabel(),this.overlay.open(),this.updateComplete.then(()=>{this.overlayActive&&this.overlay.moveFocusInto()})):(this.overlayActive=!1,this.overlay.close(),this.overlayReturnFocusTo&&(this.overlayReturnFocusTo.focus(),this.overlayReturnFocusTo=void 0),this.setAttribute("role",this.baseRole),this.removeAttribute("aria-modal"),this.updateGeneratedAriaLabel())}updateGeneratedAriaLabel(){const e=this.getAttribute("aria-label");if(e!==null&&e!==this.lastGeneratedAriaLabel)return;const t=this.heading||(this.overlayActive?O:"");t?(this.setAttribute("aria-label",t),this.lastGeneratedAriaLabel=t):(this.removeAttribute("aria-label"),this.lastGeneratedAriaLabel=null)}render(){return y`
33
+ <div class="vellum" aria-hidden="true" @click=${()=>this.close("vellum")}></div>
34
+ <div class="panel" part="panel" ${C(this.panelRef)}>
35
+ <header part="header">
36
+ ${this.heading?y`<h2>${this.heading}</h2>`:R}
37
+
38
+ <button class="close" aria-label="Close" @click=${()=>this.close("button")}>
39
+ ${E}
40
+ </button>
41
+ </header>
42
+ <div class="content" part="content">
43
+ <slot @slotchange=${this.boundAnchorViewportChange}></slot>
44
+ </div>
45
+ </div>
46
+ `}};r.styles=[...v.styles,p`
47
+ :host {
48
+ display: none;
49
+ }
50
+
51
+ :host([open]) {
52
+ display: block;
53
+
54
+ width: var(--esp-page-flyout-width, 20rem);
55
+ margin-block-start: calc(
56
+ var(--_esp-flyout-anchor-offset, 0px) - var(--_esp-flyout-viewport-shift, 0px)
57
+ );
58
+ }
59
+
60
+ .vellum {
61
+ display: none;
62
+ }
63
+
64
+ .panel {
65
+ background: var(--esp-flyout-background, var(--esp-color-background));
66
+
67
+ width: var(--esp-page-flyout-width, 20rem);
68
+ display: flex;
69
+ flex-direction: column;
70
+ max-block-size: var(--_esp-flyout-max-block-size, none);
71
+ overflow: hidden;
72
+
73
+ border-inline-start: var(--esp-flyout-border, 1px dotted var(--esp-color-border));
74
+ border-start-end-radius: var(--esp-flyout-radius, var(--esp-size-border-radius));
75
+ border-end-end-radius: var(--esp-flyout-radius, var(--esp-size-border-radius));
76
+ box-shadow: var(--esp-flyout-shadow, none);
77
+ font-family: var(--esp-font-body);
78
+ color: var(--esp-color-text);
79
+
80
+ > header {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: var(--esp-size-tiny-to-small);
84
+ padding: var(--esp-flyout-padding, var(--esp-size-padding));
85
+ padding-block-end: 0;
86
+
87
+ > h2 {
88
+ flex: 1;
89
+ margin: 0;
90
+ font-size: var(--esp-type-large);
91
+ line-height: 1.2;
92
+ }
93
+
94
+ > .close {
95
+ margin-inline-start: auto;
96
+ display: inline-flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ border: none;
100
+ background: transparent;
101
+ color: var(--esp-color-text);
102
+ cursor: pointer;
103
+ padding: var(--esp-size-tiny);
104
+
105
+ svg {
106
+ width: 1.25em;
107
+ height: 1.25em;
108
+ }
109
+ }
110
+ }
111
+
112
+ > .content {
113
+ min-block-size: 0;
114
+ overflow-y: auto;
115
+ overscroll-behavior-block: contain;
116
+ padding: var(--esp-flyout-padding, var(--esp-size-padding));
117
+ }
118
+ }
119
+
120
+
121
+ :host([open][mode="overlay"]) {
122
+ ${m}
123
+ }
124
+
125
+ :host([open][mode="overlay"]) .vellum {
126
+ ${w}
127
+ }
128
+
129
+ :host([open][mode="overlay"]) .panel {
130
+ ${g}
131
+ }
132
+
133
+
134
+ @media (max-width: 50em) {
135
+ :host([open]) {
136
+ ${m}
137
+ }
138
+
139
+ .vellum {
140
+ ${w}
141
+ }
142
+
143
+ .panel {
144
+ ${g}
145
+ }
146
+ }
147
+
148
+ @media (prefers-reduced-motion: reduce) {
149
+ .panel {
150
+ transition: none;
151
+ }
152
+ }
153
+ `],h([l({type:Boolean,reflect:!0})],r.prototype,"open",void 0),h([l({type:String})],r.prototype,"heading",void 0),h([l({reflect:!0})],r.prototype,"mode",void 0),h([l({attribute:!1})],r.prototype,"anchor",void 0),h([l({type:Boolean,reflect:!0,attribute:"match-surface"})],r.prototype,"matchSurface",void 0),h([l({type:Boolean,reflect:!0})],r.prototype,"standalone",void 0),r=h([k("esp-flyout")],r);export{r as EspalierFlyout};
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./breadcrumbs/esp-breadcrumbs.js";
14
14
  export * from "./button/esp-button.js";
15
15
  export * from "./button/esp-button-group.js";
16
16
  export * from "./color-picker/esp-color-picker.js";
17
+ export * from "./flyout/esp-flyout.js";
17
18
  export * from "./form/esp-form.js";
18
19
  export * from "./form-item/esp-form-item.js";
19
20
  export * from "./header/index.js";
@@ -57,7 +58,8 @@ export { type ValidationError, VALIDITY_CHANGED_EVENT, type ValidityChangedDetai
57
58
  export { FormFieldController, type FormFieldControllerOptions, } from "./shared/form-field-controller.js";
58
59
  export { traverseToClosest } from "./shared/utilities.js";
59
60
  export { showToast, type ToastConfig } from "./shared/toast-events.js";
60
- export { getEspBus, type EspBusEventMap, type SchemeEvents, type ToastEvents, type PopoverEvents, type SizeEvents, type PageEventMap, type SeedColorRoot, } from "./shared/bus-events.js";
61
+ export { showFlyout, closeFlyout, type FlyoutConfig, type FlyoutCloseReason, } from "./shared/flyout-events.js";
62
+ export { getEspBus, type EspBusEventMap, type SchemeEvents, type ToastEvents, type FlyoutEvents, type PopoverEvents, type SizeEvents, type PageEventMap, type SeedColorRoot, } from "./shared/bus-events.js";
61
63
  export * from "./shared/events.js";
62
64
  export { getImageDetails, releasePreviewUrl, type EspalierUploadImage, type ImageDetailsOptions, type SelectedUploadImage, type ExistingUploadImage, type ExistingImage, type ResponsiveImageUrl, type UploadCallbacks, type UploadEventDetail, } from "./image-upload/image-helpers.js";
63
65
  export { calculatePhotoLayout, type LayoutImage, type PhotoRow, } from "./shared/justified-layout.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./box/esp-box.js";export*from"./action-menu/esp-action-menu.js";export*from"./action-menu/esp-action-menu-item.js";export*from"./avatar/esp-avatar.js";export*from"./avatar/esp-profile-chip.js";export*from"./badge/esp-badge.js";export*from"./checkbox/esp-checkbox.js";export*from"./checkbox/esp-checkbox-group.js";export*from"./data-cell/esp-data-cell.js";export*from"./radio-button/esp-radio-button.js";export*from"./radio-button/esp-radio-button-group.js";export*from"./repeater/esp-repeater.js";export*from"./breadcrumbs/esp-breadcrumbs.js";export*from"./button/esp-button.js";export*from"./button/esp-button-group.js";export*from"./color-picker/esp-color-picker.js";export*from"./form/esp-form.js";export*from"./form-item/esp-form-item.js";export*from"./header/index.js";export*from"./page/esp-page.js";export*from"./popover/esp-popover.js";export*from"./dialog/esp-dialog.js";export*from"./empty-state/esp-empty-state.js";export*from"./image-upload/esp-image-upload.js";export*from"./file-upload/esp-file-upload.js";export*from"./font-picker/esp-font-picker.js";export*from"./grid/esp-grid.js";export*from"./image/esp-image.js";export*from"./lightbox/esp-lightbox.js";export*from"./info/esp-info.js";export*from"./input/esp-input.js";export*from"./textarea/esp-textarea.js";export*from"./pickers/esp-pick-one.js";export*from"./pickers/esp-pick-some.js";export*from"./pickers/esp-picker-menu.js";export*from"./pickers/esp-picker-item.js";export*from"./root/esp-root.js";export*from"./menu/index.js";export*from"./burger/esp-burger.js";export*from"./date-picker/esp-date-picker.js";export*from"./details/esp-details.js";export*from"./details/esp-details-group.js";export*from"./tabs/esp-tab.js";export*from"./tabs/esp-tab-group.js";export*from"./slider/esp-slider.js";export*from"./switch/esp-switch.js";export*from"./toaster/esp-toaster.js";export*from"./tooltip/esp-tooltip.js";export*from"./progress/esp-progress.js";export*from"./search/esp-search.js";export*from"./status-indicator/esp-status-indicator.js";export*from"./tree/esp-tree.js";export*from"./tree/esp-tree-item.js";import{DEFAULT_ICON_SPRITE_URL as ro,DEFAULT_ICON_VIEW_BOX as eo,INTENT_VARIANTS as to,normalizeIntentVariant as mo,getIconHref as po,getIconHrefForHost as fo,getIconSpriteUrl as xo}from"./shared/intent-values.js";import{EspalierElementBase as lo}from"./shared/esp-element-base.js";import{VALIDITY_CHANGED_EVENT as so}from"./shared/validation.js";import{FormFieldController as io}from"./shared/form-field-controller.js";import{traverseToClosest as Io}from"./shared/utilities.js";import{showToast as _o}from"./shared/toast-events.js";import{getEspBus as co}from"./shared/bus-events.js";export*from"./shared/events.js";import{getImageDetails as Lo,releasePreviewUrl as No}from"./image-upload/image-helpers.js";import{calculatePhotoLayout as Ho}from"./shared/justified-layout.js";import{encodeTheme as bo,parseTheme as uo,mergePartials as Co,layerThemes as Uo,buildTaprootLightTheme as Vo,buildTaprootDarkTheme as Wo,NESTED_THEME_KEYS as Bo}from"./shared/theme.js";import{WEIGHT_LABELS as vo,extractWeights as yo,normalizeWeight as Go,bestAvailableWeight as Oo,extractFamily as Ro,getFallbackFont as ko}from"./shared/font-helpers.js";import{getGoogleFonts as zo}from"./font-picker/esp-font-picker.js";export{ro as DEFAULT_ICON_SPRITE_URL,eo as DEFAULT_ICON_VIEW_BOX,lo as EspalierElementBase,io as FormFieldController,to as INTENT_VARIANTS,Bo as NESTED_THEME_KEYS,so as VALIDITY_CHANGED_EVENT,vo as WEIGHT_LABELS,Oo as bestAvailableWeight,Wo as buildTaprootDarkTheme,Vo as buildTaprootLightTheme,Ho as calculatePhotoLayout,bo as encodeTheme,Ro as extractFamily,yo as extractWeights,co as getEspBus,ko as getFallbackFont,zo as getGoogleFonts,po as getIconHref,fo as getIconHrefForHost,xo as getIconSpriteUrl,Lo as getImageDetails,Uo as layerThemes,Co as mergePartials,mo as normalizeIntentVariant,Go as normalizeWeight,uo as parseTheme,No as releasePreviewUrl,_o as showToast,Io as traverseToClosest};
1
+ export*from"./box/esp-box.js";export*from"./action-menu/esp-action-menu.js";export*from"./action-menu/esp-action-menu-item.js";export*from"./avatar/esp-avatar.js";export*from"./avatar/esp-profile-chip.js";export*from"./badge/esp-badge.js";export*from"./checkbox/esp-checkbox.js";export*from"./checkbox/esp-checkbox-group.js";export*from"./data-cell/esp-data-cell.js";export*from"./radio-button/esp-radio-button.js";export*from"./radio-button/esp-radio-button-group.js";export*from"./repeater/esp-repeater.js";export*from"./breadcrumbs/esp-breadcrumbs.js";export*from"./button/esp-button.js";export*from"./button/esp-button-group.js";export*from"./color-picker/esp-color-picker.js";export*from"./flyout/esp-flyout.js";export*from"./form/esp-form.js";export*from"./form-item/esp-form-item.js";export*from"./header/index.js";export*from"./page/esp-page.js";export*from"./popover/esp-popover.js";export*from"./dialog/esp-dialog.js";export*from"./empty-state/esp-empty-state.js";export*from"./image-upload/esp-image-upload.js";export*from"./file-upload/esp-file-upload.js";export*from"./font-picker/esp-font-picker.js";export*from"./grid/esp-grid.js";export*from"./image/esp-image.js";export*from"./lightbox/esp-lightbox.js";export*from"./info/esp-info.js";export*from"./input/esp-input.js";export*from"./textarea/esp-textarea.js";export*from"./pickers/esp-pick-one.js";export*from"./pickers/esp-pick-some.js";export*from"./pickers/esp-picker-menu.js";export*from"./pickers/esp-picker-item.js";export*from"./root/esp-root.js";export*from"./menu/index.js";export*from"./burger/esp-burger.js";export*from"./date-picker/esp-date-picker.js";export*from"./details/esp-details.js";export*from"./details/esp-details-group.js";export*from"./tabs/esp-tab.js";export*from"./tabs/esp-tab-group.js";export*from"./slider/esp-slider.js";export*from"./switch/esp-switch.js";export*from"./toaster/esp-toaster.js";export*from"./tooltip/esp-tooltip.js";export*from"./progress/esp-progress.js";export*from"./search/esp-search.js";export*from"./status-indicator/esp-status-indicator.js";export*from"./tree/esp-tree.js";export*from"./tree/esp-tree-item.js";import{DEFAULT_ICON_SPRITE_URL as eo,DEFAULT_ICON_VIEW_BOX as to,INTENT_VARIANTS as mo,normalizeIntentVariant as po,getIconHref as fo,getIconHrefForHost as xo,getIconSpriteUrl as ao}from"./shared/intent-values.js";import{EspalierElementBase as so}from"./shared/esp-element-base.js";import{VALIDITY_CHANGED_EVENT as Eo}from"./shared/validation.js";import{FormFieldController as go}from"./shared/form-field-controller.js";import{traverseToClosest as no}from"./shared/utilities.js";import{showToast as _o}from"./shared/toast-events.js";import{showFlyout as co,closeFlyout as Ao}from"./shared/flyout-events.js";import{getEspBus as No}from"./shared/bus-events.js";export*from"./shared/events.js";import{getImageDetails as Ho,releasePreviewUrl as So}from"./image-upload/image-helpers.js";import{calculatePhotoLayout as yo}from"./shared/justified-layout.js";import{encodeTheme as Uo,parseTheme as Vo,mergePartials as Wo,layerThemes as Bo,buildTaprootLightTheme as Po,buildTaprootDarkTheme as vo,NESTED_THEME_KEYS as wo}from"./shared/theme.js";import{WEIGHT_LABELS as Oo,extractWeights as Ro,normalizeWeight as ko,bestAvailableWeight as zo,extractFamily as Yo,getFallbackFont as Ko}from"./shared/font-helpers.js";import{getGoogleFonts as Xo}from"./font-picker/esp-font-picker.js";export{eo as DEFAULT_ICON_SPRITE_URL,to as DEFAULT_ICON_VIEW_BOX,so as EspalierElementBase,go as FormFieldController,mo as INTENT_VARIANTS,wo as NESTED_THEME_KEYS,Eo as VALIDITY_CHANGED_EVENT,Oo as WEIGHT_LABELS,zo as bestAvailableWeight,vo as buildTaprootDarkTheme,Po as buildTaprootLightTheme,yo as calculatePhotoLayout,Ao as closeFlyout,Uo as encodeTheme,Yo as extractFamily,Ro as extractWeights,No as getEspBus,Ko as getFallbackFont,Xo as getGoogleFonts,fo as getIconHref,xo as getIconHrefForHost,ao as getIconSpriteUrl,Ho as getImageDetails,Bo as layerThemes,Wo as mergePartials,po as normalizeIntentVariant,ko as normalizeWeight,Vo as parseTheme,So as releasePreviewUrl,co as showFlyout,_o as showToast,no as traverseToClosest};
@@ -34,6 +34,15 @@ type HeaderPosition = "normal" | "sticky" | "fixed";
34
34
  *
35
35
  * @slot sidebar - Contextual navigation placed in the left aside.
36
36
  * @slot right - Content to place in the right aside.
37
+ * @slot flyout - A transient `esp-flyout` panel that lives on the
38
+ * canvas, outside the content surface. Closed it costs no width; open
39
+ * it claims the right canvas gutter first — the surface keeps its
40
+ * alignment weighting and shifts only as far as the flyout's width
41
+ * requires — then docks as a width-competing right sidebar when no
42
+ * gutter exists, and becomes an overlay drawer on small viewports.
43
+ * The persistent `right` aside and the transient flyout are
44
+ * complementary, not alternatives. A flyout opened with an `anchor`
45
+ * aligns to that trigger and stays in the same document scroll flow.
37
46
  * @slot footer - Content to place in the footer.
38
47
  * @slot - The main page content. The main content region applies
39
48
  * `contain: inline-size` so child components cannot push the
@@ -122,7 +131,16 @@ type HeaderPosition = "normal" | "sticky" | "fixed";
122
131
  * only shows above the cap. Set to `none` to remove it.
123
132
  * @cssprop --esp-page-surface-border - An optional border on the inline
124
133
  * edges of the surface, for themes preferring a hairline over a shadow.
125
- * Defaults to `none` (e.g. `1px solid var(--esp-color-border)`).
134
+ * Defaults to `none` (e.g. `1px solid var(--esp-color-border)`). Combine
135
+ * with `--esp-page-surface-shadow: none` to switch the content frame
136
+ * from a drop shadow to a hairline, or turn both off for no frame.
137
+ * @cssprop --esp-page-main-background - The background of the main
138
+ * content well. Defaults to `transparent` (the well shows the page
139
+ * background). Set it to give the content well its own card color, or
140
+ * pair the transparent default with `--esp-page-surface-shadow: none`
141
+ * for content that floats directly on the page with no frame.
142
+ * @cssprop --esp-page-flyout-width - The width of the open flyout
143
+ * track (and of the `esp-flyout` overlay drawer). Defaults to `20rem`.
126
144
  * @cssprop --esp-page-fixed-header-offset - Offset reserved for fixed
127
145
  * headers. Defaults to `var(--esp-header-height)`.
128
146
  * @cssprop --esp-page-sticky-header-top - Top inset for sticky headers.
@@ -251,6 +269,7 @@ export declare class EspalierPage extends EspalierElementBase {
251
269
  * @param dialog The EspalierDialog to show.
252
270
  */
253
271
  AddDialog(dialog: EspalierDialog | DocumentFragment | HTMLElement): void;
272
+ protected firstUpdated(changedProperties: PropertyValues): void;
254
273
  protected updated(changedProperties: PropertyValues): void;
255
274
  protected render(): import("lit-html").TemplateResult<1>;
256
275
  static styles: import("lit").CSSResult[];