@kushagradhawan/kookie-ui 0.1.50 → 0.1.52

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 (94) hide show
  1. package/components.css +582 -116
  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 +21 -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 -1
  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/esm/components/_internal/shell-bottom.d.ts +31 -5
  38. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
  39. package/dist/esm/components/_internal/shell-bottom.js +1 -1
  40. package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
  41. package/dist/esm/components/_internal/shell-handles.d.ts.map +1 -1
  42. package/dist/esm/components/_internal/shell-handles.js +1 -1
  43. package/dist/esm/components/_internal/shell-handles.js.map +3 -3
  44. package/dist/esm/components/_internal/shell-inspector.d.ts +23 -5
  45. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
  46. package/dist/esm/components/_internal/shell-inspector.js +1 -1
  47. package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
  48. package/dist/esm/components/_internal/shell-sidebar.d.ts +24 -6
  49. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
  50. package/dist/esm/components/_internal/shell-sidebar.js +1 -1
  51. package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
  52. package/dist/esm/components/chatbar.d.ts +21 -2
  53. package/dist/esm/components/chatbar.d.ts.map +1 -1
  54. package/dist/esm/components/chatbar.js +1 -1
  55. package/dist/esm/components/chatbar.js.map +3 -3
  56. package/dist/esm/components/shell.context.d.ts +88 -1
  57. package/dist/esm/components/shell.context.d.ts.map +1 -1
  58. package/dist/esm/components/shell.context.js +1 -1
  59. package/dist/esm/components/shell.context.js.map +3 -3
  60. package/dist/esm/components/shell.d.ts +51 -13
  61. package/dist/esm/components/shell.d.ts.map +1 -1
  62. package/dist/esm/components/shell.hooks.d.ts +7 -1
  63. package/dist/esm/components/shell.hooks.d.ts.map +1 -1
  64. package/dist/esm/components/shell.hooks.js +1 -1
  65. package/dist/esm/components/shell.hooks.js.map +3 -3
  66. package/dist/esm/components/shell.js +1 -1
  67. package/dist/esm/components/shell.js.map +3 -3
  68. package/dist/esm/components/shell.types.d.ts +1 -0
  69. package/dist/esm/components/shell.types.d.ts.map +1 -1
  70. package/dist/esm/components/shell.types.js.map +2 -2
  71. package/package.json +14 -3
  72. package/schemas/base-button.json +1 -1
  73. package/schemas/button.json +1 -1
  74. package/schemas/icon-button.json +1 -1
  75. package/schemas/index.json +6 -6
  76. package/schemas/toggle-button.json +1 -1
  77. package/schemas/toggle-icon-button.json +1 -1
  78. package/src/components/_internal/base-menu.css +16 -16
  79. package/src/components/_internal/base-sidebar-menu.css +23 -20
  80. package/src/components/_internal/base-sidebar.css +13 -0
  81. package/src/components/_internal/shell-bottom.tsx +176 -49
  82. package/src/components/_internal/shell-handles.tsx +29 -4
  83. package/src/components/_internal/shell-inspector.tsx +175 -43
  84. package/src/components/_internal/shell-sidebar.tsx +177 -93
  85. package/src/components/chatbar.css +240 -21
  86. package/src/components/chatbar.tsx +280 -291
  87. package/src/components/sheet.css +8 -16
  88. package/src/components/shell.context.tsx +79 -3
  89. package/src/components/shell.css +0 -1
  90. package/src/components/shell.hooks.ts +35 -0
  91. package/src/components/shell.tsx +574 -235
  92. package/src/components/shell.types.ts +2 -0
  93. package/src/components/sidebar.css +2 -2
  94. package/styles.css +582 -116
@@ -3,7 +3,7 @@
3
3
  "title": "Kookie UI Button Components",
4
4
  "description": "Complete JSON Schema collection for all button components in Kookie UI",
5
5
  "version": "1.0.0",
6
- "generatedAt": "2025-09-12T18:14:42.772Z",
6
+ "generatedAt": "2025-09-18T07:33:50.471Z",
7
7
  "source": "Zod schemas",
