@kushagradhawan/kookie-ui 0.1.70 → 0.1.72

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 (164) hide show
  1. package/README.md +4 -0
  2. package/components.css +63 -380
  3. package/dist/cjs/components/_internal/base-button.d.ts.map +1 -1
  4. package/dist/cjs/components/_internal/base-button.js +1 -1
  5. package/dist/cjs/components/_internal/base-button.js.map +3 -3
  6. package/dist/cjs/components/_internal/shell-bottom.d.ts +2 -21
  7. package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -1
  8. package/dist/cjs/components/_internal/shell-bottom.js +1 -1
  9. package/dist/cjs/components/_internal/shell-bottom.js.map +3 -3
  10. package/dist/cjs/components/_internal/shell-inspector.d.ts +10 -21
  11. package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
  12. package/dist/cjs/components/_internal/shell-inspector.js +1 -1
  13. package/dist/cjs/components/_internal/shell-inspector.js.map +3 -3
  14. package/dist/cjs/components/_internal/shell-prop-helpers.d.ts +7 -0
  15. package/dist/cjs/components/_internal/shell-prop-helpers.d.ts.map +1 -0
  16. package/dist/cjs/components/_internal/shell-prop-helpers.js +2 -0
  17. package/dist/cjs/components/_internal/shell-prop-helpers.js.map +7 -0
  18. package/dist/cjs/components/_internal/shell-sidebar.d.ts +4 -21
  19. package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
  20. package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
  21. package/dist/cjs/components/_internal/shell-sidebar.js.map +3 -3
  22. package/dist/cjs/components/button.d.ts.map +1 -1
  23. package/dist/cjs/components/button.js +1 -1
  24. package/dist/cjs/components/button.js.map +3 -3
  25. package/dist/cjs/components/chatbar.d.ts +11 -2
  26. package/dist/cjs/components/chatbar.d.ts.map +1 -1
  27. package/dist/cjs/components/chatbar.js +1 -1
  28. package/dist/cjs/components/chatbar.js.map +3 -3
  29. package/dist/cjs/components/icon-button.d.ts.map +1 -1
  30. package/dist/cjs/components/icon-button.js +2 -2
  31. package/dist/cjs/components/icon-button.js.map +3 -3
  32. package/dist/cjs/components/schemas/shell.schema.d.ts +70 -70
  33. package/dist/cjs/components/shell.context.d.ts +1 -0
  34. package/dist/cjs/components/shell.context.d.ts.map +1 -1
  35. package/dist/cjs/components/shell.context.js.map +2 -2
  36. package/dist/cjs/components/shell.d.ts +6 -26
  37. package/dist/cjs/components/shell.d.ts.map +1 -1
  38. package/dist/cjs/components/shell.hooks.d.ts +19 -2
  39. package/dist/cjs/components/shell.hooks.d.ts.map +1 -1
  40. package/dist/cjs/components/shell.hooks.js +1 -1
  41. package/dist/cjs/components/shell.hooks.js.map +3 -3
  42. package/dist/cjs/components/shell.js +1 -1
  43. package/dist/cjs/components/shell.js.map +3 -3
  44. package/dist/cjs/components/shell.types.d.ts +21 -0
  45. package/dist/cjs/components/shell.types.d.ts.map +1 -1
  46. package/dist/cjs/components/shell.types.js +1 -1
  47. package/dist/cjs/components/shell.types.js.map +2 -2
  48. package/dist/cjs/components/toggle-button.d.ts.map +1 -1
  49. package/dist/cjs/components/toggle-button.js +1 -1
  50. package/dist/cjs/components/toggle-button.js.map +3 -3
  51. package/dist/cjs/components/toggle-icon-button.d.ts.map +1 -1
  52. package/dist/cjs/components/toggle-icon-button.js +1 -1
  53. package/dist/cjs/components/toggle-icon-button.js.map +3 -3
  54. package/dist/cjs/hooks/index.d.ts +2 -0
  55. package/dist/cjs/hooks/index.d.ts.map +1 -1
  56. package/dist/cjs/hooks/index.js +1 -1
  57. package/dist/cjs/hooks/index.js.map +3 -3
  58. package/dist/cjs/hooks/use-live-announcer.d.ts.map +1 -1
  59. package/dist/cjs/hooks/use-live-announcer.js +2 -2
  60. package/dist/cjs/hooks/use-live-announcer.js.map +3 -3
  61. package/dist/cjs/hooks/use-toggle-state.d.ts +37 -0
  62. package/dist/cjs/hooks/use-toggle-state.d.ts.map +1 -0
  63. package/dist/cjs/hooks/use-toggle-state.js +2 -0
  64. package/dist/cjs/hooks/use-toggle-state.js.map +7 -0
  65. package/dist/cjs/hooks/use-tooltip-wrapper.d.ts +29 -0
  66. package/dist/cjs/hooks/use-tooltip-wrapper.d.ts.map +1 -0
  67. package/dist/cjs/hooks/use-tooltip-wrapper.js +2 -0
  68. package/dist/cjs/hooks/use-tooltip-wrapper.js.map +7 -0
  69. package/dist/esm/components/_internal/base-button.d.ts.map +1 -1
  70. package/dist/esm/components/_internal/base-button.js +1 -1
  71. package/dist/esm/components/_internal/base-button.js.map +3 -3
  72. package/dist/esm/components/_internal/shell-bottom.d.ts +2 -21
  73. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
  74. package/dist/esm/components/_internal/shell-bottom.js +1 -1
  75. package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
  76. package/dist/esm/components/_internal/shell-inspector.d.ts +10 -21
  77. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
  78. package/dist/esm/components/_internal/shell-inspector.js +1 -1
  79. package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
  80. package/dist/esm/components/_internal/shell-prop-helpers.d.ts +7 -0
  81. package/dist/esm/components/_internal/shell-prop-helpers.d.ts.map +1 -0
  82. package/dist/esm/components/_internal/shell-prop-helpers.js +2 -0
  83. package/dist/esm/components/_internal/shell-prop-helpers.js.map +7 -0
  84. package/dist/esm/components/_internal/shell-sidebar.d.ts +4 -21
  85. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
  86. package/dist/esm/components/_internal/shell-sidebar.js +1 -1
  87. package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
  88. package/dist/esm/components/button.d.ts.map +1 -1
  89. package/dist/esm/components/button.js +1 -1
  90. package/dist/esm/components/button.js.map +3 -3
  91. package/dist/esm/components/chatbar.d.ts +11 -2
  92. package/dist/esm/components/chatbar.d.ts.map +1 -1
  93. package/dist/esm/components/chatbar.js +1 -1
  94. package/dist/esm/components/chatbar.js.map +3 -3
  95. package/dist/esm/components/icon-button.d.ts.map +1 -1
  96. package/dist/esm/components/icon-button.js +2 -2
  97. package/dist/esm/components/icon-button.js.map +3 -3
  98. package/dist/esm/components/schemas/shell.schema.d.ts +70 -70
  99. package/dist/esm/components/shell.context.d.ts +1 -0
  100. package/dist/esm/components/shell.context.d.ts.map +1 -1
  101. package/dist/esm/components/shell.context.js.map +2 -2
  102. package/dist/esm/components/shell.d.ts +6 -26
  103. package/dist/esm/components/shell.d.ts.map +1 -1
  104. package/dist/esm/components/shell.hooks.d.ts +19 -2
  105. package/dist/esm/components/shell.hooks.d.ts.map +1 -1
  106. package/dist/esm/components/shell.hooks.js +1 -1
  107. package/dist/esm/components/shell.hooks.js.map +3 -3
  108. package/dist/esm/components/shell.js +1 -1
  109. package/dist/esm/components/shell.js.map +3 -3
  110. package/dist/esm/components/shell.types.d.ts +21 -0
  111. package/dist/esm/components/shell.types.d.ts.map +1 -1
  112. package/dist/esm/components/shell.types.js.map +2 -2
  113. package/dist/esm/components/toggle-button.d.ts.map +1 -1
  114. package/dist/esm/components/toggle-button.js +1 -1
  115. package/dist/esm/components/toggle-button.js.map +3 -3
  116. package/dist/esm/components/toggle-icon-button.d.ts.map +1 -1
  117. package/dist/esm/components/toggle-icon-button.js +1 -1
  118. package/dist/esm/components/toggle-icon-button.js.map +3 -3
  119. package/dist/esm/hooks/index.d.ts +2 -0
  120. package/dist/esm/hooks/index.d.ts.map +1 -1
  121. package/dist/esm/hooks/index.js +1 -1
  122. package/dist/esm/hooks/index.js.map +3 -3
  123. package/dist/esm/hooks/use-live-announcer.d.ts.map +1 -1
  124. package/dist/esm/hooks/use-live-announcer.js +2 -2
  125. package/dist/esm/hooks/use-live-announcer.js.map +3 -3
  126. package/dist/esm/hooks/use-toggle-state.d.ts +37 -0
  127. package/dist/esm/hooks/use-toggle-state.d.ts.map +1 -0
  128. package/dist/esm/hooks/use-toggle-state.js +2 -0
  129. package/dist/esm/hooks/use-toggle-state.js.map +7 -0
  130. package/dist/esm/hooks/use-tooltip-wrapper.d.ts +29 -0
  131. package/dist/esm/hooks/use-tooltip-wrapper.d.ts.map +1 -0
  132. package/dist/esm/hooks/use-tooltip-wrapper.js +2 -0
  133. package/dist/esm/hooks/use-tooltip-wrapper.js.map +7 -0
  134. package/package.json +4 -4
  135. package/schemas/base-button.json +1 -1
  136. package/schemas/button.json +1 -1
  137. package/schemas/icon-button.json +1 -1
  138. package/schemas/index.json +6 -6
  139. package/schemas/toggle-button.json +1 -1
  140. package/schemas/toggle-icon-button.json +1 -1
  141. package/src/components/_internal/base-button.css +136 -614
  142. package/src/components/_internal/base-button.tsx +15 -13
  143. package/src/components/_internal/shell-bottom.tsx +305 -321
  144. package/src/components/_internal/shell-inspector.tsx +310 -320
  145. package/src/components/_internal/shell-prop-helpers.ts +53 -0
  146. package/src/components/_internal/shell-sidebar.tsx +370 -384
  147. package/src/components/button.tsx +13 -42
  148. package/src/components/chatbar.tsx +7 -3
  149. package/src/components/icon-button.tsx +20 -44
  150. package/src/components/image.css +10 -8
  151. package/src/components/shell.context.tsx +1 -0
  152. package/src/components/shell.hooks.ts +67 -2
  153. package/src/components/shell.tsx +199 -209
  154. package/src/components/shell.types.ts +23 -0
  155. package/src/components/toggle-button.tsx +30 -59
  156. package/src/components/toggle-icon-button.tsx +29 -51
  157. package/src/hooks/index.ts +2 -0
  158. package/src/hooks/use-live-announcer.ts +34 -7
  159. package/src/hooks/use-toggle-state.ts +72 -0
  160. package/src/hooks/use-tooltip-wrapper.ts +28 -0
  161. package/src/styles/tokens/color.css +11 -1
  162. package/styles.css +70 -381
  163. package/tokens/base.css +7 -1
  164. package/tokens.css +7 -1
