@dynamic-icon/svelte 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +34 -2
- package/dist/DynamicIcon.svelte +104 -0
- package/dist/DynamicIcon.svelte.d.ts +13 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +9 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +41 -0
- package/llms-full.txt +125 -0
- package/llms.txt +21 -0
- package/package.json +61 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dynamic-icon contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dynamic-icon/svelte
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Svelte 5 component that renders icons from database records. It knows nothing about any icon library: you register
|
|
4
|
+
sources once with `configureIcons` (backed by the framework-free [`@dynamic-icon/core`](../core), installed automatically).
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
// app root, once (identically on server and client)
|
|
8
|
+
import { configureIcons } from '@dynamic-icon/svelte';
|
|
9
|
+
import CarFront from 'lucide-svelte/icons/car-front';
|
|
10
|
+
|
|
11
|
+
configureIcons({ sources: { lucide: { 'car-front': CarFront } }, fallback: CircleHelp });
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```svelte
|
|
15
|
+
<DynamicIcon {...record.icon} size={24} class="text-primary" />
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Props: `webType`, `webRef`, `color`, `media`, `scale`, `size`, `strokeWidth`, `class`, `fallback` (snippet).
|
|
19
|
+
|
|
20
|
+
| Prop | Notes |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `webType` / `webRef` | `image`, `emoji`, `markup`, a font type (`material`, `font-awesome`, `remix`) or a configured source. |
|
|
23
|
+
| `color` | A CSS colour (`#fff`, `rgb()`, `oklch()`, `var()`, `red`) goes in `style`; anything else is added as classes. |
|
|
24
|
+
| `size` | px for numbers; a plain CSS length for strings (`1.5rem`). Invalid values (including `24px;...`) use the default size. |
|
|
25
|
+
| `scale` | Multiplies a numeric `size`, rounded. Ignored if not a positive finite number. |
|
|
26
|
+
| `fallback` | Snippet shown when unresolved; wins over `configureIcons({ fallback })`. |
|
|
27
|
+
|
|
28
|
+
- Only `image` and `markup` icons get a wrapper `<span>`; everything else is a single element. All icons are `aria-hidden`
|
|
29
|
+
(decorative): put meaning in adjacent text, or give the parent an `aria-label`.
|
|
30
|
+
- `markup` renders only through your `sanitize` function; without one it shows the fallback. `image` uses `loading="lazy"`.
|
|
31
|
+
- Icon components receive `size`, `strokeWidth`, `class`, `style` (CSS colours only) and `aria-hidden`; `lucide-svelte` icons
|
|
32
|
+
accept these as-is. Custom icons should spread rest props onto their root `<svg>`.
|
|
33
|
+
|
|
34
|
+
AI-readable docs: [`llms.txt`](./llms.txt) and [`llms-full.txt`](./llms-full.txt), shipped in the npm package, e.g.
|
|
35
|
+
`https://unpkg.com/@dynamic-icon/svelte/llms-full.txt` once published.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* @component DynamicIcon
|
|
4
|
+
* Renders a database-driven icon record. Sources, fallback and defaults come from `configureIcons()`.
|
|
5
|
+
*
|
|
6
|
+
* <DynamicIcon {...record.icon} size={24} class="text-primary" />
|
|
7
|
+
*
|
|
8
|
+
* Resolution order by `webType`: `image` | `emoji` | `markup` | font type | configured source.
|
|
9
|
+
* `class` and `color` land on the rendered element; `image`/`markup` get a sized wrapper to carry them.
|
|
10
|
+
*/
|
|
11
|
+
import type { IconProps } from './types.js';
|
|
12
|
+
import { getIconConfig, resolveIcon } from './config.js';
|
|
13
|
+
import { boxSize, boxStyle, classes, colorStyle, textStyle } from './utils.js';
|
|
14
|
+
|
|
15
|
+
const config = getIconConfig();
|
|
16
|
+
const Fallback = config.fallback;
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
webType,
|
|
20
|
+
webRef,
|
|
21
|
+
color,
|
|
22
|
+
media,
|
|
23
|
+
scale,
|
|
24
|
+
size = config.size,
|
|
25
|
+
strokeWidth = config.strokeWidth,
|
|
26
|
+
fallback,
|
|
27
|
+
class: className
|
|
28
|
+
}: IconProps = $props();
|
|
29
|
+
|
|
30
|
+
// The only per-instance derived value: caching it keeps the component from re-mounting
|
|
31
|
+
// when unrelated props (size, color, class) change.
|
|
32
|
+
const Comp = $derived(resolveIcon(webType, webRef));
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
{#if Comp}
|
|
36
|
+
<Comp
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
size={boxSize(size, scale, config.size)}
|
|
39
|
+
{strokeWidth}
|
|
40
|
+
class={classes(color, className)}
|
|
41
|
+
style={colorStyle(color)}
|
|
42
|
+
/>
|
|
43
|
+
{:else if webType === 'image' && media?.link}
|
|
44
|
+
<span
|
|
45
|
+
class={classes(color, className, 'u-icon')}
|
|
46
|
+
style={boxStyle(boxSize(size, scale, config.size), color)}
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
>
|
|
49
|
+
<img src={media.link} alt={media.caption ?? ''} loading="lazy" decoding="async" />
|
|
50
|
+
</span>
|
|
51
|
+
{:else if webType === 'markup' && webRef && config.sanitize}
|
|
52
|
+
<span
|
|
53
|
+
class={classes(color, className, 'u-icon u-markup')}
|
|
54
|
+
style={boxStyle(boxSize(size, scale, config.size), color)}
|
|
55
|
+
aria-hidden="true"
|
|
56
|
+
>
|
|
57
|
+
{@html config.sanitize(webRef)}
|
|
58
|
+
</span>
|
|
59
|
+
{:else if webType === 'emoji' && webRef}
|
|
60
|
+
<span aria-hidden="true" class={classes(color, className)} style={textStyle(boxSize(size, scale, config.size), color)}
|
|
61
|
+
>{webRef}</span
|
|
62
|
+
>
|
|
63
|
+
{:else if webType && webRef && config.fontTypes.has(webType)}
|
|
64
|
+
<i aria-hidden="true" class={classes(color, className, webRef)} style={textStyle(boxSize(size, scale, config.size), color)}></i>
|
|
65
|
+
{:else if fallback}
|
|
66
|
+
{@render fallback()}
|
|
67
|
+
{:else if Fallback}
|
|
68
|
+
<Fallback
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
size={boxSize(size, scale, config.size)}
|
|
71
|
+
{strokeWidth}
|
|
72
|
+
class={classes(color, className)}
|
|
73
|
+
style={colorStyle(color)}
|
|
74
|
+
/>
|
|
75
|
+
{/if}
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
/* Scoped CSS instead of Tailwind utilities: Tailwind does not scan node_modules by default,
|
|
79
|
+
so utility classes used inside a package would silently not exist in the consumer's build. */
|
|
80
|
+
.u-icon {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
flex-shrink: 0;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
line-height: 1;
|
|
86
|
+
user-select: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.u-icon > img {
|
|
90
|
+
width: 100%;
|
|
91
|
+
height: 100%;
|
|
92
|
+
object-fit: contain;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Injected SVG fills the box; root <svg> without an explicit fill inherits `color`. */
|
|
96
|
+
.u-markup :global(svg) {
|
|
97
|
+
width: 100%;
|
|
98
|
+
height: 100%;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.u-markup :global(svg:not([fill])) {
|
|
102
|
+
fill: currentColor;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @component DynamicIcon
|
|
3
|
+
* Renders a database-driven icon record. Sources, fallback and defaults come from `configureIcons()`.
|
|
4
|
+
*
|
|
5
|
+
* <DynamicIcon {...record.icon} size={24} class="text-primary" />
|
|
6
|
+
*
|
|
7
|
+
* Resolution order by `webType`: `image` | `emoji` | `markup` | font type | configured source.
|
|
8
|
+
* `class` and `color` land on the rendered element; `image`/`markup` get a sized wrapper to carry them.
|
|
9
|
+
*/
|
|
10
|
+
import type { IconProps } from './types.js';
|
|
11
|
+
declare const DynamicIcon: import("svelte").Component<IconProps<string>, {}, "">;
|
|
12
|
+
type DynamicIcon = ReturnType<typeof DynamicIcon>;
|
|
13
|
+
export default DynamicIcon;
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResolvedIconConfig } from "@dynamic-icon/core";
|
|
2
|
+
import type { IconComponent, IconConfig } from "./types.js";
|
|
3
|
+
/** Sets the icon configuration. Call once at app start, identically on server and client. */
|
|
4
|
+
export declare const configureIcons: (config: IconConfig) => void;
|
|
5
|
+
/** Resolves a component for `webType`/`webRef`, or `undefined`. Usable outside components. */
|
|
6
|
+
export declare const resolveIcon: (webType: string | undefined, webRef: string | undefined) => IconComponent | undefined;
|
|
7
|
+
/** @internal Read by `DynamicIcon` once per instance. */
|
|
8
|
+
export declare const getIconConfig: () => ResolvedIconConfig<IconComponent>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Typed entry points over the framework-free registry in @dynamic-icon/core.
|
|
2
|
+
// This is the only place that fixes the registry's component type to `IconComponent`.
|
|
3
|
+
import { configureIcons as configure, getIconConfig as getConfig, resolveIcon as resolve } from "@dynamic-icon/core";
|
|
4
|
+
/** Sets the icon configuration. Call once at app start, identically on server and client. */
|
|
5
|
+
export const configureIcons = (config) => configure(config);
|
|
6
|
+
/** Resolves a component for `webType`/`webRef`, or `undefined`. Usable outside components. */
|
|
7
|
+
export const resolveIcon = (webType, webRef) => resolve(webType, webRef);
|
|
8
|
+
/** @internal Read by `DynamicIcon` once per instance. */
|
|
9
|
+
export const getIconConfig = () => getConfig();
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Component, ComponentType, Snippet, SvelteComponent } from "svelte";
|
|
2
|
+
import type { IconConfig as CoreIconConfig, IconSource as CoreIconSource } from "@dynamic-icon/core";
|
|
3
|
+
/** Props `DynamicIcon` passes to every icon component. `lucide-svelte` icons already accept them. */
|
|
4
|
+
export interface IconComponentProps {
|
|
5
|
+
size?: number | string;
|
|
6
|
+
strokeWidth?: number;
|
|
7
|
+
class?: string;
|
|
8
|
+
/** Only set when `color` is a CSS colour. */
|
|
9
|
+
style?: string;
|
|
10
|
+
"aria-hidden"?: "true";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Any Svelte component usable as an icon (Svelte 5 or legacy class-based, e.g. `lucide-svelte`).
|
|
14
|
+
* Props are `any` on purpose: library icons declare their own prop types, which are never exactly
|
|
15
|
+
* `IconComponentProps`. Custom icons should accept those props and spread the rest onto their root element.
|
|
16
|
+
*/
|
|
17
|
+
export type IconComponent = Component<any, any, any> | ComponentType<SvelteComponent<any, any, any>>;
|
|
18
|
+
export type IconSource = CoreIconSource<IconComponent>;
|
|
19
|
+
export type IconConfig = CoreIconConfig<IconComponent>;
|
|
20
|
+
/** Media attached to an `image` icon. Structurally compatible with CMS file objects. */
|
|
21
|
+
export interface IconMedia {
|
|
22
|
+
link: string;
|
|
23
|
+
caption?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Icon record as served by the CMS/DB. Narrow `T` to your own `webType`s if you like. */
|
|
26
|
+
export interface IconEntry<T extends string = string> {
|
|
27
|
+
/** Unique identifier key (e.g. 'nav_home'). Not used for rendering. */
|
|
28
|
+
name?: string;
|
|
29
|
+
/** Which source/renderer handles this icon. */
|
|
30
|
+
webType?: T;
|
|
31
|
+
/** Identifier within the source ('car-front'), icon-font classes, emoji, or raw markup. */
|
|
32
|
+
webRef?: string;
|
|
33
|
+
/** A utility class (`text-primary`) or a CSS colour (`#fff`, `rgb()`, `var()`, `red`). */
|
|
34
|
+
color?: string;
|
|
35
|
+
/** Image for the `image` type. */
|
|
36
|
+
media?: IconMedia;
|
|
37
|
+
/** Per-source size normalisation factor; the result is rounded to whole pixels. Numeric `size` only. */
|
|
38
|
+
scale?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface IconProps<T extends string = string> extends IconEntry<T> {
|
|
41
|
+
/** px when a number, a CSS length (`1.5rem`) when a string. Invalid values fall back to the default size. */
|
|
42
|
+
size?: number | string;
|
|
43
|
+
class?: string;
|
|
44
|
+
strokeWidth?: number;
|
|
45
|
+
/** Rendered when the icon cannot be resolved. Overrides `IconConfig.fallback`. */
|
|
46
|
+
fallback?: Snippet;
|
|
47
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const isCssColor: (color: string | undefined) => color is string;
|
|
2
|
+
/**
|
|
3
|
+
* Final icon size. Numbers must be finite and positive and get the per-source `scale` (rounded to avoid
|
|
4
|
+
* blurry sub-pixel rendering); strings must be a plain CSS length. Invalid input yields `fallback`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const boxSize: (size: number | string, scale: number | undefined, fallback: number) => number | string;
|
|
7
|
+
/** `style` for icon components: only a CSS colour needs it. */
|
|
8
|
+
export declare const colorStyle: (color: string | undefined) => string | undefined;
|
|
9
|
+
/** `style` for the sized box around `image` / `markup` icons. */
|
|
10
|
+
export declare const boxStyle: (size: number | string, color: string | undefined) => string;
|
|
11
|
+
/** `style` for text-like icons (emoji, icon fonts). */
|
|
12
|
+
export declare const textStyle: (size: number | string, color: string | undefined) => string;
|
|
13
|
+
/** Utility-class colour first, then the consumer's class. Plain concatenation, no conflict resolution. */
|
|
14
|
+
export declare const classes: (color: string | undefined, extra: string | undefined, base?: string) => string | undefined;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A CSS colour value: hex, a keyword (`red`, `currentColor`) or a colour function (`rgb()`, `oklch()`, `var()`...).
|
|
3
|
+
* Anything else (`text-primary`, `text-red-500 dark:text-red-300`, `text-(--brand)`) is a utility class.
|
|
4
|
+
* Strict on purpose: the value ends up in a `style` attribute, so `;`, braces and quotes never qualify.
|
|
5
|
+
*/
|
|
6
|
+
const CSS_COLOR = /^(?:#[\da-f]{3,8}|[a-z]+|[a-z]+(?:-[a-z]+)*\([^;{}"'<>]*\))$/i;
|
|
7
|
+
export const isCssColor = (color) => color !== undefined && CSS_COLOR.test(color);
|
|
8
|
+
/** A plain CSS length. Anything else (e.g. `24px;background:url(x)`) never reaches a `style` attribute. */
|
|
9
|
+
const CSS_LENGTH = /^(?:\d+|\d*\.\d+)(?:px|r?em|ch|ex|vw|vh|vmin|vmax|%|pt|cm|mm|in)$/i;
|
|
10
|
+
/**
|
|
11
|
+
* Final icon size. Numbers must be finite and positive and get the per-source `scale` (rounded to avoid
|
|
12
|
+
* blurry sub-pixel rendering); strings must be a plain CSS length. Invalid input yields `fallback`.
|
|
13
|
+
*/
|
|
14
|
+
export const boxSize = (size, scale, fallback) => {
|
|
15
|
+
if (typeof size === "number") {
|
|
16
|
+
if (!(size > 0) || !Number.isFinite(size))
|
|
17
|
+
return fallback;
|
|
18
|
+
return typeof scale === "number" && scale > 0 && Number.isFinite(scale) ? Math.round(size * scale) || 1 : size;
|
|
19
|
+
}
|
|
20
|
+
return CSS_LENGTH.test(size) ? size : fallback;
|
|
21
|
+
};
|
|
22
|
+
const cssLength = (size) => (typeof size === "number" ? `${size}px` : size);
|
|
23
|
+
const colorDecl = (color) => (isCssColor(color) ? `;color:${color}` : "");
|
|
24
|
+
/** `style` for icon components: only a CSS colour needs it. */
|
|
25
|
+
export const colorStyle = (color) => isCssColor(color) ? `color:${color}` : undefined;
|
|
26
|
+
/** `style` for the sized box around `image` / `markup` icons. */
|
|
27
|
+
export const boxStyle = (size, color) => {
|
|
28
|
+
const length = cssLength(size);
|
|
29
|
+
return `width:${length};height:${length}${colorDecl(color)}`;
|
|
30
|
+
};
|
|
31
|
+
/** `style` for text-like icons (emoji, icon fonts). */
|
|
32
|
+
export const textStyle = (size, color) => `font-size:${cssLength(size)}${colorDecl(color)}`;
|
|
33
|
+
/** Utility-class colour first, then the consumer's class. Plain concatenation, no conflict resolution. */
|
|
34
|
+
export const classes = (color, extra, base) => {
|
|
35
|
+
let out = base;
|
|
36
|
+
if (color && !isCssColor(color))
|
|
37
|
+
out = out ? `${out} ${color}` : color;
|
|
38
|
+
if (extra)
|
|
39
|
+
out = out ? `${out} ${extra}` : extra;
|
|
40
|
+
return out;
|
|
41
|
+
};
|
package/llms-full.txt
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @dynamic-icon/svelte and @dynamic-icon/core: full documentation
|
|
2
|
+
|
|
3
|
+
Two packages. `@dynamic-icon/core` is a framework-free registry (pure TypeScript, zero dependencies). `@dynamic-icon/svelte`
|
|
4
|
+
is the Svelte 5 component plus typed re-exports. Installing the Svelte package pulls in core.
|
|
5
|
+
|
|
6
|
+
## 1. Purpose
|
|
7
|
+
|
|
8
|
+
Render icons whose identity is stored in a database record: `{ webType: 'lucide', webRef: 'car-front', color: 'text-primary' }`.
|
|
9
|
+
The package resolves `webType` + `webRef` to a component (or renders an image, emoji, icon-font `<i>`, or sanitized markup).
|
|
10
|
+
Designed for components that render ~1,000 icons at once.
|
|
11
|
+
|
|
12
|
+
## 2. Quick start
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// src/hooks.client.ts and src/hooks.server.ts (or one shared module imported by both): once, module scope
|
|
16
|
+
import { configureIcons } from '@dynamic-icon/svelte';
|
|
17
|
+
import CarFront from 'lucide-svelte/icons/car-front';
|
|
18
|
+
import House from 'lucide-svelte/icons/house';
|
|
19
|
+
import CircleHelp from 'lucide-svelte/icons/circle-help';
|
|
20
|
+
|
|
21
|
+
configureIcons({
|
|
22
|
+
sources: { lucide: { 'car-front': CarFront, house: House } },
|
|
23
|
+
fallback: CircleHelp
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```svelte
|
|
28
|
+
<script lang="ts">
|
|
29
|
+
import { DynamicIcon } from '@dynamic-icon/svelte';
|
|
30
|
+
let { icon } = $props(); // { webType, webRef, color?, media?, scale? } from the database
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<DynamicIcon {...icon} size={20} class="mr-2" />
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 3. API
|
|
37
|
+
|
|
38
|
+
### `configureIcons(config: IconConfig): void` (from `@dynamic-icon/svelte`; generic `configureIcons<C>` in core)
|
|
39
|
+
|
|
40
|
+
| Field | Type | Default | Meaning |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| `sources` | `Record<webType, Record<ref, Component> \| (ref) => Component \| undefined>` | `{}` | Component sources by `webType`. Names `image`, `emoji`, `markup` are reserved and throw. |
|
|
43
|
+
| `fontTypes` | `string[]` | `['material','font-awesome','remix']` | `webType`s rendered as `<i class={webRef}>`. Must not collide with sources or reserved names. |
|
|
44
|
+
| `sanitize` | `(html: string) => string` | none | Required for `markup`. Without it, `markup` icons show the fallback. |
|
|
45
|
+
| `fallback` | `Component` | none | Rendered when nothing resolves (unknown icon, empty record, failed source). A per-instance `fallback` snippet wins. |
|
|
46
|
+
| `defaults` | `{ size?: number; strokeWidth?: number }` | `{ size: 24, strokeWidth: 2 }` | Must be positive finite numbers or it throws. |
|
|
47
|
+
| `onError` | `(error: unknown) => void` | `console.error` | Receives errors thrown by function sources or `sanitize`. |
|
|
48
|
+
|
|
49
|
+
Semantics: validates eagerly; on invalid input it throws and the previous configuration stays active; on success it replaces the
|
|
50
|
+
whole configuration (idempotent). Record sources are held by reference, never copied (lookups use `Object.hasOwn`, so
|
|
51
|
+
`constructor`/`__proto__` never resolve); treat them as read-only, since later mutations are visible. Function sources are used as-is (memoise them yourself if expensive).
|
|
52
|
+
|
|
53
|
+
### `resolveIcon(webType, webRef): Component | undefined`
|
|
54
|
+
Same lookup the component uses, for use outside components. Never throws.
|
|
55
|
+
|
|
56
|
+
### `<DynamicIcon />` props
|
|
57
|
+
|
|
58
|
+
| Prop | Type | Notes |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `webType` | `string` | Which renderer/source. |
|
|
61
|
+
| `webRef` | `string` | Identifier within the source; class string for font types; the emoji; or raw markup. |
|
|
62
|
+
| `color` | `string` | CSS colour (hex, keyword, `rgb()/hsl()/oklch()/var()/color-mix()`) -> inline `color:`; anything else -> appended to `class`. |
|
|
63
|
+
| `media` | `{ link: string; caption?: string }` | For `image`. `caption` becomes `alt`. Rendered `loading="lazy" decoding="async"`. |
|
|
64
|
+
| `size` | `number \| string` | px if number; plain CSS length if string. Invalid (NaN, <=0, `24px;x:y`, `calc()`) uses `defaults.size`. |
|
|
65
|
+
| `scale` | `number` | Multiplies numeric `size`, rounded (min 1). Ignored for strings or non-positive/non-finite values. |
|
|
66
|
+
| `strokeWidth` | `number` | Passed to icon components. |
|
|
67
|
+
| `class` | `string` | Plain string (not clsx arrays). |
|
|
68
|
+
| `fallback` | `Snippet` | Per-instance fallback. |
|
|
69
|
+
|
|
70
|
+
Extra record fields such as `name` are accepted and ignored, so `{...record}` spreading works.
|
|
71
|
+
|
|
72
|
+
### Contract for icon components
|
|
73
|
+
They receive `size`, `strokeWidth`, `class`, `style` (only when `color` is a CSS colour) and `aria-hidden="true"`. `lucide-svelte` icons
|
|
74
|
+
accept these. Custom icons should spread rest props onto their root `<svg>`. Exported type `IconComponent` is intentionally
|
|
75
|
+
`Component<any, any, any> | ComponentType<SvelteComponent<any, any, any>>` because library icons never match one exact prop type.
|
|
76
|
+
|
|
77
|
+
## 4. Resolution order (per instance)
|
|
78
|
+
1. A configured source resolves `webType`/`webRef` -> render that component (no wrapper).
|
|
79
|
+
2. `image` with `media.link` -> sized wrapper `<span>` + `<img>`.
|
|
80
|
+
3. `markup` with `webRef` and a configured `sanitize` -> sized wrapper `<span>` with `{@html sanitize(webRef)}`.
|
|
81
|
+
4. `emoji` -> `<span style="font-size">`.
|
|
82
|
+
5. Font type -> `<i class="{webRef} ...">`.
|
|
83
|
+
6. Else: per-instance `fallback` snippet, else `configureIcons({ fallback })`, else nothing.
|
|
84
|
+
|
|
85
|
+
## 5. Security model
|
|
86
|
+
- DB-controlled strings reach the DOM only as: escaped text, class tokens, `<img src>`, a validated CSS colour, a validated CSS length, and (for `markup`) sanitizer output.
|
|
87
|
+
- `color` is only placed in `style` if it matches a strict colour grammar (no `;`, braces, quotes); otherwise it is a class token, which cannot inject declarations.
|
|
88
|
+
- `size` strings must match a plain CSS length; numbers must be finite and > 0.
|
|
89
|
+
- `markup` is never rendered unsanitized. Provide a real sanitizer (e.g. DOMPurify on the client, an SSR-capable one on the server); an identity function is unsafe.
|
|
90
|
+
- `media.link` is not scheme-filtered; validate image URLs upstream if they are user-supplied.
|
|
91
|
+
- Registry keys are looked up with `Object.hasOwn`: prototype keys cannot resolve.
|
|
92
|
+
- A throwing function source or sanitizer cannot crash the page: it is reported to `onError` and the icon renders as unresolved.
|
|
93
|
+
|
|
94
|
+
## 6. SSR rules
|
|
95
|
+
- The configuration is app-wide data, identical for all requests, so a process-wide singleton is safe. Never mutate a source record per request (it is shared by reference). Per-request or per-tenant icon sets are not supported; use a function source that reads your own request-scoped state only if you accept that trade-off.
|
|
96
|
+
- Call `configureIcons` once at module scope, on both server and client, with the same config (otherwise hydration markup can differ). Do not call it inside components or per request.
|
|
97
|
+
- Components read the config once at instance creation; configure before the first render.
|
|
98
|
+
- Nothing touches `window`/`document` at import or render time.
|
|
99
|
+
|
|
100
|
+
## 7. Performance notes
|
|
101
|
+
- Per icon: one registry lookup (`Object.hasOwn` + index), one `$derived`, plain string building. No `tailwind-merge`/`clsx`, no string case conversion.
|
|
102
|
+
- Bundle: the package imports no icon library. Only icons you list in `sources` ship. Use per-icon imports; never `import * as Icons`.
|
|
103
|
+
- Measured (Node, 1,000 icons): `Map.get` ~16-19 ns, `Object.hasOwn` + index ~23 ns, `globalThis[Symbol]` ~5 ns. Sources are held by reference, so the registry adds no copy of your icon data; the difference between strategies is a few ns per icon, negligible next to component instantiation.
|
|
104
|
+
- Only `image`/`markup` add a wrapper element. Component icons render a single element.
|
|
105
|
+
- Wrapper styles are scoped CSS in the component (Tailwind does not scan `node_modules`).
|
|
106
|
+
|
|
107
|
+
## 8. Architecture
|
|
108
|
+
- `@dynamic-icon/core`: pure TypeScript (`lib: ES2022`, no DOM, no deps). `configureIcons`, `resolveIcon`, `getIconConfig`, generic over the component type `C`. State: one holder object at `globalThis[Symbol.for('@dynamic-icon/core@1')]`, captured per module so reads are plain property access; duplicate installs share it; the `@1` is the holder format version.
|
|
109
|
+
- `@dynamic-icon/svelte`: `DynamicIcon.svelte`, typed wrappers fixing `C = IconComponent`, small pure helpers (`utils.ts`). Ships `.svelte` source via `svelte-package`.
|
|
110
|
+
- Store choice: a global holder (not Svelte context) with sources held by reference. Context would add a `getContext` per instance, only works during component init, needs a provider around every consumer (including other libraries' components and separately mounted roots), and buys only per-request isolation that an app-wide icon set does not need. Why not Svelte context: the registry is app-wide config; context adds a lookup per instance and only works during component init. Why not per-instance props: 1,000 icons would each carry the config.
|
|
111
|
+
- Adding another framework: create `@dynamic-icon/<framework>` with a component calling `getIconConfig<Component>()`; reuse core and `utils` logic.
|
|
112
|
+
|
|
113
|
+
## 9. Common mistakes
|
|
114
|
+
- `import * as Icons from 'lucide-svelte'` as a source: ships every icon. Register individual icons.
|
|
115
|
+
- Using PascalCase keys with kebab-case DB values: use a function source (`(ref) => map[toPascal(ref)]`) or kebab-case keys.
|
|
116
|
+
- Calling `configureIcons` in a component or per request.
|
|
117
|
+
- Expecting `markup` to render without `sanitize`.
|
|
118
|
+
- Passing `size="24px;..."` or `size="calc(...)"`: falls back to the default size.
|
|
119
|
+
- Expecting Tailwind conflict resolution between `color` and `class`: classes are concatenated, not merged.
|
|
120
|
+
- Using `webType: 'image'|'emoji'|'markup'` as a source name: throws at configure time.
|
|
121
|
+
|
|
122
|
+
## 10. Types
|
|
123
|
+
`IconEntry<T extends string = string>` { name?, webType?, webRef?, color?, media?, scale? }; `IconProps<T>` adds { size?, class?, strokeWidth?, fallback? };
|
|
124
|
+
`IconMedia` { link, caption? }; `IconConfig`, `IconSource`, `IconComponent`, `IconComponentProps` exported from `@dynamic-icon/svelte`.
|
|
125
|
+
Core exports `IconConfig<C>`, `IconSource<C>`, `ResolvedIconConfig<C>`.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @dynamic-icon/svelte
|
|
2
|
+
|
|
3
|
+
> Svelte 5 component that renders icons from database records (`{ webType: 'lucide', webRef: 'car-front' }`). Icon-library agnostic and pluggable: the app registers icon sources once with `configureIcons()`. Built for pages rendering hundreds or thousands of icons: one registry lookup and one derived value per icon, no class-merging library, no icon library imported by the package.
|
|
4
|
+
|
|
5
|
+
Key facts for code generation:
|
|
6
|
+
- Install `@dynamic-icon/svelte` (peer: `svelte@^5`). It depends on `@dynamic-icon/core`, a zero-dependency registry; do not install core separately.
|
|
7
|
+
- Call `configureIcons({ sources, fallback?, defaults?, sanitize?, fontTypes?, onError? })` exactly once, at module scope of the app root, with identical config on server and client. Never inside a component or per request.
|
|
8
|
+
- Register only the icons you need, with per-icon imports (`import CarFront from 'lucide-svelte/icons/car-front'`), keyed by the identifier stored in the database. Never `import * as` a whole library: it defeats tree-shaking.
|
|
9
|
+
- Render with `<DynamicIcon {...record.icon} size={24} class="text-primary" />`.
|
|
10
|
+
- Libraries that render `DynamicIcon` need no setup; only the host app configures.
|
|
11
|
+
- Built-in `webType`s: `image` (uses `media.link`), `emoji`, `markup` (needs `sanitize`), and icon fonts `material`, `font-awesome`, `remix` (`<i class={webRef}>`). Any other `webType` must be a configured source.
|
|
12
|
+
|
|
13
|
+
## Docs
|
|
14
|
+
|
|
15
|
+
- [Full documentation](https://unpkg.com/@dynamic-icon/svelte/llms-full.txt): API, configuration, resolution order, security model, SSR rules, performance notes, architecture
|
|
16
|
+
- [Core registry](https://unpkg.com/@dynamic-icon/core/llms.txt): the framework-free registry used underneath
|
|
17
|
+
- [README](https://unpkg.com/@dynamic-icon/svelte/README.md): human-oriented quick start
|
|
18
|
+
|
|
19
|
+
## Optional
|
|
20
|
+
|
|
21
|
+
- Same files via jsDelivr: https://cdn.jsdelivr.net/npm/@dynamic-icon/svelte/llms-full.txt
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamic-icon/svelte",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Tiny Svelte 5 component that renders database-driven icons. Sources are plugged in via @dynamic-icon/core.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"**/*.css"
|
|
9
|
+
],
|
|
10
|
+
"svelte": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"svelte": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./llms.txt": "./llms.txt",
|
|
19
|
+
"./llms-full.txt": "./llms-full.txt",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"llms.txt",
|
|
27
|
+
"llms-full.txt"
|
|
28
|
+
],
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"svelte": "^5.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@sveltejs/package": "^2.3.0",
|
|
34
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"jsdom": "^25.0.0",
|
|
37
|
+
"publint": "^0.2.12",
|
|
38
|
+
"svelte": "^5.0.0",
|
|
39
|
+
"svelte-check": "^4.0.0",
|
|
40
|
+
"typescript": "^5.6.0",
|
|
41
|
+
"vite": "^5.4.0",
|
|
42
|
+
"vitest": "^2.1.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@dynamic-icon/core": "^0.1.1"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "svelte-package -i src -o dist",
|
|
55
|
+
"prebuild": "pnpm --filter @dynamic-icon/core run build",
|
|
56
|
+
"typecheck": "svelte-check --tsconfig ./tsconfig.json",
|
|
57
|
+
"pretypecheck": "pnpm --filter @dynamic-icon/core run build",
|
|
58
|
+
"typecheck:test": "svelte-check --tsconfig ./tsconfig.test.json",
|
|
59
|
+
"pretypecheck:test": "pnpm --filter @dynamic-icon/core run build",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest",
|
|
62
|
+
"lint:package": "publint"
|
|
63
|
+
}
|
|
6
64
|
}
|