aberdeen 1.5.0 → 1.6.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/dist/aberdeen.d.ts +71 -9
- package/dist/aberdeen.js +47 -24
- package/dist/aberdeen.js.map +3 -3
- package/dist/route.js +4 -4
- package/dist/route.js.map +3 -3
- package/dist-min/aberdeen.js +7 -7
- package/dist-min/aberdeen.js.map +3 -3
- package/dist-min/route.js.map +2 -2
- package/package.json +5 -2
- package/skill/SKILL.md +579 -204
- package/skill/aberdeen.md +2322 -0
- package/skill/dispatcher.md +126 -0
- package/skill/prediction.md +73 -0
- package/skill/route.md +249 -0
- package/skill/transitions.md +59 -0
- package/src/aberdeen.ts +121 -35
- package/src/route.ts +3 -3
- package/skill/references/prediction.md +0 -45
- package/skill/references/routing.md +0 -81
- package/skill/references/transitions.md +0 -52
package/src/aberdeen.ts
CHANGED
|
@@ -1644,7 +1644,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
|
|
|
1644
1644
|
const old = dst.get(k);
|
|
1645
1645
|
dst.delete(k);
|
|
1646
1646
|
if (flags & COPY_EMIT) {
|
|
1647
|
-
emit(dst, k,
|
|
1647
|
+
emit(dst, k, EMPTY, old);
|
|
1648
1648
|
}
|
|
1649
1649
|
changed = true;
|
|
1650
1650
|
}
|
|
@@ -1677,7 +1677,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
|
|
|
1677
1677
|
const old = dst[k];
|
|
1678
1678
|
delete dst[k];
|
|
1679
1679
|
if (flags & COPY_EMIT && old !== undefined) {
|
|
1680
|
-
emit(dst, k,
|
|
1680
|
+
emit(dst, k, EMPTY, old);
|
|
1681
1681
|
}
|
|
1682
1682
|
changed = true;
|
|
1683
1683
|
}
|
|
@@ -1704,26 +1704,62 @@ export const NO_COPY = Symbol("NO_COPY");
|
|
|
1704
1704
|
(Promise.prototype as any)[NO_COPY] = true;
|
|
1705
1705
|
|
|
1706
1706
|
/**
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1707
|
+
* A reactive object containing CSS variable definitions.
|
|
1708
|
+
*
|
|
1709
|
+
* Any property you assign to `cssVars` becomes available as a CSS custom property throughout your application.
|
|
1710
|
+
*
|
|
1711
|
+
* Use {@link setSpacingCssVars} to optionally initialize `cssVars[1]` through `cssVars[12]` with an exponential spacing scale.
|
|
1712
|
+
*
|
|
1713
|
+
* When you reference a CSS variable in Aberdeen using the `$` prefix (e.g., `$primary`), it automatically resolves to `var(--primary)`.
|
|
1714
|
+
* For numeric keys (which can't be used directly as CSS custom property names), Aberdeen prefixes them with `m` (e.g., `$3` becomes `var(--m3)`).
|
|
1715
|
+
*
|
|
1716
|
+
* When you add the first property to cssVars, Aberdeen automatically creates a reactive `<style>` tag in `<head>`
|
|
1717
|
+
* containing the `:root` CSS custom property declarations. The style tag is automatically removed if cssVars becomes empty.
|
|
1718
|
+
*
|
|
1715
1719
|
* @example
|
|
1716
|
-
* ```
|
|
1720
|
+
* ```javascript
|
|
1721
|
+
* import { cssVars, setSpacingCssVars, $ } from 'aberdeen';
|
|
1722
|
+
*
|
|
1723
|
+
* // Optionally initialize spacing scale
|
|
1724
|
+
* setSpacingCssVars(); // Uses defaults: base=1, unit='rem'
|
|
1725
|
+
*
|
|
1726
|
+
* // Define custom colors - style tag is automatically created
|
|
1717
1727
|
* cssVars.primary = '#3b82f6';
|
|
1718
|
-
* cssVars
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1728
|
+
* cssVars.danger = '#ef4444';
|
|
1729
|
+
*
|
|
1730
|
+
* // Use in elements with the $ prefix
|
|
1731
|
+
* $('button bg:$primary fg:white');
|
|
1732
|
+
*
|
|
1733
|
+
* // Use spacing (if setSpacingCssVars() was called)
|
|
1734
|
+
* $('div mt:$3'); // Sets margin-top to var(--m3)
|
|
1721
1735
|
* ```
|
|
1722
1736
|
*/
|
|
1723
1737
|
export const cssVars: Record<string, string> = optProxy({});
|
|
1724
1738
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1739
|
+
/**
|
|
1740
|
+
* Initializes `cssVars[1]` through `cssVars[12]` with an exponential spacing scale.
|
|
1741
|
+
*
|
|
1742
|
+
* The scale is calculated as `2^(n-3) * base`, providing values from `0.25 * base` to `512 * base`.
|
|
1743
|
+
*
|
|
1744
|
+
* @param base - The base size for the spacing scale (default: 1). If unit is 'rem' or 'em', this is in that unit. If unit is 'px', this is the pixel value.
|
|
1745
|
+
* @param unit - The CSS unit to use (default: 'rem'). Can be 'rem', 'em', 'px', or any other valid CSS unit.
|
|
1746
|
+
*
|
|
1747
|
+
* @example
|
|
1748
|
+
* ```javascript
|
|
1749
|
+
* // Use default scale (0.25rem to 512rem)
|
|
1750
|
+
* setSpacingCssVars();
|
|
1751
|
+
*
|
|
1752
|
+
* // Use custom base size
|
|
1753
|
+
* setSpacingCssVars(16, 'px'); // 4px to 8192px
|
|
1754
|
+
*
|
|
1755
|
+
* // Use em units
|
|
1756
|
+
* setSpacingCssVars(1, 'em'); // 0.25em to 512em
|
|
1757
|
+
* ```
|
|
1758
|
+
*/
|
|
1759
|
+
export function setSpacingCssVars(base = 1, unit = 'rem'): void {
|
|
1760
|
+
for (let i = 1; i <= 12; i++) {
|
|
1761
|
+
cssVars[i] = 2 ** (i - 3) * base + unit;
|
|
1762
|
+
}
|
|
1727
1763
|
}
|
|
1728
1764
|
|
|
1729
1765
|
const DIGIT_FIRST = /^\d/;
|
|
@@ -1733,6 +1769,72 @@ function cssVarRef(name: string): string {
|
|
|
1733
1769
|
return `var(--${varName})`;
|
|
1734
1770
|
}
|
|
1735
1771
|
|
|
1772
|
+
// Automatically mount cssVars style tag to document.head when cssVars is not empty
|
|
1773
|
+
if (typeof document !== "undefined") {
|
|
1774
|
+
leakScope(() => {
|
|
1775
|
+
$(() => {
|
|
1776
|
+
if (!isEmpty(cssVars)) {
|
|
1777
|
+
mount(document.head, () => {
|
|
1778
|
+
$('style', () => {
|
|
1779
|
+
let css = ":root {\n";
|
|
1780
|
+
for(const [key, value] of Object.entries(cssVars)) {
|
|
1781
|
+
const varName = DIGIT_FIRST.test(String(key)) ? `m${key}` : key;
|
|
1782
|
+
css += ` --${varName}: ${value};\n`;
|
|
1783
|
+
}
|
|
1784
|
+
css += "}";
|
|
1785
|
+
$(`#${css}`);
|
|
1786
|
+
});
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Returns whether the user's browser prefers a dark color scheme.
|
|
1795
|
+
*
|
|
1796
|
+
* This function is reactive - scopes that call it will re-execute when the
|
|
1797
|
+
* browser's color scheme preference changes (via the `prefers-color-scheme` media query).
|
|
1798
|
+
*
|
|
1799
|
+
* Use this in combination with {@link $} and {@link cssVars} to implement theme switching:
|
|
1800
|
+
*
|
|
1801
|
+
* @returns `true` if the browser prefers dark mode, `false` if it prefers light mode.
|
|
1802
|
+
*
|
|
1803
|
+
* @example
|
|
1804
|
+
* ```javascript
|
|
1805
|
+
* import { darkMode, cssVars, $, mount } from 'aberdeen';
|
|
1806
|
+
*
|
|
1807
|
+
* // Reactively set colors based on browser preference
|
|
1808
|
+
* $(() => {
|
|
1809
|
+
* if (darkMode()) { // Optionally override this with user settings
|
|
1810
|
+
* cssVars.bg = '#1a1a1a';
|
|
1811
|
+
* cssVars.fg = '#e5e5e5';
|
|
1812
|
+
* } else {
|
|
1813
|
+
* cssVars.bg = '#ffffff';
|
|
1814
|
+
* cssVars.fg = '#000000';
|
|
1815
|
+
* }
|
|
1816
|
+
* });
|
|
1817
|
+
* ```
|
|
1818
|
+
*/
|
|
1819
|
+
export function darkMode(): boolean {
|
|
1820
|
+
if (typeof window === 'undefined' || !window.matchMedia) return false;
|
|
1821
|
+
|
|
1822
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
1823
|
+
|
|
1824
|
+
// Read from proxy to establish reactivity
|
|
1825
|
+
const changed = proxy(false);
|
|
1826
|
+
changed.value; // Subscribe caller reactive scope
|
|
1827
|
+
function onChange() {
|
|
1828
|
+
changed.value = true;
|
|
1829
|
+
}
|
|
1830
|
+
mediaQuery.addEventListener('change', onChange);
|
|
1831
|
+
clean(() => {
|
|
1832
|
+
mediaQuery.removeEventListener('change', onChange);
|
|
1833
|
+
})
|
|
1834
|
+
|
|
1835
|
+
return mediaQuery.matches;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1736
1838
|
/**
|
|
1737
1839
|
* Clone an (optionally proxied) object or array.
|
|
1738
1840
|
*
|
|
@@ -2188,7 +2290,7 @@ function styleToCss(style: object, prefix: string): string {
|
|
|
2188
2290
|
);
|
|
2189
2291
|
}
|
|
2190
2292
|
} else {
|
|
2191
|
-
const val = v == null || v === false ? "" : typeof v === 'string' ? (v[0] === '
|
|
2293
|
+
const val = v == null || v === false ? "" : typeof v === 'string' ? (v[0] === '$' ? cssVarRef(v.substring(1)) : v) : String(v);
|
|
2192
2294
|
const expanded = CSS_SHORT[k] || k;
|
|
2193
2295
|
for (const prop of (Array.isArray(expanded) ? expanded : [expanded])) {
|
|
2194
2296
|
props += `${prop.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}:${val};`;
|
|
@@ -2217,7 +2319,7 @@ function applyArg(el: Element, key: string, value: any) {
|
|
|
2217
2319
|
} else if (key[0] === "$") {
|
|
2218
2320
|
// Style (with shortcuts)
|
|
2219
2321
|
key = key.substring(1);
|
|
2220
|
-
const val = value == null || value === false ? "" : typeof value === 'string' ? (value[0] === '
|
|
2322
|
+
const val = value == null || value === false ? "" : typeof value === 'string' ? (value[0] === '$' ? cssVarRef(value.substring(1)) : value) : String(value);
|
|
2221
2323
|
const expanded = CSS_SHORT[key] || key;
|
|
2222
2324
|
if (typeof expanded === "string") {
|
|
2223
2325
|
(el as any).style[expanded] = val;
|
|
@@ -2914,20 +3016,4 @@ export function withEmitHandler(
|
|
|
2914
3016
|
}
|
|
2915
3017
|
}
|
|
2916
3018
|
|
|
2917
|
-
|
|
2918
|
-
// This runs at module load time, after all functions are defined
|
|
2919
|
-
if (typeof document !== "undefined") {
|
|
2920
|
-
leakScope(() => {
|
|
2921
|
-
mount(document.head, () => {
|
|
2922
|
-
$('style', () => {
|
|
2923
|
-
let css = ":root {\n";
|
|
2924
|
-
for(const [key, value] of Object.entries(cssVars)) {
|
|
2925
|
-
const varName = DIGIT_FIRST.test(String(key)) ? `m${key}` : key;
|
|
2926
|
-
css += ` --${varName}: ${value};\n`;
|
|
2927
|
-
}
|
|
2928
|
-
css += "}";
|
|
2929
|
-
$(`#${css}`);
|
|
2930
|
-
})
|
|
2931
|
-
});
|
|
2932
|
-
});
|
|
2933
|
-
}
|
|
3019
|
+
|
package/src/route.ts
CHANGED
|
@@ -170,9 +170,9 @@ export function go(target: RouteTarget, nav: NavType = "go"): void {
|
|
|
170
170
|
* @param target Same as for {@link go}, but merged into the current route instead deleting all state.
|
|
171
171
|
*/
|
|
172
172
|
export function push(target: RouteTarget): void {
|
|
173
|
-
|
|
174
|
-
merge(
|
|
175
|
-
go(
|
|
173
|
+
const c = clone(unproxy(current));
|
|
174
|
+
merge(c, targetToPartial(target));
|
|
175
|
+
go(c);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/**
|
|
@@ -1,45 +0,0 @@
|
|
|
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.
|
|
@@ -1,81 +0,0 @@
|
|
|
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
|
-
```
|
|
@@ -1,52 +0,0 @@
|
|
|
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
|
-
```
|