bitwrench 2.0.22 → 2.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +1 -1
- package/README.md +4 -3
- package/bin/bwmcp.js +3 -0
- package/dist/bitwrench-bccl.cjs.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
- package/dist/bitwrench-bccl.esm.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
- package/dist/bitwrench-bccl.umd.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.cjs.min.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- 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 +3 -3
- package/dist/bitwrench-lean.cjs.min.js +2 -2
- package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
- package/dist/bitwrench-lean.es5.js +3 -3
- package/dist/bitwrench-lean.es5.min.js +2 -2
- package/dist/bitwrench-lean.es5.min.js.gz +0 -0
- package/dist/bitwrench-lean.esm.js +3 -3
- package/dist/bitwrench-lean.esm.min.js +2 -2
- package/dist/bitwrench-lean.esm.min.js.gz +0 -0
- package/dist/bitwrench-lean.umd.js +3 -3
- package/dist/bitwrench-lean.umd.min.js +2 -2
- package/dist/bitwrench-lean.umd.min.js.gz +0 -0
- package/dist/bitwrench-util-css.cjs.js +1 -1
- package/dist/bitwrench-util-css.cjs.min.js +1 -1
- package/dist/bitwrench-util-css.es5.js +1 -1
- package/dist/bitwrench-util-css.es5.min.js +1 -1
- package/dist/bitwrench-util-css.esm.js +1 -1
- package/dist/bitwrench-util-css.esm.min.js +1 -1
- package/dist/bitwrench-util-css.umd.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
- package/dist/bitwrench.cjs.js +3 -3
- package/dist/bitwrench.cjs.min.js +2 -2
- package/dist/bitwrench.cjs.min.js.gz +0 -0
- package/dist/bitwrench.css +1 -1
- package/dist/bitwrench.es5.js +3 -3
- package/dist/bitwrench.es5.min.js +2 -2
- package/dist/bitwrench.es5.min.js.gz +0 -0
- package/dist/bitwrench.esm.js +3 -3
- package/dist/bitwrench.esm.min.js +2 -2
- package/dist/bitwrench.esm.min.js.gz +0 -0
- package/dist/bitwrench.umd.js +3 -3
- package/dist/bitwrench.umd.min.js +2 -2
- package/dist/bitwrench.umd.min.js.gz +0 -0
- package/dist/builds.json +57 -57
- package/dist/bwserve.cjs.js +2 -2
- package/dist/bwserve.esm.js +2 -2
- package/dist/sri.json +45 -45
- package/docs/README.md +76 -0
- package/docs/app-patterns.md +264 -0
- package/docs/bitwrench-mcp.md +426 -0
- package/docs/bitwrench_api.md +2232 -0
- package/docs/bw-attach.md +399 -0
- package/docs/bwserve.md +841 -0
- package/docs/cli.md +307 -0
- package/docs/component-cheatsheet.md +144 -0
- package/docs/component-library.md +1099 -0
- package/docs/framework-translation-table.md +33 -0
- package/docs/llm-bitwrench-guide.md +672 -0
- package/docs/routing.md +562 -0
- package/docs/state-management.md +767 -0
- package/docs/taco-format.md +373 -0
- package/docs/theming.md +309 -0
- package/docs/thinking-in-bitwrench.md +1457 -0
- package/docs/tutorial-bwserve.md +297 -0
- package/docs/tutorial-embedded.md +314 -0
- package/docs/tutorial-website.md +255 -0
- package/package.json +11 -3
- package/readme.html +5 -4
- package/src/mcp/knowledge.js +231 -0
- package/src/mcp/live.js +226 -0
- package/src/mcp/server.js +216 -0
- package/src/mcp/tools.js +369 -0
- package/src/mcp/transport.js +55 -0
- package/src/version.js +3 -3
|
@@ -0,0 +1,1457 @@
|
|
|
1
|
+
# Thinking in Bitwrench
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
0. [The Problem and the Idea](#0-the-problem-and-the-idea)
|
|
6
|
+
1. [TACO: the Shape of a UI Element](#1-taco-the-shape-of-a-ui-element)
|
|
7
|
+
2. [Styling -- CSS Is Just Strings](#2-styling--css-is-just-strings)
|
|
8
|
+
3. [It's Just JavaScript -- the Core Insight](#3-its-just-javascript--the-core-insight)
|
|
9
|
+
4. [The BCCL: Ready-Made Components](#4-the-bccl-ready-made-components)
|
|
10
|
+
5. [Three Levels of Commitment](#5-three-levels-of-commitment)
|
|
11
|
+
6. [Events and Communication](#6-events-and-communication)
|
|
12
|
+
7. [Server-Driven UI (bwserve)](#7-server-driven-ui-bwserve)
|
|
13
|
+
8. [Routing](#8-routing)
|
|
14
|
+
9. [Utilities and Color Functions](#9-utilities-and-color-functions)
|
|
15
|
+
10. [Putting It All Together -- Patterns](#10-putting-it-all-together--patterns)
|
|
16
|
+
11. [What Bitwrench Doesn't Do](#11-what-bitwrench-doesnt-do)
|
|
17
|
+
12. [Quick Reference](#12-quick-reference)
|
|
18
|
+
- [Framework Translation Table](#appendix-framework-translation-table)
|
|
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)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 0. The Problem and the Idea
|
|
25
|
+
|
|
26
|
+
Building web UIs with raw HTML, CSS, and JavaScript works — but it's 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
|
+
|
|
28
|
+
Different paradigms emerged to manage this complexity:
|
|
29
|
+
|
|
30
|
+
- **Markup generation**: JSX (React), templates (Vue, Svelte, Angular) — describe UI declaratively, let a compiler or runtime translate it to DOM operations.
|
|
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.
|
|
34
|
+
|
|
35
|
+
Each of these solves a real problem. But each also adds a layer — a new syntax, a new tool, a new abstraction to learn, configure, and maintain.
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
|
|
39
|
+
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's a JavaScript object, you get the full language at every point: variables, functions, loops, conditionals, composition. No special syntax. No compiler. No build step.
|
|
40
|
+
|
|
41
|
+
> **"If you know JavaScript, you already know bitwrench. Everything else is just learning the shape of the objects."**
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 1. TACO: the Shape of a UI Element
|
|
46
|
+
|
|
47
|
+
### From HTML to TACO
|
|
48
|
+
|
|
49
|
+
Every HTML element has a tag, attributes, and content. A TACO object mirrors this directly:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<!-- HTML -->
|
|
53
|
+
<div class="card" id="x">Hello world</div>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
// TACO — the same element as a JavaScript object
|
|
58
|
+
{ t: 'div', a: { class: 'card', id: 'x' }, c: 'Hello world' }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The mapping is direct: `t` is the tag name, `a` is an object of HTML attributes, `c` is the content. If we had a function to convert this object to real HTML, we could render it in the browser. That's exactly what bitwrench provides:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
var card = { t: 'div', a: { class: 'card', id: 'x' }, c: 'Hello world' };
|
|
65
|
+
|
|
66
|
+
bw.html(card); // → '<div class="card" id="x">Hello world</div>'
|
|
67
|
+
bw.createDOM(card); // → HTMLDivElement (a real DOM node, ready to insert)
|
|
68
|
+
bw.DOM('#target', card); // mount directly into an existing element on the page
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Three output modes from one input:
|
|
72
|
+
|
|
73
|
+
- `bw.html()` — returns an HTML string. Useful for server-side rendering, Node.js scripts, email templates.
|
|
74
|
+
- `bw.createDOM()` — returns a detached DOM element. Useful when you need to manipulate it before inserting.
|
|
75
|
+
- `bw.DOM()` — mounts the result into an existing page element. The most common use.
|
|
76
|
+
|
|
77
|
+
The TACO is data. The rendering is a separate step. You decide when and how it becomes real.
|
|
78
|
+
|
|
79
|
+
### Minimal cases
|
|
80
|
+
|
|
81
|
+
Every key is optional. These are all valid TACOs:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
{ t: 'br' } // self-closing tag
|
|
85
|
+
{ t: 'h1', c: 'Hello' } // tag + text content
|
|
86
|
+
{ t: 'input', a: { type: 'email', required: true } } // tag + attributes, no content
|
|
87
|
+
{ t: 'div' } // empty div
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
If `t` is omitted, it defaults to `'div'`.
|
|
91
|
+
|
|
92
|
+
### Raw HTML in content — bw.raw()
|
|
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.
|
|
95
|
+
|
|
96
|
+
When you need actual HTML inside a TACO (line breaks, inline formatting, HTML entities), use `bw.raw()`:
|
|
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:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
// A single child
|
|
116
|
+
{ t: 'div', c: { t: 'span', c: 'child' } }
|
|
117
|
+
|
|
118
|
+
// Multiple children
|
|
119
|
+
{ t: 'div', c: [
|
|
120
|
+
{ t: 'h2', c: 'Title' },
|
|
121
|
+
{ t: 'p', c: 'Body text' }
|
|
122
|
+
]}
|
|
123
|
+
|
|
124
|
+
// Three levels deep: page → section → card → button
|
|
125
|
+
{ t: 'div', a: { class: 'page' }, c: [
|
|
126
|
+
{ t: 'nav', c: [
|
|
127
|
+
{ t: 'a', a: { href: '/' }, c: 'Home' },
|
|
128
|
+
{ t: 'a', a: { href: '/about' }, c: 'About' }
|
|
129
|
+
]},
|
|
130
|
+
{ t: 'section', a: { class: 'content' }, c: [
|
|
131
|
+
{ t: 'div', a: { class: 'card' }, c: [
|
|
132
|
+
{ t: 'h3', c: 'Welcome' },
|
|
133
|
+
{ t: 'p', c: 'This is three levels deep.' },
|
|
134
|
+
{ t: 'button', c: 'Click me' }
|
|
135
|
+
]}
|
|
136
|
+
]}
|
|
137
|
+
]}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The HTML equivalent would be around a dozen lines of nested tags with closing tags to match. The TACO is the same structure, but as data you can store in a variable, pass to a function, or build from a loop.
|
|
141
|
+
|
|
142
|
+
### Skipping content — nulls and conditionals
|
|
143
|
+
|
|
144
|
+
`null`, `undefined`, and `false` in content arrays are silently skipped. This makes conditional rendering natural:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
var showHeader = true;
|
|
148
|
+
var isAdmin = false;
|
|
149
|
+
|
|
150
|
+
{ t: 'div', c: [
|
|
151
|
+
showHeader ? { t: 'h1', c: 'Dashboard' } : null,
|
|
152
|
+
{ t: 'p', c: 'Always visible' },
|
|
153
|
+
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
|
+
```
|
|
158
|
+
|
|
159
|
+
### The fourth key — `o:` options
|
|
160
|
+
|
|
161
|
+
The `o:` field is where non-HTML concerns live — lifecycle hooks, component state, rendering behavior. It was added to the format because bitwrench-specific metadata shouldn't go in `a:` (that compiles directly to HTML attributes), and the `data-*` attribute namespace was being used inconsistently across libraries. A separate `o:` key keeps the library's concerns cleanly separated from the DOM's, with zero risk of namespace collisions.
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
{
|
|
165
|
+
t: 'div',
|
|
166
|
+
a: { class: 'widget' }, // → becomes HTML attributes
|
|
167
|
+
c: 'Hello', // → becomes element content
|
|
168
|
+
o: { // → bitwrench-only, never in HTML output
|
|
169
|
+
mounted: function(el) { }, // called after element enters the DOM
|
|
170
|
+
unmount: function(el) { }, // called before element is removed
|
|
171
|
+
state: { count: 0 } // component state (used with o.render)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
We'll cover `o:` in detail in Section 5 (Three Levels of Commitment). For now, just know it's where non-DOM concerns live.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 2. Styling — CSS Is Just Strings
|
|
181
|
+
|
|
182
|
+
You've made a TACO and rendered it. The next question is: how do I style it?
|
|
183
|
+
|
|
184
|
+
Bitwrench doesn't care where your CSS comes from. You can use an external stylesheet, bitwrench's built-in classes, or generate CSS entirely from JavaScript. All three work together. But the JavaScript approach is where bitwrench's philosophy shines — because CSS values are just strings, and strings are something JavaScript handles naturally.
|
|
185
|
+
|
|
186
|
+
### Start simple — inline styles
|
|
187
|
+
|
|
188
|
+
The `style` attribute in a TACO works exactly like the HTML `style` attribute:
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
{ t: 'div',
|
|
192
|
+
a: { style: 'padding:1.5rem; background:#f5f5f5; border-radius:12px' },
|
|
193
|
+
c: 'A styled box'
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Nothing new here. But now put that style in a variable:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
var boxStyle = 'padding:1.5rem; background:#f5f5f5; border-radius:12px';
|
|
201
|
+
|
|
202
|
+
{ t: 'div', a: { style: boxStyle }, c: 'Box one' }
|
|
203
|
+
{ t: 'div', a: { style: boxStyle }, c: 'Box two' }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Change `boxStyle` once, both boxes update. No Sass variables. No CSS custom properties. Just a JavaScript variable.
|
|
207
|
+
|
|
208
|
+
### Shared styles across nested TACOs
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
var cardStyle = 'border:1px solid #ddd; border-radius:12px; overflow:hidden';
|
|
212
|
+
var headerStyle = 'padding:1rem; background:#336699; color:#fff';
|
|
213
|
+
var bodyStyle = 'padding:1.5rem';
|
|
214
|
+
|
|
215
|
+
{ t: 'div', a: { style: cardStyle }, c: [
|
|
216
|
+
{ t: 'div', a: { style: headerStyle }, c: 'Card Title' },
|
|
217
|
+
{ t: 'div', a: { style: bodyStyle }, c: 'Card content goes here.' }
|
|
218
|
+
]}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Three style variables, one card. Reuse `cardStyle` on every card across your page.
|
|
222
|
+
|
|
223
|
+
### Base style + overrides
|
|
224
|
+
|
|
225
|
+
For simple variations, string concatenation works:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
var base = 'border-radius:12px; padding:1rem; border:1px solid #ddd';
|
|
229
|
+
|
|
230
|
+
{ t: 'div', a: { style: base + '; background:#e8f5e9' }, c: 'Success' }
|
|
231
|
+
{ t: 'div', a: { style: base + '; background:#ffebee' }, c: 'Error' }
|
|
232
|
+
{ t: 'div', a: { style: base + '; background:#e3f2fd' }, c: 'Info' }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
For more complex composition, use objects with `Object.assign`:
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
var baseObj = { borderRadius: '12px', padding: '1rem', border: '1px solid #ddd' };
|
|
239
|
+
|
|
240
|
+
var success = Object.assign({}, baseObj, { background: '#e8f5e9' });
|
|
241
|
+
var error = Object.assign({}, baseObj, { background: '#ffebee' });
|
|
242
|
+
var info = Object.assign({}, baseObj, { background: '#e3f2fd' });
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
This is Sass `@extend` without Sass.
|
|
246
|
+
|
|
247
|
+
### CSS classes — `bw.css()` generates stylesheets from objects
|
|
248
|
+
|
|
249
|
+
When inline styles aren't enough — you need pseudo-classes, media queries, or want to reuse styles by class name — generate CSS from JavaScript objects:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
bw.injectCSS(bw.css({
|
|
253
|
+
'.card': {
|
|
254
|
+
padding: '1.5rem',
|
|
255
|
+
borderRadius: '12px',
|
|
256
|
+
border: '1px solid #ddd'
|
|
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
|
+
}));
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`bw.css()` takes a JavaScript object and returns a CSS string. `bw.injectCSS()` inserts it into the document. CamelCase properties (`borderRadius`) auto-convert to kebab-case (`border-radius`). Pseudo-classes (`:hover`, `:focus`, `:active`) and `@media` queries work as top-level keys.
|
|
268
|
+
|
|
269
|
+
### CSS variables are just JS variables
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
var brand = '#8B4513';
|
|
273
|
+
var radius = '12px';
|
|
274
|
+
var shadow = '0 4px 12px rgba(0,0,0,.08)';
|
|
275
|
+
|
|
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
|
+
|
|
283
|
+
Change `brand` once, every rule that references it updates. No build step, no preprocessor.
|
|
284
|
+
|
|
285
|
+
### Functions generate CSS rules
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
function cardStyles(accentColor) {
|
|
289
|
+
var shades = bw.deriveShades(accentColor);
|
|
290
|
+
return {
|
|
291
|
+
background: shades.light,
|
|
292
|
+
border: '1px solid ' + shades.border,
|
|
293
|
+
color: shades.darkText,
|
|
294
|
+
borderRadius: '12px'
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
bw.injectCSS(bw.css({
|
|
299
|
+
'.warning-card': cardStyles('#e67e22'),
|
|
300
|
+
'.success-card': cardStyles('#27ae60'),
|
|
301
|
+
'.info-card': cardStyles('#3498db')
|
|
302
|
+
}));
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
This is Sass mixins without Sass. And it's more powerful — the function can do arbitrary computation, derive colors algorithmically, or read configuration.
|
|
306
|
+
|
|
307
|
+
### Theme palettes — complete design systems from two colors
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
var theme = bw.makeStyles({ primary: '#336699', secondary: '#cc6633' });
|
|
311
|
+
bw.applyStyles(theme);
|
|
312
|
+
|
|
313
|
+
// theme.palette has every derived color as JS values
|
|
314
|
+
bw.injectCSS(bw.css({
|
|
315
|
+
'.my-header': {
|
|
316
|
+
background: theme.palette.primary.base,
|
|
317
|
+
color: theme.palette.primary.textOn,
|
|
318
|
+
borderBottom: '3px solid ' + theme.palette.secondary.base
|
|
319
|
+
}
|
|
320
|
+
}));
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`makeStyles()` isn't a black box. It returns the full palette as JavaScript values. You can mix theme-generated CSS with your own `bw.css()` rules using the same colors.
|
|
324
|
+
|
|
325
|
+
### @keyframes and nested at-rules
|
|
326
|
+
|
|
327
|
+
`bw.css()` handles `@media`, `@keyframes`, and all `@`-prefix rules recursively:
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
bw.injectCSS(bw.css({
|
|
331
|
+
'@keyframes fadeIn': {
|
|
332
|
+
'0%': { opacity: '0', transform: 'translateY(-10px)' },
|
|
333
|
+
'100%': { opacity: '1', transform: 'translateY(0)' }
|
|
334
|
+
},
|
|
335
|
+
'.toast': {
|
|
336
|
+
animation: 'fadeIn 0.3s ease-out',
|
|
337
|
+
padding: '0.75rem 1rem',
|
|
338
|
+
borderRadius: '8px'
|
|
339
|
+
},
|
|
340
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
341
|
+
'.toast': { animation: 'none' }
|
|
342
|
+
}
|
|
343
|
+
}));
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
All `@`-prefix keys are treated as nested blocks. No special syntax needed — it's the same JS object structure used everywhere else.
|
|
347
|
+
|
|
348
|
+
### Style composition — bw.s()
|
|
349
|
+
|
|
350
|
+
When inline styles get complex, string concatenation becomes fragile. `bw.s()` merges any number of style objects into a style string:
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
// Compose style objects — bw.s() merges them left-to-right
|
|
354
|
+
{ t: 'div', a: { style: bw.s({ display: 'flex' }, { alignItems: 'center' }, { gap: '1rem' }) }, c: [
|
|
355
|
+
{ t: 'img', a: { src: 'avatar.png', style: bw.s({ borderRadius: '0.375rem' }, { width: '40px' }) } },
|
|
356
|
+
{ t: 'span', c: 'Alice' }
|
|
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
|
+
});
|
|
364
|
+
|
|
365
|
+
// Conditional styles — null/undefined args are skipped
|
|
366
|
+
{ t: 'div', a: {
|
|
367
|
+
style: bw.s({ padding: '1rem' }, isActive ? { fontWeight: '700' } : null, { color: accent })
|
|
368
|
+
}, c: 'Status'
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`bw.s()` skips `null`/`undefined` arguments, so conditional composition works cleanly. It's `Object.assign` for CSS with a string output — runtime-composable. Unlike Tailwind class strings, you can store base styles in variables, merge them with `bw.s()`, and override individual properties.
|
|
373
|
+
|
|
374
|
+
### Responsive breakpoints — bw.responsive()
|
|
375
|
+
|
|
376
|
+
`bw.responsive()` generates `@media` rules from a JavaScript object, using bitwrench's standard breakpoints:
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
bw.injectCSS([
|
|
380
|
+
bw.css({ '.hero h1': { fontSize: '1.5rem', padding: '1rem' } }),
|
|
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()`.
|
|
389
|
+
|
|
390
|
+
### Built-in styles
|
|
391
|
+
|
|
392
|
+
Bitwrench ships with Bootstrap-inspired classes (`bw-card`, `bw-btn`, `bw-table`, etc.) that you can load with `bw.loadStyles()`. Use them, ignore them, or override them.
|
|
393
|
+
|
|
394
|
+
### Utility shorthand — bw.u() (optional plugin)
|
|
395
|
+
|
|
396
|
+
If you prefer Tailwind-style terse tokens, the `bitwrench-util-css` plugin (~1KB gzipped, loaded separately) adds `bw.u()`:
|
|
397
|
+
|
|
398
|
+
```js
|
|
399
|
+
// Returns a style object — composes with bw.s()
|
|
400
|
+
bw.u('flex gap4 p4 alignCenter')
|
|
401
|
+
// => { display: 'flex', gap: '1rem', padding: '1rem', alignItems: 'center' }
|
|
402
|
+
|
|
403
|
+
// Or as a CSS string for inline styles
|
|
404
|
+
{ t: 'div', a: { style: bw.u.css('flex gap4 p4') }, c: '...' }
|
|
405
|
+
|
|
406
|
+
// Mix shorthand with explicit properties
|
|
407
|
+
a: { style: bw.s(bw.u('flex gap4'), { borderBottom: '2px solid ' + accent }) }
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The scale is `{n} * 0.25rem`, so `p4` = 1rem, `gap8` = 2rem. Tokens cover padding, margin, gap, width, height, flex, alignment, font sizes, and colors (`bg-[#hex]`, `text-[#hex]`). Add custom tokens with `bw.u.extend({ name: styleObj })`.
|
|
411
|
+
|
|
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
|
+
|
|
414
|
+
### The key insight
|
|
415
|
+
|
|
416
|
+
Every CSS framework — Sass, Tailwind, CSS-in-JS — exists because CSS alone lacks variables, composition, and computation. JavaScript has all three. If your UI is already described in JavaScript objects, then CSS is just another set of string properties on those objects.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 3. It's Just JavaScript — the Core Insight
|
|
421
|
+
|
|
422
|
+
This section is about something you already know but may not have noticed: because a TACO is a JavaScript object literal, every field is a JavaScript expression. This is the most important thing to understand about bitwrench, and the most commonly overlooked.
|
|
423
|
+
|
|
424
|
+
### Every value is an expression
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
var title = 'Dashboard';
|
|
428
|
+
var isAdmin = true;
|
|
429
|
+
var items = ['Apples', 'Bananas', 'Cherries'];
|
|
430
|
+
|
|
431
|
+
{
|
|
432
|
+
t: isAdmin ? 'h1' : 'h2', // computed tag
|
|
433
|
+
a: {
|
|
434
|
+
class: 'header ' + (isAdmin ? 'admin' : ''), // computed class
|
|
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
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
This isn't a special feature. This is just how JavaScript object literals work. Bitwrench doesn't add anything here — it just doesn't take it away. JSX requires a compiler to turn markup back into function calls. Template strings lose structure. TACO objects are native JavaScript from start to finish.
|
|
445
|
+
|
|
446
|
+
### Functions as values — two timing modes
|
|
447
|
+
|
|
448
|
+
One of the unusual properties of TACO is that it supports two timing modes in the same object: authoring time and rendering time.
|
|
449
|
+
|
|
450
|
+
**Authoring time (IIFE)** — the function runs immediately when the object is created. The result is baked in as static data:
|
|
451
|
+
|
|
452
|
+
```js
|
|
453
|
+
// Content computed when the TACO object is created
|
|
454
|
+
{ t: 'div', c: (function() {
|
|
455
|
+
var data = getExpensiveData();
|
|
456
|
+
return data.map(function(d) { return { t: 'p', c: d.summary }; });
|
|
457
|
+
})()
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Style computed from window size at creation time
|
|
461
|
+
{ t: 'div', a: {
|
|
462
|
+
style: (function() {
|
|
463
|
+
var w = window.innerWidth;
|
|
464
|
+
return 'padding:' + (w < 768 ? '8px' : '24px');
|
|
465
|
+
})()
|
|
466
|
+
}, c: 'Responsive without @media'
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**Rendering time (function reference)** — the function is stored as-is and evaluated when bitwrench processes the tree:
|
|
471
|
+
|
|
472
|
+
```js
|
|
473
|
+
// This function runs when bw.createDOM() or bw.DOM() encounters it
|
|
474
|
+
{ t: 'div', a: {
|
|
475
|
+
style: function() { return 'opacity:' + getOpacity(); }
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
The distinction matters:
|
|
481
|
+
|
|
482
|
+
- **Authoring time** produces data. The TACO is serializable — you can send it over the wire, cache it, render it to an HTML string. This is what bwserve uses to push UI from server to client.
|
|
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.
|
|
484
|
+
|
|
485
|
+
Most template systems are either fully static (Mustache, Handlebars) or fully live (React JSX). TACO lets you choose per-field, in the same object. You decide what's fixed and what's deferred by choosing whether to call the function (IIFE) or pass it as a reference.
|
|
486
|
+
|
|
487
|
+
### Composition patterns
|
|
488
|
+
|
|
489
|
+
**Arrays compose content:**
|
|
490
|
+
|
|
491
|
+
```js
|
|
492
|
+
function makeHeader(title) {
|
|
493
|
+
return { t: 'header', c: { t: 'h1', c: title } };
|
|
494
|
+
}
|
|
495
|
+
function makeFooter() {
|
|
496
|
+
return { t: 'footer', c: '(c) 2026' };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
{ t: 'div', c: [
|
|
500
|
+
makeHeader('My App'),
|
|
501
|
+
...pages[currentPage].sections,
|
|
502
|
+
showFooter ? makeFooter() : null
|
|
503
|
+
].filter(Boolean)
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**Object.assign composes attributes:**
|
|
508
|
+
|
|
509
|
+
```js
|
|
510
|
+
var baseAttrs = { class: 'card', style: 'border-radius:12px' };
|
|
511
|
+
var clickable = { onclick: function() { alert('clicked'); }, style: 'cursor:pointer' };
|
|
512
|
+
|
|
513
|
+
{ t: 'div', a: Object.assign({}, baseAttrs, clickable), c: 'Click me' }
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**Functions are your "components":**
|
|
517
|
+
|
|
518
|
+
```js
|
|
519
|
+
function colorCard(title, body, color) {
|
|
520
|
+
return {
|
|
521
|
+
t: 'div',
|
|
522
|
+
a: { class: 'card', style: 'border-left:4px solid ' + color },
|
|
523
|
+
c: [
|
|
524
|
+
{ t: 'h3', c: title },
|
|
525
|
+
{ t: 'p', c: body }
|
|
526
|
+
]
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
{ t: 'div', c: [
|
|
531
|
+
colorCard('Warning', 'Disk space low', '#e67e22'),
|
|
532
|
+
colorCard('Success', 'Backup complete', '#27ae60'),
|
|
533
|
+
colorCard('Info', '3 updates available', '#3498db')
|
|
534
|
+
]}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
You don't need React's component concept, Vue's slots, or Svelte's `{#each}` syntax. JavaScript already has functions (components), arrays (slots), and `.map()` (iteration). TACO just gives these a shape that maps to the DOM.
|
|
538
|
+
|
|
539
|
+
### Conditionals — three ways
|
|
540
|
+
|
|
541
|
+
```js
|
|
542
|
+
// Ternary (inline)
|
|
543
|
+
{ t: 'div', c: loggedIn ? 'Welcome back' : 'Please sign in' }
|
|
544
|
+
|
|
545
|
+
// null filtering (in arrays)
|
|
546
|
+
{ t: 'nav', c: [
|
|
547
|
+
{ t: 'a', c: 'Home' },
|
|
548
|
+
isAdmin ? { t: 'a', c: 'Admin' } : null,
|
|
549
|
+
{ t: 'a', c: 'About' }
|
|
550
|
+
].filter(Boolean)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// IIFE for complex logic
|
|
554
|
+
{ t: 'div', c: (function() {
|
|
555
|
+
if (status === 'loading') return { t: 'span', c: 'Loading...' };
|
|
556
|
+
if (status === 'error') return { t: 'span', a: { class: 'error' }, c: errorMsg };
|
|
557
|
+
return resultList.map(function(r) { return { t: 'li', c: r.name }; });
|
|
558
|
+
})()
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Iteration — it's just .map()
|
|
563
|
+
|
|
564
|
+
```js
|
|
565
|
+
var users = [
|
|
566
|
+
{ name: 'Alice', role: 'admin' },
|
|
567
|
+
{ name: 'Bob', role: 'user' }
|
|
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
|
+
]}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
No `v-for`, no `{#each}`, no special key rules. Just JavaScript.
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## 4. The BCCL: Ready-Made Components
|
|
588
|
+
|
|
589
|
+
### What BCCL is and why it exists
|
|
590
|
+
|
|
591
|
+
BCCL (Bitwrench Common Component Library) is a set of 50+ factory functions that return TACO objects for common UI patterns — cards, buttons, navbars, tables, forms, modals, alerts, and more. Think of it as Bootstrap or shadcn/ui, but instead of HTML templates you get plain JavaScript objects.
|
|
592
|
+
|
|
593
|
+
The point: you can build a complete, styled page without writing a single line of CSS or HTML. Load the default styles, call the factories, render. Great for quick UIs, prototyping, embedded device interfaces, and internal tools.
|
|
594
|
+
|
|
595
|
+
Three things to know about BCCL:
|
|
596
|
+
|
|
597
|
+
1. **Every factory returns a TACO object.** The output is a plain `{t, a, c, o}` object. You can inspect it, modify any part, nest it, or pass it to any function that takes a TACO.
|
|
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.
|
|
600
|
+
|
|
601
|
+
### Factories return TACO, not DOM
|
|
602
|
+
|
|
603
|
+
```js
|
|
604
|
+
var card = bw.makeCard({ title: 'Users', content: '42 online' });
|
|
605
|
+
// card is { t:'div', a:{class:'bw-card'}, c:[...] }
|
|
606
|
+
|
|
607
|
+
var page = { t: 'div', c: [
|
|
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
|
+
]};
|
|
618
|
+
|
|
619
|
+
bw.DOM('#app', page);
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Quick inventory
|
|
623
|
+
|
|
624
|
+
| Category | Components |
|
|
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
|
|
635
|
+
|
|
636
|
+
```js
|
|
637
|
+
{ t: 'div', c: [
|
|
638
|
+
bw.makeCard({ title: 'Stats' }),
|
|
639
|
+
{ t: 'div', a: { class: 'custom-widget', style: 'padding:2rem' }, c: [
|
|
640
|
+
{ t: 'h3', c: 'Custom Section' },
|
|
641
|
+
{ t: 'p', c: 'Hand-written TACO next to a BCCL card.' }
|
|
642
|
+
]}
|
|
643
|
+
]}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Modifying BCCL output
|
|
647
|
+
|
|
648
|
+
Since BCCL returns plain objects, you can modify them before rendering:
|
|
649
|
+
|
|
650
|
+
```js
|
|
651
|
+
var card = bw.makeCard({ title: 'Users', content: '42 online' });
|
|
652
|
+
card.a.style = 'border-left:4px solid #336699';
|
|
653
|
+
card.c.push({ t: 'small', c: 'Updated 5m ago' });
|
|
654
|
+
bw.DOM('#app', card);
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## 5. Three Levels of Commitment
|
|
660
|
+
|
|
661
|
+
| Level | What you get | What you write | When to use |
|
|
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 |
|
|
666
|
+
|
|
667
|
+
Most of your UI should be Level 0. Only escalate when you need interactivity. Level 0 TACOs are composable, serializable, and free. Level 2 components have overhead — use them for the parts that actually change.
|
|
668
|
+
|
|
669
|
+
### Level 0 — pure data
|
|
670
|
+
|
|
671
|
+
```js
|
|
672
|
+
var listing = {
|
|
673
|
+
t: 'div', a: { class: 'products' },
|
|
674
|
+
c: products.map(function(p) {
|
|
675
|
+
return {
|
|
676
|
+
t: 'div', a: { class: 'card' }, c: [
|
|
677
|
+
{ t: 'h3', c: p.name },
|
|
678
|
+
{ t: 'p', c: '$' + p.price.toFixed(2) }
|
|
679
|
+
]
|
|
680
|
+
};
|
|
681
|
+
})
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
bw.DOM('#products', listing); // render once, done
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### Level 1 — render and re-render
|
|
688
|
+
|
|
689
|
+
```js
|
|
690
|
+
function renderClock() {
|
|
691
|
+
bw.DOM('#clock', { t: 'div', c: new Date().toLocaleTimeString() });
|
|
692
|
+
}
|
|
693
|
+
setInterval(renderClock, 1000);
|
|
694
|
+
renderClock();
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
You own the render loop. Call `bw.DOM()` whenever you want. Simple, explicit, good enough for many use cases.
|
|
698
|
+
|
|
699
|
+
### Level 2 -- stateful TACO with o.state + o.render
|
|
700
|
+
|
|
701
|
+
```js
|
|
702
|
+
var counter = {
|
|
703
|
+
t: 'div',
|
|
704
|
+
o: {
|
|
705
|
+
state: { count: 0 },
|
|
706
|
+
render: function(el) {
|
|
707
|
+
var s = el._bw_state;
|
|
708
|
+
bw.DOM(el, {
|
|
709
|
+
t: 'div', c: [
|
|
710
|
+
{ t: 'span', c: 'Count: ' + s.count },
|
|
711
|
+
bw.makeButton({ text: '+1', onclick: function() {
|
|
712
|
+
s.count++;
|
|
713
|
+
bw.update(el);
|
|
714
|
+
}}),
|
|
715
|
+
bw.makeButton({ text: 'Reset', onclick: function() {
|
|
716
|
+
s.count = 0;
|
|
717
|
+
bw.update(el);
|
|
718
|
+
}})
|
|
719
|
+
]
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
bw.DOM('#app', counter);
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
When state changes, call `bw.update(el)` to re-invoke the render function. The component owns its update logic -- event handlers modify `el._bw_state` directly and trigger re-render.
|
|
729
|
+
|
|
730
|
+
### When to use which level
|
|
731
|
+
|
|
732
|
+
```
|
|
733
|
+
Is the content static or computed once from data?
|
|
734
|
+
=> Level 0. Use make*() or hand-write TACO. Render with bw.DOM().
|
|
735
|
+
|
|
736
|
+
Does the content change, but you control when?
|
|
737
|
+
=> Level 1. Call bw.DOM() again when data changes.
|
|
738
|
+
|
|
739
|
+
Does the content change in response to user interaction or external events,
|
|
740
|
+
and you want structured state management?
|
|
741
|
+
=> Level 2. Use o.state + o.render + bw.update().
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
## 6. Events and Communication
|
|
747
|
+
|
|
748
|
+
### Event handlers — use onclick, not o.mounted
|
|
749
|
+
|
|
750
|
+
> **Warning: Never attach event handlers in `o.mounted`.** When a stateful component re-renders (after `bw.update()`), the old DOM children are replaced. Any listeners attached via `addEventListener` in `o.mounted` are silently lost -- no error, no warning. The click handler simply stops working after the first state change. This is the most common mistake new bitwrench developers make.
|
|
751
|
+
|
|
752
|
+
```js
|
|
753
|
+
// CORRECT — onclick in attributes. Re-attached automatically on every render.
|
|
754
|
+
bw.makeButton({
|
|
755
|
+
text: 'Save',
|
|
756
|
+
onclick: function() { save(); }
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
// CORRECT — inline in TACO
|
|
760
|
+
{ t: 'button', a: { onclick: function() { save(); } }, c: 'Save' }
|
|
761
|
+
|
|
762
|
+
// WRONG — handler silently lost when component re-renders
|
|
763
|
+
{ t: 'button', c: 'Save',
|
|
764
|
+
o: { mounted: function(el) { el.addEventListener('click', save); } }
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
`onclick` (and `onchange`, `oninput`, `onsubmit`, etc.) in `a:` is the only safe event pattern for components that re-render. Bitwrench re-attaches attribute handlers on every render automatically.
|
|
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.
|
|
771
|
+
|
|
772
|
+
### Cross-component communication — pub/sub
|
|
773
|
+
|
|
774
|
+
```js
|
|
775
|
+
// Publisher (cart module)
|
|
776
|
+
function addToCart(item) {
|
|
777
|
+
cart.push(item);
|
|
778
|
+
bw.pub('cart:updated', { count: cart.length });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Subscriber (navbar badge) -- auto-cleans when element is removed
|
|
782
|
+
bw.sub('cart:updated', function(data) {
|
|
783
|
+
navbarEl._bw_state.cartCount = data.count;
|
|
784
|
+
bw.update(navbarEl);
|
|
785
|
+
}, navbarEl);
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
`bw.pub()` and `bw.sub()` are app-wide -- not scoped to the DOM tree. Any component can publish, any component can subscribe. Pass the element as the third argument to `bw.sub()` to tie the subscription lifetime to that element (auto-cleaned on `bw.cleanup()`).
|
|
789
|
+
|
|
790
|
+
### Component handles -- o.handle and o.slots
|
|
791
|
+
|
|
792
|
+
When you need to update part of a rendered component without re-rendering the whole thing -- change a title, advance a carousel, read a form value -- use component handles.
|
|
793
|
+
|
|
794
|
+
**When to use handles vs pub/sub:** Pub/sub is for decoupled cross-component messaging ("something happened, anyone who cares can react"). Handles are for direct imperative control of a specific element ("carousel, go to slide 3"). If you have a reference to the element, use handles. If you don't know who should respond, use pub/sub.
|
|
795
|
+
|
|
796
|
+
**o.handle** attaches named methods to `el.bw`:
|
|
797
|
+
|
|
798
|
+
```js
|
|
799
|
+
var widget = {
|
|
800
|
+
t: 'div', c: [
|
|
801
|
+
{ t: 'span', a: { class: 'count' }, c: '0' },
|
|
802
|
+
{ t: 'button', a: { onclick: function() { /* ... */ } }, c: '+' }
|
|
803
|
+
],
|
|
804
|
+
o: {
|
|
805
|
+
handle: {
|
|
806
|
+
increment: function(el) {
|
|
807
|
+
var span = el.querySelector('.count');
|
|
808
|
+
span.textContent = String(Number(span.textContent) + 1);
|
|
809
|
+
},
|
|
810
|
+
reset: function(el) {
|
|
811
|
+
el.querySelector('.count').textContent = '0';
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
|
|
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
|
+
var el = bw.mount('#app', card);
|
|
827
|
+
el.bw.setTitle('Revenue'); // update just the title
|
|
828
|
+
el.bw.setContent({ t: 'b', c: '$42k' }); // accepts TACO objects
|
|
829
|
+
el.bw.getTitle(); // returns 'Revenue'
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
Slot setters update only the targeted child element. Input focus, scroll position, and animation state in sibling elements are preserved -- this is the key advantage over a full `bw.update()` re-render.
|
|
833
|
+
|
|
834
|
+
**bw.mount()** is the entry point: it works like `bw.DOM()` but returns the root element so you can access `el.bw`. Use `bw.message(selector, action, data)` when you don't have a direct reference.
|
|
835
|
+
|
|
836
|
+
All BCCL factories (`makeCard`, `makeCarousel`, `makeTabs`, `makeAccordion`, `makeModal`, `makeProgress`, `makeChipInput`, `makeStatCard`) include handles and/or slots. See [Component Library](component-library.md) for the full method table.
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## 7. Server-Driven UI (bwserve)
|
|
841
|
+
|
|
842
|
+
### The idea
|
|
843
|
+
|
|
844
|
+
Any server that can write JSON to an HTTP response can drive a bitwrench UI. The server sends TACO objects over SSE (Server-Sent Events). The browser renders them. No client-side application logic required.
|
|
845
|
+
|
|
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:
|
|
859
|
+
|
|
860
|
+
```js
|
|
861
|
+
import bwserve from 'bitwrench/bwserve';
|
|
862
|
+
|
|
863
|
+
var app = bwserve.create({ port: 8080 });
|
|
864
|
+
app.page('/', function(client) {
|
|
865
|
+
client.render('#app', {
|
|
866
|
+
t: 'div', c: [
|
|
867
|
+
{ t: 'h1', c: 'Hello from the server' },
|
|
868
|
+
{ t: 'p', a: { id: 'status' }, c: 'Connected.' },
|
|
869
|
+
{ t: 'button', a: { 'data-bw-action': 'greet' }, c: 'Say hello' }
|
|
870
|
+
]
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
app.listen();
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
The browser receives one HTML page (the "shell") with bitwrench loaded. Everything after that arrives as JSON messages over SSE.
|
|
877
|
+
|
|
878
|
+
### Incremental updates from server
|
|
879
|
+
|
|
880
|
+
```js
|
|
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
|
|
891
|
+
|
|
892
|
+
When a user clicks a `data-bw-action` element, the browser POSTs the action name to the server:
|
|
893
|
+
|
|
894
|
+
```js
|
|
895
|
+
client.on('greet', function(data) {
|
|
896
|
+
client.patch('#status', 'Hello, user!');
|
|
897
|
+
});
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### Server-side lifecycle — register and call
|
|
901
|
+
|
|
902
|
+
Register JavaScript functions on the client, then invoke them by name:
|
|
903
|
+
|
|
904
|
+
```js
|
|
905
|
+
client.register('showAlert', 'function(msg) { alert(msg); }');
|
|
906
|
+
client.call('showAlert', 'Server says hi!');
|
|
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:
|
|
916
|
+
|
|
917
|
+
```js
|
|
918
|
+
client.patch('#my-id', 'by ID');
|
|
919
|
+
client.patch('.status-bar', 'by class');
|
|
920
|
+
client.patch('[data-role="header"]', 'by attribute');
|
|
921
|
+
|
|
922
|
+
// UUID for stable addressing of dynamic content
|
|
923
|
+
var itemId = bw.uuid('item');
|
|
924
|
+
client.render('#list', { t: 'div', a: { class: itemId }, c: 'Dynamic item' });
|
|
925
|
+
client.patch('.' + itemId, 'Updated content');
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### Why this matters
|
|
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
|
+
---
|
|
937
|
+
|
|
938
|
+
## 8. Routing
|
|
939
|
+
|
|
940
|
+
Bitwrench includes a built-in client-side router. It maps URLs to view functions, supports hash mode and History API mode, and integrates with pub/sub.
|
|
941
|
+
|
|
942
|
+
### Basic SPA routing
|
|
943
|
+
|
|
944
|
+
```js
|
|
945
|
+
bw.router({
|
|
946
|
+
target: '#app',
|
|
947
|
+
routes: {
|
|
948
|
+
'/': function() { return { t: 'h1', c: 'Home' }; },
|
|
949
|
+
'/about': function() { return { t: 'h1', c: 'About' }; },
|
|
950
|
+
'/users/:id': function(params) {
|
|
951
|
+
return bw.makeCard({ title: 'User ' + params.id });
|
|
952
|
+
},
|
|
953
|
+
'*': function() { return { t: 'h1', c: '404 Not Found' }; }
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
The router reads the current URL, matches a route, calls the handler, and mounts the returned TACO into `#app`. It listens for URL changes and re-renders automatically.
|
|
959
|
+
|
|
960
|
+
### Programmatic navigation
|
|
961
|
+
|
|
962
|
+
```js
|
|
963
|
+
bw.navigate('/users/123');
|
|
964
|
+
bw.navigate('/about', { replace: true }); // replace history entry
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### Navigation links
|
|
968
|
+
|
|
969
|
+
```js
|
|
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
|
|
975
|
+
|
|
976
|
+
```js
|
|
977
|
+
// /users/42?tab=posts
|
|
978
|
+
'/users/:id': function(params) {
|
|
979
|
+
params.id; // '42'
|
|
980
|
+
params._query.tab; // 'posts'
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// /docs/api/colors (catch-all)
|
|
984
|
+
'/docs/*': function(params) {
|
|
985
|
+
params._rest; // 'api/colors'
|
|
986
|
+
}
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### Guards and hooks
|
|
990
|
+
|
|
991
|
+
```js
|
|
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
|
+
```
|
|
1004
|
+
|
|
1005
|
+
### Pub/sub integration
|
|
1006
|
+
|
|
1007
|
+
Every route change publishes `bw:route`:
|
|
1008
|
+
|
|
1009
|
+
```js
|
|
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
|
+
```
|
|
1015
|
+
|
|
1016
|
+
### Hash vs history mode
|
|
1017
|
+
|
|
1018
|
+
Hash mode (default): URLs like `#/users/123`. Works everywhere, no server config needed.
|
|
1019
|
+
|
|
1020
|
+
History mode: URLs like `/users/123`. Requires SPA fallback on the server.
|
|
1021
|
+
|
|
1022
|
+
```js
|
|
1023
|
+
bw.router({ mode: 'history', base: '/app', target: '#app', routes: { ... } });
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
### When you don't need routing
|
|
1027
|
+
|
|
1028
|
+
Many bitwrench apps are single-page dashboards where tab-switching is enough:
|
|
1029
|
+
|
|
1030
|
+
```js
|
|
1031
|
+
bw.DOM('#app', bw.makeTabs({
|
|
1032
|
+
tabs: [
|
|
1033
|
+
{ label: 'Overview', content: makeOverview() },
|
|
1034
|
+
{ label: 'Analytics', content: makeAnalytics() }
|
|
1035
|
+
]
|
|
1036
|
+
}));
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
### Server-side routing with bwserve
|
|
1040
|
+
|
|
1041
|
+
The client router complements bwserve's `app.page()`:
|
|
1042
|
+
|
|
1043
|
+
```js
|
|
1044
|
+
app.page('/', function(client) { client.render('#app', makeHomePage()); });
|
|
1045
|
+
app.page('/dashboard', function(client) { client.render('#app', makeDashboard()); });
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
See [Routing Guide](routing.md) for the full API reference, patterns, and examples.
|
|
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.
|
|
1055
|
+
|
|
1056
|
+
### Color functions
|
|
1057
|
+
|
|
1058
|
+
```js
|
|
1059
|
+
bw.hexToHsl('#336699'); // [210, 50, 40]
|
|
1060
|
+
bw.hslToHex([210, 50, 40]); // '#336699'
|
|
1061
|
+
bw.adjustLightness('#336699', 20); // lighten by 20%
|
|
1062
|
+
bw.mixColor('#336699', '#cc6633', 0.5); // blend two colors
|
|
1063
|
+
bw.textOnColor('#336699'); // '#fff' (contrast-safe text color)
|
|
1064
|
+
bw.relativeLuminance('#336699'); // WCAG 2.0 luminance value
|
|
1065
|
+
bw.deriveShades('#336699'); // { base, hover, active, light, darkText, border, focus, textOn }
|
|
1066
|
+
bw.derivePalette({ primary: '#336699', secondary: '#cc6633' }); // full 9-group palette
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
`deriveShades()` and `derivePalette()` are the building blocks behind `makeStyles()`. You can use them directly for custom color systems.
|
|
1070
|
+
|
|
1071
|
+
### URL and data utilities
|
|
1072
|
+
|
|
1073
|
+
```js
|
|
1074
|
+
bw.getURLParam('page', 'home'); // read ?page=... from URL, default 'home'
|
|
1075
|
+
bw.typeOf(x); // enhanced typeof: 'array', 'null', 'date', etc.
|
|
1076
|
+
bw.uuid('widget'); // 'bw_widget_a3f2c1' (unique ID with prefix)
|
|
1077
|
+
bw.loremIpsum(200); // 200 characters of placeholder text
|
|
1078
|
+
bw.random(1, 100); // random integer
|
|
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)
|
|
1081
|
+
bw.clip(150, 0, 100); // clamp to range (100)
|
|
1082
|
+
bw.naturalCompare('item2', 'item10'); // natural sort comparison
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
### File I/O
|
|
1086
|
+
|
|
1087
|
+
```js
|
|
1088
|
+
// Browser — save/load via download dialog or FileReader
|
|
1089
|
+
bw.saveClientFile('report.txt', content);
|
|
1090
|
+
bw.saveClientJSON('data.json', obj);
|
|
1091
|
+
bw.loadClientFile(function(data) { /* file contents */ });
|
|
1092
|
+
bw.loadClientJSON(function(obj) { /* parsed JSON */ });
|
|
1093
|
+
|
|
1094
|
+
// Node.js — same API names, uses fs
|
|
1095
|
+
bw.loadLocalFile('config.json').then(function(data) { /* ... */ });
|
|
1096
|
+
bw.saveLocalFile('output.txt', content);
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
### Relaxed JSON
|
|
1100
|
+
|
|
1101
|
+
Standard JSON requires double-quoted keys and no trailing commas. Bitwrench's relaxed JSON parser is more forgiving:
|
|
1102
|
+
|
|
1103
|
+
```js
|
|
1104
|
+
bw.parseRJSON("{ name: 'Alice', age: 30, }");
|
|
1105
|
+
// { name: 'Alice', age: 30 }
|
|
1106
|
+
```
|
|
1107
|
+
|
|
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
|
+
|
|
1110
|
+
---
|
|
1111
|
+
|
|
1112
|
+
## 10. Putting It All Together — Patterns
|
|
1113
|
+
|
|
1114
|
+
### Static page composition
|
|
1115
|
+
|
|
1116
|
+
```js
|
|
1117
|
+
bw.loadStyles();
|
|
1118
|
+
bw.loadStyles({ primary: '#336699', secondary: '#cc6633' });
|
|
1119
|
+
|
|
1120
|
+
bw.DOM('#app', [
|
|
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
|
+
```
|
|
1130
|
+
|
|
1131
|
+
### Data-driven filtered list
|
|
1132
|
+
|
|
1133
|
+
```js
|
|
1134
|
+
var allItems = [/* ... */];
|
|
1135
|
+
var filter = 'all';
|
|
1136
|
+
|
|
1137
|
+
function renderList() {
|
|
1138
|
+
var items = filter === 'all' ? allItems : allItems.filter(function(i) {
|
|
1139
|
+
return i.type === filter;
|
|
1140
|
+
});
|
|
1141
|
+
bw.DOM('#list', { t: 'div', c: items.map(function(i) {
|
|
1142
|
+
return { t: 'div', a: { class: 'item' }, c: [
|
|
1143
|
+
{ t: 'h3', c: i.name },
|
|
1144
|
+
{ t: 'p', c: i.description }
|
|
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();
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
Level 1 -- you own the render loop. No stateful TACO needed.
|
|
1160
|
+
|
|
1161
|
+
### Reactive component with state
|
|
1162
|
+
|
|
1163
|
+
```js
|
|
1164
|
+
bw.DOM('#contact', {
|
|
1165
|
+
t: 'div',
|
|
1166
|
+
o: {
|
|
1167
|
+
state: { statusMsg: '', showStatus: false },
|
|
1168
|
+
render: function(el) {
|
|
1169
|
+
var s = el._bw_state;
|
|
1170
|
+
bw.DOM(el, {
|
|
1171
|
+
t: 'div', c: [
|
|
1172
|
+
s.showStatus ? { t: 'div', c: s.statusMsg } : null,
|
|
1173
|
+
bw.makeForm({ children: [
|
|
1174
|
+
bw.makeFormGroup({ label: 'Email', input: bw.makeInput({ type: 'email', id: 'email' }) }),
|
|
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
|
+
}})
|
|
1183
|
+
]
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
### Cross-component coordination
|
|
1191
|
+
|
|
1192
|
+
```js
|
|
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
|
+
};
|
|
1208
|
+
|
|
1209
|
+
function addToCart(item) {
|
|
1210
|
+
cart.push(item);
|
|
1211
|
+
bw.pub('cart:updated', { count: cart.length, items: cart });
|
|
1212
|
+
}
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
### Theme + custom CSS
|
|
1216
|
+
|
|
1217
|
+
```js
|
|
1218
|
+
var theme = bw.makeStyles({ primary: '#336699', secondary: '#cc6633' });
|
|
1219
|
+
bw.applyStyles(theme);
|
|
1220
|
+
var accent = theme.palette.secondary.base;
|
|
1221
|
+
var accentLight = theme.palette.secondary.light;
|
|
1222
|
+
|
|
1223
|
+
bw.injectCSS(bw.css({
|
|
1224
|
+
'.hero': {
|
|
1225
|
+
background: 'linear-gradient(135deg, ' + accent + ', ' + accentLight + ')',
|
|
1226
|
+
padding: '4rem 2rem', color: '#fff'
|
|
1227
|
+
},
|
|
1228
|
+
'.hero h1': { fontSize: 'clamp(2rem, 5vw, 3.5rem)' }
|
|
1229
|
+
}));
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
### Ephemeral UI (toasts, notifications)
|
|
1233
|
+
|
|
1234
|
+
```js
|
|
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
|
+
]);
|
|
1241
|
+
|
|
1242
|
+
// Show a toast by appending to the container
|
|
1243
|
+
function showToast(message, variant) {
|
|
1244
|
+
var toastId = bw.uuid('toast');
|
|
1245
|
+
var toast = {
|
|
1246
|
+
t: 'div', a: { class: toastId, style: 'min-width:280px;margin-bottom:0.5rem' },
|
|
1247
|
+
c: bw.makeAlert({ content: message, variant: variant || 'info', dismissible: true })
|
|
1248
|
+
};
|
|
1249
|
+
var container = bw.$('#toast-container')[0];
|
|
1250
|
+
if (container) {
|
|
1251
|
+
container.appendChild(bw.createDOM(toast));
|
|
1252
|
+
setTimeout(function() {
|
|
1253
|
+
var el = bw.$('.' + toastId)[0];
|
|
1254
|
+
if (el) { bw.cleanup(el); el.remove(); }
|
|
1255
|
+
}, 3500);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
showToast('Item added to cart', 'success');
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
The toast container is part of the TACO tree. Individual toasts append into it and auto-remove after a delay. `bw.cleanup()` ensures any lifecycle hooks are properly torn down.
|
|
1262
|
+
|
|
1263
|
+
### Dashboard card — theme tokens + bw.s() + responsive + component
|
|
1264
|
+
|
|
1265
|
+
This compact example combines the key patterns: theme palette tokens (no hardcoded hex), `bw.s()` for inline style composition, `bw.responsive()` for breakpoints, and Level 1 re-rendering for live-updating stat cards.
|
|
1266
|
+
|
|
1267
|
+
```js
|
|
1268
|
+
var theme = bw.makeStyles({ primary: '#1e40af', secondary: '#059669' });
|
|
1269
|
+
bw.applyStyles(theme);
|
|
1270
|
+
var P = theme.palette;
|
|
1271
|
+
|
|
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
|
+
|
|
1282
|
+
// Stat cards — palette tokens, not hex literals
|
|
1283
|
+
var metrics = { users: 2847, revenue: 48920, orders: 384, rate: 3.2 };
|
|
1284
|
+
|
|
1285
|
+
function renderStats() {
|
|
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
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Layout uses bw.s() — no inline style strings
|
|
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
|
+
]});
|
|
1305
|
+
|
|
1306
|
+
renderStats();
|
|
1307
|
+
setInterval(function() {
|
|
1308
|
+
metrics.users += Math.round(Math.random() * 20 - 5);
|
|
1309
|
+
metrics.revenue += Math.round(Math.random() * 500 - 100);
|
|
1310
|
+
renderStats();
|
|
1311
|
+
}, 3000);
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
Key things this example proves:
|
|
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
|
|
1319
|
+
|
|
1320
|
+
---
|
|
1321
|
+
|
|
1322
|
+
## 11. What Bitwrench Doesn't Do
|
|
1323
|
+
|
|
1324
|
+
| Feature | Why not | What to use instead |
|
|
1325
|
+
|---------|---------|-------------------|
|
|
1326
|
+
| TypeScript types | Ships as UMD/ESM, works everywhere | Community .d.ts welcome, JSDoc in source |
|
|
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` |
|
|
1331
|
+
|
|
1332
|
+
---
|
|
1333
|
+
|
|
1334
|
+
## 12. Quick Reference
|
|
1335
|
+
|
|
1336
|
+
### Core rendering
|
|
1337
|
+
|
|
1338
|
+
| Function | What it does |
|
|
1339
|
+
|----------|-------------|
|
|
1340
|
+
| `bw.html(taco)` | TACO to HTML string |
|
|
1341
|
+
| `bw.createDOM(taco)` | TACO to detached DOM element |
|
|
1342
|
+
| `bw.DOM(sel, taco)` | Mount TACO into existing element |
|
|
1343
|
+
| `bw.h(tag, attrs, c, o)` | TACO constructor — returns plain `{t,a,c,o}` from positional args |
|
|
1344
|
+
| `bw.raw(str)` | Mark string as pre-escaped HTML |
|
|
1345
|
+
|
|
1346
|
+
### CSS
|
|
1347
|
+
|
|
1348
|
+
| Function | What it does |
|
|
1349
|
+
|----------|-------------|
|
|
1350
|
+
| `bw.css(rules)` | JS object to CSS string (supports `@media`, `@keyframes` recursively) |
|
|
1351
|
+
| `bw.injectCSS(css)` | Insert CSS string into document |
|
|
1352
|
+
| `bw.s(...styles)` | Merge style objects into a style string |
|
|
1353
|
+
| `bw.responsive(sel, bp)` | Generate responsive `@media` CSS from breakpoint object |
|
|
1354
|
+
| `bw.loadStyles()` | Load built-in structural CSS |
|
|
1355
|
+
| `bw.makeStyles(cfg)` | Generate styled CSS from seed colors (returns styles object) |
|
|
1356
|
+
| `bw.applyStyles(styles)` | Inject generated styles' CSS into the document |
|
|
1357
|
+
| `bw.loadStyles(cfg)` | Generate and apply styles in one call |
|
|
1358
|
+
| `bw.toggleStyles()` | Switch between primary and alternate palettes |
|
|
1359
|
+
| `bw.clearStyles()` | Remove injected styles |
|
|
1360
|
+
|
|
1361
|
+
### State (Level 2)
|
|
1362
|
+
|
|
1363
|
+
| Function | What it does |
|
|
1364
|
+
|----------|-------------|
|
|
1365
|
+
| `o.state` | Initial state object (copied to `el._bw_state`) |
|
|
1366
|
+
| `o.render(el, state)` | Render function called on mount and `bw.update()` |
|
|
1367
|
+
| `bw.update(el)` | Re-invoke `el._bw_render(el, el._bw_state)` |
|
|
1368
|
+
| `bw.patch(uuid, content)` | Update a single UUID-addressed element |
|
|
1369
|
+
| `bw.cleanup(el)` | Run unmount hooks, clear subscriptions |
|
|
1370
|
+
|
|
1371
|
+
### Component Handles
|
|
1372
|
+
|
|
1373
|
+
| Function | What it does |
|
|
1374
|
+
|----------|-------------|
|
|
1375
|
+
| `o.handle` | Object of methods attached to `el.bw` on createDOM |
|
|
1376
|
+
| `o.slots` | `{name: '.selector'}` -- auto-generates `el.bw.setName()` / `el.bw.getName()` |
|
|
1377
|
+
| `bw.mount(sel, taco)` | Like DOM() but returns the root element for `el.bw` access |
|
|
1378
|
+
| `bw.message(target, action, data)` | Dispatch to `el.bw[action](data)` by id, UUID, or selector |
|
|
1379
|
+
|
|
1380
|
+
### Communication
|
|
1381
|
+
|
|
1382
|
+
| Function | What it does |
|
|
1383
|
+
|----------|-------------|
|
|
1384
|
+
| `bw.pub(topic, data)` | Publish to all subscribers |
|
|
1385
|
+
| `bw.sub(topic, fn)` | Subscribe (returns unsub function) |
|
|
1386
|
+
| `bw.sub(topic, fn, owner)` | Subscribe with auto-cleanup when owner is removed |
|
|
1387
|
+
|
|
1388
|
+
### Routing
|
|
1389
|
+
|
|
1390
|
+
| Function | What it does |
|
|
1391
|
+
|----------|-------------|
|
|
1392
|
+
| `bw.router(config)` | Create and start a client-side router |
|
|
1393
|
+
| `bw.navigate(path, opts)` | Programmatic navigation (delegates to active router) |
|
|
1394
|
+
| `bw.link(path, content, attrs)` | Returns TACO `<a>` with navigation wired |
|
|
1395
|
+
|
|
1396
|
+
### Color
|
|
1397
|
+
|
|
1398
|
+
| Function | What it does |
|
|
1399
|
+
|----------|-------------|
|
|
1400
|
+
| `bw.hexToHsl(hex)` | Hex to [h, s, l] |
|
|
1401
|
+
| `bw.hslToHex(hsl)` | [h, s, l] to hex |
|
|
1402
|
+
| `bw.deriveShades(hex)` | 8 shade variants from one color |
|
|
1403
|
+
| `bw.derivePalette(cfg)` | Full palette from seed colors |
|
|
1404
|
+
| `bw.textOnColor(hex)` | Contrast-safe text color |
|
|
1405
|
+
| `bw.mixColor(a, b, ratio)` | Blend two colors |
|
|
1406
|
+
|
|
1407
|
+
### Utilities
|
|
1408
|
+
|
|
1409
|
+
| Function | What it does |
|
|
1410
|
+
|----------|-------------|
|
|
1411
|
+
| `bw.$('selector')` | querySelectorAll as array |
|
|
1412
|
+
| `bw.h(tag, attrs, c, o)` | TACO constructor (positional args → `{t,a,c,o}`) |
|
|
1413
|
+
| `bw.escapeHTML(str)` | Escape HTML special chars |
|
|
1414
|
+
| `bw.uuid(prefix)` | Generate unique ID |
|
|
1415
|
+
| `bw.typeOf(x)` | Enhanced typeof |
|
|
1416
|
+
| `bw.getURLParam(key, def)` | Read URL query parameter |
|
|
1417
|
+
| `bw.random(min, max)` | Random integer (or array) |
|
|
1418
|
+
| `bw.loremIpsum(n)` | Placeholder text |
|
|
1419
|
+
| `bw.mapScale(x, i0, i1, o0, o1)` | Map value between ranges |
|
|
1420
|
+
| `bw.parseRJSON(str)` | Parse relaxed JSON |
|
|
1421
|
+
| `bw.saveClientFile(name, data)` | Browser file download |
|
|
1422
|
+
| `bw.loadClientJSON(cb)` | Browser file upload (JSON) |
|
|
1423
|
+
|
|
1424
|
+
---
|
|
1425
|
+
|
|
1426
|
+
## Appendix: Framework Translation Table
|
|
1427
|
+
|
|
1428
|
+
How common UI operations map across frameworks. Each cell is the idiomatic one-liner for that framework.
|
|
1429
|
+
|
|
1430
|
+
| Operation | What it is | React | Vue 3 | Vanilla JS | Svelte 5 | Solid | Bitwrench |
|
|
1431
|
+
|-----------|-----------|-------|-------|------------|----------|-------|-----------|
|
|
1432
|
+
| **Render element** | Create and display a UI element | `<div className="card">Hi</div>` | `<div class="card">Hi</div>` | `el.innerHTML = '<div>Hi</div>'` | `<div class="card">Hi</div>` | `<div class="card">Hi</div>` | `bw.DOM('#x', {t:'div', a:{class:'card'}, c:'Hi'})` |
|
|
1433
|
+
| **Update text** | Change text content after render | `setText('new')` via `useState` | `msg.value = 'new'` | `el.textContent = 'new'` | `msg = 'new'` | `setMsg('new')` | `el._bw_state.msg = 'new'; bw.update(el)` or `bw.patch(id, 'new')` |
|
|
1434
|
+
| **Conditional render** | Show/hide based on state | `{show && <Comp/>}` | `v-if="show"` | `if (show) el.style.display = ''` | `{#if show}<Comp/>{/if}` | `<Show when={show}><Comp/></Show>` | `show ? taco : null` in `c:` array |
|
|
1435
|
+
| **List rendering** | Render array of items | `{items.map(i => <Li key={i.id}/>)}` | `v-for="i in items" :key="i.id"` | `el.innerHTML = items.map(...)` | `{#each items as i (i.id)}` | `<For each={items}>{i => ...}</For>` | `c: items.map(function(i) { return {t:'li', c:i.name} })` |
|
|
1436
|
+
| **Event handler** | Attach click/input handler | `onClick={handler}` | `@click="handler"` | `el.addEventListener('click', fn)` | `onclick={handler}` | `onClick={handler}` | `a: { onclick: fn }` |
|
|
1437
|
+
| **State declaration** | Declare reactive state | `const [x, setX] = useState(0)` | `const x = ref(0)` | `let x = 0` | `let x = $state(0)` | `const [x, setX] = createSignal(0)` | `o: { state: { x: 0 } }` |
|
|
1438
|
+
| **State update** | Change state and trigger re-render | `setX(42)` | `x.value = 42` | `x = 42; render()` | `x = 42` | `setX(42)` | `el._bw_state.x = 42; bw.update(el)` |
|
|
1439
|
+
| **Computed / derived** | Derive value from state | `useMemo(() => x * 2, [x])` | `computed(() => x.value * 2)` | `function get() { return x * 2; }` | `let d = $derived(x * 2)` | `const d = () => x() * 2` | `c: '${x}'` with Tier 2: `'${x * 2}'` |
|
|
1440
|
+
| **Side effect** | Run code on mount/change | `useEffect(() => {...}, [])` | `onMounted(() => {...})` | `window.addEventListener('load', fn)` | `$effect(() => {...})` | `onMount(() => {...})` | `o: { mounted: function(el) {...} }` |
|
|
1441
|
+
| **Cleanup on unmount** | Tear down timers/listeners | `useEffect return cleanup` | `onUnmounted(() => {...})` | manual | `return () => {...}` in `$effect` | `onCleanup(() => {...})` | `o: { unmount: fn }` or `bw.cleanup(el)` |
|
|
1442
|
+
| **Style inline** | Apply inline styles | `style={{color: 'red'}}` | `:style="{color: 'red'}"` | `el.style.color = 'red'` | `style="color:red"` | `style={{color: 'red'}}` | `a: { style: bw.s({ color: 'red' }) }` |
|
|
1443
|
+
| **Style composition** | Compose/merge styles | `{...base, ...override}` | `[baseStyle, overrideStyle]` | `Object.assign({}, base, over)` | `{...base, ...override}` | `{...base, ...override}` | `bw.s({ display: 'flex' }, { padding: '1rem' }, { color: accent })` |
|
|
1444
|
+
| **CSS class conditional** | Toggle classes | `className={active ? 'on' : ''}` | `:class="{on: active}"` | `el.classList.toggle('on')` | `class:on={active}` | `classList={{on: active()}}` | `a: { class: 'btn ' + (active ? 'on' : '') }` |
|
|
1445
|
+
| **Generate stylesheet** | Create CSS rules in JS | styled-components / emotion | `<style scoped>` | `style.textContent = css` | `<style>` block | `css\`...\`` | `bw.injectCSS(bw.css({ '.card': { padding: '1rem' } }))` |
|
|
1446
|
+
| **Responsive styles** | Breakpoint-based CSS | media query in CSS/styled | `@media` in `<style>` | `@media` in CSS file | `@media` in `<style>` | `@media` in CSS | `bw.responsive('.grid', { md: { columns: '1fr 1fr' } })` |
|
|
1447
|
+
| **Animation** | CSS keyframe animation | `@keyframes` in CSS file | `@keyframes` in `<style>` | `@keyframes` in CSS | `animate:fn` or CSS | CSS or WAAPI | `bw.css({ '@keyframes fade': { '0%': {opacity:'0'}, '100%': {opacity:'1'} } })` |
|
|
1448
|
+
| **Raw HTML** | Render unescaped HTML | `dangerouslySetInnerHTML` | `v-html="str"` | `el.innerHTML = str` | `{@html str}` | `innerHTML={str}` | `bw.raw(str)` in `c:` |
|
|
1449
|
+
| **Cross-component events** | Decouple communication | Context + useReducer / Zustand | provide/inject or Pinia | CustomEvent / EventTarget | stores | Context or signals | `bw.pub(topic, data)` / `bw.sub(topic, fn)` |
|
|
1450
|
+
| **Form input binding** | Read form values | `value={x} onChange={...}` | `v-model="x"` | `input.value` | `bind:value={x}` | `value={x()} onInput={...}` | `bw.$('#id')[0].value` or `bw.makeInput({oninput:fn})` |
|
|
1451
|
+
| **Theme / design tokens** | Apply consistent theming | ThemeProvider / CSS vars | CSS vars / provide | CSS custom properties | CSS vars | CSS vars / createContext | `bw.loadStyles({ primary: '#hex' })` or `bw.makeStyles(cfg)` => `theme.palette` |
|
|
1452
|
+
| **Build step required** | Required toolchain | Yes (Babel/Vite/webpack) | Yes (Vite or Vue CLI) | No | Yes (Svelte compiler) | Yes (Vite/Babel) | **No** — open the HTML file |
|
|
1453
|
+
| **Bundle size** | Shipped JS size | ~45KB (React + ReactDOM) | ~33KB (Vue 3) | 0KB | ~2KB (runtime) | ~7KB | **~40KB** (bitwrench UMD gzipped, includes 50+ components + CSS gen) |
|
|
1454
|
+
|
|
1455
|
+
---
|
|
1456
|
+
|
|
1457
|
+
*Bitwrench is maintained by [Manu Chatterjee](https://github.com/deftio) (deftio). BSD-2-Clause license.*
|