@marianmeres/stuic 2.1.7 → 2.1.9

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,6 @@
1
- <script lang="ts" module>
2
- const _instances = [];
3
- </script>
4
-
5
1
  <script lang="ts">
6
2
  import {
3
+ BodyScroll,
7
4
  focusTrap as focusTrapAction,
8
5
  twMerge,
9
6
  waitForNextRepaint,
@@ -11,7 +8,7 @@
11
8
  } from "../../index.js";
12
9
  import { createClog } from "@marianmeres/clog";
13
10
  import { PressedKeys, watch } from "runed";
14
- import { type Snippet } from "svelte";
11
+ import { onDestroy, onMount, tick, type Snippet } from "svelte";
15
12
  import { fade } from "svelte/transition";
16
13
 
17
14
  const clog = createClog("Backdrop").debug;
@@ -94,32 +91,15 @@
94
91
  }
95
92
  );
96
93
 
97
- // lock body scroll when open and restore back
98
- let _original: any = {};
99
94
  $effect(() => {
100
95
  if (noScrollLock) return;
101
- if (visible) {
102
- _original = window.getComputedStyle(document.body);
103
- const scrollY = window.scrollY;
104
-
105
- document.body.style.position = "fixed";
106
- document.body.style.top = `-${scrollY}px`;
107
- document.body.style.width = "100%";
108
- document.body.style.overflow = "hidden";
109
- } else {
110
- const scrollY = document.body.style.top;
111
-
112
- document.body.style.position = _original.position;
113
- document.body.style.position = "";
114
- document.body.style.top = "";
115
- document.body.style.width = "";
116
- document.body.style.overflow = "";
117
-
118
- // Restore scroll position
119
- window.scrollTo(0, parseInt(scrollY || "0") * -1);
120
- }
96
+ visible ? BodyScroll.lock() : BodyScroll.unlock();
121
97
  });
122
98
 
