@marianmeres/stuic 3.150.0 → 3.152.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.
- package/dist/actions/dim-behind/dim-behind.fixture.svelte +54 -0
- package/dist/actions/dim-behind/dim-behind.fixture.svelte.d.ts +9 -0
- package/dist/actions/dim-behind/dim-behind.svelte.d.ts +10 -0
- package/dist/actions/dim-behind/dim-behind.svelte.js +72 -41
- package/dist/actions/popover/README.md +37 -17
- package/dist/actions/popover/popover.container.fixture.svelte +26 -0
- package/dist/actions/popover/popover.container.fixture.svelte.d.ts +7 -0
- package/dist/actions/popover/popover.svelte.d.ts +10 -0
- package/dist/actions/popover/popover.svelte.js +20 -7
- package/dist/actions/spotlight/spotlight.container.fixture.svelte +33 -0
- package/dist/actions/spotlight/spotlight.container.fixture.svelte.d.ts +7 -0
- package/dist/actions/spotlight/spotlight.svelte.d.ts +9 -0
- package/dist/actions/spotlight/spotlight.svelte.js +95 -37
- package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte +3 -2
- package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte.d.ts +3 -2
- package/dist/components/AlertConfirmPrompt/Current.svelte +44 -10
- package/dist/components/AlertConfirmPrompt/Current.svelte.d.ts +3 -2
- package/dist/components/AlertConfirmPrompt/README.md +29 -27
- package/dist/components/AlertConfirmPrompt/alert-confirm-prompt-stack.svelte.d.ts +8 -0
- package/dist/components/DropdownMenu/DropdownMenu.svelte +14 -7
- package/dist/components/DropdownMenu/README.md +1 -0
- package/dist/components/Float/Float.svelte +21 -0
- package/dist/components/Float/README.md +1 -1
- package/dist/components/HoverExpandableWidth/HoverExpandableWidth.svelte +30 -5
- package/dist/utils/anchor-position.d.ts +12 -3
- package/dist/utils/anchor-position.js +35 -15
- package/dist/utils/containing-block.d.ts +55 -0
- package/dist/utils/containing-block.js +131 -0
- package/dist/utils/overlay-container.d.ts +16 -0
- package/dist/utils/overlay-container.js +12 -0
- package/package.json +12 -12
|
@@ -3,6 +3,7 @@ import { twMerge } from "../../utils/tw-merge.js";
|
|
|
3
3
|
import { addAnchorName, removeAnchorName } from "../../utils/anchor-name.js";
|
|
4
4
|
import { buildPositionTryFallbacks, clampIntoViewport, } from "../../utils/anchor-position.js";
|
|
5
5
|
import { BodyScroll } from "../../utils/body-scroll-locker.js";
|
|
6
|
+
import { resolveContainerOption } from "../../utils/overlay-container.js";
|
|
6
7
|
import SpotlightContent from "./SpotlightContent.svelte";
|
|
7
8
|
//
|
|
8
9
|
const TRANSITION = 200;
|
|
@@ -85,23 +86,35 @@ const _classAnnotation = `
|
|
|
85
86
|
border border-(--stuic-spotlight-annotation-border)
|
|
86
87
|
z-50
|
|
87
88
|
`;
|
|
89
|
+
const VIEWPORT_ORIGIN = () => ({
|
|
90
|
+
left: 0,
|
|
91
|
+
top: 0,
|
|
92
|
+
width: window.innerWidth,
|
|
93
|
+
height: window.innerHeight,
|
|
94
|
+
scaleX: 1,
|
|
95
|
+
scaleY: 1,
|
|
96
|
+
});
|
|
88
97
|
/**
|
|
89
|
-
* Builds the clip-path value for the backdrop overlay with a rounded-rectangle
|
|
98
|
+
* Builds the clip-path value for the backdrop overlay with a rounded-rectangle
|
|
99
|
+
* hole. `rect` is the target's viewport rect; `o` is the backdrop's own
|
|
100
|
+
* coordinate space (see {@link OverlayOrigin}) — clip-path coordinates are
|
|
101
|
+
* local to the backdrop element, so the hole is translated by the origin and
|
|
102
|
+
* the outer box is the backdrop's layout size.
|
|
90
103
|
*/
|
|
91
|
-
function buildClipPath(rect, padding, borderRadius) {
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const x = rect.left - padding;
|
|
95
|
-
const y = rect.top - padding;
|
|
96
|
-
const w = rect.width + padding * 2;
|
|
97
|
-
const h = rect.height + padding * 2;
|
|
104
|
+
function buildClipPath(rect, padding, borderRadius, o) {
|
|
105
|
+
const ow = o.width;
|
|
106
|
+
const oh = o.height;
|
|
107
|
+
const x = (rect.left - o.left) / o.scaleX - padding;
|
|
108
|
+
const y = (rect.top - o.top) / o.scaleY - padding;
|
|
109
|
+
const w = rect.width / o.scaleX + padding * 2;
|
|
110
|
+
const h = rect.height / o.scaleY + padding * 2;
|
|
98
111
|
const r = Math.min(borderRadius, w / 2, h / 2);
|
|
99
112
|
if (r <= 0) {
|
|
100
113
|
// Simple rectangular hole (no rounding)
|
|
101
|
-
return `polygon(evenodd, 0 0, ${
|
|
114
|
+
return `polygon(evenodd, 0 0, ${ow}px 0, ${ow}px ${oh}px, 0 ${oh}px, 0 0, ${x}px ${y}px, ${x}px ${y + h}px, ${x + w}px ${y + h}px, ${x + w}px ${y}px, ${x}px ${y}px)`;
|
|
102
115
|
}
|
|
103
116
|
// Rounded rectangular hole using SVG path syntax
|
|
104
|
-
return `path(evenodd, "M 0 0 L ${
|
|
117
|
+
return `path(evenodd, "M 0 0 L ${ow} 0 L ${ow} ${oh} L 0 ${oh} Z M ${x + r} ${y} L ${x + w - r} ${y} A ${r} ${r} 0 0 1 ${x + w} ${y + r} L ${x + w} ${y + h - r} A ${r} ${r} 0 0 1 ${x + w - r} ${y + h} L ${x + r} ${y + h} A ${r} ${r} 0 0 1 ${x} ${y + h - r} L ${x} ${y + r} A ${r} ${r} 0 0 1 ${x + r} ${y} Z")`;
|
|
105
118
|
}
|
|
106
119
|
/**
|
|
107
120
|
* Checks if content is simple (string/html) vs complex (component/snippet).
|
|
@@ -216,6 +229,35 @@ export function spotlight(targetEl, fn) {
|
|
|
216
229
|
hide();
|
|
217
230
|
}
|
|
218
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* The coordinate space the overlay's fixed elements resolve against. The
|
|
234
|
+
* backdrop is `position: fixed; inset: 0`, so its own box IS that space —
|
|
235
|
+
* the containing block's padding box when a CB-forming ancestor exists
|
|
236
|
+
* (overlay portalled into a contained/transformed shell via `container`),
|
|
237
|
+
* the viewport otherwise. Measuring it handles both with no special-casing.
|
|
238
|
+
*/
|
|
239
|
+
function overlayOrigin() {
|
|
240
|
+
if (!backdropEl)
|
|
241
|
+
return VIEWPORT_ORIGIN();
|
|
242
|
+
const r = backdropEl.getBoundingClientRect();
|
|
243
|
+
// offsetWidth/Height are layout-space; the rect is visual — the ratio is
|
|
244
|
+
// the accumulated ancestor scale. offset* are integer-rounded, so treat
|
|
245
|
+
// sub-pixel differences as "unscaled" to avoid noise on the default path.
|
|
246
|
+
const w = backdropEl.offsetWidth || r.width;
|
|
247
|
+
const h = backdropEl.offsetHeight || r.height;
|
|
248
|
+
const sx = w && Math.abs(r.width - w) > 1 ? r.width / w : 1;
|
|
249
|
+
const sy = h && Math.abs(r.height - h) > 1 ? r.height / h : 1;
|
|
250
|
+
return { left: r.left, top: r.top, width: w, height: h, scaleX: sx, scaleY: sy };
|
|
251
|
+
}
|
|
252
|
+
/** Place the invisible anchor element over the (padded) hole. */
|
|
253
|
+
function positionAnchor(rect, padding, o) {
|
|
254
|
+
if (!anchorEl)
|
|
255
|
+
return;
|
|
256
|
+
anchorEl.style.left = `${(rect.left - o.left) / o.scaleX - padding}px`;
|
|
257
|
+
anchorEl.style.top = `${(rect.top - o.top) / o.scaleY - padding}px`;
|
|
258
|
+
anchorEl.style.width = `${rect.width / o.scaleX + padding * 2}px`;
|
|
259
|
+
anchorEl.style.height = `${rect.height / o.scaleY + padding * 2}px`;
|
|
260
|
+
}
|
|
219
261
|
/**
|
|
220
262
|
* Update the clip-path hole position to match the current target rect.
|
|
221
263
|
*/
|
|
@@ -225,15 +267,11 @@ export function spotlight(targetEl, fn) {
|
|
|
225
267
|
const rect = targetEl.getBoundingClientRect();
|
|
226
268
|
const padding = currentOptions.padding ?? 8;
|
|
227
269
|
const borderRadius = currentOptions.borderRadius ?? 8;
|
|
270
|
+
const o = overlayOrigin();
|
|
228
271
|
debug("updateHolePosition()", rect);
|
|
229
|
-
backdropEl.style.clipPath = buildClipPath(rect, padding, borderRadius);
|
|
272
|
+
backdropEl.style.clipPath = buildClipPath(rect, padding, borderRadius, o);
|
|
230
273
|
// Update the invisible anchor element position
|
|
231
|
-
|
|
232
|
-
anchorEl.style.left = `${rect.left - padding}px`;
|
|
233
|
-
anchorEl.style.top = `${rect.top - padding}px`;
|
|
234
|
-
anchorEl.style.width = `${rect.width + padding * 2}px`;
|
|
235
|
-
anchorEl.style.height = `${rect.height + padding * 2}px`;
|
|
236
|
-
}
|
|
274
|
+
positionAnchor(rect, padding, o);
|
|
237
275
|
// Reposition / re-clamp the annotation. The fallback path recomputes its
|
|
238
276
|
// base left/top here; the anchor path is re-placed by the browser. Either
|
|
239
277
|
// way we re-clamp so an edge-anchored annotation stays on-screen as the
|
|
@@ -281,17 +319,21 @@ export function spotlight(targetEl, fn) {
|
|
|
281
319
|
lastRect = null;
|
|
282
320
|
}
|
|
283
321
|
/**
|
|
284
|
-
* Position annotation without CSS Anchor Positioning (fallback).
|
|
322
|
+
* Position annotation without CSS Anchor Positioning (fallback). All values
|
|
323
|
+
* are expressed in the overlay's coordinate space (see {@link overlayOrigin})
|
|
324
|
+
* — for a fixed element, `left/top/right/bottom` resolve against the
|
|
325
|
+
* containing block, which is the viewport only in the un-portalled default.
|
|
285
326
|
*/
|
|
286
327
|
function positionAnnotationFallback(rect, padding) {
|
|
287
328
|
if (!annotationEl)
|
|
288
329
|
return;
|
|
289
330
|
const pos = currentOptions.position || "bottom";
|
|
290
331
|
const offset = 8; // px fallback offset
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
const
|
|
332
|
+
const o = overlayOrigin();
|
|
333
|
+
const x = (rect.left - o.left) / o.scaleX - padding;
|
|
334
|
+
const y = (rect.top - o.top) / o.scaleY - padding;
|
|
335
|
+
const w = rect.width / o.scaleX + padding * 2;
|
|
336
|
+
const h = rect.height / o.scaleY + padding * 2;
|
|
295
337
|
// Reset position
|
|
296
338
|
annotationEl.style.left = "";
|
|
297
339
|
annotationEl.style.top = "";
|
|
@@ -300,14 +342,14 @@ export function spotlight(targetEl, fn) {
|
|
|
300
342
|
annotationEl.style.transform = "";
|
|
301
343
|
if (pos.startsWith("top")) {
|
|
302
344
|
annotationEl.style.left = `${x}px`;
|
|
303
|
-
annotationEl.style.bottom = `${
|
|
345
|
+
annotationEl.style.bottom = `${o.height - y + offset}px`;
|
|
304
346
|
}
|
|
305
347
|
else if (pos.startsWith("bottom")) {
|
|
306
348
|
annotationEl.style.left = `${x}px`;
|
|
307
349
|
annotationEl.style.top = `${y + h + offset}px`;
|
|
308
350
|
}
|
|
309
351
|
else if (pos === "left") {
|
|
310
|
-
annotationEl.style.right = `${
|
|
352
|
+
annotationEl.style.right = `${o.width - x + offset}px`;
|
|
311
353
|
annotationEl.style.top = `${y}px`;
|
|
312
354
|
}
|
|
313
355
|
else if (pos === "right") {
|
|
@@ -328,7 +370,11 @@ export function spotlight(targetEl, fn) {
|
|
|
328
370
|
function clampAnnotationIntoViewport() {
|
|
329
371
|
if (!annotationEl || !annotationShown)
|
|
330
372
|
return;
|
|
331
|
-
|
|
373
|
+
// Pass the empirically measured CB rect (the inset-0 backdrop's box) so
|
|
374
|
+
// the clamp uses the SAME coordinate space as the hole/anchor math even
|
|
375
|
+
// where the heuristic ancestor walk and the engine disagree (WebKit
|
|
376
|
+
// filter shells).
|
|
377
|
+
clampIntoViewport(annotationEl, undefined, backdropEl ? backdropEl.getBoundingClientRect() : undefined);
|
|
332
378
|
}
|
|
333
379
|
function renderContent() {
|
|
334
380
|
if (!annotationEl || !currentOptions.content)
|
|
@@ -374,8 +420,12 @@ export function spotlight(targetEl, fn) {
|
|
|
374
420
|
requestAnimationFrame(() => {
|
|
375
421
|
if (!isVisible)
|
|
376
422
|
return; // may have been hidden in the meantime
|
|
423
|
+
const container = resolveContainerOption(currentOptions.container) ?? document.body;
|
|
377
424
|
const rect = targetEl.getBoundingClientRect();
|
|
378
|
-
// 1. Create backdrop overlay
|
|
425
|
+
// 1. Create backdrop overlay. Append BEFORE building the clip-path:
|
|
426
|
+
// the hole coordinates are relative to the backdrop's own box, which
|
|
427
|
+
// is only measurable once it is in the DOM (same JS turn — nothing
|
|
428
|
+
// paints unclipped).
|
|
379
429
|
backdropEl = document.createElement("div");
|
|
380
430
|
backdropEl.style.cssText = `
|
|
381
431
|
position: fixed;
|
|
@@ -385,26 +435,29 @@ export function spotlight(targetEl, fn) {
|
|
|
385
435
|
transition-duration: ${TRANSITION}ms;
|
|
386
436
|
`;
|
|
387
437
|
backdropEl.classList.add(...twMerge("stuic-spotlight-backdrop", currentOptions.classBackdrop).split(/\s/));
|
|
388
|
-
|
|
389
|
-
|
|
438
|
+
container.appendChild(backdropEl);
|
|
439
|
+
const o = overlayOrigin();
|
|
440
|
+
backdropEl.style.clipPath = buildClipPath(rect, padding, borderRadius, o);
|
|
390
441
|
// 2. Create invisible anchor element for CSS Anchor Positioning
|
|
391
442
|
anchorEl = document.createElement("div");
|
|
392
443
|
anchorEl.style.cssText = `
|
|
393
444
|
position: fixed;
|
|
394
|
-
left: ${rect.left - padding}px;
|
|
395
|
-
top: ${rect.top - padding}px;
|
|
396
|
-
width: ${rect.width + padding * 2}px;
|
|
397
|
-
height: ${rect.height + padding * 2}px;
|
|
398
445
|
pointer-events: none;
|
|
399
446
|
z-index: -1;
|
|
400
447
|
`;
|
|
448
|
+
positionAnchor(rect, padding, o);
|
|
401
449
|
addAnchorName(anchorEl, anchorName);
|
|
402
|
-
|
|
450
|
+
container.appendChild(anchorEl);
|
|
403
451
|
// 3. Create annotation element (if content provided)
|
|
404
452
|
if (currentOptions.content) {
|
|
405
453
|
annotationEl = document.createElement("div");
|
|
406
454
|
annotationEl.setAttribute("role", "dialog");
|
|
407
455
|
if (isSupported) {
|
|
456
|
+
// NOTE: keep `vw`/`vh` here — this is the ANCHORED branch, where
|
|
457
|
+
// the element's containing block is the `position-area` region (a
|
|
458
|
+
// slice of the CB, often much smaller), so `%` would shrink the
|
|
459
|
+
// annotation. Overflow is handled by position-try + the CB-aware
|
|
460
|
+
// clamp.
|
|
408
461
|
annotationEl.style.cssText = `
|
|
409
462
|
position: fixed;
|
|
410
463
|
position-anchor: ${anchorName};
|
|
@@ -420,17 +473,18 @@ export function spotlight(targetEl, fn) {
|
|
|
420
473
|
annotationEl.classList.add(...twMerge("stuic-spotlight-annotation", _classAnnotation, currentOptions.class).split(/\s/));
|
|
421
474
|
}
|
|
422
475
|
else {
|
|
423
|
-
// Fallback positioning
|
|
476
|
+
// Fallback positioning. `90%` (not `90vw`): the left/top values
|
|
477
|
+
// are containing-block-relative, so the size must be too.
|
|
424
478
|
annotationEl.style.cssText = `
|
|
425
479
|
position: fixed;
|
|
426
480
|
transition-duration: ${TRANSITION}ms;
|
|
427
481
|
z-index: 50;
|
|
428
|
-
max-width:
|
|
482
|
+
max-width: 90%;
|
|
429
483
|
`;
|
|
430
484
|
annotationEl.classList.add(...twMerge("stuic-spotlight-annotation-fallback", _classAnnotation, currentOptions.class).split(/\s/));
|
|
431
485
|
positionAnnotationFallback(rect, padding);
|
|
432
486
|
}
|
|
433
|
-
|
|
487
|
+
container.appendChild(annotationEl);
|
|
434
488
|
renderContent();
|
|
435
489
|
}
|
|
436
490
|
// 4. Lock body scroll
|
|
@@ -457,9 +511,12 @@ export function spotlight(targetEl, fn) {
|
|
|
457
511
|
if (currentOptions.closeOnBackdropClick !== false) {
|
|
458
512
|
backdropEl.addEventListener("click", onBackdropClick);
|
|
459
513
|
}
|
|
460
|
-
// 7. Watch for target position changes
|
|
514
|
+
// 7. Watch for target position changes — and for the overlay's own
|
|
515
|
+
// coordinate space changing size (the backdrop tracks its container,
|
|
516
|
+
// which resize/scroll listeners and target-rect comparison won't see)
|
|
461
517
|
resizeObserver = new ResizeObserver(updateHolePosition);
|
|
462
518
|
resizeObserver.observe(targetEl);
|
|
519
|
+
resizeObserver.observe(backdropEl);
|
|
463
520
|
window.addEventListener("resize", updateHolePosition);
|
|
464
521
|
window.addEventListener("scroll", updateHolePosition, true);
|
|
465
522
|
// 8. Per-frame compare-loop to catch layout shifts (sibling collapses,
|
|
@@ -527,6 +584,7 @@ export function spotlight(targetEl, fn) {
|
|
|
527
584
|
onHide: opts.onHide,
|
|
528
585
|
debug: opts.debug,
|
|
529
586
|
id: opts.id,
|
|
587
|
+
container: opts.container,
|
|
530
588
|
};
|
|
531
589
|
do_debug = !!opts.debug;
|
|
532
590
|
// Register in global registry if id provided
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
classButtonPrimary?: string;
|
|
25
25
|
intentButtonCancel?: IntentColorKey;
|
|
26
26
|
intentButtonCustom?: IntentColorKey;
|
|
27
|
-
/** Intent of the primary (OK) button. Defaults to `"primary"`, except for
|
|
28
|
-
* `"warn"`
|
|
27
|
+
/** Intent of the primary (OK) button. Defaults to `"primary"`, except for
|
|
28
|
+
* `"warn"` confirm/prompt dialogs, which default to `"destructive"`. A dialog
|
|
29
|
+
* option of the same name wins over this. */
|
|
29
30
|
intentButtonPrimary?: IntentColorKey;
|
|
30
31
|
classSpinnerBox?: string;
|
|
31
32
|
defaultIcons?: Partial<
|
|
@@ -22,8 +22,9 @@ export interface Props {
|
|
|
22
22
|
classButtonPrimary?: string;
|
|
23
23
|
intentButtonCancel?: IntentColorKey;
|
|
24
24
|
intentButtonCustom?: IntentColorKey;
|
|
25
|
-
/** Intent of the primary (OK) button. Defaults to `"primary"`, except for
|
|
26
|
-
* `"warn"`
|
|
25
|
+
/** Intent of the primary (OK) button. Defaults to `"primary"`, except for
|
|
26
|
+
* `"warn"` confirm/prompt dialogs, which default to `"destructive"`. A dialog
|
|
27
|
+
* option of the same name wins over this. */
|
|
27
28
|
intentButtonPrimary?: IntentColorKey;
|
|
28
29
|
classSpinnerBox?: string;
|
|
29
30
|
defaultIcons?: Partial<Record<"info" | "success" | "warn" | "error" | "spinner", () => string | undefined>>;
|
|
@@ -39,8 +39,9 @@
|
|
|
39
39
|
classButtonPrimary?: string;
|
|
40
40
|
intentButtonCancel?: IntentColorKey;
|
|
41
41
|
intentButtonCustom?: IntentColorKey;
|
|
42
|
-
/** Intent of the primary (OK) button. Defaults to `"primary"`, except for
|
|
43
|
-
* `"warn"`
|
|
42
|
+
/** Intent of the primary (OK) button. Defaults to `"primary"`, except for
|
|
43
|
+
* `"warn"` confirm/prompt dialogs, which default to `"destructive"`. A dialog
|
|
44
|
+
* option of the same name wins over this. */
|
|
44
45
|
intentButtonPrimary?: IntentColorKey;
|
|
45
46
|
classSpinnerBox?: string;
|
|
46
47
|
defaultIcons?: Partial<
|
|
@@ -77,12 +78,24 @@
|
|
|
77
78
|
|
|
78
79
|
let current = $derived(acp?.current!);
|
|
79
80
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
81
|
+
// Button config is layered: the dialog object's own value (most specific) wins over
|
|
82
|
+
// the component level prop - same as CmpButtonOk/Cancel/Custom below.
|
|
83
|
+
|
|
84
|
+
// The "warn" variant hints a destructive action, so unless explicitly configured
|
|
85
|
+
// otherwise, render the primary button accordingly. Not for ALERT though: its ok
|
|
86
|
+
// button only dismisses the dialog (onOk defaults to `shift`), so there is no
|
|
87
|
+
// destructive action to signal - a warning-severity *message* is not the same
|
|
88
|
+
// thing as a destructive *action*.
|
|
82
89
|
let _intentButtonPrimary = $derived(
|
|
83
|
-
intentButtonPrimary ??
|
|
90
|
+
current?.intentButtonPrimary ??
|
|
91
|
+
intentButtonPrimary ??
|
|
92
|
+
(current?.variant === "warn" && current?.type !== ALERT ? "destructive" : "primary")
|
|
84
93
|
);
|
|
85
94
|
|
|
95
|
+
let _intentButtonCancel = $derived(current?.intentButtonCancel ?? intentButtonCancel);
|
|
96
|
+
|
|
97
|
+
let _intentButtonCustom = $derived(current?.intentButtonCustom ?? intentButtonCustom);
|
|
98
|
+
|
|
86
99
|
let iconHtml = $derived.by(() => {
|
|
87
100
|
let fn = current.iconFn as any;
|
|
88
101
|
if (current.iconFn === true) {
|
|
@@ -219,8 +232,15 @@
|
|
|
219
232
|
{#if current.type !== ALERT}
|
|
220
233
|
<li class={twMerge(_classMenuLi, classMenuLi, hasCustom && classMenuLiCustom)}>
|
|
221
234
|
<CmpButtonCancel
|
|
222
|
-
class={twMerge(
|
|
223
|
-
|
|
235
|
+
class={twMerge(
|
|
236
|
+
"cancel",
|
|
237
|
+
_classButton,
|
|
238
|
+
classButton,
|
|
239
|
+
classButtonCancel,
|
|
240
|
+
current.classButton,
|
|
241
|
+
current.classButtonCancel
|
|
242
|
+
)}
|
|
243
|
+
intent={_intentButtonCancel}
|
|
224
244
|
disabled={isPending}
|
|
225
245
|
onclick={createOnClick("cancel", current.onCancel)}
|
|
226
246
|
>
|
|
@@ -231,8 +251,15 @@
|
|
|
231
251
|
{#if hasCustom}
|
|
232
252
|
<li class={twMerge(_classMenuLi, classMenuLi, classMenuLiCustom)}>
|
|
233
253
|
<CmpButtonCustom
|
|
234
|
-
class={twMerge(
|
|
235
|
-
|
|
254
|
+
class={twMerge(
|
|
255
|
+
"custom",
|
|
256
|
+
_classButton,
|
|
257
|
+
classButton,
|
|
258
|
+
classButtonCustom,
|
|
259
|
+
current.classButton,
|
|
260
|
+
current.classButtonCustom
|
|
261
|
+
)}
|
|
262
|
+
intent={_intentButtonCustom}
|
|
236
263
|
disabled={isPending}
|
|
237
264
|
onclick={createOnClick("custom", current.onCustom!)}
|
|
238
265
|
>
|
|
@@ -242,7 +269,14 @@
|
|
|
242
269
|
{/if}
|
|
243
270
|
<li class={twMerge(_classMenuLi, classMenuLi, hasCustom && classMenuLiCustom)}>
|
|
244
271
|
<CmpButtonOk
|
|
245
|
-
class={twMerge(
|
|
272
|
+
class={twMerge(
|
|
273
|
+
"ok",
|
|
274
|
+
_classButton,
|
|
275
|
+
classButton,
|
|
276
|
+
classButtonPrimary,
|
|
277
|
+
current.classButton,
|
|
278
|
+
current.classButtonPrimary
|
|
279
|
+
)}
|
|
246
280
|
intent={_intentButtonPrimary}
|
|
247
281
|
disabled={isPending}
|
|
248
282
|
onclick={createOnClick("ok", current.onOk)}
|
|
@@ -22,8 +22,9 @@ interface Props {
|
|
|
22
22
|
classButtonPrimary?: string;
|
|
23
23
|
intentButtonCancel?: IntentColorKey;
|
|
24
24
|
intentButtonCustom?: IntentColorKey;
|
|
25
|
-
/** Intent of the primary (OK) button. Defaults to `"primary"`, except for
|
|
26
|
-
* `"warn"`
|
|
25
|
+
/** Intent of the primary (OK) button. Defaults to `"primary"`, except for
|
|
26
|
+
* `"warn"` confirm/prompt dialogs, which default to `"destructive"`. A dialog
|
|
27
|
+
* option of the same name wins over this. */
|
|
27
28
|
intentButtonPrimary?: IntentColorKey;
|
|
28
29
|
classSpinnerBox?: string;
|
|
29
30
|
defaultIcons?: Partial<Record<"info" | "success" | "warn" | "error" | "spinner", () => string | undefined>>;
|
|
@@ -4,23 +4,23 @@ A modern, customizable replacement for native browser `alert()`, `confirm()`, an
|
|
|
4
4
|
|
|
5
5
|
## Props
|
|
6
6
|
|
|
7
|
-
| Prop | Type | Default | Description
|
|
8
|
-
| --------------------- | ------------------------------- | ------- |
|
|
9
|
-
| `acp` | `AlertConfirmPromptStack` | - | Stack instance managing the dialog queue
|
|
10
|
-
| `forceAsHtml` | `boolean` | `false` | Render all content as HTML
|
|
11
|
-
| `class` | `string` | - | CSS classes for the modal dialog
|
|
12
|
-
| `classWrap` | `string` | - | CSS for outer wrapper
|
|
13
|
-
| `classIconBox` | `string` | - | CSS for icon container
|
|
14
|
-
| `classTitle` | `string` | - | CSS for title text
|
|
15
|
-
| `classContent` | `string` | - | CSS for content area
|
|
16
|
-
| `classInput` | `string` | - | CSS for prompt input field
|
|
17
|
-
| `classButton` | `string` | - | CSS for all buttons
|
|
18
|
-
| `classButtonPrimary` | `string` | - | CSS for OK button
|
|
19
|
-
| `classButtonCancel` | `string` | - | CSS for Cancel button
|
|
20
|
-
| `intentButtonPrimary` | `IntentColorKey` | - | Intent of the OK button; defaults to `"primary"`, or `"destructive"` for
|
|
21
|
-
| `intentButtonCancel` | `IntentColorKey` | - | Intent of the Cancel button
|
|
22
|
-
| `intentButtonCustom` | `IntentColorKey` | - | Intent of the custom button
|
|
23
|
-
| `defaultIcons` | `Record<variant, () => string>` | - | Custom icon functions per variant
|
|
7
|
+
| Prop | Type | Default | Description |
|
|
8
|
+
| --------------------- | ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
9
|
+
| `acp` | `AlertConfirmPromptStack` | - | Stack instance managing the dialog queue |
|
|
10
|
+
| `forceAsHtml` | `boolean` | `false` | Render all content as HTML |
|
|
11
|
+
| `class` | `string` | - | CSS classes for the modal dialog |
|
|
12
|
+
| `classWrap` | `string` | - | CSS for outer wrapper |
|
|
13
|
+
| `classIconBox` | `string` | - | CSS for icon container |
|
|
14
|
+
| `classTitle` | `string` | - | CSS for title text |
|
|
15
|
+
| `classContent` | `string` | - | CSS for content area |
|
|
16
|
+
| `classInput` | `string` | - | CSS for prompt input field |
|
|
17
|
+
| `classButton` | `string` | - | CSS for all buttons |
|
|
18
|
+
| `classButtonPrimary` | `string` | - | CSS for OK button |
|
|
19
|
+
| `classButtonCancel` | `string` | - | CSS for Cancel button |
|
|
20
|
+
| `intentButtonPrimary` | `IntentColorKey` | - | Intent of the OK button; defaults to `"primary"`, or `"destructive"` for `"warn"` confirm/prompt dialogs (a dialog option of the same name wins) |
|
|
21
|
+
| `intentButtonCancel` | `IntentColorKey` | - | Intent of the Cancel button |
|
|
22
|
+
| `intentButtonCustom` | `IntentColorKey` | - | Intent of the custom button |
|
|
23
|
+
| `defaultIcons` | `Record<variant, () => string>` | - | Custom icon functions per variant |
|
|
24
24
|
|
|
25
25
|
## AlertConfirmPromptStack API
|
|
26
26
|
|
|
@@ -42,16 +42,18 @@ A modern, customizable replacement for native browser `alert()`, `confirm()`, an
|
|
|
42
42
|
|
|
43
43
|
### Dialog Options
|
|
44
44
|
|
|
45
|
-
| Option
|
|
46
|
-
|
|
|
47
|
-
| `title`
|
|
48
|
-
| `content`
|
|
49
|
-
| `variant`
|
|
50
|
-
| `value`
|
|
51
|
-
| `labelOk`
|
|
52
|
-
| `labelCancel`
|
|
53
|
-
| `labelCustom`
|
|
54
|
-
| `onCustom`
|
|
45
|
+
| Option | Type | Description |
|
|
46
|
+
| ----------------------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `title` | `THC` | Dialog title |
|
|
48
|
+
| `content` | `THC` | Dialog message/content |
|
|
49
|
+
| `variant` | `"info" \| "success" \| "warn" \| "error"` | Visual style (for confirm/prompt, `"warn"` also renders the OK button as `destructive`) |
|
|
50
|
+
| `value` | `any` | Initial value for prompt |
|
|
51
|
+
| `labelOk` | `THC` | Custom OK button label |
|
|
52
|
+
| `labelCancel` | `THC` | Custom Cancel button label |
|
|
53
|
+
| `labelCustom` | `THC` | Optional third button label |
|
|
54
|
+
| `onCustom` | `(value) => void` | Custom button callback |
|
|
55
|
+
| `classButton`, `classButtonPrimary`, `classButtonCancel`, `classButtonCustom` | `string` | Per dialog button classes, merged on top of the same named component props |
|
|
56
|
+
| `intentButtonPrimary`, `intentButtonCancel`, `intentButtonCustom` | `IntentColorKey` | Per dialog button intents, overriding the same named component props (and the `"warn"` default) |
|
|
55
57
|
|
|
56
58
|
## Usage
|
|
57
59
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Component } from "svelte";
|
|
2
2
|
import type { THC } from "../Thc/Thc.svelte";
|
|
3
|
+
import type { IntentColorKey } from "../../utils/design-tokens.js";
|
|
3
4
|
/**
|
|
4
5
|
* Types of alert/confirm/prompt dialogs.
|
|
5
6
|
*/
|
|
@@ -42,6 +43,13 @@ export interface AlertConfirmPromptObj extends Record<string, any> {
|
|
|
42
43
|
CmpButtonOk?: Component;
|
|
43
44
|
CmpButtonCancel?: Component;
|
|
44
45
|
CmpButtonCustom?: Component;
|
|
46
|
+
classButton?: string;
|
|
47
|
+
classButtonPrimary?: string;
|
|
48
|
+
classButtonCancel?: string;
|
|
49
|
+
classButtonCustom?: string;
|
|
50
|
+
intentButtonPrimary?: IntentColorKey;
|
|
51
|
+
intentButtonCancel?: IntentColorKey;
|
|
52
|
+
intentButtonCustom?: IntentColorKey;
|
|
45
53
|
}
|
|
46
54
|
/**
|
|
47
55
|
* A reactive stack manager for alert, confirm, and prompt dialogs.
|
|
@@ -266,6 +266,7 @@
|
|
|
266
266
|
import Thc from "../Thc/Thc.svelte";
|
|
267
267
|
import ListItemButton from "../ListItemButton/ListItemButton.svelte";
|
|
268
268
|
import { BodyScroll } from "../../utils/body-scroll-locker.js";
|
|
269
|
+
import { fixedContainingBlockRect } from "../../utils/containing-block.js";
|
|
269
270
|
import { waitForTwoRepaints } from "../../utils/paint.js";
|
|
270
271
|
import {
|
|
271
272
|
extractSearchableItems,
|
|
@@ -542,15 +543,17 @@
|
|
|
542
543
|
await waitForTwoRepaints();
|
|
543
544
|
if (!dropdownEl || !isOpen) return;
|
|
544
545
|
|
|
546
|
+
// Measure against the dropdown's containing block, not the viewport —
|
|
547
|
+
// an ancestor with e.g. `transform` or `contain: layout|paint` is what
|
|
548
|
+
// the fixed dropdown actually resolves (and gets clipped) against.
|
|
545
549
|
const rect = dropdownEl.getBoundingClientRect();
|
|
546
|
-
const
|
|
547
|
-
const viewportHeight = window.innerHeight;
|
|
550
|
+
const cb = fixedContainingBlockRect(dropdownEl);
|
|
548
551
|
|
|
549
552
|
if (
|
|
550
|
-
rect.left <
|
|
551
|
-
rect.right >
|
|
552
|
-
rect.top <
|
|
553
|
-
rect.bottom >
|
|
553
|
+
rect.left < cb.left ||
|
|
554
|
+
rect.right > cb.right ||
|
|
555
|
+
rect.top < cb.top ||
|
|
556
|
+
rect.bottom > cb.bottom
|
|
554
557
|
) {
|
|
555
558
|
switchingToFallback = true;
|
|
556
559
|
runtimeFallback = true;
|
|
@@ -707,12 +710,16 @@
|
|
|
707
710
|
const heightStyle = searchConfig
|
|
708
711
|
? `height: ${maxHeight};`
|
|
709
712
|
: `max-height: ${maxHeight};`;
|
|
713
|
+
// `90%` (not `90vw`): the top/left/transform centering is relative to
|
|
714
|
+
// the containing block, so the size must be too — `%` resolves against
|
|
715
|
+
// the CB (identical to `vw` when the CB is the viewport, correct when
|
|
716
|
+
// an ancestor with `transform`/`contain` establishes one).
|
|
710
717
|
return `
|
|
711
718
|
position: fixed;
|
|
712
719
|
top: 50%;
|
|
713
720
|
left: 50%;
|
|
714
721
|
transform: translate(-50%, -50%);
|
|
715
|
-
max-width:
|
|
722
|
+
max-width: 90%;
|
|
716
723
|
${heightStyle}
|
|
717
724
|
${gutterStyle}
|
|
718
725
|
z-index: 50;
|
|
@@ -361,6 +361,7 @@ Use `contentBefore` for leading content (icons) and `contentAfter` for trailing
|
|
|
361
361
|
## Features
|
|
362
362
|
|
|
363
363
|
- **CSS Anchor Positioning**: Uses modern CSS anchor positioning with automatic fallback for unsupported browsers
|
|
364
|
+
- **Containing-block aware**: Overflow detection and the fallback modal measure against the dropdown's actual containing block — inside a `transform`ed or `contain: layout|paint` shell they use the shell's box, not the viewport
|
|
364
365
|
- **Full Keyboard Navigation**: Complete arrow key navigation with Home/End support
|
|
365
366
|
- **Expandable Sections**: Collapsible groups with independent toggle state
|
|
366
367
|
- **ARIA Compliant**: Proper menu roles and keyboard interaction
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
<script lang="ts">
|
|
68
68
|
import { untrack } from "svelte";
|
|
69
69
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
70
|
+
import { fixedContainingBlockRect } from "../../utils/containing-block.js";
|
|
70
71
|
import { localStorageState } from "../../utils/persistent-state.svelte.js";
|
|
71
72
|
import { draggable as draggableAction } from "../../actions/draggable.svelte.js";
|
|
72
73
|
import { iconChevronDown, iconX } from "../../icons/index.js";
|
|
@@ -130,6 +131,26 @@
|
|
|
130
131
|
|
|
131
132
|
function viewport(): FloatSize {
|
|
132
133
|
if (typeof window === "undefined") return { width: 0, height: 0 };
|
|
134
|
+
// The panel is `position: fixed`, so its `left`/`top` (and therefore all
|
|
135
|
+
// placement/clamping math) resolve against its containing block — the
|
|
136
|
+
// viewport, unless an ancestor (`transform`, `contain: layout|paint`, …)
|
|
137
|
+
// establishes one. `x`/`y` are written as `left`/`top`, i.e. LAYOUT px,
|
|
138
|
+
// while the CB rect is visual — divide out the accumulated ancestor
|
|
139
|
+
// scale (measured on the panel itself; sub-pixel offset* rounding is
|
|
140
|
+
// treated as unscaled).
|
|
141
|
+
if (el) {
|
|
142
|
+
const cb = fixedContainingBlockRect(el);
|
|
143
|
+
const r = el.getBoundingClientRect();
|
|
144
|
+
const sx =
|
|
145
|
+
el.offsetWidth && Math.abs(r.width - el.offsetWidth) > 1
|
|
146
|
+
? r.width / el.offsetWidth
|
|
147
|
+
: 1;
|
|
148
|
+
const sy =
|
|
149
|
+
el.offsetHeight && Math.abs(r.height - el.offsetHeight) > 1
|
|
150
|
+
? r.height / el.offsetHeight
|
|
151
|
+
: 1;
|
|
152
|
+
return { width: cb.width / sx, height: cb.height / sy };
|
|
153
|
+
}
|
|
133
154
|
return { width: window.innerWidth, height: window.innerHeight };
|
|
134
155
|
}
|
|
135
156
|
|
|
@@ -5,7 +5,7 @@ dev/inspector tweak panel (dat.GUI / Tweakpane style). It has a header (optional
|
|
|
5
5
|
icon, a `THC` title, an actions slot, and minimize/close buttons) and an arbitrary body.
|
|
6
6
|
|
|
7
7
|
- **Positioned by params**: numeric `x`/`y` **or** a named `placement` preset (corners / edges / center).
|
|
8
|
-
- **`position: fixed`** relative to the viewport
|
|
8
|
+
- **`position: fixed`** relative to the viewport — or to the nearest containing-block ancestor (`transform`, `contain: layout|paint`, …) when one exists — with drag **clamped** so it never leaves that box.
|
|
9
9
|
- **Draggable** by the whole header (buttons excepted).
|
|
10
10
|
- **Minimizable** to just the title bar (header button, double-click header, or methods).
|
|
11
11
|
- **Imperative control** via a `bind:this` ref (mirrors `Modal`/`ModalDialog`).
|