@visulima/tui 1.0.0-alpha.1 → 1.0.0-alpha.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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## @visulima/tui [1.0.0-alpha.2](https://github.com/visulima/visulima/compare/@visulima/tui@1.0.0-alpha.1...@visulima/tui@1.0.0-alpha.2) (2026-04-09)
2
+
3
+ ### Features
4
+
5
+ * **tui:** add controlled value prop to Tabs component ([df1158d](https://github.com/visulima/visulima/commit/df1158dc036f7159ef909f497453e2e5dd1058df))
6
+
1
7
  ## @visulima/tui 1.0.0-alpha.1 (2026-04-08)
2
8
 
3
9
  ### Features
@@ -55,7 +55,8 @@ export type Props = {
55
55
  */
56
56
  readonly colors?: TabColors;
57
57
  /**
58
- * Name of the tab to select initially.
58
+ * Name of the tab to select initially (uncontrolled mode).
59
+ * Ignored when `value` is provided.
59
60
  */
60
61
  readonly defaultValue?: string;
61
62
  /**
@@ -84,6 +85,12 @@ export type Props = {
84
85
  * @default true
85
86
  */
86
87
  readonly showIndex?: boolean;
88
+ /**
89
+ * Controlled active tab name. When provided, the component is controlled —
90
+ * the parent owns the active tab state and must update it via `onChange`.
91
+ * When omitted, the component manages its own state (uncontrolled mode).
92
+ */
93
+ readonly value?: string;
87
94
  /**
88
95
  * Width of the container Box. Also determines the separator width in column layouts.
89
96
  */
@@ -94,4 +101,4 @@ export type Props = {
94
101
  * navigation (arrow keys, Tab, number keys) and calls `onChange` when the
95
102
  * active tab changes.
96
103
  */
97
- export default function Tabs({ children, colors: colorsProp, defaultValue, flexDirection, isFocused, keyMap: keyMapProp, onChange, showIndex, width, }: Props): ReactElement;
104
+ export default function Tabs({ children, colors: colorsProp, defaultValue, flexDirection, isFocused, keyMap: keyMapProp, onChange, showIndex, value, width, }: Props): ReactElement;
package/dist/ink/index.js CHANGED
@@ -31,7 +31,7 @@ export { default as StatusMessage } from '../packem_shared/StatusMessage-BtSYxzw
31
31
  export { default as Stopwatch } from '../packem_shared/Stopwatch-DjtTCyHn.js';
32
32
  export { default as Tab } from '../packem_shared/Tab-C6r5q4hR.js';
33
33
  export { default as Table } from '../packem_shared/Table-D-rSVlzb.js';
34
- export { default as Tabs } from '../packem_shared/Tabs-BXpZwLWo.js';
34
+ export { default as Tabs } from '../packem_shared/Tabs-hki-KRnx.js';
35
35
  export { default as Text } from '../packem_shared/Text-DvAUdZcK.js';
36
36
  export { default as Timer } from '../packem_shared/Timer-wi9zuh4n.js';
37
37
  export { default as Textarea } from '../packem_shared/Textarea-Dj06R6Zd.js';
@@ -37,6 +37,7 @@ function Tabs({
37
37
  keyMap: keyMapProp,
38
38
  onChange,
39
39
  showIndex = true,
40
+ value,
40
41
  width
41
42
  }) {
42
43
  const tabs = collectTabs(children);
@@ -44,16 +45,24 @@ function Tabs({
44
45
  const tabsRef = useRef(tabs);
45
46
  tabsRef.current = tabs;
46
47
  const isColumn = flexDirection === "column" || flexDirection === "column-reverse";
47
- const [activeTab, setActiveTab] = useState(() => {
48
- if (defaultValue == null || tabCount === 0) {
48
+ const isControlled = value !== void 0;
49
+ const [internalTab, setInternalTab] = useState(() => {
50
+ const initial = value ?? defaultValue;
51
+ if (initial == null || tabCount === 0) {
49
52
  return 0;
50
53
  }
51
- const foundIndex = tabs.findIndex((tab) => tab.props.name === defaultValue);
54
+ const foundIndex = tabs.findIndex((tab) => tab.props.name === initial);
52
55
  return foundIndex === -1 ? 0 : foundIndex;
53
56
  });
57
+ const activeTab = isControlled ? Math.max(0, tabs.findIndex((tab) => tab.props.name === value)) : internalTab;
58
+ const setActiveTab = isControlled ? () => {
59
+ } : setInternalTab;
54
60
  const onChangeRef = useRef(onChange);
55
61
  onChangeRef.current = onChange;
56
62
  useEffect(() => {
63
+ if (isControlled) {
64
+ return;
65
+ }
57
66
  const tab = tabsRef.current[activeTab];
58
67
  if (tab) {
59
68
  onChangeRef.current(tab.props.name, tab);
@@ -63,33 +72,31 @@ function Tabs({
63
72
  const nextKeys = useMemo(() => keyMapProp?.next ?? [isColumn ? "downArrow" : "rightArrow"], [keyMapProp?.next, isColumn]);
64
73
  const useNumbers = keyMapProp?.useNumbers ?? true;
65
74
  const useTab = keyMapProp?.useTab ?? true;
75
+ const activeTabRef = useRef(activeTab);
76
+ activeTabRef.current = activeTab;
66
77
  const handleTabChange = useCallback((index) => {
67
78
  setActiveTab(index);
68
79
  const tab = tabsRef.current[index];
69
80
  if (tab) {
70
81
  onChangeRef.current(tab.props.name, tab);
71
82
  }
72
- }, []);
83
+ }, [setActiveTab]);
73
84
  const moveToNext = useCallback(() => {
74
- setActiveTab((current) => {
75
- const next = current + 1 >= tabCount ? 0 : current + 1;
76
- const tab = tabsRef.current[next];
77
- if (tab) {
78
- onChangeRef.current(tab.props.name, tab);
79
- }
80
- return next;
81
- });
82
- }, [tabCount]);
85
+ const next = (activeTabRef.current + 1) % tabCount;
86
+ setActiveTab(next);
87
+ const tab = tabsRef.current[next];
88
+ if (tab) {
89
+ onChangeRef.current(tab.props.name, tab);
90
+ }
91
+ }, [tabCount, setActiveTab]);
83
92
  const moveToPrevious = useCallback(() => {
84
- setActiveTab((current) => {
85
- const previous = current - 1 < 0 ? tabCount - 1 : current - 1;
86
- const tab = tabsRef.current[previous];
87
- if (tab) {
88
- onChangeRef.current(tab.props.name, tab);
89
- }
90
- return previous;
91
- });
92
- }, [tabCount]);
93
+ const previous = (activeTabRef.current - 1 + tabCount) % tabCount;
94
+ setActiveTab(previous);
95
+ const tab = tabsRef.current[previous];
96
+ if (tab) {
97
+ onChangeRef.current(tab.props.name, tab);
98
+ }
99
+ }, [tabCount, setActiveTab]);
93
100
  useInput(
94
101
  useCallback(
95
102
  (input, key) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visulima/tui",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.2",
4
4
  "description": "React-based TUI library powered by a native Rust diff engine, drop-in Ink-compatible API",
5
5
  "keywords": [
6
6
  "visulima",
@@ -101,14 +101,14 @@
101
101
  }
102
102
  },
103
103
  "optionalDependencies": {
104
- "@visulima/tui-binding-darwin-arm64": "1.0.0-alpha.1",
105
- "@visulima/tui-binding-darwin-x64": "1.0.0-alpha.1",
106
- "@visulima/tui-binding-linux-arm64-gnu": "1.0.0-alpha.1",
107
- "@visulima/tui-binding-linux-arm64-musl": "1.0.0-alpha.1",
108
- "@visulima/tui-binding-linux-x64-gnu": "1.0.0-alpha.1",
109
- "@visulima/tui-binding-linux-x64-musl": "1.0.0-alpha.1",
110
- "@visulima/tui-binding-win32-arm64-msvc": "1.0.0-alpha.1",
111
- "@visulima/tui-binding-win32-x64-msvc": "1.0.0-alpha.1"
104
+ "@visulima/tui-binding-darwin-arm64": "1.0.0-alpha.2",
105
+ "@visulima/tui-binding-darwin-x64": "1.0.0-alpha.2",
106
+ "@visulima/tui-binding-linux-arm64-gnu": "1.0.0-alpha.2",
107
+ "@visulima/tui-binding-linux-x64-gnu": "1.0.0-alpha.2",
108
+ "@visulima/tui-binding-linux-arm64-musl": "1.0.0-alpha.2",
109
+ "@visulima/tui-binding-linux-x64-musl": "1.0.0-alpha.2",
110
+ "@visulima/tui-binding-win32-arm64-msvc": "1.0.0-alpha.2",
111
+ "@visulima/tui-binding-win32-x64-msvc": "1.0.0-alpha.2"
112
112
  },
113
113
  "engines": {
114
114
  "node": ">=22.13 <=25.x"