aberdeen 1.5.0 → 1.7.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/skill/SKILL.md CHANGED
@@ -1,290 +1,875 @@
1
1
  ---
2
2
  name: aberdeen
3
- description: Expert guidance for building reactive UIs with the Aberdeen library. Covers element creation with $, reactive state with proxy(), efficient lists with onEach(), two-way binding, CSS shortcuts, and advanced features like routing, transitions, and optimistic updates.
3
+ description: Expert guidance for building reactive UIs with the Aberdeen library. Covers element creation, reactive state management, efficient list rendering, CSS integration, routing, transitions, and optimistic updates.
4
4
  ---
5
5
 
6
- # Aberdeen
6
+ Aberdeen is a reactive UI library using fine-grained reactivity via JavaScript Proxies. No virtual DOM, no build step required.
7
7
 
8
- Reactive UI library using fine-grained reactivity via JS Proxies. No virtual DOM.
8
+ # Guidance for AI Assistants
9
9
 
10
- ## Imports
11
- ```typescript
12
- import { $, proxy, onEach, ref, mount, insertCss, insertGlobalCss, cssVars, derive, map, multiMap, partition, count, isEmpty, clean, invertString } from 'aberdeen';
13
- import { grow, shrink } from 'aberdeen/transitions'; // Optional
14
- import * as route from 'aberdeen/route'; // Optional
10
+ 1. **Use string syntax by default** - `$('div.box#Hello')` is more concise than object syntax
11
+ 2. **Never concatenate user data** - Use `$('input value=', data)` not `$('input value=${data}')`
12
+ 3. **Pass observables directly** - Use `text=', ref(obj, 'key')` to avoid parent scope subscriptions
13
+ 4. **Use `onEach` for lists** - Never iterate proxy arrays with `for`/`map` in render functions
14
+ 5. **Class instances are great** - Better than plain objects for typed, structured state
15
+ 6. **CSS shortcuts** - Use $3, $4 for spacing (1rem, 2rem), $primary for colors
16
+ 7. **Minimal scopes** - Smaller reactive scopes = fewer DOM updates
17
+
18
+ # Obtaining info
19
+
20
+ The complete tutorial follows below. For detailed API reference open these files within the skill directory:
21
+
22
+ - **[aberdeen](aberdeen.md)** - Core: `$`, `proxy`, `onEach`, `ref`, `derive`, `map`, `multiMap`, `partition`, `count`, `isEmpty`, `peek`, `dump`, `clean`, `insertCss`, `insertGlobalCss`, `mount`, `runQueue`, `darkMode`
23
+ - **[route](route.md)** - Routing: `current`, `go`, `push`, `back`, `up`, `persistScroll`
24
+ - **[dispatcher](dispatcher.md)** - Path matching: `Dispatcher`, `MATCH_REST`, `MATCH_FAILED`
25
+ - **[transitions](transitions.md)** - Animations: `grow`, `shrink`
26
+ - **[prediction](prediction.md)** - Optimistic UI: `applyPrediction`, `applyCanon`
27
+
28
+ # Tutorial
29
+
30
+ ## Creating elements
31
+
32
+ This is a complete Aberdeen application:
33
+
34
+ ```javascript
35
+ import {$} from 'aberdeen';
36
+ $('h3#Hello world');
15
37
  ```
16
38
 
17
- ## Element Creation: `$`
18
- ```typescript
19
- // Tag, classes, text content
20
- $('div.container.active#Hello World');
39
+ It adds a `<h3>Hello world</h3>` element to the `<body>` (which is the default mount point).
21
40
 
22
- // Nested elements in single call (each becomes child of previous)
23
- $('div.wrapper mt:@3 span.icon');
41
+ The {@link aberdeen.$} function accepts various forms of arguments, which can be combined.
24
42
 
25
- // Attributes/properties via string syntax (preferred)
26
- $('input placeholder=Name value=initial');
43
+ When a string is passed:
44
+ - The inital part (if any) is the name of the element to be created.
45
+ - One or multiple CSS classes can be added to the 'current' element, by prefixing them with a `.`.
46
+ - Content text can be added by prefixing it with a `#`.
27
47
 
28
- // Dynamic values: end key with `=`, next arg is value
29
- $('input placeholder="Something containing spaces" value=', userInput);
30
- $('button text=', `Count: ${state.count}`);
48
+ Instead of the `#` prefix for text content, you can also use the `text=` property, like this: `$('h3 text="Hello world"')`. The double quotes are needed here only because our text contains a space.
31
49
 
32
- // Event handlers
33
- $('button text=Click click=', () => console.log('clicked'));
50
+ `$()` can accept multiple strings, so the following lines are equivalent:
34
51
 
35
- // Nested content via function (creates reactive scope)
36
- $('ul', () => {
37
- $('li#Item 1');
38
- $('li#Item 2');
39
- });
52
+ ```javascript
53
+ $('button.outline.secondary#Pressing me does nothing!');
54
+ $('button', '.outline', '.secondary', '#Pressing me does nothing!');
40
55
  ```
41
56
 
