@enigmax/icons 0.1.1 → 0.2.0
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/README.md +38 -11
- package/generate.mjs +75 -27
- package/icon-map.json +4 -0
- package/package.json +17 -1
package/README.md
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
Icons for React. 391 icons in 36 categories, each in its own module, so a
|
|
4
4
|
bundle carries only the ones it renders.
|
|
5
5
|
|
|
6
|
-
Bold duotone is the weight that ships today. The package is built to hold more
|
|
7
|
-
than one, so nothing in its name, its exports or its API is tied to that weight.
|
|
8
|
-
|
|
9
6
|
```tsx
|
|
10
7
|
import { IconMapPin } from "@enigmax/icons";
|
|
11
8
|
|
|
@@ -13,10 +10,9 @@ import { IconMapPin } from "@enigmax/icons";
|
|
|
13
10
|
```
|
|
14
11
|
|
|
15
12
|
The icons are filled, not stroked: colour comes from the surrounding text
|
|
16
|
-
colour (`currentColor`)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
and would paint the icon with an invalid value until it disappeared.
|
|
13
|
+
colour (`currentColor`). There is no line to widen, so `stroke` is accepted and
|
|
14
|
+
ignored rather than forwarded into the SVG attribute of that name, which takes
|
|
15
|
+
a colour and would paint the icon with an invalid value until it disappeared.
|
|
20
16
|
|
|
21
17
|
## Only what you use
|
|
22
18
|
|
|
@@ -36,13 +32,44 @@ import * as icons from "@enigmax/icons/categories";
|
|
|
36
32
|
Categories come from the set's own taxonomy: `arrows`, `devices`, `map`,
|
|
37
33
|
`security`, `ui`, `weather`, and thirty more.
|
|
38
34
|
|
|
35
|
+
## Weights
|
|
36
|
+
|
|
37
|
+
Bold duotone is the weight that ships today, and it is addressable by name:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { IconMapPin } from "@enigmax/icons"; // the default weight
|
|
41
|
+
import { IconMapPin } from "@enigmax/icons/bold-duotone"; // the same icon, named
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**A weight is a build dimension, not a prop.** A `weight` prop would have to
|
|
45
|
+
reach every body at runtime, which puts all of them in the bundle for any icon
|
|
46
|
+
the app imports and destroys the one thing this package is for. So each weight
|
|
47
|
+
is generated into its own directory with its own subpath, and an app pays only
|
|
48
|
+
for the weights it actually imports. Two weights of the same icon are two
|
|
49
|
+
imports, and a project that uses one never downloads the other.
|
|
50
|
+
|
|
51
|
+
Adding one is three steps and no breaking change:
|
|
52
|
+
|
|
53
|
+
1. Declare it in `icon-map.json` under `weights.available`.
|
|
54
|
+
2. Give every icon its name in that weight under `glyphs`, e.g.
|
|
55
|
+
`"IconSun": { "glyph": "sun-bold-duotone", "glyphs": { "outline": "sun-linear" }, "category": "Weather" }`.
|
|
56
|
+
Names are written out per weight rather than derived by swapping a suffix: a
|
|
57
|
+
set is free to name a weight's glyph anything, and guessing it produces an
|
|
58
|
+
icon that silently renders nothing.
|
|
59
|
+
3. `node generate.mjs --set <set.json> --weight outline`.
|
|
60
|
+
|
|
61
|
+
The default weight keeps the root paths, so nothing that already imports from
|
|
62
|
+
this package changes when a weight is added. A weight that is declared but
|
|
63
|
+
incomplete fails the test suite rather than shipping: half a weight renders
|
|
64
|
+
some icons and vanishes the rest, which reads as a broken page.
|
|
65
|
+
|
|
39
66
|
## Conventions
|
|
40
67
|
|
|
41
68
|
An external link is marked with the **diagonal arrow pointing up and to the
|
|
42
69
|
right** (`IconExternalLink`), not the box-with-an-arrow-leaving-it glyph: the
|
|
43
70
|
latter reads as clutter at small sizes in a filled set.
|
|
44
71
|
|
|
45
|
-
Brand marks (`IconBrandGithub`, `IconBrandDiscord`,
|
|
72
|
+
Brand marks (`IconBrandGithub`, `IconBrandDiscord`, ...) are re-exported from
|
|
46
73
|
`@tabler/icons-react`, an optional peer dependency. This set has no brand
|
|
47
74
|
marks, and drawing a company's logo by hand is worse than shipping the real
|
|
48
75
|
one. Install `@tabler/icons-react` only if you import a brand mark.
|
|
@@ -50,12 +77,12 @@ one. Install `@tabler/icons-react` only if you import a brand mark.
|
|
|
50
77
|
## Regenerating
|
|
51
78
|
|
|
52
79
|
```sh
|
|
53
|
-
node generate.mjs --set <path/to/set.json>
|
|
80
|
+
node generate.mjs --set <path/to/set.json> [--weight <name>]
|
|
54
81
|
```
|
|
55
82
|
|
|
56
83
|
Edit `icon-map.json` to add, remove or re-point an icon, then regenerate. A
|
|
57
|
-
name that is not in the set
|
|
58
|
-
forever.
|
|
84
|
+
name that is not in the set, or an icon with no name for the weight being
|
|
85
|
+
generated, fails the run rather than rendering an empty square forever.
|
|
59
86
|
|
|
60
87
|
## Licence
|
|
61
88
|
|
package/generate.mjs
CHANGED
|
@@ -8,7 +8,17 @@
|
|
|
8
8
|
// fetched at install time - the bodies are written here as plain strings.
|
|
9
9
|
//
|
|
10
10
|
// Usage:
|
|
11
|
-
// node generate.mjs --set <path/to/icons.json> [--out src]
|
|
11
|
+
// node generate.mjs --set <path/to/icons.json> [--weight <name>] [--out src]
|
|
12
|
+
//
|
|
13
|
+
// WEIGHT IS A BUILD DIMENSION, NOT A PROP. A `weight` prop would have to reach
|
|
14
|
+
// every body at runtime, which puts all of them in the bundle for any icon the
|
|
15
|
+
// app imports and destroys the one thing this package is for. So each weight is
|
|
16
|
+
// generated into its own directory and addressed by its own subpath, and an app
|
|
17
|
+
// pays only for the weights it actually imports.
|
|
18
|
+
//
|
|
19
|
+
// The DEFAULT weight (declared in icon-map.json) is written to the root of
|
|
20
|
+
// `--out` so the package's main entry keeps working unchanged; every other
|
|
21
|
+
// weight is written to `<out>/<weight>/`.
|
|
12
22
|
//
|
|
13
23
|
// The set is a build-time input, not a dependency of the published package: it
|
|
14
24
|
// is read here and the ~400 bodies the map names are copied out. The full set
|
|
@@ -25,10 +35,9 @@ const arg = (flag, fallback) => {
|
|
|
25
35
|
return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
|
|
26
36
|
};
|
|
27
37
|
|
|
28
|
-
const OUT = resolve(HERE, arg("--out", "src"));
|
|
29
38
|
const SET = arg("--set", null);
|
|
30
39
|
if (!SET || !existsSync(SET)) {
|
|
31
|
-
console.error("usage: node generate.mjs --set <path/to/icons.json> [--out src]");
|
|
40
|
+
console.error("usage: node generate.mjs --set <path/to/icons.json> [--weight <name>] [--out src]");
|
|
32
41
|
process.exit(1);
|
|
33
42
|
}
|
|
34
43
|
|
|
@@ -48,7 +57,7 @@ export type IconProps = {
|
|
|
48
57
|
* Accepted and ignored. These icons are filled, not stroked, so there is no
|
|
49
58
|
* line to widen - and forwarding a number into the SVG \`stroke\` attribute,
|
|
50
59
|
* which takes a COLOUR, paints the icon with an invalid value until it
|
|
51
|
-
* disappears. Kept in the signature so a stroked
|
|
60
|
+
* disappears. Kept in the signature so a stroked weight can be swapped in
|
|
52
61
|
* without touching the call sites that pass it.
|
|
53
62
|
*/
|
|
54
63
|
stroke?: number | string;
|
|
@@ -65,8 +74,8 @@ export type Icon = ForwardRefExoticComponent<IconProps & RefAttributes<SVGSVGEle
|
|
|
65
74
|
*
|
|
66
75
|
* The body is a constant written by the generator, never anything a caller
|
|
67
76
|
* supplies, which is what makes dangerouslySetInnerHTML the right tool here
|
|
68
|
-
* rather than a hole.
|
|
69
|
-
*
|
|
77
|
+
* rather than a hole. Every path carries fill="currentColor", so colour comes
|
|
78
|
+
* from the surrounding text colour and nothing else.
|
|
70
79
|
*/
|
|
71
80
|
export function icon(name: string, body: string): Icon {
|
|
72
81
|
const Component = forwardRef<SVGSVGElement, IconProps>(
|
|
@@ -90,34 +99,70 @@ export function icon(name: string, body: string): Icon {
|
|
|
90
99
|
}
|
|
91
100
|
`;
|
|
92
101
|
|
|
93
|
-
const { icons, brands } = JSON.parse(readFileSync(MAP, "utf8"));
|
|
102
|
+
const { icons, brands, weights } = JSON.parse(readFileSync(MAP, "utf8"));
|
|
103
|
+
const defaultWeight = weights?.default ?? "bold-duotone";
|
|
104
|
+
const available = weights?.available ?? [defaultWeight];
|
|
105
|
+
|
|
106
|
+
const WEIGHT = arg("--weight", defaultWeight);
|
|
107
|
+
if (!available.includes(WEIGHT)) {
|
|
108
|
+
console.error(`Unknown weight '${WEIGHT}'. icon-map.json declares: ${available.join(", ")}.`);
|
|
109
|
+
console.error("Add it to `weights.available` and give each icon its name under `glyphs` first.");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isDefault = WEIGHT === defaultWeight;
|
|
114
|
+
// The default weight owns the root of the output so the package's main entry is
|
|
115
|
+
// unchanged by this ever growing; another weight gets a directory of its own.
|
|
116
|
+
const OUT = resolve(HERE, arg("--out", "src"), isDefault ? "" : WEIGHT);
|
|
117
|
+
const FACTORY_IMPORT = isDefault ? "../icon" : "../../icon";
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What this icon is called in the requested weight.
|
|
121
|
+
*
|
|
122
|
+
* `glyph` is the default weight's name and every icon has one; another weight
|
|
123
|
+
* reads `glyphs[weight]`, which is written out per icon rather than derived by
|
|
124
|
+
* swapping a suffix - a set is free to name a weight's glyph anything at all,
|
|
125
|
+
* and guessing it produces an icon that silently renders nothing.
|
|
126
|
+
*/
|
|
127
|
+
const glyphFor = (spec) => (isDefault ? spec.glyph : spec.glyphs?.[WEIGHT]);
|
|
128
|
+
|
|
94
129
|
const drawings = JSON.parse(readFileSync(SET, "utf8")).icons;
|
|
95
130
|
|
|
96
|
-
// A name that is not in the set does not throw at runtime - it
|
|
97
|
-
// square forever - so it has to fail here instead.
|
|
98
|
-
const
|
|
131
|
+
// A name that is missing, or not in the set, does not throw at runtime - it
|
|
132
|
+
// renders an empty square forever - so it has to fail here instead.
|
|
133
|
+
const unnamed = Object.entries(icons).filter(([, spec]) => !glyphFor(spec));
|
|
134
|
+
if (unnamed.length > 0) {
|
|
135
|
+
console.error(`${unnamed.length} icon(s) have no name for weight '${WEIGHT}':`);
|
|
136
|
+
for (const [name] of unnamed.slice(0, 10)) console.error(` ${name}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
const missing = Object.entries(icons).filter(([, spec]) => !drawings[glyphFor(spec)]);
|
|
99
140
|
if (missing.length > 0) {
|
|
100
141
|
console.error("Names not present in the icon set:");
|
|
101
|
-
for (const [name, spec] of missing.slice(0, 10)) console.error(` ${name} -> ${spec
|
|
142
|
+
for (const [name, spec] of missing.slice(0, 10)) console.error(` ${name} -> ${glyphFor(spec)}`);
|
|
102
143
|
process.exit(1);
|
|
103
144
|
}
|
|
104
145
|
|
|
105
|
-
rmSync(OUT, { recursive: true, force: true });
|
|
146
|
+
rmSync(resolve(OUT, "icons"), { recursive: true, force: true });
|
|
147
|
+
rmSync(resolve(OUT, "categories"), { recursive: true, force: true });
|
|
106
148
|
mkdirSync(resolve(OUT, "icons"), { recursive: true });
|
|
107
149
|
mkdirSync(resolve(OUT, "categories"), { recursive: true });
|
|
108
150
|
|
|
109
|
-
|
|
151
|
+
// One factory for every weight: it is the same component either way, so a second
|
|
152
|
+
// weight must not ship a second copy of it.
|
|
153
|
+
if (isDefault) writeFileSync(resolve(OUT, "icon.tsx"), FACTORY, "utf8");
|
|
110
154
|
|
|
111
155
|
const names = Object.keys(icons).sort();
|
|
112
156
|
const byCategory = new Map();
|
|
113
157
|
let bytes = 0;
|
|
114
158
|
|
|
115
159
|
for (const name of names) {
|
|
116
|
-
const {
|
|
160
|
+
const { category } = icons[name];
|
|
161
|
+
const glyph = glyphFor(icons[name]);
|
|
117
162
|
const body = drawings[glyph].body;
|
|
118
163
|
bytes += body.length;
|
|
119
164
|
const file = `// ${name} - ${glyph}. GENERATED, do not hand-edit.
|
|
120
|
-
import { icon } from "
|
|
165
|
+
import { icon } from "${FACTORY_IMPORT}";
|
|
121
166
|
|
|
122
167
|
export const ${name} = icon(${JSON.stringify(name)}, ${JSON.stringify(body)});
|
|
123
168
|
`;
|
|
@@ -135,26 +180,28 @@ for (const category of categories) {
|
|
|
135
180
|
}
|
|
136
181
|
|
|
137
182
|
const index = [
|
|
138
|
-
|
|
183
|
+
`// GENERATED, do not hand-edit. Run: node generate.mjs --set <set.json> --weight ${WEIGHT}`,
|
|
139
184
|
"//",
|
|
140
185
|
"// Import from the root for a single icon (the bundler keeps only that module),",
|
|
141
186
|
"// or from a category when you want the group:",
|
|
142
187
|
"// import { IconMapPin } from \"@<scope>/icons\";",
|
|
143
188
|
"// import { IconMapPin } from \"@<scope>/icons/categories/map\";",
|
|
144
189
|
"",
|
|
145
|
-
"export type { Icon, IconProps } from \"./icon\";",
|
|
146
|
-
"",
|
|
147
190
|
];
|
|
191
|
+
if (isDefault) index.push("export type { Icon, IconProps } from \"./icon\";", "");
|
|
148
192
|
for (const name of names) index.push(`export { ${name} } from "./icons/${name}";`);
|
|
149
|
-
|
|
150
|
-
index.push("//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
193
|
+
if (isDefault) {
|
|
194
|
+
index.push("", "// Brand marks keep their real logo: this set has none, and drawing one by hand");
|
|
195
|
+
index.push("// is worse than shipping the real mark.", "export {");
|
|
196
|
+
// No comma after the last entry: a trailing one in a named export list is a style
|
|
197
|
+
// violation here, and the generator is the only thing that writes this file.
|
|
198
|
+
const sortedBrands = [...brands].sort();
|
|
199
|
+
for (const [position, brand] of sortedBrands.entries()) {
|
|
200
|
+
index.push(` ${brand}${position === sortedBrands.length - 1 ? "" : ","}`);
|
|
201
|
+
}
|
|
202
|
+
index.push("} from \"@tabler/icons-react\";");
|
|
156
203
|
}
|
|
157
|
-
index.push("
|
|
204
|
+
index.push("");
|
|
158
205
|
writeFileSync(resolve(OUT, "index.ts"), index.join("\n"), "utf8");
|
|
159
206
|
|
|
160
207
|
// A namespace export needs an identifier, so the category's display name becomes
|
|
@@ -166,6 +213,7 @@ for (const category of categories) {
|
|
|
166
213
|
}
|
|
167
214
|
writeFileSync(resolve(OUT, "categories", "index.ts"), `${catIndex.join("\n")}\n`, "utf8");
|
|
168
215
|
|
|
216
|
+
const brandNote = isDefault ? `, ${brands.length} brand re-exports` : "";
|
|
169
217
|
process.stdout.write(
|
|
170
|
-
`${names.length} icons in ${categories.length} categories
|
|
218
|
+
`${WEIGHT}: ${names.length} icons in ${categories.length} categories${brandNote}, ${(bytes / 1024).toFixed(0)} KB of bodies\n`,
|
|
171
219
|
);
|
package/icon-map.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/icons",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Icon set for React, one module per icon so a bundle carries only what it renders.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,6 +21,22 @@
|
|
|
21
21
|
"./categories/*": {
|
|
22
22
|
"types": "./dist/categories/*.d.ts",
|
|
23
23
|
"import": "./dist/categories/*.js"
|
|
24
|
+
},
|
|
25
|
+
"./bold-duotone": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./bold-duotone/icons/*": {
|
|
30
|
+
"types": "./dist/icons/*.d.ts",
|
|
31
|
+
"import": "./dist/icons/*.js"
|
|
32
|
+
},
|
|
33
|
+
"./bold-duotone/categories": {
|
|
34
|
+
"types": "./dist/categories/index.d.ts",
|
|
35
|
+
"import": "./dist/categories/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./bold-duotone/categories/*": {
|
|
38
|
+
"types": "./dist/categories/*.d.ts",
|
|
39
|
+
"import": "./dist/categories/*.js"
|
|
24
40
|
}
|
|
25
41
|
},
|
|
26
42
|
"files": [
|