@@ -3,32 +3,12 @@ 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, useResponsiveValue } from '../shell.hooks.js';
6
+ import { useResponsivePresentation, useResponsiveInitialState } 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';
10
- import type { Breakpoint, PaneMode, PaneSizePersistence, ResponsivePresentation } from '../shell.types.js';
11
-
12
- interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
13
- presentation?: ResponsivePresentation;
14
- // legacy mode removed
15
- expandedSize?: number;
16
- minSize?: number;
17
- maxSize?: number;
18
- resizable?: boolean;
19
- collapsible?: boolean;
20
- onExpand?: () => void;
21
- onCollapse?: () => void;
22
- onResize?: (size: number) => void;
23
- resizer?: React.ReactNode;
24
- onResizeStart?: (size: number) => void;
25
- onResizeEnd?: (size: number) => void;
26
- snapPoints?: number[];
27
- snapTolerance?: number;
28
- collapseThreshold?: number;
29
- paneId?: string;
30
- persistence?: PaneSizePersistence;
31
- }
10
+ import type { Breakpoint, PaneMode, PaneSizePersistence, ResponsivePresentation, PaneBaseProps } from '../shell.types.js';
11
+ import { extractPaneDomProps, mapResponsiveBooleanToPaneMode } from './shell-prop-helpers.js';
32
12
 
