@dxos/react-ui-tabs 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

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/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-tabs",
3
- "version": "0.8.4-main.fd6878d",
3
+ "version": "0.8.4-staging.60fe92afc8",
4
4
  "description": "Components for facilitating a Tabs pattern.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
- "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
11
+ "license": "FSL-1.1-Apache-2.0",
8
12
  "author": "DXOS.org",
9
- "sideEffects": true,
13
+ "sideEffects": false,
10
14
  "type": "module",
11
15
  "exports": {
12
16
  ".": {
@@ -17,41 +21,37 @@
17
21
  }
18
22
  },
19
23
  "types": "dist/types/src/index.d.ts",
20
- "typesVersions": {
21
- "*": {}
22
- },
23
24
  "files": [
24
25
  "dist",
25
26
  "src"
26
27
  ],
27
28
  "dependencies": {
28
- "@fluentui/react-tabster": "^9.24.2",
29
- "@preact-signals/safe-react": "^0.9.0",
29
+ "@fluentui/react-tabster": "9.26.11",
30
30
  "@radix-ui/primitive": "1.1.1",
31
31
  "@radix-ui/react-context": "1.1.1",
32
32
  "@radix-ui/react-primitive": "2.0.2",
33
33
  "@radix-ui/react-slot": "1.1.2",
34
34
  "@radix-ui/react-tabs": "1.1.3",
35
35
  "@radix-ui/react-use-controllable-state": "1.1.0",
36
- "@dxos/react-ui-attention": "0.8.4-main.fd6878d",
37
- "@dxos/util": "0.8.4-main.fd6878d"
36
+ "@dxos/react-ui-attention": "0.8.4-staging.60fe92afc8",
37
+ "@dxos/util": "0.8.4-staging.60fe92afc8"
38
38
  },
39
39
  "devDependencies": {
40
- "@types/react": "~18.2.0",
41
- "@types/react-dom": "~18.2.0",
42
- "react": "~18.2.0",
43
- "react-dom": "~18.2.0",
44
- "vite": "5.4.7",
45
- "@dxos/random": "0.8.4-main.fd6878d",
46
- "@dxos/react-ui": "0.8.4-main.fd6878d",
47
- "@dxos/react-ui-theme": "0.8.4-main.fd6878d",
48
- "@dxos/storybook-utils": "0.8.4-main.fd6878d"
40
+ "@types/react": "~19.2.7",
41
+ "@types/react-dom": "~19.2.3",
42
+ "react": "~19.2.3",
43
+ "react-dom": "~19.2.3",
44
+ "vite": "^8.0.16",
45
+ "@dxos/react-ui": "0.8.4-staging.60fe92afc8",
46
+ "@dxos/storybook-utils": "0.8.4-staging.60fe92afc8",
47
+ "@dxos/ui-theme": "0.8.4-staging.60fe92afc8",
48
+ "@dxos/random": "0.8.4-staging.60fe92afc8"
49
49
  },
