@revenuecat/purchases-ui-js 4.8.16 → 4.8.17

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.
Files changed (49) hide show
  1. package/dist/components/loader/Loader.stories.svelte +96 -0
  2. package/dist/components/loader/Loader.stories.svelte.d.ts +18 -0
  3. package/dist/components/loader/LoaderNode.svelte +83 -0
  4. package/dist/components/loader/LoaderNode.svelte.d.ts +4 -0
  5. package/dist/components/loader/shared-frame-ticker.d.ts +6 -0
  6. package/dist/components/loader/shared-frame-ticker.js +28 -0
  7. package/dist/components/loader-animation/LoaderAnimation.stories.svelte +96 -0
  8. package/dist/components/loader-animation/LoaderAnimation.stories.svelte.d.ts +19 -0
  9. package/dist/components/loader-animation/LoaderAnimation.svelte +223 -0
  10. package/dist/components/loader-animation/LoaderAnimation.svelte.d.ts +4 -0
  11. package/dist/components/loader-animation/resolve-animation-fill.d.ts +3 -0
  12. package/dist/components/loader-animation/resolve-animation-fill.js +12 -0
  13. package/dist/components/loader-animation/ring-geometry.d.ts +11 -0
  14. package/dist/components/loader-animation/ring-geometry.js +13 -0
  15. package/dist/components/loader-group/LoaderGroup.stories.svelte +181 -0
  16. package/dist/components/loader-group/LoaderGroup.stories.svelte.d.ts +19 -0
  17. package/dist/components/loader-group/LoaderGroup.svelte +195 -0
  18. package/dist/components/loader-group/LoaderGroup.svelte.d.ts +4 -0
  19. package/dist/components/loader-group/LoaderGroupItem.svelte +46 -0
  20. package/dist/components/loader-group/LoaderGroupItem.svelte.d.ts +4 -0
  21. package/dist/components/loader-percentage/LoaderPercentage.stories.svelte +122 -0
  22. package/dist/components/loader-percentage/LoaderPercentage.stories.svelte.d.ts +19 -0
  23. package/dist/components/loader-percentage/LoaderPercentage.svelte +65 -0
  24. package/dist/components/loader-percentage/LoaderPercentage.svelte.d.ts +4 -0
  25. package/dist/components/loader-progress-components/LoaderProgressComponent.svelte +20 -0
  26. package/dist/components/loader-progress-components/LoaderProgressComponent.svelte.d.ts +4 -0
  27. package/dist/components/loader-progress-components/LoaderProgressComponents.stories.svelte +178 -0
  28. package/dist/components/loader-progress-components/LoaderProgressComponents.stories.svelte.d.ts +19 -0
  29. package/dist/components/loader-progress-components/LoaderProgressComponents.svelte +64 -0
  30. package/dist/components/loader-progress-components/LoaderProgressComponents.svelte.d.ts +4 -0
  31. package/dist/components/paywall/Node.svelte +15 -0
  32. package/dist/components/stack/Stack.svelte +13 -1
  33. package/dist/index.d.ts +14 -1
  34. package/dist/index.js +8 -0
  35. package/dist/stores/loader.d.ts +7 -0
  36. package/dist/stores/loader.js +16 -0
  37. package/dist/types/component.d.ts +6 -1
  38. package/dist/types/components/loader-animation.d.ts +52 -0
  39. package/dist/types/components/loader-animation.js +1 -0
  40. package/dist/types/components/loader-group.d.ts +38 -0
  41. package/dist/types/components/loader-group.js +1 -0
  42. package/dist/types/components/loader-percentage.d.ts +23 -0
  43. package/dist/types/components/loader-percentage.js +1 -0
  44. package/dist/types/components/loader-progress-components.d.ts +30 -0
  45. package/dist/types/components/loader-progress-components.js +1 -0
  46. package/dist/types/components/loader.d.ts +21 -0
  47. package/dist/types/components/loader.js +1 -0
  48. package/dist/utils/video-background-host.d.ts +1 -1
  49. package/package.json +1 -1