42
- **Never concatenate user data into strings.** Use dynamic syntax:
43
- ```typescript
44
- // WRONG - XSS risk and breaks on special chars
45
- $(`input value=${userData}`);
57
+ Also, we can create multiple nested DOM elements in a single {@link aberdeen.$} invocation, *if* the parents need to have only a single child. For instance:
46
58
 
47
- // CORRECT
48
- $('input value=', userData);
59
+ ```javascript
60
+ $('div.box', '#Text within the div element...', 'input');
49
61
  ```
50
62
 
51
- ### String Syntax Reference
52
- | Syntax | Meaning |
53
- |--------|--------|
54
- | `tag` | Element name (creates child, becomes current element) |
55
- | `.class` | Add CSS class |
56
- | `#text` | Text content (rest of string) |
57
- | `prop:value` | Inline CSS style |
58
- | `attr=value` | Attribute with static string value |
59
- | `prop:` or `attr=` | Next argument is CSS prop/attribute/property/event listener |
63
+ Note that you can play around, modifying any example while seeing its live result by pressing the *Edit* button that appears when hovering over an example!
64
+
65
+ In order to pass in additional properties and attributes to the 'current' DOM element, we can use the `key=value` or `key=`, value syntax. So to extend the above example:
66
+
67
+ ```javascript
68
+ $('div.box id=cityContainer input value=London placeholder=City');
69
+ ```
70
+
71
+ Note that `value` doesn't become an HTML attribute. This (together with `selectedIndex`) is one of two special cases, where Aberdeen applies it as a DOM property instead, in order to preserve the variable type (as attributes can only be strings).
72
+
73
+ When a value ends with `=`, the next argument is used as its value. This is used for dynamic values and event listeners. So to always log the current input value to the console you can do:
74
+
75
+ ```javascript
76
+ $('div.box input value=Marshmallow input=', el => console.log(el.target.value));
77
+ ```
78
+
79
+ Note that the example is interactive - try typing something!
80
+
81
+ > **Note:** {@link aberdeen.$} also accepts object syntax as an alternative to strings (see the API reference), but the string syntax shown here is more concise and is recommended for most use cases.
82
+
83
+ ## Inline styles
84
+
85
+ To set inline CSS styles on elements, use the `property:value` (short form) or `property: value containing spaces;` (long form) syntax:
86
+
87
+ ```javascript
88
+ $('p color:red padding:8px background-color:#a882 border: 2px solid #a884; #Styled text');
89
+ ```
90
+
91
+ ### Property shortcuts
92
+
93
+ Aberdeen provides shortcuts for commonly used CSS properties, making your code more concise.
94
+
95
+ | Shortcut | Expands to |
96
+ |----------|------------|
97
+ | `m`, `mt`, `mb`, `ml`, `mr` | `margin`, `margin-top`, `margin-bottom`, `margin-left`, `margin-right` |
98
+ | `mv`, `mh` | Vertical (top+bottom) or horizontal (left+right) margins |
99
+ | `p`, `pt`, `pb`, `pl`, `pr` | `padding`, `padding-top`, `padding-bottom`, `padding-left`, `padding-right` |
100
+ | `pv`, `ph` | Vertical or horizontal padding |
101
+ | `w`, `h` | `width`, `height` |
102
+ | `bg` | `background` |
103
+ | `fg` | `color` |
104
+ | `r` | `border-radius` |
105
+
106
+ ```javascript
107
+ $('div mv:10px ph:20px bg:lightblue r:10% #Styled box');
108
+ ```
109
+
110
+ ### CSS variables
111
+
112
+ Values starting with `$` expand to native CSS custom properties via `var(--name)`. The {@link aberdeen.cssVars} object offers a convenient way of setting and updating CSS custom properties at the `:root` level.
113
+
114
+ When you add the first property to `cssVars`, Aberdeen automatically creates a reactive `<style>` tag in `<head>` containing the CSS custom property declarations.
115
+
116
+ ```javascript
117
+ import { $, cssVars } from 'aberdeen';
60
118
 
61
- ### Object Syntax (alternative)
62
- ```typescript
63
- // Equivalent to string syntax, useful for complex cases
64
- // Note how the '$' prefix is used for CSS properties
65
- $('input', { placeholder: 'Name', value: userData, $color: 'red' });
66
- $('button', { click: handler, '.active': isActive });
67
- ```
68
-
69
- ### CSS Property Shortcuts
70
- | Short | Full | Short | Full |
71
- |-------|------|-------|------|
72
- | `m` | margin | `p` | padding |
73
- | `mt`,`mb`,`ml`,`mr` | marginTop/Bottom/Left/Right | `pt`,`pb`,`pl`,`pr` | paddingTop/... |
74
- | `mv` | marginTop + marginBottom | `pv` | paddingTop + paddingBottom |
75
- | `mh` | marginLeft + marginRight | `ph` | paddingLeft + paddingRight |
76
- | `w` | width | `h` | height |
77
- | `bg` | background | `fg` | color |
78
- | `r` | borderRadius | | |
79
-
80
- ### CSS Variables (`@`)
81
- Values starting with `@` expand to native CSS custom properties via `var(--name)`. Numeric keys are prefixed with `m` (e.g., `@3` → `var(--m3)`).
82
-
83
- Predefined spacing scale:
84
- | Var | CSS Output | Value |
85
- |-----|------------|-------|
86
- | `@1` | `var(--m1)` | 0.25rem |
87
- | `@2` | `var(--m2)` | 0.5rem |
88
- | `@3` | `var(--m3)` | 1rem |
89
- | `@4` | `var(--m4)` | 2rem |
90
- | `@5` | `var(--m5)` | 4rem |
91
- | `@n` | `var(--mn)` | 2^(n-3) rem |
92
-
93
- **Best practice:** Use `@3` and `@4` for most margins/paddings. For new projects, define color variables:
94
- ```typescript
95
119
  cssVars.primary = '#3b82f6';
96
120
  cssVars.danger = '#ef4444';
97
- $('button bg:@primary fg:white#Save'); // outputs: background: var(--primary); color: white
121
+ cssVars.textLight = '#f8fafc';
122
+
123
+ $('button bg:$primary fg:$textLight #Primary');
124
+ $('button bg:$danger fg:$textLight #Danger');
98
125
  ```
99
126
 
100
- ## Reactive State: `proxy()`
101
- ```typescript
102
- // Objects (preserves type!)
103
- const state = proxy({ name: 'Alice', count: 0 });
127
+ The above generates CSS like `background: var(--primary)` and automatically injects a `:root` style defining the actual values. Since this uses native CSS custom properties, changes to `cssVars` automatically propagate to all elements using those values.
104
128
 
105
- // Primitives get wrapped in { value: T }
106
- const flag = proxy(true);
107
- flag.value = false;
129
+ ### Spacing variables
108
130
 
109
- // Class instances work great - use for typed state!
110
- class Todo {
111
- constructor(public text: string, public done = false) {}
112
- toggle() { this.done = !this.done; }
113
- }
114
- const todo: Todo = proxy(new Todo('Learn Aberdeen'));
115
- todo.toggle(); // Reactive method call!
131
+ You can optionally initialize `cssVars` with keys `1` through `12` mapping to an exponential `rem` scale using {@link aberdeen.setSpacingCssVars}. Since CSS custom property names can't start with a digit, numeric keys are prefixed with `m` (e.g., `$3` becomes `var(--m3)`):
132
+
133
+ ```javascript
134
+ import { setSpacingCssVars } from 'aberdeen';
116
135
 
117
- // Arrays
118
- const items = proxy<string[]>([]);
119
- items.push('new');
120
- delete items[0];
136
+ setSpacingCssVars(); // Default: base=1, unit='rem'
137
+ // Or customize: setSpacingCssVars(16, 'px') or setSpacingCssVars(1, 'em')
121
138
  ```
122
139
 
