@pfern/elements 0.1.11 → 0.2.1
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/LICENSE +11 -11
- package/README.md +292 -62
- package/elements.js +4 -1942
- package/env.d.ts +24 -0
- package/mathml.js +1 -0
- package/package.json +20 -23
- package/src/core/elements.js +513 -0
- package/src/core/events.js +143 -0
- package/src/core/props.js +183 -0
- package/src/core/tags.js +225 -0
- package/src/core/tick.js +116 -0
- package/src/core/types.js +696 -0
- package/src/helpers.js +73 -0
- package/src/html.js +996 -0
- package/src/mathml.js +340 -0
- package/src/router.js +51 -0
- package/src/ssr.js +173 -0
- package/src/svg.js +407 -0
- package/types/elements.d.ts +4 -1394
- package/types/mathml.d.ts +1 -0
- package/types/src/core/elements.d.ts +29 -0
- package/types/src/core/events.d.ts +15 -0
- package/types/src/core/props.d.ts +9 -0
- package/types/src/core/tags.d.ts +4 -0
- package/types/src/core/tick.d.ts +5 -0
- package/types/src/core/types.d.ts +507 -0
- package/types/src/helpers.d.ts +5 -0
- package/types/src/html.d.ts +802 -0
- package/types/src/mathml.d.ts +264 -0
- package/types/src/router.d.ts +4 -0
- package/types/src/ssr.d.ts +3 -0
- package/types/src/svg.d.ts +348 -0
package/LICENSE
CHANGED
|
@@ -5,18 +5,18 @@ Copyright (c) 2025 Paul Fernandez
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
7
7
|
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
11
|
|
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
|
13
|
-
all copies or substantial portions of the Software.
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
14
|
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
21
|
THE SOFTWARE.
|
|
22
22
|
|
package/README.md
CHANGED
|
@@ -1,38 +1,80 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- Generated by scripts/sync-readmes.js. Source: README.md -->
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
functions. Components are just functions; updates are just calling the function
|
|
5
|
-
again with new arguments.
|
|
3
|
+
# @pfern/elements
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
A functional, stateless UI toolkit for composing reactive web pages.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
Elements.js borrows the simple elegance of functional UI composition from
|
|
8
|
+
[React](https://react.dev/), distilled to its purest form:
|
|
9
|
+
|
|
10
|
+
- No JSX.
|
|
11
|
+
- No hooks or keys.
|
|
12
|
+
- No virtual DOM heuristics.
|
|
13
|
+
|
|
14
|
+
Components are pure functions; updates are just calling the function again with
|
|
15
|
+
new arguments.
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
While you may choose to manage application logic with tools like
|
|
18
|
+
[Redux](https://redux.js.org/) or [Zustand](https://github.com/pmndrs/zustand),
|
|
19
|
+
Elements.js keeps _UI state_ exactly where it belongs: in the [DOM][dom] itself.
|
|
14
20
|
|
|
21
|
+
## Principles
|
|
22
|
+
|
|
23
|
+
- **Pure data model:** UI elements are represented as data-in, data-out
|
|
24
|
+
functions. They accept W3C standard `props` and child elements as arguments, and
|
|
25
|
+
return nested arrays.
|
|
26
|
+
- **Dynamic updates:** When an event handler returns the output of a component
|
|
27
|
+
element defined within its scope, the element is updated with its new
|
|
28
|
+
arguments.
|
|
29
|
+
- **Imperative boundary, functional surface:** DOM mutation is abstracted away,
|
|
30
|
+
keeping the authoring experience functional and composable.
|
|
31
|
+
|
|
32
|
+
### Example: Recursive counter
|
|
15
33
|
```js
|
|
16
34
|
import { button, component, div, output } from '@pfern/elements'
|
|
17
35
|
|
|
18
36
|
export const counter = component((count = 0) =>
|
|
19
37
|
div(
|
|
20
38
|
output(count),
|
|
21
|
-
button(
|
|
22
|
-
|
|
23
|
-
'Increment')))
|
|
39
|
+
button({ onclick: () => counter(count + 1) },
|
|
40
|
+
'Increment')))
|
|
24
41
|
```
|
|
25
42
|
|
|
26
|
-
##
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### Install as a dependency
|
|
46
|
+
```sh
|
|
47
|
+
npm install @pfern/elements
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Optional 3D / X3DOM helpers
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npm install @pfern/elements @pfern/elements-x3dom
|
|
54
|
+
```
|
|
27
55
|
|
|
56
|
+
### Install as a minimal starter app
|
|
57
|
+
```sh
|
|
58
|
+
npx @pfern/create-elements my-app
|
|
59
|
+
cd my-app
|
|
60
|
+
npm install
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Source code for the examples on this page can be found in the
|
|
64
|
+
[examples/](https://github.com/pfernandez/elements/tree/main/examples) directory of this repository, which are hosted as a live
|
|
65
|
+
demo [here](https://pfernandez.github.io/elements). The starter app also
|
|
66
|
+
includes examples as well as simple URL router for page navigation.
|
|
67
|
+
|
|
68
|
+
## Example: Todos App
|
|
28
69
|
```js
|
|
70
|
+
import { button, component, div, form, input, li, span, ul }
|
|
71
|
+
from '@pfern/elements'
|
|
29
72
|
|
|
30
|
-
|
|
31
|
-
|
|
73
|
+
const demoItems = [{ value: 'Add my first todo', done: true },
|
|
74
|
+
{ value: 'Install elements.js', done: false }]
|
|
32
75
|
|
|
33
76
|
export const todos = component(
|
|
34
|
-
(items =
|
|
35
|
-
|
|
77
|
+
(items = demoItems) => {
|
|
36
78
|
const add = ({ todo: { value } }) =>
|
|
37
79
|
value && todos([...items, { value, done: false }])
|
|
38
80
|
|
|
@@ -44,18 +86,16 @@ export const todos = component(
|
|
|
44
86
|
|
|
45
87
|
return (
|
|
46
88
|
div({ class: 'todos' },
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
button({ onclick: () => remove(item) }, '✕'))))))})
|
|
58
|
-
|
|
89
|
+
form({ onsubmit: add },
|
|
90
|
+
input({ name: 'todo', placeholder: 'What needs doing?' }),
|
|
91
|
+
button({ type: 'submit' }, 'Add')),
|
|
92
|
+
|
|
93
|
+
ul(...items.map(item =>
|
|
94
|
+
li({ style:
|
|
95
|
+
{ 'text-decoration': item.done ? 'line-through' : 'none' } },
|
|
96
|
+
span({ onclick: () => toggle(item) }, item.value),
|
|
97
|
+
button({ onclick: () => remove(item) }, '✕'))))))
|
|
98
|
+
})
|
|
59
99
|
```
|
|
60
100
|
|
|
61
101
|
## Root Rendering Shortcut
|
|
@@ -65,10 +105,8 @@ automatically mount into the corresponding document element—no need to pass a
|
|
|
65
105
|
container.
|
|
66
106
|
|
|
67
107
|
```js
|
|
68
|
-
import {
|
|
69
|
-
|
|
70
|
-
link, main, meta, render, section, title
|
|
71
|
-
} from './elements.js'
|
|
108
|
+
import { body, h1, h2, head, header, html,
|
|
109
|
+
link, main, meta, render, section, title } from '@pfern/elements'
|
|
72
110
|
import { todos } from './components/todos.js'
|
|
73
111
|
|
|
74
112
|
render(
|
|
@@ -77,8 +115,7 @@ render(
|
|
|
77
115
|
title('Elements.js'),
|
|
78
116
|
meta({ name: 'viewport',
|
|
79
117
|
content: 'width=device-width, initial-scale=1.0' }),
|
|
80
|
-
link({ rel: 'stylesheet', href: 'css/style.css' })
|
|
81
|
-
),
|
|
118
|
+
link({ rel: 'stylesheet', href: 'css/style.css' })),
|
|
82
119
|
body(
|
|
83
120
|
header(h1('Elements.js Demo')),
|
|
84
121
|
main(
|
|
@@ -87,15 +124,36 @@ render(
|
|
|
87
124
|
todos())))))
|
|
88
125
|
```
|
|
89
126
|
|
|
90
|
-
##
|
|
127
|
+
## How Updates Work
|
|
128
|
+
|
|
129
|
+
Elements.js is designed so you typically call `render()` once at startup (see
|
|
130
|
+
`examples/index.js`). After that, updates happen by returning a vnode from an
|
|
131
|
+
event handler.
|
|
132
|
+
|
|
133
|
+
### What is a vnode?
|
|
134
|
+
|
|
135
|
+
Elements.js represents UI as plain arrays called **vnodes** (virtual nodes):
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
['div', { class: 'box' }, 'hello', ['span', {}, 'world']]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- `tag`: a string tag name (or `'fragment'` for a wrapper-less group)
|
|
142
|
+
- `props`: an object (attributes, events, and Elements.js hooks like `ontick`)
|
|
143
|
+
- `children`: strings/numbers/vnodes (and optionally `null`/`undefined` slots)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
### Declarative Events
|
|
91
147
|
|
|
92
|
-
|
|
148
|
+
- Any event handler (e.g. `onclick`, `onsubmit`, `oninput`) may return a vnode
|
|
149
|
+
array to trigger a replacement.
|
|
150
|
+
- If the handler returns `undefined` (or any non-vnode value), the event is
|
|
151
|
+
passive and the DOM is left alone.
|
|
152
|
+
- Returned vnodes are applied at the closest component boundary.
|
|
153
|
+
- If you return a vnode from an `<a href>` `onclick` handler, Elements.js
|
|
154
|
+
prevents default navigation for unmodified left-clicks.
|
|
93
155
|
|
|
94
|
-
|
|
95
|
-
vnode to trigger a subtree replacement.
|
|
96
|
-
* If the handler returns `undefined`, the event is treated as passive (no update
|
|
97
|
-
occurs).
|
|
98
|
-
* Returned vnodes are applied at the closest component boundary.
|
|
156
|
+
Errors are not swallowed: thrown errors and rejected Promises propagate.
|
|
99
157
|
|
|
100
158
|
### Form Events
|
|
101
159
|
|
|
@@ -115,14 +173,152 @@ Elements.js will automatically call `event.preventDefault()` *only if* your
|
|
|
115
173
|
handler returns a vnode.
|
|
116
174
|
|
|
117
175
|
```js
|
|
118
|
-
form({
|
|
119
|
-
|
|
120
|
-
|
|
176
|
+
form({ onsubmit: ({ todo: { value } }, e) =>
|
|
177
|
+
value && todos([...items, { value, done: false }]) })
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Routing (optional)
|
|
181
|
+
|
|
182
|
+
For SPAs, register a URL-change handler once:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
import { onNavigate } from '@pfern/elements'
|
|
186
|
+
|
|
187
|
+
onNavigate(() => App())
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
With a handler registered, `a({ href: '/path' }, ...)` intercepts unmodified
|
|
191
|
+
left-clicks for same-origin links and uses the History API instead of
|
|
192
|
+
reloading the page.
|
|
193
|
+
|
|
194
|
+
You can also call `navigate('/path')` directly.
|
|
195
|
+
|
|
196
|
+
### SSG / SSR
|
|
197
|
+
|
|
198
|
+
For build-time prerendering (static site generation) or server-side rendering,
|
|
199
|
+
Elements.js can serialize vnodes to HTML:
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
import { div, html, head, body, title, toHtmlString } from '@pfern/elements'
|
|
203
|
+
|
|
204
|
+
toHtmlString(div('Hello')) // => <div>Hello</div>
|
|
205
|
+
|
|
206
|
+
const doc = html(
|
|
207
|
+
head(title('My page')),
|
|
208
|
+
body(div('Hello')))
|
|
209
|
+
|
|
210
|
+
const htmlText = toHtmlString(doc, { doctype: true })
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Notes:
|
|
214
|
+
- Event handlers (function props like `onclick`) are dropped during
|
|
215
|
+
serialization.
|
|
216
|
+
- `innerHTML` is treated as an explicit escape hatch and is inserted verbatim.
|
|
217
|
+
|
|
218
|
+
### Explicit Rerenders
|
|
219
|
+
|
|
220
|
+
Calling `render(vtree, container)` again is supported (diff + patch). This is
|
|
221
|
+
useful for explicit rerenders (e.g. dev reload, external state updates).
|
|
222
|
+
|
|
223
|
+
To force a full remount (discarding existing DOM state), pass
|
|
224
|
+
`{ replace: true }`.
|
|
225
|
+
|
|
226
|
+
### Why Replacement (No Keys)
|
|
227
|
+
|
|
228
|
+
Replacement updates keep the model simple:
|
|
229
|
+
|
|
230
|
+
- You never have to maintain key stability.
|
|
231
|
+
- Identity is the closest component boundary.
|
|
232
|
+
- The DOM remains the single source of truth for UI state.
|
|
233
|
+
|
|
234
|
+
## Props
|
|
235
|
+
|
|
236
|
+
Element functions accept a single props object as first argument:
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
div({ id: 'x', class: 'box' }, 'hello')
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
In the DOM runtime:
|
|
243
|
+
|
|
244
|
+
- Most props are assigned via `setAttribute`.
|
|
245
|
+
- A small set of keys are treated as property exceptions when the property
|
|
246
|
+
exists on the element.
|
|
247
|
+
- Omitting a prop in a subsequent update clears it from the element.
|
|
248
|
+
|
|
249
|
+
This keeps updates symmetric and predictable.
|
|
250
|
+
|
|
251
|
+
## `ontick` (animation hook)
|
|
252
|
+
|
|
253
|
+
`ontick` is a hook (not a DOM event) that runs once per animation frame. It can
|
|
254
|
+
thread context across frames:
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
transform({
|
|
258
|
+
ontick: (el, ctx = { rotation: 0 }, dt) => {
|
|
259
|
+
el.setAttribute('rotation', `0 1 1 ${ctx.rotation}`)
|
|
260
|
+
return { ...ctx, rotation: ctx.rotation + 0.001 * dt }
|
|
261
|
+
}
|
|
121
262
|
})
|
|
122
263
|
```
|
|
123
264
|
|
|
124
|
-
|
|
125
|
-
|
|
265
|
+
`ontick` must be synchronous. If it throws (or returns a Promise), ticking
|
|
266
|
+
stops, and the error is not swallowed.
|
|
267
|
+
|
|
268
|
+
If the element is inside an `<x3d>` scene, Elements.js waits for the X3DOM
|
|
269
|
+
runtime to be ready before ticking.
|
|
270
|
+
|
|
271
|
+
## X3D / X3DOM (experimental)
|
|
272
|
+
|
|
273
|
+
The optional `@pfern/elements-x3dom` package includes elements for X3DOM’s
|
|
274
|
+
supported X3D node set. You can import them and create 3D scenes
|
|
275
|
+
declaratively:
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
npm i @pfern/elements @pfern/elements-x3dom
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Demo: Interactive 3D Cube
|
|
282
|
+
```js
|
|
283
|
+
import { appearance, box, material, scene,
|
|
284
|
+
shape, transform, viewpoint, x3d } from '@pfern/elements-x3dom'
|
|
285
|
+
|
|
286
|
+
export const cubeScene = () =>
|
|
287
|
+
x3d(
|
|
288
|
+
scene(
|
|
289
|
+
viewpoint({ position: '0 0 6', description: 'Default View' }),
|
|
290
|
+
transform({ rotation: '0 1 0 0.5' },
|
|
291
|
+
shape(
|
|
292
|
+
appearance(
|
|
293
|
+
material({ diffuseColor: '0.2 0.6 1.0' })),
|
|
294
|
+
box()))))
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Lazy Loading
|
|
298
|
+
|
|
299
|
+
X3DOM is lazy-loaded the first time you call any helper from
|
|
300
|
+
`@pfern/elements-x3dom`. For correctness and stability, it always loads the
|
|
301
|
+
vendored `x3dom-full` bundle (plus `x3dom.css`).
|
|
302
|
+
|
|
303
|
+
## Types (the docs)
|
|
304
|
+
|
|
305
|
+
Elements.js is JS-first: TypeScript is not required at runtime. This package
|
|
306
|
+
ships `.d.ts` files so editors like VSCode can provide rich inline docs and
|
|
307
|
+
autocomplete.
|
|
308
|
+
|
|
309
|
+
The goal is for type definitions to be the canonical reference for:
|
|
310
|
+
|
|
311
|
+
* HTML/SVG/X3D element helpers
|
|
312
|
+
* DOM events (including the special form-event signature)
|
|
313
|
+
* Elements.js-specific prop conventions like `ontick`, plus supported
|
|
314
|
+
prop shorthands like `style` (object) and `innerHTML` (escape hatch)
|
|
315
|
+
|
|
316
|
+
Most props are assigned as attributes. A small set of keys are treated as
|
|
317
|
+
property exceptions (when the property exists on the element): `value`,
|
|
318
|
+
`checked`, `selected`, `disabled`, `multiple`, `muted`, `volume`,
|
|
319
|
+
`currentTime`, `playbackRate`, `open`, `indeterminate`.
|
|
320
|
+
|
|
321
|
+
Omitting a prop in a subsequent update clears it from the element.
|
|
126
322
|
|
|
127
323
|
## API
|
|
128
324
|
|
|
@@ -135,6 +331,18 @@ Wrap a recursive pure function that returns a vnode.
|
|
|
135
331
|
Render a vnode into the DOM. If `vnode[0]` is `html`, `head`, or `body`, no
|
|
136
332
|
`container` is required.
|
|
137
333
|
|
|
334
|
+
Pass `{ replace: true }` to force a full remount.
|
|
335
|
+
|
|
336
|
+
### `elements`
|
|
337
|
+
|
|
338
|
+
All tag helpers are also exported in a map for dynamic use:
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
import { elements } from '@pfern/elements'
|
|
342
|
+
|
|
343
|
+
const { div, button } = elements
|
|
344
|
+
```
|
|
345
|
+
|
|
138
346
|
### DOM Elements
|
|
139
347
|
|
|
140
348
|
Every HTML and SVG tag is available as a function:
|
|
@@ -144,25 +352,47 @@ div({ id: 'box' }, 'hello')
|
|
|
144
352
|
svg({ width: 100 }, circle({ r: 10 }))
|
|
145
353
|
```
|
|
146
354
|
|
|
147
|
-
|
|
355
|
+
Curated MathML helpers are available as a separate entrypoint:
|
|
148
356
|
|
|
149
|
-
|
|
150
|
-
|
|
357
|
+
```js
|
|
358
|
+
import { apply, ci, csymbol, math } from '@pfern/elements/mathml'
|
|
151
359
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
360
|
+
math(
|
|
361
|
+
apply(csymbol({ cd: 'ski' }, 'app'), ci('f'), ci('x'))
|
|
362
|
+
)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
For X3D / X3DOM nodes, use `@pfern/elements-x3dom`:
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
import { box } from '@pfern/elements-x3dom'
|
|
369
|
+
|
|
370
|
+
box({ size: '2 2 2', solid: true })
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### `onNavigate(fn[, options])`
|
|
374
|
+
|
|
375
|
+
Register a handler to run after `popstate` (including calls to `navigate()`).
|
|
376
|
+
Use this to re-render your app on URL changes.
|
|
377
|
+
|
|
378
|
+
### `toHtmlString(vnode[, options])`
|
|
379
|
+
|
|
380
|
+
Serialize a vnode tree to HTML (SSG / SSR). Pass `{ doctype: true }` to emit
|
|
381
|
+
`<!doctype html>`.
|
|
382
|
+
|
|
383
|
+
### `navigate(path[, options])`
|
|
384
|
+
|
|
385
|
+
`navigate` updates `window.history` and dispatches a `popstate` event. It is a
|
|
386
|
+
tiny convenience for router-style apps.
|
|
155
387
|
|
|
156
388
|
### Testing Philosophy
|
|
157
389
|
|
|
158
|
-
|
|
159
|
-
`
|
|
160
|
-
|
|
390
|
+
Tests run in Node and use a small in-repo fake DOM for behavioral DOM checks.
|
|
391
|
+
See [`packages/elements/test/README.md`](https://github.com/pfernandez/elements/blob/main/packages/elements/test/README.md).
|
|
392
|
+
|
|
393
|
+
## License
|
|
161
394
|
|
|
162
|
-
|
|
395
|
+
MIT License
|
|
396
|
+
Copyright (c) 2026 Paul Fernandez
|
|
163
397
|
|
|
164
|
-
|
|
165
|
-
- For a starter app template, use `@pfern/create-elements`:
|
|
166
|
-
- https://github.com/pfernandez/create-elements
|
|
167
|
-
- `npx @pfern/create-elements my-app`
|
|
168
|
-
- More examples live in `examples/`.
|
|
398
|
+
[dom]: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
|