# @aweftjs/icons

Icon sets as modules. Name an icon in your source and the build puts that one icon in your page;
name one when the page runs and a resolver fetches it. This package holds no drawings of its own
and never will.

```
npm install @iconify-json/lucide
```

The sets are optional peers, so nothing is installed until you pick one. Any published set works:
`@iconify-json/lucide`, `@iconify-json/mdi`, `@iconify-json/tabler`, `@iconify-json/svg-spinners`.

## Naming an icon in your source

```tsx
import { Icon } from '@aweftjs/ui';

<Icon name="lucide:check" label="done" />
```

`@aweftjs/build` rewrites that literal into an import of that one icon, a few hundred bytes (over
Lucide: median 325, largest 978), and `@aweftjs/icons` generates the module it imports. Nothing
else in the set reaches your page.

The rewrite fires for a string literal shaped `set:name`, on the `name` prop of the `Icon` your
file bound from `@aweftjs/ui`, written as JSX or as an `h(Icon, { ... })` call. Nothing else moves:
a name with no colon in it, a value that is not a string literal, a spread that could carry its own
`name`, and an `Icon` bound from anywhere else all come out as they went in and are looked up
through `Icons` when the page runs.

You can write the import yourself, and it is the same module:

```tsx
import check from '@aweftjs/icons/lucide/check';

<Icon name={check} />
```

Three subpaths exist, and all three are generated from the set you installed:

| import | what you get |
|---|---|
| `@aweftjs/icons/lucide/check` | one icon, as `IconData` |
| `@aweftjs/icons/lucide/+standard` | the names `ui`'s components ask for, as an `IconPack` |
| `@aweftjs/icons/lucide` | the whole set as an `IconPack`, about 588 KB of it |

The `+` in `+standard` is deliberate: no set can publish a name with one in it, so that path can
never be an icon you wanted.

They are not files, so they need `aweft()` in your bundler or the Node loader:

```ts
import { aweft } from '@aweftjs/build';
export default { plugins: [aweft()] };
```

and `node --import @aweftjs/build/loader` where you render a page without a browser. That is the
same plugin and the same loader that compile your JSX; there is nothing extra to register.

A set named `node` could not be reached this way, because `@aweftjs/icons/node` is the generator's
own subpath. No published set has that name.

## The names your components ask for

Everything in `@aweftjs/ui` that shows an icon asks for a name, never a drawing:
`chevron-down`, `chevron-up`, `chevron-left`, `chevron-right`, `check`, `x`, `triangle-alert`,
`search`, `upload`. The list is `standardIcons`, exported by `ui`.

```tsx
import { Icons } from '@aweftjs/ui';
import standard from '@aweftjs/icons/lucide/+standard';

<Icons value={standard}><App /></Icons>
```

That costs about ten icons rather than a set. To give one of those names a drawing of your own,
put a pack in front:

```tsx
<Icons value={[{ icons: { 'chevron-left': myOwnChevron } }, standard]}><App /></Icons>
```

The nearest source wins, so your pack answers and nothing else changes.

## A spinner that moves

`@aweftjs/ui` ships no drawings and its default loader is three pulsing spans. For a drawn one,
`svg-spinners` is a set like any other and its icons carry their own motion:

```
npm install @iconify-json/svg-spinners
```

```tsx
import { Icon, LoaderContext } from '@aweftjs/ui';
import spinner from '@aweftjs/icons/svg-spinners/3-dots-fade';

<LoaderContext value={{ loading: () => <Icon name={spinner} size="1.25em" /> }}><App /></LoaderContext>
```

That one line covers every wait in `ui`: a `suspend`, a `Button` running a promise, and a
`FileDrop` entry being uploaded.

Nothing special happens for an animated set. `Icon` writes an icon's body into the element as
markup, so the `<animate>` elements inside these drawings are ordinary SVG animation the moment the
page has them, on a fresh mount and on markup a server wrote. That is checked in `ui`'s own suite,
"an animated body is written through as it is".

## A name you only have when the page runs

```tsx
import { fromUrl } from '@aweftjs/icons';

<Icons value={[standard, fromUrl('https://api.iconify.design')]}><App /></Icons>
```

`fromUrl(base)` fetches `<base>/<set>.json?icons=<name>` and reads the answer the public icon APIs
give. Point it at a route of your own to serve the icons yourself;
[`recipes/icons/main.ts`](/docs/recipes/icons/files/main.ts) has
that route in about fifteen lines, built out of an installed set.

Nothing installs it. A page that never adds it makes no requests, because a fetch to somebody
else's service is a decision you make and not one a library makes for you.

It answers null for a name with no set in it, so `check` keeps being answered locally. A page
whose server fetched an icon has to hand the client what the server got, as a pack: a hydration
waits for nothing, so a resolver alone leaves the client one drawing short of the markup it is
taking over.

A drawing that arrives this way is somebody else's markup, and `Icon` writes it into the page.
So a body that can run or reach out is refused with `unsafe-body`, naming the icon: one
carrying `<script`, an event attribute such as `onload=`, `<foreignObject`, a `javascript:`
URL, or an `href` that does not begin with `#`. A `<use href="#id">` and a `fill="url(#id)"`
pass. The refusal is thrown, not answered as null, so the page hears which source refused and
why rather than falling through to the next. An installed set is read at build time and is not
read for this.

