streetui 1.7.0 → 1.8.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/testing.cjs CHANGED
@@ -23,23 +23,38 @@ __export(testing_exports, {
23
23
  VERSION: () => VERSION,
24
24
  analyzeGraph: () => analyzeGraph,
25
25
  findAllByRole: () => findAllByRole,
26
+ findAllComponents: () => findAllComponents,
26
27
  findByRole: () => findByRole,
27
28
  findByText: () => findByText,
29
+ findComponent: () => findComponent,
28
30
  flushUpdates: () => flushUpdates,
29
31
  formatInspection: () => formatInspection,
32
+ getComponentName: () => getComponentName,
33
+ hydrateComponent: () => hydrateComponent,
30
34
  inspectCompilation: () => inspectCompilation,
31
35
  render: () => render,
36
+ renderComponent: () => renderComponent,
32
37
  renderOnce: () => renderOnce,
33
38
  renderServerThenHydrate: () => renderServerThenHydrate,
39
+ trigger: () => trigger,
34
40
  waitFor: () => waitFor
35
41
  });
36
42
  module.exports = __toCommonJS(testing_exports);
37
43
 
38
44
  // ../core/src/identity.ts
39
45
  var _counter = 0;
46
+ function nextId() {
47
+ return ++_counter;
48
+ }
40
49
  function resetIdCounter() {
41
50
  _counter = 0;
42
51
  }
52
+ function createNodeId(value) {
53
+ return value;
54
+ }
55
+ function generateNodeId(prefix = "node") {
56
+ return createNodeId(`${prefix}:${nextId()}`);
57
+ }
43
58
 
44
59
  // ../core/src/lifecycle.ts
45
60
  var CleanupRegistry = class {
@@ -1420,6 +1435,7 @@ function mountNode(ctx, graphNode, parentDom) {
1420
1435
  instance.addChild(childInstance);
1421
1436
  }
1422
1437
  dom.appendChild(parentDom, el);
1438
+ if (graphNode.type === "component") wireComponentBehavior(ctx, graphNode, instance);
1423
1439
  return instance;
1424
1440
  }
1425
1441
  function textUpdate(dom, el, textNode) {
@@ -1588,6 +1604,11 @@ function wireOverlayBehavior(ctx, graphNode, instance, target) {
1588
1604
  instance.trackCleanup(teardown);
1589
1605
  if (openSig.peek() === true) onOpenChange(true);
1590
1606
  }
1607
+ function wireComponentBehavior(ctx, graphNode, instance) {
1608
+ const fn = ctx.graph.getHandler(`__component__${graphNode.id}`);
1609
+ if (fn === void 0) return;
1610
+ for (const cleanup of fn()) instance.trackCleanup(cleanup);
1611
+ }
1591
1612
 
1592
1613
  // ../renderer/src/hydration-diagnostics.ts
1593
1614
  function formatHydrationDiagnostic(d) {
@@ -1712,6 +1733,7 @@ function hydrateNode(ctx, graphNode, domNode, path) {
1712
1733
  return instance;
1713
1734
  }
1714
1735
  hydrateChildren(ctx, graphNode, instance, domNode, path);
1736
+ if (graphNode.type === "component") wireComponentBehavior(ctx, graphNode, instance);
1715
1737
  return instance;
1716
1738
  }
1717
1739
  }