50
50
  "peerDependencies": {
51
- "react": "~18.2.0",
52
- "react-dom": "~18.2.0",
53
- "@dxos/react-ui": "0.8.4-main.fd6878d",
54
- "@dxos/react-ui-theme": "0.8.4-main.fd6878d"
51
+ "react": "~19.2.3",
52
+ "react-dom": "~19.2.3",
53
+ "@dxos/react-ui": "0.8.4-staging.60fe92afc8",
54
+ "@dxos/ui-theme": "0.8.4-staging.60fe92afc8"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public"
@@ -2,64 +2,76 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
5
6
  import React from 'react';
6
7
 
7
- import { faker } from '@dxos/random';
8
- import { Dialog, Icon } from '@dxos/react-ui';
9
- import { withTheme } from '@dxos/storybook-utils';
8
+ import { random } from '@dxos/random';
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
+ import { mx } from '@dxos/ui-theme';
10
11
 
11
- import { Tabs as NaturalTabs } from './Tabs';
12
+ import { Tabs, TabsRootProps } from './Tabs';
12
13
 
13
- faker.seed(1234);
14
+ random.seed(1234);
15
+
16
+ const DefaultStory = ({ orientation }: TabsRootProps) => {
17
+ return (
18
+ <Tabs.Root orientation={orientation} defaultValue={Object.keys(content)[3]} defaultActivePart='list'>
19
+ <Tabs.Viewport
20
+ classNames={mx(
21
+ 'w-full overflow-hidden grid',
22
+ orientation === 'vertical' && 'grid-cols-[minmax(min-content,1fr)_3fr]',
23
+ )}
24
+ >
25
+ <Tabs.Tablist>
26
+ {Object.entries(content).map(([id, { title }]) => (
27
+ <Tabs.Tab key={id} value={id}>
28
+ {title}
29
+ </Tabs.Tab>
30
+ ))}
31
+ </Tabs.Tablist>
32
+ <div className='dx-container'>
33
+ {Object.entries(content).map(([id, { panel }]) => (
34
+ <Tabs.Panel key={id} value={id}>
35
+ <p className='px-1'>{panel}</p>
36
+ </Tabs.Panel>
37
+ ))}
38
+ </div>
39
+ </Tabs.Viewport>
40
+ </Tabs.Root>
41
+ );
42
+ };
14
43
 
15
44
  const content = [...Array(24)].reduce((acc: { [key: string]: { title: string; panel: string } }, _, index) => {
16
45
  acc[`t${index}`] = {
17
- title: faker.commerce.productName(),
18
- panel: faker.lorem.paragraphs(5),
46
+ title: random.commerce.productName(),
47
+ panel: random.lorem.paragraphs(5),
19
48
  };
20
49
  return acc;
21
50
  }, {});
22
51
 
23
- export default {
52
+ const meta = {
24
53
  title: 'ui/react-ui-tabs/Tabs',
25
- component: NaturalTabs.Root,
26
- decorators: [withTheme],
27
- // parameters: { translations },
54
+ component: Tabs.Root,
55
+ render: DefaultStory,
56
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
57
+ parameters: {
58
+ layout: 'fullscreen',
59
+ },
60
+ } satisfies Meta<typeof DefaultStory>;
61
+
62
+ export default meta;
63
+
64
+ type Story = StoryObj<typeof meta>;
65
+
66
+ // TODO(burdon): Scrolling.
67
+ export const Horizontal: Story = {
68
+ args: {
69
+ orientation: 'horizontal',
70
+ },
28
71
  };
29
72
 
30
- export const Tabs = {
31
- render: () => {
32
- return (
33
- <Dialog.Root open>
34
- <Dialog.Overlay blockAlign='start'>
35
- <Dialog.Content classNames='is-[calc(100dvw-4rem)] !max-is-full'>
36
- <NaturalTabs.Root orientation='vertical' defaultValue={Object.keys(content)[3]} defaultActivePart='list'>
37
- <NaturalTabs.Viewport>
38
- <NaturalTabs.Tablist>
39
- {Object.entries(content).map(([id, { title }]) => {
40
- return (
41
- <NaturalTabs.Tab key={id} value={id}>
42
- {title}
43
- </NaturalTabs.Tab>
44
- );
45
- })}
46
- </NaturalTabs.Tablist>
47
- {Object.entries(content).map(([id, { panel }]) => {
48
- return (
49
- <NaturalTabs.Tabpanel key={id} value={id} classNames='m-1'>
50
- <NaturalTabs.BackButton density='fine'>
51
- <Icon icon='ph--arrow-left--bold' size={4} />
52
- <span>Back to tab list</span>
53
- </NaturalTabs.BackButton>
54
- <p className='pli-1'>{panel}</p>
55
- </NaturalTabs.Tabpanel>
56
- );
57
- })}
58
- </NaturalTabs.Viewport>
59
- </NaturalTabs.Root>
60
- </Dialog.Content>
61
- </Dialog.Overlay>
62
- </Dialog.Root>
63
- );
73
+ export const Vertical: Story = {
74
+ args: {
75
+ orientation: 'vertical',
64
76
  },
65
77
  };
package/src/Tabs.tsx CHANGED
@@ -6,149 +6,164 @@ import { useArrowNavigationGroup, useFocusFinders, useFocusableGroup } from '@fl
6
6
  import { createContext } from '@radix-ui/react-context';
7
7
  import * as TabsPrimitive from '@radix-ui/react-tabs';
8
8
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
9
- import React, { type ComponentPropsWithoutRef, type MouseEvent, useCallback, useLayoutEffect, useRef } from 'react';
9
+ import React, { type ComponentPropsWithoutRef, type MouseEvent, forwardRef, useCallback, useLayoutEffect } from 'react';
10
10
 
11
- import { Button, type ButtonProps, type ThemedClassName } from '@dxos/react-ui';
11
+ import {
12
+ Button,
13
+ type ButtonProps,
14
+ IconButton,
15
+ type IconButtonProps,
16
+ type ThemedClassName,
17
+ useForwardedRef,
18
+ } from '@dxos/react-ui';
12
19
  import { useAttention } from '@dxos/react-ui-attention';
13
- import { ghostHover, ghostSelectedContainerMd, mx } from '@dxos/react-ui-theme';
20
+ import { mx } from '@dxos/ui-theme';
21
+
22
+ // TODO(burdon): Move to @dxos/react-ui.
14
23
 
15
24
  type TabsActivePart = 'list' | 'panel';
16
25
 
17
26
  const TABS_NAME = 'Tabs';
18
27
 
28
+ //
29
+ // Context
30
+ //
31
+
19
32
  type TabsContextValue = {
20
33
  activePart: TabsActivePart;
21
34
  setActivePart: (nextActivePart: TabsActivePart) => void;
22
35
  attendableId?: string;
23
- verticalVariant?: 'stateful' | 'stateless';
24
36
  } & Pick<TabsPrimitive.TabsProps, 'orientation' | 'value'>;
25
37
 
26
38
  const [TabsContextProvider, useTabsContext] = createContext<TabsContextValue>(TABS_NAME, {
39
+ orientation: 'vertical',
27
40
  activePart: 'list',
28
41
  setActivePart: () => {},
29
- orientation: 'vertical',
30
42
  });
31
43
 
44
+ //
45
+ // Root
46
+ //
47
+
32
48
  type TabsRootProps = ThemedClassName<TabsPrimitive.TabsProps> &
33
- Partial<Pick<TabsContextValue, 'activePart' | 'verticalVariant' | 'attendableId'>> &
34
- Partial<{
35
- onActivePartChange: (nextActivePart: TabsActivePart) => void;
36
- defaultActivePart: TabsActivePart;
37
- }>;
38
-
39
- const TabsRoot = ({
40
- children,
41
- classNames,
42
- activePart: propsActivePart,
43
- onActivePartChange,
44
- defaultActivePart,
45
- value: propsValue,
46
- onValueChange,
47
- defaultValue,
48
- orientation = 'vertical',
49
- activationMode = 'manual',
50
- verticalVariant = 'stateful',
51
- attendableId,
52
- ...props
53
- }: TabsRootProps) => {
54
- // TODO(thure): Without these, we get Groupper/Mover `API used before initialization`, but why?
55
- const _1 = useArrowNavigationGroup();
56
- const _2 = useFocusableGroup();
57
- const [activePart = 'list', setActivePart] = useControllableState({
58
- prop: propsActivePart,
59
- onChange: onActivePartChange,
60
- defaultProp: defaultActivePart,
61
- });
62
-
63
- const [value, setValue] = useControllableState({
64
- prop: propsValue,
65
- onChange: onValueChange,
66
- defaultProp: defaultValue,
67
- });
68
-
69
- const handleValueChange = useCallback(
70
- (nextValue: string) => {
71
- setActivePart('panel');
72
- setValue(nextValue);
49
+ Partial<
50
+ Pick<TabsContextValue, 'activePart' | 'attendableId'> & {
51
+ onActivePartChange: (nextActivePart: TabsActivePart) => void;
52
+ defaultActivePart: TabsActivePart;
53
+ }
54
+ >;
55
+
56
+ const TabsRoot = forwardRef<HTMLDivElement, TabsRootProps>(
57
+ (
58
+ {
59
+ children,
60
+ classNames,
61
+ activePart: propsActivePart,
62
+ onActivePartChange,
63
+ defaultActivePart,
64
+ value: propsValue,
65
+ onValueChange,
66
+ defaultValue,
67
+ orientation = 'vertical',
68
+ activationMode = 'manual',
69
+ attendableId,
70
+ ...props
73
71
  },
74
- [value],
75
- );
72
+ forwardedRef,
73
+ ) => {
74
+ const tabsRoot = useForwardedRef(forwardedRef);
76
75
 
77
- const { findFirstFocusable } = useFocusFinders();
78
- const tabsRoot = useRef<HTMLDivElement | null>(null);
76
+ // TODO(thure): Without these, we get Groupper/Mover `API used before initialization`, but why?
77
+ const _1 = useArrowNavigationGroup();
78
+ const _2 = useFocusableGroup();
79
+ const [activePart = 'list', setActivePart] = useControllableState({
80
+ prop: propsActivePart,
81
+ onChange: onActivePartChange,
82
+ defaultProp: defaultActivePart,
83
+ });
79
84
 
80
- useLayoutEffect(() => {
81
- if (tabsRoot.current) {
82
- findFirstFocusable(tabsRoot.current)?.focus();
83
- }
84
- }, [activePart]);
85
+ const [value, setValue] = useControllableState({
86
+ prop: propsValue,
87
+ onChange: onValueChange,
88
+ defaultProp: defaultValue,
89
+ });
85
90
 
86
- return (
87
- <TabsContextProvider
88
- orientation={orientation}
89
- activePart={activePart}
90
- setActivePart={setActivePart}
91
- value={value}
92
- attendableId={attendableId}
93
- verticalVariant={verticalVariant}
94
- >
95
- <TabsPrimitive.Root
96
- activationMode={activationMode}
97
- data-active={activePart}
91
+ const handleValueChange = useCallback(
92
+ (nextValue: string) => {
93
+ setActivePart('panel');
94
+ setValue(nextValue);
95
+ },
96
+ [value],
97
+ );
98
+
99
+ const { findFirstFocusable } = useFocusFinders();
100
+
101
+ useLayoutEffect(() => {
102
+ if (tabsRoot.current) {
103
+ findFirstFocusable(tabsRoot.current)?.focus();
104
+ }
105
+ }, [activePart]);
106
+
107
+ return (
108
+ <TabsContextProvider
98
109
  orientation={orientation}
99
- {...props}
110
+ activePart={activePart}
111
+ setActivePart={setActivePart}
100
112
  value={value}
101
- onValueChange={handleValueChange}
102
- className={mx(
103
- 'overflow-hidden',
104
- orientation === 'vertical' &&
105
- verticalVariant === 'stateful' &&
106
- '[&[data-active=list]_[role=tabpanel]]:invisible @md:[&[data-active=list]_[role=tabpanel]]:visible',
107
- classNames,
108
- )}
109
- ref={tabsRoot}
113
+ attendableId={attendableId}
110
114
  >
111
- {children}
112
- </TabsPrimitive.Root>
113
- </TabsContextProvider>
114
- );
115
- };
115
+ <TabsPrimitive.Root
116
+ {...props}
117
+ className={mx('overflow-hidden', classNames)}
118
+ orientation={orientation}
119
+ activationMode={activationMode}
120
+ data-active={activePart}
121
+ value={value}
122
+ onValueChange={handleValueChange}
123
+ ref={tabsRoot}
124
+ >
125
+ {children}
126
+ </TabsPrimitive.Root>
127
+ </TabsContextProvider>
128
+ );
129
+ },
130
+ );
131
+
132
+ TabsRoot.displayName = 'Tabs.Root';
133
+
134
+ //
135
+ // Viewport
136
+ //
116
137
 
117
138
  type TabsViewportProps = ThemedClassName<ComponentPropsWithoutRef<'div'>>;
118
139
 
119
140
  const TabsViewport = ({ classNames, children, ...props }: TabsViewportProps) => {
120
- const { orientation, activePart, verticalVariant } = useTabsContext('TabsViewport');
141
+ const { activePart } = useTabsContext('TabsViewport');
121
142
  return (
122
- <div
123
- role='none'
124
- {...props}
125
- data-active={activePart}
126
- className={mx(
127
- orientation === 'vertical' &&
128
- verticalVariant === 'stateful' && [
129
- 'grid is-[200%] grid-cols-2 data-[active=panel]:mis-[-100%]',
130
- '@md:is-auto @md:data-[active=panel]:mis-0 @md:grid-cols-[minmax(min-content,1fr)_3fr] @md:gap-1',
131
- ],
132
- classNames,
133
- )}
134
- >
143
+ <div {...props} data-active={activePart} className={mx(classNames)}>
135
144
  {children}
136
145
  </div>
137
146
  );
138
147
  };
139
148
 
149
+ TabsViewport.displayName = 'Tabs.Viewport';
150
+
151
+ //
152
+ // Tablist
153
+ //
154
+
140
155
  type TabsTablistProps = ThemedClassName<TabsPrimitive.TabsListProps>;
141
156
 
142
157
  const TabsTablist = ({ children, classNames, ...props }: TabsTablistProps) => {
143
- const { orientation, verticalVariant } = useTabsContext('TabsTablist');
158
+ const { orientation } = useTabsContext('TabsTablist');
144
159
  return (
145
160
  <TabsPrimitive.List
146
161
  {...props}
162
+ data-arrow-keys={orientation === 'vertical' ? 'up down' : 'left right'}
147
163
  className={mx(
148
- 'max-bs-full is-full',
149
- // NOTE: Padding should be common to Toolbar.
150
- orientation === 'vertical' ? 'overflow-y-auto' : 'flex items-stretch justify-start overflow-x-auto p-1 gap-1',
151
- orientation === 'vertical' && verticalVariant === 'stateful' && 'place-self-start p-1',
164
+ 'max-h-full w-full',
165
+ // TODO(burdon): Should be embeddable inside Toolbar (if horizontal).
166
+ orientation === 'vertical' ? 'overflow-y-auto' : 'flex p-1 gap-1 items-stretch justify-start overflow-x-auto',
152
167
  classNames,
153
168
  )}
154
169
  >
@@ -157,6 +172,12 @@ const TabsTablist = ({ children, classNames, ...props }: TabsTablistProps) => {
157
172
  );
158
173
  };
159
174
 
175
+ TabsTablist.displayName = 'Tabs.Tablist';
176
+
177
+ //
178
+ // BackButton
179
+ //
180
+
160
181
  const TabsBackButton = ({ onClick, classNames, ...props }: ButtonProps) => {
161
182
  const { setActivePart } = useTabsContext('TabsBackButton');
162
183
  const handleClick = useCallback(
@@ -164,27 +185,38 @@ const TabsBackButton = ({ onClick, classNames, ...props }: ButtonProps) => {
164
185
  setActivePart('list');
165
186
  return onClick?.(event);
166
187
  },
167
- [onClick, setActivePart],
188
+ [setActivePart, onClick],
168
189
  );
169
190
 
170
- return <Button {...props} classNames={['is-full text-start @md:hidden mbe-2', classNames]} onClick={handleClick} />;
191
+ return <Button {...props} classNames={['@md:hidden text-start', classNames]} onClick={handleClick} />;
171
192
  };
172
193
 
194
+ TabsBackButton.displayName = 'Tabs.BackButton';
195
+
196
+ //
197
+ // TabGroupHeading
198
+ //
199
+
173
200
  type TabsTabGroupHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'h2'>>;
174
201
 
175
- const TabsTabGroupHeading = ({ children, classNames, ...props }: ThemedClassName<TabsTabGroupHeadingProps>) => {
176
- return (
177
- <h2 {...props} className={mx('mlb-1 pli-2 text-sm text-unAccent', classNames)}>
178
- {children}
179
- </h2>
180
- );
181
- };
202
+ const TabsTabGroupHeading = ({ children, classNames, ...props }: TabsTabGroupHeadingProps) => (
203
+ <h2 {...props} className={mx('my-1 px-2 text-sm text-un-accent', classNames)}>
204
+ {children}
205
+ </h2>
206
+ );
207
+
208
+ TabsTabGroupHeading.displayName = 'Tabs.TabGroupHeading';
209
+
210
+ //
211
+ // Tab
212
+ //
182
213
 
183
214
  type TabsTabProps = ButtonProps & Pick<TabsPrimitive.TabsTriggerProps, 'value'>;
184
215
 
185
216
  const TabsTab = ({ value, classNames, children, onClick, ...props }: TabsTabProps) => {
186
217
  const { setActivePart, orientation, value: contextValue, attendableId } = useTabsContext('TabsTab');
187
218
  const { hasAttention } = useAttention(attendableId);
219
+
188
220
  const handleClick = useCallback(
189
221
  // NOTE: This handler is only called if the tab is *already active*.
190
222
  (event: MouseEvent<HTMLButtonElement>) => {
@@ -197,19 +229,16 @@ const TabsTab = ({ value, classNames, children, onClick, ...props }: TabsTabProp
197
229
  return (
198
230
  <TabsPrimitive.Trigger value={value} asChild>
199
231
  <Button
200
- density='fine'
232
+ {...props}
201
233
  variant={
202
234
  orientation === 'horizontal' && contextValue === value ? (hasAttention ? 'primary' : 'default') : 'ghost'
203
235
  }
204
- {...props}
205
- onClick={handleClick}
206
236
  classNames={[
207
- 'pli-2 rounded-sm',
208
- orientation === 'vertical' && 'block justify-start text-start is-full',
209
- orientation === 'vertical' && ghostSelectedContainerMd,
210
- ghostHover,
237
+ orientation === 'vertical' && 'block justify-start text-start w-full',
238
+ orientation === 'vertical' && 'dx-selected',
211
239
  classNames,
212
240
  ]}
241
+ onClick={handleClick}
213
242
  >
214
243
  {children}
215
244
  </Button>
@@ -217,27 +246,83 @@ const TabsTab = ({ value, classNames, children, onClick, ...props }: TabsTabProp
217
246
  );
218
247
  };
219
248
 
220
- type TabsTabpanelProps = ThemedClassName<TabsPrimitive.TabsContentProps>;
249
+ TabsTab.displayName = 'Tabs.Tab';
250
+
251
+ //
252
+ // IconTab
253
+ //
254
+
255
+ type TabsIconTabProps = IconButtonProps & Pick<TabsPrimitive.TabsTriggerProps, 'value'>;
256
+
257
+ const TabsIconTab = ({ value, classNames, onClick, ...props }: TabsIconTabProps) => {
258
+ const { setActivePart, orientation, value: contextValue, attendableId } = useTabsContext('TabsTab');
259
+ const { hasAttention } = useAttention(attendableId);
260
+
261
+ // NOTE: This handler is only called if the tab is *already active*.
262
+ const handleClick = useCallback(
263
+ (event: MouseEvent<HTMLButtonElement>) => {
264
+ setActivePart('panel');
265
+ onClick?.(event);
266
+ },
267
+ [setActivePart, onClick],
268
+ );
221
269
 
222
- const TabsTabpanel = ({ classNames, children, ...props }: TabsTabpanelProps) => {
223
270
  return (
224
- <TabsPrimitive.Content {...props} className={mx('dx-focus-ring-inset-over-all', classNames)}>
225
- {children}
226
- </TabsPrimitive.Content>
271
+ <TabsPrimitive.Trigger value={value} asChild>
272
+ <IconButton
273
+ {...props}
274
+ variant={
275
+ orientation === 'horizontal' && contextValue === value ? (hasAttention ? 'primary' : 'default') : 'ghost'
276
+ }
277
+ classNames={[
278
+ orientation === 'vertical' && 'justify-start text-start w-full',
279
+ orientation === 'vertical' && 'dx-selected',
280
+ classNames,
281
+ ]}
282
+ onClick={handleClick}
283
+ />
284
+ </TabsPrimitive.Trigger>
227
285
  );
228
286
  };
229
287
 
288
+ TabsIconTab.displayName = 'Tabs.IconTab';
289
+
290
+ //
291
+ // Panel
292
+ //
293
+ // Do NOT wrap TabsPanel children in React.Activity.
294
+ // Radix TabsPrimitive.Content already unmounts inactive panels (no forceMount) — inactive tab
295
+ // content is not in the DOM and effects do not run, which is the desired behaviour.
296
+ // React.Activity (experimental in React 19) is a reconciler-level symbol that deactivates its
297
+ // subtree when mode='hidden'. It was redundant here and prevented initial render of active panels.
298
+ //
299
+
300
+ type TabsPanelProps = ThemedClassName<TabsPrimitive.TabsContentProps>;
301
+
302
+ const TabsPanel = ({ classNames, children, ...props }: TabsPanelProps) => (
303
+ <TabsPrimitive.Content {...props} className={mx('p-0! dx-focus-ring-inset-over-all', classNames)}>
304
+ {children}
305
+ </TabsPrimitive.Content>
306
+ );
307
+
308
+ TabsPanel.displayName = 'Tabs.Panel';
309
+
230
310
  type TabsTabPrimitiveProps = TabsPrimitive.TabsTriggerProps;
231
311
 
312
+ //
313
+ // Tabs
314
+ //
315
+
232
316
  export const Tabs = {
233
317
  Root: TabsRoot,
234
318
  Tablist: TabsTablist,
235
319
  Tab: TabsTab,
320
+ IconTab: TabsIconTab,
236
321
  TabPrimitive: TabsPrimitive.Trigger,
237
322
  TabGroupHeading: TabsTabGroupHeading,
238
- Tabpanel: TabsTabpanel,
239
- BackButton: TabsBackButton,
240
323
  Viewport: TabsViewport,
324
+ Panel: TabsPanel,
325
+ BackButton: TabsBackButton,
241
326
  };
242
327
 
243
328
  export type {
@@ -247,6 +332,6 @@ export type {
247
332
  TabsTabProps,
248
333
  TabsTabPrimitiveProps,
249
334
  TabsTabGroupHeadingProps,
250
- TabsTabpanelProps,
251
335
  TabsViewportProps,
336
+ TabsPanelProps,
252
337
  };