@kushagradhawan/kookie-ui 0.1.49 → 0.1.51

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.
Files changed (103) hide show
  1. package/components.css +880 -243
  2. package/dist/cjs/components/_internal/shell-bottom.d.ts +31 -5
  3. package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -1
  4. package/dist/cjs/components/_internal/shell-bottom.js +1 -1
  5. package/dist/cjs/components/_internal/shell-bottom.js.map +3 -3
  6. package/dist/cjs/components/_internal/shell-handles.d.ts.map +1 -1
  7. package/dist/cjs/components/_internal/shell-handles.js +1 -1
  8. package/dist/cjs/components/_internal/shell-handles.js.map +3 -3
  9. package/dist/cjs/components/_internal/shell-inspector.d.ts +23 -5
  10. package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
  11. package/dist/cjs/components/_internal/shell-inspector.js +1 -1
  12. package/dist/cjs/components/_internal/shell-inspector.js.map +3 -3
  13. package/dist/cjs/components/_internal/shell-sidebar.d.ts +24 -6
  14. package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
  15. package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
  16. package/dist/cjs/components/_internal/shell-sidebar.js.map +3 -3
  17. package/dist/cjs/components/chatbar.d.ts +9 -2
  18. package/dist/cjs/components/chatbar.d.ts.map +1 -1
  19. package/dist/cjs/components/chatbar.js +1 -1
  20. package/dist/cjs/components/chatbar.js.map +3 -3
  21. package/dist/cjs/components/shell.context.d.ts +88 -0
  22. package/dist/cjs/components/shell.context.d.ts.map +1 -1
  23. package/dist/cjs/components/shell.context.js +1 -1
  24. package/dist/cjs/components/shell.context.js.map +3 -3
  25. package/dist/cjs/components/shell.d.ts +51 -13
  26. package/dist/cjs/components/shell.d.ts.map +1 -1
  27. package/dist/cjs/components/shell.hooks.d.ts +7 -1
  28. package/dist/cjs/components/shell.hooks.d.ts.map +1 -1
  29. package/dist/cjs/components/shell.hooks.js +1 -1
  30. package/dist/cjs/components/shell.hooks.js.map +3 -3
  31. package/dist/cjs/components/shell.js +1 -1
  32. package/dist/cjs/components/shell.js.map +3 -3
  33. package/dist/cjs/components/shell.types.d.ts +1 -0
  34. package/dist/cjs/components/shell.types.d.ts.map +1 -1
  35. package/dist/cjs/components/shell.types.js +1 -1
  36. package/dist/cjs/components/shell.types.js.map +2 -2
  37. package/dist/cjs/components/sidebar.d.ts +7 -1
  38. package/dist/cjs/components/sidebar.d.ts.map +1 -1
  39. package/dist/cjs/components/sidebar.js +1 -1
  40. package/dist/cjs/components/sidebar.js.map +3 -3
  41. package/dist/esm/components/_internal/shell-bottom.d.ts +31 -5
  42. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
  43. package/dist/esm/components/_internal/shell-bottom.js +1 -1
  44. package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
  45. package/dist/esm/components/_internal/shell-handles.d.ts.map +1 -1
  46. package/dist/esm/components/_internal/shell-handles.js +1 -1
  47. package/dist/esm/components/_internal/shell-handles.js.map +3 -3
  48. package/dist/esm/components/_internal/shell-inspector.d.ts +23 -5
  49. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
  50. package/dist/esm/components/_internal/shell-inspector.js +1 -1
  51. package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
  52. package/dist/esm/components/_internal/shell-sidebar.d.ts +24 -6
  53. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
  54. package/dist/esm/components/_internal/shell-sidebar.js +1 -1
  55. package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
  56. package/dist/esm/components/chatbar.d.ts +9 -2
  57. package/dist/esm/components/chatbar.d.ts.map +1 -1
  58. package/dist/esm/components/chatbar.js +1 -1
  59. package/dist/esm/components/chatbar.js.map +3 -3
  60. package/dist/esm/components/shell.context.d.ts +88 -0
  61. package/dist/esm/components/shell.context.d.ts.map +1 -1
  62. package/dist/esm/components/shell.context.js +1 -1
  63. package/dist/esm/components/shell.context.js.map +3 -3
  64. package/dist/esm/components/shell.d.ts +51 -13
  65. package/dist/esm/components/shell.d.ts.map +1 -1
  66. package/dist/esm/components/shell.hooks.d.ts +7 -1
  67. package/dist/esm/components/shell.hooks.d.ts.map +1 -1
  68. package/dist/esm/components/shell.hooks.js +1 -1
  69. package/dist/esm/components/shell.hooks.js.map +3 -3
  70. package/dist/esm/components/shell.js +1 -1
  71. package/dist/esm/components/shell.js.map +3 -3
  72. package/dist/esm/components/shell.types.d.ts +1 -0
  73. package/dist/esm/components/shell.types.d.ts.map +1 -1
  74. package/dist/esm/components/shell.types.js.map +2 -2
  75. package/dist/esm/components/sidebar.d.ts +7 -1
  76. package/dist/esm/components/sidebar.d.ts.map +1 -1
  77. package/dist/esm/components/sidebar.js +1 -1
  78. package/dist/esm/components/sidebar.js.map +3 -3
  79. package/package.json +14 -3
  80. package/schemas/base-button.json +1 -1
  81. package/schemas/button.json +1 -1
  82. package/schemas/icon-button.json +1 -1
  83. package/schemas/index.json +6 -6
  84. package/schemas/toggle-button.json +1 -1
  85. package/schemas/toggle-icon-button.json +1 -1
  86. package/src/components/_internal/base-menu.css +17 -18
  87. package/src/components/_internal/base-sidebar-menu.css +23 -21
  88. package/src/components/_internal/base-sidebar.css +20 -0
  89. package/src/components/_internal/shell-bottom.tsx +176 -49
  90. package/src/components/_internal/shell-handles.tsx +29 -4
  91. package/src/components/_internal/shell-inspector.tsx +175 -43
  92. package/src/components/_internal/shell-sidebar.tsx +176 -69
  93. package/src/components/chatbar.css +240 -21
  94. package/src/components/chatbar.tsx +246 -290
  95. package/src/components/sheet.css +8 -16
  96. package/src/components/shell.context.tsx +79 -0
  97. package/src/components/shell.css +28 -2
  98. package/src/components/shell.hooks.ts +35 -0
  99. package/src/components/shell.tsx +574 -214
  100. package/src/components/shell.types.ts +2 -0
  101. package/src/components/sidebar.css +233 -33
  102. package/src/components/sidebar.tsx +247 -213
  103. package/styles.css +841 -204
