silvery 0.3.0 → 0.4.1

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 (120) hide show
  1. package/README.md +41 -145
  2. package/dist/chalk.js +3 -0
  3. package/dist/chalk.js.map +11 -0
  4. package/dist/index.js +340 -0
  5. package/dist/index.js.map +282 -0
  6. package/dist/ink.js +129 -0
  7. package/dist/ink.js.map +140 -0
  8. package/dist/runtime.js +394 -0
  9. package/dist/runtime.js.map +286 -0
  10. package/dist/theme.js +343 -0
  11. package/dist/theme.js.map +286 -0
  12. package/dist/ui/animation.js +3 -0
  13. package/dist/ui/animation.js.map +15 -0
  14. package/dist/ui/ansi.js +3 -0
  15. package/dist/ui/ansi.js.map +10 -0
  16. package/dist/ui/cli.js +8 -0
  17. package/dist/ui/cli.js.map +14 -0
  18. package/dist/ui/display.js +4 -0
  19. package/dist/ui/display.js.map +10 -0
  20. package/dist/ui/image.js +4 -0
  21. package/dist/ui/image.js.map +15 -0
  22. package/dist/ui/input.js +3 -0
  23. package/dist/ui/input.js.map +11 -0
  24. package/dist/ui/progress.js +8 -0
  25. package/dist/ui/progress.js.map +20 -0
  26. package/dist/ui/react.js +3 -0
  27. package/dist/ui/react.js.map +15 -0
  28. package/dist/ui/utils.js +3 -0
  29. package/dist/ui/utils.js.map +10 -0
  30. package/dist/ui/wrappers.js +14 -0
  31. package/dist/ui/wrappers.js.map +19 -0
  32. package/dist/ui.js +17 -0
  33. package/dist/ui.js.map +20 -0
  34. package/package.json +67 -15
  35. package/src/index.ts +67 -1
  36. package/src/runtime.ts +4 -0
  37. package/src/theme.ts +4 -0
  38. package/src/ui/animation.ts +2 -0
  39. package/src/ui/ansi.ts +2 -0
  40. package/src/ui/cli.ts +2 -0
  41. package/src/ui/display.ts +2 -0
  42. package/src/ui/image.ts +2 -0
  43. package/src/ui/input.ts +2 -0
  44. package/src/ui/progress.ts +2 -0
  45. package/src/ui/react.ts +2 -0
  46. package/src/ui/utils.ts +2 -0
  47. package/src/ui/wrappers.ts +2 -0
  48. package/src/ui.ts +4 -0
  49. package/examples/CLAUDE.md +0 -75
  50. package/examples/_banner.tsx +0 -60
  51. package/examples/cli.ts +0 -228
  52. package/examples/index.md +0 -101
  53. package/examples/inline/inline-nontty.tsx +0 -98
  54. package/examples/inline/inline-progress.tsx +0 -79
  55. package/examples/inline/inline-simple.tsx +0 -63
  56. package/examples/inline/scrollback.tsx +0 -185
  57. package/examples/interactive/_input-debug.tsx +0 -110
  58. package/examples/interactive/_stdin-test.ts +0 -71
  59. package/examples/interactive/_textarea-bare.tsx +0 -45
  60. package/examples/interactive/aichat/components.tsx +0 -468
  61. package/examples/interactive/aichat/index.tsx +0 -207
  62. package/examples/interactive/aichat/script.ts +0 -460
  63. package/examples/interactive/aichat/state.ts +0 -326
  64. package/examples/interactive/aichat/types.ts +0 -19
  65. package/examples/interactive/app-todo.tsx +0 -198
  66. package/examples/interactive/async-data.tsx +0 -208
  67. package/examples/interactive/cli-wizard.tsx +0 -332
  68. package/examples/interactive/clipboard.tsx +0 -183
  69. package/examples/interactive/components.tsx +0 -463
  70. package/examples/interactive/data-explorer.tsx +0 -506
  71. package/examples/interactive/dev-tools.tsx +0 -379
  72. package/examples/interactive/explorer.tsx +0 -747
  73. package/examples/interactive/gallery.tsx +0 -652
  74. package/examples/interactive/inline-bench.tsx +0 -136
  75. package/examples/interactive/kanban.tsx +0 -267
  76. package/examples/interactive/layout-ref.tsx +0 -185
  77. package/examples/interactive/outline.tsx +0 -171
  78. package/examples/interactive/paste-demo.tsx +0 -198
  79. package/examples/interactive/scroll.tsx +0 -77
  80. package/examples/interactive/search-filter.tsx +0 -240
  81. package/examples/interactive/task-list.tsx +0 -279
  82. package/examples/interactive/terminal.tsx +0 -798
  83. package/examples/interactive/textarea.tsx +0 -103
  84. package/examples/interactive/theme.tsx +0 -336
  85. package/examples/interactive/transform.tsx +0 -256
  86. package/examples/interactive/virtual-10k.tsx +0 -413
  87. package/examples/kitty/canvas.tsx +0 -519
  88. package/examples/kitty/generate-samples.ts +0 -236
  89. package/examples/kitty/image-component.tsx +0 -273
  90. package/examples/kitty/images.tsx +0 -604
  91. package/examples/kitty/input.tsx +0 -371
  92. package/examples/kitty/keys.tsx +0 -378
  93. package/examples/kitty/paint.tsx +0 -1017
  94. package/examples/layout/dashboard.tsx +0 -551
  95. package/examples/layout/live-resize.tsx +0 -290
  96. package/examples/layout/overflow.tsx +0 -51
  97. package/examples/playground/README.md +0 -69
  98. package/examples/playground/build.ts +0 -61
  99. package/examples/playground/index.html +0 -420
  100. package/examples/playground/playground-app.tsx +0 -416
  101. package/examples/runtime/elm-counter.tsx +0 -206
  102. package/examples/runtime/hello-runtime.tsx +0 -73
  103. package/examples/runtime/pipe-composition.tsx +0 -184
  104. package/examples/runtime/run-counter.tsx +0 -78
  105. package/examples/runtime/runtime-counter.tsx +0 -197
  106. package/examples/screenshots/generate.tsx +0 -563
  107. package/examples/scrollback-perf.tsx +0 -230
  108. package/examples/viewer.tsx +0 -654
  109. package/examples/web/build.ts +0 -365
  110. package/examples/web/canvas-app.tsx +0 -80
  111. package/examples/web/canvas.html +0 -89
  112. package/examples/web/dom-app.tsx +0 -81
  113. package/examples/web/dom.html +0 -113
  114. package/examples/web/showcase-app.tsx +0 -107
  115. package/examples/web/showcase.html +0 -34
  116. package/examples/web/showcases/index.tsx +0 -56
  117. package/examples/web/viewer-app.tsx +0 -555
  118. package/examples/web/viewer.html +0 -30
  119. package/examples/web/xterm-app.tsx +0 -105
  120. package/examples/web/xterm.html +0 -118
