@weft-js/warp-icons 0.1.1 → 0.1.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/src/icon.tsx CHANGED
@@ -1,6 +1,11 @@
1
1
  // `<Icon name>`. Literal names compile to the icon itself; what reaches this file is a name the build could only bound,
2
2
  // or not bound at all, and @weft-js/warp-icons/names holds exactly those.
3
- import { resolve, type IconActionName, type IconName, type IconVariant } from '@weft-js/warp-icons/names'
3
+ import {
4
+ resolve,
5
+ type IconActionName,
6
+ type IconName,
7
+ type IconVariant,
8
+ } from '@weft-js/warp-icons/names'
4
9
  import { untrack } from '@weft-js/core'
5
10
  import type { IconTrigger } from './action'
6
11
  import type { IconComponent, IconProps } from './svg'
@@ -9,10 +14,18 @@ export type { IconName, IconVariant }
9
14
  /** An icon by name, or the icon itself. */
10
15
  export type IconRef = IconName | IconComponent
11
16
  /** `<Icon>`'s props: a name, and `solid` or `action` with a name that has one. */
12
- export type IconByNameProps = ({ name: IconRef; solid?: boolean; action?: undefined } | { name: IconActionName; action: IconTrigger | boolean; solid?: undefined }) & IconProps
17
+ export type IconByNameProps = (
18
+ | { name: IconRef; solid?: boolean; action?: undefined }
19
+ | { name: IconActionName; action: IconTrigger | boolean; solid?: undefined }
20
+ ) &
21
+ IconProps
13
22
 
14
23
  /** An icon by name. A literal name compiles to the icon itself; anything else is looked up at runtime. */
15
24
  export function Icon({ name, solid, ...rest }: IconByNameProps) {
16
- const Draw = untrack(() => (typeof name === 'function' ? name : resolve(name, rest.action ? 'action' : solid ? 'solid' : undefined)))
25
+ const Draw = untrack(() =>
26
+ typeof name === 'function'
27
+ ? name
28
+ : resolve(name, rest.action ? 'action' : solid ? 'solid' : undefined),
29
+ )
17
30
  return Draw ? <Draw {...rest} /> : null
18
31
  }
package/src/icons.css CHANGED
@@ -1,5 +1,6 @@
1
1
  /* Every @weft-js/warp-icons svg, with or without warp. warp's own base rule says the same; this one lets the set stand alone. */
2
2
  /* warp's layer order, first: a layer ranks by when the page first names it, and this file can load before warp.css. */
3
+ @layer warp, app, theme, base, components, utilities;
3
4
  @layer warp.tokens, warp.base, warp.signature, warp.layout, warp.components, warp.inputs, warp.inputs-rich, warp.data, warp.app, warp.flows, warp.marketing, warp.canvas, warp.flow;