123
- ## Reactive Scopes
124
- Functions passed to `$` create **scopes**. When proxy data accessed inside changes:
125
- 1. All effects from previous run are **cleaned** (DOM elements removed, `clean()` callbacks run)
126
- 2. Function re-runs
140
+ | Value | CSS Output | Result (default) |
141
+ |-------|------------|------------------|
142
+ | `$1` | `var(--m1)` | 0.25rem |
143
+ | `$2` | `var(--m2)` | 0.5rem |
144
+ | `$3` | `var(--m3)` | 1rem |
145
+ | `$4` | `var(--m4)` | 2rem |
146
+ | `$5` | `var(--m5)` | 4rem |
147
+ | ... | ... | 2^(n-3) rem |
148
+
149
+ ```javascript
150
+ $('div mt:$3 ph:$4 #This text has 1rem top margin, 2rem left+right padding');
151
+ ```
127
152
 
128
- ```typescript
129
- $('div', () => {
130
- // Re-runs when state.name changes, replacing the h1
131
- $(`h1#Hello ${state.name}`);
153
+ If you want different spacing, you can customize the base and unit when calling `setSpacingCssVars()`, or dynamically modify the values.
154
+
155
+ These shortcuts and variables are also available when using {@link aberdeen.insertCss}.
156
+
157
+ ## Nesting content
158
+ Of course, putting everything in a single {@link aberdeen.$} call will get messy soon, and you'll often want to nest more than one child within a parent. To do that, you can pass in a *content* function to {@link aberdeen.$}, like this:
159
+
160
+ ```javascript
161
+ $('div.box.row id=cityContainer', () => {
162
+ $('input value=London placeholder=City');
163
+ $('button text=Confirm click=', () => alert("You got it!"));
132
164
  });
133
165
  ```
134
166
 
135
- ### Granular Updates
136
- Split scopes for minimal DOM updates:
137
- ```typescript
167
+ Why are we passing in a function instead of just, say, an array of children? I'm glad you asked! :-) For each such function Aberdeen will create an *observer*, which will play a major part in what comes next...
168
+
169
+ ## Observable objects
170
+ Aberdeen's reactivity system is built around observable objects. These are created using the {@link aberdeen.proxy} function:
171
+
172
+ When you access properties of a proxied object within an observer function (the function passed to {@link aberdeen.$}), Aberdeen automatically tracks these dependencies. If the values change later, the observer function will re-run, updating only the affected parts of the DOM.
173
+
174
+ ```javascript
175
+ import { $, proxy } from 'aberdeen';
176
+
177
+ const user = proxy({
178
+ name: 'Alice',
179
+ age: 28,
180
+ city: 'Aberdeen',
181
+ });
182
+
138
183
  $('div', () => {
139
- $('h1', () => $(`#${state.title}`)); // Only title text re-renders
140
- $('p', () => $(`#${state.body}`)); // Only body text re-renders
141
- // Or
142
- $('p#', ref(state, 'body')); // Two-way maps {body: x} to {value: x}, which will be read reactively by $
184
+ $(`h3#Hello, ${user.name}!`);
185
+ $(`p#You are ${user.age} years old.`);
143
186
  });
187
+
188
+ setInterval(() => {
189
+ user.name = 'Bob';
190
+ user.age++;
191
+ }, 2000);
144
192
  ```
145
193
 
146
- ### Passing Observables Directly
147
- Avoid subscribing in parent scope:
148
- ```typescript
194
+ As the content function of our `div` is subscribed to both `user.name` and `user.age`, modifying either of these would trigger a re-run of that function, first undoing any side-effects (most notably: inserting DOM elements) of the earlier run. If, however `user.city` is changed, no re-run would be triggered as the function is not subscribed to that property.
195
+
196
+ So if either property changes, both the `<h3>` and `<p>` are recreated as the inner most observer function tracking the changes is re-run. If you want to redraw on an even granular level, you can of course:
197
+
198
+ ```javascript
199
+ const user = proxy({
200
+ name: 'Alice',
201
+ age: 28,
202
+ });
203
+
149
204
  $('div', () => {
150
- // Not great: reruns this scope when state.count changes
151
- $('span text=', state.count); // Subscribes here!
205
+ $(`h3`, () => {
206
+ console.log('Name draws:', user.name)
207
+ $(`#Hello, ${user.name}!`);
208
+ });
209
+ $(`p`, () => {
210
+ console.log('Age draws:', user.age)
211
+ $(`#You are ${user.age} years old.`);
212
+ });
213
+ });
214
+
215
+ setInterval(() => {
216
+ user.age++;
217
+ }, 2000);
218
+ ```
219
+
220
+ Now, updating `user.name` would only cause the *Hello* text node to be replaced, leaving the `<div>`, `<h3>` and `<p>` elements as they were.
221
+
222
+ ## Conditional rendering
223
+
224
+ Within an observer function (such as created by passing a function to {@link aberdeen.$}), you can use regular JavaScript logic. Like `if` and `else`, for instance:
225
+
226
+ ```javascript
227
+ const user = proxy({
228
+ loggedIn: false
152
229
  });
153
230
 
154
231
  $('div', () => {
155
- // Good: only text node updates, this function does not rerun
156
- $('span text=', ref(state, 'count')); // Passes observable, subscribes internally
232
+ if (user.loggedIn) {
233
+ $('button.outline text=Logout click=', () => user.loggedIn = false);
234
+ } else {
235
+ $('button text=Login click=', () => user.loggedIn = true);
236
+ }
157
237
  });
158
238
  ```
159
239
 
160
- Or just use a single-value proxy `const count = proxy(0);` and pass it directly `$('span text=', count);`.
240
+ ## Observable primitive values
161
241
 
162
- ### Manual Cleanup with `clean()`
163
- Register cleanup for non-$ side effects:
164
- ```typescript
165
- $(() => {
166
- if (!reactive.value) return;
167
- const timer = setInterval(() => console.log('tick'), 1000);
168
- clean(() => clearInterval(timer)); // Runs on scope cleanup
242
+ The {@link aberdeen.proxy} method wraps an object in a JavaScript [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). As this doesn't work for primitive values (like numbers, strings and booleans), the method will *create* an object in order to make it observable. The observable value is made available as its `.value` property.
243
+
244
+ ```javascript
245
+ const cnt = proxy(42);
246
+ $('div.row', () => {
247
+ // This scope will not have to redraw
248
+ $('button text=- click=', () => cnt.value--);
249
+ $('div text=', cnt);
250
+ $('button text=+ click=', () => cnt.value++);
169
251
  });
170
252
  ```
171
253
 
172
- ## Lists: `onEach()`
173
- ```typescript
174
- onEach(items, (item, index) => {
175
- $('li', () => $(`#${item.text}`));
176
- }, item => item.id); // Optional sort key
254
+ The reason the `div.row` scope doesn't redraw when `cnt.value` changes is that we're passing the entire `cnt` observable object to the `text:` property. Aberdeen then internally subscribes to `cnt.value` for just that text node, ensuring minimal updates.
255
+
256
+ If we would have done `$('div', {text: count.value});` instead, we *would* have subscribed to `count.value` within the `div.row` scope, meaning we'd be redrawing the two buttons and the div every time the count changes.
257
+
258
+ This also works for other properties, such as inline styles:
259
+
260
+ ```javascript
261
+ import { $, proxy } from 'aberdeen';
262
+
263
+ const textColor = proxy('blue');
264
+
265
+ $('div.box color:', textColor, '#Click me to change color', 'click=', () => {
266
+ textColor.value = textColor.value === 'blue' ? 'red' : 'blue';
267
+ });
177
268
  ```