8
8
  "components": {
9
9
  "base-button": {
@@ -287,7 +287,7 @@
287
287
  "title": "Base-button Component Props",
288
288
  "description": "Props schema for the base-button component in Kookie UI",
289
289
  "version": "1.0.0",
290
- "generatedAt": "2025-09-12T18:14:42.763Z",
290
+ "generatedAt": "2025-09-18T07:33:50.462Z",
291
291
  "source": "Zod schema"
292
292
  },
293
293
  "button": {
@@ -822,7 +822,7 @@
822
822
  "title": "Button Component Props",
823
823
  "description": "Props schema for the button component in Kookie UI",
824
824
  "version": "1.0.0",
825
- "generatedAt": "2025-09-12T18:14:42.768Z",
825
+ "generatedAt": "2025-09-18T07:33:50.467Z",
826
826
  "source": "Zod schema"
827
827
  },
828
828
  "icon-button": {
@@ -1140,7 +1140,7 @@
1140
1140
  "title": "Icon-button Component Props",
1141
1141
  "description": "Props schema for the icon-button component in Kookie UI",
1142
1142
  "version": "1.0.0",
1143
- "generatedAt": "2025-09-12T18:14:42.770Z",
1143
+ "generatedAt": "2025-09-18T07:33:50.468Z",
1144
1144
  "source": "Zod schema"
1145
1145
  },
1146
1146
  "toggle-button": {
@@ -1683,7 +1683,7 @@
1683
1683
  "title": "Toggle-button Component Props",
1684
1684
  "description": "Props schema for the toggle-button component in Kookie UI",
1685
1685
  "version": "1.0.0",
1686
- "generatedAt": "2025-09-12T18:14:42.771Z",
1686
+ "generatedAt": "2025-09-18T07:33:50.469Z",
1687
1687
  "source": "Zod schema"
1688
1688
  },
1689
1689
  "toggle-icon-button": {
@@ -2009,7 +2009,7 @@
2009
2009
  "title": "Toggle-icon-button Component Props",
2010
2010
  "description": "Props schema for the toggle-icon-button component in Kookie UI",
2011
2011
  "version": "1.0.0",
2012
- "generatedAt": "2025-09-12T18:14:42.772Z",
2012
+ "generatedAt": "2025-09-18T07:33:50.470Z",
2013
2013
  "source": "Zod schema"
2014
2014
  }
2015
2015
  }
@@ -538,6 +538,6 @@
538
538
  "title": "Toggle-button Component Props",
539
539
  "description": "Props schema for the toggle-button component in Kookie UI",
540
540
  "version": "1.0.0",
541
- "generatedAt": "2025-09-12T18:14:42.771Z",
541
+ "generatedAt": "2025-09-18T07:33:50.469Z",
542
542
  "source": "Zod schema"
543
543
  }
@@ -321,6 +321,6 @@
321
321
  "title": "Toggle-icon-button Component Props",
322
322
  "description": "Props schema for the toggle-icon-button component in Kookie UI",
323
323
  "version": "1.0.0",
324
- "generatedAt": "2025-09-12T18:14:42.772Z",
324
+ "generatedAt": "2025-09-18T07:33:50.470Z",
325
325
  "source": "Zod schema"
326
326
  }
@@ -51,7 +51,7 @@
51
51
  box-sizing: border-box;
52
52
 
53
53
  :where(.rt-BaseMenuContent:has(.rt-ScrollAreaScrollbar[data-orientation='vertical'])) & {
54
- padding-right: var(--space-3);
54
+ padding-inline-end: var(--space-3);
55
55
  }
56
56
  }
57
57
 
@@ -62,8 +62,8 @@
62
62
  min-height: var(--base-menu-item-height);
63
63
  padding-top: var(--base-menu-item-padding-y);
64
64
  padding-bottom: var(--base-menu-item-padding-y);
65
- padding-left: var(--base-menu-item-padding-left);
66
- padding-right: var(--base-menu-item-padding-right);
65
+ padding-inline-start: var(--base-menu-item-padding-left);
66
+ padding-inline-end: var(--base-menu-item-padding-right);
67
67
  box-sizing: border-box;
68
68
  position: relative;
69
69
  outline: none;
@@ -95,19 +95,19 @@
95
95
  .rt-BaseMenuShortcut {
96
96
  display: flex;
97
97
  align-items: center;
98
- margin-left: auto;
99
- padding-left: var(--space-4);
100
- margin-right: calc(-2px * var(--scaling)); /* Pulls closer to edge */
98
+ margin-inline-start: auto;
99
+ padding-inline-start: var(--space-4);
100
+ margin-inline-end: calc(-2px * var(--scaling)); /* Pulls closer to edge */
101
101
  }
