@genarou/blazir-icons 1.2.16 → 1.3.2

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.
Files changed (81) hide show
  1. package/LICENSE +121 -0
  2. package/README.md +1206 -0
  3. package/dist/CustomIcon.svelte +30 -0
  4. package/dist/CustomIcon.svelte.d.ts +14 -0
  5. package/dist/Icon.svelte +282 -102
  6. package/dist/Icon.svelte.d.ts +12 -5
  7. package/dist/IconBadge.svelte +75 -0
  8. package/dist/IconBadge.svelte.d.ts +16 -0
  9. package/dist/IconBase.svelte +89 -57
  10. package/dist/effects.js +1 -3
  11. package/dist/icons/Camera.svelte +19 -0
  12. package/dist/icons/Camera.svelte.d.ts +4 -0
  13. package/dist/icons/Cards.svelte +19 -0
  14. package/dist/icons/Cards.svelte.d.ts +4 -0
  15. package/dist/icons/CloudAlert.svelte +19 -0
  16. package/dist/icons/CloudAlert.svelte.d.ts +4 -0
  17. package/dist/icons/CloudCheck.svelte +19 -0
  18. package/dist/icons/CloudCheck.svelte.d.ts +4 -0
  19. package/dist/icons/CloudDownload.svelte +19 -0
  20. package/dist/icons/CloudDownload.svelte.d.ts +4 -0
  21. package/dist/icons/Colors.svelte +13 -0
  22. package/dist/icons/Colors.svelte.d.ts +4 -0
  23. package/dist/icons/CreditCard.svelte +19 -0
  24. package/dist/icons/CreditCard.svelte.d.ts +4 -0
  25. package/dist/icons/Desktop.svelte +19 -0
  26. package/dist/icons/Desktop.svelte.d.ts +4 -0
  27. package/dist/icons/DoughnutChart.svelte +19 -0
  28. package/dist/icons/DoughnutChart.svelte.d.ts +4 -0
  29. package/dist/icons/Earth.svelte +19 -0
  30. package/dist/icons/Earth.svelte.d.ts +4 -0
  31. package/dist/icons/Globe.svelte +19 -0
  32. package/dist/icons/Globe.svelte.d.ts +4 -0
  33. package/dist/icons/LightHub.svelte +19 -0
  34. package/dist/icons/LightHub.svelte.d.ts +4 -0
  35. package/dist/icons/Link.svelte +19 -0
  36. package/dist/icons/Link.svelte.d.ts +4 -0
  37. package/dist/icons/More.svelte +13 -0
  38. package/dist/icons/More.svelte.d.ts +4 -0
  39. package/dist/icons/Power.svelte +19 -0
  40. package/dist/icons/Power.svelte.d.ts +4 -0
  41. package/dist/icons/Receipt.svelte +19 -0
  42. package/dist/icons/Receipt.svelte.d.ts +4 -0
  43. package/dist/icons/SharedFolder.svelte +13 -0
  44. package/dist/icons/SharedFolder.svelte.d.ts +4 -0
  45. package/dist/icons/Sidebar.svelte +13 -0
  46. package/dist/icons/Sidebar.svelte.d.ts +4 -0
  47. package/dist/icons/Sync.svelte +19 -0
  48. package/dist/icons/Sync.svelte.d.ts +4 -0
  49. package/dist/icons/Tags.svelte +13 -0
  50. package/dist/icons/Tags.svelte.d.ts +4 -0
  51. package/dist/icons/Tools.svelte +20 -0
  52. package/dist/icons/Tools.svelte.d.ts +4 -0
  53. package/dist/icons/Upload.svelte +12 -57
  54. package/dist/icons/Wifi.svelte +19 -0
  55. package/dist/icons/Wifi.svelte.d.ts +4 -0
  56. package/dist/icons/lazy-registry.d.ts +21 -0
  57. package/dist/icons/lazy-registry.js +251 -0
  58. package/dist/icons/registry.d.ts +146 -129
  59. package/dist/icons/registry.js +184 -132
  60. package/dist/icons-api.d.ts +67 -257
  61. package/dist/icons-api.js +84 -453
  62. package/dist/index.d.ts +5 -5
  63. package/dist/index.js +14 -11
  64. package/dist/plugin/index.d.ts +46 -0
  65. package/dist/plugin/index.js +327 -0
  66. package/dist/smart-cache.d.ts +35 -0
  67. package/dist/smart-cache.js +192 -0
  68. package/dist/types.d.ts +19 -2
  69. package/dist/utils/sanitize.d.ts +25 -0
  70. package/dist/utils/sanitize.js +109 -0
  71. package/package.json +23 -13
  72. package/dist/icons/Aws.svelte +0 -19
  73. package/dist/icons/Aws.svelte.d.ts +0 -4
  74. package/dist/icons/Facebook.svelte +0 -18
  75. package/dist/icons/Facebook.svelte.d.ts +0 -4
  76. package/dist/icons/Golang.svelte +0 -17
  77. package/dist/icons/Golang.svelte.d.ts +0 -4
  78. package/dist/icons/Google.svelte +0 -18
  79. package/dist/icons/Google.svelte.d.ts +0 -4
  80. package/dist/icons/Paypal.svelte +0 -21
  81. package/dist/icons/Paypal.svelte.d.ts +0 -4
