streetui 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -138,6 +138,7 @@ __export(src_exports, {
138
138
  reactiveListItemSignature: () => reactiveListItemSignature,
139
139
  readState: () => readState,
140
140
  reconcileChildren: () => reconcileChildren,
141
+ reconcileChildrenByPlan: () => reconcileChildrenByPlan,
141
142
  renderToString: () => renderToString,
142
143
  reportDiagnostic: () => reportDiagnostic,
143
144
  required: () => required,
@@ -168,7 +169,7 @@ __export(src_exports, {
168
169
  module.exports = __toCommonJS(src_exports);
169
170
 
170
171
  // src/version.ts
171
- var VERSION = "1.0.0";
172
+ var VERSION = "1.3.0";
172
173
 
173
174
  // ../state/src/signal.ts
174
175
  var _activeConsumer = null;
@@ -970,6 +971,7 @@ var ApplicationGraph = class {
970
971
  this.handlers.delete(`__signal__${ref.signalId}`);
971
972
  }
972
973
  this.handlers.delete(`__listbuild__${node.id}`);
974
+ this.handlers.delete(`__listplan__${node.id}`);
973
975
  }
974
976
  // ── Handler registry ──────────────────────────────────────────────────────
975
977
  registerHandler(key, fn) {
@@ -1265,16 +1267,38 @@ var ContainerBuilderBase = class extends ContentBuilderBase {
1265
1267
  const itemKey = reactiveListItemKey(item, index);
1266
1268
  const itemNode = graph.createNode("list-item", {
1267
1269
  key: itemKey,
1268
- props: { key: itemKey, _sig: reactiveListItemSignature(item) }
1270
+ // `_item` records the source item *reference* so the reconciler can
1271
+ // short-circuit unchanged rows by identity (no signature hashing); `_sig`
1272
+ // is the content signature used to detect an in-place data change when the
1273
+ // reference differs. Both are internal metadata (leading `_`) and never
1274
+ // reach the DOM.
1275
+ props: {
1276
+ key: itemKey,
1277
+ _sig: reactiveListItemSignature(item),
1278
+ // The item reference is stored as opaque internal metadata (never
1279
+ // rendered); cast through `unknown` since `T` is not a `PropValue`.
1280
+ _item: item
1281
+ }
1269
1282
  });
1270
1283
  renderItem(item, index, new ContainerBuilderImpl(itemNode, graph));
1271
1284
  return itemNode;
1272
1285
  };
1273
- const buildAll = (raw) => {
1286
+ const buildPlan = (raw) => {
1274
1287
  const arr = Array.isArray(raw) ? raw : [];
1275
- return arr.map((item, i) => buildItem(item, i));
1288
+ const plan = new Array(arr.length);
1289
+ for (let i = 0; i < arr.length; i++) {
1290
+ const item = arr[i];
1291
+ const index = i;
1292
+ plan[i] = {
1293
+ key: reactiveListItemKey(item, index),
1294
+ item,
1295
+ sig: () => reactiveListItemSignature(item),
1296
+ build: () => buildItem(item, index)
1297
+ };
1298
+ }
1299
+ return plan;
1276
1300
  };
1277
- graph.registerHandler(`__listbuild__${node.id}`, buildAll);
1301
+ graph.registerHandler(`__listplan__${node.id}`, buildPlan);
1278
1302
  const current = isSignal(items) ? items.peek() : items;
1279
1303
  const initial = Array.isArray(current) ? current : [];
1280
1304
  initial.forEach((item, i) => {
@@ -2419,6 +2443,7 @@ function patchProp(dom, element, name, oldValue, newValue) {
2419
2443
 
2420
2444
  // ../renderer/src/events.ts
2421
2445
  function wireEvents(dom, graph, node, element, instance) {
2446
+ if (node.events.length === 0) return;
2422
2447
  for (const eventDesc of node.events) {
2423
2448
  const handler = graph.getHandler(eventDesc.handlerKey);
2424
2449
  if (handler === void 0) continue;
@@ -2548,6 +2573,109 @@ function reconcileChildren(ctx, parentDom, oldInstances, newNodes, mountFn) {
2548
2573
  reorderDom(ctx, parentDom, newInstances);
2549
2574
  return { instances: newInstances, removed };
2550
2575
  }
2576
+ function reconcileChildrenByPlan(ctx, parentDom, oldInstances, plan, mountFn) {
2577
+ const oldByKey = /* @__PURE__ */ new Map();
2578
+ for (const inst of oldInstances) {
2579
+ oldByKey.set(inst.graphNode.key ?? inst.graphNode.id, inst);
2580
+ }
2581
+ const newInstances = [];
2582
+ const usedKeys = /* @__PURE__ */ new Set();
2583
+ const built = [];
2584
+ for (const entry of plan) {
2585
+ const existing = oldByKey.get(entry.key);
2586
+ if (existing !== void 0) {
2587
+ usedKeys.add(entry.key);
2588
+ const oldItem = existing.graphNode.getProp("_item");
2589
+ if (!Object.is(oldItem, entry.item)) {
2590
+ const newSig = entry.sig();
2591
+ const oldSig = existing.graphNode.getProp("_sig");
2592
+ if (!Object.is(oldSig, newSig)) {
2593
+ const freshNode = entry.build();
2594
+ built.push(freshNode);
2595
+ patchExistingInstance(ctx, existing, freshNode);
2596
+ reconcileItemChildren(ctx, existing, freshNode, mountFn);
2597
+ existing.graphNode.setProp("_sig", newSig);
2598
+ }
2599
+ existing.graphNode.setProp("_item", entry.item);
2600
+ }
2601
+ newInstances.push(existing);
2602
+ } else {
2603
+ const freshNode = entry.build();
2604
+ built.push(freshNode);
2605
+ const inst = mountFn(freshNode, parentDom);
2606
+ newInstances.push(inst);
2607
+ }
2608
+ }
2609
+ const removed = [];
2610
+ for (const inst of oldInstances) {
2611
+ const key = inst.graphNode.key ?? inst.graphNode.id;
2612
+ if (!usedKeys.has(key)) removed.push(inst);
2613
+ }
2614
+ for (const inst of removed) {
2615
+ const parent = ctx.dom.parentNode(inst.domNode);
2616
+ if (parent !== null) ctx.dom.removeChild(parent, inst.domNode);
2617
+ inst.dispose();
2618
+ }
2619
+ reorderDomMinimal(ctx, parentDom, oldInstances, newInstances);
2620
+ return { instances: newInstances, removed, built };
2621
+ }
2622
+ function reorderDomMinimal(ctx, parentDom, oldInstances, newInstances) {
2623
+ const n = newInstances.length;
2624
+ if (n === 0) return;
2625
+ const oldIndexOf = /* @__PURE__ */ new Map();
2626
+ for (let i = 0; i < oldInstances.length; i++) oldIndexOf.set(oldInstances[i], i);
2627
+ const source = new Array(n);
2628
+ let moved = false;
2629
+ let lastSeen = -1;
2630
+ for (let i = 0; i < n; i++) {
2631
+ const oi = oldIndexOf.get(newInstances[i]);
2632
+ if (oi === void 0) {
2633
+ source[i] = -1;
2634
+ moved = true;
2635
+ } else {
2636
+ source[i] = oi;
2637
+ if (oi < lastSeen) moved = true;
2638
+ else lastSeen = oi;
2639
+ }
2640
+ }
2641
+ if (!moved) return;
2642
+ const keep = longestIncreasingSubsequence(source);
2643
+ let refNode = null;
2644
+ for (let i = n - 1; i >= 0; i--) {
2645
+ const domNode = newInstances[i].domNode;
2646
+ if (source[i] === -1 || !keep.has(i)) {
2647
+ if (ctx.dom.nextSibling(domNode) !== refNode) {
2648
+ ctx.dom.insertBefore(parentDom, domNode, refNode);
2649
+ }
2650
+ }
2651
+ refNode = domNode;
2652
+ }
2653
+ }
2654
+ function longestIncreasingSubsequence(source) {
2655
+ const keep = /* @__PURE__ */ new Set();
2656
+ const n = source.length;
2657
+ const tails = [];
2658
+ const prev = new Array(n).fill(-1);
2659
+ for (let i = 0; i < n; i++) {
2660
+ const v = source[i];
2661
+ if (v < 0) continue;
2662
+ let lo = 0;
2663
+ let hi = tails.length;
2664
+ while (lo < hi) {
2665
+ const mid = lo + hi >> 1;
2666
+ if (source[tails[mid]] < v) lo = mid + 1;
2667
+ else hi = mid;
2668
+ }
2669
+ if (lo > 0) prev[i] = tails[lo - 1];
2670
+ tails[lo] = i;
2671
+ }
2672
+ let idx = tails.length > 0 ? tails[tails.length - 1] : -1;
2673
+ while (idx >= 0) {
2674
+ keep.add(idx);
2675
+ idx = prev[idx];
2676
+ }
2677
+ return keep;
2678
+ }
2551
2679
  function reconcileItemChildren(ctx, itemInstance, newItemNode, mountFn) {
2552
2680
  const el = itemInstance.domNode;
2553
2681
  if (!ctx.dom.isElement(el)) return;
@@ -2649,10 +2777,12 @@ function mountNode(ctx, graphNode, parentDom) {
2649
2777
  const textNode = dom.createTextNode(text);
2650
2778
  dom.appendChild(el2, textNode);
2651
2779
  applyNodeProps(ctx, graphNode, el2);
2652
- wireEvents(dom, graph, graphNode, el2, new NodeInstance(graphNode, el2));
2653
2780
  const instance2 = new NodeInstance(graphNode, el2);
2654
2781
  ctx.instances.set(graphNode.id, instance2);
2655
- wireSignalBindings(ctx, graphNode, instance2, textUpdate(dom, el2, textNode));
2782
+ wireEvents(dom, graph, graphNode, el2, instance2);
2783
+ if (graphNode.stateRefs.length !== 0) {
2784
+ wireSignalBindings(ctx, graphNode, instance2, textUpdate(dom, el2, textNode));
2785
+ }
2656
2786
  dom.appendChild(parentDom, el2);
2657
2787
  return instance2;
2658
2788
  }
@@ -2666,7 +2796,9 @@ function mountNode(ctx, graphNode, parentDom) {
2666
2796
  const instance2 = new NodeInstance(graphNode, el2);
2667
2797
  ctx.instances.set(graphNode.id, instance2);
2668
2798
  wireEvents(dom, graph, graphNode, el2, instance2);
2669
- wireSignalBindings(ctx, graphNode, instance2, headingUpdate(dom, el2));
2799
+ if (graphNode.stateRefs.length !== 0) {
2800
+ wireSignalBindings(ctx, graphNode, instance2, headingUpdate(dom, el2));
2801
+ }
2670
2802
  dom.appendChild(parentDom, el2);
2671
2803
  return instance2;
2672
2804
  }
@@ -2682,7 +2814,9 @@ function mountNode(ctx, graphNode, parentDom) {
2682
2814
  const instance2 = new NodeInstance(graphNode, el2);
2683
2815
  ctx.instances.set(graphNode.id, instance2);
2684
2816
  wireEvents(dom, graph, graphNode, el2, instance2);
2685
- wireSignalBindings(ctx, graphNode, instance2, inputUpdate(dom, el2));
2817
+ if (graphNode.stateRefs.length !== 0) {
2818
+ wireSignalBindings(ctx, graphNode, instance2, inputUpdate(dom, el2));
2819
+ }
2686
2820
  dom.appendChild(parentDom, el2);
2687
2821
  return instance2;
2688
2822
  }
@@ -2730,7 +2864,9 @@ function mountNode(ctx, graphNode, parentDom) {
2730
2864
  const instance2 = new NodeInstance(graphNode, el2);
2731
2865
  ctx.instances.set(graphNode.id, instance2);
2732
2866
  wireEvents(dom, graph, graphNode, el2, instance2);
2733
- wireSignalBindings(ctx, graphNode, instance2, buttonUpdate(dom, el2));
2867
+ if (graphNode.stateRefs.length !== 0) {
2868
+ wireSignalBindings(ctx, graphNode, instance2, buttonUpdate(dom, el2));
2869
+ }
2734
2870
  dom.appendChild(parentDom, el2);
2735
2871
  return instance2;
2736
2872
  }
@@ -2757,11 +2893,11 @@ function mountNode(ctx, graphNode, parentDom) {
2757
2893
  dom.setAttribute(el, "data-streetui-key", String(itemKey));
2758
2894
  }
2759
2895
  }
2760
- if (graphNode.type === "form") {
2761
- wireEvents(dom, graph, graphNode, el, new NodeInstance(graphNode, el));
2762
- }
2763
2896
  const instance = new NodeInstance(graphNode, el);
2764
2897
  ctx.instances.set(graphNode.id, instance);
2898
+ if (graphNode.type === "form") {
2899
+ wireEvents(dom, graph, graphNode, el, instance);
2900
+ }
2765
2901
  for (const child of graphNode.children) {
2766
2902
  const childInstance = mountNode(ctx, child, el);
2767
2903
  instance.addChild(childInstance);
@@ -2812,12 +2948,15 @@ function buttonUpdate(dom, el) {
2812
2948
  };
2813
2949
  }
2814
2950
  function applyNodeProps(ctx, graphNode, el) {
2815
- for (const [key, value] of Object.entries(graphNode.props)) {
2951
+ const props = graphNode.props;
2952
+ for (const key in props) {
2953
+ if (!Object.hasOwn(props, key)) continue;
2816
2954
  if (SKIP_PROP_KEYS.has(key)) continue;
2817
- applyProp(ctx.dom, el, key, value);
2955
+ applyProp(ctx.dom, el, key, props[key]);
2818
2956
  }
2819
2957
  }
2820
2958
  function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
2959
+ if (graphNode.stateRefs.length === 0) return;
2821
2960
  for (const stateRef of graphNode.stateRefs) {
2822
2961
  const signalKey = `__signal__${stateRef.signalId}`;
2823
2962
  const maybeSig = ctx.graph.getHandler(signalKey);
@@ -2829,18 +2968,45 @@ function wireSignalBindings(ctx, graphNode, instance, onUpdate) {
2829
2968
  }
2830
2969
  }
2831
2970
  function wireReactiveList(ctx, graphNode, instance, el) {
2971
+ const plan = ctx.graph.getHandler(`__listplan__${graphNode.id}`);
2832
2972
  const build = ctx.graph.getHandler(`__listbuild__${graphNode.id}`);
2833
- if (build === void 0) return;
2973
+ if (plan === void 0 && build === void 0) return;
2834
2974
  for (const stateRef of graphNode.stateRefs) {
2835
2975
  if (stateRef.propKey !== "items") continue;
2836
2976
  const sig = ctx.graph.getHandler(`__signal__${stateRef.signalId}`);
2837
2977
  if (sig === void 0 || typeof sig.subscribe !== "function") continue;
2838
2978
  const unsub = sig.subscribe((value) => {
2839
- reconcileReactiveList(ctx, graphNode, instance, el, build(value));
2979
+ if (plan !== void 0) {
2980
+ reconcileReactiveListByPlan(ctx, graphNode, instance, el, plan(value));
2981
+ } else {
2982
+ reconcileReactiveList(ctx, graphNode, instance, el, build(value));
2983
+ }
2840
2984
  });
2841
2985
  instance.trackCleanup(unsub);
2842
2986
  }
2843
2987
  }
2988
+ function reconcileReactiveListByPlan(ctx, listNode, listInstance, listEl, plan) {
2989
+ const oldInstances = [...listInstance.children];
2990
+ const result = reconcileChildrenByPlan(
2991
+ ctx,
2992
+ listEl,
2993
+ oldInstances,
2994
+ plan,
2995
+ (node, parent) => mountNode(ctx, node, parent)
2996
+ );
2997
+ listInstance.children.length = 0;
2998
+ for (const inst of result.instances) listInstance.children.push(inst);
2999
+ for (const removed of result.removed) {
3000
+ forgetInstance(ctx, removed);
3001
+ ctx.graph.detachNode(removed.graphNode);
3002
+ }
3003
+ const adopted = new Set(result.instances.map((i) => i.graphNode));
3004
+ for (const node of result.built ?? []) {
3005
+ if (!adopted.has(node)) ctx.graph.detachNode(node);
3006
+ }
3007
+ for (const child of [...listNode.children]) listNode.removeChild(child);
3008
+ for (const inst of result.instances) listNode.appendChild(inst.graphNode);
3009
+ }
2844
3010
  function reconcileReactiveList(ctx, listNode, listInstance, listEl, newNodes) {
2845
3011
  const oldInstances = [...listInstance.children];
2846
3012
  const result = reconcileChildren(
@@ -2920,14 +3086,18 @@ function hydrateNode(ctx, graphNode, domNode, path) {
2920
3086
  const instance = new NodeInstance(graphNode, domNode);
2921
3087
  ctx.instances.set(graphNode.id, instance);
2922
3088
  wireEvents(dom, graph, graphNode, domNode, instance);
2923
- wireSignalBindings(ctx, graphNode, instance, textUpdate(dom, domNode, textNode));
3089
+ if (graphNode.stateRefs.length !== 0) {
3090
+ wireSignalBindings(ctx, graphNode, instance, textUpdate(dom, domNode, textNode));
3091
+ }
2924
3092
  return instance;
2925
3093
  }
2926
3094
  case "heading": {
2927
3095
  const instance = new NodeInstance(graphNode, domNode);
2928
3096
  ctx.instances.set(graphNode.id, instance);
2929
3097
  wireEvents(dom, graph, graphNode, domNode, instance);
2930
- wireSignalBindings(ctx, graphNode, instance, headingUpdate(dom, domNode));
3098
+ if (graphNode.stateRefs.length !== 0) {
3099
+ wireSignalBindings(ctx, graphNode, instance, headingUpdate(dom, domNode));
3100
+ }
2931
3101
  return instance;
2932
3102
  }
2933
3103
  case "input": {
@@ -2936,14 +3106,18 @@ function hydrateNode(ctx, graphNode, domNode, path) {
2936
3106
  const value = graphNode.getProp("value");
2937
3107
  if (value !== void 0) dom.setProperty(domNode, "value", String(value));
2938
3108
  wireEvents(dom, graph, graphNode, domNode, instance);
2939
- wireSignalBindings(ctx, graphNode, instance, inputUpdate(dom, domNode));
3109
+ if (graphNode.stateRefs.length !== 0) {
3110
+ wireSignalBindings(ctx, graphNode, instance, inputUpdate(dom, domNode));
3111
+ }
2940
3112
  return instance;
2941
3113
  }
2942
3114
  case "button": {
2943
3115
  const instance = new NodeInstance(graphNode, domNode);
2944
3116
  ctx.instances.set(graphNode.id, instance);
2945
3117
  wireEvents(dom, graph, graphNode, domNode, instance);
2946
- wireSignalBindings(ctx, graphNode, instance, buttonUpdate(dom, domNode));
3118
+ if (graphNode.stateRefs.length !== 0) {
3119
+ wireSignalBindings(ctx, graphNode, instance, buttonUpdate(dom, domNode));
3120
+ }
2947
3121
  return instance;
2948
3122
  }
2949
3123
  case "image":
@@ -2979,10 +3153,11 @@ function hydrateChildren(ctx, parentGraphNode, parentInstance, parentDom, parent
2979
3153
  const expected = parentGraphNode.children;
2980
3154
  const actual = elementChildren(ctx, parentDom);
2981
3155
  let cursor = 0;
3156
+ const diag = ctx.hydrationDiagnostics !== void 0;
2982
3157
  for (let i = 0; i < expected.length; i++) {
2983
3158
  const childNode = expected[i];
2984
3159
  const want = expectedTag(ctx, childNode);
2985
- const childPath = `${parentPath} / ${childNode.type}[${i}]`;
3160
+ const childPath = diag ? `${parentPath} / ${childNode.type}[${i}]` : parentPath;
2986
3161
  const actualEl = actual[cursor];
2987
3162
  if (actualEl !== void 0 && ctx.dom.isElement(actualEl) && ctx.dom.tagName(actualEl) === want) {
2988
3163
  const inst = hydrateNode(ctx, childNode, actualEl, childPath);
@@ -3834,6 +4009,10 @@ function inspectApplication(compiled) {
3834
4009
  const errors = diags.filter((d) => d.severity === "error").length;
3835
4010
  const perfAcc = { totalNodes: 0, maxDepth: 0, eventHandlers: 0, stateBindings: 0, largestChildCount: 0 };
3836
4011
  collectPerf(graph, signals.size, perfAcc);
4012
+ let reactiveLists = 0;
4013
+ for (const key of compiled.graph.handlers.keys()) {
4014
+ if (key.startsWith("__listplan__")) reactiveLists += 1;
4015
+ }
3837
4016
  return {
3838
4017
  identity: {
3839
4018
  name: compiled.name,
@@ -3855,7 +4034,8 @@ function inspectApplication(compiled) {
3855
4034
  eventHandlers: perfAcc.eventHandlers,
3856
4035
  stateBindings: perfAcc.stateBindings,
3857
4036
  distinctSignals: signals.size,
3858
- largestChildCount: perfAcc.largestChildCount
4037
+ largestChildCount: perfAcc.largestChildCount,
4038
+ reactiveLists
3859
4039
  }
3860
4040
  };
3861
4041
  }
@@ -4254,6 +4434,7 @@ function defineConfig(config) {
4254
4434
  reactiveListItemSignature,
4255
4435
  readState,
4256
4436
  reconcileChildren,
4437
+ reconcileChildrenByPlan,
4257
4438
  renderToString,
4258
4439
  reportDiagnostic,
4259
4440
  required,