@@ -0,0 +1,3 @@
1
+ var easings={linear:(t)=>t,ease:(t)=>t<0.5?2*t*t:-1+(4-2*t)*t,easeIn:(t)=>t*t,easeOut:(t)=>t*(2-t),easeInOut:(t)=>t<0.5?2*t*t:-1+(4-2*t)*t,easeInCubic:(t)=>t*t*t,easeOutCubic:(t)=>--t*t*t+1};function resolveEasing(easing){return typeof easing==="function"?easing:easings[easing]}import{useState,useEffect,useRef,useCallback}from"react";var TICK_MS=33;function useAnimation(options){let{duration,easing="linear",delay=0,onComplete,enabled=!0}=options,[value,setValue]=useState(0),[isAnimating,setIsAnimating]=useState(!1),startTimeRef=useRef(0),intervalRef=useRef(null),onCompleteRef=useRef(onComplete);onCompleteRef.current=onComplete;let epochRef=useRef(0),easingFn=resolveEasing(easing),stopInterval=useCallback(()=>{if(intervalRef.current!==null)clearInterval(intervalRef.current),intervalRef.current=null},[]),startAnimation=useCallback(()=>{stopInterval(),epochRef.current++;let epoch=epochRef.current;setValue(0),setIsAnimating(!0);let begin=()=>{if(epochRef.current!==epoch)return;startTimeRef.current=performance.now(),intervalRef.current=setInterval(()=>{if(epochRef.current!==epoch)return;let elapsed=performance.now()-startTimeRef.current,raw=Math.min(elapsed/duration,1),eased=easingFn(raw);if(setValue(eased),raw>=1)stopInterval(),setIsAnimating(!1),onCompleteRef.current?.()},TICK_MS)};if(delay>0)setTimeout(()=>begin(),delay);else begin()},[duration,delay,easingFn,stopInterval]);useEffect(()=>{if(!enabled){stopInterval(),setValue(0),setIsAnimating(!1);return}return startAnimation(),()=>{stopInterval()}},[enabled,startAnimation,stopInterval]);let reset=useCallback(()=>{startAnimation()},[startAnimation]);return{value,isAnimating,reset}}import{useState as useState2,useEffect as useEffect2,useRef as useRef2}from"react";var TICK_MS2=33;function useTransition(targetValue,options){let{duration=300,easing="easeOut"}=options??{},[currentValue,setCurrentValue]=useState2(targetValue),fromRef=useRef2(targetValue),toRef=useRef2(targetValue),startTimeRef=useRef2(0),intervalRef=useRef2(null),isFirstRef=useRef2(!0),easingFn=resolveEasing(easing);return useEffect2(()=>{if(isFirstRef.current){isFirstRef.current=!1;return}if(targetValue===toRef.current)return;if(fromRef.current=currentValue,toRef.current=targetValue,startTimeRef.current=performance.now(),intervalRef.current!==null)clearInterval(intervalRef.current);return intervalRef.current=setInterval(()=>{let elapsed=performance.now()-startTimeRef.current,raw=Math.min(elapsed/duration,1),eased=easingFn(raw),interpolated=fromRef.current+(toRef.current-fromRef.current)*eased;if(setCurrentValue(interpolated),raw>=1){if(setCurrentValue(toRef.current),intervalRef.current!==null)clearInterval(intervalRef.current),intervalRef.current=null}},TICK_MS2),()=>{if(intervalRef.current!==null)clearInterval(intervalRef.current),intervalRef.current=null}},[targetValue,duration,easingFn]),currentValue}import{useEffect as useEffect3,useRef as useRef3}from"react";function useInterval(callback,ms,enabled=!0){let callbackRef=useRef3(callback);callbackRef.current=callback,useEffect3(()=>{if(!enabled)return;let id=setInterval(()=>{callbackRef.current()},ms);return()=>{clearInterval(id)}},[ms,enabled])}import{useCallback as useCallback2,useEffect as useEffect4,useRef as useRef4}from"react";function useTimeout(callback,ms,enabled=!0){let callbackRef=useRef4(callback);callbackRef.current=callback;let timerRef=useRef4(null),clear=useCallback2(()=>{if(timerRef.current!==null)clearTimeout(timerRef.current),timerRef.current=null},[]),reset=useCallback2(()=>{if(clear(),enabled)timerRef.current=setTimeout(()=>{timerRef.current=null,callbackRef.current()},ms)},[ms,enabled,clear]);return useEffect4(()=>{if(!enabled){clear();return}return timerRef.current=setTimeout(()=>{timerRef.current=null,callbackRef.current()},ms),clear},[ms,enabled,clear]),{reset,clear}}import{useRef as useRef5}from"react";function useLatest(value){let ref=useRef5(value);return ref.current=value,ref}export{useTimeout,useLatest,useInterval,useAnimation,useTransition as useAnimatedTransition,resolveEasing,easings};
2
+
3
+ //# debugId=AF8A4AC26C55CDD264756E2164756E21
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../packages/ag-react/src/ui/animation/easing.ts", "../../packages/ag-react/src/ui/animation/useAnimation.ts", "../../packages/ag-react/src/ui/animation/useTransition.ts", "../../packages/ag-react/src/ui/animation/useInterval.ts", "../../packages/ag-react/src/ui/animation/useTimeout.ts", "../../packages/ag-react/src/ui/animation/useLatest.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Easing Functions\n *\n * Maps time progress (0-1) to value progress (0-1) for smooth animations.\n * Includes common presets and a resolver for name-or-function usage.\n */\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/** Easing function: maps time progress (0-1) to value progress (0-1) */\nexport type EasingFn = (t: number) => number\n\n// ============================================================================\n// Presets\n// ============================================================================\n\nexport const easings = {\n linear: (t: number) => t,\n ease: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),\n easeIn: (t: number) => t * t,\n easeOut: (t: number) => t * (2 - t),\n easeInOut: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),\n easeInCubic: (t: number) => t * t * t,\n easeOutCubic: (t: number) => --t * t * t + 1,\n} as const satisfies Record<string, EasingFn>\n\nexport type EasingName = keyof typeof easings\n\n// ============================================================================\n// Resolver\n// ============================================================================\n\n/** Resolve an easing — accepts a name string or a custom function. */\nexport function resolveEasing(easing: EasingName | EasingFn): EasingFn {\n return typeof easing === \"function\" ? easing : easings[easing]\n}\n",
6
+ "/**\n * useAnimation - Animate a value from 0 to 1 over a duration.\n *\n * Drives a single animation cycle with configurable easing, delay,\n * and completion callback. Targets ~30fps (33ms interval) since\n * terminals don't benefit from higher refresh rates.\n */\n\nimport { useState, useEffect, useRef, useCallback } from \"react\"\nimport { resolveEasing, type EasingName, type EasingFn } from \"./easing\"\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface UseAnimationOptions {\n /** Duration in milliseconds */\n duration: number\n /** Easing function or preset name */\n easing?: EasingName | EasingFn\n /** Delay before starting (ms) */\n delay?: number\n /** Called when animation completes */\n onComplete?: () => void\n /** Whether to run the animation (default: true) */\n enabled?: boolean\n}\n\nexport interface UseAnimationResult {\n /** Current progress value (0 to 1, eased) */\n value: number\n /** Whether the animation is still running */\n isAnimating: boolean\n /** Reset and replay the animation */\n reset: () => void\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** ~30fps tick interval for terminal animations */\nconst TICK_MS = 33\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/**\n * Animate a value from 0 to 1 over a duration with easing.\n *\n * @example\n * ```tsx\n * function FadeIn({ children }) {\n * const { value } = useAnimation({ duration: 300, easing: \"easeOut\" })\n * return <Text dimColor={value < 1}>{children}</Text>\n * }\n * ```\n */\nexport function useAnimation(options: UseAnimationOptions): UseAnimationResult {\n const { duration, easing = \"linear\", delay = 0, onComplete, enabled = true } = options\n\n const [value, setValue] = useState(0)\n const [isAnimating, setIsAnimating] = useState(false)\n\n const startTimeRef = useRef(0)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const onCompleteRef = useRef(onComplete)\n onCompleteRef.current = onComplete\n\n // Epoch bumps on each reset to invalidate stale intervals\n const epochRef = useRef(0)\n\n const easingFn = resolveEasing(easing)\n\n const stopInterval = useCallback(() => {\n if (intervalRef.current !== null) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }, [])\n\n const startAnimation = useCallback(() => {\n stopInterval()\n epochRef.current++\n const epoch = epochRef.current\n\n setValue(0)\n setIsAnimating(true)\n\n const begin = () => {\n // Guard against stale starts after a reset\n if (epochRef.current !== epoch) return\n\n startTimeRef.current = performance.now()\n\n intervalRef.current = setInterval(() => {\n // Guard against stale ticks after a reset\n if (epochRef.current !== epoch) return\n\n const elapsed = performance.now() - startTimeRef.current\n const raw = Math.min(elapsed / duration, 1)\n const eased = easingFn(raw)\n\n setValue(eased)\n\n if (raw >= 1) {\n stopInterval()\n setIsAnimating(false)\n onCompleteRef.current?.()\n }\n }, TICK_MS)\n }\n\n if (delay > 0) {\n setTimeout(() => begin(), delay)\n } else {\n begin()\n }\n }, [duration, delay, easingFn, stopInterval])\n\n // Start on mount (if enabled)\n useEffect(() => {\n if (!enabled) {\n stopInterval()\n setValue(0)\n setIsAnimating(false)\n return\n }\n\n startAnimation()\n\n return () => {\n stopInterval()\n }\n }, [enabled, startAnimation, stopInterval])\n\n const reset = useCallback(() => {\n startAnimation()\n }, [startAnimation])\n\n return { value, isAnimating, reset }\n}\n",
7
+ "/**\n * useTransition - Smoothly interpolate between numeric values.\n *\n * When the target value changes, animates from the current value toward\n * the new target. If the target changes mid-animation, restarts from\n * the current interpolated position. Targets ~30fps.\n */\n\nimport { useState, useEffect, useRef } from \"react\"\nimport { resolveEasing, type EasingName, type EasingFn } from \"./easing\"\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface UseTransitionOptions {\n /** Duration in milliseconds (default: 300) */\n duration?: number\n /** Easing function or preset name (default: \"easeOut\") */\n easing?: EasingName | EasingFn\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** ~30fps tick interval for terminal animations */\nconst TICK_MS = 33\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/**\n * Smoothly interpolate when the target value changes.\n *\n * Returns the current interpolated value. On the first render, returns\n * the target value immediately (no animation). Subsequent changes\n * animate from the previous value to the new target.\n *\n * @example\n * ```tsx\n * function ScrollOffset({ target }) {\n * const smooth = useTransition(target, { duration: 200, easing: \"easeOut\" })\n * return <Box marginTop={Math.round(smooth)}>...</Box>\n * }\n * ```\n */\nexport function useTransition(targetValue: number, options?: UseTransitionOptions): number {\n const { duration = 300, easing = \"easeOut\" } = options ?? {}\n\n const [currentValue, setCurrentValue] = useState(targetValue)\n\n const fromRef = useRef(targetValue)\n const toRef = useRef(targetValue)\n const startTimeRef = useRef(0)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const isFirstRef = useRef(true)\n\n const easingFn = resolveEasing(easing)\n\n useEffect(() => {\n // On first render, snap to target without animation\n if (isFirstRef.current) {\n isFirstRef.current = false\n return\n }\n\n // If target hasn't changed, nothing to do\n if (targetValue === toRef.current) return\n\n // Start from wherever we currently are\n fromRef.current = currentValue\n toRef.current = targetValue\n startTimeRef.current = performance.now()\n\n // Clear any existing interval\n if (intervalRef.current !== null) {\n clearInterval(intervalRef.current)\n }\n\n intervalRef.current = setInterval(() => {\n const elapsed = performance.now() - startTimeRef.current\n const raw = Math.min(elapsed / duration, 1)\n const eased = easingFn(raw)\n const interpolated = fromRef.current + (toRef.current - fromRef.current) * eased\n\n setCurrentValue(interpolated)\n\n if (raw >= 1) {\n // Snap to exact target and stop\n setCurrentValue(toRef.current)\n if (intervalRef.current !== null) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }\n }, TICK_MS)\n\n return () => {\n if (intervalRef.current !== null) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [targetValue, duration, easingFn])\n\n return currentValue\n}\n",
8
+ "/**\n * useInterval - Run a callback on a fixed interval.\n *\n * Uses Dan Abramov's ref pattern to avoid stale closures.\n * The callback is NOT called on mount — only on subsequent ticks.\n */\n\nimport { useEffect, useRef } from \"react\"\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/**\n * Run a callback on a fixed interval.\n *\n * The callback is NOT called on mount — only on ticks after the interval\n * elapses. Uses a ref for the callback to avoid stale closures.\n *\n * @param callback - Function to call on each tick\n * @param ms - Interval in milliseconds\n * @param enabled - Whether the interval is active (default: true)\n */\nexport function useInterval(callback: () => void, ms: number, enabled = true): void {\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useEffect(() => {\n if (!enabled) return\n\n const id = setInterval(() => {\n callbackRef.current()\n }, ms)\n\n return () => {\n clearInterval(id)\n }\n }, [ms, enabled])\n}\n",
9
+ "/**\n * useTimeout - Run a callback after a delay.\n *\n * Uses a ref for the callback to avoid stale closures (Dan Abramov pattern).\n * The timer resets when `ms` or `enabled` changes. When `enabled` becomes false,\n * the timer is cleared. Returns a `reset` function to restart the timer.\n *\n * Unlike useInterval, this fires exactly once per enable/reset cycle.\n */\n\nimport { useCallback, useEffect, useRef } from \"react\"\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/**\n * Run a callback after a delay.\n *\n * The callback fires once after `ms` milliseconds. The timer resets when\n * `ms` or `enabled` changes. Returns `{ reset, clear }` for manual control.\n *\n * @param callback - Function to call when the timer fires\n * @param ms - Delay in milliseconds\n * @param enabled - Whether the timer is active (default: true)\n */\nexport function useTimeout(callback: () => void, ms: number, enabled = true): { reset: () => void; clear: () => void } {\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n const clear = useCallback(() => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current)\n timerRef.current = null\n }\n }, [])\n\n const reset = useCallback(() => {\n clear()\n if (enabled) {\n timerRef.current = setTimeout(() => {\n timerRef.current = null\n callbackRef.current()\n }, ms)\n }\n }, [ms, enabled, clear])\n\n useEffect(() => {\n if (!enabled) {\n clear()\n return\n }\n\n timerRef.current = setTimeout(() => {\n timerRef.current = null\n callbackRef.current()\n }, ms)\n\n return clear\n }, [ms, enabled, clear])\n\n return { reset, clear }\n}\n",
10
+ "/**\n * useLatest - Always-current ref to a value.\n *\n * The classic React pattern for avoiding stale closures in callbacks,\n * timers, and effects. Returns a ref whose `.current` is always the\n * latest value — safe to read from any async context.\n *\n * ```tsx\n * const countRef = useLatest(count)\n * useInterval(() => {\n * console.log(countRef.current) // always fresh\n * }, 1000)\n * ```\n */\n\nimport { useRef } from \"react\"\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/**\n * Returns a ref that always holds the latest value.\n *\n * Useful when a callback needs access to current state/props without\n * re-creating the callback (which would reset timers, event listeners, etc).\n *\n * @param value - The value to track\n * @returns A ref whose `.current` is always `value`\n */\nexport function useLatest<T>(value: T): { readonly current: T } {\n const ref = useRef(value)\n ref.current = value\n return ref\n}\n"
11
+ ],
12
+ "mappings": "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",
13
+ "debugId": "AF8A4AC26C55CDD264756E2164756E21",
14
+ "names": []
15
+ }
@@ -0,0 +1,3 @@
1
+ var CURSOR_HIDE="\x1B[?25l",CURSOR_SHOW="\x1B[?25h",CURSOR_TO_START="\r",CLEAR_LINE_END="\x1B[K",CLEAR_LINE="\x1B[2K",CLEAR_SCREEN="\x1B[2J\x1B[H",cursorUp=(n=1)=>`\x1B[${n}A`,cursorDown=(n=1)=>`\x1B[${n}B`,CURSOR_SAVE="\x1B[s",CURSOR_RESTORE="\x1B[u";function write(text,stream=process.stdout){stream.write(text)}function writeLine(text,stream=process.stdout){stream.write(`\r${text}\x1B[K`)}function withCursor(fn,stream=process.stdout){stream.write("\x1B[?25l");let restore=()=>stream.write("\x1B[?25h");try{let result=fn();if(result instanceof Promise)return result.finally(restore);return restore(),Promise.resolve(result)}catch(error){throw restore(),error}}function isTTY(stream=process.stdout){if(process.env.FORCE_TTY==="1")return!0;return stream.isTTY??!1}function getTerminalWidth(stream=process.stdout){return stream.columns??80}export{writeLine,write,withCursor,isTTY,getTerminalWidth,cursorUp,cursorDown,CURSOR_TO_START,CURSOR_SHOW,CURSOR_SAVE,CURSOR_RESTORE,CURSOR_HIDE,CLEAR_SCREEN,CLEAR_LINE_END,CLEAR_LINE};
2
+
3
+ //# debugId=BAF68F797D13ADDF64756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../packages/ag-react/src/ui/cli/ansi.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * ANSI escape code utilities for terminal control\n */\n\n/** Hide the cursor */\nexport const CURSOR_HIDE = \"\\x1b[?25l\"\n\n/** Show the cursor */\nexport const CURSOR_SHOW = \"\\x1b[?25h\"\n\n/** Move cursor to beginning of line */\nexport const CURSOR_TO_START = \"\\r\"\n\n/** Clear from cursor to end of line */\nexport const CLEAR_LINE_END = \"\\x1b[K\"\n\n/** Clear entire line */\nexport const CLEAR_LINE = \"\\x1b[2K\"\n\n/** Clear screen and move to top-left */\nexport const CLEAR_SCREEN = \"\\x1b[2J\\x1b[H\"\n\n/** Move cursor up N lines */\nexport const cursorUp = (n: number = 1): string => `\\x1b[${n}A`\n\n/** Move cursor down N lines */\nexport const cursorDown = (n: number = 1): string => `\\x1b[${n}B`\n\n/** Save cursor position */\nexport const CURSOR_SAVE = \"\\x1b[s\"\n\n/** Restore cursor position */\nexport const CURSOR_RESTORE = \"\\x1b[u\"\n\n/**\n * Write to stream with proper handling\n */\nexport function write(text: string, stream: NodeJS.WriteStream = process.stdout): void {\n stream.write(text)\n}\n\n/**\n * Clear the current line and write new text\n */\nexport function writeLine(text: string, stream: NodeJS.WriteStream = process.stdout): void {\n stream.write(`${CURSOR_TO_START}${text}${CLEAR_LINE_END}`)\n}\n\n/**\n * Wrap a function to handle cursor visibility\n * Hides cursor on start, shows on completion/error\n */\nexport function withCursor<T>(fn: () => T | Promise<T>, stream: NodeJS.WriteStream = process.stdout): Promise<T> {\n stream.write(CURSOR_HIDE)\n\n const restore = () => stream.write(CURSOR_SHOW)\n\n try {\n const result = fn()\n if (result instanceof Promise) {\n return result.finally(restore)\n }\n restore()\n return Promise.resolve(result)\n } catch (error) {\n restore()\n throw error\n }\n}\n\n/**\n * Check if stream is a TTY (supports ANSI codes)\n * Also respects FORCE_TTY environment variable for testing\n */\nexport function isTTY(stream: NodeJS.WriteStream = process.stdout): boolean {\n if (process.env.FORCE_TTY === \"1\") return true\n return stream.isTTY ?? false\n}\n\n/**\n * Get terminal width\n */\nexport function getTerminalWidth(stream: NodeJS.WriteStream = process.stdout): number {\n return stream.columns ?? 80\n}\n"
6
+ ],
7
+ "mappings": "AAKO,IAAM,YAAc,YAGd,YAAc,YAGd,gBAAkB,KAGlB,eAAiB,SAGjB,WAAa,UAGb,aAAe,gBAGf,SAAW,CAAC,EAAY,IAAc,QAAQ,KAG9C,WAAa,CAAC,EAAY,IAAc,QAAQ,KAGhD,YAAc,SAGd,eAAiB,SAKvB,SAAS,KAAK,CAAC,KAAc,OAA6B,QAAQ,OAAc,CACrF,OAAO,MAAM,IAAI,EAMZ,SAAS,SAAS,CAAC,KAAc,OAA6B,QAAQ,OAAc,CACzF,OAAO,MAAM,KAAqB,YAAuB,EAOpD,SAAS,UAAa,CAAC,GAA0B,OAA6B,QAAQ,OAAoB,CAC/G,OAAO,MAhDkB,WAgDD,EAExB,IAAM,QAAU,IAAM,OAAO,MA/CJ,WA+CqB,EAE9C,GAAI,CACF,IAAM,OAAS,GAAG,EAClB,GAAI,kBAAkB,QACpB,OAAO,OAAO,QAAQ,OAAO,EAG/B,OADA,QAAQ,EACD,QAAQ,QAAQ,MAAM,EAC7B,MAAO,MAAO,CAEd,MADA,QAAQ,EACF,OAQH,SAAS,KAAK,CAAC,OAA6B,QAAQ,OAAiB,CAC1E,GAAI,QAAQ,IAAI,YAAc,IAAK,MAAO,GAC1C,OAAO,OAAO,OAAS,GAMlB,SAAS,gBAAgB,CAAC,OAA6B,QAAQ,OAAgB,CACpF,OAAO,OAAO,SAAW",
8
+ "debugId": "BAF68F797D13ADDF64756E2164756E21",
9
+ "names": []
10
+ }
package/dist/ui/cli.js ADDED
@@ -0,0 +1,8 @@
1
+ import chalk from"chalk";var CURSOR_HIDE="\x1B[?25l",CURSOR_SHOW="\x1B[?25h",CURSOR_TO_START="\r",CLEAR_LINE_END="\x1B[K",CLEAR_LINE="\x1B[2K",CLEAR_SCREEN="\x1B[2J\x1B[H",cursorUp=(n=1)=>`\x1B[${n}A`,cursorDown=(n=1)=>`\x1B[${n}B`,CURSOR_SAVE="\x1B[s",CURSOR_RESTORE="\x1B[u";function write(text,stream=process.stdout){stream.write(text)}function writeLine(text,stream=process.stdout){stream.write(`\r${text}\x1B[K`)}function withCursor(fn,stream=process.stdout){stream.write("\x1B[?25l");let restore=()=>stream.write("\x1B[?25h");try{let result=fn();if(result instanceof Promise)return result.finally(restore);return restore(),Promise.resolve(result)}catch(error){throw restore(),error}}function isTTY(stream=process.stdout){if(process.env.FORCE_TTY==="1")return!0;return stream.isTTY??!1}function getTerminalWidth(stream=process.stdout){return stream.columns??80}var SPINNER_FRAMES={dots:["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"],line:["-","\\","|","/"],arc:["◜","◠","◝","◞","◡","◟"],bounce:["⠁","⠂","⠄","⠂"],pulse:["█","▓","▒","░","▒","▓"]},SPINNER_INTERVALS={dots:80,line:120,arc:100,bounce:120,pulse:100};class Spinner{text;style;color;stream;hideCursor;interval;frameIndex=0;timer=null;isSpinning=!1;constructor(textOrOptions){let options=typeof textOrOptions==="string"?{text:textOrOptions}:textOrOptions??{};this.text=options.text??"",this.style=options.style??"dots",this.color=options.color??"cyan",this.stream=options.stream??process.stdout,this.hideCursor=options.hideCursor??!0,this.interval=options.interval??SPINNER_INTERVALS[this.style]}get currentText(){return this.text}set currentText(value){if(this.text=value,this.isSpinning)this.render()}get spinning(){return this.isSpinning}start(text){if(text!==void 0)this.text=text;if(this.isSpinning)return this;if(this.isSpinning=!0,this.frameIndex=0,this.hideCursor&&isTTY(this.stream))write(CURSOR_HIDE,this.stream);return this.render(),this.timer=setInterval(()=>{this.frameIndex=(this.frameIndex+1)%SPINNER_FRAMES[this.style].length,this.render()},this.interval),this}stop(){if(!this.isSpinning)return this;if(this.isSpinning=!1,this.timer)clearInterval(this.timer),this.timer=null;if(this.clear(),this.hideCursor&&isTTY(this.stream))write(CURSOR_SHOW,this.stream);return this}succeed(text){return this.stopWithSymbol(chalk.green("✔"),text??this.text)}fail(text){return this.stopWithSymbol(chalk.red("✖"),text??this.text)}warn(text){return this.stopWithSymbol(chalk.yellow("⚠"),text??this.text)}info(text){return this.stopWithSymbol(chalk.blue("ℹ"),text??this.text)}clear(){if(isTTY(this.stream))write(`${CURSOR_TO_START}${CLEAR_LINE_END}`,this.stream);return this}render(){let frame=SPINNER_FRAMES[this.style][this.frameIndex],colorFn=chalk[this.color],coloredFrame=colorFn?colorFn(frame):frame,output=this.text?`${coloredFrame} ${this.text}`:coloredFrame;if(isTTY(this.stream))write(`${CURSOR_TO_START}${output}${CLEAR_LINE_END}`,this.stream)}stopWithSymbol(symbol,text){return this.stop(),write(`${symbol} ${text}
2
+ `,this.stream),this}[Symbol.dispose](){this.stop()}static start(textOrOptions){let spinner=new Spinner(textOrOptions);return spinner.start(),()=>spinner.stop()}}function createSpinner(options){let spinner=new Spinner({...options,text:""}),callable=(text)=>{if(!spinner.spinning)spinner.start(text);else spinner.currentText=text};return callable.stop=()=>spinner.stop(),callable.succeed=(text)=>spinner.succeed(text),callable.fail=(text)=>spinner.fail(text),callable.warn=(text)=>spinner.warn(text),callable.info=(text)=>spinner.info(text),callable[Symbol.dispose]=()=>spinner.stop(),callable}import chalk2 from"chalk";function calculateETA(buffer,current,total){if(buffer.length<2)return null;let first=buffer[0],last=buffer[buffer.length-1],elapsed=(last.time-first.time)/1000,progress=last.value-first.value;if(elapsed<=0||progress<=0)return null;let rate=progress/elapsed;return(total-current)/rate}function formatETA(eta){if(eta===null||!isFinite(eta))return"--:--";if(eta>86400)return">1d";let hours=Math.floor(eta/3600),minutes=Math.floor(eta%3600/60),seconds=Math.floor(eta%60);if(hours>0)return`${hours}:${minutes.toString().padStart(2,"0")}:${seconds.toString().padStart(2,"0")}`;return`${minutes}:${seconds.toString().padStart(2,"0")}`}var DEFAULT_ETA_BUFFER_SIZE=10;var DEFAULT_FORMAT=":bar :percent | :current/:total | ETA: :eta";class ProgressBar{total;format;width;complete;incomplete;stream;hideCursor;phases;current=0;phase=null;startTime=null;isActive=!1;etaBuffer=[];constructor(options={}){this.total=options.total??100,this.format=options.format??DEFAULT_FORMAT,this.width=options.width??40,this.complete=options.complete??"█",this.incomplete=options.incomplete??"░",this.stream=options.stream??process.stdout,this.hideCursor=options.hideCursor??!0,this.phases=options.phases??{}}start(initialValue=0,initialTotal){if(initialTotal!==void 0)this.total=initialTotal;if(this.current=initialValue,this.startTime=Date.now(),this.isActive=!0,this.etaBuffer=[{time:this.startTime,value:initialValue}],this.hideCursor&&isTTY(this.stream))write(CURSOR_HIDE,this.stream);return this.render(),this}update(value,tokens){this.current=Math.min(value,this.total);let now=Date.now();if(this.etaBuffer.push({time:now,value:this.current}),this.etaBuffer.length>DEFAULT_ETA_BUFFER_SIZE)this.etaBuffer.shift();if(this.isActive)this.render(tokens);return this}increment(amount=1,tokens){return this.update(this.current+amount,tokens)}setPhase(phaseName,options){if(this.phase=phaseName,options?.total!==void 0)this.total=options.total;if(options?.current!==void 0)this.current=options.current,this.etaBuffer=[{time:Date.now(),value:this.current}];if(this.isActive)this.render();return this}stop(clear=!1){if(!this.isActive)return this;if(this.isActive=!1,clear&&isTTY(this.stream))write(`${CURSOR_TO_START}${CLEAR_LINE_END}`,this.stream);else write(`
3
+ `,this.stream);if(this.hideCursor&&isTTY(this.stream))write(CURSOR_SHOW,this.stream);return this}getETASeconds(){return calculateETA(this.etaBuffer,this.current,this.total)}render(tokens){let percent=this.total>0?this.current/this.total:0,eta=this.getETASeconds(),completeLength=Math.round(this.width*percent),incompleteLength=this.width-completeLength,bar=this.complete.repeat(completeLength)+this.incomplete.repeat(incompleteLength),phaseDisplay=this.phase?this.phases[this.phase]??this.phase:"",elapsed=this.startTime?(Date.now()-this.startTime)/1000:0,rate=elapsed>0?this.current/elapsed:0,output=this.format.replace(":bar",chalk2.cyan(bar)).replace(":percent",`${Math.round(percent*100)}%`.padStart(4)).replace(":current",String(this.current)).replace(":total",String(this.total)).replace(":eta",formatETA(eta)).replace(":elapsed",formatETA(elapsed)).replace(":rate",rate.toFixed(1)).replace(":phase",chalk2.dim(phaseDisplay));if(tokens)for(let[key,value]of Object.entries(tokens))output=output.replace(`:${key}`,String(value));let termWidth=getTerminalWidth(this.stream);if(output.length>termWidth)output=output.slice(0,termWidth-1);if(isTTY(this.stream))write(`${CURSOR_TO_START}${output}${CLEAR_LINE_END}`,this.stream)}get ratio(){return this.total>0?this.current/this.total:0}get percentage(){return Math.round(this.ratio*100)}[Symbol.dispose](){this.stop()}}import chalk3 from"chalk";var STATUS_ICONS={pending:chalk3.gray("○"),running:"",completed:chalk3.green("✔"),failed:chalk3.red("✖"),skipped:chalk3.yellow("⊘")};class MultiProgress{tasks=new Map;taskOrder=[];stream;isActive=!1;timer=null;frameIndex=0;renderedLines=0;constructor(stream=process.stdout){this.stream=stream}add(title,options={}){let id=`task-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,task={id,title,type:options.type??"spinner",status:"pending",total:options.total,current:0,spinnerStyle:options.spinnerStyle??"dots",indent:options.indent??0};if(this.tasks.set(id,task),options.insertAfter){let afterIndex=this.taskOrder.indexOf(options.insertAfter);if(afterIndex>=0)this.taskOrder.splice(afterIndex+1,0,id);else this.taskOrder.push(id)}else this.taskOrder.push(id);if(this.isActive)this.render();return new TaskHandle(this,id)}start(){if(this.isActive)return this;if(this.isActive=!0,isTTY(this.stream))write(CURSOR_HIDE,this.stream);return this.render(),this.timer=setInterval(()=>{this.frameIndex=(this.frameIndex+1)%10,this.render()},80),this}[Symbol.dispose](){this.stop()}stop(clear=!1){if(!this.isActive)return this;if(this.isActive=!1,this.timer)clearInterval(this.timer),this.timer=null;if(clear&&isTTY(this.stream)){if(this.renderedLines>0){write(cursorUp(this.renderedLines),this.stream);for(let i=0;i<this.renderedLines;i++)write(`${CLEAR_LINE}
4
+ `,this.stream);write(cursorUp(this.renderedLines),this.stream)}}else this.render(),write(`
5
+ `,this.stream);if(isTTY(this.stream))write(CURSOR_SHOW,this.stream);return this}_updateTask(id,updates){let task=this.tasks.get(id);if(task){if(Object.assign(task,updates),this.isActive&&updates.status)this.render()}}_getTask(id){return this.tasks.get(id)}render(){if(!isTTY(this.stream))return;if(this.renderedLines>0)write(cursorUp(this.renderedLines),this.stream);let lines=[];for(let id of this.taskOrder){let task=this.tasks.get(id);if(!task)continue;let icon;if(task.status==="running")if(task.type==="group")icon=STATUS_ICONS.pending;else{let frames=SPINNER_FRAMES[task.spinnerStyle??"dots"];icon=chalk3.cyan(frames[this.frameIndex%frames.length])}else icon=STATUS_ICONS[task.status];let line=`${" ".repeat(task.indent??0)}${icon} ${task.title}`;if(task.type==="bar"&&task.total&&task.total>0){let percent=task.current/task.total,barWidth=20,filled=Math.round(20*percent),empty=20-filled,bar=chalk3.cyan("█".repeat(filled))+chalk3.gray("░".repeat(empty));line+=` ${bar} ${Math.round(percent*100)}%`}if(task.status==="completed"&&task.completionTime!==void 0)line+=chalk3.dim(` ${task.completionTime}ms`);lines.push(line)}for(let line of lines)write(`${CLEAR_LINE}${line}
6
+ `,this.stream);this.renderedLines=lines.length}}class TaskHandle{multi;_id;constructor(multi,_id){this.multi=multi;this._id=_id}get id(){return this._id}start(){return this.multi._updateTask(this._id,{status:"running"}),this}update(current){return this.multi._updateTask(this._id,{current}),this}complete(titleOrTime){let updates={status:"completed"};if(typeof titleOrTime==="number")updates.completionTime=titleOrTime;else if(titleOrTime)updates.title=titleOrTime;return this.multi._updateTask(this._id,updates),this}fail(title){let updates={status:"failed"};if(title)updates.title=title;return this.multi._updateTask(this._id,updates),this}skip(title){let updates={status:"skipped"};if(title)updates.title=title;return this.multi._updateTask(this._id,updates),this}setTitle(title){return this.multi._updateTask(this._id,{title}),this}setType(type){return this.multi._updateTask(this._id,{type}),this}get status(){return this.multi._getTask(this._id)?.status??"pending"}}export{writeLine,write,withCursor,isTTY,getTerminalWidth,cursorUp,cursorDown,createSpinner,Spinner,SPINNER_FRAMES,ProgressBar,MultiProgress,CURSOR_TO_START,CURSOR_SHOW,CURSOR_SAVE,CURSOR_RESTORE,CURSOR_HIDE,CLEAR_SCREEN,CLEAR_LINE_END,CLEAR_LINE};
7
+
8
+ //# debugId=78921910D2D138E864756E2164756E21
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../packages/ag-react/src/ui/cli/spinner.ts", "../../packages/ag-react/src/ui/cli/ansi.ts", "../../packages/ag-react/src/ui/cli/progress-bar.ts", "../../packages/ag-react/src/ui/utils/eta.ts", "../../packages/ag-react/src/ui/cli/multi-progress.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * CLI Spinner - Animated indeterminate progress indicator\n */\n\nimport chalk from \"chalk\"\nimport type { SpinnerOptions, SpinnerStyle } from \"../types.js\"\nimport { CURSOR_HIDE, CURSOR_SHOW, CURSOR_TO_START, CLEAR_LINE_END, write, isTTY } from \"./ansi\"\n\n/** Spinner animation frames by style */\nexport const SPINNER_FRAMES: Record<SpinnerStyle, string[]> = {\n dots: [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"],\n line: [\"-\", \"\\\\\", \"|\", \"/\"],\n arc: [\"◜\", \"◠\", \"◝\", \"◞\", \"◡\", \"◟\"],\n bounce: [\"⠁\", \"⠂\", \"⠄\", \"⠂\"],\n pulse: [\"█\", \"▓\", \"▒\", \"░\", \"▒\", \"▓\"],\n}\n\n/** Default intervals for each style (ms) */\nexport const SPINNER_INTERVALS: Record<SpinnerStyle, number> = {\n dots: 80,\n line: 120,\n arc: 100,\n bounce: 120,\n pulse: 100,\n}\n\n/**\n * Spinner class for CLI progress indication\n *\n * @example\n * ```ts\n * const spinner = new Spinner(\"Loading...\");\n * spinner.start();\n * await doWork();\n * spinner.succeed(\"Done!\");\n * ```\n */\nexport class Spinner {\n private text: string\n private style: SpinnerStyle\n private color: string\n private stream: NodeJS.WriteStream\n private hideCursor: boolean\n private interval: number\n\n private frameIndex = 0\n private timer: ReturnType<typeof setInterval> | null = null\n private isSpinning = false\n\n constructor(textOrOptions?: string | SpinnerOptions) {\n const options: SpinnerOptions = typeof textOrOptions === \"string\" ? { text: textOrOptions } : (textOrOptions ?? {})\n\n this.text = options.text ?? \"\"\n this.style = options.style ?? \"dots\"\n this.color = options.color ?? \"cyan\"\n this.stream = options.stream ?? process.stdout\n this.hideCursor = options.hideCursor ?? true\n this.interval = options.interval ?? SPINNER_INTERVALS[this.style]\n }\n\n /** Get current spinner text */\n get currentText(): string {\n return this.text\n }\n\n /** Set spinner text (updates immediately if spinning) */\n set currentText(value: string) {\n this.text = value\n if (this.isSpinning) {\n this.render()\n }\n }\n\n /** Check if spinner is currently active */\n get spinning(): boolean {\n return this.isSpinning\n }\n\n /**\n * Start the spinner animation\n */\n start(text?: string): this {\n if (text !== undefined) {\n this.text = text\n }\n\n if (this.isSpinning) {\n return this\n }\n\n this.isSpinning = true\n this.frameIndex = 0\n\n if (this.hideCursor && isTTY(this.stream)) {\n write(CURSOR_HIDE, this.stream)\n }\n\n this.render()\n this.timer = setInterval(() => {\n this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES[this.style].length\n this.render()\n }, this.interval)\n\n return this\n }\n\n /**\n * Stop the spinner\n */\n stop(): this {\n if (!this.isSpinning) {\n return this\n }\n\n this.isSpinning = false\n\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n\n this.clear()\n\n if (this.hideCursor && isTTY(this.stream)) {\n write(CURSOR_SHOW, this.stream)\n }\n\n return this\n }\n\n /**\n * Stop with success message (green checkmark)\n */\n succeed(text?: string): this {\n return this.stopWithSymbol(chalk.green(\"✔\"), text ?? this.text)\n }\n\n /**\n * Stop with failure message (red X)\n */\n fail(text?: string): this {\n return this.stopWithSymbol(chalk.red(\"✖\"), text ?? this.text)\n }\n\n /**\n * Stop with warning message (yellow warning)\n */\n warn(text?: string): this {\n return this.stopWithSymbol(chalk.yellow(\"⚠\"), text ?? this.text)\n }\n\n /**\n * Stop with info message (blue info)\n */\n info(text?: string): this {\n return this.stopWithSymbol(chalk.blue(\"ℹ\"), text ?? this.text)\n }\n\n /**\n * Clear the spinner line\n */\n clear(): this {\n if (isTTY(this.stream)) {\n write(`${CURSOR_TO_START}${CLEAR_LINE_END}`, this.stream)\n }\n return this\n }\n\n private render(): void {\n const frame = SPINNER_FRAMES[this.style][this.frameIndex]\n const colorFn = (chalk as unknown as Record<string, (s: string) => string>)[this.color]\n const coloredFrame = colorFn ? colorFn(frame!) : frame!\n const output = this.text ? `${coloredFrame} ${this.text}` : coloredFrame\n\n if (isTTY(this.stream)) {\n write(`${CURSOR_TO_START}${output}${CLEAR_LINE_END}`, this.stream)\n }\n }\n\n private stopWithSymbol(symbol: string, text: string): this {\n this.stop()\n write(`${symbol} ${text}\\n`, this.stream)\n return this\n }\n\n /**\n * Dispose the spinner (calls stop)\n */\n [Symbol.dispose](): void {\n this.stop()\n }\n\n /**\n * Static helper to quickly start a spinner\n * Returns a stop function\n *\n * @example\n * ```ts\n * const stop = Spinner.start(\"Loading...\");\n * await doWork();\n * stop();\n * ```\n */\n static start(textOrOptions?: string | SpinnerOptions): () => void {\n const spinner = new Spinner(textOrOptions)\n spinner.start()\n return () => spinner.stop()\n }\n}\n\n/**\n * Callable spinner interface - call with text to show/update\n *\n * @example\n * ```ts\n * {\n * using spinner = createSpinner({ style: \"dots\" });\n * spinner(\"Loading...\"); // Shows spinner with text\n * spinner(\"Still loading...\"); // Updates text\n * } // Auto-stops via Symbol.dispose\n * ```\n */\nexport interface CallableSpinner extends Disposable {\n /** Call with text to show/update the spinner */\n (text: string): void\n /** Stop the spinner */\n stop(): void\n /** Stop with success message (green checkmark) */\n succeed(text?: string): void\n /** Stop with failure message (red X) */\n fail(text?: string): void\n /** Stop with warning message (yellow warning) */\n warn(text?: string): void\n /** Stop with info message (blue info) */\n info(text?: string): void\n}\n\n/**\n * Create a callable, disposable spinner\n *\n * The spinner is lazy - it won't show anything until you call it with text.\n * Use with `using` for automatic cleanup:\n *\n * @example\n * ```ts\n * {\n * using spinner = createSpinner({ style: \"dots\" });\n * // Nothing visible yet\n *\n * spinner(\"Loading repo...\"); // Now shows spinner\n * spinner(\"Applying events...\"); // Updates text\n * } // Auto-stops via Symbol.dispose\n * ```\n */\nexport function createSpinner(options?: SpinnerOptions): CallableSpinner {\n const spinner = new Spinner({ ...options, text: \"\" })\n\n const callable = ((text: string) => {\n // Always restart if not spinning (handles initial call and after succeed/fail/etc)\n if (!spinner.spinning) {\n spinner.start(text)\n } else {\n spinner.currentText = text\n }\n }) as CallableSpinner\n\n callable.stop = () => spinner.stop()\n callable.succeed = (text) => spinner.succeed(text)\n callable.fail = (text) => spinner.fail(text)\n callable.warn = (text) => spinner.warn(text)\n callable.info = (text) => spinner.info(text)\n callable[Symbol.dispose] = () => spinner.stop()\n\n return callable\n}\n",
6
+ "/**\n * ANSI escape code utilities for terminal control\n */\n\n/** Hide the cursor */\nexport const CURSOR_HIDE = \"\\x1b[?25l\"\n\n/** Show the cursor */\nexport const CURSOR_SHOW = \"\\x1b[?25h\"\n\n/** Move cursor to beginning of line */\nexport const CURSOR_TO_START = \"\\r\"\n\n/** Clear from cursor to end of line */\nexport const CLEAR_LINE_END = \"\\x1b[K\"\n\n/** Clear entire line */\nexport const CLEAR_LINE = \"\\x1b[2K\"\n\n/** Clear screen and move to top-left */\nexport const CLEAR_SCREEN = \"\\x1b[2J\\x1b[H\"\n\n/** Move cursor up N lines */\nexport const cursorUp = (n: number = 1): string => `\\x1b[${n}A`\n\n/** Move cursor down N lines */\nexport const cursorDown = (n: number = 1): string => `\\x1b[${n}B`\n\n/** Save cursor position */\nexport const CURSOR_SAVE = \"\\x1b[s\"\n\n/** Restore cursor position */\nexport const CURSOR_RESTORE = \"\\x1b[u\"\n\n/**\n * Write to stream with proper handling\n */\nexport function write(text: string, stream: NodeJS.WriteStream = process.stdout): void {\n stream.write(text)\n}\n\n/**\n * Clear the current line and write new text\n */\nexport function writeLine(text: string, stream: NodeJS.WriteStream = process.stdout): void {\n stream.write(`${CURSOR_TO_START}${text}${CLEAR_LINE_END}`)\n}\n\n/**\n * Wrap a function to handle cursor visibility\n * Hides cursor on start, shows on completion/error\n */\nexport function withCursor<T>(fn: () => T | Promise<T>, stream: NodeJS.WriteStream = process.stdout): Promise<T> {\n stream.write(CURSOR_HIDE)\n\n const restore = () => stream.write(CURSOR_SHOW)\n\n try {\n const result = fn()\n if (result instanceof Promise) {\n return result.finally(restore)\n }\n restore()\n return Promise.resolve(result)\n } catch (error) {\n restore()\n throw error\n }\n}\n\n/**\n * Check if stream is a TTY (supports ANSI codes)\n * Also respects FORCE_TTY environment variable for testing\n */\nexport function isTTY(stream: NodeJS.WriteStream = process.stdout): boolean {\n if (process.env.FORCE_TTY === \"1\") return true\n return stream.isTTY ?? false\n}\n\n/**\n * Get terminal width\n */\nexport function getTerminalWidth(stream: NodeJS.WriteStream = process.stdout): number {\n return stream.columns ?? 80\n}\n",
7
+ "/**\n * CLI ProgressBar - Determinate progress indicator with ETA\n */\n\nimport chalk from \"chalk\"\nimport type { ProgressBarOptions } from \"../types.js\"\nimport { CURSOR_HIDE, CURSOR_SHOW, CURSOR_TO_START, CLEAR_LINE_END, write, isTTY, getTerminalWidth } from \"./ansi\"\nimport { calculateETA, formatETA, DEFAULT_ETA_BUFFER_SIZE, type ETASample } from \"../utils/eta\"\n\n/** Default format string */\nconst DEFAULT_FORMAT = \":bar :percent | :current/:total | ETA: :eta\"\n\n/**\n * ProgressBar class for CLI progress indication\n *\n * @example\n * ```ts\n * const bar = new ProgressBar({ total: 100 });\n * bar.start();\n * for (let i = 0; i <= 100; i++) {\n * await doWork();\n * bar.update(i);\n * }\n * bar.stop();\n * ```\n */\nexport class ProgressBar {\n private total: number\n private format: string\n private width: number\n private complete: string\n private incomplete: string\n private stream: NodeJS.WriteStream\n private hideCursor: boolean\n private phases: Record<string, string>\n\n private current = 0\n private phase: string | null = null\n private startTime: number | null = null\n private isActive = false\n\n // ETA smoothing - track last N update times\n private etaBuffer: ETASample[] = []\n\n constructor(options: ProgressBarOptions = {}) {\n this.total = options.total ?? 100\n this.format = options.format ?? DEFAULT_FORMAT\n this.width = options.width ?? 40\n this.complete = options.complete ?? \"█\"\n this.incomplete = options.incomplete ?? \"░\"\n this.stream = options.stream ?? process.stdout\n this.hideCursor = options.hideCursor ?? true\n this.phases = options.phases ?? {}\n }\n\n /**\n * Start the progress bar\n */\n start(initialValue = 0, initialTotal?: number): this {\n if (initialTotal !== undefined) {\n this.total = initialTotal\n }\n\n this.current = initialValue\n this.startTime = Date.now()\n this.isActive = true\n this.etaBuffer = [{ time: this.startTime, value: initialValue }]\n\n if (this.hideCursor && isTTY(this.stream)) {\n write(CURSOR_HIDE, this.stream)\n }\n\n this.render()\n return this\n }\n\n /**\n * Update progress value\n */\n update(value: number, tokens?: Record<string, string | number>): this {\n this.current = Math.min(value, this.total)\n\n // Update ETA buffer\n const now = Date.now()\n this.etaBuffer.push({ time: now, value: this.current })\n if (this.etaBuffer.length > DEFAULT_ETA_BUFFER_SIZE) {\n this.etaBuffer.shift()\n }\n\n if (this.isActive) {\n this.render(tokens)\n }\n\n return this\n }\n\n /**\n * Increment progress by amount (default: 1)\n */\n increment(amount = 1, tokens?: Record<string, string | number>): this {\n return this.update(this.current + amount, tokens)\n }\n\n /**\n * Set the current phase (for multi-phase progress)\n */\n setPhase(phaseName: string, options?: { current?: number; total?: number }): this {\n this.phase = phaseName\n\n if (options?.total !== undefined) {\n this.total = options.total\n }\n if (options?.current !== undefined) {\n this.current = options.current\n // Reset ETA buffer on phase change\n this.etaBuffer = [{ time: Date.now(), value: this.current }]\n }\n\n if (this.isActive) {\n this.render()\n }\n\n return this\n }\n\n /**\n * Stop the progress bar\n */\n stop(clear = false): this {\n if (!this.isActive) {\n return this\n }\n\n this.isActive = false\n\n if (clear && isTTY(this.stream)) {\n write(`${CURSOR_TO_START}${CLEAR_LINE_END}`, this.stream)\n } else {\n write(\"\\n\", this.stream)\n }\n\n if (this.hideCursor && isTTY(this.stream)) {\n write(CURSOR_SHOW, this.stream)\n }\n\n return this\n }\n\n /** Get ETA in seconds using smoothed rate */\n private getETASeconds(): number | null {\n return calculateETA(this.etaBuffer, this.current, this.total)\n }\n\n /**\n * Render the progress bar\n */\n private render(tokens?: Record<string, string | number>): void {\n const percent = this.total > 0 ? this.current / this.total : 0\n const eta = this.getETASeconds()\n\n // Build the bar\n const completeLength = Math.round(this.width * percent)\n const incompleteLength = this.width - completeLength\n const bar = this.complete.repeat(completeLength) + this.incomplete.repeat(incompleteLength)\n\n // Get phase display name\n const phaseDisplay = this.phase ? (this.phases[this.phase] ?? this.phase) : \"\"\n\n // Calculate rate\n const elapsed = this.startTime ? (Date.now() - this.startTime) / 1000 : 0\n const rate = elapsed > 0 ? this.current / elapsed : 0\n\n // Replace tokens in format string\n let output = this.format\n .replace(\":bar\", chalk.cyan(bar))\n .replace(\":percent\", `${Math.round(percent * 100)}%`.padStart(4))\n .replace(\":current\", String(this.current))\n .replace(\":total\", String(this.total))\n .replace(\":eta\", formatETA(eta))\n .replace(\":elapsed\", formatETA(elapsed))\n .replace(\":rate\", rate.toFixed(1))\n .replace(\":phase\", chalk.dim(phaseDisplay))\n\n // Replace custom tokens\n if (tokens) {\n for (const [key, value] of Object.entries(tokens)) {\n output = output.replace(`:${key}`, String(value))\n }\n }\n\n // Truncate to terminal width\n const termWidth = getTerminalWidth(this.stream)\n if (output.length > termWidth) {\n output = output.slice(0, termWidth - 1)\n }\n\n if (isTTY(this.stream)) {\n write(`${CURSOR_TO_START}${output}${CLEAR_LINE_END}`, this.stream)\n }\n }\n\n /**\n * Get current progress ratio (0-1)\n */\n get ratio(): number {\n return this.total > 0 ? this.current / this.total : 0\n }\n\n /**\n * Get current progress percentage (0-100)\n */\n get percentage(): number {\n return Math.round(this.ratio * 100)\n }\n\n /**\n * Dispose the progress bar (calls stop)\n */\n [Symbol.dispose](): void {\n this.stop()\n }\n}\n",
8
+ "/**\n * Shared ETA calculation utilities\n */\n\n/** Sample point for ETA calculation */\nexport interface ETASample {\n time: number\n value: number\n}\n\n/** ETA calculation result */\nexport interface ETAResult {\n /** Estimated seconds remaining, or null if insufficient data */\n seconds: number | null\n /** Formatted ETA string (e.g., \"1:30\", \"2:15:30\", \"--:--\") */\n formatted: string\n}\n\n/**\n * Calculate ETA from a buffer of samples\n *\n * @param buffer - Array of {time, value} samples\n * @param current - Current progress value\n * @param total - Total target value\n * @returns ETA in seconds (null if insufficient data)\n *\n * @example\n * ```ts\n * const buffer = [\n * { time: 1000, value: 0 },\n * { time: 2000, value: 10 },\n * ];\n * const eta = calculateETA(buffer, 10, 100);\n * // eta = 9 (9 seconds remaining at 10 items/sec)\n * ```\n */\nexport function calculateETA(buffer: ETASample[], current: number, total: number): number | null {\n if (buffer.length < 2) {\n return null\n }\n\n const first = buffer[0]!\n const last = buffer[buffer.length - 1]!\n\n const elapsed = (last.time - first.time) / 1000 // seconds\n const progress = last.value - first.value\n\n if (elapsed <= 0 || progress <= 0) {\n return null\n }\n\n const rate = progress / elapsed // items per second\n const remaining = total - current\n\n return remaining / rate\n}\n\n/**\n * Format ETA seconds as human-readable string\n *\n * @param eta - ETA in seconds (null for unknown)\n * @returns Formatted string (e.g., \"1:30\", \"2:15:30\", \"--:--\", \">1d\")\n *\n * @example\n * ```ts\n * formatETA(90) // \"1:30\"\n * formatETA(3665) // \"1:01:05\"\n * formatETA(null) // \"--:--\"\n * formatETA(100000) // \">1d\"\n * ```\n */\nexport function formatETA(eta: number | null): string {\n if (eta === null || !isFinite(eta)) {\n return \"--:--\"\n }\n\n if (eta > 86400) {\n // > 24 hours\n return \">1d\"\n }\n\n const hours = Math.floor(eta / 3600)\n const minutes = Math.floor((eta % 3600) / 60)\n const seconds = Math.floor(eta % 60)\n\n if (hours > 0) {\n return `${hours}:${minutes.toString().padStart(2, \"0\")}:${seconds.toString().padStart(2, \"0\")}`\n }\n\n return `${minutes}:${seconds.toString().padStart(2, \"0\")}`\n}\n\n/**\n * Calculate and format ETA in one call\n *\n * @param buffer - Array of {time, value} samples\n * @param current - Current progress value\n * @param total - Total target value\n * @returns Object with seconds (number|null) and formatted string\n */\nexport function getETA(buffer: ETASample[], current: number, total: number): ETAResult {\n const seconds = calculateETA(buffer, current, total)\n return {\n seconds,\n formatted: formatETA(seconds),\n }\n}\n\n/** Default buffer size for ETA smoothing */\nexport const DEFAULT_ETA_BUFFER_SIZE = 10\n\n/**\n * Create an ETA tracker with automatic buffer management\n *\n * @param bufferSize - Number of samples to keep (default: 10)\n * @returns ETA tracker object\n *\n * @example\n * ```ts\n * const tracker = createETATracker();\n * tracker.record(0);\n * // ... later ...\n * tracker.record(50);\n * const eta = tracker.getETA(50, 100);\n * console.log(eta.formatted); // \"0:30\"\n * ```\n */\nexport function createETATracker(bufferSize = DEFAULT_ETA_BUFFER_SIZE) {\n const buffer: ETASample[] = []\n\n return {\n /** Record a new sample */\n record(value: number): void {\n buffer.push({ time: Date.now(), value })\n if (buffer.length > bufferSize) {\n buffer.shift()\n }\n },\n\n /** Get current ETA */\n getETA(current: number, total: number): ETAResult {\n return getETA(buffer, current, total)\n },\n\n /** Reset the buffer */\n reset(): void {\n buffer.length = 0\n },\n\n /** Get buffer for external use */\n getBuffer(): readonly ETASample[] {\n return buffer\n },\n }\n}\n",
9
+ "/**\n * MultiProgress - Container for managing multiple concurrent progress indicators\n */\n\nimport chalk from \"chalk\"\nimport type { SpinnerStyle, TaskStatus } from \"../types.js\"\nimport { CURSOR_HIDE, CURSOR_SHOW, CLEAR_LINE, cursorUp, write, isTTY } from \"./ansi\"\nimport { Spinner, SPINNER_FRAMES } from \"./spinner\"\nimport { ProgressBar } from \"./progress-bar\"\n\n/** Status icons */\nconst STATUS_ICONS: Record<TaskStatus, string> = {\n pending: chalk.gray(\"○\"),\n running: \"\", // Will be replaced with spinner frame\n completed: chalk.green(\"✔\"),\n failed: chalk.red(\"✖\"),\n skipped: chalk.yellow(\"⊘\"),\n}\n\n/** Task configuration */\ninterface TaskConfig {\n title: string\n type: \"spinner\" | \"bar\" | \"group\"\n status: TaskStatus\n total?: number\n current?: number\n spinnerStyle?: SpinnerStyle\n indent?: number\n}\n\n/** Internal task state */\ninterface TaskState extends TaskConfig {\n id: string\n /** Completion time in ms (shown dimmed after title on completion) */\n completionTime?: number\n}\n\n/**\n * MultiProgress - Manage multiple concurrent progress indicators\n *\n * @example\n * ```ts\n * const multi = new MultiProgress();\n *\n * const download = multi.add(\"Downloading files\", { type: \"bar\", total: 100 });\n * const process = multi.add(\"Processing\", { type: \"spinner\" });\n *\n * download.start();\n * download.update(50);\n * download.complete();\n *\n * process.start();\n * process.complete();\n *\n * multi.stop();\n * ```\n */\nexport class MultiProgress {\n private tasks: Map<string, TaskState> = new Map()\n private taskOrder: string[] = []\n private stream: NodeJS.WriteStream\n private isActive = false\n private timer: ReturnType<typeof setInterval> | null = null\n private frameIndex = 0\n private renderedLines = 0\n\n constructor(stream: NodeJS.WriteStream = process.stdout) {\n this.stream = stream\n }\n\n /**\n * Add a new task\n * @param insertAfter - ID of task to insert after (for hierarchical display)\n */\n add(\n title: string,\n options: {\n type?: \"spinner\" | \"bar\" | \"group\"\n total?: number\n spinnerStyle?: SpinnerStyle\n indent?: number\n insertAfter?: string\n } = {},\n ): TaskHandle {\n const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n\n const task: TaskState = {\n id,\n title,\n type: options.type ?? \"spinner\",\n status: \"pending\",\n total: options.total,\n current: 0,\n spinnerStyle: options.spinnerStyle ?? \"dots\",\n indent: options.indent ?? 0,\n }\n\n this.tasks.set(id, task)\n\n // Insert after specified task, or append to end\n if (options.insertAfter) {\n const afterIndex = this.taskOrder.indexOf(options.insertAfter)\n if (afterIndex >= 0) {\n this.taskOrder.splice(afterIndex + 1, 0, id)\n } else {\n this.taskOrder.push(id)\n }\n } else {\n this.taskOrder.push(id)\n }\n\n if (this.isActive) {\n this.render()\n }\n\n return new TaskHandle(this, id)\n }\n\n /**\n * Start the multi-progress display\n */\n start(): this {\n if (this.isActive) {\n return this\n }\n\n this.isActive = true\n\n if (isTTY(this.stream)) {\n write(CURSOR_HIDE, this.stream)\n }\n\n this.render()\n\n // Start animation timer\n this.timer = setInterval(() => {\n this.frameIndex = (this.frameIndex + 1) % 10\n this.render()\n }, 80)\n\n return this\n }\n\n /**\n * Dispose the multi-progress display (calls stop)\n */\n [Symbol.dispose](): void {\n this.stop()\n }\n\n /**\n * Stop the multi-progress display\n * @param clear - If true, clear all task lines from terminal\n */\n stop(clear = false): this {\n if (!this.isActive) {\n return this\n }\n\n this.isActive = false\n\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n\n if (clear && isTTY(this.stream)) {\n // Clear all rendered lines\n if (this.renderedLines > 0) {\n write(cursorUp(this.renderedLines), this.stream)\n for (let i = 0; i < this.renderedLines; i++) {\n write(`${CLEAR_LINE}\\n`, this.stream)\n }\n write(cursorUp(this.renderedLines), this.stream)\n }\n } else {\n // Final render\n this.render()\n write(\"\\n\", this.stream)\n }\n\n if (isTTY(this.stream)) {\n write(CURSOR_SHOW, this.stream)\n }\n\n return this\n }\n\n /** @internal Update task state */\n _updateTask(id: string, updates: Partial<TaskState>): void {\n const task = this.tasks.get(id)\n if (task) {\n Object.assign(task, updates)\n // Only render immediately for status changes (complete/fail/etc.)\n // Progress updates (current/total) are debounced by the 80ms animation timer\n if (this.isActive && updates.status) {\n this.render()\n }\n }\n }\n\n /** @internal Get task state */\n _getTask(id: string): TaskState | undefined {\n return this.tasks.get(id)\n }\n\n private render(): void {\n if (!isTTY(this.stream)) {\n return\n }\n\n // Move cursor up to clear previous render\n if (this.renderedLines > 0) {\n write(cursorUp(this.renderedLines), this.stream)\n }\n\n const lines: string[] = []\n\n for (const id of this.taskOrder) {\n const task = this.tasks.get(id)\n if (!task) continue\n\n let icon: string\n if (task.status === \"running\") {\n if (task.type === \"group\") {\n // Groups don't animate - keep pending icon while running\n icon = STATUS_ICONS.pending\n } else {\n const frames = SPINNER_FRAMES[task.spinnerStyle ?? \"dots\"]\n icon = chalk.cyan(frames[this.frameIndex % frames.length])\n }\n } else {\n icon = STATUS_ICONS[task.status]\n }\n\n const indent = \" \".repeat(task.indent ?? 0)\n let line = `${indent}${icon} ${task.title}`\n\n // Add progress bar for bar type\n if (task.type === \"bar\" && task.total && task.total > 0) {\n const percent = task.current! / task.total\n const barWidth = 20\n const filled = Math.round(barWidth * percent)\n const empty = barWidth - filled\n const bar = chalk.cyan(\"█\".repeat(filled)) + chalk.gray(\"░\".repeat(empty))\n line += ` ${bar} ${Math.round(percent * 100)}%`\n }\n\n // Add completion time in dimmed text\n if (task.status === \"completed\" && task.completionTime !== undefined) {\n line += chalk.dim(` ${task.completionTime}ms`)\n }\n\n lines.push(line)\n }\n\n // Clear and write each line\n for (const line of lines) {\n write(`${CLEAR_LINE}${line}\\n`, this.stream)\n }\n\n this.renderedLines = lines.length\n }\n}\n\n/**\n * Handle for controlling an individual task\n */\nclass TaskHandle {\n constructor(\n private multi: MultiProgress,\n private _id: string,\n ) {}\n\n /** Get task ID (for insertAfter) */\n get id(): string {\n return this._id\n }\n\n /** Start the task (set status to running) */\n start(): this {\n this.multi._updateTask(this._id, { status: \"running\" })\n return this\n }\n\n /** Update progress (for bar type) */\n update(current: number): this {\n this.multi._updateTask(this._id, { current })\n return this\n }\n\n /** Mark task as completed */\n complete(titleOrTime?: string | number): this {\n const updates: Partial<TaskState> = { status: \"completed\" }\n if (typeof titleOrTime === \"number\") {\n // Numeric = completion time in ms (preserves current title)\n updates.completionTime = titleOrTime\n } else if (titleOrTime) {\n // String = new title (legacy behavior)\n updates.title = titleOrTime\n }\n this.multi._updateTask(this._id, updates)\n return this\n }\n\n /** Mark task as failed */\n fail(title?: string): this {\n const updates: Partial<TaskState> = { status: \"failed\" }\n if (title) updates.title = title\n this.multi._updateTask(this._id, updates)\n return this\n }\n\n /** Mark task as skipped */\n skip(title?: string): this {\n const updates: Partial<TaskState> = { status: \"skipped\" }\n if (title) updates.title = title\n this.multi._updateTask(this._id, updates)\n return this\n }\n\n /** Update task title */\n setTitle(title: string): this {\n this.multi._updateTask(this._id, { title })\n return this\n }\n\n /** Change task type (e.g., from spinner to group when sub-steps are added) */\n setType(type: \"spinner\" | \"bar\" | \"group\"): this {\n this.multi._updateTask(this._id, { type })\n return this\n }\n\n /** Get current status */\n get status(): TaskStatus {\n return this.multi._getTask(this._id)?.status ?? \"pending\"\n }\n}\n\nexport type { TaskHandle }\n"
10
+ ],
11
+ "mappings": "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",
12
+ "debugId": "78921910D2D138E864756E2164756E21",
13
+ "names": []
14
+ }
@@ -0,0 +1,4 @@
1
+ import{jsxDEV}from"react/jsx-dev-runtime";var BOX={topLeft:"┌",topRight:"┐",bottomLeft:"└",bottomRight:"┘",horizontal:"─",vertical:"│",leftT:"├",rightT:"┤",topT:"┬",bottomT:"┴",cross:"┼"};function Table({columns,data,border=!1}){let effectiveColumns=calculateColumnWidths(columns,data),lines=[];if(border)lines.push(buildBorderLine(effectiveColumns,"top"));if(lines.push(buildDataRow(effectiveColumns,getHeaderRow(effectiveColumns),border)),border)lines.push(buildBorderLine(effectiveColumns,"middle"));for(let row of data)lines.push(buildDataRow(effectiveColumns,row,border));if(border)lines.push(buildBorderLine(effectiveColumns,"bottom"));return jsxDEV("span",{"data-table":!0,"data-border":border,children:lines.join(`
2
+ `)},void 0,!1,void 0,this)}function calculateColumnWidths(columns,data){return columns.map((col)=>{if(col.width!==void 0)return{...col,effectiveWidth:col.width};let maxWidth=col.header.length;for(let row of data){let value=String(row[col.key]??"");maxWidth=Math.max(maxWidth,value.length)}return{...col,effectiveWidth:maxWidth}})}function getHeaderRow(columns){let row={};for(let col of columns)row[col.key]=col.header;return row}function buildBorderLine(columns,position){let left=position==="top"?BOX.topLeft:position==="bottom"?BOX.bottomLeft:BOX.leftT,right=position==="top"?BOX.topRight:position==="bottom"?BOX.bottomRight:BOX.rightT,join=position==="top"?BOX.topT:position==="bottom"?BOX.bottomT:BOX.cross,segments=columns.map((col)=>BOX.horizontal.repeat(col.effectiveWidth+2));return left+segments.join(join)+right}function buildDataRow(columns,row,border){let cells=columns.map((col)=>{let value=String(row[col.key]??"");return formatCell(value,col.effectiveWidth,col.align??"left")});if(border)return BOX.vertical+" "+cells.join(" "+BOX.vertical+" ")+" "+BOX.vertical;return cells.join(" ")}function formatCell(value,width,align){if(value.length>width)return value.slice(0,width-1)+"…";let padding=width-value.length;switch(align){case"right":return" ".repeat(padding)+value;case"center":{let leftPad=Math.floor(padding/2),rightPad=padding-leftPad;return" ".repeat(leftPad)+value+" ".repeat(rightPad)}case"left":default:return value+" ".repeat(padding)}}export{Table};
3
+
4
+ //# debugId=FFF09C5CBB835CB364756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../packages/ag-react/src/ui/display/Table.tsx"],
4
+ "sourcesContent": [
5
+ "/**\n * React Table component for silvery/Ink TUI apps\n */\n\nimport React from \"react\"\nimport type { TableProps, TableColumn } from \"../types.js\"\n\n/**\n * Unicode box drawing characters for borders\n */\nconst BOX = {\n topLeft: \"┌\",\n topRight: \"┐\",\n bottomLeft: \"└\",\n bottomRight: \"┘\",\n horizontal: \"─\",\n vertical: \"│\",\n leftT: \"├\",\n rightT: \"┤\",\n topT: \"┬\",\n bottomT: \"┴\",\n cross: \"┼\",\n} as const\n\n/**\n * Data grid display component for React TUI apps\n *\n * @example\n * ```tsx\n * import { Table } from \"@silvery/ag-react/ui/display\";\n *\n * const columns = [\n * { key: \"name\", header: \"Name\", width: 20 },\n * { key: \"status\", header: \"Status\", width: 10, align: \"center\" },\n * { key: \"count\", header: \"Count\", width: 8, align: \"right\" },\n * ];\n *\n * const data = [\n * { name: \"Item 1\", status: \"active\", count: 42 },\n * { name: \"Item 2\", status: \"pending\", count: 7 },\n * ];\n *\n * function DataView() {\n * return <Table columns={columns} data={data} border />;\n * }\n * ```\n */\nexport function Table({ columns, data, border = false }: TableProps): React.ReactElement {\n // Calculate effective column widths\n const effectiveColumns = calculateColumnWidths(columns, data)\n\n const lines: string[] = []\n\n if (border) {\n // Top border\n lines.push(buildBorderLine(effectiveColumns, \"top\"))\n }\n\n // Header row\n lines.push(buildDataRow(effectiveColumns, getHeaderRow(effectiveColumns), border))\n\n if (border) {\n // Separator after header\n lines.push(buildBorderLine(effectiveColumns, \"middle\"))\n }\n\n // Data rows\n for (const row of data) {\n lines.push(buildDataRow(effectiveColumns, row, border))\n }\n\n if (border) {\n // Bottom border\n lines.push(buildBorderLine(effectiveColumns, \"bottom\"))\n }\n\n return (\n <span data-table data-border={border}>\n {lines.join(\"\\n\")}\n </span>\n )\n}\n\n/**\n * Calculate effective column widths based on content if not specified\n */\nfunction calculateColumnWidths(\n columns: TableColumn[],\n data: Array<Record<string, unknown>>,\n): Array<TableColumn & { effectiveWidth: number }> {\n return columns.map((col) => {\n if (col.width !== undefined) {\n return { ...col, effectiveWidth: col.width }\n }\n\n // Calculate width from content\n let maxWidth = col.header.length\n\n for (const row of data) {\n const value = String(row[col.key] ?? \"\")\n maxWidth = Math.max(maxWidth, value.length)\n }\n\n return { ...col, effectiveWidth: maxWidth }\n })\n}\n\n/**\n * Create header row object from columns\n */\nfunction getHeaderRow(columns: Array<TableColumn & { effectiveWidth: number }>): Record<string, unknown> {\n const row: Record<string, unknown> = {}\n for (const col of columns) {\n row[col.key] = col.header\n }\n return row\n}\n\n/**\n * Build a border line (top, middle, or bottom)\n */\nfunction buildBorderLine(\n columns: Array<TableColumn & { effectiveWidth: number }>,\n position: \"top\" | \"middle\" | \"bottom\",\n): string {\n const left = position === \"top\" ? BOX.topLeft : position === \"bottom\" ? BOX.bottomLeft : BOX.leftT\n const right = position === \"top\" ? BOX.topRight : position === \"bottom\" ? BOX.bottomRight : BOX.rightT\n const join = position === \"top\" ? BOX.topT : position === \"bottom\" ? BOX.bottomT : BOX.cross\n\n const segments = columns.map((col) => BOX.horizontal.repeat(col.effectiveWidth + 2))\n\n return left + segments.join(join) + right\n}\n\n/**\n * Build a data row (header or content)\n */\nfunction buildDataRow(\n columns: Array<TableColumn & { effectiveWidth: number }>,\n row: Record<string, unknown>,\n border: boolean,\n): string {\n const cells = columns.map((col) => {\n const value = String(row[col.key] ?? \"\")\n return formatCell(value, col.effectiveWidth, col.align ?? \"left\")\n })\n\n if (border) {\n return BOX.vertical + \" \" + cells.join(\" \" + BOX.vertical + \" \") + \" \" + BOX.vertical\n }\n\n return cells.join(\" \")\n}\n\n/**\n * Format a cell value with alignment and truncation\n */\nfunction formatCell(value: string, width: number, align: \"left\" | \"center\" | \"right\"): string {\n // Truncate if too long\n if (value.length > width) {\n return value.slice(0, width - 1) + \"…\"\n }\n\n // Pad according to alignment\n const padding = width - value.length\n\n switch (align) {\n case \"right\":\n return \" \".repeat(padding) + value\n case \"center\": {\n const leftPad = Math.floor(padding / 2)\n const rightPad = padding - leftPad\n return \" \".repeat(leftPad) + value + \" \".repeat(rightPad)\n }\n case \"left\":\n default:\n return value + \" \".repeat(padding)\n }\n}\n"
6
+ ],
7
+ "mappings": "0CAUA,IAAM,IAAM,CACV,QAAS,IACT,SAAU,IACV,WAAY,IACZ,YAAa,IACb,WAAY,IACZ,SAAU,IACV,MAAO,IACP,OAAQ,IACR,KAAM,IACN,QAAS,IACT,MAAO,GACT,EAyBO,SAAS,KAAK,EAAG,QAAS,KAAM,OAAS,IAAyC,CAEvF,IAAM,iBAAmB,sBAAsB,QAAS,IAAI,EAEtD,MAAkB,CAAC,EAEzB,GAAI,OAEF,MAAM,KAAK,gBAAgB,iBAAkB,KAAK,CAAC,EAMrD,GAFA,MAAM,KAAK,aAAa,iBAAkB,aAAa,gBAAgB,EAAG,MAAM,CAAC,EAE7E,OAEF,MAAM,KAAK,gBAAgB,iBAAkB,QAAQ,CAAC,EAIxD,QAAW,OAAO,KAChB,MAAM,KAAK,aAAa,iBAAkB,IAAK,MAAM,CAAC,EAGxD,GAAI,OAEF,MAAM,KAAK,gBAAgB,iBAAkB,QAAQ,CAAC,EAGxD,OACE,OAEE,OAFF,CAAM,aAAU,GAAC,cAAa,OAA9B,SACG,MAAM,KAAK;AAAA,CAAI,GADlB,qBAEE,EAON,SAAS,qBAAqB,CAC5B,QACA,KACiD,CACjD,OAAO,QAAQ,IAAI,CAAC,MAAQ,CAC1B,GAAI,IAAI,QAAU,OAChB,MAAO,IAAK,IAAK,eAAgB,IAAI,KAAM,EAI7C,IAAI,SAAW,IAAI,OAAO,OAE1B,QAAW,OAAO,KAAM,CACtB,IAAM,MAAQ,OAAO,IAAI,IAAI,MAAQ,EAAE,EACvC,SAAW,KAAK,IAAI,SAAU,MAAM,MAAM,EAG5C,MAAO,IAAK,IAAK,eAAgB,QAAS,EAC3C,EAMH,SAAS,YAAY,CAAC,QAAmF,CACvG,IAAM,IAA+B,CAAC,EACtC,QAAW,OAAO,QAChB,IAAI,IAAI,KAAO,IAAI,OAErB,OAAO,IAMT,SAAS,eAAe,CACtB,QACA,SACQ,CACR,IAAM,KAAO,WAAa,MAAQ,IAAI,QAAU,WAAa,SAAW,IAAI,WAAa,IAAI,MACvF,MAAQ,WAAa,MAAQ,IAAI,SAAW,WAAa,SAAW,IAAI,YAAc,IAAI,OAC1F,KAAO,WAAa,MAAQ,IAAI,KAAO,WAAa,SAAW,IAAI,QAAU,IAAI,MAEjF,SAAW,QAAQ,IAAI,CAAC,MAAQ,IAAI,WAAW,OAAO,IAAI,eAAiB,CAAC,CAAC,EAEnF,OAAO,KAAO,SAAS,KAAK,IAAI,EAAI,MAMtC,SAAS,YAAY,CACnB,QACA,IACA,OACQ,CACR,IAAM,MAAQ,QAAQ,IAAI,CAAC,MAAQ,CACjC,IAAM,MAAQ,OAAO,IAAI,IAAI,MAAQ,EAAE,EACvC,OAAO,WAAW,MAAO,IAAI,eAAgB,IAAI,OAAS,MAAM,EACjE,EAED,GAAI,OACF,OAAO,IAAI,SAAW,IAAM,MAAM,KAAK,IAAM,IAAI,SAAW,GAAG,EAAI,IAAM,IAAI,SAG/E,OAAO,MAAM,KAAK,IAAI,EAMxB,SAAS,UAAU,CAAC,MAAe,MAAe,MAA4C,CAE5F,GAAI,MAAM,OAAS,MACjB,OAAO,MAAM,MAAM,EAAG,MAAQ,CAAC,EAAI,IAIrC,IAAM,QAAU,MAAQ,MAAM,OAE9B,OAAQ,WACD,QACH,MAAO,IAAI,OAAO,OAAO,EAAI,UAC1B,SAAU,CACb,IAAM,QAAU,KAAK,MAAM,QAAU,CAAC,EAChC,SAAW,QAAU,QAC3B,MAAO,IAAI,OAAO,OAAO,EAAI,MAAQ,IAAI,OAAO,QAAQ,CAC1D,KACK,eAEH,OAAO,MAAQ,IAAI,OAAO,OAAO",
8
+ "debugId": "FFF09C5CBB835CB364756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,4 @@
1
+ function encodeKittyImage(pngData,opts){let b64=pngData.toString("base64"),chunks=splitIntoChunks(b64,4096);if(chunks.length===0)return`\x1B_G${buildParams(opts,0)};\x1B\\`;if(chunks.length===1)return`\x1B_G${buildParams(opts,0)};${chunks[0]}\x1B\\`;let parts=[];parts.push(`\x1B_G${buildParams(opts,1)};${chunks[0]}\x1B\\`);for(let i=1;i<chunks.length-1;i++)parts.push(`\x1B_Gm=1;${chunks[i]}\x1B\\`);return parts.push(`\x1B_Gm=0;${chunks[chunks.length-1]}\x1B\\`),parts.join("")}function deleteKittyImage(id){return`\x1B_Ga=d,d=i,i=${id}\x1B\\`}function isKittyGraphicsSupported(){let term=process.env.TERM??"",termProgram=process.env.TERM_PROGRAM??"";if(term==="xterm-kitty"||termProgram==="kitty")return!0;if(termProgram==="WezTerm")return!0;if(termProgram==="ghostty")return!0;if(termProgram==="konsole")return!0;return!1}function buildParams(opts,more){let parts=["a=T","f=100",`m=${more}`];if(opts?.width!=null)parts.push(`s=${opts.width}`);if(opts?.height!=null)parts.push(`v=${opts.height}`);if(opts?.id!=null)parts.push(`i=${opts.id}`);return parts.join(",")}function splitIntoChunks(str,size){if(str.length===0)return[];let chunks=[];for(let i=0;i<str.length;i+=size)chunks.push(str.slice(i,i+size));return chunks}function encodeSixel(imageData){let{width,height,data}=imageData;if(width===0||height===0||data.length===0)return"\x1BPq\x1B\\";let palette=new Map,pixelColors=new Uint16Array(width*height),nextColorIndex=1;for(let y=0;y<height;y++)for(let x=0;x<width;x++){let offset=(y*width+x)*4,r=data[offset],g=data[offset+1],b=data[offset+2];if(data[offset+3]<128)continue;let qr=r>>2&63,qg=g>>2&63,qb=b>>2&63,key=`${qr},${qg},${qb}`,idx=palette.get(key);if(idx==null)if(nextColorIndex>=256)idx=1;else idx=nextColorIndex++,palette.set(key,idx);pixelColors[y*width+x]=idx}let parts=[];parts.push(`"1;1;${width};${height}`);for(let[key,idx]of palette){let[qr,qg,qb]=key.split(",").map(Number),rPct=Math.round(qr/63*100),gPct=Math.round(qg/63*100),bPct=Math.round(qb/63*100);parts.push(`#${idx};2;${rPct};${gPct};${bPct}`)}for(let bandY=0;bandY<height;bandY+=6){if(bandY>0)parts.push("-");let bandColors=new Set;for(let dy=0;dy<6&&bandY+dy<height;dy++)for(let x=0;x<width;x++){let ci=pixelColors[(bandY+dy)*width+x];if(ci>0)bandColors.add(ci)}let first=!0;for(let colorIdx of bandColors){if(!first)parts.push("$");first=!1,parts.push(`#${colorIdx}`);for(let x=0;x<width;x++){let sixelBits=0;for(let dy=0;dy<6;dy++){let y=bandY+dy;if(y<height&&pixelColors[y*width+x]===colorIdx)sixelBits|=1<<dy}parts.push(String.fromCharCode(sixelBits+63))}}}return`\x1BPq${parts.join("")}\x1B\\`}function isSixelSupported(){let term=process.env.TERM??"",termProgram=process.env.TERM_PROGRAM??"";if(termProgram==="mlterm"||term.startsWith("mlterm"))return!0;if(termProgram==="foot"||term==="foot"||term==="foot-extra")return!0;if(termProgram==="WezTerm")return!0;if(termProgram==="mintty")return!0;return!1}import{readFileSync}from"node:fs";import{useContext as useContext2,useEffect,useMemo,useRef as useRef2}from"react";import{createContext}from"react";var TermContext=createContext(null),NodeContext=createContext(null),StdoutContext=createContext(null),StderrContext=createContext(null),RuntimeContext=createContext(null),FocusManagerContext=createContext(null);import{useContext,useLayoutEffect,useReducer,useRef}from"react";function rectEqual(a,b){if(a===b)return!0;if(!a||!b)return!1;return a.x===b.x&&a.y===b.y&&a.width===b.width&&a.height===b.height}function useContentRect(){let node=useContext(NodeContext),[,forceUpdate]=useReducer((x)=>x+1,0);return useLayoutEffect(()=>{if(!node)return;let handleLayoutComplete=()=>{if(!rectEqual(node.prevLayout,node.contentRect))forceUpdate()};return node.layoutSubscribers.add(handleLayoutComplete),()=>{node.layoutSubscribers.delete(handleLayoutComplete)}},[node]),node?.contentRect??{x:0,y:0,width:0,height:0}}import{jsxDEV}from"react/jsx-dev-runtime";function detectProtocol(preferred){if(preferred==="kitty")return isKittyGraphicsSupported()?"kitty":null;if(preferred==="sixel")return isSixelSupported()?"sixel":null;if(isKittyGraphicsSupported())return"kitty";if(isSixelSupported())return"sixel";return null}var nextImageId=1;function Image({src,width:requestedWidth,height:requestedHeight,fallback="[image]",protocol:preferredProtocol="auto"}){let contentRect=useContentRect(),stdoutCtx=useContext2(StdoutContext),imageIdRef=useRef2(null),pngData=useMemo(()=>{if(Buffer.isBuffer(src))return src;try{return readFileSync(src)}catch{return null}},[src]),effectiveWidth=requestedWidth??contentRect.width,effectiveHeight=requestedHeight??Math.max(1,Math.floor(effectiveWidth/2)),activeProtocol=useMemo(()=>detectProtocol(preferredProtocol),[preferredProtocol]);if(activeProtocol==="kitty"&&imageIdRef.current==null)imageIdRef.current=nextImageId++;if(useEffect(()=>{if(!pngData||!stdoutCtx||!activeProtocol)return;if(effectiveWidth<=0||effectiveHeight<=0)return;let{write}=stdoutCtx;if(activeProtocol==="kitty"){let seq=encodeKittyImage(pngData,{width:effectiveWidth,height:effectiveHeight,id:imageIdRef.current??void 0});write(seq)}},[pngData,stdoutCtx,activeProtocol,effectiveWidth,effectiveHeight]),useEffect(()=>{let id=imageIdRef.current;if(activeProtocol!=="kitty"||id==null||!stdoutCtx)return;return()=>{stdoutCtx.write(deleteKittyImage(id))}},[activeProtocol,stdoutCtx]),!activeProtocol||!pngData)return jsxDEV("silvery-box",{width:effectiveWidth,height:effectiveHeight,children:jsxDEV("silvery-text",{children:fallback},void 0,!1,void 0,this)},void 0,!1,void 0,this);let spaceLine=" ".repeat(Math.max(0,effectiveWidth)),spaceContent=Array.from({length:Math.max(0,effectiveHeight)},()=>spaceLine).join(`
2
+ `);return jsxDEV("silvery-box",{width:effectiveWidth,height:effectiveHeight,children:jsxDEV("silvery-text",{children:spaceContent},void 0,!1,void 0,this)},void 0,!1,void 0,this)}export{isSixelSupported,isKittyGraphicsSupported,encodeSixel,encodeKittyImage,deleteKittyImage,Image};
3
+
4
+ //# debugId=4C8FB7BC1ACBEC3E64756E2164756E21
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../packages/ag-react/src/ui/image/kitty-graphics.ts", "../../packages/ag-react/src/ui/image/sixel-encoder.ts", "../../packages/ag-react/src/ui/image/Image.tsx", "../../packages/ag-react/src/context.ts", "../../packages/ag-react/src/hooks/useLayout.ts", "../../packages/ag/src/types.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Kitty Graphics Protocol\n *\n * Encodes and manages images using the Kitty terminal graphics protocol.\n * Images are transmitted as base64-encoded PNG data via APC (Application\n * Program Command) escape sequences.\n *\n * Protocol reference: https://sw.kovidgoyal.net/kitty/graphics-protocol/\n *\n * Key concepts:\n * - `a=T` — transmit and display the image\n * - `f=100` — format is PNG (raw PNG data, terminal decodes it)\n * - `m=0|1` — 0 = last/only chunk, 1 = more chunks follow\n * - Chunks should be <= 4096 bytes of base64 to avoid overwhelming the terminal\n * - Images can be assigned an `i=<id>` for later deletion\n */\n\nconst APC_START = \"\\x1b_G\"\nconst ST = \"\\x1b\\\\\"\n\n/** Maximum base64 bytes per chunk (Kitty recommendation) */\nconst MAX_CHUNK_SIZE = 4096\n\nexport interface KittyImageOptions {\n /** Image width in terminal columns */\n width?: number\n /** Image height in terminal rows */\n height?: number\n /** Image ID for later reference/deletion (positive integer) */\n id?: number\n}\n\n/**\n * Encode a PNG image into Kitty graphics protocol escape sequences.\n *\n * The image data is base64-encoded and split into chunks of <= 4096 bytes.\n * Each chunk is wrapped in an APC escape sequence. The first chunk carries\n * the image metadata (action, format, dimensions, ID). Subsequent chunks\n * only carry `m=1` or `m=0` to indicate continuation.\n *\n * @param pngData - Raw PNG image data\n * @param opts - Optional dimensions and ID\n * @returns A string containing the complete escape sequence(s)\n *\n * @example\n * ```ts\n * import { readFileSync } from \"fs\"\n * import { encodeKittyImage } from \"@silvery/ag-react\"\n *\n * const png = readFileSync(\"photo.png\")\n * const seq = encodeKittyImage(png, { width: 40, height: 20 })\n * process.stdout.write(seq)\n * ```\n */\nexport function encodeKittyImage(pngData: Buffer, opts?: KittyImageOptions): string {\n const b64 = pngData.toString(\"base64\")\n const chunks = splitIntoChunks(b64, MAX_CHUNK_SIZE)\n\n if (chunks.length === 0) {\n // Empty image — send a single empty payload\n return `${APC_START}${buildParams(opts, 0)};${ST}`\n }\n\n if (chunks.length === 1) {\n // Single chunk — m=0 (last/only)\n return `${APC_START}${buildParams(opts, 0)};${chunks[0]}${ST}`\n }\n\n // Multiple chunks\n const parts: string[] = []\n\n // First chunk carries full metadata, m=1 (more follows)\n parts.push(`${APC_START}${buildParams(opts, 1)};${chunks[0]}${ST}`)\n\n // Middle chunks — only m=1\n for (let i = 1; i < chunks.length - 1; i++) {\n parts.push(`${APC_START}m=1;${chunks[i]}${ST}`)\n }\n\n // Last chunk — m=0\n parts.push(`${APC_START}m=0;${chunks[chunks.length - 1]}${ST}`)\n\n return parts.join(\"\")\n}\n\n/**\n * Generate an escape sequence to delete a Kitty image by ID.\n *\n * Uses `a=d` (delete) with `d=i` (delete by image ID).\n *\n * @param id - The image ID to delete\n * @returns The delete escape sequence\n *\n * @example\n * ```ts\n * process.stdout.write(deleteKittyImage(42))\n * ```\n */\nexport function deleteKittyImage(id: number): string {\n return `${APC_START}a=d,d=i,i=${id}${ST}`\n}\n\n/**\n * Check if the current terminal likely supports the Kitty graphics protocol.\n *\n * This is a heuristic based on `TERM` and `TERM_PROGRAM` environment variables.\n * For definitive detection, use a terminal query (send the graphics protocol\n * query and check for a response), but that requires async I/O.\n *\n * Known supporting terminals: Kitty, WezTerm, Ghostty (partial), Konsole (partial).\n *\n * @returns `true` if the terminal likely supports Kitty graphics\n */\nexport function isKittyGraphicsSupported(): boolean {\n const term = process.env.TERM ?? \"\"\n const termProgram = process.env.TERM_PROGRAM ?? \"\"\n\n // Kitty terminal\n if (term === \"xterm-kitty\" || termProgram === \"kitty\") return true\n\n // WezTerm supports Kitty graphics protocol\n if (termProgram === \"WezTerm\") return true\n\n // Ghostty supports Kitty graphics\n if (termProgram === \"ghostty\") return true\n\n // Konsole 22.04+ supports Kitty graphics\n if (termProgram === \"konsole\") return true\n\n return false\n}\n\n// ============================================================================\n// Internal helpers\n// ============================================================================\n\n/**\n * Build the Kitty graphics protocol parameter string for the first chunk.\n */\nfunction buildParams(opts: KittyImageOptions | undefined, more: 0 | 1): string {\n const parts = [`a=T`, `f=100`, `m=${more}`]\n\n if (opts?.width != null) parts.push(`s=${opts.width}`)\n if (opts?.height != null) parts.push(`v=${opts.height}`)\n if (opts?.id != null) parts.push(`i=${opts.id}`)\n\n return parts.join(\",\")\n}\n\n/**\n * Split a string into chunks of at most `size` characters.\n */\nfunction splitIntoChunks(str: string, size: number): string[] {\n if (str.length === 0) return []\n\n const chunks: string[] = []\n for (let i = 0; i < str.length; i += size) {\n chunks.push(str.slice(i, i + size))\n }\n return chunks\n}\n",
6
+ "/**\n * Sixel Encoder (Minimal Implementation)\n *\n * Sixel is an older image protocol supported by terminals like xterm, mlterm,\n * foot, and some others. Images are encoded as DCS (Device Control String)\n * sequences where each character encodes 6 vertical pixels.\n *\n * DCS format: `ESC P <params> q <sixel-data> ESC \\`\n *\n * This is a minimal implementation that produces valid Sixel output for\n * simple images. For production use with complex images, consider using\n * a dedicated Sixel library that handles color quantization and dithering.\n *\n * Protocol reference: https://en.wikipedia.org/wiki/Sixel\n *\n * TODO: Full Sixel encoding with proper color quantization, dithering,\n * and compression. The current implementation handles basic RGBA image data\n * with a simple nearest-color palette approach.\n */\n\nconst DCS_START = \"\\x1bP\"\nconst ST = \"\\x1b\\\\\"\n\n/** Sixel introduces a color with `#<index>;2;<r>;<g>;<b>` (RGB percentages 0-100) */\nconst SIXEL_NEWLINE = \"-\"\n\nexport interface SixelImageData {\n /** Image width in pixels */\n width: number\n /** Image height in pixels */\n height: number\n /** RGBA pixel data (4 bytes per pixel: R, G, B, A), row-major order */\n data: Uint8Array\n}\n\n/**\n * Encode RGBA image data as a Sixel escape sequence.\n *\n * This is a basic implementation that:\n * 1. Quantizes colors to a small palette (up to 256 colors)\n * 2. Encodes 6-row bands as Sixel characters\n * 3. Wraps in a DCS escape sequence\n *\n * For transparent pixels (alpha < 128), the background shows through.\n *\n * @param imageData - Image dimensions and RGBA pixel data\n * @returns A DCS escape sequence containing the Sixel-encoded image\n *\n * @example\n * ```ts\n * const img = { width: 10, height: 12, data: new Uint8Array(10 * 12 * 4) }\n * const seq = encodeSixel(img)\n * process.stdout.write(seq)\n * ```\n */\nexport function encodeSixel(imageData: SixelImageData): string {\n const { width, height, data } = imageData\n\n if (width === 0 || height === 0 || data.length === 0) {\n return `${DCS_START}q${ST}`\n }\n\n // Build a simple palette by collecting unique (quantized) colors\n const palette = new Map<string, number>()\n const pixelColors = new Uint16Array(width * height) // palette index per pixel (0 = transparent)\n let nextColorIndex = 1 // 0 reserved for transparent/background\n\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const offset = (y * width + x) * 4\n const r = data[offset]!\n const g = data[offset + 1]!\n const b = data[offset + 2]!\n const a = data[offset + 3]!\n\n if (a < 128) {\n // Transparent — leave as 0\n continue\n }\n\n // Quantize to 6-bit per channel (64 levels) to keep palette small\n const qr = (r >> 2) & 0x3f\n const qg = (g >> 2) & 0x3f\n const qb = (b >> 2) & 0x3f\n const key = `${qr},${qg},${qb}`\n\n let idx = palette.get(key)\n if (idx == null) {\n if (nextColorIndex >= 256) {\n // Palette full — find closest existing color (simple fallback)\n idx = 1\n } else {\n idx = nextColorIndex++\n palette.set(key, idx)\n }\n }\n\n pixelColors[y * width + x] = idx\n }\n }\n\n // Build Sixel data\n const parts: string[] = []\n\n // Raster attributes: Pan;Pad;Ph;Pv (aspect ratio 1:1, width, height)\n parts.push(`\"1;1;${width};${height}`)\n\n // Define palette colors\n for (const [key, idx] of palette) {\n const [qr, qg, qb] = key.split(\",\").map(Number)\n // Convert from 6-bit (0-63) to percentage (0-100)\n const rPct = Math.round((qr! / 63) * 100)\n const gPct = Math.round((qg! / 63) * 100)\n const bPct = Math.round((qb! / 63) * 100)\n parts.push(`#${idx};2;${rPct};${gPct};${bPct}`)\n }\n\n // Encode pixel data in 6-row bands\n for (let bandY = 0; bandY < height; bandY += 6) {\n if (bandY > 0) {\n parts.push(SIXEL_NEWLINE) // Move to next sixel row\n }\n\n // For each color in the palette, emit the sixel row\n // (Only emit colors that appear in this band)\n const bandColors = new Set<number>()\n for (let dy = 0; dy < 6 && bandY + dy < height; dy++) {\n for (let x = 0; x < width; x++) {\n const ci = pixelColors[(bandY + dy) * width + x]!\n if (ci > 0) bandColors.add(ci)\n }\n }\n\n let first = true\n for (const colorIdx of bandColors) {\n if (!first) {\n parts.push(\"$\") // Carriage return within sixel line (reposition to start)\n }\n first = false\n\n parts.push(`#${colorIdx}`)\n\n // Build the sixel characters for this color in this band\n for (let x = 0; x < width; x++) {\n let sixelBits = 0\n for (let dy = 0; dy < 6; dy++) {\n const y = bandY + dy\n if (y < height && pixelColors[y * width + x] === colorIdx) {\n sixelBits |= 1 << dy\n }\n }\n // Sixel character = bits + 63 (0x3F)\n parts.push(String.fromCharCode(sixelBits + 63))\n }\n }\n }\n\n return `${DCS_START}q${parts.join(\"\")}${ST}`\n}\n\n/**\n * Check if the current terminal likely supports the Sixel protocol.\n *\n * This is a heuristic based on environment variables. For definitive\n * detection, send a DA1 (Device Attributes) query and check for \"4\"\n * in the response, but that requires async I/O.\n *\n * Known supporting terminals: xterm (with +sixel), mlterm, foot, mintty,\n * WezTerm, Contour, Sixel-enabled builds of various terminals.\n *\n * @returns `true` if the terminal likely supports Sixel\n */\nexport function isSixelSupported(): boolean {\n const term = process.env.TERM ?? \"\"\n const termProgram = process.env.TERM_PROGRAM ?? \"\"\n\n // mlterm supports Sixel natively\n if (termProgram === \"mlterm\" || term.startsWith(\"mlterm\")) return true\n\n // foot supports Sixel\n if (termProgram === \"foot\" || term === \"foot\" || term === \"foot-extra\") return true\n\n // WezTerm supports Sixel\n if (termProgram === \"WezTerm\") return true\n\n // mintty supports Sixel\n if (termProgram === \"mintty\") return true\n\n // xterm might support Sixel if compiled with +sixel\n // We can't know for sure from env alone, so we don't claim support\n // (the user can set protocol='sixel' explicitly)\n\n return false\n}\n",
7
+ "/**\n * Image Component\n *\n * Renders bitmap images in supported terminals using the Kitty graphics\n * protocol (primary) or Sixel (fallback). When neither is supported,\n * displays a text placeholder.\n *\n * Since terminal images are escape-sequence-based and don't fit the cell\n * buffer model, the component reserves visual space with a Box of the\n * requested dimensions and uses `useEffect` to write image data directly\n * to stdout after render.\n *\n * @example\n * ```tsx\n * import { readFileSync } from \"fs\"\n * import { Image } from \"@silvery/ag-react\"\n *\n * const png = readFileSync(\"photo.png\")\n * <Image src={png} width={40} height={20} />\n *\n * // With file path\n * <Image src=\"/path/to/image.png\" width={40} height={20} />\n *\n * // Auto-detect protocol, fall back to text\n * <Image src={png} width={40} height={20} fallback=\"[photo]\" />\n * ```\n */\n\nimport { readFileSync } from \"node:fs\"\nimport { type JSX, useContext, useEffect, useMemo, useRef } from \"react\"\nimport { StdoutContext } from \"@silvery/ag-react/context\"\nimport { useContentRect } from \"@silvery/ag-react/hooks/useLayout\"\nimport { encodeKittyImage, isKittyGraphicsSupported, deleteKittyImage } from \"./kitty-graphics\"\nimport { isSixelSupported } from \"./sixel-encoder\"\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type ImageProtocol = \"kitty\" | \"sixel\" | \"auto\"\n\nexport interface ImageProps {\n /** PNG image data (Buffer) or file path (string) to a PNG file */\n src: Buffer | string\n /** Width in terminal columns. If omitted, uses available width from layout. */\n width?: number\n /** Height in terminal rows. If omitted, defaults to half the width (rough aspect ratio). */\n height?: number\n /** Text to display when image rendering is not supported. Default: \"[image]\" */\n fallback?: string\n /** Which protocol to use. Default: \"auto\" (tries Kitty, then Sixel, then fallback) */\n protocol?: ImageProtocol\n}\n\n// ============================================================================\n// Protocol Detection\n// ============================================================================\n\n/**\n * Determine the best available image protocol.\n * Returns null if no image protocol is available.\n */\nfunction detectProtocol(preferred: ImageProtocol): \"kitty\" | \"sixel\" | null {\n if (preferred === \"kitty\") {\n return isKittyGraphicsSupported() ? \"kitty\" : null\n }\n if (preferred === \"sixel\") {\n return isSixelSupported() ? \"sixel\" : null\n }\n\n // Auto-detect: prefer Kitty, fall back to Sixel\n if (isKittyGraphicsSupported()) return \"kitty\"\n if (isSixelSupported()) return \"sixel\"\n return null\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\n/** Incrementing image ID counter for Kitty protocol */\nlet nextImageId = 1\n\n/**\n * Renders a bitmap image in the terminal.\n *\n * The component operates in two phases:\n * 1. **Layout phase**: Renders a Box that reserves the visual space\n * (filled with spaces so the cell buffer has the right dimensions).\n * 2. **Effect phase**: After render, writes the image escape sequence\n * directly to stdout, positioned over the reserved space.\n *\n * When image protocols are not available, the fallback text is shown instead.\n */\nexport function Image({\n src,\n width: requestedWidth,\n height: requestedHeight,\n fallback = \"[image]\",\n protocol: preferredProtocol = \"auto\",\n}: ImageProps): JSX.Element {\n const contentRect = useContentRect()\n const stdoutCtx = useContext(StdoutContext)\n const imageIdRef = useRef<number | null>(null)\n\n // Resolve image data\n const pngData = useMemo(() => {\n if (Buffer.isBuffer(src)) return src\n // String path — read file synchronously (during render is fine for a path)\n try {\n return readFileSync(src)\n } catch {\n return null\n }\n }, [src])\n\n // Determine effective dimensions\n const effectiveWidth = requestedWidth ?? contentRect.width\n const effectiveHeight = requestedHeight ?? Math.max(1, Math.floor(effectiveWidth / 2))\n\n // Detect protocol support\n const activeProtocol = useMemo(() => detectProtocol(preferredProtocol), [preferredProtocol])\n\n // Assign a stable image ID for Kitty (for cleanup on unmount)\n if (activeProtocol === \"kitty\" && imageIdRef.current == null) {\n imageIdRef.current = nextImageId++\n }\n\n // Write image escape sequences after render\n useEffect(() => {\n if (!pngData || !stdoutCtx || !activeProtocol) return\n if (effectiveWidth <= 0 || effectiveHeight <= 0) return\n\n const { write } = stdoutCtx\n\n if (activeProtocol === \"kitty\") {\n const seq = encodeKittyImage(pngData, {\n width: effectiveWidth,\n height: effectiveHeight,\n id: imageIdRef.current ?? undefined,\n })\n write(seq)\n } else if (activeProtocol === \"sixel\") {\n // For Sixel, we would need the decoded pixel data.\n // Since we receive PNG, and decoding PNG requires a library,\n // Sixel rendering from raw PNG is deferred. The Kitty protocol\n // can transmit PNG directly (f=100), but Sixel cannot.\n // For now, Sixel only works if src is already decoded pixel data.\n // This is a known limitation noted in the module docs.\n //\n // If someone passes a Buffer that's already RGBA pixel data\n // (not PNG), this would need a flag. For now, Sixel falls through\n // to fallback when src is PNG.\n }\n }, [pngData, stdoutCtx, activeProtocol, effectiveWidth, effectiveHeight])\n\n // Cleanup: delete Kitty image on unmount\n useEffect(() => {\n const id = imageIdRef.current\n if (activeProtocol !== \"kitty\" || id == null || !stdoutCtx) return\n\n return () => {\n stdoutCtx.write(deleteKittyImage(id))\n }\n }, [activeProtocol, stdoutCtx])\n\n // If no protocol or no image data, render fallback text\n if (!activeProtocol || !pngData) {\n return (\n <silvery-box width={effectiveWidth} height={effectiveHeight}>\n <silvery-text>{fallback}</silvery-text>\n </silvery-box>\n )\n }\n\n // Reserve visual space with an empty box.\n // The image is drawn over this space via stdout escape sequences.\n // Fill with spaces so the cell buffer allocates the right area.\n const spaceLine = \" \".repeat(Math.max(0, effectiveWidth))\n const spaceContent = Array.from({ length: Math.max(0, effectiveHeight) }, () => spaceLine).join(\"\\n\")\n\n return (\n <silvery-box width={effectiveWidth} height={effectiveHeight}>\n <silvery-text>{spaceContent}</silvery-text>\n </silvery-box>\n )\n}\n",
8
+ "/**\n * Silvery React Contexts\n *\n * Provides contexts for:\n * - TermContext: Access to Term instance (for styling/detection)\n * - NodeContext: Access to the current SilveryNode (for useContentRect)\n * - RuntimeContext: Unified input/app controls (replaces Events/Input/Stdin/App contexts)\n * - StdoutContext: Access to stdout\n * - StderrContext: Access to stderr\n */\n\nimport type { Term } from \"@silvery/ag-term/ansi\"\nimport { createContext } from \"react\"\nimport type { FocusManager } from \"@silvery/ag/focus-manager\"\nimport type { Key } from \"@silvery/ag/keys\"\nimport type { AgNode } from \"@silvery/ag/types\"\n\n// ============================================================================\n// Term Context\n// ============================================================================\n\n/**\n * Context that provides access to the Term instance.\n * Used by useTerm() hook to access terminal capabilities and styling.\n */\nexport const TermContext = createContext<Term | null>(null)\n\n// ============================================================================\n// Node Context\n// ============================================================================\n\n/**\n * Context that provides access to the current SilveryNode.\n * Used by useContentRect() to subscribe to layout changes.\n *\n * Each Box component wraps its children in a NodeContext.Provider\n * with its corresponding SilveryNode.\n */\nexport const NodeContext = createContext<AgNode | null>(null)\n\n// ============================================================================\n// Stdio Context\n// ============================================================================\n\nexport interface StdoutContextValue {\n /** Standard output stream */\n stdout: NodeJS.WriteStream\n /** Write to stdout */\n write: (data: string) => void\n /**\n * Notify the scheduler that lines were written to stdout externally.\n * Used by useScrollback to report lines written between renders so that\n * inline mode cursor positioning accounts for the displacement.\n */\n notifyScrollback?: (lines: number) => void\n /**\n * Reset inline cursor state in the output phase.\n * Used by useScrollback on resize to clear cursor tracking before\n * re-emitting frozen items at the new width.\n */\n resetInlineCursor?: () => void\n /**\n * Get inline cursor row relative to render region start. -1 if unknown.\n * Used by useScrollback to position frozen items at the render region start.\n */\n getInlineCursorRow?: () => number\n /**\n * Promote frozen content to scrollback via the output phase.\n * Instead of writing directly to stdout (which causes flicker),\n * this passes the content to the output phase which writes frozen content\n * + live content in a single target.write() — no blanking, no cursor desync.\n */\n promoteScrollback?: (frozenContent: string, frozenLineCount: number) => void\n}\n\n/**\n * Context for stdout access.\n * Used by useStdout() hook.\n */\nexport const StdoutContext = createContext<StdoutContextValue | null>(null)\n\nexport interface StderrContextValue {\n /** Standard error stream */\n stderr: NodeJS.WriteStream\n /** Write to stderr */\n write: (data: string) => void\n}\n\n/**\n * Context for stderr access.\n * Used by useStderr() hook.\n */\nexport const StderrContext = createContext<StderrContextValue | null>(null)\n\n// ============================================================================\n// Runtime Context (typed bidirectional event bus — TEA)\n// ============================================================================\n\n/**\n * Base events every runtime provides.\n * Apps extend this to add custom events (e.g., BoardEvents adds \"op\").\n */\nexport interface BaseRuntimeEvents {\n /** Keyboard input: [parsedInput, keyMetadata] */\n input: [input: string, key: Key]\n /** Bracketed paste: [pastedText] */\n paste: [text: string]\n /** Terminal window focus change: [isFocused] */\n focus: [focused: boolean]\n}\n\n/**\n * Extract handler function type from an event map entry.\n */\ntype EventHandler<Args extends unknown[]> = (...args: Args) => void\n\n/**\n * Typed bidirectional event bus + app lifecycle controls.\n *\n * Replaces EventsContext, InputContext, StdinContext, and AppContext with\n * a single typed interface. Components never see stdin or raw mode.\n *\n * Generic parameter E extends BaseRuntimeEvents — all runtimes provide\n * at least \"input\" and \"paste\" events. Apps can extend with custom events:\n *\n * ```tsx\n * interface BoardEvents extends BaseRuntimeEvents {\n * op: [BoardOp]\n * }\n * const rt = useRuntime<BoardEvents>()\n * rt?.on(\"input\", handler) // runtime → view\n * rt?.emit(\"op\", { type: \"cursor_down\" }) // view → runtime\n * ```\n *\n * Present in interactive mode (run/render/createApp/test renderer).\n * Absent (null) in static mode (renderStatic).\n */\nexport interface RuntimeContextValue<E extends BaseRuntimeEvents = BaseRuntimeEvents> {\n /** Subscribe to a typed event. Returns cleanup function. */\n on<K extends string & keyof E>(event: K, handler: EventHandler<E[K] extends unknown[] ? E[K] : never>): () => void\n /** Emit a typed event (view → runtime). */\n emit<K extends string & keyof E>(event: K, ...args: E[K] extends unknown[] ? E[K] : never): void\n /** Exit the application with optional error. */\n exit: (error?: Error) => void\n /** Pause rendering output (for screen switching). */\n pause?: () => void\n /** Resume rendering after pause. */\n resume?: () => void\n}\n\n/**\n * Context that provides the typed runtime event bus.\n *\n * When non-null: interactive mode — useInput works, components can subscribe\n * to events via rt.on() and emit via rt.emit().\n *\n * When null: static mode — useInput throws (by design), use useRuntime()\n * for components that need to work in both modes.\n */\nexport const RuntimeContext = createContext<RuntimeContextValue | null>(null)\n\n// ============================================================================\n// Focus Manager Context (tree-based focus system)\n// ============================================================================\n\n/**\n * Context for the tree-based focus manager.\n * Provides the FocusManager instance to useFocusable(), useFocusWithin(), and useFocusManager() hooks.\n */\nexport const FocusManagerContext = createContext<FocusManager | null>(null)\n",
9
+ "/**\n * Layout Hooks\n *\n * Hooks for accessing element positions in silvery components.\n *\n * Two coordinate systems:\n * - Content rect: Position within scrollable content (like CSS offsetTop/offsetLeft)\n * - Screen rect: Position on terminal screen (like CSS getBoundingClientRect)\n */\n\nimport { useContext, useLayoutEffect, useReducer, useRef } from \"react\"\nimport { NodeContext } from \"../context\"\nimport { type Rect, rectEqual } from \"@silvery/ag/types\"\n\nexport type { Rect }\n\n// ============================================================================\n// Content Rect Hooks (position within scrollable content)\n// ============================================================================\n\n/**\n * Returns the content-relative position for the current component.\n * Like CSS offsetTop/offsetLeft - position within scrollable content.\n *\n * On first render, returns { x: 0, y: 0, width: 0, height: 0 }.\n * After layout completes, automatically re-renders with actual dimensions.\n *\n * @example\n * ```tsx\n * function Header() {\n * const { width } = useContentRect();\n * return <Text>{'='.repeat(width)}</Text>;\n * }\n * ```\n */\nexport function useContentRect(): Rect {\n const node = useContext(NodeContext)\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0)\n\n useLayoutEffect(() => {\n if (!node) return\n\n const handleLayoutComplete = () => {\n if (!rectEqual(node.prevLayout, node.contentRect)) {\n forceUpdate()\n }\n }\n\n node.layoutSubscribers.add(handleLayoutComplete)\n return () => {\n node.layoutSubscribers.delete(handleLayoutComplete)\n }\n }, [node])\n\n return node?.contentRect ?? { x: 0, y: 0, width: 0, height: 0 }\n}\n\n/**\n * Callback invoked with content-relative position after render.\n * Does NOT cause re-renders - use for position registration in large lists.\n *\n * @example\n * ```tsx\n * function Card({ id, onLayout }) {\n * useContentRectCallback((rect) => {\n * onLayout(id, rect);\n * });\n * return <Box>...</Box>;\n * }\n * ```\n */\nexport function useContentRectCallback(callback: (rect: Rect) => void): void {\n const node = useContext(NodeContext)\n\n // Use ref to always have current callback without re-subscribing\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useLayoutEffect(() => {\n if (!node) return\n\n const handleLayoutComplete = () => {\n if (node.contentRect) {\n callbackRef.current(node.contentRect)\n }\n }\n\n node.layoutSubscribers.add(handleLayoutComplete)\n\n // Also call immediately if layout already computed\n if (node.contentRect) {\n callbackRef.current(node.contentRect)\n }\n\n return () => {\n node.layoutSubscribers.delete(handleLayoutComplete)\n }\n }, [node])\n}\n\n// ============================================================================\n// Screen Rect Hooks (position on terminal screen)\n// ============================================================================\n\n/**\n * Returns the screen-relative position for the current component.\n * Like CSS getBoundingClientRect - actual position on terminal screen.\n *\n * Accounts for scroll offsets of all ancestor containers.\n * Use this for visual navigation between columns with different scroll positions.\n *\n * On first render, returns { x: 0, y: 0, width: 0, height: 0 }.\n * After layout completes, automatically re-renders with actual dimensions.\n *\n * @example\n * ```tsx\n * function Card({ id }) {\n * const { y } = useScreenRect();\n * // y is the actual screen row, accounting for scroll\n * return <Box>Card at screen row {y}</Box>;\n * }\n * ```\n */\nexport function useScreenRect(): Rect {\n const node = useContext(NodeContext)\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0)\n const prevScreenRectRef = useRef<Rect | null>(null)\n\n useLayoutEffect(() => {\n if (!node) return\n\n const handleLayoutComplete = () => {\n // Re-render when screenRect changes (can happen from scroll offset changes\n // even when contentRect stays the same)\n if (!rectEqual(prevScreenRectRef.current, node.screenRect)) {\n prevScreenRectRef.current = node.screenRect\n forceUpdate()\n }\n }\n\n node.layoutSubscribers.add(handleLayoutComplete)\n return () => {\n node.layoutSubscribers.delete(handleLayoutComplete)\n }\n }, [node])\n\n return node?.screenRect ?? { x: 0, y: 0, width: 0, height: 0 }\n}\n\n/**\n * Callback invoked with screen-relative position after render.\n * Does NOT cause re-renders - use for position registration in large lists.\n *\n * This is the recommended hook for cross-column visual navigation:\n * register card positions with screen coordinates to find cards at\n * the same visual Y position regardless of column scroll state.\n *\n * @example\n * ```tsx\n * function Card({ id, onLayout }) {\n * useScreenRectCallback((rect) => {\n * // rect.y is screen position, accounting for scroll\n * onLayout(id, rect.y);\n * });\n * return <Box>...</Box>;\n * }\n * ```\n */\nexport function useScreenRectCallback(callback: (rect: Rect) => void): void {\n const node = useContext(NodeContext)\n\n // Use ref to always have current callback without re-subscribing\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useLayoutEffect(() => {\n if (!node) return\n\n const handleLayoutComplete = () => {\n if (node.screenRect) {\n callbackRef.current(node.screenRect)\n }\n }\n\n node.layoutSubscribers.add(handleLayoutComplete)\n\n // Also call immediately if screen rect already computed\n if (node.screenRect) {\n callbackRef.current(node.screenRect)\n }\n\n return () => {\n node.layoutSubscribers.delete(handleLayoutComplete)\n }\n }, [node])\n}\n\n// ============================================================================\n// Render Rect Hooks (actual render position, accounting for sticky offsets)\n// ============================================================================\n\n/**\n * Returns the actual render position for the current component.\n * For non-sticky nodes, this equals `useScreenRect()`.\n * For sticky nodes (position=\"sticky\"), this accounts for sticky render\n * offsets — the position where pixels are actually painted on screen.\n *\n * Use this for hit testing, cursor positioning, and any feature that\n * needs to know where a node visually appears on screen.\n *\n * On first render, returns { x: 0, y: 0, width: 0, height: 0 }.\n * After layout completes, automatically re-renders with actual dimensions.\n *\n * @example\n * ```tsx\n * function StickyHeader() {\n * const { y } = useRenderRect();\n * // y is the actual render row, accounting for sticky offset\n * return <Box position=\"sticky\" stickyTop={0}>Header at row {y}</Box>;\n * }\n * ```\n */\nexport function useRenderRect(): Rect {\n const node = useContext(NodeContext)\n const [, forceUpdate] = useReducer((x: number) => x + 1, 0)\n const prevRenderRectRef = useRef<Rect | null>(null)\n\n useLayoutEffect(() => {\n if (!node) return\n\n const handleLayoutComplete = () => {\n // Re-render when renderRect changes (can happen from sticky offset\n // changes even when screenRect stays the same)\n if (!rectEqual(prevRenderRectRef.current, node.renderRect)) {\n prevRenderRectRef.current = node.renderRect\n forceUpdate()\n }\n }\n\n node.layoutSubscribers.add(handleLayoutComplete)\n return () => {\n node.layoutSubscribers.delete(handleLayoutComplete)\n }\n }, [node])\n\n return node?.renderRect ?? { x: 0, y: 0, width: 0, height: 0 }\n}\n\n/**\n * Callback invoked with actual render position after render.\n * Does NOT cause re-renders - use for position registration in large lists.\n *\n * For non-sticky nodes, the rect equals screenRect. For sticky nodes,\n * it reflects the actual render position accounting for sticky offsets.\n *\n * @example\n * ```tsx\n * function Card({ id, onLayout }) {\n * useRenderRectCallback((rect) => {\n * // rect.y is actual render row, accounting for sticky\n * onLayout(id, rect.y);\n * });\n * return <Box>...</Box>;\n * }\n * ```\n */\nexport function useRenderRectCallback(callback: (rect: Rect) => void): void {\n const node = useContext(NodeContext)\n\n // Use ref to always have current callback without re-subscribing\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useLayoutEffect(() => {\n if (!node) return\n\n const handleLayoutComplete = () => {\n if (node.renderRect) {\n callbackRef.current(node.renderRect)\n }\n }\n\n node.layoutSubscribers.add(handleLayoutComplete)\n\n // Also call immediately if render rect already computed\n if (node.renderRect) {\n callbackRef.current(node.renderRect)\n }\n\n return () => {\n node.layoutSubscribers.delete(handleLayoutComplete)\n }\n }, [node])\n}\n",
10
+ "/**\n * Silvery Types\n *\n * Core types for the Silvery renderer architecture.\n */\n\nimport type { FocusEventProps } from \"./focus-events\"\nimport type { LayoutNode } from \"@silvery/ag-term/layout-engine\"\nimport type { MouseEventProps } from \"@silvery/ag-term/mouse-events\"\n\n// ============================================================================\n// Layout Types\n// ============================================================================\n\n/**\n * A rectangle with position and size.\n * All values are in terminal columns/rows (integers).\n */\nexport interface Rect {\n /** X position (0-indexed terminal column) */\n x: number\n /** Y position (0-indexed terminal row) */\n y: number\n /** Width in terminal columns */\n width: number\n /** Height in terminal rows */\n height: number\n}\n\n/**\n * Check if two rects are equal (same position and size).\n */\nexport function rectEqual(a: Rect | null, b: Rect | null): boolean {\n if (a === b) return true\n if (!a || !b) return false\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height\n}\n\n// ============================================================================\n// Node Types\n// ============================================================================\n\n/**\n * Silvery node types - the primitive elements in the render tree.\n */\nexport type AgNodeType = \"silvery-root\" | \"silvery-box\" | \"silvery-text\"\n\n/**\n * Flexbox properties that can be applied to Box nodes.\n */\nexport interface FlexboxProps {\n // Size\n width?: number | string\n height?: number | string\n minWidth?: number | string\n minHeight?: number | string\n maxWidth?: number | string\n maxHeight?: number | string\n\n // Flex\n flexGrow?: number\n flexShrink?: number\n flexBasis?: number | string\n flexDirection?: \"row\" | \"column\" | \"row-reverse\" | \"column-reverse\"\n flexWrap?: \"nowrap\" | \"wrap\" | \"wrap-reverse\"\n\n // Alignment\n alignItems?: \"flex-start\" | \"flex-end\" | \"center\" | \"stretch\" | \"baseline\"\n alignSelf?: \"auto\" | \"flex-start\" | \"flex-end\" | \"center\" | \"stretch\" | \"baseline\"\n alignContent?: \"flex-start\" | \"flex-end\" | \"center\" | \"stretch\" | \"space-between\" | \"space-around\" | \"space-evenly\"\n justifyContent?: \"flex-start\" | \"flex-end\" | \"center\" | \"space-between\" | \"space-around\" | \"space-evenly\"\n\n // Spacing\n padding?: number\n paddingTop?: number\n paddingBottom?: number\n paddingLeft?: number\n paddingRight?: number\n paddingX?: number\n paddingY?: number\n margin?: number\n marginTop?: number\n marginBottom?: number\n marginLeft?: number\n marginRight?: number\n marginX?: number\n marginY?: number\n gap?: number\n columnGap?: number\n rowGap?: number\n\n // Position\n position?: \"relative\" | \"absolute\" | \"sticky\" | \"static\"\n\n // Position offsets (used with position='absolute' or position='relative')\n top?: number | string\n left?: number | string\n bottom?: number | string\n right?: number | string\n\n // Sticky offsets (only used when position='sticky')\n // The element will \"stick\" when it reaches this offset from the container edge\n stickyTop?: number\n stickyBottom?: number\n\n // Aspect ratio\n aspectRatio?: number\n\n // Display\n display?: \"flex\" | \"none\"\n\n // Overflow\n overflow?: \"visible\" | \"hidden\" | \"scroll\"\n overflowX?: \"visible\" | \"hidden\"\n overflowY?: \"visible\" | \"hidden\"\n\n // Scroll control (only used when overflow='scroll')\n /** Child index to ensure visible (edge-based: only scrolls if off-screen) */\n scrollTo?: number\n /** Explicit scroll offset in rows (used when scrollTo is undefined for frozen scroll state) */\n scrollOffset?: number\n}\n\n/**\n * Props for testing and identification.\n * These props are stored in the node for DOM query access.\n */\nexport interface TestProps {\n /** Element ID for DOM queries and visual debugging */\n id?: string\n /** Test ID for querying nodes (like Playwright's data-testid) */\n testID?: string\n /** Allow arbitrary data-* attributes for testing */\n [key: `data-${string}`]: unknown\n}\n\n/**\n * Underline style variants (SGR 4:x codes).\n * - false: no underline\n * - 'single': standard underline (SGR 4 or 4:1)\n * - 'double': double underline (SGR 4:2)\n * - 'curly': curly/wavy underline (SGR 4:3)\n * - 'dotted': dotted underline (SGR 4:4)\n * - 'dashed': dashed underline (SGR 4:5)\n */\nexport type UnderlineStyle = false | \"single\" | \"double\" | \"curly\" | \"dotted\" | \"dashed\"\n\n/**\n * Style properties for text rendering.\n */\nexport interface StyleProps {\n color?: string\n backgroundColor?: string\n bold?: boolean\n dim?: boolean\n /** Alias for dim (Ink compatibility) */\n dimColor?: boolean\n italic?: boolean\n /** Enable underline. Use underlineStyle for style variants. */\n underline?: boolean\n /**\n * Underline style variant: 'single' | 'double' | 'curly' | 'dotted' | 'dashed'.\n * Setting this implies underline=true. Takes precedence over underline prop.\n */\n underlineStyle?: UnderlineStyle\n /**\n * Underline color (independent of text color).\n * Uses SGR 58 (underline color). Falls back to text color if not specified.\n */\n underlineColor?: string\n strikethrough?: boolean\n inverse?: boolean\n}\n\n/**\n * Props for Box component.\n */\nexport interface BoxProps extends FlexboxProps, StyleProps, TestProps, MouseEventProps, FocusEventProps {\n borderStyle?: \"single\" | \"double\" | \"round\" | \"bold\" | \"singleDouble\" | \"doubleSingle\" | \"classic\"\n borderColor?: string\n borderTop?: boolean\n borderBottom?: boolean\n borderLeft?: boolean\n borderRight?: boolean\n\n /**\n * Outline style — renders border characters at the box edges without affecting layout.\n *\n * Unlike `borderStyle` which adds border dimensions to the layout (making the content\n * area smaller), `outlineStyle` draws border characters that OVERLAP the content area.\n * The layout engine sees no border at all — outline is purely visual.\n *\n * Use cases: selection indicators, hover highlights, focus rings — anything that\n * should visually frame a box without shifting content.\n */\n outlineStyle?: \"single\" | \"double\" | \"round\" | \"bold\" | \"singleDouble\" | \"doubleSingle\" | \"classic\"\n /** Foreground color for the outline */\n outlineColor?: string\n /** Apply dim styling to the outline */\n outlineDimColor?: boolean\n /** Show top outline edge (default: true) */\n outlineTop?: boolean\n /** Show bottom outline edge (default: true) */\n outlineBottom?: boolean\n /** Show left outline edge (default: true) */\n outlineLeft?: boolean\n /** Show right outline edge (default: true) */\n outlineRight?: boolean\n\n /**\n * Override theme for this subtree — $token colors resolve against this theme.\n * Pushed onto the context theme stack during content phase tree walk.\n */\n theme?: import(\"@silvery/theme\").Theme\n\n /** CSS pointer-events equivalent. \"none\" makes this node and its subtree invisible to hit testing. */\n pointerEvents?: \"auto\" | \"none\"\n\n onLayout?: (layout: Rect) => void\n\n /**\n * Show scroll overflow indicators (▲N / ▼N) for scrollable containers.\n *\n * For bordered containers, indicators appear on the border.\n * For borderless containers, indicators overlay the content at top-right/bottom-right.\n *\n * Only applies when overflow='scroll'.\n */\n overflowIndicator?: boolean\n}\n\n/**\n * Props for Text component.\n */\nexport interface TextProps extends StyleProps, TestProps, MouseEventProps {\n children?: React.ReactNode\n wrap?: \"wrap\" | \"truncate\" | \"truncate-start\" | \"truncate-middle\" | \"truncate-end\" | \"clip\" | boolean\n /** Internal transform function applied to each rendered line. Used by Transform component. */\n internal_transform?: (line: string, index: number) => string\n}\n\n/**\n * The core Silvery node - represents an element in the render tree.\n *\n * Each node has:\n * - A Yoga node for layout calculation\n * - Computed layout after Yoga runs\n * - Subscribers that get notified when layout changes\n * - Dirty flags for incremental updates\n */\nexport interface AgNode {\n /** Node type */\n type: AgNodeType\n\n /** Props passed to this node */\n props: BoxProps | TextProps | Record<string, unknown>\n\n /** Child nodes */\n children: AgNode[]\n\n /** Parent node (null for root) */\n parent: AgNode | null\n\n /** The layout node for layout calculation (null for raw text nodes) */\n layoutNode: LayoutNode | null\n\n /** Computed layout from previous render (for change detection) */\n prevLayout: Rect | null\n\n /**\n * Content-relative position (like CSS offsetTop/offsetLeft).\n * Position within the scrollable content, ignoring scroll offsets.\n * Set after layout phase.\n */\n contentRect: Rect | null\n\n /**\n * Screen-relative position (like CSS getBoundingClientRect).\n * Actual position on the terminal screen, accounting for scroll offsets.\n * Set after screen rect phase.\n *\n * Note: For sticky children, this reflects the node's layout position\n * adjusted for scroll offsets, NOT the actual render position. Use\n * `renderRect` for the actual pixel position on screen.\n */\n screenRect: Rect | null\n\n /** Previous screen rect (for change detection in notifyLayoutSubscribers) */\n prevScreenRect: Rect | null\n\n /**\n * Actual render position on the terminal screen.\n * For non-sticky nodes, this equals `screenRect`.\n * For sticky nodes (position=\"sticky\"), this accounts for sticky render\n * offsets — the position where pixels are actually painted.\n *\n * Use this for hit testing, cursor positioning, and any feature that\n * needs to know where a node visually appears on screen.\n * Set after screen rect phase.\n */\n renderRect: Rect | null\n\n /** Previous render rect (for change detection) */\n prevRenderRect: Rect | null\n\n /** True if layout changed THIS frame (position or size).\n * Set by propagateLayout in layout phase. Cleared by content phase.\n * This is the authoritative signal for \"did layout change?\" — unlike\n * !rectEqual(prevLayout, contentRect) which becomes stale when layout\n * phase skips (no dirty nodes). */\n layoutChangedThisFrame: boolean\n\n /** True if layout-affecting props changed and Yoga needs recalculation.\n * Set by reconciler on prop changes. Cleared after layout phase. */\n layoutDirty: boolean\n\n /** True if content changed but layout didn't (e.g., text content update).\n * Set by reconciler. Cleared by content phase after rendering.\n * NOTE: measure phase may clear this for its text-collection cache —\n * stylePropsDirty acts as the surviving witness for style changes. */\n contentDirty: boolean\n\n /** True if visual props changed (color, backgroundColor, borderStyle, etc.).\n * Set by reconciler alongside contentDirty. Survives measure phase clearing\n * of contentDirty, ensuring content phase still detects style changes.\n * Cleared by content phase after rendering. */\n stylePropsDirty: boolean\n\n /** True if backgroundColor specifically changed (added, modified, or removed).\n * Set by reconciler when backgroundColor prop changes. Used by content phase\n * to avoid cascading re-renders for border-only paint changes (borderColor\n * doesn't affect the content area). Cleared by content phase. */\n bgDirty: boolean\n\n /** True if this node or any descendant has dirty content/layout.\n * Propagated upward by reconciler when any descendant is dirtied.\n * When only subtreeDirty (no other flags), the node's OWN rendering is\n * skipped — only descendants are traversed. Cleared by content phase. */\n subtreeDirty: boolean\n\n /** True if direct children were added, removed, or reordered.\n * Set by reconciler on child list changes. Triggers own repaint\n * (gap regions may need clearing) and forces child re-render.\n * Cleared by content phase. */\n childrenDirty: boolean\n\n /** Callbacks subscribed to layout changes (used by useContentRect) */\n layoutSubscribers: Set<() => void>\n\n /** Text content for text nodes */\n textContent?: string\n\n /** True if this is a raw text node (created by createTextInstance) */\n isRawText?: boolean\n\n /** True if this node is hidden (for Suspense support) */\n hidden?: boolean\n\n /** Sticky children with computed render positions (for non-scroll containers).\n * When a parent has sticky children but is NOT a scroll container, this array\n * holds the computed render offsets. Same shape as scrollState.stickyChildren. */\n stickyChildren?: Array<{\n /** Index of the sticky child */\n index: number\n /** Computed Y offset to render at (relative to parent content area) */\n renderOffset: number\n /** Original natural Y position (relative to parent content area) */\n naturalTop: number\n /** Height of the sticky element */\n height: number\n }>\n\n /** Inline rects for virtual text nodes (no layout node). Computed during text rendering.\n * Array for wrapped text (one rect per line fragment). Enables hit testing on nested Text. */\n inlineRects?: Array<{ x: number; y: number; width: number; height: number }> | null\n\n /** Scroll state for overflow='scroll' containers */\n scrollState?: {\n /** Current scroll offset (in terminal rows) */\n offset: number\n /** Previous scroll offset from last render (for incremental rendering) */\n prevOffset: number\n /** Total content height (all children) */\n contentHeight: number\n /** Visible height (container height minus borders/padding) */\n viewportHeight: number\n /** Index of first visible child */\n firstVisibleChild: number\n /** Index of last visible child */\n lastVisibleChild: number\n /** Previous first visible child from last render (for incremental rendering) */\n prevFirstVisibleChild: number\n /** Previous last visible child from last render (for incremental rendering) */\n prevLastVisibleChild: number\n /** Count of items hidden above viewport */\n hiddenAbove: number\n /** Count of items hidden below viewport */\n hiddenBelow: number\n /** Sticky children with their computed render positions */\n stickyChildren?: Array<{\n /** Index of the sticky child */\n index: number\n /** Computed Y offset to render at (relative to viewport, not content) */\n renderOffset: number\n /** Original natural Y position (before sticky adjustment) */\n naturalTop: number\n /** Height of the sticky element */\n height: number\n }>\n }\n}\n\n// ============================================================================\n// Terminal Buffer Types\n// ============================================================================\n\n/**\n * Text attributes that can be applied to a cell.\n */\nexport interface CellAttrs {\n bold?: boolean\n dim?: boolean\n italic?: boolean\n /** Simple underline flag (for backwards compatibility) */\n underline?: boolean\n /**\n * Underline style: 'single' | 'double' | 'curly' | 'dotted' | 'dashed'.\n * When set, takes precedence over the underline boolean.\n */\n underlineStyle?: UnderlineStyle\n strikethrough?: boolean\n inverse?: boolean\n}\n\n/**\n * A single cell in the terminal buffer.\n */\nexport interface Cell {\n /** The character (grapheme cluster) in this cell */\n char: string\n /** Foreground color (ANSI code or RGB) */\n fg: string | null\n /** Background color (ANSI code or RGB) */\n bg: string | null\n /** Text attributes */\n attrs: CellAttrs\n /** True if this is a wide character (CJK) that takes 2 cells */\n wide: boolean\n /** True if this cell is the continuation of a wide character */\n continuation: boolean\n}\n\n/**\n * Interface for the terminal buffer.\n */\nexport interface TerminalBuffer {\n readonly width: number\n readonly height: number\n getCell(x: number, y: number): Cell\n setCell(x: number, y: number, cell: Cell): void\n clear(): void\n}\n\n// ============================================================================\n// Event Types\n// ============================================================================\n\n/**\n * Keyboard event with key information and modifiers.\n */\nexport interface KeyEvent {\n type: \"key\"\n /** The key pressed (character or key name like 'ArrowUp') */\n key: string\n /** Ctrl modifier was held */\n ctrl?: boolean\n /** Meta/Alt modifier was held */\n meta?: boolean\n /** Shift modifier was held */\n shift?: boolean\n /** Alt/Option modifier was held */\n alt?: boolean\n /** Super/Cmd modifier was held. Requires Kitty protocol. */\n super?: boolean\n /** Hyper modifier was held. Requires Kitty protocol. */\n hyper?: boolean\n /** Kitty event type. Requires Kitty flag 2. */\n eventType?: \"press\" | \"repeat\" | \"release\"\n /** CapsLock is active. Kitty modifier bit 6. */\n capsLock?: boolean\n /** NumLock is active. Kitty modifier bit 7. */\n numLock?: boolean\n}\n\n/**\n * Mouse event with position and button information.\n */\nexport interface MouseEvent {\n type: \"mouse\"\n /** X position in terminal columns (0-indexed) */\n x: number\n /** Y position in terminal rows (0-indexed) */\n y: number\n /** Mouse button (0=left, 1=middle, 2=right) */\n button: number\n /** Event action */\n action: \"down\" | \"up\" | \"move\" | \"wheel\"\n /** Wheel delta for scroll events */\n delta?: number\n}\n\n/**\n * Terminal resize event.\n */\nexport interface ResizeEvent {\n type: \"resize\"\n /** New width in columns */\n width: number\n /** New height in rows */\n height: number\n}\n\n/**\n * Terminal focus event.\n */\nexport interface FocusEvent {\n type: \"focus\"\n}\n\n/**\n * Terminal blur event.\n */\nexport interface BlurEvent {\n type: \"blur\"\n}\n\n/**\n * Signal event (SIGINT, SIGTERM, etc.).\n */\nexport interface SignalEvent {\n type: \"signal\"\n /** Signal name (e.g., 'SIGINT', 'SIGTERM') */\n signal: string\n}\n\n/**\n * Custom event for extensibility.\n */\nexport interface CustomEvent {\n type: \"custom\"\n /** Event name */\n name: string\n /** Event data */\n data: unknown\n}\n\n/**\n * Union of all event types.\n *\n * Events drive the render loop in interactive mode. When events are present,\n * the render loop runs until exit() is called. When events are absent,\n * the render completes when the UI is stable.\n */\nexport type Event = KeyEvent | MouseEvent | ResizeEvent | FocusEvent | BlurEvent | SignalEvent | CustomEvent\n\n/**\n * Event source that can be subscribed to and unsubscribed from.\n */\nexport interface EventSource {\n /** Subscribe to events, returns unsubscribe function */\n subscribe(handler: (event: Event) => void): () => void\n /** Convert to async iterable */\n [Symbol.asyncIterator](): AsyncIterator<Event>\n}\n\n// ============================================================================\n// TermDef - Minimal Render Configuration\n// ============================================================================\n\n// ColorLevel is re-exported from ansi in index.ts\n// Import here for use in TermDef\nimport type { ColorLevel } from \"@silvery/ag-term/ansi\"\n\n/**\n * Minimal surface for configuring render().\n *\n * TermDef provides a simple way to configure rendering without requiring\n * a full Term instance. It's useful for:\n * - Static rendering (just width/height, no events)\n * - Testing (mock dimensions and events)\n * - Quick scripts (auto-detect everything from stdin/stdout)\n *\n * The presence of `events` (or `stdin` which auto-creates events)\n * determines the render mode:\n * - No events → static mode (render until stable)\n * - Has events → interactive mode (render until exit() called)\n *\n * @example\n * ```tsx\n * // Static render with custom width\n * const output = await render(<App />, { width: 100 })\n *\n * // Interactive with stdin/stdout\n * await render(<App />, { stdin: process.stdin, stdout: process.stdout })\n *\n * // Custom events\n * await render(<App />, { events: myEventSource })\n * ```\n */\nexport interface TermDef {\n // -------------------------------------------------------------------------\n // Output Configuration\n // -------------------------------------------------------------------------\n\n /** Output stream (used for dimensions if not specified) */\n stdout?: NodeJS.WriteStream\n\n /** Width in columns (default: stdout?.columns ?? 80) */\n width?: number\n\n /** Height in rows (default: stdout?.rows ?? 24) */\n height?: number\n\n /** Color support (true=detect, false=none, or specific level) */\n colors?: boolean | ColorLevel | null\n\n // -------------------------------------------------------------------------\n // Input Configuration\n // -------------------------------------------------------------------------\n\n /**\n * Event source for interactive mode.\n *\n * When present, render runs until exit() is called.\n * When absent, render completes when UI is stable.\n */\n events?: AsyncIterable<Event> | EventSource\n\n /**\n * Standard input stream.\n *\n * When provided (and events is not), automatically creates input events\n * from stdin, enabling interactive mode.\n */\n stdin?: NodeJS.ReadStream\n}\n\n// ============================================================================\n// Render Context Types\n// ============================================================================\n\n/**\n * Options passed to the render function.\n */\nexport interface RenderOptions {\n stdout?: NodeJS.WriteStream\n stdin?: NodeJS.ReadStream\n exitOnCtrlC?: boolean\n debug?: boolean\n}\n\n/**\n * The render instance returned by render().\n */\nexport interface RenderInstance {\n /** Re-render with new element */\n rerender: (element: React.ReactNode) => void\n /** Unmount and clean up */\n unmount: () => void\n /** Wait for render to complete */\n waitUntilExit: () => Promise<void>\n /** Clear terminal output */\n clear: () => void\n}\n"
11
+ ],
12
+ "mappings": "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",
13
+ "debugId": "4C8FB7BC1ACBEC3E64756E2164756E21",
14
+ "names": []
15
+ }
@@ -0,0 +1,3 @@
1
+ import{useState,useCallback}from"react";import{jsxDEV,Fragment}from"react/jsx-dev-runtime";function TextInput({value,onChange,placeholder,mask,autocomplete,onAutocomplete,onSubmit,cursorPosition,focused=!0}){let displayValue=mask?mask.repeat(value.length):value,suggestion=getAutocompleteSuggestion(value,autocomplete),cursor=cursorPosition??value.length,beforeCursor=displayValue.slice(0,cursor),afterCursor=displayValue.slice(cursor),suggestionSuffix=suggestion?suggestion.slice(value.length):"";return jsxDEV("span",{"data-silvery-ui-text-input":!0,"data-focused":focused,children:!value&&placeholder?jsxDEV("span",{"data-color":"dim",children:placeholder},void 0,!1,void 0,this):jsxDEV(Fragment,{children:[jsxDEV("span",{children:beforeCursor},void 0,!1,void 0,this),focused&&jsxDEV("span",{"data-cursor":!0,"data-inverse":!0,children:afterCursor[0]||" "},void 0,!1,void 0,this),jsxDEV("span",{children:afterCursor.slice(1)},void 0,!1,void 0,this),suggestionSuffix&&jsxDEV("span",{"data-color":"dim",children:suggestionSuffix},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)}function useTextInput(options={}){let[value,setValue]=useState(options.initialValue??""),[cursorPosition,setCursorPosition]=useState(value.length),displayValue=options.mask?options.mask.repeat(value.length):value,suggestion=getAutocompleteSuggestion(value,options.autocomplete),handleInput=useCallback((input,key)=>{if(key.return){options.onSubmit?.(value);return}if(key.backspace||key.delete){if(cursorPosition>0)setValue((v)=>v.slice(0,cursorPosition-1)+v.slice(cursorPosition)),setCursorPosition((p)=>Math.max(0,p-1));return}if(key.leftArrow){setCursorPosition((p)=>Math.max(0,p-1));return}if(key.rightArrow){setCursorPosition((p)=>Math.min(value.length,p+1));return}if(key.ctrl||key.meta||!input)return;setValue((v)=>v.slice(0,cursorPosition)+input+v.slice(cursorPosition)),setCursorPosition((p)=>p+input.length)},[value,cursorPosition,options.onSubmit]),acceptSuggestion=useCallback(()=>{if(suggestion)setValue(suggestion),setCursorPosition(suggestion.length)},[suggestion]),clear=useCallback(()=>{setValue(""),setCursorPosition(0)},[]);return{value,setValue,displayValue,suggestion,cursorPosition,setCursorPosition,handleInput,acceptSuggestion,clear}}function getAutocompleteSuggestion(value,autocomplete){if(!value||!autocomplete?.length)return;let lowerValue=value.toLowerCase();return autocomplete.find((item)=>item.toLowerCase().startsWith(lowerValue)&&item.length>value.length)}import{useState as useState2,useEffect,useCallback as useCallback2}from"react";import{jsxDEV as jsxDEV2}from"react/jsx-dev-runtime";function Select({options,value,onChange,maxVisible=10,highlightIndex:controlledHighlightIndex,onHighlightChange}){let selectedIndex=options.findIndex((opt)=>opt.value===value),[internalHighlightIndex,setInternalHighlightIndex]=useState2(selectedIndex>=0?selectedIndex:0),highlightIndex=controlledHighlightIndex??internalHighlightIndex,scrollOffset=Math.max(0,Math.min(highlightIndex-Math.floor(maxVisible/2),options.length-maxVisible)),visibleOptions=options.slice(scrollOffset,scrollOffset+maxVisible),hasMoreAbove=scrollOffset>0,hasMoreBelow=scrollOffset+maxVisible<options.length;return useEffect(()=>{if(controlledHighlightIndex===void 0&&selectedIndex>=0)setInternalHighlightIndex(selectedIndex)},[selectedIndex,controlledHighlightIndex]),jsxDEV2("div",{"data-silvery-select":!0,children:[hasMoreAbove&&jsxDEV2("div",{"data-silvery-select-scroll-indicator":"up",children:"..."},void 0,!1,void 0,this),visibleOptions.map((option,visibleIdx)=>{let actualIndex=scrollOffset+visibleIdx,isSelected=option.value===value;return jsxDEV2("div",{"data-silvery-select-option":!0,"data-selected":isSelected,"data-highlighted":actualIndex===highlightIndex,children:[jsxDEV2("span",{"data-silvery-select-indicator":!0,children:isSelected?">":" "},void 0,!1,void 0,this),jsxDEV2("span",{"data-silvery-select-label":!0,children:option.label},void 0,!1,void 0,this)]},actualIndex,!0,void 0,this)}),hasMoreBelow&&jsxDEV2("div",{"data-silvery-select-scroll-indicator":"down",children:"..."},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function useSelect({options,initialValue,onChange}){let initialIndex=initialValue!==void 0?options.findIndex((opt)=>opt.value===initialValue):0,[highlightIndex,setHighlightIndex]=useState2(Math.max(0,initialIndex)),[value,setValue]=useState2(initialValue),moveUp=useCallback2(()=>{setHighlightIndex((i)=>Math.max(0,i-1))},[]),moveDown=useCallback2(()=>{setHighlightIndex((i)=>Math.min(options.length-1,i+1))},[options.length]),select=useCallback2(()=>{let option=options[highlightIndex];if(option)setValue(option.value),onChange?.(option.value)},[highlightIndex,options,onChange]);return{value,highlightIndex,moveUp,moveDown,select,setHighlightIndex}}export{useTextInput,useSelect,TextInput,Select};
2
+
3
+ //# debugId=310F48A2D6C8194F64756E2164756E21
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../packages/ag-react/src/ui/input/TextInput.tsx", "../../packages/ag-react/src/ui/input/Select.tsx"],
4
+ "sourcesContent": [
5
+ "/**\n * React TextInput component for silvery/Ink TUI apps\n */\n\nimport React, { useState, useCallback } from \"react\"\nimport type { TextInputProps } from \"../types.js\"\n\n/**\n * Single-line text input component for React TUI apps\n *\n * This is a controlled component that renders the current input state.\n * It does NOT handle keyboard input directly - that's the caller's responsibility\n * (via useInput hook in Ink, or similar).\n *\n * @example\n * ```tsx\n * import { TextInput } from \"@silvery/ag-react/ui/input\";\n * import { useInput } from \"ink\";\n *\n * function MyForm() {\n * const [value, setValue] = useState(\"\");\n *\n * useInput((input, key) => {\n * if (key.backspace || key.delete) {\n * setValue(v => v.slice(0, -1));\n * } else if (!key.ctrl && !key.meta && input) {\n * setValue(v => v + input);\n * }\n * });\n *\n * return <TextInput value={value} onChange={setValue} placeholder=\"Type here...\" />;\n * }\n *\n * // With password masking\n * <TextInput value={password} onChange={setPassword} mask=\"*\" />\n *\n * // With autocomplete\n * <TextInput\n * value={query}\n * onChange={setQuery}\n * autocomplete={[\"apple\", \"apricot\", \"avocado\"]}\n * />\n * ```\n */\nexport function TextInput({\n value,\n onChange,\n placeholder,\n mask,\n autocomplete,\n onAutocomplete,\n onSubmit,\n cursorPosition,\n focused = true,\n}: TextInputProps): React.ReactElement {\n // Calculate display value (masked or plain)\n const displayValue = mask ? mask.repeat(value.length) : value\n\n // Find matching autocomplete suggestion\n const suggestion = getAutocompleteSuggestion(value, autocomplete)\n\n // Cursor position defaults to end of input\n const cursor = cursorPosition ?? value.length\n\n // Build the display: value + cursor + suggestion suffix\n const beforeCursor = displayValue.slice(0, cursor)\n const afterCursor = displayValue.slice(cursor)\n const suggestionSuffix = suggestion ? suggestion.slice(value.length) : \"\"\n\n // Show placeholder if empty and not focused or no value\n const showPlaceholder = !value && placeholder\n\n return (\n <span data-silvery-ui-text-input data-focused={focused}>\n {showPlaceholder ? (\n <span data-color=\"dim\">{placeholder}</span>\n ) : (\n <>\n <span>{beforeCursor}</span>\n {focused && (\n <span data-cursor data-inverse>\n {afterCursor[0] || \" \"}\n </span>\n )}\n <span>{afterCursor.slice(1)}</span>\n {suggestionSuffix && <span data-color=\"dim\">{suggestionSuffix}</span>}\n </>\n )}\n </span>\n )\n}\n\n/**\n * Hook for managing text input state with autocomplete support\n *\n * @example\n * ```tsx\n * function SearchInput() {\n * const { value, displayValue, suggestion, handleInput, acceptSuggestion, clear } =\n * useTextInput({ autocomplete: [\"apple\", \"banana\", \"cherry\"] });\n *\n * useInput((input, key) => {\n * if (key.tab && suggestion) {\n * acceptSuggestion();\n * } else {\n * handleInput(input, key);\n * }\n * });\n *\n * return <Text>{displayValue}</Text>;\n * }\n * ```\n */\nexport function useTextInput(\n options: {\n initialValue?: string\n mask?: string\n autocomplete?: string[]\n onSubmit?: (value: string) => void\n } = {},\n): {\n value: string\n setValue: (value: string) => void\n displayValue: string\n suggestion: string | undefined\n cursorPosition: number\n setCursorPosition: (pos: number) => void\n handleInput: (input: string, key: InputKey) => void\n acceptSuggestion: () => void\n clear: () => void\n} {\n const [value, setValue] = useState(options.initialValue ?? \"\")\n const [cursorPosition, setCursorPosition] = useState(value.length)\n\n const displayValue = options.mask ? options.mask.repeat(value.length) : value\n\n const suggestion = getAutocompleteSuggestion(value, options.autocomplete)\n\n const handleInput = useCallback(\n (input: string, key: InputKey) => {\n if (key.return) {\n options.onSubmit?.(value)\n return\n }\n\n if (key.backspace || key.delete) {\n if (cursorPosition > 0) {\n setValue((v) => v.slice(0, cursorPosition - 1) + v.slice(cursorPosition))\n setCursorPosition((p) => Math.max(0, p - 1))\n }\n return\n }\n\n if (key.leftArrow) {\n setCursorPosition((p) => Math.max(0, p - 1))\n return\n }\n\n if (key.rightArrow) {\n setCursorPosition((p) => Math.min(value.length, p + 1))\n return\n }\n\n // Ignore control characters\n if (key.ctrl || key.meta || !input) {\n return\n }\n\n // Insert character at cursor position\n setValue((v) => v.slice(0, cursorPosition) + input + v.slice(cursorPosition))\n setCursorPosition((p) => p + input.length)\n },\n [value, cursorPosition, options.onSubmit],\n )\n\n const acceptSuggestion = useCallback(() => {\n if (suggestion) {\n setValue(suggestion)\n setCursorPosition(suggestion.length)\n }\n }, [suggestion])\n\n const clear = useCallback(() => {\n setValue(\"\")\n setCursorPosition(0)\n }, [])\n\n return {\n value,\n setValue,\n displayValue,\n suggestion,\n cursorPosition,\n setCursorPosition,\n handleInput,\n acceptSuggestion,\n clear,\n }\n}\n\n/** Key object type (matches Ink's Key interface) */\ninterface InputKey {\n return?: boolean\n backspace?: boolean\n delete?: boolean\n leftArrow?: boolean\n rightArrow?: boolean\n upArrow?: boolean\n downArrow?: boolean\n tab?: boolean\n escape?: boolean\n ctrl?: boolean\n meta?: boolean\n shift?: boolean\n}\n\n/**\n * Find a matching autocomplete suggestion for the current input\n */\nfunction getAutocompleteSuggestion(value: string, autocomplete?: string[]): string | undefined {\n if (!value || !autocomplete?.length) {\n return undefined\n }\n\n const lowerValue = value.toLowerCase()\n return autocomplete.find((item) => item.toLowerCase().startsWith(lowerValue) && item.length > value.length)\n}\n",
6
+ "/**\n * React Select component for silvery/Ink TUI apps\n *\n * Single-choice selection list with keyboard navigation.\n */\n\nimport React, { useState, useEffect, useCallback } from \"react\"\nimport type { SelectProps, SelectOption } from \"../types.js\"\n\n/**\n * Scrollable single-choice selection list\n *\n * @example\n * ```tsx\n * import { Select } from \"@silvery/ag-react/ui/input\";\n *\n * function SettingsView() {\n * const [theme, setTheme] = useState(\"light\");\n *\n * return (\n * <Select\n * options={[\n * { label: \"Light\", value: \"light\" },\n * { label: \"Dark\", value: \"dark\" },\n * { label: \"System\", value: \"system\" },\n * ]}\n * value={theme}\n * onChange={setTheme}\n * />\n * );\n * }\n * ```\n */\nexport function Select<T>({\n options,\n value,\n onChange,\n maxVisible = 10,\n highlightIndex: controlledHighlightIndex,\n onHighlightChange,\n}: SelectProps<T>): React.ReactElement {\n // Find the index of the currently selected value\n const selectedIndex = options.findIndex((opt) => opt.value === value)\n\n // Internal highlight state (for uncontrolled mode)\n const [internalHighlightIndex, setInternalHighlightIndex] = useState(selectedIndex >= 0 ? selectedIndex : 0)\n\n // Use controlled or internal highlight index\n const highlightIndex = controlledHighlightIndex ?? internalHighlightIndex\n\n // Calculate scroll window\n const scrollOffset = Math.max(0, Math.min(highlightIndex - Math.floor(maxVisible / 2), options.length - maxVisible))\n const visibleOptions = options.slice(scrollOffset, scrollOffset + maxVisible)\n const hasMoreAbove = scrollOffset > 0\n const hasMoreBelow = scrollOffset + maxVisible < options.length\n\n // Sync internal highlight when value changes externally\n useEffect(() => {\n if (controlledHighlightIndex === undefined && selectedIndex >= 0) {\n setInternalHighlightIndex(selectedIndex)\n }\n }, [selectedIndex, controlledHighlightIndex])\n\n return (\n <div data-silvery-select>\n {hasMoreAbove && <div data-silvery-select-scroll-indicator=\"up\">...</div>}\n {visibleOptions.map((option, visibleIdx) => {\n const actualIndex = scrollOffset + visibleIdx\n const isSelected = option.value === value\n const isHighlighted = actualIndex === highlightIndex\n\n return (\n <div key={actualIndex} data-silvery-select-option data-selected={isSelected} data-highlighted={isHighlighted}>\n <span data-silvery-select-indicator>{isSelected ? \">\" : \" \"}</span>\n <span data-silvery-select-label>{option.label}</span>\n </div>\n )\n })}\n {hasMoreBelow && <div data-silvery-select-scroll-indicator=\"down\">...</div>}\n </div>\n )\n}\n\n/**\n * Hook for managing select state with keyboard navigation\n *\n * @example\n * ```tsx\n * function MySelect() {\n * const options = [\n * { label: \"Option A\", value: \"a\" },\n * { label: \"Option B\", value: \"b\" },\n * ];\n *\n * const { highlightIndex, moveUp, moveDown, select, value } = useSelect({\n * options,\n * initialValue: \"a\",\n * });\n *\n * useInput((input, key) => {\n * if (key.upArrow) moveUp();\n * if (key.downArrow) moveDown();\n * if (key.return) select();\n * });\n *\n * return <Select options={options} value={value} highlightIndex={highlightIndex} />;\n * }\n * ```\n */\nexport function useSelect<T>({\n options,\n initialValue,\n onChange,\n}: {\n options: SelectOption<T>[]\n initialValue?: T\n onChange?: (value: T) => void\n}): {\n value: T | undefined\n highlightIndex: number\n moveUp: () => void\n moveDown: () => void\n select: () => void\n setHighlightIndex: (index: number) => void\n} {\n const initialIndex = initialValue !== undefined ? options.findIndex((opt) => opt.value === initialValue) : 0\n\n const [highlightIndex, setHighlightIndex] = useState(Math.max(0, initialIndex))\n const [value, setValue] = useState<T | undefined>(initialValue)\n\n const moveUp = useCallback(() => {\n setHighlightIndex((i) => Math.max(0, i - 1))\n }, [])\n\n const moveDown = useCallback(() => {\n setHighlightIndex((i) => Math.min(options.length - 1, i + 1))\n }, [options.length])\n\n const select = useCallback(() => {\n const option = options[highlightIndex]\n if (option) {\n setValue(option.value)\n onChange?.(option.value)\n }\n }, [highlightIndex, options, onChange])\n\n return {\n value,\n highlightIndex,\n moveUp,\n moveDown,\n select,\n setHighlightIndex,\n }\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "310F48A2D6C8194F64756E2164756E21",
10
+ "names": []
11
+ }