@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.
- package/LICENSE +121 -0
- package/README.md +1206 -0
- package/dist/CustomIcon.svelte +30 -0
- package/dist/CustomIcon.svelte.d.ts +14 -0
- package/dist/Icon.svelte +282 -102
- package/dist/Icon.svelte.d.ts +12 -5
- package/dist/IconBadge.svelte +75 -0
- package/dist/IconBadge.svelte.d.ts +16 -0
- package/dist/IconBase.svelte +89 -57
- package/dist/effects.js +1 -3
- package/dist/icons/Camera.svelte +19 -0
- package/dist/icons/Camera.svelte.d.ts +4 -0
- package/dist/icons/Cards.svelte +19 -0
- package/dist/icons/Cards.svelte.d.ts +4 -0
- package/dist/icons/CloudAlert.svelte +19 -0
- package/dist/icons/CloudAlert.svelte.d.ts +4 -0
- package/dist/icons/CloudCheck.svelte +19 -0
- package/dist/icons/CloudCheck.svelte.d.ts +4 -0
- package/dist/icons/CloudDownload.svelte +19 -0
- package/dist/icons/CloudDownload.svelte.d.ts +4 -0
- package/dist/icons/Colors.svelte +13 -0
- package/dist/icons/Colors.svelte.d.ts +4 -0
- package/dist/icons/CreditCard.svelte +19 -0
- package/dist/icons/CreditCard.svelte.d.ts +4 -0
- package/dist/icons/Desktop.svelte +19 -0
- package/dist/icons/Desktop.svelte.d.ts +4 -0
- package/dist/icons/DoughnutChart.svelte +19 -0
- package/dist/icons/DoughnutChart.svelte.d.ts +4 -0
- package/dist/icons/Earth.svelte +19 -0
- package/dist/icons/Earth.svelte.d.ts +4 -0
- package/dist/icons/Globe.svelte +19 -0
- package/dist/icons/Globe.svelte.d.ts +4 -0
- package/dist/icons/LightHub.svelte +19 -0
- package/dist/icons/LightHub.svelte.d.ts +4 -0
- package/dist/icons/Link.svelte +19 -0
- package/dist/icons/Link.svelte.d.ts +4 -0
- package/dist/icons/More.svelte +13 -0
- package/dist/icons/More.svelte.d.ts +4 -0
- package/dist/icons/Power.svelte +19 -0
- package/dist/icons/Power.svelte.d.ts +4 -0
- package/dist/icons/Receipt.svelte +19 -0
- package/dist/icons/Receipt.svelte.d.ts +4 -0
- package/dist/icons/SharedFolder.svelte +13 -0
- package/dist/icons/SharedFolder.svelte.d.ts +4 -0
- package/dist/icons/Sidebar.svelte +13 -0
- package/dist/icons/Sidebar.svelte.d.ts +4 -0
- package/dist/icons/Sync.svelte +19 -0
- package/dist/icons/Sync.svelte.d.ts +4 -0
- package/dist/icons/Tags.svelte +13 -0
- package/dist/icons/Tags.svelte.d.ts +4 -0
- package/dist/icons/Tools.svelte +20 -0
- package/dist/icons/Tools.svelte.d.ts +4 -0
- package/dist/icons/Upload.svelte +12 -57
- package/dist/icons/Wifi.svelte +19 -0
- package/dist/icons/Wifi.svelte.d.ts +4 -0
- package/dist/icons/lazy-registry.d.ts +21 -0
- package/dist/icons/lazy-registry.js +251 -0
- package/dist/icons/registry.d.ts +146 -129
- package/dist/icons/registry.js +184 -132
- package/dist/icons-api.d.ts +67 -257
- package/dist/icons-api.js +84 -453
- package/dist/index.d.ts +5 -5
- package/dist/index.js +14 -11
- package/dist/plugin/index.d.ts +46 -0
- package/dist/plugin/index.js +327 -0
- package/dist/smart-cache.d.ts +35 -0
- package/dist/smart-cache.js +192 -0
- package/dist/types.d.ts +19 -2
- package/dist/utils/sanitize.d.ts +25 -0
- package/dist/utils/sanitize.js +109 -0
- package/package.json +23 -13
- package/dist/icons/Aws.svelte +0 -19
- package/dist/icons/Aws.svelte.d.ts +0 -4
- package/dist/icons/Facebook.svelte +0 -18
- package/dist/icons/Facebook.svelte.d.ts +0 -4
- package/dist/icons/Golang.svelte +0 -17
- package/dist/icons/Golang.svelte.d.ts +0 -4
- package/dist/icons/Google.svelte +0 -18
- package/dist/icons/Google.svelte.d.ts +0 -4
- package/dist/icons/Paypal.svelte +0 -21
- 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
|
-
<!--
|
|
1
|
+
<!-- Icon.svelte — Refactored v2 -->
|
|
2
|
+
<!-- Cache inteligente adaptativo + lazy loading opcional + actions diff -->
|
|
2
3
|
<script lang="ts">
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
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,
|
|
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
|
-
//
|
|
32
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
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
|
-
//
|
|
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 ===
|
|
124
|
+
return typeof v === 'number' ? `${v}ms` : String(v);
|
|
53
125
|
}
|
|
54
126
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
198
|
+
return toSizePx(s);
|
|
113
199
|
});
|
|
114
200
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
|
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 {
|
package/dist/Icon.svelte.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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,
|
|
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:
|
|
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}
|