bitwrench 2.0.31 → 2.1.0
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 +210 -125
- package/dist/bitwrench-bccl.cjs.js +349 -188
- package/dist/bitwrench-bccl.cjs.min.js +2 -39
- package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
- package/dist/bitwrench-bccl.esm.js +349 -188
- package/dist/bitwrench-bccl.esm.min.js +2 -39
- package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
- package/dist/bitwrench-bccl.umd.js +349 -188
- package/dist/bitwrench-bccl.umd.min.js +2 -39
- package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
- package/dist/bitwrench-code-edit.cjs.js +17 -6
- package/dist/bitwrench-code-edit.cjs.min.js +2 -20
- package/dist/bitwrench-code-edit.es5.js +8 -3
- package/dist/bitwrench-code-edit.es5.min.js +2 -19
- package/dist/bitwrench-code-edit.esm.js +17 -6
- package/dist/bitwrench-code-edit.esm.min.js +2 -19
- package/dist/bitwrench-code-edit.umd.js +17 -6
- package/dist/bitwrench-code-edit.umd.min.js +2 -19
- package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
- package/dist/bitwrench-debug.js +1 -1
- package/dist/bitwrench-debug.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +2492 -1628
- package/dist/bitwrench-lean.cjs.min.js +2 -80
- package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
- package/dist/bitwrench-lean.es5.js +2740 -1838
- package/dist/bitwrench-lean.es5.min.js +2 -49
- package/dist/bitwrench-lean.es5.min.js.gz +0 -0
- package/dist/bitwrench-lean.esm.js +2492 -1628
- package/dist/bitwrench-lean.esm.min.js +2 -80
- package/dist/bitwrench-lean.esm.min.js.gz +0 -0
- package/dist/bitwrench-lean.umd.js +2492 -1628
- package/dist/bitwrench-lean.umd.min.js +2 -80
- package/dist/bitwrench-lean.umd.min.js.gz +0 -0
- package/dist/bitwrench-util-color.cjs.js +251 -0
- package/dist/bitwrench-util-color.cjs.min.js +3 -0
- package/dist/bitwrench-util-color.es5.js +256 -0
- package/dist/bitwrench-util-color.es5.min.js +3 -0
- package/dist/bitwrench-util-color.esm.js +241 -0
- package/dist/bitwrench-util-color.esm.min.js +3 -0
- package/dist/bitwrench-util-color.umd.js +257 -0
- package/dist/bitwrench-util-color.umd.min.js +3 -0
- package/dist/bitwrench-util-css.cjs.js +2 -1
- package/dist/bitwrench-util-css.cjs.min.js +2 -21
- package/dist/bitwrench-util-css.es5.js +2 -1
- package/dist/bitwrench-util-css.es5.min.js +2 -20
- package/dist/bitwrench-util-css.esm.js +2 -1
- package/dist/bitwrench-util-css.esm.min.js +1 -19
- package/dist/bitwrench-util-css.umd.js +2 -1
- package/dist/bitwrench-util-css.umd.min.js +2 -20
- package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
- package/dist/bitwrench.cjs.js +2826 -1801
- package/dist/bitwrench.cjs.min.js +2 -99
- package/dist/bitwrench.cjs.min.js.gz +0 -0
- package/dist/bitwrench.css +403 -479
- package/dist/bitwrench.d.ts +70 -73
- package/dist/bitwrench.es5.js +3106 -2020
- package/dist/bitwrench.es5.min.js +2 -67
- package/dist/bitwrench.es5.min.js.gz +0 -0
- package/dist/bitwrench.esm.js +2826 -1801
- package/dist/bitwrench.esm.min.js +2 -99
- package/dist/bitwrench.esm.min.js.gz +0 -0
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +2826 -1801
- package/dist/bitwrench.umd.min.js +2 -99
- package/dist/bitwrench.umd.min.js.gz +0 -0
- package/dist/builds.json +222 -134
- package/dist/bwserve.cjs.js +308 -298
- package/dist/bwserve.d.ts +157 -0
- package/dist/bwserve.esm.js +309 -299
- package/dist/sri.json +54 -46
- package/docs/README.md +6 -3
- package/docs/app-patterns.md +7 -6
- package/docs/bitwrench-for-wasm.md +53 -54
- package/docs/bitwrench-mcp.md +2 -2
- package/docs/bitwrench-northstar-principles.md +406 -0
- package/docs/bitwrench-taco-schema-discussion.md +2 -2
- package/docs/bitwrench_api.md +191 -106
- package/docs/bitwrench_typescript_usage.md +5 -5
- package/docs/bw-attach.md +29 -75
- package/docs/bwserve.md +200 -168
- package/docs/cli.md +36 -12
- package/docs/component-cheatsheet.md +2 -2
- package/docs/component-library.md +4 -4
- package/docs/component-lifecycle.md +234 -0
- package/docs/drift-lint.md +268 -0
- package/docs/framework-translation-table.md +4 -4
- package/docs/llm-bitwrench-guide.md +60 -50
- package/docs/routing.md +11 -13
- package/docs/state-management.md +110 -109
- package/docs/taco-format.md +13 -14
- package/docs/theming.md +13 -3
- package/docs/thinking-in-bitwrench.md +858 -983
- package/docs/tutorial-bwserve.md +37 -36
- package/docs/tutorial-embedded.md +10 -21
- package/docs/tutorial-website.md +2 -2
- package/package.json +38 -7
- package/readme.html +262 -160
- package/src/bitwrench-bccl-entry.js +2 -2
- package/src/bitwrench-bccl.js +346 -185
- package/src/bitwrench-code-edit.js +16 -5
- package/src/bitwrench-color-utils.js +117 -181
- package/src/bitwrench-file-ops.js +2 -2
- package/src/bitwrench-lean.js +4 -3
- package/src/bitwrench-router.js +5 -2
- package/src/bitwrench-styles.js +420 -504
- package/src/bitwrench-util-color.js +240 -0
- package/src/bitwrench-util-css.js +1 -0
- package/src/bitwrench-utils.js +4 -0
- package/src/bitwrench.d.ts +70 -73
- package/src/bitwrench.h +5 -0
- package/src/bitwrench.js +1939 -933
- package/src/bwserve/attach.js +0 -1
- package/src/bwserve/bwclient.js +172 -32
- package/src/bwserve/bwshell.js +0 -4
- package/src/bwserve/client.js +59 -220
- package/src/bwserve/index.js +78 -42
- package/src/bwserve.d.ts +157 -0
- package/src/bwserve.h +5 -0
- package/src/cli/attach.js +12 -75
- package/src/cli/convert.js +2 -2
- package/src/cli/serve.js +37 -35
- package/src/generate-css.js +1 -1
- package/src/mcp/knowledge.js +4 -4
- package/src/mcp/live.js +21 -13
- package/src/mcp/tools.js +0 -1
- package/src/version.js +3 -7
|
@@ -2,86 +2,92 @@
|
|
|
2
2
|
|
|
3
3
|
## Table of Contents
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
11. [What Bitwrench Doesn't Do](#11-what-bitwrench-doesnt-do)
|
|
5
|
+
1. [The Problem and the Idea](#1-the-problem-and-the-idea)
|
|
6
|
+
2. [TACO to HTML String](#2-taco-to-html-string)
|
|
7
|
+
3. [TACO to Live DOM](#3-taco-to-live-dom)
|
|
8
|
+
4. [JavaScript Makes TACOs Composable](#4-javascript-makes-tacos-composable)
|
|
9
|
+
5. [Styling Grows Naturally](#5-styling-grows-naturally)
|
|
10
|
+
6. [Events and Behavior](#6-events-and-behavior)
|
|
11
|
+
7. [Lifecycle: `o:` Options](#7-lifecycle-o-options)
|
|
12
|
+
8. [BCCL: Ready-Made Components](#8-bccl-ready-made-components)
|
|
13
|
+
9. [Routing, Utilities, Advanced](#9-routing-utilities-advanced)
|
|
14
|
+
10. [What Bitwrench Does Instead](#10-what-bitwrench-does-instead)
|
|
15
|
+
11. [Server-Driven UI and CLI](#11-server-driven-ui-and-cli)
|
|
17
16
|
12. [Quick Reference](#12-quick-reference)
|
|
18
17
|
- [Framework Translation Table](#appendix-framework-translation-table)
|
|
18
|
+
- [Postscript: Validation](#postscript-validation)
|
|
19
19
|
|
|
20
|
-
**Related docs:** [Component Cheat Sheet](component-cheatsheet.md) | [State Management](state-management.md) | [Component Library](component-library.md) | [LLM Guide](llm-bitwrench-guide.md)
|
|
20
|
+
**Related docs:** [North Star Principles](bitwrench-northstar-principles.md) | [Component Cheat Sheet](component-cheatsheet.md) | [State Management](state-management.md) | [Component Library](component-library.md) | [LLM Guide](llm-bitwrench-guide.md)
|
|
21
21
|
|
|
22
|
-
---
|
|
23
22
|
|
|
24
|
-
##
|
|
23
|
+
## 1. The Problem and the Idea
|
|
25
24
|
|
|
26
|
-
Building web UIs with raw HTML, CSS, and JavaScript works
|
|
25
|
+
Building web UIs with raw HTML, CSS, and JavaScript works -- but it is painful. HTML is verbose. Styling the same element across a page means copying CSS rules or managing class hierarchies. Adding interactivity means wiring up event listeners, tracking state in variables, and manually updating the DOM when things change. The more complex the UI, the more copy-paste, the more boilerplate, the more places things can go wrong.
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
Frameworks emerged to manage this -- React, Vue, Svelte for rendering; Sass, Tailwind for styling; Redux, Zustand for state -- each adding a new syntax, a new tool, a new layer. Each solves a real problem. But each also adds an abstraction to learn, configure, and maintain.
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
- **Styling**: Sass and Less added variables and mixins. Tailwind invented utility classes. CSS-in-JS libraries generate styles at runtime. CSS Modules scope class names to avoid conflicts.
|
|
32
|
-
- **State management**: React hooks, Vue reactivity, Svelte stores, Redux, Zustand — track application state and automatically re-render when it changes.
|
|
33
|
-
- **Build tooling**: Babel, webpack, Vite, esbuild — transpile, bundle, tree-shake, hot-reload. Required infrastructure to connect the pieces.
|
|
29
|
+
Bitwrench takes a different approach. Instead of adding layers, it leans into what the browser already provides -- the DOM for structure, CSS for styling, JavaScript for behavior -- and uses the JavaScript language itself to manage all three concerns.
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
The mechanism is a plain JavaScript object called a **TACO**: `{t, a, c, o}` -- Tag, Attributes, Content, Options. A `{taco}` describes a UI element the same way HTML does, but because it is a JavaScript object, you get the full language at every point: variables, functions, loops, conditionals, composition. No special syntax. No compiler. No build step.
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
> **"If you know JavaScript, you already know bitwrench. Everything else is just learning the shape of the objects -- and a small set of conventions for mounting, lifecycle, and updates that the rest of this document covers."**
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
This document walks a single example forward -- from a static HTML string to a live, interactive, server-driven application -- one layer at a time.
|
|
40
36
|
|
|
41
|
-
> **"If you know JavaScript, you already know bitwrench. Everything else is just learning the shape of the objects."**
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
## 1. TACO: the Shape of a UI Element
|
|
38
|
+
## 2. TACO to HTML String
|
|
46
39
|
|
|
47
40
|
### From HTML to TACO
|
|
48
41
|
|
|
49
|
-
Every HTML element has a tag, attributes, and content. A
|
|
42
|
+
Every HTML element has a tag, attributes, and content. A `{taco}` object mirrors this directly:
|
|
50
43
|
|
|
51
44
|
```html
|
|
52
45
|
<!-- HTML -->
|
|
53
|
-
<div class="
|
|
46
|
+
<div class="greeting" id="hero">Hello world</div>
|
|
54
47
|
```
|
|
55
48
|
|
|
56
49
|
```js
|
|
57
|
-
// TACO
|
|
58
|
-
{ t: 'div', a: { class: '
|
|
50
|
+
// TACO -- the same element as a JavaScript object
|
|
51
|
+
{ t: 'div', a: { class: 'greeting', id: 'hero' }, c: 'Hello world' }
|
|
59
52
|
```
|
|
60
53
|
|
|
61
|
-
The mapping is direct: `t` is the tag name, `a` is an object of HTML attributes, `c` is the content.
|
|
54
|
+
The mapping is direct: `t` is the tag name, `a` is an object of HTML attributes, `c` is the content. The function `bw.html()` converts this object to an HTML string:
|
|
62
55
|
|
|
63
56
|
```js
|
|
64
|
-
var
|
|
57
|
+
var greeting = { t: 'div', a: { class: 'greeting', id: 'hero' }, c: 'Hello world' };
|
|
65
58
|
|
|
66
|
-
bw.html(
|
|
67
|
-
|
|
68
|
-
bw.DOM('#target', card); // mount directly into an existing element on the page
|
|
59
|
+
bw.html(greeting);
|
|
60
|
+
// => '<div class="greeting" id="hero">Hello world</div>'
|
|
69
61
|
```
|
|
70
62
|
|
|
71
|
-
|
|
63
|
+
That is the entire concept. Everything else builds on it.
|
|
64
|
+
|
|
65
|
+
> **A note on syntax:** Examples in this document use `var` and `function()` to show that bitwrench requires no transpiler -- it runs in any JS environment as-is. In your own code, use `const`, `let`, and arrow functions freely.
|
|
66
|
+
|
|
67
|
+
Content strings are HTML-escaped by default. `c: '<b>bold</b>'` renders as visible text, not markup. When you need actual HTML, use `bw.raw()` (covered in Section 9).
|
|
68
|
+
|
|
69
|
+
We will use a **contact card** as a running example throughout this document, building it up one capability at a time. Here is the card as a `{taco}`:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
var card = {
|
|
73
|
+
t: 'div', a: { class: 'card' }, c: [
|
|
74
|
+
{ t: 'h3', c: 'Alice' },
|
|
75
|
+
{ t: 'p', c: 'alice@example.com' }
|
|
76
|
+
]
|
|
77
|
+
};
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
bw.html(card);
|
|
80
|
+
// => '<div class="card"><h3>Alice</h3><p>alice@example.com</p></div>'
|
|
81
|
+
```
|
|
76
82
|
|
|
77
|
-
|
|
83
|
+
A tag, attributes, nested content -- the same shape as the greeting, just with children. We will mount this card to the DOM in Section 3, make it reusable in Section 4, style it in Section 5, and add interactivity in Section 6.
|
|
78
84
|
|
|
79
85
|
### Minimal cases
|
|
80
86
|
|
|
81
87
|
Every key is optional. These are all valid TACOs:
|
|
82
88
|
|
|
83
89
|
```js
|
|
84
|
-
{ t: 'br' } //
|
|
90
|
+
{ t: 'br' } // void element
|
|
85
91
|
{ t: 'h1', c: 'Hello' } // tag + text content
|
|
86
92
|
{ t: 'input', a: { type: 'email', required: true } } // tag + attributes, no content
|
|
87
93
|
{ t: 'div' } // empty div
|
|
@@ -89,27 +95,9 @@ Every key is optional. These are all valid TACOs:
|
|
|
89
95
|
|
|
90
96
|
If `t` is omitted, it defaults to `'div'`.
|
|
91
97
|
|
|
92
|
-
###
|
|
93
|
-
|
|
94
|
-
By default, bitwrench escapes all content — `<b>bold</b>` renders as the literal text `<b>bold</b>`, not bold text. This prevents XSS and is almost always what you want.
|
|
98
|
+
### Nesting -- TACOs inside TACOs
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```js
|
|
99
|
-
// Without bw.raw() — the <br> and <span> are escaped to visible text
|
|
100
|
-
{ t: 'h1', c: 'Coffee That<br>Tells a <span>Story</span>' }
|
|
101
|
-
// Renders: Coffee That<br>Tells a <span>Story</span>
|
|
102
|
-
|
|
103
|
-
// With bw.raw() — the HTML is rendered as-is
|
|
104
|
-
{ t: 'h1', c: bw.raw('Coffee That<br>Tells a <span class="accent">Story</span>') }
|
|
105
|
-
// Renders: Coffee That (line break) Tells a Story (styled)
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
`bw.raw()` returns a sentinel object `{ __bw_raw: true, v: str }` — it doesn't modify the string, it marks it. Bitwrench checks for this marker during rendering and skips escaping. Never use `bw.raw()` on user-provided input.
|
|
109
|
-
|
|
110
|
-
### Nesting — TACOs inside TACOs
|
|
111
|
-
|
|
112
|
-
Content (`c:`) can be a string, another TACO, or an array of both. This nesting is recursive — TACOs go as deep as your UI requires:
|
|
100
|
+
Content (`c:`) can be a string, another `{taco}`, or an array of both:
|
|
113
101
|
|
|
114
102
|
```js
|
|
115
103
|
// A single child
|
|
@@ -120,9 +108,12 @@ Content (`c:`) can be a string, another TACO, or an array of both. This nesting
|
|
|
120
108
|
{ t: 'h2', c: 'Title' },
|
|
121
109
|
{ t: 'p', c: 'Body text' }
|
|
122
110
|
]}
|
|
111
|
+
```
|
|
123
112
|
|
|
124
|
-
|
|
125
|
-
|
|
113
|
+
Let's build something more substantial -- a page with a nav and a card:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
var page = { t: 'div', a: { class: 'page' }, c: [
|
|
126
117
|
{ t: 'nav', c: [
|
|
127
118
|
{ t: 'a', a: { href: '/' }, c: 'Home' },
|
|
128
119
|
{ t: 'a', a: { href: '/about' }, c: 'About' }
|
|
@@ -130,16 +121,17 @@ Content (`c:`) can be a string, another TACO, or an array of both. This nesting
|
|
|
130
121
|
{ t: 'section', a: { class: 'content' }, c: [
|
|
131
122
|
{ t: 'div', a: { class: 'card' }, c: [
|
|
132
123
|
{ t: 'h3', c: 'Welcome' },
|
|
133
|
-
{ t: 'p', c: 'This is
|
|
124
|
+
{ t: 'p', c: 'This is a card inside a section inside a page.' },
|
|
134
125
|
{ t: 'button', c: 'Click me' }
|
|
135
126
|
]}
|
|
136
127
|
]}
|
|
137
|
-
]}
|
|
138
|
-
```
|
|
128
|
+
]};
|
|
139
129
|
|
|
140
|
-
|
|
130
|
+
bw.html(page);
|
|
131
|
+
// => full HTML string with all nested tags
|
|
132
|
+
```
|
|
141
133
|
|
|
142
|
-
###
|
|
134
|
+
### Conditionals -- nulls are skipped
|
|
143
135
|
|
|
144
136
|
`null`, `undefined`, and `false` in content arrays are silently skipped. This makes conditional rendering natural:
|
|
145
137
|
|
|
@@ -147,911 +139,880 @@ The HTML equivalent would be around a dozen lines of nested tags with closing ta
|
|
|
147
139
|
var showHeader = true;
|
|
148
140
|
var isAdmin = false;
|
|
149
141
|
|
|
150
|
-
{ t: 'div', c: [
|
|
142
|
+
var dashboard = { t: 'div', c: [
|
|
151
143
|
showHeader ? { t: 'h1', c: 'Dashboard' } : null,
|
|
152
144
|
{ t: 'p', c: 'Always visible' },
|
|
153
145
|
isAdmin ? { t: 'a', c: 'Admin Panel' } : null
|
|
154
|
-
]}
|
|
155
|
-
// Renders: <h1>Dashboard</h1><p>Always visible</p>
|
|
156
|
-
// The admin link is skipped entirely.
|
|
157
|
-
```
|
|
146
|
+
]};
|
|
158
147
|
|
|
159
|
-
|
|
148
|
+
bw.html(dashboard);
|
|
149
|
+
// => '<div><h1>Dashboard</h1><p>Always visible</p></div>'
|
|
150
|
+
// The admin link is omitted entirely.
|
|
151
|
+
```
|
|
160
152
|
|
|
161
|
-
|
|
153
|
+
### Arrays and .map() -- lists from data
|
|
162
154
|
|
|
163
155
|
```js
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
}
|
|
156
|
+
var items = ['Apples', 'Bananas', 'Cherries'];
|
|
157
|
+
|
|
158
|
+
var list = { t: 'ul', c: items.map(function(item) {
|
|
159
|
+
return { t: 'li', c: item };
|
|
160
|
+
})};
|
|
161
|
+
|
|
162
|
+
bw.html(list);
|
|
163
|
+
// => '<ul><li>Apples</li><li>Bananas</li><li>Cherries</li></ul>'
|
|
174
164
|
```
|
|
175
165
|
|
|
176
|
-
|
|
166
|
+
No `v-for`, no `{#each}`, no special syntax. Just `.map()`.
|
|
177
167
|
|
|
178
|
-
|
|
168
|
+
### The TACO shape: `{t, a, c, o}`
|
|
179
169
|
|
|
180
|
-
|
|
170
|
+
You have seen `t`, `a`, and `c`. There is a fourth key -- `o` (options) -- for lifecycle hooks, component state, and behavior. We will cover it in Section 7. For now, know the full shape:
|
|
181
171
|
|
|
182
|
-
|
|
172
|
+
| Key | Purpose | Goes to HTML? |
|
|
173
|
+
|-----|---------|---------------|
|
|
174
|
+
| `t` | Tag name | Yes |
|
|
175
|
+
| `a` | HTML attributes | Yes |
|
|
176
|
+
| `c` | Content (text, TACOs, arrays) | Yes |
|
|
177
|
+
| `o` | Options (state, lifecycle, handles) | No -- bitwrench only |
|
|
183
178
|
|
|
184
|
-
|
|
179
|
+
If you prefer positional arguments: `bw.h('div', {class: 'card'}, 'Hello')` returns the same `{taco}` object. Use whichever is clearer in context.
|
|
185
180
|
|
|
186
|
-
### Start simple — inline styles
|
|
187
181
|
|
|
188
|
-
|
|
182
|
+
## 3. TACO to Live DOM
|
|
189
183
|
|
|
190
|
-
|
|
191
|
-
{ t: 'div',
|
|
192
|
-
a: { style: 'padding:1.5rem; background:#f5f5f5; border-radius:12px' },
|
|
193
|
-
c: 'A styled box'
|
|
194
|
-
}
|
|
195
|
-
```
|
|
184
|
+
We can generate HTML strings, but a live page needs DOM elements.
|
|
196
185
|
|
|
197
|
-
|
|
186
|
+
### bw.mount() -- the primary path
|
|
198
187
|
|
|
199
|
-
|
|
200
|
-
var boxStyle = 'padding:1.5rem; background:#f5f5f5; border-radius:12px';
|
|
188
|
+
`bw.mount()` takes a CSS selector and a `{taco}`. It unmounts and removes the target's existing children, creates the new `{taco}` tree, inserts it as the target's content, and returns the newly created root element:
|
|
201
189
|
|
|
202
|
-
|
|
203
|
-
|
|
190
|
+
```html
|
|
191
|
+
<div id="app"></div>
|
|
192
|
+
<script>
|
|
193
|
+
bw.mount('#app', { t: 'h1', c: 'Hello from bitwrench' });
|
|
194
|
+
// The #app div now contains: <h1>Hello from bitwrench</h1>
|
|
195
|
+
</script>
|
|
204
196
|
```
|
|
205
197
|
|
|
206
|
-
|
|
198
|
+
`bw.mount()` returns the root element it created. This is the most common way to render UI.
|
|
207
199
|
|
|
208
|
-
|
|
200
|
+
Let's mount the page we built in Section 2:
|
|
209
201
|
|
|
210
202
|
```js
|
|
211
|
-
var
|
|
212
|
-
|
|
213
|
-
|
|
203
|
+
var page = { t: 'div', a: { class: 'page' }, c: [
|
|
204
|
+
{ t: 'nav', c: [
|
|
205
|
+
{ t: 'a', a: { href: '/' }, c: 'Home' },
|
|
206
|
+
{ t: 'a', a: { href: '/about' }, c: 'About' }
|
|
207
|
+
]},
|
|
208
|
+
{ t: 'section', c: [
|
|
209
|
+
{ t: 'h3', c: 'Welcome' },
|
|
210
|
+
{ t: 'p', c: 'Live in the browser.' }
|
|
211
|
+
]}
|
|
212
|
+
]};
|
|
214
213
|
|
|
215
|
-
|
|
216
|
-
{ t: 'div', a: { style: headerStyle }, c: 'Card Title' },
|
|
217
|
-
{ t: 'div', a: { style: bodyStyle }, c: 'Card content goes here.' }
|
|
218
|
-
]}
|
|
214
|
+
bw.mount('#app', page);
|
|
219
215
|
```
|
|
220
216
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
### Base style + overrides
|
|
224
|
-
|
|
225
|
-
For simple variations, string concatenation works:
|
|
217
|
+
Continuing our contact card: mounting it is a one-liner.
|
|
226
218
|
|
|
227
219
|
```js
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
{ t: '
|
|
231
|
-
{ t: '
|
|
232
|
-
|
|
220
|
+
bw.mount('#app', {
|
|
221
|
+
t: 'div', a: { class: 'card' }, c: [
|
|
222
|
+
{ t: 'h3', c: 'Alice' },
|
|
223
|
+
{ t: 'p', c: 'alice@example.com' }
|
|
224
|
+
]
|
|
225
|
+
});
|
|
226
|
+
// The #app div now contains a live card element in the DOM.
|
|
233
227
|
```
|
|
234
228
|
|
|
235
|
-
|
|
229
|
+
### bw.create() -- detached elements
|
|
236
230
|
|
|
237
|
-
|
|
238
|
-
var baseObj = { borderRadius: '12px', padding: '1rem', border: '1px solid #ddd' };
|
|
231
|
+
Sometimes you need a DOM element before inserting it. `bw.create()` returns a detached DOM node:
|
|
239
232
|
|
|
240
|
-
|
|
241
|
-
var
|
|
242
|
-
|
|
233
|
+
```js
|
|
234
|
+
var el = bw.create({ t: 'div', c: 'Not in the page yet' });
|
|
235
|
+
// el is an HTMLDivElement, but it is not attached to the document.
|
|
236
|
+
// You can inspect it, modify it, then insert it manually.
|
|
237
|
+
// In practice, prefer bw.mount('#target', taco) instead of manual insertion.
|
|
243
238
|
```
|
|
244
239
|
|
|
245
|
-
|
|
240
|
+
Use `bw.create()` when you need to manipulate the element before it goes into the page. For most cases, `bw.mount()` is simpler and handles lifecycle automatically.
|
|
246
241
|
|
|
247
|
-
###
|
|
242
|
+
### bw.DOM() and bw.el()
|
|
248
243
|
|
|
249
|
-
|
|
244
|
+
`bw.DOM()` is an exact alias for `bw.mount()`. Use whichever reads better in context.
|
|
245
|
+
|
|
246
|
+
`bw.el()` resolves an element by selector and can optionally update or transform it:
|
|
250
247
|
|
|
251
248
|
```js
|
|
252
|
-
bw.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
'.card:hover': {
|
|
259
|
-
boxShadow: '0 4px 12px rgba(0,0,0,.1)'
|
|
260
|
-
},
|
|
261
|
-
'@media (max-width: 768px)': {
|
|
262
|
-
'.card': { padding: '0.75rem' }
|
|
263
|
-
}
|
|
264
|
-
}));
|
|
249
|
+
bw.el('#title'); // find element
|
|
250
|
+
bw.el('#title', 'New text'); // set text content
|
|
251
|
+
bw.el('#app', { t: 'h1', c: 'Hi' }); // mount a {taco}
|
|
252
|
+
bw.el('.card', function(el) { // apply a function
|
|
253
|
+
el.style.opacity = '0.5';
|
|
254
|
+
});
|
|
265
255
|
```
|
|
266
256
|
|
|
267
|
-
|
|
257
|
+
### Three output modes, one input
|
|
268
258
|
|
|
269
|
-
|
|
259
|
+
| Function | Returns | Use when |
|
|
260
|
+
|----------|---------|----------|
|
|
261
|
+
| `bw.html(taco)` | HTML string | SSR, Node.js scripts, email templates |
|
|
262
|
+
| `bw.create(taco)` | Detached DOM element | Need to manipulate before inserting |
|
|
263
|
+
| `bw.mount(sel, taco)` | Mounted DOM element | Most common -- render into the page |
|
|
270
264
|
|
|
271
|
-
|
|
272
|
-
var brand = '#8B4513';
|
|
273
|
-
var radius = '12px';
|
|
274
|
-
var shadow = '0 4px 12px rgba(0,0,0,.08)';
|
|
265
|
+
The `{taco}` is data. The rendering step is separate. You decide when and how it becomes real.
|
|
275
266
|
|
|
276
|
-
bw.injectCSS(bw.css({
|
|
277
|
-
'.card': { borderRadius: radius, boxShadow: shadow, borderColor: brand },
|
|
278
|
-
'.badge': { borderRadius: radius, background: brand, color: '#fff' },
|
|
279
|
-
'.btn': { borderRadius: radius, background: brand }
|
|
280
|
-
}));
|
|
281
|
-
```
|
|
282
267
|
|
|
283
|
-
|
|
268
|
+
## 4. JavaScript Makes TACOs Composable
|
|
284
269
|
|
|
285
|
-
|
|
270
|
+
Mounting a single `{taco}` is useful, but real pages have many elements. Because `{taco}` objects are plain JavaScript, composition is natural -- every field is a JavaScript expression. This is the most important thing to understand about bitwrench.
|
|
271
|
+
|
|
272
|
+
### Functions are your components
|
|
273
|
+
|
|
274
|
+
A function that returns a `{taco}` is a component. No class, no decorator, no registration:
|
|
286
275
|
|
|
287
276
|
```js
|
|
288
|
-
function
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
background: shades.light,
|
|
292
|
-
border: '1px solid ' + shades.border,
|
|
293
|
-
color: shades.darkText,
|
|
294
|
-
borderRadius: '12px'
|
|
295
|
-
};
|
|
277
|
+
function greeting(name) {
|
|
278
|
+
return { t: 'h2', c: 'Hello, ' + name + '!' };
|
|
296
279
|
}
|
|
297
280
|
|
|
298
|
-
bw.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}));
|
|
281
|
+
bw.mount('#app', { t: 'div', c: [
|
|
282
|
+
greeting('Alice'),
|
|
283
|
+
greeting('Bob')
|
|
284
|
+
]});
|
|
303
285
|
```
|
|
304
286
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
### Theme palettes — complete design systems from two colors
|
|
287
|
+
Continuing our card: make it a function, and it works for any contact.
|
|
308
288
|
|
|
309
289
|
```js
|
|
310
|
-
|
|
311
|
-
|
|
290
|
+
function contactCard(name, email) {
|
|
291
|
+
return {
|
|
292
|
+
t: 'div', a: { class: 'card' }, c: [
|
|
293
|
+
{ t: 'h3', c: name },
|
|
294
|
+
{ t: 'p', c: email }
|
|
295
|
+
]
|
|
296
|
+
};
|
|
297
|
+
}
|
|
312
298
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
color: theme.palette.primary.textOn,
|
|
318
|
-
borderBottom: '3px solid ' + theme.palette.secondary.base
|
|
319
|
-
}
|
|
320
|
-
}));
|
|
299
|
+
bw.mount('#app', { t: 'div', c: [
|
|
300
|
+
contactCard('Alice', 'alice@example.com'),
|
|
301
|
+
contactCard('Bob', 'bob@example.com')
|
|
302
|
+
]});
|
|
321
303
|
```
|
|
322
304
|
|
|
323
|
-
|
|
305
|
+
Same pattern as the `greeting()` function, but now it produces structured content. This is the entire component model.
|
|
324
306
|
|
|
325
|
-
###
|
|
307
|
+
### Component factory pattern
|
|
326
308
|
|
|
327
|
-
|
|
309
|
+
Build reusable components as factory functions with options:
|
|
328
310
|
|
|
329
311
|
```js
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
'@media (prefers-reduced-motion: reduce)': {
|
|
341
|
-
'.toast': { animation: 'none' }
|
|
342
|
-
}
|
|
343
|
-
}));
|
|
344
|
-
```
|
|
312
|
+
function colorCard(title, body, color) {
|
|
313
|
+
return {
|
|
314
|
+
t: 'div',
|
|
315
|
+
a: { class: 'card', style: 'border-left:4px solid ' + color },
|
|
316
|
+
c: [
|
|
317
|
+
{ t: 'h3', c: title },
|
|
318
|
+
{ t: 'p', c: body }
|
|
319
|
+
]
|
|
320
|
+
};
|
|
321
|
+
}
|
|
345
322
|
|
|
346
|
-
|
|
323
|
+
bw.mount('#app', { t: 'div', c: [
|
|
324
|
+
colorCard('Warning', 'Disk space low', '#e67e22'),
|
|
325
|
+
colorCard('Success', 'Backup complete', '#27ae60'),
|
|
326
|
+
colorCard('Info', '3 updates available', '#3498db')
|
|
327
|
+
]});
|
|
328
|
+
```
|
|
347
329
|
|
|
348
|
-
###
|
|
330
|
+
### .map() for lists
|
|
349
331
|
|
|
350
|
-
|
|
332
|
+
Render arrays of data using `.map()`:
|
|
351
333
|
|
|
352
334
|
```js
|
|
353
|
-
|
|
354
|
-
{
|
|
355
|
-
{
|
|
356
|
-
{
|
|
357
|
-
]
|
|
358
|
-
|
|
359
|
-
// Store base styles in variables, merge with custom properties
|
|
360
|
-
var cardHeader = bw.s({ display: 'flex' }, { justifyContent: 'space-between' }, { padding: '1rem' }, {
|
|
361
|
-
borderBottom: '1px solid #eee',
|
|
362
|
-
background: theme.palette.primary.light
|
|
363
|
-
});
|
|
335
|
+
var users = [
|
|
336
|
+
{ name: 'Alice', role: 'admin' },
|
|
337
|
+
{ name: 'Bob', role: 'user' },
|
|
338
|
+
{ name: 'Carol', role: 'user' }
|
|
339
|
+
];
|
|
364
340
|
|
|
365
|
-
|
|
366
|
-
{ t: '
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
341
|
+
bw.mount('#app', { t: 'table', c: [
|
|
342
|
+
{ t: 'thead', c: { t: 'tr', c: [
|
|
343
|
+
{ t: 'th', c: 'Name' }, { t: 'th', c: 'Role' }
|
|
344
|
+
]}},
|
|
345
|
+
{ t: 'tbody', c: users.map(function(u) {
|
|
346
|
+
return { t: 'tr', c: [
|
|
347
|
+
{ t: 'td', c: u.name },
|
|
348
|
+
{ t: 'td', c: u.role }
|
|
349
|
+
]};
|
|
350
|
+
})}
|
|
351
|
+
]});
|
|
370
352
|
```
|
|
371
353
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
### Responsive breakpoints — bw.responsive()
|
|
375
|
-
|
|
376
|
-
`bw.responsive()` generates `@media` rules from a JavaScript object, using bitwrench's standard breakpoints:
|
|
354
|
+
### Conditionals -- three ways
|
|
377
355
|
|
|
378
356
|
```js
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
bw.responsive('.hero h1', {
|
|
382
|
-
md: { fontSize: '2.5rem', padding: '2rem' },
|
|
383
|
-
xl: { fontSize: '3.5rem' }
|
|
384
|
-
})
|
|
385
|
-
].join('\n'));
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
The breakpoints (`sm`, `md`, `lg`, `xl`) match bitwrench's grid system. `bw.responsive()` returns a CSS string — join it with `bw.css()` output and pass the combined string to `bw.injectCSS()`.
|
|
357
|
+
// Ternary (inline)
|
|
358
|
+
{ t: 'div', c: loggedIn ? 'Welcome back' : 'Please sign in' }
|
|
389
359
|
|
|
390
|
-
|
|
360
|
+
// null filtering (in arrays)
|
|
361
|
+
{ t: 'nav', c: [
|
|
362
|
+
{ t: 'a', c: 'Home' },
|
|
363
|
+
isAdmin ? { t: 'a', c: 'Admin' } : null,
|
|
364
|
+
{ t: 'a', c: 'About' }
|
|
365
|
+
]}
|
|
391
366
|
|
|
392
|
-
|
|
367
|
+
// IIFE for complex logic
|
|
368
|
+
{ t: 'div', c: (function() {
|
|
369
|
+
if (status === 'loading') return { t: 'span', c: 'Loading...' };
|
|
370
|
+
if (status === 'error') return { t: 'span', a: { class: 'error' }, c: errorMsg };
|
|
371
|
+
return results.map(function(r) { return { t: 'li', c: r.name }; });
|
|
372
|
+
})()
|
|
373
|
+
}
|
|
374
|
+
```
|
|
393
375
|
|
|
394
|
-
###
|
|
376
|
+
### Composing larger pages
|
|
395
377
|
|
|
396
|
-
|
|
378
|
+
Functions compose into full page layouts:
|
|
397
379
|
|
|
398
380
|
```js
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
381
|
+
function makeHeader(title) {
|
|
382
|
+
return { t: 'header', c: { t: 'h1', c: title } };
|
|
383
|
+
}
|
|
402
384
|
|
|
403
|
-
|
|
404
|
-
{ t: '
|
|
385
|
+
function makeFooter() {
|
|
386
|
+
return { t: 'footer', c: '(c) 2026' };
|
|
387
|
+
}
|
|
405
388
|
|
|
406
|
-
|
|
407
|
-
|
|
389
|
+
bw.mount('#app', { t: 'div', c: [
|
|
390
|
+
makeHeader('My App'),
|
|
391
|
+
{ t: 'main', c: users.map(function(u) {
|
|
392
|
+
return colorCard(u.name, u.role, u.role === 'admin' ? '#e67e22' : '#3498db');
|
|
393
|
+
})},
|
|
394
|
+
makeFooter()
|
|
395
|
+
]});
|
|
408
396
|
```
|
|
409
397
|
|
|
410
|
-
|
|
398
|
+
No template language needed. JavaScript already has functions (components), arrays (children), and `.map()` (iteration). The TACO format gives these a shape that maps to the DOM.
|
|
411
399
|
|
|
412
|
-
This is entirely optional — `bw.s()` and `bw.css()` handle everything without it. But for rapid prototyping, shorter token strings mean less typing and (for LLMs) fewer tokens.
|
|
413
400
|
|
|
414
|
-
|
|
401
|
+
## 5. Styling Grows Naturally
|
|
415
402
|
|
|
416
|
-
|
|
403
|
+
We can compose structure, but unstyled HTML is not a UI. Because styles are just strings and objects in JavaScript, bitwrench handles CSS the same way it handles markup -- with plain JS.
|
|
417
404
|
|
|
418
|
-
|
|
405
|
+
### Start simple -- class and style attributes
|
|
419
406
|
|
|
420
|
-
|
|
407
|
+
The `style` and `class` attributes in a `{taco}` work exactly like their HTML counterparts:
|
|
421
408
|
|
|
422
|
-
|
|
409
|
+
```js
|
|
410
|
+
bw.mount('#app', { t: 'div',
|
|
411
|
+
a: { class: 'card', style: 'padding:1.5rem; background:#f5f5f5; border-radius:12px' },
|
|
412
|
+
c: 'A styled card'
|
|
413
|
+
});
|
|
414
|
+
```
|
|
423
415
|
|
|
424
|
-
|
|
416
|
+
Put the style in a variable and reuse it:
|
|
425
417
|
|
|
426
418
|
```js
|
|
427
|
-
var
|
|
428
|
-
var isAdmin = true;
|
|
429
|
-
var items = ['Apples', 'Bananas', 'Cherries'];
|
|
419
|
+
var cardStyle = 'padding:1.5rem; background:#f5f5f5; border-radius:12px';
|
|
430
420
|
|
|
431
|
-
{
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
style: 'color:' + (isAdmin ? 'red' : 'black') // computed style
|
|
436
|
-
},
|
|
437
|
-
c: [
|
|
438
|
-
title, // variable as content
|
|
439
|
-
...items.map(function(i) { return { t: 'li', c: i }; }) // .map() → children
|
|
440
|
-
]
|
|
441
|
-
}
|
|
421
|
+
bw.mount('#app', { t: 'div', c: [
|
|
422
|
+
{ t: 'div', a: { style: cardStyle }, c: 'Card one' },
|
|
423
|
+
{ t: 'div', a: { style: cardStyle }, c: 'Card two' }
|
|
424
|
+
]});
|
|
442
425
|
```
|
|
443
426
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
### Functions as values — two timing modes
|
|
427
|
+
Change `cardStyle` once, and the next time your render function runs, both cards reflect the update. No preprocessor needed.
|
|
447
428
|
|
|
448
|
-
|
|
429
|
+
### bw.s() -- merge style objects
|
|
449
430
|
|
|
450
|
-
|
|
431
|
+
When inline styles get complex, string concatenation becomes fragile. `bw.s()` merges style objects into a style string:
|
|
451
432
|
|
|
452
433
|
```js
|
|
453
|
-
|
|
454
|
-
{
|
|
455
|
-
var data = getExpensiveData();
|
|
456
|
-
return data.map(function(d) { return { t: 'p', c: d.summary }; });
|
|
457
|
-
})()
|
|
458
|
-
}
|
|
434
|
+
var flex = { display: 'flex', alignItems: 'center', gap: '1rem' };
|
|
435
|
+
var padded = { padding: '1rem' };
|
|
459
436
|
|
|
460
|
-
|
|
461
|
-
{ t: '
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return 'padding:' + (w < 768 ? '8px' : '24px');
|
|
465
|
-
})()
|
|
466
|
-
}, c: 'Responsive without @media'
|
|
467
|
-
}
|
|
437
|
+
bw.mount('#app', { t: 'div', a: { style: bw.s(flex, padded) }, c: [
|
|
438
|
+
{ t: 'img', a: { src: 'avatar.png', style: bw.s({ borderRadius: '50%', width: '40px' }) } },
|
|
439
|
+
{ t: 'span', c: 'Alice' }
|
|
440
|
+
]});
|
|
468
441
|
```
|
|
469
442
|
|
|
470
|
-
|
|
443
|
+
`bw.s()` skips `null` and `undefined` arguments, so conditional composition works cleanly:
|
|
471
444
|
|
|
472
445
|
```js
|
|
473
|
-
// This function runs when bw.createDOM() or bw.DOM() encounters it
|
|
474
446
|
{ t: 'div', a: {
|
|
475
|
-
style:
|
|
476
|
-
|
|
477
|
-
}
|
|
447
|
+
style: bw.s(
|
|
448
|
+
{ padding: '1rem' },
|
|
449
|
+
isActive ? { fontWeight: '700' } : null,
|
|
450
|
+
{ color: accent }
|
|
451
|
+
)
|
|
452
|
+
}, c: 'Status' }
|
|
478
453
|
```
|
|
479
454
|
|
|
480
|
-
|
|
455
|
+
### bw.css() and bw.injectCSS() -- generate stylesheets
|
|
481
456
|
|
|
482
|
-
|
|
483
|
-
- **Rendering time** produces behavior. The function executes at the moment the UI is rendered. It can access the current window size, user preferences, sensor readings, the current time — anything available in the browser at that moment.
|
|
457
|
+
When you need pseudo-classes, media queries, or reusable class names, generate CSS from JavaScript objects:
|
|
484
458
|
|
|
485
|
-
|
|
459
|
+
```js
|
|
460
|
+
bw.injectCSS(bw.css({
|
|
461
|
+
'.card': {
|
|
462
|
+
padding: '1.5rem',
|
|
463
|
+
borderRadius: '12px',
|
|
464
|
+
border: '1px solid #ddd'
|
|
465
|
+
},
|
|
466
|
+
'.card:hover': {
|
|
467
|
+
boxShadow: '0 4px 12px rgba(0,0,0,.1)'
|
|
468
|
+
},
|
|
469
|
+
'@media (max-width: 768px)': {
|
|
470
|
+
'.card': { padding: '0.75rem' }
|
|
471
|
+
}
|
|
472
|
+
}));
|
|
473
|
+
```
|
|
486
474
|
|
|
487
|
-
|
|
475
|
+
`bw.css()` converts a JavaScript object to a CSS string. CamelCase properties (`borderRadius`) auto-convert to kebab-case (`border-radius`). `bw.injectCSS()` inserts the result into the document.
|
|
488
476
|
|
|
489
|
-
|
|
477
|
+
### Use JavaScript variables as style tokens
|
|
490
478
|
|
|
491
479
|
```js
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
function makeFooter() {
|
|
496
|
-
return { t: 'footer', c: '(c) 2026' };
|
|
497
|
-
}
|
|
480
|
+
var brand = '#336699';
|
|
481
|
+
var radius = '12px';
|
|
498
482
|
|
|
499
|
-
{
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
483
|
+
bw.injectCSS(bw.css({
|
|
484
|
+
'.card': { borderRadius: radius, borderColor: brand },
|
|
485
|
+
'.badge': { borderRadius: radius, background: brand, color: '#fff' },
|
|
486
|
+
'.btn': { borderRadius: radius, background: brand }
|
|
487
|
+
}));
|
|
505
488
|
```
|
|
506
489
|
|
|
507
|
-
|
|
490
|
+
Change `brand` once and re-run `bw.injectCSS(bw.css(...))` -- every rule that references it updates. No build step needed to generate the CSS; just call the functions again.
|
|
491
|
+
|
|
492
|
+
Continuing our card: let's style the contact card with `bw.css()`.
|
|
508
493
|
|
|
509
494
|
```js
|
|
510
|
-
|
|
511
|
-
|
|
495
|
+
bw.injectCSS(bw.css({
|
|
496
|
+
'.card': {
|
|
497
|
+
padding: '1.5rem',
|
|
498
|
+
borderRadius: '12px',
|
|
499
|
+
border: '1px solid #ddd',
|
|
500
|
+
maxWidth: '300px'
|
|
501
|
+
},
|
|
502
|
+
'.card h3': { margin: '0 0 0.5rem 0' },
|
|
503
|
+
'.card p': { margin: '0', color: '#666' }
|
|
504
|
+
}));
|
|
512
505
|
|
|
513
|
-
|
|
506
|
+
bw.mount('#app', contactCard('Alice', 'alice@example.com'));
|
|
507
|
+
// Now the card renders with rounded corners, padding, and subtle text.
|
|
514
508
|
```
|
|
515
509
|
|
|
516
|
-
|
|
510
|
+
### Functions generate CSS rules
|
|
517
511
|
|
|
518
512
|
```js
|
|
519
|
-
function
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
};
|
|
513
|
+
function cardStyles(accentColor) {
|
|
514
|
+
var shades = bw.deriveShades(accentColor);
|
|
515
|
+
return {
|
|
516
|
+
background: shades.light,
|
|
517
|
+
border: '1px solid ' + shades.border,
|
|
518
|
+
color: shades.darkText,
|
|
519
|
+
borderRadius: '12px'
|
|
520
|
+
};
|
|
528
521
|
}
|
|
529
522
|
|
|
530
|
-
{
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
523
|
+
bw.injectCSS(bw.css({
|
|
524
|
+
'.warning-card': cardStyles('#e67e22'),
|
|
525
|
+
'.success-card': cardStyles('#27ae60'),
|
|
526
|
+
'.info-card': cardStyles('#3498db')
|
|
527
|
+
}));
|
|
535
528
|
```
|
|
536
529
|
|
|
537
|
-
|
|
530
|
+
This accomplishes what Sass mixins do, but with plain JavaScript functions -- no extra compilation step or grammar to learn.
|
|
538
531
|
|
|
539
|
-
###
|
|
532
|
+
### bw.makeStyles() and bw.applyStyles() -- theme and component style system
|
|
540
533
|
|
|
541
|
-
|
|
542
|
-
// Ternary (inline)
|
|
543
|
-
{ t: 'div', c: loggedIn ? 'Welcome back' : 'Please sign in' }
|
|
534
|
+
Generate a coherent theme from two seed colors:
|
|
544
535
|
|
|
545
|
-
|
|
546
|
-
{
|
|
547
|
-
|
|
548
|
-
isAdmin ? { t: 'a', c: 'Admin' } : null,
|
|
549
|
-
{ t: 'a', c: 'About' }
|
|
550
|
-
].filter(Boolean)
|
|
551
|
-
}
|
|
536
|
+
```js
|
|
537
|
+
var theme = bw.makeStyles({ primary: '#336699', secondary: '#cc6633' });
|
|
538
|
+
bw.applyStyles(theme);
|
|
552
539
|
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
540
|
+
// theme.palette has every derived color as JS values
|
|
541
|
+
bw.injectCSS(bw.css({
|
|
542
|
+
'.my-header': {
|
|
543
|
+
background: theme.palette.primary.base,
|
|
544
|
+
color: theme.palette.primary.textOn,
|
|
545
|
+
borderBottom: '3px solid ' + theme.palette.secondary.base
|
|
546
|
+
}
|
|
547
|
+
}));
|
|
560
548
|
```
|
|
561
549
|
|
|
562
|
-
|
|
550
|
+
`bw.loadStyles()` is a shorthand that generates and applies in one call:
|
|
563
551
|
|
|
564
552
|
```js
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
];
|
|
569
|
-
|
|
570
|
-
{ t: 'table', c: [
|
|
571
|
-
{ t: 'thead', c: { t: 'tr', c: [
|
|
572
|
-
{ t: 'th', c: 'Name' }, { t: 'th', c: 'Role' }
|
|
573
|
-
]}},
|
|
574
|
-
{ t: 'tbody', c: users.map(function(u) {
|
|
575
|
-
return { t: 'tr', c: [
|
|
576
|
-
{ t: 'td', c: u.name },
|
|
577
|
-
{ t: 'td', c: u.role }
|
|
578
|
-
]};
|
|
579
|
-
})}
|
|
580
|
-
]}
|
|
553
|
+
bw.loadStyles({ primary: '#336699', secondary: '#cc6633' });
|
|
554
|
+
// With no arguments, loads structural CSS only (no colors)
|
|
555
|
+
bw.loadStyles();
|
|
581
556
|
```
|
|
582
557
|
|
|
583
|
-
|
|
558
|
+
### @keyframes and at-rules
|
|
584
559
|
|
|
585
|
-
|
|
560
|
+
`bw.css()` handles `@media`, `@keyframes`, and all `@`-prefix rules recursively:
|
|
586
561
|
|
|
587
|
-
|
|
562
|
+
```js
|
|
563
|
+
bw.injectCSS(bw.css({
|
|
564
|
+
'@keyframes fadeIn': {
|
|
565
|
+
'0%': { opacity: '0', transform: 'translateY(-10px)' },
|
|
566
|
+
'100%': { opacity: '1', transform: 'translateY(0)' }
|
|
567
|
+
},
|
|
568
|
+
'.toast': {
|
|
569
|
+
animation: 'fadeIn 0.3s ease-out',
|
|
570
|
+
padding: '0.75rem 1rem',
|
|
571
|
+
borderRadius: '8px'
|
|
572
|
+
}
|
|
573
|
+
}));
|
|
574
|
+
```
|
|
588
575
|
|
|
589
|
-
###
|
|
576
|
+
### The styling ladder
|
|
590
577
|
|
|
591
|
-
|
|
578
|
+
| Need | Tool | Example |
|
|
579
|
+
|------|------|---------|
|
|
580
|
+
| One-off inline style | `a: { style: '...' }` | Quick prototyping |
|
|
581
|
+
| Composed inline styles | `bw.s(obj1, obj2)` | Reusable style objects |
|
|
582
|
+
| Class-based CSS | `bw.css()` + `bw.injectCSS()` | Pseudo-classes, media queries |
|
|
583
|
+
| Theme + component styles | `bw.makeStyles()` + `bw.applyStyles()` | Consistent theming from seed colors |
|
|
592
584
|
|
|
593
|
-
|
|
585
|
+
Start at the top. Move down when you need more power. Each level builds on the one before.
|
|
594
586
|
|
|
595
|
-
Three things to know about BCCL:
|
|
596
587
|
|
|
597
|
-
|
|
598
|
-
2. **There are no tricks.** BCCL factories are regular functions that construct TACO objects. They don't use private APIs or special rendering paths. Anything a BCCL factory does, you can do by hand.
|
|
599
|
-
3. **BCCL is optional.** You can use it for everything, use it selectively, or ignore it entirely and build your own components from scratch.
|
|
588
|
+
## 6. Events and Behavior
|
|
600
589
|
|
|
601
|
-
|
|
590
|
+
A styled card looks right but does nothing. Adding behavior means adding event handler functions to `a:` -- the same place every other attribute lives.
|
|
602
591
|
|
|
603
|
-
|
|
604
|
-
var card = bw.makeCard({ title: 'Users', content: '42 online' });
|
|
605
|
-
// card is { t:'div', a:{class:'bw-card'}, c:[...] }
|
|
592
|
+
### Inline event handlers
|
|
606
593
|
|
|
607
|
-
|
|
608
|
-
bw.makeNavbar({ brand: 'My App', items: [
|
|
609
|
-
{ text: 'Home', href: '#' },
|
|
610
|
-
{ text: 'About', href: '#about' }
|
|
611
|
-
]}),
|
|
612
|
-
{ t: 'div', a: { class: 'content' }, c: [
|
|
613
|
-
bw.makeAlert({ content: 'Welcome!', variant: 'success' }),
|
|
614
|
-
card
|
|
615
|
-
]},
|
|
616
|
-
bw.makeTable({ data: users, sortable: true })
|
|
617
|
-
]};
|
|
594
|
+
Event handlers go in `a:` as function values:
|
|
618
595
|
|
|
619
|
-
|
|
596
|
+
```js
|
|
597
|
+
bw.mount('#app', {
|
|
598
|
+
t: 'button',
|
|
599
|
+
a: { onclick: function() { alert('Clicked!'); } },
|
|
600
|
+
c: 'Click me'
|
|
601
|
+
});
|
|
620
602
|
```
|
|
621
603
|
|
|
622
|
-
|
|
604
|
+
All standard DOM events work: `onclick`, `onchange`, `oninput`, `onsubmit`, `onkeydown`, etc.
|
|
623
605
|
|
|
624
|
-
|
|
625
|
-
|----------|-----------|
|
|
626
|
-
| Layout | makeNavbar, makeContainer, makeRow, makeCol, makeStack, makeSection |
|
|
627
|
-
| Content | makeCard, makeAlert, makeBadge, makeStatCard, makeTimeline, makeHero |
|
|
628
|
-
| Forms | makeInput, makeSelect, makeTextarea, makeForm, makeFormGroup, makeSearchInput |
|
|
629
|
-
| Data | makeTable, makeTableFromArray, makeBarChart, makeProgress, makePagination |
|
|
630
|
-
| Interactive | makeButton, makeAccordion, makeTabs, makeModal, makeCarousel, makeTooltip, makeDropdown |
|
|
631
|
-
|
|
632
|
-
See `docs/component-library.md` for full signatures and options.
|
|
633
|
-
|
|
634
|
-
### Mix BCCL with your own TACOs
|
|
606
|
+
Continuing our card: add a click handler that copies the email address.
|
|
635
607
|
|
|
636
608
|
```js
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
609
|
+
function contactCard(name, email) {
|
|
610
|
+
return {
|
|
611
|
+
t: 'div', a: {
|
|
612
|
+
class: 'card',
|
|
613
|
+
style: 'cursor:pointer',
|
|
614
|
+
onclick: function() { alert('Email: ' + email); }
|
|
615
|
+
},
|
|
616
|
+
c: [
|
|
617
|
+
{ t: 'h3', c: name },
|
|
618
|
+
{ t: 'p', c: email }
|
|
619
|
+
]
|
|
620
|
+
};
|
|
621
|
+
}
|
|
644
622
|
```
|
|
645
623
|
|
|
646
|
-
|
|
624
|
+
The handler closes over `email` -- no data binding, no state management. Just a closure.
|
|
625
|
+
|
|
626
|
+
### Re-rendering on interaction
|
|
647
627
|
|
|
648
|
-
|
|
628
|
+
Combine event handlers with `bw.mount()` to build interactive UIs without any state framework:
|
|
649
629
|
|
|
650
630
|
```js
|
|
651
|
-
var
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
631
|
+
var filter = 'all';
|
|
632
|
+
var items = [
|
|
633
|
+
{ name: 'Widget A', type: 'widget' },
|
|
634
|
+
{ name: 'Gadget B', type: 'gadget' },
|
|
635
|
+
{ name: 'Widget C', type: 'widget' }
|
|
636
|
+
];
|
|
656
637
|
|
|
657
|
-
|
|
638
|
+
function renderApp() {
|
|
639
|
+
var filtered = filter === 'all'
|
|
640
|
+
? items
|
|
641
|
+
: items.filter(function(i) { return i.type === filter; });
|
|
642
|
+
|
|
643
|
+
bw.mount('#app', { t: 'div', c: [
|
|
644
|
+
{ t: 'div', c: ['all', 'widget', 'gadget'].map(function(f) {
|
|
645
|
+
return {
|
|
646
|
+
t: 'button',
|
|
647
|
+
a: {
|
|
648
|
+
onclick: function() { filter = f; renderApp(); },
|
|
649
|
+
style: 'margin:0.25rem; font-weight:' + (filter === f ? '700' : '400')
|
|
650
|
+
},
|
|
651
|
+
c: f
|
|
652
|
+
};
|
|
653
|
+
})},
|
|
654
|
+
{ t: 'ul', c: filtered.map(function(i) {
|
|
655
|
+
return { t: 'li', c: i.name };
|
|
656
|
+
})}
|
|
657
|
+
]});
|
|
658
|
+
}
|
|
658
659
|
|
|
659
|
-
|
|
660
|
+
renderApp();
|
|
661
|
+
```
|
|
660
662
|
|
|
661
|
-
|
|
662
|
-
|-------|-------------|---------------|-------------|
|
|
663
|
-
| **0 -- Data** | A plain JS object | `makeCard({...})` or `{t,a,c}` | Static content, SSR, data-driven lists |
|
|
664
|
-
| **1 -- DOM** | A rendered DOM tree | `bw.DOM('#x', taco)` | One-shot renders, manual re-renders |
|
|
665
|
-
| **2 -- Stateful** | A reactive component | `o.state` + `o.render` + `bw.update()` | State that changes, re-rendering UI |
|
|
663
|
+
This is the simplest interactive pattern: a render function that calls `bw.mount()`. When data changes, call the function again. No framework magic.
|
|
666
664
|
|
|
667
|
-
|
|
665
|
+
### Handling form data
|
|
668
666
|
|
|
669
|
-
|
|
667
|
+
`bw.$()` returns an array of matching DOM elements (like `querySelectorAll` but always an array), so we use `[0]` to get the first match:
|
|
670
668
|
|
|
671
669
|
```js
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
|
|
670
|
+
bw.mount('#app', {
|
|
671
|
+
t: 'form',
|
|
672
|
+
a: { onsubmit: function(e) {
|
|
673
|
+
e.preventDefault();
|
|
674
|
+
var name = bw.$('#name-input')[0].value;
|
|
675
|
+
var email = bw.$('#email-input')[0].value;
|
|
676
|
+
console.log('Submitted:', name, email);
|
|
677
|
+
}},
|
|
678
|
+
c: [
|
|
679
|
+
{ t: 'input', a: { id: 'name-input', type: 'text', placeholder: 'Name' } },
|
|
680
|
+
{ t: 'input', a: { id: 'email-input', type: 'email', placeholder: 'Email' } },
|
|
681
|
+
{ t: 'button', a: { type: 'submit' }, c: 'Submit' }
|
|
682
|
+
]
|
|
683
|
+
});
|
|
685
684
|
```
|
|
686
685
|
|
|
687
|
-
### Level 1 — render and re-render
|
|
688
686
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
setInterval(renderClock, 1000);
|
|
694
|
-
renderClock();
|
|
695
|
-
```
|
|
687
|
+
## 7. Lifecycle: `o:` Options
|
|
688
|
+
|
|
689
|
+
Event handlers give us interactivity, but real components need state that persists across renders and cleanup logic for resources. That is what the `o:` (options) key provides.
|
|
696
690
|
|
|
697
|
-
|
|
691
|
+
The `o:` key is where non-HTML concerns live. It was separated from `a:` because `a:` compiles directly to HTML attributes -- library metadata should not leak into the DOM.
|
|
698
692
|
|
|
699
|
-
|
|
693
|
+
Bitwrench supports two broad update styles. Use `o.render` with `bw.refresh()` when a component should rebuild its contents from state. Use handles, slots, patching, or replacement when a component can update a specific part of the DOM directly. Pub/sub is available for communication between components that should not hold direct references to one another.
|
|
694
|
+
|
|
695
|
+
### o.state -- component state
|
|
700
696
|
|
|
701
697
|
```js
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
]
|
|
720
|
-
});
|
|
698
|
+
bw.mount('#app', {
|
|
699
|
+
t: 'div',
|
|
700
|
+
o: {
|
|
701
|
+
state: { count: 0 },
|
|
702
|
+
render: function(el, state) {
|
|
703
|
+
bw.mount(el, { t: 'div', c: [
|
|
704
|
+
{ t: 'span', c: 'Count: ' + state.count },
|
|
705
|
+
{ t: 'button', a: { onclick: function() {
|
|
706
|
+
state.count++;
|
|
707
|
+
bw.refresh(el);
|
|
708
|
+
}}, c: '+1' },
|
|
709
|
+
{ t: 'button', a: { onclick: function() {
|
|
710
|
+
state.count = 0;
|
|
711
|
+
bw.refresh(el);
|
|
712
|
+
}}, c: 'Reset' }
|
|
713
|
+
]});
|
|
714
|
+
}
|
|
721
715
|
}
|
|
722
|
-
|
|
723
|
-
};
|
|
724
|
-
|
|
725
|
-
bw.DOM('#app', counter);
|
|
716
|
+
});
|
|
726
717
|
```
|
|
727
718
|
|
|
728
|
-
|
|
719
|
+
`o.state` is assigned by reference to `el._bw_state` at creation time (not cloned). `o.render` is called with `(el, state)` on mount and on every `bw.refresh(el)`. When state changes, mutate the state object and call `bw.refresh(el)` to re-render.
|
|
729
720
|
|
|
730
|
-
|
|
721
|
+
Continuing our card: let's add an expanded/collapsed state to the contact card.
|
|
722
|
+
|
|
723
|
+
```js
|
|
724
|
+
function contactCard(name, email, phone) {
|
|
725
|
+
return {
|
|
726
|
+
t: 'div', a: { class: 'card' },
|
|
727
|
+
o: {
|
|
728
|
+
state: { expanded: false },
|
|
729
|
+
render: function(el, state) {
|
|
730
|
+
bw.mount(el, { t: 'div', c: [
|
|
731
|
+
{ t: 'h3', a: { onclick: function() {
|
|
732
|
+
state.expanded = !state.expanded;
|
|
733
|
+
bw.refresh(el);
|
|
734
|
+
}, style: 'cursor:pointer' }, c: name + (state.expanded ? ' \u25B2' : ' \u25BC') },
|
|
735
|
+
{ t: 'p', c: email },
|
|
736
|
+
state.expanded ? { t: 'p', c: phone } : null
|
|
737
|
+
]});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
731
742
|
|
|
743
|
+
bw.mount('#app', contactCard('Alice', 'alice@example.com', '+1-555-0100'));
|
|
744
|
+
// Clicking the name toggles the phone number.
|
|
732
745
|
```
|
|
733
|
-
Is the content static or computed once from data?
|
|
734
|
-
=> Level 0. Use make*() or hand-write TACO. Render with bw.DOM().
|
|
735
746
|
|
|
736
|
-
|
|
737
|
-
=> Level 1. Call bw.DOM() again when data changes.
|
|
747
|
+
The card now has real state. The render function receives `(el, state)`, reads `state.expanded`, and conditionally shows the phone number. `bw.refresh(el)` re-renders when state changes.
|
|
738
748
|
|
|
739
|
-
|
|
740
|
-
and you want structured state management?
|
|
741
|
-
=> Level 2. Use o.state + o.render + bw.update().
|
|
742
|
-
```
|
|
749
|
+
### o.mounted and o.unmount -- lifecycle hooks
|
|
743
750
|
|
|
744
|
-
|
|
751
|
+
```js
|
|
752
|
+
{
|
|
753
|
+
t: 'div', a: { class: 'sensor-display' },
|
|
754
|
+
o: {
|
|
755
|
+
mounted: function(el) {
|
|
756
|
+
// Called after the element enters the DOM.
|
|
757
|
+
// Good for: observers, timers, third-party library init.
|
|
758
|
+
el._resizeObs = new ResizeObserver(function(entries) {
|
|
759
|
+
console.log('Resized:', entries[0].contentRect.width);
|
|
760
|
+
});
|
|
761
|
+
el._resizeObs.observe(el);
|
|
762
|
+
},
|
|
763
|
+
unmount: function(el) {
|
|
764
|
+
// Called before the element is removed from the DOM.
|
|
765
|
+
// Good for: cleanup of observers, timers, connections.
|
|
766
|
+
if (el._resizeObs) el._resizeObs.disconnect();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
```
|
|
745
771
|
|
|
746
|
-
|
|
772
|
+
Use `mounted` for setup that needs the real DOM element -- observers, measuring dimensions, initializing third-party libraries. Use `unmount` to clean up.
|
|
747
773
|
|
|
748
|
-
|
|
774
|
+
**Automatic cleanup:** bitwrench runs a document-level MutationObserver that detects when elements are removed from the DOM -- even by third-party code or raw DOM operations. When a component disappears, its `unmount` hooks fire and bitwrench releases its lifecycle registrations and pub/sub subscriptions so that it does not retain the removed subtree. You do not need to detect DOM removal manually -- declare cleanup in `o.unmount`, and bitwrench invokes it when the component leaves the DOM.
|
|
749
775
|
|
|
750
|
-
|
|
776
|
+
**Prefer event handlers in `a:`.** Listeners attached directly to child nodes during `o.mounted` will be lost if those children are replaced by `bw.refresh()`. Handlers in `a:` are re-attached automatically on every render. Reserve imperative listeners in `o.mounted` for integrations that require them (e.g. window-level events), and clean them up in `o.unmount`:
|
|
751
777
|
|
|
752
778
|
```js
|
|
753
|
-
// CORRECT
|
|
754
|
-
bw.makeButton({
|
|
755
|
-
text: 'Save',
|
|
756
|
-
onclick: function() { save(); }
|
|
757
|
-
})
|
|
758
|
-
|
|
759
|
-
// CORRECT — inline in TACO
|
|
779
|
+
// CORRECT -- handler in attributes, survives re-renders
|
|
760
780
|
{ t: 'button', a: { onclick: function() { save(); } }, c: 'Save' }
|
|
761
781
|
|
|
762
|
-
// WRONG
|
|
782
|
+
// WRONG -- handler lost after first re-render
|
|
763
783
|
{ t: 'button', c: 'Save',
|
|
764
784
|
o: { mounted: function(el) { el.addEventListener('click', save); } }
|
|
765
785
|
}
|
|
766
786
|
```
|
|
767
787
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
**When is `o.mounted` appropriate?** Only for non-event setup that needs the actual DOM element reference — IntersectionObserver, ResizeObserver, third-party library initialization, measuring element dimensions. Never for click/input/change handlers.
|
|
788
|
+
### o.handle -- component methods
|
|
771
789
|
|
|
772
|
-
|
|
790
|
+
`o.handle` attaches named methods to `el.bw`. These are the cheap update path -- surgical DOM updates without tearing down and rebuilding:
|
|
773
791
|
|
|
774
792
|
```js
|
|
775
|
-
|
|
776
|
-
function addToCart(item) {
|
|
777
|
-
cart.push(item);
|
|
778
|
-
bw.pub('cart:updated', { count: cart.length });
|
|
779
|
-
}
|
|
793
|
+
var counterEl;
|
|
780
794
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
795
|
+
var counter = {
|
|
796
|
+
t: 'div', c: [
|
|
797
|
+
{ t: 'span', a: { class: 'count' }, c: '0' },
|
|
798
|
+
{ t: 'button', a: { onclick: function() {
|
|
799
|
+
counterEl.bw.increment();
|
|
800
|
+
}}, c: '+' }
|
|
801
|
+
],
|
|
802
|
+
o: {
|
|
803
|
+
handle: {
|
|
804
|
+
increment: function(el) {
|
|
805
|
+
var span = el.querySelector('.count');
|
|
806
|
+
span.textContent = String(Number(span.textContent) + 1);
|
|
807
|
+
},
|
|
808
|
+
reset: function(el) {
|
|
809
|
+
el.querySelector('.count').textContent = '0';
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
};
|
|
791
814
|
|
|
792
|
-
|
|
815
|
+
counterEl = bw.mount('#app', counter);
|
|
816
|
+
counterEl.bw.increment(); // updates count without re-rendering the tree
|
|
817
|
+
counterEl.bw.reset(); // same -- surgical, targeted
|
|
818
|
+
```
|
|
793
819
|
|
|
794
|
-
|
|
820
|
+
### o.slots -- auto-generated setters and getters
|
|
795
821
|
|
|
796
|
-
|
|
822
|
+
`o.slots` maps names to CSS selectors. Bitwrench auto-generates `el.bw.setName()` and `el.bw.getName()` for each:
|
|
797
823
|
|
|
798
824
|
```js
|
|
799
|
-
var
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
},
|
|
810
|
-
reset: function(el) {
|
|
811
|
-
el.querySelector('.count').textContent = '0';
|
|
812
|
-
}
|
|
825
|
+
var card = {
|
|
826
|
+
t: 'div', a: { class: 'card' }, c: [
|
|
827
|
+
{ t: 'h3', a: { class: 'card-title' }, c: 'Initial Title' },
|
|
828
|
+
{ t: 'div', a: { class: 'card-body' }, c: 'Initial content' }
|
|
829
|
+
],
|
|
830
|
+
o: {
|
|
831
|
+
slots: {
|
|
832
|
+
title: '.card-title',
|
|
833
|
+
body: '.card-body'
|
|
834
|
+
}
|
|
813
835
|
}
|
|
814
|
-
}
|
|
815
836
|
};
|
|
816
837
|
|
|
817
|
-
var el = bw.mount('#app', widget);
|
|
818
|
-
el.bw.increment(); // updates the count without re-rendering the whole component
|
|
819
|
-
el.bw.reset();
|
|
820
|
-
```
|
|
821
|
-
|
|
822
|
-
**o.slots** auto-generates setters and getters for named content areas:
|
|
823
|
-
|
|
824
|
-
```js
|
|
825
|
-
var card = bw.makeCard({ title: 'Stats', content: '0' });
|
|
826
838
|
var el = bw.mount('#app', card);
|
|
827
|
-
el.bw.setTitle('Revenue');
|
|
828
|
-
el.bw.
|
|
829
|
-
el.bw.getTitle();
|
|
839
|
+
el.bw.setTitle('Revenue'); // update just the title
|
|
840
|
+
el.bw.setBody({ t: 'b', c: '$42,000' }); // accepts TACOs
|
|
841
|
+
el.bw.getTitle(); // returns 'Revenue'
|
|
830
842
|
```
|
|
831
843
|
|
|
832
|
-
Slot setters update only the targeted
|
|
844
|
+
Slot setters update only the targeted element. Input focus, scroll position, and animation state in siblings are preserved -- this is the key advantage over `bw.refresh()`.
|
|
833
845
|
|
|
834
|
-
|
|
846
|
+
### Why o: is separate from a:
|
|
835
847
|
|
|
836
|
-
|
|
848
|
+
| Key | Compiles to HTML | Purpose |
|
|
849
|
+
|-----|-----------------|---------|
|
|
850
|
+
| `a:` | Yes | HTML attributes: class, id, style, onclick, data-*, href, src, etc. |
|
|
851
|
+
| `o:` | No | Bitwrench concerns: state, render, handle, slots, mounted, unmount |
|
|
837
852
|
|
|
838
|
-
|
|
853
|
+
If `o:` properties went in `a:`, they would appear as HTML attributes in the rendered output -- or worse, in `bw.html()` string output. The separation keeps framework metadata out of generated HTML and makes the serializable subset of a `{taco}` easier to identify. (A `{taco}` is serializable when it contains only JSON-compatible values -- no event-handler functions in `a:`, no lifecycle or render functions in `o:`, no DOM references.)
|
|
839
854
|
|
|
840
|
-
|
|
855
|
+
### The update cost spectrum
|
|
841
856
|
|
|
842
|
-
|
|
857
|
+
| Operation | Cost | What happens |
|
|
858
|
+
|-----------|------|-------------|
|
|
859
|
+
| `el.bw.method()` | Surgical | Component updates its own DOM |
|
|
860
|
+
| Slot setters (`el.bw.setTitle()`) | Targeted | Replaces content at a cached DOM target |
|
|
861
|
+
| `bw.message(ref, action, data)` | Dispatch | Calls `el.bw[action](data)` by selector |
|
|
862
|
+
| `bw.update(ref, data)` | Dispatch | Calls `el.bw.update(data)`, warns if missing |
|
|
863
|
+
| `bw.patch(id, content)` | Targeted | Updates a single element's content |
|
|
864
|
+
| `bw.replace(ref, taco)` | Element swap | Unmounts old, mounts new at same position |
|
|
865
|
+
| `bw.refresh(ref)` | Full rebuild | Unmounts children, re-renders from `o.render` |
|
|
843
866
|
|
|
844
|
-
|
|
867
|
+
Prefer methods at the top of this list. Use `bw.refresh()` only when you need a full re-render.
|
|
845
868
|
|
|
846
|
-
|
|
847
|
-
Server (any language) Browser
|
|
848
|
-
| |
|
|
849
|
-
|-- SSE: {replace, #app, taco} --> bw.apply() --> DOM update
|
|
850
|
-
|-- SSE: {patch, #counter, "42"} -> targeted text update
|
|
851
|
-
|-- SSE: {append, #log, taco} ---> new child added
|
|
852
|
-
| |
|
|
853
|
-
|<-- POST: {action: "click"} --+ user interaction
|
|
854
|
-
```
|
|
855
|
-
|
|
856
|
-
### Initial UI delivery
|
|
857
|
-
|
|
858
|
-
The server sends a `replace` message with a TACO that becomes the page content:
|
|
869
|
+
### Cross-component communication -- pub/sub
|
|
859
870
|
|
|
860
871
|
```js
|
|
861
|
-
|
|
872
|
+
// Publisher
|
|
873
|
+
function addToCart(item) {
|
|
874
|
+
cart.push(item);
|
|
875
|
+
bw.pub('cart:updated', { count: cart.length });
|
|
876
|
+
}
|
|
862
877
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
{ t: 'button', a: { 'data-bw-action': 'greet' }, c: 'Say hello' }
|
|
870
|
-
]
|
|
871
|
-
});
|
|
872
|
-
});
|
|
873
|
-
app.listen();
|
|
878
|
+
// Subscriber -- navEl is the element returned by bw.mount() for a nav component
|
|
879
|
+
// Auto-cleans when element is removed
|
|
880
|
+
bw.sub('cart:updated', function(data) {
|
|
881
|
+
navEl._bw_state.cartCount = data.count;
|
|
882
|
+
bw.refresh(navEl);
|
|
883
|
+
}, navEl);
|
|
874
884
|
```
|
|
875
885
|
|
|
876
|
-
|
|
886
|
+
`bw.pub()` and `bw.sub()` are app-wide. Pass the element as the third argument to `bw.sub()` to tie the subscription lifetime to that element.
|
|
877
887
|
|
|
878
|
-
### Incremental updates from server
|
|
879
888
|
|
|
880
|
-
|
|
881
|
-
client.patch('#status', 'Processing...');
|
|
882
|
-
client.append('#log', { t: 'div', c: 'Event at ' + new Date().toISOString() });
|
|
883
|
-
client.remove('#old-notification');
|
|
884
|
-
client.batch([
|
|
885
|
-
{ type: 'patch', target: '#status', content: 'Done.' },
|
|
886
|
-
{ type: 'remove', target: '#spinner' }
|
|
887
|
-
]);
|
|
888
|
-
```
|
|
889
|
-
|
|
890
|
-
### Client events back to server
|
|
889
|
+
## 8. BCCL: Ready-Made Components
|
|
891
890
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
```js
|
|
895
|
-
client.on('greet', function(data) {
|
|
896
|
-
client.patch('#status', 'Hello, user!');
|
|
897
|
-
});
|
|
898
|
-
```
|
|
891
|
+
Building every component from scratch teaches you the model, but for common patterns -- cards, tables, modals, alerts -- bitwrench ships ready-made factories.
|
|
899
892
|
|
|
900
|
-
|
|
893
|
+
BCCL (Bitwrench Common Component Library) is a set of factory functions that return `{taco}` objects for common UI patterns. Think of it as Bootstrap, but instead of HTML templates you get JavaScript objects.
|
|
901
894
|
|
|
902
|
-
|
|
895
|
+
### Factories return TACOs, not DOM
|
|
903
896
|
|
|
904
897
|
```js
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
// Built-in calls
|
|
909
|
-
client.call('scrollTo', '#section-2');
|
|
910
|
-
client.call('redirect', '/dashboard');
|
|
911
|
-
```
|
|
912
|
-
|
|
913
|
-
### Addressing modes
|
|
914
|
-
|
|
915
|
-
Target any element by CSS selector or UUID:
|
|
898
|
+
var card = bw.makeCard({ title: 'Users', content: '42 online' });
|
|
899
|
+
// card is { t:'div', a:{class:'bw_bccl_card'}, c:[...] }
|
|
900
|
+
// It is a plain {taco} -- inspect it, modify it, nest it.
|
|
916
901
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
902
|
+
bw.mount('#app', { t: 'div', c: [
|
|
903
|
+
bw.makeNavbar({ brand: 'My App', items: [
|
|
904
|
+
{ text: 'Home', href: '#' },
|
|
905
|
+
{ text: 'About', href: '#about' }
|
|
906
|
+
]}),
|
|
907
|
+
{ t: 'div', a: { class: 'content' }, c: [
|
|
908
|
+
bw.makeAlert({ content: 'Welcome!', variant: 'success' }),
|
|
909
|
+
card
|
|
910
|
+
]},
|
|
911
|
+
bw.makeTable({ data: users, sortable: true })
|
|
912
|
+
]});
|
|
926
913
|
```
|
|
927
914
|
|
|
928
|
-
###
|
|
929
|
-
|
|
930
|
-
- **Language-agnostic**: any server that writes SSE can do this — Python, Go, Rust, C, shell scripts.
|
|
931
|
-
- **LLMs**: an AI can emit TACO objects directly — orders of magnitude fewer tokens than HTML/JSX.
|
|
932
|
-
- **Embedded**: an ESP32 serves one HTML page with bitwrench, then pushes sensor data as patches over SSE.
|
|
933
|
-
- **Replaces Streamlit/Gradio**: same server-driven pattern, not locked to Python, full TACO composition model.
|
|
934
|
-
- **Relaxed JSON**: bwserve accepts unquoted keys, single quotes, trailing commas — convenient for embedded C code.
|
|
935
|
-
|
|
936
|
-
---
|
|
915
|
+
### Quick inventory
|
|
937
916
|
|
|
938
|
-
|
|
917
|
+
| Category | Components |
|
|
918
|
+
|----------|-----------|
|
|
919
|
+
| Layout | makeNavbar, makeContainer, makeRow, makeCol, makeStack, makeSection |
|
|
920
|
+
| Content | makeCard, makeAlert, makeBadge, makeStatCard, makeTimeline, makeHero |
|
|
921
|
+
| Forms | makeInput, makeSelect, makeTextarea, makeForm, makeFormGroup, makeSearchInput |
|
|
922
|
+
| Data | makeTable, makeTableFromArray, makeBarChart, makeProgress, makePagination |
|
|
923
|
+
| Interactive | makeButton, makeAccordion, makeTabs, makeModal, makeCarousel, makeTooltip, makeDropdown |
|
|
939
924
|
|
|
940
|
-
|
|
925
|
+
See `docs/component-library.md` for full signatures.
|
|
941
926
|
|
|
942
|
-
###
|
|
927
|
+
### Mix BCCL with hand-written TACOs
|
|
943
928
|
|
|
944
929
|
```js
|
|
945
|
-
bw.
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
},
|
|
953
|
-
'*': function() { return { t: 'h1', c: '404 Not Found' }; }
|
|
954
|
-
}
|
|
955
|
-
});
|
|
930
|
+
bw.mount('#app', { t: 'div', c: [
|
|
931
|
+
bw.makeCard({ title: 'Stats' }),
|
|
932
|
+
{ t: 'div', a: { style: 'padding:2rem' }, c: [
|
|
933
|
+
{ t: 'h3', c: 'Custom Section' },
|
|
934
|
+
{ t: 'p', c: 'Hand-written {taco} next to a BCCL card.' }
|
|
935
|
+
]}
|
|
936
|
+
]});
|
|
956
937
|
```
|
|
957
938
|
|
|
958
|
-
|
|
939
|
+
### Modify BCCL output before rendering
|
|
959
940
|
|
|
960
|
-
|
|
941
|
+
Since BCCL returns plain objects, you can modify them:
|
|
961
942
|
|
|
962
943
|
```js
|
|
963
|
-
bw.
|
|
964
|
-
|
|
944
|
+
var card = bw.makeCard({ title: 'Users', content: '42 online' });
|
|
945
|
+
card.a.style = 'border-left:4px solid #336699';
|
|
946
|
+
card.c.push({ t: 'small', c: 'Updated 5m ago' });
|
|
947
|
+
bw.mount('#app', card);
|
|
965
948
|
```
|
|
966
949
|
|
|
967
|
-
###
|
|
950
|
+
### BCCL components include handles and slots
|
|
968
951
|
|
|
969
|
-
|
|
970
|
-
// bw.link() returns a TACO <a> with onclick wired to bw.navigate()
|
|
971
|
-
bw.link('/about', 'About Us', { class: 'nav-item' })
|
|
972
|
-
```
|
|
973
|
-
|
|
974
|
-
### Route parameters and query strings
|
|
952
|
+
The stateful and interactive BCCL components (cards, tables, tabs, modals, toasts, accordions, and others) wire up `o.handle` and/or `o.slots` automatically. Purely structural factories (rows, containers, buttons) return plain TACOs with no handles -- they don't need any:
|
|
975
953
|
|
|
976
954
|
```js
|
|
977
|
-
|
|
978
|
-
'
|
|
979
|
-
|
|
980
|
-
params._query.tab; // 'posts'
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// /docs/api/colors (catch-all)
|
|
984
|
-
'/docs/*': function(params) {
|
|
985
|
-
params._rest; // 'api/colors'
|
|
986
|
-
}
|
|
955
|
+
var el = bw.mount('#app', bw.makeCard({ title: 'Stats', content: '0' }));
|
|
956
|
+
el.bw.setTitle('Revenue');
|
|
957
|
+
el.bw.setContent({ t: 'b', c: '$42k' });
|
|
987
958
|
```
|
|
988
959
|
|
|
989
|
-
|
|
960
|
+
See [Component Library](component-library.md) for the full method table per component.
|
|
990
961
|
|
|
991
|
-
|
|
992
|
-
bw.router({
|
|
993
|
-
target: '#app',
|
|
994
|
-
routes: { ... },
|
|
995
|
-
before: function(to, from) {
|
|
996
|
-
if (to === '/admin' && !loggedIn) return '/login'; // redirect
|
|
997
|
-
if (to === '/locked') return false; // block
|
|
998
|
-
},
|
|
999
|
-
after: function(to, from) {
|
|
1000
|
-
window.scrollTo(0, 0);
|
|
1001
|
-
}
|
|
1002
|
-
});
|
|
1003
|
-
```
|
|
962
|
+
### Three things to know
|
|
1004
963
|
|
|
1005
|
-
|
|
964
|
+
1. Every factory returns a `{taco}`. The output is a plain `{t, a, c, o}` object.
|
|
965
|
+
2. There are no tricks. BCCL factories are regular functions. Anything they do, you can do by hand.
|
|
966
|
+
3. BCCL is optional. Use it for everything, use it selectively, or ignore it entirely.
|
|
1006
967
|
|
|
1007
|
-
Every route change publishes `bw:route`:
|
|
1008
968
|
|
|
1009
|
-
|
|
1010
|
-
bw.sub('bw:route', function(data) {
|
|
1011
|
-
// data.path, data.params, data.query, data.from
|
|
1012
|
-
navEl.bw.setActive(data.path);
|
|
1013
|
-
}, navEl);
|
|
1014
|
-
```
|
|
969
|
+
## 9. Routing, Utilities, Advanced
|
|
1015
970
|
|
|
1016
|
-
|
|
971
|
+
Sections 2-8 cover the core model. This section covers the remaining tools bitwrench provides: routing, declared dataflow, color utilities, and general-purpose helpers.
|
|
1017
972
|
|
|
1018
|
-
|
|
973
|
+
### Client-side routing
|
|
1019
974
|
|
|
1020
|
-
|
|
975
|
+
Bitwrench includes a built-in client-side router:
|
|
1021
976
|
|
|
1022
977
|
```js
|
|
1023
|
-
bw.router({
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
978
|
+
bw.router({
|
|
979
|
+
target: '#app',
|
|
980
|
+
routes: {
|
|
981
|
+
'/': function() { return { t: 'h1', c: 'Home' }; },
|
|
982
|
+
'/about': function() { return { t: 'h1', c: 'About' }; },
|
|
983
|
+
'/users/:id': function(params) {
|
|
984
|
+
return bw.makeCard({ title: 'User ' + params.id });
|
|
985
|
+
},
|
|
986
|
+
'*': function() { return { t: 'h1', c: '404 Not Found' }; }
|
|
987
|
+
}
|
|
988
|
+
});
|
|
1027
989
|
|
|
1028
|
-
|
|
990
|
+
// Programmatic navigation
|
|
991
|
+
bw.navigate('/users/123');
|
|
1029
992
|
|
|
1030
|
-
|
|
1031
|
-
bw.
|
|
1032
|
-
tabs: [
|
|
1033
|
-
{ label: 'Overview', content: makeOverview() },
|
|
1034
|
-
{ label: 'Analytics', content: makeAnalytics() }
|
|
1035
|
-
]
|
|
1036
|
-
}));
|
|
993
|
+
// Navigation links (returns {taco})
|
|
994
|
+
bw.link('/about', 'About Us', { class: 'nav-item' })
|
|
1037
995
|
```
|
|
1038
996
|
|
|
1039
|
-
|
|
997
|
+
Route parameters (`/users/:id`), query strings (`params._query.tab`), catch-all routes (`/docs/*` with `params._rest`), guards (`before`/`after` hooks), and hash vs. history mode are all supported. See [Routing Guide](routing.md) for the full API.
|
|
1040
998
|
|
|
1041
|
-
|
|
999
|
+
### bw.derive() -- declared dataflow
|
|
1000
|
+
|
|
1001
|
+
`bw.derive()` recomputes a derived value when its input topics publish:
|
|
1042
1002
|
|
|
1043
1003
|
```js
|
|
1044
|
-
|
|
1045
|
-
|
|
1004
|
+
bw.derive(['cart:updated', 'discount:changed'], function(cartData, discountData) {
|
|
1005
|
+
var total = cartData.total * (1 - discountData.rate);
|
|
1006
|
+
return { total: total };
|
|
1007
|
+
}, 'order:total');
|
|
1008
|
+
|
|
1009
|
+
// Subscribes automatically; fires when either input publishes
|
|
1010
|
+
bw.sub('order:total', function(data) {
|
|
1011
|
+
bw.el('#total', '$' + data.total.toFixed(2));
|
|
1012
|
+
});
|
|
1046
1013
|
```
|
|
1047
1014
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
---
|
|
1051
|
-
|
|
1052
|
-
## 9. Utilities and Color Functions
|
|
1053
|
-
|
|
1054
|
-
Bitwrench includes utility functions that show up regularly in UI work. These aren't the main attraction, but they eliminate common boilerplate.
|
|
1015
|
+
The combiner function receives the latest value from each input topic as positional arguments, in the same order as the `inputs` array. The dependency graph is explicit and written in source code -- not assembled by getter traps at runtime.
|
|
1055
1016
|
|
|
1056
1017
|
### Color functions
|
|
1057
1018
|
|
|
@@ -1061,345 +1022,261 @@ bw.hslToHex([210, 50, 40]); // '#336699'
|
|
|
1061
1022
|
bw.adjustLightness('#336699', 20); // lighten by 20%
|
|
1062
1023
|
bw.mixColor('#336699', '#cc6633', 0.5); // blend two colors
|
|
1063
1024
|
bw.textOnColor('#336699'); // '#fff' (contrast-safe text color)
|
|
1064
|
-
bw.relativeLuminance('#336699'); // WCAG 2.0 luminance value
|
|
1065
1025
|
bw.deriveShades('#336699'); // { base, hover, active, light, darkText, border, focus, textOn }
|
|
1066
|
-
bw.derivePalette({ primary: '#336699', secondary: '#cc6633' }); // full
|
|
1026
|
+
bw.derivePalette({ primary: '#336699', secondary: '#cc6633' }); // full palette
|
|
1067
1027
|
```
|
|
1068
1028
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
### URL and data utilities
|
|
1029
|
+
### Utility functions
|
|
1072
1030
|
|
|
1073
1031
|
```js
|
|
1074
|
-
bw.
|
|
1075
|
-
bw.
|
|
1076
|
-
bw.
|
|
1032
|
+
bw.typeOf([1, 2, 3]); // 'array' (enhanced typeof)
|
|
1033
|
+
bw.uuid('widget'); // 'bw_uuid_widget_a3f2c1' (unique ID)
|
|
1034
|
+
bw.escapeHTML('<script>'); // '<script>'
|
|
1035
|
+
bw.getURLParam('page', 'home'); // read ?page=... from URL
|
|
1077
1036
|
bw.loremIpsum(200); // 200 characters of placeholder text
|
|
1078
|
-
bw.
|
|
1079
|
-
bw.random(5, 1, 100); // array of 5 random integers
|
|
1080
|
-
bw.mapScale(75, 0, 100, 0, 255); // map value between ranges (191.25)
|
|
1037
|
+
bw.mapScale(75, 0, 100, 0, 255); // map between ranges (191.25)
|
|
1081
1038
|
bw.clip(150, 0, 100); // clamp to range (100)
|
|
1082
1039
|
bw.naturalCompare('item2', 'item10'); // natural sort comparison
|
|
1040
|
+
bw.parseJSONFlex("{ name: 'Alice' }"); // flexible JSON (unquoted keys, single quotes, r-prefix)
|
|
1083
1041
|
```
|
|
1084
1042
|
|
|
1085
|
-
###
|
|
1043
|
+
### Raw HTML -- bw.raw()
|
|
1044
|
+
|
|
1045
|
+
By default, bitwrench escapes all content to prevent XSS. When you need actual HTML inside a `{taco}`, use `bw.raw()`:
|
|
1086
1046
|
|
|
1087
1047
|
```js
|
|
1088
|
-
//
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
bw.loadClientFile(function(data) { /* file contents */ });
|
|
1092
|
-
bw.loadClientJSON(function(obj) { /* parsed JSON */ });
|
|
1048
|
+
// Without bw.raw() -- <br> is escaped to visible text
|
|
1049
|
+
{ t: 'h1', c: 'Line One<br>Line Two' }
|
|
1050
|
+
// Renders: Line One<br>Line Two
|
|
1093
1051
|
|
|
1094
|
-
//
|
|
1095
|
-
bw.
|
|
1096
|
-
|
|
1052
|
+
// With bw.raw() -- HTML rendered as-is
|
|
1053
|
+
{ t: 'h1', c: bw.raw('Line One<br>Line Two') }
|
|
1054
|
+
// Renders: Line One (line break) Line Two
|
|
1097
1055
|
```
|
|
1098
1056
|
|
|
1099
|
-
|
|
1057
|
+
Never use `bw.raw()` on user-provided input.
|
|
1100
1058
|
|
|
1101
|
-
|
|
1059
|
+
### File I/O
|
|
1102
1060
|
|
|
1103
1061
|
```js
|
|
1104
|
-
|
|
1105
|
-
|
|
1062
|
+
// Browser
|
|
1063
|
+
bw.saveClientFile('report.txt', content);
|
|
1064
|
+
bw.loadClientJSON(function(obj) { /* parsed JSON */ });
|
|
1065
|
+
|
|
1066
|
+
// Node.js
|
|
1067
|
+
bw.loadClientFile('config.json', function(data) { /* ... */ });
|
|
1106
1068
|
```
|
|
1107
1069
|
|
|
1108
|
-
Unquoted keys, single quotes, trailing commas — all accepted. Especially useful for embedded systems where producing strict JSON is awkward, and for bwserve protocol messages from simple scripts.
|
|
1109
1070
|
|
|
1110
|
-
|
|
1071
|
+
## 10. What Bitwrench Does Instead
|
|
1111
1072
|
|
|
1112
|
-
|
|
1073
|
+
| You might expect | What bitwrench does |
|
|
1074
|
+
|------------------|---------------------|
|
|
1075
|
+
| Virtual DOM + diffing | Components update their own DOM directly via handles and cached slots, without a virtual-tree walk or full component rebuild |
|
|
1076
|
+
| CSS purging / tree-shaking | `bw.css()` generates only the rules you write -- nothing unused exists to purge |
|
|
1077
|
+
| SSR hydration | `bw.html()` renders TACOs to strings in Node; `bw.mount()` renders to DOM in the browser -- same input, two outputs |
|
|
1078
|
+
| Build step / bundler | Load via `<script>` tag, CDN, or ESM `import` -- works without tooling, benefits from it optionally |
|
|
1079
|
+
| Automatic state tracking | Explicit updates via methods, `bw.refresh()`, and pub/sub -- all wiring is visible in source |
|
|
1080
|
+
| TypeScript required | Ships `dist/bitwrench.d.ts` for full type checking -- supported, never required. See [TypeScript Usage Guide](bitwrench_typescript_usage.md) |
|
|
1113
1081
|
|
|
1114
|
-
### Static page composition
|
|
1115
1082
|
|
|
1116
|
-
|
|
1117
|
-
bw.loadStyles();
|
|
1118
|
-
bw.loadStyles({ primary: '#336699', secondary: '#cc6633' });
|
|
1083
|
+
## 11. Server-Driven UI and CLI
|
|
1119
1084
|
|
|
1120
|
-
|
|
1121
|
-
bw.makeNavbar({ brand: 'Acme', items: [
|
|
1122
|
-
{ text: 'Home', href: '#' }, { text: 'About', href: '#about' }
|
|
1123
|
-
]}),
|
|
1124
|
-
makeHeroSection(data.hero),
|
|
1125
|
-
makeFeatureGrid(data.features),
|
|
1126
|
-
bw.makeTable({ data: data.pricing, sortable: true }),
|
|
1127
|
-
makeFooter()
|
|
1128
|
-
]);
|
|
1129
|
-
```
|
|
1085
|
+
Everything in Sections 1-10 runs in the browser. But a `{taco}` without function values in `o:` is pure data -- it serializes to JSON. This means any program, in any language, can generate `{taco}` objects and send them to a browser for rendering. Bitwrench ships two tools that build on this property.
|
|
1130
1086
|
|
|
1131
|
-
###
|
|
1087
|
+
### bwserve -- server-driven UI over SSE
|
|
1132
1088
|
|
|
1133
|
-
|
|
1134
|
-
var allItems = [/* ... */];
|
|
1135
|
-
var filter = 'all';
|
|
1089
|
+
bwserve is a protocol that turns the browser into a display and input surface for a program running elsewhere. The server pushes `{taco}` objects and patches over Server-Sent Events; user interactions come back as named actions.
|
|
1136
1090
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
})
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
]};
|
|
1146
|
-
})});
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
bw.DOM('#filters', { t: 'div', c: ['all', 'widget', 'gadget'].map(function(f) {
|
|
1150
|
-
return bw.makeButton({
|
|
1151
|
-
text: f,
|
|
1152
|
-
variant: filter === f ? 'primary' : 'outline-secondary',
|
|
1153
|
-
onclick: function() { filter = f; renderList(); }
|
|
1154
|
-
});
|
|
1155
|
-
})});
|
|
1156
|
-
renderList();
|
|
1091
|
+
```
|
|
1092
|
+
Server (any language) Browser
|
|
1093
|
+
| |
|
|
1094
|
+
|-- SSE: {replace, #app, taco} --> bw.apply() --> DOM update
|
|
1095
|
+
|-- SSE: {patch, #counter, "42"} -> targeted text update
|
|
1096
|
+
|-- SSE: {append, #log, taco} ---> new child added
|
|
1097
|
+
| |
|
|
1098
|
+
|<-- POST: {action: "click"} ---+ user clicks bw_act_*
|
|
1157
1099
|
```
|
|
1158
1100
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
### Reactive component with state
|
|
1101
|
+
A minimal server in Node.js:
|
|
1162
1102
|
|
|
1163
1103
|
```js
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
var s = el._bw_state;
|
|
1170
|
-
bw.DOM(el, {
|
|
1104
|
+
import bwserve from 'bitwrench/bwserve';
|
|
1105
|
+
|
|
1106
|
+
var app = bwserve.create({ port: 8080 });
|
|
1107
|
+
app.page('/', function(client) {
|
|
1108
|
+
client.mount('#app', {
|
|
1171
1109
|
t: 'div', c: [
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
bw.makeFormGroup({ label: 'Message', input: bw.makeTextarea({ id: 'msg', rows: 4 }) }),
|
|
1176
|
-
bw.makeButton({ text: 'Send', type: 'submit', variant: 'primary' })
|
|
1177
|
-
], onsubmit: function(e) {
|
|
1178
|
-
e.preventDefault();
|
|
1179
|
-
s.statusMsg = 'Sent!';
|
|
1180
|
-
s.showStatus = true;
|
|
1181
|
-
bw.update(el);
|
|
1182
|
-
}})
|
|
1110
|
+
{ t: 'h1', c: 'Hello from the server' },
|
|
1111
|
+
{ t: 'p', a: { id: 'status' }, c: 'Connected.' },
|
|
1112
|
+
{ t: 'button', a: { class: 'bw_act_greet' }, c: 'Say hello' }
|
|
1183
1113
|
]
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1114
|
+
});
|
|
1187
1115
|
});
|
|
1116
|
+
app.listen();
|
|
1188
1117
|
```
|
|
1189
1118
|
|
|
1190
|
-
|
|
1119
|
+
The browser receives one HTML shell page with bitwrench loaded. Everything after that arrives as JSON messages over SSE.
|
|
1191
1120
|
|
|
1192
|
-
|
|
1193
|
-
var cartBadge = {
|
|
1194
|
-
t: 'span',
|
|
1195
|
-
o: {
|
|
1196
|
-
state: { count: 0 },
|
|
1197
|
-
mounted: function(el) {
|
|
1198
|
-
bw.sub('cart:updated', function(d) {
|
|
1199
|
-
el._bw_state.count = d.count;
|
|
1200
|
-
bw.update(el);
|
|
1201
|
-
}, el);
|
|
1202
|
-
},
|
|
1203
|
-
render: function(el) {
|
|
1204
|
-
bw.DOM(el, { t: 'span', c: 'Cart (' + el._bw_state.count + ')' });
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
};
|
|
1121
|
+
### Incremental updates
|
|
1208
1122
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1123
|
+
```js
|
|
1124
|
+
client.patch('#status', { text: 'Processing...' });
|
|
1125
|
+
client.append('#log', { t: 'div', c: 'Event at ' + new Date().toISOString() });
|
|
1126
|
+
client.remove('#old-notification');
|
|
1127
|
+
client.batch([
|
|
1128
|
+
{ type: 'patch', ref: '#status', text: 'Done.', v: 1 },
|
|
1129
|
+
{ type: 'remove', ref: '#spinner', v: 1 }
|
|
1130
|
+
]);
|
|
1213
1131
|
```
|
|
1214
1132
|
|
|
1215
|
-
###
|
|
1133
|
+
### Client events with bw_act_*
|
|
1134
|
+
|
|
1135
|
+
In Sections 6-7, every event handler was a local JavaScript function (`onclick: function() { ... }`). In server-driven mode, there is a second path: the click is forwarded to the server instead. Add a `bw_act_*` CSS class to any element, and when the user clicks it, the browser POSTs the action name to the server. No `onclick` handler is needed on the client:
|
|
1216
1136
|
|
|
1217
1137
|
```js
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
var accent = theme.palette.secondary.base;
|
|
1221
|
-
var accentLight = theme.palette.secondary.light;
|
|
1138
|
+
// Client-side {taco} (no onclick handler needed)
|
|
1139
|
+
{ t: 'button', a: { class: 'bw_act_greet' }, c: 'Say hello' }
|
|
1222
1140
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
},
|
|
1228
|
-
'.hero h1': { fontSize: 'clamp(2rem, 5vw, 3.5rem)' }
|
|
1229
|
-
}));
|
|
1141
|
+
// Server-side handler
|
|
1142
|
+
client.on('greet', function(data) {
|
|
1143
|
+
client.patch('#status', { text: 'Hello, user!' });
|
|
1144
|
+
});
|
|
1230
1145
|
```
|
|
1231
1146
|
|
|
1232
|
-
|
|
1147
|
+
In a fully server-driven application, domain logic can remain on the server. The browser renders messages and relays named user actions back. Local handlers and server actions can coexist in the same page; the `{taco}` shape is the same either way.
|
|
1233
1148
|
|
|
1234
|
-
|
|
1235
|
-
// Add a toast container to your page layout (once)
|
|
1236
|
-
bw.DOM('#app', [
|
|
1237
|
-
{ t: 'div', a: { id: 'toast-container',
|
|
1238
|
-
style: 'position:fixed;top:1rem;right:1rem;z-index:9999' } },
|
|
1239
|
-
// ... rest of your page
|
|
1240
|
-
]);
|
|
1149
|
+
### bwcli -- file conversion and pipe server
|
|
1241
1150
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
if (el) { bw.cleanup(el); el.remove(); }
|
|
1255
|
-
}, 3500);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
showToast('Item added to cart', 'success');
|
|
1151
|
+
`bwcli` is a command-line tool that converts files to styled standalone HTML pages and acts as a bridge between any language and the bwserve protocol.
|
|
1152
|
+
|
|
1153
|
+
```bash
|
|
1154
|
+
# Convert Markdown to a self-contained HTML page
|
|
1155
|
+
bwcli README.md -o index.html --standalone
|
|
1156
|
+
|
|
1157
|
+
# Apply a theme preset
|
|
1158
|
+
bwcli doc.md -o doc.html --standalone --theme ocean
|
|
1159
|
+
|
|
1160
|
+
# Pipe server -- any language becomes a bwserve backend
|
|
1161
|
+
bwcli serve --port 8080 --input-port 9000
|
|
1162
|
+
curl -X POST http://localhost:9000 -d '{"type":"patch","ref":"temp","content":"23.5 C"}'
|
|
1259
1163
|
```
|
|
1260
1164
|
|
|
1261
|
-
|
|
1165
|
+
With `bwcli serve`, a Python script, a shell loop, or a C program on a microcontroller can push UI updates to connected browsers by POSTing JSON to the pipe server's input port. No JavaScript on the server side at all.
|
|
1262
1166
|
|
|
1263
|
-
###
|
|
1167
|
+
### When to use bwserve
|
|
1264
1168
|
|
|
1265
|
-
|
|
1169
|
+
- **Language-agnostic**: any server that writes SSE can drive the UI -- Python, Go, Rust, C, shell scripts.
|
|
1170
|
+
- **LLM-native**: an AI emits `{taco}` JSON directly -- potentially more compact and easier to validate than generated HTML or JSX.
|
|
1171
|
+
- **Embedded**: an ESP32 serves one HTML page with bitwrench, then pushes sensor data as patches. C macros ship in `embedded_c/`.
|
|
1172
|
+
- **Streamlit/Gradio-style applications**: the same broad server-driven pattern, but language-neutral and based on the full TACO composition model.
|
|
1266
1173
|
|
|
1267
|
-
|
|
1268
|
-
var theme = bw.makeStyles({ primary: '#1e40af', secondary: '#059669' });
|
|
1269
|
-
bw.applyStyles(theme);
|
|
1270
|
-
var P = theme.palette;
|
|
1174
|
+
See the [bwserve docs](bwserve.md) for the full protocol, the [CLI docs](cli.md) for all flags and options, and the [ESP32 tutorial](tutorial-embedded.md) for a complete embedded walkthrough.
|
|
1271
1175
|
|
|
1272
|
-
// Responsive grid — base stacks, md goes 2-col, lg goes 4-col
|
|
1273
|
-
bw.injectCSS(bw.css({
|
|
1274
|
-
'.stat-grid': { display: 'grid', gap: '1rem', marginBottom: '1.5rem' }
|
|
1275
|
-
}));
|
|
1276
|
-
bw.injectCSS(bw.responsive('.stat-grid', {
|
|
1277
|
-
base: { gridTemplateColumns: '1fr' },
|
|
1278
|
-
md: { gridTemplateColumns: 'repeat(2, 1fr)' },
|
|
1279
|
-
lg: { gridTemplateColumns: 'repeat(4, 1fr)' }
|
|
1280
|
-
}));
|
|
1281
1176
|
|
|
1282
|
-
|
|
1283
|
-
var metrics = { users: 2847, revenue: 48920, orders: 384, rate: 3.2 };
|
|
1177
|
+
## 12. Quick Reference
|
|
1284
1178
|
|
|
1285
|
-
|
|
1286
|
-
bw.DOM('#stats', { t: 'div', a: { class: 'stat-grid' }, c: [
|
|
1287
|
-
bw.makeStatCard({ value: metrics.users.toLocaleString(), label: 'Users', variant: 'primary' }),
|
|
1288
|
-
bw.makeStatCard({ value: '$' + metrics.revenue.toLocaleString(), label: 'Revenue', variant: 'success' }),
|
|
1289
|
-
bw.makeStatCard({ value: metrics.orders.toString(), label: 'Orders', variant: 'info' }),
|
|
1290
|
-
bw.makeStatCard({ value: metrics.rate + '%', label: 'Conversion' })
|
|
1291
|
-
]});
|
|
1292
|
-
}
|
|
1179
|
+
The tutorial ends here. The following tables are a compact lookup reference for the APIs introduced above.
|
|
1293
1180
|
|
|
1294
|
-
|
|
1295
|
-
bw.DOM('#app', { t: 'div', c: [
|
|
1296
|
-
{ t: 'div', a: { style: bw.s({ display: 'flex' }, { justifyContent: 'space-between' },
|
|
1297
|
-
{ alignItems: 'center' }, { background: P.primary.base, color: '#fff', padding: '1.5rem 2rem' }) },
|
|
1298
|
-
c: [
|
|
1299
|
-
{ t: 'h1', a: { style: bw.s({ margin: '0', fontSize: '1.5rem' }) }, c: 'Dashboard' },
|
|
1300
|
-
{ t: 'span', a: { style: bw.s({ opacity: '0.8', fontSize: '0.85rem' }) }, c: 'Live' }
|
|
1301
|
-
]
|
|
1302
|
-
},
|
|
1303
|
-
{ t: 'div', a: { id: 'stats', style: bw.s({ padding: '1rem' }, { maxWidth: '1200px', margin: '0 auto' }) } }
|
|
1304
|
-
]});
|
|
1181
|
+
### The lifecycle
|
|
1305
1182
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
metrics.users += Math.round(Math.random() * 20 - 5);
|
|
1309
|
-
metrics.revenue += Math.round(Math.random() * 500 - 100);
|
|
1310
|
-
renderStats();
|
|
1311
|
-
}, 3000);
|
|
1183
|
+
```
|
|
1184
|
+
define -> create -> hydrate -> mount -> update -> unmount
|
|
1312
1185
|
```
|
|
1313
1186
|
|
|
1314
|
-
|
|
1315
|
-
- **No hardcoded hex in layout** -- colors come from `theme.palette`
|
|
1316
|
-
- **No inline style strings** — `bw.s({ display: 'flex' }, { padding: '1rem' }, ...)` composes style objects
|
|
1317
|
-
- **Responsive without media queries in HTML** — `bw.responsive()` generates `@media` CSS
|
|
1318
|
-
- **Re-render is just calling `bw.DOM()` again** — Level 1, no framework magic
|
|
1187
|
+
Each phase has one verb. In practice, create+hydrate are fused for efficiency. Convenience verbs (`bw.mount`, `bw.append`, `bw.replace`, `bw.remove`, `bw.refresh`) compose phase verbs -- they never reimplement them.
|
|
1319
1188
|
|
|
1320
|
-
|
|
1189
|
+
### Core rendering
|
|
1321
1190
|
|
|
1322
|
-
|
|
1191
|
+
| Function | What it does |
|
|
1192
|
+
|----------|-------------|
|
|
1193
|
+
| `bw.html(taco)` | `{taco}` to HTML string |
|
|
1194
|
+
| `bw.create(taco)` | `{taco}` to detached, hydrated DOM element |
|
|
1195
|
+
| `bw.mount(sel, taco)` | Mount `{taco}` into existing element; returns root element |
|
|
1196
|
+
| `bw.DOM(sel, taco)` | Alias for `bw.mount()` |
|
|
1197
|
+
| `bw.el(sel, apply)` | Find element by selector; optionally apply content/function |
|
|
1198
|
+
| `bw.h(tag, attrs, c, o)` | `{taco}` constructor from positional args |
|
|
1199
|
+
| `bw.raw(str)` | Mark string as pre-escaped HTML |
|
|
1323
1200
|
|
|
1324
|
-
|
|
1325
|
-
|---------|---------|-------------------|
|
|
1326
|
-
| TypeScript types | Ships `dist/bitwrench.d.ts` with full type declarations | See [TypeScript Usage Guide](bitwrench_typescript_usage.md) |
|
|
1327
|
-
| Virtual DOM | Targeted patches via UUID refs are sufficient | `bw.patch()`, `o.render` + `bw.update()` |
|
|
1328
|
-
| CSS purging | You generate only what you use via `bw.css()` | N/A |
|
|
1329
|
-
| SSR hydration | `bw.html()` for SSR, `bw.DOM()` for client | Full page render via `bw.html()` in Node |
|
|
1330
|
-
| Module bundling | No build step required | `<script>` tag, CDN, or ESM `import` |
|
|
1201
|
+
### Lifecycle verbs (atomic)
|
|
1331
1202
|
|
|
1332
|
-
|
|
1203
|
+
These are low-level lifecycle primitives. Most applications only need `bw.mount()`, `bw.refresh()`, and `bw.unmount()`.
|
|
1333
1204
|
|
|
1334
|
-
|
|
1205
|
+
| Function | What it does |
|
|
1206
|
+
|----------|-------------|
|
|
1207
|
+
| `bw.create(taco)` | Build hydrated, detached DOM from `{taco}` |
|
|
1208
|
+
| `bw.hydrate(el, taco)` | Wire lifecycle from `{taco}` onto existing DOM node |
|
|
1209
|
+
| `bw.mountTree(el)` | Register inserted subtree; fire `mounted` hooks |
|
|
1210
|
+
| `bw.unmount(el)` | Tear down subtree lifecycle; fire `unmount` hooks |
|
|
1211
|
+
| `bw.unmountChildren(el)` | Unmount descendants only; element untouched |
|
|
1212
|
+
| `bw.detach(el)` | Remove from document, keep lifecycle intact (keep-alive) |
|
|
1335
1213
|
|
|
1336
|
-
###
|
|
1214
|
+
### Convenience verbs (compound)
|
|
1337
1215
|
|
|
1338
1216
|
| Function | What it does |
|
|
1339
1217
|
|----------|-------------|
|
|
1340
|
-
| `bw.
|
|
1341
|
-
| `bw.
|
|
1342
|
-
| `bw.
|
|
1343
|
-
| `bw.
|
|
1344
|
-
| `bw.
|
|
1218
|
+
| `bw.mount(sel, taco)` | = `unmountChildren` + `create` + insert + `mountTree` |
|
|
1219
|
+
| `bw.append(target, taco)` | Add child without touching existing; returns new element |
|
|
1220
|
+
| `bw.replace(ref, taco)` | Swap element at DOM position; returns new element |
|
|
1221
|
+
| `bw.remove(ref)` | = `unmount` + `el.remove()` |
|
|
1222
|
+
| `bw.refresh(ref)` | = `unmountChildren` + re-render + `mountTree` (the heavy path) |
|
|
1345
1223
|
|
|
1346
1224
|
### CSS
|
|
1347
1225
|
|
|
1348
1226
|
| Function | What it does |
|
|
1349
1227
|
|----------|-------------|
|
|
1350
|
-
| `bw.css(rules)` | JS object to CSS string (supports `@media`, `@keyframes`
|
|
1228
|
+
| `bw.css(rules)` | JS object to CSS string (supports `@media`, `@keyframes`) |
|
|
1351
1229
|
| `bw.injectCSS(css)` | Insert CSS string into document |
|
|
1352
1230
|
| `bw.s(...styles)` | Merge style objects into a style string |
|
|
1353
1231
|
| `bw.responsive(sel, bp)` | Generate responsive `@media` CSS from breakpoint object |
|
|
1354
|
-
| `bw.loadStyles()` |
|
|
1355
|
-
| `bw.makeStyles(cfg)` | Generate
|
|
1356
|
-
| `bw.applyStyles(styles)` | Inject generated styles
|
|
1357
|
-
| `bw.
|
|
1358
|
-
| `bw.toggleStyles()` | Switch between primary and alternate palettes |
|
|
1232
|
+
| `bw.loadStyles(cfg?)` | Generate + apply styles (no args = structural CSS only) |
|
|
1233
|
+
| `bw.makeStyles(cfg)` | Generate styles from seed colors (returns styles object) |
|
|
1234
|
+
| `bw.applyStyles(styles)` | Inject generated styles into document |
|
|
1235
|
+
| `bw.toggleThemeMode(scope?)` | Switch between primary and alternate palettes |
|
|
1359
1236
|
| `bw.clearStyles()` | Remove injected styles |
|
|
1360
1237
|
|
|
1361
|
-
### State
|
|
1238
|
+
### State and updates
|
|
1362
1239
|
|
|
1363
1240
|
| Function | What it does |
|
|
1364
1241
|
|----------|-------------|
|
|
1365
|
-
| `o.state` | Initial state object (
|
|
1366
|
-
| `o.render(el, state)` | Render function called on mount and `bw.
|
|
1367
|
-
| `bw.
|
|
1368
|
-
| `bw.
|
|
1369
|
-
| `bw.
|
|
1242
|
+
| `o.state` | Initial state object (assigned by reference to `el._bw_state`) |
|
|
1243
|
+
| `o.render(el, state)` | Render function; called on mount and `bw.refresh()` |
|
|
1244
|
+
| `bw.refresh(ref)` | Re-invoke render -- tears down and rebuilds children |
|
|
1245
|
+
| `bw.update(ref, data)` | Dispatch to `el.bw.update(data)` -- never falls back to rebuild |
|
|
1246
|
+
| `bw.patch(id, content)` | Update addressed element's content |
|
|
1370
1247
|
|
|
1371
|
-
### Component
|
|
1248
|
+
### Component handles
|
|
1372
1249
|
|
|
1373
1250
|
| Function | What it does |
|
|
1374
1251
|
|----------|-------------|
|
|
1375
|
-
| `o.handle` | Object of methods attached to `el.bw`
|
|
1376
|
-
| `o.slots` | `{name: '.selector'}` -- auto-generates `el.bw.setName()
|
|
1377
|
-
| `bw.mount(sel, taco)` |
|
|
1378
|
-
| `bw.message(target, action, data)` | Dispatch to `el.bw[action](data)` by
|
|
1252
|
+
| `o.handle` | Object of methods attached to `el.bw` at create time |
|
|
1253
|
+
| `o.slots` | `{name: '.selector'}` -- auto-generates `el.bw.setName()`/`getName()` |
|
|
1254
|
+
| `bw.mount(sel, taco)` | Returns root element for `el.bw` access |
|
|
1255
|
+
| `bw.message(target, action, data)` | Dispatch to `el.bw[action](data)` by selector |
|
|
1379
1256
|
|
|
1380
1257
|
### Communication
|
|
1381
1258
|
|
|
1382
1259
|
| Function | What it does |
|
|
1383
1260
|
|----------|-------------|
|
|
1384
|
-
| `bw.pub(topic, data)` | Publish to all subscribers
|
|
1385
|
-
| `bw.sub(topic, fn)` | Subscribe (
|
|
1386
|
-
| `bw.
|
|
1387
|
-
| `bw.
|
|
1261
|
+
| `bw.pub(topic, data)` | Publish to all subscribers |
|
|
1262
|
+
| `bw.sub(topic, fn, owner?)` | Subscribe (with optional auto-cleanup on unmount) |
|
|
1263
|
+
| `bw.once(topic, fn, el?)` | One-shot subscribe |
|
|
1264
|
+
| `bw.derive(inputs, fn, output)` | Declared dataflow between topics |
|
|
1388
1265
|
|
|
1389
1266
|
### Routing
|
|
1390
1267
|
|
|
1391
1268
|
| Function | What it does |
|
|
1392
1269
|
|----------|-------------|
|
|
1393
1270
|
| `bw.router(config)` | Create and start a client-side router |
|
|
1394
|
-
| `bw.navigate(path, opts)` | Programmatic navigation
|
|
1395
|
-
| `bw.link(path, content, attrs)` | Returns
|
|
1271
|
+
| `bw.navigate(path, opts)` | Programmatic navigation |
|
|
1272
|
+
| `bw.link(path, content, attrs)` | Returns `{taco}` `<a>` with navigation wired |
|
|
1396
1273
|
|
|
1397
1274
|
### Color
|
|
1398
1275
|
|
|
1399
1276
|
| Function | What it does |
|
|
1400
1277
|
|----------|-------------|
|
|
1401
|
-
| `bw.hexToHsl(hex)` | Hex to [h, s, l] |
|
|
1402
|
-
| `bw.hslToHex(hsl)` | [h, s, l] to hex |
|
|
1278
|
+
| `bw.hexToHsl(hex)` | Hex to `[h, s, l]` |
|
|
1279
|
+
| `bw.hslToHex(hsl)` | `[h, s, l]` to hex |
|
|
1403
1280
|
| `bw.deriveShades(hex)` | 8 shade variants from one color |
|
|
1404
1281
|
| `bw.derivePalette(cfg)` | Full palette from seed colors |
|
|
1405
1282
|
| `bw.textOnColor(hex)` | Contrast-safe text color |
|
|
@@ -1410,49 +1287,47 @@ Key things this example proves:
|
|
|
1410
1287
|
| Function | What it does |
|
|
1411
1288
|
|----------|-------------|
|
|
1412
1289
|
| `bw.$('selector')` | querySelectorAll as array |
|
|
1413
|
-
| `bw.
|
|
1290
|
+
| `bw.el(sel, apply?)` | Find + optionally apply content/function |
|
|
1414
1291
|
| `bw.escapeHTML(str)` | Escape HTML special chars |
|
|
1415
1292
|
| `bw.uuid(prefix)` | Generate unique ID |
|
|
1416
1293
|
| `bw.typeOf(x)` | Enhanced typeof |
|
|
1417
1294
|
| `bw.getURLParam(key, def)` | Read URL query parameter |
|
|
1418
|
-
| `bw.random(min, max)` | Random integer (or array) |
|
|
1419
1295
|
| `bw.loremIpsum(n)` | Placeholder text |
|
|
1420
1296
|
| `bw.mapScale(x, i0, i1, o0, o1)` | Map value between ranges |
|
|
1421
|
-
| `bw.
|
|
1297
|
+
| `bw.parseJSONFlex(str)` | Parse flexible JSON (unquoted keys, single quotes, r-prefix) |
|
|
1422
1298
|
| `bw.saveClientFile(name, data)` | Browser file download |
|
|
1423
1299
|
| `bw.loadClientJSON(cb)` | Browser file upload (JSON) |
|
|
1424
1300
|
|
|
1425
|
-
---
|
|
1426
1301
|
|
|
1427
1302
|
## Appendix: Framework Translation Table
|
|
1428
1303
|
|
|
1429
|
-
How common UI operations map across frameworks
|
|
1430
|
-
|
|
1431
|
-
| Operation |
|
|
1432
|
-
|
|
1433
|
-
|
|
|
1434
|
-
|
|
|
1435
|
-
|
|
|
1436
|
-
|
|
|
1437
|
-
|
|
|
1438
|
-
|
|
|
1439
|
-
|
|
|
1440
|
-
|
|
|
1441
|
-
|
|
|
1442
|
-
|
|
|
1443
|
-
|
|
|
1444
|
-
|
|
|
1445
|
-
|
|
|
1446
|
-
|
|
|
1447
|
-
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
*Bitwrench is maintained by [
|
|
1304
|
+
How common UI operations map across frameworks:
|
|
1305
|
+
|
|
1306
|
+
| Operation | React | Vue 3 | Bitwrench |
|
|
1307
|
+
|-----------|-------|-------|-----------|
|
|
1308
|
+
| Render element | `<div className="card">Hi</div>` | `<div class="card">Hi</div>` | `bw.mount('#x', {t:'div', a:{class:'card'}, c:'Hi'})` |
|
|
1309
|
+
| Update text | `setText('new')` via `useState` | `msg.value = 'new'` | `el.bw.setContent('new')` or `bw.patch(id, 'new')` |
|
|
1310
|
+
| Conditional | `{show && <Comp/>}` | `v-if="show"` | `show ? taco : null` in `c:` array |
|
|
1311
|
+
| List render | `{items.map(i => <Li key={i.id}/>)}` | `v-for="i in items"` | `c: items.map(function(i) { return {t:'li', c:i.name} })` |
|
|
1312
|
+
| Event handler | `onClick={handler}` | `@click="handler"` | `a: { onclick: fn }` |
|
|
1313
|
+
| Declare state | `const [x, setX] = useState(0)` | `const x = ref(0)` | `o: { state: { x: 0 } }` |
|
|
1314
|
+
| Update state | `setX(42)` | `x.value = 42` | `el._bw_state.x = 42; bw.refresh(el)` |
|
|
1315
|
+
| Side effect | `useEffect(() => {...}, [])` | `onMounted(() => {...})` | `o: { mounted: function(el) {...} }` |
|
|
1316
|
+
| Cleanup | `useEffect return cleanup` | `onUnmounted(() => {...})` | `o: { unmount: fn }` |
|
|
1317
|
+
| Inline style | `style={{color: 'red'}}` | `:style="{color: 'red'}"` | `a: { style: bw.s({color:'red'}) }` |
|
|
1318
|
+
| Generate CSS | styled-components / emotion | `<style scoped>` | `bw.injectCSS(bw.css({'.card': {padding:'1rem'}}))` |
|
|
1319
|
+
| Raw HTML | `dangerouslySetInnerHTML` | `v-html="str"` | `bw.raw(str)` in `c:` |
|
|
1320
|
+
| Cross-component | Context / Zustand | provide/inject / Pinia | `bw.pub(topic, data)` / `bw.sub(topic, fn)` |
|
|
1321
|
+
| Theme tokens | ThemeProvider / CSS vars | CSS vars / provide | `bw.makeStyles(cfg)` => `theme.palette` |
|
|
1322
|
+
| Build step? | Yes (Babel/Vite) | Yes (Vite/CLI) | **No** -- open the HTML file |
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
## Postscript: Validation
|
|
1326
|
+
|
|
1327
|
+
A `{taco}` is a plain JavaScript object with a known shape: `t` is a string, `a` is an object of attributes, `c` is a string, array, or nested `{taco}`, and `o` is an object with specific keys (`state`, `render`, `handle`, `slots`, `mounted`, `unmount`). That shape is simple enough to describe as a JSON Schema, a TypeScript interface, or a validation function.
|
|
1328
|
+
|
|
1329
|
+
This means you can validate `{taco}` objects before they reach the DOM. A server can check that wire-format TACOs conform to an allowed subset before sending them to the client. A test suite can assert that a component factory returns well-formed output. An LLM generating UI can have its output validated against the schema before it is rendered. A content management system can enforce that editors produce valid `{taco}` structures.
|
|
1330
|
+
|
|
1331
|
+
Bitwrench does not ship a schema or enforce one at runtime -- the library is permissive by design, and validation has a cost. But the fact that the entire UI description is a plain object with a documented shape means validation is always available as an option. This is a property that template strings, JSX, and HTML do not have without a parser.
|
|
1332
|
+
|
|
1333
|
+
*Bitwrench is maintained by [deftio](https://github.com/deftio). BSD-2-Clause license.*
|