@simon_he/vue-tui 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -1,18 +1,47 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="./docs/public/brand/vue-tui-logo-on-dark.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="./docs/public/brand/vue-tui-logo-on-light.svg">
5
+ <img alt="Vue TUI" src="./docs/public/brand/vue-tui-logo-on-light.svg" width="360">
6
+ </picture>
7
+ </p>
8
+
1
9
  # @simon_he/vue-tui
2
10
 
3
- Vue TUI is a Vue 3 terminal UI toolkit for building terminal-style interfaces that can render in a browser or in a real CLI. It gives you Vue components, terminal cell rendering, event dispatch, markdown rendering, and high-throughput primitives for lists, logs, and streaming transcripts.
11
+ [![npm version](https://img.shields.io/npm/v/@simon_he/vue-tui?style=flat-square)](https://www.npmjs.com/package/@simon_he/vue-tui)
12
+ [![npm downloads](https://img.shields.io/npm/dm/@simon_he/vue-tui?style=flat-square)](https://www.npmjs.com/package/@simon_he/vue-tui)
13
+ [![CI](https://github.com/Simon-He95/vue-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/Simon-He95/vue-tui/actions/workflows/ci.yml)
14
+ [![Runtime compatibility](https://github.com/Simon-He95/vue-tui/actions/workflows/runtime-compat.yml/badge.svg)](https://github.com/Simon-He95/vue-tui/actions/workflows/runtime-compat.yml)
15
+ [![Security](https://github.com/Simon-He95/vue-tui/actions/workflows/security.yml/badge.svg)](https://github.com/Simon-He95/vue-tui/actions/workflows/security.yml)
16
+ ![TypeScript types](https://img.shields.io/npm/types/@simon_he/vue-tui?style=flat-square)
17
+ ![Node version](https://img.shields.io/node/v/@simon_he/vue-tui?style=flat-square)
18
+ [![MIT License](https://img.shields.io/github/license/Simon-He95/vue-tui?style=flat-square)](./license)
19
+
20
+ [Docs](https://vue-tui.pages.dev/) | [npm](https://www.npmjs.com/package/@simon_he/vue-tui) | [GitHub](https://github.com/Simon-He95/vue-tui) | [Issues](https://github.com/Simon-He95/vue-tui/issues)
21
+
22
+ Vue TUI is a Vue 3 terminal UI toolkit for building terminal-style interfaces that render to browser DOM, real CLI stdout, and headless tests.
23
+
24
+ Use it to build browser terminal dashboards, Vue-powered CLI apps, streaming markdown transcripts, log viewers, virtual lists, and AI agent consoles with one shared component model.
25
+
26
+ ## Why Vue TUI?
27
+
28
+ - **Vue component model for terminal UIs**: build with `TerminalProvider`, `TBox`, `TInput`, `TList`, `TTable`, and more.
29
+ - **Browser + CLI renderers**: render the same UI model to browser DOM or real stdout.
30
+ - **High-throughput surfaces**: virtual lists, append-only logs, streaming markdown, and agent transcripts.
31
+ - **Clear host boundaries**: browser-safe root entrypoint, CLI-only APIs under `/cli`, sanitized links, and explicit host permissions.
32
+
33
+ ## Agent UI Showcase
34
+
35
+ A real agent console built on `@simon_he/vue-tui` can stream agent output, markdown content, tool-call status, and input chrome in one terminal surface.
4
36
 
5
- Use it when you want:
37
+ [![Agent console rendering built with vue-tui](./docs/public/agent-console-rendering-poster.png)](./docs/public/agent-console-rendering.mp4)
6
38
 
7
- - Browser-hosted terminal interfaces, dashboards, demos, and playgrounds.
8
- - CLI apps that use Vue component composition instead of imperative stdout drawing.
9
- - Shared UI code that can run against a DOM renderer, a stdout renderer, or headless tests.
10
- - Large terminal surfaces such as virtual lists, append-only logs, markdown transcripts, and agent console UIs.
39
+ [Watch the rendering demo](./docs/public/agent-console-rendering.mp4)
11
40
 
12
41
  ## Install
13
42
 
14
43
  ```bash
15
- pnpm add @simon_he/vue-tui@rc vue
44
+ pnpm add @simon_he/vue-tui vue
16
45
  ```
17
46
 
18
47
  Vue is a peer dependency. The current package supports Vue `>=3.3.0 <4`.
@@ -217,19 +246,25 @@ This table is a category overview. The generated API reference for root, `/vue`,
217
246
 
218
247
  ## Documentation
219
248
 
220
- | Page | Purpose |
221
- | ---------------------------------------------------------------- | -------------------------------------------------------------------- |
222
- | [Docs home](./docs/index.md) | Product overview and reading path |
223
- | [Examples index](./docs/examples.md) | Browser, terminal, and smoke example commands |
224
- | [Core API](./docs/api.md) | Terminal, renderer, events, runtime, planes, and scheduler contracts |
225
- | [Performance](./docs/performance.md) | Practical performance guidance |
226
- | [Benchmarks](./docs/benchmarks.md) | Release benchmark budgets, sample results, and comparison boundaries |
227
- | [OpenTUI Solid comparison](./docs/compare-opentui-solid.md) | Same-scenario comparison protocol and public claim boundaries |
228
- | [High-throughput rendering](./docs/high-throughput-rendering.md) | Scheduler, dirty rows, mailbox, log, and renderer architecture |
229
- | [Component acceptance](./docs/components-acceptance.md) | Release readiness checks for component API and behavior |
230
- | [Agent Console](./docs/agent-console.md) | Streaming transcript example stack |
231
- | [Release candidate](./docs/release-candidate.md) | 1.0 RC validation, package export checks, and migration notes |
232
- | [Security policy](./SECURITY.md) | Vulnerability reporting and terminal permission boundaries |
249
+ | Page | Purpose |
250
+ | ------------------------------------------------------------------ | -------------------------------------------------------------------- |
251
+ | [Docs home](./docs/index.md) | Product overview and reading path |
252
+ | [Vue Terminal UI](./docs/guide/vue-terminal-ui.md) | English landing page for terminal-style Vue interfaces |
253
+ | [Vue CLI UI](./docs/guide/vue-cli-ui.md) | CLI app model with Vue component composition |
254
+ | [CLI stdout renderer](./docs/guide/cli-stdout-renderer.md) | Stdout renderer, stdin driver, cleanup, and terminal output |
255
+ | [Terminal log viewer](./docs/guide/terminal-log-viewer.md) | Append-only logs, retained windows, wrapping, links, and search |
256
+ | [Markdown transcripts](./docs/guide/markdown-transcript.md) | Static and streaming markdown transcript rendering |
257
+ | [Examples index](./docs/examples.md) | Browser, terminal, and smoke example commands |
258
+ | [Core API](./docs/api.md) | Terminal, renderer, events, runtime, planes, and scheduler contracts |
259
+ | [Terminal UI best practices](./docs/terminal-ui-best-practices.md) | Cell layout, input focus, render invalidation, transcripts, tests |
260
+ | [Performance](./docs/performance.md) | Practical performance guidance |
261
+ | [Benchmarks](./docs/benchmarks.md) | Release benchmark budgets, sample results, and comparison boundaries |
262
+ | [OpenTUI Solid comparison](./docs/compare-opentui-solid.md) | Same-scenario comparison protocol and public claim boundaries |
263
+ | [High-throughput rendering](./docs/high-throughput-rendering.md) | Scheduler, dirty rows, mailbox, log, and renderer architecture |
264
+ | [Component acceptance](./docs/components-acceptance.md) | Release readiness checks for component API and behavior |
265
+ | [Agent Console](./docs/agent-console.md) | Streaming transcript example stack |
266
+ | [Release candidate](./docs/release-candidate.md) | 1.0 RC validation, package export checks, and migration notes |
267
+ | [Security policy](./SECURITY.md) | Vulnerability reporting and terminal permission boundaries |
233
268
 
234
269
  Run the docs locally:
235
270
 
@@ -1,4 +1,4 @@
1
- import { getPlaneTerminal } from "./create-terminal-C2oiWFJU.js";
1
+ import { getPlaneTerminal } from "./create-terminal-DBFNYr8f.js";
2
2
  import { charCellWidth, hasEncodedControl } from "./hyperlink-Bn02UXJb.js";
3
3
  import { DialogContextKey, EventZIndexContextKey, ImeAnchorContextKey, LayoutContextKey, RenderPlaneContextKey, RenderStackKey, TInputPluginsContextKey, TerminalContextKey, forEachTextCellSegment, graphemeRangeAt, hasTextWidthAsciiFastPath, intersectRect, padEndByCells$1 as padEndByCells, repeatChar, sanitizeInlineText, sanitizeTextBlock, sliceByCells, sliceByCellsRange, spaces, textCellWidth$1 as textCellWidth, translateRect, useLayout, useRenderNode, useRenderStack, useTerminal, useTerminalNode, useVisibility, withTextWidthProvider, wrapByCells } from "./wheel-scroll-vBRaQ7p-.js";
4
4
  import { TView } from "./linkify-D_gzn9Xq.js";
@@ -4466,18 +4466,45 @@ function normalizeVisibleLimit$1(value) {
4466
4466
  function isCommandPaletteRowSelectable(item) {
4467
4467
  return Boolean(item && item.kind !== "separator" && item.kind !== "group" && !item.disabled);
4468
4468
  }
4469
- function substringMatcher(item, query) {
4470
- const q = query.trim().toLowerCase();
4471
- if (!q) return { score: 0 };
4472
- const labelRanges = computeCommandPaletteMatchRanges(item.label, q);
4473
- const detailRanges = item.detail ? computeCommandPaletteMatchRanges(item.detail, q) : [];
4474
- const keywordMatch = (item.keywords ?? []).some((keyword) => String(keyword ?? "").toLowerCase().includes(q));
4475
- if (!labelRanges.length && !detailRanges.length && !keywordMatch) return null;
4476
- return {
4477
- score: labelRanges.length ? 100 : detailRanges.length ? 50 : 10,
4478
- labelRanges,
4479
- detailRanges
4480
- };
4469
+ function defaultSubstringEntries(sourceItems, query) {
4470
+ const q = query.trim();
4471
+ if (!q) return sourceItems.map((item, sourceIndex) => ({
4472
+ item,
4473
+ sourceIndex,
4474
+ match: { score: 0 }
4475
+ }));
4476
+ const needle = q.toLowerCase();
4477
+ const labelMatches = [];
4478
+ const detailMatches = [];
4479
+ const keywordMatches = [];
4480
+ for (let sourceIndex = 0; sourceIndex < sourceItems.length; sourceIndex++) {
4481
+ const item = sourceItems[sourceIndex];
4482
+ if (item.kind === "separator" || item.kind === "group") continue;
4483
+ const label = String(item.label ?? "");
4484
+ const detail = item.detail == null ? "" : String(item.detail);
4485
+ if (label.toLowerCase().includes(needle)) {
4486
+ labelMatches.push({
4487
+ item,
4488
+ sourceIndex,
4489
+ match: { score: 100 }
4490
+ });
4491
+ continue;
4492
+ }
4493
+ if (detail.toLowerCase().includes(needle)) {
4494
+ detailMatches.push({
4495
+ item,
4496
+ sourceIndex,
4497
+ match: { score: 50 }
4498
+ });
4499
+ continue;
4500
+ }
4501
+ if ((item.keywords ?? []).some((keyword) => String(keyword ?? "").toLowerCase().includes(needle))) keywordMatches.push({
4502
+ item,
4503
+ sourceIndex,
4504
+ match: { score: 10 }
4505
+ });
4506
+ }
4507
+ return labelMatches.concat(detailMatches, keywordMatches);
4481
4508
  }
4482
4509
  function fuzzyMatcher(item, query) {
4483
4510
  const q = query.trim().toLowerCase();
@@ -4732,20 +4759,26 @@ const TCommandPalette = defineComponent({
4732
4759
  const filteredEntries = computed(() => {
4733
4760
  const q = query.value.trim();
4734
4761
  const sourceItems = props.itemsProvider ? providerItems.value ?? [] : props.items;
4735
- const matcher = props.matcher ?? (props.filterStrategy === "fuzzy" ? fuzzyMatcher : substringMatcher);
4736
- const entries = sourceItems.flatMap((item, sourceIndex) => {
4737
- if (item.kind === "separator" || item.kind === "group") return q ? [] : [{
4738
- item,
4739
- sourceIndex,
4740
- match: { score: 0 }
4741
- }];
4762
+ if (!props.matcher && props.filterStrategy === "substring") return defaultSubstringEntries(sourceItems, q);
4763
+ const matcher = props.matcher ?? fuzzyMatcher;
4764
+ const entries = [];
4765
+ for (let sourceIndex = 0; sourceIndex < sourceItems.length; sourceIndex++) {
4766
+ const item = sourceItems[sourceIndex];
4767
+ if (item.kind === "separator" || item.kind === "group") {
4768
+ if (!q) entries.push({
4769
+ item,
4770
+ sourceIndex,
4771
+ match: { score: 0 }
4772
+ });
4773
+ continue;
4774
+ }
4742
4775
  const match = matcher(item, q);
4743
- return match ? [{
4776
+ if (match) entries.push({
4744
4777
  item,
4745
4778
  sourceIndex,
4746
4779
  match
4747
- }] : [];
4748
- });
4780
+ });
4781
+ }
4749
4782
  entries.sort((a, b) => b.match.score - a.match.score || a.sourceIndex - b.sourceIndex);
4750
4783
  return entries;
4751
4784
  });
@@ -5383,6 +5416,40 @@ const TSelect = defineComponent({
5383
5416
  const typeaheadQuery = ref("");
5384
5417
  const query = computed(() => props.query ?? innerQuery.value);
5385
5418
  const options = computed(() => providerOptions.value ?? props.options);
5419
+ function getOptionLabel(opt) {
5420
+ return isOptionObject(opt) ? opt.label : opt;
5421
+ }
5422
+ function getOptionKind(opt) {
5423
+ return isOptionObject(opt) ? opt.kind : void 0;
5424
+ }
5425
+ function getOptionValue(opt, index) {
5426
+ if (props.valueMode === "index") return index;
5427
+ if (props.valueMode === "option") return opt;
5428
+ return isOptionObject(opt) && "value" in opt ? opt.value : getOptionLabel(opt);
5429
+ }
5430
+ const optionValueIndex = computed(() => {
5431
+ if (props.valueMode === "index") return null;
5432
+ const indexed = new Map();
5433
+ const opts = options.value;
5434
+ for (let index = 0; index < opts.length; index++) {
5435
+ const value = getOptionValue(opts[index], index);
5436
+ if (typeof value === "number" && value === 0) continue;
5437
+ if (!indexed.has(value)) indexed.set(value, index);
5438
+ }
5439
+ return indexed;
5440
+ });
5441
+ function valuesEqual(a, b) {
5442
+ return Object.is(a, b);
5443
+ }
5444
+ function modelIndex(value, useValueIndex = props.multiple) {
5445
+ if (props.valueMode === "index") return normalizeModelIndex(value);
5446
+ if (useValueIndex) {
5447
+ const indexed = optionValueIndex.value;
5448
+ const cached = indexed?.get(value);
5449
+ if (cached != null) return cached;
5450
+ }
5451
+ return options.value.findIndex((opt, optIndex) => valuesEqual(getOptionValue(opt, optIndex), value));
5452
+ }
5386
5453
  const initialActive = (() => {
5387
5454
  const max = Math.max(0, options.value.length - 1);
5388
5455
  if (!props.multiple) {
@@ -5442,24 +5509,6 @@ const TSelect = defineComponent({
5442
5509
  }
5443
5510
  innerActive.value = clamp(active.value, 0, max);
5444
5511
  });
5445
- function getOptionLabel(opt) {
5446
- return isOptionObject(opt) ? opt.label : opt;
5447
- }
5448
- function getOptionKind(opt) {
5449
- return isOptionObject(opt) ? opt.kind : void 0;
5450
- }
5451
- function getOptionValue(opt, index) {
5452
- if (props.valueMode === "index") return index;
5453
- if (props.valueMode === "option") return opt;
5454
- return isOptionObject(opt) && "value" in opt ? opt.value : getOptionLabel(opt);
5455
- }
5456
- function valuesEqual(a, b) {
5457
- return Object.is(a, b);
5458
- }
5459
- function modelIndex(value) {
5460
- if (props.valueMode === "index") return normalizeModelIndex(value);
5461
- return options.value.findIndex((opt, optIndex) => valuesEqual(getOptionValue(opt, optIndex), value));
5462
- }
5463
5512
  function isOptionInteractive(opt) {
5464
5513
  if (!opt) return false;
5465
5514
  const kind = getOptionKind(opt);
@@ -1,13 +1,13 @@
1
- import { TERMINAL_RENDER_PLANES, createTerminal, getPlaneTerminal, resetPlaneRowsForRender, scrollPlaneRows } from "./create-terminal-C2oiWFJU.js";
1
+ import { TERMINAL_RENDER_PLANES, createTerminal, getPlaneTerminal, resetPlaneRowsForRender, scrollPlaneRows } from "./create-terminal-DBFNYr8f.js";
2
2
  import { sanitizeDomHref } from "./hyperlink-Bn02UXJb.js";
3
- import { DOM_RENDERER_CAPABILITIES, createDomRenderer } from "./dom-renderer-BqGijQID.js";
3
+ import { DOM_RENDERER_CAPABILITIES, createDomRenderer } from "./dom-renderer-BniJSQyX.js";
4
4
  import { EventZIndexContextKey, ImeAnchorContextKey, LayoutContextKey, RenderStackKey, TInputPluginsContextKey, TPathPickerProviderContextKey, TerminalContextKey, VisibilityContextKey, applyWheelScroll, clearTextCaches, createWheelScrollState, forEachTextCellSegment, formatInlineCellLine, injectionKey, intersectRect, normalizeCellRect, padEndByCells$1 as padEndByCells, resetWheelScrollState, sanitizeInlineText, sanitizeTextBlock, sliceByCells, sliceByCellsRange, spaces, textCellWidth$1 as textCellWidth, translateRect, useLayout, useRenderNode, useTerminal, useTerminalNode, useVisibility, withTextRenderPass } from "./wheel-scroll-vBRaQ7p-.js";
5
5
  import { TView, TuiThemeContextKey, createFrameMailbox, createTheme, defaultActiveStyle, defaultDimStyle, linkifyTextSegments, tuiDefaultTheme } from "./linkify-D_gzn9Xq.js";
6
6
  import { SUPPRESS_TERMINAL_POINTER_DOWN, SUPPRESS_TERMINAL_POINTER_MOVE, SUPPRESS_TERMINAL_POINTER_UP, createEventManager, createRuntime } from "./runtime-rfcZCyJq.js";
7
7
  import { createFramePerfStore, createTraceStore } from "./trace-CjzYeIxN.js";
8
8
  import { envFlag, envString, firstNonEmptyEnv } from "./env-Cl8qmNFN.js";
9
9
  import { createTerminalSelectionController } from "./terminal-selection-DBinO6p5.js";
10
- import { TBox, TInput, TRenderPlane, TText, clamp, fitCellText, mergeStyle, repeatToCells, resolveOverlayPlacement } from "./TSelect-BwJJbuTt.js";
10
+ import { TBox, TInput, TRenderPlane, TText, clamp, fitCellText, mergeStyle, repeatToCells, resolveOverlayPlacement } from "./TSelect-DR0FomSu.js";
11
11
  import { framePerfNow, mergeFramePerfReason } from "./frame-perf-C6Xfg85O.js";
12
12
  import { computed, defineComponent, effectScope, getCurrentInstance, h, inject, nextTick, onBeforeUnmount, onMounted, onScopeDispose, onUnmounted, provide, ref, shallowReactive, shallowRef, toRef, watch, watchEffect } from "vue";
13
13
 
@@ -2991,6 +2991,17 @@ function rowKey$1(row, index, key) {
2991
2991
  function sameRowKey(a, b) {
2992
2992
  return Object.is(a, b);
2993
2993
  }
2994
+ function selectedRowKeyIndex(keys) {
2995
+ if (!keys?.length) return null;
2996
+ const indexed = new Set();
2997
+ for (let index = 0; index < keys.length; index++) {
2998
+ if (!(index in keys)) continue;
2999
+ const key = keys[index];
3000
+ if (typeof key === "number" && key === 0) return null;
3001
+ indexed.add(key);
3002
+ }
3003
+ return indexed;
3004
+ }
2994
3005
  const TTable = defineComponent({
2995
3006
  name: "TTable",
2996
3007
  props: {
@@ -3099,10 +3110,19 @@ const TTable = defineComponent({
3099
3110
  const rowStyle = computed(() => mergeStyle(defaultStyle.value, theme.value.components.TTable?.rowStyle, props.style));
3100
3111
  const selectedRowStyle = computed(() => mergeStyle(rowStyle.value, theme.value.components.TTable?.selectedStyle, props.selectedStyle));
3101
3112
  const activeRowStyle = computed(() => mergeStyle(theme.value.components.TTable?.activeStyle, props.activeStyle));
3113
+ const selectedRowKeysIndex = computed(() => selectedRowKeyIndex(props.selectedRowKeys));
3102
3114
  const bodyRows = computed(() => {
3103
3115
  const top = props.header ? 2 : 0;
3104
3116
  return props.rows.slice(0, Math.max(0, props.h - top));
3105
3117
  });
3118
+ function isRowSelected(key) {
3119
+ if (props.selectedRowKey !== void 0 && sameRowKey(key, props.selectedRowKey)) return true;
3120
+ const selectedKeys = props.selectedRowKeys;
3121
+ if (!selectedKeys?.length) return false;
3122
+ const indexed = selectedRowKeysIndex.value;
3123
+ if (indexed) return indexed.has(key);
3124
+ return selectedKeys.some((candidate) => sameRowKey(candidate, key));
3125
+ }
3106
3126
  return () => {
3107
3127
  const children = [];
3108
3128
  if (props.header && props.h > 0) {
@@ -3169,7 +3189,7 @@ const TTable = defineComponent({
3169
3189
  const row = bodyRows.value[index];
3170
3190
  const y = (props.header ? 2 : 0) + index;
3171
3191
  const key = rowKey$1(row, index, props.rowKey);
3172
- const selected = props.selectedRowKey !== void 0 && sameRowKey(key, props.selectedRowKey) || Boolean(props.selectedRowKeys?.some((candidate) => sameRowKey(candidate, key)));
3192
+ const selected = isRowSelected(key);
3173
3193
  const active = props.activeRowKey !== void 0 && sameRowKey(key, props.activeRowKey);
3174
3194
  children.push(h(TView, {
3175
3195
  key: `row:${String(key)}`,
@@ -3387,34 +3407,45 @@ const TDataTable = defineComponent({
3387
3407
  },
3388
3408
  setup(props, { emit }) {
3389
3409
  const innerScrollTop = ref(0);
3390
- const indexedRows = computed(() => props.rows.map((row, originalIndex) => ({
3391
- row,
3392
- originalIndex
3393
- })));
3410
+ const hasLocalFilter = computed(() => !props.manualFilter && props.filterable && Boolean(props.filter.trim()));
3411
+ const hasLocalSort = computed(() => !props.manualSort && props.sortable && Boolean(props.sortBy));
3412
+ const indexedRows = computed(() => {
3413
+ if (!hasLocalFilter.value && !hasLocalSort.value) return null;
3414
+ return props.rows.map((row, originalIndex) => ({
3415
+ row,
3416
+ originalIndex
3417
+ }));
3418
+ });
3394
3419
  const filteredRows = computed(() => {
3395
- if (props.manualFilter) return indexedRows.value;
3396
- if (!props.filterable || !props.filter.trim()) return indexedRows.value;
3420
+ const rows = indexedRows.value;
3421
+ if (!rows) return null;
3422
+ if (!hasLocalFilter.value) return rows;
3397
3423
  const query = props.filter.trim().toLowerCase();
3398
- if (props.filterPredicate) return indexedRows.value.filter(({ row, originalIndex }) => props.filterPredicate(row, query, props.columns, originalIndex));
3399
- return indexedRows.value.filter(({ row, originalIndex }) => props.columns.some((column) => displayValue(column, row, originalIndex).toLowerCase().includes(query)));
3424
+ if (props.filterPredicate) return rows.filter(({ row, originalIndex }) => props.filterPredicate(row, query, props.columns, originalIndex));
3425
+ return rows.filter(({ row, originalIndex }) => props.columns.some((column) => displayValue(column, row, originalIndex).toLowerCase().includes(query)));
3400
3426
  });
3401
3427
  const sortedRows = computed(() => {
3402
- const rows = [...filteredRows.value];
3403
- if (props.manualSort || !props.sortable || !props.sortBy) return rows;
3428
+ const rows = filteredRows.value;
3429
+ if (!rows) return null;
3430
+ if (!hasLocalSort.value) return rows;
3431
+ const sorted = [...rows];
3404
3432
  const column = props.columns.find((candidate) => candidate.key === props.sortBy);
3405
- rows.sort((a, b) => {
3433
+ sorted.sort((a, b) => {
3406
3434
  const result = props.sorter && column ? props.sorter(a.row, b.row, column) : compareValues(a.row[props.sortBy], b.row[props.sortBy]);
3407
3435
  return props.sortDirection === "desc" ? -result : result;
3408
3436
  });
3409
- return rows;
3437
+ return sorted;
3410
3438
  });
3411
3439
  function nonNegativeInteger(value) {
3412
3440
  const n = Math.floor(Number(value));
3413
3441
  return Number.isFinite(n) ? Math.max(0, n) : 0;
3414
3442
  }
3415
3443
  const visibleRowCapacity = computed(() => Math.max(0, nonNegativeInteger(props.h) - 2));
3444
+ function processedRowCount() {
3445
+ return sortedRows.value?.length ?? props.rows.length;
3446
+ }
3416
3447
  function maxScrollTop$1() {
3417
- return Math.max(0, sortedRows.value.length - visibleRowCapacity.value);
3448
+ return Math.max(0, processedRowCount() - visibleRowCapacity.value);
3418
3449
  }
3419
3450
  function clampScrollTop$1(value) {
3420
3451
  return Math.min(maxScrollTop$1(), nonNegativeInteger(value));
@@ -3430,7 +3461,28 @@ const TDataTable = defineComponent({
3430
3461
  watch(() => [normalizedScrollTop.value, requestedScrollTop.value], ([next, requested]) => {
3431
3462
  if (next !== requested) setScrollTop(next);
3432
3463
  }, { immediate: true });
3433
- const visibleRows = computed(() => sortedRows.value.slice(normalizedScrollTop.value, normalizedScrollTop.value + visibleRowCapacity.value));
3464
+ function dataEntryAt(absoluteIndex) {
3465
+ const rows = sortedRows.value;
3466
+ if (rows) return rows[absoluteIndex];
3467
+ const row = props.rows[absoluteIndex];
3468
+ return row ? {
3469
+ row,
3470
+ originalIndex: absoluteIndex
3471
+ } : void 0;
3472
+ }
3473
+ function originalIndexForAbsoluteIndex(absoluteIndex) {
3474
+ return dataEntryAt(absoluteIndex)?.originalIndex ?? absoluteIndex;
3475
+ }
3476
+ const visibleRows = computed(() => {
3477
+ const top = normalizedScrollTop.value;
3478
+ const bottom = top + visibleRowCapacity.value;
3479
+ const rows = sortedRows.value;
3480
+ if (rows) return rows.slice(top, bottom);
3481
+ return props.rows.slice(top, bottom).map((row, index) => ({
3482
+ row,
3483
+ originalIndex: top + index
3484
+ }));
3485
+ });
3434
3486
  const tableRows = computed(() => visibleRows.value.map(({ row }) => row));
3435
3487
  const activeAbsoluteIndex = ref(null);
3436
3488
  const activeRowIdentity = ref(NO_ACTIVE_ROW);
@@ -3438,7 +3490,7 @@ const TDataTable = defineComponent({
3438
3490
  const columns = computed(() => props.columns.map((column) => {
3439
3491
  const format = column.format ? (value, row, visibleIndex) => {
3440
3492
  const absoluteIndex = normalizedScrollTop.value + visibleIndex;
3441
- const originalIndex = sortedRows.value[absoluteIndex]?.originalIndex ?? absoluteIndex;
3493
+ const originalIndex = originalIndexForAbsoluteIndex(absoluteIndex);
3442
3494
  return column.format(value, row, originalIndex);
3443
3495
  } : void 0;
3444
3496
  if (!props.sortable || column.key !== props.sortBy) return format ? {
@@ -3454,7 +3506,7 @@ const TDataTable = defineComponent({
3454
3506
  }));
3455
3507
  function originalIndexAt(index) {
3456
3508
  const absoluteIndex = normalizedScrollTop.value + index;
3457
- return sortedRows.value[absoluteIndex]?.originalIndex ?? absoluteIndex;
3509
+ return originalIndexForAbsoluteIndex(absoluteIndex);
3458
3510
  }
3459
3511
  function dataTableRowKey(row, index) {
3460
3512
  return rowKey(row, originalIndexAt(index), props.rowKey);
@@ -3472,18 +3524,42 @@ const TDataTable = defineComponent({
3472
3524
  return new Set(props.selectedRowKey === void 0 ? [] : [props.selectedRowKey]);
3473
3525
  }
3474
3526
  const activeRowKey = computed(() => activeRowIdentity.value === NO_ACTIVE_ROW ? void 0 : activeRowIdentity.value);
3475
- watch(() => [props.rowKey, ...sortedRows.value.map((entry) => dataRowKey(entry))], () => {
3527
+ const activeRawRowKey = computed(() => {
3528
+ if (activeAbsoluteIndex.value == null || sortedRows.value) return NO_ACTIVE_ROW;
3529
+ const index = activeAbsoluteIndex.value;
3530
+ const row = props.rows[index];
3531
+ return row ? rowKey(row, index, props.rowKey) : NO_ACTIVE_ROW;
3532
+ });
3533
+ function findDataIndexByKey(key) {
3534
+ const rows = sortedRows.value;
3535
+ if (rows) return rows.findIndex((entry) => Object.is(dataRowKey(entry), key));
3536
+ return props.rows.findIndex((row, index) => Object.is(rowKey(row, index, props.rowKey), key));
3537
+ }
3538
+ watch(() => [
3539
+ props.rowKey,
3540
+ props.rows,
3541
+ sortedRows.value,
3542
+ activeRowIdentity.value
3543
+ ], () => {
3476
3544
  if (activeRowIdentity.value === NO_ACTIVE_ROW) return;
3477
- const nextIndex = sortedRows.value.findIndex((entry) => Object.is(dataRowKey(entry), activeRowIdentity.value));
3545
+ const nextIndex = findDataIndexByKey(activeRowIdentity.value);
3478
3546
  if (nextIndex < 0) {
3479
3547
  clearActiveRow();
3480
3548
  return;
3481
3549
  }
3482
3550
  activeAbsoluteIndex.value = nextIndex;
3483
3551
  });
3552
+ watch(() => [props.rowKey, activeRawRowKey.value], ([, key]) => {
3553
+ if (activeRowIdentity.value === NO_ACTIVE_ROW) return;
3554
+ if (key === NO_ACTIVE_ROW) {
3555
+ clearActiveRow();
3556
+ return;
3557
+ }
3558
+ if (!Object.is(activeRowIdentity.value, key)) activeRowIdentity.value = key;
3559
+ });
3484
3560
  function setActiveAbsoluteIndex(absoluteIndex) {
3485
- const clamped = Math.max(0, Math.min(sortedRows.value.length - 1, absoluteIndex));
3486
- const entry = sortedRows.value[clamped];
3561
+ const clamped = Math.max(0, Math.min(processedRowCount() - 1, absoluteIndex));
3562
+ const entry = dataEntryAt(clamped);
3487
3563
  if (!entry) {
3488
3564
  clearActiveRow();
3489
3565
  return null;
@@ -3550,7 +3626,7 @@ const TDataTable = defineComponent({
3550
3626
  if (event.key === "Enter" || event.key === " ") {
3551
3627
  const active = setActiveAbsoluteIndex(current);
3552
3628
  if (!active) return;
3553
- const entry = sortedRows.value[active.dataIndex];
3629
+ const entry = dataEntryAt(active.dataIndex);
3554
3630
  if (!entry) return;
3555
3631
  const visibleIndex = active.dataIndex - active.scrollTop;
3556
3632
  commitSelection(entry, Math.max(0, Math.min(Math.max(0, visibleRowCapacity.value - 1), visibleIndex)), active.dataIndex);
@@ -6315,8 +6391,10 @@ const TLinkifyText = defineComponent({
6315
6391
 
6316
6392
  //#endregion
6317
6393
  //#region src/vue/components/TTree.ts
6318
- function flattenTree(nodes, expanded, depth = 0, out = []) {
6394
+ function flattenTree(nodes, expanded, limit, depth = 0, out = []) {
6395
+ if (out.length >= limit) return out;
6319
6396
  for (const node of nodes) {
6397
+ if (out.length >= limit) break;
6320
6398
  const expandable = Boolean(node.children?.length);
6321
6399
  const isExpanded = expanded.has(node.id);
6322
6400
  out.push({
@@ -6325,10 +6403,14 @@ function flattenTree(nodes, expanded, depth = 0, out = []) {
6325
6403
  expandable,
6326
6404
  expanded: isExpanded
6327
6405
  });
6328
- if (expandable && isExpanded) flattenTree(node.children, expanded, depth + 1, out);
6406
+ if (expandable && isExpanded) flattenTree(node.children, expanded, limit, depth + 1, out);
6329
6407
  }
6330
6408
  return out;
6331
6409
  }
6410
+ function visibleRowLimit(height) {
6411
+ const limit = Math.floor(height);
6412
+ return Number.isFinite(limit) ? Math.max(0, limit) : 0;
6413
+ }
6332
6414
  const TTree = defineComponent({
6333
6415
  name: "TTree",
6334
6416
  props: {
@@ -6395,7 +6477,7 @@ const TTree = defineComponent({
6395
6477
  const { defaultStyle } = useTerminal();
6396
6478
  const baseStyle = computed(() => mergeStyle(defaultStyle.value, props.style));
6397
6479
  const expandedSet = computed(() => new Set(props.expandedIds));
6398
- const rows = computed(() => flattenTree(props.nodes, expandedSet.value).slice(0, props.h));
6480
+ const rows = computed(() => flattenTree(props.nodes, expandedSet.value, visibleRowLimit(props.h)));
6399
6481
  function toggle(item) {
6400
6482
  if (item.node.disabled || !item.expandable) return;
6401
6483
  const ids = new Set(props.expandedIds);