@marianmeres/stuic 2.56.0 → 2.57.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.
@@ -22,7 +22,7 @@
22
22
  /** Optional string for color hash calculation (e.g., email, user ID). Falls back to `initials` */
23
23
  hashSource?: string;
24
24
  /** Size preset or custom Tailwind size class */
25
- size?: "sm" | "md" | "lg" | "xl" | string;
25
+ size?: "sm" | "md" | "lg" | "xl" | "2xl" | string;
26
26
  /** Click handler - when provided, renders as a button */
27
27
  onclick?: (event: MouseEvent) => void;
28
28
  /** Background color (Tailwind class). Ignored if autoColor=true */
@@ -62,9 +62,10 @@
62
62
 
63
63
  const SIZE_PRESETS: Record<string, { container: string; icon: number }> = {
64
64
  sm: { container: "size-8 text-xs", icon: 16 },
65
- md: { container: "size-10 text-sm", icon: 20 },
66
- lg: { container: "size-14 text-base", icon: 28 },
67
- xl: { container: "size-16 text-lg", icon: 32 },
65
+ md: { container: "size-10 text-base", icon: 20 },
66
+ lg: { container: "size-12 text-lg", icon: 28 },
67
+ xl: { container: "size-14 text-xl", icon: 32 },
68
+ "2xl": { container: "size-16 text-2xl", icon: 36 },
68
69
  };
69
70
 
70
71
  // Extract initials from input string (email, name, or raw initials)
@@ -23,7 +23,7 @@ export interface Props {
23
23
  /** Optional string for color hash calculation (e.g., email, user ID). Falls back to `initials` */
24
24
  hashSource?: string;
25
25
  /** Size preset or custom Tailwind size class */
26
- size?: "sm" | "md" | "lg" | "xl" | string;
26
+ size?: "sm" | "md" | "lg" | "xl" | "2xl" | string;
27
27
  /** Click handler - when provided, renders as a button */
28
28
  onclick?: (event: MouseEvent) => void;
29
29
  /** Background color (Tailwind class). Ignored if autoColor=true */
@@ -271,11 +271,12 @@
271
271
  import Thc from "../Thc/Thc.svelte";
272
272
  import "./index.css";
273
273
  import { BodyScroll } from "../../utils/body-scroll-locker.js";
274
+ import { waitForTwoRepaints } from "../../utils/paint.js";
274
275
 
275
276
  let {
276
277
  items,
277
278
  isOpen = $bindable(false),
278
- position = "bottom-span-left",
279
+ position = "bottom-span-right",
279
280
  offset = "0.25rem",
280
281
  maxHeight = "300px",
281
282
  closeOnSelect = true,
@@ -314,7 +315,14 @@
314
315
  let wrapperEl: HTMLDivElement = $state()!;
315
316
  let activeItemEl: HTMLButtonElement | undefined = $state();
316
317
  const reducedMotion = prefersReducedMotion();
317
- const isSupported = untrack(() => !forceFallback && isAnchorPositioningSupported());
318
+
319
+ // Runtime overflow detection state
320
+ let runtimeFallback = $state(false);
321
+ let switchingToFallback = false; // Non-reactive flag to prevent recursion
322
+
323
+ const isSupported = $derived(
324
+ !forceFallback && !runtimeFallback && isAnchorPositioningSupported()
325
+ );
318
326
 
319
327
  // Track expanded sections (independent toggle - multiple can be open)
320
328
  let expandedSections = $state<Set<string | number>>(new Set());
@@ -380,6 +388,50 @@
380
388
  }
381
389
  });
382
390
 