4
5
  @layer warp.components {
5
6
  :where(svg[data-icon]) { inline-size: var(--warp-icon-md, 1em); block-size: var(--warp-icon-md, 1em); flex: none; fill: none; stroke: currentColor; stroke-width: var(--warp-icon-stroke, 1.5); stroke-linecap: round; stroke-linejoin: round; }
@@ -7,23 +8,35 @@
7
8
  :where(svg[data-icon] [data-qs]) { stroke-width: 1; }
8
9
  :where(svg[data-icon] [data-bs]) { stroke-width: 2; }
9
10
  :where(svg[data-icon] [data-bw]) { stroke-width: 2.5; }
11
+ /* hollow's inside mark: lighter than the body round it */
12
+ :where(svg[data-icon] [data-bi]) { stroke-width: 1.5; }
10
13
  /* the weft, the stroke that says what the icon means: colour it with --warp-icon-weft-color */
11
14
  :where(svg[data-icon] [data-w]) { stroke: var(--warp-icon-weft-color, currentColor); }
12
15
  /* pixels: a 16 grid in the 24 box. In forced colours currentColor is the system's, so these follow it. */
13
16
  :where(svg[data-icon] [data-p], svg[data-icon] [data-c]) { stroke: none; transform: scale(1.5); shape-rendering: crispEdges; }
14
17
  :where(svg[data-icon] [data-p]) { fill: currentColor; }
15
18
  :where(svg[data-icon] [data-p][data-w]) { fill: var(--warp-icon-weft-color, currentColor); }
16
- /* An icon picks its form from its own size (10 §6). Dither is depth, and at 16 px it reads as noise, so it needs 20. */
17
- :where(svg[data-icon]) { container-type: size; }
19
+ /* An icon picks its form from its own size (10 §6). Dither is depth, and at 16 px it reads as noise, so it needs 20.
20
+ Only an icon that has dither measures itself: a size container is a layout of its own, and a list of 200 rows with
21
+ an icon each spent 70 ms (4x CPU) laying them out. */
22
+ :where(svg[data-icon][data-dither]) { container-type: size; }
23
+ /* A hollow mark measures itself too: under 20 px its gap widens (forms.mjs), or it would close up and read as solid. */
24
+ :where(svg[data-icon]:has(> mask)) { container-type: size; }
25
+ :where(svg[data-icon] [data-m]) { display: none; }
26
+ @container (max-width: 19.5px) { :where(svg[data-icon] [data-m]) { display: inline; } }
18
27
  :where(svg[data-icon] [data-d]) { opacity: 0; }
19
28
  @container (min-width: 20px) { :where(svg[data-icon] [data-d]) { opacity: 1; } }
20
29
  /* solid: a filled body, its offset shade, and what's cut from it */
21
30
  :where(svg[data-icon] [data-s]) { fill: currentColor; stroke: none; }
22
31
  :where(svg[data-icon] [data-sh]) { stroke: currentColor; stroke-width: 1.5; }
32
+ /* an open mark's closed shapes, filled in solid */
33
+ :where(svg[data-icon] [data-f]) { fill: currentColor; }
34
+ :where(svg[data-icon] [data-f][data-w]) { fill: var(--warp-icon-weft-color, currentColor); }
23
35
  :where(svg[data-icon] [data-so]) { fill: currentColor; fill-opacity: .3; stroke: none; }
24
36
  :where(svg[data-icon] [data-k]) { stroke: var(--warp-icon-knock, Canvas); stroke-width: 2.5; }
25
37
  :where(svg[data-icon] [data-k][data-qs]) { stroke-width: 1.5; }
26
38
  :where(svg[data-icon] [data-hi]) { stroke-width: 1.25; }
27
39
  :where(svg[data-icon] [data-c]) { fill: var(--warp-icon-knock, Canvas); }
28
- @media (forced-colors: active) { :where(svg[data-icon] [data-so]) { display: none; } }
40
+ :where(svg[data-icon] [data-sx]) { stroke-opacity: .3; }
41
+ @media (forced-colors: active) { :where(svg[data-icon] [data-so], svg[data-icon] [data-sx]) { display: none; } }
29
42
  }
package/src/late.tsx CHANGED
@@ -4,7 +4,10 @@ import { state } from '@weft-js/core'
4
4
  import type { IconComponent, IconProps } from './svg'
5
5
  import { icon, Svg } from './svg'
6
6
 
7
- type Lookup = (name: string, variant?: string) => IconComponent | undefined | Promise<IconComponent | undefined>
7
+ type Lookup = (
8
+ name: string,
9
+ variant?: string,
10
+ ) => IconComponent | undefined | Promise<IconComponent | undefined>
8
11
  const held = new Map<string, IconComponent>()
9
12
 
