@nine-thirty-five/material-symbols-react 2.1.5 → 2.2.1

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.
Files changed (92) hide show
  1. package/README.md +36 -6
  2. package/bin/find.js +78 -0
  3. package/bin/search.d.ts +18 -0
  4. package/bin/search.js +47 -0
  5. package/dist/_base.d.ts +6 -0
  6. package/dist/_base.js +13 -1
  7. package/dist/manifest.json +1 -1
  8. package/dist/outlined/100/filled/index.d.ts +2 -0
  9. package/dist/outlined/100/filled/index.js +9 -7
  10. package/dist/outlined/100/index.d.ts +2 -0
  11. package/dist/outlined/100/index.js +9 -7
  12. package/dist/outlined/200/filled/index.d.ts +2 -0
  13. package/dist/outlined/200/filled/index.js +9 -7
  14. package/dist/outlined/200/index.d.ts +2 -0
  15. package/dist/outlined/200/index.js +9 -7
  16. package/dist/outlined/300/filled/index.d.ts +2 -0
  17. package/dist/outlined/300/filled/index.js +9 -7
  18. package/dist/outlined/300/index.d.ts +2 -0
  19. package/dist/outlined/300/index.js +9 -7
  20. package/dist/outlined/400/filled/index.d.ts +2 -0
  21. package/dist/outlined/400/filled/index.js +9 -7
  22. package/dist/outlined/400/index.d.ts +2 -0
  23. package/dist/outlined/400/index.js +9 -7
  24. package/dist/outlined/500/filled/index.d.ts +2 -0
  25. package/dist/outlined/500/filled/index.js +9 -7
  26. package/dist/outlined/500/index.d.ts +2 -0
  27. package/dist/outlined/500/index.js +9 -7
  28. package/dist/outlined/600/filled/index.d.ts +2 -0
  29. package/dist/outlined/600/filled/index.js +9 -7
  30. package/dist/outlined/600/index.d.ts +2 -0
  31. package/dist/outlined/600/index.js +9 -7
  32. package/dist/outlined/700/filled/index.d.ts +2 -0
  33. package/dist/outlined/700/filled/index.js +9 -7
  34. package/dist/outlined/700/index.d.ts +2 -0
  35. package/dist/outlined/700/index.js +9 -7
  36. package/dist/rounded/100/filled/index.d.ts +2 -0
  37. package/dist/rounded/100/filled/index.js +9 -7
  38. package/dist/rounded/100/index.d.ts +2 -0
  39. package/dist/rounded/100/index.js +9 -7
  40. package/dist/rounded/200/filled/index.d.ts +2 -0
  41. package/dist/rounded/200/filled/index.js +9 -7
  42. package/dist/rounded/200/index.d.ts +2 -0
  43. package/dist/rounded/200/index.js +9 -7
  44. package/dist/rounded/300/filled/index.d.ts +2 -0
  45. package/dist/rounded/300/filled/index.js +9 -7
  46. package/dist/rounded/300/index.d.ts +2 -0
  47. package/dist/rounded/300/index.js +9 -7
  48. package/dist/rounded/400/filled/index.d.ts +2 -0
  49. package/dist/rounded/400/filled/index.js +9 -7
  50. package/dist/rounded/400/index.d.ts +2 -0
  51. package/dist/rounded/400/index.js +9 -7
  52. package/dist/rounded/500/filled/index.d.ts +2 -0
  53. package/dist/rounded/500/filled/index.js +9 -7
  54. package/dist/rounded/500/index.d.ts +2 -0
  55. package/dist/rounded/500/index.js +9 -7
  56. package/dist/rounded/600/filled/index.d.ts +2 -0
  57. package/dist/rounded/600/filled/index.js +9 -7
  58. package/dist/rounded/600/index.d.ts +2 -0
  59. package/dist/rounded/600/index.js +9 -7
  60. package/dist/rounded/700/filled/index.d.ts +2 -0
  61. package/dist/rounded/700/filled/index.js +9 -7
  62. package/dist/rounded/700/index.d.ts +2 -0
  63. package/dist/rounded/700/index.js +9 -7
  64. package/dist/sharp/100/filled/index.d.ts +2 -0
  65. package/dist/sharp/100/filled/index.js +9 -7
  66. package/dist/sharp/100/index.d.ts +2 -0
  67. package/dist/sharp/100/index.js +9 -7
  68. package/dist/sharp/200/filled/index.d.ts +2 -0
  69. package/dist/sharp/200/filled/index.js +9 -7
  70. package/dist/sharp/200/index.d.ts +2 -0
  71. package/dist/sharp/200/index.js +9 -7
  72. package/dist/sharp/300/filled/index.d.ts +2 -0
  73. package/dist/sharp/300/filled/index.js +9 -7
  74. package/dist/sharp/300/index.d.ts +2 -0
  75. package/dist/sharp/300/index.js +9 -7
  76. package/dist/sharp/400/filled/index.d.ts +2 -0
  77. package/dist/sharp/400/filled/index.js +9 -7
  78. package/dist/sharp/400/index.d.ts +2 -0
  79. package/dist/sharp/400/index.js +9 -7
  80. package/dist/sharp/500/filled/index.d.ts +2 -0
  81. package/dist/sharp/500/filled/index.js +9 -7
  82. package/dist/sharp/500/index.d.ts +2 -0
  83. package/dist/sharp/500/index.js +9 -7
  84. package/dist/sharp/600/filled/index.d.ts +2 -0
  85. package/dist/sharp/600/filled/index.js +9 -7
  86. package/dist/sharp/600/index.d.ts +2 -0
  87. package/dist/sharp/600/index.js +9 -7
  88. package/dist/sharp/700/filled/index.d.ts +2 -0
  89. package/dist/sharp/700/filled/index.js +9 -7
  90. package/dist/sharp/700/index.d.ts +2 -0
  91. package/dist/sharp/700/index.js +9 -7
  92. package/package.json +66 -62