## When something is missing

A set you have not installed:

```
set-not-installed: the icon set "tabler" is not installed; run: npm install @iconify-json/tabler.
Install the icon set the message names, or hand a pack of your own to Icons.
```

A name the set does not have is `icon-not-in-set`, naming both. Both happen while the page is
being built, not while it is running, which is the point of naming an icon in your source.

A name nothing in your `Icons` stack answers is a loud assert from `ui` in development, and
nothing at all in a release build. The full list of refusals is `errors.txt`.

## What this package never decides

Which sets you install. Whether your page fetches anything. What an icon looks like. It ships no
icon data, and nothing in the gate reads this package for drawings. `npm run dependencies` at the repo root checks something else, that no icon set is a
dependency rather than an optional peer.

## API

Every export of `@aweftjs/icons`, its signature as the compiler resolves it, and its block comment.

### `@aweftjs/icons`

#### `IconSet`

```ts
interface IconSet { readonly prefix?: string; readonly icons?: Readonly<Record<string, SetIcon>>; readonly aliases?: Readonly<Record<string, SetAlias>>; readonly width?: number; readonly height?: number; }
```

A whole set, or the part of one an API answered with.

#### `SetAlias`

```ts
interface SetAlias { readonly parent: string; readonly rotate?: number; readonly hFlip?: boolean; readonly vFlip?: boolean; }
```

One name pointing at another in the same set, with its own turns and flips on top.

#### `SetIcon`

```ts
interface SetIcon { readonly body: string; readonly width?: number; readonly height?: number; readonly left?: number; readonly top?: number; readonly rotate?: number; readonly hFlip?: boolean; readonly vFlip?: boolean; }
```

One icon as a set publishes it: the drawing, and its box only where it differs from the set's.

#### `fromUrl`

```ts
fromUrl: (base: string) => (name: string) => Promise<IconData | null>
```

A resolver that fetches one icon at a time from an icon API.

Nothing installs this. Add it to `Icons` where you want it, and a page that does not stays a
page that makes no requests.

The request is `<base>/<set>.json?icons=<name>` and the answer is
`{ prefix, icons: { <name>: data }, aliases?, width?, height? }`. A root size in the answer
applies to an icon that carries none, and an alias is followed once.

**Params**

- `base`: where the API lives, with no trailing slash, such as `https://api.iconify.design`

**Returns** a resolver `Icons` takes. It answers null for a name with no set in it, for a set that does not know the name, and for an answer that is not the shape above, whether it fails to parse at all or parses to something else, so the next source in the stack is asked. A request that fails to reach the far end rejects, and `Icon` reports that naming the icon and the reason.

**Throws** `unsafe-body` when the drawing carries a `<script`, an event attribute, a `<foreignObject`, a `javascript:` URL, or an `href` that does not begin with `#`. The next source is not asked: the page hears which source refused and why.

**Example**

```ts
<Icons value={[myPack, fromUrl('https://api.iconify.design')]}><App /></Icons>
```

### `@aweftjs/icons/node`

#### `moduleFor`

```ts
moduleFor: (request: string, from: string) => string | null
```

The source of the module one `@aweftjs/icons` import names.

Three requests are answered, and they are the three subpaths an application writes:

| request | the module |
|---|---|
| `lucide` | the whole installed set as an `IconPack`, with its root size |
| `lucide/check` | one icon as `IconData`, an alias followed once, the set's box applied |
| `lucide/+standard` | the names in `ui`'s `standardIcons` that this set has, as an `IconPack` |

A name the set lacks is left out of the standard selection rather than refused, because the
application answers it with a pack of its own in front.

**Params**

- `request`: what follows `@aweftjs/icons/` in the import
- `from`: the directory to resolve `@iconify-json/<set>` from, usually the directory of the file that wrote the import

**Returns** the module's source, `export default { ... }`, or null when the request is none of the three, which leaves the import to ordinary module resolution. A set name and an icon name are the sets' own grammar, lowercase letters and digits with single dashes between them, and `+standard` is the one exception; anything else is not a request.

**Throws** a refusal with reason `set-not-installed` when `@iconify-json/<set>` does not resolve from `from`, whose message carries the install command; `icon-not-in-set` when the set has no icon under the name asked for.

**Example**

```ts
const source = moduleFor('lucide/check', dirname(importer));
```

## Refusals

Every error this package raises carries a `reason` to switch on and a `fix` that says what to do. These are its reasons, from `errors.txt`.

| reason | fix |
|---|---|
| `icon-not-in-set` | Name an icon the set publishes, or install the set that has it. |
| `set-not-installed` | Install the icon set the message names, or hand a pack of your own to Icons. |
| `unsafe-body` | Serve icons from a source you trust, or take that icon out of the set it came from. |

## Recipes

The programs in the stack's gate that use this package, each a job someone would have.

- [`recipes/icons`](/docs/recipes/icons): Icons named three ways, and what each way puts in the bundle
