@enigmax/icons 0.1.0 → 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/LICENSE +0 -9
- package/README.md +41 -16
- package/generate.mjs +83 -39
- package/icon-map.json +395 -391
- package/package.json +18 -2
- package/src/index.ts +1 -1
package/LICENSE
CHANGED
|
@@ -19,12 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
The icon artwork is the Solar Icon Set by 480 Design, licensed under
|
|
26
|
-
Creative Commons Attribution 4.0 International (CC BY 4.0):
|
|
27
|
-
https://creativecommons.org/licenses/by/4.0/
|
|
28
|
-
|
|
29
|
-
Attribution is required by that licence. Keep this notice, and surface the
|
|
30
|
-
credit somewhere in any application that ships these icons.
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @enigmax/icons
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Icons for React. 391 icons in 36 categories, each in its own module, so a
|
|
4
|
+
bundle carries only the ones it renders.
|
|
5
5
|
|
|
6
6
|
```tsx
|
|
7
7
|
import { IconMapPin } from "@enigmax/icons";
|
|
@@ -10,10 +10,9 @@ import { IconMapPin } from "@enigmax/icons";
|
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
The icons are filled, not stroked: colour comes from the surrounding text
|
|
13
|
-
colour (`currentColor`)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
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.
|
|
17
16
|
|
|
18
17
|
## Only what you use
|
|
19
18
|
|
|
@@ -33,13 +32,44 @@ import * as icons from "@enigmax/icons/categories";
|
|
|
33
32
|
Categories come from the set's own taxonomy: `arrows`, `devices`, `map`,
|
|
34
33
|
`security`, `ui`, `weather`, and thirty more.
|
|
35
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
|
+
|
|
36
66
|
## Conventions
|
|
37
67
|
|
|
38
68
|
An external link is marked with the **diagonal arrow pointing up and to the
|
|
39
69
|
right** (`IconExternalLink`), not the box-with-an-arrow-leaving-it glyph: the
|
|
40
70
|
latter reads as clutter at small sizes in a filled set.
|
|
41
71
|
|
|
42
|
-
Brand marks (`IconBrandGithub`, `IconBrandDiscord`,
|
|
72
|
+
Brand marks (`IconBrandGithub`, `IconBrandDiscord`, ...) are re-exported from
|
|
43
73
|
`@tabler/icons-react`, an optional peer dependency. This set has no brand
|
|
44
74
|
marks, and drawing a company's logo by hand is worse than shipping the real
|
|
45
75
|
one. Install `@tabler/icons-react` only if you import a brand mark.
|
|
@@ -47,18 +77,13 @@ one. Install `@tabler/icons-react` only if you import a brand mark.
|
|
|
47
77
|
## Regenerating
|
|
48
78
|
|
|
49
79
|
```sh
|
|
50
|
-
node generate.mjs --
|
|
80
|
+
node generate.mjs --set <path/to/set.json> [--weight <name>]
|
|
51
81
|
```
|
|
52
82
|
|
|
53
83
|
Edit `icon-map.json` to add, remove or re-point an icon, then regenerate. A
|
|
54
|
-
name that is not in the set
|
|
55
|
-
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.
|
|
56
86
|
|
|
57
87
|
## Licence
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). That licence
|
|
61
|
-
requires attribution: keep this notice, and surface it somewhere in any app
|
|
62
|
-
that ships these icons (an About or Credits screen is the usual place).
|
|
63
|
-
|
|
64
|
-
The package code is MIT.
|
|
89
|
+
MIT.
|
package/generate.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Generate the icon package's source from icon-map.json and
|
|
2
|
+
// Generate the icon package's source from icon-map.json and a set of drawings.
|
|
3
3
|
//
|
|
4
4
|
// One module per icon, one barrel per category, and a root index that re-exports
|
|
5
5
|
// everything. That shape is what lets a bundler drop what an app never renders:
|
|
@@ -8,15 +8,21 @@
|
|
|
8
8
|
// fetched at install time - the bodies are written here as plain strings.
|
|
9
9
|
//
|
|
10
10
|
// Usage:
|
|
11
|
-
// node generate.mjs --
|
|
11
|
+
// node generate.mjs --set <path/to/icons.json> [--weight <name>] [--out src]
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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.
|
|
16
18
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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>/`.
|
|
22
|
+
//
|
|
23
|
+
// The set is a build-time input, not a dependency of the published package: it
|
|
24
|
+
// is read here and the ~400 bodies the map names are copied out. The full set
|
|
25
|
+
// is several thousand icons, of which this keeps about five percent.
|
|
20
26
|
import { fileURLToPath } from "node:url";
|
|
21
27
|
import { resolve, dirname } from "node:path";
|
|
22
28
|
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs";
|
|
@@ -29,10 +35,9 @@ const arg = (flag, fallback) => {
|
|
|
29
35
|
return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
|
|
30
36
|
};
|
|
31
37
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.error("usage: node generate.mjs --solar <path/to/solar-icons.json> [--out src]");
|
|
38
|
+
const SET = arg("--set", null);
|
|
39
|
+
if (!SET || !existsSync(SET)) {
|
|
40
|
+
console.error("usage: node generate.mjs --set <path/to/icons.json> [--weight <name>] [--out src]");
|
|
36
41
|
process.exit(1);
|
|
37
42
|
}
|
|
38
43
|
|
|
@@ -52,7 +57,7 @@ export type IconProps = {
|
|
|
52
57
|
* Accepted and ignored. These icons are filled, not stroked, so there is no
|
|
53
58
|
* line to widen - and forwarding a number into the SVG \`stroke\` attribute,
|
|
54
59
|
* which takes a COLOUR, paints the icon with an invalid value until it
|
|
55
|
-
* disappears. Kept in the signature so a stroked
|
|
60
|
+
* disappears. Kept in the signature so a stroked weight can be swapped in
|
|
56
61
|
* without touching the call sites that pass it.
|
|
57
62
|
*/
|
|
58
63
|
stroke?: number | string;
|
|
@@ -69,8 +74,8 @@ export type Icon = ForwardRefExoticComponent<IconProps & RefAttributes<SVGSVGEle
|
|
|
69
74
|
*
|
|
70
75
|
* The body is a constant written by the generator, never anything a caller
|
|
71
76
|
* supplies, which is what makes dangerouslySetInnerHTML the right tool here
|
|
72
|
-
* rather than a hole.
|
|
73
|
-
*
|
|
77
|
+
* rather than a hole. Every path carries fill="currentColor", so colour comes
|
|
78
|
+
* from the surrounding text colour and nothing else.
|
|
74
79
|
*/
|
|
75
80
|
export function icon(name: string, body: string): Icon {
|
|
76
81
|
const Component = forwardRef<SVGSVGElement, IconProps>(
|
|
@@ -94,34 +99,70 @@ export function icon(name: string, body: string): Icon {
|
|
|
94
99
|
}
|
|
95
100
|
`;
|
|
96
101
|
|
|
97
|
-
const { icons, brands } = JSON.parse(readFileSync(MAP, "utf8"));
|
|
98
|
-
const
|
|
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
|
+
}
|
|
99
112
|
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
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
|
+
|
|
129
|
+
const drawings = JSON.parse(readFileSync(SET, "utf8")).icons;
|
|
130
|
+
|
|
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)]);
|
|
103
140
|
if (missing.length > 0) {
|
|
104
141
|
console.error("Names not present in the icon set:");
|
|
105
|
-
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)}`);
|
|
106
143
|
process.exit(1);
|
|
107
144
|
}
|
|
108
145
|
|
|
109
|
-
rmSync(OUT, { recursive: true, force: true });
|
|
146
|
+
rmSync(resolve(OUT, "icons"), { recursive: true, force: true });
|
|
147
|
+
rmSync(resolve(OUT, "categories"), { recursive: true, force: true });
|
|
110
148
|
mkdirSync(resolve(OUT, "icons"), { recursive: true });
|
|
111
149
|
mkdirSync(resolve(OUT, "categories"), { recursive: true });
|
|
112
150
|
|
|
113
|
-
|
|
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");
|
|
114
154
|
|
|
115
155
|
const names = Object.keys(icons).sort();
|
|
116
156
|
const byCategory = new Map();
|
|
117
157
|
let bytes = 0;
|
|
118
158
|
|
|
119
159
|
for (const name of names) {
|
|
120
|
-
const {
|
|
121
|
-
const
|
|
160
|
+
const { category } = icons[name];
|
|
161
|
+
const glyph = glyphFor(icons[name]);
|
|
162
|
+
const body = drawings[glyph].body;
|
|
122
163
|
bytes += body.length;
|
|
123
|
-
const file = `// ${name} - ${
|
|
124
|
-
import { icon } from "
|
|
164
|
+
const file = `// ${name} - ${glyph}. GENERATED, do not hand-edit.
|
|
165
|
+
import { icon } from "${FACTORY_IMPORT}";
|
|
125
166
|
|
|
126
167
|
export const ${name} = icon(${JSON.stringify(name)}, ${JSON.stringify(body)});
|
|
127
168
|
`;
|
|
@@ -139,26 +180,28 @@ for (const category of categories) {
|
|
|
139
180
|
}
|
|
140
181
|
|
|
141
182
|
const index = [
|
|
142
|
-
|
|
183
|
+
`// GENERATED, do not hand-edit. Run: node generate.mjs --set <set.json> --weight ${WEIGHT}`,
|
|
143
184
|
"//",
|
|
144
185
|
"// Import from the root for a single icon (the bundler keeps only that module),",
|
|
145
186
|
"// or from a category when you want the group:",
|
|
146
187
|
"// import { IconMapPin } from \"@<scope>/icons\";",
|
|
147
188
|
"// import { IconMapPin } from \"@<scope>/icons/categories/map\";",
|
|
148
189
|
"",
|
|
149
|
-
"export type { Icon, IconProps } from \"./icon\";",
|
|
150
|
-
"",
|
|
151
190
|
];
|
|
191
|
+
if (isDefault) index.push("export type { Icon, IconProps } from \"./icon\";", "");
|
|
152
192
|
for (const name of names) index.push(`export { ${name} } from "./icons/${name}";`);
|
|
153
|
-
|
|
154
|
-
index.push("//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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\";");
|
|
160
203
|
}
|
|
161
|
-
index.push("
|
|
204
|
+
index.push("");
|
|
162
205
|
writeFileSync(resolve(OUT, "index.ts"), index.join("\n"), "utf8");
|
|
163
206
|
|
|
164
207
|
// A namespace export needs an identifier, so the category's display name becomes
|
|
@@ -170,6 +213,7 @@ for (const category of categories) {
|
|
|
170
213
|
}
|
|
171
214
|
writeFileSync(resolve(OUT, "categories", "index.ts"), `${catIndex.join("\n")}\n`, "utf8");
|
|
172
215
|
|
|
216
|
+
const brandNote = isDefault ? `, ${brands.length} brand re-exports` : "";
|
|
173
217
|
process.stdout.write(
|
|
174
|
-
`${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`,
|
|
175
219
|
);
|