@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,509 @@
1
+ # Symbols / DOMQL — UI Patterns, Accessibility & AI Optimization
2
+
3
+ ---
4
+
5
+ ## Part 1: UI/UX Patterns
6
+
7
+ ### Loading State
8
+
9
+ ```js
10
+ export const DataList = {
11
+ state: { items: [], loading: true, error: null },
12
+
13
+ Loader: { if: ({ state }) => state.loading, extends: 'Spinner' },
14
+ Error: {
15
+ if: ({ state }) => Boolean(state.error),
16
+ attr: { role: 'alert' },
17
+ text: ({ state }) => state.error
18
+ },
19
+ Items: {
20
+ if: ({ state }) => !state.loading && !state.error,
21
+ children: ({ state }) => state.items,
22
+ childExtends: 'ListItem'
23
+ },
24
+
25
+ onRender: async (el, state) => {
26
+ try {
27
+ const items = await el.call('fetchItems')
28
+ state.update({ items, loading: false })
29
+ } catch (e) {
30
+ state.update({ error: e.message, loading: false })
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ### Toggle / Accordion
37
+
38
+ ```js
39
+ export const Accordion = {
40
+ state: { open: false },
41
+ Header: {
42
+ extends: 'Flex',
43
+ attr: (el, s) => ({
44
+ 'aria-expanded': s.open,
45
+ 'aria-controls': 'accordion-body'
46
+ }),
47
+ onClick: (ev, el) => { el.parent.state.toggle('open') }
48
+ },
49
+ Body: {
50
+ id: 'accordion-body',
51
+ if: ({ state }) => state.open,
52
+ html: ({ props }) => props.content
53
+ }
54
+ }
55
+ ```
56
+
57
+ ### Active List Item
58
+
59
+ ```js
60
+ export const Menu = {
61
+ state: { active: null },
62
+ childExtends: 'NavLink',
63
+ childProps: {
64
+ isActive: ({ key, state }) => state.active === key,
65
+ '.active': { fontWeight: '600', color: 'primary' },
66
+ onClick: (ev, el, state) => { state.update({ active: el.key }) }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### Modal (v3 complete pattern)
72
+
73
+ ```js
74
+ // components/ModalCard.js
75
+ export const ModalCard = {
76
+ position: 'absolute', flexAlign: 'center center',
77
+ top: 0, left: 0, boxSize: '100% 100%',
78
+ transition: 'all C defaultBezier',
79
+ opacity: '0', visibility: 'hidden', pointerEvents: 'none', zIndex: '-1',
80
+
81
+ isActive: (el, s) => s.root.activeModal,
82
+ '.isActive': { opacity: '1', zIndex: 999999, visibility: 'visible', pointerEvents: 'initial' },
83
+
84
+ // Close on backdrop click, prevent close on content click
85
+ onClick: (event, element) => { element.call('closeModal') },
86
+ childProps: { onClick: (ev) => { ev.stopPropagation() } },
87
+
88
+ // ARIA
89
+ attr: { role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Dialog' },
90
+
91
+ // Trap focus
92
+ onRender: (el) => {
93
+ const focusable = el.node.querySelectorAll('button, [href], input, [tabindex]:not([tabindex="-1"])')
94
+ if (focusable.length) focusable[0].focus()
95
+ },
96
+ onKeydown: (e, el) => {
97
+ if (e.key === 'Escape') el.call('closeModal')
98
+ },
99
+
100
+ InnerContent: { /* ... */ }
101
+ }
102
+
103
+ // functions/showModal.js
104
+ export const showModal = function showModal(path) {
105
+ const modalEl = this.lookup('ModalCard')
106
+ const modalNode = modalEl.node
107
+ // FadeIn: force browser to paint opacity:0 before transition to opacity:1
108
+ modalNode.style.opacity = '0'
109
+ modalNode.style.visibility = 'visible'
110
+ this.state.root.update({ activeModal: true }, { onlyUpdate: 'ModalCard' })
111
+ modalNode.style.opacity = '0'
112
+ modalNode.offsetHeight // force reflow
113
+ modalNode.style.opacity = ''
114
+ }
115
+
116
+ // functions/closeModal.js
117
+ export const closeModal = function closeModal() {
118
+ const modalEl = this.lookup('ModalCard')
119
+ const modalNode = modalEl.node
120
+ modalNode.style.opacity = '0'
121
+ setTimeout(() => {
122
+ modalNode.style.opacity = ''
123
+ modalNode.style.visibility = ''
124
+ this.state.root.update({ activeModal: false }, { onlyUpdate: 'ModalCard' })
125
+ }, 280) // match CSS transition duration
126
+ }
127
+ ```
128
+
129
+ ### Tab Switching (DOM ID pattern)
130
+
131
+ Use DOM IDs for view switching — NOT reactive `display` bindings (causes rendering failures).
132
+
133
+ ```js
134
+ // Page definition
135
+ HomeView: { id: 'view-home', extends: 'Flex', ... },
136
+ ExploreView: { id: 'view-explore', extends: 'Flex', display: 'none', ... },
137
+
138
+ // Navbar
139
+ Navbar: {
140
+ childExtends: 'Button',
141
+ childProps: { onClick: (e, el) => { el.call('switchView', el.key.toLowerCase()) } }
142
+ }
143
+
144
+ // functions/switchView.js
145
+ export const switchView = function switchView(view) {
146
+ ['home', 'explore', 'profile'].forEach(v => {
147
+ const el = document.getElementById('view-' + v)
148
+ if (el) el.style.display = v === view ? 'flex' : 'none'
149
+ })
150
+ }
151
+ ```
152
+
153
+ ### Dynamic Class Toggle
154
+
155
+ ```js
156
+ export const Button = {
157
+ '.active': { background: 'primary', color: 'white' },
158
+ isActive: ({ props }) => props.active // adds/removes .active class
159
+ }
160
+ ```
161
+
162
+ ### Dynamic Form / Async Submit
163
+
164
+ ```js
165
+ export const LoginForm = {
166
+ tag: 'form',
167
+ state: { loading: false, error: null },
168
+ attr: { 'aria-live': 'polite' },
169
+
170
+ onSubmit: async (event, el, state) => {
171
+ event.preventDefault()
172
+ state.update({ loading: true, error: null })
173
+ try {
174
+ await el.call('login', new FormData(el.node))
175
+ el.call('router', '/dashboard', el.__ref.root)
176
+ } catch (e) {
177
+ state.update({ loading: false, error: e.message })
178
+ }
179
+ },
180
+
181
+ Field: {
182
+ Input: { type: 'email', name: 'email', attr: { required: 'true' } }
183
+ },
184
+ Error: {
185
+ if: ({ state }) => Boolean(state.error),
186
+ attr: { role: 'alert' },
187
+ text: ({ state }) => state.error,
188
+ color: 'red'
189
+ },
190
+ SubmitButton: {
191
+ attr: (el, s) => ({ 'aria-busy': s.loading }),
192
+ text: ({ state }) => state.loading ? 'Signing in…' : 'Sign in'
193
+ }
194
+ }
195
+ ```
196
+
197
+ ### Responsive Layout
198
+
199
+ ```js
200
+ export const Layout = {
201
+ extends: 'Grid',
202
+ columns: 'repeat(4, 1fr)',
203
+ gap: 'B',
204
+
205
+ '@tabletS': { columns: 'repeat(2, 1fr)' },
206
+ '@mobileL': { columns: '1fr', gap: 'A' },
207
+ }
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Part 2: Accessibility
213
+
214
+ ### Semantic Atoms First
215
+
216
+ Always prefer built-in semantic atoms over generic containers:
217
+
218
+ ```js
219
+ // ✅ CORRECT — semantic atoms
220
+ Button: { text: 'Submit' } // <button>
221
+ Link: { text: 'Dashboard', href: '/dashboard' } // <a>
222
+ Input: { placeholder: 'Search...' } // <input>
223
+ Nav: { Link: { text: 'Home', href: '/' } } // <nav>
224
+ Header: {} // <header>
225
+ Footer: {} // <footer>
226
+ Main: {} // <main>
227
+
228
+ // ❌ WRONG — loses semantics
229
+ Box: { tag: 'div', text: 'Submit', onClick: fn } // div is not a button
230
+ ```
231
+
232
+ ### ARIA Attributes
233
+
234
+ ```js
235
+ // Landmark roles
236
+ Box: { attr: { role: 'alert' }, text: 'Error occurred' }
237
+ Box: { attr: { role: 'status', 'aria-live': 'polite' }, text: '3 results found' }
238
+
239
+ // Labels
240
+ Input: { attr: { 'aria-label': 'Search networks' } }
241
+ Button: { icon: 'x', attr: { 'aria-label': 'Close dialog' } }
242
+ Icon: { name: 'settings', attr: { 'aria-hidden': 'true' } }
243
+
244
+ // Dynamic state
245
+ Button: {
246
+ attr: (el, s) => ({ 'aria-expanded': s.isOpen, 'aria-controls': 'dropdown-menu' }),
247
+ onClick: (e, el, s) => s.update({ isOpen: !s.isOpen })
248
+ }
249
+ ```
250
+
251
+ ### Keyboard Navigation
252
+
253
+ ```js
254
+ // Custom keyboard interaction (listbox pattern)
255
+ {
256
+ extends: 'Flex',
257
+ attr: { role: 'listbox', tabindex: '0', 'aria-label': 'Select option' },
258
+ onKeydown: (e, el, s) => {
259
+ if (e.key === 'ArrowDown') {
260
+ e.preventDefault()
261
+ s.update({ activeIndex: Math.min(s.activeIndex + 1, s.items.length - 1) })
262
+ }
263
+ if (e.key === 'ArrowUp') {
264
+ e.preventDefault()
265
+ s.update({ activeIndex: Math.max(s.activeIndex - 1, 0) })
266
+ }
267
+ if (e.key === 'Enter' || e.key === ' ') {
268
+ e.preventDefault()
269
+ el.call('selectItem', s.items[s.activeIndex])
270
+ }
271
+ if (e.key === 'Escape') el.call('closeDropdown')
272
+ }
273
+ }
274
+ ```
275
+
276
+ Tabindex rules:
277
+ - `tabindex: '0'` — in tab order
278
+ - `tabindex: '-1'` — focusable programmatically only
279
+ - Never use `tabindex > 0`
280
+
281
+ ### Focus Styles
282
+
283
+ ```js
284
+ Button: {
285
+ ':focus': { outline: 'none' },
286
+ ':focus-visible': { outline: 'solid Y blue.3', outlineOffset: '2px' }
287
+ }
288
+ ```
289
+
290
+ ### Accessible Forms
291
+
292
+ ```js
293
+ // Visible label association
294
+ Label: { text: 'Email', attr: { for: 'email-input' } }
295
+ Input: { id: 'email-input', type: 'email', attr: { required: 'true', 'aria-required': 'true' } }
296
+
297
+ // Described by helper text
298
+ Input: { id: 'password', type: 'password', attr: { 'aria-describedby': 'password-hint' } }
299
+ P: { id: 'password-hint', text: 'Must be at least 8 characters' }
300
+
301
+ // Error state
302
+ Input: {
303
+ attr: (el, s) => ({
304
+ 'aria-invalid': s.hasError ? 'true' : undefined,
305
+ 'aria-describedby': s.hasError ? 'error-msg' : undefined
306
+ })
307
+ }
308
+ P: { id: 'error-msg', attr: { role: 'alert' }, color: 'red', text: (el, s) => s.errorMessage }
309
+ ```
310
+
311
+ ### Images and Icons
312
+
313
+ ```js
314
+ // Informative image
315
+ Img: { src: 'chart.png', alt: 'Network uptime over last 30 days' }
316
+
317
+ // Decorative image
318
+ Img: { src: 'decoration.png', alt: '', attr: { 'aria-hidden': 'true' } }
319
+
320
+ // Icon-only button
321
+ Button: { icon: 'x', attr: { 'aria-label': 'Close' } }
322
+
323
+ // Decorative icon next to text
324
+ Button: { Icon: { name: 'search', attr: { 'aria-hidden': 'true' } }, text: 'Search' }
325
+ ```
326
+
327
+ ### Color and Contrast
328
+
329
+ - Do NOT rely on color alone for error/success states — always combine with icon + text
330
+ - Use `title`, `caption`, `paragraph` semantic tokens for sufficient contrast
331
+ - Low opacity colors (`gray 0.3`) likely fail WCAG AA — verify contrast ratio
332
+
333
+ ---
334
+
335
+ ## Part 3: AI Agent Optimization
336
+
337
+ ### `aid-*` Attributes for Machine Parsing
338
+
339
+ Include `aid-*` attributes so AI agents can parse structural intent:
340
+
341
+ ```js
342
+ export const HeroSection = {
343
+ extends: 'Section',
344
+ attr: {
345
+ 'aid-type': 'main',
346
+ 'aid-desc': 'Primary hero section with CTA',
347
+ 'aid-state': 'idle',
348
+ 'aid-cnt-type': 'info'
349
+ },
350
+ H1: { text: 'Welcome to Symbols' },
351
+ P: { text: 'Build UIs with declarative objects.' },
352
+ Button: { text: 'Get Started', theme: 'primary' }
353
+ }
354
+ ```
355
+
356
+ | `aid-type` values | Meaning |
357
+ | ----------------- | ---------------------------------------------------- |
358
+ | `header` | Page header |
359
+ | `nav` | Navigation |
360
+ | `main` | Primary content |
361
+ | `content` | Content section |
362
+ | `complementary` | Supplementary (sidebar, aside) |
363
+ | `interactive` | Form, control panel |
364
+ | `modal` | Dialog overlay |
365
+ | `alert` | Error or notification |
366
+ | `search` | Search interface |
367
+
368
+ | `aid-state` values | Meaning |
369
+ | ------------------ | -------------------- |
370
+ | `idle` | Default state |
371
+ | `loading` | Fetching data |
372
+ | `processing` | Submitting/computing |
373
+ | `done` | Completed |
374
+ | `error` | Error state |
375
+
376
+ ### JSON-LD Structured Data
377
+
378
+ Include JSON-LD for entity representation by AI agents and search engines:
379
+
380
+ ```js
381
+ export const StructuredData = {
382
+ tag: 'script',
383
+ attr: { type: 'application/ld+json' },
384
+ html: (el, s) => JSON.stringify({
385
+ '@context': 'https://schema.org',
386
+ '@type': 'Product',
387
+ name: s.name,
388
+ description: s.description,
389
+ offers: {
390
+ '@type': 'Offer',
391
+ price: s.price,
392
+ priceCurrency: s.currency,
393
+ },
394
+ }),
395
+ }
396
+ ```
397
+
398
+ Schema types to use: `Organization`, `Product`, `Service`, `Article`, `FAQPage`, `BreadcrumbList`
399
+
400
+ Structured data must match server-rendered content exactly.
401
+
402
+ ### Semantic Heading Structure
403
+
404
+ ```
405
+ One H1 per page defining the primary subject.
406
+ Logical hierarchy: H1 → H2 → H3 — never skip levels.
407
+ Heading hierarchy is used by AI agents to determine page structure.
408
+ ```
409
+
410
+ ### AI-Accessible Tool Exposure (Chrome WebMCP)
411
+
412
+ ```js
413
+ export const CheckOrderTool = {
414
+ extends: 'Form',
415
+ attr: {
416
+ 'data-mcp-tool': 'checkOrderStatus',
417
+ 'data-mcp-description': 'Check the status of an order by ID'
418
+ },
419
+ Input: { attr: { type: 'text', name: 'orderId', placeholder: 'Order ID' } },
420
+ Button: { type: 'submit', text: 'Check' }
421
+ }
422
+ ```
423
+
424
+ ### llms.txt Support
425
+
426
+ Provide `/llms.txt` at your project root for AI routing guidance:
427
+
428
+ ```text
429
+ # Organization Name
430
+ # Purpose: Platform description
431
+
432
+ ## Key pages
433
+ - /products
434
+ - /api/docs
435
+ - /support
436
+
437
+ ## Preferred interactions
438
+ - /api/v2/ programmatic access
439
+ - /search?q= for search
440
+
441
+ ## Data accuracy notes
442
+ - Prices, shipping, inventory levels
443
+ ```
444
+
445
+ ### Server-Rendered Critical Content
446
+
447
+ - All critical content (text, headings, prices, key data) must be server-rendered in the initial HTML
448
+ - Do not rely on client-side-only rendering for content AI agents need to parse
449
+ - `aria-busy="true"` while loading; `aria-busy="false"` when complete
450
+
451
+ ### Failure Pattern Recognition
452
+
453
+ Watch for these anti-patterns that break AI comprehension:
454
+ - Excessive divs/boxes without semantic meaning
455
+ - Non-descriptive link text ("click here", "read more")
456
+ - Missing or skipped heading levels
457
+ - Critical content rendered client-side only
458
+ - Conflicting metadata (page title vs. H1 vs. JSON-LD)
459
+ - Missing `alt` text on informative images
460
+
461
+ ---
462
+
463
+ ## Part 4: Design Principles
464
+
465
+ ### Visual Hierarchy
466
+
467
+ - Lead with the most important content (F-pattern, Z-pattern)
468
+ - Use typographic scale (`H1`→`H6`, `P`, `Caption`) to create hierarchy
469
+ - Adequate whitespace — breathing room improves comprehension
470
+
471
+ ### Component Button Hierarchy
472
+
473
+ | Level | Component | Use |
474
+ | ------------ | ------------------ | ----------------------- |
475
+ | Primary | `theme: 'primary'` | Main CTA (one per view) |
476
+ | Secondary | `theme: 'dialog'` | Supporting actions |
477
+ | Tertiary | `theme: 'transparent'` | Least important |
478
+ | Destructive | `theme: 'warning'` | Irreversible actions |
479
+
480
+ ### Responsive Behavior
481
+
482
+ ```js
483
+ // Mobile-first approach
484
+ Component: {
485
+ padding: 'A', // mobile base
486
+ '@tabletS': { padding: 'B' }, // tablet
487
+ '@screenS': { padding: 'C' }, // desktop
488
+ }
489
+ ```
490
+
491
+ ### Transitions & Micro-interactions
492
+
493
+ ```js
494
+ // Standard transition
495
+ Component: {
496
+ transition: 'B defaultBezier', // B ≈ 280ms
497
+ transitionProperty: 'opacity, transform'
498
+ }
499
+
500
+ // Hover feedback
501
+ Button: {
502
+ ':hover': { opacity: 0.9, transform: 'scale(1.015)' },
503
+ ':active': { opacity: 1, transform: 'scale(0.995)' }
504
+ }
505
+ ```
506
+
507
+ Easing: `defaultBezier` = `cubic-bezier(.29, .67, .51, .97)` (smooth ease-out)
508
+
509
+ Do NOT animate layout properties (`width`, `height`, `top`, `left`) — they force reflow. Animate `transform` and `opacity` instead.