aberdeen 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -1644,7 +1679,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
1644
1679
  const old = dst.get(k);
1645
1680
  dst.delete(k);
1646
1681
  if (flags & COPY_EMIT) {
1647
- emit(dst, k, undefined, old);
1682
+ emit(dst, k, EMPTY, old);
1648
1683
  }
1649
1684
  changed = true;
1650
1685
  }
@@ -1677,7 +1712,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
1677
1712
  const old = dst[k];
1678
1713
  delete dst[k];
1679
1714
  if (flags & COPY_EMIT && old !== undefined) {
1680
- emit(dst, k, undefined, old);
1715
+ emit(dst, k, EMPTY, old);
1681
1716
  }
1682
1717
  changed = true;
1683
1718
  }
@@ -1704,33 +1739,148 @@ export const NO_COPY = Symbol("NO_COPY");
1704
1739
  (Promise.prototype as any)[NO_COPY] = true;
1705
1740
 
1706
1741
  /**
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
- *
1742
+ * A reactive object containing CSS variable definitions.
1743
+ *
1744
+ * Any property you assign to `cssVars` becomes available as a CSS custom property throughout your application.
1745
+ *
1746
+ * Use {@link setSpacingCssVars} to optionally initialize `cssVars[1]` through `cssVars[12]` with an exponential spacing scale.
1747
+ *
1748
+ * When you reference a CSS variable in Aberdeen using the `$` prefix (e.g., `$primary`), it automatically resolves to `var(--primary)`.
1749
+ * 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)`).
1750
+ *
1751
+ * When you add the first property to cssVars, Aberdeen automatically creates a reactive `<style>` tag in `<head>`
1752
+ * containing the `:root` CSS custom property declarations. The style tag is automatically removed if cssVars becomes empty.
1753
+ *
1715
1754
  * @example
1716
- * ```typescript
1755
+ * ```javascript
1756
+ * import { cssVars, setSpacingCssVars, $ } from 'aberdeen';
1757
+ *
1758
+ * // Optionally initialize spacing scale
1759
+ * setSpacingCssVars(); // Uses defaults: base=1, unit='rem'
1760
+ *
1761
+ * // Define custom colors - style tag is automatically created
1717
1762
  * 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)
1763
+ * cssVars.danger = '#ef4444';
1764
+ *
1765
+ * // Use in elements with the $ prefix
1766
+ * $('button bg:$primary fg:white');
1767
+ *
1768
+ * // Use spacing (if setSpacingCssVars() was called)
1769
+ * $('div mt:$3'); // Sets margin-top to var(--m3)
1721
1770
  * ```
1722
1771
  */
1723
1772
  export const cssVars: Record<string, string> = optProxy({});
1724
1773
 
1725
- for (let i = 1; i <= 12; i++) {
1726
- cssVars[i] = 2 ** (i - 3) + "rem";
1774
+ /**
1775
+ * Initializes `cssVars[0]` through `cssVars[12]` with an exponential spacing scale.
1776
+ *
1777
+ * The scale is calculated as `2^(n-3) * base`, providing values from `0.25 * base` to `512 * base`.
1778
+ *
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'.
1781
+ *
1782
+ * @example
1783
+ * ```javascript
1784
+ * import { setSpacingCssVars, cssVars, onEach, $} from 'aberdeen';
1785
+ * // Use default scale (0.25rem to 512rem)
1786
+ * setSpacingCssVars();
1787
+ *
1788
+ * // Use custom base size
1789
+ * setSpacingCssVars(16, 'px'); // 4px to 8192px
1790
+ *
1791
+ * // Use em units
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
1798
+ * ```
1799
+ */
1800
+ export function setSpacingCssVars(base = 1, unit = 'rem'): void {
1801
+ for (let i = 0; i <= 12; i++) {
1802
+ cssVars[i] = 2 ** (i - 3) * base + unit;
1803
+ }
1727
1804
  }
1728
1805
 
1806
+ // Matches: (1) parenthesized content, (2) quoted content, (3) $varName at start or after space
1807
+ const CSS_VAR_PATTERN = /(\([^)]*\))|("[^"]*")|(^| )\$(\w+)/g;
1729
1808
  const DIGIT_FIRST = /^\d/;
