@nine-thirty-five/material-symbols-react 2.1.4 → 2.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 +34 -3
- package/bin/find.js +78 -0
- package/bin/search.d.ts +18 -0
- package/bin/search.js +47 -0
- package/dist/_base.d.ts +6 -0
- package/dist/_base.js +13 -1
- package/dist/manifest.json +1 -1
- package/dist/outlined/100/filled/index.d.ts +1 -0
- package/dist/outlined/100/filled/index.js +1 -0
- package/dist/outlined/100/index.d.ts +1 -0
- package/dist/outlined/100/index.js +1 -0
- package/dist/outlined/200/filled/index.d.ts +1 -0
- package/dist/outlined/200/filled/index.js +1 -0
- package/dist/outlined/200/index.d.ts +1 -0
- package/dist/outlined/200/index.js +1 -0
- package/dist/outlined/300/filled/index.d.ts +1 -0
- package/dist/outlined/300/filled/index.js +1 -0
- package/dist/outlined/300/index.d.ts +1 -0
- package/dist/outlined/300/index.js +1 -0
- package/dist/outlined/400/filled/index.d.ts +1 -0
- package/dist/outlined/400/filled/index.js +1 -0
- package/dist/outlined/400/index.d.ts +1 -0
- package/dist/outlined/400/index.js +1 -0
- package/dist/outlined/500/filled/index.d.ts +1 -0
- package/dist/outlined/500/filled/index.js +1 -0
- package/dist/outlined/500/index.d.ts +1 -0
- package/dist/outlined/500/index.js +1 -0
- package/dist/outlined/600/filled/index.d.ts +1 -0
- package/dist/outlined/600/filled/index.js +1 -0
- package/dist/outlined/600/index.d.ts +1 -0
- package/dist/outlined/600/index.js +1 -0
- package/dist/outlined/700/filled/index.d.ts +1 -0
- package/dist/outlined/700/filled/index.js +1 -0
- package/dist/outlined/700/index.d.ts +1 -0
- package/dist/outlined/700/index.js +1 -0
- package/dist/rounded/100/filled/index.d.ts +1 -0
- package/dist/rounded/100/filled/index.js +1 -0
- package/dist/rounded/100/index.d.ts +1 -0
- package/dist/rounded/100/index.js +1 -0
- package/dist/rounded/200/filled/index.d.ts +1 -0
- package/dist/rounded/200/filled/index.js +1 -0
- package/dist/rounded/200/index.d.ts +1 -0
- package/dist/rounded/200/index.js +1 -0
- package/dist/rounded/300/filled/index.d.ts +1 -0
- package/dist/rounded/300/filled/index.js +1 -0
- package/dist/rounded/300/index.d.ts +1 -0
- package/dist/rounded/300/index.js +1 -0
- package/dist/rounded/400/filled/index.d.ts +1 -0
- package/dist/rounded/400/filled/index.js +1 -0
- package/dist/rounded/400/index.d.ts +1 -0
- package/dist/rounded/400/index.js +1 -0
- package/dist/rounded/500/filled/index.d.ts +1 -0
- package/dist/rounded/500/filled/index.js +1 -0
- package/dist/rounded/500/index.d.ts +1 -0
- package/dist/rounded/500/index.js +1 -0
- package/dist/rounded/600/filled/index.d.ts +1 -0
- package/dist/rounded/600/filled/index.js +1 -0
- package/dist/rounded/600/index.d.ts +1 -0
- package/dist/rounded/600/index.js +1 -0
- package/dist/rounded/700/filled/index.d.ts +1 -0
- package/dist/rounded/700/filled/index.js +1 -0
- package/dist/rounded/700/index.d.ts +1 -0
- package/dist/rounded/700/index.js +1 -0
- package/dist/sharp/100/filled/index.d.ts +1 -0
- package/dist/sharp/100/filled/index.js +1 -0
- package/dist/sharp/100/index.d.ts +1 -0
- package/dist/sharp/100/index.js +1 -0
- package/dist/sharp/200/filled/index.d.ts +1 -0
- package/dist/sharp/200/filled/index.js +1 -0
- package/dist/sharp/200/index.d.ts +1 -0
- package/dist/sharp/200/index.js +1 -0
- package/dist/sharp/300/filled/index.d.ts +1 -0
- package/dist/sharp/300/filled/index.js +1 -0
- package/dist/sharp/300/index.d.ts +1 -0
- package/dist/sharp/300/index.js +1 -0
- package/dist/sharp/400/filled/index.d.ts +1 -0
- package/dist/sharp/400/filled/index.js +1 -0
- package/dist/sharp/400/index.d.ts +1 -0
- package/dist/sharp/400/index.js +1 -0
- package/dist/sharp/500/filled/index.d.ts +1 -0
- package/dist/sharp/500/filled/index.js +1 -0
- package/dist/sharp/500/index.d.ts +1 -0
- package/dist/sharp/500/index.js +1 -0
- package/dist/sharp/600/filled/index.d.ts +1 -0
- package/dist/sharp/600/filled/index.js +1 -0
- package/dist/sharp/600/index.d.ts +1 -0
- package/dist/sharp/600/index.js +1 -0
- package/dist/sharp/700/filled/index.d.ts +1 -0
- package/dist/sharp/700/filled/index.js +1 -0
- package/dist/sharp/700/index.d.ts +1 -0
- package/dist/sharp/700/index.js +1 -0
- package/package.json +66 -62
package/README.md
CHANGED
|
@@ -96,6 +96,19 @@ Every icon accepts all SVG props (`SVGProps<SVGSVGElement>`), plus a convenience
|
|
|
96
96
|
- **`size`** — `number | string`. Sets both `width` and `height`. Defaults to `"1em"`, so icons
|
|
97
97
|
scale with the surrounding `font-size`. Pass `width`/`height` explicitly to override.
|
|
98
98
|
- **Color** — icons render with `fill="currentColor"`, so they inherit `color` from CSS.
|
|
99
|
+
- **`title`** — accessible label, rendered as an SVG `<title>` with `role="img"`.
|
|
100
|
+
|
|
101
|
+
### Accessibility
|
|
102
|
+
|
|
103
|
+
Icons are **decorative by default**: they render with `aria-hidden="true"` so screen readers skip
|
|
104
|
+
them. To make an icon meaningful, give it a label — via `title`, `aria-label`, or
|
|
105
|
+
`aria-labelledby` — and it switches to `role="img"` with the label as its accessible name:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
<Delete /> {/* decorative: aria-hidden="true" */}
|
|
109
|
+
<Delete title="Delete item" /> {/* semantic: role="img" + <title> */}
|
|
110
|
+
<Delete aria-label="Delete item" />
|
|
111
|
+
```
|
|
99
112
|
|
|
100
113
|
## Tree-shaking & bundle size
|
|
101
114
|
|
|
@@ -146,11 +159,29 @@ The catalog is also importable directly:
|
|
|
146
159
|
import manifest from '@nine-thirty-five/material-symbols-react/manifest.json' with { type: 'json' };
|
|
147
160
|
```
|
|
148
161
|
|
|
162
|
+
### Search from the command line
|
|
163
|
+
|
|
164
|
+
The package ships a tiny dependency-free CLI over the same catalog:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx @nine-thirty-five/material-symbols-react find trash
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
Delete delete [UI actions]
|
|
172
|
+
DeleteForever delete_forever [UI actions]
|
|
173
|
+
…
|
|
174
|
+
|
|
175
|
+
import { Delete } from '@nine-thirty-five/material-symbols-react/outlined';
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Options: `--limit/-n <N>` (default 10), `--json` for machine-readable output.
|
|
179
|
+
|
|
149
180
|
## ESM only
|
|
150
181
|
|
|
151
|
-
This package ships **ES Modules only**. It works in all modern bundlers and ESM Node.
|
|
152
|
-
|
|
153
|
-
`import()`.
|
|
182
|
+
This package ships **ES Modules only**. It works in all modern bundlers and ESM Node. On
|
|
183
|
+
Node ≥ 22, CommonJS code can `require()` it directly thanks to `require(esm)`; on older Node or
|
|
184
|
+
under `node16`/`nodenext` TypeScript resolution from CJS, use a dynamic `import()`.
|
|
154
185
|
|
|
155
186
|
## Migrating from v1
|
|
156
187
|
|
package/bin/find.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { searchIcons } from './search.js';
|
|
4
|
+
|
|
5
|
+
const USAGE = `Search the Material Symbols catalog shipped with this package.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
npx @nine-thirty-five/material-symbols-react find <query…> [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--limit, -n <N> Maximum results to print (default 10)
|
|
12
|
+
--json Print raw JSON (name, component, categories, tags)
|
|
13
|
+
--help, -h Show this help
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
npx @nine-thirty-five/material-symbols-react find trash
|
|
17
|
+
npx @nine-thirty-five/material-symbols-react find arrow back -n 5
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
function main() {
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
if (argv[0] === 'find' || argv[0] === 'search') argv.shift();
|
|
23
|
+
|
|
24
|
+
let limit = 10;
|
|
25
|
+
let json = false;
|
|
26
|
+
const query = [];
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const a = argv[i];
|
|
29
|
+
if (a === '--help' || a === '-h') {
|
|
30
|
+
console.log(USAGE);
|
|
31
|
+
return;
|
|
32
|
+
} else if (a === '--json') {
|
|
33
|
+
json = true;
|
|
34
|
+
} else if (a === '--limit' || a === '-n') {
|
|
35
|
+
limit = parseInt(argv[++i], 10) || 10;
|
|
36
|
+
} else {
|
|
37
|
+
query.push(a);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (query.length === 0) {
|
|
41
|
+
console.log(USAGE);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// bin/ and dist/ are siblings both in the published package and in the repo.
|
|
47
|
+
const manifestUrl = new URL('../dist/manifest.json', import.meta.url);
|
|
48
|
+
if (!fs.existsSync(manifestUrl)) {
|
|
49
|
+
console.error(
|
|
50
|
+
'dist/manifest.json not found — in a repo checkout, run `npm run generate:sample` first.'
|
|
51
|
+
);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const manifest = JSON.parse(fs.readFileSync(manifestUrl, 'utf8'));
|
|
56
|
+
const results = searchIcons(manifest.icons, query, limit);
|
|
57
|
+
|
|
58
|
+
if (json) {
|
|
59
|
+
console.log(JSON.stringify(results, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (results.length === 0) {
|
|
63
|
+
console.log(`No icons match "${query.join(' ')}".`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const width = Math.max(...results.map((r) => r.component.length));
|
|
68
|
+
for (const r of results) {
|
|
69
|
+
const cats = r.categories?.length ? ` [${r.categories.join(', ')}]` : '';
|
|
70
|
+
console.log(`${r.component.padEnd(width)} ${r.name}${cats}`);
|
|
71
|
+
}
|
|
72
|
+
console.log(
|
|
73
|
+
`\nimport { ${results[0].component} } from '${manifest.package}/outlined';`
|
|
74
|
+
);
|
|
75
|
+
console.log(`Pattern: ${manifest.importPattern}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main();
|
package/bin/search.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ManifestIcon {
|
|
2
|
+
/** Material Symbols name (snake_case). */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Exported component identifier (PascalCase). */
|
|
5
|
+
component: string;
|
|
6
|
+
categories: string[];
|
|
7
|
+
tags: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Rank `icons` against query terms. Every term must match somewhere
|
|
12
|
+
* (name, component, tags or categories) for an icon to qualify.
|
|
13
|
+
*/
|
|
14
|
+
export declare function searchIcons(
|
|
15
|
+
icons: ManifestIcon[],
|
|
16
|
+
query: string[],
|
|
17
|
+
limit?: number
|
|
18
|
+
): ManifestIcon[];
|
package/bin/search.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search over the manifest.json icon catalog that ships with the package.
|
|
3
|
+
* Pure and dependency-free so it can be unit-tested without a generated dist.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Score one icon against a single lower-cased term. 0 = no match. */
|
|
7
|
+
function scoreTerm(icon, term) {
|
|
8
|
+
const name = icon.name.toLowerCase();
|
|
9
|
+
const component = icon.component.toLowerCase();
|
|
10
|
+
if (name === term || component === term) return 100;
|
|
11
|
+
if (name.split('_').includes(term)) return 50;
|
|
12
|
+
if (name.includes(term) || component.includes(term)) return 30;
|
|
13
|
+
const tags = icon.tags ?? [];
|
|
14
|
+
if (tags.some((t) => t.toLowerCase() === term)) return 25;
|
|
15
|
+
if (tags.some((t) => t.toLowerCase().includes(term))) return 10;
|
|
16
|
+
const categories = icon.categories ?? [];
|
|
17
|
+
if (categories.some((c) => c.toLowerCase().includes(term))) return 5;
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rank icons against query terms. Every term must match somewhere
|
|
23
|
+
* (name, component, tags or categories) for an icon to qualify.
|
|
24
|
+
*/
|
|
25
|
+
export function searchIcons(icons, query, limit = 10) {
|
|
26
|
+
const terms = query.map((t) => t.toLowerCase().trim()).filter(Boolean);
|
|
27
|
+
if (terms.length === 0) return [];
|
|
28
|
+
|
|
29
|
+
const scored = [];
|
|
30
|
+
for (const icon of icons) {
|
|
31
|
+
let total = 0;
|
|
32
|
+
for (const term of terms) {
|
|
33
|
+
const s = scoreTerm(icon, term);
|
|
34
|
+
if (s === 0) {
|
|
35
|
+
total = 0;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
total += s;
|
|
39
|
+
}
|
|
40
|
+
if (total > 0) scored.push({ icon, score: total });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
scored.sort(
|
|
44
|
+
(a, b) => b.score - a.score || a.icon.name.localeCompare(b.icon.name)
|
|
45
|
+
);
|
|
46
|
+
return scored.slice(0, limit).map((s) => s.icon);
|
|
47
|
+
}
|
package/dist/_base.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import type { ForwardRefExoticComponent, RefAttributes, SVGProps } from 'react';
|
|
|
3
3
|
export interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'ref'> {
|
|
4
4
|
/** Sets both width and height. Defaults to "1em" so icons scale with font-size. */
|
|
5
5
|
size?: number | string;
|
|
6
|
+
/**
|
|
7
|
+
* Accessible label, rendered as an SVG `<title>` with `role="img"`.
|
|
8
|
+
* Without it (or `aria-label`/`aria-labelledby`) the icon is decorative
|
|
9
|
+
* and gets `aria-hidden="true"`.
|
|
10
|
+
*/
|
|
11
|
+
title?: string;
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
export type Icon = ForwardRefExoticComponent<
|
package/dist/_base.js
CHANGED
|
@@ -9,11 +9,20 @@ import { createElement, forwardRef } from 'react';
|
|
|
9
9
|
* Wrapped in `forwardRef` so `<Icon ref={…} />` reaches the underlying `<svg>`
|
|
10
10
|
* in both React 18 and 19.
|
|
11
11
|
*
|
|
12
|
+
* Accessibility: icons are decorative by default (`aria-hidden="true"`).
|
|
13
|
+
* Passing `title`, `aria-label` or `aria-labelledby` makes the icon
|
|
14
|
+
* semantic instead: it gets `role="img"`, and `title` renders an SVG
|
|
15
|
+
* `<title>` as the accessible name.
|
|
16
|
+
*
|
|
12
17
|
* @param {string} d - SVG path data.
|
|
13
18
|
* @returns {import('./base').Icon}
|
|
14
19
|
*/
|
|
15
20
|
const base = (d) =>
|
|
16
|
-
forwardRef(function Icon({ size = '1em', ...props }, ref) {
|
|
21
|
+
forwardRef(function Icon({ size = '1em', title, ...props }, ref) {
|
|
22
|
+
const labelled =
|
|
23
|
+
title != null ||
|
|
24
|
+
props['aria-label'] != null ||
|
|
25
|
+
props['aria-labelledby'] != null;
|
|
17
26
|
return createElement(
|
|
18
27
|
'svg',
|
|
19
28
|
{
|
|
@@ -22,9 +31,12 @@ const base = (d) =>
|
|
|
22
31
|
width: size,
|
|
23
32
|
height: size,
|
|
24
33
|
fill: 'currentColor',
|
|
34
|
+
'aria-hidden': labelled ? undefined : true,
|
|
35
|
+
role: labelled ? 'img' : undefined,
|
|
25
36
|
...props,
|
|
26
37
|
ref,
|
|
27
38
|
},
|
|
39
|
+
title ? createElement('title', null, title) : null,
|
|
28
40
|
createElement('path', { d })
|
|
29
41
|
);
|
|
30
42
|
});
|