@ctrl-ui/react 0.8.10 → 0.8.11

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.
@@ -0,0 +1,140 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { Input } from "./input.js";
4
+ import { createContext, useContext, useRef, useState } from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ import { Toolbar } from "@base-ui/react/toolbar";
7
+ import { Radio } from "@base-ui/react/radio";
8
+ import { RadioGroup } from "@base-ui/react/radio-group";
9
+ //#region src/ui/icon-picker.tsx
10
+ const IconPickerContext = createContext(null);
11
+ function useIconPicker() {
12
+ const picker = useContext(IconPickerContext);
13
+ if (!picker) throw new Error("IconPicker parts must be rendered inside <IconPicker>.");
14
+ return picker;
15
+ }
16
+ function IconPicker({ items, columns = 8, value, defaultValue, onValueChange, className, style, children, ...props }) {
17
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
18
+ const [search, setSearch] = useState("");
19
+ const selectIcon = (selectedValue) => {
20
+ if (value === void 0) setUncontrolledValue(selectedValue);
21
+ onValueChange?.(selectedValue);
22
+ };
23
+ const pickerStyle = {
24
+ "--_icon-picker-columns": columns,
25
+ ...style
26
+ };
27
+ return /* @__PURE__ */ jsx(IconPickerContext.Provider, {
28
+ value: {
29
+ items,
30
+ columns,
31
+ selectedValue: value ?? uncontrolledValue,
32
+ selectIcon,
33
+ search,
34
+ setSearch
35
+ },
36
+ children: /* @__PURE__ */ jsx("div", {
37
+ "data-control-ui": "icon-picker",
38
+ "data-control-family": "icon-picker",
39
+ "data-slot": "root",
40
+ className,
41
+ style: pickerStyle,
42
+ ...props,
43
+ children
44
+ })
45
+ });
46
+ }
47
+ function IconPickerSearch({ placeholder = "Search icons…", "aria-label": ariaLabel = "Search icons", className, onChange, ...props }) {
48
+ const picker = useIconPicker();
49
+ return /* @__PURE__ */ jsx(Input, {
50
+ type: "search",
51
+ size: "sm",
52
+ placeholder,
53
+ "aria-label": ariaLabel,
54
+ autoComplete: "off",
55
+ spellCheck: false,
56
+ className: cn("mx-2 mt-2 w-[calc(100%-var(--spacing)*4)]", className),
57
+ ...props,
58
+ value: picker.search,
59
+ onChange: (event) => {
60
+ onChange?.(event);
61
+ if (!event.defaultPrevented) picker.setSearch(event.target.value);
62
+ }
63
+ });
64
+ }
65
+ function IconPickerContent({ className, empty = "No icons found.", "aria-label": ariaLabel = "Icons", ...props }) {
66
+ const picker = useIconPicker();
67
+ const buttonsRef = useRef(/* @__PURE__ */ new Map());
68
+ const normalizedSearch = picker.search.trim().toLocaleLowerCase();
69
+ const filteredItems = picker.items.filter((item) => [item.label, ...item.keywords ?? []].some((text) => text.toLocaleLowerCase().includes(normalizedSearch)));
70
+ const moveGridFocus = (event) => {
71
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
72
+ const currentIndex = filteredItems.findIndex((item) => buttonsRef.current.get(item.value) === event.target);
73
+ if (currentIndex < 0) return;
74
+ event.preventDefault();
75
+ const direction = event.key === "ArrowDown" ? 1 : -1;
76
+ const nextIndex = Math.max(0, Math.min(filteredItems.length - 1, currentIndex + direction * picker.columns));
77
+ buttonsRef.current.get(filteredItems[nextIndex].value)?.focus();
78
+ };
79
+ return /* @__PURE__ */ jsx("div", {
80
+ "data-control-ui": "icon-picker",
81
+ "data-control-family": "icon-picker",
82
+ "data-slot": "content",
83
+ className,
84
+ ...props,
85
+ children: filteredItems.length === 0 ? /* @__PURE__ */ jsx("div", {
86
+ "data-control-ui": "icon-picker",
87
+ "data-control-family": "icon-picker",
88
+ "data-slot": "empty",
89
+ role: "status",
90
+ children: empty
91
+ }) : /* @__PURE__ */ jsx(Toolbar.Root, {
92
+ "data-control-ui": "icon-picker",
93
+ "data-control-family": "icon-picker",
94
+ "data-slot": "grid",
95
+ "aria-label": ariaLabel,
96
+ loopFocus: false,
97
+ onKeyDownCapture: moveGridFocus,
98
+ children: filteredItems.map((item) => /* @__PURE__ */ jsx(Toolbar.Button, {
99
+ type: "button",
100
+ "aria-label": item.label,
101
+ "aria-pressed": picker.selectedValue === item.value,
102
+ "data-control-ui": "icon-picker",
103
+ "data-control-family": "icon-picker",
104
+ "data-slot": "icon",
105
+ ref: (button) => {
106
+ if (!button) return;
107
+ buttonsRef.current.set(item.value, button);
108
+ return () => {
109
+ buttonsRef.current.delete(item.value);
110
+ };
111
+ },
112
+ onClick: () => picker.selectIcon(item.value),
113
+ children: /* @__PURE__ */ jsx("span", {
114
+ "aria-hidden": "true",
115
+ children: item.icon
116
+ })
117
+ }, item.value))
118
+ })
119
+ });
120
+ }
121
+ function IconPickerColors({ colors, "aria-label": ariaLabel = "Icon color", ...props }) {
122
+ return /* @__PURE__ */ jsx(RadioGroup, {
123
+ "data-control-ui": "icon-picker",
124
+ "data-control-family": "icon-picker",
125
+ "data-slot": "colors",
126
+ "aria-label": ariaLabel,
127
+ ...props,
128
+ children: colors.map((color) => /* @__PURE__ */ jsx(Radio.Root, {
129
+ value: color.value,
130
+ "aria-label": color.label,
131
+ "data-control-ui": "icon-picker",
132
+ "data-control-family": "icon-picker",
133
+ "data-slot": "color",
134
+ style: { color: color.color },
135
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true" })
136
+ }, color.value))
137
+ });
138
+ }
139
+ //#endregion
140
+ export { IconPicker, IconPickerColors, IconPickerContent, IconPickerSearch };
@@ -35,17 +35,13 @@ function MarkdownCode({ className, children, node: _node, ...props }) {
35
35
  if (lang === "diff" && value.includes("@@")) return /* @__PURE__ */ jsx(CodeDiff, {
36
36
  patch: value,
37
37
  lang: "diff",
38
- diffStyle: "unified",
39
- className: "my-4"
40
- });
41
- return /* @__PURE__ */ jsxs(Code, {
42
- className: "my-4",
43
- children: [/* @__PURE__ */ jsxs(CodeHeader, { children: [lang ? /* @__PURE__ */ jsx(CodeTitle, { children: lang }) : null, /* @__PURE__ */ jsx(CodeActions, { children: /* @__PURE__ */ jsx(CodeCopy, { value }) })] }), /* @__PURE__ */ jsx(CodeContent, {
44
- code: value,
45
- lang,
46
- highlight: lang ? "auto" : "none"
47
- })]
38
+ diffStyle: "unified"
48
39
  });
40
+ return /* @__PURE__ */ jsxs(Code, { children: [/* @__PURE__ */ jsxs(CodeHeader, { children: [lang ? /* @__PURE__ */ jsx(CodeTitle, { children: lang }) : null, /* @__PURE__ */ jsx(CodeActions, { children: /* @__PURE__ */ jsx(CodeCopy, { value }) })] }), /* @__PURE__ */ jsx(CodeContent, {
41
+ code: value,
42
+ lang,
43
+ highlight: lang ? "auto" : "none"
44
+ })] });
49
45
  }