33
13
  type BottomOpenChangeMeta = { reason: 'init' | 'toggle' | 'responsive' };
34
14
  type BottomControlledProps = { open: boolean | Partial<Record<Breakpoint, boolean>>; onOpenChange?: (open: boolean, meta: BottomOpenChangeMeta) => void; defaultOpen?: never };
@@ -36,7 +16,7 @@ type BottomUncontrolledProps = { defaultOpen?: boolean; onOpenChange?: (open: bo
36
16
  type BottomSizeControlledProps = { size: number | string; defaultSize?: never };
37
17
  type BottomSizeUncontrolledProps = { defaultSize?: number | string; size?: never };
38
18
  type BottomSizeChangeMeta = { reason: 'init' | 'resize' | 'controlled' };
39
- type BottomPublicProps = PaneProps &
19
+ type BottomPublicProps = PaneBaseProps &
40
20
  (BottomControlledProps | BottomUncontrolledProps) &
41
21
  (BottomSizeControlledProps | BottomSizeUncontrolledProps) & {
42
22
  onSizeChange?: (size: number, meta: BottomSizeChangeMeta) => void;
@@ -46,334 +26,338 @@ type BottomPublicProps = PaneProps &
46
26
 
47
27
  type BottomComponent = React.ForwardRefExoticComponent<BottomPublicProps & React.RefAttributes<HTMLDivElement>> & { Handle: typeof BottomHandle };
48
28
 
49
- export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>(
50
- (
51
- {
52
- className,
53
- presentation = 'fixed',
54
- // removed legacy props
55
- // new API
56
- defaultOpen,
57
- open,
58
- onOpenChange,
59
- expandedSize = 200,
60
- minSize = 100,
61
- maxSize = 400,
62
- resizable = false,
63
- collapsible = true,
64
- onExpand,
65
- onCollapse,
66
- onResize,
67
- onResizeStart,
68
- onResizeEnd,
69
- snapPoints,
70
- snapTolerance,
71
- collapseThreshold,
72
- paneId,
73
- persistence,
74
- children,
75
- style,
76
- ...props
77
- },
78
- ref,
79
- ) => {
80
- const shell = useShell();
81
- const resolvedPresentation = useResponsivePresentation(presentation);
82
- const isOverlay = resolvedPresentation === 'overlay';
83
- const isStacked = resolvedPresentation === 'stacked';
84
- const localRef = React.useRef<HTMLDivElement | null>(null);
85
- const setRef = React.useCallback(
86
- (node: HTMLDivElement | null) => {
87
- localRef.current = node;
88
- if (typeof ref === 'function') ref(node);
89
- else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
90
- },
91
- [ref],
92
- );
93
- const childArray = React.Children.toArray(children) as React.ReactElement[];
94
- const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === BottomHandle);
95
- const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === BottomHandle));
29
+ const BOTTOM_DOM_PROP_KEYS = [
30
+ 'className',
31
+ 'children',
32
+ 'defaultOpen',
33
+ 'open',
34
+ 'onOpenChange',
35
+ 'size',
36
+ 'defaultSize',
37
+ 'onSizeChange',
38
+ 'sizeUpdate',
39
+ 'sizeUpdateMs',
40
+ 'style',
41
+ ] as const satisfies readonly (keyof BottomPublicProps)[];
96
42
 
97
- // Throttled/debounced emitter for onSizeChange
98
- const onSizeChange = (props as any).onSizeChange;
99
- const sizeUpdate = (props as any).sizeUpdate;
100
- const sizeUpdateMs = (props as any).sizeUpdateMs;
101
- const emitSizeChange = React.useMemo(() => {
102
- const cb = onSizeChange as undefined | ((s: number, meta: BottomSizeChangeMeta) => void);
103
- const strategy = sizeUpdate as undefined | 'throttle' | 'debounce';
104
- const ms = sizeUpdateMs ?? 50;
105
- if (!cb) return () => {};
106
- if (strategy === 'debounce') {
107
- let t: any = null;
108
- return (s: number, meta: BottomSizeChangeMeta) => {
109
- if (t) clearTimeout(t);
110
- t = setTimeout(() => {
111
- cb(s, meta);
112
- }, ms);
113
- };
114
- }
115
- if (strategy === 'throttle') {
116
- let last = 0;
117
- return (s: number, meta: BottomSizeChangeMeta) => {
118
- const now = Date.now();
119
- if (now - last >= ms) {
120
- last = now;
121
- cb(s, meta);
122
- }
123
- };
124
- }
125
- return (s: number, meta: BottomSizeChangeMeta) => cb(s, meta);
126
- }, [onSizeChange, sizeUpdate, sizeUpdateMs]);
43
+ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initialProps, ref) => {
44
+ const {
45
+ className,
46
+ presentation = 'fixed',
47
+ defaultOpen,
48
+ open,
49
+ onOpenChange,
50
+ expandedSize = 200,
51
+ minSize = 100,
52
+ maxSize = 400,
53
+ resizable = false,
54
+ collapsible = true,
55
+ onExpand,
56
+ onCollapse,
57
+ onResize,
58
+ onResizeStart,
59
+ onResizeEnd,
60
+ snapPoints,
61
+ snapTolerance,
62
+ collapseThreshold,
63
+ paneId,
64
+ persistence,
65
+ children,
66
+ style,
67
+ size,
68
+ defaultSize,
69
+ onSizeChange,
70
+ sizeUpdate,
71
+ sizeUpdateMs = 50,
72
+ } = initialProps;
73
+ const bottomDomProps = extractPaneDomProps(initialProps, BOTTOM_DOM_PROP_KEYS);
74
+ const shell = useShell();
75
+ const resolvedPresentation = useResponsivePresentation(presentation);
76
+ const isOverlay = resolvedPresentation === 'overlay';
77
+ const isStacked = resolvedPresentation === 'stacked';
78
+ const localRef = React.useRef<HTMLDivElement | null>(null);
79
+ const setRef = React.useCallback(
80
+ (node: HTMLDivElement | null) => {
81
+ localRef.current = node;
82
+ if (typeof ref === 'function') ref(node);
83
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
84
+ },
85
+ [ref],
86
+ );
87
+ const childArray = React.Children.toArray(children) as React.ReactElement[];
88
+ const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === BottomHandle);
89
+ const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === BottomHandle));
127
90
 
