uraniyum 1.0.9 → 1.1.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.
package/src/styles.ts CHANGED
@@ -1,164 +1,257 @@
1
1
  export type CSSValue = string | number | (() => string | number);
2
+
2
3
  export interface CSSProperties {
3
- [key: string]: CSSValue | CSSProperties;
4
+ [key: string]: CSSValue | CSSProperties | undefined;
5
+ }
6
+
7
+ export interface StyleRule {
8
+ [key: string]: CSSValue | CSSProperties | undefined;
4
9
  }
5
- export type StyleRule = CSSProperties & {
6
- ':hover'?: CSSProperties;
7
- ':focus'?: CSSProperties;
8
- ':active'?: CSSProperties;
9
- ':disabled'?: CSSProperties;
10
- [key: string]: any;
11
- };
12
- export type StylesMap<T extends Record<string, any> = {}> = {
13
- [K in keyof T]: StyleRule;
14
- };
15
- export type ClassNames<T extends Record<string, any>> = { [K in keyof T]: string };
16
-
17
- const styleCache = new Map<string, string>();
10
+
11
+ export type StylesMap = Record<string, StyleRule>;
12
+ export type ClassNames<T extends StylesMap> = { [K in keyof T]: string };
13
+
14
+ const styleCache = new Map<string, true>();
18
15
  const keyframeCache = new Map<string, string>();
19
16
  let styleElement: HTMLStyleElement | null = null;
17
+ let styleSheet: CSSStyleSheet | null = null;
20
18
 
21
19
  const ALLOWED_PREFIXES = [
22
- 'root', 'button', 'icon', 'text', 'container', 'wrapper',
23
- 'card', 'header', 'section'
20
+ "root",
21
+ "button",
22
+ "icon",
23
+ "text",
24
+ "container",
25
+ "wrapper",
26
+ "card",
27
+ "header",
28
+ "section",
24
29
  ];
25
30
 
