tosijs-ui 1.16.18 → 1.16.19
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/CHANGELOG.md +23 -0
- package/dist/icon-rules.d.ts +20 -0
- package/dist/icon-rules.js +72 -0
- package/dist/icon-suffixes.d.ts +8 -0
- package/dist/icon-suffixes.js +31 -0
- package/dist/icon-svg.d.ts +7 -0
- package/dist/icon-svg.js +79 -9
- package/dist/icons.d.ts +1 -5
- package/dist/icons.js +35 -97
- package/dist/iife.js +94 -94
- package/dist/iife.js.map +8 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.19
|
|
4
|
+
|
|
5
|
+
- **`iconSvg()` resolves the built-in icons that rotate or flip another.** 27 built-in
|
|
6
|
+
names are redirects with a rotation or flip (`chevronDown` is `chevronRight90r`; also
|
|
7
|
+
`chevronLeft`/`Up`, `arrowDown`/`Left`/`Up`, `cornerDownLeft`, `rotateCcw`, `thumbsDown`
|
|
8
|
+
and others), and `iconSvg()` returned `undefined` for every one of them, silently. They
|
|
9
|
+
now return markup with the transform applied about the centre of the viewBox. A rotation
|
|
10
|
+
or flip written inline (`iconSvg('chevronRight90r')`) and other inline names returned
|
|
11
|
+
undefined silently too: a rotation or flip now resolves, and anything else throws. A Playwright
|
|
12
|
+
test draws each one both ways, through `iconSvg` and through `icons.<name>()`, and
|
|
13
|
+
finds no pixel that differs by more than antialiasing, in all three browsers.
|
|
14
|
+
- **`iconSvg()` throws for a name it cannot turn into one SVG.** A stack
|
|
15
|
+
(`cloudSync: 'spin120Loader40s_30x$cloud'`, or `tosi$hat` written inline), a rule
|
|
16
|
+
(`unLock`, `spin90Loader`), a style suffix other than rotation or flip (`check50o`), a
|
|
17
|
+
redirect to a name that is not an icon, or a redirect cycle all returned `undefined`,
|
|
18
|
+
the same answer as a name that does not exist, so a build script rendered nothing and
|
|
19
|
+
said nothing. Each now throws, saying which it is and, where the icon renderer would draw
|
|
20
|
+
it, that `icons.<name>()` does so where there is a DOM. **Code that relied on `undefined`
|
|
21
|
+
for those names now gets an exception;** a name nothing would draw still returns
|
|
22
|
+
`undefined`, as documented. This is the follow-up to #224, for the DOM-free path.
|
|
23
|
+
- `iconSvg()` reads suffixes with the same grammar as the icon renderer, now shared
|
|
24
|
+
between them: `edit2_90r` is `edit2` turned 90°, not -90°.
|
|
25
|
+
|
|
3
26
|
## 1.16.18
|
|
4
27
|
|
|
5
28
|
- **A `defineIcons` name for a stacked composition draws the stack.** `defineIcons({
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ElementPart } from 'tosijs';
|
|
2
|
+
export interface IconRule {
|
|
3
|
+
prefix: string | RegExp;
|
|
4
|
+
apply: (baseName: string, match: RegExpMatchArray | string, parts: ElementPart[]) => Element | string | null;
|
|
5
|
+
}
|
|
6
|
+
/** A registered icon, or style suffixes on one (`chevronRight50o`). */
|
|
7
|
+
export declare function canResolve(name: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* `spinN<Icon>` animates, so it is drawn by the DOM renderer, which installs `apply` when
|
|
10
|
+
* it loads. Its prefix is here so that every module knows the rule exists.
|
|
11
|
+
*/
|
|
12
|
+
export declare const spinRule: IconRule;
|
|
13
|
+
export declare const iconRules: IconRule[];
|
|
14
|
+
/** The icon a rule would apply to in `name`, or null if the rule does not match it. */
|
|
15
|
+
export declare function matchRule(rule: IconRule, name: string): {
|
|
16
|
+
baseName: string;
|
|
17
|
+
match: RegExpMatchArray | string;
|
|
18
|
+
} | null;
|
|
19
|
+
/** Whether some rule would draw `name`: it matches, and the icon it applies to exists. */
|
|
20
|
+
export declare const ruleDraws: (name: string) => boolean;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Composition rules (`spinNLoader`, `unLock`, `checkFile`, …) and how a name is matched to
|
|
3
|
+
one, in ONE place. `icons.ts` (the DOM renderer) draws through them; `icon-svg.ts` (markup
|
|
4
|
+
without a DOM) asks the same question, "would the renderer draw this name?", so it can tell
|
|
5
|
+
a rule it cannot render from a name nothing would draw. It once guessed rules from the shape
|
|
6
|
+
of a name instead, and threw for `redLock`. DOM-free, so `icon-svg.ts` can import it.
|
|
7
|
+
|
|
8
|
+
Rules an app adds with `iconRules.push()` are seen by both, as long as they are pushed
|
|
9
|
+
before the name is asked for.
|
|
10
|
+
*/
|
|
11
|
+
import iconData from './icon-data.js';
|
|
12
|
+
import { splitSuffixes } from './icon-suffixes.js';
|
|
13
|
+
/** A registered icon, or style suffixes on one (`chevronRight50o`). */
|
|
14
|
+
export function canResolve(name) {
|
|
15
|
+
const data = iconData;
|
|
16
|
+
if (data[name])
|
|
17
|
+
return true;
|
|
18
|
+
const split = splitSuffixes(name);
|
|
19
|
+
return split != null && !!data[split.baseName];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `spinN<Icon>` animates, so it is drawn by the DOM renderer, which installs `apply` when
|
|
23
|
+
* it loads. Its prefix is here so that every module knows the rule exists.
|
|
24
|
+
*/
|
|
25
|
+
export const spinRule = {
|
|
26
|
+
prefix: /^spin(_?\d+)/,
|
|
27
|
+
apply() {
|
|
28
|
+
throw new Error('spin icons are drawn by the icon renderer (tosijs-ui/icons)');
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export const iconRules = [
|
|
32
|
+
spinRule,
|
|
33
|
+
{
|
|
34
|
+
prefix: 'un',
|
|
35
|
+
apply: (baseName) => `slash25o$${baseName}75s75o`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
prefix: 'check',
|
|
39
|
+
apply: (baseName) => `check75o_00aa00S$${baseName}75s50o`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
prefix: 'cancel',
|
|
43
|
+
apply: (baseName) => `x75o_cc0000S$${baseName}75s50o`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
prefix: 'search',
|
|
47
|
+
apply: (baseName) => `search80s30x30y$${baseName}50o`,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
/** The icon a rule would apply to in `name`, or null if the rule does not match it. */
|
|
51
|
+
export function matchRule(rule, name) {
|
|
52
|
+
if (rule.prefix instanceof RegExp) {
|
|
53
|
+
const m = name.match(new RegExp(rule.prefix.source + '(.+)$'));
|
|
54
|
+
if (!m)
|
|
55
|
+
return null;
|
|
56
|
+
const tail = m[m.length - 1];
|
|
57
|
+
return { baseName: tail[0].toLowerCase() + tail.slice(1), match: m };
|
|
58
|
+
}
|
|
59
|
+
if (!name.startsWith(rule.prefix) || name.length <= rule.prefix.length) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
baseName: name[rule.prefix.length].toLowerCase() +
|
|
64
|
+
name.slice(rule.prefix.length + 1),
|
|
65
|
+
match: rule.prefix,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Whether some rule would draw `name`: it matches, and the icon it applies to exists. */
|
|
69
|
+
export const ruleDraws = (name) => iconRules.some((rule) => {
|
|
70
|
+
const matched = matchRule(rule, name);
|
|
71
|
+
return matched != null && canResolve(matched.baseName);
|
|
72
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const SUFFIX_RE: RegExp;
|
|
2
|
+
/** A name split into its base and its suffix tokens, in order, or null if it has none. */
|
|
3
|
+
export declare function splitSuffixes(name: string): {
|
|
4
|
+
baseName: string;
|
|
5
|
+
tokens: string[];
|
|
6
|
+
} | null;
|
|
7
|
+
/** The signed number in a numeric token: `_45r` is -45. */
|
|
8
|
+
export declare const suffixNumber: (token: string) => number;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The icon style-suffix grammar, in ONE place. `icons.ts` (the DOM renderer) and
|
|
3
|
+
`icon-svg.ts` (markup without a DOM) both read names through it. `icon-svg.ts` kept its own
|
|
4
|
+
copy for one release candidate, and the two disagreed within a day: it read the `_` in
|
|
5
|
+
`edit2_90r` as a minus sign and rotated the wrong way. DOM-free, so `icon-svg.ts` can
|
|
6
|
+
import it.
|
|
7
|
+
|
|
8
|
+
Suffixes are a value then a letter code:
|
|
9
|
+
50o (opacity), 75s (scale), 20x (translateX%), _10y (translateY%)
|
|
10
|
+
90r (rotate 90°), _45r (rotate -45°), 0f (flipH), 1f (flipV)
|
|
11
|
+
_FF0000F (fill hex), _f00S (stroke hex), 3W (stroke-width)
|
|
12
|
+
_brandColorF (fill var), _accentS (stroke var)
|
|
13
|
+
Icon names ending in digits need a _ separator: edit2_50o
|
|
14
|
+
*/
|
|
15
|
+
export const SUFFIX_RE = /(?:(?<=[a-zA-Z_])(?:_?\d+[osxyr]|[01]f|\d+W)|_[a-zA-Z0-9]+[FS])+$/;
|
|
16
|
+
const SUFFIX_TOKEN_RE = /_?\d+[osxyr]|[01]f|_[a-zA-Z0-9]+[FS]|\d+W/g;
|
|
17
|
+
/** A name split into its base and its suffix tokens, in order, or null if it has none. */
|
|
18
|
+
export function splitSuffixes(name) {
|
|
19
|
+
const match = name.match(SUFFIX_RE);
|
|
20
|
+
if (!match)
|
|
21
|
+
return null;
|
|
22
|
+
let baseName = name.slice(0, match.index);
|
|
23
|
+
// Strip the trailing _ separator (for digit-ending names: edit2_50o)
|
|
24
|
+
if (baseName.endsWith('_'))
|
|
25
|
+
baseName = baseName.slice(0, -1);
|
|
26
|
+
if (!baseName)
|
|
27
|
+
return null;
|
|
28
|
+
return { baseName, tokens: match[0].match(SUFFIX_TOKEN_RE) };
|
|
29
|
+
}
|
|
30
|
+
/** The signed number in a numeric token: `_45r` is -45. */
|
|
31
|
+
export const suffixNumber = (token) => parseInt(token.replace('_', '-'));
|
package/dist/icon-svg.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw SVG markup for an icon, or `undefined` when nothing by that name would draw.
|
|
3
|
+
*
|
|
4
|
+
* Throws when the name would draw but cannot be one SVG string: a stack (`a$b`), a rule
|
|
5
|
+
* (`spin…`, `un…`), a style suffix other than rotation or flip, a redirect to a name that
|
|
6
|
+
* is not an icon, or a redirect cycle. Those need `icons.<name>()` where there is a DOM.
|
|
7
|
+
*/
|
|
1
8
|
export declare function iconSvg(name: string): string | undefined;
|
|
2
9
|
/** Every icon name currently registered, including any added via defineIcons. */
|
|
3
10
|
export declare function iconNames(): string[];
|
package/dist/icon-svg.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
```js
|
|
6
6
|
import { iconSvg, iconNames } from 'tosijs-ui'
|
|
7
7
|
|
|
8
|
-
const markup = iconSvg('tosi') // '<svg …>…</svg>'
|
|
8
|
+
const markup = iconSvg('tosi') // '<svg …>…</svg>', or undefined for an unknown name
|
|
9
9
|
preview.innerHTML =
|
|
10
10
|
`<p>${iconNames().length} icons registered.</p>` +
|
|
11
11
|
`<div style="width:48px">${markup}</div>`
|
|
@@ -19,6 +19,14 @@ what the live-example runner can rewrite.)
|
|
|
19
19
|
is registered. Neither touches the DOM, so both work in a build script, a server-rendered
|
|
20
20
|
template, or an ePub pass.
|
|
21
21
|
|
|
22
|
+
Redirects are followed, including the built-in ones that rotate or flip another icon
|
|
23
|
+
(`chevronDown` is `chevronRight90r`), and a rotation or flip written inline works too
|
|
24
|
+
(`iconSvg('chevronRight90r')`). A name the icon renderer would draw but that cannot be one
|
|
25
|
+
SVG string **throws**, saying why: a stack (`a$b`), a rule (`spin90Loader`, `unLock`), any
|
|
26
|
+
other style suffix (`check50o`), a redirect to a name that is not an icon, or a redirect
|
|
27
|
+
cycle. Render those with `icons.<name>()` where there is a DOM; the suffixes and rules are
|
|
28
|
+
described on the [[icons]] page. Only a name that nothing would draw returns `undefined`.
|
|
29
|
+
|
|
22
30
|
Deliberately a separate module from `icons.ts`: that one imports tosijs to build
|
|
23
31
|
elements, so importing it drags in code that needs `HTMLElement` and throws in a build
|
|
24
32
|
script — the exact context that wanted the markup in the first place. This module
|
|
@@ -32,18 +40,80 @@ resorted to when putting its own logo on a generated page. There is nothing to p
|
|
|
32
40
|
the map ships in the bundle either way.
|
|
33
41
|
*/
|
|
34
42
|
import iconData from './icon-data.js';
|
|
43
|
+
import { splitSuffixes, suffixNumber } from './icon-suffixes.js';
|
|
44
|
+
import { canResolve, ruleDraws } from './icon-rules.js';
|
|
45
|
+
const data = iconData;
|
|
46
|
+
// Rotation and flip, the only suffixes the built-in redirects use (`chevronDown` is
|
|
47
|
+
// `chevronRight90r`). Anything else needs the DOM renderer.
|
|
48
|
+
const isGeometry = (token) => /^(_?\d+r|[01]f)$/.test(token);
|
|
49
|
+
const stripEscape = (name) => name.endsWith('_') ? name.slice(0, -1) : name;
|
|
50
|
+
// Whether the icon renderer would draw something for a name: a registered icon, suffixes on
|
|
51
|
+
// one (`chevronRight50o`), a stack (`a$b`), or a rule applied to one (`spin90Loader`,
|
|
52
|
+
// `unLock`). The rules and suffixes are the renderer's own (icon-rules, icon-suffixes).
|
|
53
|
+
const isComposition = (name) => name.includes('$') || canResolve(name) || ruleDraws(name);
|
|
54
|
+
/**
|
|
55
|
+
* Raw SVG markup for an icon, or `undefined` when nothing by that name would draw.
|
|
56
|
+
*
|
|
57
|
+
* Throws when the name would draw but cannot be one SVG string: a stack (`a$b`), a rule
|
|
58
|
+
* (`spin…`, `un…`), a style suffix other than rotation or flip, a redirect to a name that
|
|
59
|
+
* is not an icon, or a redirect cycle. Those need `icons.<name>()` where there is a DOM.
|
|
60
|
+
*/
|
|
35
61
|
export function iconSvg(name) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
const key = stripEscape(name);
|
|
63
|
+
if (!isComposition(key))
|
|
64
|
+
return undefined;
|
|
65
|
+
return resolve(name, key, new Set());
|
|
66
|
+
}
|
|
67
|
+
function resolve(asked, name, seen) {
|
|
68
|
+
const key = stripEscape(name);
|
|
69
|
+
const value = data[key];
|
|
70
|
+
if (value !== undefined) {
|
|
42
71
|
if (value.startsWith('<'))
|
|
43
72
|
return value;
|
|
44
|
-
key
|
|
73
|
+
if (seen.has(key)) {
|
|
74
|
+
throw new Error(`iconSvg("${asked}"): the icon redirects in a cycle`);
|
|
75
|
+
}
|
|
76
|
+
seen.add(key);
|
|
77
|
+
return resolve(asked, value, seen);
|
|
78
|
+
}
|
|
79
|
+
// The renderer's order for a name that is not registered: a stack, then a rule, then
|
|
80
|
+
// suffixes. So `checkCircle90r` is the `check` rule over `circle90r`, not `checkCircle`
|
|
81
|
+
// turned, and only a name no rule claims gets the transform.
|
|
82
|
+
const split = key.includes('$') || ruleDraws(key) ? null : splitSuffixes(key);
|
|
83
|
+
if (split &&
|
|
84
|
+
data[split.baseName] !== undefined &&
|
|
85
|
+
split.tokens.every(isGeometry)) {
|
|
86
|
+
return transformed(asked, resolve(asked, split.baseName, seen), split.tokens);
|
|
87
|
+
}
|
|
88
|
+
const what = key === stripEscape(asked) ? `"${key}"` : `it resolves to "${key}", which`;
|
|
89
|
+
if (isComposition(key)) {
|
|
90
|
+
throw new Error(`iconSvg("${asked}"): ${what} is not a single SVG (a stack, a rule, or a style ` +
|
|
91
|
+
`suffix other than rotation or flip). Render it with icons.${stripEscape(asked)}() ` +
|
|
92
|
+
`where there is a DOM.`);
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`iconSvg("${asked}"): it redirects to "${key}", which is not a registered icon`);
|
|
95
|
+
}
|
|
96
|
+
// Apply rotate/flip suffixes about the centre of the viewBox, as the DOM renderer does
|
|
97
|
+
// with `transform-origin: 50% 50%`.
|
|
98
|
+
function transformed(asked, svg, tokens) {
|
|
99
|
+
const parts = svg.match(/^(\s*<svg\b[^>]*>)([\s\S]*)(<\/svg>\s*)$/);
|
|
100
|
+
const viewBox = parts?.[1]
|
|
101
|
+
.match(/viewBox="([^"]+)"/)?.[1]
|
|
102
|
+
.split(/[\s,]+/)
|
|
103
|
+
.map(Number);
|
|
104
|
+
if (!parts || !viewBox || viewBox.length !== 4 || viewBox.some(isNaN)) {
|
|
105
|
+
throw new Error(`iconSvg("${asked}"): cannot transform an icon without a viewBox`);
|
|
45
106
|
}
|
|
46
|
-
|
|
107
|
+
const [x, y, width, height] = viewBox;
|
|
108
|
+
const cx = x + width / 2;
|
|
109
|
+
const cy = y + height / 2;
|
|
110
|
+
const ops = tokens.map((token) => token.endsWith('r')
|
|
111
|
+
? `rotate(${suffixNumber(token)})`
|
|
112
|
+
: token === '0f'
|
|
113
|
+
? 'scale(-1 1)'
|
|
114
|
+
: 'scale(1 -1)');
|
|
115
|
+
const transform = `translate(${cx} ${cy}) ${ops.join(' ')} translate(${-cx} ${-cy})`;
|
|
116
|
+
return `${parts[1]}<g transform="${transform}">${parts[2]}</g>${parts[3]}`;
|
|
47
117
|
}
|
|
48
118
|
/** Every icon name currently registered, including any added via defineIcons. */
|
|
49
119
|
export function iconNames() {
|
package/dist/icons.d.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { ElementCreator, ElementPart } from 'tosijs';
|
|
2
2
|
import { SVGIconMap } from './icon-types.js';
|
|
3
|
+
export { iconRules, type IconRule } from './icon-rules.js';
|
|
3
4
|
export declare const defineIcons: (newIcons: {
|
|
4
5
|
[key: string]: string;
|
|
5
6
|
}) => void;
|
|
6
7
|
export { iconSvg, iconNames } from './icon-svg.js';
|
|
7
8
|
export type { IconElement, IconName, SVGIconMap } from './icon-types.js';
|
|
8
9
|
export declare const svg2DataUrl: (icon: Element, fill?: string, stroke?: string, strokeWidth?: number) => string;
|
|
9
|
-
export interface IconRule {
|
|
10
|
-
prefix: string | RegExp;
|
|
11
|
-
apply: (baseName: string, match: RegExpMatchArray | string, parts: ElementPart[]) => Element | string | null;
|
|
12
|
-
}
|
|
13
|
-
export declare const iconRules: IconRule[];
|
|
14
10
|
/** Wrap icon elements in a composite container (for custom function rules) */
|
|
15
11
|
export declare function wrapIcon(prop: string, parts: ElementPart[], ...children: Element[]): HTMLSpanElement;
|
|
16
12
|
/** Resolve an icon name through the full pipeline (redirects, suffixes, rules, stacking) */
|
package/dist/icons.js
CHANGED
|
@@ -564,6 +564,9 @@ copies or substantial portions of the Software.
|
|
|
564
564
|
*/
|
|
565
565
|
import { elements, svgElements, Color, vars, varDefault, withAttributes, } from 'tosijs';
|
|
566
566
|
import iconData from './icon-data.js';
|
|
567
|
+
import { splitSuffixes, suffixNumber } from './icon-suffixes.js';
|
|
568
|
+
import { iconRules, spinRule, matchRule, canResolve } from './icon-rules.js';
|
|
569
|
+
export { iconRules } from './icon-rules.js';
|
|
567
570
|
export const defineIcons = (newIcons) => {
|
|
568
571
|
Object.assign(iconData, newIcons);
|
|
569
572
|
};
|
|
@@ -629,47 +632,28 @@ export const svg2DataUrl = (icon, fill, stroke, strokeWidth) => {
|
|
|
629
632
|
};
|
|
630
633
|
// Appends _ to names ending in digits to prevent suffix ambiguity
|
|
631
634
|
const safeIconSuffix = (name) => /\d$/.test(name) ? name + '_' : name;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
},
|
|
655
|
-
},
|
|
656
|
-
{
|
|
657
|
-
prefix: 'un',
|
|
658
|
-
apply: (baseName) => `slash25o$${baseName}75s75o`,
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
prefix: 'check',
|
|
662
|
-
apply: (baseName) => `check75o_00aa00S$${baseName}75s50o`,
|
|
663
|
-
},
|
|
664
|
-
{
|
|
665
|
-
prefix: 'cancel',
|
|
666
|
-
apply: (baseName) => `x75o_cc0000S$${baseName}75s50o`,
|
|
667
|
-
},
|
|
668
|
-
{
|
|
669
|
-
prefix: 'search',
|
|
670
|
-
apply: (baseName) => `search80s30x30y$${baseName}50o`,
|
|
671
|
-
},
|
|
672
|
-
];
|
|
635
|
+
// The rule list and how names match it live in icon-rules.ts, shared with iconSvg. Spin
|
|
636
|
+
// animates, so its renderer is installed here.
|
|
637
|
+
spinRule.apply = (baseName, match, parts) => {
|
|
638
|
+
const dps = match[1].replace('_', '-');
|
|
639
|
+
const duration = 360 / Math.abs(parseFloat(dps));
|
|
640
|
+
const direction = dps.startsWith('-') ? 'reverse' : 'normal';
|
|
641
|
+
// Resolve baseName with suffixes intact — they apply to the icon
|
|
642
|
+
const icon = resolveIcon(baseName, []);
|
|
643
|
+
const el = icon;
|
|
644
|
+
if (el.animate) {
|
|
645
|
+
const base = el.style.transform || '';
|
|
646
|
+
el.animate([
|
|
647
|
+
{ transform: `${base} rotate(0deg)` },
|
|
648
|
+
{ transform: `${base} rotate(360deg)` },
|
|
649
|
+
], {
|
|
650
|
+
duration: duration * 1000,
|
|
651
|
+
iterations: Infinity,
|
|
652
|
+
direction,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return wrapIcon(baseName, parts, icon);
|
|
656
|
+
};
|
|
673
657
|
let iconIdCounter = 0;
|
|
674
658
|
function makeIcon(spec, parts) {
|
|
675
659
|
const div = elements.div();
|
|
@@ -748,38 +732,13 @@ export function wrapIcon(prop, parts, ...children) {
|
|
|
748
732
|
}
|
|
749
733
|
return wrapper;
|
|
750
734
|
}
|
|
751
|
-
function canResolve(name) {
|
|
752
|
-
const data = iconData;
|
|
753
|
-
if (data[name])
|
|
754
|
-
return true;
|
|
755
|
-
const parsed = parseStyleSuffixes(name);
|
|
756
|
-
return parsed != null && !!data[parsed.baseName];
|
|
757
|
-
}
|
|
758
735
|
function composeIcon(prop, parts) {
|
|
759
736
|
for (const rule of iconRules) {
|
|
760
|
-
|
|
761
|
-
let match;
|
|
762
|
-
if (rule.prefix instanceof RegExp) {
|
|
763
|
-
const re = new RegExp(rule.prefix.source + '(.+)$');
|
|
764
|
-
const m = prop.match(re);
|
|
765
|
-
if (!m)
|
|
766
|
-
continue;
|
|
767
|
-
baseName = m[m.length - 1];
|
|
768
|
-
baseName = baseName[0].toLowerCase() + baseName.slice(1);
|
|
769
|
-
match = m;
|
|
770
|
-
}
|
|
771
|
-
else {
|
|
772
|
-
if (!prop.startsWith(rule.prefix) || prop.length <= rule.prefix.length)
|
|
773
|
-
continue;
|
|
774
|
-
baseName =
|
|
775
|
-
prop[rule.prefix.length].toLowerCase() +
|
|
776
|
-
prop.slice(rule.prefix.length + 1);
|
|
777
|
-
match = rule.prefix;
|
|
778
|
-
}
|
|
737
|
+
const matched = matchRule(rule, prop);
|
|
779
738
|
// Only apply if baseName can actually resolve to an icon
|
|
780
|
-
if (!canResolve(baseName))
|
|
739
|
+
if (!matched || !canResolve(matched.baseName))
|
|
781
740
|
continue;
|
|
782
|
-
const result = rule.apply(safeIconSuffix(baseName), match, parts);
|
|
741
|
+
const result = rule.apply(safeIconSuffix(matched.baseName), matched.match, parts);
|
|
783
742
|
if (typeof result === 'string')
|
|
784
743
|
return resolveIcon(result, parts);
|
|
785
744
|
if (result)
|
|
@@ -789,41 +748,20 @@ function composeIcon(prop, parts) {
|
|
|
789
748
|
}
|
|
790
749
|
// Redirect names being resolved right now, so a redirect cycle ends instead of recursing.
|
|
791
750
|
const expanding = new Set();
|
|
792
|
-
//
|
|
793
|
-
// 50o (opacity), 75s (scale), 20x (translateX%), _10y (translateY%)
|
|
794
|
-
// 90r (rotate 90°), _45r (rotate -45°), 0f (flipH), 1f (flipV)
|
|
795
|
-
// _FF0000F (fill hex), _f00S (stroke hex), 3W (stroke-width)
|
|
796
|
-
// _brandColorF (fill var), _accentS (stroke var)
|
|
797
|
-
// Icon names ending in digits need _ separator: edit2_50o
|
|
798
|
-
const SUFFIX_RE = /(?:(?<=[a-zA-Z_])(?:_?\d+[osxyr]|[01]f|\d+W)|_[a-zA-Z0-9]+[FS])+$/;
|
|
751
|
+
// The suffix grammar lives in icon-suffixes.ts, shared with iconSvg.
|
|
799
752
|
function parseStyleSuffixes(name) {
|
|
800
|
-
const
|
|
801
|
-
if (!
|
|
802
|
-
return null;
|
|
803
|
-
let baseName = name.slice(0, match.index);
|
|
804
|
-
if (!baseName)
|
|
805
|
-
return null;
|
|
806
|
-
// Strip trailing _ separator (for digit-ending names: edit2_50o)
|
|
807
|
-
if (baseName.endsWith('_'))
|
|
808
|
-
baseName = baseName.slice(0, -1);
|
|
809
|
-
if (!baseName)
|
|
753
|
+
const split = splitSuffixes(name);
|
|
754
|
+
if (!split)
|
|
810
755
|
return null;
|
|
756
|
+
const { baseName } = split;
|
|
811
757
|
const data = iconData;
|
|
812
758
|
// Accept if baseName is in iconData, or matches a composition rule prefix
|
|
813
759
|
if (!data[baseName]) {
|
|
814
|
-
|
|
815
|
-
if (rule.prefix instanceof RegExp) {
|
|
816
|
-
return rule.prefix.test(baseName);
|
|
817
|
-
}
|
|
818
|
-
return (baseName.startsWith(rule.prefix) && baseName.length > rule.prefix.length);
|
|
819
|
-
});
|
|
820
|
-
if (!matchesRule)
|
|
760
|
+
if (!iconRules.some((rule) => matchRule(rule, baseName)))
|
|
821
761
|
return null;
|
|
822
762
|
}
|
|
823
763
|
const style = {};
|
|
824
|
-
const suffixes =
|
|
825
|
-
if (!suffixes)
|
|
826
|
-
return null;
|
|
764
|
+
const suffixes = split.tokens;
|
|
827
765
|
const transforms = [];
|
|
828
766
|
for (const s of suffixes) {
|
|
829
767
|
const code = s[s.length - 1];
|
|
@@ -842,7 +780,7 @@ function parseStyleSuffixes(name) {
|
|
|
842
780
|
style.strokeWidth = s.slice(0, -1);
|
|
843
781
|
}
|
|
844
782
|
else {
|
|
845
|
-
const val =
|
|
783
|
+
const val = suffixNumber(s);
|
|
846
784
|
switch (code) {
|
|
847
785
|
case 'o':
|
|
848
786
|
style.opacity = String(val / 100);
|