1730
- function cssVarRef(name: string): string {
1731
- // Prefix numeric keys with 'm' (CSS custom property names can't start with a digit)
1732
- const varName = DIGIT_FIRST.test(name) ? `m${name}` : name;
1733
- 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
+ });
1823
+ }
1824
+
1825
+ // Automatically mount cssVars style tag to document.head when cssVars is not empty
1826
+ if (typeof document !== "undefined") {
1827
+ leakScope(() => {
1828
+ $(() => {
1829
+ if (!isEmpty(cssVars)) {
1830
+ mount(document.head, () => {
1831
+ $('style', () => {
1832
+ let css = ":root {\n";
1833
+ for(const [key, value] of Object.entries(cssVars)) {
1834
+ const varName = DIGIT_FIRST.test(String(key)) ? `m${key}` : key;
1835
+ css += ` --${varName}: ${value};\n`;
1836
+ }
1837
+ css += "}";
1838
+ $(`#${css}`);
1839
+ });
1840
+ });
1841
+ }
1842
+ });
1843
+ });
1844
+ }
1845
+
1846
+
1847
+ let darkModeState: {value: boolean} | undefined;
1848
+
1849
+ /**
1850
+ * Returns whether the user's browser prefers a dark color scheme.
1851
+ *
1852
+ * This function is reactive - scopes that call it will re-execute when the
1853
+ * browser's color scheme preference changes (via the `prefers-color-scheme` media query).
1854
+ *
1855
+ * Use this in combination with {@link $} and {@link cssVars} to implement theme switching:
1856
+ *
1857
+ * @returns `true` if the browser prefers dark mode, `false` if it prefers light mode.
1858
+ *
1859
+ * @example
1860
+ * ```javascript
1861
+ * import { darkMode, cssVars, $, mount } from 'aberdeen';
1862
+ *
1863
+ * // Reactively set colors based on browser preference
1864
+ * $(() => {
1865
+ * cssVars.bg = darkMode() ? '#1a1a1a' : '#ffffff';
1866
+ * cssVars.fg = darkMode() ? '#e5e5e5' : '#000000';
1867
+ * });
1868
+ *
1869
+ * $('div bg:$bg fg:$fg p:1rem #Colors change based on system dark mode preference');
1870
+ * ```
1871
+ */
1872
+ export function darkMode(): boolean {
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);
1881
+ }
1882
+
1883
+ return darkModeState.value;
1734
1884
  }
1735
1885
 
1736
1886
  /**
@@ -1833,12 +1983,9 @@ function applyBind(el: HTMLInputElement, target: any) {
1833
1983
  };
1834
1984
  } else {
1835
1985
  onInputChange = () => {
1836
- target.value =
1837
- type === "number" || type === "range"
1838
- ? el.value === ""
1839
- ? null
1840
- : +el.value
1841
- : el.value;
1986
+ target.value = type === "number" || type === "range"
1987
+ ? el.value === "" ? null : +el.value
1988
+ : el.value;
1842
1989
  };
1843
1990
  if (value === undefined) onInputChange();
1844
1991
  onProxyChange = () => {
@@ -1894,38 +2041,62 @@ const SPECIAL_PROPS: { [key: string]: (el: Element, value: any) => void } = {
1894
2041
  *
1895
2042
  * @param {...(string | function | object | false | undefined | null)} args - Any number of arguments can be given. How they're interpreted depends on their types:
1896
2043
  *
1897
- * - `string`: Strings can be used to create and insert new elements, set classnames for the *current* element, and add text to the current element.
1898
- * The format of a string is: (**tag** | `.` **class** | **key**=**val** | **key**="**long val**")* ('#' **text** | **key**=)?
1899
- * So there can be:
1900
- * - 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.
1901
- * - 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.
1902
- * - 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).
1903
- * - 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.
1904
- * - 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")`.
1905
- * - `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}).
1906
- * - `object`: When an object is passed in, its key-value pairs are used to modify the *current* element in the following ways...
1907
- * - `{<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.
1908
- * - `{<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.
1909
- * - `{".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.
1910
- * - `{<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}`.
1911
- * - `{$<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'}`.
1912
- * - `{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`.
1913
- * - `{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`.
1914
- * - `{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.
1915
- * - `{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`.
1916
- * - `{<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:
1917
- * ```typescript
1918
- * const myColor = proxy('red');
1919
- * $('p#Test', {$color: myColor, click: () => myColor.value = 'yellow'})
1920
- * // Clicking the text will cause it to change color without recreating the <p> itself
1921
- * ```
1922
- * This is often used together with {@link ref}, in order to use properties other than `.value`.
1923
- * - `{text: string|number}`: Add the value as a `TextNode` to the *current* element.
1924
- * - `{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.
1925
- - `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.
1926
- *
1927
- * @returns The most inner DOM element that was created (not counting text nodes nor elements created by content functions),
1928
- * 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.
1929
2100
  *
1930
2101
  * @example Create Element
1931
2102
  * ```typescript
@@ -1972,6 +2143,14 @@ const SPECIAL_PROPS: { [key: string]: (el: Element, value: any) => void } = {
1972
2143
  * }
1973
2144
  * });
1974
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`.
1975
2154
  */
