aberdeen 1.18.2 → 1.19.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.
Files changed (152) hide show
  1. package/README.md +9 -0
  2. package/dist/src/aberdeen.d.ts +27 -0
  3. package/{dist-docs/assets/aberdeen/aberdeen.js → dist/src/aberdeen.dev.js} +776 -15
  4. package/dist/src/aberdeen.dev.js.map +12 -0
  5. package/dist/src/aberdeen.js +59 -13
  6. package/dist/src/aberdeen.js.map +3 -3
  7. package/dist/src/devtools.d.ts +26 -0
  8. package/html-to-aberdeen +9 -5
  9. package/package.json +17 -10
  10. package/skill/A.md +127 -0
  11. package/skill/Dispatcher.md +51 -0
  12. package/skill/OPAQUE.md +15 -0
  13. package/skill/PromiseProxy.md +25 -0
  14. package/skill/Route.md +50 -0
  15. package/skill/SKILL.md +272 -156
  16. package/skill/applyCanon.md +20 -0
  17. package/skill/applyPrediction.md +15 -0
  18. package/skill/back.md +11 -0
  19. package/skill/clean.md +43 -0
  20. package/skill/clone.md +15 -0
  21. package/skill/copy.md +40 -0
  22. package/skill/count.md +32 -0
  23. package/skill/cssVars.md +15 -0
  24. package/skill/darkMode.md +26 -0
  25. package/skill/derive.md +56 -0
  26. package/skill/developer-tools.md +23 -0
  27. package/skill/disableCreateDestroy.md +11 -0
  28. package/skill/dump.md +37 -0
  29. package/skill/freeze.md +20 -0
  30. package/skill/full-example-multi-page-app.md +146 -0
  31. package/skill/go.md +25 -0
  32. package/skill/grow.md +13 -0
  33. package/skill/html-to-aberdeen.md +9 -0
  34. package/skill/insertCss.md +79 -0
  35. package/skill/insertGlobalCss.md +68 -0
  36. package/skill/interceptLinks.md +19 -0
  37. package/skill/invertString.md +32 -0
  38. package/skill/isEmpty.md +38 -0
  39. package/skill/map.md +16 -0
  40. package/skill/matchCurrent.md +27 -0
  41. package/skill/merge.md +27 -0
  42. package/skill/mount.md +48 -0
  43. package/skill/multiMap.md +15 -0
  44. package/skill/onEach.md +14 -0
  45. package/skill/partition.md +15 -0
  46. package/skill/peek.md +36 -0
  47. package/skill/persistScroll.md +13 -0
  48. package/skill/proxy.md +11 -0
  49. package/skill/push.md +13 -0
  50. package/skill/ref.md +39 -0
  51. package/skill/runQueue.md +34 -0
  52. package/skill/setErrorHandler.md +48 -0
  53. package/skill/setLog.md +9 -0
  54. package/skill/setSpacingCssVars.md +31 -0
  55. package/skill/shrink.md +13 -0
  56. package/skill/unmountAll.md +9 -0
  57. package/skill/unproxy.md +43 -0
  58. package/skill/up.md +13 -0
  59. package/src/aberdeen.ts +194 -73
  60. package/src/devtools.ts +685 -0
  61. package/dist-docs/.nojekyll +0 -1
  62. package/dist-docs/Tutorial/index.html +0 -305
  63. package/dist-docs/aberdeen/A/index.html +0 -116
  64. package/dist-docs/aberdeen/CUSTOM_DUMP/index.html +0 -7
  65. package/dist-docs/aberdeen/NO_COPY/index.html +0 -2
  66. package/dist-docs/aberdeen/OPAQUE/index.html +0 -12
  67. package/dist-docs/aberdeen/PromiseProxy/index.html +0 -8
  68. package/dist-docs/aberdeen/clean/index.html +0 -11
  69. package/dist-docs/aberdeen/clone/index.html +0 -5
  70. package/dist-docs/aberdeen/copy/index.html +0 -22
  71. package/dist-docs/aberdeen/count/index.html +0 -7
  72. package/dist-docs/aberdeen/cssVars/index.html +0 -11
  73. package/dist-docs/aberdeen/darkMode/index.html +0 -9
  74. package/dist-docs/aberdeen/default/index.html +0 -342
  75. package/dist-docs/aberdeen/derive/index.html +0 -18
  76. package/dist-docs/aberdeen/disableCreateDestroy/index.html +0 -6
  77. package/dist-docs/aberdeen/dump/index.html +0 -11
  78. package/dist-docs/aberdeen/index.html +0 -54
  79. package/dist-docs/aberdeen/insertCss/index.html +0 -30
  80. package/dist-docs/aberdeen/insertGlobalCss/index.html +0 -19
  81. package/dist-docs/aberdeen/invertString/index.html +0 -13
  82. package/dist-docs/aberdeen/isEmpty/index.html +0 -12
  83. package/dist-docs/aberdeen/map/index.html +0 -26
  84. package/dist-docs/aberdeen/merge/index.html +0 -11
  85. package/dist-docs/aberdeen/mount/index.html +0 -17
  86. package/dist-docs/aberdeen/multiMap/index.html +0 -28
  87. package/dist-docs/aberdeen/onEach/index.html +0 -19
  88. package/dist-docs/aberdeen/partition/index.html +0 -36
  89. package/dist-docs/aberdeen/peek/index.html +0 -40
  90. package/dist-docs/aberdeen/proxy/index.html +0 -35
  91. package/dist-docs/aberdeen/ref/index.html +0 -13
  92. package/dist-docs/aberdeen/runQueue/index.html +0 -13
  93. package/dist-docs/aberdeen/setErrorHandler/index.html +0 -16
  94. package/dist-docs/aberdeen/setSpacingCssVars/index.html +0 -8
  95. package/dist-docs/aberdeen/unmountAll/index.html +0 -5
  96. package/dist-docs/aberdeen/unproxy/index.html +0 -11
  97. package/dist-docs/assets/aberdeen/aberdeen.d.ts +0 -963
  98. package/dist-docs/assets/aberdeen/aberdeen.js.map +0 -11
  99. package/dist-docs/assets/aberdeen/dispatcher.d.ts +0 -57
  100. package/dist-docs/assets/aberdeen/dispatcher.js +0 -66
  101. package/dist-docs/assets/aberdeen/dispatcher.js.map +0 -10
  102. package/dist-docs/assets/aberdeen/helpers/reverseSortedSet.d.ts +0 -96
  103. package/dist-docs/assets/aberdeen/prediction.d.ts +0 -33
  104. package/dist-docs/assets/aberdeen/prediction.js +0 -112
  105. package/dist-docs/assets/aberdeen/prediction.js.map +0 -10
  106. package/dist-docs/assets/aberdeen/route.d.ts +0 -137
  107. package/dist-docs/assets/aberdeen/route.js +0 -287
  108. package/dist-docs/assets/aberdeen/route.js.map +0 -10
  109. package/dist-docs/assets/aberdeen/transitions.d.ts +0 -18
  110. package/dist-docs/assets/aberdeen/transitions.js +0 -47
  111. package/dist-docs/assets/aberdeen/transitions.js.map +0 -10
  112. package/dist-docs/assets/custom.js +0 -573
  113. package/dist-docs/assets/favicon.png +0 -0
  114. package/dist-docs/assets/hierarchy.js +0 -1
  115. package/dist-docs/assets/highlight.css +0 -134
  116. package/dist-docs/assets/icons.js +0 -18
  117. package/dist-docs/assets/icons.svg +0 -1
  118. package/dist-docs/assets/main.js +0 -60
  119. package/dist-docs/assets/navigation.js +0 -1
  120. package/dist-docs/assets/search.js +0 -1
  121. package/dist-docs/assets/style.css +0 -1640
  122. package/dist-docs/dispatcher/Dispatcher/index.html +0 -21
  123. package/dist-docs/dispatcher/MATCH_FAILED/index.html +0 -2
  124. package/dist-docs/dispatcher/MATCH_REST/index.html +0 -2
  125. package/dist-docs/dispatcher/index.html +0 -4
  126. package/dist-docs/hierarchy.html +0 -1
  127. package/dist-docs/index.html +0 -61
  128. package/dist-docs/media/CHANGELOG.md +0 -266
  129. package/dist-docs/modules.html +0 -1
  130. package/dist-docs/prediction/applyCanon/index.html +0 -13
  131. package/dist-docs/prediction/applyPrediction/index.html +0 -8
  132. package/dist-docs/prediction/index.html +0 -9
  133. package/dist-docs/route/Route/index.html +0 -23
  134. package/dist-docs/route/back/index.html +0 -4
  135. package/dist-docs/route/current/index.html +0 -2
  136. package/dist-docs/route/go/index.html +0 -14
  137. package/dist-docs/route/index.html +0 -15
  138. package/dist-docs/route/interceptLinks/index.html +0 -8
  139. package/dist-docs/route/matchCurrent/index.html +0 -10
  140. package/dist-docs/route/persistScroll/index.html +0 -6
  141. package/dist-docs/route/push/index.html +0 -6
  142. package/dist-docs/route/setLog/index.html +0 -3
  143. package/dist-docs/route/up/index.html +0 -5
  144. package/dist-docs/sitemap.xml +0 -239
  145. package/dist-docs/transitions/grow/index.html +0 -6
  146. package/dist-docs/transitions/index.html +0 -5
  147. package/dist-docs/transitions/shrink/index.html +0 -6
  148. package/skill/aberdeen.md +0 -4586
  149. package/skill/dispatcher.md +0 -129
  150. package/skill/prediction.md +0 -73
  151. package/skill/route.md +0 -322
  152. package/skill/transitions.md +0 -59
