@genarou/blazir-icons 1.2.11 → 1.2.15

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/dist/Icon.svelte CHANGED
@@ -1,251 +1,206 @@
1
+ <!-- DynamicIcon.svelte -->
1
2
  <script lang="ts">
3
+ import { coerceSize } from ".";
2
4
  import { iconRegistry, type IconName } from "./icons/registry";
3
- import {
4
- iconPresets,
5
- iconVariants,
6
- type IconPreset,
7
- type IconVariant,
8
- } from "./presets";
5
+ import { iconPresets, iconVariants } from "./presets";
9
6
  import type { IconProps } from "./types";
10
- import { coerceSize } from "./utils/defaults";
11
-
12
- // ───────────────────────────────────────────────────────────────────────────
13
- // POOLS (1 IO por root scrolleable, 1 RO global)
14
- // ───────────────────────────────────────────────────────────────────────────
15
- type RootKey = Element | undefined;
16
- type Entry = {
17
- el: HTMLElement;
18
- onViewportChange?: (v: boolean) => void;
19
- onSizeChange?: (w: number, h: number) => void;
20
- };
21
-
22
- class IntersectionPool {
23
- private pools = new Map<
24
- RootKey,
25
- { io: IntersectionObserver; entries: Set<Entry> }
26
- >();
27
-
28
- register(root: RootKey, entry: Entry) {
29
- let pool = this.pools.get(root);
30
- if (!pool) {
31
- const io = new IntersectionObserver(
32
- (entries) => {
33
- const p = this.pools.get(root);
34
- if (!p) return;
35
- for (const e of entries) {
36
- const tgt = e.target as HTMLElement;
37
- for (const item of p.entries) {
38
- if (item.el === tgt) {
39
- item.onViewportChange?.(e.isIntersecting);
40
- break;
41
- }
42
- }
43
- }
44
- },
45
- { root: root as Element | undefined, rootMargin: "200px" }
46
- );
47
- pool = { io, entries: new Set() };
48
- this.pools.set(root, pool);
49
- }
50
7
 
51
- pool.entries.add(entry);
52
- pool.io.observe(entry.el);
53
-
54
- return () => {
55
- const p = this.pools.get(root);
56
- if (!p) return;
57
- p.io.unobserve(entry.el);
58
- p.entries.delete(entry);
59
- if (p.entries.size === 0) {
60
- p.io.disconnect();
61
- this.pools.delete(root);
62
- }
63
- };
64
- }
65
- }
8
+ // Bridge de actions externas (no importamos lazy aquí)
9
+ type ActionFn<T = HTMLElement, P = any> = (
10
+ node: T,
11
+ params?: P
12
+ ) => void | { update?: (p?: P) => void; destroy?: () => void };
66
13
 
