aberdeen 1.6.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/src/aberdeen.ts CHANGED
@@ -53,7 +53,7 @@ function queue(runner: QueueRunner) {
53
53
  * ```typescript
54
54
  * const data = proxy("before");
55
55
  *
56
- * $('#'+data);
56
+ * $('#', data);
57
57
  * console.log(1, document.body.innerHTML); // before
58
58
  *
59
59
  * // Make an update that should cause the DOM to change.
@@ -166,9 +166,7 @@ abstract class Scope implements QueueRunner {
166
166
 
167
167
  [ptr: ReverseSortedSetPointer]: this;
168
168
 
169
- onChange(index: any): void {
170
- queue(this);
171
- }
169
+ abstract onChange(target: TargetType, index: any, newData: any, oldData: any): void;
172
170
  abstract queueRun(): void;
173
171
 
174
172
  abstract getLastNode(): Node | undefined;
@@ -213,6 +211,7 @@ abstract class ContentScope extends Scope {
213
211
 
214
212
  abstract svg: boolean;
215
213
  abstract el: Element;
214
+ private changes: undefined | Map<TargetType, Map<any, any>>; // target => (index => oldData)
216
215
 
217
216
  constructor(
218
217
  cleaners: Array<{ delete: (scope: Scope) => void } | (() => void)> = [],
@@ -246,7 +245,42 @@ abstract class ContentScope extends Scope {
246
245
  this.lastChild = undefined;
247
246
  }
248
247
 
248
+ onChange(target: TargetType, index: any, newData: any, oldData: any): void {
249
+ if (!this.changes) {
250
+ this.changes = new Map();
251
+ queue(this);
252
+ }
253
+
254
+ let targetDelta = this.changes.get(target);
255
+ if (!targetDelta) {
256
+ targetDelta = new Map();
257
+ this.changes.set(target, targetDelta);
258
+ }
259
+
260
+ if (targetDelta.has(index)) {
261
+ // Already changed before, keep original oldData
262
+ // Unless it changed back to original value
263
+ if (targetDelta.get(index) === newData) targetDelta.delete(index);
264
+ } else {
265
+ targetDelta.set(index, oldData);
266
+ }
267
+ }
268
+
269
+ fetchHasChanges(): boolean {
270
+ if (!this.changes) return false;
271
+ for(const targetDelta of this.changes.values()) {
272
+ if (targetDelta.size > 0) {
273
+ delete this.changes;
274
+ return true;
275
+ }
276
+ }
277
+ delete this.changes;
278
+ return false;
279
+ }
280
+
281
+
249
282
  queueRun() {
283
+ if (!this.fetchHasChanges()) return;
250
284
  this.remove();
251
285
 
252
286
  topRedrawScope = this;
@@ -258,10 +292,6 @@ abstract class ContentScope extends Scope {
258
292
  return this.getLastNode() || this.getPrecedingNode();
259
293
  }
260
294
 
261
- onChange() {
262
- queue(this);
263
- }
264
-
265
295
  getChildPrevSibling() {
266
296
  return this.lastChild;
267
297
  }
@@ -479,11 +509,10 @@ class OnEachScope extends Scope {
479
509
  byIndex: Map<any, OnEachItemScope> = new Map();
480
510
 
481
511
  /** The reverse-ordered list of item scopes, not including those for which makeSortKey returned undefined. */
482
- sortedSet: ReverseSortedSet<OnEachItemScope, "sortKey"> =
483
- new ReverseSortedSet("sortKey");
512
+ sortedSet: ReverseSortedSet<OnEachItemScope, "sortKey"> = new ReverseSortedSet("sortKey");
484
513
 
485
514
  /** Indexes that have been created/removed and need to be handled in the next `queueRun`. */
486
- changedIndexes: Set<any> = new Set();
515
+ changedIndexes: Map<any, any> = new Map(); // index => old value
487
516
 
488
517
  constructor(
489
518
  proxy: TargetType,
@@ -493,8 +522,7 @@ class OnEachScope extends Scope {
493
522
  public makeSortKey?: (value: any, key: any) => SortKeyType,
494
523
  ) {
495
524
  super();
496
- const target: TargetType = (this.target =
497
- (proxy as any)[TARGET_SYMBOL] || proxy);
525
+ const target: TargetType = (this.target = (proxy as any)[TARGET_SYMBOL] || proxy);
498
526
 
499
527
  subscribe(target, ANY_SYMBOL, this);
500
528
  this.prevSibling = currentScope.getChildPrevSibling();
@@ -518,16 +546,27 @@ class OnEachScope extends Scope {
518
546
  return findLastNodeInPrevSiblings(this.prevSibling);
519
547
  }
520
548
 
521
- onChange(index: any) {
522
- if (!(this.target instanceof Array) || typeof index === "number")
523
- this.changedIndexes.add(index);
524
- queue(this);
549
+ onChange(target: TargetType, index: any, newData: any, oldData: any) {
550
+ // target === this.target
551
+ if (!(target instanceof Array) || typeof index === "number") {
552
+ if (this.changedIndexes.has(index)) {
553
+ if (this.changedIndexes.get(index) === newData) {
554
+ // Data changed back to original value, so ignore it
555
+ this.changedIndexes.delete(index);
556
+ }
557
+ // Else, data changed a second time
558
+ } else {
559
+ // Initial data change
560
+ this.changedIndexes.set(index, oldData);
561
+ queue(this);
562
+ }
563
+ }
525
564
  }
526
565
 
527
566
  queueRun() {
528
567
  const indexes = this.changedIndexes;
529
- this.changedIndexes = new Set();
530
- for (const index of indexes) {
568
+ this.changedIndexes = new Map();
569
+ for (const index of indexes.keys()) {
531
570
  const oldScope = this.byIndex.get(index);
532
571
  if (oldScope) oldScope.remove();
533
572
 
@@ -633,6 +672,8 @@ class OnEachItemScope extends ContentScope {
633
672
  /* c8 ignore next */
634
673
  if (currentScope !== ROOT_SCOPE) internalError(4);
635
674
 
675
+ if (!this.fetchHasChanges()) return;
676
+
636
677
  // We're not calling `remove` here, as we don't want to remove ourselves from
637
678
  // the sorted set. `redraw` will take care of that, if needed.
638
679
  // Also, we can't use `getLastNode` here, as we've hacked it to return the
@@ -959,23 +1000,27 @@ export function isEmpty(proxied: TargetType): boolean {
959
1000
 
960
1001
  if (target instanceof Array) {
961
1002
  subscribe(target, "length", (index: any, newData: any, oldData: any) => {
962
- if (!newData !== !oldData) queue(scope);
1003
+ if (!newData !== !oldData) scope.onChange(target, EMPTY, !newData, !oldData);
963
1004
  });
964
1005
  return !target.length;
965
1006
  }
966
1007
 
967
1008
  if (target instanceof Map) {
968
1009
  subscribe(target, MAP_SIZE_SYMBOL, (index: any, newData: any, oldData: any) => {
969
- if (!newData !== !oldData) queue(scope);
1010
+ if (!newData !== !oldData) scope.onChange(target, EMPTY, !newData, !oldData);
970
1011
  });
971
1012
  return !target.size;
972
1013
  }
973
1014
 
974
- const result = isObjEmpty(target);
1015
+ let oldEmpty = isObjEmpty(target);
975
1016
  subscribe(target, ANY_SYMBOL, (index: any, newData: any, oldData: any) => {
976
- if (result ? oldData === EMPTY : newData === EMPTY) queue(scope);
1017
+ if ((newData === EMPTY) !== (oldData === EMPTY)) {
1018
+ const newEmpty = isObjEmpty(target);
1019
+ scope.onChange(target, EMPTY, newEmpty, oldEmpty);
1020
+ oldEmpty = newEmpty;
1021
+ }
977
1022
  });
978
- return result;
1023
+ return oldEmpty;
979
1024
  }
980
1025
 
981
1026
  /** @private */
@@ -1050,7 +1095,7 @@ export function defaultEmitHandler(
1050
1095
  if (byIndex) {
1051
1096
  for (const observer of byIndex) {
1052
1097
  if (typeof observer === "function") observer(index, newData, oldData);
1053
- else observer.onChange(index);
1098
+ else observer.onChange(target, index, newData, oldData);
1054
1099
  }
1055
1100
  }
1056
1101
  }
@@ -1516,8 +1561,6 @@ function copySet(dst: any, dstKey: any, src: any, flags: number): boolean {
1516
1561
  * Like {@link copy}, but uses merge semantics. Properties in `dst` not present in `src` are kept.
1517
1562
  * `null`/`undefined` in `src` delete properties in `dst`.
1518
1563
  *
1519
- * When the destination is an object and the source is an array, its keys are used as (sparse) array indices.
1520
- *
1521
1564
  * @example Basic merge
1522
1565
  * ```typescript
1523
1566
  * const source = { b: { c: 99 }, d: undefined }; // d: undefined will delete
@@ -1528,14 +1571,6 @@ function copySet(dst: any, dstKey: any, src: any, flags: number): boolean {
1528
1571
  * console.log(dest); // proxy({ a: 1, b: { c: 99, x: 5, y: 6 }, c: { z: 7 } })
1529
1572
  * ```
1530
1573
  *
1531
- * @example Partial Array Merge
1532
- * ```typescript
1533
- * const messages = proxy(['msg1', 'msg2', 'msg3']);
1534
- * const update = { 1: 'updated msg2' }; // Update using object key as index
1535
- * merge(messages, update);
1536
- * console.log(messages); // proxy(['msg1', 'updated msg2', 'msg3'])
1537
- * ```
1538
- *
1539
1574
  */
1540
1575
  export function merge<T extends object>(dst: T, value: Partial<T>): boolean;
1541
1576
  export function merge<T extends object>(dst: T, dstKey: keyof T, value: Partial<T[typeof dstKey]>): boolean;
@@ -1737,15 +1772,16 @@ export const NO_COPY = Symbol("NO_COPY");
1737
1772
  export const cssVars: Record<string, string> = optProxy({});
1738
1773
 
1739
1774
  /**
1740
- * Initializes `cssVars[1]` through `cssVars[12]` with an exponential spacing scale.
1775
+ * Initializes `cssVars[0]` through `cssVars[12]` with an exponential spacing scale.
1741
1776
  *
1742
1777
  * The scale is calculated as `2^(n-3) * base`, providing values from `0.25 * base` to `512 * base`.
1743
1778
  *
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.
1779
+ * @param base - The base size for the spacing scale that will apply to `cssVars[3]`. Every step up the scale will double this, while every step down will halve it. Defaults to 1.
1780
+ * @param unit - The CSS unit to use, like 'rem', 'em', or 'px'. Defaults to 'rem'.
1746
1781
  *
1747
1782
  * @example
1748
1783
  * ```javascript
1784
+ * import { setSpacingCssVars, cssVars, onEach, $} from 'aberdeen';
1749
1785
  * // Use default scale (0.25rem to 512rem)
1750
1786
  * setSpacingCssVars();
1751
1787
  *
@@ -1754,19 +1790,36 @@ export const cssVars: Record<string, string> = optProxy({});
1754
1790
  *
1755
1791
  * // Use em units
1756
1792
  * setSpacingCssVars(1, 'em'); // 0.25em to 512em
1793
+ *
1794
+ * // Show the last generated spacing values
1795
+ * onEach(cssVars, (value, key) => {
1796
+ * $(`div #${key} → ${value}`)
1797
+ * }, (value, key) => parseInt(key)); // Numeric sort
1757
1798
  * ```
1758
1799
  */
1759
1800
  export function setSpacingCssVars(base = 1, unit = 'rem'): void {
1760
- for (let i = 1; i <= 12; i++) {
1801
+ for (let i = 0; i <= 12; i++) {
1761
1802
  cssVars[i] = 2 ** (i - 3) * base + unit;
1762
1803
  }
1763
1804
  }
1764
1805
 
1806
+ // Matches: (1) parenthesized content, (2) quoted content, (3) $varName at start or after space
1807
+ const CSS_VAR_PATTERN = /(\([^)]*\))|("[^"]*")|(^| )\$(\w+)/g;
1765
1808
  const DIGIT_FIRST = /^\d/;
1766
- function cssVarRef(name: string): string {
1767
- // Prefix numeric keys with 'm' (CSS custom property names can't start with a digit)
1768
- const varName = DIGIT_FIRST.test(name) ? `m${name}` : name;
1769
- return `var(--${varName})`;
1809
+
1810
+ /**
1811
+ * Expands all `$varName` patterns in a CSS value to `var(--varName)`.
1812
+ * Only matches `$` at the start of the value or after a space.
1813
+ * Content inside parentheses or quotes is preserved as-is.
1814
+ * Numeric names get an 'm' prefix (e.g., `$3` → `var(--m3)`).
1815
+ */
1816
+ function cssVarRef(value: string): string {
1817
+ if (value.indexOf('$') < 0) return value;
1818
+ return value.replace(CSS_VAR_PATTERN, (match, parens, quoted, prefix, name) => {
1819
+ if (parens || quoted) return match;
1820
+ const varName = DIGIT_FIRST.test(name) ? `m${name}` : name;
1821
+ return `${prefix}var(--${varName})`;
1822
+ });
1770
1823
  }
1771
1824
 
1772
1825
  // Automatically mount cssVars style tag to document.head when cssVars is not empty
@@ -1790,6 +1843,9 @@ if (typeof document !== "undefined") {
1790
1843
  });
1791
1844
  }
1792
1845
 
1846
+
1847
+ let darkModeState: {value: boolean} | undefined;
1848
+
1793
1849
  /**
1794
1850
  * Returns whether the user's browser prefers a dark color scheme.
1795
1851
  *
@@ -1806,33 +1862,25 @@ if (typeof document !== "undefined") {
1806
1862
  *
1807
1863
  * // Reactively set colors based on browser preference
1808
1864
  * $(() => {
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
- * }
1865
+ * cssVars.bg = darkMode() ? '#1a1a1a' : '#ffffff';
1866
+ * cssVars.fg = darkMode() ? '#e5e5e5' : '#000000';
1816
1867
  * });
1868
+ *
1869
+ * $('div bg:$bg fg:$fg p:1rem #Colors change based on system dark mode preference');
1817
1870
  * ```
1818
1871
  */
1819
1872
  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;
1873
+ if (!darkModeState) {
1874
+ // Initialize on first use
1875
+
1876
+ if (typeof window === 'undefined' || !window.matchMedia) return false;
1877
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
1878
+
1879
+ darkModeState = proxy({ value: mediaQuery.matches });
1880
+ mediaQuery.addEventListener('change', () => darkModeState!.value = mediaQuery.matches);
1829
1881
  }
1830
- mediaQuery.addEventListener('change', onChange);
1831
- clean(() => {
1832
- mediaQuery.removeEventListener('change', onChange);
1833
- })
1834
-
1835
- return mediaQuery.matches;
1882
+
1883
+ return darkModeState.value;
1836
1884
  }
1837
1885
 
1838
1886
  /**
@@ -1935,12 +1983,9 @@ function applyBind(el: HTMLInputElement, target: any) {
1935
1983
  };
1936
1984
  } else {
1937
1985
  onInputChange = () => {
1938
- target.value =
1939
- type === "number" || type === "range"
1940
- ? el.value === ""
1941
- ? null
1942
- : +el.value
1943
- : el.value;
1986
+ target.value = type === "number" || type === "range"
1987
+ ? el.value === "" ? null : +el.value
1988
+ : el.value;
1944
1989
  };
1945
1990
  if (value === undefined) onInputChange();
1946
1991
  onProxyChange = () => {
@@ -1996,38 +2041,62 @@ const SPECIAL_PROPS: { [key: string]: (el: Element, value: any) => void } = {
1996
2041
  *
1997
2042
  * @param {...(string | function | object | false | undefined | null)} args - Any number of arguments can be given. How they're interpreted depends on their types:
1998
2043
  *
1999
- * - `string`: Strings can be used to create and insert new elements, set classnames for the *current* element, and add text to the current element.
2000
- * The format of a string is: (**tag** | `.` **class** | **key**=**val** | **key**="**long val**")* ('#' **text** | **key**=)?
2001
- * So there can be:
2002
- * - Any number of **tag** element, like `h1` or `div`. These elements are created, added to the *current* element, and become the new *current* element for the rest of this `$` function execution.
2003
- * - Any number of CSS classes prefixed by `.` characters. These classes will be added to the *current* element. Optionally, CSS classes can be appended to a **tag** without a space. So both `div.myclass` and `div .myclass` are valid and do the same thing.
2004
- * - Any number of key/value pairs with string values, like `placeholder="Your name"` or `data-id=123`. These will be handled according to the rules specified for `object`, below, but with the caveat that values can only be strings. The quotes around string values are optional, unless the value contains spaces. It's not possible to escape quotes within the value. If you want to do that, or if you have user-provided values, use the `object` syntax (see below) or end your string with `key=` followed by the data as a separate argument (see below).
2005
- * - The string may end in a '#' followed by text, which will be added as a TextNode to the *current* element. The text ranges til the end of the string, and may contain any characters, including spaces and quotes.
2006
- * - Alternatively, the string may end in a key followed by an '=' character, in which case the value is expected as a separate argument. The key/value pair is set according to the rules specified for `object` below. This is useful when the value is not a string or contains spaces or user data. Example: `$('button text="Click me" click=', () => alert('Clicked!'))` or `$('input.value=', someUserData, "placeholder=", "Type your stuff")`.
2007
- * - `function`: When a function (without argument nor a return value) is passed in, it will be reactively executed in its own observer scope, preserving the *current element*. So any `$()` invocations within this function will create DOM elements with our *current* element as parent. If the function reads observable data, and that data is changed later on, the function we re-execute (after side effects, such as DOM modifications through `$`, have been cleaned - see also {@link clean}).
2008
- * - `object`: When an object is passed in, its key-value pairs are used to modify the *current* element in the following ways...
2009
- * - `{<attrName>: any}`: The common case is setting the value as an HTML attribute named key. So `{placeholder: "Your name"}` would add `placeholder="Your name"` to the current HTML element.
2010
- * - `{<propName>: boolean}` or `{value: any}` or `{selectedIndex: number}`: If the value is a boolean, or if the key is `value` or `selectedIndex`, it is set on the `current` element as a DOM property instead of an HTML attribute. For example `{checked: true}` would do `el.checked = true` for the *current* element.
2011
- * - `{".class": boolean}`: If the key starts with a `.` character, its either added to or removed from the *current* element as a CSS class, based on the truthiness of the value. So `{".hidden": hide}` would toggle the `hidden` CSS class.
2012
- * - `{<eventName>: function}`: If the value is a `function` it is set as an event listener for the event with the name given by the key. For example: `{click: myClickHandler}`.
2013
- * - `{$<styleProp>: value}`: If the key starts with a `$` character, set a CSS style property with the name of the rest of the key to the given value. Example: `{$backgroundColor: 'red'}`.
2014
- * - `{create: string}`: Add the value string as a CSS class to the *current* element, *after* the browser has finished doing a layout pass. This behavior only triggers when the scope setting the `create` is the top-level scope being (re-)run. This allows for creation transitions, without triggering the transitions for deeply nested elements being drawn as part of a larger component. The string may also contain multiple dot-separated CSS classes, such as `.fade.grow`.
2015
- * - `{destroy: string}`: When the *current* element is a top-level element to be removed (due to reactivity cleanup), actual removal from the DOM is delayed by 2 seconds, and in the mean time the value string is added as a CSS class to the element, allowing for a deletion transition. The string may also contain multiple dot-separated CSS classes, such as `.fade.shrink`.
2016
- * - `{create: function}` and `{destroy: function}`: The function is invoked when the *current* element is the top-level element being created/destroyed. It can be used for more involved creation/deletion animations. In case of `destroy`, the function is responsible for actually removing the element from the DOM (eventually). See `transitions.ts` in the Aberdeen source code for some examples.
2017
- * - `{bind: <obsValue>}`: Create a two-way binding element between the `value` property of the given observable (proxy) variable, and the *current* input element (`<input>`, `<select>` or `<textarea>`). This is often used together with {@link ref}, in order to use properties other than `.value`.
2018
- * - `{<any>: <obsvalue>}`: Create a new observer scope and read the `value` property of the given observable (proxy) variable from within it, and apply the contained value using any of the other rules in this list. Example:
2019
- * ```typescript
2020
- * const myColor = proxy('red');
2021
- * $('p#Test', {$color: myColor, click: () => myColor.value = 'yellow'})
2022
- * // Clicking the text will cause it to change color without recreating the <p> itself
2023
- * ```
2024
- * This is often used together with {@link ref}, in order to use properties other than `.value`.
2025
- * - `{text: string|number}`: Add the value as a `TextNode` to the *current* element.
2026
- * - `{html: string}`: Add the value as HTML to the *current* element. This should only be used in exceptional situations. And of course, beware of XSS.
2027
- - `Node`: If a DOM Node (Element or TextNode) is passed in, it is added as a child to the *current* element. If the Node is an Element, it becomes the new *current* element for the rest of this `$` function execution.
2028
- *
2029
- * @returns The most inner DOM element that was created (not counting text nodes nor elements created by content functions),
2030
- * or undefined if no elements were created.
2044
+ * ### String arguments
2045
+ * Strings can be used to create and insert new elements, set classnames for the *current* element, and add text to the current element.
2046
+ * The format of a string is: (**tag** | `.` **class** | **key**[=:]**val** | **key**[=:]"**val containing spaces**")* ('#' **text** | **key**[=:])?
2047
+ *
2048
+ * So a string may consist of any number of...
2049
+ * - **tag** elements, like `h1` or `div`. These elements are created, added to the *current* element, and become the new *current* element for the rest of this `$` function execution.
2050
+ * - CSS classes prefixed by `.` characters. These classes will be added to the *current* element. Optionally, CSS classes can be appended to a **tag** without a space. So both `div.myclass` and `div .myclass` are valid and do the same thing.
2051
+ * - Property key/value pairs, like `type=password`, `placeholder="Your name"` or `data-id=123`. When the value contains spaces, it needs to be quoted with either "double quotes", 'single quotes' or \`backticks\`. Quotes within quoted values cannot be escaped (see the next rule for a solution). Key/value pairs will be handled according to *Property rules* below, but with the caveat that values can only be strings.
2052
+ * - CSS key/value pairs using two syntaxes:
2053
+ * - **Short form** `key:value` (no space after colon): The value ends at the next whitespace. Example: `m:$3 bg:red r:8px`
2054
+ * - **Long form** `key: value;` (space after colon): The value continues until a semicolon. Example: `box-shadow: 2px 0 6px black; transition: all 0.3s ease;`
2055
+ *
2056
+ * Both forms support CSS shortcuts (see below). You can mix them: `m:$3 box-shadow: 0 2px 4px rgba(0,0,0,0.2); bg:$cardBg`
2057
+ * The elements must be separated by spaces, except before a `.cssClass` if it is preceded by either **tag** or another CSS class.
2058
+ *
2059
+ * And a string may end in...
2060
+ * - A '#' followed by text, which will be added as a `TextNode` to the *current* element. The text ranges til the end of the string, and may contain any characters, including spaces and quotes.
2061
+ * - A key followed by an '=' character, in which case the value is expected as a separate argument. The key/value pair is set according to the *Property rules* below. This is useful when the value is not a string or contains spaces or user data. Example: `$('button text="Click me" click=', () => alert('Clicked!'))` or `$('input.value=', someUserData, "placeholder=", "Type your stuff")`. In case the value is a proxied object, its `.value` property will be applied reactively without needing to rerender the parent scope.
2062
+ * - A key followed by a ':' character (with no value), in which case the value is expected as a separate argument. The value is treated as a CSS value to be set inline on the *current* element. Example: `$('div margin-top:', someValueInPx)`. In case the value is a proxied object, its `.value` property will be applied reactively without needing to rerender the parent scope.
2063
+ *
2064
+ * ### Function arguments
2065
+ * When a function (without arguments nor a return value) is passed in, it will be reactively executed in its own observer scope, preserving the *current* element. So any `$()` invocations within this function will add child elements to or set properties on that element. If the function reads observable data, and that data is changed later on, the function we re-execute (after side effects, such as DOM modifications through `$`, have been cleaned - see also {@link clean}).
2066
+ *
2067
+ * ### Object arguments
2068
+ * When an object is passed in, its key-value pairs are used to modify the *current* element according to the *Property rules* below, *unless* the key starts with a `$` character, in which case that character is stripped of and the key/value pair is treated as a CSS property, subject to the *CSS shortcuts* below. In case a value is a proxied object, its `.value` property will be applied reactively without needing to rerender the parent scope. In most cases, the string notation (`key=` and `key:`) is preferred over this object notation, for readability.
2069
+ *
2070
+ * ### DOM node arguments
2071
+ * When a DOM Node (Element or TextNode) is passed in, it is added as a child to the *current* element. If the Node is an Element, it becomes the new *current* element for the rest of this `$` function execution.
2072
+ *
2073
+ * ### Property rules
2074
+ * - **Attribute:** The common case is setting the value as an HTML attribute named key. For example `$('input placeholder=Name')` results in `<input placeholder="Name">`.
2075
+ * - **Event listener:** If the value is a `function` it is set as an event listener for the event with the name given by the key. For example: $('button text=Press! click=', () => alert('Clicked!'))`. The event listener will be removed when the current scope is destroyed.
2076
+ * - **DOM property:** When the value is a boolean, or the key is `"value"` or `"selectedIndex"`, it is set on the `current` element as a DOM property instead of an HTML attribute. For example `$('checked=', true)` would do `el.checked = true` for the *current* element.
2077
+ * - **Conditional CSS class:** If the key starts with a `.` character, its either added to or removed from the *current* element as a CSS class, based on the truthiness of the value. So `$('.hidden=', isHidden)` would toggle the `hidden` CSS class. This only works if the `=` is the last character of the string, and the next argument is the value. Its common for the value to be a proxied object, in which case its `.value` is reactively applied without needing to rerender the parent scope.
2078
+ * - **Create transition:** When the key is `"create"`, the value will be added as a CSS class to the *current* element immediately, and then removed right after the browser has finished doing a layout pass. This behavior only triggers when the scope setting the `create` is the top-level scope being (re-)run. This allows for creation transitions, without triggering the transitions for deeply nested elements being drawn as part of a larger component. The string may also contain multiple dot-separated CSS classes, such as `.fade.grow`. The initial dot is optional. Alternatively, to allow for more complex transitions, the value may be a function that receives the `HTMLElement` being created as its only argument. It is *only* called if this is the top-level element being created in this scope run. See `transitions.ts` in the Aberdeen source code for some examples.
2079
+ * - **Destroy transition:** When the key is `"destroy"` the value will be used to apply a CSS transition if the *current* element is later on removed from the DOM and is the top-level element to be removed. This happens as follows: actual removal from the DOM is delayed by 2 seconds, and in the mean-time the value string is added as a CSS class to the element, allowing for a deletion transition. The string may also contain multiple dot-separated CSS classes, such as `.fade.shrink`. The initial dot is optional. Alternatively, to allow for more complex transitions, the value may be a function that receives the `HTMLElement` to be removed from the DOM as its only argument. This function may perform any transitions and is then itself responsible for eventually removing the element from the DOM. See `transitions.ts` in the Aberdeen source code for some examples.
2080
+ * - **Two-way data binding:** When the key is `"bind"` a two-way binding between the `.value` property of the given proxied object, and the *current* input element (`<input>`, `<select>` or `<textarea>`) is created. This is often used together with {@link ref}, in order to use properties other than `.value`.
2081
+ * - **Text:**: If the key is `"text"`, the value will be appended as a `TextNode` to the *current* element. The same can also be done with the `#` syntax in string arguments, though `text=` allows additional properties to come after in the same string: `$('button text=Hello click=', alert)`.
2082
+ * - **Unsafe HTML:** When the key is `"html"`, the value will be added as HTML to the *current* element. This should only be used in exceptional situations. Beware of XSS! Never use this with untrusted user data.
2083
+ *
2084
+ * ### CSS shortcuts
2085
+ * For conciseness, Aberdeen supports some CSS shortcuts when setting CSS properties.
2086
+ * | Shortcut | Expands to |
2087
+ * |----------|------------|
2088
+ * | `m`, `mt`, `mb`, `ml`, `mr` | `margin`, `margin-top`, `margin-bottom`, `margin-left`, `margin-right` |
2089
+ * | `mv`, `mh` | Vertical (top+bottom) or horizontal (left+right) margins |
2090
+ * | `p`, `pt`, `pb`, `pl`, `pr` | `padding`, `padding-top`, `padding-bottom`, `padding-left`, `padding-right` |
2091
+ * | `pv`, `ph` | Vertical or horizontal padding |
2092
+ * | `w`, `h` | `width`, `height` |
2093
+ * | `bg` | `background` |
2094
+ * | `fg` | `color` |
2095
+ * | `r` | `border-radius` |
2096
+ *
2097
+ * Also, when the value is a string starting with `$`, it is treated as a reference to a CSS variable, expanding to `var(--variableName)`. For numeric variable names (which can't be used directly as CSS custom property names), Aberdeen prefixes them with `m`, so `$3` expands to `var(--m3)`. This is primarily intended for use with {@link setSpacingCssVars}, which initializes spacing variables named `0` through `12` with an exponential spacing scale.
2098
+ *
2099
+ * @returns The most inner DOM element that was created (not counting text nodes nor elements created by content functions), or the current element if no new element was created. You should normally not need to use the return value - use this function's DOM manipulation abilities instead. One valid use case is when integrating with non-Aberdeen code that requires a reference to a DOM element.
2031
2100
  *
2032
2101
  * @example Create Element
2033
2102
  * ```typescript
@@ -2074,6 +2143,14 @@ const SPECIAL_PROPS: { [key: string]: (el: Element, value: any) => void } = {
2074
2143
  * }
2075
2144
  * });
2076
2145
  * ```
2146
+ *
2147
+ * @example Proxied objects as values
2148
+ * ```typescript
2149
+ * const myColor = proxy('red');
2150
+ * $('p text="The color is " text=', myColor, 'click=', () => myColor.value = 'yellow')
2151
+ * // Clicking the text will cause it to change color without recreating the <p> itself
2152
+ * ```
2153
+ * This is often used together with {@link ref}, in order to use properties other than `.value`.
2077
2154
  */
2078
2155
 
2079
2156
  export function $(...args: any[]): undefined | Element {
@@ -2092,16 +2169,37 @@ export function $(...args: any[]): undefined | Element {
2092
2169
  nextPos = findFirst(arg, " .=:#", pos);
2093
2170
  const next = arg[nextPos];
2094
2171
 
2095
- if (next === ":" || next === "=") {
2096
- let key = arg.substring(pos, nextPos);
2097
- if (next === ':') key = '$' + key; // Style prefix
2172
+ if (next === ":") {
2173
+ // Style property: key:value or key: value;
2174
+ const key = '$' + arg.substring(pos, nextPos);
2175
+ if (nextPos + 1 >= argLen) {
2176
+ applyArg(el, key, args[++argIndex]);
2177
+ break;
2178
+ }
2179
+ if (arg[nextPos + 1] === ' ') {
2180
+ // Long form: "key: value;" - read until semicolon
2181
+ const endIndex = findFirst(arg, ";", nextPos + 2);
2182
+ const value = arg.substring(nextPos + 2, endIndex).trim();
2183
+ applyArg(el, key, value);
2184
+ nextPos = endIndex;
2185
+ } else {
2186
+ // Short form: "key:value" - read until whitespace
2187
+ const endIndex = findFirst(arg, " ", nextPos + 1);
2188
+ const value = arg.substring(nextPos + 1, endIndex);
2189
+ applyArg(el, key, value);
2190
+ nextPos = endIndex;
2191
+ }
2192
+ } else if (next === "=") {
2193
+ // Attribute: key=value or key="quoted value"
2194
+ const key = arg.substring(pos, nextPos);
2098
2195
  if (nextPos + 1 >= argLen) {
2099
2196
  applyArg(el, key, args[++argIndex]);
2100
2197
  break;
2101
2198
  }
2102
- if (arg[nextPos+1] === '"') {
2103
- const endIndex = findFirst(arg, '"', nextPos + 2);
2104
- const value = arg.substring(nextPos+2, endIndex);
2199
+ const afterEquals = arg[nextPos + 1];
2200
+ if (afterEquals === '"' || afterEquals === "'" || afterEquals === "`") {
2201
+ const endIndex = findFirst(arg, afterEquals, nextPos + 2);
2202
+ const value = arg.substring(nextPos + 2, endIndex);
2105
2203
  applyArg(el, key, value);
2106
2204
  nextPos = endIndex;
2107
2205
  } else {
@@ -2133,6 +2231,7 @@ export function $(...args: any[]): undefined | Element {
2133
2231
  applyArg(el, arg.substring(nextPos, classEnd), args[++argIndex]);
2134
2232
  nextPos = classEnd;
2135
2233
  } else {
2234
+ // An unconditional class name
2136
2235
  let className: any = arg.substring(nextPos + 1, classEnd);
2137
2236
  el.classList.add(className || args[++argIndex]);
2138
2237
  nextPos = classEnd - 1;
@@ -2182,126 +2281,222 @@ let cssCount = 0;
2182
2281
  /**
2183
2282
  * Inserts CSS rules into the document, scoping them with a unique class name.
2184
2283
  *
2185
- * Takes a JavaScript object representation of CSS rules. camelCased property keys are
2186
- * converted to kebab-case (e.g., `fontSize` becomes `font-size`).
2284
+ * The `style` parameter can be either:
2285
+ * - A **concise style string** (for rules applying to the root class).
2286
+ * - An **object** where keys are selectors (with `&` representing the root class)
2287
+ * and values are concise style strings or nested objects. When the key does not contain `&`,
2288
+ * it is treated as a descendant selector. So `{p: "color:red"}` becomes `".AbdStlX p { color: red; }"` with `AbdStlX` being the generated class name.
2289
+ *
2290
+ * ### Concise Style Strings
2291
+ *
2292
+ * Concise style strings use two syntaxes (same as inline CSS in {@link $}):
2293
+ * - **Short form** `key:value` (no space after colon): The value ends at the next whitespace.
2294
+ * Example: `'m:$3 bg:red r:8px'`
2295
+ * - **Long form** `key: value;` (space after colon): The value continues until a semicolon.
2296
+ * Example: `'box-shadow: 2px 0 6px black; transition: all 0.3s ease;'`
2297
+ *
2298
+ * Both forms can be mixed: `'m:$3 box-shadow: 0 2px 4px rgba(0,0,0,0.2); bg:$cardBg'`
2299
+ *
2300
+ * Supports the same CSS shortcuts as {@link $} and CSS variable references with `$` (e.g., `$primary`, `$3`).
2301
+ *
2302
+ * @param style - A concise style string or a style object.
2303
+ * @returns The unique class name prefix used for scoping (e.g., `.AbdStl1`).
2304
+ * Use this prefix with {@link $} to apply the styles.
2305
+ *
2306
+ * @example Basic Usage with Shortcuts and CSS Variables
2307
+ * ```typescript
2308
+ * const cardClass = insertCss({
2309
+ * '&': 'bg:white p:$4 r:8px transition: background-color 0.3s;',
2310
+ * '&:hover': 'bg:#f5f5f5',
2311
+ * });
2187
2312
  *
2188
- * @param style - An object where keys are CSS selectors (or camelCased properties) and values are
2189
- * CSS properties or nested rule objects.
2190
- * - Selectors are usually combined as a descendant-relationship (meaning just a space character) with their parent selector.
2191
- * - In case a selector contains a `&`, that character will be replaced by the parent selector.
2192
- * - Selectors will be split on `,` characters, each combining with the parent selector with *or* semantics.
2193
- * - Selector starting with `'@'` define at-rules like media queries. They may be nested within regular selectors.
2194
- * @param global - Deprecated! Use {@link insertGlobalCss} instead.
2195
- * @returns The unique class name prefix used for scoping (e.g., `.AbdStl1`). Use this
2196
- * prefix with {@link $} to apply the styles.
2313
+ * $('section', cardClass, () => {
2314
+ * $('p#Card content');
2315
+ * });
2316
+ * ```
2197
2317
  *
2198
- * @example Scoped Styles
2318
+ * @example Nested Selectors and Media Queries
2199
2319
  * ```typescript
2200
- * const scopeClass = insertCss({
2201
- * color: 'red',
2202
- * padding: '10px',
2203
- * '&:hover': { // Use '&' for the root scoped selector
2204
- * backgroundColor: '#535'
2205
- * },
2206
- * '.child-element': { // Nested selector
2207
- * fontWeight: 'bold'
2208
- * },
2209
- * '@media (max-width: 600px)': {
2210
- * padding: '5px'
2320
+ * const formClass = insertCss({
2321
+ * '&': 'bg:#0004 p:$3 r:$2',
2322
+ * button: {
2323
+ * '&': 'bg:$primary fg:white p:$2 r:4px cursor:pointer',
2324
+ * '&:hover': 'bg:$primaryHover',
2325
+ * '&:disabled': 'bg:#ccc cursor:not-allowed',
2326
+ * '.icon': 'display:inline-block mr:$1',
2327
+ * '@media (max-width: 600px)': 'p:$1 font-size:14px'
2211
2328
  * }
2212
2329
  * });
2213
- * // scopeClass might be ".AbdStl1"
2214
2330
  *
2215
- * // Apply the styles
2216
- * $(scopeClass, () => { // Add class to the div
2217
- * $(`#Scoped content`);
2218
- * $('div.child-element#Child'); // .AbdStl1 .child-element rule applies
2331
+ * $('form', formClass, () => {
2332
+ * $('button', () => {
2333
+ * $('span.icon text=🔥');
2334
+ * $('#Click Me');
2335
+ * });
2219
2336
  * });
2220
2337
  * ```
2338
+ *
2339
+ * @example Complex CSS Values
2340
+ * ```typescript
2341
+ * const badge = insertCss({
2342
+ * '&::before': 'content: "★"; color:gold mr:$1',
2343
+ * '&': 'position:relative box-shadow: 0 2px 8px rgba(0,0,0,0.15);'
2344
+ * });
2345
+ *
2346
+ * $(badge + ' span#Product Name');
2347
+ * ```
2221
2348
  */
2222
- export function insertCss(style: object, global = false): string {
2223
- const prefix = global ? "" : `.AbdStl${++cssCount}`;
2224
- const css = styleToCss(style, prefix);
2349
+ export function insertCss(style: string | object): string {
2350
+ const prefix = `.AbdStl${++cssCount}`;
2351
+ const css = typeof style === 'string' ? styleStringToCss(style, prefix) : objectToCss(style, prefix);
2225
2352
  if (css) $(`style#${css}`);
2226
2353
  return prefix;
2227
2354
  }
2228
2355
 
2356
+ function objectToCss(style: object, prefix: string): string {
2357
+ let css = "";
2358
+
2359
+ for (const [key, val] of Object.entries(style)) {
2360
+ if (val && typeof val === 'object') {
2361
+ // Nested object for media queries or compound selectors
2362
+ if (key.startsWith("@")) {
2363
+ // Media query or @ rule - nest the content
2364
+ css += `${key}{\n${objectToCss(val, prefix)}}\n`;
2365
+ } else {
2366
+ // Regular nested selector
2367
+ const sel = key === '&' ? prefix :
2368
+ key.includes("&") ? key.replace(/&/g, prefix) :
2369
+ `${prefix} ${key}`.trim();
2370
+ css += objectToCss(val, sel);
2371
+ }
2372
+ } else if (typeof val === 'string') {
2373
+ if (key.startsWith("@")) {
2374
+ // Media query with string value - wrap it
2375
+ css += `${key}{\n${styleStringToCss(val, prefix)}}\n`;
2376
+ } else {
2377
+ // String value - parse as style string
2378
+ const sel = key.includes("&") ? key.replace(/&/g, prefix) : `${prefix} ${key}`.trim();
2379
+ css += styleStringToCss(val, sel);
2380
+ }
2381
+ }
2382
+ }
2383
+
2384
+ return css;
2385
+ }
2386
+
2387
+
2388
+ const KEBAB_SEGMENT = /-([a-z])/g;
2389
+ function toCamel(p: string) {
2390
+ return p.replace(KEBAB_SEGMENT, (_, l) => l.toUpperCase());
2391
+ }
2392
+
2393
+ function styleStringToCss(styleStr: string, selector: string): string {
2394
+ let props = "";
2395
+
2396
+ for (let pos = 0, len = styleStr.length; pos < len;) {
2397
+ while (styleStr[pos] === ' ') pos++; // Skip whitespace
2398
+ if (pos >= len) break;
2399
+
2400
+ const colon = styleStr.indexOf(':', pos);
2401
+ if (colon === -1) break;
2402
+ const key = styleStr.substring(pos, colon);
2403
+ pos = colon + 1;
2404
+
2405
+ // Parse value based on whether there's a space after the colon
2406
+ let val: string;
2407
+ if (styleStr[pos] === ' ') {
2408
+ // Long form: space after colon means value ends at semicolon
2409
+ pos++; // skip the space
2410
+ const semi = styleStr.indexOf(';', pos);
2411
+ val = styleStr.substring(pos, semi === -1 ? len : semi).trim();
2412
+ pos = semi === -1 ? len : semi + 1;
2413
+ } else {
2414
+ // Short form: no space means value ends at whitespace
2415
+ const space = styleStr.indexOf(' ', pos);
2416
+ val = styleStr.substring(pos, space === -1 ? len : space);
2417
+ pos = space === -1 ? len : space;
2418
+ }
2419
+
2420
+ // Expand shortcuts and add to props
2421
+ const v = cssVarRef(val);
2422
+ const exp = CSS_SHORT[key] || key;
2423
+ props += typeof exp === 'string' ? `${exp}:${v};` : exp.map(p => `${p}:${v};`).join('');
2424
+ }
2425
+
2426
+ return props ? `${selector}{${props}}\n` : "";
2427
+ }
2428
+
2229
2429
  /**
2230
- * Inserts CSS rules globally.
2430
+ * Inserts CSS rules globally (unscoped).
2231
2431
  *
2232
2432
  * Works exactly like {@link insertCss}, but without prefixing selectors with a unique class name.
2433
+ * This is useful for global resets, base styles, or styles that need to apply to the entire document.
2233
2434
  *
2234
- * @example Global Styles
2435
+ * Accepts the same concise style string syntax and CSS shortcuts as {@link insertCss}.
2436
+ * See {@link insertCss} for detailed documentation on syntax and shortcuts.
2437
+ *
2438
+ * @param style - Object with selectors as keys and concise CSS strings as values.
2439
+ *
2440
+ * @example Global Reset and Base Styles
2441
+ * ```typescript
2442
+ * // Set up global styles using CSS shortcuts
2443
+ * insertGlobalCss({
2444
+ * "*": "m:0 p:0 box-sizing:border-box",
2445
+ * "body": "font-family: system-ui, sans-serif; m:0 p:$3 bg:#434 fg:#d0dafa",
2446
+ * "a": "text-decoration:none fg:#57f",
2447
+ * "a:hover": "text-decoration:underline",
2448
+ * "code": "font-family:monospace bg:#222 fg:#afc p:4px r:3px"
2449
+ * });
2450
+ *
2451
+ * $('h2#Title without margins');
2452
+ * $('a#This is a link');
2453
+ * $('code#const x = 42;');
2454
+ * ```
2455
+ *
2456
+ * @example Responsive Global Styles
2235
2457
  * ```typescript
2236
2458
  * insertGlobalCss({
2237
- * '*': {
2238
- * fontFamily: 'monospace',
2239
- * m: 0, // Using shortcut for margin
2459
+ * "html": "font-size:16px",
2460
+ * "body": "line-height:1.6",
2461
+ * "h1, h2, h3": "font-weight:600 mt:$4 mb:$2",
2462
+ * "@media (max-width: 768px)": {
2463
+ * "html": "font-size:14px",
2464
+ * "body": "p:$2"
2240
2465
  * },
2241
- * 'a': {
2242
- * textDecoration: 'none',
2243
- * fg: "@primary", // Using foreground shortcut and CSS variable
2466
+ * "@media (prefers-color-scheme: dark)": {
2467
+ * "body": "bg:#1a1a1a fg:#e5e5e5",
2468
+ * "code": "bg:#2a2a2a"
2244
2469
  * }
2245
2470
  * });
2246
- *
2247
- * $('a#Styled link');
2248
2471
  * ```
2249
2472
  */
2250
- export function insertGlobalCss(style: object): string {
2251
- return insertCss(style, true);
2473
+ export function insertGlobalCss(style: object) {
2474
+ const css = objectToCss(style, "");
2475
+ if (css) $(`style#${css}`);
2252
2476
  }
2253
2477
 
2254
2478
  const CSS_SHORT: Record<string, string | string[]> = {
2255
2479
  m: "margin",
2256
- mt: "marginTop",
2257
- mb: "marginBottom",
2258
- ml: "marginLeft",
2259
- mr: "marginRight",
2260
- mh: ["marginLeft", "marginRight"],
2261
- mv: ["marginTop", "marginBottom"],
2480
+ mt: "margin-top",
2481
+ mb: "margin-bottom",
2482
+ ml: "margin-left",
2483
+ mr: "margin-right",
2484
+ mh: ["margin-left", "margin-right"],
2485
+ mv: ["margin-top", "margin-bottom"],
2262
2486
  p: "padding",
2263
- pt: "paddingTop",
2264
- pb: "paddingBottom",
2265
- pl: "paddingLeft",
2266
- pr: "paddingRight",
2267
- ph: ["paddingLeft", "paddingRight"],
2268
- pv: ["paddingTop", "paddingBottom"],
2487
+ pt: "padding-top",
2488
+ pb: "padding-bottom",
2489
+ pl: "padding-left",
2490
+ pr: "padding-right",
2491
+ ph: ["padding-left", "padding-right"],
2492
+ pv: ["padding-top", "padding-bottom"],
2269
2493
  w: "width",
2270
2494
  h: "height",
2271
2495
  bg: "background",
2272
2496
  fg: "color",
2273
- r: "borderRadius",
2497
+ r: "border-radius",
2274
2498
  };
2275
2499
 
2276
- function styleToCss(style: object, prefix: string): string {
2277
- let props = "";
2278
- let rules = "";
2279
- for (const kOr of Object.keys(style)) {
2280
- const v = (style as any)[kOr];
2281
- for (const k of kOr.split(/, ?/g)) {
2282
- if (v && typeof v === "object") {
2283
- if (k.startsWith("@")) {
2284
- // media queries
2285
- rules += `${k}{\n${styleToCss(v, prefix)}}\n`;
2286
- } else {
2287
- rules += styleToCss(
2288
- v,
2289
- k.includes("&") ? k.replace(/&/g, prefix) : `${prefix} ${k}`,
2290
- );
2291
- }
2292
- } else {
2293
- const val = v == null || v === false ? "" : typeof v === 'string' ? (v[0] === '$' ? cssVarRef(v.substring(1)) : v) : String(v);
2294
- const expanded = CSS_SHORT[k] || k;
2295
- for (const prop of (Array.isArray(expanded) ? expanded : [expanded])) {
2296
- props += `${prop.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}:${val};`;
2297
- }
2298
- }
2299
- }
2300
- }
2301
- if (props) rules = `${prefix.trimStart() || "*"}{${props}}\n${rules}`;
2302
- return rules;
2303
- }
2304
-
2305
2500
  function applyArg(el: Element, key: string, value: any) {
2306
2501
  if (typeof value === "object" && value !== null && value[TARGET_SYMBOL]) {
2307
2502
  // Value is a proxy
@@ -2319,12 +2514,12 @@ function applyArg(el: Element, key: string, value: any) {
2319
2514
  } else if (key[0] === "$") {
2320
2515
  // Style (with shortcuts)
2321
2516
  key = key.substring(1);
2322
- const val = value == null || value === false ? "" : typeof value === 'string' ? (value[0] === '$' ? cssVarRef(value.substring(1)) : value) : String(value);
2517
+ const val = value == null || value === false ? "" : typeof value === 'string' ? cssVarRef(value) : String(value);
2323
2518
  const expanded = CSS_SHORT[key] || key;
2324
2519
  if (typeof expanded === "string") {
2325
- (el as any).style[expanded] = val;
2520
+ (el as any).style[toCamel(expanded)] = val;
2326
2521
  } else {
2327
- for (const prop of expanded) (el as any).style[prop] = val;
2522
+ for (const prop of expanded) (el as any).style[toCamel(prop)] = val;
2328
2523
  }
2329
2524
  } else if (value == null) {
2330
2525
  // Value left empty
@@ -2380,7 +2575,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
2380
2575
  *
2381
2576
  * try {
2382
2577
  * // Attempt to show a custom message in the UI
2383
- * $('div.error-message#Oops, something went wrong!');
2578
+ * $('div#Oops, something went wrong!', errorClass);
2384
2579
  * } catch (e) {
2385
2580
  * // Ignore errors during error handling itself
2386
2581
  * }
@@ -2389,15 +2584,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
2389
2584
  * });
2390
2585
  *
2391
2586
  * // Styling for our custom error message
2392
- * insertCss({
2393
- * '.error-message': {
2394
- * backgroundColor: '#e31f00',
2395
- * display: 'inline-block',
2396
- * color: 'white',
2397
- * borderRadius: '3px',
2398
- * padding: '2px 4px',
2399
- * }
2400
- * }, true); // global style
2587
+ * const errorClass = insertCss('background-color:#e31f00 display:inline-block color:white r:3px padding: 2px 4px;');
2401
2588
  *
2402
2589
  * // Cause an error within a render scope.
2403
2590
  * $('div.box', () => {
@@ -2412,34 +2599,6 @@ export function setErrorHandler(
2412
2599
  onError = handler || defaultOnError;
2413
2600
  }
2414
2601
 
2415
- /**
2416
- * Gets the parent DOM `Element` where nodes created by {@link $} would currently be inserted.
2417
- *
2418
- * This is context-dependent based on the current reactive scope (e.g., inside a {@link mount}
2419
- * call or a {@link $} element's render function).
2420
- *
2421
- * **Note:** While this provides access to the DOM element, directly manipulating it outside
2422
- * of Aberdeen's control is generally discouraged. Prefer reactive updates using {@link $}.
2423
- *
2424
- * @returns The current parent `Element` for DOM insertion.
2425
- *
2426
- * @example Get parent for attaching a third-party library
2427
- * ```typescript
2428
- * function thirdPartyLibInit(parentElement) {
2429
- * parentElement.innerHTML = "This element is managed by a <em>third party</em> lib."
2430
- * }
2431
- *
2432
- * $('div.box', () => {
2433
- * // Get the div.box element just created
2434
- * const containerElement = getParentElement();
2435
- * thirdPartyLibInit(containerElement);
2436
- * });
2437
- * ```
2438
- */
2439
- export function getParentElement(): Element {
2440
- return currentScope.el;
2441
- }
2442
-
2443
2602
  /**
2444
2603
  * Registers a cleanup function to be executed just before the current reactive scope
2445
2604
  * is destroyed or redraws.
@@ -2585,7 +2744,8 @@ export function mount(parentElement: Element, func: () => void) {
2585
2744
  * Removes all Aberdeen-managed DOM nodes and stops all active reactive scopes
2586
2745
  * (created by {@link mount}, {@link derive}, {@link $} with functions, etc.).
2587
2746
  *
2588
- * This effectively cleans up the entire Aberdeen application state.
2747
+ * This effectively cleans up the entire Aberdeen application state. Aside from in
2748
+ * automated tests, there should probably be little reason to call this function.
2589
2749
  */
2590
2750
  export function unmountAll() {
2591
2751
  ROOT_SCOPE.remove();
@@ -2706,7 +2866,7 @@ export function map(
2706
2866
  } else {
2707
2867
  out = optProxy({});
2708
2868
  }
2709
-
2869
+
2710
2870
  onEach(source, (item: any, key: symbol | string | number) => {
2711
2871
  const value = func(item, key);
2712
2872
  if (value !== undefined) {