aberdeen 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -172,7 +172,30 @@ Some further examples:
172
172
 
173
173
  And you may want to study the examples above, of course!
174
174
 
175
+
176
+ ## AI Integration
177
+
178
+ If you use Claude Code, GitHub Copilot or another AI agents that supports Skills, Aberdeen includes a `skill/` directory that provides specialized knowledge to the AI about how to use the library effectively.
179
+
180
+ To use this, it is recommended to symlink the skill into your project's `.claude/skills` directory:
181
+
182
+ ```bash
183
+ mkdir -p .claude/skills
184
+ ln -s ../../node_modules/aberdeen/skill .claude/skills/aberdeen
185
+ ```
186
+
187
+
175
188
  ## Changelog
189
+ ### 1.4.1 (2026-01-14)
190
+
191
+ **Additions:**
192
+ - Created an AI agent Skill (Claude Code, GitHub Copilot) for using Aberdeen in your projects.
193
+
194
+ **Enhancements:**
195
+ - The `html-to-aberdeen` tool now automatically converts `style` attributes to Aberdeen's CSS shortcuts (like `mt:10px` for `margin-top: 10px`) and uses the modern `#text` syntax.
196
+
197
+ **Fixes:**
198
+ - Fixed an issue in `docs/live-code.js` where it was still trying to import the removed `observe` function.
176
199
 
177
200
  ### 1.4.0 (2025-01-07)
178
201
 
package/html-to-aberdeen CHANGED
@@ -227,13 +227,77 @@ function convertHTMLToAberdeen(html) {
227
227
  }
228
228
 
229
229
  // Process a node and return Aberdeen code