178
- - Renders only changed items efficiently
179
- - Sort key: `number | string | [number|string, ...]` or `undefined` to hide item
180
- - Use `invertString(str)` for descending string sort
181
- - Works on arrays, objects, and Maps
182
269
 
183
- ## Two-Way Binding
184
- ```typescript
185
- $('input bind=', ref(state, 'name'));
186
- $('input type=checkbox bind=', ref(state, 'active'));
187
- $('select bind=', ref(state, 'choice'), () => {
188
- $('option value=a#Option A');
189
- $('option value=b#Option B');
270
+ This way, when `textColor.value` changes, only the style is updated without recreating the element.
271
+
272
+
273
+ ## Observable arrays
274
+
275
+ You can create observable arrays too. They work just like regular arrays, apart from being observable.
276
+
277
+ ```javascript
278
+ const items = proxy([1, 2, 3]);
279
+
280
+ $('h3', () => {
281
+ // This subscribes to the length of the array and to the value at `items.length-1` in the array.
282
+ $('#Last item: '+items[items.length-1]);
283
+ })
284
+
285
+ $('ul', () => {
286
+ // This subscribes to the entire array, and thus redraws all <li>s when any item changes.
287
+ // In the next section, we'll learn about a better way.
288
+ for (const item of items) {
289
+ $(`li#Item ${item}`);
290
+ }
190
291
  });
292
+
293
+ $('button text=Add click=', () => items.push(items.length+1));
191
294
  ```
192
295
 
193
- ## Derived Values
296
+ ## TypeScript and classes
297
+
298
+ Though this tutorial mostly uses plain JavaScript to explain the concepts, Aberdeen is written in and aimed towards TypeScript.
299
+
300
+ Class instances, like any other object, can be proxied to make them reactive.
194
301
 
195
- ### `derive()` - Derived primitives
196
302
  ```typescript
197
- const doubled: { value: number } = derive(() => state.count * 2);
198
- $('span text=', doubled);
303
+ class Widget {
304
+ constructor(public name: string, public width: number, public height: number) {}
305
+ grow() { this.width *= 2; }
306
+ toString() { return `${this.name}Widget (${this.width}x${this.height})`; }
307
+ }
308
+
309
+ let graph: Widget = proxy(new Widget('Graph', 200, 100));
310
+
311
+ $('h3', () => $('#'+graph));
312
+ $('button text=Grow click=', () => graph.grow());
199
313
  ```
200
314
 
201
- ### Collection functions
202
- ```typescript
203
- // count() - returns { value: number } proxy
204
- const total: { value: number } = count(items);
315
+ The type returned by {@link aberdeen.proxy} matches the input type, meaning the type system does not distinguish proxied and unproxied objects. That makes sense, as they have the exact same methods and properties (though proxied objects may have additional side effects).
316
+
317
+
318
+ ## Efficient list rendering with onEach
319
+ For rendering lists efficiently, Aberdeen provides the {@link aberdeen.onEach} function. It takes three arguments:
320
+ 1. The array to iterate over.
321
+ 2. A render function that receives the item and its index.
322
+ 3. An optional order function, that returns the value by which the item is to be sorted. By default, the output is sorted by array index.
323
+
324
+ ```javascript
325
+ import { $, proxy, onEach } from 'aberdeen';
326
+
327
+ const items = proxy([]);
328
+
329
+ const randomInt = (max) => parseInt(Math.random() * max);
330
+ const randomWord = () => Math.random().toString(36).substring(2, 12).replace(/[0-9]+/g, '').replace(/^\w/, c => c.toUpperCase());
331
+
332
+ // Make random mutations
333
+ setInterval(() => {
334
+ if (randomInt(3)) items[randomInt(7)] = {label: randomWord(), prio: randomInt(4)};
335
+ else delete items[randomInt(7)];
336
+ }, 500);
337
+
338
+ $('div.row.wide height:250px', () => {
339
+ $('div.box#By index', () => {
340
+ onEach(items, (item, index) => {
341
+ // Called only for items that are created/updated
342
+ $(`li#${item.label} (prio ${item.prio})`)
343
+ });
344
+ })
345
+ $('div.box#By label', () => {
346
+ onEach(items, (item, index) => {
347
+ $(`li#${item.label} (prio ${item.prio})`)
348
+ }, item => item.label);
349
+ })
350
+ $('div.box#By desc prio, then label', () => {
351
+ onEach(items, (item, index) => {
352
+ $(`li#${item.label} (prio ${item.prio})`)
353
+ }, item => [-item.prio, item.label]);
354
+ })
355
+ })
356
+ ```
357
+
358
+ We can also use {@link aberdeen.onEach} to reactively iterate over *objects*. In that case, the render and order functions receive `(value, key)` instead of `(value, index)` as their arguments.
205
359
 
206
- // isEmpty() - returns boolean, re-runs scope only when emptiness changes
207
- if (isEmpty(items)) $('p#No items');
360
+ ```javascript
361
+ const pairs = proxy({A: 'Y', B: 'X',});
208
362
 
209
- // map() - returns proxied array/object of same shape
210
- const names: string[] = map(users, u => u.active ? u.name : undefined);
363
+ const randomWord = () => Math.random().toString(36).substring(2, 12).replace(/[0-9]+/g, '').replace(/^\w/, c => c.toUpperCase());
211
364
 
212
- // multiMap() - each input produces multiple outputs
213
- const byId: Record<string, User> = multiMap(users, u => ({ [u.id]: u }));
365
+ $('button text="Add item" click=', () => pairs[randomWord()] = randomWord());
214
366
 