@@ -0,0 +1,30 @@
1
+ <!-- CustomIcon.svelte -->
2
+ <script lang="ts">
3
+ import IconBase from "./IconBase.svelte";
4
+ import { sanitizeSvg } from "./utils/sanitize.js";
5
+ import type { IconProps } from "./types";
6
+
7
+ interface CustomIconProps extends Omit<IconProps, "children"> {
8
+ /** Raw SVG inner markup: paths, circles, polygons, etc. */
9
+ svgContent: string;
10
+ /**
11
+ * Sanitize svgContent against XSS before rendering.
12
+ * Default: true — always on when content comes from external sources.
13
+ * Set to false only for static, trusted SVG strings defined in your own code.
14
+ */
15
+ sanitize?: boolean;
16
+ }
17
+
18
+ const {
19
+ svgContent,
20
+ sanitize = true,
21
+ viewBox = "0 0 24 24",
22
+ ...rest
23
+ }: CustomIconProps = $props();
24
+
25
+ const safeContent = $derived(sanitize ? sanitizeSvg(svgContent) : svgContent);
26
+ </script>
27
+
28
+ <IconBase {viewBox} {...rest}>
29
+ {@html safeContent}
30
+ </IconBase>
@@ -0,0 +1,14 @@
1
+ import type { IconProps } from "./types";
2
+ interface CustomIconProps extends Omit<IconProps, "children"> {
3
+ /** Raw SVG inner markup: paths, circles, polygons, etc. */
4
+ svgContent: string;
5
+ /**
6
+ * Sanitize svgContent against XSS before rendering.
7
+ * Default: true — always on when content comes from external sources.
8
+ * Set to false only for static, trusted SVG strings defined in your own code.
9
+ */
10
+ sanitize?: boolean;
11
+ }
12
+ declare const CustomIcon: import("svelte").Component<CustomIconProps, {}, "">;
13
+ type CustomIcon = ReturnType<typeof CustomIcon>;
14
+ export default CustomIcon;
package/dist/Icon.svelte CHANGED
@@ -1,22 +1,37 @@
1
- <!-- DynamicIcon.svelte -->
1
+ <!-- Icon.svelte — Refactored v2 -->
2
+ <!-- Cache inteligente adaptativo + lazy loading opcional + actions diff -->
2
3
  <script lang="ts">