26
- function ensureStyleElement() {
27
- if (!styleElement) {
28
- styleElement = document.createElement('style');
29
- styleElement.id = 'styles';
30
- document.head.appendChild(styleElement);
31
+ const KEYFRAMES_PREFIX = "@keyframes ";
32
+
33
+ function ensureStyleSheet(): CSSStyleSheet {
34
+ if (!styleSheet) {
35
+ if (!styleElement) {
36
+ styleElement = document.createElement("style");
37
+ styleElement.id = "styles";
38
+ document.head.appendChild(styleElement);
39
+ }
40
+ styleSheet = styleElement.sheet as CSSStyleSheet;
31
41
  }
32
- return styleElement;
42
+ return styleSheet;
33
43
  }
34
44
 
35
45
  function hashString(str: string): string {
36
46
  let hash = 5381;
37
- for (let i = 0; i < str.length; i++) hash = (hash * 33) ^ str.charCodeAt(i);
47
+ for (let i = 0; i < str.length; i++) {
48
+ hash = (hash * 33) ^ str.charCodeAt(i);
49
+ }
38
50
  return (hash >>> 0).toString(36);
39
51
  }
40
52
 
41
- const kebabCache = new Map<string, string>();
53
+ const camelToKebabCache = new Map<string, string>();
54
+
42
55
  function camelToKebab(str: string): string {
43
- if (!kebabCache.has(str)) {
44
- kebabCache.set(str, str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase());
56
+ const cached = camelToKebabCache.get(str);
57
+ if (cached) return cached;
58
+
59
+ const result = str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
60
+ camelToKebabCache.set(str, result);
61
+ return result;
62
+ }
63
+
64
+ function stableHashRule(rule: Record<string, any>): string {
65
+ const parts: string[] = [];
66
+
67
+ for (const key in rule) {
68
+ if (!Object.prototype.hasOwnProperty.call(rule, key)) continue;
69
+ const val = rule[key];
70
+
71
+ if (key.startsWith("@") || typeof val === "object" || val == null) continue;
72
+
73
+ let v: string;
74
+ if (typeof val === "function") v = val.toString();
75
+ else v = String(val);
76
+
77
+ parts.push(`${key}:${v}`);
45
78
  }
46
- return kebabCache.get(str)!;
79
+
80
+ parts.sort();
81
+ return hashString(parts.join("|"));
47
82
  }
48
83
 
49
- function stringifyDecls(props: CSSProperties, keyframes: Map<string, string>): string {
84
+ function stringifyDecls(
85
+ props: Record<string, any>,
86
+ keyframes: Map<string, string>,
87
+ ): string {
50
88
  const out: string[] = [];
51
- for (const [key, raw] of Object.entries(props)) {
89
+
90
+ for (const key in props) {
91
+ if (!Object.prototype.hasOwnProperty.call(props, key)) continue;
92
+ const raw = props[key];
93
+
52
94
  if (raw == null || typeof raw === "object") continue;
53
- let val = typeof raw === "function" ? raw() : raw;
54
95
 
55
- if (typeof val === "string") {
96
+ let val: string | number =
97
+ typeof raw === "function" ? raw() : (raw as string | number);
98
+
99
+ if (typeof val === "string" && keyframes.size > 0) {
56
100
  for (const [orig, uniq] of keyframes) {
57
- val = val.replaceAll(`$${orig}`, uniq);
101
+ if (val.includes(`$${orig}`)) {
102
+ val = val.replaceAll(`$${orig}`, uniq);
103
+ }
58
104
  }
59
105
  }
106
+
60
107
  out.push(`${camelToKebab(key)}:${val}`);
61
108
  }
109
+
62
110
  return out.join(";");
63
111
  }
64
112
 
65
- const KEYFRAMES_REGEX = /^@keyframes /;
66
-
67
- function registerKeyframes(name: string, frames: Record<string, CSSProperties>): string {
113
+ function registerKeyframes(
114
+ name: string,
115
+ frames: Record<string, any>,
116
+ ): string {
68
117
  const hash = hashString(name + JSON.stringify(frames));
69
118
  const unique = `${name}-${hash}`;
70
119
 
71
- if (keyframeCache.has(unique)) return unique;
120
+ const cached = keyframeCache.get(unique);
121
+ if (cached) return cached;
122
+
123
+ const sheet = ensureStyleSheet();
72
124
 
73
- const rules = Object.entries(frames)
74
- .map(([step, styles]) => `${step}{${stringifyDecls(styles, new Map())}}`)
75
- .join("");
125
+ const steps: string[] = [];
126
+ for (const step in frames) {
127
+ if (!Object.prototype.hasOwnProperty.call(frames, step)) continue;
128
+ const decls = stringifyDecls(frames[step] ?? {}, new Map());
129
+ steps.push(`${step}{${decls}}`);
130
+ }
76
131
 
77
- ensureStyleElement().appendChild(
78
- document.createTextNode(`@keyframes ${unique}{${rules}}\n`)
79
- );
132
+ const ruleText = `@keyframes ${unique}{${steps.join("")}}`;
133
+ sheet.insertRule(ruleText, sheet.cssRules.length);
80
134
  keyframeCache.set(unique, unique);
135
+
81
136
  return unique;
82
137
  }
83
138
 
84
- function buildRules(className: string, rule: StyleRule, keyframes: Map<string, string>): string[] {
139
+ function buildRules(
140
+ className: string,
141
+ rule: Record<string, any>,
142
+ keyframes: Map<string, string>,
143
+ ): string[] {
85
144
  const rules: string[] = [];
86
- const base: CSSProperties = {};
145
+ const baseDecls: string[] = [];
87
146
 
88
- for (const [key, val] of Object.entries(rule)) {
89
- if (KEYFRAMES_REGEX.test(key)) continue;
147
+ for (const key in rule) {
148
+ if (!Object.prototype.hasOwnProperty.call(rule, key)) continue;
149
+ const val = rule[key];
90
150
 
91
- if (key.startsWith(':') && typeof val === "object") {
92
- rules.push(`.${className}${key}{${stringifyDecls(val, keyframes)}}`);
151
+ if (key.startsWith("@keyframes")) {
152
+ continue;
93
153
  }
94
- else if (key.startsWith('@media') && typeof val === "object") {
95
- rules.push(`${key}{.${className}{${stringifyDecls(val, keyframes)}}}`);
154
+
155
+ if (!val) continue;
156
+
157
+ if (key[0] === ":" && typeof val === "object") {
158
+ const decls = stringifyDecls(val ?? {}, keyframes);
159
+ if (decls) {
160
+ rules.push(`.${className}${key}{${decls}}`);
161
+ }
162
+ continue;
96
163
  }
97
- else if (key.includes('&') && typeof val === "object") {
98
- const selector = key.replaceAll("&", `.${className}`);
99
- rules.push(`${selector}{${stringifyDecls(val, keyframes)}}`);
164
+
165
+ if (key.startsWith("@media") && typeof val === "object") {
166
+ const decls = stringifyDecls(val ?? {}, keyframes);
167
+ if (decls) {
168
+ rules.push(`${key}{.${className}{${decls}}}`);
169
+ }
170
+ continue;
100
171
  }
101
- else if (!key.startsWith('@')) {
102
- base[key] = val;
172
+
173
+ if (key.includes("&") && typeof val === "object") {
174
+ const selector = key.replace(/&/g, `.${className}`);
175
+ const decls = stringifyDecls(val ?? {}, keyframes);
176
+ if (decls) {
177
+ rules.push(`${selector}{${decls}}`);
178
+ }
179
+ continue;
180
+ }
181
+
182
+ if (!key.startsWith("@") && typeof val !== "object") {
183
+ const raw = typeof val === "function" ? val() : val;
184
+ baseDecls.push(`${camelToKebab(key)}:${raw}`);
103
185
  }
104
186
  }
105
187
 
106
- if (Object.keys(base).length > 0) {
107
- rules.push(`.${className}{${stringifyDecls(base, keyframes)}}`);
188
+ if (baseDecls.length > 0) {
189
+ rules.push(`.${className}{${baseDecls.join(";")}}`);
108
190
  }
109
191
 
110
192
  return rules;
111
193
  }
112
194
 
113
195
  export function makeStyles<T extends StylesMap>(styles: T) {
114
- return () => {
196
+ return (): ClassNames<T> => {
115
197
  const classNames = {} as ClassNames<T>;
116
- const collectedRules: string[] = [];
117
198
  const keyframesMap = new Map<string, string>();
199
+ const sheet = ensureStyleSheet();
118
200
 
119
- for (const [key, val] of Object.entries(styles)) {
120
- if (KEYFRAMES_REGEX.test(key)) {
121
- const name = key.replace('@keyframes ', '');
122
- const unique = registerKeyframes(name, val as any);
123
- keyframesMap.set(name, unique);
124
- }
201
+ for (const key in styles) {
202
+ if (!Object.prototype.hasOwnProperty.call(styles, key)) continue;
203
+ if (!key.startsWith(KEYFRAMES_PREFIX)) continue;
204
+
205
+ const name = key.slice(KEYFRAMES_PREFIX.length);
206
+ const frames = styles[key] as unknown as Record<string, any>;
207
+ const unique = registerKeyframes(name, frames);
208
+ keyframesMap.set(name, unique);
125
209
  }
126
210
 
127
- for (const [slot, rule] of Object.entries(styles)) {
128
- if (KEYFRAMES_REGEX.test(slot)) continue;
211
+ for (const slot in styles) {
212
+ if (!Object.prototype.hasOwnProperty.call(styles, slot)) continue;
213
+ if (slot.startsWith(KEYFRAMES_PREFIX)) continue;
129
214
 
130
- const hash = hashString(JSON.stringify(rule));
131
- const className =
132
- ALLOWED_PREFIXES.includes(slot) ? `${slot}-${hash}` : `css-${hash}`;
215
+ const rule = styles[slot] as StyleRule;
216
+ const hash = stableHashRule(rule);
217
+ const className = ALLOWED_PREFIXES.includes(slot)
218
+ ? `${slot}-${hash}`
219
+ : `css-${hash}`;
133
220
 
134
221
  if (!styleCache.has(className)) {
135
- const rules = buildRules(className, rule as StyleRule, keyframesMap);
136
- collectedRules.push(...rules);
137
- styleCache.set(className, className);
222
+ const rules = buildRules(className, rule, keyframesMap);
223
+ for (const r of rules) {
224
+ sheet.insertRule(r, sheet.cssRules.length);
225
+ }
226
+ styleCache.set(className, true);
138
227
  }
139
228
 
140
- classNames[slot as keyof T] = className;
141
- }
142
-
143
- if (collectedRules.length) {
144
- const el = ensureStyleElement();
145
- for (const r of collectedRules) el.appendChild(document.createTextNode(r + "\n"));
229
+ (classNames as any)[slot] = className;
146
230
  }
147
231
 
148
232
  return classNames;
149
233
  };
150
234
  }
151
235
 
152
- export const mergeClasses = (...classes: (string | undefined | null | false)[]) =>
153
- classes.filter(Boolean).join(" ");
236
+ export const mergeClasses = (
237
+ ...classes: (string | undefined | null | false)[]
238
+ ) => classes.filter(Boolean).join(" ");
239
+
240
+ export function mergeStyleSets<T extends StylesMap>(
241
+ ...sets: (Partial<T> | undefined)[]
242
+ ): ClassNames<T> {
243
+ const merged: StylesMap = {};
154
244
 
155
- export function mergeStyleSets<T extends StylesMap>(...sets: (T | undefined)[]) {
156
- const merged: any = {};
157
245
  for (const set of sets) {
158
246
  if (!set) continue;
159
- for (const k in set) {
160
- merged[k] = { ...(merged[k] || {}), ...set[k] };
247
+ for (const key in set) {
248
+ if (!Object.prototype.hasOwnProperty.call(set, key)) continue;
249
+
250
+ const prev = (merged[key] as StyleRule) ?? {};
251
+ const cur = (set[key] as StyleRule) ?? {};
252
+ merged[key] = { ...prev, ...cur } as StyleRule;
161
253
  }
162
254
  }
163
- return makeStyles(merged)();
164
- }
255
+
256
+ return makeStyles(merged as T)();
257
+ }
package/src/tags.ts CHANGED
@@ -1,77 +1,84 @@
1
- import { derive, state, State } from "./state";
1
+ import { derive, state } from "./state";
2
+ import type { State } from "./state";
2
3
 
3
4
  function isState(x: any): x is State<any> {
4
- return (
5
- x &&
6
- typeof x === "object" &&
7
- "subs" in x &&
8
- Object.prototype.hasOwnProperty.call(x, "val")
9
- );
5
+ return x && typeof x === "object" && "subs" in x && "val" in x;
10
6
  }
11
7
 
12
- export const tags: Record<string, (attrs?: any, ...children: any[]) => HTMLElement> =
8
+ type Child = Node | string | number | null | undefined | State<any> | (() => any) | Child[];
9
+
10
+ export const tags: Record<string, (...children: any[]) => HTMLElement> =
13
11
  new Proxy({}, {
14
12
  get(_, tag: string) {
15
- return (attrsOrChild?: any, ...children: any[]) => {
13
+ return (...children: any[]): HTMLElement => {
16
14
  const el = document.createElement(tag);
17
15
 
18
- if (
19
- attrsOrChild &&
20
- typeof attrsOrChild === "object" &&
21
- !(attrsOrChild instanceof Node) &&
22
- !isState(attrsOrChild) &&
23
- typeof attrsOrChild !== "function"
24
- ) {
25
- for (const [k, v] of Object.entries(attrsOrChild)) {
26
- el.setAttribute(k, String(v));
27
- }
28
- } else if (attrsOrChild != null) {
29
- children.unshift(attrsOrChild);
30
- }
31
-
32
- for (let child of children.flat()) {
33
-
34
- if (typeof child === "function") {
35
- let node: Node = document.createTextNode("");
36
- el.appendChild(node);
37
-
38
- derive(() => {
39
- const value = child();
40
-
41
- const newNode: Node =
42
- value instanceof Node
43
- ? value
44
- : document.createTextNode(String(value ?? ""));
45
-
46
- node.parentNode?.replaceChild(newNode, node);
47
- node = newNode;
48
- });
49
-
50
- continue;
51
- }
52
-
53
- if (isState(child)) {
54
- const node = document.createTextNode(String(child.val));
55
- el.appendChild(node);
56
-
57
- child.subs.add(() => {
58
- node.textContent = String(child.val);
59
- });
60
-
61
- continue;
62
- }
63
-
64
- if (child instanceof Node) {
65
- el.appendChild(child);
66
- continue;
67
- }
68
-
69
- el.appendChild(
70
- document.createTextNode(String(child))
71
- );
72
- }
16
+ processChildren(el, children);
73
17
 
74
18
  return el;
75
19
  };
76
20
  }
77
21
  });
22
+
23
+ function processChildren(el: HTMLElement, children: any[]) {
24
+ for (const child of children.flat()) {
25
+ if (child == null || child === false) continue;
26
+
27
+ if (typeof child === "function") {
28
+ mountReactiveFunction(el, child);
29
+ continue;
30
+ }
31
+
32
+ if (isState(child)) {
33
+ mountReactiveState(el, child);
34
+ continue;
35
+ }
36
+
37
+ if (child instanceof Node) {
38
+ el.appendChild(child);
39
+ continue;
40
+ }
41
+
42
+ el.appendChild(document.createTextNode(String(child)));
43
+ }
44
+ }
45
+
46
+ function mountReactiveFunction(el: HTMLElement, fn: () => any) {
47
+ let node: Node = document.createTextNode("");
48
+ el.appendChild(node);
49
+
50
+ derive(() => {
51
+ const value = fn();
52
+
53
+ if (value instanceof Node) {
54
+ if (node !== value) {
55
+ node.parentNode?.replaceChild(value, node);
56
+ node = value;
57
+ }
58
+ return;
59
+ }
60
+
61
+ const text = String(value ?? "");
62
+ if (node.nodeType === Node.TEXT_NODE) {
63
+ if ((node as Text).data !== text) {
64
+ (node as Text).data = text;
65
+ }
66
+ } else {
67
+ const newNode = document.createTextNode(text);
68
+ node.parentNode?.replaceChild(newNode, node);
69
+ node = newNode;
70
+ }
71
+ });
72
+ }
73
+
74
+ function mountReactiveState(el: HTMLElement, st: State<any>) {
75
+ const node = document.createTextNode(String(st.val));
76
+ el.appendChild(node);
77
+
78
+ st.subs.add(() => {
79
+ const newVal = String(st.val);
80
+ if (node.data !== newVal) {
81
+ node.data = newVal;
82
+ }
83
+ });
84
+ }