svseeds 0.4.10 → 0.4.12

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.
@@ -33,15 +33,93 @@
33
33
  window.document.documentElement.classList.add(theme);
34
34
  }
35
35
 
36
+ const THEME_SELECTOR = ":root";
37
+ const THEME_LAYER_NAME = "theme";
38
+ const THEME_NAME_KEY = "$theme";
39
+ const THEME_MEDIA_REGEX = /\(prefers-color-scheme:\s*(light|dark)\s*\)/i;
40
+ const THEME_COLOR_PREFIX = "--color-";
41
+ const THEME_TARGET_PREFIX = "--svs-";
36
42
  const ariaLabel = "Toggle theme color";
37
43
  const preset = "svs-dark-toggle";
38
44
 
45
+ type CustomProps = Record<string, string>;
46
+ type ThemeProps = Map<string, CustomProps>;
47
+ function getThemeProps(): ThemeProps {
48
+ if (typeof window === "undefined") return new Map();
49
+ genTwColors();
50
+ const sheets = window.document.styleSheets;
51
+ const rules: CustomProps[] = [];
52
+ for (let i = 0; i < sheets.length; i++) {
53
+ scanGroup(sheets[i])?.forEach((x) => rules.push(x));
54
+ }
55
+ twColors.clear();
56
+ return castRulesToMap(rules);
57
+ }
58
+ function castRulesToMap(rules: CustomProps[]): ThemeProps {
59
+ const map: ThemeProps = new Map();
60
+ rules.forEach(({ $theme, ...props }) => map.set($theme, props));
61
+ return map;
62
+ }
63
+ function getRules(sheet: CSSStyleSheet | CSSGroupingRule): CSSRuleList | undefined {
64
+ try {
65
+ return sheet.cssRules;
66
+ } catch (e) { // mainly, due to CORS
67
+ return;
68
+ }
69
+ }
70
+ function scanGroup(group: CSSStyleSheet | CSSGroupingRule, theme?: string): CustomProps[] {
71
+ const rules = getRules(group);
72
+ if (!rules) return [];
73
+ const ret: CustomProps[] = [];
74
+ for (let i = 0; i < rules.length; i++) {
75
+ filterRule(rules[i], theme)?.forEach((x) => ret.push(x));
76
+ }
77
+ return ret;
78
+ }
79
+ function filterRule(rule: CSSRule, theme?: string): CustomProps[] | undefined {
80
+ if (rule instanceof CSSLayerBlockRule && rule.name === THEME_LAYER_NAME) theme = "light";
81
+ if (rule instanceof CSSMediaRule) theme = getThemeName(rule.conditionText);
82
+ if (rule instanceof CSSGroupingRule) return scanGroup(rule, theme);
83
+ if (!(rule instanceof CSSStyleRule) && !(rule instanceof CSSPageRule)) return;
84
+ if (rule.selectorText.includes(THEME_SELECTOR)) {
85
+ const fallback = rule.selectorText === THEME_SELECTOR ? "light" : undefined;
86
+ return getCustomProps(rule.style, theme ?? fallback);
87
+ };
88
+ }
89
+ function getThemeName(condition: string): string | undefined {
90
+ const match = condition.match(THEME_MEDIA_REGEX);
91
+ return match?.[1]?.toLowerCase();
92
+ }
93
+ function getCustomProps(style: CSSStyleDeclaration, theme?: string): CustomProps[] | undefined {
94
+ if (!theme) return;
95
+ const props: CustomProps = { [THEME_NAME_KEY]: theme };
96
+ for (let i = 0; i < style.length; i++) {
97
+ const prop = style[i];
98
+ if (prop.startsWith("--") && isThemeCustomProp(prop)) {
99
+ props[prop] = style.getPropertyValue(prop).trim();
100
+ }
101
+ }
102
+ return Object.keys(props).length > 1 ? [props] : undefined;
103
+ }
104
+ function isThemeCustomProp(name: string): boolean {
105
+ if (name.startsWith(THEME_COLOR_PREFIX)) {
106
+ return !twColors.has(name);
107
+ }
108
+ return name.startsWith(THEME_TARGET_PREFIX);
109
+ }
110
+ function genTwColors() {
111
+ const TW_COLORS = ["red","orange","amber","yellow","lime","green","emerald","teal","cyan","sky","blue","indigo","violet","purple","fuchsia","pink","rose","slate","gray","zinc","neutral","stone"];
112
+ const TW_NUMBERS = ["50","100","200","300","400","500","600","700","800","900","950"];
113
+ TW_COLORS.forEach((color) => {
114
+ TW_NUMBERS.forEach((num) => {
115
+ twColors.add(`${THEME_COLOR_PREFIX}${color}-${num}`);
116
+ });
117
+ });
118
+ }
119
+
39
120
  class Theme {
40
- static #MEDIA_PREFER_DARK = "(prefers-color-scheme: dark)";
41
- static #REGEX = /\(prefers-color-scheme:\s*(light|dark)\s*\)/i;
42
- static #SELECTOR = ":root";
43
- #props: Map<string, Record<string, string>> = new Map();
44
- #theme;
121
+ #props: ThemeProps;
122
+ #theme: string;
45
123
  set dark(bool: boolean) {
46
124
  this.#switch(bool);
47
125
  }
@@ -51,7 +129,7 @@
51
129
 
52
130
  constructor() {
53
131
  this.#theme = Theme.getPreferTheme();
54
- this.#initProps();
132
+ this.#props = getThemeProps();
55
133
  this.#setColorScheme();
56
134
  setThemeToRoot(this.#theme);
57
135
  }
@@ -78,55 +156,10 @@
78
156
  const themes = Object.keys(this.#props).filter((x) => x === THEME.LIGHT || x === THEME.DARK);
79
157
  window.document.documentElement.style.colorScheme = themes.join(" ");
80
158
  }
81
- #initProps() {
82
- if (typeof window === "undefined") return;
83
- const sheets = window.document.styleSheets;
84
- for (let i = 0; i < sheets.length; i++) {
85
- this.#scanGroup(sheets[i]);
86
- }
87
- }
88
- #scanGroup(group: CSSStyleSheet | CSSGroupingRule, theme?: string) {
89
- const rules = Theme.#getRules(group);
90
- if (!rules) return;
91
- for (let i = 0; i < rules.length; i++) {
92
- this.#scanRule(rules[i], theme);
93
- }
94
- }
95
- #scanRule(rule: CSSRule, theme?: string) {
96
- if (rule instanceof CSSMediaRule) theme = this.#getTheme(rule.conditionText);
97
- if (rule instanceof CSSGroupingRule) return this.#scanGroup(rule, theme);
98
- if (!(rule instanceof CSSStyleRule) && !(rule instanceof CSSPageRule)) return;
99
- if (rule.selectorText.includes(Theme.#SELECTOR)) this.#setProps(rule.style, theme);
100
- }
101
- #setProps(style: CSSStyleDeclaration, theme?: string) {
102
- if (!theme) return;
103
- for (let i = 0; i < style.length; i++) {
104
- const prop = style[i];
105
- if (prop.startsWith("--")) {
106
- const value = theme ? style.getPropertyValue(prop).trim() : "";
107
- this.#setEachProp(theme, prop, value);
108
- }
109
- }
110
- }
111
- #getTheme(condition: string): string | undefined {
112
- const match = condition.match(Theme.#REGEX);
113
- return match?.[1]?.toLowerCase();
114
- }
115
- #setEachProp(theme: string, name: string, value: string) {
116
- if (!theme) return;
117
- if (!this.#props.has(theme)) this.#props.set(theme, {});
118
- this.#props.get(theme)![name] = value;
119
- }
120
- static #getRules(sheet: CSSStyleSheet | CSSGroupingRule): CSSRuleList | undefined {
121
- try {
122
- return sheet.cssRules;
123
- } catch (e) { // mainly, due to CORS
124
- return;
125
- }
126
- }
159
+
127
160
  static #isPreferDark(): boolean {