3
- import { coerceSize } from ".";
4
- import { iconRegistry, type IconName } from "./icons/registry";
5
- import { iconPresets, iconVariants } from "./presets";
6
- import type { IconProps } from "./types";
7
-
8
- // Bridge de actions externas (no importamos lazy aquí)
4
+ import type { Component } from 'svelte';
5
+ import { coerceSize } from './utils/defaults.js';
6
+ import { iconRegistry, type IconName } from './icons/registry.js';
7
+ import { iconPresets, iconVariants } from './presets.js';
8
+ import type { IconProps } from './types.js';
9
+ import {
10
+ getPresetVariantMerge,
11
+ arePropsStable,
12
+ buildStructuralKey,
13
+ getCachedMergedProps,
14
+ setCachedMergedProps,
15
+ toSizePx,
16
+ } from './smart-cache.js';
17
+ import {
18
+ getLoadedIcon,
19
+ loadIcon,
20
+ } from './icons/lazy-registry.js';
21
+
22
+ // ============================================================================
23
+ // TIPOS
24
+ // ============================================================================
9
25
  type ActionFn<T = HTMLElement, P = any> = (
10
26
  node: T,
11
27
  params?: P
12
28
  ) => void | { update?: (p?: P) => void; destroy?: () => void };
13
29
 
14
30
  type ActionEntry<T = HTMLElement> = [ActionFn<T, any>, any?];
15
-
16
31
  type IconPresetName = keyof typeof iconPresets;
17
32
  type IconVariantName = keyof typeof iconVariants;
18
33
 