@@ -10,14 +10,13 @@
10
10
  flex: 1;
11
11
  display: flex;
12
12
  flex-direction: column;
13
- padding-bottom: var(--base-menu-content-padding);
14
13
  box-sizing: border-box;
15
14
  list-style: none;
16
15
  margin: 0;
17
16
  min-height: 0;
18
17
 
19
18
  :where(.rt-SidebarContent:has(.rt-ScrollAreaScrollbar[data-orientation='vertical'])) & {
20
- padding-right: var(--space-3);
19
+ padding-inline-end: var(--space-3);
21
20
  }
22
21
  }
23
22
 
@@ -34,8 +33,8 @@
34
33
  min-height: var(--base-menu-item-height);
35
34
  padding-top: var(--base-menu-item-padding-y);
36
35
  padding-bottom: var(--base-menu-item-padding-y);
37
- padding-left: var(--base-menu-item-padding-left);
38
- padding-right: var(--base-menu-item-padding-right);
36
+ padding-inline-start: var(--base-menu-item-padding-left);
37
+ padding-inline-end: var(--base-menu-item-padding-right);
39
38
  box-sizing: border-box;
40
39
  position: relative;
41
40
  outline: none;
@@ -43,11 +42,14 @@
43
42
  background: none;
44
43
  border: none;
45
44
  width: 100%;
46
- text-align: left;
45
+ text-align: start;
47
46
  /* No default border radius - inherited from size-specific rules */
48
47
 
49
- /* Transitions - inherit from base menu */
50
- transition: var(--transition-menu);
48
+ /* Step 3: restrict to paint-only transitions to avoid Chrome compositing nudge */
49
+ transition:
50
+ background-color var(--motion-duration-micro) var(--motion-ease-standard),
51
+ color var(--motion-duration-small) var(--motion-ease-standard),
52
+ font-weight var(--motion-duration-small) var(--motion-ease-standard);
51
53
 