128
161
  if (typeof window === "undefined") return true;
129
- return window.matchMedia(Theme.#MEDIA_PREFER_DARK).matches;
162
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
130
163
  }
131
164
  static #getThemeString(dark: boolean): string {
132
165
  return dark ? THEME.DARK : THEME.LIGHT;
@@ -135,6 +168,7 @@
135
168
  return Theme.#getThemeString(Theme.#isPreferDark());
136
169
  }
137
170
  }
171
+ const twColors = new Set<string>();
138
172
  const theme = new Theme();
139
173
 
140
174
  import { VARIANT, omit } from "./core";
@@ -180,12 +214,12 @@
180
214
  </Toggle>
181
215
 
182
216
  {#snippet svgDark()}
183
- <svg style="width: 100%; height: 100%;" viewBox="0 0 24 24" aria-hidden="true">
217
+ <svg style="width:100%;height:100%;" viewBox="0 0 24 24" aria-hidden="true">
184
218
  <path fill-rule="evenodd" clip-rule="evenodd" d="M12.2256 2.00253C9.59172 1.94346 6.93894 2.9189 4.92893 4.92891C1.02369 8.83415 1.02369 15.1658 4.92893 19.071C8.83418 22.9763 15.1658 22.9763 19.0711 19.071C21.0811 17.061 22.0565 14.4082 21.9975 11.7743C21.9796 10.9772 21.8669 10.1818 21.6595 9.40643C21.0933 9.9488 20.5078 10.4276 19.9163 10.8425C18.5649 11.7906 17.1826 12.4053 15.9301 12.6837C14.0241 13.1072 12.7156 12.7156 12 12C11.2844 11.2844 10.8928 9.97588 11.3163 8.0699C11.5947 6.81738 12.2094 5.43511 13.1575 4.08368C13.5724 3.49221 14.0512 2.90664 14.5935 2.34046C13.8182 2.13305 13.0228 2.02041 12.2256 2.00253ZM17.6569 17.6568C18.9081 16.4056 19.6582 14.8431 19.9072 13.2186C16.3611 15.2643 12.638 15.4664 10.5858 13.4142C8.53361 11.362 8.73568 7.63895 10.7814 4.09281C9.1569 4.34184 7.59434 5.09193 6.34315 6.34313C3.21895 9.46732 3.21895 14.5326 6.34315 17.6568C9.46734 20.781 14.5327 20.781 17.6569 17.6568Z" />
185
219
  </svg>
186
220
  {/snippet}
187
221
  {#snippet svgLight()}
188
- <svg style="width: 100%; height: 100%;" viewBox="0 0 24 24" aria-hidden="true">
222
+ <svg style="width:100%;height:100%;" viewBox="0 0 24 24" aria-hidden="true">
189
223
  <path fill-rule="evenodd" clip-rule="evenodd" d="M12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18Z" />
190
224
  <path fill-rule="evenodd" clip-rule="evenodd" d="M11 0H13V4.06189C12.6724 4.02104 12.3387 4 12 4C11.6613 4 11.3276 4.02104 11 4.06189V0ZM7.0943 5.68018L4.22173 2.80761L2.80752 4.22183L5.6801 7.09441C6.09071 6.56618 6.56608 6.0908 7.0943 5.68018ZM4.06189 11H0V13H4.06189C4.02104 12.6724 4 12.3387 4 12C4 11.6613 4.02104 11.3276 4.06189 11ZM5.6801 16.9056L2.80751 19.7782L4.22173 21.1924L7.0943 18.3198C6.56608 17.9092 6.09071 17.4338 5.6801 16.9056ZM11 19.9381V24H13V19.9381C12.6724 19.979 12.3387 20 12 20C11.6613 20 11.3276 19.979 11 19.9381ZM16.9056 18.3199L19.7781 21.1924L21.1923 19.7782L18.3198 16.9057C17.9092 17.4339 17.4338 17.9093 16.9056 18.3199ZM19.9381 13H24V11H19.9381C19.979 11.3276 20 11.6613 20 12C20 12.3387 19.979 12.6724 19.9381 13ZM18.3198 7.0943L21.1923 4.22183L19.7781 2.80762L16.9056 5.6801C17.4338 6.09071 17.9092 6.56608 18.3198 7.0943Z" />
191
225
  </svg>
@@ -6,7 +6,7 @@
6
6
  children: Snippet<[string]>; // Snippet<[variant]>
7
7
  open?: boolean; // bindable (false)
8
8
  closable?: boolean; // (true)
9
- trigger?: HTMLElement; // bindable
9
+ id?: string; // bindable
10
10
  ariaLabel?: string;
11
11
  element?: HTMLDialogElement; // bindable
12
12
  styling?: SVSClass;
@@ -19,7 +19,7 @@
19
19
  children: Snippet<[string]>; // Snippet<[variant]>
20
20
  open?: boolean; // bindable (false)
21
21
  closable?: boolean; // (true)
22
- trigger?: HTMLElement; // bindable
22
+ id?: string; // bindable
23
23
  ariaLabel?: string;
24
24
  element?: HTMLDialogElement; // bindable
25
25
  styling?: SVSClass;
@@ -31,23 +31,18 @@
31
31
  const preset = "svs-modal";
32
32
 
33
33
  import { type Snippet, untrack } from "svelte";
34
- import { type SVSClass, VARIANT, PARTS, fnClass } from "./core";
34
+ import { type SVSClass, VARIANT, PARTS, elemId, fnClass } from "./core";
35
35
  </script>
36
36
 
37
37
  <script lang="ts">
38
- let { children, open = $bindable(false), closable = true, trigger = $bindable(), ariaLabel, element = $bindable(), styling, variant = $bindable("") }: ModalProps = $props();
38
+ let { children, open = $bindable(false), closable = true, id = $bindable(), ariaLabel, element = $bindable(), styling, variant = $bindable("") }: ModalProps = $props();
39
39
 
40
40
  // *** Initialize *** //
41
41
  if (!variant) variant = VARIANT.NEUTRAL;
42
42
  const cls = fnClass(preset, styling);
43
+ if (!id) id = elemId.id;
43
44
 
44
45
  // *** Bind Handlers *** //
45
- $effect(() => {
46
- open;
47
- untrack(() => {
48
- if (!open) trigger?.focus();
49
- });
50
- });
51
46
  $effect.pre(() => {
52
47
  open;
53
48
  untrack(() => toggle());
@@ -62,24 +57,25 @@
62
57
 
63
58
  // *** Event Handlers *** //
64
59
  const onclick = closable ? (ev: MouseEvent) => { if (ev.target === element) open = false; } : undefined;
65
- const onkeydown = closable ? (ev: KeyboardEvent) => { if (ev.key === "Escape") ev.preventDefault(); } : undefined;
66
- function onclose() {
67
- open = false;
60
+ const onkeydown = closable ? undefined : (ev: KeyboardEvent) => { if (ev.key === "Escape") ev.preventDefault(); };
61
+ function ontoggle() {
62
+ open = element?.open ?? false;
68
63
  }
69
64
  $effect(() => untrack(() => { if (open) element?.showModal(); }));
70
65
  </script>
71
66
 
72
67
  <!---------------------------------------->
73
68
 
74
- <!-- svelte-ignore a11y_autofocus -->
75
- <dialog bind:this={element} class={cls(PARTS.WHOLE, variant)} aria-label={ariaLabel} {onclick} {onkeydown} {onclose} autofocus={true}>
69
+ <dialog bind:this={element} class={cls(PARTS.WHOLE, variant)} aria-label={ariaLabel} {id} {onclick} {onkeydown} {ontoggle} autofocus={true}>
76
70
  <div class={cls(PARTS.MAIN, variant)}>
77
71
  {@render children(variant)}
78
72
  </div>
79
73
  </dialog>
80
74
 
81
75
  <style>
82
- :global(html:has(dialog[open])) {
83
- overflow: hidden;
76
+ :global {
77
+ html:has(dialog[open]) {
78
+ overflow: hidden;
79
+ }
84
80
  }
85
81
  </style>
@@ -2,7 +2,7 @@ export interface ModalProps {
2
2
  children: Snippet<[string]>;
3
3
  open?: boolean;
4
4
  closable?: boolean;
5
- trigger?: HTMLElement;
5
+ id?: string;
6
6
  ariaLabel?: string;
7
7
  element?: HTMLDialogElement;
8
8
  styling?: SVSClass;
@@ -19,7 +19,7 @@ import { type SVSClass } from "./core";
19
19
  * children: Snippet<[string]>; // Snippet<[variant]>
20
20
  * open?: boolean; // bindable (false)
21
21
  * closable?: boolean; // (true)
22
- * trigger?: HTMLElement; // bindable
22
+ * id?: string; // bindable
23
23
  * ariaLabel?: string;
24
24
  * element?: HTMLDialogElement; // bindable
25
25
  * styling?: SVSClass;
@@ -27,6 +27,6 @@ import { type SVSClass } from "./core";
27
27
  * }
28
28
  * ```
29
29
  */
30
- declare const Modal: import("svelte").Component<ModalProps, {}, "variant" | "open" | "element" | "trigger">;
30
+ declare const Modal: import("svelte").Component<ModalProps, {}, "variant" | "id" | "open" | "element">;
31
31
  type Modal = ReturnType<typeof Modal>;
32
32
  export default Modal;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svseeds",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Simple components for Svelte.",
5
5
  "type": "module",
6
6
  "main": "./index.js",