@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.
- package/README.md +1 -0
- package/package.json +5 -2
- package/symbols_mcp/skills/AUDIT.md +148 -174
- package/symbols_mcp/skills/BRAND_IDENTITY.md +75 -0
- package/symbols_mcp/skills/COMPONENTS.md +266 -0
- package/symbols_mcp/skills/COOKBOOK.md +850 -0
- package/symbols_mcp/skills/DEFAULT_COMPONENTS.md +3491 -1637
- package/symbols_mcp/skills/DEFAULT_LIBRARY.md +301 -0
- package/symbols_mcp/skills/DESIGN_CRITIQUE.md +70 -59
- package/symbols_mcp/skills/DESIGN_DIRECTION.md +109 -175
- package/symbols_mcp/skills/DESIGN_SYSTEM.md +722 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_ARCHITECT.md +65 -57
- package/symbols_mcp/skills/DESIGN_TO_CODE.md +83 -64
- package/symbols_mcp/skills/DESIGN_TREND.md +62 -50
- package/symbols_mcp/skills/FIGMA_MATCHING.md +69 -58
- package/symbols_mcp/skills/LEARNINGS.md +374 -0
- package/symbols_mcp/skills/MARKETING_ASSETS.md +71 -59
- package/symbols_mcp/skills/MIGRATION.md +561 -0
- package/symbols_mcp/skills/PATTERNS.md +536 -0
- package/symbols_mcp/skills/PRESENTATION.md +78 -0
- package/symbols_mcp/skills/PROJECT_STRUCTURE.md +398 -0
- package/symbols_mcp/skills/RULES.md +519 -0
- package/symbols_mcp/skills/RUNNING_APPS.md +476 -0
- package/symbols_mcp/skills/SEO-METADATA.md +64 -9
- package/symbols_mcp/skills/SNIPPETS.md +598 -0
- package/symbols_mcp/skills/SSR-BRENDER.md +99 -0
- package/symbols_mcp/skills/SYNTAX.md +835 -0
- package/symbols_mcp/skills/ACCESSIBILITY.md +0 -471
- package/symbols_mcp/skills/ACCESSIBILITY_AUDITORY.md +0 -70
- package/symbols_mcp/skills/AGENT_INSTRUCTIONS.md +0 -265
- package/symbols_mcp/skills/BRAND_INDENTITY.md +0 -69
- package/symbols_mcp/skills/BUILT_IN_COMPONENTS.md +0 -304
- package/symbols_mcp/skills/CLAUDE.md +0 -2158
- package/symbols_mcp/skills/CLI_QUICK_START.md +0 -205
- package/symbols_mcp/skills/DEFAULT_DESIGN_SYSTEM.md +0 -496
- package/symbols_mcp/skills/DESIGN_SYSTEM_CONFIG.md +0 -487
- package/symbols_mcp/skills/DESIGN_SYSTEM_IN_PROPS.md +0 -136
- package/symbols_mcp/skills/DOMQL_v2-v3_MIGRATION.md +0 -236
- package/symbols_mcp/skills/MIGRATE_TO_SYMBOLS.md +0 -634
- package/symbols_mcp/skills/OPTIMIZATIONS_FOR_AGENT.md +0 -253
- package/symbols_mcp/skills/PROJECT_SETUP.md +0 -217
- package/symbols_mcp/skills/QUICKSTART.md +0 -79
- package/symbols_mcp/skills/REMOTE_PREVIEW.md +0 -144
- package/symbols_mcp/skills/SYMBOLS_LOCAL_INSTRUCTIONS.md +0 -1405
- package/symbols_mcp/skills/THE_PRESENTATION.md +0 -69
- 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
|
+
```
|