package/README.md CHANGED
@@ -2,15 +2,14 @@
2
2
 
3
3
  Use [Google's Material Symbols](https://fonts.google.com/icons?icon.set=Material+Symbols) in React — **every style, weight and fill**, fully tree-shakeable, zero runtime dependencies.
4
4
 
5
- <div align="center">
5
+ Live example: [nine-thirty-five.github.io/material-symbols-react](https://nine-thirty-five.github.io/material-symbols-react)
6
+
6
7
 
7
8
  [![NPM version][npm-image]][npm-url]
8
9
  [![Downloads][download-image]][npm-downloads]
9
10
  ![npm-typescript]
10
11
  [![GitHub License](https://img.shields.io/badge/license-Apache--2.0-green)](./LICENSE)
11
12
 
12
- </div>
13
-
14
13
  - [Installation](#installation)
15
14
  - [Usage](#usage)
16
15
  - [Importing](#importing)
@@ -96,6 +95,19 @@ Every icon accepts all SVG props (`SVGProps<SVGSVGElement>`), plus a convenience
96
95
  - **`size`** — `number | string`. Sets both `width` and `height`. Defaults to `"1em"`, so icons
97
96
  scale with the surrounding `font-size`. Pass `width`/`height` explicitly to override.
98
97
  - **Color** — icons render with `fill="currentColor"`, so they inherit `color` from CSS.
98
+ - **`title`** — accessible label, rendered as an SVG `<title>` with `role="img"`.
99
+
100
+ ### Accessibility
101
+
102
+ Icons are **decorative by default**: they render with `aria-hidden="true"` so screen readers skip
103
+ them. To make an icon meaningful, give it a label — via `title`, `aria-label`, or
104
+ `aria-labelledby` — and it switches to `role="img"` with the label as its accessible name:
105
+
106
+ ```tsx
107
+ <Delete /> {/* decorative: aria-hidden="true" */}
108
+ <Delete title="Delete item" /> {/* semantic: role="img" + <title> */}
109
+ <Delete aria-label="Delete item" />
110
+ ```
99
111
 
100
112
  ## Tree-shaking & bundle size
101
113
 
@@ -146,11 +158,29 @@ The catalog is also importable directly:
146
158
  import manifest from '@nine-thirty-five/material-symbols-react/manifest.json' with { type: 'json' };
147
159
  ```
148
160
 
161
+ ### Search from the command line
162
+
163
+ The package ships a tiny dependency-free CLI over the same catalog:
164
+
165
+ ```bash
166
+ npx @nine-thirty-five/material-symbols-react find trash
167
+ ```
168
+
169
+ ```text
170
+ Delete delete [UI actions]
171
+ DeleteForever delete_forever [UI actions]
172
+
173
+
174
+ import { Delete } from '@nine-thirty-five/material-symbols-react/outlined';
175
+ ```
176
+
177
+ Options: `--limit/-n <N>` (default 10), `--json` for machine-readable output.
178
+
149
179
  ## ESM only
150
180
 
151
- This package ships **ES Modules only**. It works in all modern bundlers and ESM Node. If you need
152
- to consume it from a CommonJS module under `node16`/`nodenext` resolution, use a dynamic
153
- `import()`.
181
+ This package ships **ES Modules only**. It works in all modern bundlers and ESM Node. On
182
+ Node 22, CommonJS code can `require()` it directly thanks to `require(esm)`; on older Node or
183
+ under `node16`/`nodenext` TypeScript resolution from CJS, use a dynamic `import()`.
154
184
 
155
185
  ## Migrating from v1
156
186
 
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();
@@ -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
  });