@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/core.d.ts CHANGED
@@ -290,7 +290,10 @@ declare class BrowserRenderer {
290
290
  private isDragOver;
291
291
  private cleanupListeners;
292
292
  private mouseDownCell;
293
- constructor(canvas: HTMLCanvasElement, cols: number, rows: number);
293
+ private backgroundColor;
294
+ constructor(canvas: HTMLCanvasElement, cols: number, rows: number, options?: {
295
+ backgroundColor?: string;
296
+ });
294
297
  private pixelToCell;
295
298
  private setupDomListeners;
296
299
  start(): void;
@@ -315,8 +318,14 @@ interface TUIProps {
315
318
  fontFamily?: string;
316
319
  /** Auto-focus the canvas for keyboard input (default: true) */
317
320
  autoFocus?: boolean;
321
+ /** Background color for the canvas (default: transparent) */
322
+ backgroundColor?: string;
318
323
  /** Called when the renderer is ready */
319
324
  onReady?: (renderer: BrowserRenderer) => void;
325
+ /** Columns to use for SSR headless render (default: 80) */
326
+ fallbackCols?: number;
327
+ /** Rows to use for SSR headless render (default: 24) */
328
+ fallbackRows?: number;
320
329
  }
321
330
  /**
322
331
  * A single React component that renders TUI content to an HTML5 Canvas.
@@ -333,7 +342,7 @@ interface TUIProps {
333
342
  *
334
343
  * No dynamic imports, no wrapper chains. Just a component.
335
344
  */
336
- declare function TUI({ children, style, className, fontSize, fontFamily, autoFocus, onReady, }: TUIProps): react_jsx_runtime.JSX.Element;
345
+ declare function TUI({ children, style, className, fontSize, fontFamily, autoFocus, backgroundColor, onReady, fallbackCols, fallbackRows, }: TUIProps): react_jsx_runtime.JSX.Element;
337
346
 
338
347
  interface MountOptions {
339
348
  /** Number of columns (auto-calculated from canvas size if omitted) */
@@ -443,6 +452,7 @@ declare class BrowserTextBufferView {
443
452
  private _viewportWidth;
444
453
  private _viewportHeight;
445
454
  private _truncate;
455
+ textAlign: "left" | "center" | "right";
446
456
  private _selection;
447
457
  private _selectionBg;
448
458
  private _selectionFg;
@@ -532,4 +542,33 @@ declare function calculateGridSize(widthPx: number, heightPx: number, cellWidth:
532
542
  rows: number;
533
543
  };
534
544
 
535
- export { BrowserBuffer, BrowserContext, BrowserRenderContext, BrowserRenderer, type BrowserRoot, BrowserTextBuffer, BrowserTextBufferView, CanvasPainter, type MountOptions, type MountResult, SelectionManager, TUI, type TUIProps, calculateGridSize, createBrowserRoot, isBrowser, isCanvasSupported, mountGridland, useBrowserContext, useFileDrop, usePaste };
545
+ interface ReadableCharBuffer {
546
+ width: number;
547
+ height: number;
548
+ char: Uint32Array;
549
+ }
550
+ declare function bufferToText(buffer: ReadableCharBuffer): string;
551
+
552
+ declare function setHeadlessRootRenderableClass(cls: any): void;
553
+ interface HeadlessRendererOptions {
554
+ cols: number;
555
+ rows: number;
556
+ }
557
+ declare class HeadlessRenderer {
558
+ buffer: BrowserBuffer;
559
+ renderContext: BrowserRenderContext;
560
+ root: any;
561
+ constructor(options: HeadlessRendererOptions);
562
+ renderOnce(): void;
563
+ toText(): string;
564
+ resize(cols: number, rows: number): void;
565
+ }
566
+
567
+ interface HeadlessRoot {
568
+ render(node: ReactNode): void;
569
+ renderToText(node: ReactNode): string;
570
+ unmount(): void;
571
+ }
572
+ declare function createHeadlessRoot(renderer: HeadlessRenderer): HeadlessRoot;
573
+
574
+ export { BrowserBuffer, BrowserContext, BrowserRenderContext, BrowserRenderer, type BrowserRoot, BrowserTextBuffer, BrowserTextBufferView, CanvasPainter, HeadlessRenderer, type HeadlessRendererOptions, type HeadlessRoot, type MountOptions, type MountResult, SelectionManager, TUI, type TUIProps, bufferToText, calculateGridSize, createBrowserRoot, createHeadlessRoot, isBrowser, isCanvasSupported, mountGridland, setHeadlessRootRenderableClass, useBrowserContext, useFileDrop, usePaste };
package/dist/core.js CHANGED
@@ -405,6 +405,7 @@ import * as LineNumberRenderable_star from "../../../../opentui/packages/core/sr
405
405
  import * as Markdown_star from "../../../../opentui/packages/core/src/renderables/Markdown";
406
406
  import * as FrameBuffer_star from "../../../../opentui/packages/core/src/renderables/FrameBuffer";
407
407
  import * as TextBufferRenderable_star from "../../../../opentui/packages/core/src/renderables/TextBufferRenderable";
408
+ import { TextBufferRenderable } from "../../../../opentui/packages/core/src/renderables/TextBufferRenderable";
408
409
 
409
410
  // src/browser-text-buffer.ts
410
411
  var BrowserTextBuffer = class _BrowserTextBuffer {
@@ -569,6 +570,7 @@ var BrowserTextBufferView = class _BrowserTextBufferView {
569
570
  _viewportWidth = 0;
570
571
  _viewportHeight = 0;
571
572
  _truncate = false;
573
+ textAlign = "left";
572
574
  _selection = null;
573
575
  _selectionBg;
574
576
  _selectionFg;
@@ -653,12 +655,18 @@ var BrowserTextBufferView = class _BrowserTextBufferView {
653
655
  setViewportSize(width, height) {
654
656
  this._viewportWidth = width;
655
657
  this._viewportHeight = height;
658
+ if (this._wrapMode !== "none" && width > 0 && this._wrapWidth !== width) {
659
+ this._wrapWidth = width;
660
+ }
656
661
  }
657
662
  setViewport(x, y, width, height) {
658
663
  this._viewportX = x;
659
664
  this._viewportY = y;
660
665
  this._viewportWidth = width;
661
666
  this._viewportHeight = height;
667
+ if (this._wrapMode !== "none" && width > 0 && this._wrapWidth !== width) {
668
+ this._wrapWidth = width;
669
+ }
662
670
  }
663
671
  setTabIndicator(_indicator) {
664
672
  }
@@ -1000,6 +1008,19 @@ import {
1000
1008
  createTimeline
1001
1009
  } from "../../../../opentui/packages/core/src/animation/Timeline";
1002
1010
  import * as Yoga from "yoga-layout";
1011
+ Object.defineProperty(TextBufferRenderable.prototype, "textAlign", {
1012
+ get() {
1013
+ return this.textBufferView?.textAlign ?? "left";
1014
+ },
1015
+ set(value) {
1016
+ if (this.textBufferView) {
1017
+ this.textBufferView.textAlign = value;
1018
+ this.requestRender();
1019
+ }
1020
+ },
1021
+ enumerable: true,
1022
+ configurable: true
1023
+ });
1003
1024
  function resolveRenderLib() {
1004
1025
  return null;
1005
1026
  }
@@ -1404,10 +1425,20 @@ var BrowserBuffer = class _BrowserBuffer {
1404
1425
  if (!view || !view.getVisibleLines) return;
1405
1426
  const lines = view.getVisibleLines();
1406
1427
  if (!lines) return;
1428
+ const textAlign = view.textAlign;
1429
+ const viewWidth = view._viewportWidth;
1407
1430
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
1408
1431
  const line = lines[lineIdx];
1409
1432
  if (!line) continue;
1410
1433
  let curX = x;
1434
+ if (textAlign && textAlign !== "left" && viewWidth) {
1435
+ const lineWidth = line.chunks.reduce((sum, c) => sum + c.text.length, 0);
1436
+ if (textAlign === "center") {
1437
+ curX = x + Math.floor((viewWidth - lineWidth) / 2);
1438
+ } else if (textAlign === "right") {
1439
+ curX = x + viewWidth - lineWidth;
1440
+ }
1441
+ }
1411
1442
  for (const chunk of line.chunks) {
1412
1443
  const text = chunk.text;
1413
1444
  const fgColor = chunk.fg;
@@ -2150,6 +2181,41 @@ var SelectionManager = class {
2150
2181
  }
2151
2182
  };
2152
2183
 
2184
+ // src/render-pipeline.ts
2185
+ function executeRenderPipeline(buffer, renderContext, root, deltaTime) {
2186
+ buffer.clear();
2187
+ const lifecyclePasses = renderContext.getLifecyclePasses();
2188
+ for (const renderable of lifecyclePasses) {
2189
+ if (renderable.onLifecyclePass) {
2190
+ renderable.onLifecyclePass();
2191
+ }
2192
+ }
2193
+ root.calculateLayout();
2194
+ const renderList = [];
2195
+ root.updateLayout(deltaTime, renderList);
2196
+ for (const cmd of renderList) {
2197
+ switch (cmd.action) {
2198
+ case "pushScissorRect":
2199
+ buffer.pushScissorRect(cmd.x, cmd.y, cmd.width, cmd.height);
2200
+ break;
2201
+ case "popScissorRect":
2202
+ buffer.popScissorRect();
2203
+ break;
2204
+ case "pushOpacity":
2205
+ buffer.pushOpacity(cmd.opacity);
2206
+ break;
2207
+ case "popOpacity":
2208
+ buffer.popOpacity();
2209
+ break;
2210
+ case "render":
2211
+ cmd.renderable.render(buffer, deltaTime);
2212
+ break;
2213
+ }
2214
+ }
2215
+ buffer.clearScissorRects();
2216
+ buffer.clearOpacity();
2217
+ }
2218
+
2153
2219
  // src/browser-renderer.ts
2154
2220
  var RootRenderableClass = null;
2155
2221
  function setRootRenderableClass(cls) {
@@ -2174,7 +2240,8 @@ var BrowserRenderer = class _BrowserRenderer {
2174
2240
  isDragOver = false;
2175
2241
  cleanupListeners = [];
2176
2242
  mouseDownCell = null;
2177
- constructor(canvas, cols, rows) {
2243
+ backgroundColor = null;
2244
+ constructor(canvas, cols, rows, options) {
2178
2245
  this.canvas = canvas;
2179
2246
  this.cols = cols;
2180
2247
  this.rows = rows;
@@ -2191,6 +2258,7 @@ var BrowserRenderer = class _BrowserRenderer {
2191
2258
  canvas.style.width = `${cols * this.cellWidth}px`;
2192
2259
  canvas.style.height = `${rows * this.cellHeight}px`;
2193
2260
  this.ctx2d.scale(dpr, dpr);
2261
+ this.backgroundColor = options?.backgroundColor ?? null;
2194
2262
  canvas.style.cursor = "text";
2195
2263
  this.buffer = BrowserBuffer.create(cols, rows, "wcwidth");
2196
2264
  this.renderContext = new BrowserRenderContext(cols, rows);
@@ -2337,40 +2405,15 @@ var BrowserRenderer = class _BrowserRenderer {
2337
2405
  this.lastTime = now;
2338
2406
  if (!this.needsRender) return;
2339
2407
  this.needsRender = false;
2340
- this.buffer.clear();
2341
- const lifecyclePasses = this.renderContext.getLifecyclePasses();
2342
- for (const renderable of lifecyclePasses) {
2343
- if (renderable.onLifecyclePass) {
2344
- renderable.onLifecyclePass();
2345
- }
2346
- }
2347
- this.root.calculateLayout();
2348
- const renderList = [];
2349
- this.root.updateLayout(deltaTime, renderList);
2350
- for (const cmd of renderList) {
2351
- switch (cmd.action) {
2352
- case "pushScissorRect":
2353
- this.buffer.pushScissorRect(cmd.x, cmd.y, cmd.width, cmd.height);
2354
- break;
2355
- case "popScissorRect":
2356
- this.buffer.popScissorRect();
2357
- break;
2358
- case "pushOpacity":
2359
- this.buffer.pushOpacity(cmd.opacity);
2360
- break;
2361
- case "popOpacity":
2362
- this.buffer.popOpacity();
2363
- break;
2364
- case "render":
2365
- cmd.renderable.render(this.buffer, deltaTime);
2366
- break;
2367
- }
2368
- }
2369
- this.buffer.clearScissorRects();
2370
- this.buffer.clearOpacity();
2408
+ executeRenderPipeline(this.buffer, this.renderContext, this.root, deltaTime);
2371
2409
  const dpr = window.devicePixelRatio || 1;
2372
2410
  this.ctx2d.setTransform(dpr, 0, 0, dpr, 0, 0);
2373
- this.ctx2d.clearRect(0, 0, this.canvas.width, this.canvas.height);
2411
+ if (this.backgroundColor) {
2412
+ this.ctx2d.fillStyle = this.backgroundColor;
2413
+ this.ctx2d.fillRect(0, 0, this.canvas.width, this.canvas.height);
2414
+ } else {
2415
+ this.ctx2d.clearRect(0, 0, this.canvas.width, this.canvas.height);
2416
+ }
2374
2417
  this.painter.paint(this.ctx2d, this.buffer, this.selection);
2375
2418
  };
2376
2419
  resize(cols, rows) {
@@ -2487,13 +2530,14 @@ function useBrowserContext() {
2487
2530
  // src/create-browser-root.tsx
2488
2531
  import { _render } from "../../../opentui/packages/react/src/reconciler/reconciler";
2489
2532
  import { AppContext } from "../../../opentui/packages/react/src/components/app";
2490
- import { ErrorBoundary } from "../../../opentui/packages/react/src/components/error-boundary";
2533
+ import { ErrorBoundary as _ErrorBoundary } from "../../../opentui/packages/react/src/components/error-boundary";
2491
2534
  import { jsx } from "react/jsx-runtime";
2535
+ var ErrorBoundary = _ErrorBoundary;
2492
2536
  function createBrowserRoot(renderer) {
2493
2537
  let unmountFn = null;
2494
2538
  return {
2495
2539
  render(node) {
2496
- 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 }) }) });
2540
+ 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 }) }) });
2497
2541
  unmountFn = _render(element, renderer.root);
2498
2542
  },
2499
2543
  unmount() {
@@ -2505,7 +2549,105 @@ function createBrowserRoot(renderer) {
2505
2549
 
2506
2550
  // src/TUI.tsx
2507
2551
  import { RootRenderable as RootRenderable2 } from "@opentui/core";
2552
+
2553
+ // src/buffer-to-text.ts
2554
+ function bufferToText(buffer) {
2555
+ const lines = [];
2556
+ for (let row = 0; row < buffer.height; row++) {
2557
+ let line = "";
2558
+ for (let col = 0; col < buffer.width; col++) {
2559
+ const idx = row * buffer.width + col;
2560
+ const charCode = buffer.char[idx];
2561
+ line += charCode === 0 ? " " : String.fromCodePoint(charCode);
2562
+ }
2563
+ lines.push(line.trimEnd());
2564
+ }
2565
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
2566
+ lines.pop();
2567
+ }
2568
+ return lines.join("\n");
2569
+ }
2570
+
2571
+ // src/headless-renderer.ts
2572
+ var RootRenderableClass2 = null;
2573
+ function setHeadlessRootRenderableClass(cls) {
2574
+ RootRenderableClass2 = cls;
2575
+ }
2576
+ var HeadlessRenderer = class {
2577
+ buffer;
2578
+ renderContext;
2579
+ root;
2580
+ // RootRenderable
2581
+ constructor(options) {
2582
+ const { cols, rows } = options;
2583
+ this.buffer = BrowserBuffer.create(cols, rows, "wcwidth");
2584
+ this.renderContext = new BrowserRenderContext(cols, rows);
2585
+ this.renderContext.setOnRenderRequest(() => {
2586
+ });
2587
+ if (!RootRenderableClass2) {
2588
+ throw new Error(
2589
+ "RootRenderableClass not set. Call setHeadlessRootRenderableClass before creating HeadlessRenderer."
2590
+ );
2591
+ }
2592
+ this.root = new RootRenderableClass2(this.renderContext);
2593
+ }
2594
+ renderOnce() {
2595
+ executeRenderPipeline(this.buffer, this.renderContext, this.root, 0);
2596
+ }
2597
+ toText() {
2598
+ return bufferToText(this.buffer);
2599
+ }
2600
+ resize(cols, rows) {
2601
+ this.buffer.resize(cols, rows);
2602
+ this.renderContext.resize(cols, rows);
2603
+ this.root.resize(cols, rows);
2604
+ }
2605
+ };
2606
+
2607
+ // src/create-headless-root.tsx
2608
+ import { _render as _render2, reconciler } from "../../../opentui/packages/react/src/reconciler/reconciler";
2609
+ import { AppContext as AppContext2 } from "../../../opentui/packages/react/src/components/app";
2610
+ import { ErrorBoundary as _ErrorBoundary2 } from "../../../opentui/packages/react/src/components/error-boundary";
2508
2611
  import { jsx as jsx2 } from "react/jsx-runtime";
2612
+ var ErrorBoundary2 = _ErrorBoundary2;
2613
+ var _r = reconciler;
2614
+ var flushSync = _r.flushSyncFromReconciler ?? _r.flushSync;
2615
+ function createHeadlessRoot(renderer) {
2616
+ let container = null;
2617
+ return {
2618
+ render(node) {
2619
+ const element = /* @__PURE__ */ jsx2(BrowserContext.Provider, { value: { renderContext: renderer.renderContext }, children: /* @__PURE__ */ jsx2(
2620
+ AppContext2.Provider,
2621
+ {
2622
+ value: {
2623
+ keyHandler: renderer.renderContext.keyInput,
2624
+ renderer: renderer.renderContext
2625
+ },
2626
+ children: /* @__PURE__ */ jsx2(ErrorBoundary2, { children: node })
2627
+ }
2628
+ ) });
2629
+ container = _render2(element, renderer.root);
2630
+ },
2631
+ renderToText(node) {
2632
+ flushSync(() => {
2633
+ this.render(node);
2634
+ });
2635
+ renderer.renderOnce();
2636
+ return renderer.toText();
2637
+ },
2638
+ unmount() {
2639
+ if (container) {
2640
+ reconciler.updateContainer(null, container, null, () => {
2641
+ });
2642
+ reconciler.flushSyncWork();
2643
+ container = null;
2644
+ }
2645
+ }
2646
+ };
2647
+ }
2648
+
2649
+ // src/TUI.tsx
2650
+ import { jsx as jsx3 } from "react/jsx-runtime";
2509
2651
  function TUI({
2510
2652
  children,
2511
2653
  style,
@@ -2513,7 +2655,10 @@ function TUI({
2513
2655
  fontSize = 14,
2514
2656
  fontFamily = "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace",
2515
2657
  autoFocus = true,
2516
- onReady
2658
+ backgroundColor,
2659
+ onReady,
2660
+ fallbackCols = 80,
2661
+ fallbackRows = 24
2517
2662
  }) {
2518
2663
  const containerRef = useRef(null);
2519
2664
  const canvasRef = useRef(null);
@@ -2535,7 +2680,7 @@ function TUI({
2535
2680
  const containerRect = container.getBoundingClientRect();
2536
2681
  const cols = Math.max(1, Math.floor(containerRect.width / cellSize.width));
2537
2682
  const rows = Math.max(1, Math.floor(containerRect.height / cellSize.height));
2538
- const renderer = new BrowserRenderer(canvas, cols, rows);
2683
+ const renderer = new BrowserRenderer(canvas, cols, rows, { backgroundColor });
2539
2684
  rendererRef.current = renderer;
2540
2685
  const root = createBrowserRoot(renderer);
2541
2686
  rootRef.current = root;
@@ -2567,16 +2712,43 @@ function TUI({
2567
2712
  rendererRef.current = null;
2568
2713
  rootRef.current = null;
2569
2714
  };
2570
- }, [isClient, fontSize, fontFamily]);
2715
+ }, [isClient, fontSize, fontFamily, backgroundColor]);
2571
2716
  useEffect(() => {
2572
2717
  if (rootRef.current) {
2573
2718
  rootRef.current.render(children);
2574
2719
  }
2575
2720
  }, [children]);
2576
2721
  if (!isClient) {
2577
- return /* @__PURE__ */ jsx2("div", { style, className, children: /* @__PURE__ */ jsx2("div", { style: { width: "100%", height: "100%" } }) });
2722
+ const isServer = typeof window === "undefined";
2723
+ let text = "";
2724
+ if (isServer) {
2725
+ setHeadlessRootRenderableClass(RootRenderable2);
2726
+ const renderer = new HeadlessRenderer({ cols: fallbackCols, rows: fallbackRows });
2727
+ const root = createHeadlessRoot(renderer);
2728
+ text = root.renderToText(children);
2729
+ root.unmount();
2730
+ }
2731
+ return /* @__PURE__ */ jsx3("div", { style, className, children: /* @__PURE__ */ jsx3(
2732
+ "pre",
2733
+ {
2734
+ suppressHydrationWarning: true,
2735
+ "aria-hidden": true,
2736
+ style: {
2737
+ fontFamily,
2738
+ fontSize,
2739
+ margin: 0,
2740
+ position: "absolute",
2741
+ width: "1px",
2742
+ height: "1px",
2743
+ overflow: "hidden",
2744
+ clip: "rect(0, 0, 0, 0)",
2745
+ whiteSpace: "pre"
2746
+ },
2747
+ children: text
2748
+ }
2749
+ ) });
2578
2750
  }
2579
- return /* @__PURE__ */ jsx2(
2751
+ return /* @__PURE__ */ jsx3(
2580
2752
  "div",
2581
2753
  {
2582
2754
  ref: containerRef,
@@ -2586,7 +2758,7 @@ function TUI({
2586
2758
  ...style
2587
2759
  },
2588
2760
  className,
2589
- children: /* @__PURE__ */ jsx2(
2761
+ children: /* @__PURE__ */ jsx3(
2590
2762
  "canvas",
2591
2763
  {
2592
2764
  ref: canvasRef,
@@ -2722,13 +2894,17 @@ export {
2722
2894
  BrowserTextBuffer,
2723
2895
  BrowserTextBufferView,
2724
2896
  CanvasPainter,
2897
+ HeadlessRenderer,
2725
2898
  SelectionManager,
2726
2899
  TUI,
2900
+ bufferToText,
2727
2901
  calculateGridSize,
2728
2902
  createBrowserRoot,
2903
+ createHeadlessRoot,
2729
2904
  isBrowser,
2730
2905
  isCanvasSupported,
2731
2906
  mountGridland,
2907
+ setHeadlessRootRenderableClass,
2732
2908
  useBrowserContext,
2733
2909
  useFileDrop,
2734
2910
  usePaste