@@ -0,0 +1,96 @@
1
+ <script module lang="ts">
2
+ import LoaderNode from "./LoaderNode.svelte";
3
+ import { componentDecorator } from "../../stories/component-decorator";
4
+ import { paywallDecorator } from "../../stories/paywall-decorator";
5
+ import type { LoaderProps } from "../../types/components/loader";
6
+ import type { LoaderAnimationProps } from "../../types/components/loader-animation";
7
+ import type { StackProps } from "../../types/components/stack";
8
+ import { defineMeta } from "@storybook/addon-svelte-csf";
9
+
10
+ const circularBar: LoaderAnimationProps = {
11
+ type: "circular_loader_animation",
12
+ id: "bar",
13
+ name: "Loader animation",
14
+ track: {
15
+ diameter: { type: "fixed", value: 96 },
16
+ path_width: 8,
17
+ endcap: "round",
18
+ color: { light: { type: "hex", value: "#E5E2FA" } },
19
+ },
20
+ fill: {
21
+ path_width: 8,
22
+ endcap: "round",
23
+ color: { light: { type: "hex", value: "#6C63FF" } },
24
+ },
25
+ };
26
+
27
+ const linearBar: LoaderAnimationProps = {
28
+ type: "linear_loader_animation",
29
+ id: "bar",
30
+ name: "Loader animation",
31
+ track: {
32
+ size: {
33
+ width: { type: "fixed", value: 240 },
34
+ height: { type: "fixed", value: 12 },
35
+ },
36
+ shape: { type: "pill" },
37
+ color: { light: { type: "hex", value: "#E5E2FA" } },
38
+ },
39
+ fill: {
40
+ size: {
41
+ width: { type: "fixed", value: 240 },
42
+ height: { type: "fixed", value: 12 },
43
+ },
44
+ shape: { type: "pill" },
45
+ color: { light: { type: "hex", value: "#6C63FF" } },
46
+ },
47
+ };
48
+
49
+ const stack: StackProps = {
50
+ type: "stack",
51
+ id: "loader-stack",
52
+ name: "Loader stack",
53
+ components: [circularBar],
54
+ size: {
55
+ width: { type: "fit" },
56
+ height: { type: "fit" },
57
+ },
58
+ dimension: {
59
+ type: "vertical",
60
+ alignment: "center",
61
+ distribution: "center",
62
+ },
63
+ spacing: 0,
64
+ margin: { top: 0, trailing: 0, bottom: 0, leading: 0 },
65
+ padding: { top: 0, trailing: 0, bottom: 0, leading: 0 },
66
+ background_color: null,
67
+ background: null,
68
+ border: null,
69
+ shape: null,
70
+ shadow: null,
71
+ };
72
+
73
+ const { Story } = defineMeta({
74
+ title: "Components/Loader",
75
+ component: LoaderNode,
76
+ decorators: [paywallDecorator(), componentDecorator()],
77
+ args: {
78
+ type: "loader",
79
+ id: "loader",
80
+ name: "Loader",
81
+ loading_duration_ms: 5000,
82
+ action: { type: "workflow" },
83
+ stack,
84
+ } satisfies LoaderProps,
85
+ });
86
+ </script>
87
+
88
+ <Story name="Circular" parameters={{ chromatic: { disableSnapshot: true } }} />
89
+
90
+ <Story
91
+ name="Linear"
92
+ args={{
93
+ stack: { ...stack, components: [linearBar] },
94
+ }}
95
+ parameters={{ chromatic: { disableSnapshot: true } }}
96
+ />
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const Loader: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type Loader = InstanceType<typeof Loader>;
18
+ export default Loader;
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import { untrack } from "svelte";
3
+ import { writable } from "svelte/store";
4
+
5
+ import { sharedFrameNow } from "./shared-frame-ticker";
6
+
7
+ import Stack from "../stack/Stack.svelte";
8
+ import { useConditionContext } from "../../stores/condition-context.svelte";
9
+ import {
10
+ setLoaderContext,
11
+ type LoaderContextValue,
12
+ } from "../../stores/loader";
13
+ import { getPaywallContext } from "../../stores/paywall";
14
+ import type { LoaderProps } from "../../types/components/loader";
15
+ import { resolveOverrideProperties } from "../../utils/style-utils";
16
+
17
+ const props: LoaderProps = $props();
18
+
19
+ const { onButtonAction } = getPaywallContext();
20
+ const buildConditionContext = useConditionContext({ state: true });
21
+ const actualProps = $derived({
22
+ ...props,
23
+ ...resolveOverrideProperties(buildConditionContext(), props.overrides),
24
+ });
25
+
26
+ // Publishes progress (0–1) only; geometry belongs to the animation.
27
+ const loaderStore = writable<LoaderContextValue>({ progress: 0 });
28
+ setLoaderContext(loaderStore);
29
+
30
+ const isVisible = $derived(actualProps.visible !== false);
31
+
32
+ // Gated by visibility so a hidden loader can't dispatch in the background.
33
+ // untrack: the store's synchronous replay would otherwise make actualProps
34
+ // a dependency too, restarting the timer on any unrelated state change.
35
+ $effect(() => {
36
+ if (!isVisible) return;
37
+
38
+ return untrack(() => {
39
+ let fired = false;
40
+ let unsubscribe: (() => void) | undefined;
41
+ let dispatchFrame: number | undefined;
42
+
43
+ // Stamp startTs at subscribe time — the shared store replays its
44
+ // current value to new subscribers, which is not this loader's start.
45
+ const startTs = performance.now();
46
+
47
+ const onFrame = (now: number) => {
48
+ const elapsed = Math.max(0, now - startTs);
49
+ const next = Math.min(
50
+ 1,
51
+ elapsed / Math.max(1, actualProps.loading_duration_ms),
52
+ );
53
+ loaderStore.set({ progress: next });
54
+ if (next < 1 || fired) return;
55
+
56
+ fired = true;
57
+ // Detach before dispatching; the action unmounts this screen.
58
+ unsubscribe?.();
59
+ unsubscribe = undefined;
60
+
61
+ // Pass own id for workflow actions, matching ButtonNode's convention.
62
+ const actionId =
63
+ actualProps.action.type === "workflow" ? actualProps.id : undefined;
64
+ // Defer past the next paint so a loader_progress_components entry at
65
+ // appears_at: 100 gets a chance to render before navigation unmounts.
66
+ dispatchFrame = requestAnimationFrame(() =>
67
+ onButtonAction(actualProps.action, actionId),
68
+ );
69
+ };
70
+
71
+ unsubscribe = sharedFrameNow.subscribe(onFrame);
72
+
73
+ return () => {
74
+ unsubscribe?.();
75
+ if (dispatchFrame !== undefined) cancelAnimationFrame(dispatchFrame);
76
+ };
77
+ });
78
+ });
79
+ </script>
80
+
81
+ {#if isVisible}
82
+ <Stack {...actualProps.stack} testId="loader" />
83
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { LoaderProps } from "../../types/components/loader";
2
+ declare const LoaderNode: import("svelte").Component<LoaderProps, {}, "">;
3
+ type LoaderNode = ReturnType<typeof LoaderNode>;
4
+ export default LoaderNode;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Frame-paced monotonic clock (`performance.now()` ms) shared by every Loader,
3
+ * so N loaders drive one `requestAnimationFrame` loop rather than N. Shares
4
+ * only the clock, not the phase — each Loader keeps its own start timestamp.
5
+ */
6
+ export declare const sharedFrameNow: import("svelte/store").Readable<number>;
@@ -0,0 +1,28 @@
1
+ import { readable } from "svelte/store";
2
+ /**
3
+ * Frame-paced monotonic clock (`performance.now()` ms) shared by every Loader,
4
+ * so N loaders drive one `requestAnimationFrame` loop rather than N. Shares
5
+ * only the clock, not the phase — each Loader keeps its own start timestamp.
6
+ */
7
+ export const sharedFrameNow = readable(performance.now(), (set) => {
8
+ // Publish a fresh value before the first frame lands. Without this a new
9
+ // subscriber is replayed the module-load timestamp, and any consumer that
10
+ // treats its first value as a start time measures elapsed-since-page-load.
11
+ set(performance.now());
12
+ let frameId;
13
+ let stopped = false;
14
+ const tick = (now) => {
15
+ set(now);
16
+ // `set` can synchronously drop the last subscriber (stop() below), which
17
+ // must prevent this still-running call from scheduling another frame.
18
+ if (stopped)
19
+ return;
20
+ frameId = requestAnimationFrame(tick);
21
+ };
22
+ frameId = requestAnimationFrame(tick);
23
+ return () => {
24
+ stopped = true;
25
+ if (frameId !== undefined)
26
+ cancelAnimationFrame(frameId);
27
+ };
28
+ });
@@ -0,0 +1,96 @@
1
+ <script module lang="ts">
2
+ import LoaderAnimation from "./LoaderAnimation.svelte";
3
+ import { componentDecorator } from "../../stories/component-decorator";
4
+ import { paywallDecorator } from "../../stories/paywall-decorator";
5
+ import { setLoaderContext } from "../../stores/loader";
6
+ import type { LoaderAnimationProps } from "../../types/components/loader-animation";
7
+ import { defineMeta } from "@storybook/addon-svelte-csf";
8
+ import { writable } from "svelte/store";
9
+ import type { DecoratorFunction, Renderer } from "storybook/internal/csf";
10
+
11
+ /**
12
+ * Mock loader context with a fixed progress so visual stories are deterministic.
13
+ * Geometry comes from the animation's own props, so the context carries progress only.
14
+ */
15
+ function staticLoaderDecorator<TRenderer extends Renderer, TArgs>(
16
+ progress: number,
17
+ ): DecoratorFunction<TRenderer, TArgs> {
18
+ return (Story) => {
19
+ setLoaderContext(writable({ progress }));
20
+ return Story();
21
+ };
22
+ }
23
+
24
+ const linearArgs = {
25
+ type: "linear_loader_animation",
26
+ track: {
27
+ size: {
28
+ width: { type: "fixed", value: 240 },
29
+ height: { type: "fixed", value: 12 },
30
+ },
31
+ shape: { type: "pill" },
32
+ color: { light: { type: "hex", value: "#E5E2FA" } },
33
+ },
34
+ fill: {
35
+ size: {
36
+ width: { type: "fixed", value: 240 },
37
+ height: { type: "fixed", value: 12 },
38
+ },
39
+ shape: { type: "pill" },
40
+ color: { light: { type: "hex", value: "#6C63FF" } },
41
+ },
42
+ } satisfies Partial<LoaderAnimationProps>;
43
+
44
+ const { Story } = defineMeta({
45
+ title: "Components/LoaderAnimation",
46
+ component: LoaderAnimation,
47
+ decorators: [paywallDecorator(), componentDecorator()],
48
+ args: {
49
+ type: "circular_loader_animation",
50
+ id: "bar",
51
+ name: "Loader animation",
52
+ track: {
53
+ diameter: { type: "fixed", value: 96 },
54
+ path_width: 8,
55
+ endcap: "round",
56
+ color: { light: { type: "hex", value: "#E5E2FA" } },
57
+ },
58
+ fill: {
59
+ path_width: 8,
60
+ endcap: "round",
61
+ color: { light: { type: "hex", value: "#6C63FF" } },
62
+ },
63
+ } satisfies LoaderAnimationProps,
64
+ });
65
+ </script>
66
+
67
+ <Story
68
+ name="Circular — In progress"
69
+ decorators={[staticLoaderDecorator(0.7)]}
70
+ />
71
+
72
+ <Story name="Circular — Complete" decorators={[staticLoaderDecorator(1)]} />
73
+
74
+ <Story
75
+ name="Circular — Large diameter"
76
+ args={{
77
+ track: {
78
+ diameter: { type: "fixed", value: 200 },
79
+ path_width: 8,
80
+ endcap: "round",
81
+ color: { light: { type: "hex", value: "#E5E2FA" } },
82
+ },
83
+ }}
84
+ decorators={[staticLoaderDecorator(0.7)]}
85
+ />
86
+
87
+ <Story
88
+ name="Linear — In progress"
89
+ decorators={[staticLoaderDecorator(0.4)]}
90
+ args={linearArgs}
91
+ />
92
+ <Story
93
+ name="Linear — Complete"
94
+ decorators={[staticLoaderDecorator(1)]}
95
+ args={linearArgs}
96
+ />
@@ -0,0 +1,19 @@
1
+ import LoaderAnimation from "./LoaderAnimation.svelte";
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const LoaderAnimation: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type LoaderAnimation = InstanceType<typeof LoaderAnimation>;
19
+ export default LoaderAnimation;
@@ -0,0 +1,223 @@
1
+ <script lang="ts">
2
+ import { getColorModeContext } from "../../stores/color-mode";
3
+ import { useConditionContext } from "../../stores/condition-context.svelte";
4
+ import { getRequiredLoaderContext } from "../../stores/loader";
5
+ import type {
6
+ CircularAnimationLayer,
7
+ CircularLoaderAnimationProps,
8
+ LinearAnimationLayer,
9
+ LinearLoaderAnimationProps,
10
+ LoaderAnimationProps,
11
+ } from "../../types/components/loader-animation";
12
+ import { resolveOverrideProperties } from "../../utils/style-utils";
13
+
14
+ import { resolveAnimationFill } from "./resolve-animation-fill";
15
+ import { ringRadius, ringViewBoxSize, svgLinecap } from "./ring-geometry";
16
+ import {
17
+ css,
18
+ mapBorder,
19
+ mapBorderRadius,
20
+ mapColor,
21
+ mapColorInfo,
22
+ mapColorMode,
23
+ mapShadow,
24
+ mapSize,
25
+ mapSpacing,
26
+ } from "../../utils/base-utils";
27
+ import type { ColorGradientInfo } from "../../types/colors";
28
+
29
+ const props: LoaderAnimationProps = $props();
30
+ const svgId = $props.id();
31
+ const trackGradientId = `${svgId}-track`;
32
+ const fillGradientId = `${svgId}-fill`;
33
+
34
+ // SVG stroke can't take a CSS gradient() string — needs url(#id) into a
35
+ // real <linearGradient>/<radialGradient> def instead.
36
+ function svgPaint(info: ColorGradientInfo, id: string): string {
37
+ return info.type === "alias" || info.type === "hex"
38
+ ? mapColorInfo(info)
39
+ : `url(#${id})`;
40
+ }
41
+
42
+ const buildConditionContext = useConditionContext({ state: true });
43
+
44
+ const circularOverrideProps = $derived.by(() =>
45
+ props.type === "circular_loader_animation"
46
+ ? resolveOverrideProperties<CircularLoaderAnimationProps>(
47
+ buildConditionContext(),
48
+ props.overrides,
49
+ )
50
+ : {},
51
+ );
52
+ const linearOverrideProps = $derived.by(() =>
53
+ props.type === "linear_loader_animation"
54
+ ? resolveOverrideProperties<LinearLoaderAnimationProps>(
55
+ buildConditionContext(),
56
+ props.overrides,
57
+ )
58
+ : {},
59
+ );
60
+
61
+ const getColorMode = getColorModeContext();
62
+ const colorMode = $derived(getColorMode());
63
+
64
+ const loader = getRequiredLoaderContext(props.type);
65
+ const progress = $derived(Math.min(1, Math.max(0, $loader.progress)));
66
+
67
+ const diameterWidth = (layer: CircularAnimationLayer): string =>
68
+ layer.diameter.type === "fill" ? "100%" : mapSize(layer.diameter);
69
+
70
+ // ── Pill geometry ─────────────────────────────────────────────────────────
71
+ const pillWidth = (layer: LinearAnimationLayer) =>
72
+ layer.size.width.type === "fill" ? "100%" : mapSize(layer.size.width);
73
+
74
+ const pillLayerStyle = (layer: LinearAnimationLayer) => ({
75
+ height: mapSize(layer.size.height),
76
+ "border-radius": mapBorderRadius(layer.shape ?? null),
77
+ background: mapColor(colorMode, layer.color),
78
+ ...mapBorder(colorMode, layer.border ?? null),
79
+ "box-shadow": mapShadow(colorMode, layer.shadow ?? null),
80
+ });
81
+ </script>
82
+
83
+ {#snippet gradientDef(info: ColorGradientInfo, id: string)}
84
+ {#if info.type === "linear"}
85
+ <linearGradient
86
+ {id}
87
+ gradientTransform="rotate({info.degrees - 90}, 0.5, 0.5)"
88
+ >
89
+ {#each info.points as point (`${point.percent}-${point.color}`)}
90
+ <stop offset="{point.percent}%" stop-color={point.color} />
91
+ {/each}
92
+ </linearGradient>
93
+ {:else if info.type === "radial"}
94
+ <radialGradient {id}>
95
+ {#each info.points as point (`${point.percent}-${point.color}`)}
96
+ <stop offset="{point.percent}%" stop-color={point.color} />
97
+ {/each}
98
+ </radialGradient>
99
+ {/if}
100
+ {/snippet}
101
+
102
+ {#if props.type === "circular_loader_animation"}
103
+ {@const actualProps = { ...props, ...circularOverrideProps }}
104
+ {#if actualProps.visible !== false}
105
+ {@const viewBoxSize = ringViewBoxSize(actualProps.track)}
106
+ {@const centre = viewBoxSize / 2}
107
+ {@const fillLayer = resolveAnimationFill(
108
+ actualProps.track,
109
+ actualProps.fill,
110
+ )}
111
+ {@const fillCircumference =
112
+ 2 * Math.PI * ringRadius(viewBoxSize, fillLayer.path_width)}
113
+ {@const trackInfo = mapColorMode(colorMode, actualProps.track.color)}
114
+ {@const fillInfo = mapColorMode(colorMode, fillLayer.color)}
115
+ {@const outerStyle = css({
116
+ display: "inline-flex",
117
+ "align-items": "center",
118
+ "justify-content": "center",
119
+ "aspect-ratio": "1",
120
+ "flex-shrink": "0",
121
+ width: diameterWidth(actualProps.track),
122
+ margin: mapSpacing(actualProps.margin ?? null),
123
+ padding: mapSpacing(actualProps.padding ?? null),
124
+ })}
125
+ <div style={outerStyle}>
126
+ <svg
127
+ width="100%"
128
+ height="100%"
129
+ viewBox="0 0 {viewBoxSize} {viewBoxSize}"
130
+ aria-hidden="true"
131
+ class="rc-loader-animation rc-loader-animation--circular"
132
+ >
133
+ <defs>
134
+ {@render gradientDef(trackInfo, trackGradientId)}
135
+ {@render gradientDef(fillInfo, fillGradientId)}
136
+ </defs>
137
+ <circle
138
+ cx={centre}
139
+ cy={centre}
140
+ r={ringRadius(viewBoxSize, actualProps.track.path_width)}
141
+ fill="none"
142
+ stroke={svgPaint(trackInfo, trackGradientId)}
143
+ stroke-width={actualProps.track.path_width}
144
+ stroke-linecap={svgLinecap(actualProps.track.endcap)}
145
+ />
146
+ <circle
147
+ cx={centre}
148
+ cy={centre}
149
+ r={ringRadius(viewBoxSize, fillLayer.path_width)}
150
+ fill="none"
151
+ stroke={svgPaint(fillInfo, fillGradientId)}
152
+ stroke-width={fillLayer.path_width}
153
+ stroke-linecap={svgLinecap(fillLayer.endcap)}
154
+ stroke-dasharray={fillCircumference}
155
+ stroke-dashoffset={fillCircumference * (1 - progress)}
156
+ transform="rotate(-90 {centre} {centre})"
157
+ />
158
+ </svg>
159
+ </div>
160
+ {/if}
161
+ {:else}
162
+ {@const actualProps = { ...props, ...linearOverrideProps }}
163
+ {#if actualProps.visible !== false}
164
+ {@const fillLayer = resolveAnimationFill(
165
+ actualProps.track,
166
+ actualProps.fill,
167
+ )}
168
+ {@const outerStyle = css({
169
+ display: "block",
170
+ "flex-shrink": "0",
171
+ width: pillWidth(actualProps.track),
172
+ margin: mapSpacing(actualProps.margin ?? null),
173
+ padding: mapSpacing(actualProps.padding ?? null),
174
+ })}
175
+ <div style={outerStyle}>
176
+ <div
177
+ class="rc-loader-animation rc-loader-animation--linear rc-gradient-border"
178
+ style={css({
179
+ position: "relative",
180
+ width: "100%",
181
+ "box-sizing": "border-box",
182
+ ...pillLayerStyle(actualProps.track),
183
+ })}
184
+ >
185
+ <div
186
+ style={css({
187
+ width: "100%",
188
+ height: "100%",
189
+ overflow: "hidden",
190
+ "border-radius": mapBorderRadius(actualProps.track.shape ?? null),
191
+ })}
192
+ >
193
+ <div
194
+ class="rc-loader-animation__fill rc-gradient-border"
195
+ style={css({
196
+ width: `${progress * 100}%`,
197
+ ...pillLayerStyle(fillLayer),
198
+ })}
199
+ ></div>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ {/if}
204
+ {/if}
205
+
206
+ <style>
207
+ .rc-loader-animation {
208
+ box-sizing: border-box;
209
+ }
210
+
211
+ .rc-loader-animation__fill {
212
+ box-sizing: border-box;
213
+ transition: width 0.05s linear;
214
+ }
215
+
216
+ /* Inline transition would beat this — keep it in the stylesheet so
217
+ reduced-motion can actually override it. */
218
+ @media (prefers-reduced-motion: reduce) {
219
+ .rc-loader-animation__fill {
220
+ transition: none;
221
+ }
222
+ }
223
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { LoaderAnimationProps } from "../../types/components/loader-animation";
2
+ declare const LoaderAnimation: import("svelte").Component<LoaderAnimationProps, {}, "">;
3
+ type LoaderAnimation = ReturnType<typeof LoaderAnimation>;
4
+ export default LoaderAnimation;
@@ -0,0 +1,3 @@
1
+ import type { CircularAnimationLayer, LinearAnimationLayer } from "../../types/components/loader-animation";
2
+ /** Merges the fill's divergences over the complete track layer. */
3
+ export declare const resolveAnimationFill: <L extends CircularAnimationLayer | LinearAnimationLayer>(track: L, fill: Partial<Omit<L, "diameter">> | undefined) => L;
@@ -0,0 +1,12 @@
1
+ /** Merges the fill's divergences over the complete track layer. */
2
+ export const resolveAnimationFill = (track, fill) => {
3
+ if (!fill)
4
+ return track;
5
+ const resolved = { ...track };
6
+ for (const [key, value] of Object.entries(fill)) {
7
+ // An explicit undefined must not blank out the inherited value.
8
+ if (value !== undefined)
9
+ Object.assign(resolved, { [key]: value });
10
+ }
11
+ return resolved;
12
+ };
@@ -0,0 +1,11 @@
1
+ import type { CircularAnimationLayer } from "../../types/components/loader-animation";
2
+ /**
3
+ * A fixed diameter pegs the viewBox 1:1 to real CSS pixels, so `path_width`
4
+ * (typed as a literal pixel value) renders at its exact configured width.
5
+ * `fill`/`relative` diameters have no static pixel size to peg to, so their
6
+ * stroke width scales with the rendered size instead.
7
+ */
8
+ export declare const DEFAULT_RING_VIEWBOX = 100;
9
+ export declare const ringViewBoxSize: (layer: CircularAnimationLayer) => number;
10
+ export declare const ringRadius: (viewBoxSize: number, pathWidth: number) => number;
11
+ export declare const svgLinecap: (endcap: "round" | "square") => "round" | "butt";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A fixed diameter pegs the viewBox 1:1 to real CSS pixels, so `path_width`
3
+ * (typed as a literal pixel value) renders at its exact configured width.
4
+ * `fill`/`relative` diameters have no static pixel size to peg to, so their
5
+ * stroke width scales with the rendered size instead.
6
+ */
7
+ export const DEFAULT_RING_VIEWBOX = 100;
8
+ export const ringViewBoxSize = (layer) => layer.diameter.type === "fixed" ? layer.diameter.value : DEFAULT_RING_VIEWBOX;
9
+ export const ringRadius = (viewBoxSize, pathWidth) => Math.max(0, viewBoxSize / 2 - pathWidth / 2);
10
+ // `square` renders as `butt` — a flat cut exactly at the mathematical
11
+ // endpoint, so the stroke doesn't visibly start before 12 o'clock. `round`
12
+ // keeps its natural bulge past the endpoint.
13
+ export const svgLinecap = (endcap) => endcap === "round" ? "round" : "butt";