@marianmeres/stuic 2.1.19 → 2.1.21

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.
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import { svgCircle, type SvgCircleOptions } from "../../utils/svg-circle.js";
3
+ import { twMerge } from "../../utils/tw-merge.js";
4
+
5
+ let {
6
+ strokeWidth = 10,
7
+ completeness = 1,
8
+ bgStrokeColor,
9
+ class: classProp = "",
10
+ roundedEdges = true,
11
+ rotate = 0,
12
+ strokeWidthRatio = 0,
13
+ style,
14
+ }: Partial<SvgCircleOptions> & { style?: string } = $props();
15
+
16
+ let container: HTMLDivElement = $state()!;
17
+
18
+ const circle = svgCircle({
19
+ strokeWidth,
20
+ completeness,
21
+ bgStrokeColor,
22
+ roundedEdges,
23
+ rotate,
24
+ strokeWidthRatio,
25
+ });
26
+
27
+ $effect(() => {
28
+ container.appendChild(circle.svg);
29
+ });
30
+
31
+ $effect(() => {
32
+ circle.setCompleteness(completeness);
33
+ });
34
+
35
+ $effect(() => {
36
+ circle.setRotate(rotate);
37
+ });
38
+ </script>
39
+
40
+ <div bind:this={container} class={twMerge("size-6", classProp)} {style}></div>
@@ -0,0 +1,7 @@
1
+ import { type SvgCircleOptions } from "../../utils/svg-circle.js";
2
+ type $$ComponentProps = Partial<SvgCircleOptions> & {
3
+ style?: string;
4
+ };
5
+ declare const Circle: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type Circle = ReturnType<typeof Circle>;
7
+ export default Circle;
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import { createTickerRAF } from "@marianmeres/ticker";
3
+ import { onDestroy } from "svelte";
4
+ import { oscillate } from "../../utils/oscillate.js";
5
+ import { twMerge } from "../../utils/tw-merge.js";
6
+ import Circle from "../Circle/Circle.svelte";
7
+
8
+ let {
9
+ class: classProp = "",
10
+ bgStrokeColor = "rgba(0 0 0 / .1)",
11
+ }: { class?: string; bgStrokeColor?: string } = $props();
12
+
13
+ /**
14
+ * NOTE: we happen to have 4 distinct values here which effect the overall look and feel...
15
+ * 1. the tick frequency
16
+ * 2. the oscillation time input (seconds)
17
+ * 3. the oscillation speed factor (1)
18
+ * 4. the animation-spin duration
19
+ */
20
+
21
+ const ticker = createTickerRAF(50, true);
22
+ let completeness = $derived(oscillate($ticker / 1000, 0.15, 0.85));
23
+
24
+ onDestroy(ticker.stop);
25
+ </script>
26
+
27
+ <Circle
28
+ {completeness}
29
+ class={twMerge("stuic-spinner-circle animate-spin", classProp)}
30
+ {bgStrokeColor}
31
+ style="animation-duration: .75s"
32
+ />
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ class?: string;
3
+ bgStrokeColor?: string;
4
+ };
5
+ declare const SpinnerCircle: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type SpinnerCircle = ReturnType<typeof SpinnerCircle>;
7
+ export default SpinnerCircle;
@@ -1,2 +1,3 @@
1
1
  export { default as Spinner } from "./Spinner.svelte";
2
- export { default as SpinnerUnicode, spinnerCreateBackAndForthCharFrames, type SpinnerUnicodeVariant, } from "./SpinnerUnicode.svelte";
2
+ export { default as SpinnerCircle } from "./SpinnerCircle.svelte";
3
+ export { spinnerCreateBackAndForthCharFrames, default as SpinnerUnicode, type SpinnerUnicodeVariant, } from "./SpinnerUnicode.svelte";
@@ -1,2 +1,3 @@
1
1
  export { default as Spinner } from "./Spinner.svelte";
2
- export { default as SpinnerUnicode, spinnerCreateBackAndForthCharFrames, } from "./SpinnerUnicode.svelte";
2
+ export { default as SpinnerCircle } from "./SpinnerCircle.svelte";
3
+ export { spinnerCreateBackAndForthCharFrames, default as SpinnerUnicode, } from "./SpinnerUnicode.svelte";
@@ -14,6 +14,7 @@ export * from "./maybe-json-parse.js";
14
14
  export * from "./maybe-json-stringify.js";
15
15
  export * from "./nl2br.js";
16
16
  export * from "./omit-pick.js";
17
+ export * from "./oscillate.js";
17
18
  export * from "./paint.js";
18
19
  export * from "./persistent-state.svelte.js";
19
20
  export * from "./prefers-reduced-motion.svelte.js";
@@ -23,6 +24,7 @@ export * from "./seconds.js";
23
24
  export * from "./sleep.js";
24
25
  export * from "./storage-abstraction.js";
25
26
  export * from "./str-hash.js";
27
+ export * from "./svg-circle.js";
26
28
  export * from "./switch.svelte.js";
27
29
  export * from "./throttle.js";
28
30
  export * from "./tr.js";
@@ -14,6 +14,7 @@ export * from "./maybe-json-parse.js";
14
14
  export * from "./maybe-json-stringify.js";
15
15
  export * from "./nl2br.js";
16
16
  export * from "./omit-pick.js";
17
+ export * from "./oscillate.js";
17
18
  export * from "./paint.js";
18
19
  export * from "./persistent-state.svelte.js";
19
20
  export * from "./prefers-reduced-motion.svelte.js";
@@ -23,6 +24,7 @@ export * from "./seconds.js";
23
24
  export * from "./sleep.js";
24
25
  export * from "./storage-abstraction.js";
25
26
  export * from "./str-hash.js";
27
+ export * from "./svg-circle.js";
26
28
  export * from "./switch.svelte.js";