50
46
  function MarkdownH1({ className, node: _node, ...props }) {
51
47
  return /* @__PURE__ */ jsx("h1", {
@@ -1,13 +1,14 @@
1
1
  import { MarkdownKnobStyle } from "../knob-contracts/markdown-knobs.js";
2
2
  import { CSSProperties, ComponentProps } from "react";
3
3
  //#region src/ui/markdown.d.ts
4
- export type MarkdownProps = Omit<ComponentProps<"div">, "children" | "style"> & {
5
- content: string;
4
+ export type MarkdownRootProps = Omit<ComponentProps<"div">, "style"> & {
6
5
  style?: CSSProperties & MarkdownKnobStyle;
7
6
  };
8
- export declare function MarkdownRoot({ className, ...props }: Omit<ComponentProps<"div">, "style"> & {
9
- style?: CSSProperties & MarkdownKnobStyle;
10
- }): import("react").JSX.Element;
7
+ export type MarkdownProps = Omit<MarkdownRootProps, "children"> & {
8
+ content: string;
9
+ };
10
+ export declare function MarkdownRoot({ className, ...props }: MarkdownRootProps): import("react").JSX.Element;
11
+ export declare function MarkdownFlow(props: MarkdownRootProps): import("react").JSX.Element;
11
12
  export declare function Markdown({ content, className, ...props }: MarkdownProps): import("react").JSX.Element;
12
13
  export type MarkdownComponentProps = ComponentProps<typeof Markdown>;
13
14
  //#endregion
@@ -13,19 +13,27 @@ function MarkdownRoot({ className, ...props }) {
13
13
  ...props
14
14
  });
15
15
  }
16
+ function MarkdownFlow(props) {
17
+ return /* @__PURE__ */ jsx("div", {
18
+ "data-control-ui": "markdown",
19
+ "data-control-family": "markdown",
20
+ "data-slot": "flow",
21
+ ...props
22
+ });
23
+ }
16
24
  function Markdown({ content, className, ...props }) {
17
25
  return /* @__PURE__ */ jsx(MarkdownRoot, {
18
26
  className,
19
27
  ...props,
20
- children: /* @__PURE__ */ jsx(Streamdown, {
28
+ children: /* @__PURE__ */ jsx(MarkdownFlow, { children: /* @__PURE__ */ jsx(Streamdown, {
21
29
  mode: "streaming",
22
30
  parseIncompleteMarkdown: true,
23
31
  controls: false,
24
32
  components: markdownComponents,
25
- className: "space-y-4 whitespace-normal [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
33
+ className: "cui-markdown-streamdown [&>[data-control-family]]:[margin-block:revert-layer]",
26
34
  children: content
27
- })
35
+ }) })
28
36
  });
29
37
  }
30
38
  //#endregion
31
- export { Markdown, MarkdownRoot };
39
+ export { Markdown, MarkdownFlow, MarkdownRoot };
@@ -18,6 +18,9 @@ export declare function PopoverTrigger({ className, ...props }: ComponentProps<t
18
18
  }): import("react").JSX.Element;
19
19
  export declare function PopoverAnchor(props: ComponentProps<typeof Popover$1.Trigger>): import("react").JSX.Element;
20
20
  export declare function PopoverContent({ className, children, side, align, sideOffset, collisionPadding, padding, ...props }: PopoverPopupProps): import("react").JSX.Element;
21
+ export declare function PopoverViewport({ children, ref, ...props }: ComponentProps<"div"> & {
22
+ style?: CSSProperties & PopupKnobStyle;
23
+ }): import("react").JSX.Element;
21
24
  export declare function PopoverHeader({ className, ...props }: ComponentProps<"div"> & {
22
25
  style?: CSSProperties & PopupKnobStyle;
23
26
  }): import("react").JSX.Element;
@@ -2,6 +2,7 @@
2
2
  import { controlEffectsAttribute } from "../skin.js";
3
3
  import { cn } from "../lib/cn.js";
4
4
  import { useSkin } from "../skin-provider.js";
5
+ import { useImperativeHandle, useLayoutEffect, useRef } from "react";
5
6
  import { jsx } from "react/jsx-runtime";
6
7
  import { Popover as Popover$1 } from "@base-ui/react/popover";
7
8
  //#region src/ui/popover.tsx
@@ -45,6 +46,40 @@ function PopoverContent({ className, children, side = "bottom", align = "center"
45
46
  })
46
47
  }) });