19
- interface DynamicIconProps extends Omit<IconProps, "children"> {
34
+ interface DynamicIconProps extends Omit<IconProps, 'children'> {
20
35
  name?: IconName;
21
36
  preset?: IconPresetName;
22
37
  variant?: IconVariantName;
@@ -24,155 +39,268 @@
24
39
  style?: string;
25
40
  /** Pasas aquí tus actions: p.ej. actions={[[lazyClass, opts]]} */
26
41
  actions?: ActionEntry[];
42
+ /**
43
+ * Activa lazy loading: el ícono se carga como dynamic import() separado.
44
+ * Útil para code-splitting cuando usas muchos íconos distintos.
45
+ * Por defecto: false (usa el registry estático, comportamiento original).
46
+ */
47
+ lazy?: boolean;
27
48
  }
28
49
 
29
50
  const p: DynamicIconProps = $props();
30
51
 
31
- // 🔥 OPTIMIZACIÓN: Cache para props combinadas
32
- const propsCache = new Map<string, any>();
33
-
34
- // 🔥 OPTIMIZACIÓN: Derivados en lugar de effects + states
52
+ // ============================================================================
53
+ // RESOLUCIÓN DEL COMPONENTE
54
+ // ============================================================================
35
55
  const iconName = $derived(p.name ?? null);
36
- const Comp = $derived(iconName ? iconRegistry[iconName] : null);
37
56
 
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
- });
57
+ // --- Modo EAGER (default): usa el registry estático ya cargado ---
58
+ // Mismo comportamiento que antes: sin latencia, sin skeleton en primer render.
59
+ const _eagerComp = $derived(
60
+ iconName ? (iconRegistry[iconName as IconName] ?? null) : null
61
+ );
62
+
63
+ // --- Modo LAZY (opt-in): dynamic import() por ícono ---
64
+ // Primer render: busca en caché de íconos ya cargados (getLoadedIcon).
65
+ // Si no está, muestra skeleton y carga en background.
66
+ let _lazyComp = $state<Component | null>(null);
67
+ let _lazyLoading = $state(false);
68
+ let _lazyCurrentName = $state<string | null>(null);
69
+
70
+ $effect(() => {
71
+ if (!p.lazy) return;
72
+
73
+ const name = iconName;
74
+
75
+ if (!name) {
76
+ _lazyComp = null;
77
+ _lazyCurrentName = null;
78
+ _lazyLoading = false;
79
+ return;
80
+ }
81
+
82
+ // Si es el mismo nombre y ya lo cargamos → no hacer nada
83
+ if (_lazyCurrentName === name && _lazyComp) return;
43
84
 
44
- const variantProps = $derived(() => {
45
- const key = p.variant as IconVariantName | undefined;
46
- return key ? iconVariants[key] : {};
85
+ // Intentar sincrónico primero (caché de íconos ya cargados)
86
+ const synced = getLoadedIcon(name) ?? (iconRegistry[name as IconName] ?? null);
87
+ if (synced) {
88
+ _lazyComp = synced;
89
+ _lazyCurrentName = name;
90
+ _lazyLoading = false;
91
+ return;
92
+ }
93
+
94
+ // Carga asíncrona
95
+ _lazyLoading = true;
96
+ _lazyCurrentName = name;
97
+ loadIcon(name).then((comp) => {
98
+ // Solo actualizar si el nombre sigue siendo el mismo
99
+ if (_lazyCurrentName === name) {
100
+ _lazyComp = comp;
101
+ _lazyLoading = false;
102
+ }
103
+ });
47
104
  });
48
105
 
49
- // 🔥 OPTIMIZACIÓN: Helper para normalizar ms
106
+ // Componente activo según modo
107
+ const Comp = $derived(p.lazy ? _lazyComp : _eagerComp);
108
+ const isLoading = $derived(p.lazy ? _lazyLoading : false);
109
+
110
+ // ============================================================================
111
+ // CACHE INTELIGENTE DE MERGE — PRESET + VARIANT
112
+ //
113
+ // getPresetVariantMerge devuelve SIEMPRE la misma referencia de objeto
114
+ // para la misma combinación (preset, variant). Esto garantiza que $derived
115
+ // no re-ejecute child si solo cambió la referencia pero no el contenido.
116
+ // ============================================================================
117
+ const baseProps = $derived(getPresetVariantMerge(p.preset, p.variant));
118
+
119
+ // ============================================================================
120
+ // NORMALIZACIÓN DE TIMING
121
+ // ============================================================================
50
122
  function toMs(v: unknown): string | undefined {
51
123
  if (v == null) return undefined;
52
- return typeof v === "number" ? `${v}ms` : String(v);
124
+ return typeof v === 'number' ? `${v}ms` : String(v);
53
125
  }
54
126
 
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 };
127
+ // ============================================================================
128
+ // CHILD PROPS merge base + overrides de usuario
129
+ //
130
+ // Estrategia de cache adaptativo:
131
+ // 1. Si TODAS las props del usuario son primitivas (stable) → cache global LRU
132
+ // con clave estructural. La misma config siempre devuelve la MISMA referencia.
133
+ // 2. Si hay props inestables (effects, actions, attrs, objetos) → merge directo
134
+ // sin cache, pero baseProps sigue siendo estable para evitar spreads extra.
135
+ //
136
+ // Usamos $derived.by (idiomático en Svelte 5) en lugar de $derived(() => fn).
137
+ // $derived.by trackea dependencias correctamente y caching del resultado.
138
+ // ============================================================================
139
+ const child = $derived.by((): IconProps => {
140
+ // Extraer props que no se pasan al hijo
141
+ const {
142
+ class: _cls,
143
+ style: _sty,
144
+ actions: _act,
145
+ lazy: _lz,
146
+ preset: _pre,
147
+ variant: _var,
148
+ name: _nm,
149
+ ...userProps
150
+ } = p;
151
+
152
+ // Detección de estabilidad: ¿todos los valores son primitivos?
153
+ const stable = arePropsStable(userProps as Record<string, unknown>);
154
+
155
+ if (stable) {
156
+ // Cache LRU global: misma config → misma referencia exacta
157
+ const key = buildStructuralKey(userProps as Record<string, unknown>);
158
+ const cached = getCachedMergedProps(key);
159
+ if (cached) return cached;
160
+
161
+ // Merge: baseProps (stable reference) → user overrides
162
+ const merged: any = { ...baseProps, ...userProps };
163
+
164
+ // Normalizar timing de animación a string con unidad
165
+ if (merged.animationDuration !== undefined) {
166
+ merged.animationDuration = toMs(merged.animationDuration);
167
+ }
168
+ if (merged.animationDelay !== undefined) {
169
+ merged.animationDelay = toMs(merged.animationDelay);
170
+ }
77
171
 
78
- if (normalized.animationDuration !== undefined) {
79
- normalized.animationDuration = toMs(normalized.animationDuration);
172
+ // Object.freeze para inmutabilidad interna; cast de vuelta a IconProps
173
+ // para que el spread en la plantilla sea compatible con el tipo del componente
174
+ const result = Object.freeze(merged) as unknown as IconProps;
175
+ setCachedMergedProps(key, result as Readonly<IconProps>);
176
+ return result;
80
177
  }
81
178
 
82
- if (normalized.animationDelay !== undefined) {
83
- normalized.animationDelay = toMs(normalized.animationDelay);
179
+ // Props inestables: merge directo (no cacheable globalmente)
180
+ // baseProps sigue siendo la misma referencia si preset/variant no cambiaron
181
+ const merged: any = { ...baseProps, ...userProps };
182
+ if (merged.animationDuration !== undefined) {
183
+ merged.animationDuration = toMs(merged.animationDuration);
84
184
  }
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
- }
185
+ if (merged.animationDelay !== undefined) {
186
+ merged.animationDelay = toMs(merged.animationDelay);
100
187
  }
101
-
102
- return result;
188
+ return merged as IconProps;
103
189
  });
104
190
 
105
- // 🔥 OPTIMIZACIÓN: Box size como derivado
106
- const boxSize = $derived(() => {
107
- const rawSize =
108
- (p as any).size ??
109
- (presetProps() as any).size ??
110
- (variantProps() as any).size;
191
+ // ============================================================================
192
+ // SIZING usa toSizePx del smart-cache para estabilidad referencial
193
+ // ============================================================================
194
+ const boxSize = $derived.by(() => {
195
+ // Prioridad: prop directa > preset > variant (baseProps ya los combina)
196
+ const rawSize = (p as any).size ?? (baseProps as any).size;
111
197
  const s = coerceSize(rawSize, 24);
112
- return typeof s === "number" ? `${s}px` : s;
198
+ return toSizePx(s);
113
199
  });
114
200
 
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
- );
201
+ // ============================================================================
202
+ // ATRIBUTOS DEL HOST
203
+ // ============================================================================
204
+ const hostClass = $derived(
205
+ p.class ? `bz-icon-wrapper ${p.class}` : 'bz-icon-wrapper'
206
+ );
207
+
208
+ const hostStyle = $derived.by(() => {
209
+ const sz = boxSize;
210
+ const base =
211
+ `--bz-icon-size:${sz};width:${sz};height:${sz};` +
212
+ `inline-size:${sz};block-size:${sz};`;
213
+ return p.style ? base + p.style : base;
127
214
  });
128
215
 
129
- // Key para forzar remonte y reiniciar animaciones
216
+ // ============================================================================
217
+ // MOUNT KEY — para reiniciar animaciones desde actions externas
218
+ // Solo se incrementa vía evento lazyMount, no en cada render.
219
+ // ============================================================================
130
220
  let mountKey = $state(0);
131
221
 
132
- // 🔥 OPTIMIZACIÓN: Bridge de actions más eficiente
222
+ function onLazyMount() {
223
+ mountKey++;
224
+ }
225
+
226
+ // ============================================================================
227
+ // ACTIONS BRIDGE — diff en lugar de destroy/recreate
228
+ //
229
+ // Mejora sobre la versión anterior:
230
+ // - Si las FUNCIONES de action son las mismas (mismas referencias) → solo
231
+ // llama update(newParams) en cada handle. Preserva estado interno.
232
+ // - Solo destruye y recrea si cambian las funciones de action (distinto conjunto).
233
+ //
234
+ // Esto es crítico para actions con estado (tooltips, lazy observers, etc.)
235
+ // ============================================================================
133
236
  function applyActions(node: HTMLElement, entries: ActionEntry[] = []) {
134
- if (!entries || entries.length === 0) {
135
- return { update() {}, destroy() {} };
136
- }
237
+ if (!entries?.length) return { update() {}, destroy() {} };
137
238
 
239
+ let prevEntries: ActionEntry[] = entries;
138
240
  let handles = entries
139
241
  .map(([fn, params]) => fn?.(node, params))
140
242
  .filter(Boolean);
141
243
 
142
244
  return {
143
245
  update(next: ActionEntry[] = []) {
144
- // Cleanup anterior
246
+ if (!next?.length) {
247
+ handles.forEach((h) => h?.destroy?.());
248
+ handles = [];
249
+ prevEntries = [];
250
+ return;
251
+ }
252
+
253
+ // Mismo número de actions y mismas referencias de función → diff params
254
+ if (next.length === prevEntries.length) {
255
+ let sameFns = true;
256
+ for (let i = 0; i < next.length; i++) {
257
+ if (next[i][0] !== prevEntries[i][0]) {
258
+ sameFns = false;
259
+ break;
260
+ }
261
+ }
262
+ if (sameFns) {
263
+ // Solo actualizar parámetros — preserva estado interno de cada action
264
+ handles.forEach((h, i) => h?.update?.(next[i][1]));
265
+ prevEntries = next;
266
+ return;
267
+ }
268
+ }
269
+
270
+ // Distintas funciones → destroy + recreate
145
271
  handles.forEach((h) => h?.destroy?.());
146
-
147
- // Aplicar nuevos
272
+ prevEntries = next;
148
273
  handles = next
149
274
  .map(([fn, params]) => fn?.(node, params))
150
275
  .filter(Boolean);
151
276
  },
277
+
152
278
  destroy() {
153
279
  handles.forEach((h) => h?.destroy?.());
154
280
  handles = [];
281
+ prevEntries = [];
155
282
  },
156
283
  };
157
284
  }
158
-
159
- // Reanima al recibir el evento del action
160
- function onLazyMount() {
161
- mountKey++;
162
- }
163
285
  </script>
164
286
 
165
287
  {#if Comp}
166
288
  <span
167
289
  class={hostClass}
168
- style={hostStyle()}
290
+ style={p.tooltipDelay != null
291
+ ? `${hostStyle};--bz-tooltip-delay:${p.tooltipDelay}ms`
292
+ : hostStyle}
293
+ data-bz-tooltip={p.tooltip || undefined}
169
294
  use:applyActions={p.actions ?? []}
170
295
  onlazyMount={onLazyMount}
171
296
  >
172
297
  {#key mountKey}
173
- <Comp {...child()} />
298
+ <!-- svelte-ignore: cast necesario para componentes dinámicos con props genéricas -->
299
+ <Comp {...(child as any)} />
174
300
  {/key}
175
301
  </span>
302
+ {:else if isLoading}
303
+ <span class="bz-icon-skeleton" aria-hidden="true"></span>
176
304
  {:else if iconName}
177
305
  <span class="bz-icon-error" data-icon={iconName}>
178
306
  ⚠️ Icon not found: {iconName}
@@ -189,10 +317,55 @@
189
317
  justify-content: center;
190
318
  position: relative;
191
319
  pointer-events: auto;
192
- contain: layout style paint;
320
+ contain: layout style;
193
321
  content-visibility: auto;
194
322
  }
195
323
 
324
+ /* ── Tooltip CSS — configurable, dark mode, sin JS ── */
325
+ .bz-icon-wrapper[data-bz-tooltip]::before,
326
+ .bz-icon-wrapper[data-bz-tooltip]::after {
327
+ position: absolute;
328
+ bottom: calc(100% + 8px);
329
+ left: 50%;
330
+ transform: translateX(-50%);
331
+ pointer-events: none;
332
+ opacity: 0;
333
+ transition: opacity 0.15s ease var(--bz-tooltip-delay, 0ms),
334
+ transform 0.15s ease var(--bz-tooltip-delay, 0ms);
335
+ z-index: 9999;
336
+ }
337
+
338
+ /* Cuerpo del tooltip */
339
+ .bz-icon-wrapper[data-bz-tooltip]::after {
340
+ content: attr(data-bz-tooltip);
341
+ background: var(--bz-tooltip-bg, #1a1a1a);
342
+ color: var(--bz-tooltip-color, #fff);
343
+ font-size: 11px;
344
+ font-weight: 500;
345
+ line-height: 1.4;
346
+ padding: 5px 9px;
347
+ border-radius: 6px;
348
+ white-space: nowrap;
349
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
350
+ transform: translateX(-50%) translateY(3px);
351
+ }
352
+
353
+ /* Flecha del tooltip */
354
+ .bz-icon-wrapper[data-bz-tooltip]::before {
355
+ content: '';
356
+ border: 5px solid transparent;
357
+ border-top-color: var(--bz-tooltip-bg, #1a1a1a);
358
+ bottom: calc(100% + 0px);
359
+ transform: translateX(-50%) translateY(3px);
360
+ }
361
+
362
+ /* Mostrar en hover */
363
+ .bz-icon-wrapper[data-bz-tooltip]:hover::after,
364
+ .bz-icon-wrapper[data-bz-tooltip]:hover::before {
365
+ opacity: 1;
366
+ transform: translateX(-50%) translateY(0);
367
+ }
368
+
196
369
  .bz-icon-wrapper :global(svg) {
197
370
  pointer-events: bounding-box !important;
198
371
  }
@@ -208,6 +381,7 @@
208
381
  background: color-mix(in oklab, currentColor 12%, transparent);
209
382
  opacity: 0.5;
210
383
  }
384
+
211
385
  .bz-icon-error {
212
386
  display: inline-block;
213
387
  width: 1em;
@@ -220,32 +394,38 @@
220
394
  border-radius: var(--ui-radius, 0.25rem);
221
395
  font-weight: 600;
222
396
  }
397
+
223
398
  .is-entering {
224
399
  opacity: 0.001;
225
400
  transform: translateY(4px);
226
401
  animation: ic-enter 160ms ease-out forwards;
227
402
  }
403
+
228
404
  .is-leaving {
229
405
  opacity: 1;
230
406
  transform: none;
231
407
  animation: ic-leave 160ms ease-in forwards;
232
408
  }
409
+
233
410
  .is-mounted {
234
411
  opacity: 1;
235
412
  transform: none;
236
413
  }
414
+
237
415
  @keyframes ic-enter {
238
416
  to {
239
417
  opacity: 1;
240
418
  transform: none;
241
419
  }
242
420
  }
421
+
243
422
  @keyframes ic-leave {
244
423
  to {
245
424
  opacity: 0.001;
246
425
  transform: translateY(4px);
247
426
  }
248
427
  }
428
+
249
429
  @media (prefers-reduced-motion: reduce) {
250
430
  .is-entering,
251
431
  .is-leaving {
@@ -1,6 +1,7 @@
1
- import { type IconName } from "./icons/registry";
2
- import { iconPresets, iconVariants } from "./presets";
3
- import type { IconProps } from "./types";
1
+ import type { Component } from 'svelte';
2
+ import { type IconName } from './icons/registry.js';
3
+ import { iconPresets, iconVariants } from './presets.js';
4
+ import type { IconProps } from './types.js';
4
5
  type ActionFn<T = HTMLElement, P = any> = (node: T, params?: P) => void | {
5
6
  update?: (p?: P) => void;
6
7
  destroy?: () => void;
@@ -8,7 +9,7 @@ type ActionFn<T = HTMLElement, P = any> = (node: T, params?: P) => void | {
8
9
  type ActionEntry<T = HTMLElement> = [ActionFn<T, any>, any?];
9
10
  type IconPresetName = keyof typeof iconPresets;
10
11
  type IconVariantName = keyof typeof iconVariants;
11
- interface DynamicIconProps extends Omit<IconProps, "children"> {
12
+ interface DynamicIconProps extends Omit<IconProps, 'children'> {
12
13
  name?: IconName;
13
14
  preset?: IconPresetName;
14
15
  variant?: IconVariantName;
@@ -16,7 +17,13 @@ interface DynamicIconProps extends Omit<IconProps, "children"> {
16
17
  style?: string;
17
18
  /** Pasas aquí tus actions: p.ej. actions={[[lazyClass, opts]]} */
18
19
  actions?: ActionEntry[];
20
+ /**
21
+ * Activa lazy loading: el ícono se carga como dynamic import() separado.
22
+ * Útil para code-splitting cuando usas muchos íconos distintos.
23
+ * Por defecto: false (usa el registry estático, comportamiento original).
24
+ */
25
+ lazy?: boolean;
19
26
  }
20
- declare const Icon: import("svelte").Component<DynamicIconProps, {}, "">;
27
+ declare const Icon: Component<DynamicIconProps, {}, "">;
21
28
  type Icon = ReturnType<typeof Icon>;
22
29
  export default Icon;
@@ -0,0 +1,75 @@
1
+ <!-- IconBadge.svelte — Wrapper ligero que añade un badge/dot sobre cualquier Icon -->
2
+ <script lang="ts">
3
+ import Icon from "./Icon.svelte";
4
+ import type { DynamicIconProps } from "./types";
5
+
6
+ interface BadgeProps extends DynamicIconProps {
7
+ /** true = punto rojo | number = conteo | string = texto */
8
+ badge?: boolean | number | string;
9
+ /** Color de fondo del badge. Default: rojo */
10
+ badgeColor?: string;
11
+ /** Color del texto del badge. Default: blanco */
12
+ badgeTextColor?: string;
13
+ /** Tamaño del badge en px. Default: 8 (dot) o 16 (con texto) */
14
+ badgeSize?: number;
15
+ /** Posición del badge. Default: top-right */
16
+ badgePosition?: "top-right" | "top-left" | "bottom-right" | "bottom-left";
17
+ }
18
+
19
+ const {
20
+ badge,
21
+ badgeColor = "#ef4444",
22
+ badgeTextColor = "#fff",
23
+ badgeSize,
24
+ badgePosition = "top-right",
25
+ ...iconProps
26
+ }: BadgeProps = $props();
27
+
28
+ const showBadge = $derived(badge !== false && badge != null);
29
+ const badgeText = $derived(
30
+ badge === true || badge === false ? "" : String(badge)
31
+ );
32
+ const hasBadgeText = $derived(badgeText.length > 0);
33
+ const size = $derived(badgeSize ?? (hasBadgeText ? 16 : 8));
34
+
35
+ // Posición absoluta del badge
36
+ const posStyle = $derived.by(() => {
37
+ const offset = `-${Math.round(size * 0.3)}px`;
38
+ switch (badgePosition) {
39
+ case "top-left": return `top:${offset};left:${offset};`;
40
+ case "bottom-right": return `bottom:${offset};right:${offset};`;
41
+ case "bottom-left": return `bottom:${offset};left:${offset};`;
42
+ default: return `top:${offset};right:${offset};`;
43
+ }
44
+ });
45
+ </script>
46
+
47
+ {#if showBadge}
48
+ <span style="position:relative;display:inline-flex;vertical-align:middle;line-height:0;">
49
+ <Icon {...iconProps} />
50
+ <span
51
+ aria-label={hasBadgeText ? badgeText : undefined}
52
+ aria-hidden={hasBadgeText ? undefined : "true"}
53
+ style="
54
+ position:absolute;
55
+ {posStyle}
56
+ min-width:{size}px;
57
+ height:{size}px;
58
+ background:{badgeColor};
59
+ border-radius:9999px;
60
+ display:inline-flex;
61
+ align-items:center;
62
+ justify-content:center;
63
+ font-size:{Math.max(size - 5, 8)}px;
64
+ font-weight:700;
65
+ color:{badgeTextColor};
66
+ line-height:1;
67
+ padding:{hasBadgeText ? '0 4px' : '0'};
68
+ pointer-events:none;
69
+ box-sizing:border-box;
70
+ "
71
+ >{badgeText}</span>
72
+ </span>
73
+ {:else}
74
+ <Icon {...iconProps} />
75
+ {/if}