52
54
  /* Fix sticky text highlighting after selection in Firefox */
53
55
  user-select: none;
@@ -147,14 +149,14 @@
147
149
  }
148
150
 
149
151
  .rt-SidebarMenuSubList {
150
- padding-left: var(--space-4);
151
- border-left: 1px solid var(--gray-a5);
152
- margin-left: var(--space-3);
152
+ padding-inline-start: var(--space-4);
153
+ border-inline-start: 1px solid var(--gray-a5);
154
+ margin-inline-start: var(--space-3);
153
155
  }
154
156
 
155
157
  /* Sub-menu items have consistent heights based on size - match dropdown menu exactly */
156
158
  :where(.rt-SidebarContent.rt-r-size-1) :where(.rt-SidebarMenuSubList) .rt-SidebarMenuButton {
157
- padding-left: var(--space-3);
159
+ padding-inline-start: var(--space-3);
158
160
  padding-top: calc(var(--space-1) * 0.75);
159
161
  padding-bottom: calc(var(--space-1) * 0.75);
160
162
  min-height: var(--space-5); /* 20px */
@@ -162,10 +164,10 @@
162
164
  }
163
165
 
164
166
  :where(.rt-SidebarContent.rt-r-size-2) :where(.rt-SidebarMenuSubList) .rt-SidebarMenuButton {
165
- padding-left: var(--space-3);
167
+ padding-inline-start: var(--space-3);
166
168
  padding-top: var(--space-1);
167
169
  padding-bottom: var(--space-1);
168
- min-height: var(--space-6); /* 24px */
170
+ min-height: var(--space-6); /* 32px */
169
171
  font-size: var(--font-size-2);
170
172
  }
171
173
 
@@ -176,8 +178,8 @@
176
178
  min-height: var(--base-menu-item-height);
177
179
  padding-top: var(--base-menu-item-padding-y);
178
180
  padding-bottom: var(--base-menu-item-padding-y);
179
- padding-left: var(--base-menu-item-padding-left);
180
- padding-right: var(--base-menu-item-padding-right);
181
+ padding-inline-start: var(--base-menu-item-padding-left);
182
+ padding-inline-end: var(--base-menu-item-padding-right);
181
183
  box-sizing: border-box;
182
184
  color: var(--gray-a10);
183
185
  user-select: none;
@@ -193,27 +195,27 @@
193
195
  .rt-SidebarMenuButton:where(:has(.rt-SidebarMenuShortcut, .rt-SidebarMenuBadge)) {
194
196
  /* Use base menu padding tokens */
195
197
  :where(.rt-SidebarContent.rt-r-size-1) & {
196
- padding-right: var(--base-menu-item-padding-y); /* Matches top/bottom padding */
198
+ padding-inline-end: var(--base-menu-item-padding-y); /* Matches top/bottom padding */
197
199
  }
198
200
 
199
201
  :where(.rt-SidebarContent.rt-r-size-2) & {
200
- padding-right: var(--base-menu-item-padding-y); /* Matches top/bottom padding */
202
+ padding-inline-end: var(--base-menu-item-padding-y); /* Matches top/bottom padding */
201
203
  }
202
204
  }
203
205
 
204
206
  .rt-SidebarMenuShortcut {
205
207
  display: flex;
206
208
  align-items: center;
207
- margin-left: auto;
208
- padding-left: var(--space-4);
209
+ margin-inline-start: auto;
210
+ padding-inline-start: var(--space-4);
209
211
  color: var(--gray-a11);
210
212
  }
211
213
 
212
214
  .rt-SidebarMenuBadge {
213
215
  display: flex;
214
216
  align-items: center;
215
- margin-left: auto;
216
- padding-left: var(--space-2);
217
+ margin-inline-start: auto;
218
+ padding-inline-start: var(--space-2);
217
219
  }
218
220
 
219
221
  /* Add balanced spacing for sidebar menu items while preserving base menu border radius */
@@ -78,6 +78,17 @@
78
78
  box-shadow: none !important;
79
79
  border-radius: 0 !important;
80
80
 
