@genarou/blazir-icons 1.2.13 → 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
@@ -28,84 +28,126 @@
28
28
 
29
29
  const p: DynamicIconProps = $props();
30
30
 
31
- let iconName = $state<IconName | null>(null);
32
- let Comp = $state<any>(null);
31
+ // 🔥 OPTIMIZACIÓN: Cache para props combinadas
32
+ const propsCache = new Map<string, any>();
33
33
 
34
- let presetProps = $state<Partial<IconProps>>({});
35
- let variantProps = $state<Partial<IconProps>>({});
34
+ // 🔥 OPTIMIZACIÓN: Derivados en lugar de effects + states
35
+ const iconName = $derived(p.name ?? null);
36
+ const Comp = $derived(iconName ? iconRegistry[iconName] : null);
36
37
 
37
- let child = $state<IconProps>({} as IconProps);
38
- let boxSize = $state<string>("1em");
39
- let hostClass = $state<string>("bz-icon-wrapper");
40
- let hostStyle = $state<string>("");
41
-
42
- // Key para forzar remonte y reiniciar animaciones
43
- let mountKey = $state(0);
44
-
45
- // name -> componente
46
- $effect(() => {
47
- iconName = p.name ?? null;
48
- Comp = iconName ? iconRegistry[iconName] : null;
49
- });
50
-
51
- // preset/variant -> props parciales
52
- $effect(() => {
38
+ // 🔥 OPTIMIZACIÓN: Preset y variant como derivados
39
+ const presetProps = $derived(() => {
53
40
  const key = p.preset as IconPresetName | undefined;
54
- presetProps = key ? iconPresets[key] : {};
41
+ return key ? iconPresets[key] : {};
55
42
  });
56
- $effect(() => {
43
+
44
+ const variantProps = $derived(() => {
57
45
  const key = p.variant as IconVariantName | undefined;
58
- variantProps = key ? iconVariants[key] : {};
46
+ return key ? iconVariants[key] : {};
59
47
  });
60
48
 
61
- // Limpia class/style/actions del host antes de pasar al hijo
62
- $effect(() => {
63
- const {
64
- class: _class,
65
- style: _style,
66
- actions: _actions,
67
- ...restRaw
68
- } = { ...presetProps, ...variantProps, ...p } as Record<string, unknown>;
69
-
70
- const toMs = (v: unknown) =>
71
- v == null ? undefined : typeof v === "number" ? `${v}ms` : (v as string);
72
-
73
- child = {
74
- ...(restRaw as IconProps),
75
- animationDuration: toMs((restRaw as any).animationDuration),
76
- animationDelay: toMs((restRaw as any).animationDelay),
77
- } as IconProps;
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);
53
+ }
54
+
55
+ // 🔥 OPTIMIZACIÓN: Child props con memoización
56
+ const child = $derived(() => {
57
+ const cacheKey = `${p.preset || ""}-${p.variant || ""}-${p.name || ""}`;
58
+
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,
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;
78
103
  });
79
104
 
80
- // Tamaño del wrapper
81
- $effect(() => {
105
+ // 🔥 OPTIMIZACIÓN: Box size como derivado
106
+ const boxSize = $derived(() => {
82
107
  const rawSize =
83
108
  (p as any).size ??
84
- (presetProps as any).size ??
85
- (variantProps as any).size;
109
+ (presetProps() as any).size ??
110
+ (variantProps() as any).size;
86
111
  const s = coerceSize(rawSize, 24);
87
- boxSize = typeof s === "number" ? `${s}px` : s;
112
+ return typeof s === "number" ? `${s}px` : s;
88
113
  });
89
114
 
90
- // Host attrs
91
- $effect(() => {
92
- hostClass = `bz-icon-wrapper ${p.class ?? ""}`.trim();
93
- hostStyle =
94
- `--bz-icon-size:${boxSize};` +
95
- `width:${boxSize};height:${boxSize};` +
96
- `inline-size:${boxSize};block-size:${boxSize};` +
97
- `${p.style ?? ""}`;
115
+ // 🔥 OPTIMIZACIÓN: Host class como derivado
116
+ const hostClass = $derived(`bz-icon-wrapper ${p.class ?? ""}`.trim());
117
+
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
+ );
98
127
  });
99
128
 
100
- // Bridge para aplicar actions externas
129
+ // Key para forzar remonte y reiniciar animaciones
130
+ let mountKey = $state(0);
131
+
132
+ // 🔥 OPTIMIZACIÓN: Bridge de actions más eficiente
101
133
  function applyActions(node: HTMLElement, entries: ActionEntry[] = []) {
102
- let handles =
103
- entries?.map(([fn, params]) => fn?.(node, params)).filter(Boolean) ?? [];
134
+ if (!entries || entries.length === 0) {
135
+ return { update() {}, destroy() {} };
136
+ }
137
+
138
+ let handles = entries
139
+ .map(([fn, params]) => fn?.(node, params))
140
+ .filter(Boolean);
141
+
104
142
  return {
105
143
  update(next: ActionEntry[] = []) {
144
+ // Cleanup anterior
106
145
  handles.forEach((h) => h?.destroy?.());
107
- handles =
108
- next?.map(([fn, params]) => fn?.(node, params)).filter(Boolean) ?? [];
146
+
147
+ // Aplicar nuevos
148
+ handles = next
149
+ .map(([fn, params]) => fn?.(node, params))
150
+ .filter(Boolean);
109
151
  },
110
152
  destroy() {
111
153
  handles.forEach((h) => h?.destroy?.());
@@ -114,7 +156,7 @@
114
156
  };
115
157
  }
116
158
 
117
- // Reanima al recibir el evento del action (asegúrate de que el action emita "lazyMount")
159
+ // Reanima al recibir el evento del action
118
160
  function onLazyMount() {
119
161
  mountKey++;
120
162
  }
@@ -123,12 +165,12 @@
123
165
  {#if Comp}
124
166
  <span
125
167
  class={hostClass}
126
- style={hostStyle}
168
+ style={hostStyle()}
127
169
  use:applyActions={p.actions ?? []}
128
170
  onlazyMount={onLazyMount}
129
171
  >
130
172
  {#key mountKey}
131
- <Comp {...child} />
173
+ <Comp {...child()} />
132
174
  {/key}
133
175
  </span>
134
176
  {:else if iconName}
@@ -140,6 +182,25 @@
140
182
  {/if}
141
183
 
142
184
  <style>
185
+ .bz-icon-wrapper {
186
+ cursor: pointer;
187
+ display: inline-flex;
188
+ align-items: center;
189
+ justify-content: center;
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;
198
+ }
199
+
200
+ .bz-icon-wrapper :global(svg *) {
201
+ pointer-events: none !important;
202
+ }
203
+
143
204
  .bz-icon-skeleton {
144
205
  width: 100%;
145
206
  height: 100%;
@@ -1,32 +1,38 @@
1
1
  <!-- IconBase.svelte -->
2
2
  <script lang="ts">
3
3
  import { type IconEffectOptions, iconEffects } from "./effects.js";
4
-
5
4
  import type { IconMode, IconProps } from "./types.js";
6
- import { combineTransforms, getAnimationStyle } from "./utils/animations.js";
5
+ import { getAnimationStyle } from "./utils/animations.js";
7
6
  import {
8
7
  commonDefaults,
9
8
  modeDefaults,
10
9
  normalizeClass,
11
10
  } from "./utils/defaults.js";
12
11
 
12
+ // Constantes
13
13
  const DEFAULT_MS = 180;
14
14
  const DEFAULT_EASING = "cubic-bezier(.2,.8,.2,1)";
15
+ const DEFAULT_ICON_COLOR = "var(--icon-fg, var(--ui-muted-fg, currentColor))";
16
+
17
+ const MS_REGEX = /ms$/;
18
+ const S_REGEX = /s$/;
15
19
 
16
20
  function normalizeMs(v?: number | string): number {
17
21
  if (v == null) return DEFAULT_MS;
18
22
  if (typeof v === "number") return v;
19
23
  const s = String(v).trim();
20
- if (s.endsWith("ms")) return parseFloat(s);
21
- if (s.endsWith("s")) return parseFloat(s) * 1000;
24
+ if (MS_REGEX.test(s)) return parseFloat(s);
25
+ if (S_REGEX.test(s)) return parseFloat(s) * 1000;
22
26
  const n = parseFloat(s);
23
27
  return Number.isFinite(n) ? n : DEFAULT_MS;
24
28
  }
25
29
 
30
+ let uidCounter = 0;
26
31
  function uid(): string {
27
- return Math.random().toString(36).slice(2);
32
+ return `bz-${Date.now().toString(36)}-${(uidCounter++).toString(36)}`;
28
33
  }
29
34
 
35
+ // Props
30
36
  const props: IconProps & { mode?: IconMode } = $props();
31
37
 
32
38
  // Hover (nativo + heredado)
@@ -35,34 +41,23 @@
35
41
  internalHovered || (props.parentHoverContext?.hovered ?? false)
36
42
  );
37
43
 
38
- // Color efectivo
39
- const DEFAULT_ICON_COLOR = "var(--icon-fg, var(--ui-muted-fg, currentColor))";
44
+ // Color
40
45
  const effectiveColor = $derived(
41
46
  (isHovered && props.hoverColor ? props.hoverColor : props.color) ??
42
47
  DEFAULT_ICON_COLOR
43
48
  );
44
49
 
45
- // Visuales / defaults
50
+ // Mode
46
51
  const mode = $derived(props.mode ?? "solid");
52
+
53
+ // Derivados base
47
54
  const klass = $derived(normalizeClass(props));
48
55
  const common = $derived(commonDefaults(props));
49
56
  const propsWithEffectiveColor = $derived({ ...props, color: effectiveColor });
50
57
  const visual = $derived(modeDefaults(mode, propsWithEffectiveColor));
51
-
52
- // Sólo para variables de timing (no clases de animación)
53
58
  const timingStyle = $derived(getAnimationStyle(props));
54
59
 
55
- // Transforms declarativos (NO incluye rotate/flip; esos van en el <g> geometry)
56
- const cssTransformOnly = $derived(
57
- combineTransforms({
58
- ...props,
59
- rotate: undefined as any,
60
- flipH: false,
61
- flipV: false,
62
- })
63
- );
64
-
65
- // Duraciones / easing
60
+ // Transiciones
66
61
  const hoverMs = $derived(
67
62
  normalizeMs(props.transitionMs ?? props.animationDuration)
68
63
  );
@@ -70,62 +65,149 @@
70
65
  props.transitionEasing ?? props.animationEasing ?? DEFAULT_EASING
71
66
  );
72
67
 
73
- // Spin continuo → inline animation (sin clases)
74
- const spinDuration = $derived(
75
- props.spin
76
- ? props.spin === true
77
- ? "1s"
78
- : typeof props.spin === "number"
79
- ? `${props.spin}ms`
80
- : String(props.spin)
81
- : null
82
- );
83
-
84
- // Pulse/Bounce/Wiggle/Heartbeat/Elastic continuos (boolean props) → inline animation
68
+ // Animaciones continuas
69
+ const spinDuration = $derived(() => {
70
+ if (!props.spin) return null;
71
+ if (props.spin === true) return "1s";
72
+ return typeof props.spin === "number"
73
+ ? `${props.spin}ms`
74
+ : String(props.spin);
75
+ });
85
76
  const wantsPulse = $derived(!!props.pulse);
86
77
  const wantsBounce = $derived(!!props.bounce);
87
78
  const wantsWiggle = $derived(!!props.wiggle);
88
79
  const wantsHeartbeat = $derived(!!props.heartbeat);
89
80
  const wantsElastic = $derived(!!props.elastic);
90
81
 
91
- // Construye style inline (NO escribimos transform cuando haya capa FX)
82
+ // ===== Chevron state grados (se animará por CSS) =====
83
+ const effectiveChevronState = $derived(() => {
84
+ if (props.chevronState) return props.chevronState;
85
+ if (props.chevronOpen !== undefined) {
86
+ return props.chevronOpen ? "open" : "closed";
87
+ }
88
+ return undefined;
89
+ });
90
+
91
+ const chevronDeg = $derived((): number | null => {
92
+ switch (effectiveChevronState()) {
93
+ case "open":
94
+ case "down":
95
+ return 90;
96
+ case "up":
97
+ return -90;
98
+ case "left":
99
+ return 180;
100
+ case "right":
101
+ case "closed":
102
+ return 0;
103
+ default:
104
+ return null;
105
+ }
106
+ });
107
+
108
+ // Clase final
109
+ const finalClass = $derived(
110
+ `bz-icon ${klass}${effectiveChevronState() ? " bz-icon-chevron" : ""}`.trim()
111
+ );
112
+
113
+ // ===== SVG transform (solo rotate/flip declarativos) =====
114
+ // Rot/flip en <g> para no interferir con la transición del <svg>
115
+ function getViewBoxCenter(vb: string) {
116
+ const parts = vb.trim().split(/\s+/);
117
+ const minX = Number(parts[0]);
118
+ const minY = Number(parts[1]);
119
+ const w = Number(parts[2]);
120
+ const h = Number(parts[3]);
121
+ return { cx: minX + w / 2, cy: minY + h / 2 };
122
+ }
123
+ const center = $derived(getViewBoxCenter(common.viewBox));
124
+
125
+ const svgTransform = $derived(() => {
126
+ const cmds: string[] = [];
127
+
128
+ // Rotate (prop)
129
+ if (props.rotate != null) {
130
+ const r =
131
+ typeof props.rotate === "number"
132
+ ? props.rotate
133
+ : parseFloat(String(props.rotate));
134
+ if (!isNaN(r)) cmds.push(`rotate(${r} ${center.cx} ${center.cy})`);
135
+ }
136
+
137
+ // Flip (prop)
138
+ if (props.flipH || props.flipV) {
139
+ const sx = props.flipH ? -1 : 1;
140
+ const sy = props.flipV ? -1 : 1;
141
+ cmds.push(
142
+ `translate(${center.cx} ${center.cy}) scale(${sx} ${sy}) translate(${-center.cx} ${-center.cy})`
143
+ );
144
+ }
145
+
146
+ // ⛔️ OJO: chevron NO va aquí (aquí no hay transición CSS)
147
+ return cmds.length ? cmds.join(" ") : "";
148
+ });
149
+
150
+ // ===== Style inline del <svg> (incluye transición y chevron por CSS) =====
92
151
  const style = $derived(() => {
93
152
  const parts: string[] = [];
94
153
 
95
154
  // estilo previo del usuario
96
- if (props.style)
155
+ if (props.style) {
97
156
  parts.push(props.style.endsWith(";") ? props.style : `${props.style};`);
157
+ }
98
158
 
99
159
  // color
100
- if (effectiveColor) parts.push(`color:${effectiveColor};`);
101
-
102
- // transform declarativo (translate/scale fuera de rotate/flip)
103
- if (cssTransformOnly) parts.push(`transform:${cssTransformOnly};`);
104
- parts.push("transform-origin:center;");
160
+ parts.push(`color:${effectiveColor};`);
105
161
 
106
- // timing variables
107
- if (timingStyle) parts.push(timingStyle);
162
+ // Vars de timing (si las hay)
163
+ const tstyle = timingStyle;
164
+ if (tstyle) parts.push(tstyle);
108
165
 
109
- // transición en hover
166
+ // transición para transform/opacidad/stroke/fill
110
167
  parts.push(
111
168
  `transition:color ${hoverMs}ms ${hoverEase},fill ${hoverMs}ms ${hoverEase},` +
112
169
  `stroke ${hoverMs}ms ${hoverEase},transform ${hoverMs}ms ${hoverEase},opacity ${hoverMs}ms ${hoverEase};`
113
170
  );
114
171
 
115
- // Animaciones continuas inline (evitamos clases para no chocar)
116
- const animations: string[] = [];
117
- if (spinDuration)
118
- animations.push(`__icon_spin ${spinDuration} linear infinite`);
119
- if (wantsPulse)
120
- animations.push(`bz-icon-pulse 1200ms cubic-bezier(.2,.8,.2,1) infinite`);
121
- if (wantsBounce) animations.push(`bz-icon-bounce 400ms ease infinite`);
122
- if (wantsWiggle)
123
- animations.push(`bz-icon-wiggle 200ms ease-in-out infinite`);
124
- if (wantsElastic)
125
- animations.push(`bz-icon-elastic 300ms ease-out infinite`);
126
- if (wantsHeartbeat)
127
- animations.push(`bz-icon-heartbeat 1000ms ease-in-out infinite`);
128
- if (animations.length) parts.push(`animation:${animations.join(",")};`);
172
+ // Var CSS para chevron (si hay estado). Usamos rotate(Xdeg)
173
+ const deg = chevronDeg();
174
+ if (deg != null) {
175
+ parts.push(`--bz-chevron-rot: rotate(${deg}deg);`);
176
+ } else {
177
+ // mantener consistente
178
+ parts.push(`--bz-chevron-rot: rotate(0deg);`);
179
+ }
180
+
181
+ // Transform del SVG: incluir SIEMPRE la var del chevron al final
182
+ // (puedes añadir más términos aquí si usas CSS-only transforms)
183
+ parts.push(`transform-origin:center;`);
184
+ parts.push(`transform: var(--bz-chevron-rot);`);
185
+
186
+ // Animaciones declarativas (spin/pulse/etc) — se apilan con transform
187
+ const spin = spinDuration();
188
+ if (
189
+ spin ||
190
+ wantsPulse ||
191
+ wantsBounce ||
192
+ wantsWiggle ||
193
+ wantsElastic ||
194
+ wantsHeartbeat
195
+ ) {
196
+ const animations: string[] = [];
197
+ if (spin) animations.push(`__icon_spin ${spin} linear infinite`);
198
+ if (wantsPulse)
199
+ animations.push(
200
+ `bz-icon-pulse 1200ms cubic-bezier(.2,.8,.2,1) infinite`
201
+ );
202
+ if (wantsBounce) animations.push(`bz-icon-bounce 400ms ease infinite`);
203
+ if (wantsWiggle)
204
+ animations.push(`bz-icon-wiggle 200ms ease-in-out infinite`);
205
+ if (wantsElastic)
206
+ animations.push(`bz-icon-elastic 300ms ease-out infinite`);
207
+ if (wantsHeartbeat)
208
+ animations.push(`bz-icon-heartbeat 1000ms ease-in-out infinite`);
209
+ parts.push(`animation:${animations.join(",")};`);
210
+ }
129
211
 
130
212
  return parts.join(" ");
131
213
  });
@@ -142,69 +224,37 @@
142
224
  !props.decorative && props.title ? computedTitleId : undefined
143
225
  );
144
226
 
145
- // Tamaño final
227
+ // Tamaño y attrs
146
228
  const finalSize = $derived(
147
229
  typeof common.size === "number" ? `${common.size}px` : common.size
148
230
  );
149
-
150
- // Atributos seguros
151
231
  const safeAttrs = $derived(() => {
152
- const a = { ...(props.attrs ?? {}) };
153
- delete (a as any).width;
154
- delete (a as any).height;
155
- return a;
156
- });
157
-
158
- // 👇 Importante: NO agregamos animationClasses aquí para evitar choques
159
- const finalClass = $derived(
160
- `bz-icon ${klass} ${props.chevronState ? "bz-icon-chevron" : ""}`.trim()
161
- );
162
-
163
- // Rotate/Flip a nivel de GEOMETRÍA (en el <g>)
164
- function getViewBoxCenter(vb: string) {
165
- const [minX, minY, w, h] = vb.trim().split(/\s+/).map(Number);
166
- return { cx: minX + w / 2, cy: minY + h / 2 };
167
- }
168
- const center = $derived(getViewBoxCenter(common.viewBox));
169
- const svgTransform = $derived(() => {
170
- const cmds: string[] = [];
171
- if (props.rotate != null) {
172
- const r =
173
- typeof props.rotate === "number"
174
- ? props.rotate
175
- : parseFloat(String(props.rotate));
176
- if (!isNaN(r)) cmds.push(`rotate(${r} ${center.cx} ${center.cy})`);
177
- }
178
- if (props.flipH || props.flipV) {
179
- const sx = props.flipH ? -1 : 1,
180
- sy = props.flipV ? -1 : 1;
181
- cmds.push(
182
- `translate(${center.cx} ${center.cy}) scale(${sx} ${sy}) translate(${-center.cx} ${-center.cy})`
183
- );
184
- }
185
- return cmds.join(" ");
232
+ if (!props.attrs) return {};
233
+ const { width, height, ...rest } = props.attrs as any;
234
+ return rest;
186
235
  });
187
236
 
188
- // Capa FX: efectos programáticos (scale/slide/rotateOnHover/pressScale)
237
+ // FX layer
189
238
  let svgRef: SVGSVGElement | null = $state(null);
190
239
  let fxLayer: SVGGElement | null = $state(null);
191
240
 
241
+ // Efectos (incluye heartbeatOnHover si viene en attrs/effects)
192
242
  $effect(() => {
193
243
  if (!fxLayer) return;
194
-
195
244
  const effectsOpts: IconEffectOptions = { ...(props.effects ?? {}) };
196
- const a = props.attrs ?? {};
197
- if (a.spinOnHover) effectsOpts.spinOnHover = true;
198
- if (a.bounceOnHover) effectsOpts.bounceOnHover = true;
199
- if (a.wiggleOnHover) effectsOpts.wiggleOnHover = true;
200
- if (a.slideOnHover) effectsOpts.slideOnHover = a.slideOnHover;
201
- if (a.morphOnHover) effectsOpts.morphOnHover = a.morphOnHover;
202
- if (a.elasticOnClick) effectsOpts.elasticOnClick = true;
203
- if (a.heartbeatOnActive) effectsOpts.heartbeatOnActive = true;
204
- if (a.hoverScale) effectsOpts.hoverScale = a.hoverScale;
205
- if (a.pressScale) effectsOpts.pressScale = a.pressScale;
206
-
207
- // ⛳ Aplica transforms sobre fxLayer (no sobre el <svg>)
245
+ const a = props.attrs;
246
+ if (a) {
247
+ if (a.spinOnHover) effectsOpts.spinOnHover = true;
248
+ if (a.bounceOnHover) effectsOpts.bounceOnHover = true;
249
+ if (a.wiggleOnHover) effectsOpts.wiggleOnHover = true;
250
+ if (a.slideOnHover) effectsOpts.slideOnHover = a.slideOnHover;
251
+ if (a.morphOnHover) effectsOpts.morphOnHover = a.morphOnHover;
252
+ if (a.elasticOnClick) effectsOpts.elasticOnClick = true;
253
+ if (a.heartbeatOnActive) effectsOpts.heartbeatOnActive = true;
254
+ if ((a as any).heartbeatOnHover) effectsOpts.heartbeatOnHover = true;
255
+ if (a.hoverScale) effectsOpts.hoverScale = a.hoverScale;
256
+ if (a.pressScale) effectsOpts.pressScale = a.pressScale;
257
+ }
208
258
  const controller = iconEffects(fxLayer, effectsOpts);
209
259
  return () => controller.destroy();
210
260
  });
@@ -243,8 +293,6 @@
243
293
  onmouseleave={handleMouseLeave}
244
294
  >
245
295
  {#if props.title}<title id={computedTitleId}>{props.title}</title>{/if}
246
-
247
- <!-- 🎯 CAPA FX: aquí se aplican transformaciones programáticas (iconEffects) -->
248
296
  <g bind:this={fxLayer} transform={svgTransform()}>
249
297
  {@render props.children?.()}
250
298
  </g>
@@ -268,7 +316,6 @@
268
316
  }
269
317
  }
270
318
 
271
- /* Keyframes usados por animaciones inline */
272
319
  @keyframes __icon_spin {
273
320
  to {
274
321
  transform: rotate(360deg);
package/dist/effects.d.ts CHANGED
@@ -6,6 +6,8 @@ export type IconEffectOptions = {
6
6
  morphOnHover?: "play" | "pause" | "menu" | "close" | "arrow" | "check";
7
7
  elasticOnClick?: boolean;
8
8
  heartbeatOnActive?: boolean;
9
+ /** ✅ nuevo: heartbeat al hacer hover */
10
+ heartbeatOnHover?: boolean;
9
11
  pulse?: boolean;
10
12
  hoverScale?: number;
11
13
  pressScale?: number;