128
- const didInitRef = React.useRef(false);
129
- const didInitFromDefaultOpenRef = React.useRef(false);
130
- const resolvedDefaultOpen = useResponsiveValue(defaultOpen as any);
131
- React.useEffect(() => {
132
- if (didInitRef.current) return;
133
- if (!shell.currentBreakpointReady) return;
134
- didInitRef.current = true;
135
- if (typeof open === 'undefined' && typeof defaultOpen !== 'undefined') {
136
- const initial = Boolean(resolvedDefaultOpen);
137
- shell.setBottomMode(initial ? 'expanded' : 'collapsed');
138
- didInitFromDefaultOpenRef.current = true;
91
+ // Throttled/debounced emitter for onSizeChange
92
+ const normalizedControlledOpen = React.useMemo(() => mapResponsiveBooleanToPaneMode(open), [open]);
93
+ const normalizedDefaultOpen = React.useMemo(() => mapResponsiveBooleanToPaneMode(defaultOpen), [defaultOpen]);
94
+ const openIsResponsive = typeof open === 'object' && open !== null;
95
+ useResponsiveInitialState<PaneMode>({
96
+ controlledValue: normalizedControlledOpen,
97
+ defaultValue: normalizedDefaultOpen,
98
+ currentValue: shell.bottomMode,
99
+ setValue: shell.setBottomMode,
100
+ breakpointReady: shell.currentBreakpointReady,
101
+ controlledIsResponsive: openIsResponsive,
102
+ onResponsiveChange: (next) => onOpenChange?.(next === 'expanded', { reason: 'responsive' }),
103
+ onInit: (initial) => {
104
+ if (typeof open === 'undefined') {
105
+ onOpenChange?.(initial === 'expanded', { reason: 'init' });
139
106
  }
140
- }, [shell, open, defaultOpen, resolvedDefaultOpen]);
107
+ },
108
+ });
141
109
 
142
- // Dev guards
143
- const wasControlledRef = React.useRef<boolean | null>(null);
144
- if (process.env.NODE_ENV !== 'production') {
145
- if (typeof open !== 'undefined' && typeof defaultOpen !== 'undefined') {
146
- console.error('Shell.Bottom: Do not pass both `open` and `defaultOpen`. Choose one.');
147
- }
148
- if (typeof (props as any).size !== 'undefined' && typeof (props as any).defaultSize !== 'undefined') {
149
- console.error('Shell.Bottom: Do not pass both `size` and `defaultSize`. Choose one.');
150
- }
110
+ const emitSizeChange = React.useMemo(() => {
111
+ const cb = onSizeChange as undefined | ((s: number, meta: BottomSizeChangeMeta) => void);
112
+ const strategy = sizeUpdate as undefined | 'throttle' | 'debounce';
113
+ const ms = sizeUpdateMs ?? 50;
114
+ if (!cb) return () => {};
115
+ if (strategy === 'debounce') {
116
+ let t: any = null;
117
+ return (s: number, meta: BottomSizeChangeMeta) => {
118
+ if (t) clearTimeout(t);
119
+ t = setTimeout(() => {
120
+ cb(s, meta);
121
+ }, ms);
122
+ };
151
123
  }
124
+ if (strategy === 'throttle') {
125
+ let last = 0;
126
+ return (s: number, meta: BottomSizeChangeMeta) => {
127
+ const now = Date.now();
128
+ if (now - last >= ms) {
129
+ last = now;
130
+ cb(s, meta);
131
+ }
132
+ };
133
+ }
134
+ return (s: number, meta: BottomSizeChangeMeta) => cb(s, meta);
135
+ }, [onSizeChange, sizeUpdate, sizeUpdateMs]);
152
136
 
153
- React.useEffect(() => {
154
- const isControlled = typeof open !== 'undefined';
155
- if (wasControlledRef.current === null) {
156
- wasControlledRef.current = isControlled;
157
- return;
158
- }
159
- if (wasControlledRef.current !== isControlled) {
160
- console.warn('Shell.Bottom: Switching between controlled and uncontrolled `open` is not supported.');
161
- wasControlledRef.current = isControlled;
162
- }
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
- }, [shell, open]);
170
-
171
- const responsiveNotifiedRef = React.useRef(false);
137
+ // Dev guards
138
+ const wasControlledRef = React.useRef<boolean | null>(null);
139
+ if (process.env.NODE_ENV !== 'production') {
140
+ if (typeof open !== 'undefined' && typeof defaultOpen !== 'undefined') {
141
+ console.error('Shell.Bottom: Do not pass both `open` and `defaultOpen`. Choose one.');
142
+ }
143
+ if (typeof size !== 'undefined' && typeof defaultSize !== 'undefined') {
144
+ console.error('Shell.Bottom: Do not pass both `size` and `defaultSize`. Choose one.');
145
+ }
146
+ }
172
147
 
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
- }, [shell, resolvedOpen]);
148
+ React.useEffect(() => {
149
+ const isControlled = typeof open !== 'undefined';
150
+ if (wasControlledRef.current === null) {
151
+ wasControlledRef.current = isControlled;
152
+ return;
153
+ }
154
+ if (wasControlledRef.current !== isControlled) {
155
+ console.warn('Shell.Bottom: Switching between controlled and uncontrolled `open` is not supported.');
156
+ wasControlledRef.current = isControlled;
157
+ }
158
+ }, [open]);
180
159
 
