@symbo.ls/mcp 1.0.10 → 1.0.13

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 (46) hide show
  1. package/README.md +1 -0
  2. package/package.json +5 -2
  3. package/symbols_mcp/skills/AUDIT.md +148 -174
  4. package/symbols_mcp/skills/BRAND_IDENTITY.md +75 -0
  5. package/symbols_mcp/skills/COMPONENTS.md +266 -0
  6. package/symbols_mcp/skills/COOKBOOK.md +850 -0
  7. package/symbols_mcp/skills/DEFAULT_COMPONENTS.md +3491 -1637
  8. package/symbols_mcp/skills/DEFAULT_LIBRARY.md +301 -0
  9. package/symbols_mcp/skills/DESIGN_CRITIQUE.md +70 -59
  10. package/symbols_mcp/skills/DESIGN_DIRECTION.md +109 -175
  11. package/symbols_mcp/skills/DESIGN_SYSTEM.md +722 -0
  12. package/symbols_mcp/skills/DESIGN_SYSTEM_ARCHITECT.md +65 -57
  13. package/symbols_mcp/skills/DESIGN_TO_CODE.md +83 -64
  14. package/symbols_mcp/skills/DESIGN_TREND.md +62 -50
  15. package/symbols_mcp/skills/FIGMA_MATCHING.md +69 -58
  16. package/symbols_mcp/skills/LEARNINGS.md +374 -0
  17. package/symbols_mcp/skills/MARKETING_ASSETS.md +71 -59
  18. package/symbols_mcp/skills/MIGRATION.md +561 -0
  19. package/symbols_mcp/skills/PATTERNS.md +536 -0
  20. package/symbols_mcp/skills/PRESENTATION.md +78 -0
  21. package/symbols_mcp/skills/PROJECT_STRUCTURE.md +398 -0
  22. package/symbols_mcp/skills/RULES.md +519 -0
  23. package/symbols_mcp/skills/RUNNING_APPS.md +476 -0
  24. package/symbols_mcp/skills/SEO-METADATA.md +64 -9
  25. package/symbols_mcp/skills/SNIPPETS.md +598 -0
  26. package/symbols_mcp/skills/SSR-BRENDER.md +99 -0
  27. package/symbols_mcp/skills/SYNTAX.md +835 -0
  28. package/symbols_mcp/skills/ACCESSIBILITY.md +0 -471
  29. package/symbols_mcp/skills/ACCESSIBILITY_AUDITORY.md +0 -70
  30. package/symbols_mcp/skills/AGENT_INSTRUCTIONS.md +0 -265
  31. package/symbols_mcp/skills/BRAND_INDENTITY.md +0 -69
  32. package/symbols_mcp/skills/BUILT_IN_COMPONENTS.md +0 -304
  33. package/symbols_mcp/skills/CLAUDE.md +0 -2158
  34. package/symbols_mcp/skills/CLI_QUICK_START.md +0 -205
  35. package/symbols_mcp/skills/DEFAULT_DESIGN_SYSTEM.md +0 -496
  36. package/symbols_mcp/skills/DESIGN_SYSTEM_CONFIG.md +0 -487
  37. package/symbols_mcp/skills/DESIGN_SYSTEM_IN_PROPS.md +0 -136
  38. package/symbols_mcp/skills/DOMQL_v2-v3_MIGRATION.md +0 -236
  39. package/symbols_mcp/skills/MIGRATE_TO_SYMBOLS.md +0 -634
  40. package/symbols_mcp/skills/OPTIMIZATIONS_FOR_AGENT.md +0 -253
  41. package/symbols_mcp/skills/PROJECT_SETUP.md +0 -217
  42. package/symbols_mcp/skills/QUICKSTART.md +0 -79
  43. package/symbols_mcp/skills/REMOTE_PREVIEW.md +0 -144
  44. package/symbols_mcp/skills/SYMBOLS_LOCAL_INSTRUCTIONS.md +0 -1405
  45. package/symbols_mcp/skills/THE_PRESENTATION.md +0 -69
  46. package/symbols_mcp/skills/UI_UX_PATTERNS.md +0 -68