@@ -1910,7 +1932,8 @@ function analyzeGraph(graph) {
1910
1932
  const isList = node.type === "reactive-list";
1911
1933
  const isConditional = node.type === "conditional";
1912
1934
  const isPortal = node.type === "portal";
1913
- const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal;
1935
+ const isComponent = node.type === "component";
1936
+ const isStatic = node.stateRefs.length === 0 && !hasEvents && !isList && !isConditional && !isPortal && !isComponent;
1914
1937
  const isStaticSubtree = isStatic && allChildrenStatic;
1915
1938
  nodes.set(node.id, {
1916
1939
  isStatic,
@@ -2335,21 +2358,1084 @@ function renderServerThenHydrate(build, options = {}) {
2335
2358
  };
2336
2359
  }
2337
2360
 
2361
+ // ../state/src/signal.ts
2362
+ var _activeConsumer = null;
2363
+ function withConsumer(consumer, fn) {
2364
+ const prev = _activeConsumer;
2365
+ _activeConsumer = consumer;
2366
+ try {
2367
+ return fn();
2368
+ } finally {
2369
+ _activeConsumer = prev;
2370
+ }
2371
+ }
2372
+ var _batchDepth = 0;
2373
+ var _pendingFlushes = /* @__PURE__ */ new Map();
2374
+ function _enqueueBatchFlush(sig, value) {
2375
+ _pendingFlushes.set(sig, { signal: sig, value });
2376
+ }
2377
+ var Signal = class {
2378
+ _value;
2379
+ _subscribers = /* @__PURE__ */ new Set();
2380
+ _consumers = /* @__PURE__ */ new Set();
2381
+ constructor(initial) {
2382
+ this._value = initial;
2383
+ }
2384
+ get() {
2385
+ if (_activeConsumer !== null) {
2386
+ this._consumers.add(_activeConsumer);
2387
+ _activeConsumer._addSource(this);
2388
+ }
2389
+ return this._value;
2390
+ }
2391
+ peek() {
2392
+ return this._value;
2393
+ }
2394
+ set(value) {
2395
+ if (Object.is(this._value, value)) return;
2396
+ this._value = value;
2397
+ if (_batchDepth > 0) {
2398
+ _enqueueBatchFlush(this, value);
2399
+ } else {
2400
+ this._flush(value);
2401
+ }
2402
+ }
2403
+ update(fn) {
2404
+ this.set(fn(this._value));
2405
+ }
2406
+ subscribe(fn) {
2407
+ this._subscribers.add(fn);
2408
+ return () => {
2409
+ this._subscribers.delete(fn);
2410
+ };
2411
+ }
2412
+ _removeConsumer(consumer) {
2413
+ this._consumers.delete(consumer);
2414
+ }
2415
+ /**
2416
+ * Called by the batch machinery after the batch has completed.
2417
+ * Notifies subscribers with the final coalesced value.
2418
+ */
2419
+ _flushBatch(value) {
2420
+ this._flush(value);
2421
+ }
2422
+ _flush(value) {
2423
+ for (const sub of [...this._subscribers]) sub(value);
2424
+ for (const consumer of [...this._consumers]) consumer._invalidate();
2425
+ }
2426
+ /**
2427
+ * @internal DevTools inspection only. The number of live observers
2428
+ * (direct subscribers plus derived/effect consumers). Read-only; never
2429
+ * mutates reactive state.
2430
+ */
2431
+ _observerCount() {
2432
+ return this._subscribers.size + this._consumers.size;
2433
+ }
2434
+ };
2435
+ var DerivedSignal = class {
2436
+ _value = void 0;
2437
+ _dirty = true;
2438
+ _disposed = false;
2439
+ _fn;
2440
+ _subscribers = /* @__PURE__ */ new Set();
2441
+ /** All upstream sources this derived currently reads from. */
2442
+ _sources = /* @__PURE__ */ new Set();
2443
+ /** Downstream consumers that depend on this derived. */
2444
+ _consumers = /* @__PURE__ */ new Set();
2445
+ constructor(fn) {
2446
+ this._fn = fn;
2447
+ }
2448
+ get() {
2449
+ if (_activeConsumer !== null) {
2450
+ this._consumers.add(_activeConsumer);
2451
+ _activeConsumer._addSource(this);
2452
+ }
2453
+ if (this._dirty) this._recompute();
2454
+ return this._value;
2455
+ }
2456
+ peek() {
2457
+ if (this._dirty) this._recompute();
2458
+ return this._value;
2459
+ }
2460
+ subscribe(fn) {
2461
+ if (this._dirty) this._recompute();
2462
+ this._subscribers.add(fn);
2463
+ return () => {
2464
+ this._subscribers.delete(fn);
2465
+ };
2466
+ }
2467
+ _addSource(src) {
2468
+ this._sources.add(src);
2469
+ }
2470
+ _removeConsumer(consumer) {
2471
+ this._consumers.delete(consumer);
2472
+ }
2473
+ _invalidate() {
2474
+ if (this._disposed) return;
2475
+ this._dirty = true;
2476
+ const newVal = this.peek();
2477
+ for (const sub of [...this._subscribers]) sub(newVal);
2478
+ for (const consumer of [...this._consumers]) consumer._invalidate();
2479
+ }
2480
+ _recompute() {
2481
+ for (const src of this._sources) src._removeConsumer(this);
2482
+ this._sources.clear();
2483
+ this._value = withConsumer(this, this._fn);
2484
+ this._dirty = false;
2485
+ }
2486
+ dispose() {
2487
+ this._disposed = true;
2488
+ for (const src of this._sources) src._removeConsumer(this);
2489
+ this._sources.clear();
2490
+ this._subscribers.clear();
2491
+ this._consumers.clear();
2492
+ }
2493
+ /**
2494
+ * @internal DevTools inspection only. Live observers (subscribers plus
2495
+ * downstream consumers). Read-only.
2496
+ */
2497
+ _observerCount() {
2498
+ return this._subscribers.size + this._consumers.size;
2499
+ }
2500
+ };
2501
+ var Effect = class {
2502
+ _fn;
2503
+ _cleanup = void 0;
2504
+ _disposed = false;
2505
+ _sources = /* @__PURE__ */ new Set();
2506
+ constructor(fn) {
2507
+ this._fn = fn;
2508
+ this._run();
2509
+ }
2510
+ _addSource(src) {
2511
+ this._sources.add(src);
2512
+ }
2513
+ _invalidate() {
2514
+ if (this._disposed) return;
2515
+ this._run();
2516
+ }
2517
+ _run() {
2518
+ for (const src of this._sources) src._removeConsumer(this);
2519
+ this._sources.clear();
2520
+ if (typeof this._cleanup === "function") this._cleanup();
2521
+ const result = withConsumer(this, this._fn);
2522
+ this._cleanup = typeof result === "function" ? result : void 0;
2523
+ }
2524
+ dispose() {
2525
+ this._disposed = true;
2526
+ for (const src of this._sources) src._removeConsumer(this);
2527
+ this._sources.clear();
2528
+ if (typeof this._cleanup === "function") this._cleanup();
2529
+ this._cleanup = void 0;
2530
+ }
2531
+ };
2532
+ function signal(initial) {
2533
+ return new Signal(initial);
2534
+ }
2535
+ function derived(fn) {
2536
+ return new DerivedSignal(fn);
2537
+ }
2538
+ function effect(fn) {
2539
+ const e = new Effect(fn);
2540
+ return () => e.dispose();
2541
+ }
2542
+
2543
+ // ../dsl/src/builders.ts
2544
+ function isSignal(v) {
2545
+ return v !== null && typeof v === "object" && typeof v["get"] === "function" && typeof v["subscribe"] === "function";
2546
+ }
2547
+ function bindValue(graph, node, propKey, value) {
2548
+ if (isSignal(value)) {
2549
+ const signalId = `${node.id}:${propKey}`;
2550
+ node.stateRefs.push({ signalId, propKey });
2551
+ graph.registerHandler(`__signal__${signalId}`, value);
2552
+ return value.peek();
2553
+ }
2554
+ return value;
2555
+ }
2556
+ function applyA11yProps(props, options) {
2557
+ if (options.role !== void 0) props["role"] = options.role;
2558
+ if (options.tabIndex !== void 0) props["tabindex"] = String(options.tabIndex);
2559
+ if (options.ariaLabel !== void 0) props["aria-label"] = options.ariaLabel;
2560
+ if (options.ariaLabelledBy !== void 0) props["aria-labelledby"] = options.ariaLabelledBy;
2561
+ if (options.ariaDescribedBy !== void 0) props["aria-describedby"] = options.ariaDescribedBy;
2562
+ if (options.ariaExpanded !== void 0) props["aria-expanded"] = String(options.ariaExpanded);
2563
+ if (options.ariaControls !== void 0) props["aria-controls"] = options.ariaControls;
2564
+ if (options.ariaHidden !== void 0) props["aria-hidden"] = String(options.ariaHidden);
2565
+ if (options.ariaLive !== void 0) props["aria-live"] = options.ariaLive;
2566
+ if (options.ariaCurrent !== void 0) props["aria-current"] = String(options.ariaCurrent);
2567
+ if (options.ariaInvalid !== void 0) props["aria-invalid"] = String(options.ariaInvalid);
2568
+ if (options.ariaRequired !== void 0) props["aria-required"] = String(options.ariaRequired);
2569
+ if (options.ariaModal !== void 0) props["aria-modal"] = String(options.ariaModal);
2570
+ }
2571
+ function containerProps(options) {
2572
+ const props = {};
2573
+ if (options.class !== void 0) props["class"] = options.class;
2574
+ if (options.id !== void 0) props["id"] = options.id;
2575
+ if (options.key !== void 0) props["key"] = options.key;
2576
+ applyA11yProps(props, options);
2577
+ return props;
2578
+ }
2579
+ function itemIdentity(item, index) {
2580
+ if (item !== null && typeof item === "object") {
2581
+ const obj = item;
2582
+ if ("id" in obj) return `id:${String(obj["id"])}`;
2583
+ if ("key" in obj) return `key:${String(obj["key"])}`;
2584
+ return `idx:${index}`;
2585
+ }
2586
+ return `val:${String(item)}`;
2587
+ }
2588
+ function itemValueSignature(item) {
2589
+ try {
2590
+ return JSON.stringify(item) ?? String(item);
2591
+ } catch {
2592
+ return String(item);
2593
+ }
2594
+ }
2595
+ function reactiveListItemSignature(item) {
2596
+ return itemValueSignature(item);
2597
+ }
2598
+ function reactiveListItemKey(item, index) {
2599
+ return itemIdentity(item, index);
2600
+ }
2601
+ var OVERLAY_KINDS = {
2602
+ dialog: {
2603
+ role: "dialog",
2604
+ modal: true,
2605
+ takesFocus: true,
2606
+ ariaModal: true,
2607
+ defaultCloseOnEscape: true,
2608
+ defaultRestoreFocus: true
2609
+ },
2610
+ popover: {
2611
+ role: "dialog",
2612
+ modal: false,
2613
+ takesFocus: true,
2614
+ ariaModal: false,
2615
+ defaultCloseOnEscape: true,
2616
+ defaultRestoreFocus: true
2617
+ },
2618
+ tooltip: {
2619
+ role: "tooltip",
2620
+ modal: false,
2621
+ takesFocus: false,
2622
+ ariaModal: false,
2623
+ defaultCloseOnEscape: false,
2624
+ defaultRestoreFocus: false
2625
+ },
2626
+ dropdown: {
2627
+ role: "menu",
2628
+ modal: false,
2629
+ takesFocus: true,
2630
+ ariaModal: false,
2631
+ defaultCloseOnEscape: true,
2632
+ defaultRestoreFocus: true
2633
+ },
2634
+ toast: {
2635
+ role: "status",
2636
+ modal: false,
2637
+ takesFocus: false,
2638
+ ariaModal: false,
2639
+ ariaLive: "polite",
2640
+ defaultCloseOnEscape: false,
2641
+ defaultRestoreFocus: false
2642
+ }
2643
+ };
2644
+ var ContentBuilderBase = class {
2645
+ constructor(_node, _graph) {
2646
+ this._node = _node;
2647
+ this._graph = _graph;
2648
+ }
2649
+ heading(text, options = {}) {
2650
+ const props = { level: options.level ?? 1 };
2651
+ if (options.class !== void 0) props["class"] = options.class;
2652
+ if (options.id !== void 0) props["id"] = options.id;
2653
+ applyA11yProps(props, options);
2654
+ const node = this._graph.createNode("heading", { parent: this._node, props });
2655
+ const resolved = bindValue(this._graph, node, "text", text);
2656
+ node.setProp("text", resolved);
2657
+ }
2658
+ text(content, options = {}) {
2659
+ const props = {};
2660
+ if (options.class !== void 0) props["class"] = options.class;
2661
+ if (options.id !== void 0) props["id"] = options.id;
2662
+ applyA11yProps(props, options);
2663
+ const node = this._graph.createNode("text", { parent: this._node, props });
2664
+ const resolved = bindValue(this._graph, node, "text", content);
2665
+ node.setProp("text", resolved);
2666
+ }
2667
+ button(label, options = {}) {
2668
+ const props = {};
2669
+ if (options.class !== void 0) props["class"] = options.class;
2670
+ if (options.id !== void 0) props["id"] = options.id;
2671
+ applyA11yProps(props, options);
2672
+ const node = this._graph.createNode("button", { parent: this._node, props });
2673
+ const resolved = bindValue(this._graph, node, "label", label);
2674
+ node.setProp("label", resolved);
2675
+ if (options.disabled !== void 0) {
2676
+ const resolvedDisabled = bindValue(this._graph, node, "disabled", options.disabled);
2677
+ node.setProp("disabled", resolvedDisabled);
2678
+ }
2679
+ if (options.onClick !== void 0) {
2680
+ const handlerKey = `click:${node.id}`;
2681
+ this._graph.registerHandler(handlerKey, options.onClick);
2682
+ node.addEvent({ type: "click", handlerKey });
2683
+ }
2684
+ }
2685
+ input(options = {}) {
2686
+ const props = {};
2687
+ props["inputType"] = options.type ?? "text";
2688
+ if (options.placeholder !== void 0) props["placeholder"] = options.placeholder;
2689
+ if (options.class !== void 0) props["class"] = options.class;
2690
+ if (options.id !== void 0) props["id"] = options.id;
2691
+ applyA11yProps(props, options);
2692
+ const nodeOpts = {
2693
+ props,
2694
+ parent: this._node
2695
+ };
2696
+ if (options.id !== void 0) nodeOpts.key = options.id;
2697
+ const node = this._graph.createNode("input", nodeOpts);
2698
+ const bindSignal = options.bind;
2699
+ const valueBindable = bindSignal !== void 0 ? bindSignal : options.value;
2700
+ const inputHandler = bindSignal !== void 0 ? (v) => bindSignal.set(v) : options.onInput;
2701
+ if (valueBindable !== void 0) {
2702
+ const resolved = bindValue(this._graph, node, "value", valueBindable);
2703
+ node.setProp("value", resolved);
2704
+ }
2705
+ if (options.disabled !== void 0) {
2706
+ const resolved = bindValue(this._graph, node, "disabled", options.disabled);
2707
+ node.setProp("disabled", resolved);
2708
+ }
2709
+ if (inputHandler !== void 0) {
2710
+ const handlerKey = `input:${node.id}`;
2711
+ this._graph.registerHandler(handlerKey, inputHandler);
2712
+ node.addEvent({ type: "input", handlerKey });
2713
+ }
2714
+ if (options.onChange !== void 0) {
2715
+ const handlerKey = `change:${node.id}`;
2716
+ this._graph.registerHandler(handlerKey, options.onChange);
2717
+ node.addEvent({ type: "change", handlerKey });
2718
+ }
2719
+ }
2720
+ image(options) {
2721
+ const props = {
2722
+ src: options.src,
2723
+ alt: options.alt
2724
+ };
2725
+ if (options.width !== void 0) props["width"] = options.width;
2726
+ if (options.height !== void 0) props["height"] = options.height;
2727
+ if (options.class !== void 0) props["class"] = options.class;
2728
+ if (options.id !== void 0) props["id"] = options.id;
2729
+ applyA11yProps(props, options);
2730
+ const nodeOpts = {
2731
+ props,
2732
+ parent: this._node
2733
+ };
2734
+ if (options.id !== void 0) nodeOpts.key = options.id;
2735
+ this._graph.createNode("image", nodeOpts);
2736
+ }
2737
+ link(label, options) {
2738
+ const props = {
2739
+ href: options.href,
2740
+ external: options.external ?? false
2741
+ };
2742
+ if (options.class !== void 0) props["class"] = options.class;
2743
+ if (options.id !== void 0) props["id"] = options.id;
2744
+ applyA11yProps(props, options);
2745
+ const node = this._graph.createNode("link", { parent: this._node, props });
2746
+ const resolved = bindValue(this._graph, node, "label", label);
2747
+ node.setProp("label", resolved);
2748
+ if (options.onClick !== void 0) {
2749
+ const handlerKey = `click:${node.id}`;
2750
+ this._graph.registerHandler(handlerKey, options.onClick);
2751
+ node.addEvent({ type: "click", handlerKey });
2752
+ }
2753
+ }
2754
+ };
2755
+ var ContainerBuilderBase = class extends ContentBuilderBase {
2756
+ section(key, builder, options = {}) {
2757
+ const node = this._graph.createNode("section", {
2758
+ key,
2759
+ parent: this._node,
2760
+ props: containerProps(options)
2761
+ });
2762
+ builder(new SectionBuilderImpl(node, this._graph));
2763
+ }
2764
+ container(key, builder, options = {}) {
2765
+ const node = this._graph.createNode("container", {
2766
+ key,
2767
+ parent: this._node,
2768
+ props: containerProps(options)
2769
+ });
2770
+ builder(new ContainerBuilderImpl(node, this._graph));
2771
+ }
2772
+ list(key, builder, options = {}) {
2773
+ const node = this._graph.createNode("list", {
2774
+ key,
2775
+ parent: this._node,
2776
+ props: containerProps(options)
2777
+ });
2778
+ builder(new ListBuilderImpl(node, this._graph));
2779
+ }
2780
+ listOf(key, items, renderItem, options = {}) {
2781
+ const graph = this._graph;
2782
+ const node = graph.createNode("reactive-list", {
2783
+ key,
2784
+ parent: this._node,
2785
+ props: containerProps(options)
2786
+ });
2787
+ const signalId = `${node.id}:items`;
2788
+ node.stateRefs.push({ signalId, propKey: "items" });
2789
+ graph.registerHandler(`__signal__${signalId}`, items);
2790
+ const buildItem = (item, index) => {
2791
+ const itemKey = reactiveListItemKey(item, index);
2792
+ const itemNode = graph.createNode("list-item", {
2793
+ key: itemKey,
2794
+ // `_item` records the source item *reference* so the reconciler can
2795
+ // short-circuit unchanged rows by identity (no signature hashing); `_sig`
2796
+ // is the content signature used to detect an in-place data change when the
2797
+ // reference differs. Both are internal metadata (leading `_`) and never
2798
+ // reach the DOM.
2799
+ props: {
2800
+ key: itemKey,
2801
+ _sig: reactiveListItemSignature(item),
2802
+ // The item reference is stored as opaque internal metadata (never
2803
+ // rendered); cast through `unknown` since `T` is not a `PropValue`.
2804
+ _item: item
2805
+ }
2806
+ });
2807
+ renderItem(item, index, new ContainerBuilderImpl(itemNode, graph));
2808
+ return itemNode;
2809
+ };
2810
+ const buildPlan = (raw) => {
2811
+ const arr = Array.isArray(raw) ? raw : [];
2812
+ const plan = new Array(arr.length);
2813
+ for (let i = 0; i < arr.length; i++) {
2814
+ const item = arr[i];
2815
+ const index = i;
2816
+ plan[i] = {
2817
+ key: reactiveListItemKey(item, index),
2818
+ item,
2819
+ sig: () => reactiveListItemSignature(item),
2820
+ build: () => buildItem(item, index)
2821
+ };
2822
+ }
2823
+ return plan;
2824
+ };
2825
+ graph.registerHandler(`__listplan__${node.id}`, buildPlan);
2826
+ const current = isSignal(items) ? items.peek() : items;
2827
+ const initial = Array.isArray(current) ? current : [];
2828
+ initial.forEach((item, i) => {
2829
+ node.appendChild(buildItem(item, i));
2830
+ });
2831
+ }
2832
+ form(key, builder, options = {}) {
2833
+ const props = containerProps(options);
2834
+ const node = this._graph.createNode("form", {
2835
+ key,
2836
+ parent: this._node,
2837
+ props
2838
+ });
2839
+ if (options.onSubmit !== void 0) {
2840
+ const handlerKey = `submit:${node.id}`;
2841
+ this._graph.registerHandler(handlerKey, options.onSubmit);
2842
+ node.addEvent({ type: "submit", handlerKey });
2843
+ }
2844
+ builder(new FormBuilderImpl(node, this._graph));
2845
+ }
2846
+ when(condition, builder, elseBuilder) {
2847
+ const graph = this._graph;
2848
+ const node = graph.createNode("conditional", {
2849
+ parent: this._node,
2850
+ props: containerProps({})
2851
+ });
2852
+ const buildBranch = (build, tag) => {
2853
+ const branchKey = `when-${tag}:${node.id}`;
2854
+ const branch = graph.createNode("container", {
2855
+ key: branchKey,
2856
+ props: { key: branchKey }
2857
+ });
2858
+ build(new ContainerBuilderImpl(branch, graph));
2859
+ return branch;
2860
+ };
2861
+ const buildAll = (raw) => {
2862
+ if (raw) return [buildBranch(builder, "then")];
2863
+ return elseBuilder !== void 0 ? [buildBranch(elseBuilder, "else")] : [];
2864
+ };
2865
+ if (isSignal(condition)) {
2866
+ const signalId = `${node.id}:items`;
2867
+ node.stateRefs.push({ signalId, propKey: "items" });
2868
+ graph.registerHandler(`__signal__${signalId}`, condition);
2869
+ graph.registerHandler(`__listbuild__${node.id}`, buildAll);
2870
+ const current = condition.peek();
2871
+ for (const child of buildAll(current)) node.appendChild(child);
2872
+ } else {
2873
+ for (const child of buildAll(condition)) node.appendChild(child);
2874
+ }
2875
+ }
2876
+ errorBoundary(id, builder, options) {
2877
+ const sources = options.source === void 0 ? [] : Array.isArray(options.source) ? [...options.source] : [options.source];
2878
+ const localError = signal(void 0);
2879
+ const retryNonce = signal(0);
2880
+ const readError = () => {
2881
+ const local = localError.peek();
2882
+ if (local !== void 0 && local !== null) return local;
2883
+ for (const s of sources) {
2884
+ const e = s.peek();
2885
+ if (e !== void 0 && e !== null) return e;
2886
+ }
2887
+ return void 0;
2888
+ };
2889
+ const hasError = derived(() => {
2890
+ retryNonce.get();
2891
+ localError.get();
2892
+ for (const s of sources) s.get();
2893
+ return readError() !== void 0;
2894
+ });
2895
+ const retry = () => {
2896
+ localError.set(void 0);
2897
+ options.onRetry?.();
2898
+ retryNonce.update((n) => n + 1);
2899
+ };
2900
+ this.container(id, (c) => {
2901
+ c.when(
2902
+ hasError,
2903
+ // Error state → fallback.
2904
+ (fb) => options.fallback(fb, readError(), retry),
2905
+ // Healthy state → body, guarded against synchronous build throws.
2906
+ (body) => {
2907
+ try {
2908
+ builder(body);
2909
+ } catch (err) {
2910
+ queueMicrotask(() => localError.set(err));
2911
+ }
2912
+ }
2913
+ );
2914
+ }, { id });
2915
+ }
2916
+ // ── Portals & overlays ──────────────────────────────────────────────────────
2917
+ portal(key, builder, options = {}) {
2918
+ const node = this._graph.createNode("portal", {
2919
+ key,
2920
+ parent: this._node,
2921
+ props: containerProps(options)
2922
+ });
2923
+ builder(new ContainerBuilderImpl(node, this._graph));
2924
+ }
2925
+ /**
2926
+ * Shared assembly for every overlay kind: a `portal` node whose single child
2927
+ * is a `when(open, panel)` conditional. The panel container carries the
2928
+ * kind's ARIA semantics; `builder` fills it. An `__overlay__<portalId>`
2929
+ * descriptor is registered so the renderer wires focus/keyboard behavior to
2930
+ * the same `open` signal that drives the panel. Reuses existing primitives
2931
+ * (portal + when + container) — no new render path.
2932
+ */
2933
+ _overlay(kind, key, options, builder) {
2934
+ const graph = this._graph;
2935
+ const portalNode = graph.createNode("portal", {
2936
+ key,
2937
+ parent: this._node,
2938
+ props: { key }
2939
+ });
2940
+ const openBindable = options.open;
2941
+ const openSignal = isSignal(openBindable) ? openBindable : signal(openBindable);
2942
+ const panelOptions = {
2943
+ role: options.role ?? kind.role,
2944
+ ...kind.ariaModal ? { ariaModal: true } : {},
2945
+ ...kind.ariaLive !== void 0 ? { ariaLive: kind.ariaLive } : {},
2946
+ ...options.class !== void 0 ? { class: options.class } : {},
2947
+ ...options.ariaLabel !== void 0 ? { ariaLabel: options.ariaLabel } : {},
2948
+ ...options.ariaLabelledBy !== void 0 ? { ariaLabelledBy: options.ariaLabelledBy } : {},
2949
+ ...options.ariaDescribedBy !== void 0 ? { ariaDescribedBy: options.ariaDescribedBy } : {}
2950
+ };
2951
+ const portalBuilder = new ContainerBuilderImpl(portalNode, graph);
2952
+ portalBuilder.when(openSignal, (panelHost) => {
2953
+ panelHost.container(`${key}__panel`, builder, panelOptions);
2954
+ });
2955
+ const descriptor = {
2956
+ open: openSignal,
2957
+ modal: kind.modal,
2958
+ takesFocus: kind.takesFocus,
2959
+ closeOnEscape: options.closeOnEscape ?? kind.defaultCloseOnEscape,
2960
+ restoreFocus: options.restoreFocus ?? kind.defaultRestoreFocus,
2961
+ ...options.initialFocusId !== void 0 ? { initialFocusId: options.initialFocusId } : {},
2962
+ ...options.onClose !== void 0 ? { onClose: options.onClose } : {}
2963
+ };
2964
+ graph.registerHandler(
2965
+ `__overlay__${portalNode.id}`,
2966
+ () => descriptor
2967
+ );
2968
+ }
2969
+ dialog(key, options, builder) {
2970
+ this._overlay(OVERLAY_KINDS.dialog, key, options, builder);
2971
+ }
2972
+ popover(key, options, builder) {
2973
+ this._overlay(OVERLAY_KINDS.popover, key, options, builder);
2974
+ }
2975
+ tooltip(key, options, builder) {
2976
+ this._overlay(OVERLAY_KINDS.tooltip, key, options, builder);
2977
+ }
2978
+ dropdown(key, options, builder) {
2979
+ this._overlay(OVERLAY_KINDS.dropdown, key, options, builder);
2980
+ }
2981
+ toast(key, options, builder) {
2982
+ this._overlay(OVERLAY_KINDS.toast, key, options, builder);
2983
+ }
2984
+ // ── Components ────────────────────────────────────────────────────────────
2985
+ /**
2986
+ * Instantiate a reusable component (§3–§9). Creates a `'component'` node
2987
+ * (rendered as a `<div>` wrapper — preserves the one-node/one-element
2988
+ * positional-hydration invariant), then runs `def.setup(props, ctx)`
2989
+ * synchronously to obtain the render function and fills the component's own
2990
+ * container scope with it — structurally identical to `container`/
2991
+ * `errorBoundary`. Cleanups the setup registers via `ctx.effect`/
2992
+ * `ctx.onCleanup` are collected into a closure and exposed to the renderer
2993
+ * through a `__component__<id>` handler (mirroring `__overlay__`); the mount/
2994
+ * hydrate paths read it and route each into `NodeInstance.trackCleanup`, so
2995
+ * teardown runs (children-first) when the component leaves the graph.
2996
+ *
2997
+ * `setup` runs once per instance here at build time. When this component sits
2998
+ * inside a keyed list / conditional, a rebuild disposes the old instance
2999
+ * (running its cleanups + pruning its `__component__` entry) and re-runs this
3000
+ * method for the new node — so re-invocation is safe and leak-free.
3001
+ */
3002
+ component(key, def, props, children) {
3003
+ const graph = this._graph;
3004
+ const node = graph.createNode("component", {
3005
+ key,
3006
+ parent: this._node,
3007
+ // `data-streetui-component` is a non-underscore prop, so it reaches the
3008
+ // DOM as an attribute and is visible to DevTools (§21) — unlike the
3009
+ // internal `_`-prefixed metadata the renderer hides.
3010
+ props: { key, "data-streetui-component": def.name }
3011
+ });
3012
+ const cleanups = [];
3013
+ const ctx = {
3014
+ key,
3015
+ onCleanup(fn) {
3016
+ cleanups.push(fn);
3017
+ },
3018
+ effect(fn) {
3019
+ cleanups.push(effect(fn));
3020
+ },
3021
+ renderChildren(content) {
3022
+ if (children !== void 0) children(content);
3023
+ }
3024
+ };
3025
+ let render2;
3026
+ try {
3027
+ render2 = def.setup(props, ctx);
3028
+ render2(new ContainerBuilderImpl(node, graph));
3029
+ } catch (err) {
3030
+ queueMicrotask(() => {
3031
+ throw err;
3032
+ });
3033
+ }
3034
+ if (cleanups.length > 0) {
3035
+ graph.registerHandler(
3036
+ `__component__${node.id}`,
3037
+ () => cleanups
3038
+ );
3039
+ }
3040
+ }
3041
+ };
3042
+ var SectionBuilderImpl = class extends ContainerBuilderBase {
3043
+ };
3044
+ var ContainerBuilderImpl = class extends ContainerBuilderBase {
3045
+ };
3046
+ var FormBuilderImpl = class extends ContainerBuilderBase {
3047
+ };
3048
+ var ListBuilderImpl = class extends ContentBuilderBase {
3049
+ item(key, builder, options = {}) {
3050
+ const node = this._graph.createNode("list-item", {
3051
+ key,
3052
+ parent: this._node,
3053
+ props: containerProps(options)
3054
+ });
3055
+ builder(new ContainerBuilderImpl(node, this._graph));
3056
+ }
3057
+ };
3058
+ var PageBuilderImpl = class extends ContainerBuilderBase {
3059
+ };
3060
+ var AppBuilder = class {
3061
+ constructor(_graph) {
3062
+ this._graph = _graph;
3063
+ }
3064
+ page(key, builder) {
3065
+ const node = this._graph.createNode("page", {
3066
+ key,
3067
+ parent: this._graph.root,
3068
+ props: { key }
3069
+ });
3070
+ builder(new PageBuilderImpl(node, this._graph));
3071
+ }
3072
+ };
3073
+
3074
+ // ../graph/src/graph-node.ts
3075
+ var GraphNode = class _GraphNode {
3076
+ id;
3077
+ type;
3078
+ key;
3079
+ props;
3080
+ events;
3081
+ stateRefs;
3082
+ children;
3083
+ parent;
3084
+ constructor(type, options = {}) {
3085
+ this.type = type;
3086
+ this.id = options.id ?? generateNodeId(type);
3087
+ this.key = options.key;
3088
+ this.props = options.props ?? {};
3089
+ this.events = options.events ?? [];
3090
+ this.stateRefs = options.stateRefs ?? [];
3091
+ this.children = [];
3092
+ this.parent = null;
3093
+ }
3094
+ // ── Child management ────────────────────────────────────────────────────────
3095
+ appendChild(child) {
3096
+ if (child.parent !== null) {
3097
+ child.parent.removeChild(child);
3098
+ }
3099
+ child.parent = this;
3100
+ this.children.push(child);
3101
+ }
3102
+ insertBefore(child, reference) {
3103
+ const idx = this.children.indexOf(reference);
3104
+ if (idx === -1) {
3105
+ this.appendChild(child);
3106
+ return;
3107
+ }
3108
+ if (child.parent !== null) {
3109
+ child.parent.removeChild(child);
3110
+ }
3111
+ child.parent = this;
3112
+ this.children.splice(idx, 0, child);
3113
+ }
3114
+ removeChild(child) {
3115
+ const idx = this.children.indexOf(child);
3116
+ if (idx === -1) return;
3117
+ this.children.splice(idx, 1);
3118
+ child.parent = null;
3119
+ }
3120
+ replaceChild(newChild, oldChild) {
3121
+ const idx = this.children.indexOf(oldChild);
3122
+ if (idx === -1) {
3123
+ throw new Error(`GraphNode.replaceChild: oldChild is not a child of this node`);
3124
+ }
3125
+ if (newChild.parent !== null) {
3126
+ newChild.parent.removeChild(newChild);
3127
+ }
3128
+ oldChild.parent = null;
3129
+ newChild.parent = this;
3130
+ this.children.splice(idx, 1, newChild);
3131
+ }
3132
+ // ── Prop helpers ────────────────────────────────────────────────────────────
3133
+ setProp(key, value) {
3134
+ this.props = { ...this.props, [key]: value };
3135
+ }
3136
+ getProp(key) {
3137
+ return this.props[key];
3138
+ }
3139
+ // ── Event helpers ───────────────────────────────────────────────────────────
3140
+ addEvent(descriptor) {
3141
+ this.events.push(descriptor);
3142
+ }
3143
+ removeEvent(type) {
3144
+ this.events = this.events.filter((e) => e.type !== type);
3145
+ }
3146
+ // ── Queries ─────────────────────────────────────────────────────────────────
3147
+ get isLeaf() {
3148
+ return this.children.length === 0;
3149
+ }
3150
+ get depth() {
3151
+ let d = 0;
3152
+ let node = this.parent;
3153
+ while (node !== null) {
3154
+ d++;
3155
+ node = node.parent;
3156
+ }
3157
+ return d;
3158
+ }
3159
+ get root() {
3160
+ let node = this;
3161
+ while (node.parent !== null) {
3162
+ node = node.parent;
3163
+ }
3164
+ return node;
3165
+ }
3166
+ /** Shallow clone — does not clone children. */
3167
+ shallowClone() {
3168
+ const opts = {
3169
+ props: { ...this.props },
3170
+ events: [...this.events],
3171
+ stateRefs: [...this.stateRefs]
3172
+ };
3173
+ if (this.key !== void 0) opts.key = this.key;
3174
+ return new _GraphNode(this.type, opts);
3175
+ }
3176
+ };
3177
+
3178
+ // ../graph/src/graph.ts
3179
+ var ApplicationGraph = class {
3180
+ root;
3181
+ name;
3182
+ version;
3183
+ _nodeIndex = /* @__PURE__ */ new Map();
3184
+ /** Handler registry — maps handlerKey → actual function */
3185
+ handlers = /* @__PURE__ */ new Map();
3186
+ constructor(options) {
3187
+ this.name = options.name;
3188
+ this.version = options.version ?? "0.0.1";
3189
+ this.root = new GraphNode("application", { props: { name: options.name } });
3190
+ this._nodeIndex.set(this.root.id, this.root);
3191
+ }
3192
+ // ── Node creation & attachment ────────────────────────────────────────────
3193
+ createNode(type, options = {}) {
3194
+ const nodeOpts = {};
3195
+ if (options.key !== void 0) nodeOpts.key = options.key;
3196
+ if (options.props !== void 0) nodeOpts.props = options.props;
3197
+ const node = new GraphNode(type, nodeOpts);
3198
+ this._nodeIndex.set(node.id, node);
3199
+ if (options.parent !== void 0) {
3200
+ options.parent.appendChild(node);
3201
+ }
3202
+ return node;
3203
+ }
3204
+ attachNode(node, parent) {
3205
+ this._nodeIndex.set(node.id, node);
3206
+ parent.appendChild(node);
3207
+ }
3208
+ detachNode(node) {
3209
+ if (node.parent !== null) {
3210
+ node.parent.removeChild(node);
3211
+ }
3212
+ this._removeFromIndex(node);
3213
+ }
3214
+ _removeFromIndex(node) {
3215
+ this._nodeIndex.delete(node.id);
3216
+ this._unregisterNodeHandlers(node);
3217
+ for (const child of node.children) {
3218
+ this._removeFromIndex(child);
3219
+ }
3220
+ }
3221
+ /**
3222
+ * Remove every handler-registry entry owned by a single node. A node owns:
3223
+ * - one entry per event descriptor (its `handlerKey`),
3224
+ * - one `__signal__<signalId>` entry per state ref (signalIds are namespaced
3225
+ * by node id, so they are never shared between nodes), and
3226
+ * - a `__listbuild__<id>` entry if it is a reactive-list.
3227
+ * Called for every node in a detached subtree so removing list items (or
3228
+ * discarding freshly-built-but-unadopted item subtrees) leaves no stale
3229
+ * registrations behind.
3230
+ */
3231
+ _unregisterNodeHandlers(node) {
3232
+ for (const event of node.events) {
3233
+ this.handlers.delete(event.handlerKey);
3234
+ }
3235
+ for (const ref of node.stateRefs) {
3236
+ this.handlers.delete(`__signal__${ref.signalId}`);
3237
+ }
3238
+ this.handlers.delete(`__listbuild__${node.id}`);
3239
+ this.handlers.delete(`__listplan__${node.id}`);
3240
+ this.handlers.delete(`__overlay__${node.id}`);
3241
+ this.handlers.delete(`__component__${node.id}`);
3242
+ }
3243
+ // ── Handler registry ──────────────────────────────────────────────────────
3244
+ registerHandler(key, fn) {
3245
+ this.handlers.set(key, fn);
3246
+ }
3247
+ getHandler(key) {
3248
+ return this.handlers.get(key);
3249
+ }
3250
+ /** True if a handler is currently registered under `key`. Inspection helper. */
3251
+ hasHandler(key) {
3252
+ return this.handlers.has(key);
3253
+ }
3254
+ /** Number of currently-registered handlers. Inspection helper. */
3255
+ get handlerCount() {
3256
+ return this.handlers.size;
3257
+ }
3258
+ // ── Lookup ────────────────────────────────────────────────────────────────
3259
+ findById(id) {
3260
+ return this._nodeIndex.get(id);
3261
+ }
3262
+ findAll(predicate) {
3263
+ const results = [];
3264
+ this._walk(this.root, (node) => {
3265
+ if (predicate(node)) results.push(node);
3266
+ });
3267
+ return results;
3268
+ }
3269
+ findByType(type) {
3270
+ return this.findAll((n) => n.type === type);
3271
+ }
3272
+ // ── Traversal ─────────────────────────────────────────────────────────────
3273
+ walk(visitor) {
3274
+ this._walk(this.root, visitor, 0);
3275
+ }
3276
+ _walk(node, visitor, depth = 0) {
3277
+ visitor(node, depth);
3278
+ for (const child of node.children) {
3279
+ this._walk(child, visitor, depth + 1);
3280
+ }
3281
+ }
3282
+ get nodeCount() {
3283
+ return this._nodeIndex.size;
3284
+ }
3285
+ // ── Validation ────────────────────────────────────────────────────────────
3286
+ validate() {
3287
+ const dc = new DiagnosticCollector();
3288
+ this.walk((node) => {
3289
+ for (const event of node.events) {
3290
+ if (!this.handlers.has(event.handlerKey)) {
3291
+ dc.warn(
3292
+ "GRAPH_MISSING_HANDLER",
3293
+ `Node "${node.id}" references handler "${event.handlerKey}" which is not registered`,
3294
+ { nodeId: node.id }
3295
+ );
3296
+ }
3297
+ }
3298
+ if (node.type === "page" && node.parent?.type !== "application") {
3299
+ dc.error(
3300
+ "GRAPH_PAGE_DEPTH",
3301
+ `Page node "${node.id}" must be a direct child of the application root`,
3302
+ { nodeId: node.id }
3303
+ );
3304
+ }
3305
+ });
3306
+ return dc;
3307
+ }
3308
+ // ── Serialization ─────────────────────────────────────────────────────────
3309
+ serialize() {
3310
+ return {
3311
+ name: this.name,
3312
+ version: this.version,
3313
+ root: this._serializeNode(this.root)
3314
+ };
3315
+ }
3316
+ _serializeNode(node) {
3317
+ const result = {
3318
+ id: node.id,
3319
+ type: node.type,
3320
+ key: node.key,
3321
+ props: node.props,
3322
+ events: node.events,
3323
+ stateRefs: node.stateRefs,
3324
+ children: node.children.map((c) => this._serializeNode(c))
3325
+ };
3326
+ return result;
3327
+ }
3328
+ };
3329
+
3330
+ // ../dsl/src/dsl.ts
3331
+ var StreetApp = class {
3332
+ _graph;
3333
+ _builder;
3334
+ constructor(options) {
3335
+ const graphOpts = { name: options.name };
3336
+ if (options.version !== void 0) graphOpts.version = options.version;
3337
+ this._graph = new ApplicationGraph(graphOpts);
3338
+ this._builder = new AppBuilder(this._graph);
3339
+ }
3340
+ page(key, builder) {
3341
+ this._builder.page(key, builder);
3342
+ return this;
3343
+ }
3344
+ /** Compile to ApplicationGraph — validates and returns the graph. */
3345
+ build() {
3346
+ const dc = this._graph.validate();
3347
+ dc.throwIfErrors();
3348
+ return this._graph;
3349
+ }
3350
+ /** Access graph before building (useful for inspection). */
3351
+ get graph() {
3352
+ return this._graph;
3353
+ }
3354
+ };
3355
+ var streetui = {
3356
+ app(options) {
3357
+ return new StreetApp(options);
3358
+ }
3359
+ };
3360
+
3361
+ // ../testing/src/component.ts
3362
+ var COMPONENT_ATTR = "data-streetui-component";
3363
+ function renderComponent(def, props, children) {
3364
+ const app = streetui.app({ name: `test:${def.name}` });
3365
+ app.page("host", (page) => {
3366
+ page.component("root", def, props, children);
3367
+ });
3368
+ const result = render(app);
3369
+ const component = result.container.querySelector(`[${COMPONENT_ATTR}]`);
3370
+ if (component === null) {
3371
+ throw new Error(`[StreetUI Testing] renderComponent: no component element rendered for "${def.name}"`);
3372
+ }
3373
+ return { ...result, component };
3374
+ }
3375
+ function hydrateComponent(build, options = {}) {
3376
+ return renderServerThenHydrate(() => {
3377
+ const { def, props, children } = build();
3378
+ const app = streetui.app({ name: `test:${def.name}` });
3379
+ app.page("host", (page) => page.component("root", def, props, children));
3380
+ return app;
3381
+ }, options);
3382
+ }
3383
+ function findAllComponents(container, name) {
3384
+ const selector = name === void 0 ? `[${COMPONENT_ATTR}]` : `[${COMPONENT_ATTR}="${name}"]`;
3385
+ return Array.from(container.querySelectorAll(selector));
3386
+ }
3387
+ function findComponent(container, name) {
3388
+ const matches = findAllComponents(container, name);
3389
+ const named = name !== void 0 ? ` named "${name}"` : "";
3390
+ if (matches.length === 0) {
3391
+ throw new Error(`[StreetUI Testing] No component${named} found`);
3392
+ }
3393
+ if (matches.length > 1) {
3394
+ throw new Error(
3395
+ `[StreetUI Testing] Found ${matches.length} components${named} \u2014 pass a name to disambiguate`
3396
+ );
3397
+ }
3398
+ return matches[0];
3399
+ }
3400
+ function getComponentName(el) {
3401
+ return el.getAttribute(COMPONENT_ATTR);
3402
+ }
3403
+ function trigger(el, type, init = {}) {
3404
+ const base = { bubbles: true, cancelable: true, ...init };
3405
+ let event;
3406
+ if (type.startsWith("key")) {
3407
+ event = new KeyboardEvent(type, base);
3408
+ } else if (type.startsWith("mouse") || type === "click" || type === "dblclick") {
3409
+ event = new MouseEvent(type, base);
3410
+ } else if (type === "input" || type === "change") {
3411
+ event = new Event(type, base);
3412
+ } else {
3413
+ event = new Event(type, base);
3414
+ }
3415
+ el.dispatchEvent(event);
3416
+ }
3417
+
2338
3418
  // src/version.ts
2339
- var VERSION = "1.7.0";
3419
+ var VERSION = "1.8.0";
2340
3420
  // Annotate the CommonJS export names for ESM import in node:
2341
3421
  0 && (module.exports = {
2342
3422
  VERSION,
2343
3423
  analyzeGraph,
2344
3424
  findAllByRole,
3425
+ findAllComponents,
2345
3426
  findByRole,
2346
3427
  findByText,
3428
+ findComponent,
2347
3429
  flushUpdates,
2348
3430
  formatInspection,
3431
+ getComponentName,
3432
+ hydrateComponent,
2349
3433
  inspectCompilation,
2350
3434
  render,
3435
+ renderComponent,
2351
3436
  renderOnce,
2352
3437
  renderServerThenHydrate,
3438
+ trigger,
2353
3439
  waitFor
2354
3440
  });
2355
3441
  //# sourceMappingURL=testing.cjs.map