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/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, undefined, old);
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, undefined, old);
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
- * CSS variables that are output as native CSS custom properties.
1708
- *
1709
- * When a CSS value starts with `@`, it becomes `var(--name)` (or `var(--mN)` for numeric keys).
1710
- * Pre-initialized with keys '1'-'12' mapping to an exponential rem scale (e.g., @1=0.25rem, @3=1rem).
1711
- *
1712
- * Changes to cssVars are automatically reflected in a `<style>` tag in `<head>`, making updates
1713
- * reactive across all elements using those variables.
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
- * ```typescript
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[3] = '16px'; // Override @3 to be 16px instead of 1rem
1719
- * $('p color:@primary'); // Sets color to var(--primary)
1720
- * $('div mt:@3'); // Sets margin-top to var(--m3)
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
- for (let i = 1; i <= 12; i++) {
1726
- cssVars[i] = 2 ** (i - 3) + "rem";
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] === '@' ? cssVarRef(v.substring(1)) : v) : String(v);
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] === '@' ? cssVarRef(value.substring(1)) : value) : String(value);
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
- // Initialize the cssVars style tag in document.head
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
- let copy = clone(unproxy(current));
174
- merge(copy, targetToPartial(target));
175
- go(copy);
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
- ```