67
- class ResizePool {
68
- private ro: ResizeObserver | null = null;
69
- private entries = new Set<Entry>();
70
-
71
- private ensure() {
72
- if (this.ro || typeof ResizeObserver === "undefined") return;
73
- this.ro = new ResizeObserver((ros) => {
74
- for (const r of ros) {
75
- const tgt = r.target as HTMLElement;
76
- for (const item of this.entries) {
77
- if (item.el === tgt) {
78
- item.onSizeChange?.(tgt.offsetWidth, tgt.offsetHeight);
79
- break;
80
- }
81
- }
82
- }
83
- });
84
- }
14
+ type ActionEntry<T = HTMLElement> = [ActionFn<T, any>, any?];
85
15
 
86
- register(entry: Entry) {
87
- this.ensure();
88
- this.entries.add(entry);
89
- this.ro?.observe(entry.el);
90
- entry.onSizeChange?.(entry.el.offsetWidth, entry.el.offsetHeight); // inicial
91
- return () => {
92
- this.ro?.unobserve(entry.el);
93
- this.entries.delete(entry);
94
- if (this.entries.size === 0) {
95
- this.ro?.disconnect();
96
- this.ro = null;
97
- }
98
- };
99
- }
16
+ type IconPresetName = keyof typeof iconPresets;
17
+ type IconVariantName = keyof typeof iconVariants;
18
+
19
+ interface DynamicIconProps extends Omit<IconProps, "children"> {
20
+ name?: IconName;
21
+ preset?: IconPresetName;
22
+ variant?: IconVariantName;
23
+ class?: string;
24
+ style?: string;
25
+ /** Pasas aquí tus actions: p.ej. actions={[[lazyClass, opts]]} */
26
+ actions?: ActionEntry[];
100
27
  }
101
28
 
102
- const IO_POOL = new IntersectionPool();
103
- const RO_POOL = new ResizePool();
29
+ const p: DynamicIconProps = $props();
104
30
 
105
- function getScrollRoot(el: HTMLElement | null): Element | undefined {
106
- let p: HTMLElement | null = el?.parentElement ?? null;
107
- while (p) {
108
- const s = getComputedStyle(p);
109
- if (/(auto|scroll)/.test(`${s.overflow}${s.overflowX}${s.overflowY}`))
110
- return p;
111
- p = p.parentElement;
112
- }
113
- return undefined;
114
- }
31
+ // 🔥 OPTIMIZACIÓN: Cache para props combinadas
32
+ const propsCache = new Map<string, any>();
115
33
 
116
- // ───────────────────────────────────────────────────────────────────────────
117
- // PROPS
118
- // ───────────────────────────────────────────────────────────────────────────
119
- interface DynamicIconProps extends Omit<IconProps, "children"> {
120
- name: IconName;
121
- preset?: IconPreset;
122
- variant?: IconVariant;
123
- skeleton?: boolean;
34
+ // 🔥 OPTIMIZACIÓN: Derivados en lugar de effects + states
35
+ const iconName = $derived(p.name ?? null);
36
+ const Comp = $derived(iconName ? iconRegistry[iconName] : null);
37
+
38
+ // 🔥 OPTIMIZACIÓN: Preset y variant como derivados
39
+ const presetProps = $derived(() => {
40
+ const key = p.preset as IconPresetName | undefined;
41
+ return key ? iconPresets[key] : {};
42
+ });
43
+
44
+ const variantProps = $derived(() => {
45
+ const key = p.variant as IconVariantName | undefined;
46
+ return key ? iconVariants[key] : {};
47
+ });
48
+
49
+ // 🔥 OPTIMIZACIÓN: Helper para normalizar ms
50
+ function toMs(v: unknown): string | undefined {
51
+ if (v == null) return undefined;
52
+ return typeof v === "number" ? `${v}ms` : String(v);
124
53
  }
125
54
 
126
- const props: DynamicIconProps = $props();
127
- const inputProps = $derived(() => ({ skeleton: false, ...props }));
128
-
129
- const Comp = $derived.by(() => iconRegistry[inputProps().name]);
130
- const presetProps = $derived(() =>
131
- inputProps().preset ? iconPresets[inputProps().preset!] : {}
132
- );
133
- const variantProps = $derived(() =>
134
- inputProps().variant ? iconVariants[inputProps().variant!] : {}
135
- );
136
- const mergedProps = $derived(() => ({
137
- ...presetProps(),
138
- ...variantProps(),
139
- ...inputProps(),
140
- }));
141
-
142
- // 🔹 No pasar `skeleton` al hijo
143
- const childProps = $derived(() => {
144
- const { skeleton, animationDuration, animationDelay, ...rest } =
145
- mergedProps();
146
-
147
- const toMsString = (v?: string | number) =>
148
- v === undefined ? undefined : typeof v === "number" ? `${v}ms` : v;
55
+ // 🔥 OPTIMIZACIÓN: Child props con memoización
56
+ const child = $derived(() => {
57
+ const cacheKey = `${p.preset || ""}-${p.variant || ""}-${p.name || ""}`;
149
58
 
150
- return {
151
- ...rest,
152
- animationDuration: toMsString(animationDuration),
153
- animationDelay: toMsString(animationDelay),
59
+ // Check cache primero
60
+ const cached = propsCache.get(cacheKey);
61
+ if (cached && cached.timestamp > Date.now() - 1000) {
62
+ // Cache válido por 1s
63
+ return { ...cached.props, ...p };
64
+ }
65
+
66
+ const merged = {
67
+ ...presetProps(),
68
+ ...variantProps(),
69
+ ...p,
154
70
  };
71
+
72
+ // Extraer y eliminar propiedades que no van al hijo
73
+ const { class: _class, style: _style, actions: _actions, ...rest } = merged;
74
+
75
+ // Normalizar animationDuration y animationDelay
76
+ const normalized: any = { ...rest };
77
+
78
+ if (normalized.animationDuration !== undefined) {
79
+ normalized.animationDuration = toMs(normalized.animationDuration);
80
+ }
81
+
82
+ if (normalized.animationDelay !== undefined) {
83
+ normalized.animationDelay = toMs(normalized.animationDelay);
84
+ }
85
+
86
+ const result = normalized as IconProps;
87
+
88
+ // Guardar en cache
89
+ propsCache.set(cacheKey, {
90
+ props: result,
91
+ timestamp: Date.now(),
92
+ });
93
+
94
+ // Limpiar cache viejo (mantener solo últimos 50)
95
+ if (propsCache.size > 50) {
96
+ const firstKey = propsCache.keys().next().value;
97
+ if (firstKey !== undefined) {
98
+ propsCache.delete(firstKey);
99
+ }
100
+ }
101
+
102
+ return result;
155
103
  });
156
104
 
105
+ // 🔥 OPTIMIZACIÓN: Box size como derivado
157
106
  const boxSize = $derived(() => {
158
- const s = coerceSize(mergedProps().size, 24);
107
+ const rawSize =
108
+ (p as any).size ??
109
+ (presetProps() as any).size ??
110
+ (variantProps() as any).size;
111
+ const s = coerceSize(rawSize, 24);
159
112
  return typeof s === "number" ? `${s}px` : s;
160
113
  });
161
114
 
162
- // ───────────────────────────────────────────────────────────────────────────
163
- // Lazy-mount con pooling
164
- // ───────────────────────────────────────────────────────────────────────────
165
- let hostEl: HTMLElement | null = $state(null);
166
- let isVisible = $state(false);
167
- let inViewport = false,
168
- hasSize = false;
169
- let unregIO: (() => void) | null = null,
170
- unregRO: (() => void) | null = null;
171
-
172
- function tryMount() {
173
- if (!isVisible && inViewport && hasSize) {
174
- isVisible = true;
175
- unregIO?.();
176
- unregIO = null;
177
- unregRO?.();
178
- unregRO = null;
179
- }
180
- }
115
+ // 🔥 OPTIMIZACIÓN: Host class como derivado
116
+ const hostClass = $derived(`bz-icon-wrapper ${p.class ?? ""}`.trim());
181
117
 
182
- $effect(() => {
183
- if (
184
- typeof window === "undefined" ||
185
- typeof IntersectionObserver === "undefined"
186
- ) {
187
- isVisible = true;
188
- return;
189
- }
190
- if (!hostEl || isVisible) return;
191
-
192
- const root = getScrollRoot(hostEl) ?? undefined;
193
- unregIO = IO_POOL.register(root, {
194
- el: hostEl,
195
- onViewportChange: (v) => {
196
- inViewport = v;
197
- tryMount();
198
- },
199
- });
118
+ // 🔥 OPTIMIZACIÓN: Host style como derivado
119
+ const hostStyle = $derived(() => {
120
+ const size = boxSize();
121
+ return (
122
+ `--bz-icon-size:${size};` +
123
+ `width:${size};height:${size};` +
124
+ `inline-size:${size};block-size:${size};` +
125
+ `${p.style ?? ""}`
126
+ );
127
+ });
128
+
129
+ // Key para forzar remonte y reiniciar animaciones
130
+ let mountKey = $state(0);
200
131
 
201
- if (typeof ResizeObserver !== "undefined") {
202
- unregRO = RO_POOL.register({
203
- el: hostEl,
204
- onSizeChange: (w, h) => {
205
- hasSize = w > 0 && h > 0;
206
- tryMount();
207
- },
208
- });
209
- } else {
210
- hasSize = hostEl.offsetWidth > 0 && hostEl.offsetHeight > 0;
211
- tryMount();
132
+ // 🔥 OPTIMIZACIÓN: Bridge de actions más eficiente
133
+ function applyActions(node: HTMLElement, entries: ActionEntry[] = []) {
134
+ if (!entries || entries.length === 0) {
135
+ return { update() {}, destroy() {} };
212
136
  }
213
137
 
214
- return () => {
215
- unregIO?.();
216
- unregIO = null;
217
- unregRO?.();
218
- unregRO = null;
138
+ let handles = entries
139
+ .map(([fn, params]) => fn?.(node, params))
140
+ .filter(Boolean);
141
+
142
+ return {
143
+ update(next: ActionEntry[] = []) {
144
+ // Cleanup anterior
145
+ handles.forEach((h) => h?.destroy?.());
146
+
147
+ // Aplicar nuevos
148
+ handles = next
149
+ .map(([fn, params]) => fn?.(node, params))
150
+ .filter(Boolean);
151
+ },
152
+ destroy() {
153
+ handles.forEach((h) => h?.destroy?.());
154
+ handles = [];
155
+ },
219
156
  };
220
- });
157
+ }
158
+
159
+ // Reanima al recibir el evento del action
160
+ function onLazyMount() {
161
+ mountKey++;
162
+ }
221
163
  </script>
222
164
 
223
165
  {#if Comp}
224
166
  <span
225
- bind:this={hostEl}
226
- class="bz-icon-wrapper"
227
- style={`inline-size:${boxSize};block-size:${boxSize};`}
228
- data-mounted={isVisible ? "true" : "false"}
167
+ class={hostClass}
168
+ style={hostStyle()}
169
+ use:applyActions={p.actions ?? []}
170
+ onlazyMount={onLazyMount}
229
171
  >
230
- {#if isVisible}
231
- <Comp {...childProps()} />
232
- {:else if mergedProps().skeleton}
233
- <span class="bz-icon-skeleton" aria-hidden="true"></span>
234
- {/if}
172
+ {#key mountKey}
173
+ <Comp {...child()} />
174
+ {/key}
235
175
  </span>
236
- {:else}
237
- <span class="bz-icon-error" data-icon={inputProps().name}>
238
- ⚠️ Icon not found: {inputProps().name}
176
+ {:else if iconName}
177
+ <span class="bz-icon-error" data-icon={iconName}>
178
+ ⚠️ Icon not found: {iconName}
239
179
  </span>
180
+ {:else}
181
+ <span class="bz-icon-skeleton" aria-hidden="true"></span>
240
182
  {/if}
241
183
 
242
184
  <style>
243
185
  .bz-icon-wrapper {
186
+ cursor: pointer;
244
187
  display: inline-flex;
245
188
  align-items: center;
246
189
  justify-content: center;
247
- line-height: 1;
190
+ position: relative;
191
+ pointer-events: auto;
192
+ contain: layout style paint;
193
+ content-visibility: auto;
194
+ }
195
+
196
+ .bz-icon-wrapper :global(svg) {
197
+ pointer-events: bounding-box !important;
248
198
  }
199
+
200
+ .bz-icon-wrapper :global(svg *) {
201
+ pointer-events: none !important;
202
+ }
203
+
249
204
  .bz-icon-skeleton {
250
205
  width: 100%;
251
206
  height: 100%;
@@ -265,4 +220,37 @@
265
220
  border-radius: var(--ui-radius, 0.25rem);
266
221
  font-weight: 600;
267
222
  }
223
+ .is-entering {
224
+ opacity: 0.001;
225
+ transform: translateY(4px);
226
+ animation: ic-enter 160ms ease-out forwards;
227
+ }
228
+ .is-leaving {
229
+ opacity: 1;
230
+ transform: none;
231
+ animation: ic-leave 160ms ease-in forwards;
232
+ }
233
+ .is-mounted {
234
+ opacity: 1;
235
+ transform: none;
236
+ }
237
+ @keyframes ic-enter {
238
+ to {
239
+ opacity: 1;
240
+ transform: none;
241
+ }
242
+ }
243
+ @keyframes ic-leave {
244
+ to {
245
+ opacity: 0.001;
246
+ transform: translateY(4px);
247
+ }
248
+ }
249
+ @media (prefers-reduced-motion: reduce) {
250
+ .is-entering,
251
+ .is-leaving {
252
+ animation: none;
253
+ transform: none;
254
+ }
255
+ }
268
256
  </style>
@@ -1,11 +1,21 @@
1
1
  import { type IconName } from "./icons/registry";
2
- import { type IconPreset, type IconVariant } from "./presets";
2
+ import { iconPresets, iconVariants } from "./presets";
3
3
  import type { IconProps } from "./types";
4
+ type ActionFn<T = HTMLElement, P = any> = (node: T, params?: P) => void | {
5
+ update?: (p?: P) => void;
6
+ destroy?: () => void;
7
+ };
8
+ type ActionEntry<T = HTMLElement> = [ActionFn<T, any>, any?];
9
+ type IconPresetName = keyof typeof iconPresets;
10
+ type IconVariantName = keyof typeof iconVariants;
4
11
  interface DynamicIconProps extends Omit<IconProps, "children"> {
5
- name: IconName;
6
- preset?: IconPreset;
7
- variant?: IconVariant;
8
- skeleton?: boolean;
12
+ name?: IconName;
13
+ preset?: IconPresetName;
14
+ variant?: IconVariantName;
15
+ class?: string;
16
+ style?: string;
17
+ /** Pasas aquí tus actions: p.ej. actions={[[lazyClass, opts]]} */
18
+ actions?: ActionEntry[];
9
19
  }
10
20
  declare const Icon: import("svelte").Component<DynamicIconProps, {}, "">;
11
21
  type Icon = ReturnType<typeof Icon>;