package/src/aberdeen.ts CHANGED
@@ -16,6 +16,7 @@ interface QueueRunner {
16
16
 
17
17
  let sortedQueue: ReverseSortedSet<QueueRunner, "prio"> | undefined; // When set, a runQueue is scheduled or currently running.
18
18
  let runQueueDepth = 0; // Incremented when a queue event causes another queue event to be added. Reset when queue is empty. Throw when >= 42 to break (infinite) recursion.
19
+ let freezeCount = 0; // While > 0 (see `freeze()`), `runQueue` is paused and updates accumulate.
19
20
  let topRedrawScope: Scope | undefined; // The scope that triggered the current redraw. Elements drawn at this scope level may trigger 'create' animations.
20
21
  // During a teardown (a scope being cleaned), this holds the element that *survives* that teardown:
21
22
  // the element whose child content is being removed, but which itself stays in the DOM. Value-restoring
@@ -108,6 +109,7 @@ function queue(runner: QueueRunner) {
108
109
  * ```
109
110
  */
110
111
  export function runQueue(): void {
112
+ if (freezeCount) return; // Paused by `freeze()`; updates stay queued until thawed.
111
113
  let time = Date.now();
112
114
  while (sortedQueue) {
113
115
  const runner = sortedQueue.fetchLast();
@@ -121,6 +123,33 @@ export function runQueue(): void {
121
123
  if (time > 9) console.debug(`Aberdeen queue took ${time}ms`);
122
124
  }
123
125
 
126
+ /**
127
+ * Pause processing of reactive updates until the returned *thaw* function is called.
128
+ *
129
+ * While frozen, changes to observed data still accumulate, but no re-renders run. Freezes
130
+ * stack: if there are multiple outstanding freezes, redraws resume only once the last one is
131
+ * thawed. This is useful to batch an async burst of changes into a single update pass, or to
132
+ * hold the UI steady (e.g. the dev tools use it for "freeze redraws").
133
+ *
134
+ * @returns A function that releases this freeze. Calling it more than once has no effect.
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const thaw = A.freeze();
139
+ * // ...make many changes without intermediate redraws...
140
+ * thaw(); // redraws run now (if no other freezes remain)
141
+ * ```
142
+ */
143
+ export function freeze(): () => void {
144
+ freezeCount++;
145
+ let released = false;
146
+ return () => {
147
+ if (released) return;
148
+ released = true;
149
+ if (!--freezeCount) runQueue();
150
+ };
151
+ }
152
+
124
153
  /**
125
154
  * A sort key, as used by {@link onEach}, is a value that determines the order of items. It can
126
155
  * be a number, string, or an array of numbers/strings. The sort key is used to sort items
@@ -315,6 +344,7 @@ abstract class ContentScope extends Scope {
315
344
  if (!this.changes) {
316
345
  this.changes = new Map();
317
346
  queue(this);
347
+ dev?.schedule(this, index, oldData, newData, new Error());
318
348
  }
319
349
 
320
350
  let targetDelta = this.changes.get(target);
@@ -388,6 +418,9 @@ class ChainedScope extends ContentScope {
388
418
  // We're always adding ourselve as a cleaner, in order to run our own cleaners
389
419
  // and to remove ourselve from the queue (if we happen to be in there).
390
420
  if (!useParentCleaners) currentScope.cleaners.push(this);
421
+
422
+ // Covers RegularScope, ResultScope and SetArgScope; the enclosing render scope is the parent.
423
+ dev?.create(this, currentScope, new Error());
391
424
  }
392
425
 
393
426
  getPrecedingNode(): Node | undefined {
@@ -425,6 +458,7 @@ class RegularScope extends ChainedScope {
425
458
  redraw() {
426
459
  const savedScope = currentScope;
427
460
  currentScope = this;
461
+ dev?.render(this);
428
462
  try {
429
463
  this.renderer();
430
464
  } catch (e) {
@@ -453,7 +487,10 @@ class MountScope extends ContentScope {
453
487
  ) {
454
488
  super();
455
489
  this.svg = el.namespaceURI === 'http://www.w3.org/2000/svg';
456
-
490
+
491
+ // Register before the first redraw so its render is attributed to this scope.
492
+ dev?.create(this, currentScope, new Error());
493
+
457
494
  const oldTopRedrawScope = topRedrawScope;
458
495
  topRedrawScope = this;
459
496
  this.redraw();
@@ -604,6 +641,9 @@ class OnEachScope extends Scope {
604
641
 
605
642
  currentScope.cleaners.push(this);
606
643
 
644
+ // Register before creating item scopes, so they can attach under this scope.
645
+ dev?.create(this, currentScope, new Error());
646
+
607
647
  // Do _addChild() calls for initial items
608
648
  if (target instanceof Array) {
609
649
  for (let i = 0; i < target.length; i++) {
@@ -642,7 +682,12 @@ class OnEachScope extends Scope {
642
682
  this.changedIndexes = new Map();
643
683
  for (const index of indexes.keys()) {
644
684
  const oldScope = this.byIndex.get(index);
645
- if (oldScope) oldScope.remove();
685
+ if (oldScope) {
686
+ oldScope.remove();
687
+ // The old item scope is discarded here (recreated below if it survives), so drop it
688
+ // from the dev tree; a fresh OnEachItemScope re-attaches itself.
689
+ dev?.delete(oldScope);
690
+ }
646
691
 
647
692
  if (this.target instanceof Set || this.target instanceof Map ? this.target.has(index) : index in this.target) {
648
693
  // Item still exists
@@ -707,6 +752,9 @@ class OnEachItemScope extends ContentScope {
707
752
 
708
753
  // Don't register to be cleaned by parent scope, as the OnEachScope will manage this for us (for efficiency)
709
754
 
755
+ // An onEach item is its own scope; its parent in the tree is the OnEachScope.
756
+ dev?.create(this, this.parent, new Error());
757
+
710
758
  if (topRedraw) topRedrawScope = this;
711
759
  this.redraw();
712
760
  }
@@ -792,6 +840,7 @@ class OnEachItemScope extends ContentScope {
792
840
  // Since makeSortKey may get() the Store, we'll need to set currentScope first.
793
841
  const savedScope = currentScope;
794
842
  currentScope = this;
843
+ dev?.render(this);
795
844
 
796
845
  let sortKey: any;
797
846
  try {
@@ -850,6 +899,7 @@ class OnEachItemScope extends ContentScope {
850
899
  function addNode(el: Element, node: Node) {
851
900
  if (el !== currentScope.el) {
852
901
  el.appendChild(node);
902
+ dev?.node(currentScope, node, false);
853
903
  return;
854
904
  }
855
905
  const parentEl = currentScope.el;
@@ -859,6 +909,7 @@ function addNode(el: Element, node: Node) {
859
909
  prevNode ? prevNode.nextSibling : parentEl.firstChild,
860
910
  );
861
911
  currentScope.lastChild = node;
912
+ dev?.node(currentScope, node, true);
862
913
  }
863
914
 
864
915
  /**
@@ -868,6 +919,62 @@ function addNode(el: Element, node: Node) {
868
919
  const ROOT_SCOPE = new RootScope();
869
920
  let currentScope: ContentScope = ROOT_SCOPE;
870
921
 
922
+ // === Developer tools instrumentation =======================================
923
+ // The core emits events through `dev(type, ...args)` only when the dev tools are connected.
924
+ // The tools — the scope tree, node→scope / element→scope maps, stack traces, the UI, and the
925
+ // `?abdev=1` / Ctrl-Alt-A bootstrap — all live in `./devtools`, which *wraps* this module:
926
+ // in a dev build the `aberdeen` entry point is that wrapper, and it installs the sink via
927
+ // `A._setDev` synchronously at load (before the app's first `mount()`, so no buffering).
928
+ //
929
+ // There is deliberately no build-time flag: the hooks are always present, costing only a
930
+ // nullish check (`dev?.x(…)` doesn't even evaluate its arguments — no `new Error()`) when no
931
+ // tools are attached. Keeping them unconditional avoids the footgun of a stripped core being
932
+ // paired with the wrapper, or vice-versa.
933
+
934
+ /** @internal The instrumentation hooks the dev tools install via {@link _setDev}. Scopes are
935
+ * passed opaquely (`any`); the tools read `.constructor.name`, `.el`, etc. off them. */
936
+ export interface DevHooks {
937
+ /** A scope was created under `parent` (its onEach scope, for items); `err` holds the creation
938
+ * stack, or is `undefined` for scopes replayed on connect (they predate the tools). */
939
+ create(scope: any, parent: any, err: Error | undefined): void;
940
+ /** A scope is about to (re)render, replacing the DOM nodes and child scopes it last produced. */
941
+ render(scope: any): void;
942
+ /** A proxy change (`target[index]`, `oldData`→`newData`) scheduled `scope` to re-render; `err` holds the triggering stack. */
943
+ schedule(scope: any, index: any, oldData: any, newData: any, err: Error): void;
944
+ /** `scope` inserted DOM `node`; `top` marks a direct sibling at the scope's own level (vs. a descendant). */
945
+ node(scope: any, node: Node, top: boolean): void;
946
+ /** `scope` subscribed to `target[index]`. */
947
+ read(scope: any, target: any, index: any): void;
948
+ /** `scope` was permanently removed (an onEach item that no longer exists). */
949
+ delete(scope: any): void;
950
+ }
951
+
952
+ /** @internal Event sink, installed by the dev-tools wrapper via {@link _setDev}. */
953
+ let dev: DevHooks | undefined;
954
+
955
+ /** @internal Install (or, with `undefined`, remove) the dev-tools hooks. Used by the
956
+ * `./devtools` wrapper. A named `@internal` export (rather than an `A` member) so it is
957
+ * stripped from the public type declarations.
958
+ *
959
+ * On connect the *already existing* scope tree is replayed as `create` events (with no
960
+ * stack — those scopes predate the tools), so the tool shows the current tree immediately,
961
+ * not only what changes afterwards. Their child scopes live in `cleaners` (and onEach items
962
+ * in `byIndex`); nodes/reads/stacks for them fill in naturally as they re-render. */
963
+ export function _setDev(hooks: DevHooks | undefined) {
964
+ dev = hooks;
965
+ if (!hooks) return;
966
+ const childScopes = (scope: any): Scope[] =>
967
+ scope instanceof OnEachScope
968
+ ? [...scope.byIndex.values()]
969
+ : (scope.cleaners as any[]).filter((c): c is Scope => c instanceof Scope);
970
+ const replay = (scope: Scope, parent: Scope) => {
971
+ hooks.create(scope, parent, undefined);
972
+ for (const child of childScopes(scope)) replay(child, scope);
973
+ };
974
+ for (const child of childScopes(ROOT_SCOPE)) replay(child, ROOT_SCOPE);
975
+ }
976
+ // === End developer tools instrumentation ===================================
977
+
871
978
  /**
872
979
  * Execute a function in a never-cleaned root scope. Even {@link unmountAll} will not
873
980
  * clean up observers/nodes created by the function.
@@ -925,6 +1032,8 @@ function subscribe(
925
1032
  ) {
926
1033
  if (observer === ROOT_SCOPE || peeking) return;
927
1034
 
1035
+ if (observer === currentScope) dev?.read(currentScope, target, index);
1036
+
928
1037
  let byTarget = subscribers.get(target);
929
1038
  if (!byTarget) subscribers.set(target, (byTarget = new Map()));
930
1039
 
@@ -2565,6 +2674,10 @@ let cssCount = 0;
2565
2674
  * Both forms can be mixed: `'m:$3 box-shadow: 0 2px 4px rgba(0,0,0,0.2); bg:$cardBg'`
2566
2675
  *
2567
2676
  * Supports the same CSS shortcuts as {@link A | A} and CSS variable references with `$` (e.g., `$primary`, `$3`).
2677
+ *
2678
+ * CSS is inserted into the <head> in an order relative to other insert(Global)Css items that is consistent
2679
+ * based on when the containing Aberdeen scope was first defined. This allows changing styles without changing
2680
+ * order-based precedence.
2568
2681
  *
2569
2682
  * @param style - A concise style string or a style object.
2570
2683
  * @returns The unique class name prefix used for scoping (e.g., `.AbdStl1`).
@@ -2617,13 +2730,87 @@ export function insertCss(style: string | object): string {
2617
2730
  const prefix = `.AbdStl${++cssCount}`;
2618
2731
  const css = typeof style === 'string' ? styleStringToCss(style, prefix) : objectToCss(style, prefix);
2619
2732
  if (css) {
2620
- let cnt = cssSnippetCount++;
2621
- cssSnippets[cnt] = css;
2622
- clean(() => delete cssSnippets[cnt]);
2733
+ const cnt = cssSnippetCount++;
2734
+ const key = arrayToStr([currentScope.prio, cnt]);
2735
+ cssSnippets[key] = css;
2736
+ clean(() => delete cssSnippets[key]);
2623
2737
  }
2624
2738
  return prefix;
2625
2739
  }
2626
- let cssSnippets = proxy({} as Record<number, string>);
2740
+
2741
+ /**
2742
+ * Inserts CSS rules globally (unscoped).
2743
+ *
2744
+ * Works exactly like {@link insertCss}, but without prefixing selectors with a unique class name.
2745
+ * This is useful for global resets, base styles, or styles that need to apply to the entire document.
2746
+ *
2747
+ * Accepts the same concise style string syntax and CSS shortcuts as {@link insertCss}.
2748
+ * See {@link insertCss} for detailed documentation on syntax and shortcuts.
2749
+ *
2750
+ * @param style - Object with selectors as keys and concise CSS strings as values.
2751
+ *
2752
+ * CSS is inserted into the <head> in an order relative to other insert(Global)Css items that is consistent
2753
+ * based on when the containing Aberdeen scope was first defined. This allows changing styles without changing
2754
+ * order-based precedence.
2755
+ *
2756
+ * @example Global Reset and Base Styles
2757
+ * ```typescript
2758
+ * // Set up global styles using CSS shortcuts
2759
+ * A.insertGlobalCss({
2760
+ * "*": "m:0 p:0 box-sizing:border-box",
2761
+ * "body": "font-family: system-ui, sans-serif; m:0 p:$3 bg:#434 fg:#d0dafa",
2762
+ * "a": "text-decoration:none fg:#57f",
2763
+ * "a:hover": "text-decoration:underline",
2764
+ * "code": "font-family:monospace bg:#222 fg:#afc p:4px r:3px"
2765
+ * });
2766
+ *
2767
+ * A('h2#Title without margins');
2768
+ * A('a#This is a link');
2769
+ * A('code#const x = 42;');
2770
+ * ```
2771
+ *
2772
+ * @example Responsive Global Styles
2773
+ * ```typescript
2774
+ * A.insertGlobalCss({
2775
+ * "html": "font-size:16px",
2776
+ * "body": "line-height:1.6",
2777
+ * "h1, h2, h3": "font-weight:600 mt:$4 mb:$2",
2778
+ * "@media (max-width: 768px)": {
2779
+ * "html": "font-size:14px",
2780
+ * "body": "p:$2"
2781
+ * },
2782
+ * "@media (prefers-color-scheme: dark)": {
2783
+ * "body": "bg:#1a1a1a fg:#e5e5e5",
2784
+ * "code": "bg:#2a2a2a"
2785
+ * }
2786
+ * });
2787
+ * ```
2788
+ * At-rules such as `@media` and `@keyframes` should use nested objects. For keyframes,
2789
+ * the step selectors (`0%`, `50%`, `100%`, etc.) become the nested keys and each value
2790
+ * should be a concise CSS declaration string.
2791
+ *
2792
+ * @example Animation Keyframes
2793
+ * ```typescript
2794
+ * A.insertGlobalCss({
2795
+ * "@keyframes connection-pulse": {
2796
+ * "0%": "box-shadow: inset 0 0 0 0 rgba(255, 70, 70, 0.4);",
2797
+ * "50%": "box-shadow: inset 0 0 0 6px rgba(255, 70, 70, 0.08);",
2798
+ * "100%": "box-shadow: inset 0 0 0 0 rgba(255, 70, 70, 0.4);"
2799
+ * }
2800
+ * });
2801
+ * ```
2802
+ */
2803
+ export function insertGlobalCss(style: object) {
2804
+ const css = objectToCss(style, "");
2805
+ if (css) {
2806
+ const cnt = cssSnippetCount++;
2807
+ const key = arrayToStr([currentScope.prio, cnt]);
2808
+ cssSnippets[key] = css;
2809
+ clean(() => delete cssSnippets[key]);
2810
+ }
2811
+ }
2812
+
2813
+ let cssSnippets = proxy({} as Record<string, string>);
2627
2814
  let cssSnippetCount = 0;
2628
2815
 
2629
2816
  function combinePrefixSelector(prefix: string, key: string): string {
@@ -2707,73 +2894,6 @@ function styleStringToCss(styleStr: string, selector: string): string {
2707
2894
  return props ? `${selector}{${props}}\n` : "";
2708
2895
  }
2709
2896
 
2710
- /**
2711
- * Inserts CSS rules globally (unscoped).
2712
- *
2713
- * Works exactly like {@link insertCss}, but without prefixing selectors with a unique class name.
2714
- * This is useful for global resets, base styles, or styles that need to apply to the entire document.
2715
- *
2716
- * Accepts the same concise style string syntax and CSS shortcuts as {@link insertCss}.
2717
- * See {@link insertCss} for detailed documentation on syntax and shortcuts.
2718
- *
2719
- * @param style - Object with selectors as keys and concise CSS strings as values.
2720
- *
2721
- * @example Global Reset and Base Styles
2722
- * ```typescript
2723
- * // Set up global styles using CSS shortcuts
2724
- * A.insertGlobalCss({
2725
- * "*": "m:0 p:0 box-sizing:border-box",
2726
- * "body": "font-family: system-ui, sans-serif; m:0 p:$3 bg:#434 fg:#d0dafa",
2727
- * "a": "text-decoration:none fg:#57f",
2728
- * "a:hover": "text-decoration:underline",
2729
- * "code": "font-family:monospace bg:#222 fg:#afc p:4px r:3px"
2730
- * });
2731
- *
2732
- * A('h2#Title without margins');
2733
- * A('a#This is a link');
2734
- * A('code#const x = 42;');
2735
- * ```
2736
- *
2737
- * @example Responsive Global Styles
2738
- * ```typescript
2739
- * A.insertGlobalCss({
2740
- * "html": "font-size:16px",
2741
- * "body": "line-height:1.6",
2742
- * "h1, h2, h3": "font-weight:600 mt:$4 mb:$2",
2743
- * "@media (max-width: 768px)": {
2744
- * "html": "font-size:14px",
2745
- * "body": "p:$2"
2746
- * },
2747
- * "@media (prefers-color-scheme: dark)": {
2748
- * "body": "bg:#1a1a1a fg:#e5e5e5",
2749
- * "code": "bg:#2a2a2a"
2750
- * }
2751
- * });
2752
- * ```
2753
- * At-rules such as `@media` and `@keyframes` should use nested objects. For keyframes,
2754
- * the step selectors (`0%`, `50%`, `100%`, etc.) become the nested keys and each value
2755
- * should be a concise CSS declaration string.
2756
- *
2757
- * @example Animation Keyframes
2758
- * ```typescript
2759
- * A.insertGlobalCss({
2760
- * "@keyframes connection-pulse": {
2761
- * "0%": "box-shadow: inset 0 0 0 0 rgba(255, 70, 70, 0.4);",
2762
- * "50%": "box-shadow: inset 0 0 0 6px rgba(255, 70, 70, 0.08);",
2763
- * "100%": "box-shadow: inset 0 0 0 0 rgba(255, 70, 70, 0.4);"
2764
- * }
2765
- * });
2766
- * ```
2767
- */
2768
- export function insertGlobalCss(style: object) {
2769
- const css = objectToCss(style, "");
2770
- if (css) {
2771
- let cnt = cssSnippetCount++;
2772
- cssSnippets[cnt] = css;
2773
- clean(() => delete cssSnippets[cnt]);
2774
- }
2775
- }
2776
-
2777
2897
  const CSS_SHORT: Record<string, string | string[]> = {
2778
2898
  m: "margin",
2779
2899
  mt: "margin-top",
@@ -3597,6 +3717,7 @@ export default Object.assign(A, {
3597
3717
  /** {@inheritDoc derive} */ derive,
3598
3718
  /** {@inheritDoc disableCreateDestroy} */ disableCreateDestroy,
3599
3719
  /** {@inheritDoc dump} */ dump,
3720
+ /** {@inheritDoc freeze} */ freeze,
3600
3721
  /** {@inheritDoc insertCss} */ insertCss,
3601
3722
  /** {@inheritDoc insertGlobalCss} */ insertGlobalCss,
3602
3723
  /** {@inheritDoc invertString} */ invertString,