81
+ /* Step 2: stabilize text rasterization to avoid Chrome AA flicker */
82
+ -webkit-font-smoothing: antialiased;
83
+ text-rendering: optimizeLegibility;
84
+
85
+ /* Add gap between groups using flex */
86
+ /* stylelint-disable-next-line selector-max-specificity */
87
+ & .rt-BaseMenuViewport {
88
+ /* gap: var(--base-menu-content-padding); */
89
+ gap: var(--space-5);
90
+ }
91
+
81
92
  /* Ensure ScrollArea takes full height within SidebarContent */
82
93
  & :where(.rt-ScrollAreaRoot) {
83
94
  flex: 1;
@@ -92,6 +103,15 @@
92
103
  flex-direction: column;
93
104
  min-height: 0;
94
105
  }
106
+
107
+ /* Step 1: promote menu viewport to its own layer (Chrome jitter test) */
108
+ & :where(.rt-BaseMenuViewport) {
109
+ transform: translateZ(0);
110
+ backface-visibility: hidden;
111
+ will-change: transform;
112
+ /* Step 4: isolate painting to prevent cross-layer nudges */
113
+ contain: paint;
114
+ }
95
115
  }
96
116
 
97
117
  /* Sidebar Footer */
@@ -3,7 +3,7 @@ import classNames from 'classnames';
3
3
  import * as Sheet from '../sheet.js';
4
4
  import { VisuallyHidden } from '../visually-hidden.js';
5
5
  import { useShell } from '../shell.context.js';
6
- import { useResponsivePresentation } from '../shell.hooks.js';
6
+ import { useResponsivePresentation, useResponsiveValue } from '../shell.hooks.js';
7
7
  import { PaneResizeContext } from './shell-resize.js';
8
8
  import { BottomHandle, PaneHandle } from './shell-handles.js';
9
9
  import { BREAKPOINTS } from '../shell.types.js';
@@ -11,9 +11,7 @@ import type { Breakpoint, PaneMode, PaneSizePersistence, ResponsivePresentation
11
11
 
12
12
  interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
13
13
  presentation?: ResponsivePresentation;
14
- mode?: PaneMode;
15
- defaultMode?: any;
16
- onModeChange?: (mode: PaneMode) => void;
14
+ // legacy mode removed
17
15
  expandedSize?: number;
18
16
  minSize?: number;
19
17
  maxSize?: number;
@@ -32,16 +30,32 @@ interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
32
30
  persistence?: PaneSizePersistence;
33
31
  }
34
32
 
35
- type BottomComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: typeof BottomHandle };
33
+ type BottomOpenChangeMeta = { reason: 'init' | 'toggle' | 'responsive' };
34
+ type BottomControlledProps = { open: boolean | Partial<Record<Breakpoint, boolean>>; onOpenChange?: (open: boolean, meta: BottomOpenChangeMeta) => void; defaultOpen?: never };
35
+ type BottomUncontrolledProps = { defaultOpen?: boolean; onOpenChange?: (open: boolean, meta: BottomOpenChangeMeta) => void; open?: never };
36
+ type BottomSizeControlledProps = { size: number | string; defaultSize?: never };
37
+ type BottomSizeUncontrolledProps = { defaultSize?: number | string; size?: never };
38
+ type BottomSizeChangeMeta = { reason: 'init' | 'resize' | 'controlled' };
39
+ type BottomPublicProps = PaneProps &
40
+ (BottomControlledProps | BottomUncontrolledProps) &
41
+ (BottomSizeControlledProps | BottomSizeUncontrolledProps) & {
42
+ onSizeChange?: (size: number, meta: BottomSizeChangeMeta) => void;
43
+ sizeUpdate?: 'throttle' | 'debounce';
44
+ sizeUpdateMs?: number;
45
+ };
36
46
 