10
13
  function deferred(key: string, find: () => Promise<IconComponent | undefined>): IconComponent {
@@ -12,9 +15,33 @@ function deferred(key: string, find: () => Promise<IconComponent | undefined>):
12
15
  if (made) return made
13
16
  let got: Promise<IconComponent | undefined> | undefined
14
17
  made = (props: IconProps) => {
15
- const ready = state<IconComponent | null>(null), failed = state(false)
16
- if (typeof window !== 'undefined') (got ??= find()).then(i => { if (i) ready.value = i; else failed.value = true }, () => { got = undefined; failed.value = true })
17
- return <>{ready.value ? ready.value(props) : <Svg {...props} data-loading={failed.value ? undefined : ''} data-failed={failed.value ? '' : undefined} html="" />}</>
18
+ const ready = state<IconComponent | null>(null),
19
+ failed = state(false)
20
+ if (typeof window !== 'undefined')
21
+ (got ??= find()).then(
22
+ (i) => {
23
+ if (i) ready.value = i
24
+ else failed.value = true
25
+ },
26
+ () => {
27
+ got = undefined
28
+ failed.value = true
29
+ },
30
+ )
31
+ return (
32
+ <>
33
+ {ready.value ? (
34
+ ready.value(props)
35
+ ) : (
36
+ <Svg
37
+ {...props}
38
+ data-loading={failed.value ? undefined : ''}
39
+ data-failed={failed.value ? '' : undefined}
40
+ html=""
41
+ />
42
+ )}
43
+ </>
44
+ )
18
45
  }
19
46
  held.set(key, made)
20
47
  return made
@@ -22,15 +49,22 @@ function deferred(key: string, find: () => Promise<IconComponent | undefined>):
22
49
 
23
50
  let loaded: Promise<Lookup> | undefined
24
51
  /** From the pack, a chunk of its own: the first use on a page fetches it. */
25
- export const late = (name: string, variant: string | undefined, pack: () => Promise<Lookup>) => deferred(`${name}:${variant ?? ''}`, async () => (await (loaded ??= pack()))(name, variant))
52
+ export const late = (name: string, variant: string | undefined, pack: () => Promise<Lookup>) =>
53
+ deferred(`${name}:${variant ?? ''}`, async () => (await (loaded ??= pack()))(name, variant))
26
54
 
27
55
  // Only paths go in, with their d and data attributes: what a remote host sends is data, never markup that can run.
28
- const clean = (svg: string) => (svg.match(/<path(?:\s+data-[a-z]+(?:="[^"<>]*")?|\s+d="[^"<>]*"|\s+transform="translate\([\d. ]+\)")*\s*\/>/g) ?? []).join('')
56
+ const clean = (svg: string) =>
57
+ (
58
+ svg.match(
59
+ /<path(?:\s+data-[a-z]+(?:="[^"<>]*")?|\s+d="[^"<>]*"|\s+transform="translate\([\d. ]+\)")*\s*\/>/g,
60
+ ) ?? []
61
+ ).join('')
29
62
 
30
63
  /** From a host of static files (`weft icons publish`): one small fetch per icon. */
31
- export const remote = (source: string) => (name: string, variant?: string) => deferred(`${name}:${variant ?? ''}`, async () => {
32
- const response = await fetch(`${source}${name}${variant ? `.${variant}` : ''}.svg`)
33
- if (!response.ok) return undefined
34
- const html = clean(await response.text())
35
- return html ? icon(html) : undefined
36
- })
64
+ export const remote = (source: string) => (name: string, variant?: string) =>
65
+ deferred(`${name}:${variant ?? ''}`, async () => {
66
+ const response = await fetch(`${source}${name}${variant ? `.${variant}` : ''}.svg`)
67
+ if (!response.ok) return undefined
68
+ const html = clean(await response.text())
69
+ return html ? icon(html) : undefined
70
+ })
package/src/lookup.tsx CHANGED
@@ -6,4 +6,5 @@ const table = all as unknown as Record<string, IconComponent>
6
6
  const title = (name: string) => name.replace(/(^|-)(.)/g, (_, __, c) => c.toUpperCase())
7
7
 
8
8
  /** The icon of a kebab-case name, in `solid` or `action` if asked: undefined for a name nobody drew. */
9
- export const lookup = (name: string, variant?: string): IconComponent | undefined => table[`${title(name)}Icon${variant ? variant[0]!.toUpperCase() + variant.slice(1) : ''}`]
9
+ export const lookup = (name: string, variant?: string): IconComponent | undefined =>
10
+ table[`${title(name)}Icon${variant ? variant[0]!.toUpperCase() + variant.slice(1) : ''}`]
package/src/set.tsx CHANGED
@@ -5,7 +5,10 @@ import type { IconComponent, IconProps } from './svg'
5
5
 
6
6
  /** An `<Icon>` over these icons only: names stay typed, and whatever isn't listed tree-shakes away. */
7
7
  export function iconSet<const S extends Record<string, IconComponent>>(icons: S) {
8
- return function Icon({ name, ...rest }: { name: (keyof S & string) | IconComponent } & IconProps) {
8
+ return function Icon({
9
+ name,
10
+ ...rest
11
+ }: { name: (keyof S & string) | IconComponent } & IconProps) {
9
12
  const icon = untrack(() => name)
10
13
  const Draw: IconComponent = typeof icon === 'function' ? icon : icons[icon]!
11
14
  return <Draw {...rest} />
package/src/svg.tsx CHANGED
@@ -7,7 +7,7 @@ import './icons.css'
7
7
  export type IconProps = {
8
8
  /** An accessible name: the icon becomes role="img". Without one it is aria-hidden. */
9
9
  label?: string | undefined
10
- /** Width and height, in px or any CSS length. warp's own sizes apply otherwise. */
10
+ /** Width and height, in px or any CSS length. Warp's own sizes apply otherwise. */
11
11
  size?: number | string | undefined
12
12
  /** Show this tier whatever the page's expression is. */
13
13
  tier?: 'quiet' | 'balanced' | 'vivid' | undefined
@@ -21,11 +21,30 @@ export type IconComponent = (props: IconProps) => JSX.Element
21
21
  const length = (size: number | string) => (typeof size === 'number' ? `${size}px` : size)
22
22
 
23
23
  export function Svg({ label, size, tier, html, style, ...rest }: IconProps & { html: string }) {
24
- // oxlint-disable-next-line weft/no-html-sink -- `html` is this package's generated art: `icon` and `acting` set it after the props spread, so no caller reaches it
25
- return <svg data-icon data-expression={tier} viewBox="0 0 24 24" role={label ? 'img' : undefined} aria-label={label} aria-hidden={label ? undefined : 'true'} focusable="false" style={size === undefined ? style : `width:${length(size)};height:${length(size)}${style ? `;${style}` : ''}`} innerHTML={html} {...rest} />
24
+ // oxlint-disable weft/no-html-sink -- `html` is this package's generated art: `icon` and `acting` set it after the props spread, so no caller reaches it
25
+ return (
26
+ <svg
27
+ data-icon
28
+ data-expression={tier}
29
+ viewBox="0 0 24 24"
30
+ role={label ? 'img' : undefined}
31
+ aria-label={label}
32
+ aria-hidden={label ? undefined : 'true'}
33
+ focusable="false"
34
+ style={
35
+ size === undefined
36
+ ? style
37
+ : `width:${length(size)};height:${length(size)}${style ? `;${style}` : ''}`
38
+ }
39
+ innerHTML={html} /* oxlint-enable weft/no-html-sink */
40
+ {...rest}
41
+ />
42
+ )
26
43
  }
27
44
 
28
45
  /*#__NO_SIDE_EFFECTS__*/
29
46
  export function icon(line: string, pixels = ''): IconComponent {
30
- return p => <Svg {...p} html={line + pixels} />
47
+ // Dither shows from 20 px, which the icon learns by measuring itself (icons.css): only one that has it asks.
48
+ const dither = /\sdata-d=/.test(pixels) || /\sdata-d=/.test(line) || undefined
49
+ return (p) => <Svg data-dither={dither} {...p} html={line + pixels} />
31
50
  }
package/src/tiers.css CHANGED
@@ -1,6 +1,7 @@
1
1
  /* The runtime tier switch. warp's expression stops set these four properties, so the nearest data-expression wins with
2
2
  no script and no reflow; a page without warp shows balanced. */
3
3
  /* warp's layer order, first: a layer ranks by when the page first names it, and this file can load before warp.css. */
4
+ @layer warp, app, theme, base, components, utilities;
4
5
  @layer warp.tokens, warp.base, warp.signature, warp.layout, warp.components, warp.inputs, warp.inputs-rich, warp.data, warp.app, warp.flows, warp.marketing, warp.canvas, warp.flow;
5
6
  @layer warp.components {
6
7
  :where(svg[data-icon] [data-t="o"]) { display: var(--warp-icon-o, inline); }