@gridland/web 0.1.0 → 0.2.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.
package/dist/next.d.ts CHANGED
@@ -289,7 +289,10 @@ declare class BrowserRenderer {
289
289
  private isDragOver;
290
290
  private cleanupListeners;
291
291
  private mouseDownCell;
292
- constructor(canvas: HTMLCanvasElement, cols: number, rows: number);
292
+ private backgroundColor;
293
+ constructor(canvas: HTMLCanvasElement, cols: number, rows: number, options?: {
294
+ backgroundColor?: string;
295
+ });
293
296
  private pixelToCell;
294
297
  private setupDomListeners;
295
298
  start(): void;
@@ -314,8 +317,14 @@ interface TUIProps {
314
317
  fontFamily?: string;
315
318
  /** Auto-focus the canvas for keyboard input (default: true) */
316
319
  autoFocus?: boolean;
320
+ /** Background color for the canvas (default: transparent) */
321
+ backgroundColor?: string;
317
322
  /** Called when the renderer is ready */
318
323
  onReady?: (renderer: BrowserRenderer) => void;
324
+ /** Columns to use for SSR headless render (default: 80) */
325
+ fallbackCols?: number;
326
+ /** Rows to use for SSR headless render (default: 24) */
327
+ fallbackRows?: number;
319
328
  }
320
329
  /**
321
330
  * A single React component that renders TUI content to an HTML5 Canvas.
@@ -332,6 +341,6 @@ interface TUIProps {
332
341
  *
333
342
  * No dynamic imports, no wrapper chains. Just a component.
334
343
  */
335
- declare function TUI({ children, style, className, fontSize, fontFamily, autoFocus, onReady, }: TUIProps): react_jsx_runtime.JSX.Element;
344
+ declare function TUI({ children, style, className, fontSize, fontFamily, autoFocus, backgroundColor, onReady, fallbackCols, fallbackRows, }: TUIProps): react_jsx_runtime.JSX.Element;
336
345
 
337
346
  export { TUI, type TUIProps };
package/dist/next.js CHANGED
@@ -401,6 +401,7 @@ import * as LineNumberRenderable_star from "../../../../opentui/packages/core/sr
401
401
  import * as Markdown_star from "../../../../opentui/packages/core/src/renderables/Markdown";
402
402
  import * as FrameBuffer_star from "../../../../opentui/packages/core/src/renderables/FrameBuffer";
403
403
  import * as TextBufferRenderable_star from "../../../../opentui/packages/core/src/renderables/TextBufferRenderable";
404
+ import { TextBufferRenderable } from "../../../../opentui/packages/core/src/renderables/TextBufferRenderable";
404
405
 
405
406
  // src/browser-text-buffer.ts
406
407
  var BrowserTextBuffer = class _BrowserTextBuffer {
@@ -565,6 +566,7 @@ var BrowserTextBufferView = class _BrowserTextBufferView {
565
566
  _viewportWidth = 0;
566
567
  _viewportHeight = 0;
567
568
  _truncate = false;
569
+ textAlign = "left";
568
570
  _selection = null;
569
571
  _selectionBg;
570
572
  _selectionFg;
@@ -649,12 +651,18 @@ var BrowserTextBufferView = class _BrowserTextBufferView {
649
651
  setViewportSize(width, height) {
650
652
  this._viewportWidth = width;
651
653
  this._viewportHeight = height;
654
+ if (this._wrapMode !== "none" && width > 0 && this._wrapWidth !== width) {
655
+ this._wrapWidth = width;
656
+ }
652
657
  }
653
658
  setViewport(x, y, width, height) {
654
659
  this._viewportX = x;
655
660
  this._viewportY = y;
656
661
  this._viewportWidth = width;
657
662
  this._viewportHeight = height;
663
+ if (this._wrapMode !== "none" && width > 0 && this._wrapWidth !== width) {
664
+ this._wrapWidth = width;
665
+ }
658
666
  }
659
667
  setTabIndicator(_indicator) {
660
668
  }
@@ -996,6 +1004,19 @@ import {
996
1004
  createTimeline
997
1005
  } from "../../../../opentui/packages/core/src/animation/Timeline";
998
1006
  import * as Yoga from "yoga-layout";
1007
+ Object.defineProperty(TextBufferRenderable.prototype, "textAlign", {
1008
+ get() {
1009
+ return this.textBufferView?.textAlign ?? "left";
1010
+ },
1011
+ set(value) {
1012
+ if (this.textBufferView) {
1013
+ this.textBufferView.textAlign = value;
1014
+ this.requestRender();
1015
+ }
1016
+ },
1017
+ enumerable: true,
1018
+ configurable: true
1019
+ });
999
1020
  function resolveRenderLib() {
1000
1021
  return null;
1001
1022
  }
@@ -1400,10 +1421,20 @@ var BrowserBuffer = class _BrowserBuffer {
1400
1421
  if (!view || !view.getVisibleLines) return;
1401
1422
  const lines = view.getVisibleLines();
1402
1423
  if (!lines) return;
1424
+ const textAlign = view.textAlign;
1425
+ const viewWidth = view._viewportWidth;
1403
1426
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
1404
1427
  const line = lines[lineIdx];
1405
1428
  if (!line) continue;
1406
1429
  let curX = x;
1430
+ if (textAlign && textAlign !== "left" && viewWidth) {
1431
+ const lineWidth = line.chunks.reduce((sum, c) => sum + c.text.length, 0);
1432
+ if (textAlign === "center") {
1433
+ curX = x + Math.floor((viewWidth - lineWidth) / 2);
1434
+ } else if (textAlign === "right") {
1435
+ curX = x + viewWidth - lineWidth;
1436
+ }
1437
+ }
1407
1438
  for (const chunk of line.chunks) {
1408
1439
  const text = chunk.text;
1409
1440
  const fgColor = chunk.fg;
@@ -2146,6 +2177,41 @@ var SelectionManager = class {
2146
2177
  }
2147
2178
  };
2148
2179
 
2180
+ // src/render-pipeline.ts
2181
+ function executeRenderPipeline(buffer, renderContext, root, deltaTime) {
2182
+ buffer.clear();
2183
+ const lifecyclePasses = renderContext.getLifecyclePasses();
2184
+ for (const renderable of lifecyclePasses) {
2185
+ if (renderable.onLifecyclePass) {
2186
+ renderable.onLifecyclePass();
2187
+ }
2188
+ }
2189
+ root.calculateLayout();
2190
+ const renderList = [];
2191
+ root.updateLayout(deltaTime, renderList);
2192
+ for (const cmd of renderList) {
2193
+ switch (cmd.action) {
2194
+ case "pushScissorRect":
2195
+ buffer.pushScissorRect(cmd.x, cmd.y, cmd.width, cmd.height);
2196
+ break;
2197
+ case "popScissorRect":
2198
+ buffer.popScissorRect();
2199
+ break;
2200
+ case "pushOpacity":
2201
+ buffer.pushOpacity(cmd.opacity);
2202
+ break;
2203
+ case "popOpacity":
2204
+ buffer.popOpacity();
2205
+ break;
2206
+ case "render":
2207
+ cmd.renderable.render(buffer, deltaTime);
2208
+ break;
2209
+ }
2210
+ }
2211
+ buffer.clearScissorRects();
2212
+ buffer.clearOpacity();
2213
+ }
2214
+
2149
2215
  // src/browser-renderer.ts
2150
2216
  var RootRenderableClass = null;
2151
2217
  function setRootRenderableClass(cls) {
@@ -2170,7 +2236,8 @@ var BrowserRenderer = class _BrowserRenderer {
2170
2236
  isDragOver = false;
2171
2237
  cleanupListeners = [];
2172
2238
  mouseDownCell = null;
2173
- constructor(canvas, cols, rows) {
2239
+ backgroundColor = null;
2240
+ constructor(canvas, cols, rows, options) {
2174
2241
  this.canvas = canvas;
2175
2242
  this.cols = cols;
2176
2243
  this.rows = rows;
@@ -2187,6 +2254,7 @@ var BrowserRenderer = class _BrowserRenderer {
2187
2254
  canvas.style.width = `${cols * this.cellWidth}px`;
2188
2255
  canvas.style.height = `${rows * this.cellHeight}px`;
2189
2256
  this.ctx2d.scale(dpr, dpr);
2257
+ this.backgroundColor = options?.backgroundColor ?? null;
2190
2258
  canvas.style.cursor = "text";
2191
2259
  this.buffer = BrowserBuffer.create(cols, rows, "wcwidth");
2192
2260
  this.renderContext = new BrowserRenderContext(cols, rows);
@@ -2333,40 +2401,15 @@ var BrowserRenderer = class _BrowserRenderer {
2333
2401
  this.lastTime = now;
2334
2402
  if (!this.needsRender) return;
2335
2403
  this.needsRender = false;
2336
- this.buffer.clear();
2337
- const lifecyclePasses = this.renderContext.getLifecyclePasses();
2338
- for (const renderable of lifecyclePasses) {
2339
- if (renderable.onLifecyclePass) {
2340
- renderable.onLifecyclePass();
2341
- }
2342
- }
2343
- this.root.calculateLayout();
2344
- const renderList = [];
2345
- this.root.updateLayout(deltaTime, renderList);
2346
- for (const cmd of renderList) {
2347
- switch (cmd.action) {
2348
- case "pushScissorRect":
2349
- this.buffer.pushScissorRect(cmd.x, cmd.y, cmd.width, cmd.height);
2350
- break;
2351
- case "popScissorRect":
2352
- this.buffer.popScissorRect();
2353
- break;
2354
- case "pushOpacity":
2355
- this.buffer.pushOpacity(cmd.opacity);
2356
- break;
2357
- case "popOpacity":
2358
- this.buffer.popOpacity();
2359
- break;
2360
- case "render":
2361
- cmd.renderable.render(this.buffer, deltaTime);
2362
- break;
2363
- }
2364
- }
2365
- this.buffer.clearScissorRects();
2366
- this.buffer.clearOpacity();
2404
+ executeRenderPipeline(this.buffer, this.renderContext, this.root, deltaTime);
2367
2405
  const dpr = window.devicePixelRatio || 1;
2368
2406
  this.ctx2d.setTransform(dpr, 0, 0, dpr, 0, 0);
2369
- this.ctx2d.clearRect(0, 0, this.canvas.width, this.canvas.height);
2407
+ if (this.backgroundColor) {
2408
+ this.ctx2d.fillStyle = this.backgroundColor;
2409
+ this.ctx2d.fillRect(0, 0, this.canvas.width, this.canvas.height);
2410
+ } else {
2411
+ this.ctx2d.clearRect(0, 0, this.canvas.width, this.canvas.height);
2412
+ }
2370
2413
  this.painter.paint(this.ctx2d, this.buffer, this.selection);
2371
2414
  };
2372
2415
  resize(cols, rows) {
@@ -2476,13 +2519,14 @@ var BrowserContext = createContext(null);
2476
2519
  // src/create-browser-root.tsx
2477
2520
  import { _render } from "../../../opentui/packages/react/src/reconciler/reconciler";
2478
2521
  import { AppContext } from "../../../opentui/packages/react/src/components/app";
2479
- import { ErrorBoundary } from "../../../opentui/packages/react/src/components/error-boundary";
2522
+ import { ErrorBoundary as _ErrorBoundary } from "../../../opentui/packages/react/src/components/error-boundary";
2480
2523
  import { jsx } from "react/jsx-runtime";
2524
+ var ErrorBoundary = _ErrorBoundary;
2481
2525
  function createBrowserRoot(renderer) {
2482
2526
  let unmountFn = null;
2483
2527
  return {
2484
2528
  render(node) {
2485
- const element = /* @__PURE__ */ jsx(BrowserContext.Provider, { value: { renderContext: renderer.renderContext }, children: /* @__PURE__ */ jsx(AppContext.Provider, { value: { keyHandler: renderer.renderContext.keyInput, renderer: null }, children: /* @__PURE__ */ jsx(ErrorBoundary, { children: node }) }) });
2529
+ const element = /* @__PURE__ */ jsx(BrowserContext.Provider, { value: { renderContext: renderer.renderContext }, children: /* @__PURE__ */ jsx(AppContext.Provider, { value: { keyHandler: renderer.renderContext.keyInput, renderer: renderer.renderContext }, children: /* @__PURE__ */ jsx(ErrorBoundary, { children: node }) }) });
2486
2530
  unmountFn = _render(element, renderer.root);
2487
2531
  },
2488
2532
  unmount() {
@@ -2494,7 +2538,105 @@ function createBrowserRoot(renderer) {
2494
2538
 
2495
2539
  // src/TUI.tsx
2496
2540
  import { RootRenderable as RootRenderable2 } from "@opentui/core";
2541
+
2542
+ // src/buffer-to-text.ts
2543
+ function bufferToText(buffer) {
2544
+ const lines = [];
2545
+ for (let row = 0; row < buffer.height; row++) {
2546
+ let line = "";
2547
+ for (let col = 0; col < buffer.width; col++) {
2548
+ const idx = row * buffer.width + col;
2549
+ const charCode = buffer.char[idx];
2550
+ line += charCode === 0 ? " " : String.fromCodePoint(charCode);
2551
+ }
2552
+ lines.push(line.trimEnd());
2553
+ }
2554
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
2555
+ lines.pop();
2556
+ }
2557
+ return lines.join("\n");
2558
+ }
2559
+
2560
+ // src/headless-renderer.ts
2561
+ var RootRenderableClass2 = null;
2562
+ function setHeadlessRootRenderableClass(cls) {
2563
+ RootRenderableClass2 = cls;
2564
+ }
2565
+ var HeadlessRenderer = class {
2566
+ buffer;
2567
+ renderContext;
2568
+ root;
2569
+ // RootRenderable
2570
+ constructor(options) {
2571
+ const { cols, rows } = options;
2572
+ this.buffer = BrowserBuffer.create(cols, rows, "wcwidth");
2573
+ this.renderContext = new BrowserRenderContext(cols, rows);
2574
+ this.renderContext.setOnRenderRequest(() => {
2575
+ });
2576
+ if (!RootRenderableClass2) {
2577
+ throw new Error(
2578
+ "RootRenderableClass not set. Call setHeadlessRootRenderableClass before creating HeadlessRenderer."
2579
+ );
2580
+ }
2581
+ this.root = new RootRenderableClass2(this.renderContext);
2582
+ }
2583
+ renderOnce() {
2584
+ executeRenderPipeline(this.buffer, this.renderContext, this.root, 0);
2585
+ }
2586
+ toText() {
2587
+ return bufferToText(this.buffer);
2588
+ }
2589
+ resize(cols, rows) {
2590
+ this.buffer.resize(cols, rows);
2591
+ this.renderContext.resize(cols, rows);
2592
+ this.root.resize(cols, rows);
2593
+ }
2594
+ };
2595
+
2596
+ // src/create-headless-root.tsx
2597
+ import { _render as _render2, reconciler } from "../../../opentui/packages/react/src/reconciler/reconciler";
2598
+ import { AppContext as AppContext2 } from "../../../opentui/packages/react/src/components/app";
2599
+ import { ErrorBoundary as _ErrorBoundary2 } from "../../../opentui/packages/react/src/components/error-boundary";
2497
2600
  import { jsx as jsx2 } from "react/jsx-runtime";
2601
+ var ErrorBoundary2 = _ErrorBoundary2;
2602
+ var _r = reconciler;
2603
+ var flushSync = _r.flushSyncFromReconciler ?? _r.flushSync;
2604
+ function createHeadlessRoot(renderer) {
2605
+ let container = null;
2606
+ return {
2607
+ render(node) {
2608
+ const element = /* @__PURE__ */ jsx2(BrowserContext.Provider, { value: { renderContext: renderer.renderContext }, children: /* @__PURE__ */ jsx2(
2609
+ AppContext2.Provider,
2610
+ {
2611
+ value: {
2612
+ keyHandler: renderer.renderContext.keyInput,
2613
+ renderer: renderer.renderContext
2614
+ },
2615
+ children: /* @__PURE__ */ jsx2(ErrorBoundary2, { children: node })
2616
+ }
2617
+ ) });
2618
+ container = _render2(element, renderer.root);
2619
+ },
2620
+ renderToText(node) {
2621
+ flushSync(() => {
2622
+ this.render(node);
2623
+ });
2624
+ renderer.renderOnce();
2625
+ return renderer.toText();
2626
+ },
2627
+ unmount() {
2628
+ if (container) {
2629
+ reconciler.updateContainer(null, container, null, () => {
2630
+ });
2631
+ reconciler.flushSyncWork();
2632
+ container = null;
2633
+ }
2634
+ }
2635
+ };
2636
+ }
2637
+
2638
+ // src/TUI.tsx
2639
+ import { jsx as jsx3 } from "react/jsx-runtime";
2498
2640
  function TUI({
2499
2641
  children,
2500
2642
  style,
@@ -2502,7 +2644,10 @@ function TUI({
2502
2644
  fontSize = 14,
2503
2645
  fontFamily = "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace",
2504
2646
  autoFocus = true,
2505
- onReady
2647
+ backgroundColor,
2648
+ onReady,
2649
+ fallbackCols = 80,
2650
+ fallbackRows = 24
2506
2651
  }) {
2507
2652
  const containerRef = useRef(null);
2508
2653
  const canvasRef = useRef(null);
@@ -2524,7 +2669,7 @@ function TUI({
2524
2669
  const containerRect = container.getBoundingClientRect();
2525
2670
  const cols = Math.max(1, Math.floor(containerRect.width / cellSize.width));
2526
2671
  const rows = Math.max(1, Math.floor(containerRect.height / cellSize.height));
2527
- const renderer = new BrowserRenderer(canvas, cols, rows);
2672
+ const renderer = new BrowserRenderer(canvas, cols, rows, { backgroundColor });
2528
2673
  rendererRef.current = renderer;
2529
2674
  const root = createBrowserRoot(renderer);
2530
2675
  rootRef.current = root;
@@ -2556,16 +2701,43 @@ function TUI({
2556
2701
  rendererRef.current = null;
2557
2702
  rootRef.current = null;
2558
2703
  };
2559
- }, [isClient, fontSize, fontFamily]);
2704
+ }, [isClient, fontSize, fontFamily, backgroundColor]);
2560
2705
  useEffect(() => {
2561
2706
  if (rootRef.current) {
2562
2707
  rootRef.current.render(children);
2563
2708
  }
2564
2709
  }, [children]);
2565
2710
  if (!isClient) {
2566
- return /* @__PURE__ */ jsx2("div", { style, className, children: /* @__PURE__ */ jsx2("div", { style: { width: "100%", height: "100%" } }) });
2711
+ const isServer = typeof window === "undefined";
2712
+ let text = "";
2713
+ if (isServer) {
2714
+ setHeadlessRootRenderableClass(RootRenderable2);
2715
+ const renderer = new HeadlessRenderer({ cols: fallbackCols, rows: fallbackRows });
2716
+ const root = createHeadlessRoot(renderer);
2717
+ text = root.renderToText(children);
2718
+ root.unmount();
2719
+ }
2720
+ return /* @__PURE__ */ jsx3("div", { style, className, children: /* @__PURE__ */ jsx3(
2721
+ "pre",
2722
+ {
2723
+ suppressHydrationWarning: true,
2724
+ "aria-hidden": true,
2725
+ style: {
2726
+ fontFamily,
2727
+ fontSize,
2728
+ margin: 0,
2729
+ position: "absolute",
2730
+ width: "1px",
2731
+ height: "1px",
2732
+ overflow: "hidden",
2733
+ clip: "rect(0, 0, 0, 0)",
2734
+ whiteSpace: "pre"
2735
+ },
2736
+ children: text
2737
+ }
2738
+ ) });
2567
2739
  }
2568
- return /* @__PURE__ */ jsx2(
2740
+ return /* @__PURE__ */ jsx3(
2569
2741
  "div",
2570
2742
  {
2571
2743
  ref: containerRef,
@@ -2575,7 +2747,7 @@ function TUI({
2575
2747
  ...style
2576
2748
  },
2577
2749
  className,
2578
- children: /* @__PURE__ */ jsx2(
2750
+ children: /* @__PURE__ */ jsx3(
2579
2751
  "canvas",
2580
2752
  {
2581
2753
  ref: canvasRef,