1976
2155
 
1977
2156
  export function $(...args: any[]): undefined | Element {
@@ -1990,16 +2169,37 @@ export function $(...args: any[]): undefined | Element {
1990
2169
  nextPos = findFirst(arg, " .=:#", pos);
1991
2170
  const next = arg[nextPos];
1992
2171
 
1993
- if (next === ":" || next === "=") {
1994
- let key = arg.substring(pos, nextPos);
1995
- 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);
1996
2195
  if (nextPos + 1 >= argLen) {
1997
2196
  applyArg(el, key, args[++argIndex]);
1998
2197
  break;
1999
2198
  }
2000
- if (arg[nextPos+1] === '"') {
2001
- const endIndex = findFirst(arg, '"', nextPos + 2);
2002
- 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);
2003
2203
  applyArg(el, key, value);
2004
2204
  nextPos = endIndex;
2005
2205
  } else {
@@ -2031,6 +2231,7 @@ export function $(...args: any[]): undefined | Element {
2031
2231
  applyArg(el, arg.substring(nextPos, classEnd), args[++argIndex]);
2032
2232
  nextPos = classEnd;
2033
2233
  } else {
2234
+ // An unconditional class name
2034
2235
  let className: any = arg.substring(nextPos + 1, classEnd);
2035
2236
  el.classList.add(className || args[++argIndex]);
2036
2237
  nextPos = classEnd - 1;
@@ -2080,126 +2281,222 @@ let cssCount = 0;
2080
2281
  /**
2081
2282
  * Inserts CSS rules into the document, scoping them with a unique class name.
2082
2283
  *
2083
- * Takes a JavaScript object representation of CSS rules. camelCased property keys are
2084
- * 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.
2085
2289
  *
2086
- * @param style - An object where keys are CSS selectors (or camelCased properties) and values are
2087
- * CSS properties or nested rule objects.
2088
- * - Selectors are usually combined as a descendant-relationship (meaning just a space character) with their parent selector.
2089
- * - In case a selector contains a `&`, that character will be replaced by the parent selector.
2090
- * - Selectors will be split on `,` characters, each combining with the parent selector with *or* semantics.
2091
- * - Selector starting with `'@'` define at-rules like media queries. They may be nested within regular selectors.
2092
- * @param global - Deprecated! Use {@link insertGlobalCss} instead.
2093
- * @returns The unique class name prefix used for scoping (e.g., `.AbdStl1`). Use this
2094
- * prefix with {@link $} to apply the styles.
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.
2095
2305
  *
2096
- * @example Scoped Styles
2306
+ * @example Basic Usage with Shortcuts and CSS Variables
2097
2307
  * ```typescript
2098
- * const scopeClass = insertCss({
2099
- * color: 'red',
2100
- * padding: '10px',
2101
- * '&:hover': { // Use '&' for the root scoped selector
2102
- * backgroundColor: '#535'
2103
- * },
2104
- * '.child-element': { // Nested selector
2105
- * fontWeight: 'bold'
2106
- * },
2107
- * '@media (max-width: 600px)': {
2108
- * padding: '5px'
2308
+ * const cardClass = insertCss({
2309
+ * '&': 'bg:white p:$4 r:8px transition: background-color 0.3s;',
2310
+ * '&:hover': 'bg:#f5f5f5',
2311
+ * });
2312
+ *
2313
+ * $('section', cardClass, () => {
2314
+ * $('p#Card content');
2315
+ * });
2316
+ * ```
2317
+ *
2318
+ * @example Nested Selectors and Media Queries
2319
+ * ```typescript
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'
2109
2328
  * }
2110
2329
  * });
2111
- * // scopeClass might be ".AbdStl1"
2112
2330
  *
2113
- * // Apply the styles
2114
- * $(scopeClass, () => { // Add class to the div
2115
- * $(`#Scoped content`);
2116
- * $('div.child-element#Child'); // .AbdStl1 .child-element rule applies
2331
+ * $('form', formClass, () => {
2332
+ * $('button', () => {
2333
+ * $('span.icon text=🔥');
2334
+ * $('#Click Me');
2335
+ * });
2117
2336
  * });
2118
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
+ * ```
2119
2348
  */
2120
- export function insertCss(style: object, global = false): string {
2121
- const prefix = global ? "" : `.AbdStl${++cssCount}`;
2122
- 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);
2123
2352
  if (css) $(`style#${css}`);
2124
2353
  return prefix;
2125
2354
  }
2126
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
+
2127
2429
  /**
2128
- * Inserts CSS rules globally.
2430
+ * Inserts CSS rules globally (unscoped).
2129
2431
  *
2130
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.
2434
+ *
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.
2131
2439
  *
2132
- * @example Global Styles
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
2133
2457
  * ```typescript
2134
2458
  * insertGlobalCss({
2135
- * '*': {
2136
- * fontFamily: 'monospace',
2137
- * 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"
2138
2465
  * },
2139
- * 'a': {
2140
- * textDecoration: 'none',
2141
- * fg: "@primary", // Using foreground shortcut and CSS variable
2466
+ * "@media (prefers-color-scheme: dark)": {
2467
+ * "body": "bg:#1a1a1a fg:#e5e5e5",
2468
+ * "code": "bg:#2a2a2a"
2142
2469
  * }
2143
2470
  * });
2144
- *
2145
- * $('a#Styled link');
2146
2471
  * ```
2147
2472
  */
2148
- export function insertGlobalCss(style: object): string {
2149
- return insertCss(style, true);
2473
+ export function insertGlobalCss(style: object) {
2474
+ const css = objectToCss(style, "");
2475
+ if (css) $(`style#${css}`);
2150
2476
  }
2151
2477
 
2152
2478
  const CSS_SHORT: Record<string, string | string[]> = {
2153
2479
  m: "margin",
2154
- mt: "marginTop",
2155
- mb: "marginBottom",
2156
- ml: "marginLeft",
2157
- mr: "marginRight",
2158
- mh: ["marginLeft", "marginRight"],
2159
- 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"],
2160
2486
  p: "padding",
2161
- pt: "paddingTop",
2162
- pb: "paddingBottom",
2163
- pl: "paddingLeft",
2164
- pr: "paddingRight",
2165
- ph: ["paddingLeft", "paddingRight"],
2166
- 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"],
2167
2493
  w: "width",
2168
2494
  h: "height",
2169
2495
  bg: "background",
2170
2496
  fg: "color",
2171
- r: "borderRadius",
2497
+ r: "border-radius",
2172
2498
  };
2173
2499
 
2174
- function styleToCss(style: object, prefix: string): string {
2175
- let props = "";
2176
- let rules = "";
2177
- for (const kOr of Object.keys(style)) {
2178
- const v = (style as any)[kOr];
2179
- for (const k of kOr.split(/, ?/g)) {
2180
- if (v && typeof v === "object") {
2181
- if (k.startsWith("@")) {
2182
- // media queries
2183
- rules += `${k}{\n${styleToCss(v, prefix)}}\n`;
2184
- } else {
2185
- rules += styleToCss(
2186
- v,
2187
- k.includes("&") ? k.replace(/&/g, prefix) : `${prefix} ${k}`,
2188
- );
2189
- }
2190
- } else {
2191
- const val = v == null || v === false ? "" : typeof v === 'string' ? (v[0] === '@' ? cssVarRef(v.substring(1)) : v) : String(v);
2192
- const expanded = CSS_SHORT[k] || k;
2193
- for (const prop of (Array.isArray(expanded) ? expanded : [expanded])) {
2194
- props += `${prop.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}:${val};`;
2195
- }
2196
- }
2197
- }
2198
- }
2199
- if (props) rules = `${prefix.trimStart() || "*"}{${props}}\n${rules}`;
2200
- return rules;
2201
- }
2202
-
2203
2500
  function applyArg(el: Element, key: string, value: any) {
2204
2501
  if (typeof value === "object" && value !== null && value[TARGET_SYMBOL]) {
2205
2502
  // Value is a proxy
@@ -2217,12 +2514,12 @@ function applyArg(el: Element, key: string, value: any) {
2217
2514
  } else if (key[0] === "$") {
2218
2515
  // Style (with shortcuts)
2219
2516
  key = key.substring(1);
2220
- 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);
2221
2518
  const expanded = CSS_SHORT[key] || key;
2222
2519
  if (typeof expanded === "string") {
2223
- (el as any).style[expanded] = val;
2520
+ (el as any).style[toCamel(expanded)] = val;
2224
2521
  } else {
2225
- for (const prop of expanded) (el as any).style[prop] = val;
2522
+ for (const prop of expanded) (el as any).style[toCamel(prop)] = val;
2226
2523
  }
2227
2524
  } else if (value == null) {
2228
2525
  // Value left empty
@@ -2278,7 +2575,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
2278
2575
  *
2279
2576
  * try {
2280
2577
  * // Attempt to show a custom message in the UI
2281
- * $('div.error-message#Oops, something went wrong!');
2578
+ * $('div#Oops, something went wrong!', errorClass);
2282
2579
  * } catch (e) {
2283
2580
  * // Ignore errors during error handling itself
2284
2581
  * }
@@ -2287,15 +2584,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
2287
2584
  * });
2288
2585
  *
2289
2586
  * // Styling for our custom error message
2290
- * insertCss({
2291
- * '.error-message': {
2292
- * backgroundColor: '#e31f00',
2293
- * display: 'inline-block',
2294
- * color: 'white',
2295
- * borderRadius: '3px',
2296
- * padding: '2px 4px',
2297
- * }
2298
- * }, true); // global style
2587
+ * const errorClass = insertCss('background-color:#e31f00 display:inline-block color:white r:3px padding: 2px 4px;');
2299
2588
  *
2300
2589
  * // Cause an error within a render scope.
2301
2590
  * $('div.box', () => {
@@ -2310,34 +2599,6 @@ export function setErrorHandler(
2310
2599
  onError = handler || defaultOnError;
2311
2600
  }
2312
2601
 
2313
- /**
2314
- * Gets the parent DOM `Element` where nodes created by {@link $} would currently be inserted.
2315
- *
2316
- * This is context-dependent based on the current reactive scope (e.g., inside a {@link mount}
2317
- * call or a {@link $} element's render function).
2318
- *
2319
- * **Note:** While this provides access to the DOM element, directly manipulating it outside
2320
- * of Aberdeen's control is generally discouraged. Prefer reactive updates using {@link $}.
2321
- *
2322
- * @returns The current parent `Element` for DOM insertion.
2323
- *
2324
- * @example Get parent for attaching a third-party library
2325
- * ```typescript
2326
- * function thirdPartyLibInit(parentElement) {
2327
- * parentElement.innerHTML = "This element is managed by a <em>third party</em> lib."
2328
- * }
2329
- *
2330
- * $('div.box', () => {
2331
- * // Get the div.box element just created
2332
- * const containerElement = getParentElement();
2333
- * thirdPartyLibInit(containerElement);
2334
- * });
2335
- * ```
2336
- */
2337
- export function getParentElement(): Element {
2338
- return currentScope.el;
2339
- }
2340
-
2341
2602
  /**
2342
2603
  * Registers a cleanup function to be executed just before the current reactive scope
2343
2604
  * is destroyed or redraws.
@@ -2483,7 +2744,8 @@ export function mount(parentElement: Element, func: () => void) {
2483
2744
  * Removes all Aberdeen-managed DOM nodes and stops all active reactive scopes
2484
2745
  * (created by {@link mount}, {@link derive}, {@link $} with functions, etc.).
2485
2746
  *
2486
- * 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.
2487
2749
  */
2488
2750
  export function unmountAll() {
2489
2751
  ROOT_SCOPE.remove();
@@ -2604,7 +2866,7 @@ export function map(
2604
2866
  } else {
2605
2867
  out = optProxy({});
2606
2868
  }
2607
-
2869
+
2608
2870
  onEach(source, (item: any, key: symbol | string | number) => {
2609
2871
  const value = func(item, key);
2610
2872
  if (value !== undefined) {
@@ -2914,20 +3176,4 @@ export function withEmitHandler(
2914
3176
  }
2915
3177
  }
2916
3178
 
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
- }
3179
+