99
+ // we need onDestroy as well
100
+ // Note, that this will also reset if nested... (which is not desired, but ignoring)
101
+ onDestroy(BodyScroll.unlock);
102
+
123
103
  $effect(() => {
124
104
  function onkeydown(e: KeyboardEvent) {
125
105
  if (e.key === "Escape" && typeof onEscape === "function") {
@@ -13,7 +13,7 @@
13
13
 
14
14
  hover:brightness-105
15
15
  active:brightness-95
16
- disabled:hover:brightness-100
16
+ disabled:hover:brightness-100 disabled:opacity-50
17
17
 
18
18
  focus:brightness-105
19
19
  focus:border-button-border-focus focus:dark:border-button-border-focus-dark
@@ -1,4 +1,4 @@
1
- export declare const BUTTON_STUIC_BASE_CLASSES = "\n\t\tbg-button-bg text-button-text \n\t\tdark:bg-button-bg-dark dark:text-button-text-dark\n\t\tfont-mono text-sm text-center \n\t\tleading-none\n\t\tborder-1\n\t\tborder-button-border dark:border-button-border-dark\n\t\trounded-md\n\t\tinline-flex items-center justify-center gap-x-2\n\t\tpx-3 py-2\n\t\tselect-none\n\n\t\thover:brightness-105\n\t\tactive:brightness-95\n\t\tdisabled:hover:brightness-100\n\n\t\tfocus:brightness-105\n\t\tfocus:border-button-border-focus focus:dark:border-button-border-focus-dark\n\n\t\t focus:outline-4 focus:outline-black/10 focus:dark:outline-white/20\n\t\tfocus-visible:outline-4 focus-visible:outline-black/10 focus-visible:dark:outline-white/20\n\t";
1
+ export declare const BUTTON_STUIC_BASE_CLASSES = "\n\t\tbg-button-bg text-button-text \n\t\tdark:bg-button-bg-dark dark:text-button-text-dark\n\t\tfont-mono text-sm text-center \n\t\tleading-none\n\t\tborder-1\n\t\tborder-button-border dark:border-button-border-dark\n\t\trounded-md\n\t\tinline-flex items-center justify-center gap-x-2\n\t\tpx-3 py-2\n\t\tselect-none\n\n\t\thover:brightness-105\n\t\tactive:brightness-95\n\t\tdisabled:hover:brightness-100 disabled:opacity-50\n\n\t\tfocus:brightness-105\n\t\tfocus:border-button-border-focus focus:dark:border-button-border-focus-dark\n\n\t\t focus:outline-4 focus:outline-black/10 focus:dark:outline-white/20\n\t\tfocus-visible:outline-4 focus-visible:outline-black/10 focus-visible:dark:outline-white/20\n\t";
2
2
  export declare const BUTTON_STUIC_PRESET_CLASSES: any;
3
3
  import type { Snippet } from "svelte";
4
4
  import type { HTMLButtonAttributes } from "svelte/elements";
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { onClickOutside } from "runed";
3
- import { onMount, tick, type Snippet } from "svelte";
3
+ import { onDestroy, onMount, tick, type Snippet } from "svelte";
4
4
  import { focusTrap } from "../../actions/focus-trap.js";
5
5
  import { stopPropagation } from "../../utils/event-modifiers.js";
6
6
  import { twMerge } from "../../utils/tw-merge.js";
7
7
  import { createClog } from "@marianmeres/clog";
8
8
  import { waitForNextRepaint } from "../../utils/paint.js";
9
+ import { BodyScroll } from "../../utils/body-scroll-locker.js";
9
10
 
10
11
  const clog = createClog("ModalDialog").debug;
11
12
 
@@ -35,7 +36,9 @@
35
36
  preClose,
36
37
  }: Props = $props();
37
38
 
38
- let visible = $state(false);
39
+ // important to start as undefined (because of scroll save/restore)
40
+ let visible: boolean | undefined = $state(undefined);
41
+
39
42
  let dialog = $state<HTMLDialogElement>()!;
40
43
  let box = $state<HTMLDivElement>()!;
41
44
  let _opener: undefined | null | HTMLElement = $state();
@@ -76,31 +79,16 @@
76
79
  () => !noClickOutsideClose && close()
77
80
  );
78
81
 
79
- let _original: any = {};
80
82
  $effect(() => {
81
- // if (noScrollLock) return;
82
- if (visible) {
83
- _original = window.getComputedStyle(document.body);
84
- const scrollY = window.scrollY;
85
-
86
- document.body.style.position = "fixed";
87
- document.body.style.top = `-${scrollY}px`;
88
- document.body.style.width = "100%";
89
- document.body.style.overflow = "hidden";
90
- } else {
91
- const scrollY = document.body.style.top;
92
-
93
- document.body.style.position = _original.position;
94
- document.body.style.position = "";
95
- document.body.style.top = "";
96
- document.body.style.width = "";
97
- document.body.style.overflow = "";
98
-
99
- // Restore scroll position
100
- window.scrollTo(0, parseInt(scrollY || "0") * -1);
101
- }
83
+ // noop if we're undefined ($effect runs immediately as onMount)
84
+ if (visible === undefined) return;
85
+ visible ? BodyScroll.lock() : BodyScroll.unlock();
102
86
  });
103
87
 
88
+ // we need onDestroy as well
89
+ // Note, that this will also reset if nested... (which is not desired, but ignoring)
90
+ onDestroy(BodyScroll.unlock);
91
+
104
92
  // $inspect("Modal dialog mounted, is visible:", visible).with(clog);
105
93
  </script>
106
94
 
package/dist/index.css CHANGED
@@ -6,7 +6,7 @@
6
6
  @source "./";
7
7
 
8
8
  /* "components" looks like a better fit here, but forms plugin uses "utilities"
9
- so, since we need to override, sticking with that*/
9
+ so, since we need to override, sticking with that */
10
10
  @layer utilities {
11
11
  @import "./actions/tooltip/index.css";
12
12
  @import "./components/Button/index.css";
@@ -29,6 +29,6 @@ so, since we need to override, sticking with that*/
29
29
  [role="button"]:disabled,
30
30
  input:disabled {
31
31
  cursor: not-allowed !important;
32
- /* opacity: 0.5 !important; */
32
+ /* opacity: 0.5 !important; moved to Button itself, so it can be overridden */
33
33
  }
34
34
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Helper for "locking" and "unlocking" body scroll (window.scrollY) position
3
+ */
4
+ export declare class BodyScroll {
5
+ static lock(): void;
6
+ static unlock(): void;
7
+ private static _get_body_style;
8
+ private static _restore_body_styles;
9
+ }
@@ -0,0 +1,76 @@
1
+ import { createClog } from "@marianmeres/clog";
2
+ const clog = createClog("BodyScroll").debug;
3
+ /**
4
+ * Helper for "locking" and "unlocking" body scroll (window.scrollY) position
5
+ */
6
+ export class BodyScroll {
7
+ static lock() {
8
+ const data = document.body.dataset;
9
+ // Only save the scroll position if it hasn't been saved already
10
+ if (data.originalScrollY === undefined) {
11
+ const scrollY = window.scrollY || window.pageYOffset;
12
+ // Save body styles as serialized json
13
+ data.originalScrollStyleBackup = BodyScroll._get_body_style();
14
+ // Save the original scroll position in dataset
15
+ data.originalScrollY = `${scrollY}`;
16
+ data.scrollLockCount = "1";
17
+ // Apply the fixed positioning
18
+ document.body.style.position = "fixed";
19
+ document.body.style.top = `-${scrollY}px`;
20
+ document.body.style.width = "100%";
21
+ document.body.style.overflow = "hidden";
22
+ }
23
+ else {
24
+ // Another component already locked the scroll, just increment the counter
25
+ const currentCount = parseInt(data.scrollLockCount, 10);
26
+ data.scrollLockCount = `${currentCount + 1}`;
27
+ }
28
+ }
29
+ static unlock() {
30
+ const data = document.body.dataset;
31
+ // Only proceed if scroll is currently locked
32
+ if (data.scrollLockCount !== undefined) {
33
+ const count = parseInt(data.scrollLockCount, 10);
34
+ if (count > 1) {
35
+ // Other components still need the lock, just decrement the counter
36
+ data.scrollLockCount = `${count - 1}`;
37
+ }
38
+ else {
39
+ // This is the last component, restore everything
40
+ const originalScrollY = parseInt(data.originalScrollY, 10);
41
+ BodyScroll._restore_body_styles(data.originalScrollStyleBackup);
42
+ // Remove our data attributes
43
+ delete data.originalScrollY;
44
+ delete data.originalScrollStyleBackup;
45
+ delete data.scrollLockCount;
46
+ // Restore the scroll position
47
+ window.scrollTo(0, originalScrollY);
48
+ }
49
+ }
50
+ }
51
+ static _get_body_style() {
52
+ // we want only explicitly defined, not computed
53
+ const style = document.body.style;
54
+ return JSON.stringify({
55
+ position: style.position || null,
56
+ top: style.position || null,
57
+ width: style.width || null,
58
+ overflow: style.overflow || null,
59
+ });
60
+ }
61
+ static _restore_body_styles(originalJsonString) {
62
+ let original = { position: null, top: null, width: null, overflow: null };
63
+ try {
64
+ original = JSON.parse(originalJsonString);
65
+ }
66
+ catch (e) { }
67
+ ["position", "top", "width", "overflow"].forEach((k) => {
68
+ if (original[k] !== null) {
69
+ document.body.style[k] = original[k];
70
+ }
71
+ else {
72
+ document.body.style.removeProperty(k);
73
+ }
74
+ });
75
+ }
76
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Converts a CSS HEX color string to an Oklch color string.
3
+ */
4
+ export declare function hexToOklch(hex: string): string;
5
+ /**
6
+ * Converts a sRGB (0-255) to an Oklch color string.
7
+ */
8
+ export declare function rgbToOklch(rgb: {
9
+ r: number;
10
+ g: number;
11
+ b: number;
12
+ }): string;
13
+ /**
14
+ * Converts a HEX string to an RGB object.
15
+ */
16
+ export declare function hexToRgb(hex: string): {
17
+ r: number;
18
+ g: number;
19
+ b: number;
20
+ } | null;
21
+ /**
22
+ * Converts sRGB (0-255) to linear RGB (0.0-1.0).
23
+ */
24
+ export declare function srgbToLinearRgb({ r, g, b }: {
25
+ r: number;
26
+ g: number;
27
+ b: number;
28
+ }): {
29
+ r: number;
30
+ g: number;
31
+ b: number;
32
+ };
33
+ /**
34
+ * Converts linear RGB (0.0-1.0) to Oklab.
35
+ */
36
+ export declare function linearRgbToOklab({ r, g, b }: {
37
+ r: number;
38
+ g: number;
39
+ b: number;
40
+ }): {
41
+ l: number;
42
+ a: number;
43
+ b: number;
44
+ };
45
+ /**
46
+ * Converts Oklab to Oklch.
47
+ */
48
+ export declare function oklabToOklch({ l, a, b }: {
49
+ l: number;
50
+ a: number;
51
+ b: number;
52
+ }): {
53
+ l: number;
54
+ c: number;
55
+ h: number;
56
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Converts a CSS HEX color string to an Oklch color string.
3
+ */
4
+ export function hexToOklch(hex) {
5
+ // 1. Parse HEX to RGB
6
+ const rgb = hexToRgb(hex);
7
+ if (!rgb) {
8
+ throw new Error("Invalid HEX color format.");
9
+ }
10
+ // 2. Convert sRGB (0-255) to oklab
11
+ return rgbToOklch(rgb);
12
+ }
13
+ /**
14
+ * Converts a sRGB (0-255) to an Oklch color string.
15
+ */
16
+ export function rgbToOklch(rgb) {
17
+ // 1. Convert sRGB (0-255) to Linear RGB (0.0-1.0)
18
+ const linearRgb = srgbToLinearRgb(rgb);
19
+ // 2. Convert Linear RGB to Oklab
20
+ const oklab = linearRgbToOklab(linearRgb);
21
+ // 3. Convert Oklab to Oklch
22
+ const oklch = oklabToOklch(oklab);
23
+ // 4. Format as CSS string
24
+ // L is 0-1, formatted as 0-100%
25
+ // C is 0-0.4 (approx), formatted as a number
26
+ // h is 0-360, formatted as a number (degrees)
27
+ const l = (oklch.l * 100).toFixed(1);
28
+ const c = oklch.c.toFixed(3);
29
+ // Handle hue: NaN becomes 0
30
+ const h = isNaN(oklch.h) ? "0" : oklch.h.toFixed(1);
31
+ return `oklch(${l}% ${c} ${h})`;
32
+ }
33
+ /**
34
+ * Converts a HEX string to an RGB object.
35
+ */
36
+ export function hexToRgb(hex) {
37
+ let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
38
+ hex = hex.replace(shorthandRegex, (m, r, g, b) => {
39
+ return r + r + g + g + b + b;
40
+ });
41
+ let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
42
+ return result
43
+ ? {
44
+ r: parseInt(result[1], 16),
45
+ g: parseInt(result[2], 16),
46
+ b: parseInt(result[3], 16),
47
+ }
48
+ : null;
49
+ }
50
+ /**
51
+ * Converts sRGB (0-255) to linear RGB (0.0-1.0).
52
+ */
53
+ export function srgbToLinearRgb({ r, g, b }) {
54
+ const convert = (val) => {
55
+ let v = val / 255;
56
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
57
+ };
58
+ return {
59
+ r: convert(r),
60
+ g: convert(g),
61
+ b: convert(b),
62
+ };
63
+ }
64
+ /**
65
+ * Converts linear RGB (0.0-1.0) to Oklab.
66
+ */
67
+ export function linearRgbToOklab({ r, g, b }) {
68
+ // Based on the conversion matrices from the Oklab color space specification
69
+ const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
70
+ const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
71
+ const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
72
+ const l_ = Math.cbrt(l);
73
+ const m_ = Math.cbrt(m);
74
+ const s_ = Math.cbrt(s);
75
+ return {
76
+ l: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
77
+ a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
78
+ b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
79
+ };
80
+ }
81
+ /**
82
+ * Converts Oklab to Oklch.
83
+ */
84
+ export function oklabToOklch({ l, a, b }) {
85
+ const c = Math.sqrt(a * a + b * b);
86
+ let h = Math.atan2(b, a) * (180 / Math.PI);
87
+ // Normalize hue to be between 0 and 360
88
+ if (h < 0) {
89
+ h += 360;
90
+ }
91
+ return { l, c, h };
92
+ }
@@ -1,12 +1,12 @@
1
+ export * from "./body-scroll-locker.js";
1
2
  export * from "./breakpoint.svelte.js";
3
+ export * from "./colors.js";
2
4
  export * from "./debounce.js";
3
5
  export * from "./device-pointer.svelte.js";
4
6
  export * from "./escape-regex.js";
5
7
  export * from "./event-emitter.js";
6
8
  export * from "./event-modifiers.js";
7
9
  export * from "./get-id.js";
8
- export * from "./hex-to-oklch.js";
9
- export * from "./hex-to-rgb.js";
10
10
  export * from "./is-browser.js";
11
11
  export * from "./is-mac.js";
12
12
  export * from "./is-nullish.js";
@@ -1,12 +1,12 @@
1
+ export * from "./body-scroll-locker.js";
1
2
  export * from "./breakpoint.svelte.js";
3
+ export * from "./colors.js";
2
4
  export * from "./debounce.js";
3
5
  export * from "./device-pointer.svelte.js";
4
6
  export * from "./escape-regex.js";
5
7
  export * from "./event-emitter.js";
6
8
  export * from "./event-modifiers.js";
7
9
  export * from "./get-id.js";
8
- export * from "./hex-to-oklch.js";
9
- export * from "./hex-to-rgb.js";
10
10
  export * from "./is-browser.js";
11
11
  export * from "./is-mac.js";
12
12
  export * from "./is-nullish.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -1 +0,0 @@
1
- export {};
@@ -1,53 +0,0 @@
1
- "use strict";
2
- /**
3
- * Converts a hex color string to OKLCH
4
- * @param {string} hex - The hex color string (with or without leading #)
5
- * @returns {Object} An object with l (lightness), c (chroma), and h (hue) properties
6
- */
7
- function hexToOklch(hex) {
8
- // Remove the hash if it exists
9
- hex = hex.replace(/^#/, "");
10
- // Handle both 3-digit and 6-digit formats
11
- if (hex.length === 3) {
12
- hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
13
- }
14
- // Parse the hex values to RGB (0-1)
15
- const r = parseInt(hex.substring(0, 2), 16) / 255;
16
- const g = parseInt(hex.substring(2, 4), 16) / 255;
17
- const b = parseInt(hex.substring(4, 6), 16) / 255;
18
- // Convert RGB to linear RGB (removing gamma correction)
19
- const linearR = r <= 0.04045 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
20
- const linearG = g <= 0.04045 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
21
- const linearB = b <= 0.04045 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
22
- // Convert to XYZ
23
- const x = 0.4124 * linearR + 0.3576 * linearG + 0.1805 * linearB;
24
- const y = 0.2126 * linearR + 0.7152 * linearG + 0.0722 * linearB;
25
- const z = 0.0193 * linearR + 0.1192 * linearG + 0.9505 * linearB;
26
- // Convert XYZ to LMS (cone response)
27
- const l = 0.819 * x + 0.3619 * y - 0.1289 * z;
28
- const m = 0.0329 * x + 0.9293 * y + 0.0361 * z;
29
- const s = 0.0482 * x + 0.2645 * y + 0.6886 * z;
30
- // Apply non-linearity to LMS
31
- const lp = Math.cbrt(l);
32
- const mp = Math.cbrt(m);
33
- const sp = Math.cbrt(s);
34
- // Convert to Oklab
35
- const L = 0.2104 * lp + 0.7936 * mp - 0.004 * sp;
36
- const a = 1.9779 * lp - 2.4285 * mp + 0.4505 * sp;
37
- const bb = 0.0259 * lp + 0.7827 * mp - 0.8086 * sp;
38
- // Convert Oklab to Oklch
39
- const C = Math.sqrt(a * a + bb * bb);
40
- let h = (Math.atan2(bb, a) * 180) / Math.PI;
41
- // Ensure hue is positive
42
- if (h < 0) {
43
- h += 360;
44
- }
45
- return {
46
- l: parseFloat(L.toFixed(4)),
47
- c: parseFloat(C.toFixed(4)),
48
- h: parseFloat(h.toFixed(2)),
49
- };
50
- }
51
- // Example usage:
52
- // const oklch = hexToOklch("#ff5733");
53
- // console.log(oklch); // Example output: { l: 0.6321, c: 0.1549, h: 27.23 }
@@ -1,6 +0,0 @@
1
- /** Will convert #fff or #ffffff to {r: 255, g: 255, b: 255} */
2
- export declare function hexToRgb(hex: string): {
3
- r: number;
4
- g: number;
5
- b: number;
6
- };
@@ -1,12 +0,0 @@
1
- /** Will convert #fff or #ffffff to {r: 255, g: 255, b: 255} */
2
- export function hexToRgb(hex) {
3
- hex = hex.replace(/^#/, "");
4
- // both 3-digit and 6-digit formats
5
- if (hex.length === 3) {
6
- hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
7
- }
8
- const r = parseInt(hex.substring(0, 2), 16);
9
- const g = parseInt(hex.substring(2, 4), 16);
10
- const b = parseInt(hex.substring(4, 6), 16);
11
- return { r, g, b };
12
- }