47
48
  }
49
+ function PopoverViewport({ children, ref, ...props }) {
50
+ const viewportRef = useRef(null);
51
+ const contentRef = useRef(null);
52
+ useImperativeHandle(ref, () => {
53
+ if (!viewportRef.current) throw new Error("PopoverViewport is not mounted.");
54
+ return viewportRef.current;
55
+ }, []);
56
+ useLayoutEffect(() => {
57
+ const viewport = viewportRef.current;
58
+ const content = contentRef.current;
59
+ if (!viewport || !content) return;
60
+ const updateHeight = () => viewport.style.setProperty("--_popover-viewport-height", `${content.offsetHeight}px`);
61
+ updateHeight();
62
+ const observer = new ResizeObserver(updateHeight);
63
+ observer.observe(content);
64
+ return () => observer.disconnect();
65
+ }, []);
66
+ return /* @__PURE__ */ jsx("div", {
67
+ "data-control-ui": "popover",
68
+ "data-control-family": "popup",
69
+ "data-popup-kind": "popover",
70
+ "data-slot": "viewport",
71
+ ...props,
72
+ ref: viewportRef,
73
+ children: /* @__PURE__ */ jsx("div", {
74
+ "data-control-ui": "popover",
75
+ "data-control-family": "popup",
76
+ "data-popup-kind": "popover",
77
+ "data-slot": "viewport-content",
78
+ ref: contentRef,
79
+ children
80
+ })
81
+ });
82
+ }
48
83
  function PopoverHeader({ className, ...props }) {
49
84
  return /* @__PURE__ */ jsx("div", {
50
85
  "data-control-ui": "popover",
@@ -86,4 +121,4 @@ function PopoverClose({ className, ...props }) {
86
121
  });
87
122
  }
88
123
  //#endregion
89
- export { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, popoverContentPaddings };
124
+ export { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, PopoverViewport, popoverContentPaddings };
@@ -5,6 +5,8 @@ import { ScrollArea as ScrollArea$1 } from "@base-ui/react/scroll-area";
5
5
  //#region src/ui/scroll-area.d.ts
6
6
  export declare const scrollAreaScrollbarVisibilities: readonly ["scroll", "hover", "always"];
7
7
  export type ScrollAreaScrollbarVisibility = (typeof scrollAreaScrollbarVisibilities)[number];
8
+ export declare const scrollAreaScrollbarGutters: readonly ["auto", "stable"];
9
+ export type ScrollAreaScrollbarGutter = (typeof scrollAreaScrollbarGutters)[number];
8
10
  export type ScrollAreaLockAxis = "x" | "y" | "both";
9
11
  export type ScrollAreaViewportProps = Omit<ComponentProps<"div">, "children" | "className" | "ref"> & {
10
12
  "data-control-ui"?: string;
@@ -25,6 +27,7 @@ export type ScrollAreaProps = Omit<ComponentProps<"div">, "style"> & {
25
27
  blurProps?: Pick<ProgressiveBlurProps, "style">;
26
28
  lockAxis?: ScrollAreaLockAxis;
27
29
  scrollbarVisibility?: ScrollAreaScrollbarVisibility;
30
+ scrollbarGutter?: ScrollAreaScrollbarGutter;
28
31
  };
29
- export declare function ScrollArea({ className, viewportClassName, contentClassName, viewportProps, viewportRef, maxHeight, mask, blur, blurProps, lockAxis, scrollbarVisibility, children, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }: ScrollAreaProps): import("react").JSX.Element;
32
+ export declare function ScrollArea({ className, viewportClassName, contentClassName, viewportProps, viewportRef, maxHeight, mask, blur, blurProps, lockAxis, scrollbarVisibility, scrollbarGutter, children, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }: ScrollAreaProps): import("react").JSX.Element;
30
33
  //#endregion
@@ -10,14 +10,20 @@ const scrollAreaScrollbarVisibilities = [
10
10
  "hover",
11
11
  "always"
12
12
  ];
13
- function Scrollbar({ orientation, visibility, thumbStyle }) {
13
+ const scrollAreaScrollbarGutters = ["auto", "stable"];
14
+ function Scrollbar({ orientation, visibility, gutter, thumbStyle }) {
14
15
  return /* @__PURE__ */ jsx(ScrollArea$1.Scrollbar, {
15
16
  orientation,
17
+ keepMounted: gutter === "stable",
16
18
  "data-control-ui": "scroll-area",
17
19
  "data-control-family": "scroll-area",
18
20
  "data-slot": "scrollbar",
19
21
  "data-visibility": visibility,
20
22
  className: cn("m-px flex touch-none select-none", orientation === "vertical" ? "justify-center" : "flex-col"),
23
+ style: gutter === "stable" ? {
24
+ position: "relative",
25
+ inset: "auto"
26
+ } : void 0,
21
27
  children: /* @__PURE__ */ jsx(ScrollArea$1.Thumb, {
22
28
  "data-control-ui": "scroll-area",
23
29
  "data-control-family": "scroll-area",
@@ -68,9 +74,11 @@ function EdgeBlurs({ blurProps, lockX, lockY }) {
68
74
  })
69
75
  ] });
70
76
  }
71
- function ScrollArea({ className, viewportClassName, contentClassName, viewportProps, viewportRef, maxHeight, mask = true, blur, blurProps, lockAxis, scrollbarVisibility = "hover", children, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }) {
77
+ function ScrollArea({ className, viewportClassName, contentClassName, viewportProps, viewportRef, maxHeight, mask = true, blur, blurProps, lockAxis, scrollbarVisibility, scrollbarGutter, children, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }) {
72
78
  const skin = useSkin();
73
79
  const resolvedBlur = blur ?? skin.scrollAreaBlur ?? false;
80
+ const resolvedVisibility = scrollbarVisibility ?? skin.scrollAreaScrollbarVisibility ?? "hover";
81
+ const resolvedGutter = scrollbarGutter ?? skin.scrollAreaScrollbarGutter ?? "auto";
74
82
  const lockX = lockAxis === "x" || lockAxis === "both";
75
83
  const lockY = lockAxis === "y" || lockAxis === "both";
76
84
  const { style: viewportPropsStyle, ...resolvedViewportProps } = viewportProps ?? {};
@@ -85,6 +93,7 @@ function ScrollArea({ className, viewportClassName, contentClassName, viewportPr
85
93
  "data-mask": mask || void 0,
86
94
  "data-blur": resolvedBlur || void 0,
87
95
  "data-lock-axis": lockAxis,
96
+ "data-scrollbar-gutter": resolvedGutter,
88
97
  className: cn("relative overflow-hidden", className),
89
98
  style,
90
99
  children: [
@@ -114,22 +123,28 @@ function ScrollArea({ className, viewportClassName, contentClassName, viewportPr
114
123
  }),
115
124
  !lockY && /* @__PURE__ */ jsx(Scrollbar, {
116
125
  orientation: "vertical",
117
- visibility: scrollbarVisibility,
126
+ visibility: resolvedVisibility,
127
+ gutter: resolvedGutter,
118
128
  thumbStyle
119
129
  }),
120
130
  !lockX && /* @__PURE__ */ jsx(Scrollbar, {
121
131
  orientation: "horizontal",
122
- visibility: scrollbarVisibility,
132
+ visibility: resolvedVisibility,
133
+ gutter: resolvedGutter,
123
134
  thumbStyle
124
135
  }),
125
136
  !lockX && !lockY && /* @__PURE__ */ jsx(ScrollArea$1.Corner, {
126
137
  "data-control-ui": "scroll-area",
127
138
  "data-control-family": "scroll-area",
128
139
  "data-slot": "corner",
129
- style: cornerStyle
140
+ style: resolvedGutter === "stable" ? {
141
+ ...cornerStyle,
142
+ position: "relative",
143
+ inset: "auto"
144
+ } : cornerStyle
130
145
  })
131
146
  ]
132
147
  });
133
148
  }
134
149
  //#endregion
135
- export { ScrollArea, scrollAreaScrollbarVisibilities };
150
+ export { ScrollArea, scrollAreaScrollbarGutters, scrollAreaScrollbarVisibilities };
package/dist/ui/tabs.js CHANGED
@@ -1,14 +1,18 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
- import { Children, Fragment, createContext, isValidElement, useContext, useRef, useState } from "react";
3
+ import { Children, Fragment, createContext, isValidElement, useContext, useEffect, useRef, useState } from "react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { Tabs as Tabs$1 } from "@base-ui/react/tabs";
6
6
  //#region src/ui/tabs.tsx
7
+ function refreshTabsPanelHeight(panel) {
8
+ if (panel.node.inert || panel.node.hidden) return;
9
+ panel.height = panel.node.getBoundingClientRect().height;
10
+ }
7
11
  const TabsPanelsContext = createContext(null);
8
- function useRegisterTabsPanel() {
9
- const registerPanel = useContext(TabsPanelsContext);
10
- if (!registerPanel) throw new Error("TabsPanel must be rendered inside <Tabs>.");
11
- return registerPanel;
12
+ function useTabsPanels() {
13
+ const context = useContext(TabsPanelsContext);
14
+ if (!context) throw new Error("TabsPanel must be rendered inside <Tabs>.");
15
+ return context;
12
16
  }
13
17
  function setRef(ref, value) {
14
18
  if (typeof ref === "function") return ref(value);
@@ -16,30 +20,61 @@ function setRef(ref, value) {
16
20
  }
17
21
  function Tabs({ className, onValueChange, children, ...props }) {
18
22
  const rootRef = useRef(null);
19
- const panelsRef = useRef(/* @__PURE__ */ new Map());
20
- const clearPrevHeight = useRef(0);
23
+ const activePanelRef = useRef(null);
24
+ const clearPreviousHeightFrame = useRef(0);
25
+ const [exitingValue, setExitingValue] = useState(null);
26
+ const [holdPreviousHeight] = useState(() => (height) => {
27
+ const root = rootRef.current;
28
+ if (!root) return;
29
+ root.style.setProperty("--aui-slide-prev-height", height);
30
+ cancelAnimationFrame(clearPreviousHeightFrame.current);
31
+ clearPreviousHeightFrame.current = requestAnimationFrame(() => {
32
+ clearPreviousHeightFrame.current = requestAnimationFrame(() => {
33
+ root.style.removeProperty("--aui-slide-prev-height");
34
+ root.removeAttribute("data-slide-starting");
35
+ });
36
+ });
37
+ });
38
+ useEffect(() => () => cancelAnimationFrame(clearPreviousHeightFrame.current), []);
21
39
  const [registerPanel] = useState(() => (value, node) => {
22
- if (!node) return;
23
- panelsRef.current.set(value, node);
40
+ if (!(node && !node.inert && !node.hidden)) return;
41
+ const previousPanel = activePanelRef.current;
42
+ const root = rootRef.current;
43
+ if (root && previousPanel && previousPanel.value !== value) {
44
+ root.setAttribute("data-slide-starting", "");
45
+ holdPreviousHeight(root.style.getPropertyValue("--aui-slide-prev-height") || `${previousPanel.height}px`);
46
+ setExitingValue(previousPanel.value);
47
+ }
48
+ activePanelRef.current = {
49
+ value,
50
+ node,
51
+ height: node.getBoundingClientRect().height
52
+ };
53
+ if (root) {
54
+ root.style.setProperty("--_tabs-panel-top", `${node.offsetTop}px`);
55
+ root.style.setProperty("--_tabs-panel-left", `${node.offsetLeft}px`);
56
+ root.style.setProperty("--_tabs-panel-width", `${node.offsetWidth}px`);
57
+ root.style.setProperty("--_tabs-panel-height", `${node.offsetHeight}px`);
58
+ }
24
59
  return () => {
25
- if (panelsRef.current.get(value) === node) panelsRef.current.delete(value);
60
+ const currentPanel = activePanelRef.current;
61
+ if (currentPanel && currentPanel.node === node) refreshTabsPanelHeight(currentPanel);
26
62
  };
27
63
  });
28
64
  const handleValueChange = (value) => {
29
- const root = rootRef.current;
30
- if (root) for (const panel of panelsRef.current.values()) {
31
- if (panel.inert || panel.hidden) continue;
32
- root.style.setProperty("--aui-slide-prev-height", `${panel.getBoundingClientRect().height}px`);
33
- cancelAnimationFrame(clearPrevHeight.current);
34
- clearPrevHeight.current = requestAnimationFrame(() => {
35
- clearPrevHeight.current = requestAnimationFrame(() => root.style.removeProperty("--aui-slide-prev-height"));
36
- });
37
- break;
65
+ const activePanel = activePanelRef.current;
66
+ if (activePanel) {
67
+ refreshTabsPanelHeight(activePanel);
68
+ holdPreviousHeight(`${activePanel.height}px`);
69
+ setExitingValue(activePanel.value);
38
70
  }
39
71
  onValueChange?.(value);
40
72
  };
41
73
  return /* @__PURE__ */ jsx(TabsPanelsContext.Provider, {
42
- value: registerPanel,
74
+ value: {
75
+ registerPanel,
76
+ exitingValue
77
+ },
43
78
  children: /* @__PURE__ */ jsx(Tabs$1.Root, {
44
79
  "data-control-ui": "tabs",
45
80
  "data-control-family": "tabs",
@@ -101,7 +136,7 @@ function TabsTab({ className, ...props }) {
101
136
  });
102
137
  }
103
138
  function TabsPanel({ className, value, ref, ...props }) {
104
- const registerPanel = useRegisterTabsPanel();
139
+ const { registerPanel, exitingValue } = useTabsPanels();
105
140
  const panelRef = (node) => {
106
141
  const unregister = registerPanel(value, node);
107
142
  const cleanup = setRef(ref, node);
@@ -119,7 +154,8 @@ function TabsPanel({ className, value, ref, ...props }) {
119
154
  "data-control-family": "tabs",
120
155
  "data-slot": "panel",
121
156
  "data-slide": "panel",
122
- className: cn("data-[hidden]:hidden [&[hidden]]:hidden", className),
157
+ "data-slide-exiting": exitingValue === value ? "" : void 0,
158
+ className: cn("data-[hidden]:hidden [&[hidden]]:hidden [&[inert]:not([data-slide=panel][data-slide-exiting][data-ending-style])]:hidden [&[inert][data-ending-style]]:m-0", "not-supports-[anchor-name:--aui-slide-panel]:[&[inert]]:hidden not-supports-[anchor-scope:--aui-slide-panel]:[&[inert]]:hidden", className),
123
159
  ...props
124
160
  });
125
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl-ui/react",
3
- "version": "0.8.10",
3
+ "version": "0.8.11",
4
4
  "description": "Control UI components, blocks, and primitives with neutral defaults and consumer-owned themes.",
5
5
  "license": "MIT",
6
6
  "repository": {