@juspay/svelte-ui-components 2.23.1 → 2.24.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,9 +1,27 @@
1
1
  <script lang="ts">
2
2
  import type { ImgProperties } from './properties';
3
3
 
4
- let { src, alt, fallback, onerror, classes }: ImgProperties = $props();
4
+ let { src, alt, fallback, onerror, classes, inlineSvg, transformSvg }: ImgProperties = $props();
5
5
 
6
6
  let currentSrc = $derived(src);
7
+ // Per-source failure marker: when inlining a given URL fails we fall back to
8
+ // the plain <img> for that URL (which drives the regular fallback/onerror
9
+ // chain), while a new src still gets a fresh inlining attempt.
10
+ let failedInlineSrc: string | null = $state(null);
11
+
12
+ function isSvgSource(source: string): boolean {
13
+ if (source.startsWith('data:image/svg+xml')) {
14
+ return true;
15
+ }
16
+ const path = source.split('?')[0] ?? '';
17
+ return path.endsWith('.svg');
18
+ }
19
+
20
+ let shouldInline = $derived(
21
+ (inlineSvg === true || typeof transformSvg === 'function') &&
22
+ isSvgSource(currentSrc) &&
23
+ failedInlineSrc !== currentSrc
24
+ );
7
25
 
8
26
  function handleFallback(): void {
9
27
  if (typeof fallback === 'string' && fallback.length > 0 && currentSrc !== fallback) {
@@ -12,12 +30,98 @@
12
30
  onerror?.();
13
31
  }
14
32
  }
33
+
34
+ type InlineParams = {
35
+ url: string;
36
+ transform: ((svg: string) => string) | null;
37
+ };
38
+
39
+ async function loadInlineSvg(
40
+ host: SVGSVGElement,
41
+ url: string,
42
+ transform: ((svg: string) => string) | null,
43
+ signal: AbortSignal
44
+ ): Promise<void> {
45
+ try {
46
+ const response = await fetch(url, { signal });
47
+ if (!response.ok) {
48
+ failedInlineSrc = url;
49
+ return;
50
+ }
51
+ const rawSvg = await response.text();
52
+ const markup = typeof transform === 'function' ? transform(rawSvg) : rawSvg;
53
+ const parsed = new DOMParser().parseFromString(markup, 'image/svg+xml');
54
+ if (parsed.querySelector('parsererror') !== null) {
55
+ failedInlineSrc = url;
56
+ return;
57
+ }
58
+ const sourceSvg = parsed.querySelector('svg');
59
+ if (sourceSvg === null) {
60
+ failedInlineSrc = url;
61
+ return;
62
+ }
63
+ // The action may have been torn down (or re-run) while awaiting the
64
+ // body — bail before mutating so a stale fetch never paints over the
65
+ // latest one.
66
+ if (signal.aborted) {
67
+ return;
68
+ }
69
+ while (host.firstChild !== null) {
70
+ host.removeChild(host.firstChild);
71
+ }
72
+ for (const attribute of Array.from(sourceSvg.attributes)) {
73
+ if (attribute.name === 'class') {
74
+ continue;
75
+ }
76
+ host.setAttribute(attribute.name, attribute.value);
77
+ }
78
+ while (sourceSvg.firstChild !== null) {
79
+ host.appendChild(sourceSvg.firstChild);
80
+ }
81
+ } catch {
82
+ if (!signal.aborted) {
83
+ failedInlineSrc = url;
84
+ }
85
+ }
86
+ }
87
+
88
+ function inlineSvgInto(host: SVGSVGElement, params: InlineParams) {
89
+ let controller = new AbortController();
90
+
91
+ function load(current: InlineParams): void {
92
+ controller.abort();
93
+ controller = new AbortController();
94
+ void loadInlineSvg(host, current.url, current.transform, controller.signal);
95
+ }
96
+
97
+ load(params);
98
+
99
+ return {
100
+ update(next: InlineParams): void {
101
+ load(next);
102
+ },
103
+ destroy(): void {
104
+ controller.abort();
105
+ }
106
+ };
107
+ }
15
108
  </script>
16
109
 
17
- <img class={classes ?? ''} src={currentSrc} {alt} onerror={handleFallback} />
110
+ {#if shouldInline}
111
+ <svg
112
+ use:inlineSvgInto={{ url: currentSrc, transform: transformSvg ?? null }}
113
+ class={classes ?? ''}
114
+ role={alt.length > 0 ? 'img' : null}
115
+ aria-label={alt.length > 0 ? alt : null}
116
+ aria-hidden={alt.length === 0 ? 'true' : null}
117
+ ></svg>
118
+ {:else}
119
+ <img class={classes ?? ''} src={currentSrc} {alt} onerror={handleFallback} />
120
+ {/if}
18
121
 
19
122
  <style>
20
- img {
123
+ img,
124
+ svg {
21
125
  object-fit: var(--image-object-fit);
22
126
  height: var(--image-height, 24px);
23
127
  width: var(--image-width, 24px);
@@ -30,7 +134,8 @@
30
134
  transition: var(--image-transition);
31
135
  }
32
136
 
33
- img:hover {
137
+ img:hover,
138
+ svg:hover {
34
139
  background: var(--image-hover-background, var(--image-background));
35
140
  border: var(--image-hover-border, var(--image-border));
36
141
  }
@@ -6,6 +6,22 @@ export type MandatoryImgProperties = {
6
6
  export type OptionalImgProperties = {
7
7
  fallback?: string | null;
8
8
  classes?: string;
9
+ /**
10
+ * Fetch `.svg` / `data:image/svg+xml` sources and inline their markup into an
11
+ * `<svg>` host element so page CSS (currentColor, fill/stroke overrides) applies
12
+ * to the icon. Non-SVG sources always render the plain `<img>`. If fetching or
13
+ * parsing fails, the component falls back to the plain `<img>` (and from there
14
+ * to the regular `fallback`/`onerror` chain).
15
+ */
16
+ inlineSvg?: boolean;
17
+ /**
18
+ * Hook to rewrite the fetched SVG markup before it is inlined (e.g. recolor
19
+ * hardcoded fills). Receives the raw SVG text and returns the transformed text.
20
+ * Providing a transform implies `inlineSvg`. The inlining effect re-runs when
21
+ * the prop identity changes, so consumers can pass a closure over reactive
22
+ * state (e.g. a theme colour) to get live re-renders on theme change.
23
+ */
24
+ transformSvg?: (svg: string) => string;
9
25
  };
10
26
  export type ImgEventProperties = {
11
27
  onerror?: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.23.1",
3
+ "version": "2.24.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",