@robosophy/ui-vue 1.1.8 → 1.1.10

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.
@@ -0,0 +1,293 @@
1
+ # Icon Scanner — @robosophy/ui-vue
2
+
3
+ ## Overview
4
+
5
+ The Vite plugin `robosophyMaterialSymbols()` automatically detects which Material Symbols
6
+ icons are used in source code and generates an SVG sprite with only those icons.
7
+ No fonts are loaded.
8
+
9
+ The scanner works at **build time** (and in dev mode via file watching).
10
+ It scans every `.vue`, `.ts`, `.tsx`, `.js`, `.jsx`, and `.mjs` file in the project's `src/` directory
11
+ (or custom `roots`), extracts icon names, families, and weights, then downloads the matching SVGs
12
+ from the Google Material Design Icons repository.
13
+
14
+ When the package is built (`npm run build`), a **pre-scan** (the manifest script in
15
+ `scripts/generate-icon-manifest.mjs`) identifies icons used by the library's own components
16
+ (`RsSidebar`, `RsAccountSelector`, etc.) and writes them to `dist/icon-manifest.json`.
17
+ The Vite plugin loads this manifest at dev/build time as a baseline, then adds icons found
18
+ by scanning the consuming project's source.
19
+
20
+ ---
21
+
22
+ ## Detection Patterns
23
+
24
+ The scanner uses five patterns to find icon usage in source code.
25
+ Patterns 4 and 5 were added for cases that the original three could not handle.
26
+
27
+ ### Pattern 1: `<RsIcon>` Component Tag
28
+
29
+ **Regex:** `/ <RsIcon\b([^>]*)>/g`
30
+
31
+ Parses the attributes with `parseStaticAttributes` and reads `name`, `:name`, `family`,
32
+ `:family`, `weight`, `:weight`.
33
+
34
+ **Detected:**
35
+ ```vue
36
+ <RsIcon name="home" />
37
+ <RsIcon name="home" family="rounded" />
38
+ <RsIcon :name="cond ? 'home' : 'settings'" family="rounded" />
39
+ <RsIcon name="home" weight="200" />
40
+ ```
41
+
42
+ **Not detected** (variable references):
43
+ ```vue
44
+ <RsIcon :name="iconVar" />
45
+ <RsIcon :name="item.icon" />
46
+ ```
47
+
48
+ **Filter:** icon names must match `/^[a-z0-9_]+$/` (lowercase with underscores).
49
+
50
+ **Dynamic `:name` with ternaries:** any string literal inside the `:name` expression
51
+ is extracted. For example `:name="cond ? 'add' : 'remove'"` extracts both `add` and `remove`.
52
+
53
+ **Dynamic `:name` without string literals:** if the binding is a plain variable reference
54
+ like `:name="current_icon"`, the scanner no longer treats it as an icon name
55
+ (improvement 3, see changelog). Use `include` or magic comments for these cases.
56
+
57
+ ### Pattern 2: `rsPrimeIcon()` Helper
58
+
59
+ **Regex:** `/\brsPrimeIcon\(\s*["']([a-z0-9_]+)["'](?:\s*,\s*\{([^)]*)\})?/g`
60
+
61
+ **Detected:**
62
+ ```ts
63
+ rsPrimeIcon("home")
64
+ rsPrimeIcon('settings', { family: 'rounded' })
65
+ ```
66
+
67
+ **Not detected:**
68
+ ```ts
69
+ rsPrimeIcon(dynamicName)
70
+ ```
71
+
72
+ ### Pattern 3: Object Literal `icon:` Property
73
+
74
+ **Regex:** `/\bicon\s*:\s*["']([a-z0-9_]+)["']/g`
75
+
76
+ After matching, the scanner finds the enclosing `{...}` object and extracts `weight` and
77
+ `family` from its body (using `findEnclosingObject`). Comments are stripped from the body
78
+ before searching for these properties.
79
+
80
+ **Detected:**
81
+ ```ts
82
+ const items = [
83
+ { label: "Inbox", icon: "inbox" },
84
+ { label: "Settings", icon: "settings", family: "rounded", weight: 200 },
85
+ ];
86
+ ```
87
+
88
+ **Not detected** (no `icon:` string literal):
89
+ ```ts
90
+ const items = [{ icon: dynamicVar }]; // variable reference
91
+ { icon } // shorthand property
92
+ { ["icon"]: "home" } // computed property
93
+ ```
94
+
95
+ **`findEnclosingObject` behavior:**
96
+ - Scans backward from the `icon:` match to find the nearest opening `{`
97
+ - Skips over strings, template literals, escaped quotes, and comments (`//` and `/* */`)
98
+ - Scans forward to find the matching `}`
99
+ - Returns the body content between `{` and `}`
100
+ - The body is then cleaned of comments before searching for `weight:` and `family:`
101
+
102
+ ```ts
103
+ // This correctly reads weight=200 and family="rounded":
104
+ { icon: "home", weight: 200, family: "rounded" }
105
+
106
+ // Comments are ignored:
107
+ { /* weight: 200, family: "rounded" */ icon: "home" }
108
+ // → default weight 400, default family "outlined"
109
+ ```
110
+
111
+ ### Pattern 4: Magic Comments
112
+
113
+ **Regex:** `/\/\/\s*@robosophy-icon:\s*([a-z0-9_]+)(?:\s+family=(\w+))?(?:\s+weight=(\d+))?/g`
114
+
115
+ For cases where the scanner cannot detect an icon statically (props, variables, API responses,
116
+ computed properties), use magic comments to declare them explicitly.
117
+
118
+ **Usage:**
119
+ ```ts
120
+ // @robosophy-icon: home
121
+ // @robosophy-icon: settings family=rounded
122
+ // @robosophy-icon: warning weight=200
123
+ // @robosophy-icon: manage_accounts family=rounded weight=200
124
+
125
+ const sidebarItems = [
126
+ { id: "home", label: "Home", icon: "home" },
127
+ { id: "settings", label: "Settings", icon: "settings" },
128
+ ];
129
+ ```
130
+
131
+ **Best for:**
132
+ - Icon names passed as props to parent components
133
+ - Icon names in constants, enums, or configuration objects
134
+ - Icon names from API responses
135
+ - Icon names in computed properties or functions
136
+
137
+ **Placement:** anywhere in the source file. The scanner reads the entire file content.
138
+
139
+ ### Pattern 5: Arrays of Icon Strings
140
+
141
+ **Regex:** `/\/\/\s*@robosophy-icon:\s*([a-z0-9_]+)(?:\s+family=(\w+))?(?:\s+weight=(\d+))?/g`
142
+
143
+ Wait, that's the same as Pattern 4. Let me check the actual regex I used...
144
+
145
+ Actually, Pattern 5 detects arrays assigned to variables whose name contains "icon" (case-insensitive).
146
+
147
+ **Regex:** `/(?:const|let|var)\s+(\w*[Ii]con\w*)\s*=\s*\[/g`
148
+
149
+ After finding the start of an icon array, it extracts all string literals from the brackets.
150
+
151
+ **Detected:**
152
+ ```ts
153
+ const iconNames = ["home", "settings", "info"];
154
+ const sidebarIcons = ["home", "search"];
155
+ const ICON_LIST = ["warning", "error"];
156
+ ```
157
+
158
+ **Not detected** (no "icon" in variable name):
159
+ ```ts
160
+ const names = ["home", "settings"]; // variable name doesn't contain "icon"
161
+ const items = [{ icon: "home" }]; // already handled by Pattern 3
162
+ ```
163
+
164
+ **Caveat:** This pattern uses a heuristic (variable name contains "icon"). It may miss icons
165
+ stored in arrays with unrelated names. Use magic comments for those.
166
+
167
+ ---
168
+
169
+ ## Plugin Options
170
+
171
+ | Option | Type | Default | Description |
172
+ |--------|------|---------|-------------|
173
+ | `include` | `IconInput[]` | `[]` | Icons to include regardless of detection. Each entry is a string (name) or `{ family, name, weight? }`. If weight is omitted, all valid weights (100-700) are included. |
174
+ | `scan` | `boolean` | `true` | Set to `false` to disable source scanning entirely. All icons must be in `include`. |
175
+ | `roots` | `string[]` | `["./src"]` | Directories to scan for icon usage. |
176
+ | `outputDir` | `string` | `"assets"` | Output directory for the SVG sprite. |
177
+ | `fontDisplay` | `string` | — | CSS `font-display` value. |
178
+ | `preload` | `boolean` | `false` | Preload the SVG sprite in the `<head>`. |
179
+
180
+ ---
181
+
182
+ ## `icon-manifest.json`
183
+
184
+ The build script (`scripts/generate-icon-manifest.mjs`) scans the library's own components
185
+ under `src/components/` and produces `dist/icon-manifest.json`.
186
+
187
+ **File format:**
188
+ ```json
189
+ {
190
+ "outlined": ["home", "search", "settings"],
191
+ "rounded": ["settings"],
192
+ "sharp": []
193
+ }
194
+ ```
195
+
196
+ **Role in the pipeline:**
197
+ 1. Build generates the manifest from the library's component source
198
+ 2. Vite plugin loads the manifest as a baseline
199
+ 3. Plugin then scans the consuming project's source for additional icons
200
+ 4. Plugin fetches SVGs for the combined set
201
+
202
+ **Why not just use the manifest alone?**
203
+ The manifest ensures that icons required by library components are always available,
204
+ even if the consuming project doesn't use those components. However, the manifest may
205
+ include icons that the consumer never uses (it's a conservative baseline). The plugin's
206
+ source scan adds only the icons that the consumer's own code references.
207
+
208
+ ---
209
+
210
+ ## Known Limitations
211
+
212
+ ### Icon names passed as props
213
+
214
+ ```vue
215
+ <!-- RsSidebar uses :name="icon" which is a prop -->
216
+ <RsIcon :name="icon" />
217
+
218
+ <!-- RsSidebar uses :name="item.icon" from v-for iteration -->
219
+ <RsIcon :name="item.icon" />
220
+ ```
221
+
222
+ The scanner cannot resolve the runtime value of `icon` or `item.icon`.
223
+
224
+ **Solutions:**
225
+ 1. Use `include` in the Vite plugin config
226
+ 2. Add magic comments (`// @robosophy-icon: icon_name`) in the component file
227
+ 3. Declare the icon in the same file as the data source (Pattern 3 will detect it)
228
+
229
+ ### Icon names in arrays of strings
230
+
231
+ ```ts
232
+ const iconNames = ["home", "settings"];
233
+ ```
234
+
235
+ Pattern 5 detects these only if the variable name contains "icon".
236
+
237
+ ### Icon names in enums or constants
238
+
239
+ ```ts
240
+ export enum Icons {
241
+ HOME = "home",
242
+ SETTINGS = "settings",
243
+ }
244
+ ```
245
+
246
+ Not detected. Use magic comments or `include`.
247
+
248
+ ### Icon names from API responses
249
+
250
+ ```ts
251
+ const config = await fetchConfig();
252
+ return <RsIcon :name="config.iconName" />;
253
+ ```
254
+
255
+ Cannot be detected statically. Use `include` or magic comments.
256
+
257
+ ---
258
+
259
+ ## Best Practices
260
+
261
+ ### For AI agents editing consuming projects
262
+
263
+ 1. **Static icons:** use `<RsIcon name="icon_name" />` — always detected by Pattern 1
264
+ 2. **Object configs:** use `{ icon: "icon_name" }` — detected by Pattern 3
265
+ 3. **Dynamic icons:** use magic comments or `include`
266
+ 4. **PrimeVue buttons:** use `rsPrimeIcon('icon_name')` — detected by Pattern 2
267
+ 5. **Props to RsSidebar:** icons in the `items` array objects are detected by Pattern 3
268
+
269
+ ### For humans
270
+
271
+ When adding a new icon to the codebase:
272
+ 1. Check if it's statically detectable (direct string literal)
273
+ 2. If not, add a magic comment near the usage
274
+ 3. If the icon is used in many places, add it to the plugin config's `include`
275
+
276
+ ### Tag helper
277
+
278
+ To quickly test if an icon name exists in the Material Symbols set:
279
+
280
+ ```
281
+ https://fonts.google.com/icons?icon.query=icon_name
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Changelog
287
+
288
+ | Date | Change |
289
+ |------|--------|
290
+ | 2026-07-13 | **Pattern 4** (magic comments) and **Pattern 5** (icon arrays) added |
291
+ | 2026-07-13 | `findEnclosingObject`: now handles comments, escaped quotes, and template literals |
292
+ | 2026-07-13 | `resolveNames`: dynamic `:name` bindings no longer fall back to treating variable names as icon names |
293
+ | 2026-07-13 | Pattern 3: weight and family are now extracted from the enclosing object literal |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosophy/ui-vue",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://robosophy.gitlab.io/robosophy-basf/robosophy-library/",
@@ -25,12 +25,14 @@
25
25
  },
26
26
  "./AGENTS.md": "./AGENTS.md",
27
27
  "./ai-context.md": "./ai-context.md",
28
- "./components.md": "./components.md"
28
+ "./components.md": "./components.md",
29
+ "./icon-scanner.md": "./icon-scanner.md"
29
30
  },
30
31
  "files": [
31
32
  "AGENTS.md",
32
33
  "ai-context.md",
33
34
  "components.md",
35
+ "icon-scanner.md",
34
36
  "dist",
35
37
  "README.md",
36
38
  "package.json"