102
102
 
103
103
  .rt-BaseMenuSubTriggerIcon {
104
104
  color: var(--gray-12);
105
- margin-right: calc(-2px * var(--scaling));
105
+ margin-inline-end: calc(-2px * var(--scaling));
106
106
  }
107
107
 
108
108
  .rt-BaseMenuItemIndicator {
109
109
  position: absolute;
110
- left: 0;
110
+ inset-inline-start: 0;
111
111
  width: var(--base-menu-item-padding-left);
112
112
  display: inline-flex;
113
113
  align-items: center;
@@ -118,8 +118,8 @@
118
118
  height: 1px;
119
119
  margin-top: var(--space-2);
120
120
  margin-bottom: var(--space-2);
121
- margin-left: var(--base-menu-item-padding-left);
122
- margin-right: var(--base-menu-item-padding-right);
121
+ margin-inline-start: var(--base-menu-item-padding-left);
122
+ margin-inline-end: var(--base-menu-item-padding-right);
123
123
  }
124
124
 
125
125
  .rt-BaseMenuLabel {
@@ -128,8 +128,8 @@
128
128
  min-height: var(--base-menu-item-height);
129
129
  padding-top: var(--base-menu-item-padding-y);
130
130
  padding-bottom: var(--base-menu-item-padding-y);
131
- padding-left: var(--base-menu-item-padding-left);
132
- padding-right: var(--base-menu-item-padding-right);
131
+ padding-inline-start: var(--base-menu-item-padding-left);
132
+ padding-inline-end: var(--base-menu-item-padding-right);
133
133
  box-sizing: border-box;
134
134
  color: var(--gray-a10);
135
135
  user-select: none;
@@ -165,7 +165,7 @@
165
165
  --base-menu-content-padding: var(--space-2);
166
166
  --base-menu-item-padding-left: calc(var(--space-5) / 1.2);
167
167
  --base-menu-item-padding-right: var(--space-2);
168
- --base-menu-item-padding-y: var(--space-2);
168
+ --base-menu-item-padding-y: var(--space-1);
169
169
  --base-menu-item-height: var(--space-5);
170
170
  border-radius: var(--radius-3);
171
171
 
@@ -188,7 +188,7 @@
188
188
  font-size: var(--font-size-1);
189
189
  line-height: var(--line-height-1);
190
190
  letter-spacing: var(--letter-spacing-1);
191
- padding-left: calc(var(--base-menu-item-padding-left) + var(--component-gap-2));
191
+ padding-inline-start: calc(var(--base-menu-item-padding-left) + var(--component-gap-2));
192
192
  }
193
193
 
194
194
  & :where(.rt-BaseMenuItemIndicatorIcon, .rt-BaseMenuSubTriggerIcon) {
@@ -209,7 +209,7 @@
209
209
  --base-menu-content-padding: var(--space-3);
210
210
  --base-menu-item-padding-left: var(--space-3);
211
211
  --base-menu-item-padding-right: var(--space-3);
212
- --base-menu-item-padding-y: var(--space-2);
212
+ --base-menu-item-padding-y: var(--space-1);
213
213
  --base-menu-item-height: var(--space-6);
214
214
  border-radius: var(--radius-5);
215
215
 
@@ -254,7 +254,7 @@
254
254
  --base-menu-item-padding-left: var(--space-3);
255
255
  --base-menu-item-padding-right: var(--space-3);
256
256
  --base-menu-item-padding-y: var(--space-2);
257
- --base-menu-item-height: var(--space-6);
257
+ --base-menu-item-height: var(--space-7);
258
258
  border-radius: var(--radius-6);
259
259
 
260
260
  & :where(.rt-BaseMenuItem) {
@@ -16,7 +16,7 @@
16
16
  min-height: 0;
17
17
 
18
18
  :where(.rt-SidebarContent:has(.rt-ScrollAreaScrollbar[data-orientation='vertical'])) & {
19
- padding-right: var(--space-3);
19
+ padding-inline-end: var(--space-3);
20
20
  }
21
21
  }
22
22
 
@@ -33,8 +33,8 @@
33
33
  min-height: var(--base-menu-item-height);
34
34
  padding-top: var(--base-menu-item-padding-y);
35
35
  padding-bottom: var(--base-menu-item-padding-y);
36
- padding-left: var(--base-menu-item-padding-left);
37
- 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);
38
38
  box-sizing: border-box;