181
- const initNotifiedRef = React.useRef(false);
182
- const lastBottomModeRef = React.useRef<PaneMode | null>(null);
183
- React.useEffect(() => {
184
- if (!initNotifiedRef.current && typeof open === 'undefined' && defaultOpen && shell.bottomMode === 'expanded') {
185
- onOpenChange?.(true, { reason: 'init' });
186
- initNotifiedRef.current = true;
187
- }
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;
160
+ const initNotifiedRef = React.useRef(false);
161
+ const lastBottomModeRef = React.useRef<PaneMode | null>(null);
162
+ React.useEffect(() => {
163
+ if (!initNotifiedRef.current && typeof open === 'undefined' && defaultOpen && shell.bottomMode === 'expanded') {
164
+ onOpenChange?.(true, { reason: 'init' });
165
+ initNotifiedRef.current = true;
166
+ }
167
+ if (typeof open === 'undefined') {
168
+ if (lastBottomModeRef.current !== null && lastBottomModeRef.current !== shell.bottomMode) {
169
+ onOpenChange?.(shell.bottomMode === 'expanded', { reason: 'toggle' });
196
170
  }
197
- }, [shell.bottomMode, open, defaultOpen, onOpenChange]);
171
+ lastBottomModeRef.current = shell.bottomMode;
172
+ }
173
+ }, [shell.bottomMode, open, defaultOpen, onOpenChange]);
198
174
 