@@ -0,0 +1,850 @@
1
+ # Symbols Cookbook — DOMQL v3 Recipes
2
+
3
+ 28 complete, runnable DOMQL v3 component recipes covering state, events, rendering, data fetching, and more.
4
+
5
+ ---
6
+
7
+ ## 1. State Toggle
8
+
9
+ Toggle a boolean and update UI.
10
+
11
+ ```js
12
+ export const StateToggle = {
13
+ flexFlow: 'y',
14
+ gap: 'A',
15
+ state: {
16
+ isOn: false,
17
+ },
18
+ Button: {
19
+ tag: 'button',
20
+ text: (element, state) => state.isOn ? 'Turn Off' : 'Turn On',
21
+ onClick: (event, element, state) => state.toggle('isOn'),
22
+ },
23
+ LabelTag: {
24
+ align: 'center',
25
+ gap: 'X2',
26
+ Circle: {
27
+ boxSize: 'X2',
28
+ isActive: (element, state) => state.isOn,
29
+ round: 'C1',
30
+ '.isActive': {
31
+ background: 'green',
32
+ },
33
+ '!isActive': {
34
+ background: 'gray',
35
+ },
36
+ },
37
+ P: {
38
+ text: (element, state) => state.isOn ? 'ON' : 'OFF',
39
+ },
40
+ },
41
+ }
42
+ ```
43
+
44
+ ---
45
+
46
+ ## 2. Counter
47
+
48
+ Increment and decrement a number.
49
+
50
+ ```js
51
+ export const Counter = {
52
+ state: {
53
+ count: 0,
54
+ },
55
+ Flex: {
56
+ align: 'center',
57
+ gap: 'A',
58
+ Button: {
59
+ text: '+',
60
+ onClick: (event, element, state) => state.update({
61
+ count: state.count + 1
62
+ }),
63
+ },
64
+ P: {
65
+ text: (element, state) => 'Count: ' + state.count,
66
+ },
67
+ },
68
+ }
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 3. Conditional Rendering
74
+
75
+ Show or hide content based on state.
76
+
77
+ ```js
78
+ export const ConditionalRender = {
79
+ state: {
80
+ show: false,
81
+ },
82
+ Button: {
83
+ tag: 'button',
84
+ text: 'Toggle Secret',
85
+ onClick: (event, element, state) => state.update({
86
+ show: !state.show
87
+ }),
88
+ },
89
+ Secret: {
90
+ if: (element, state) => state.show,
91
+ P: {
92
+ text: 'The secret content is now visible!',
93
+ },
94
+ },
95
+ }
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 4. Async Data Fetch
101
+
102
+ Fetch data from an API and update state.
103
+
104
+ ```js
105
+ export const AsyncFetch = {
106
+ state: {
107
+ buttonText: 'Load Data',
108
+ },
109
+ P: {
110
+ width: 'G',
111
+ text: '{{ setup }}',
112
+ },
113
+ P_2: {
114
+ fontWeight: 'bold',
115
+ width: 'G',
116
+ margin: '0 0 C',
117
+ text: '{{ punchline }}',
118
+ },
119
+ Button: {
120
+ text: '{{ buttonText }}',
121
+ onClick: async (event, element, state) => {
122
+ state.update({ buttonText: 'loading...' })
123
+ const res = await fetch('https://official-joke-api.appspot.com/jokes/programming/random')
124
+ const data = await res.json()
125
+ state.update({
126
+ ...data[0],
127
+ buttonText: 'Load Data'
128
+ })
129
+ },
130
+ },
131
+ }
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 5. Form Input
137
+
138
+ Capture input and reflect in UI.
139
+
140
+ ```js
141
+ export const FormInput = {
142
+ state: {
143
+ name: '',
144
+ },
145
+ Input: {
146
+ placeholder: 'Enter your name',
147
+ onInput: (event, element, state) => state.update({
148
+ name: element.node.value
149
+ }),
150
+ },
151
+ P: {
152
+ text: (element, state) => state.name ?
153
+ 'Hello, ' + state.name + '!' : 'Waiting for input...',
154
+ },
155
+ }
156
+ ```
157
+
158
+ ---
159
+
160
+ ## 6. Form Validation
161
+
162
+ Validation with visual feedback.
163
+
164
+ ```js
165
+ export const FormValidation = {
166
+ state: {
167
+ email: '',
168
+ isValid: true,
169
+ },
170
+ Input: {
171
+ type: 'email',
172
+ placeholder: 'Enter email',
173
+ onInput: (e, el, s) => {
174
+ const val = el.node.value
175
+ const isValid = /^[^@]+@[^@]+\.[^@]+$/.test(val)
176
+ s.update({ email: val, isValid })
177
+ },
178
+ },
179
+ P: {
180
+ text: (el, s) => s.isValid ? 'Valid email' : 'Invalid email',
181
+ '.isValid': { color: 'green' },
182
+ '!isValid': { color: 'red' },
183
+ },
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 7. Two-Way Binding
190
+
191
+ Sync multiple inputs with shared state.
192
+
193
+ ```js
194
+ export const TwoWayBinding = {
195
+ state: {
196
+ text: '',
197
+ },
198
+ Flex: {
199
+ gap: 'A',
200
+ Input_1: {
201
+ placeholder: 'Type here...',
202
+ value: '{{ text }}',
203
+ onInput: (e, el, s) => s.update({ text: el.node.value }),
204
+ },
205
+ Input_2: {
206
+ placeholder: 'Mirrors input 1',
207
+ value: '{{ text }}',
208
+ onInput: (e, el, s) => s.update({ text: el.node.value }),
209
+ },
210
+ },
211
+ P: {
212
+ text: (el, s) => 'Shared: ' + s.text,
213
+ },
214
+ }
215
+ ```
216
+
217
+ ---
218
+
219
+ ## 8. Clock (Auto-update)
220
+
221
+ Auto-update every second using setInterval.
222
+
223
+ ```js
224
+ export const Clock = {
225
+ state: {
226
+ time: '',
227
+ },
228
+ onRender: (el, s) => {
229
+ const int = setInterval(() => {
230
+ s.update({ time: new Date().toLocaleTimeString() })
231
+ }, 1000)
232
+ return () => clearInterval(int)
233
+ },
234
+ P: {
235
+ text: (el, s) => 'Time: ' + s.time,
236
+ },
237
+ }
238
+ ```
239
+
240
+ ---
241
+
242
+ ## 9. Tabs
243
+
244
+ Switch content via active state using children array.
245
+
246
+ ```js
247
+ export const Tabs = {
248
+ state: {
249
+ active: 'Home',
250
+ },
251
+ Flex: {
252
+ gap: 'A2',
253
+ children: ['Home', 'Profile', 'Settings'],
254
+ childrenAs: 'state',
255
+ childExtends: 'Button',
256
+ childProps: {
257
+ text: '{{ value }}',
258
+ onClick: (event, element, state) => state.parent.update({
259
+ active: state.value
260
+ }),
261
+ },
262
+ },
263
+ P: {
264
+ text: 'Tab: {{ active }}',
265
+ },
266
+ }
267
+ ```
268
+
269
+ ---
270
+
271
+ ## 10. Accordion
272
+
273
+ Expand and collapse sections.
274
+
275
+ ```js
276
+ export const Accordion = {
277
+ state: {
278
+ open: null,
279
+ },
280
+ Ul: {
281
+ children: [
282
+ { title: 'Intro', text: 'Welcome!' },
283
+ { title: 'Details', text: 'Here is more.' },
284
+ { title: 'Summary', text: 'Done reading.' },
285
+ ],
286
+ childrenAs: 'state',
287
+ childExtends: 'Li',
288
+ childProps: {
289
+ H6: {
290
+ text: '{{ title }}',
291
+ margin: '0',
292
+ onClick: (event, element, state) => state.parent.update({
293
+ open: state.parent.open === state.title ? null : state.title
294
+ }),
295
+ },
296
+ P: {
297
+ if: (element, state) => state.parent.open === state.title,
298
+ margin: 'X 0 C1',
299
+ text: '{{ text }}',
300
+ },
301
+ },
302
+ },
303
+ }
304
+ ```
305
+
306
+ ---
307
+
308
+ ## 11. Todo App
309
+
310
+ Add and toggle tasks with keyboard input.
311
+
312
+ ```js
313
+ export const TodoApp = {
314
+ state: {
315
+ tasks: [],
316
+ },
317
+ Input: {
318
+ placeholder: 'New task...',
319
+ onKeydown: (e, el, s) => {
320
+ if (e.key === 'Enter') {
321
+ const value = el.node.value.trim()
322
+ if (value) {
323
+ s.update({
324
+ tasks: [...s.tasks, { text: value, isDone: false }]
325
+ })
326
+ el.node.value = ''
327
+ }
328
+ }
329
+ },
330
+ },
331
+ Ul: {
332
+ children: (el, s) => s.tasks,
333
+ childrenAs: 'state',
334
+ childExtends: 'Li',
335
+ childProps: {
336
+ text: '{{ text }}',
337
+ onClick: (e, el, s) =>
338
+ s.parent.update({
339
+ tasks: s.parent.tasks.map(t =>
340
+ t.text === s.text ? { ...t, isDone: !t.isDone } : t
341
+ ),
342
+ }),
343
+ '.isDone': { textDecoration: 'line-through' },
344
+ '!isDone': { textDecoration: 'none' },
345
+ },
346
+ },
347
+ }
348
+ ```
349
+
350
+ ---
351
+
352
+ ## 12. Dynamic List
353
+
354
+ Add and remove list items dynamically.
355
+
356
+ ```js
357
+ export const DynamicList = {
358
+ state: {
359
+ items: ['Apples', 'Oranges'],
360
+ },
361
+ Flex: {
362
+ gap: 'A',
363
+ Button_Add: {
364
+ text: 'Add Item',
365
+ onClick: (e, el, s) => s.replace({
366
+ items: [...s.items, 'Item ' + (s.items.length + 1)]
367
+ }),
368
+ },
369
+ Button_Remove: {
370
+ text: 'Remove Last',
371
+ onClick: (e, el, s) => s.replace({
372
+ items: s.items.slice(0, -1)
373
+ }),
374
+ },
375
+ },
376
+ Ul: {
377
+ children: (el, s) => s.items,
378
+ childrenAs: 'state',
379
+ childExtends: 'Li',
380
+ childProps: { text: '{{ value }}' },
381
+ },
382
+ }
383
+ ```
384
+
385
+ ---
386
+
387
+ ## 13. API Pagination
388
+
389
+ Fetch paginated data with prev/next buttons.
390
+
391
+ ```js
392
+ export const ApiPagination = {
393
+ state: {
394
+ page: 1,
395
+ data: [],
396
+ },
397
+ scope: {
398
+ load: async (el, s) => {
399
+ const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=3&_page=' + s.page)
400
+ const json = await res.json()
401
+ s.replace({ data: json })
402
+ },
403
+ },
404
+ onRender: (el, s) => {
405
+ el.scope.load(el, s)
406
+ },
407
+ Flex: {
408
+ gap: 'A',
409
+ Button_Prev: {
410
+ text: 'Prev',
411
+ onClick: (e, el, s) => {
412
+ if (s.page > 1) {
413
+ s.update({ page: s.page - 1, data: [{ title: 'loading' }] })
414
+ el.scope.load(el, s)
415
+ }
416
+ },
417
+ },
418
+ Button_Next: {
419
+ text: 'Next',
420
+ onClick: (e, el, s) => {
421
+ s.update({ page: s.page + 1, data: [{ title: 'loading' }] })
422
+ el.scope.load(el, s)
423
+ },
424
+ },
425
+ },
426
+ Ul: {
427
+ children: (el, s) => s.data,
428
+ childrenAs: 'state',
429
+ childExtends: 'Li',
430
+ childProps: { text: '{{ title }}' },
431
+ },
432
+ }
433
+ ```
434
+
435
+ ---
436
+
437
+ ## 14. Modal Window
438
+
439
+ Open and close a modal overlay.
440
+
441
+ ```js
442
+ export const ModalExample = {
443
+ state: {
444
+ open: false,
445
+ },
446
+ Button: {
447
+ text: 'Open Modal',
448
+ onClick: (e, el, s) => s.update({ open: true }),
449
+ },
450
+ Modal: {
451
+ if: (el, s) => s.open,
452
+ position: 'fixed',
453
+ top: '50%',
454
+ left: '50%',
455
+ transform: 'translate(-50%, -50%)',
456
+ theme: 'dialog',
457
+ padding: 'C2',
458
+ P: { text: 'This is a modal window.' },
459
+ Button: {
460
+ text: 'Close',
461
+ onClick: (e, el, s) => s.update({ open: false }),
462
+ },
463
+ },
464
+ }
465
+ ```
466
+
467
+ ---
468
+
469
+ ## 15. WebSocket
470
+
471
+ Receive live messages via WebSocket.
472
+
473
+ ```js
474
+ export const WebSocketDemo = {
475
+ state: {
476
+ msgs: [],
477
+ },
478
+ scope: {},
479
+ onRender: (el, s) => {
480
+ const ws = new WebSocket('wss://ws.postman-echo.com/raw')
481
+ el.scope.ws = ws
482
+ ws.onopen = () => ws.send('Hello WebSocket!')
483
+ ws.onmessage = (e) => {
484
+ s.update({ msgs: [...s.msgs, e.data] })
485
+ }
486
+ },
487
+ Ul: {
488
+ children: (el, s) => s.msgs,
489
+ childrenAs: 'state',
490
+ childExtends: 'Li',
491
+ childProps: { text: '{{ value }}' },
492
+ },
493
+ Button: {
494
+ text: 'Send new message',
495
+ onClick: (event, element, state) => {
496
+ element.scope.ws.send('Message at ' + new Date().toLocaleTimeString())
497
+ },
498
+ },
499
+ }
500
+ ```
501
+
502
+ ---
503
+
504
+ ## 16. Theme Switcher
505
+
506
+ Toggle light/dark theme dynamically.
507
+
508
+ ```js
509
+ export const ThemeSwitcher = {
510
+ state: {
511
+ theme: 'light',
512
+ },
513
+ Button: {
514
+ text: (element, state) => state.theme === 'light' ? 'Go Dark' : 'Go Light',
515
+ onClick: (event, element, state) => state.update({
516
+ theme: state.theme === 'light' ? 'dark' : 'light'
517
+ }),
518
+ },
519
+ Box: {
520
+ theme: (el, s) => 'document @' + s.theme,
521
+ P: {
522
+ text: (el, s) => 'Theme: ' + s.theme,
523
+ },
524
+ },
525
+ }
526
+ ```
527
+
528
+ ---
529
+
530
+ ## 17. Fade Animation
531
+
532
+ Animate opacity on toggle with CSS transitions.
533
+
534
+ ```js
535
+ export const FadeAnimation = {
536
+ state: {
537
+ isVisible: true,
538
+ },
539
+ IconButton: {
540
+ margin: '- B2 C1',
541
+ icon: (el, s) => s.isVisible ? 'eye' : 'eyeOff',
542
+ onClick: (e, el, s) => s.toggle('isVisible'),
543
+ },
544
+ Flex: {
545
+ '.isVisible': { opacity: '1' },
546
+ '!isVisible': { opacity: '0' },
547
+ transition: 'opacity 0.5s',
548
+ theme: 'dialog',
549
+ boxSize: 'E',
550
+ marginTop: 'A',
551
+ Box: { margin: 'auto', text: 'Content' },
552
+ },
553
+ }
554
+ ```
555
+
556
+ ---
557
+
558
+ ## 18. Stopwatch
559
+
560
+ Start, stop, and reset timer.
561
+
562
+ ```js
563
+ export const Stopwatch = {
564
+ state: {
565
+ running: false,
566
+ time: 0,
567
+ },
568
+ onRender: (el, s) => {
569
+ setInterval(() => {
570
+ if (s.running) s.update({ time: s.time + 1 })
571
+ }, 1000)
572
+ },
573
+ Flex: {
574
+ gap: 'A',
575
+ Button_Start: {
576
+ text: (el, s) => s.running ? 'Pause' : 'Start',
577
+ onClick: (e, el, s) => s.update({ running: !s.running }),
578
+ },
579
+ Button_Reset: {
580
+ text: 'Reset',
581
+ onClick: (e, el, s) => s.update({ time: 0 }),
582
+ },
583
+ },
584
+ P: {
585
+ text: (el, s) => 'Elapsed: ' + s.time + 's',
586
+ },
587
+ }
588
+ ```
589
+
590
+ ---
591
+
592
+ ## 19. Progress Bar
593
+
594
+ Fill dynamically with state.
595
+
596
+ ```js
597
+ export const ProgressBar = {
598
+ state: {
599
+ progress: 0,
600
+ },
601
+ Button: {
602
+ text: 'Increase',
603
+ onClick: (event, element, state) =>
604
+ state.update({ progress: Math.min(state.progress + 10, 100) }),
605
+ },
606
+ Bar: {
607
+ width: (el, s) => s.progress + '%',
608
+ height: '20px',
609
+ background: 'green',
610
+ },
611
+ P: {
612
+ text: (element, state) => 'Progress: ' + state.progress + '%',
613
+ },
614
+ }
615
+ ```
616
+
617
+ ---
618
+
619
+ ## 20. Draggable Box
620
+
621
+ Drag an element using mouse events.
622
+
623
+ ```js
624
+ export const DragBox = {
625
+ state: {
626
+ dragging: false,
627
+ },
628
+ Box: {
629
+ boxSize: '90px',
630
+ background: 'blue',
631
+ position: 'absolute',
632
+ onMousedown: (e, el, s) => (s.dragging = true),
633
+ onMouseup: (e, el, s) => (s.dragging = false),
634
+ onMousemove: (e, el, s) => {
635
+ if (s.dragging) el.setProps({
636
+ left: (e.clientX - 45) + 'px',
637
+ top: (e.clientY - 45) + 'px'
638
+ })
639
+ },
640
+ },
641
+ }
642
+ ```
643
+
644
+ ---
645
+
646
+ ## 21. Lazy Image
647
+
648
+ Load image on intersection using IntersectionObserver.
649
+
650
+ ```js
651
+ export const LazyImage = {
652
+ state: {
653
+ loaded: false,
654
+ },
655
+ onRender: (el, s) => {
656
+ const observer = new IntersectionObserver(entries => {
657
+ if (entries[0].isIntersecting) s.update({ loaded: true })
658
+ })
659
+ observer.observe(el.Img.node)
660
+ },
661
+ Img: {
662
+ src: (el, s) => s.loaded ? 'https://picsum.photos/300/200' : '',
663
+ alt: 'Lazy loaded image',
664
+ },
665
+ }
666
+ ```
667
+
668
+ ---
669
+
670
+ ## 22. Text Typer
671
+
672
+ Animate text one character at a time.
673
+
674
+ ```js
675
+ export const TextTyper = {
676
+ state: {
677
+ text: '',
678
+ full: 'Welcome to Symbols!',
679
+ },
680
+ scope: {
681
+ type: (el, s) => {
682
+ let i = 0
683
+ const int = setInterval(() => {
684
+ if (i < s.full.length) {
685
+ s.update({ text: s.text + s.full[i++] })
686
+ } else clearInterval(int)
687
+ }, 75)
688
+ },
689
+ },
690
+ onRender: (el, s) => el.scope.type(el, s),
691
+ P: {
692
+ text: '{{ text }}',
693
+ onClick: (ev, el, s) => {
694
+ s.text = ''
695
+ el.scope.type(el, s)
696
+ },
697
+ },
698
+ }
699
+ ```
700
+
701
+ ---
702
+
703
+ ## 23. Temperature Converter
704
+
705
+ Convert Celsius to Fahrenheit in real time.
706
+
707
+ ```js
708
+ export const TempConverter = {
709
+ state: { c: 0, f: 32 },
710
+ Input_C: {
711
+ type: 'number',
712
+ value: '{{ c }}',
713
+ onInput: (e, el, s) => {
714
+ const c = parseFloat(el.node.value)
715
+ s.update({ c, f: (c * 9 / 5) + 32 })
716
+ },
717
+ },
718
+ Input_F: {
719
+ type: 'number',
720
+ value: '{{ f }}',
721
+ onInput: (e, el, s) => {
722
+ const f = parseFloat(el.node.value)
723
+ s.update({ f, c: (f - 32) * 5 / 9 })
724
+ },
725
+ },
726
+ P: {
727
+ text: (el, s) => s.c + '°C = ' + s.f.toFixed(1) + '°F',
728
+ },
729
+ }
730
+ ```
731
+
732
+ ---
733
+
734
+ ## 24. Image Gallery
735
+
736
+ Cycle through images with state.
737
+
738
+ ```js
739
+ export const ImageGallery = {
740
+ state: {
741
+ index: 0,
742
+ images: [
743
+ 'https://picsum.photos/200/200',
744
+ 'https://picsum.photos/201/200',
745
+ 'https://picsum.photos/202/200',
746
+ ],
747
+ },
748
+ Img: {
749
+ src: (el, s) => s.images[s.index],
750
+ alt: 'Gallery image',
751
+ },
752
+ Hr: {},
753
+ Button_Next: {
754
+ text: 'Next',
755
+ onClick: (e, el, s) =>
756
+ s.update({ index: (s.index + 1) % s.images.length }),
757
+ },
758
+ }
759
+ ```
760
+
761
+ ---
762
+
763
+ ## 25. Local Storage
764
+
765
+ Persist state between page reloads.
766
+
767
+ ```js
768
+ export const LocalStorage = {
769
+ state: { note: '' },
770
+ onInit: (el, s) => {
771
+ s.note = localStorage.getItem('note') || ''
772
+ },
773
+ Textarea: {
774
+ placeholder: 'Write something...',
775
+ value: '{{ note }}',
776
+ onInput: (event, element, state) => {
777
+ const value = element.node.value
778
+ state.update({ note: value })
779
+ localStorage.setItem('note', value)
780
+ },
781
+ },
782
+ P: {
783
+ text: (element, state) => 'Saved: ' + state.note,
784
+ },
785
+ }
786
+ ```
787
+
788
+ ---
789
+
790
+ ## 26. Slider Control
791
+
792
+ Adjust a range and reflect in state.
793
+
794
+ ```js
795
+ export const SliderControl = {
796
+ state: { volume: 50 },
797
+ Input: {
798
+ type: 'range',
799
+ min: 0,
800
+ max: 100,
801
+ value: '{{ volume }}',
802
+ onInput: (e, el, s) => s.update({
803
+ volume: parseInt(el.node.value)
804
+ }),
805
+ },
806
+ P: {
807
+ text: (el, s) => 'Volume: ' + s.volume,
808
+ },
809
+ }
810
+ ```
811
+
812
+ ---
813
+
814
+ ## 27. Keyboard Shortcut
815
+
816
+ Listen for keypress and update state.
817
+
818
+ ```js
819
+ export const KeyboardShortcut = {
820
+ state: { key: 'None' },
821
+ onRender: (el, s) => {
822
+ window.addEventListener('keydown', e =>
823
+ s.update({ key: e.key })
824
+ )
825
+ },
826
+ P: {
827
+ text: (el, s) => 'Last key pressed: ' + s.key,
828
+ },
829
+ }
830
+ ```
831
+
832
+ ---
833
+
834
+ ## 28. Mouse Tracker
835
+
836
+ Track cursor position in real-time.
837
+
838
+ ```js
839
+ export const MouseTracker = {
840
+ state: { x: 0, y: 0 },
841
+ onRender: (el, s) => {
842
+ window.addEventListener('mousemove', e =>
843
+ s.update({ x: e.clientX, y: e.clientY })
844
+ )
845
+ },
846
+ P: {
847
+ text: (el, s) => 'X: ' + s.x + ', Y: ' + s.y,
848
+ },
849
+ }
850
+ ```