semajsx 0.8.0 → 0.9.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 (44) hide show
  1. package/dist/{client-BrupjhG0.mjs → client-CEJQ4fit.mjs} +3 -3
  2. package/dist/{client-BrupjhG0.mjs.map → client-CEJQ4fit.mjs.map} +1 -1
  3. package/dist/{document-DsiJO2jG.mjs → document-Cbz4084O.mjs} +2 -2
  4. package/dist/{document-XKyAs62C.mjs → document-Cfdhi7vG.mjs} +2 -2
  5. package/dist/{document-XKyAs62C.mjs.map → document-Cfdhi7vG.mjs.map} +1 -1
  6. package/dist/dom/index.mjs +2 -2
  7. package/dist/dom/jsx-dev-runtime.mjs +1 -1
  8. package/dist/dom/jsx-runtime.mjs +1 -1
  9. package/dist/index.d.mts +11 -0
  10. package/dist/index.d.mts.map +1 -1
  11. package/dist/index.mjs +1 -1
  12. package/dist/{jsx-runtime-Dc77fsnM.d.mts → jsx-runtime-tdaY-P9K.d.mts} +2 -2
  13. package/dist/{jsx-runtime-Dc77fsnM.d.mts.map → jsx-runtime-tdaY-P9K.d.mts.map} +1 -1
  14. package/dist/{lucide-Ddt_N9dJ.mjs → lucide-DWk3itzO.mjs} +3 -3
  15. package/dist/{lucide-Ddt_N9dJ.mjs.map → lucide-DWk3itzO.mjs.map} +1 -1
  16. package/dist/{resource-pm7qP-jV.mjs → resource-BU0Po0ez.mjs} +2 -2
  17. package/dist/{resource-pm7qP-jV.mjs.map → resource-BU0Po0ez.mjs.map} +1 -1
  18. package/dist/{src-Cv4rRVzv.mjs → src--YS4EvMz.mjs} +9 -6
  19. package/dist/src--YS4EvMz.mjs.map +1 -0
  20. package/dist/{src-CXY-7FC3.mjs → src-77V1Plyd.mjs} +665 -129
  21. package/dist/src-77V1Plyd.mjs.map +1 -0
  22. package/dist/{src-SqJ6k7Xv.mjs → src-BTG08Qnh.mjs} +4 -4
  23. package/dist/{src-SqJ6k7Xv.mjs.map → src-BTG08Qnh.mjs.map} +1 -1
  24. package/dist/{src-C_aFsFJ3.mjs → src-Cm12Y2XV.mjs} +2 -2
  25. package/dist/{src-C_aFsFJ3.mjs.map → src-Cm12Y2XV.mjs.map} +1 -1
  26. package/dist/{src-CAyv9Uf9.mjs → src-Mucdq4zw.mjs} +6 -6
  27. package/dist/{src-CAyv9Uf9.mjs.map → src-Mucdq4zw.mjs.map} +1 -1
  28. package/dist/ssg/index.mjs +6 -6
  29. package/dist/ssg/plugins/docs-theme.mjs +9 -9
  30. package/dist/ssg/plugins/lucide.mjs +3 -3
  31. package/dist/ssr/client.mjs +4 -4
  32. package/dist/ssr/index.mjs +5 -5
  33. package/dist/terminal/index.d.mts +248 -4
  34. package/dist/terminal/index.d.mts.map +1 -1
  35. package/dist/terminal/index.mjs +3 -3
  36. package/dist/terminal/jsx-dev-runtime.d.mts +2 -2
  37. package/dist/terminal/jsx-dev-runtime.mjs +1 -1
  38. package/dist/terminal/jsx-runtime.d.mts +2 -2
  39. package/dist/terminal/jsx-runtime.mjs +1 -1
  40. package/dist/{types-Bj5q5x2Q.d.mts → types-Bm8rZGKW.d.mts} +2 -2
  41. package/dist/{types-Bj5q5x2Q.d.mts.map → types-Bm8rZGKW.d.mts.map} +1 -1
  42. package/package.json +1 -1
  43. package/dist/src-CXY-7FC3.mjs.map +0 -1
  44. package/dist/src-Cv4rRVzv.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { t as signal } from "./signal-4PgGfydw.mjs";
2
2
  import { t as computed } from "./computed-BpjqvQu1.mjs";
3
- import { c as when, i as jsx, t as createRenderer, v as Fragment } from "./src-Cv4rRVzv.mjs";
3
+ import { a as jsxs, c as when, i as jsx, t as createRenderer, v as Fragment } from "./src--YS4EvMz.mjs";
4
4
  import Yoga from "yoga-layout-prebuilt";
5
5
  import ansiEscapes from "ansi-escapes";
6
6
  import stringWidth from "string-width";
@@ -11,54 +11,62 @@ import wrapAnsi from "wrap-ansi";
11
11
 
12
12
  //#region ../terminal/src/utils/colors.ts
