@symbo.ls/mcp 1.0.9 → 1.0.11

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,488 @@
1
+ # Symbols / DOMQL v3 — Strict Rules for AI Agents
2
+
3
+ You are working in a **Symbols.app** / DOMQL v3 project. These rules are absolute and override any general coding instincts. Violations cause silent failures (black page, nothing renders).
4
+
5
+ ---
6
+
7
+ ## CRITICAL: v3 Syntax Only
8
+
9
+ | v3 ✅ USE THIS | v2 ❌ NEVER USE |
10
+ | -------------------------- | -------------------------------- |
11
+ | `extends: 'Component'` | ~~`extend: 'Component'`~~ |
12
+ | `childExtends: 'Component'`| ~~`childExtend: 'Component'`~~ |
13
+ | `onClick: fn` | ~~`on: { click: fn }`~~ |
14
+ | `onRender: fn` | ~~`on: { render: fn }`~~ |
15
+ | props flattened at root | ~~`props: { ... }` wrapper~~ |
16
+ | individual prop functions | ~~`props: ({ state }) => ({})` function~~ |
17
+ | `flexAlign: 'center center'`| ~~`align: 'center center'`~~ |
18
+ | `children` + `childExtends`| ~~`$collection`, `$propsCollection`~~ |
19
+ | `children` + `childrenAs: 'state'`| ~~`$stateCollection`~~ |
20
+
21
+ ---
22
+
23
+ ## Rule 1 — Components are OBJECTS, never functions
24
+
25
+ ```js
26
+ // ✅ CORRECT
27
+ export const Header = { extends: 'Flex', padding: 'A' }
28
+
29
+ // ❌ WRONG — never do this
30
+ export const Header = (el, state) => ({ padding: 'A' })
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Rule 2 — NO imports between project files — EVER
36
+
37
+ Components reference each other by PascalCase key in the object tree. Never use `import` between `components/`, `pages/`, `functions/`, etc.
38
+
39
+ ```js
40
+ // ❌ WRONG
41
+ import { Navbar } from './Navbar.js'
42
+
43
+ // ✅ CORRECT — just use the key name in the tree
44
+ Nav: { extends: 'Navbar' }
45
+ ```
46
+
47
+ **Only exception**: `pages/index.js` is the route registry — imports ARE allowed there.
48
+
49
+ ```js
50
+ // pages/index.js — only file where imports are permitted
51
+ import { main } from './main.js'
52
+ export default { '/': main }
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Rule 3 — `components/index.js` — use `export *` NOT `export * as`
58
+
59
+ `export * as Foo` wraps everything in a namespace object and **breaks component resolution entirely**.
60
+
61
+ ```js
62
+ // ✅ CORRECT
63
+ export * from './Navbar.js'
64
+ export * from './PostCard.js'
65
+
66
+ // ❌ WRONG — breaks everything
67
+ export * as Navbar from './Navbar.js'
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Rule 4 — Pages extend `'Page'`, not `'Flex'` or `'Box'`
73
+
74
+ ```js
75
+ // ✅ CORRECT
76
+ export const main = { extends: 'Page', ... }
77
+
78
+ // ❌ WRONG
79
+ export const main = { extends: 'Flex', ... }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Rule 5 — All folders are flat — no subfolders
85
+
86
+ ```
87
+ components/Navbar.js ✅
88
+ components/nav/Navbar.js ❌
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Rule 6 — PascalCase keys = child components (auto-extends)
94
+
95
+ ```js
96
+ // The key "Hgroup" auto-extends the registered Hgroup component
97
+ export const MyCard = {
98
+ Hgroup: { // ← auto-extends: 'Hgroup' because key matches registered component
99
+ gap: '0', // ← override merges with base Hgroup
100
+ }
101
+ }
102
+ ```
103
+
104
+ If you need a different base, set `extends` explicitly.
105
+
106
+ ---
107
+
108
+ ## Rule 7 — State — use `s.update()`, never mutate directly
109
+
110
+ ```js
111
+ onClick: (e, el, s) => s.update({ count: s.count + 1 }) // ✅ CORRECT
112
+ onClick: (e, el, s) => { s.count = s.count + 1 } // ❌ WRONG — no re-render
113
+ ```
114
+
115
+ Root-level global state: `s.root.update({ key: val })`
116
+
117
+ ---
118
+
119
+ ## Rule 8 — `el.call('fn', arg)` — `this` is the element inside the function
120
+
121
+ ```js
122
+ // functions/myFn.js
123
+ export const myFn = function myFn(arg1) {
124
+ const node = this.node // 'this' is the DOMQL element
125
+ }
126
+
127
+ onClick: (e, el) => el.call('myFn', someArg) // ✅ CORRECT
128
+ onClick: (e, el) => el.call('myFn', el, someArg) // ❌ WRONG — el passed twice
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Rule 9 — Icons: NEVER use `Icon` inside `Button` — use `Svg` atom
134
+
135
+ ```js
136
+ // ✅ CORRECT
137
+ MyBtn: {
138
+ extends: 'Flex', tag: 'button', flexAlign: 'center center', cursor: 'pointer',
139
+ Svg: { viewBox: '0 0 24 24', width: '22', height: '22',
140
+ html: '<path d="..." fill="currentColor"/>' }
141
+ }
142
+
143
+ // ❌ WRONG — Icon will NOT render inside Button
144
+ MyBtn: { extends: 'Button', Icon: { name: 'heart' } }
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Rule 10 — `childExtends` MUST be a named component string, never an inline object
150
+
151
+ Inline `childExtends` objects cause ALL property values to be concatenated as visible text content on every child.
152
+
153
+ ```js
154
+ // ✅ CORRECT — reference a named component
155
+ childExtends: 'NavLink'
156
+
157
+ // ❌ WRONG — dumps all prop values as raw text on every child
158
+ childExtends: {
159
+ tag: 'button',
160
+ background: 'transparent', // renders as visible text!
161
+ border: '2px solid transparent'
162
+ }
163
+ ```
164
+
165
+ Define shared styles as a named component in `components/`, register in `components/index.js`, then reference by name.
166
+
167
+ ---
168
+
169
+ ## Rule 11 — Color token syntax (dot-notation)
170
+
171
+ Color tokens use **dot-notation** for opacity and `+`/`-`/`=` for tone modifiers:
172
+
173
+ ```js
174
+ // ✅ CORRECT — dot-notation opacity
175
+ { color: 'white.7' }
176
+ { background: 'black.5' }
177
+ { background: 'gray.92+8' } // opacity 0.92, tone +8
178
+ { color: 'gray+16' } // full opacity, tone +16
179
+ { color: 'gray=90' } // absolute lightness 90%
180
+
181
+ // ❌ WRONG — old space-separated syntax (no longer supported)
182
+ { color: 'white .7' }
183
+ { color: 'gray 1 +16' }
184
+ ```
185
+
186
+ For rarely-used colors, define named tokens in `designSystem/COLOR.js` instead.
187
+
188
+ ---
189
+
190
+ ## Rule 12 — Border, boxShadow, textShadow — space-separated (CSS-like)
191
+
192
+ These properties use **space-separated** syntax matching CSS conventions:
193
+
194
+ ```js
195
+ // ✅ CORRECT — space-separated, CSS order
196
+ { border: '1px solid gray.1' }
197
+ { border: 'solid mediumGrey' }
198
+ { boxShadow: 'black.1 0 A C C' }
199
+ { textShadow: 'gray1 6px 6px' }
200
+
201
+ // Multiple shadows use commas (CSS standard)
202
+ { boxShadow: 'black.1 0 A C C, white.5 0 B D D' }
203
+
204
+ // Raw CSS passes through unchanged
205
+ { boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }
206
+
207
+ // ❌ WRONG — old comma-separated syntax (no longer supported)
208
+ { border: 'solid, gray, 1px' }
209
+ { boxShadow: 'black .1, 0, A, C, C' }
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Rule 13 — CSS override precedence — component level beats props level
215
+
216
+ ```js
217
+ // ✅ CORRECT — override at component level to match base component level
218
+ export const MyLink = {
219
+ extends: 'Link',
220
+ color: 'mediumBlue', // WINS — same declaration level
221
+ }
222
+
223
+ // ❌ WRONG — props block CANNOT override component-level CSS
224
+ export const MyLink = {
225
+ extends: 'Link',
226
+ props: { color: 'mediumBlue' } // LOSES to Link's component-level color
227
+ }
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Rule 14 — Dynamic HTML attributes go in `attr: {}`, not at root level
233
+
234
+ ```js
235
+ // ✅ CORRECT — attr block resolves functions
236
+ export const Input = {
237
+ type: 'text', // static default at root
238
+ attr: {
239
+ type: ({ props }) => props.type // dynamic resolution in attr
240
+ }
241
+ }
242
+
243
+ // ❌ WRONG — function at component level gets stringified into the HTML attribute
244
+ export const Input = {
245
+ type: ({ props }) => props.type // renders as: type="(el)=>el.props.type"
246
+ }
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Rule 15 — `onRender` — guard against double-init
252
+
253
+ ```js
254
+ onRender: (el) => {
255
+ if (el.__initialized) return
256
+ el.__initialized = true
257
+ // imperative logic here
258
+ }
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Rule 16 — SVGs belong in `designSystem/svg_data.js`, not inline in components
264
+
265
+ Store SVG markup in the design system and reference via context:
266
+
267
+ ```js
268
+ // designSystem/svg_data.js
269
+ export default {
270
+ folderTopRight: '<svg ...>...</svg>',
271
+ folderBottomLeft: '<svg ...>...</svg>',
272
+ }
273
+
274
+ // In component — reference from designSystem
275
+ Svg: {
276
+ src: ({ context }) => context.designSystem.SVG_DATA && context.designSystem.SVG_DATA.folderTopRight,
277
+ aspectRatio: '466 / 48',
278
+ }
279
+
280
+ // ❌ WRONG — inline SVG string in component
281
+ Svg: {
282
+ src: '<svg fill="none" viewBox="0 0 466 48">...</svg>',
283
+ }
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Rule 17 — `customRouterElement` for persistent layouts
289
+
290
+ Use `customRouterElement` in config to render pages inside a specific element instead of the root:
291
+
292
+ ```js
293
+ // config.js
294
+ export default {
295
+ router: {
296
+ customRouterElement: 'Folder.Content' // dot-separated path from root
297
+ }
298
+ }
299
+ ```
300
+
301
+ The `/` (main) page defines the persistent layout. Sub-pages are rendered inside the target element without re-creating the layout.
302
+
303
+ ---
304
+
305
+ ## Rule 18a — Tab/view switching — use DOM IDs + function, NOT reactive `display`
306
+
307
+ Reactive `display: (el, s) => ...` on multiple full-page trees causes rendering failures.
308
+
309
+ ```js
310
+ // ✅ CORRECT — use DOM ID pattern
311
+ HomeView: { id: 'view-home', extends: 'Flex', ... },
312
+ ExploreView: { id: 'view-explore', extends: 'Flex', display: 'none', ... },
313
+
314
+ // functions/switchView.js
315
+ export const switchView = function switchView(view) {
316
+ ['home', 'explore'].forEach(v => {
317
+ const el = document.getElementById('view-' + v)
318
+ if (el) el.style.display = v === view ? 'flex' : 'none'
319
+ })
320
+ }
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Rule 17 — v3 Conditional Props — use `isX` + `'.isX'`
326
+
327
+ ```js
328
+ // ✅ NEW — v3 conditional props pattern
329
+ export const ModalCard = {
330
+ opacity: '0',
331
+ visibility: 'hidden',
332
+
333
+ isActive: (el, s) => s.root.activeModal,
334
+ '.isActive': {
335
+ opacity: '1',
336
+ visibility: 'visible',
337
+ },
338
+ }
339
+
340
+ // ❌ OLD — props function with conditional spread (deprecated)
341
+ export const ModalCard = {
342
+ props: (el, s) => ({
343
+ ...(s.root.activeModal ? { opacity: '1' } : { opacity: '0' })
344
+ }),
345
+ }
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Rule 18 — CSS transitions require forced reflow
351
+
352
+ DOMQL + Emotion apply all CSS changes in one JS tick. The browser skips the "before" state. Fix:
353
+
354
+ ```js
355
+ // FadeIn pattern
356
+ modalNode.style.opacity = '0'
357
+ modalNode.style.visibility = 'visible'
358
+ state.root.update({ activeModal: true }, { onlyUpdate: 'ModalCard' })
359
+ modalNode.style.opacity = '0'
360
+ modalNode.offsetHeight // forces reflow — browser paints opacity:0
361
+ modalNode.style.opacity = '' // releases — Emotion class opacity:1 triggers transition
362
+
363
+ // FadeOut pattern
364
+ modalNode.style.opacity = '0'
365
+ setTimeout(() => {
366
+ modalNode.style.opacity = ''
367
+ modalNode.style.visibility = ''
368
+ state.root.update({ activeModal: false }, { onlyUpdate: 'ModalCard' })
369
+ }, 280) // match CSS transition duration
370
+ ```
371
+
372
+ ---
373
+
374
+ ## Rule 19 — Semantic-First Architecture
375
+
376
+ Use semantic components, never generic divs for meaningful content:
377
+
378
+ | Intent | Use |
379
+ | ------------------------- | ---------------- |
380
+ | Page header | Header |
381
+ | Navigation | Nav |
382
+ | Primary content | Main |
383
+ | Standalone article/entity | Article |
384
+ | Thematic grouping | Section |
385
+ | Sidebar | Aside |
386
+ | Actions | Button |
387
+ | Navigation links | Link |
388
+ | User input | Input / Form |
389
+
390
+ ---
391
+
392
+ ## Rule 20 — ARIA and accessibility attributes
393
+
394
+ ```js
395
+ attr: {
396
+ role: 'button',
397
+ tabindex: '0',
398
+ 'aria-label': ({ props }) => props.label,
399
+ 'aria-busy': ({ state }) => state.loading,
400
+ 'aria-live': 'polite'
401
+ }
402
+ ```
403
+
404
+ Use native elements instead of role overrides wherever possible.
405
+
406
+ ---
407
+
408
+ ## Project structure quick reference
409
+
410
+ ```
411
+ smbls/
412
+ ├── index.js # export * as components, export default pages, etc.
413
+ ├── state.js # export default { ... }
414
+ ├── dependencies.js # export default { 'pkg': 'version' }
415
+ ├── components/
416
+ │ ├── index.js # export * from './Foo.js' ← flat re-exports ONLY
417
+ │ └── Navbar.js # export const Navbar = { ... }
418
+ ├── pages/
419
+ │ ├── index.js # import + export default { '/': main }
420
+ │ └── main.js # export const main = { extends: 'Page', ... }
421
+ ├── functions/
422
+ │ ├── index.js # export * from './switchView.js'
423
+ │ └── switchView.js # export const switchView = function(...) {}
424
+ └── designSystem/
425
+ └── index.js # export default { COLOR, THEME, ... }
426
+ ```
427
+
428
+ ---
429
+
430
+ ## Rule 21 — Picture `src` goes on Img child, never on Picture
431
+
432
+ The HTML `<picture>` tag does NOT support `src` as an attribute. In v3, lowercase props move to `element.props`, so `element.parent.src` returns `undefined`.
433
+
434
+ ```js
435
+ // ✅ CORRECT — src on the Img child
436
+ Picture: {
437
+ Img: { src: '/files/photo.jpg' },
438
+ width: '100%',
439
+ aspectRatio: '16/9'
440
+ }
441
+
442
+ // ❌ WRONG — src on Picture is silently ignored
443
+ Picture: { src: '/files/photo.jpg', width: '100%' }
444
+ ```
445
+
446
+ ---
447
+
448
+ ## Rule 22 — Component key `Map` auto-detects as `<map>` — add `tag: 'div'`
449
+
450
+ The key `Map` is auto-detected as the HTML `<map>` element (for image maps), which defaults to `display: inline` and has height 0. Always add `tag: 'div'` when using `Map` as a component name:
451
+
452
+ ```js
453
+ export const Map = {
454
+ extends: 'Flex',
455
+ tag: 'div', // prevents <map> auto-detection
456
+ // ...
457
+ }
458
+ ```
459
+
460
+ ---
461
+
462
+ ## Rule 23 — `/files/` path resolution
463
+
464
+ File paths like `/files/logo.png` reference the framework's embedded file system via `context.files`. The `/files/` prefix is stripped automatically when resolving — keys are just filenames (e.g., `"logo.png"`, not `"/files/logo.png"`).
465
+
466
+ ---
467
+
468
+ ## Output Verification Checklist
469
+
470
+ Before finalizing any generated code, verify:
471
+
472
+ - [ ] Components are objects, not functions
473
+ - [ ] No cross-file imports except `pages/index.js`
474
+ - [ ] `components/index.js` uses `export *`, not `export * as`
475
+ - [ ] All folders are flat (no subfolders)
476
+ - [ ] Pages extend `'Page'`
477
+ - [ ] v3 event syntax (`onClick`, `onRender`, not `on: { click: ... }`)
478
+ - [ ] `flexAlign` not `align` for Flex shorthand
479
+ - [ ] State updated via `state.update()`, never direct mutation
480
+ - [ ] `childExtends` references named component strings only
481
+ - [ ] No opacity modifier syntax in color props (`color: 'white .7'` is invalid)
482
+ - [ ] Dynamic HTML attributes are in `attr: {}` block, not at root level
483
+ - [ ] One H1 per page; logical heading hierarchy H1→H2→H3
484
+ - [ ] Buttons for actions, Links for navigation
485
+ - [ ] Forms have labeled inputs with `name` and `type` attributes
486
+ - [ ] Picture `src` is on the Img child, not on Picture itself
487
+ - [ ] `Map` component key has `tag: 'div'` to avoid `<map>` auto-detection
488
+ - [ ] `$propsCollection` / `$stateCollection` replaced with `children` pattern
@@ -1,6 +1,6 @@
1
1
  # SEO Metadata