39
39
  position: relative;
40
40
  outline: none;
@@ -42,11 +42,14 @@
42
42
  background: none;
43
43
  border: none;
44
44
  width: 100%;
45
- text-align: left;
45
+ text-align: start;
46
46
  /* No default border radius - inherited from size-specific rules */
47
47
 
48
- /* Transitions - inherit from base menu */
49
- 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);
50
53
 
51
54
  /* Fix sticky text highlighting after selection in Firefox */
52
55
  user-select: none;
@@ -146,14 +149,14 @@
146
149
  }
147
150
 
148
151
  .rt-SidebarMenuSubList {
149
- padding-left: var(--space-4);
150
- border-left: 1px solid var(--gray-a5);
151
- 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);
152
155
  }
153
156
 
154
157
  /* Sub-menu items have consistent heights based on size - match dropdown menu exactly */
155
158
  :where(.rt-SidebarContent.rt-r-size-1) :where(.rt-SidebarMenuSubList) .rt-SidebarMenuButton {
156
- padding-left: var(--space-3);
159
+ padding-inline-start: var(--space-3);
157
160
  padding-top: calc(var(--space-1) * 0.75);
158
161
  padding-bottom: calc(var(--space-1) * 0.75);
159
162
  min-height: var(--space-5); /* 20px */
@@ -161,10 +164,10 @@
161
164
  }
162
165
 
163
166
  :where(.rt-SidebarContent.rt-r-size-2) :where(.rt-SidebarMenuSubList) .rt-SidebarMenuButton {
164
- padding-left: var(--space-3);
167
+ padding-inline-start: var(--space-3);
165
168
  padding-top: var(--space-1);
166
169
  padding-bottom: var(--space-1);
167
- min-height: var(--space-6); /* 24px */
170
+ min-height: var(--space-6); /* 32px */
168
171
  font-size: var(--font-size-2);
169
172
  }
170
173
 
@@ -175,8 +178,8 @@
175
178
  min-height: var(--base-menu-item-height);
176
179
  padding-top: var(--base-menu-item-padding-y);
177
180
  padding-bottom: var(--base-menu-item-padding-y);
178
- padding-left: var(--base-menu-item-padding-left);
179
- 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);
180
183
  box-sizing: border-box;
181
184
  color: var(--gray-a10);
182
185
  user-select: none;
@@ -192,27 +195,27 @@
192
195
  .rt-SidebarMenuButton:where(:has(.rt-SidebarMenuShortcut, .rt-SidebarMenuBadge)) {
193
196
  /* Use base menu padding tokens */
194
197
  :where(.rt-SidebarContent.rt-r-size-1) & {
195
- 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 */
196
199
  }
197
200
 
198
201
  :where(.rt-SidebarContent.rt-r-size-2) & {
199
- 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 */
200
203
  }
201
204
  }
202
205
 
203
206
  .rt-SidebarMenuShortcut {
204
207
  display: flex;
205
208
  align-items: center;
206
- margin-left: auto;
207
- padding-left: var(--space-4);
209
+ margin-inline-start: auto;
210
+ padding-inline-start: var(--space-4);
208
211
  color: var(--gray-a11);
209
212
  }
210
213
 
211
214
  .rt-SidebarMenuBadge {
212
215
  display: flex;
213
216
  align-items: center;
214
- margin-left: auto;
215
- padding-left: var(--space-2);
217
+ margin-inline-start: auto;
218
+ padding-inline-start: var(--space-2);
216
219
  }
217
220
 
218
221
  /* Add balanced spacing for sidebar menu items while preserving base menu border radius */
@@ -78,6 +78,10 @@
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
+
81
85
  /* Add gap between groups using flex */
82
86
  /* stylelint-disable-next-line selector-max-specificity */
83
87
  & .rt-BaseMenuViewport {
@@ -99,6 +103,15 @@
99
103
  flex-direction: column;
100
104
  min-height: 0;
101
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
+ }
102
115
  }
103
116
 
104
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`,