@symbo.ls/mcp 1.0.10 → 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,520 @@
1
+ # Migration Guide — DOMQL v2→v3 & React/Angular/Vue→Symbols
2
+
3
+ ---
4
+
5
+ ## Part 1: DOMQL v2 → v3 Migration
6
+
7
+ ### Summary of changes
8
+
9
+ | Change | v2 ❌ | v3 ✅ |
10
+ | ------------------------- | ----------------------------- | ----------------------------- |
11
+ | CSS props wrapper | `props: { padding: 'A' }` | `padding: 'A'` (at root) |
12
+ | Events wrapper | `on: { click: fn }` | `onClick: fn` (at root) |
13
+ | Inheritance | `extend: 'Component'` | `extends: 'Component'` |
14
+ | Child extend | `childExtend: 'Item'` | `childExtends: 'Item'` |
15
+ | Child element detection | any key including lowercase | PascalCase keys ONLY |
16
+
17
+ ### Flatten `props` and `on`
18
+
19
+ ```js
20
+ // v2
21
+ {
22
+ props: { position: 'absolute', gap: 'A' },
23
+ on: {
24
+ render: e => {},
25
+ click: (e, t) => {},
26
+ wheel: (e, t) => {},
27
+ },
28
+ }
29
+
30
+ // v3
31
+ {
32
+ position: 'absolute',
33
+ gap: 'A',
34
+ onRender: e => {},
35
+ onClick: (e, t) => {},
36
+ onWheel: (e, t) => {},
37
+ }
38
+ ```
39
+
40
+ Rules:
41
+ - Move all `props` entries directly to the component object root
42
+ - Remove the `on` wrapper; prefix each event with `on` + `CapitalizedEventName`
43
+ - Applies to root and all nested elements
44
+
45
+ ### Rename properties
46
+
47
+ ```js
48
+ // v2
49
+ { extend: SomeComponent, childExtend: AnotherComponent }
50
+
51
+ // v3
52
+ { extends: SomeComponent, childExtends: AnotherComponent }
53
+ ```
54
+
55
+ ### Replace `$stateCollection` with `children` + `childrenAs: 'state'`
56
+
57
+ ```js
58
+ // v2
59
+ {
60
+ childExtend: 'TeamItem',
61
+ $stateCollection: (el, s) => s.data
62
+ }
63
+
64
+ // v3 — Option A: childrenAs: 'state' (explicit)
65
+ {
66
+ childExtends: 'TeamItem',
67
+ childrenAs: 'state',
68
+ children: (el, s) => s.data
69
+ }
70
+
71
+ // v3 — Option B: state: 'dataKey' shorthand (preferred for nested state)
72
+ {
73
+ state: 'data',
74
+ childExtends: 'TeamItem',
75
+ children: ({ state }) => state
76
+ }
77
+ ```
78
+
79
+ #### `state: true` on child components
80
+
81
+ Components used with `childExtends` that read individual item state (e.g. `({ state }) => state.title`) need `state: true` at their root so each child receives its own state from the parent's children array:
82
+
83
+ ```js
84
+ // Parent container
85
+ export const TeamList = {
86
+ state: 'members',
87
+ childExtends: 'TeamItem',
88
+ children: ({ state }) => state
89
+ }
90
+
91
+ // Child component — state: true is REQUIRED
92
+ export const TeamItem = {
93
+ state: true,
94
+ Title: { text: ({ state }) => state.name },
95
+ Role: { text: ({ state }) => state.role }
96
+ }
97
+ ```
98
+
99
+ #### Common pitfall: `children: ({ state }) => state.data`
100
+
101
+ **WRONG**: `children: ({ state }) => state.data` does NOT properly pass individual state to child components — all children receive the parent's full state instead of their own item.
102
+
103
+ **CORRECT**: Use `state: 'data'` on the container first, then `children: ({ state }) => state`:
104
+
105
+ ```js
106
+ // ❌ WRONG — children don't get individual state
107
+ { children: ({ state }) => state.data, childExtends: 'Item' }
108
+
109
+ // ✅ CORRECT — state: 'data' narrows scope, children get individual items
110
+ { state: 'data', children: ({ state }) => state, childExtends: 'Item' }
111
+ ```
112
+
113
+ ### Replace `$propsCollection` with `children`
114
+
115
+ ```js
116
+ // v2
117
+ {
118
+ childExtend: 'Paragraph',
119
+ $propsCollection: ({ state }) => state.parse()
120
+ }
121
+
122
+ // v3
123
+ {
124
+ childExtends: 'Paragraph',
125
+ children: ({ state }) => state.parse()
126
+ }
127
+ ```
128
+
129
+ `$propsCollection` is fully removed in v3. Replace 1:1 with `children`. The same pattern applies to array data:
130
+
131
+ ```js
132
+ // v2
133
+ $propsCollection: ({ state }) => state
134
+
135
+ // v3
136
+ children: ({ state }) => state
137
+ ```
138
+
139
+ ### Replace `props: (fn)` function with individual prop functions
140
+
141
+ ```js
142
+ // v2 — dynamic props function (REMOVED in v3)
143
+ {
144
+ props: ({ state }) => ({
145
+ color: state.active ? 'red' : 'blue',
146
+ opacity: state.loading ? 0.5 : 1
147
+ })
148
+ }
149
+
150
+ // v3 — each property is its own function
151
+ {
152
+ color: ({ state }) => state.active ? 'red' : 'blue',
153
+ opacity: ({ state }) => state.loading ? 0.5 : 1
154
+ }
155
+ ```
156
+
157
+ ### Child element detection
158
+
159
+ ```js
160
+ // v2 — lowercase keys could create elements
161
+ { div: {} } // creates <div>
162
+
163
+ // v3 — only PascalCase keys create elements
164
+ { div: {} } // treated as a plain prop, NOT rendered
165
+ { Div: {} } // creates a child element (equivalent to { extends: 'Div' })
166
+ ```
167
+
168
+ ### Replace array spread `...[items]` with `children: [items]`
169
+
170
+ ```js
171
+ // v2 — numeric keys from spread are lowercase, treated as CSS in v3
172
+ {
173
+ childExtend: 'NavLink',
174
+ ...[{ text: 'About us' }, { text: 'Hiring' }]
175
+ }
176
+
177
+ // v3 — use children array
178
+ {
179
+ childExtends: 'NavLink',
180
+ children: [{ text: 'About us' }, { text: 'Hiring' }]
181
+ }
182
+ ```
183
+
184
+ Array spread creates numeric keys (`0`, `1`, `2`…) which v3 treats as CSS properties (lowercase). Always use `children` array instead.
185
+
186
+ ### Picture `src` must go on Img child
187
+
188
+ 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`.
189
+
190
+ ```js
191
+ // ❌ WRONG — src on Picture is silently ignored
192
+ Picture: { src: '/files/photo.jpg', width: '100%' }
193
+
194
+ // ✅ CORRECT — src goes on the Img child inside Picture
195
+ Picture: {
196
+ Img: { src: '/files/photo.jpg' },
197
+ width: '100%',
198
+ aspectRatio: '16/9'
199
+ }
200
+ ```
201
+
202
+ For theme-aware sources, use `@dark`/`@light` with `srcset` on Picture, but always put the default `src` on the Img child.
203
+
204
+ ### `<map>` tag auto-detection
205
+
206
+ Component keys named `Map` auto-detect as the HTML `<map>` tag (for image maps), which defaults to `display: inline` and has height 0. If using `Map` as a component name for geographic maps or similar, add `tag: 'div'`:
207
+
208
+ ```js
209
+ export const Map = {
210
+ extends: 'Flex',
211
+ tag: 'div', // prevents <map> auto-detection
212
+ // ...
213
+ }
214
+ ```
215
+
216
+ ### Non-existent base components
217
+
218
+ These v2 component names don't exist in v3 uikit:
219
+ - `extends: 'Page'` → use `extends: 'Flex', flexFlow: 'column'`
220
+ - `extends: 'Overflow'` → use `extends: 'Flex'`
221
+
222
+ ### `state: true` vs `state: 'fieldName'` for children
223
+
224
+ - `state: true` — for `childExtends`-created children; maps individual array items as each child's state
225
+ - `state: 'fieldName'` — for direct PascalCase children that need a specific field from parent state
226
+
227
+ ```js
228
+ // childExtends children — use state: true on the child component
229
+ export const ListItem = {
230
+ state: true,
231
+ Title: { text: ({ state }) => state.name }
232
+ }
233
+
234
+ // Direct PascalCase child — use state: 'fieldName'
235
+ Description: {
236
+ state: 'description',
237
+ childExtends: 'Paragraph',
238
+ children: ({ state }) => state.parse()
239
+ }
240
+ ```
241
+
242
+ ### Full example
243
+
244
+ ```js
245
+ // v2
246
+ {
247
+ extend: 'Flex',
248
+ childExtend: 'ListItem',
249
+ props: { position: 'absolute' },
250
+ attr: { 'gs-w': 1, 'gs-h': 1 },
251
+ SectionTitle: { text: 'Notes' },
252
+ Box: {
253
+ props: {
254
+ '--section-background': '#7a5e0e55',
255
+ id: 'editorjs',
256
+ },
257
+ on: {
258
+ frame: (e, t) => {},
259
+ render: e => {},
260
+ wheel: (e, t) => {},
261
+ dblclick: (e, t) => { e.stopPropagation() },
262
+ },
263
+ },
264
+ }
265
+
266
+ // v3
267
+ {
268
+ extends: 'Flex',
269
+ childExtends: 'ListItem',
270
+ position: 'absolute',
271
+ attr: { 'gs-w': 1, 'gs-h': 1 },
272
+ SectionTitle: { text: 'Notes' },
273
+ Box: {
274
+ '--section-background': '#7a5e0e55',
275
+ id: 'editorjs',
276
+ onFrame: (e, t) => {},
277
+ onRender: e => {},
278
+ onWheel: (e, t) => {},
279
+ onDblclick: (e, t) => { e.stopPropagation() },
280
+ },
281
+ }
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Part 2: React / Angular / Vue → Symbols
287
+
288
+ ### From React
289
+
290
+ | React Pattern | Symbols Equivalent |
291
+ | -------------------------------------- | ------------------------------------------------------------------------- |
292
+ | `function Component()` / `class` | `export const Component = { extends: 'Flex', ... }` |
293
+ | `import Component from './Component'` | Reference by key: `{ Component: {} }` |
294
+ | `useState(val)` | `state: { key: val }` + `s.update({ key: newVal })` |
295
+ | `useEffect(() => {}, [])` | `onRender: (el, s) => {}` |
296
+ | `useEffect(() => {}, [dep])` | `onStateUpdate: (changes, el, s) => {}` |
297
+ | `useContext` | `s.root` for global state |
298
+ | `useRef` | `el.node` for DOM access |
299
+ | `props.onClick` | `onClick: (e, el, s) => {}` |
300
+ | `props.children` | PascalCase child keys or `children` array |
301
+ | `{condition && <Component />}` | `if: (el, s) => condition` |
302
+ | `{items.map(i => <Item key={i.id}/>)}` | `children: (el, s) => s.items, childExtends: 'Item'` |
303
+ | `className="flex gap-4"` | `flow: 'x', gap: 'A'` (design tokens) |
304
+ | `style={{ padding: '16px' }}` | `padding: 'A'` |
305
+ | `<Link to="/page">` | `Link: { href: '/page', text: '...' }` |
306
+ | `useNavigate()` / `history.push` | `el.call('router', '/path', el.getRoot())` |
307
+ | Redux / Zustand store | `state/` folder + `s.root` access |
308
+ | `useMemo` / `useCallback` | Dynamic prop function: `text: (el, s) => s.a + s.b` |
309
+ | CSS Modules / styled-components | Flatten styles as props with design tokens |
310
+ | `<form onSubmit>` | `tag: 'form', onSubmit: (ev, el, s) => { ev.preventDefault(); ... }` |
311
+ | `fetch()` in components | `functions/fetch.js` + `el.call('fetch', method, path, data)` |
312
+
313
+ ### From Angular
314
+
315
+ | Angular Pattern | Symbols Equivalent |
316
+ | ---------------------------------- | ---------------------------------------------------------------------- |
317
+ | `@Component({ template, styles })` | Plain object with flattened props and child keys |
318
+ | `@Input() propName` | `propName: value` at root |
319
+ | `@Output() eventName` | `onEventName: (e, el, s) => {}` |
320
+ | `*ngIf="condition"` | `if: (el, s) => condition` |
321
+ | `*ngFor="let item of items"` | `children: (el, s) => s.items, childExtends: 'X'` |
322
+ | `[ngClass]="{ active: isActive }"` | `.isActive: { background: 'primary' }` |
323
+ | `(click)="handler()"` | `onClick: (e, el, s) => {}` |
324
+ | `{{ interpolation }}` | `text: '{{ key }}'` or `text: (el, s) => s.key` |
325
+ | `ngOnInit()` | `onInit: (el, s) => {}` |
326
+ | `ngAfterViewInit()` | `onRender: (el, s) => {}` |
327
+ | `ngOnDestroy()` | Return cleanup fn from `onRender` |
328
+ | `ngOnChanges(changes)` | `onStateUpdate: (changes, el, s) => {}` |
329
+ | Services / DI | `functions/` folder + `el.call('serviceFn', args)` |
330
+ | `RouterModule` routes | `pages/index.js` route mapping |
331
+ | `routerLink="/path"` | `Link: { href: '/path' }` |
332
+ | `Router.navigate(['/path'])` | `el.call('router', '/path', el.getRoot())` |
333
+ | NgRx / BehaviorSubject store | `state/` folder + `s.root` access |
334
+ | SCSS / component styles | Flatten to props with design tokens |
335
+ | Reactive Forms | `tag: 'form'`, `Input` children with `name`, `onSubmit` handler |
336
+
337
+ ### From Vue
338
+
339
+ | Vue Pattern | Symbols Equivalent |
340
+ | --------------------------------- | --------------------------------------------------------------------------------- |
341
+ | `<template>` + `<script>` SFC | Single object with child keys and flattened props |
342
+ | `:propName="value"` (v-bind) | `propName: value` or `propName: (el, s) => s.value` |
343
+ | `@click="handler"` (v-on) | `onClick: (e, el, s) => {}` |
344
+ | `v-if="condition"` | `if: (el, s) => condition` |
345
+ | `v-show="condition"` | `hide: (el, s) => !condition` |
346
+ | `v-for="item in items"` | `children: (el, s) => s.items, childExtends: 'X'` |
347
+ | `v-model="value"` | `value: '{{ key }}'` + `onInput: (e, el, s) => s.update({ key: e.target.value })` |
348
+ | `ref="myRef"` | `el.node` or `el.lookup('Key')` |
349
+ | `data()` / `ref()` / `reactive()` | `state: { key: value }` |
350
+ | `computed` | `text: (el, s) => s.first + ' ' + s.last` |
351
+ | `watch` | `onStateUpdate: (changes, el, s) => {}` |
352
+ | `mounted()` | `onRender: (el, s) => {}` |
353
+ | `created()` / `setup()` | `onInit: (el, s) => {}` |
354
+ | Vuex / Pinia store | `state/` folder + `s.root` access |
355
+ | `<router-link to="/path">` | `Link: { href: '/path' }` |
356
+ | `$router.push('/path')` | `el.call('router', '/path', el.getRoot())` |
357
+ | `<slot>` | PascalCase child keys or `content` property |
358
+ | `<slot name="header">` | Named child key: `Header: {}` |
359
+ | Mixins / Composables | `extends` for shared logic; `functions/` for shared utilities |
360
+ | `$emit('eventName', data)` | `s.parent.update({ key: data })` or callback via state |
361
+
362
+ ---
363
+
364
+ ## Part 3: CSS → Design Tokens
365
+
366
+ | CSS | Symbols |
367
+ | ----------------------------------------------------- | -------------------------------------------- |
368
+ | `padding: 16px` | `padding: 'A'` |
369
+ | `padding: 16px 26px` | `padding: 'A B'` |
370
+ | `margin: 0 auto` | `margin: '- auto'` |
371
+ | `gap: 10px` | `gap: 'Z'` |
372
+ | `border-radius: 12px` | `borderRadius: 'Z1'` or `round: 'Z1'` |
373
+ | `width: 42px; height: 42px` | `boxSize: 'C'` |
374
+ | `display: flex; flex-direction: column` | `flow: 'y'` |
375
+ | `display: flex; flex-direction: row` | `flow: 'x'` |
376
+ | `align-items: center; justify-content: center` | `flexAlign: 'center center'` |
377
+ | `display: grid; grid-template-columns: repeat(3,1fr)` | `extends: 'Grid', columns: 'repeat(3, 1fr)'` |
378
+ | `font-size: 20px` | `fontSize: 'A1'` |
379
+ | `font-weight: 500` | `fontWeight: '500'` |
380
+ | `color: rgba(255,255,255,0.65)` | `color: 'white.65'` |
381
+ | `background: #000` | `background: 'black'` |
382
+ | `background: rgba(0,0,0,0.5)` | `background: 'black.5'` |
383
+ | `cursor: pointer` | `cursor: 'pointer'` |
384
+ | `overflow: hidden` | `overflow: 'hidden'` |
385
+ | `position: absolute; inset: 0` | `position: 'absolute', inset: '0'` |
386
+ | `z-index: 99` | `zIndex: 99` |
387
+ | `transition: all 0.3s ease` | `transition: 'A defaultBezier'` |
388
+ | `:hover { background: #333 }` | `':hover': { background: 'gray3' }` |
389
+ | `@media (max-width: 768px) { ... }` | `'@mobileL': { ... }` |
390
+
391
+ ### Spacing token quick reference
392
+
393
+ | Token | ~px | Token | ~px | Token | ~px |
394
+ | ----- | --- | ----- | --- | ----- | --- |
395
+ | X | 3 | A | 16 | D | 67 |
396
+ | Y | 6 | A1 | 20 | E | 109 |
397
+ | Z | 10 | A2 | 22 | F | 177 |
398
+ | Z1 | 12 | B | 26 | | |
399
+ | Z2 | 14 | B1 | 32 | | |
400
+ | | | B2 | 36 | | |
401
+ | | | C | 42 | | |
402
+ | | | C1 | 52 | | |
403
+ | | | C2 | 55 | | |
404
+
405
+ ---
406
+
407
+ ## Part 4: v3 Component Template
408
+
409
+ Use this as the base template for every new component:
410
+
411
+ ```js
412
+ // components/ComponentName.js
413
+ export const ComponentName = {
414
+ extends: 'Flex',
415
+
416
+ // Props flattened at root (design tokens)
417
+ padding: 'A B',
418
+ background: 'surface',
419
+ borderRadius: 'B',
420
+ gap: 'Z',
421
+
422
+ // Lifecycle events
423
+ onRender: (el, s) => {},
424
+ onInit: (el, s) => {},
425
+
426
+ // DOM events
427
+ onClick: (e, el, s) => {},
428
+
429
+ // Conditional cases (v3 pattern)
430
+ isActive: false,
431
+ '.isActive': { background: 'primary', color: 'white' },
432
+
433
+ // Responsive
434
+ '@mobileL': { padding: 'A' },
435
+ '@tabletS': { padding: 'B' },
436
+
437
+ // Children — PascalCase keys, no imports
438
+ Header: {},
439
+ Content: {
440
+ Article: { text: 'Hello' },
441
+ },
442
+ Footer: {},
443
+ }
444
+ ```
445
+
446
+ ---
447
+
448
+ ## Part 5: State Management Migration
449
+
450
+ | Framework pattern | Symbols equivalent |
451
+ | ------------------------- | ---------------------------------------------------- |
452
+ | Global store (Redux, Pinia, NgRx) | `state/index.js` with initial flat state. Access via `s.root` in any component |
453
+ | Local component state | `state: { key: val }` on the component |
454
+ | Derived/computed state | Dynamic prop function: `text: (el, s) => derived(s)` |
455
+ | Async data fetch | `onRender: async (el, s) => { ... s.update({data}) }` |
456
+ | State persistence | Functions that read/write to localStorage/cookie |
457
+
458
+ Example async state pattern:
459
+ ```js
460
+ export const DataView = {
461
+ state: { data: null, loading: true, error: null },
462
+
463
+ onRender: async (el, state) => {
464
+ try {
465
+ const data = await el.call('fetchData', el.props.id)
466
+ state.update({ data, loading: false })
467
+ } catch (e) {
468
+ state.update({ error: e.message, loading: false })
469
+ }
470
+ }
471
+ }
472
+ ```
473
+
474
+ ---
475
+
476
+ ## Part 6: Color/Border/Shadow Syntax Migration (v3.1)
477
+
478
+ ### Color tokens: space-separated to dot-notation
479
+
480
+ | Old | New | Notes |
481
+ |---|---|---|
482
+ | `'white .1'` | `'white.1'` | Opacity 0.1 |
483
+ | `'gray 0.85'` | `'gray.85'` | Opacity 0.85 |
484
+ | `'gray .92 +8'` | `'gray.92+8'` | Opacity + relative tone |
485
+ | `'gray 1 +16'` | `'gray+16'` | Alpha 1 = default, omit |
486
+ | `'gray 1 -68'` | `'gray-68'` | Relative tone |
487
+ | `'gray 1 90'` | `'gray=90'` | Absolute lightness (=prefix) |
488
+ | `'white 1 -78'` | `'white-78'` | Tone only |
489
+
490
+ ### border: comma-separated to space-separated (CSS order)
491
+
492
+ | Old | New |
493
+ |---|---|
494
+ | `'solid, gray, 1px'` | `'1px solid gray'` |
495
+ | `'gray6 .1, solid, 1px'` | `'1px solid gray6.1'` |
496
+ | `'solid, mediumGrey'` | `'solid mediumGrey'` |
497
+ | `'1px, solid'` | `'1px solid'` |
498
+
499
+ ### boxShadow: commas to spaces within shadow, pipe to comma between shadows
500
+
501
+ | Old | New |
502
+ |---|---|
503
+ | `'white .1, 0, A, C, C'` | `'white.1 0 A C C'` |
504
+ | `'black .10, 0px, 2px, 8px, 0px'` | `'black.1 0px 2px 8px 0px'` |
505
+ | `'a, b \| c, d'` | `'a b, c d'` |
506
+
507
+ ### textStroke/textShadow: comma-separated to space-separated
508
+
509
+ | Old | New |
510
+ |---|---|
511
+ | `'1px, gray6'` | `'1px gray6'` |
512
+ | `'gray1, 6px, 6px'` | `'gray1 6px 6px'` |
513
+
514
+ ### CSS fallback
515
+
516
+ Raw CSS values now pass through unchanged:
517
+ ```js
518
+ boxShadow: '0 2px 8px rgba(0,0,0,0.1)' // passes through as-is
519
+ border: '1px solid #333' // passes through as-is
520
+ ```