37
- export const Bottom = React.forwardRef<HTMLDivElement, PaneProps>(
47
+ type BottomComponent = React.ForwardRefExoticComponent<BottomPublicProps & React.RefAttributes<HTMLDivElement>> & { Handle: typeof BottomHandle };
48
+
49
+ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>(
38
50
  (
39
51
  {
40
52
  className,
41
53
  presentation = 'fixed',
42
- mode,
43
- defaultMode = 'collapsed',
44
- onModeChange,
54
+ // removed legacy props
55
+ // new API
56
+ defaultOpen,
57
+ open,
58
+ onOpenChange,
45
59
  expandedSize = 200,
46
60
  minSize = 100,
47
61
  maxSize = 400,
@@ -80,58 +94,107 @@ export const Bottom = React.forwardRef<HTMLDivElement, PaneProps>(
80
94
  const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === BottomHandle);
81
95
  const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === BottomHandle));
82
96
 
83
- const resolveResponsiveMode = React.useCallback((): PaneMode => {
84
- if (typeof defaultMode === 'string') return defaultMode as PaneMode;
85
- const dm = defaultMode as Partial<Record<Breakpoint, PaneMode>> | undefined;
86
- if (dm && dm[shell.currentBreakpoint as Breakpoint]) {
87
- return dm[shell.currentBreakpoint as Breakpoint] as PaneMode;
97
+ // Throttled/debounced emitter for onSizeChange
98
+ const emitSizeChange = React.useMemo(() => {
99
+ const cb = (props as any).onSizeChange as undefined | ((s: number, meta: BottomSizeChangeMeta) => void);
100
+ const strategy = (props as any).sizeUpdate as undefined | 'throttle' | 'debounce';
101
+ const ms = (props as any).sizeUpdateMs ?? 50;
102
+ if (!cb) return () => {};
103
+ if (strategy === 'debounce') {
104
+ let t: any = null;
105
+ return (s: number, meta: BottomSizeChangeMeta) => {
106
+ if (t) clearTimeout(t);
107
+ t = setTimeout(() => {
108
+ cb(s, meta);
109
+ }, ms);
110
+ };
88
111
  }
89
- const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
90
- const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat('initial' as Breakpoint);
91
- const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
92
- for (let i = startIdx + 1; i < order.length; i++) {
93
- const bp = order[i];
94
- if (dm && dm[bp]) {
95
- return dm[bp] as PaneMode;
96
- }
112
+ if (strategy === 'throttle') {
113
+ let last = 0;
114
+ return (s: number, meta: BottomSizeChangeMeta) => {
115
+ const now = Date.now();
116
+ if (now - last >= ms) {
117
+ last = now;
118
+ cb(s, meta);
119
+ }
120
+ };
97
121
  }
98
- return 'collapsed';
99
- }, [defaultMode, shell.currentBreakpoint]);
122
+ return (s: number, meta: BottomSizeChangeMeta) => cb(s, meta);
123
+ }, [(props as any).onSizeChange, (props as any).sizeUpdate, (props as any).sizeUpdateMs]);
100
124
 
101
125
  const didInitRef = React.useRef(false);
126
+ const didInitFromDefaultOpenRef = React.useRef(false);
127
+ const resolvedDefaultOpen = useResponsiveValue(defaultOpen as any);
102
128
  React.useEffect(() => {
103
129
  if (didInitRef.current) return;
130
+ if (!shell.currentBreakpointReady) return;
104
131
  didInitRef.current = true;
105
- if (mode === undefined) {
106
- const initial = resolveResponsiveMode();
107
- if (shell.bottomMode !== initial) shell.setBottomMode(initial);
132
+ if (typeof open === 'undefined' && typeof defaultOpen !== 'undefined') {
133
+ const initial = Boolean(resolvedDefaultOpen);
134
+ shell.setBottomMode(initial ? 'expanded' : 'collapsed');
135
+ didInitFromDefaultOpenRef.current = true;
108
136
  }
109
- }, []);
137
+ }, [shell.currentBreakpointReady, open, defaultOpen, resolvedDefaultOpen]);
110
138
 
111
- const lastBottomBpRef = React.useRef<Breakpoint | null>(null);
112
- const lastResolvedBottomRef = React.useRef<PaneMode | null>(null);
113
- React.useEffect(() => {
114
- if (mode !== undefined) return;
115
- if (!shell.currentBreakpointReady) return;
116
- if (lastBottomBpRef.current === shell.currentBreakpoint) return;
117
- lastBottomBpRef.current = shell.currentBreakpoint as Breakpoint;
118
- const next = resolveResponsiveMode();
119
- if (lastResolvedBottomRef.current === next) return;
120
- lastResolvedBottomRef.current = next;
121
- if (next !== shell.bottomMode) shell.setBottomMode(next);
122
- }, [mode, shell.currentBreakpoint, shell.currentBreakpointReady, resolveResponsiveMode, shell.bottomMode, shell.setBottomMode]);
139
+ // Dev guards
140
+ const wasControlledRef = React.useRef<boolean | null>(null);
141
+ if (process.env.NODE_ENV !== 'production') {
142
+ if (typeof open !== 'undefined' && typeof defaultOpen !== 'undefined') {
143
+ // eslint-disable-next-line no-console
144
+ console.error('Shell.Bottom: Do not pass both `open` and `defaultOpen`. Choose one.');
145
+ }
146
+ if (typeof (props as any).size !== 'undefined' && typeof (props as any).defaultSize !== 'undefined') {
147
+ // eslint-disable-next-line no-console
148
+ console.error('Shell.Bottom: Do not pass both `size` and `defaultSize`. Choose one.');
149
+ }
150
+ }
123
151
 
124
152
  React.useEffect(() => {
125
- if (mode !== undefined && shell.bottomMode !== mode) {
126
- shell.setBottomMode(mode);
153
+ const isControlled = typeof open !== 'undefined';
154
+ if (wasControlledRef.current === null) {
155
+ wasControlledRef.current = isControlled;
156
+ return;
157
+ }
158
+ if (wasControlledRef.current !== isControlled) {
159
+ // eslint-disable-next-line no-console
160
+ console.warn('Shell.Bottom: Switching between controlled and uncontrolled `open` is not supported.');
161
+ wasControlledRef.current = isControlled;
127
162
  }
128
- }, [mode, shell]);
163
+ }, [open]);
164
+
165
+ // Controlled sync (responsive handled below)
166
+ React.useEffect(() => {
167
+ if (typeof open === 'undefined') return;
168
+ shell.setBottomMode(open ? 'expanded' : 'collapsed');
169
+ }, [open]);
170
+
171
+ const responsiveNotifiedRef = React.useRef(false);
172
+
173
+ // Controlled responsive open
174
+ const resolvedOpen = useResponsiveValue(open);
175
+ React.useEffect(() => {
176
+ if (typeof resolvedOpen === 'undefined') return;
177
+ const shouldExpand = Boolean(resolvedOpen);
178
+ shell.setBottomMode(shouldExpand ? 'expanded' : 'collapsed');
179
+ }, [resolvedOpen]);
129
180
 
181
+ const initNotifiedRef = React.useRef(false);
182
+ const lastBottomModeRef = React.useRef<PaneMode | null>(null);
130
183
  React.useEffect(() => {
131
- if (mode === undefined) {
132
- onModeChange?.(shell.bottomMode);
184
+ if (!initNotifiedRef.current && typeof open === 'undefined' && defaultOpen && shell.bottomMode === 'expanded') {
185
+ onOpenChange?.(true, { reason: 'init' });
186
+ initNotifiedRef.current = true;
133
187
  }
134
- }, [shell.bottomMode, mode, onModeChange]);
188
+ if (typeof open === 'undefined') {
189
+ if (lastBottomModeRef.current !== null && lastBottomModeRef.current !== shell.bottomMode) {
190
+ if (!responsiveNotifiedRef.current) {
191
+ onOpenChange?.(shell.bottomMode === 'expanded', { reason: 'toggle' });
192
+ }
193
+ responsiveNotifiedRef.current = false;
194
+ }
195
+ lastBottomModeRef.current = shell.bottomMode;
196
+ }
197
+ }, [shell.bottomMode, open, defaultOpen, onOpenChange]);
135
198
 
136
199
  React.useEffect(() => {
137
200
  if (shell.bottomMode === 'expanded') {
@@ -194,6 +257,7 @@ export const Bottom = React.forwardRef<HTMLDivElement, PaneProps>(
194
257
  onResizeStart,
195
258
  onResizeEnd: (size) => {
196
259
  onResizeEnd?.(size);
260
+ emitSizeChange(size, { reason: 'resize' });
197
261
  persistenceAdapter?.save?.(size);
198
262
  },
199
263
  target: 'bottom',
@@ -209,6 +273,69 @@ export const Bottom = React.forwardRef<HTMLDivElement, PaneProps>(
209
273
  </PaneResizeContext.Provider>
210
274
  ) : null;
211
275
 
276
+ // Strip control/size props from DOM spread (moved above overlay return to keep hook order consistent)
277
+ const {
278
+ defaultOpen: _bottomDefaultOpenIgnored,
279
+ open: _bottomOpenIgnored,
280
+ onOpenChange: _bottomOnOpenChangeIgnored,
281
+ size: _bottomSizeIgnored,
282
+ defaultSize: _bottomDefaultSizeIgnored,
283
+ onSizeChange: _bottomOnSizeChangeIgnored,
284
+ sizeUpdate: _szu,
285
+ sizeUpdateMs: _szums,
286
+ ...bottomDomProps
287
+ } = props as any;
288
+
289
+ // Normalize CSS lengths to px (moved above overlay return to keep hook order consistent)
290
+ const normalizeToPx = React.useCallback((value: number | string | undefined): number | undefined => {
291
+ if (value == null) return undefined;
292
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
293
+ const str = String(value).trim();
294
+ if (!str) return undefined;
295
+ if (str.endsWith('px')) return Number.parseFloat(str);
296
+ if (str.endsWith('rem')) {
297
+ const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
298
+ return Number.parseFloat(str) * rem;
299
+ }
300
+ if (str.endsWith('%')) {
301
+ const pct = Number.parseFloat(str);
302
+ const base = document.documentElement.clientHeight || window.innerHeight || 0;
303
+ return (pct / 100) * base;
304
+ }
305
+ const n = Number.parseFloat(str);
306
+ return Number.isFinite(n) ? n : undefined;
307
+ }, []);
308
+
309
+ // Apply defaultSize on mount when uncontrolled (moved above overlay return)
310
+ React.useEffect(() => {
311
+ if (!localRef.current) return;
312
+ if (typeof (props as any).size === 'undefined' && typeof (props as any).defaultSize !== 'undefined') {
313
+ const px = normalizeToPx((props as any).defaultSize);
314
+ if (typeof px === 'number' && Number.isFinite(px)) {
315
+ const minPx = typeof minSize === 'number' ? minSize : undefined;
316
+ const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
317
+ const clamped = Math.min(maxPx ?? px, Math.max(minPx ?? px, px));
318
+ localRef.current.style.setProperty('--bottom-size', `${clamped}px`);
319
+ emitSizeChange(clamped, { reason: 'init' });
320
+ }
321
+ }
322
+ // eslint-disable-next-line react-hooks/exhaustive-deps
323
+ }, []);
324
+
325
+ // Controlled size sync (moved above overlay return)
326
+ React.useEffect(() => {
327
+ if (!localRef.current) return;
328
+ if (typeof (props as any).size === 'undefined') return;
329
+ const px = normalizeToPx((props as any).size);
330
+ if (typeof px === 'number' && Number.isFinite(px)) {
331
+ const minPx = typeof minSize === 'number' ? minSize : undefined;
332
+ const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
333
+ const clamped = Math.min(maxPx ?? px, Math.max(minPx ?? px, px));
334
+ localRef.current.style.setProperty('--bottom-size', `${clamped}px`);
335
+ emitSizeChange(clamped, { reason: 'controlled' });
336
+ }
337
+ }, [(props as any).size, minSize, maxSize, normalizeToPx, emitSizeChange]);
338
+
212
339
  if (isOverlay) {
213
340
  const open = shell.bottomMode === 'expanded';
214
341
  return (
@@ -225,13 +352,13 @@ export const Bottom = React.forwardRef<HTMLDivElement, PaneProps>(
225
352
 
226
353
  return (
227
354
  <div
228
- {...props}
355
+ {...bottomDomProps}
229
356
  ref={setRef}
230
357
  className={classNames('rt-ShellBottom', className)}
231
358
  data-mode={shell.bottomMode}
232
359
  data-peek={shell.peekTarget === 'bottom' || undefined}
233
- data-presentation={resolvedPresentation}
234
- data-open={(isStacked && isExpanded) || undefined}
360
+ data-presentation={shell.currentBreakpointReady ? resolvedPresentation : undefined}
361
+ data-open={(shell.currentBreakpointReady && isStacked && isExpanded) || undefined}
235
362
  style={{
236
363
  ...style,
237
364
  ['--bottom-size' as any]: `${expandedSize}px`,
@@ -84,8 +84,16 @@ export const PaneHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsW
84
84
  handleEl.releasePointerCapture(pointerId);
85
85
  } catch {}
86
86
  window.removeEventListener('pointermove', handleMove as any);
87
+ document.removeEventListener('pointermove', handleMove as any);
88
+ window.removeEventListener('mousemove', handleMove as any);
89
+ document.removeEventListener('mousemove', handleMove as any);
90
+ handleEl.removeEventListener('pointermove', handleMove as any);
87
91
  window.removeEventListener('pointerup', handleUp as any);
92
+ document.removeEventListener('pointerup', handleUp as any);
93
+ window.removeEventListener('mouseup', handleUp as any);
94
+ document.removeEventListener('mouseup', handleUp as any);
88
95
  window.removeEventListener('pointercancel', handleUp as any);
96
+ document.removeEventListener('pointercancel', handleUp as any);
89
97
  window.removeEventListener('keydown', handleKey as any);
90
98
  handleEl.removeEventListener('lostpointercapture', handleUp as any);
91
99
  container.removeAttribute('data-resizing');
@@ -120,8 +128,17 @@ export const PaneHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsW
120
128
  }
121
129
  };
122
130
  window.addEventListener('pointermove', handleMove as any);
131
+ document.addEventListener('pointermove', handleMove as any);
132
+ // Fallbacks for environments that don't fully support PointerEvent on window
133
+ window.addEventListener('mousemove', handleMove as any);
134
+ document.addEventListener('mousemove', handleMove as any);
135
+ handleEl.addEventListener('pointermove', handleMove as any);
123
136
  window.addEventListener('pointerup', handleUp as any);
137
+ document.addEventListener('pointerup', handleUp as any);
138
+ window.addEventListener('mouseup', handleUp as any);
139
+ document.addEventListener('mouseup', handleUp as any);
124
140
  window.addEventListener('pointercancel', handleUp as any);
141
+ document.addEventListener('pointercancel', handleUp as any);
125
142
  window.addEventListener('keydown', handleKey as any);
126
143
  handleEl.addEventListener('lostpointercapture', handleUp as any);
127
144
  activeCleanupRef.current = cleanup;
@@ -132,13 +149,20 @@ export const PaneHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsW
132
149
  onKeyDown={(e) => {
133
150
  if (!containerRef.current) return;
134
151
  const container = containerRef.current;
135
- const current = parseFloat(getComputedStyle(container).getPropertyValue(cssVarName) || `${defaultSize}`);
152
+ const rawCurrent = getComputedStyle(container).getPropertyValue(cssVarName);
153
+ const parsedCurrent = Number.parseFloat(rawCurrent.trim());
154
+ const current = Number.isFinite(parsedCurrent) ? parsedCurrent : defaultSize;
136
155
  const clamp = (v: number) => Math.min(Math.max(v, minSize), maxSize);
137
156
  const step = e.shiftKey ? 32 : 8;
138
157
  let delta = 0;
139
158
  if (orientation === 'vertical') {
140
- if (e.key === 'ArrowRight') delta = step;
141
- else if (e.key === 'ArrowLeft') delta = -step;
159
+ const docDir = typeof document !== 'undefined' ? document.dir : undefined;
160
+ const cssDir = getComputedStyle(container).direction;
161
+ const hasRtlAncestor = !!(container.closest && container.closest('[dir="rtl"]'));
162
+ const isRtl = docDir === 'rtl' || cssDir === 'rtl' || hasRtlAncestor;
163
+ if (e.key === 'ArrowRight')
164
+ delta = isRtl ? -step : step; // inline-end
165
+ else if (e.key === 'ArrowLeft') delta = isRtl ? step : -step; // inline-start
142
166
  } else {
143
167
  if (e.key === 'ArrowDown') delta = step;
144
168
  else if (e.key === 'ArrowUp') delta = -step;
@@ -166,7 +190,8 @@ export const PaneHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsW
166
190
  if (delta !== 0) {
167
191
  e.preventDefault();
168
192
  onResizeStart?.(current);
169
- const next = clamp(current + (edge === 'start' && orientation === 'vertical' ? -delta : delta));
193
+ const signedDelta = orientation === 'vertical' ? (edge === 'start' ? -delta : delta) : delta;
194
+ const next = clamp(current + signedDelta);
170
195
  container.style.setProperty(cssVarName, `${next}px`);
171
196
  (e.currentTarget as HTMLElement).setAttribute('aria-valuenow', String(next));
172
197
  onResize?.(next);