215
- // partition() - sort items into buckets
216
- const byStatus: Record<string, Record<number, Task>> = partition(tasks, t => t.status);
367
+ $('div.row.wide margin-top:1em', () => {
368
+ $('div.box#By key', () => {
369
+ onEach(pairs, (value, key) => {
370
+ $(`li#${key}: ${value}`)
371
+ });
372
+ })
373
+ $('div.box#By desc value', () => {
374
+ onEach(pairs, (value, key) => {
375
+ $(`li#${key}: ${value}`)
376
+ }, value => invertString(value));
377
+ })
378
+ })
217
379
  ```
218
380
 
219
- ## Component-Local CSS
220
- `insertCss` returns a unique class name (e.g., `.AbdStl1`). Call at **module top-level**, not inside render functions:
221
- ```typescript
222
- // At top of file
223
- const boxStyle = insertCss({
224
- bg: '@primary',
225
- r: '@2',
226
- button: {
227
- m: '@2',
228
- '&:hover': { opacity: 0.8 }
229
- }
381
+ Note the use of the provided {@link aberdeen.invertString} function to reverse-sort by a string value.
382
+
383
+ ## Two-way binding
384
+ Aberdeen makes it easy to create two-way bindings between form elements (the various `<input>` types, `<textarea>` and `<select>`) and your data, by passing an observable object with a `.value` as `bind:` property to {@link aberdeen.$}.
385
+
386
+ To bind to object properties not named .value (e.g., user.name), use {@link aberdeen.ref}. This creates a new observable proxy whose .value property directly maps to the specified property (e.g., name) on your original observable object (e.g., user).
387
+
388
+ ```javascript
389
+ import { $, proxy, ref } from 'aberdeen';
390
+
391
+ const user = proxy({
392
+ name: 'Alice',
393
+ active: false
230
394
  });
231
395
 
232
- // In render code
233
- $('div', boxStyle, 'button#Click');
396
+ // Text input binding
397
+ $('input placeholder=Name bind=', ref(user, 'name'));
398
+
399
+ // Checkbox binding
400
+ $('label', () => {
401
+ $('input type=checkbox bind=', ref(user, 'active'));
402
+ }, '#Active');
403
+
404
+ // Display the current state
405
+ $('div.box', () => {
406
+ $(`p#Name: ${user.name} `, () => {
407
+ // Binding works both ways
408
+ $('button.outline.secondary#!', {
409
+ click: () => user.name += '!'
410
+ });
411
+ });
412
+ $(`p#Status: ${user.active ? 'Active' : 'Inactive'}`);
413
+ });
234
414
  ```
235
415
 
236
- For global styles (no class prefix):
237
- ```typescript
416
+ ## CSS
417
+ Through the {@link aberdeen.insertCss} function, Aberdeen provides a way to create component-local CSS.
418
+
419
+ For simple single-element styles, you can pass a string directly:
420
+
421
+ ```javascript
422
+ import { $, insertCss } from 'aberdeen';
423
+
424
+ const simpleCard = insertCss("bg:#f0f0f0 p:$3 r:8px");
425
+ $('div', simpleCard, '#Card content');
426
+ ```
427
+
428
+ For more complex styles with nested selectors, pass an object where each key is a selector and each value is a style string using the same `property:value` syntax as inline styles:
429
+
430
+ ```javascript
431
+ import { $, insertCss } from 'aberdeen';
432
+
433
+ // Create a CSS class that can be applied to elements
434
+ const myBoxStyle = insertCss({
435
+ "&": "border-color:#6936cd background-color:#1b0447",
436
+ "button": "background-color:#6936cd border:0 transition: box-shadow 0.3s; box-shadow: 0 0 4px #ff6a0044;",
437
+ "button:hover": "box-shadow: 0 0 16px #ff6a0088;"
438
+ });
439
+
440
+ // myBoxStyle is now something like ".AbdStl1", the name for a generated CSS class.
441
+ // Here's how to use it:
442
+ $('div.box', myBoxStyle, 'button#Click me');
443
+ ```
444
+
445
+ The `"&"` selector refers to the element with the generated class itself. Child selectors like `"button"` are scoped to descendants of that element, while pseudo-selectors like `"&:hover"` apply to the element itself.
446
+
447
+ This allows you to create single-file components with advanced CSS rules. The {@link aberdeen.insertGlobalCss} function can be used to add CSS without a class prefix - it accepts the same string or object syntax.
448
+
449
+ Both functions support the same CSS shortcuts and variables as inline styles (see above). For example:
450
+
451
+ ```javascript
452
+ import { cssVars, insertGlobalCss } from 'aberdeen';
453
+ cssVars.boxBg = '#f0f0e0';
238
454
  insertGlobalCss({
239
- body: { m: 0, fontFamily: 'system-ui' },
240
- 'a': { fg: '@primary' }
455
+ "body": "m:0", // Using shortcut for margin
456
+ "form": "bg:$boxBg mv:$3" // Using background shortcut, CSS variable, and spacing value
241
457
  });
242
458
  ```
243
459
 
244
- CSS can be reactive when needed (e.g., theme switching):
245
- ```typescript
460
+ Of course, if you dislike JavaScript-based CSS and/or prefer to use some other way to style your components, you can just ignore this Aberdeen function.
461
+
462
+ ## Transitions
463
+ Aberdeen allows you to easily apply transitions on element creation and element destruction:
464
+
465
+ ```javascript
466
+ let titleStyle = insertCss({
467
+ "&": "transition: all 1s ease-out; transform-origin: left center;",
468
+ "&.faded": "opacity:0",
469
+ "&.imploded": "transform:scale(0.1)",
470
+ "&.exploded": "transform:scale(5)"
471
+ });
472
+
473
+ const show = proxy(true);
474
+ $('label', () => {
475
+ $('input type=checkbox bind=', show);
476
+ $('#Show title');
477
+ });
246
478
  $(() => {
247
- insertCss({ bg: theme.dark ? '#222' : '#fff' });
479
+ if (!show.value) return;
480
+ $('h2#(Dis)appearing text', titleStyle, 'create=faded.imploded destroy=faded.exploded');
248
481
  });