230
+ const CSS_PROPERTY_TO_SHORTCUT = {
231
+ 'margin': 'm',
232
+ 'margin-top': 'mt',
233
+ 'margin-bottom': 'mb',
234
+ 'margin-left': 'ml',
235
+ 'margin-right': 'mr',
236
+ 'padding': 'p',
237
+ 'padding-top': 'pt',
238
+ 'padding-bottom': 'pb',
239
+ 'padding-left': 'pl',
240
+ 'padding-right': 'pr',
241
+ 'width': 'w',
242
+ 'height': 'h',
243
+ 'background': 'bg',
244
+ 'color': 'fg',
245
+ 'border-radius': 'r',
246
+ };
247
+
248
+ function kebabToCamel(str) {
249
+ return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
250
+ }
251
+
252
+ function convertStyleToAberdeen(styleString) {
253
+ const rules = styleString.split(';').map(s => s.trim()).filter(Boolean);
254
+ const resultParts = [];
255
+
256
+ const props = {};
257
+ for (const rule of rules) {
258
+ const colonIndex = rule.indexOf(':');
259
+ if (colonIndex === -1) continue;
260
+ const key = rule.substring(0, colonIndex).trim();
261
+ const value = rule.substring(colonIndex + 1).trim();
262
+ props[key] = value;
263
+ }
264
+
265
+ const addPart = (key, value) => {
266
+ if (value.includes(' ')) {
267
+ resultParts.push(`${key}:"${value}"`);
268
+ } else {
269
+ resultParts.push(`${key}:${value}`);
270
+ }
271
+ };
272
+
273
+ const handleGroup = (prop1, prop2, shortcut) => {
274
+ if (props[prop1] && props[prop2] && props[prop1] === props[prop2]) {
275
+ addPart(shortcut, props[prop1]);
276
+ delete props[prop1];
277
+ delete props[prop2];
278
+ }
279
+ };
280
+
281
+ handleGroup('margin-top', 'margin-bottom', 'mv');
282
+ handleGroup('margin-left', 'margin-right', 'mh');
283
+ handleGroup('padding-top', 'padding-bottom', 'pv');
284
+ handleGroup('padding-left', 'padding-right', 'ph');
285
+
286
+ for (const [key, value] of Object.entries(props)) {
287
+ const shortcut = CSS_PROPERTY_TO_SHORTCUT[key] || kebabToCamel(key);
288
+ addPart(shortcut, value);
289
+ }
290
+
291
+ return resultParts.join(' ');
292
+ }
293
+
230
294
  function processNode(node, indentLevel = 0) {
231
295
  const indent = ' '.repeat(indentLevel);
232
296
 
233
297
  // Handle text nodes
234
298
  if (node.type === 'text') {
235
299
  const text = node.content.trim();
236
- return text ? `${indent}$(':${escapeString(text)}');\n` : ``;
300
+ return text ? `${indent}$('#${escapeString(text)}');\n` : ``;
237
301
  }
238
302
 
239
303
  // Handle comments
@@ -273,8 +337,17 @@ function processElement(node, indentLevel) {
273
337
 
274
338
  result += tagName + classes;
275
339
 
276
- // Add attributes (excluding class)
277
- const attributes = element.attributes.filter(attr => attr.name !== 'class');
340
+ // Add style shorthand
341
+ const styleAttr = element.attributes.find(attr => attr.name === 'style');
342
+ if (styleAttr) {
343
+ const styleShorthand = convertStyleToAberdeen(styleAttr.value);
344
+ if (styleShorthand) {
345
+ result += ' ' + styleShorthand;
346
+ }
347
+ }
348
+
349
+ // Add attributes (excluding class and style)
350
+ const attributes = element.attributes.filter(attr => attr.name !== 'class' && attr.name !== 'style');
278
351
  const { attrString, separateArgs } = buildAttributeString(attributes);
279
352
  result += attrString;
280
353
 
@@ -287,8 +360,8 @@ function processElement(node, indentLevel) {
287
360
  const textContent = element.children[0].content.trim();
288
361
  if (i === chain.length - 1) {
289
362
  // If it's the last element in the chain and there are no other children,
290
- // use the ':' syntax for text
291
- result += ':' + escapeString(textContent);
363
+ // use the '#' syntax for text
364
+ result += '#' + escapeString(textContent);
292
365
  } else {
293
366
  // Treat text like any other attribute
294
367
  const textAttr = { name: 'text', value: textContent };
@@ -379,7 +452,7 @@ function buildAttributeString(attributes) {
379
452
 
380
453
  if (value === '') {
381
454
  // Boolean attribute
382
- attrString += ` ${attr.name}`;
455
+ attrString += ` ${attr.name}=""`;
383
456
  } else if (value.includes('"')) {
384
457
  // Contains double quotes - must use separate argument
385
458
  attrString += ` ${attr.name}=`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aberdeen",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "author": "Frank van Viegen",
5
5
  "main": "dist-min/aberdeen.js",
6
6
  "devDependencies": {
@@ -41,7 +41,8 @@
41
41
  "files": [
42
42
  "dist",
43
43
  "dist-min",
44
- "src"
44
+ "src",
45
+ "skill"
45
46
  ],
46
47
  "bin": {
47
48
  "html-to-aberdeen": "./html-to-aberdeen"
package/skill/SKILL.md ADDED
@@ -0,0 +1,285 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Aberdeen
7
+
8
+ Reactive UI library using fine-grained reactivity via JS Proxies. No virtual DOM.
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
15
+ ```
16
+
17
+ ## Element Creation: `$`
18
+ ```typescript
19
+ // Tag, classes, text content
20
+ $('div.container.active#Hello World');
21
+
22
+ // Nested elements in single call (each becomes child of previous)
23
+ $('div.wrapper mt:@3 span.icon');
24
+
25
+ // Attributes/properties via string syntax (preferred)
26
+ $('input placeholder=Name value=initial');
27
+
28
+ // Dynamic values: end key with `=`, next arg is value
29
+ $('input placeholder="Something containing spaces" value=', userInput);
30
+ $('button text=', `Count: ${state.count}`);
31
+
32
+ // Event handlers
33
+ $('button#Click click=', () => console.log('clicked'));
34
+
35
+ // Nested content via function (creates reactive scope)
36
+ $('ul', () => {
37
+ $('li#Item 1');
38
+ $('li#Item 2');
39
+ });
40
+ ```
41
+
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}`);
46
+
47
+ // CORRECT
48
+ $('input value=', userData);
49
+ ```
50
+
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 |
60
+
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 `@` reference `cssVars`. Predefined spacing scale:
82
+ | Var | Value | Var | Value |
83
+ |-----|-------|-----|-------|
84
+ | `@1` | 0.25rem | `@4` | 2rem |
85
+ | `@2` | 0.5rem | `@5` | 4rem |
86
+ | `@3` | 1rem | `@n` | 2^(n-3) rem |
87
+
88
+ **Best practice:** Use `@3` and `@4` for most margins/paddings. For new projects, define color variables:
89
+ ```typescript
90
+ cssVars.primary = '#3b82f6';
91
+ cssVars.danger = '#ef4444';
92
+ $('button bg:@primary fg:white#Save');
93
+ ```
94
+
95
+ ## Reactive State: `proxy()`
96
+ ```typescript
97
+ // Objects (preserves type!)
98
+ const state = proxy({ name: 'Alice', count: 0 });
99
+
100
+ // Primitives get wrapped in { value: T }
101
+ const flag = proxy(true);
102
+ flag.value = false;
103
+
104
+ // Class instances work great - use for typed state!
105
+ class Todo {
106
+ constructor(public text: string, public done = false) {}
107
+ toggle() { this.done = !this.done; }
108
+ }
109
+ const todo: Todo = proxy(new Todo('Learn Aberdeen'));
110
+ todo.toggle(); // Reactive method call!
111
+
112
+ // Arrays
113
+ const items = proxy<string[]>([]);
114
+ items.push('new');
115
+ delete items[0];
116
+ ```
117
+
118
+ ## Reactive Scopes
119
+ Functions passed to `$` create **scopes**. When proxy data accessed inside changes:
120
+ 1. All effects from previous run are **cleaned** (DOM elements removed, `clean()` callbacks run)
121
+ 2. Function re-runs
122
+
123
+ ```typescript
124
+ $('div', () => {
125
+ // Re-runs when state.name changes, replacing the h1
126
+ $(`h1#Hello ${state.name}`);
127
+ });
128
+ ```
129
+
130
+ ### Granular Updates
131
+ Split scopes for minimal DOM updates:
132
+ ```typescript
133
+ $('div', () => {
134
+ $('h1', () => $(`#${state.title}`)); // Only title text re-renders
135
+ $('p', () => $(`#${state.body}`)); // Only body text re-renders
136
+ // Or
137
+ $('p#', ref(state, 'body')); // Two-way maps {body: x} to {value: x}, which will be read reactively by $
138
+ });
139
+ ```
140
+
141
+ ### Passing Observables Directly
142
+ Avoid subscribing in parent scope:
143
+ ```typescript
144
+ $('div', () => {
145
+ // Not great: reruns this scope when state.count changes
146
+ $('span text=', state.count); // Subscribes here!
147
+ });
148
+
149
+ $('div', () => {
150
+ // Good: only text node updates, this function does not rerun
151
+ $('span text=', ref(state, 'count')); // Passes observable, subscribes internally
152
+ });
153
+ ```
154
+
155
+ Or just use a single-value proxy `const count = proxy(0);` and pass it directly `$('span text=', count);`.
156
+
157
+ ### Manual Cleanup with `clean()`
158
+ Register cleanup for non-$ side effects:
159
+ ```typescript
160
+ $(() => {
161
+ if (!reactive.value) return;
162
+ const timer = setInterval(() => console.log('tick'), 1000);
163
+ clean(() => clearInterval(timer)); // Runs on scope cleanup
164
+ });
165
+ ```
166
+
167
+ ## Lists: `onEach()`
168
+ ```typescript
169
+ onEach(items, (item, index) => {
170
+ $('li', () => $(`#${item.text}`));
171
+ }, item => item.id); // Optional sort key
172
+ ```
173
+ - Renders only changed items efficiently
174
+ - Sort key: `number | string | [number|string, ...]` or `undefined` to hide item
175
+ - Use `invertString(str)` for descending string sort
176
+ - Works on arrays, objects, and Maps
177
+
178
+ ## Two-Way Binding
179
+ ```typescript
180
+ $('input bind=', ref(state, 'name'));
181
+ $('input type=checkbox bind=', ref(state, 'active'));
182
+ $('select bind=', ref(state, 'choice'), () => {
183
+ $('option value=a#Option A');
184
+ $('option value=b#Option B');
185
+ });
186
+ ```
187
+
188
+ ## Derived Values
189
+
190
+ ### `derive()` - Derived primitives
191
+ ```typescript
192
+ const doubled: { value: number } = derive(() => state.count * 2);
193
+ $('span text=', doubled);
194
+ ```
195
+
196
+ ### Collection functions
197
+ ```typescript
198
+ // count() - returns { value: number } proxy
199
+ const total: { value: number } = count(items);
200
+
201
+ // isEmpty() - returns boolean, re-runs scope only when emptiness changes
202
+ if (isEmpty(items)) $('p#No items');
203
+
204
+ // map() - returns proxied array/object of same shape
205
+ const names: string[] = map(users, u => u.active ? u.name : undefined);
206
+
207
+ // multiMap() - each input produces multiple outputs
208
+ const byId: Record<string, User> = multiMap(users, u => ({ [u.id]: u }));
209
+
210
+ // partition() - sort items into buckets
211
+ const byStatus: Record<string, Record<number, Task>> = partition(tasks, t => t.status);
212
+ ```
213
+
214
+ ## Component-Local CSS
215
+ `insertCss` returns a unique class name (e.g., `.AbdStl1`). Call at **module top-level**, not inside render functions:
216
+ ```typescript
217
+ // At top of file
218
+ const boxStyle = insertCss({
219
+ bg: '@primary',
220
+ r: '@2',
221
+ button: {
222
+ m: '@2',
223
+ '&:hover': { opacity: 0.8 }
224
+ }
225
+ });
226
+
227
+ // In render code
228
+ $('div', boxStyle, 'button#Click');
229
+ ```
230
+
231
+ For global styles (no class prefix):
232
+ ```typescript
233
+ insertGlobalCss({
234
+ body: { m: 0, fontFamily: 'system-ui' },
235
+ 'a': { fg: '@primary' }
236
+ });
237
+ ```
238
+
239
+ CSS can be reactive when needed (e.g., theme switching):
240
+ ```typescript
241
+ $(() => {
242
+ insertCss({ bg: theme.dark ? '#222' : '#fff' });
243
+ });
244
+ ```
245
+
246
+ ## Transitions
247
+ The `create` and `destroy` properties enable enter/leave animations:
248
+ ```typescript
249
+ import { grow, shrink } from 'aberdeen/transitions';
250
+
251
+ // Built-in grow/shrink for smooth height/width animations
252
+ onEach(items, item => {
253
+ $('li create=', grow, 'destroy=', shrink, `#${item.text}`);
254
+ });
255
+
256
+ // CSS class-based transitions
257
+ $('div create=.fade-in destroy=.fade-out#Animated');
258
+ // On create: class added briefly then removed (after layout)
259
+ // On destroy: class added, element removed after 2s delay
260
+ ```
261
+
262
+ Only triggers for **top-level** elements of a (re-)running scope, not deeply nested children.
263
+
264
+ ## HTML Conversion Tool
265
+ Convert HTML to Aberdeen syntax:
266
+ ```bash
267
+ echo '<div class="box"><p>Hello</p></div>' | npx html-to-aberdeen
268
+ # Output: $('div.box', () => { $('p#Hello'); });
269
+ ```
270
+
271
+ ## Advanced Features
272
+ - **Routing**: [references/routing.md](references/routing.md) - Browser history routing and path dispatching
273
+ - **Transitions**: [references/transitions.md](references/transitions.md) - Detailed animation patterns
274
+ - **Predictions**: [references/prediction.md](references/prediction.md) - Optimistic UI with auto-revert
275
+
276
+ ## Best Practices
277
+ 1. **Type everything:** Use TypeScript. `proxy()` preserves types; class instances work great.
278
+ 2. **Use CSS variables:** Define `@primary`, `@secondary`, etc. in `cssVars` for colors.
279
+ 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.
280
+ 4. **Minimize scope size:** Smaller reactive scopes = fewer DOM updates.
281
+ 5. **Use `onEach` for lists:** Never iterate proxied arrays with `for`/`map` in render functions.
282
+ 6. **Pass observables directly:** `$('span text=', observable)` not interpolation.
283
+ 7. **Components are functions:** Just write functions that call `$`.
284
+ 8. **Top-level CSS:** Call `insertCss` at module level, not in render functions.
285
+ 9. **Dynamic values:** Always use `attr=', value` syntax for user data.
@@ -0,0 +1,45 @@
1
+ # Prediction (Optimistic UI)
2
+
3
+ Apply UI changes immediately, auto-revert when server responds.
4
+
5
+ ## API
6
+
7
+ ```typescript
8
+ import { applyPrediction, applyCanon } from 'aberdeen/prediction';
9
+ ```
10
+
11
+ ### `applyPrediction(func)`
12
+ Runs function and records all proxy changes as a "prediction".
13
+ Returns a `Patch` to use as `dropPatches` later, when the server responds.
14
+
15
+ ### `applyCanon(func?, dropPatches?)`
16
+ 1. Reverts all predictions
17
+ 2. Runs `func` (typically applies server data)
18
+ 3. Drops specified patches
19
+ 4. Re-applies remaining predictions that still apply cleanly
20
+
21
+ ## Example
22
+ ```typescript
23
+ async function toggleTodo(todo: Todo) {
24
+ // Optimistic update
25
+ const patch = applyPrediction(() => {
26
+ todo.done = !todo.done;
27
+ });
28
+
29
+ try {
30
+ const data = await api.updateTodo(todo.id, { done: todo.done });
31
+
32
+ // Server responded - apply canonical state
33
+ applyCanon(() => {
34
+ Object.assign(todo, data);
35
+ }, [patch]);
36
+ } catch {
37
+ // On error, just drop the prediction to revert
38
+ applyCanon(undefined, [patch]);
39
+ }
40
+ }
41
+ ```
42
+
43
+ ## When to Use
44
+ - When you want immediate UI feedback for user actions for which a server is authoritative.
45
+ - As doing this manually for each such case is tedious, this should usually be integrated into the data updating/fetching layer.
@@ -0,0 +1,81 @@
1
+ # Routing and Dispatching
2
+
3
+ ## Router (`aberdeen/route`)
4
+
5
+ The `current` object is a reactive proxy of the current URL state.
6
+
7
+ ### Properties
8
+ | Property | Type | Description |
9
+ |----------|------|-------------|
10
+ | `path` | `string` | Normalized path (e.g., `/users/123`) |
11
+ | `p` | `string[]` | Path segments (e.g., `['users', '123']`) |
12
+ | `search` | `Record<string,string>` | Query parameters |
13
+ | `hash` | `string` | URL hash including `#` |
14
+ | `state` | `Record<string,any>` | JSON-compatible state data |
15
+ | `nav` | `NavType` | How we got here: `load`, `back`, `forward`, `go`, `push` |
16
+ | `depth` | `number` | Navigation stack depth (starts at 1) |
17
+
18
+ ### Navigation Functions
19
+ ```typescript
20
+ import * as route from 'aberdeen/route';
21
+
22
+ route.go('/users/42'); // Navigate to new URL
23
+ route.go({ p: ['users', 42], hash: 'top' }); // Object form
24
+ route.push({ search: { tab: 'feed' } }); // Merge into current route
25
+ route.back(); // Go back in history
26
+ route.back({ path: '/home' }); // Back to matching entry, or replace
27
+ route.up(); // Go up one path level
28
+ route.persistScroll(); // Save/restore scroll position
29
+ ```
30
+
31
+ ### Reactive Routing Example
32
+ ```typescript
33
+ import * as route from 'aberdeen/route';
34
+
35
+ $(() => {
36
+ const [section, id] = route.current.p;
37
+ if (section === 'users') drawUser(id);
38
+ else if (section === 'settings') drawSettings();
39
+ else drawHome();
40
+ });
41
+ ```
42
+
43
+ ## Dispatcher (`aberdeen/dispatcher`)
44
+
45
+ Type-safe path segment matching for complex routing.
46
+
47
+ ```typescript
48
+ import { Dispatcher, matchRest } from 'aberdeen/dispatcher';
49
+ import * as route from 'aberdeen/route';
50
+
51
+ const d = new Dispatcher();
52
+
53
+ // Literal string match
54
+ d.addRoute('home', () => drawHome());
55
+
56
+ // Number extraction (uses built-in Number function)
57
+ d.addRoute('user', Number, (id) => drawUser(id));
58
+
59
+ // String extraction
60
+ d.addRoute('user', Number, 'post', String, (userId, postId) => {
61
+ drawPost(userId, postId);
62
+ });
63
+
64
+ // Rest of path as array
65
+ d.addRoute('search', matchRest, (terms: string[]) => {
66
+ performSearch(terms);
67
+ });
68
+
69
+ // Dispatch in reactive scope
70
+ $(() => {
71
+ if (!d.dispatch(route.current.p)) {
72
+ draw404();
73
+ }
74
+ });
75
+ ```
76
+
77
+ ### Custom Matchers
78
+ ```typescript
79
+ const uuid = (s: string) => /^[0-9a-f-]{36}$/.test(s) ? s : matchFailed;
80
+ d.addRoute('item', uuid, (id) => drawItem(id));
81
+ ```
@@ -0,0 +1,52 @@
1
+ # Transitions
2
+
3
+ Animate elements entering/leaving the DOM via the `create` and `destroy` properties.
4
+
5
+ **Important:** Transitions only trigger for **top-level** elements of a scope being (re-)run. Deeply nested elements drawn as part of a larger redraw do not trigger transitions.
6
+
7
+ ## Built-in Transitions
8
+
9
+ ```typescript
10
+ import { grow, shrink } from 'aberdeen/transitions';
11
+
12
+ // Apply to individual elements
13
+ $('div create=', grow, 'destroy=', shrink, '#Animated');
14
+
15
+ // Common with onEach for list animations
16
+ onEach(items, item => {
17
+ $('li create=', grow, 'destroy=', shrink, `#${item.text}`);
18
+ });
19
+ ```
20
+
21
+ - `grow`: Scales element from 0 to full size with margin animation
22
+ - `shrink`: Scales element to 0 and removes from DOM after animation
23
+
24
+ Both detect horizontal flex containers and animate width instead of height.
25
+
26
+ ## CSS-Based Transitions
27
+
28
+ For custom transitions, use CSS class strings (dot-separated):
29
+ ```typescript
30
+ const fadeStyle = insertCss({
31
+ transition: 'all 0.3s ease-out',
32
+ '&.hidden': { opacity: 0, transform: 'translateY(-10px)' }
33
+ });
34
+
35
+ // Class added briefly on create (removed after layout)
36
+ // Class added on destroy (element removed after 2s delay)
37
+ $('div', fadeStyle, 'create=.hidden destroy=.hidden#Fading element');
38
+ ```
39
+
40
+ ## Custom Transition Functions
41
+
42
+ For full control, pass functions. For `destroy`, your function must remove the element:
43
+ ```typescript
44
+ $('div create=', (el: HTMLElement) => {
45
+ // Animate on mount - element already in DOM
46
+ el.animate([{ opacity: 0 }, { opacity: 1 }], 300);
47
+ }, 'destroy=', (el: HTMLElement) => {
48
+ // YOU must remove the element when done
49
+ el.animate([{ opacity: 1 }, { opacity: 0 }], 300)
50
+ .finished.then(() => el.remove());
51
+ }, '#Custom animated');
52
+ ```