Skip to content

Commit f52f97b

Browse files
committed
docs(docs): add docs site and update USAGE 🍥
- Add docs/ with index.html, README.md, and assets - Add USAGE examples for create, render with refs/onNodeMount, and validation errors - Add docs reference link in USAGE Reference section - Capitalize TOC and section anchors in USAGE - Expand Layout and Style documentation and type reference in USAGE
1 parent 10b1f46 commit f52f97b

File tree

19 files changed

+1695
-26
lines changed

19 files changed

+1695
-26
lines changed

USAGE.md

Lines changed: 100 additions & 26 deletions
Large diffs are not rendered by default.

docs/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Schema2UI Demo
2+
3+
Interactive browser demo for Schema2UI: **create****render** with shared layout, components, and sections from `assets/`. Uses **refs** and **onNodeMount** for dialog, form submit, counter, and output sync.
4+
5+
## Run locally
6+
7+
From the **repo root**:
8+
9+
1. Serve the repo (e.g. `npx serve .`):
10+
11+
```bash
12+
npx serve .
13+
```
14+
15+
2. Open **http://localhost:3000/docs/** (or the port shown).
16+
17+
The demo imports Schema2UI from `https://esm.sh/jsr/@neabyte/schema2ui`, so no build is required.
18+
19+
Opening `docs/index.html` via `file://` can block ES module loading; use a local server.
20+
21+
## What the demo does
22+
23+
Single-page app built from **el.root()** with these sections (in order):
24+
25+
| Section | Description |
26+
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
27+
| **Header** | Sticky bar: logo (Icon), nav links (#features, #form, #more, #docs), “Open Modal” button. |
28+
| **Hero** | Title, paragraph, figure (img + figcaption with lazy load), “Get Started” + “View GitHub” links. |
29+
| **Features** | 3-column grid of Cards (title, description, badge). |
30+
| **Form** | Two columns: left = form (InputGroup name/email, select+optgroup, textarea, fieldset+checkbox, disabled/readonly examples, Submit); right = Icon gallery, scroll area, counter button + label. Submit shows an alert with field values. |
31+
| **More** | Grid: details/summary, ul/ol, dl/dt/dd, blockquote, code, progress, meter, void tags, download link. |
32+
| **Table** | Table with caption, thead, tbody, tfoot. |
33+
| **Showcase** | One-page demo of many Schema2UI features: template, picture+source, hr, pre, datalist, output (synced to input), ARIA, search, article/aside, cite/q/mark/abbr, custom element (el.node), layout x/y, iframe, required/inputmode, select multiple, hidden, popover, audio, video. |
34+
| **Footer** | Copyright + Privacy/Terms links. |
35+
| **Dialog** | Modal (id `main-modal`); opened from header “Open Modal”, closed via “Close” button. |
36+
37+
**refs** and **onNodeMount** are used for:
38+
39+
- **Dialog**`refs.get('main-modal')?.showModal()` / `.close()` from header and close button.
40+
- **Counter**`refs.get('btn-counter')` and `refs.get('count-label')` to update click count text.
41+
- **Form submit**`refs.get('user-name')`, `user-email`, `user-role`, `user-bio`, `newsletter` to read values and show alert.
42+
- **Focus** — focus `user-name` input on mount.
43+
- **Output** — sync `showcase-num` input value to `showcase-output` on input and on mount.
44+
45+
## Assets structure
46+
47+
| File / folder | Description |
48+
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49+
| `index.html` | Entry HTML: `#app` + `<script type="module" src="./assets/demo.js">`. |
50+
| `assets/style.css` | Global styles (e.g. reset, body). |
51+
| `assets/demo.js` | Imports create, render, el from `https://esm.sh/jsr/@neabyte/schema2ui`; builds definition from sections; calls create() and render() with refs, signal, onNodeMount. |
52+
| `assets/constants.js` | Exports `borderColor`, `icons` (SVG path strings). |
53+
| `assets/layout.js` | Exports `Layout` (container, flexCenter, flexBetween, grid, section), `Theme`, `merge()`. |
54+
| `assets/components.js` | Exports `Card`, `InputGroup`, `Button`, `Icon` (schema nodes built with el). |
55+
| `assets/sections/*.js` | One function per section (e.g. headerSection, heroSection); each receives `ctx` (el, Layout, merge, Components, icons, borderColor) and returns a single node. |
56+
| `assets/sections/index.js` | Barrel: re-exports all section functions. |
57+
58+
The demo composes the definition in code (no JSON templates). Sections and components use Schema2UI **layout** and **style** (and **attrs** where needed, e.g. for grid or `attrs.style`).

docs/assets/components.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Local color and style tokens for components (not exported).
2+
const accent = '#0d9488'
3+
const textMuted = '#86868b'
4+
const textPrimary = '#f5f5f7'
5+
const borderColor = 'rgba(255, 255, 255, 0.1)'
6+
const cardBg = 'rgba(30, 30, 40, 0.7)'
7+
8+
const btnBase = {
9+
padding: '10px 20px',
10+
borderRadius: '20px',
11+
cursor: 'pointer',
12+
font: '600 14px sans-serif',
13+
border: 'none'
14+
}
15+
16+
// Card: block with title, description, and optional badge. Returns a schema node (not a DOM element).
17+
export const Card = (el, { title, description, badge }) =>
18+
el.div(
19+
{
20+
style: {
21+
background: cardBg,
22+
border: `1px solid ${borderColor}`,
23+
borderRadius: '16px',
24+
padding: '20px'
25+
}
26+
},
27+
el.div(
28+
{
29+
layout: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' },
30+
style: { margin: '0 0 12px 0' }
31+
},
32+
el.h3({ style: { margin: '0', font: 'bold 14px sans-serif' } }, title),
33+
badge
34+
? el.span(
35+
{
36+
style: {
37+
background: 'rgba(13, 148, 136, 0.2)',
38+
color: accent,
39+
padding: '4px 8px',
40+
borderRadius: '4px',
41+
font: '12px sans-serif'
42+
}
43+
},
44+
badge
45+
)
46+
: null
47+
),
48+
el.p({ style: { color: textMuted, margin: '0', font: '14px sans-serif' } }, description)
49+
)
50+
51+
// InputGroup: label + input; id is used for for/label and input id. attrs are forwarded to the input.
52+
export const InputGroup = (el, { id, label, type = 'text', placeholder, ...attrs }) =>
53+
el.div(
54+
{ style: { margin: '0 0 24px 0' } },
55+
el.label(
56+
{
57+
attrs: { for: id },
58+
style: { margin: '0 0 10px 0', font: '14px sans-serif', color: textPrimary }
59+
},
60+
label
61+
),
62+
el.input({
63+
id,
64+
attrs: { type, placeholder, style: 'box-sizing: border-box', ...attrs },
65+
layout: { width: '100%' },
66+
style: {
67+
background: 'rgba(255, 255, 255, 0.05)',
68+
border: `1px solid ${borderColor}`,
69+
color: textPrimary,
70+
padding: '12px',
71+
borderRadius: '8px'
72+
}
73+
})
74+
)
75+
76+
// Button: primary or secondary; style/layout/attrs can be overridden. buttonAttrs used for disabled, type, etc.
77+
export const Button = (
78+
el,
79+
{ id, content, variant = 'primary', style, layout, attrs: buttonAttrs = {}, ...rest }
80+
) => {
81+
const buttonStyle = {
82+
...btnBase,
83+
...(variant === 'primary'
84+
? { background: accent, color: 'white' }
85+
: { background: 'transparent', border: `1px solid ${borderColor}`, color: textPrimary })
86+
}
87+
return el.button(
88+
{ id, attrs: { ...buttonAttrs }, style: { ...buttonStyle, ...style }, layout },
89+
content
90+
)
91+
}
92+
93+
// Icon: SVG from path (d); size sets width/height. el.node is used for non-standard tags or SVG.
94+
export const Icon = (el, { size = 24, path }) =>
95+
el.node(
96+
'svg',
97+
{
98+
attrs: {
99+
width: size,
100+
height: size,
101+
viewBox: '0 0 24 24',
102+
fill: 'none',
103+
stroke: 'currentColor',
104+
'stroke-width': '2'
105+
}
106+
},
107+
el.node('path', { attrs: { d: path, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' } })
108+
)

docs/assets/constants.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Border color shared across components for consistency.
2+
export const borderColor = 'rgba(255, 255, 255, 0.1)'
3+
4+
// SVG path strings (d attribute) for icons; used with el.node('svg') or Components.Icon.
5+
export const icons = {
6+
menu: 'M4 6h16M4 12h16M4 18h16',
7+
zap: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z',
8+
layers: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5',
9+
code: 'm18 16 4-4-4-4M6 8l-4 4 4 4M14.5 4l-5 16',
10+
check: 'M20 6L9 17l-5-5',
11+
user: 'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2',
12+
mail: 'm22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7',
13+
settings:
14+
'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z'
15+
}

docs/assets/demo.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { create, render, el } from 'https://esm.sh/jsr/@neabyte/schema2ui'
2+
import { Layout, merge } from './layout.js'
3+
import { borderColor, icons } from './constants.js'
4+
import {
5+
headerSection,
6+
heroSection,
7+
featuresSection,
8+
formSection,
9+
dragSection,
10+
moreSection,
11+
tableSection,
12+
showcaseSection,
13+
footerSection,
14+
dialogSection
15+
} from './sections/index.js'
16+
import * as Components from './components.js'
17+
18+
// Context passed to each section: el (builder), Layout, merge, Components, icons, borderColor.
19+
const sectionContext = { el, Layout, merge, Components, icons, borderColor }
20+
21+
// el.root() returns { root: [...] }; children are the results of section calls (each one node).
22+
const definition = el.root(
23+
headerSection(sectionContext),
24+
el.main(
25+
heroSection(sectionContext),
26+
featuresSection(sectionContext),
27+
formSection(sectionContext),
28+
dragSection(sectionContext),
29+
moreSection(sectionContext),
30+
tableSection(sectionContext),
31+
showcaseSection(sectionContext)
32+
),
33+
footerSection(sectionContext),
34+
dialogSection(sectionContext)
35+
)
36+
37+
// create() validates the definition and returns a frozen schema ready for render.
38+
const schema = create(definition)
39+
const refs = new Map()
40+
const app = document.getElementById('app')
41+
const abortController = new AbortController()
42+
let clickCount = 0
43+
44+
// render() mounts the DOM into app. refs is populated for nodes with id; onNodeMount runs after each element is mounted.
45+
render(schema, app, {
46+
refs,
47+
signal: abortController.signal,
48+
onNodeMount(node, element) {
49+
if (node.id && node.id.startsWith('drag-item-')) {
50+
if (element.draggable !== undefined) {
51+
element.draggable = true
52+
} else {
53+
element.setAttribute('draggable', 'true')
54+
}
55+
element.addEventListener('dragstart', e => {
56+
e.dataTransfer.setData('text/plain', node.id)
57+
e.dataTransfer.effectAllowed = 'move'
58+
element.style.opacity = '0.5'
59+
})
60+
element.addEventListener('dragend', () => {
61+
element.style.opacity = ''
62+
})
63+
}
64+
if (node.id === 'drag-dropzone') {
65+
element.addEventListener('dragover', e => {
66+
e.preventDefault()
67+
e.dataTransfer.dropEffect = 'move'
68+
element.style.backgroundColor = 'rgba(255, 255, 255, 0.05)'
69+
})
70+
element.addEventListener('dragleave', () => {
71+
element.style.backgroundColor = ''
72+
})
73+
element.addEventListener('drop', e => {
74+
e.preventDefault()
75+
element.style.backgroundColor = ''
76+
const id = e.dataTransfer.getData('text/plain')
77+
const label = refs.get('drag-dropLabel')
78+
const draggedEl = refs.get(id)
79+
if (draggedEl && draggedEl.parentNode && element !== draggedEl) {
80+
element.appendChild(draggedEl)
81+
if (label) {
82+
label.textContent = 'Dropped: ' + id
83+
label.style.color = '#f5f5f7'
84+
}
85+
} else if (label) {
86+
label.textContent = 'Dropped: ' + id
87+
label.style.color = '#f5f5f7'
88+
}
89+
})
90+
}
91+
if (node.id === 'btn-modal') {
92+
element.addEventListener('click', () => {
93+
refs.get('main-modal')?.showModal()
94+
})
95+
}
96+
if (node.id === 'modal-close') {
97+
element.addEventListener('click', () => {
98+
refs.get('main-modal')?.close()
99+
})
100+
}
101+
if (node.id === 'btn-counter') {
102+
element.addEventListener('click', () => {
103+
clickCount++
104+
element.textContent = `Count: ${clickCount}`
105+
const countLabel = refs.get('count-label')
106+
if (countLabel) countLabel.textContent = `You clicked ${clickCount} times!`
107+
})
108+
}
109+
if (node.id === 'submit-data') {
110+
element.addEventListener('click', () => {
111+
const userName = refs.get('user-name').value
112+
const userEmail = refs.get('user-email').value
113+
const userRole = refs.get('user-role').value
114+
const userBio = refs.get('user-bio')?.value ?? ''
115+
const isNewsletterChecked = refs.get('newsletter').checked
116+
alert(
117+
`Submitted!\nName: ${userName}\nEmail: ${userEmail}\nRole: ${userRole}\nBio: ${userBio || '(empty)'}\nNewsletter: ${isNewsletterChecked}`
118+
)
119+
})
120+
}
121+
if (node.id === 'user-name') {
122+
element.focus()
123+
}
124+
if (node.id === 'showcase-num') {
125+
const outputElement = refs.get('showcase-output')
126+
if (outputElement) outputElement.value = element.value
127+
element.addEventListener('input', () => {
128+
const outputEl = refs.get('showcase-output')
129+
if (outputEl) outputEl.value = element.value
130+
})
131+
}
132+
if (node.id === 'showcase-output') {
133+
const inputElement = refs.get('showcase-num')
134+
if (inputElement) element.value = inputElement.value
135+
}
136+
}
137+
})

docs/assets/layout.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Base colors and tokens for layout (used only in Layout/Theme, not exported separately).
2+
const borderColor = 'rgba(255, 255, 255, 0.1)'
3+
const cardBg = 'rgba(30, 30, 40, 0.7)'
4+
const accent = '#0d9488'
5+
const textMuted = '#86868b'
6+
const textPrimary = '#f5f5f7'
7+
8+
// Layout object used in sections: container, flex, grid. layout/style are schema props for Schema2UI.
9+
export const Layout = {
10+
container: {
11+
layout: { width: '100%', maxWidth: 1200 },
12+
style: { margin: '0 auto', padding: '0 20px' }
13+
},
14+
flexCenter: {
15+
layout: { display: 'flex', alignItems: 'center', justifyContent: 'center' }
16+
},
17+
flexBetween: {
18+
layout: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }
19+
},
20+
grid: (cols = 3, gap = 20) => ({
21+
attrs: { style: `display: grid; grid-template-columns: repeat(${cols}, 1fr); gap: ${gap}px` }
22+
}),
23+
section: {
24+
style: { padding: '80px 0' }
25+
}
26+
}
27+
28+
export const Theme = { borderColor, cardBg, accent, textMuted, textPrimary }
29+
30+
// Deep-merges schema objects (layout/style); use to combine e.g. Layout.container + Layout.grid.
31+
export const merge = (...sourceObjects) => {
32+
const result = {}
33+
for (const sourceObject of sourceObjects) {
34+
for (const key in sourceObject) {
35+
if (
36+
typeof sourceObject[key] === 'object' &&
37+
sourceObject[key] !== null &&
38+
!Array.isArray(sourceObject[key])
39+
) {
40+
result[key] = Object.assign(result[key] || {}, sourceObject[key])
41+
} else {
42+
result[key] = sourceObject[key]
43+
}
44+
}
45+
}
46+
return result
47+
}

0 commit comments

Comments
 (0)