249
482
  ```
250
483
 
251
- ## Transitions
252
- The `create` and `destroy` properties enable enter/leave animations:
253
- ```typescript
484
+ - The creation transition works by briefly adding the given CSS classes on element creation, and immediately removing them after the initial browser layout has taken place.
485
+ - The destruction transition works by delaying the removal of the element from the DOM by two seconds (currently hardcoded - should be enough for any reasonable transition), while adding the given CSS classes.
486
+
487
+ Though this approach is easy (you just need to provide some CSS), you may require more control over the specifics, for instance in order to animate the layout height (or width) taken by the element as well. (Note how the document height changes in the example above are rather ugly.) For this, `create` and `destroy` may be functions instead of CSS class names. For more control, create and destroy can also accept functions. While custom function details are beyond this tutorial, Aberdeen offers ready-made {@link transitions.grow} and {@link transitions.shrink} transition functions (which also serve as excellent examples for creating your own):
488
+
489
+ ```javascript
490
+ import { $, proxy, onEach } from 'aberdeen';
254
491
  import { grow, shrink } from 'aberdeen/transitions';
255
492
 
256
- // Built-in grow/shrink for smooth height/width animations
257
- onEach(items, item => {
258
- $('li create=', grow, 'destroy=', shrink, `#${item.text}`);
493
+ const items = proxy([]);
494
+
495
+ const randomInt = (max) => parseInt(Math.random() * max);
496
+ const randomWord = () => Math.random().toString(36).substring(2, 12).replace(/[0-9]+/g, '').replace(/^\w/, c => c.toUpperCase());
497
+
498
+ // Make random mutations
499
+ setInterval(() => {
500
+ if (randomInt(3)) items[randomInt(7)] = {label: randomWord(), prio: randomInt(4)};
501
+ else delete items[randomInt(7)];
502
+ }, 500);
503
+
504
+ $('div.row.wide height:250px', () => {
505
+ $('div.box#By index', () => {
506
+ onEach(items, (item, index) => {
507
+ $(`li#${item.label} (prio ${item.prio})`, {create: grow, destroy: shrink})
508
+ });
509
+ })
510
+ $('div.box#By label', () => {
511
+ onEach(items, (item, index) => {
512
+ $(`li#${item.label} (prio ${item.prio})`, {create: grow, destroy: shrink})
513
+ }, item => item.label);
514
+ })
515
+ $('div.box#By desc prio, then label', () => {
516
+ onEach(items, (item, index) => {
517
+ $(`li#${item.label} (prio ${item.prio})`, {create: grow, destroy: shrink})
518
+ }, item => [-item.prio, item.label]);
519
+ })
520
+ });
521
+ ```
522
+
523
+ ## Advanced: Peeking without subscribing
524
+
525
+ Sometimes you need to read reactive data inside an observer scope without creating a subscription to that data. The {@link aberdeen.peek} function allows you to do this:
526
+
527
+ ```javascript
528
+ import { $, proxy, peek } from 'aberdeen';
529
+
530
+ const data = proxy({ a: 1, b: 2 });
531
+
532
+ $(() => {
533
+ // This scope only re-runs when data.a changes
534
+ // Changes to data.b won't trigger a re-render
535
+ $(`h2#a == ${data.a} && b == ${peek(data, 'b')}`);
536
+ });
537
+
538
+ $(`button text="a++ (will update)" click=`, () => data.a++);
539
+ $(`button ml:1rem text="b++ (won't update)" click=`, () => data.b++);
540
+ ```
541
+
542
+ You can also pass a function to `peek()` to execute it without any subscriptions:
543
+
544
+ ```javascript
545
+ const a = proxy(42);
546
+ const b = proxy(7);
547
+ const sum = peek(() => a.value + b.value); // Reads both without subscribing
548
+ $('#Sum is: '+sum);
549
+ setInterval(() => a.value++, 1000); // Won't update
550
+ ```
551
+
552
+ This can be useful to avoid rerenders (of even rerender loops) when you only need a point-in-time snapshot of some reactive data.
553
+
554
+ ## Derived values
555
+ An observer scope doesn't *need* to create DOM elements. It may also perform other side effects, such as modifying other observable objects. For instance:
556
+
557
+ ```javascript
558
+ // NOTE: See below for a better way.
559
+ const original = proxy(1);
560
+ const derived = proxy();
561
+ $(() => {
562
+ derived.value = original.value * 42;
563
+ });
564
+
565
+ $('h3 text=', derived);
566
+ $('button text=Increment click=', () => original.value++);
567
+ ```
568
+
569
+ The {@link aberdeen.derive} function makes the above a little easier. It works just like passing a function to {@link aberdeen.$}, creating an observer, the only difference being that the value returned by the function is reactively assigned to the `value` property of the observable object returned by `derive`. So the above could also be written as:
570
+
571
+ ```javascript
572
+ const original = proxy(1);
573
+ const derived = derive(() => original.value * 42);
574
+
575
+ $('h3 text=', derived);
576
+ $('button text=Increment click=', () => original.value++);
577
+ ```
578
+
579
+ For deriving values from (possibly large) arrays or objects, Aberdeen provides specialized functions that enable fast, incremental updates to derived data: {@link aberdeen.map} (each item becomes zero or one derived item), {@link aberdeen.multiMap} (each item becomes any number of derived items), {@link aberdeen.count} (reactively counts the number of object properties), {@link aberdeen.isEmpty} (true when the object/array has no items) and {@link aberdeen.partition} (sorts each item into one or more buckets). An example:
580
+
581
+ ```javascript
582
+ import * as aberdeen from 'aberdeen';
583
+ const {$, proxy} = aberdeen;
584
+
585
+ // Create some random data
586
+ const people = proxy({});
587
+ const randomInt = (max) => parseInt(Math.random() * max);
588
+ setInterval(() => {
589
+ people[randomInt(250)] = {height: 150+randomInt(60), weight: 45+randomInt(90)};
590
+ }, 250);
591
+
592
+ // Do some mapping, counting and observing
593
+ const totalCount = aberdeen.count(people);
594
+ const bmis = aberdeen.map(people,
595
+ person => Math.round(person.weight / ((person.height/100) ** 2))
596
+ );
597
+ const overweightBmis = aberdeen.map(bmis, // Use map() as a filter
598
+ bmi => bmi > 25 ? bmi : undefined
599
+ );
600
+ const overweightCount = aberdeen.count(overweightBmis);
601
+ const message = aberdeen.derive(
602
+ () => `There are ${totalCount.value} people, of which ${overweightCount.value} are overweight.`
603
+ );
604
+
605
+ // Show the results
606
+ $('p text=', message);
607
+ $(() => {
608
+ // isEmpty only causes a re-run when the count changes between zero and non-zero
609
+ if (aberdeen.isEmpty(overweightBmis)) return;
610
+ $('p#These are their BMIs:', () => {
611
+ aberdeen.onEach(overweightBmis, bmi => $('# '+bmi), bmi => -bmi);
612
+ // Sort by descending BMI
613
+ });
614
+ })
615
+ ```
616
+
617
+ ## Debugging with dump()
618
+
619
+ The {@link aberdeen.dump} function creates a live, interactive tree view of any data structure in the DOM. It's particularly useful for debugging reactive state:
620
+
621
+ ```javascript
622
+ import { $, proxy, dump } from 'aberdeen';
623
+
624
+ const state = proxy({
625
+ user: { name: 'Frank', kids: 1 },
626
+ items: ['a', 'b']
627
+ });
628
+
629
+ $('h2#Live State Dump');
630
+ dump(state);
631
+
632
+ // The dump updates automatically as state changes
633
+ $('button text="Update state" click=', () => {
634
+ state.user.kids++;
635
+ state.items.push('new');
636
+ });
637
+ ```
638
+
639
+ The dump renders recursively using `<ul>` and `<li>` elements, showing all properties and their values. It updates reactively when any proxied data changes. It is intended for debugging, though with some CSS styling you may find it useful in some simple real-world scenarios as well.
640
+
641
+
642
+ ## html-to-aberdeen
643
+
644
+ Sometimes, you want to just paste a largish block of HTML into your application (and then maybe modify it to bind some actual data). Having to translate HTML to `$` calls manually is little fun, so there's a tool for that:
645
+
646
+ ```sh
647
+ npx html-to-aberdeen
648
+ ```
649
+
650
+ It takes HTML on stdin (paste it and press `ctrl-d` for end-of-file), and outputs JavaScript on stdout.
651
+
652
+ > Caveat: This tool has been vibe coded (thanks Claude!) with very little code review. As it doesn't use the filesystem nor the network, I'd say it's safe to use though! :-) Also, it happens to work pretty well.
653
+
654
+ ## Routing
655
+
656
+ Aberdeen provides an optional built-in router via the {@link route} module. The router is reactive and integrates seamlessly with browser history.
657
+
658
+ The {@link route.current} object is an observable that reflects the current URL:
659
+
660
+ ```javascript
661
+ import { $ } from 'aberdeen';
662
+ import * as route from 'aberdeen/route';
663
+
664
+ $(() => {
665
+ $(`p#Path string: ${route.current.path}`); // eg "/example/123"
666
+ $(`p#Path segments: ${JSON.stringify(route.current.p)}`); // eg ["example", "123"]
259
667
  });