13
13
  /**
14
+ * Color name to chalk function mapping (hoisted to module level for performance)
15
+ */
16
+ const colors = {
17
+ black: chalk.black,
18
+ red: chalk.red,
19
+ green: chalk.green,
20
+ yellow: chalk.yellow,
21
+ blue: chalk.blue,
22
+ magenta: chalk.magenta,
23
+ cyan: chalk.cyan,
24
+ white: chalk.white,
25
+ gray: chalk.gray,
26
+ grey: chalk.grey,
27
+ blackBright: chalk.blackBright,
28
+ redBright: chalk.redBright,
29
+ greenBright: chalk.greenBright,
30
+ yellowBright: chalk.yellowBright,
31
+ blueBright: chalk.blueBright,
32
+ magentaBright: chalk.magentaBright,
33
+ cyanBright: chalk.cyanBright,
34
+ whiteBright: chalk.whiteBright
35
+ };
36
+ /**
37
+ * Background color name to chalk function mapping (hoisted to module level for performance)
38
+ */
39
+ const bgColors = {
40
+ black: chalk.bgBlack,
41
+ red: chalk.bgRed,
42
+ green: chalk.bgGreen,
43
+ yellow: chalk.bgYellow,
44
+ blue: chalk.bgBlue,
45
+ magenta: chalk.bgMagenta,
46
+ cyan: chalk.bgCyan,
47
+ white: chalk.bgWhite,
48
+ gray: chalk.bgGray,
49
+ grey: chalk.bgGrey,
50
+ blackBright: chalk.bgBlackBright,
51
+ redBright: chalk.bgRedBright,
52
+ greenBright: chalk.bgGreenBright,
53
+ yellowBright: chalk.bgYellowBright,
54
+ blueBright: chalk.bgBlueBright,
55
+ magentaBright: chalk.bgMagentaBright,
56
+ cyanBright: chalk.bgCyanBright,
57
+ whiteBright: chalk.bgWhiteBright
58
+ };
59
+ /**
14
60
  * Get a chalk color function by name
15
61
  */
16
62
  function getChalkColor(colorName) {
17
- return {
18
- black: chalk.black,
19
- red: chalk.red,
20
- green: chalk.green,
21
- yellow: chalk.yellow,
22
- blue: chalk.blue,
23
- magenta: chalk.magenta,
24
- cyan: chalk.cyan,
25
- white: chalk.white,
26
- gray: chalk.gray,
27
- grey: chalk.grey,
28
- blackBright: chalk.blackBright,
29
- redBright: chalk.redBright,
30
- greenBright: chalk.greenBright,
31
- yellowBright: chalk.yellowBright,
32
- blueBright: chalk.blueBright,
33
- magentaBright: chalk.magentaBright,
34
- cyanBright: chalk.cyanBright,
35
- whiteBright: chalk.whiteBright
36
- }[colorName] || chalk;
63
+ return colors[colorName] || chalk;
37
64
  }
38
65
  /**
39
66
  * Get a chalk background color function by name
40
67
  */
41
68
  function getChalkBgColor(colorName) {
42
- return {
43
- black: chalk.bgBlack,
44
- red: chalk.bgRed,
45
- green: chalk.bgGreen,
46
- yellow: chalk.bgYellow,
47
- blue: chalk.bgBlue,
48
- magenta: chalk.bgMagenta,
49
- cyan: chalk.bgCyan,
50
- white: chalk.bgWhite,
51
- gray: chalk.bgGray,
52
- grey: chalk.bgGrey,
53
- blackBright: chalk.bgBlackBright,
54
- redBright: chalk.bgRedBright,
55
- greenBright: chalk.bgGreenBright,
56
- yellowBright: chalk.bgYellowBright,
57
- blueBright: chalk.bgBlueBright,
58
- magentaBright: chalk.bgMagentaBright,
59
- cyanBright: chalk.bgCyanBright,
60
- whiteBright: chalk.bgWhiteBright
61
- }[colorName] || chalk;
69
+ return bgColors[colorName] || chalk;
62
70
  }
63
71
 
64
72
  //#endregion