27
29
  export * from "./throttle.js";
28
30
  export * from "./tr.js";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Returns an oscillating value (sine wave) between a min and max.
3
+ */
4
+ export declare function oscillate(time: number, min?: number, max?: number, speed?: number): number;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Returns an oscillating value (sine wave) between a min and max.
3
+ */
4
+ export function oscillate(time, min = 0, max = 1, speed = 1) {
5
+ // Calculate the midpoint (the center of the oscillation)
6
+ // e.g., if min=10, max=50, midpoint is (50+10)/2 = 30
7
+ const midpoint = (max + min) / 2;
8
+ // Calculate the amplitude (half the distance between min and max)
9
+ // e.g., if min=10, max=50, amplitude is (50-10)/2 = 20
10
+ const amplitude = (max - min) / 2;
11
+ // Calculate the oscillation
12
+ // 1. Math.sin(time * speed) gives a value between -1 and 1
13
+ // 2. Multiplying by amplitude scales it to [-amplitude, amplitude] (e.g., [-20, 20])
14
+ // 3. Adding midpoint shifts the range to [midpoint - amplitude, midpoint + amplitude]
15
+ // (e.g., [30 - 20, 30 + 20] which is [10, 50])
16
+ return midpoint + Math.sin(time * speed) * amplitude;
17
+ }
@@ -0,0 +1,16 @@
1
+ export interface SvgCircleOptions {
2
+ radius: number;
3
+ strokeWidth: number;
4
+ completeness: number;
5
+ class: string;
6
+ bgStrokeColor: string;
7
+ roundedEdges: boolean;
8
+ rotate: number;
9
+ strokeWidthRatio: number;
10
+ }
11
+ /** Will construct and return svg circle DOM element based on input options */
12
+ export declare function svgCircle(options?: Partial<SvgCircleOptions>): {
13
+ svg: SVGSVGElement;
14
+ setCompleteness(completeness: number): void;
15
+ setRotate(rotate: number): void;
16
+ };
@@ -0,0 +1,76 @@
1
+ function _normalize_completness(v) {
2
+ return Math.max(0, Math.min(1, v));
3
+ }
4
+ function _normalize_rotate(v) {
5
+ return v % 360;
6
+ }
7
+ function _normalize_cls(v) {
8
+ return [
9
+ ...new Set(v
10
+ .split(" ")
11
+ .map((v) => v.trim())
12
+ .filter(Boolean)),
13
+ ];
14
+ }
15
+ /** Will construct and return svg circle DOM element based on input options */
16
+ export function svgCircle(options = {}) {
17
+ let { strokeWidth = 10, completeness = 1, bgStrokeColor = "", class: classProp = "", roundedEdges = true, rotate = 0, strokeWidthRatio = 0, } = options ?? {};
18
+ completeness = _normalize_completness(completeness);
19
+ // calculate radius based on viewBox, leaving room for stroke
20
+ let actualStrokeWidth = strokeWidth;
21
+ if (strokeWidthRatio) {
22
+ const maxStrokeWidth = strokeWidthRatio * 50; // percentage of radius
23
+ actualStrokeWidth = Math.min(strokeWidth, maxStrokeWidth);
24
+ }
25
+ const radius = 50 - actualStrokeWidth / 2;
26
+ const center = 50;
27
+ const circumference = 2 * Math.PI * radius;
28
+ const dashArray = circumference;
29
+ const dashOffset = circumference * (1 - completeness);
30
+ const linecap = roundedEdges ? "round" : "butt";
31
+ // the svg element
32
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
33
+ svg.setAttribute("width", "100%");
34
+ svg.setAttribute("height", "100%");
35
+ svg.setAttribute("viewBox", "0 0 100 100");
36
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
37
+ if (classProp)
38
+ svg.classList.add(..._normalize_cls(classProp));
39
+ // optional background
40
+ if (bgStrokeColor) {
41
+ const bgCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
42
+ bgCircle.setAttribute("cx", `${center}`);
43
+ bgCircle.setAttribute("cy", `${center}`);
44
+ bgCircle.setAttribute("r", `${radius}`);
45
+ bgCircle.setAttribute("fill", "none");
46
+ bgCircle.setAttribute("stroke", bgStrokeColor);
47
+ bgCircle.setAttribute("stroke-width", `${actualStrokeWidth}`);
48
+ svg.appendChild(bgCircle);
49
+ }
50
+ // the circle
51
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
52
+ circle.setAttribute("cx", `${center}`);
53
+ circle.setAttribute("cy", `${center}`);
54
+ circle.setAttribute("r", `${radius}`);
55
+ circle.setAttribute("fill", "none");
56
+ circle.setAttribute("stroke", "currentColor");
57
+ circle.setAttribute("stroke-width", `${actualStrokeWidth}`);
58
+ circle.setAttribute("stroke-dasharray", `${dashArray}`);
59
+ circle.setAttribute("stroke-dashoffset", `${dashOffset}`);
60
+ circle.setAttribute("stroke-linecap", linecap);
61
+ circle.setAttribute("transform-origin", "center");
62
+ circle.setAttribute("transform", `rotate(${_normalize_rotate(rotate)})`);
63
+ //
64
+ svg.appendChild(circle);
65
+ return {
66
+ svg,
67
+ setCompleteness(completeness) {
68
+ completeness = _normalize_completness(completeness);
69
+ const dashOffset = circumference * (1 - completeness);
70
+ circle.setAttribute("stroke-dashoffset", `${dashOffset}`);
71
+ },
72
+ setRotate(rotate) {
73
+ circle.setAttribute("transform", `rotate(${_normalize_rotate(rotate)})`);
74
+ },
75
+ };
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.1.19",
3
+ "version": "2.1.21",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",