668
+ ```
260
669
 
261
- // CSS class-based transitions
262
- $('div create=.fade-in destroy=.fade-out#Animated');
263
- // On create: class added briefly then removed (after layout)
264
- // On destroy: class added, element removed after 2s delay
670
+ To navigate programmatically, use {@link route.go}:
671
+
672
+ ```javascript
673
+ import { $ } from 'aberdeen';
674
+ import * as route from 'aberdeen/route';
675
+ console.log('pn', location.protocol, location.host, location.hostname, location.pathname);
676
+
677
+ $('button#Go to settings', {
678
+ click: () => route.go('/settings')
679
+ });
680
+
681
+ // Or using path segments
682
+ $('button ml:1rem #Go to user 123', {
683
+ click: () => route.go({p: ['users', 123]})
684
+ });
265
685
  ```
266
686
 
267
- Only triggers for **top-level** elements of a (re-)running scope, not deeply nested children.
687
+ For convenience, you can call {@link route.interceptLinks} once to automatically convert clicks on local `<a>` tags into Aberdeen routing, so you can use regular anchor tags without manual click handlers. Example: `$('a href=/settings text=Settings')`.
688
+
689
+ ```javascript
690
+ import { $ } from 'aberdeen';
691
+ import * as route from 'aberdeen/route';
692
+
693
+ route.interceptLinks(); // Just once on startup:
268
694
 
269
- ## HTML Conversion Tool
270
- Convert HTML to Aberdeen syntax:
271
- ```bash
272
- echo '<div class="box"><p>Hello</p></div>' | npx html-to-aberdeen
273
- # Output: $('div.box', () => { $('p#Hello'); });
695
+ $('a role=button href=/settings #Go to settings')
274
696
  ```
275
697
 
