@symbo.ls/mcp 1.0.11 → 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 +1 -1
- package/symbols_mcp/skills/AUDIT.md +148 -174
- package/symbols_mcp/skills/BRAND_IDENTITY.md +75 -0
- package/symbols_mcp/skills/COMPONENTS.md +151 -306
- package/symbols_mcp/skills/COOKBOOK.md +850 -0
- package/symbols_mcp/skills/DEFAULT_COMPONENTS.md +3856 -0
- 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 +473 -181
- 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 +158 -117
- package/symbols_mcp/skills/PATTERNS.md +101 -74
- package/symbols_mcp/skills/PRESENTATION.md +78 -0
- package/symbols_mcp/skills/PROJECT_STRUCTURE.md +114 -116
- package/symbols_mcp/skills/RULES.md +179 -148
- package/symbols_mcp/skills/RUNNING_APPS.md +476 -0
- package/symbols_mcp/skills/SEO-METADATA.md +33 -18
- package/symbols_mcp/skills/SNIPPETS.md +598 -0
- package/symbols_mcp/skills/SSR-BRENDER.md +99 -0
- package/symbols_mcp/skills/SYNTAX.md +356 -298
- package/symbols_mcp/skills/BRAND_INDENTITY.md +0 -69
- package/symbols_mcp/skills/THE_PRESENTATION.md +0 -69
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
# Symbols Snippets — Production Component Patterns
|
|
2
|
+
|
|
3
|
+
11 production-tested component patterns. Use as starting points for common UI needs.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Navigation Header
|
|
8
|
+
|
|
9
|
+
Sticky responsive header with logo, nav links, and mobile menu button.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
export const Header = {
|
|
13
|
+
tag: 'header',
|
|
14
|
+
position: 'sticky',
|
|
15
|
+
top: '0',
|
|
16
|
+
width: '100%',
|
|
17
|
+
zIndex: '100',
|
|
18
|
+
padding: 'Z2 B',
|
|
19
|
+
align: 'center space-between',
|
|
20
|
+
theme: 'header',
|
|
21
|
+
|
|
22
|
+
Logo: {
|
|
23
|
+
extends: 'Link',
|
|
24
|
+
href: '/',
|
|
25
|
+
text: 'Brand',
|
|
26
|
+
fontWeight: '700',
|
|
27
|
+
fontSize: 'B',
|
|
28
|
+
textDecoration: 'none',
|
|
29
|
+
onClick: (e, el) => {
|
|
30
|
+
e.preventDefault()
|
|
31
|
+
el.router('/', el.getRoot())
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
Nav: {
|
|
36
|
+
gap: 'B',
|
|
37
|
+
align: 'center',
|
|
38
|
+
|
|
39
|
+
children: ['Home', 'About', 'Contact'],
|
|
40
|
+
childrenAs: 'state',
|
|
41
|
+
childExtends: 'Link',
|
|
42
|
+
childProps: {
|
|
43
|
+
text: '{{ value }}',
|
|
44
|
+
href: (el, s) => '/' + s.value.toLowerCase(),
|
|
45
|
+
textDecoration: 'none',
|
|
46
|
+
':hover': { opacity: '.7' },
|
|
47
|
+
onClick: (e, el, s) => {
|
|
48
|
+
e.preventDefault()
|
|
49
|
+
el.router('/' + s.value.toLowerCase(), el.getRoot())
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
'@tabletS': { display: 'none' },
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
MenuButton: {
|
|
57
|
+
extends: 'IconButton',
|
|
58
|
+
icon: 'menu',
|
|
59
|
+
display: 'none',
|
|
60
|
+
'@tabletS': { display: 'flex' },
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 2. Hero Section
|
|
68
|
+
|
|
69
|
+
Full-width hero with heading, description, and CTA buttons.
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
export const Hero = {
|
|
73
|
+
flow: 'y',
|
|
74
|
+
align: 'center center',
|
|
75
|
+
padding: 'F A',
|
|
76
|
+
gap: 'B',
|
|
77
|
+
textAlign: 'center',
|
|
78
|
+
|
|
79
|
+
Tag: {
|
|
80
|
+
tag: 'span',
|
|
81
|
+
text: 'NEW',
|
|
82
|
+
padding: 'X A',
|
|
83
|
+
round: 'Z',
|
|
84
|
+
theme: 'primary',
|
|
85
|
+
fontSize: 'Y',
|
|
86
|
+
fontWeight: '600',
|
|
87
|
+
letterSpacing: '1px',
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
H: {
|
|
91
|
+
tag: 'h1',
|
|
92
|
+
text: 'Build faster with Symbols',
|
|
93
|
+
maxWidth: 'H',
|
|
94
|
+
fontSize: 'E',
|
|
95
|
+
fontWeight: '800',
|
|
96
|
+
lineHeight: '1.1',
|
|
97
|
+
'@tabletS': { fontSize: 'D' },
|
|
98
|
+
'@mobileL': { fontSize: 'C2' },
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
P: {
|
|
102
|
+
text: 'Design system framework for building modern interfaces',
|
|
103
|
+
maxWidth: 'G',
|
|
104
|
+
fontSize: 'A2',
|
|
105
|
+
opacity: '.7',
|
|
106
|
+
lineHeight: '1.6',
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
Actions: {
|
|
110
|
+
gap: 'A',
|
|
111
|
+
align: 'center',
|
|
112
|
+
Button_Primary: {
|
|
113
|
+
text: 'Get Started',
|
|
114
|
+
theme: 'primary',
|
|
115
|
+
padding: 'Z2 B',
|
|
116
|
+
},
|
|
117
|
+
Button_Secondary: {
|
|
118
|
+
text: 'Learn More',
|
|
119
|
+
theme: 'secondary',
|
|
120
|
+
padding: 'Z2 B',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 3. Feature Card
|
|
129
|
+
|
|
130
|
+
Card with icon, title, and description.
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
export const FeatureCard = {
|
|
134
|
+
flow: 'y',
|
|
135
|
+
gap: 'A',
|
|
136
|
+
padding: 'B',
|
|
137
|
+
round: 'A',
|
|
138
|
+
theme: 'card',
|
|
139
|
+
|
|
140
|
+
Icon: {
|
|
141
|
+
name: 'star',
|
|
142
|
+
boxSize: 'B',
|
|
143
|
+
color: 'primary',
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
H: {
|
|
147
|
+
tag: 'h3',
|
|
148
|
+
text: 'Feature Title',
|
|
149
|
+
fontSize: 'A2',
|
|
150
|
+
fontWeight: '600',
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
P: {
|
|
154
|
+
text: 'Description of the feature goes here.',
|
|
155
|
+
opacity: '.7',
|
|
156
|
+
lineHeight: '1.5',
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 4. Feature Grid
|
|
164
|
+
|
|
165
|
+
Responsive grid of feature cards using `children` + `childExtends`.
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
export const FeatureGrid = {
|
|
169
|
+
extends: 'Grid',
|
|
170
|
+
columns: 'repeat(3, 1fr)',
|
|
171
|
+
gap: 'B',
|
|
172
|
+
padding: 'D A',
|
|
173
|
+
|
|
174
|
+
'@tabletS': { columns: 'repeat(2, 1fr)' },
|
|
175
|
+
'@mobileL': { columns: '1fr' },
|
|
176
|
+
|
|
177
|
+
children: [
|
|
178
|
+
{ icon: 'zap', title: 'Fast', description: 'Lightning fast rendering' },
|
|
179
|
+
{ icon: 'shield', title: 'Secure', description: 'Built-in security features' },
|
|
180
|
+
{ icon: 'code', title: 'Clean', description: 'No-import architecture' },
|
|
181
|
+
],
|
|
182
|
+
childrenAs: 'state',
|
|
183
|
+
childExtends: 'FeatureCard',
|
|
184
|
+
childProps: {
|
|
185
|
+
Icon: { name: '{{ icon }}' },
|
|
186
|
+
H: { text: '{{ title }}' },
|
|
187
|
+
P: { text: '{{ description }}' },
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 5. Pricing Card
|
|
195
|
+
|
|
196
|
+
Pricing option with features list and CTA.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
export const PriceCard = {
|
|
200
|
+
flow: 'y',
|
|
201
|
+
padding: 'C',
|
|
202
|
+
round: 'A',
|
|
203
|
+
theme: 'card',
|
|
204
|
+
gap: 'B',
|
|
205
|
+
minWidth: 'F',
|
|
206
|
+
|
|
207
|
+
Header: {
|
|
208
|
+
flow: 'y',
|
|
209
|
+
gap: 'X2',
|
|
210
|
+
H: {
|
|
211
|
+
tag: 'h3',
|
|
212
|
+
text: 'Pro Plan',
|
|
213
|
+
fontSize: 'A2',
|
|
214
|
+
},
|
|
215
|
+
Price: {
|
|
216
|
+
align: 'end',
|
|
217
|
+
gap: 'X',
|
|
218
|
+
H2: {
|
|
219
|
+
tag: 'span',
|
|
220
|
+
text: '$29',
|
|
221
|
+
fontSize: 'D',
|
|
222
|
+
fontWeight: '800',
|
|
223
|
+
},
|
|
224
|
+
P: {
|
|
225
|
+
text: '/month',
|
|
226
|
+
opacity: '.5',
|
|
227
|
+
marginBottom: 'X2',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
Features: {
|
|
233
|
+
flow: 'y',
|
|
234
|
+
gap: 'Z2',
|
|
235
|
+
children: [
|
|
236
|
+
'Unlimited projects',
|
|
237
|
+
'Priority support',
|
|
238
|
+
'Custom themes',
|
|
239
|
+
'Team collaboration',
|
|
240
|
+
],
|
|
241
|
+
childrenAs: 'state',
|
|
242
|
+
childExtends: 'Flex',
|
|
243
|
+
childProps: {
|
|
244
|
+
align: 'center',
|
|
245
|
+
gap: 'Z',
|
|
246
|
+
Icon: { name: 'check', color: 'green', boxSize: 'Z2' },
|
|
247
|
+
Text: { text: '{{ value }}' },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
Button: {
|
|
252
|
+
text: 'Get Started',
|
|
253
|
+
theme: 'primary',
|
|
254
|
+
width: '100%',
|
|
255
|
+
padding: 'Z2',
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 6. Testimonial Card
|
|
263
|
+
|
|
264
|
+
User testimonial with avatar and quote.
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
export const TestimonialCard = {
|
|
268
|
+
flow: 'y',
|
|
269
|
+
gap: 'A2',
|
|
270
|
+
padding: 'B',
|
|
271
|
+
round: 'A',
|
|
272
|
+
theme: 'card',
|
|
273
|
+
|
|
274
|
+
Quote: {
|
|
275
|
+
tag: 'blockquote',
|
|
276
|
+
text: '"This framework changed how we build interfaces."',
|
|
277
|
+
fontSize: 'A1',
|
|
278
|
+
lineHeight: '1.6',
|
|
279
|
+
fontStyle: 'italic',
|
|
280
|
+
opacity: '.85',
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
Author: {
|
|
284
|
+
align: 'center',
|
|
285
|
+
gap: 'A',
|
|
286
|
+
Avatar: {
|
|
287
|
+
extends: 'Img',
|
|
288
|
+
round: '100%',
|
|
289
|
+
boxSize: 'B2 B2',
|
|
290
|
+
},
|
|
291
|
+
Info: {
|
|
292
|
+
flow: 'y',
|
|
293
|
+
gap: '0',
|
|
294
|
+
Name: { text: 'Jane Doe', fontWeight: '600', fontSize: 'Z2' },
|
|
295
|
+
Role: { text: 'CTO at Acme', opacity: '.6', fontSize: 'Y' },
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 7. Search with Dropdown
|
|
304
|
+
|
|
305
|
+
Search input with filtered results dropdown.
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
export const SearchDropdown = {
|
|
309
|
+
state: {
|
|
310
|
+
query: '',
|
|
311
|
+
items: ['Dashboard', 'Settings', 'Profile', 'Analytics', 'Reports'],
|
|
312
|
+
},
|
|
313
|
+
position: 'relative',
|
|
314
|
+
|
|
315
|
+
Input: {
|
|
316
|
+
placeholder: 'Search...',
|
|
317
|
+
padding: 'Z2 A',
|
|
318
|
+
width: '100%',
|
|
319
|
+
onInput: (e, el, s) => s.update({ query: el.node.value }),
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
Results: {
|
|
323
|
+
if: (el, s) => s.query.length > 0,
|
|
324
|
+
position: 'absolute',
|
|
325
|
+
top: '100%',
|
|
326
|
+
left: '0',
|
|
327
|
+
width: '100%',
|
|
328
|
+
theme: 'dropdown',
|
|
329
|
+
round: '0 0 Z Z',
|
|
330
|
+
maxHeight: 'E',
|
|
331
|
+
overflow: 'auto',
|
|
332
|
+
zIndex: '10',
|
|
333
|
+
|
|
334
|
+
children: (el, s) =>
|
|
335
|
+
s.items.filter(i => i.toLowerCase().includes(s.query.toLowerCase())),
|
|
336
|
+
childrenAs: 'state',
|
|
337
|
+
childExtends: 'Box',
|
|
338
|
+
childProps: {
|
|
339
|
+
text: '{{ value }}',
|
|
340
|
+
padding: 'Z A',
|
|
341
|
+
cursor: 'pointer',
|
|
342
|
+
':hover': { background: 'hover' },
|
|
343
|
+
onClick: (e, el, s) => {
|
|
344
|
+
s.root.update({ query: s.value })
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 8. Footer
|
|
354
|
+
|
|
355
|
+
Responsive footer with column layout.
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
export const Footer = {
|
|
359
|
+
tag: 'footer',
|
|
360
|
+
flow: 'y',
|
|
361
|
+
gap: 'C',
|
|
362
|
+
padding: 'D B',
|
|
363
|
+
theme: 'footer',
|
|
364
|
+
|
|
365
|
+
Columns: {
|
|
366
|
+
extends: 'Grid',
|
|
367
|
+
columns: 'repeat(4, 1fr)',
|
|
368
|
+
gap: 'C',
|
|
369
|
+
'@tabletS': { columns: 'repeat(2, 1fr)' },
|
|
370
|
+
'@mobileL': { columns: '1fr' },
|
|
371
|
+
|
|
372
|
+
Column_Product: {
|
|
373
|
+
flow: 'y',
|
|
374
|
+
gap: 'Z2',
|
|
375
|
+
H: { tag: 'h4', text: 'Product', fontSize: 'Z2', fontWeight: '600' },
|
|
376
|
+
Link_1: { text: 'Features', href: '/features' },
|
|
377
|
+
Link_2: { text: 'Pricing', href: '/pricing' },
|
|
378
|
+
Link_3: { text: 'Docs', href: '/docs' },
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
Column_Company: {
|
|
382
|
+
flow: 'y',
|
|
383
|
+
gap: 'Z2',
|
|
384
|
+
H: { tag: 'h4', text: 'Company', fontSize: 'Z2', fontWeight: '600' },
|
|
385
|
+
Link_1: { text: 'About', href: '/about' },
|
|
386
|
+
Link_2: { text: 'Blog', href: '/blog' },
|
|
387
|
+
Link_3: { text: 'Careers', href: '/careers' },
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
Bottom: {
|
|
392
|
+
align: 'center space-between',
|
|
393
|
+
borderTop: '1px solid',
|
|
394
|
+
borderTopColor: 'gray3',
|
|
395
|
+
paddingTop: 'A',
|
|
396
|
+
P: { text: '© 2025 Brand. All rights reserved.', opacity: '.5', fontSize: 'Y' },
|
|
397
|
+
},
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 9. Data Table
|
|
404
|
+
|
|
405
|
+
Structured data table from state array.
|
|
406
|
+
|
|
407
|
+
```js
|
|
408
|
+
export const DataTable = {
|
|
409
|
+
state: {
|
|
410
|
+
rows: [
|
|
411
|
+
{ name: 'Alice', role: 'Engineer', status: 'Active' },
|
|
412
|
+
{ name: 'Bob', role: 'Designer', status: 'Away' },
|
|
413
|
+
{ name: 'Carol', role: 'Manager', status: 'Active' },
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
tag: 'table',
|
|
418
|
+
width: '100%',
|
|
419
|
+
borderCollapse: 'collapse',
|
|
420
|
+
|
|
421
|
+
Thead: {
|
|
422
|
+
tag: 'thead',
|
|
423
|
+
Tr: {
|
|
424
|
+
tag: 'tr',
|
|
425
|
+
Th_Name: { tag: 'th', text: 'Name', padding: 'Z A', textAlign: 'left' },
|
|
426
|
+
Th_Role: { tag: 'th', text: 'Role', padding: 'Z A', textAlign: 'left' },
|
|
427
|
+
Th_Status: { tag: 'th', text: 'Status', padding: 'Z A', textAlign: 'left' },
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
Tbody: {
|
|
432
|
+
tag: 'tbody',
|
|
433
|
+
children: (el, s) => s.rows,
|
|
434
|
+
childrenAs: 'state',
|
|
435
|
+
childExtends: {
|
|
436
|
+
tag: 'tr',
|
|
437
|
+
borderBottom: '1px solid',
|
|
438
|
+
borderBottomColor: 'gray2',
|
|
439
|
+
},
|
|
440
|
+
childProps: {
|
|
441
|
+
Td_Name: { tag: 'td', text: '{{ name }}', padding: 'Z A' },
|
|
442
|
+
Td_Role: { tag: 'td', text: '{{ role }}', padding: 'Z A' },
|
|
443
|
+
Td_Status: { tag: 'td', text: '{{ status }}', padding: 'Z A' },
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## 10. Layout with Sidebar
|
|
452
|
+
|
|
453
|
+
App layout with sidebar navigation and main content area.
|
|
454
|
+
|
|
455
|
+
```js
|
|
456
|
+
export const AppLayout = {
|
|
457
|
+
flow: 'x',
|
|
458
|
+
width: '100%',
|
|
459
|
+
minHeight: '100vh',
|
|
460
|
+
|
|
461
|
+
Sidebar: {
|
|
462
|
+
flow: 'y',
|
|
463
|
+
width: 'F',
|
|
464
|
+
padding: 'A',
|
|
465
|
+
gap: 'X2',
|
|
466
|
+
theme: 'sidebar',
|
|
467
|
+
'@tabletS': { display: 'none' },
|
|
468
|
+
|
|
469
|
+
children: [
|
|
470
|
+
{ label: 'Dashboard', icon: 'home', path: '/' },
|
|
471
|
+
{ label: 'Projects', icon: 'folder', path: '/projects' },
|
|
472
|
+
{ label: 'Settings', icon: 'settings', path: '/settings' },
|
|
473
|
+
],
|
|
474
|
+
childrenAs: 'state',
|
|
475
|
+
childExtends: 'Flex',
|
|
476
|
+
childProps: {
|
|
477
|
+
align: 'center',
|
|
478
|
+
gap: 'Z',
|
|
479
|
+
padding: 'Z A',
|
|
480
|
+
round: 'Z',
|
|
481
|
+
cursor: 'pointer',
|
|
482
|
+
':hover': { background: 'hover' },
|
|
483
|
+
Icon: { name: '{{ icon }}', boxSize: 'Z2' },
|
|
484
|
+
Text: { text: '{{ label }}' },
|
|
485
|
+
onClick: (e, el, s) => el.router(s.path, el.getRoot()),
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
Main: {
|
|
490
|
+
flow: 'y',
|
|
491
|
+
flex: '1',
|
|
492
|
+
padding: 'B',
|
|
493
|
+
overflow: 'auto',
|
|
494
|
+
},
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## 11. Notification Toast
|
|
501
|
+
|
|
502
|
+
Auto-dismissing fixed-position notification.
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
export const Toast = {
|
|
506
|
+
align: 'center',
|
|
507
|
+
gap: 'A',
|
|
508
|
+
padding: 'A B',
|
|
509
|
+
round: 'Z',
|
|
510
|
+
position: 'fixed',
|
|
511
|
+
bottom: 'B',
|
|
512
|
+
right: 'B',
|
|
513
|
+
zIndex: '1000',
|
|
514
|
+
theme: 'dialog',
|
|
515
|
+
transition: 'opacity 0.3s, transform 0.3s',
|
|
516
|
+
|
|
517
|
+
Icon: {
|
|
518
|
+
name: 'check',
|
|
519
|
+
boxSize: 'A',
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
Text: {
|
|
523
|
+
text: 'Operation successful',
|
|
524
|
+
fontSize: 'Z2',
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
CloseButton: {
|
|
528
|
+
extends: 'IconButton',
|
|
529
|
+
icon: 'x',
|
|
530
|
+
boxSize: 'A',
|
|
531
|
+
onClick: (e, el) => {
|
|
532
|
+
el.parent.node.style.opacity = '0'
|
|
533
|
+
setTimeout(() => el.parent.setProps({ if: false }), 300)
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## 12. Contact Form
|
|
542
|
+
|
|
543
|
+
Multi-field form with submit handling.
|
|
544
|
+
|
|
545
|
+
```js
|
|
546
|
+
export const ContactForm = {
|
|
547
|
+
tag: 'form',
|
|
548
|
+
flow: 'y',
|
|
549
|
+
gap: 'A',
|
|
550
|
+
maxWidth: 'G',
|
|
551
|
+
|
|
552
|
+
state: {
|
|
553
|
+
name: '',
|
|
554
|
+
email: '',
|
|
555
|
+
message: '',
|
|
556
|
+
submitted: false,
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
Field_Name: {
|
|
560
|
+
extends: 'Field',
|
|
561
|
+
label: 'Name',
|
|
562
|
+
Input: {
|
|
563
|
+
placeholder: 'Your name',
|
|
564
|
+
onInput: (e, el, s) => s.update({ name: el.node.value }),
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
Field_Email: {
|
|
569
|
+
extends: 'Field',
|
|
570
|
+
label: 'Email',
|
|
571
|
+
Input: {
|
|
572
|
+
type: 'email',
|
|
573
|
+
placeholder: 'you@example.com',
|
|
574
|
+
onInput: (e, el, s) => s.update({ email: el.node.value }),
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
Field_Message: {
|
|
579
|
+
extends: 'Field',
|
|
580
|
+
label: 'Message',
|
|
581
|
+
Textarea: {
|
|
582
|
+
placeholder: 'Your message...',
|
|
583
|
+
rows: 4,
|
|
584
|
+
onInput: (e, el, s) => s.update({ message: el.node.value }),
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
Button: {
|
|
589
|
+
text: (el, s) => s.submitted ? 'Sent!' : 'Send Message',
|
|
590
|
+
theme: 'primary',
|
|
591
|
+
width: '100%',
|
|
592
|
+
onClick: async (e, el, s) => {
|
|
593
|
+
e.preventDefault()
|
|
594
|
+
s.update({ submitted: true })
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
}
|
|
598
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Server-Side Rendering with Brender
|
|
2
|
+
|
|
3
|
+
Pre-render Symbols apps to static HTML using `@symbo.ls/brender`. Uses linkedom (virtual DOM) to run the same DOMQL component tree server-side, producing HTML with `data-br` hydration keys for client-side reconnection without re-rendering.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Quick Start — CLI
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Render all static routes
|
|
11
|
+
smbls brender
|
|
12
|
+
|
|
13
|
+
# Custom output directory
|
|
14
|
+
smbls brender --out-dir build
|
|
15
|
+
|
|
16
|
+
# Without prefetch or ISR client bundle
|
|
17
|
+
smbls brender --no-prefetch --no-isr
|
|
18
|
+
|
|
19
|
+
# Watch mode
|
|
20
|
+
smbls brender --watch
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Output goes to `dist-brender/` by default (configurable via `brenderDistDir` in `symbols.json`), separate from the SPA's `dist/` folder.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick Start — Programmatic
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { renderPage, loadProject } from '@symbo.ls/brender'
|
|
31
|
+
|
|
32
|
+
const data = await loadProject('/path/to/project')
|
|
33
|
+
const result = await renderPage(data, '/about', { prefetch: true })
|
|
34
|
+
|
|
35
|
+
// result.html -> complete <!DOCTYPE html> page
|
|
36
|
+
// result.route -> '/about'
|
|
37
|
+
// result.brKeyCount -> number of hydration keys
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## How It Works
|
|
43
|
+
|
|
44
|
+
### Render Phase (Server)
|
|
45
|
+
|
|
46
|
+
1. Create virtual DOM with linkedom
|
|
47
|
+
2. Run DOMQL `create()` against it — full component tree resolves
|
|
48
|
+
3. Stamp `data-br="br-N"` on every element node (sequential, deterministic)
|
|
49
|
+
4. Return HTML string, registry, and element tree
|
|
50
|
+
|
|
51
|
+
### Hydrate Phase (Browser)
|
|
52
|
+
|
|
53
|
+
1. Pre-rendered HTML already in DOM — instant page display
|
|
54
|
+
2. DOMQL re-creates element tree from source definitions
|
|
55
|
+
3. `hydrate()` matches `data-br` keys between DOMQL tree and real DOM
|
|
56
|
+
4. Bidirectional links: `element.node = domNode` and `domNode.ref = element`
|
|
57
|
+
5. Reactive updates, event handlers, and state changes work as if client-rendered
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Key APIs
|
|
62
|
+
|
|
63
|
+
| Function | Purpose |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `renderElement(def, opts?)` | Render a single component to HTML |
|
|
66
|
+
| `render(data, opts?)` | Render a full project (routing, state, designSystem) |
|
|
67
|
+
| `renderPage(data, route, opts?)` | Complete HTML page with metadata, CSS, fonts |
|
|
68
|
+
| `prefetchPageData(data, route)` | SSR data prefetching via DB adapter |
|
|
69
|
+
| `hydrate(element, opts?)` | Client-side: reconnect DOMQL tree to DOM |
|
|
70
|
+
| `loadProject(path)` | Import a `symbols/` directory structure |
|
|
71
|
+
| `generateSitemap(data)` | Generate sitemap.xml from routes |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Features
|
|
76
|
+
|
|
77
|
+
| Feature | Details |
|
|
78
|
+
|---|---|
|
|
79
|
+
| Metadata | Title, description, Open Graph, Twitter cards from declarative `metadata` objects |
|
|
80
|
+
| Emotion CSS | Full CSS extraction including emotion-generated rules, CSS variables, reset, font imports |
|
|
81
|
+
| Theme support | Generates `prefers-color-scheme` media queries and `[data-theme]` selectors (no JS needed) |
|
|
82
|
+
| Data prefetching | Executes declarative `fetch` definitions during SSR via DB adapter (Supabase) |
|
|
83
|
+
| ISR | Optional client bundle for hydration + SPA navigation after initial static load |
|
|
84
|
+
| Sitemap | Auto-generated `sitemap.xml` from route definitions |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
In `symbols.json`:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"brender": true,
|
|
95
|
+
"brenderDistDir": "dist-brender"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Param routes (e.g. `/blog/:id`) are automatically skipped during static generation — they require runtime data.
|