@marianmeres/stuic 2.21.1 → 2.23.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.
@@ -1,5 +1,6 @@
1
1
  import { mount, unmount } from "svelte";
2
2
  import { twMerge } from "../../utils/tw-merge.js";
3
+ import { addAnchorName, removeAnchorName } from "../../utils/anchor-name.js";
3
4
  import PopoverContent from "./PopoverContent.svelte";
4
5
  //
5
6
  import "./index.css";
@@ -162,7 +163,8 @@ export function popover(anchorEl, fn) {
162
163
  let currentOptions = {};
163
164
  // Initialize anchor element - anchor-name is always set
164
165
  // In forceFallback mode, the CSS is just ignored
165
- anchorEl.style.cssText += `anchor-name: ${anchorName};`;
166
+ // Use addAnchorName to support multiple anchor names on same element (e.g., popover + tooltip)
167
+ addAnchorName(anchorEl, anchorName);
166
168
  anchorEl.setAttribute("aria-haspopup", "dialog");
167
169
  anchorEl.setAttribute("aria-expanded", "false");
168
170
  anchorEl.setAttribute("aria-controls", id);
@@ -446,6 +448,8 @@ export function popover(anchorEl, fn) {
446
448
  anchorEl.removeEventListener("mouseleave", scheduleHide);
447
449
  anchorEl.removeEventListener("focus", scheduleShow);
448
450
  anchorEl.removeEventListener("blur", scheduleHide);
451
+ // Remove anchor name (preserves other anchor names on element)
452
+ removeAnchorName(anchorEl, anchorName);
449
453
  // Cleanup popover on unmount
450
454
  if (mountedComponent) {
451
455
  unmount(mountedComponent);
@@ -1,4 +1,5 @@
1
1
  import { twMerge } from "../../utils/tw-merge.js";
2
+ import { addAnchorName, removeAnchorName } from "../../utils/anchor-name.js";
2
3
  //
3
4
  import "./index.css";
4
5
  const TIMEOUT = 200;
@@ -103,7 +104,8 @@ export function tooltip(anchorEl, fn) {
103
104
  const id = `tooltip-${rnd}`;
104
105
  const anchorName = `--anchor-${rnd}`;
105
106
  // node once init
106
- anchorEl.style.cssText += `anchor-name: ${anchorName};`;
107
+ // Use addAnchorName to support multiple anchor names on same element (e.g., popover + tooltip)
108
+ addAnchorName(anchorEl, anchorName);
107
109
  anchorEl.setAttribute("aria-describedby", id);
108
110
  anchorEl.setAttribute("aria-expanded", "false");
109
111
  const debug = (...args) => {
@@ -230,6 +232,8 @@ export function tooltip(anchorEl, fn) {
230
232
  anchorEl.removeEventListener("mouseleave", schedule_hide);
231
233
  anchorEl.removeEventListener("focus", schedule_show);
232
234
  anchorEl.removeEventListener("blur", schedule_hide);
235
+ // Remove anchor name (preserves other anchor names on element)
236
+ removeAnchorName(anchorEl, anchorName);
233
237
  // might not have been initialized
234
238
  tooltipEl?.removeEventListener("mouseenter", schedule_show);
235
239
  tooltipEl?.removeEventListener("mouseleave", schedule_hide);
@@ -0,0 +1,127 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+
4
+ export interface Props extends Omit<
5
+ HTMLAttributes<HTMLDivElement>,
6
+ "children" | "class"
7
+ > {
8
+ /** Shape variant */
9
+ variant?: "text" | "circle" | "rectangle";
10
+ /** Width (e.g., "100%", "200px") */
11
+ width?: string;
12
+ /** Height (e.g., "1rem", "40px") */
13
+ height?: string;
14
+ /** Shorthand for circle size (sets both width & height) */
15
+ size?: string;
16
+ /** Number of text lines (for text variant) */
17
+ lines?: number;
18
+ /** Gap between lines (for text variant) */
19
+ gap?: string;
20
+ /** Last line width (for text variant) */
21
+ lastLineWidth?: string;
22
+ /** Animation style */
23
+ animation?: "shimmer" | "pulse" | "none";
24
+ /** Animation duration */
25
+ duration?: string;
26
+ /** Border radius (boolean for default, string for custom) */
27
+ rounded?: boolean | string;
28
+ /** Accessibility label */
29
+ ariaLabel?: string;
30
+ /** Bindable element reference */
31
+ el?: HTMLDivElement;
32
+ /** CSS class */
33
+ class?: string | string[];
34
+ }
35
+ </script>
36
+
37
+ <script lang="ts">
38
+ import "./index.css";
39
+ import { twMerge } from "../../utils/tw-merge.js";
40
+ import { prefersReducedMotion } from "../../utils/prefers-reduced-motion.svelte.js";
41
+
42
+ let {
43
+ variant = "rectangle",
44
+ width,
45
+ height,
46
+ size,
47
+ lines = 1,
48
+ gap = "0.5rem",
49
+ lastLineWidth = "75%",
50
+ animation = "shimmer",
51
+ duration = "1.5s",
52
+ rounded = true,
53
+ ariaLabel,
54
+ el = $bindable(),
55
+ class: classProp = "",
56
+ ...restProps
57
+ }: Props = $props();
58
+
59
+ const reduceMotion = prefersReducedMotion();
60
+
61
+ const effectiveAnimation = $derived(reduceMotion.current ? "none" : animation);
62
+
63
+ const baseClass = $derived(
64
+ twMerge(
65
+ "block bg-neutral-200 dark:bg-neutral-700",
66
+ effectiveAnimation === "shimmer" && "stuic-skeleton-shimmer",
67
+ effectiveAnimation === "pulse" && "stuic-skeleton-pulse",
68
+ variant === "circle" && "stuic-skeleton-circle",
69
+ rounded === true && variant !== "circle" && "rounded",
70
+ classProp
71
+ )
72
+ );
73
+
74
+ const baseStyle = $derived.by(() => {
75
+ const styles: string[] = [];
76
+ if (duration) styles.push(`--skeleton-duration: ${duration}`);
77
+ if (variant === "circle" && size) {
78
+ styles.push(`width: ${size}`, `height: ${size}`);
79
+ } else {
80
+ if (width) styles.push(`width: ${width}`);
81
+ if (height) styles.push(`height: ${height}`);
82
+ }
83
+ if (typeof rounded === "string") {
84
+ styles.push(`border-radius: ${rounded}`);
85
+ }
86
+ return styles.join("; ");
87
+ });
88
+ </script>
89
+
90
+ {#if variant === "text" && lines > 1}
91
+ <div
92
+ bind:this={el}
93
+ role="status"
94
+ aria-busy="true"
95
+ aria-label={ariaLabel}
96
+ class="stuic-skeleton-text-container"
97
+ style:gap
98
+ {...restProps}
99
+ >
100
+ {#each Array(lines) as _, i}
101
+ {@const isLast = i === lines - 1}
102
+ <div
103
+ class={baseClass}
104
+ style="{baseStyle}; width: {isLast
105
+ ? lastLineWidth
106
+ : width || '100%'}; height: {height || '1rem'}"
107
+ ></div>
108
+ {/each}
109
+ {#if ariaLabel}
110
+ <span class="sr-only">{ariaLabel}</span>
111
+ {/if}
112
+ </div>
113
+ {:else}
114
+ <div
115
+ bind:this={el}
116
+ role="status"
117
+ aria-busy="true"
118
+ aria-label={ariaLabel}
119
+ class={baseClass}
120
+ style={baseStyle}
121
+ {...restProps}
122
+ >
123
+ {#if ariaLabel}
124
+ <span class="sr-only">{ariaLabel}</span>
125
+ {/if}
126
+ </div>
127
+ {/if}
@@ -0,0 +1,33 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "class"> {
3
+ /** Shape variant */
4
+ variant?: "text" | "circle" | "rectangle";
5
+ /** Width (e.g., "100%", "200px") */
6
+ width?: string;
7
+ /** Height (e.g., "1rem", "40px") */
8
+ height?: string;
9
+ /** Shorthand for circle size (sets both width & height) */
10
+ size?: string;
11
+ /** Number of text lines (for text variant) */
12
+ lines?: number;
13
+ /** Gap between lines (for text variant) */
14
+ gap?: string;
15
+ /** Last line width (for text variant) */
16
+ lastLineWidth?: string;
17
+ /** Animation style */
18
+ animation?: "shimmer" | "pulse" | "none";
19
+ /** Animation duration */
20
+ duration?: string;
21
+ /** Border radius (boolean for default, string for custom) */
22
+ rounded?: boolean | string;
23
+ /** Accessibility label */
24
+ ariaLabel?: string;
25
+ /** Bindable element reference */
26
+ el?: HTMLDivElement;
27
+ /** CSS class */
28
+ class?: string | string[];
29
+ }
30
+ import "./index.css";
31
+ declare const Skeleton: import("svelte").Component<Props, {}, "el">;
32
+ type Skeleton = ReturnType<typeof Skeleton>;
33
+ export default Skeleton;
@@ -0,0 +1,62 @@
1
+ /* Define CSS custom properties for use in gradients */
2
+ :root {
3
+ --skeleton-base: #e5e5e5;
4
+ --skeleton-highlight: #f5f5f5;
5
+ }
6
+
7
+ .dark {
8
+ --skeleton-base: #404040;
9
+ --skeleton-highlight: #525252;
10
+ }
11
+
12
+ .stuic-skeleton-circle {
13
+ border-radius: 50%;
14
+ }
15
+
16
+ .stuic-skeleton-text-container {
17
+ display: flex;
18
+ flex-direction: column;
19
+ }
20
+
21
+ /* Shimmer animation */
22
+ @keyframes skeleton-shimmer {
23
+ 0% {
24
+ background-position: -200% 0;
25
+ }
26
+ 100% {
27
+ background-position: 200% 0;
28
+ }
29
+ }
30
+
31
+ .stuic-skeleton-shimmer {
32
+ background: linear-gradient(
33
+ 90deg,
34
+ var(--skeleton-base) 25%,
35
+ var(--skeleton-highlight) 50%,
36
+ var(--skeleton-base) 75%
37
+ );
38
+ background-size: 200% 100%;
39
+ animation: skeleton-shimmer var(--skeleton-duration, 1.5s) ease-in-out infinite;
40
+ }
41
+
42
+ /* Pulse animation */
43
+ @keyframes skeleton-pulse {
44
+ 0%, 100% {
45
+ opacity: 1;
46
+ }
47
+ 50% {
48
+ opacity: 0.4;
49
+ }
50
+ }
51
+
52
+ .stuic-skeleton-pulse {
53
+ animation: skeleton-pulse var(--skeleton-duration, 1.5s) ease-in-out infinite;
54
+ }
55
+
56
+ /* Reduced motion */
57
+ @media (prefers-reduced-motion: reduce) {
58
+ .stuic-skeleton-shimmer,
59
+ .stuic-skeleton-pulse {
60
+ animation: none;
61
+ }
62
+ }
@@ -0,0 +1 @@
1
+ export { default as Skeleton, type Props as SkeletonProps } from "./Skeleton.svelte";
@@ -0,0 +1 @@
1
+ export { default as Skeleton } from "./Skeleton.svelte";
@@ -97,7 +97,7 @@
97
97
  class={twMerge(
98
98
  "stuic-switch",
99
99
  `m-2
100
- relative inline-flex flex-shrink-0 items-center
100
+ relative inline-flex shrink-0 items-center
101
101
  rounded-full cursor-pointer
102
102
 
103
103
  transition-colors duration-100
package/dist/index.d.ts CHANGED
@@ -40,6 +40,7 @@ export * from "./components/Modal/index.js";
40
40
  export * from "./components/ModalDialog/index.js";
41
41
  export * from "./components/Notifications/index.js";
42
42
  export * from "./components/Progress/index.js";
43
+ export * from "./components/Skeleton/index.js";
43
44
  export * from "./components/SlidingPanels/index.js";
44
45
  export * from "./components/Spinner/index.js";
45
46
  export * from "./components/Switch/index.js";
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ export * from "./components/Modal/index.js";
41
41
  export * from "./components/ModalDialog/index.js";
42
42
  export * from "./components/Notifications/index.js";
43
43
  export * from "./components/Progress/index.js";
44
+ export * from "./components/Skeleton/index.js";
44
45
  export * from "./components/SlidingPanels/index.js";
45
46
  export * from "./components/Spinner/index.js";
46
47
  export * from "./components/Switch/index.js";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Utilities for managing CSS anchor-name property when multiple actions
3
+ * (like popover and tooltip) need to anchor to the same element.
4
+ *
5
+ * CSS anchor-name accepts a comma-separated list of names, allowing multiple
6
+ * anchor references on a single element.
7
+ */
8
+ /**
9
+ * Adds an anchor name to an element without overwriting existing ones.
10
+ *
11
+ * @param el - The element to add the anchor name to
12
+ * @param name - The anchor name to add (e.g., "--anchor-popover-xyz")
13
+ */
14
+ export declare function addAnchorName(el: HTMLElement, name: string): void;
15
+ /**
16
+ * Removes an anchor name from an element, preserving other anchor names.
17
+ *
18
+ * @param el - The element to remove the anchor name from
19
+ * @param name - The anchor name to remove
20
+ */
21
+ export declare function removeAnchorName(el: HTMLElement, name: string): void;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Utilities for managing CSS anchor-name property when multiple actions
3
+ * (like popover and tooltip) need to anchor to the same element.
4
+ *
5
+ * CSS anchor-name accepts a comma-separated list of names, allowing multiple
6
+ * anchor references on a single element.
7
+ */
8
+ /**
9
+ * Adds an anchor name to an element without overwriting existing ones.
10
+ *
11
+ * @param el - The element to add the anchor name to
12
+ * @param name - The anchor name to add (e.g., "--anchor-popover-xyz")
13
+ */
14
+ export function addAnchorName(el, name) {
15
+ const current = el.style.getPropertyValue("anchor-name").trim();
16
+ if (current) {
17
+ // Append to existing list (avoid duplicates)
18
+ const names = current.split(",").map((n) => n.trim());
19
+ if (!names.includes(name)) {
20
+ el.style.setProperty("anchor-name", `${current}, ${name}`);
21
+ }
22
+ }
23
+ else {
24
+ el.style.setProperty("anchor-name", name);
25
+ }
26
+ }
27
+ /**
28
+ * Removes an anchor name from an element, preserving other anchor names.
29
+ *
30
+ * @param el - The element to remove the anchor name from
31
+ * @param name - The anchor name to remove
32
+ */
33
+ export function removeAnchorName(el, name) {
34
+ const current = el.style.getPropertyValue("anchor-name").trim();
35
+ if (current) {
36
+ const names = current
37
+ .split(",")
38
+ .map((n) => n.trim())
39
+ .filter((n) => n !== name);
40
+ if (names.length > 0) {
41
+ el.style.setProperty("anchor-name", names.join(", "));
42
+ }
43
+ else {
44
+ el.style.removeProperty("anchor-name");
45
+ }
46
+ }
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.21.1",
3
+ "version": "2.23.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",