199
- React.useEffect(() => {
200
- if (shell.bottomMode === 'expanded') {
201
- onExpand?.();
202
- } else {
203
- onCollapse?.();
204
- }
205
- }, [shell.bottomMode, onExpand, onCollapse]);
175
+ React.useEffect(() => {
176
+ if (shell.bottomMode === 'expanded') {
177
+ onExpand?.();
178
+ } else {
179
+ onCollapse?.();
180
+ }
181
+ }, [shell.bottomMode, onExpand, onCollapse]);
206
182
 
207
- const isExpanded = shell.bottomMode === 'expanded';
183
+ const isExpanded = shell.bottomMode === 'expanded';
208
184
 
209
- const persistenceAdapter = React.useMemo(() => {
210
- if (!paneId || persistence) return persistence;
211
- const key = `kookie-ui:shell:bottom:${paneId}`;
212
- const adapter: PaneSizePersistence = {
213
- load: () => {
214
- if (typeof window === 'undefined') return undefined;
185
+ const persistenceAdapter = React.useMemo(() => {
186
+ if (!paneId || persistence) return persistence;
187
+ const key = `kookie-ui:shell:bottom:${paneId}`;
188
+ const adapter: PaneSizePersistence = {
189
+ load: () => {
190
+ if (typeof window === 'undefined') return undefined;
191
+ try {
215
192
  const v = window.localStorage.getItem(key);
216
193
  return v ? Number(v) : undefined;
217
- },
218
- save: (size: number) => {
219
- if (typeof window === 'undefined') return;
194
+ } catch (err) {
195
+ if (process.env.NODE_ENV !== 'production') {
196
+ console.warn('Shell.Bottom: failed to load persisted size', err);
197
+ }
198
+ return undefined;
199
+ }
200
+ },
201
+ save: (size: number) => {
202
+ if (typeof window === 'undefined') return;
203
+ try {
220
204
  window.localStorage.setItem(key, String(size));
221
- },
222
- };
223
- return adapter;
224
- }, [paneId, persistence]);
225
-
226
- React.useEffect(() => {
227
- let mounted = true;
228
- (async () => {
229
- if (!resizable || !persistenceAdapter?.load || isOverlay) return;
230
- const loaded = await persistenceAdapter.load();
231
- if (mounted && typeof loaded === 'number' && localRef.current) {
232
- localRef.current.style.setProperty('--bottom-size', `${loaded}px`);
233
- onResize?.(loaded);
205
+ } catch (err) {
206
+ if (process.env.NODE_ENV !== 'production') {
207
+ console.warn('Shell.Bottom: failed to save persisted size', err);
208
+ }
234
209
  }
235
- })();
236
- return () => {
237
- mounted = false;
238
- };
239
- }, [resizable, persistenceAdapter, onResize, isOverlay]);
240
-
241
- const handleEl =
242
- resizable && !isOverlay && isExpanded ? (
243
- <PaneResizeContext.Provider
244
- value={{
245
- containerRef: localRef,
246
- cssVarName: '--bottom-size',
247
- minSize,
248
- maxSize,
249
- defaultSize: expandedSize,
250
- orientation: 'horizontal',
251
- edge: 'start',
252
- computeNext: (client, startClient, startSize) => {
253
- const delta = client - startClient;
254
- return startSize - delta;
255
- },
256
- onResize,
257
- onResizeStart,
258
- onResizeEnd: (size) => {
259
- onResizeEnd?.(size);
260
- emitSizeChange(size, { reason: 'resize' });
261
- persistenceAdapter?.save?.(size);
262
- },
263
- target: 'bottom',
264
- collapsible,
265
- snapPoints,
266
- snapTolerance: snapTolerance ?? 8,
267
- collapseThreshold,
268
- requestCollapse: () => shell.setBottomMode('collapsed'),
269
- requestToggle: () => shell.togglePane('bottom'),
270
- }}
271
- >
272
- {handleChildren.length > 0 ? handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i })) : <PaneHandle />}
273
- </PaneResizeContext.Provider>
274
- ) : null;
210
+ },
211
+ };
212
+ return adapter;
213
+ }, [paneId, persistence]);
275
214
 
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;
215
+ React.useEffect(() => {
216
+ let mounted = true;
217
+ if (!resizable || !persistenceAdapter?.load || isOverlay) return undefined;
218
+ const loaded = persistenceAdapter.load();
219
+ const applyLoaded = (value?: number) => {
220
+ if (!mounted || typeof value !== 'number' || !localRef.current) return;
221
+ localRef.current.style.setProperty('--bottom-size', `${value}px`);
222
+ onResize?.(value);
223
+ };
224
+ if (loaded instanceof Promise) {
225
+ loaded.then(applyLoaded).catch((err) => {
226
+ if (process.env.NODE_ENV !== 'production') {
227
+ console.warn('Shell.Bottom: failed to load persisted size', err);
228
+ }
229
+ });
230
+ } else {
231
+ applyLoaded(loaded);
232
+ }
233
+ return () => {
234
+ mounted = false;
235
+ };
236
+ }, [resizable, persistenceAdapter, onResize, isOverlay]);
288
237
 
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
- }, []);
238
+ const handleEl =
239
+ resizable && !isOverlay && isExpanded ? (
240
+ <PaneResizeContext.Provider
241
+ value={{
242
+ containerRef: localRef,
243
+ cssVarName: '--bottom-size',
244
+ minSize,
245
+ maxSize,
246
+ defaultSize: expandedSize,
247
+ orientation: 'horizontal',
248
+ edge: 'start',
249
+ computeNext: (client, startClient, startSize) => {
250
+ const delta = client - startClient;
251
+ return startSize - delta;
252
+ },
253
+ onResize,
254
+ onResizeStart,
255
+ onResizeEnd: (size) => {
256
+ onResizeEnd?.(size);
257
+ emitSizeChange(size, { reason: 'resize' });
258
+ persistenceAdapter?.save?.(size);
259
+ },
260
+ target: 'bottom',
261
+ collapsible,
262
+ snapPoints,
263
+ snapTolerance: snapTolerance ?? 8,
264
+ collapseThreshold,
265
+ requestCollapse: () => shell.setBottomMode('collapsed'),
266
+ requestToggle: () => shell.togglePane('bottom'),
267
+ }}
268
+ >
269
+ {handleChildren.length > 0 ? handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i })) : <PaneHandle />}
270
+ </PaneResizeContext.Provider>
271
+ ) : null;
308
272
 
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
- }, []);
273
+ // Strip control/size props from DOM spread (moved above overlay return to keep hook order consistent)
274
+ // Normalize CSS lengths to px (moved above overlay return to keep hook order consistent)
275
+ const normalizeToPx = React.useCallback((value: number | string | undefined): number | undefined => {
276
+ if (value == null) return undefined;
277
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
278
+ const str = String(value).trim();
279
+ if (!str) return undefined;
280
+ if (str.endsWith('px')) return Number.parseFloat(str);
281
+ if (str.endsWith('rem')) {
282
+ const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
283
+ return Number.parseFloat(str) * rem;
284
+ }
285
+ if (str.endsWith('%')) {
286
+ const pct = Number.parseFloat(str);
287
+ const base = document.documentElement.clientHeight || window.innerHeight || 0;
288
+ return (pct / 100) * base;
289
+ }
290
+ const n = Number.parseFloat(str);
291
+ return Number.isFinite(n) ? n : undefined;
292
+ }, []);
324
293
 