2
2
 
3
- DOMQL provides comprehensive SEO metadata support through a declarative `metadata` object. All SEO, social, structured data, and platform-specific properties are configured in a single, unified, type-safe structure.
3
+ Symbols provides comprehensive SEO metadata support through the `@symbo.ls/helmet` plugin. Define a declarative `metadata` object on your app, pages, or any component. The same code works at runtime (updates DOM `<head>` tags) and during SSR (generates HTML via brender).
4
4
 
5
5
  The system automatically:
6
6
 
@@ -8,6 +8,8 @@ The system automatically:
8
8
  - Expands array values into multiple tags
9
9
  - Handles namespace prefixes (`og:`, `twitter:`, `article:`, `product:`, `DC:`, `itemprop:`, `http-equiv:`)
10
10
  - Outputs valid HTML head markup
11
+ - Supports function values receiving `(element, state)` for dynamic metadata
12
+ - Merges metadata from global SEO settings, app-level, and page-level (page wins)
11
13
 
12
14
  ---
13
15
 
@@ -108,3 +110,41 @@ export default {
108
110
  },
109
111
  };
110
112
  ```
113
+
114
+ ---
115
+
116
+ # Dynamic Metadata
117
+
118
+ Metadata values can be functions receiving `(element, state)`. Both the whole object and individual properties support this:
119
+
120
+ ```js
121
+ // Whole metadata as function
122
+ export const productPage = {
123
+ metadata: (el, s) => ({
124
+ title: s.product.name + ' — My Store',
125
+ description: s.product.description,
126
+ 'og:image': s.product.image
127
+ })
128
+ }
129
+
130
+ // Individual properties as functions
131
+ export const profilePage = {
132
+ metadata: {
133
+ title: (el, s) => `${s.user.name} — My App`,
134
+ description: (el, s) => s.user.bio,
135
+ 'og:image': '/default-avatar.png' // static fallback
136
+ }
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ # Merge Priority
143
+
144
+ When metadata is defined at multiple levels, higher priority wins:
145
+
146
+ 1. `data.integrations.seo` — global SEO settings (lowest)
147
+ 2. `data.app.metadata` — app-level defaults
148
+ 3. `page.metadata` — page-level overrides (highest)
149
+
150
+ If no `title` is found after merging, it falls back to `page.state.title`, then `data.name`.