@@ -378,7 +386,8 @@ var TerminalRenderer = class {
378
386
  this.buffer = [];
379
387
  this.previousOutput = "";
380
388
  this.lastOutputHeight = 0;
381
- this.wasRawMode = false;
389
+ this.resizeHandler = null;
390
+ this.resizeCallback = null;
382
391
  this.root = {
383
392
  type: "root",
384
393
  stream,
@@ -393,11 +402,22 @@ var TerminalRenderer = class {
393
402
  this.root.yogaNode.setFlexDirection(Yoga.FLEX_DIRECTION_COLUMN);
394
403
  this.root.yogaNode.setAlignItems(Yoga.ALIGN_FLEX_START);
395
404
  }
396
- if (process.stdin.isTTY && process.stdin.setRawMode) {
397
- this.wasRawMode = process.stdin.isRaw || false;
398
- if (!this.wasRawMode) process.stdin.setRawMode(true);
399
- }
400
405
  this.root.stream.write(ansiEscapes.cursorHide);
406
+ this.resizeHandler = () => {
407
+ const { columns, rows } = this.root.stream;
408
+ if (this.root.yogaNode) {
409
+ this.root.yogaNode.setWidth(columns || 80);
410
+ this.root.yogaNode.setHeight(rows || 24);
411
+ }
412
+ if (this.resizeCallback) this.resizeCallback();
413
+ };
414
+ this.root.stream.on("resize", this.resizeHandler);
415
+ }
416
+ /**
417
+ * Set a callback to be called on terminal resize (triggers re-render).
418
+ */
419
+ setResizeCallback(callback) {
420
+ this.resizeCallback = callback;
401
421
  }
402
422
  /**
403
423
  * Get the root node
@@ -515,7 +535,10 @@ var TerminalRenderer = class {
515
535
  this.render();
516
536
  this.root.stream.write("\n");
517
537
  this.root.stream.write(ansiEscapes.cursorShow);
518
- if (process.stdin.isTTY && process.stdin.setRawMode && !this.wasRawMode) process.stdin.setRawMode(false);
538
+ if (this.resizeHandler) {
539
+ this.root.stream.removeListener("resize", this.resizeHandler);
540
+ this.resizeHandler = null;
541
+ }
519
542
  if (this.root.yogaNode) this.root.yogaNode.freeRecursive();
520
543
  }
521
544
  };
@@ -543,99 +566,392 @@ function setSignalProperty(node, key, signal) {
543
566
  });
544
567
  }
545
568
  /**
569
+ * Set of all recognized style property names (hoisted to module level for performance)
570
+ */
571
+ const styleProps = new Set([
572
+ "flexDirection",
573
+ "justifyContent",
574
+ "alignItems",
575
+ "flexGrow",
576
+ "flexShrink",
577
+ "flexBasis",
578
+ "width",
579
+ "height",
580
+ "minWidth",
581
+ "minHeight",
582
+ "maxWidth",
583
+ "maxHeight",
584
+ "margin",
585
+ "marginLeft",
586
+ "marginRight",
587
+ "marginTop",
588
+ "marginBottom",
589
+ "marginInline",
590
+ "marginBlock",
591
+ "padding",
592
+ "paddingLeft",
593
+ "paddingRight",
594
+ "paddingTop",
595
+ "paddingBottom",
596
+ "paddingInline",
597
+ "paddingBlock",
598
+ "border",
599
+ "borderColor",
600
+ "color",
601
+ "backgroundColor",
602
+ "bold",
603
+ "italic",
604
+ "underline",
605
+ "strikethrough",
606
+ "dim"
607
+ ]);
608
+ /**
546
609
  * Check if a property is a style property
547
610
  */
548
611
  function isStyleProperty(key) {
549
- return new Set([
550
- "flexDirection",
551
- "justifyContent",
552
- "alignItems",
553
- "flexGrow",
554
- "flexShrink",
555
- "flexBasis",
556
- "width",
557
- "height",
558
- "minWidth",
559
- "minHeight",
560
- "maxWidth",
561
- "maxHeight",
562
- "margin",
563
- "marginLeft",
564
- "marginRight",
565
- "marginTop",
566
- "marginBottom",
567
- "marginInline",
568
- "marginBlock",
569
- "padding",
570
- "paddingLeft",
571
- "paddingRight",
572
- "paddingTop",
573
- "paddingBottom",
574
- "paddingInline",
575
- "paddingBlock",
576
- "border",
577
- "borderColor",
578
- "color",
579
- "backgroundColor",
580
- "bold",
581
- "italic",
582
- "underline",
583
- "strikethrough",
584
- "dim"
585
- ]).has(key);
612
+ return styleProps.has(key);
586
613
  }
587
614
 
588
615
  //#endregion
589
- //#region ../terminal/src/components/ExitHint.tsx
590
- /** @jsxImportSource @semajsx/terminal */
616
+ //#region ../terminal/src/context.ts
591
617
  /**
592
- * Global exiting signal for terminal rendering
593
- * Set to true during unmount to hide exit hints in final render
618
+ * The active terminal session. Set by render(), cleared on unmount.
594
619
  */
595
- const globalExitingSignal = signal(false);
620
+ let activeSession = null;
596
621
  /**
597
- * Get the global exiting signal
598
- * Used internally by render() to coordinate with ExitHint component
622
+ * Create a fresh terminal session
599
623
  */
600
- function getExitingSignal() {
601
- return globalExitingSignal;
624
+ function createTerminalSession() {
625
+ return {
626
+ keyboardListeners: [],
627
+ lastKeySignal: signal(null),
628
+ keyboardInstalled: false,
629
+ stdinHandler: null,
630
+ exitCallback: null,
631
+ cleanupCallbacks: [],
632
+ exitingSignal: signal(false)
633
+ };
602
634
  }
603
635
  /**
604
- * Reset the exiting signal (useful for testing or multiple render cycles)
636
+ * Set the active terminal session
605
637
  */
606
- function resetExitingSignal() {
607
- globalExitingSignal.value = false;
638
+ function setActiveSession(session) {
639
+ activeSession = session;
608
640
  }
609
641
  /**
610
- * ExitHint component - hides its children during the final render before exit
642
+ * Get the active terminal session, or null if no render is active
643
+ */
644
+ function getActiveSession() {
645
+ return activeSession;
646
+ }
647
+
648
+ //#endregion
649
+ //#region ../terminal/src/keyboard.ts
650
+ /**
651
+ * Parse raw stdin data into a KeyEvent
652
+ */
653
+ function parseKeyEvent(data) {
654
+ const raw = data.toString();
655
+ const event = {
656
+ key: "",
657
+ ctrl: false,
658
+ shift: false,
659
+ meta: false,
660
+ raw
661
+ };
662
+ if (raw === "\x1B[A") {
663
+ event.key = "up";
664
+ return event;
665
+ }
666
+ if (raw === "\x1B[B") {
667
+ event.key = "down";
668
+ return event;
669
+ }
670
+ if (raw === "\x1B[C") {
671
+ event.key = "right";
672
+ return event;
673
+ }
674
+ if (raw === "\x1B[D") {
675
+ event.key = "left";
676
+ return event;
677
+ }
678
+ if (raw === "\x1B[H" || raw === "\x1B[1~") {
679
+ event.key = "home";
680
+ return event;
681
+ }
682
+ if (raw === "\x1B[F" || raw === "\x1B[4~") {
683
+ event.key = "end";
684
+ return event;
685
+ }
686
+ if (raw === "\x1B[5~") {
687
+ event.key = "pageup";
688
+ return event;
689
+ }
690
+ if (raw === "\x1B[6~") {
691
+ event.key = "pagedown";
692
+ return event;
693
+ }
694
+ if (raw === "\x1B[3~") {
695
+ event.key = "delete";
696
+ return event;
697
+ }
698
+ if (raw === "\x1B[2~") {
699
+ event.key = "insert";
700
+ return event;
701
+ }
702
+ if (raw.length === 2 && raw[0] === "\x1B") {
703
+ event.meta = true;
704
+ event.key = raw[1];
705
+ return event;
706
+ }
707
+ if (raw === "\x1B") {
708
+ event.key = "escape";
709
+ return event;
710
+ }
711
+ if (raw === "") {
712
+ event.key = "c";
713
+ event.ctrl = true;
714
+ return event;
715
+ }
716
+ if (raw === "\r" || raw === "\n") {
717
+ event.key = "return";
718
+ return event;
719
+ }
720
+ if (raw === " ") {
721
+ event.key = "tab";
722
+ return event;
723
+ }
724
+ if (raw === "" || raw === "\b") {
725
+ event.key = "backspace";
726
+ return event;
727
+ }
728
+ if (raw === " ") {
729
+ event.key = "space";
730
+ return event;
731
+ }
732
+ if (raw.length === 1) {
733
+ const code = raw.charCodeAt(0);
734
+ if (code >= 1 && code <= 26) {
735
+ event.ctrl = true;
736
+ event.key = String.fromCharCode(code + 96);
737
+ return event;
738
+ }
739
+ }
740
+ if (raw.length === 1) {
741
+ event.key = raw;
742
+ if (raw >= "A" && raw <= "Z") event.shift = true;
743
+ return event;
744
+ }
745
+ event.key = raw;
746
+ return event;
747
+ }
748
+ /**
749
+ * Install the keyboard handler on stdin for the active render context.
750
+ * Called automatically by render() when setting up keyboard input.
751
+ */
752
+ function installKeyboardHandler() {
753
+ const ctx = getActiveSession();
754
+ if (!ctx || ctx.keyboardInstalled) return;
755
+ ctx.keyboardInstalled = true;
756
+ ctx.stdinHandler = (data) => {
757
+ const event = parseKeyEvent(data);
758
+ ctx.lastKeySignal.value = event;
759
+ const listeners = [...ctx.keyboardListeners];
760
+ for (const listener of listeners) listener(event);
761
+ };
762
+ if (process.stdin.isTTY) process.stdin.on("data", ctx.stdinHandler);
763
+ }
764
+ /**
765
+ * Uninstall the keyboard handler for the active render context.
766
+ * Called during cleanup/unmount.
767
+ */
768
+ function uninstallKeyboardHandler() {
769
+ const ctx = getActiveSession();
770
+ if (!ctx || !ctx.keyboardInstalled) return;
771
+ ctx.keyboardInstalled = false;
772
+ if (ctx.stdinHandler) {
773
+ process.stdin.removeListener("data", ctx.stdinHandler);
774
+ ctx.stdinHandler = null;
775
+ }
776
+ ctx.keyboardListeners = [];
777
+ }
778
+ /**
779
+ * Subscribe to keyboard events with a callback.
780
+ * Returns an unsubscribe function.
611
781
  *
612
- * This is useful for hiding "Press Ctrl+C to exit" messages in the final
613
- * terminal output, keeping only the actual content visible.
782
+ * @example
783
+ * ```tsx
784
+ * const unsub = onKeypress((event) => {
785
+ * if (event.key === "up") moveUp();
786
+ * if (event.key === "down") moveDown();
787
+ * if (event.key === "return") confirm();
788
+ * });
614
789
  *
615
- * The component automatically detects when unmount() is called and reactively
616
- * hides its children during the final render using signal-based reactivity.
790
+ * // Later: unsub();
791
+ * ```
792
+ */
793
+ function onKeypress(handler) {
794
+ const ctx = getActiveSession();
795
+ if (!ctx) return () => {};
796
+ ctx.keyboardListeners.push(handler);
797
+ installKeyboardHandler();
798
+ return () => {
799
+ const idx = ctx.keyboardListeners.indexOf(handler);
800
+ if (idx !== -1) ctx.keyboardListeners.splice(idx, 1);
801
+ };
802
+ }
803
+ /**
804
+ * Get a readonly signal of the last keypress event.
805
+ * Useful for reactive UIs that need to respond to any key.
617
806
  *
618
807
  * @example
619
808
  * ```tsx
620
- * render(
621
- * <box flexDirection="column" padding={1}>
622
- * <text bold>Counter: {count}</text>
809
+ * const lastKey = useKeypress();
810
+ * // lastKey.value is null initially, then updated on each keypress
811
+ * ```
812
+ */
813
+ function useKeypress() {
814
+ const ctx = getActiveSession();
815
+ if (!ctx) return signal(null);
816
+ installKeyboardHandler();
817
+ return ctx.lastKeySignal;
818
+ }
819
+
820
+ //#endregion
821
+ //#region ../terminal/src/hooks.ts
822
+ /**
823
+ * Terminal hooks for interactive CLI applications
824
+ */
825
+ /**
826
+ * Set the exit callback on the active render context.
827
+ * Called internally by render().
828
+ */
829
+ function setExitCallback(callback) {
830
+ const ctx = getActiveSession();
831
+ if (ctx) ctx.exitCallback = callback;
832
+ }
833
+ /**
834
+ * Programmatic exit hook - allows components to trigger unmount.
623
835
  *
624
- * <ExitHint>
625
- * <text dim marginTop={1} color="yellow">
626
- * Press Ctrl+C or ESC to exit
627
- * </text>
628
- * </ExitHint>
629
- * </box>
630
- * );
836
+ * Returns a function that, when called, unmounts the terminal app
837
+ * (equivalent to pressing Ctrl+C).
838
+ *
839
+ * @example
840
+ * ```tsx
841
+ * function App() {
842
+ * const exit = useExit();
843
+ *
844
+ * onKeypress((event) => {
845
+ * if (event.key === "q") exit();
846
+ * });
847
+ *
848
+ * return <text>Press q to quit</text>;
849
+ * }
631
850
  * ```
851
+ */
852
+ function useExit() {
853
+ return () => {
854
+ const ctx = getActiveSession();
855
+ if (ctx?.exitCallback) ctx.exitCallback();
856
+ };
857
+ }
858
+ /**
859
+ * Check whether stdin raw mode is supported and/or active.
632
860
  *
633
- * Result after exit:
634
- * - The counter remains visible
635
- * - The exit hint is hidden from final output
861
+ * Useful for graceful degradation in non-TTY environments (CI, pipes).
862
+ *
863
+ * @example
864
+ * ```tsx
865
+ * const { supported, active } = isRawModeSupported();
866
+ * if (!supported) {
867
+ * print(<text>Interactive mode not available</text>);
868
+ * }
869
+ * ```
636
870
  */
637
- function ExitHint({ children }) {
638
- return when(computed(globalExitingSignal, (isExiting) => !isExiting), children);
871
+ function isRawModeSupported() {
872
+ const supported = Boolean(process.stdin.isTTY && process.stdin.setRawMode);
873
+ return {
874
+ supported,
875
+ active: supported && Boolean(process.stdin.isRaw)
876
+ };
877
+ }
878
+
879
+ //#endregion
880
+ //#region ../terminal/src/lifecycle.ts
881
+ /**
882
+ * Component lifecycle management for terminal rendering.
883
+ *
884
+ * Provides an onCleanup registry that components can use to register
885
+ * cleanup callbacks (timers, listeners, etc.) that run during unmount.
886
+ *
887
+ * Supports two modes:
888
+ * - Per-component scope (via onBeforeComponent/onAfterComponent hooks in core)
889
+ * Cleanups attached to RenderedNode.subscriptions, run when component unmounts
890
+ * - Global fallback (when called outside component render)
891
+ * Cleanups stored in TerminalSession.cleanupCallbacks, run on full unmount
892
+ */
893
+ /**
894
+ * Stack of per-component cleanup scopes.
895
+ * Pushed by onBeforeComponent(), popped by onAfterComponent().
896
+ */
897
+ const cleanupScopes = [];
898
+ /**
899
+ * Push a new per-component cleanup scope.
900
+ * Called by core's renderComponent via onBeforeComponent strategy hook.
901
+ */
902
+ function pushCleanupScope() {
903
+ cleanupScopes.push([]);
904
+ }
905
+ /**
906
+ * Pop the current cleanup scope and return collected callbacks.
907
+ * Called by core's renderComponent via onAfterComponent strategy hook.
908
+ */
909
+ function popCleanupScope() {
910
+ return cleanupScopes.pop() ?? [];
911
+ }
912
+ /**
913
+ * Register a cleanup callback that will run when the component unmounts.
914
+ *
915
+ * When called during component rendering, the callback is attached to
916
+ * the component's RenderedNode and runs when that component is unmounted
917
+ * (including via conditional rendering with when()/signal).
918
+ *
919
+ * When called outside component rendering (e.g., in render setup code),
920
+ * falls back to the global session cleanup list.
921
+ *
922
+ * @example
923
+ * ```tsx
924
+ * function Timer() {
925
+ * const elapsed = signal(0);
926
+ * const timer = setInterval(() => { elapsed.value++ }, 1000);
927
+ * onCleanup(() => clearInterval(timer));
928
+ * return <text>Elapsed: {elapsed}s</text>;
929
+ * }
930
+ * ```
931
+ */
932
+ function onCleanup(fn) {
933
+ if (cleanupScopes.length > 0) {
934
+ cleanupScopes[cleanupScopes.length - 1].push(fn);
935
+ return;
936
+ }
937
+ const ctx = getActiveSession();
938
+ if (ctx) ctx.cleanupCallbacks.push(fn);
939
+ }
940
+ /**
941
+ * Run all registered global cleanup callbacks and clear the registry.
942
+ * Called internally by render() during unmount.
943
+ *
944
+ * Note: Per-component cleanups are handled by core's unmount/cleanupSubscriptions.
945
+ * This only flushes the global fallback list.
946
+ */
947
+ function flushCleanups() {
948
+ const ctx = getActiveSession();
949
+ if (!ctx) return;
950
+ const callbacks = [...ctx.cleanupCallbacks];
951
+ ctx.cleanupCallbacks = [];
952
+ for (const fn of callbacks) try {
953
+ fn();
954
+ } catch {}
639
955
  }
640
956
 
641
957
  //#endregion
@@ -651,7 +967,9 @@ const { renderNode, cleanupSubscriptions } = createRenderer({
651
967
  removeChild,
652
968
  replaceNode,
653
969
  setProperty,
654
- setSignalProperty
970
+ setSignalProperty,
971
+ onBeforeComponent: pushCleanupScope,
972
+ onAfterComponent: popCleanupScope
655
973
  });
656
974
  /**
657
975
  * Render a VNode tree to the terminal
@@ -681,7 +999,8 @@ const { renderNode, cleanupSubscriptions } = createRenderer({
681
999
  */
682
1000
  function render(element, options = {}) {
683
1001
  const { renderer, autoRender = true, fps = 60, stream: outputStream = process.stdout } = options;
684
- resetExitingSignal();
1002
+ const session = createTerminalSession();
1003
+ setActiveSession(session);
685
1004
  const autoCreated = !renderer;
686
1005
  const actualRenderer = renderer || new TerminalRenderer(outputStream);
687
1006
  const root = actualRenderer.getRoot();
@@ -692,15 +1011,24 @@ function render(element, options = {}) {
692
1011
  }
693
1012
  actualRenderer.render();
694
1013
  let isRendering = false;
1014
+ let renderPending = false;
695
1015
  const safeRender = () => {
696
- if (isRendering) return;
1016
+ if (isRendering) {
1017
+ renderPending = true;
1018
+ return;
1019
+ }
697
1020
  isRendering = true;
698
1021
  try {
699
1022
  actualRenderer.render();
1023
+ while (renderPending) {
1024
+ renderPending = false;
1025
+ actualRenderer.render();
1026
+ }
700
1027
  } finally {
701
1028
  isRendering = false;
702
1029
  }
703
1030
  };
1031
+ actualRenderer.setResizeCallback(safeRender);
704
1032
  let renderInterval = null;
705
1033
  if (autoRender) {
706
1034
  const interval = Math.floor(1e3 / fps);
@@ -714,8 +1042,11 @@ function render(element, options = {}) {
714
1042
  });
715
1043
  const originalRawMode = process.stdin.isTTY && process.stdin.isRaw;
716
1044
  let handleExit = null;
717
- let handleKeypress = null;
1045
+ let exitKeyUnsub = null;
1046
+ let cleaned = false;
718
1047
  const cleanup = () => {
1048
+ if (cleaned) return;
1049
+ cleaned = true;
719
1050
  if (renderInterval) {
720
1051
  clearInterval(renderInterval);
721
1052
  renderInterval = null;
@@ -724,19 +1055,28 @@ function render(element, options = {}) {
724
1055
  process.removeListener("SIGINT", handleExit);
725
1056
  process.removeListener("SIGTERM", handleExit);
726
1057
  }
727
- if (handleKeypress) process.stdin.removeListener("data", handleKeypress);
1058
+ flushCleanups();
1059
+ setExitCallback(null);
1060
+ if (exitKeyUnsub) {
1061
+ exitKeyUnsub();
1062
+ exitKeyUnsub = null;
1063
+ }
1064
+ uninstallKeyboardHandler();
728
1065
  if (process.stdin.isTTY && process.stdin.setRawMode) try {
729
1066
  process.stdin.setRawMode(originalRawMode || false);
730
1067
  } catch {}
731
1068
  cleanupSubscriptions(rendered);
1069
+ actualRenderer.setResizeCallback(null);
732
1070
  actualRenderer.destroy();
1071
+ setActiveSession(null);
733
1072
  if (exitResolver) exitResolver();
734
1073
  };
735
1074
  const unmount = () => {
736
- getExitingSignal().value = true;
1075
+ session.exitingSignal.value = true;
737
1076
  actualRenderer.render();
738
1077
  cleanup();
739
1078
  };
1079
+ setExitCallback(unmount);
740
1080
  if (autoCreated) {
741
1081
  handleExit = () => {
742
1082
  unmount();
@@ -748,13 +1088,12 @@ function render(element, options = {}) {
748
1088
  if (process.stdin.isTTY) {
749
1089
  process.stdin.setRawMode(true);
750
1090
  process.stdin.resume();
751
- handleKeypress = (data) => {
752
- const key = data.toString();
753
- if (key === "" || key === "\x1B") {
1091
+ installKeyboardHandler();
1092
+ exitKeyUnsub = onKeypress((event) => {
1093
+ if (event.key === "c" && event.ctrl || event.key === "escape") {
754
1094
  if (handleExit) handleExit();
755
1095
  }
756
- };
757
- process.stdin.on("data", handleKeypress);
1096
+ });
758
1097
  }
759
1098
  } catch (err) {
760
1099
  cleanup();
@@ -819,6 +1158,41 @@ function print(element, options = {}) {
819
1158
  if (process.stdin.isTTY && process.stdin.setRawMode && wasRawMode) process.stdin.setRawMode(true);
820
1159
  }
821
1160
 
1161
+ //#endregion
1162
+ //#region ../terminal/src/components/ExitHint.tsx
1163
+ /** @jsxImportSource @semajsx/terminal */
1164
+ /**
1165
+ * ExitHint component - hides its children during the final render before exit
1166
+ *
1167
+ * This is useful for hiding "Press Ctrl+C to exit" messages in the final
1168
+ * terminal output, keeping only the actual content visible.
1169
+ *
1170
+ * The component automatically detects when unmount() is called and reactively
1171
+ * hides its children during the final render using signal-based reactivity.
1172
+ *
1173
+ * @example
1174
+ * ```tsx
1175
+ * render(
1176
+ * <box flexDirection="column" padding={1}>
1177
+ * <text bold>Counter: {count}</text>
1178
+ *
1179
+ * <ExitHint>
1180
+ * <text dim marginTop={1} color="yellow">
1181
+ * Press Ctrl+C or ESC to exit
1182
+ * </text>
1183
+ * </ExitHint>
1184
+ * </box>
1185
+ * );
1186
+ * ```
1187
+ *
1188
+ * Result after exit:
1189
+ * - The counter remains visible
1190
+ * - The exit hint is hidden from final output
1191
+ */
1192
+ function ExitHint({ children }) {
1193
+ return when(computed(getActiveSession()?.exitingSignal ?? signal(false), (isExiting) => !isExiting), children);
1194
+ }
1195
+
822
1196
  //#endregion
823
1197
  //#region ../terminal/src/components/BlankLine.tsx
824
1198
  /**
@@ -842,5 +1216,167 @@ function BlankLine({ count = 1 }) {
842
1216
  }
843
1217
 
844
1218
  //#endregion
845
- export { renderBackground as C, getChalkColor as E, setText as S, getChalkBgColor as T, getParent as _, setProperty as a, removeChild as b, renderTextElement as c, applyStyle as d, collectText as f, getNextSibling as g, createTextNode as h, ExitHint as i, renderTextNode as l, createElement as m, print as n, setSignalProperty as o, createComment as p, render as r, TerminalRenderer as s, BlankLine as t, appendChild as u, insertBefore as v, renderBorder as w, replaceNode as x, markNodeAsDirty as y };
846
- //# sourceMappingURL=src-CXY-7FC3.mjs.map
1219
+ //#region ../terminal/src/components/Spinner.tsx
1220
+ /** @jsxImportSource @semajsx/terminal */
1221
+ /**
1222
+ * Built-in spinner frame sets
1223
+ */
1224
+ const spinnerFrames = {
1225
+ dots: {
1226
+ frames: [
1227
+ "⠋",
1228
+ "⠙",
1229
+ "⠹",
1230
+ "⠸",
1231
+ "⠼",
1232
+ "⠴",
1233
+ "⠦",
1234
+ "⠧",
1235
+ "⠇",
1236
+ "⠏"
1237
+ ],
1238
+ interval: 80
1239
+ },
1240
+ line: {
1241
+ frames: [
1242
+ "-",
1243
+ "\\",
1244
+ "|",
1245
+ "/"
1246
+ ],
1247
+ interval: 130
1248
+ },
1249
+ arc: {
1250
+ frames: [
1251
+ "◜",
1252
+ "◠",
1253
+ "◝",
1254
+ "◞",
1255
+ "◡",
1256
+ "◟"
1257
+ ],
1258
+ interval: 100
1259
+ },
1260
+ bouncingBar: {
1261
+ frames: [
1262
+ "[ ]",
1263
+ "[= ]",
1264
+ "[== ]",
1265
+ "[=== ]",
1266
+ "[ ===]",
1267
+ "[ ==]",
1268
+ "[ =]",
1269
+ "[ ]"
1270
+ ],
1271
+ interval: 80
1272
+ }
1273
+ };
1274
+ /**
1275
+ * Spinner component - animated loading indicator for terminal UIs.
1276
+ *
1277
+ * @example
1278
+ * ```tsx
1279
+ * <Spinner />
1280
+ * <Spinner type="line" label="Loading..." />
1281
+ * <Spinner frames={["🌑", "🌒", "🌓", "🌔", "🌕"]} interval={150} />
1282
+ * ```
1283
+ */
1284
+ function Spinner({ type = "dots", frames: customFrames, interval: customInterval, label, color = "cyan" }) {
1285
+ const config = spinnerFrames[type];
1286
+ const frames = customFrames ?? config.frames;
1287
+ const interval = customInterval ?? config.interval;
1288
+ const frameSignal = signal(frames[0]);
1289
+ let index = 0;
1290
+ const timer = setInterval(() => {
1291
+ index = (index + 1) % frames.length;
1292
+ frameSignal.value = frames[index];
1293
+ }, interval);
1294
+ onCleanup(() => clearInterval(timer));
1295
+ if (label) return /* @__PURE__ */ jsxs("box", {
1296
+ flexDirection: "row",
1297
+ children: [/* @__PURE__ */ jsx("text", {
1298
+ color,
1299
+ children: frameSignal
1300
+ }), /* @__PURE__ */ jsxs("text", { children: [" ", label] })]
1301
+ });
1302
+ return /* @__PURE__ */ jsx("text", {
1303
+ color,
1304
+ children: frameSignal
1305
+ });
1306
+ }
1307
+
1308
+ //#endregion
1309
+ //#region ../terminal/src/components/MultiSelect.tsx
1310
+ /** @jsxImportSource @semajsx/terminal */
1311
+ /**
1312
+ * MultiSelect component - interactive multi-selection menu.
1313
+ *
1314
+ * Navigate with arrow keys, toggle with Space, confirm with Enter, cancel with Escape.
1315
+ *
1316
+ * @example
1317
+ * ```tsx
1318
+ * <MultiSelect
1319
+ * title="Select frameworks:"
1320
+ * options={[
1321
+ * { label: "React", value: "react" },
1322
+ * { label: "Vue", value: "vue" },
1323
+ * { label: "Svelte", value: "svelte" },
1324
+ * ]}
1325
+ * onConfirm={(selected) => console.log("Selected:", selected)}
1326
+ * />
1327
+ * ```
1328
+ */
1329
+ function MultiSelect({ options, onConfirm, onCancel, title, indicator = "❯", selectedIndicator = "◉", unselectedIndicator = "◯", focusColor = "cyan", selectedColor = "green" }) {
1330
+ const focusIndex = signal(0);
1331
+ const selectedSet = signal(/* @__PURE__ */ new Set());
1332
+ const unsub = onKeypress((event) => {
1333
+ if (event.key === "up") focusIndex.value = Math.max(0, focusIndex.value - 1);
1334
+ else if (event.key === "down") focusIndex.value = Math.min(options.length - 1, focusIndex.value + 1);
1335
+ else if (event.key === "space") {
1336
+ const current = new Set(selectedSet.value);
1337
+ const value = options[focusIndex.value].value;
1338
+ if (current.has(value)) current.delete(value);
1339
+ else current.add(value);
1340
+ selectedSet.value = current;
1341
+ } else if (event.key === "return") {
1342
+ unsub();
1343
+ onConfirm(Array.from(selectedSet.value));
1344
+ } else if (event.key === "escape" && onCancel) {
1345
+ unsub();
1346
+ onCancel();
1347
+ }
1348
+ });
1349
+ onCleanup(unsub);
1350
+ const items = options.map((option, i) => {
1351
+ const isFocused = computed(focusIndex, (idx) => idx === i);
1352
+ const isSelected = computed(selectedSet, (set) => set.has(option.value));
1353
+ const prefix = computed(isSelected, (sel) => sel ? selectedIndicator : unselectedIndicator);
1354
+ const line = computed(isFocused, (focused) => focused ? `${indicator} ${prefix.value} ${option.label}` : ` ${prefix.value} ${option.label}`);
1355
+ return /* @__PURE__ */ jsx("text", {
1356
+ color: computed(isFocused, (focused) => {
1357
+ if (focused) return focusColor;
1358
+ if (isSelected.value) return selectedColor;
1359
+ }),
1360
+ children: line
1361
+ }, option.value);
1362
+ });
1363
+ return /* @__PURE__ */ jsxs("box", {
1364
+ flexDirection: "column",
1365
+ children: [
1366
+ title ? /* @__PURE__ */ jsx("text", {
1367
+ bold: true,
1368
+ children: title
1369
+ }) : null,
1370
+ items,
1371
+ /* @__PURE__ */ jsx("text", {
1372
+ dim: true,
1373
+ marginTop: 1,
1374
+ children: "↑/↓ navigate · space toggle · enter confirm"
1375
+ })
1376
+ ]
1377
+ });
1378
+ }
1379
+
1380
+ //#endregion
1381
+ export { replaceNode as A, createElement as C, insertBefore as D, getParent as E, getChalkColor as F, renderBackground as M, renderBorder as N, markNodeAsDirty as O, getChalkBgColor as P, createComment as S, getNextSibling as T, renderTextElement as _, ExitHint as a, applyStyle as b, onCleanup as c, onKeypress as d, parseKeyEvent as f, TerminalRenderer as g, setSignalProperty as h, BlankLine as i, setText as j, removeChild as k, isRawModeSupported as l, setProperty as m, Spinner as n, print as o, useKeypress as p, spinnerFrames as r, render as s, MultiSelect as t, useExit as u, renderTextNode as v, createTextNode as w, collectText as x, appendChild as y };
1382
+ //# sourceMappingURL=src-77V1Plyd.mjs.map