325
- // Controlled size sync (moved above overlay return)
326
- const controlledSize = (props as any).size;
327
- React.useEffect(() => {
328
- if (!localRef.current) return;
329
- if (typeof controlledSize === 'undefined') return;
330
- const px = normalizeToPx(controlledSize);
294
+ // Apply defaultSize on mount when uncontrolled (moved above overlay return)
295
+ React.useEffect(() => {
296
+ if (!localRef.current) return;
297
+ if (typeof size === 'undefined' && typeof defaultSize !== 'undefined') {
298
+ const px = normalizeToPx(defaultSize);
331
299
  if (typeof px === 'number' && Number.isFinite(px)) {
332
300
  const minPx = typeof minSize === 'number' ? minSize : undefined;
333
301
  const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
334
302
  const clamped = Math.min(maxPx ?? px, Math.max(minPx ?? px, px));
335
303
  localRef.current.style.setProperty('--bottom-size', `${clamped}px`);
336
- emitSizeChange(clamped, { reason: 'controlled' });
304
+ emitSizeChange(clamped, { reason: 'init' });
337
305
  }
338
- }, [controlledSize, minSize, maxSize, normalizeToPx, emitSizeChange]);
306
+ }
307
+ // eslint-disable-next-line react-hooks/exhaustive-deps
308
+ }, []);
339
309
 
