@nine-thirty-five/material-symbols-react 2.1.5 → 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.
Files changed (92) hide show
  1. package/README.md +34 -3
  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 +1 -0
  9. package/dist/outlined/100/filled/index.js +1 -0
  10. package/dist/outlined/100/index.d.ts +1 -0
  11. package/dist/outlined/100/index.js +1 -0
  12. package/dist/outlined/200/filled/index.d.ts +1 -0
  13. package/dist/outlined/200/filled/index.js +1 -0
  14. package/dist/outlined/200/index.d.ts +1 -0
  15. package/dist/outlined/200/index.js +1 -0
  16. package/dist/outlined/300/filled/index.d.ts +1 -0
  17. package/dist/outlined/300/filled/index.js +1 -0
  18. package/dist/outlined/300/index.d.ts +1 -0
  19. package/dist/outlined/300/index.js +1 -0
  20. package/dist/outlined/400/filled/index.d.ts +1 -0
  21. package/dist/outlined/400/filled/index.js +1 -0
  22. package/dist/outlined/400/index.d.ts +1 -0
  23. package/dist/outlined/400/index.js +1 -0
  24. package/dist/outlined/500/filled/index.d.ts +1 -0
  25. package/dist/outlined/500/filled/index.js +1 -0
  26. package/dist/outlined/500/index.d.ts +1 -0
  27. package/dist/outlined/500/index.js +1 -0
  28. package/dist/outlined/600/filled/index.d.ts +1 -0
  29. package/dist/outlined/600/filled/index.js +1 -0
  30. package/dist/outlined/600/index.d.ts +1 -0
  31. package/dist/outlined/600/index.js +1 -0
  32. package/dist/outlined/700/filled/index.d.ts +1 -0
  33. package/dist/outlined/700/filled/index.js +1 -0
  34. package/dist/outlined/700/index.d.ts +1 -0
  35. package/dist/outlined/700/index.js +1 -0
  36. package/dist/rounded/100/filled/index.d.ts +1 -0
  37. package/dist/rounded/100/filled/index.js +1 -0
  38. package/dist/rounded/100/index.d.ts +1 -0
  39. package/dist/rounded/100/index.js +1 -0
  40. package/dist/rounded/200/filled/index.d.ts +1 -0
  41. package/dist/rounded/200/filled/index.js +1 -0
  42. package/dist/rounded/200/index.d.ts +1 -0
  43. package/dist/rounded/200/index.js +1 -0
  44. package/dist/rounded/300/filled/index.d.ts +1 -0
  45. package/dist/rounded/300/filled/index.js +1 -0
  46. package/dist/rounded/300/index.d.ts +1 -0
  47. package/dist/rounded/300/index.js +1 -0
  48. package/dist/rounded/400/filled/index.d.ts +1 -0
  49. package/dist/rounded/400/filled/index.js +1 -0
  50. package/dist/rounded/400/index.d.ts +1 -0
  51. package/dist/rounded/400/index.js +1 -0
  52. package/dist/rounded/500/filled/index.d.ts +1 -0
  53. package/dist/rounded/500/filled/index.js +1 -0
  54. package/dist/rounded/500/index.d.ts +1 -0
  55. package/dist/rounded/500/index.js +1 -0
  56. package/dist/rounded/600/filled/index.d.ts +1 -0
  57. package/dist/rounded/600/filled/index.js +1 -0
  58. package/dist/rounded/600/index.d.ts +1 -0
  59. package/dist/rounded/600/index.js +1 -0
  60. package/dist/rounded/700/filled/index.d.ts +1 -0
  61. package/dist/rounded/700/filled/index.js +1 -0
  62. package/dist/rounded/700/index.d.ts +1 -0
  63. package/dist/rounded/700/index.js +1 -0
  64. package/dist/sharp/100/filled/index.d.ts +1 -0
  65. package/dist/sharp/100/filled/index.js +1 -0
  66. package/dist/sharp/100/index.d.ts +1 -0
  67. package/dist/sharp/100/index.js +1 -0
  68. package/dist/sharp/200/filled/index.d.ts +1 -0
  69. package/dist/sharp/200/filled/index.js +1 -0
  70. package/dist/sharp/200/index.d.ts +1 -0
  71. package/dist/sharp/200/index.js +1 -0
  72. package/dist/sharp/300/filled/index.d.ts +1 -0
  73. package/dist/sharp/300/filled/index.js +1 -0
  74. package/dist/sharp/300/index.d.ts +1 -0
  75. package/dist/sharp/300/index.js +1 -0
  76. package/dist/sharp/400/filled/index.d.ts +1 -0
  77. package/dist/sharp/400/filled/index.js +1 -0
  78. package/dist/sharp/400/index.d.ts +1 -0
  79. package/dist/sharp/400/index.js +1 -0
  80. package/dist/sharp/500/filled/index.d.ts +1 -0
  81. package/dist/sharp/500/filled/index.js +1 -0
  82. package/dist/sharp/500/index.d.ts +1 -0
  83. package/dist/sharp/500/index.js +1 -0
  84. package/dist/sharp/600/filled/index.d.ts +1 -0
  85. package/dist/sharp/600/filled/index.js +1 -0
  86. package/dist/sharp/600/index.d.ts +1 -0
  87. package/dist/sharp/600/index.js +1 -0
  88. package/dist/sharp/700/filled/index.d.ts +1 -0
  89. package/dist/sharp/700/filled/index.js +1 -0
  90. package/dist/sharp/700/index.d.ts +1 -0
  91. package/dist/sharp/700/index.js +1 -0
  92. 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. If you need
152
- to consume it from a CommonJS module under `node16`/`nodenext` resolution, use a dynamic
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();
@@ -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
  });