@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.
- package/dist/actions/popover/popover.svelte.js +5 -1
- package/dist/actions/tooltip/tooltip.svelte.js +5 -1
- package/dist/components/Skeleton/Skeleton.svelte +127 -0
- package/dist/components/Skeleton/Skeleton.svelte.d.ts +33 -0
- package/dist/components/Skeleton/index.css +62 -0
- package/dist/components/Skeleton/index.d.ts +1 -0
- package/dist/components/Skeleton/index.js +1 -0
- package/dist/components/Switch/Switch.svelte +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/anchor-name.d.ts +21 -0
- package/dist/utils/anchor-name.js +47 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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";
|
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
|
+
}
|