340
- if (isOverlay) {
341
- const open = shell.bottomMode === 'expanded';
342
- return (
343
- <Sheet.Root open={open} onOpenChange={(o) => shell.setBottomMode(o ? 'expanded' : 'collapsed')}>
344
- <Sheet.Content side="bottom" style={{ padding: 0 }} height={{ initial: `${expandedSize}px` }}>
345
- <VisuallyHidden>
346
- <Sheet.Title>Bottom panel</Sheet.Title>
347
- </VisuallyHidden>
348
- {contentChildren}
349
- </Sheet.Content>
350
- </Sheet.Root>
351
- );
310
+ // Controlled size sync (moved above overlay return)
311
+ const controlledSize = size;
312
+ React.useEffect(() => {
313
+ if (!localRef.current) return;
314
+ if (typeof controlledSize === 'undefined') return;
315
+ const px = normalizeToPx(controlledSize);
316
+ if (typeof px === 'number' && Number.isFinite(px)) {
317
+ const minPx = typeof minSize === 'number' ? minSize : undefined;
318
+ const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
319
+ const clamped = Math.min(maxPx ?? px, Math.max(minPx ?? px, px));
320
+ localRef.current.style.setProperty('--bottom-size', `${clamped}px`);
321
+ emitSizeChange(clamped, { reason: 'controlled' });
352
322
  }
323
+ }, [controlledSize, minSize, maxSize, normalizeToPx, emitSizeChange]);
353
324
 
325
+ if (isOverlay) {
326
+ const open = shell.bottomMode === 'expanded';
354
327
  return (
355
- <div
356
- {...bottomDomProps}
357
- ref={setRef}
358
- className={classNames('rt-ShellBottom', className)}
359
- data-mode={shell.bottomMode}
360
- data-peek={shell.peekTarget === 'bottom' || undefined}
361
- data-presentation={shell.currentBreakpointReady ? resolvedPresentation : undefined}
362
- data-open={(shell.currentBreakpointReady && isStacked && isExpanded) || undefined}
363
- style={{
364
- ...style,
365
- ['--bottom-size' as any]: `${expandedSize}px`,
366
- ['--bottom-min-size' as any]: `${minSize}px`,
367
- ['--bottom-max-size' as any]: `${maxSize}px`,
368
- }}
369
- >
370
- <div className="rt-ShellBottomContent" data-visible={isExpanded || undefined}>
328
+ <Sheet.Root open={open} onOpenChange={(o) => shell.setBottomMode(o ? 'expanded' : 'collapsed')}>
329
+ <Sheet.Content side="bottom" style={{ padding: 0 }} height={{ initial: `${expandedSize}px` }}>
330
+ <VisuallyHidden>
331
+ <Sheet.Title>Bottom panel</Sheet.Title>
332
+ </VisuallyHidden>
371
333
  {contentChildren}
372
- </div>
373
- {handleEl}
374
- </div>
334
+ </Sheet.Content>
335
+ </Sheet.Root>
375
336
  );
376
- },
377
- ) as BottomComponent;
337
+ }
338
+
339
+ return (
340
+ <div
341
+ {...bottomDomProps}
342
+ ref={setRef}
343
+ className={classNames('rt-ShellBottom', className)}
344
+ data-mode={shell.bottomMode}
345
+ data-peek={shell.peekTarget === 'bottom' || undefined}
346
+ data-presentation={shell.currentBreakpointReady ? resolvedPresentation : undefined}
347
+ data-open={(shell.currentBreakpointReady && isStacked && isExpanded) || undefined}
348
+ style={{
349
+ ...style,
350
+ ['--bottom-size' as any]: `${expandedSize}px`,
351
+ ['--bottom-min-size' as any]: `${minSize}px`,
352
+ ['--bottom-max-size' as any]: `${maxSize}px`,
353
+ }}
354
+ >
355
+ <div className="rt-ShellBottomContent" data-visible={isExpanded || undefined}>
356
+ {contentChildren}
357
+ </div>
358
+ {handleEl}
359
+ </div>
360
+ );
361
+ }) as BottomComponent;
378
362
  Bottom.displayName = 'Shell.Bottom';
379
363
  Bottom.Handle = BottomHandle;