391
+ // Reset runtime fallback when menu closes
392
+ $effect(() => {
393
+ if (!isOpen) {
394
+ // Unlock body scroll if we were in runtime fallback mode
395
+ // (must do this before resetting runtimeFallback, otherwise isSupported
396
+ // becomes true and the main body scroll effect skips the unlock)
397
+ if (runtimeFallback && !noScrollLock) {
398
+ BodyScroll.unlock();
399
+ }
400
+ runtimeFallback = false;
401
+ }
402
+ });
403
+
404
+ // Runtime viewport overflow detection
405
+ $effect(() => {
406
+ if (!isOpen || !dropdownEl || forceFallback || runtimeFallback) return;
407
+ if (!isAnchorPositioningSupported()) return;
408
+ if (switchingToFallback) return;
409
+
410
+ const checkOverflow = async () => {
411
+ await waitForTwoRepaints();
412
+ if (!dropdownEl || !isOpen) return;
413
+
414
+ const rect = dropdownEl.getBoundingClientRect();
415
+ const viewportWidth = window.innerWidth;
416
+ const viewportHeight = window.innerHeight;
417
+
418
+ if (
419
+ rect.left < 0 ||
420
+ rect.right > viewportWidth ||
421
+ rect.top < 0 ||
422
+ rect.bottom > viewportHeight
423
+ ) {
424
+ switchingToFallback = true;
425
+ runtimeFallback = true;
426
+ requestAnimationFrame(() => {
427
+ switchingToFallback = false;
428
+ });
429
+ }
430
+ };
431
+
432
+ checkOverflow();
433
+ });
434
+
383
435
  // Scroll active item into view
384
436
  $effect(() => {
385
437
  if (isOpen && _navItems.active?.id) {
@@ -3,10 +3,44 @@
3
3
  scrollbar-width: thin;
4
4
  }
5
5
 
6
+ @position-try --pop-top {
7
+ position-area: top; /* above, centered */
8
+ }
9
+ @position-try --pop-top-span-right {
10
+ position-area: top span-right; /* above, aligned to anchor's left edge */
11
+ }
12
+ @position-try --pop-top-span-left {
13
+ position-area: top span-left; /* above, aligned to anchor's right edge */
14
+ }
15
+ @position-try --pop-bottom {
16
+ position-area: bottom; /* below, centered */
17
+ }
18
+ @position-try --pop-bottom-span-right {
19
+ position-area: bottom span-right;
20
+ }
21
+ @position-try --pop-bottom-span-left {
22
+ position-area: bottom span-left;
23
+ }
24
+ @position-try --pop-left {
25
+ position-area: left;
26
+ }
27
+ @position-try --pop-right {
28
+ position-area: right;
29
+ }
30
+
6
31
  /* CSS Anchor Positioning supported mode - flip only when viewport overflow */
7
32
  @supports (anchor-name: --anchor) {
8
33
  .stuic-dropdown-menu-dropdown {
9
- position-try-fallbacks: flip-block, flip-inline;
34
+ /* position-try-fallbacks: flip-block, flip-inline; */
35
+
36
+ /* position-area is set via inline style based on position param */
37
+
38
+ /* fallbacks ensure popover stays within viewport */
39
+ /* order: try other bottom positions first, then top, then left/right */
40
+ position-try-fallbacks:
41
+ flip-inline, --pop-bottom-span-right, --pop-bottom-span-left, --pop-bottom,
42
+ flip-block, --pop-top-span-right, --pop-top-span-left, --pop-top,
43
+ --pop-left, --pop-right;
10
44
  }
11
45
  }
12
46
 
@@ -29,7 +29,8 @@ const _breakpoints = [
29
29
  * ```
30
30
  */
31
31
  export class Breakpoint {
32
- static #singleton;
32
+ /** Singleton */
33
+ static #instance;
33
34
  #bp = $derived.by(() => {
34
35
  const w = innerWidth.current || 0;
35
36
  return _breakpoints.reduce((m, [k, v]) => {
@@ -39,7 +40,11 @@ export class Breakpoint {
39
40
  }, { current: null, sm: false, md: false, lg: false, xl: false, "2xl": false });
40
41
  });
41
42
  static get instance() {
42
- return (Breakpoint.#singleton ??= new Breakpoint());
43
+ // return (Breakpoint.#instance ??= new Breakpoint();) // does not work with Svelte correctly...
44
+ if (!Breakpoint.#instance) {
45
+ Breakpoint.#instance = new Breakpoint();
46
+ }
47
+ return Breakpoint.#instance;
43
48
  }
44
49
  get current() {
45
50
  return this.#bp.current;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.56.0",
3
+ "version": "2.57.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",