276
- ## Advanced Features
277
- - **Routing**: [references/routing.md](references/routing.md) - Browser history routing and path dispatching
278
- - **Transitions**: [references/transitions.md](references/transitions.md) - Detailed animation patterns
279
- - **Predictions**: [references/prediction.md](references/prediction.md) - Optimistic UI with auto-revert
698
+ The {@link route.push} function is useful for overlays that should be closeable with browser back:
699
+
700
+ ```javascript
701
+ import { $ } from 'aberdeen';
702
+ import * as route from 'aberdeen/route';
703
+
704
+ $('button#Open modal', {
705
+ click: () => route.push({state: {modal: 'settings'}})
706
+ });
707
+
708
+ $(() => {
709
+ if (!route.current.state.modal) return;
710
+ $('div.modal-overlay', {
711
+ click: () => route.back({state: {modal: undefined}})
712
+ }, () => {
713
+ $('div.modal#Modal content here');
714
+ });
715
+ });
716
+ ```
717
+
718
+ Optionally, you can use the {@link dispatcher.Dispatcher} class for declarative routing. It allows you to register route patterns with associated handler functions, which are invoked when the current route matches the pattern. It can match typed parameters and rest parameters.
719
+
720
+ ## Prediction
721
+
722
+ When building interactive applications with client-server communication, Aberdeen's prediction system allows for optimistic UI updates. The {@link prediction.applyPrediction} function records changes to any proxied objects made within its callback. These changes are treated as *predictions* that may later be confirmed or reverted based on server responses. When a server response arrives, the {@link prediction.applyCanon} function applies authoritative changes from the server, reverting any conflicting predictions while attempting to reapply non-conflicting ones.
723
+
724
+ ## Full Example: Multi-page App
725
+
726
+ Here's a complete example (a contact manager) demonstrating routing, state management, CSS, dark mode, and dynamic content:
727
+
728
+ ```typescript
729
+ import { $, proxy, onEach, cssVars, ref, darkMode, insertCss, insertGlobalCss, setSpacingCssVars, map } from 'aberdeen';
730
+ import * as route from 'aberdeen/route';
731
+ import { Dispatcher } from 'aberdeen/dispatcher';
732
+ import { grow, shrink } from 'aberdeen/transitions';
733
+
734
+ class Contact {
735
+ constructor(
736
+ public id: number,
737
+ public firstName: string,
738
+ public lastName: string,
739
+ public email: string,
740
+ public phone: string
741
+ ) {}
742
+ }
743
+
744
+ // Enable link interception for SPA navigation
745
+ route.interceptLinks();
746
+
747
+ // Initialize $1-$12 CSS variables for consistent spacing ($2=0.5rem, $3=1rem, $4=2rem, etc.)
748
+ setSpacingCssVars();
749
+
750
+ // Reactive theme based on system preference
751
+ $(() => {
752
+ cssVars.primary = '#2563eb';
753
+ cssVars.bg = darkMode() ? '#0f172a' : '#ffffff';
754
+ cssVars.fg = darkMode() ? '#e2e8f0' : '#1e293b';
755
+ cssVars.cardBg = darkMode() ? '#1e293b' : '#f8fafc';
756
+ cssVars.border = darkMode() ? '#334155' : '#e2e8f0';
757
+ });
758
+
759
+ // Global styles for semantic HTML elements that apply everywhere
760
+ insertGlobalCss({
761
+ "*": "m:0 p:0",
762
+ "body": "bg:$bg fg:$fg font-family: system-ui, sans-serif;",
763
+ "a": "color:$primary text-decoration:none",
764
+ "a:hover": "text-decoration:underline",
765
+ "a[role=button]": "bg:$primary fg:white r:8px p:$2",
766
+ });
767
+
768
+ // Application state
769
+ const contacts = proxy([
770
+ new Contact(1, 'Emma', 'Wilson', 'emma.wilson@email.com', '555-0101'),
771
+ new Contact(2, 'James', 'Anderson', 'j.anderson@email.com', '555-0102'),
772
+ new Contact(3, 'Sofia', 'Martinez', 'sofia.m@email.com', '555-0103'),
773
+ new Contact(4, 'Liam', 'Brown', 'liam.brown@email.com', '555-0104')
774
+ ]);
775
+
776
+ // Router setup
777
+ const dispatcher = new Dispatcher();
778
+ dispatcher.addRoute(drawHome);
779
+ dispatcher.addRoute('contacts', drawContactList);
780
+ dispatcher.addRoute('contacts', Number, drawContactDetail);
781
+
782
+ // Main app
783
+ $('div.app', () => {
784
+ $('nav display:flex gap:$3 p:$3 border-bottom: 1px solid $border;', () => {
785
+ $('a href=/ text=Home font-weight:', route.current.p.length === 0 ? 'bold' : 'normal');
786
+ $('a href=/contacts text=Contacts font-weight:', route.current.p[0] === 'contacts' ? 'bold' : 'normal');
787
+ });
788
+ $('main p:$3', () => dispatcher.dispatch(route.current.p));
789
+ });
790
+
791
+ function drawHome() {
792
+ $('h1#Contact Manager');
793
+ $('p#A modern contact list with search, sort, and dark mode support.');
794
+ }
795
+
796
+ // Contact card styles
797
+ const cardStyle = insertCss({
798
+ "&": "bg:$cardBg border: 1px solid $border; r:8px p:$3 mv:$2 display:block transition: transform 0.2s;",
799
+ "&:hover": "transform:translateX(4px)",
800
+ "a&": "color:inherit;",
801
+ });
802
+
803
+ const filterStyle = insertCss({
804
+ "&": "display:flex gap:$3 mv:$3",
805
+ "> *": "p:$2 r:4px bg:$bg fg:$fg border: 1px solid $border;",
806
+ });
807
+
808
+ function drawContactList() {
809
+ $('h1#Contacts');
810
+
811
+ // Search and sort controls
812
+ $('div', filterStyle, () => {
813
+ $('input flex:1 placeholder="Search contacts..." bind=', ref(route.current.search, 'q'));
814
+ $('select bind=', ref(route.current.search, 'sort'), () => {
815
+ $('option value=firstName #First Name');
816
+ $('option value=lastName #Last Name');
817
+ $('option value=email #Email');
818
+ });
819
+ });
820
+
821
+ // Contact list
822
+ $('div', () => {
823
+ const sortBy = route.current.search.sort || 'firstName';
824
+
825
+ const filtered = map(contacts, contact => {
826
+ const query = route.current.search.q;
827
+ if (query) {
828
+ const info = `${contact.firstName} ${contact.lastName} ${contact.email}`;
829
+ if (!info.toLowerCase().includes(query.toLowerCase())) return; // Skip!
830
+ }
831
+ return contact;
832
+ });
833
+
834
+ onEach(filtered, contact => {
835
+ $('a', cardStyle, 'create=', grow, 'destroy=', shrink, `href=/contacts/${contact.id}`, () => {
836
+ $('h2', () => {
837
+ $('span font-weight:normal text=', contact.firstName+" ");
838
+ $('span text=', contact.lastName);
839
+ });
840
+ $('div text=', contact.email);
841
+ });
842
+ }, contact => contact[sortBy].toLowerCase());
843
+
844
+ $(`a role=button mt:$3 text="Add new contact" href=/contacts/${contacts.length}`);
845
+ });
846
+ }
847
+
848
+ // Detail form styles
849
+ const detailStyle = insertCss({
850
+ "&": "bg:$cardBg border: 1px solid $border; r:8px p:$4 max-width:600px",
851
+ "label": "display:block font-weight:600 mt:$3 mb:$2",
852
+ "input": "w:100% p:$2 r:4px border: 1px solid $border; bg:$bg fg:$fg"
853
+ });
854
+
855
+ function drawContactDetail(id: number) {
856
+ const contact = contacts[id] ||= {};
857
+
858
+ $('a role=button href=/contacts #← Back');
859
+
860
+ $('div mt:$3', detailStyle, () => {
861
+ $('h2 mb:$2 text=', ref(contact, 'firstName'), 'text=', ' ', 'text=', ref(contact, 'lastName'));
862
+ $('label text="First Name" input bind=', ref(contact, 'firstName'));
863
+ $('label text="Last Name" input bind=', ref(contact, 'lastName'));
864
+ $('label text="Email" input type=email bind=', ref(contact, 'email'));
865
+ $('label text="Phone" input type=tel bind=', ref(contact, 'phone'));
866
+ });
867
+ }
868
+ ```
869
+
870
+ ## Further reading
871
+
872
+ If you've understood all/most of the above, you should be ready to get going with Aberdeen! You may also find these links helpful:
280
873
 
281
- ## Best Practices
282
- 1. **Type everything:** Use TypeScript. `proxy()` preserves types; class instances work great.
283
- 2. **Use CSS variables:** Define `@primary`, `@secondary`, etc. in `cssVars` for colors.
284
- 3. **Use spacing scale:** Prefer `@3`, `@4` for margins/paddings over hardcoded values, for consistency and easy theming/scaling. Don't use when exact pixel values are needed.
285
- 4. **Minimize scope size:** Smaller reactive scopes = fewer DOM updates.
286
- 5. **Use `onEach` for lists:** Never iterate proxied arrays with `for`/`map` in render functions.
287
- 6. **Pass observables directly:** `$('span text=', observable)` not interpolation.
288
- 7. **Components are functions:** Just write functions that call `$`.
289
- 8. **Top-level CSS:** Call `insertCss` at module level, not in render functions.
290
- 9. **Dynamic values:** Always use `attr=', value` syntax for user data.
874
+ - [Reference documentation](https://aberdeenjs.org/modules.html)
875
+ - [Examples](https://aberdeenjs.org/#examples)