@kushagradhawan/kookie-ui 0.1.71 → 0.1.73

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 (118) hide show
  1. package/README.md +4 -0
  2. package/components.css +69 -382
  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.map +1 -1
  7. package/dist/cjs/components/_internal/shell-bottom.js +1 -1
  8. package/dist/cjs/components/_internal/shell-bottom.js.map +3 -3
  9. package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
  10. package/dist/cjs/components/_internal/shell-inspector.js +1 -1
  11. package/dist/cjs/components/_internal/shell-inspector.js.map +3 -3
  12. package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
  13. package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
  14. package/dist/cjs/components/_internal/shell-sidebar.js.map +3 -3
  15. package/dist/cjs/components/button.d.ts.map +1 -1
  16. package/dist/cjs/components/button.js +1 -1
  17. package/dist/cjs/components/button.js.map +3 -3
  18. package/dist/cjs/components/chatbar.d.ts.map +1 -1
  19. package/dist/cjs/components/chatbar.js.map +2 -2
  20. package/dist/cjs/components/icon-button.d.ts.map +1 -1
  21. package/dist/cjs/components/icon-button.js +2 -2
  22. package/dist/cjs/components/icon-button.js.map +3 -3
  23. package/dist/cjs/components/shell.d.ts.map +1 -1
  24. package/dist/cjs/components/shell.js +1 -1
  25. package/dist/cjs/components/shell.js.map +3 -3
  26. package/dist/cjs/components/toggle-button.d.ts.map +1 -1
  27. package/dist/cjs/components/toggle-button.js +1 -1
  28. package/dist/cjs/components/toggle-button.js.map +3 -3
  29. package/dist/cjs/components/toggle-icon-button.d.ts.map +1 -1
  30. package/dist/cjs/components/toggle-icon-button.js +1 -1
  31. package/dist/cjs/components/toggle-icon-button.js.map +3 -3
  32. package/dist/cjs/hooks/index.d.ts +2 -0
  33. package/dist/cjs/hooks/index.d.ts.map +1 -1
  34. package/dist/cjs/hooks/index.js +1 -1
  35. package/dist/cjs/hooks/index.js.map +3 -3
  36. package/dist/cjs/hooks/use-live-announcer.d.ts.map +1 -1
  37. package/dist/cjs/hooks/use-live-announcer.js +2 -2
  38. package/dist/cjs/hooks/use-live-announcer.js.map +3 -3
  39. package/dist/cjs/hooks/use-toggle-state.d.ts +37 -0
  40. package/dist/cjs/hooks/use-toggle-state.d.ts.map +1 -0
  41. package/dist/cjs/hooks/use-toggle-state.js +2 -0
  42. package/dist/cjs/hooks/use-toggle-state.js.map +7 -0
  43. package/dist/cjs/hooks/use-tooltip-wrapper.d.ts +29 -0
  44. package/dist/cjs/hooks/use-tooltip-wrapper.d.ts.map +1 -0
  45. package/dist/cjs/hooks/use-tooltip-wrapper.js +2 -0
  46. package/dist/cjs/hooks/use-tooltip-wrapper.js.map +7 -0
  47. package/dist/esm/components/_internal/base-button.d.ts.map +1 -1
  48. package/dist/esm/components/_internal/base-button.js +1 -1
  49. package/dist/esm/components/_internal/base-button.js.map +3 -3
  50. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
  51. package/dist/esm/components/_internal/shell-bottom.js +1 -1
  52. package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
  53. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
  54. package/dist/esm/components/_internal/shell-inspector.js +1 -1
  55. package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
  56. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
  57. package/dist/esm/components/_internal/shell-sidebar.js +1 -1
  58. package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
  59. package/dist/esm/components/button.d.ts.map +1 -1
  60. package/dist/esm/components/button.js +1 -1
  61. package/dist/esm/components/button.js.map +3 -3
  62. package/dist/esm/components/chatbar.d.ts.map +1 -1
  63. package/dist/esm/components/chatbar.js.map +2 -2
  64. package/dist/esm/components/icon-button.d.ts.map +1 -1
  65. package/dist/esm/components/icon-button.js +2 -2
  66. package/dist/esm/components/icon-button.js.map +3 -3
  67. package/dist/esm/components/shell.d.ts.map +1 -1
  68. package/dist/esm/components/shell.js +1 -1
  69. package/dist/esm/components/shell.js.map +3 -3
  70. package/dist/esm/components/toggle-button.d.ts.map +1 -1
  71. package/dist/esm/components/toggle-button.js +1 -1
  72. package/dist/esm/components/toggle-button.js.map +3 -3
  73. package/dist/esm/components/toggle-icon-button.d.ts.map +1 -1
  74. package/dist/esm/components/toggle-icon-button.js +1 -1
  75. package/dist/esm/components/toggle-icon-button.js.map +3 -3
  76. package/dist/esm/hooks/index.d.ts +2 -0
  77. package/dist/esm/hooks/index.d.ts.map +1 -1
  78. package/dist/esm/hooks/index.js +1 -1
  79. package/dist/esm/hooks/index.js.map +3 -3
  80. package/dist/esm/hooks/use-live-announcer.d.ts.map +1 -1
  81. package/dist/esm/hooks/use-live-announcer.js +2 -2
  82. package/dist/esm/hooks/use-live-announcer.js.map +3 -3
  83. package/dist/esm/hooks/use-toggle-state.d.ts +37 -0
  84. package/dist/esm/hooks/use-toggle-state.d.ts.map +1 -0
  85. package/dist/esm/hooks/use-toggle-state.js +2 -0
  86. package/dist/esm/hooks/use-toggle-state.js.map +7 -0
  87. package/dist/esm/hooks/use-tooltip-wrapper.d.ts +29 -0
  88. package/dist/esm/hooks/use-tooltip-wrapper.d.ts.map +1 -0
  89. package/dist/esm/hooks/use-tooltip-wrapper.js +2 -0
  90. package/dist/esm/hooks/use-tooltip-wrapper.js.map +7 -0
  91. package/package.json +4 -4
  92. package/schemas/base-button.json +1 -1
  93. package/schemas/button.json +1 -1
  94. package/schemas/icon-button.json +1 -1
  95. package/schemas/index.json +6 -6
  96. package/schemas/toggle-button.json +1 -1
  97. package/schemas/toggle-icon-button.json +1 -1
  98. package/src/components/_internal/base-button.css +136 -614
  99. package/src/components/_internal/base-button.tsx +15 -13
  100. package/src/components/_internal/shell-bottom.tsx +31 -5
  101. package/src/components/_internal/shell-inspector.tsx +31 -5
  102. package/src/components/_internal/shell-sidebar.tsx +34 -6
  103. package/src/components/button.tsx +13 -42
  104. package/src/components/chatbar.tsx +1 -13
  105. package/src/components/icon-button.tsx +20 -44
  106. package/src/components/image.css +10 -8
  107. package/src/components/shell.css +10 -11
  108. package/src/components/shell.tsx +59 -11
  109. package/src/components/toggle-button.tsx +30 -59
  110. package/src/components/toggle-icon-button.tsx +29 -51
  111. package/src/hooks/index.ts +2 -0
  112. package/src/hooks/use-live-announcer.ts +34 -7
  113. package/src/hooks/use-toggle-state.ts +72 -0
  114. package/src/hooks/use-tooltip-wrapper.ts +28 -0
  115. package/src/styles/tokens/color.css +11 -1
  116. package/styles.css +76 -383
  117. package/tokens/base.css +7 -1
  118. package/tokens.css +7 -1
@@ -83,9 +83,7 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
83
83
  // This helps developers migrate to the new material prop
84
84
  React.useEffect(() => {
85
85
  if (props.panelBackground !== undefined) {
86
- console.warn(
87
- 'Warning: The `panelBackground` prop is deprecated and will be removed in a future version. Use `material` prop instead.',
88
- );
86
+ console.warn('Warning: The `panelBackground` prop is deprecated and will be removed in a future version. Use `material` prop instead.');
89
87
  }
90
88
  }, [props.panelBackground]);
91
89
 
@@ -96,6 +94,10 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
96
94
  // This prevents layout thrashing when using translucent materials
97
95
  const buttonRef = React.useRef<HTMLElement>(null);
98
96
 
97
+ // Use a ref to track current material value to avoid stale closures in setTimeout
98
+ const materialRef = React.useRef(effectiveMaterial);
99
+ materialRef.current = effectiveMaterial;
100
+
99
101
  React.useEffect(() => {
100
102
  const button = buttonRef.current;
101
103
  if (!button) return;
@@ -106,14 +108,17 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
106
108
  // Add will-change when material is translucent to optimize rendering
107
109
  button.style.setProperty('will-change', 'backdrop-filter');
108
110
 
111
+ // Track timeout for cleanup
112
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
113
+
109
114
  // Clean up will-change after transition completes to prevent memory leaks
110
115
  const cleanup = () => {
111
- const transitionDuration =
112
- getComputedStyle(button).getPropertyValue('--duration-2') || '75ms';
116
+ const transitionDuration = getComputedStyle(button).getPropertyValue('--duration-2') || '75ms';
113
117
  const duration = parseInt(transitionDuration) || 75;
114
118
 
115
- setTimeout(() => {
116
- if (button && effectiveMaterial !== 'translucent') {
119
+ timeoutId = setTimeout(() => {
120
+ // Use ref to get current value, not stale closure value
121
+ if (button && materialRef.current !== 'translucent') {
117
122
  button.style.setProperty('will-change', 'auto');
118
123
  }
119
124
  }, duration);
@@ -124,6 +129,7 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
124
129
  observer.observe(button, { attributes: true, attributeFilter: ['data-material'] });
125
130
 
126
131
  return () => {
132
+ if (timeoutId) clearTimeout(timeoutId);
127
133
  observer.disconnect();
128
134
  button.style.setProperty('will-change', 'auto');
129
135
  };
@@ -138,8 +144,7 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
138
144
 
139
145
  // Only pass disabled for elements that support it
140
146
  // This prevents invalid HTML attributes on unsupported elements
141
- const shouldPassDisabled =
142
- asChild || !as || ['button', 'input', 'textarea', 'select'].includes(as);
147
+ const shouldPassDisabled = asChild || !as || ['button', 'input', 'textarea', 'select'].includes(as);
143
148
 
144
149
  // Determine if we are rendering a real <button> element so we can set a safe
145
150
  // default type. Native <button> defaults to type="submit" which can cause
@@ -221,10 +226,7 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
221
226
  {/* Centered spinner overlay during loading state */}
222
227
  <Flex asChild align="center" justify="center" position="absolute" inset="0">
223
228
  <span>
224
- <Spinner
225
- size={mapResponsiveProp(size, mapButtonSizeToSpinnerSize)}
226
- aria-hidden="true"
227
- />
229
+ <Spinner size={mapResponsiveProp(size, mapButtonSizeToSpinnerSize)} aria-hidden="true" />
228
230
  </span>
229
231
  </Flex>
230
232
  </>
@@ -172,13 +172,39 @@ export const Bottom = React.forwardRef<HTMLDivElement, BottomPublicProps>((initi
172
172
  }
173
173
  }, [shell.bottomMode, open, defaultOpen, onOpenChange]);
174
174
 
175
+ // Track previous mode to only fire callbacks on actual user-initiated state transitions.
176
+ // We wait for breakpointReady to ensure the initial state sync from useResponsiveInitialState
177
+ // is complete before enabling callbacks. This avoids spurious callbacks during initialization.
178
+ const prevBottomModeRef = React.useRef<PaneMode | null>(null);
179
+ const hasInitializedRef = React.useRef(false);
175
180
  React.useEffect(() => {
176
- if (shell.bottomMode === 'expanded') {
177
- onExpand?.();
178
- } else {
179
- onCollapse?.();
181
+ const currentMode = shell.bottomMode;
182
+
183
+ // Wait for breakpoint to be ready before enabling callbacks
184
+ if (!shell.currentBreakpointReady) {
185
+ prevBottomModeRef.current = currentMode;
186
+ return;
187
+ }
188
+
189
+ // Skip the first run after breakpoint is ready - this captures the post-sync state
190
+ if (!hasInitializedRef.current) {
191
+ hasInitializedRef.current = true;
192
+ prevBottomModeRef.current = currentMode;
193
+ return;
194
+ }
195
+
196
+ const prevMode = prevBottomModeRef.current;
197
+
198
+ // Only fire on actual state transitions
199
+ if (prevMode !== null && prevMode !== currentMode) {
200
+ if (currentMode === 'expanded') {
201
+ onExpand?.();
202
+ } else if (currentMode === 'collapsed') {
203
+ onCollapse?.();
204
+ }
205
+ prevBottomModeRef.current = currentMode;
180
206
  }
181
- }, [shell.bottomMode, onExpand, onCollapse]);
207
+ }, [shell.bottomMode, shell.currentBreakpointReady, onExpand, onCollapse]);
182
208
 
183
209
  const isExpanded = shell.bottomMode === 'expanded';
184
210
 
@@ -173,13 +173,39 @@ export const Inspector = React.forwardRef<HTMLDivElement, InspectorPublicProps>(
173
173
  }
174
174
  }, [shell.inspectorMode, open, defaultOpen, onOpenChange]);
175
175
 
176
+ // Track previous mode to only fire callbacks on actual user-initiated state transitions.
177
+ // We wait for breakpointReady to ensure the initial state sync from useResponsiveInitialState
178
+ // is complete before enabling callbacks. This avoids spurious callbacks during initialization.
179
+ const prevInspectorModeRef = React.useRef<PaneMode | null>(null);
180
+ const hasInitializedRef = React.useRef(false);
176
181
  React.useEffect(() => {
177
- if (shell.inspectorMode === 'expanded') {
178
- onExpand?.();
179
- } else {
180
- onCollapse?.();
182
+ const currentMode = shell.inspectorMode;
183
+
184
+ // Wait for breakpoint to be ready before enabling callbacks
185
+ if (!shell.currentBreakpointReady) {
186
+ prevInspectorModeRef.current = currentMode;
187
+ return;
188
+ }
189
+
190
+ // Skip the first run after breakpoint is ready - this captures the post-sync state
191
+ if (!hasInitializedRef.current) {
192
+ hasInitializedRef.current = true;
193
+ prevInspectorModeRef.current = currentMode;
194
+ return;
195
+ }
196
+
197
+ const prevMode = prevInspectorModeRef.current;
198
+
199
+ // Only fire on actual state transitions
200
+ if (prevMode !== null && prevMode !== currentMode) {
201
+ if (currentMode === 'expanded') {
202
+ onExpand?.();
203
+ } else if (currentMode === 'collapsed') {
204
+ onCollapse?.();
205
+ }
206
+ prevInspectorModeRef.current = currentMode;
181
207
  }
182
- }, [shell.inspectorMode, onExpand, onCollapse]);
208
+ }, [shell.inspectorMode, shell.currentBreakpointReady, onExpand, onCollapse]);
183
209
 
184
210
  const isExpanded = shell.inspectorMode === 'expanded';
185
211
 
@@ -188,14 +188,42 @@ export const Sidebar = React.forwardRef<HTMLDivElement, SidebarPublicProps>((ini
188
188
  }
189
189
  }, [shell.sidebarMode, state, onStateChange]);
190
190
 
191
- // Emit expand/collapse events
191
+ // Track previous mode to only fire callbacks on actual user-initiated state transitions.
192
+ // We wait for breakpointReady to ensure the initial state sync from useResponsiveInitialState
193
+ // is complete before enabling callbacks. This avoids spurious callbacks during initialization.
194
+ const prevSidebarModeRef = React.useRef<SidebarMode | null>(null);
195
+ const hasInitializedRef = React.useRef(false);
192
196
  React.useEffect(() => {
193
- if (shell.sidebarMode === 'expanded') {
194
- onExpand?.();
195
- } else {
196
- onCollapse?.();
197
+ const currentMode = shell.sidebarMode as SidebarMode;
198
+
199
+ // Wait for breakpoint to be ready before enabling callbacks
200
+ if (!shell.currentBreakpointReady) {
201
+ prevSidebarModeRef.current = currentMode;
202
+ return;
203
+ }
204
+
205
+ // Skip the first run after breakpoint is ready - this captures the post-sync state
206
+ if (!hasInitializedRef.current) {
207
+ hasInitializedRef.current = true;
208
+ prevSidebarModeRef.current = currentMode;
209
+ return;
210
+ }
211
+
212
+ const prevMode = prevSidebarModeRef.current;
213
+
214
+ // Only fire on actual state transitions
215
+ if (prevMode !== null && prevMode !== currentMode) {
216
+ // onExpand: when becoming visible (collapsed → thin/expanded)
217
+ if (prevMode === 'collapsed' && currentMode !== 'collapsed') {
218
+ onExpand?.();
219
+ }
220
+ // onCollapse: when becoming hidden (any → collapsed)
221
+ else if (currentMode === 'collapsed') {
222
+ onCollapse?.();
223
+ }
224
+ prevSidebarModeRef.current = currentMode;
197
225
  }
198
- }, [shell.sidebarMode, onExpand, onCollapse]);
226
+ }, [shell.sidebarMode, shell.currentBreakpointReady, onExpand, onCollapse]);
199
227
 
200
228
  // Option A: thin is width-only; content remains visible whenever not collapsed
201
229
  const isContentVisible = shell.sidebarMode !== 'collapsed';
@@ -3,6 +3,7 @@ import classNames from 'classnames';
3
3
 
4
4
  import { BaseButton } from './_internal/base-button.js';
5
5
  import { Tooltip } from './tooltip.js';
6
+ import { useTooltipWrapper } from '../hooks/use-tooltip-wrapper.js';
6
7
  import type { BaseButtonProps } from './_internal/base-button.js';
7
8
 
8
9
  /**
@@ -84,9 +85,7 @@ type ButtonProps<C extends React.ElementType = 'button'> = ButtonOwnProps & {
84
85
  * Button component type that supports polymorphic rendering
85
86
  * @template C - The element type to render as
86
87
  */
87
- type ButtonComponent = <C extends React.ElementType = 'button'>(
88
- props: ButtonProps<C> & { ref?: React.ForwardedRef<ButtonElement> },
89
- ) => React.ReactElement | null;
88
+ type ButtonComponent = <C extends React.ElementType = 'button'>(props: ButtonProps<C> & { ref?: React.ForwardedRef<ButtonElement> }) => React.ReactElement | null;
90
89
 
91
90
  /**
92
91
  * Button component for triggering actions throughout your interface
@@ -113,27 +112,11 @@ type ButtonComponent = <C extends React.ElementType = 'button'>(
113
112
  */
114
113
  const Button = React.forwardRef(
115
114
  (
116
- {
117
- className,
118
- tooltip,
119
- tooltipSide = 'top',
120
- tooltipAlign = 'center',
121
- tooltipDelayDuration,
122
- tooltipDisableHoverableContent,
123
- overrideStyles,
124
- ...props
125
- }: ButtonProps,
115
+ { className, style, tooltip, tooltipSide = 'top', tooltipAlign = 'center', tooltipDelayDuration, tooltipDisableHoverableContent, overrideStyles, ...props }: ButtonProps,
126
116
  forwardedRef: React.ForwardedRef<ButtonElement>,
127
117
  ) => {
128
- // Generate unique ID for tooltip accessibility
129
- const tooltipId = React.useId();
130
- const hasTooltip = Boolean(tooltip);
131
-
132
- // Prepare accessibility props for tooltip integration
133
- const tooltipAccessibilityProps = React.useMemo(
134
- () => (hasTooltip ? { 'aria-describedby': tooltipId } : {}),
135
- [hasTooltip, tooltipId],
136
- );
118
+ // Use shared tooltip wrapper hook for accessibility props
119
+ const { tooltipId, hasTooltip, accessibilityProps: tooltipAccessibilityProps } = useTooltipWrapper(tooltip);
137
120
 
138
121
  // Create the base button element with tooltip accessibility props
139
122
  // Map overrideStyles to CSS variables consumed by the override variant rules
@@ -152,7 +135,7 @@ const Button = React.forwardRef(
152
135
  setVar(`--button-override-${prefix}filter`, s.filter);
153
136
  setVar(`--button-override-${prefix}outline`, s.outline);
154
137
  setVar(`--button-override-${prefix}outline-offset`, s.outlineOffset);
155
- setVar(`--button-override-${prefix}opacity`, s.opacity as any);
138
+ setVar(`--button-override-${prefix}opacity`, s.opacity);
156
139
  };
157
140
 
158
141
  apply('', overrideStyles.normal);
@@ -167,35 +150,23 @@ const Button = React.forwardRef(
167
150
  setVar('--button-override-focus-outline-offset', overrideStyles.focus.outlineOffset);
168
151
  }
169
152
 
170
- return vars as unknown as React.CSSProperties;
153
+ return vars as React.CSSProperties;
171
154
  }, [overrideStyles]);
172
155
 
173
- const button = (
174
- <BaseButton
175
- {...props}
176
- {...tooltipAccessibilityProps}
177
- ref={forwardedRef}
178
- className={classNames('rt-Button', className)}
179
- style={overrideVars ? { ...overrideVars, ...(props as any).style } : (props as any).style}
180
- />
181
- );
156
+ // Combine override styles with user-provided styles
157
+ const combinedStyle = React.useMemo(() => (overrideVars ? { ...overrideVars, ...style } : style), [overrideVars, style]);
158
+
159
+ const button = <BaseButton {...props} {...tooltipAccessibilityProps} ref={forwardedRef} className={classNames('rt-Button', className)} style={combinedStyle} />;
182
160
 
183
161
  // If no tooltip is provided, return the button as-is for better performance
184
- if (!tooltip) {
162
+ if (!hasTooltip) {
185
163
  return button;
186
164
  }
187
165
 
188
166
  // Wrap with Tooltip when tooltip content is provided
189
167
  // This creates a compound component that handles both button and tooltip functionality
190
168
  return (
191
- <Tooltip
192
- content={tooltip}
193
- side={tooltipSide}
194
- align={tooltipAlign}
195
- delayDuration={tooltipDelayDuration}
196
- disableHoverableContent={tooltipDisableHoverableContent}
197
- id={tooltipId}
198
- >
169
+ <Tooltip content={tooltip} side={tooltipSide} align={tooltipAlign} delayDuration={tooltipDelayDuration} disableHoverableContent={tooltipDisableHoverableContent} id={tooltipId}>
199
170
  {button}
200
171
  </Tooltip>
201
172
  );
@@ -1130,19 +1130,7 @@ type SendProps = Omit<IconButtonProps, 'aria-label' | 'aria-labelledby'> & {
1130
1130
  };
1131
1131
 
1132
1132
  const Send = React.forwardRef<HTMLButtonElement, SendProps>((props, forwardedRef) => {
1133
- const {
1134
- asChild,
1135
- clearOnSend = true,
1136
- disabled,
1137
- children,
1138
- className,
1139
- style,
1140
- size: sizeProp,
1141
- variant: variantProp,
1142
- 'aria-label': ariaLabel,
1143
- 'aria-labelledby': ariaLabelledby,
1144
- ...buttonProps
1145
- } = props;
1133
+ const { asChild, clearOnSend = true, disabled, children, className, style, size: sizeProp, variant: variantProp, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...buttonProps } = props;
1146
1134
  const ctx = useChatbarContext();
1147
1135
 
1148
1136
  const trimmed = ctx.value.trim();
@@ -3,6 +3,7 @@ import classNames from 'classnames';
3
3
 
4
4
  import { BaseButton } from './_internal/base-button.js';
5
5
  import { Tooltip } from './tooltip.js';
6
+ import { useTooltipWrapper } from '../hooks/use-tooltip-wrapper.js';
6
7
  import type { BaseButtonProps } from './_internal/base-button.js';
7
8
 
8
9
  type IconButtonElement = React.ElementRef<typeof BaseButton>;
@@ -57,9 +58,7 @@ type IconButtonProps<C extends React.ElementType = 'button'> = IconButtonOwnProp
57
58
  * IconButton component type that supports polymorphic rendering
58
59
  * @template C - The element type to render as
59
60
  */
60
- type IconButtonComponent = <C extends React.ElementType = 'button'>(
61
- props: IconButtonProps<C> & { ref?: React.ForwardedRef<IconButtonElement> },
62
- ) => React.ReactElement | null;
61
+ type IconButtonComponent = <C extends React.ElementType = 'button'>(props: IconButtonProps<C> & { ref?: React.ForwardedRef<IconButtonElement> }) => React.ReactElement | null;
63
62
 
64
63
  /**
65
64
  * IconButton component for compact, accessible icon-only interactions
@@ -103,68 +102,45 @@ type IconButtonComponent = <C extends React.ElementType = 'button'>(
103
102
  */
104
103
  const IconButton = React.forwardRef(
105
104
  (
106
- {
107
- className,
108
- tooltip,
109
- tooltipSide = 'top',
110
- tooltipAlign = 'center',
111
- tooltipDelayDuration,
112
- tooltipDisableHoverableContent,
113
- ...props
114
- }: IconButtonProps,
105
+ { className, tooltip, tooltipSide = 'top', tooltipAlign = 'center', tooltipDelayDuration, tooltipDisableHoverableContent, ...props }: IconButtonProps,
115
106
  forwardedRef: React.ForwardedRef<IconButtonElement>,
116
107
  ) => {
117
- // Generate unique ID for tooltip accessibility
118
- const tooltipId = React.useId();
108
+ // Use shared tooltip wrapper hook for accessibility props
109
+ const { tooltipId, hasTooltip, accessibilityProps: tooltipAccessibilityProps } = useTooltipWrapper(tooltip);
110
+
119
111
  // Runtime accessibility validation to ensure WCAG compliance
120
112
  // This helps catch accessibility issues during development
121
113
  const hasAriaLabel = 'aria-label' in props && props['aria-label'];
122
114
  const hasAriaLabelledBy = 'aria-labelledby' in props && props['aria-labelledby'];
123
115
  const hasChildren = 'children' in props && props.children;
124
116
 
125
- // Throw descriptive error if no accessible name is provided
117
+ // Validate accessible name - throw in development, log error in production
126
118
  if (!hasAriaLabel && !hasAriaLabelledBy && !hasChildren) {
127
- throw new Error(
119
+ const errorMessage =
128
120
  'IconButton: Icon buttons must have an accessible name. Please provide either:' +
129
- '\n- aria-label prop with descriptive text' +
130
- '\n- aria-labelledby prop referencing a label element' +
131
- '\n- or visible text children',
132
- );
133
- }
121
+ '\n- aria-label prop with descriptive text' +
122
+ '\n- aria-labelledby prop referencing a label element' +
123
+ '\n- or visible text children';
134
124
 
135
- // Prepare accessibility props for tooltip integration
136
- const hasTooltip = Boolean(tooltip);
137
- const tooltipAccessibilityProps = React.useMemo(
138
- () => (hasTooltip ? { 'aria-describedby': tooltipId } : {}),
139
- [hasTooltip, tooltipId],
140
- );
125
+ if (process.env.NODE_ENV === 'development') {
126
+ throw new Error(errorMessage);
127
+ } else {
128
+ console.error(errorMessage);
129
+ }
130
+ }
141
131
 
142
132
  // Create the base icon button element with accessibility props
143
- const iconButton = (
144
- <BaseButton
145
- {...props}
146
- {...tooltipAccessibilityProps}
147
- ref={forwardedRef}
148
- className={classNames('rt-IconButton', className)}
149
- />
150
- );
133
+ const iconButton = <BaseButton {...props} {...tooltipAccessibilityProps} ref={forwardedRef} className={classNames('rt-IconButton', className)} />;
151
134
 
152
135
  // If no tooltip is provided, return the icon button as-is for better performance
153
- if (!tooltip) {
136
+ if (!hasTooltip) {
154
137
  return iconButton;
155
138
  }
156
139
 
157
140
  // Wrap with Tooltip when tooltip content is provided
158
141
  // This creates a compound component that handles both button and tooltip functionality
159
142
  return (
160
- <Tooltip
161
- content={tooltip}
162
- side={tooltipSide}
163
- align={tooltipAlign}
164
- delayDuration={tooltipDelayDuration}
165
- disableHoverableContent={tooltipDisableHoverableContent}
166
- id={tooltipId}
167
- >
143
+ <Tooltip content={tooltip} side={tooltipSide} align={tooltipAlign} delayDuration={tooltipDelayDuration} disableHoverableContent={tooltipDisableHoverableContent} id={tooltipId}>
168
144
  {iconButton}
169
145
  </Tooltip>
170
146
  );
@@ -63,7 +63,7 @@
63
63
  .rt-Image {
64
64
  border: 1px solid CanvasText;
65
65
  }
66
-
66
+
67
67
  /* Enhanced focus visibility in forced colors mode */
68
68
  .rt-Image:where(:focus-visible) {
69
69
  outline: 2px solid Highlight;
@@ -78,7 +78,9 @@
78
78
  */
79
79
  .rt-Image:where(:any-link, button, label) {
80
80
  cursor: pointer;
81
- transition: var(--transition-card), filter var(--motion-duration-small) var(--motion-ease-standard); /* Smooth transitions for interactive states */
81
+ transition:
82
+ var(--transition-card),
83
+ filter var(--motion-duration-small) var(--motion-ease-standard); /* Smooth transitions for interactive states */
82
84
 
83
85
  /*
84
86
  * Hover effects with progressive enhancement
@@ -87,7 +89,7 @@
87
89
  @media (hover: hover) {
88
90
  &:where(:hover) {
89
91
  box-shadow: var(--shadow-3); /* Subtle elevation on hover */
90
- filter: brightness(1.05) contrast(1.02); /* Slight brightness/contrast boost */
92
+ filter: brightness(1.08) contrast(1.02); /* Slight brightness/contrast boost */
91
93
  }
92
94
  }
93
95
 
@@ -116,13 +118,15 @@
116
118
  */
117
119
  :where(:any-link, button, label) {
118
120
  cursor: pointer;
119
- transition: var(--transition-card), filter var(--motion-duration-small) var(--motion-ease-standard); /* Smooth transitions for interactive states */
121
+ transition:
122
+ var(--transition-card),
123
+ filter var(--motion-duration-small) var(--motion-ease-standard); /* Smooth transitions for interactive states */
120
124
 
121
125
  /* Hover effects for wrapper elements */
122
126
  @media (hover: hover) {
123
127
  &:where(:hover) {
124
128
  /* box-shadow: var(--shadow-3); */
125
- filter: brightness(1.05) contrast(1.02);
129
+ filter: brightness(1.08) contrast(1.02);
126
130
  }
127
131
  }
128
132
 
@@ -141,8 +145,6 @@
141
145
  outline-offset: -2px;
142
146
  }
143
147
 
144
-
145
-
146
148
  /*
147
149
  * Object-fit variants for responsive image scaling
148
150
  * These classes control how images are resized to fit their containers
@@ -217,7 +219,7 @@
217
219
  color: var(--gray-11); /* Subtle but readable color */
218
220
  margin-top: var(--space-2); /* Consistent spacing from image */
219
221
  text-align: center; /* Centered alignment for visual balance */
220
-
222
+
221
223
  /*
222
224
  * Text wrapping and hyphenation for better layout
223
225
  * Prevents caption text from breaking the layout
@@ -189,17 +189,16 @@
189
189
  width: var(--sidebar-thin-size, 64px);
190
190
  }
191
191
 
192
- .rt-ShellSidebar[data-mode='collapsed'] {
193
- width: 0px;
194
- /* Delay container collapse until content fade completes */
195
- transition-delay: var(--motion-duration-small);
196
- }
197
-
198
192
  /* Keep collapsed sidebar out of flow to avoid layout blips when exiting peek */
199
193
  .rt-ShellSidebar[data-mode='collapsed'] {
194
+ width: 0px;
200
195
  position: absolute;
201
196
  inset-block: 0;
202
197
  inset-inline-start: 0;
198
+ flex-shrink: 0;
199
+ flex-basis: 0;
200
+ /* Delay container collapse until content fade completes */
201
+ transition-delay: var(--motion-duration-small);
203
202
  }
204
203
 
205
204
  .rt-ShellSidebarContent {
@@ -293,16 +292,16 @@
293
292
  width: var(--inspector-size, 320px);
294
293
  }
295
294
 
295
+ /* Keep collapsed inspector out of flow to avoid layout issues */
296
296
  .rt-ShellInspector[data-mode='collapsed'] {
297
297
  width: 0px;
298
- /* Delay container collapse until content fade completes */
299
- transition-delay: var(--motion-duration-small);
300
- }
301
-
302
- .rt-ShellInspector[data-mode='collapsed'] {
303
298
  position: absolute;
304
299
  inset-block: 0;
305
300
  inset-inline-end: 0;
301
+ flex-shrink: 0;
302
+ flex-basis: 0;
303
+ /* Delay container collapse until content fade completes */
304
+ transition-delay: var(--motion-duration-small);
306
305
  }
307
306
 
308
307
  .rt-ShellInspectorContent {
@@ -265,13 +265,57 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
265
265
  (el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Inspector' && typeof (el as any).props?.open !== 'undefined' && Boolean((el as any).props?.open),
266
266
  );
267
267
 
268
+ // Detect Sidebar initial state from props
269
+ const getSidebarInitialState = (): SidebarMode => {
270
+ const sidebarEl = initialChildren.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Sidebar');
271
+ if (!sidebarEl) return 'expanded';
272
+ const props = (sidebarEl as any).props;
273
+ // Check controlled state first
274
+ if (typeof props?.state !== 'undefined') {
275
+ if (typeof props.state === 'string') return props.state as SidebarMode;
276
+ // Responsive object - use 'initial' breakpoint or first defined value
277
+ if (typeof props.state === 'object') {
278
+ return (props.state.initial ?? Object.values(props.state)[0] ?? 'expanded') as SidebarMode;
279
+ }
280
+ }
281
+ // Check defaultState
282
+ if (typeof props?.defaultState !== 'undefined') {
283
+ if (typeof props.defaultState === 'string') return props.defaultState as SidebarMode;
284
+ if (typeof props.defaultState === 'object') {
285
+ return (props.defaultState.initial ?? Object.values(props.defaultState)[0] ?? 'expanded') as SidebarMode;
286
+ }
287
+ }
288
+ return 'expanded';
289
+ };
290
+
291
+ // Detect Bottom initial state from props
292
+ const getBottomInitialState = (): PaneMode => {
293
+ const bottomEl = initialChildren.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Bottom');
294
+ if (!bottomEl) return 'collapsed';
295
+ const props = (bottomEl as any).props;
296
+ // Check controlled open first
297
+ if (typeof props?.open !== 'undefined') {
298
+ if (typeof props.open === 'boolean') return props.open ? 'expanded' : 'collapsed';
299
+ // Responsive object - use 'initial' breakpoint or first defined value
300
+ if (typeof props.open === 'object') {
301
+ const val = props.open.initial ?? Object.values(props.open)[0];
302
+ return val ? 'expanded' : 'collapsed';
303
+ }
304
+ }
305
+ // Check defaultOpen
306
+ if (typeof props?.defaultOpen !== 'undefined') {
307
+ return props.defaultOpen ? 'expanded' : 'collapsed';
308
+ }
309
+ return 'collapsed';
310
+ };
311
+
268
312
  // Pane state management via reducer
269
313
  const [paneState, dispatchPane] = React.useReducer(paneReducer, {
270
314
  leftMode: hasPanelDefaultOpen || hasRailDefaultOpen ? 'expanded' : 'collapsed',
271
315
  panelMode: hasPanelDefaultOpen ? 'expanded' : 'collapsed',
272
- sidebarMode: 'expanded',
316
+ sidebarMode: getSidebarInitialState(),
273
317
  inspectorMode: hasInspectorDefaultOpen || hasInspectorOpenControlled ? 'expanded' : 'collapsed',
274
- bottomMode: 'collapsed',
318
+ bottomMode: getBottomInitialState(),
275
319
  });
276
320
  const setLeftMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_LEFT_MODE', mode }), []);
277
321
  const setPanelMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_PANEL_MODE', mode }), []);
@@ -450,17 +494,21 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
450
494
  const peekCtxValue = React.useMemo(() => ({ peekTarget, setPeekTarget, peekPane, clearPeek }), [peekTarget, setPeekTarget, peekPane, clearPeek]);
451
495
  const actionsCtxValue = React.useMemo(() => ({ togglePane, expandPane, collapsePane, setSidebarToggleComputer }), [togglePane, expandPane, collapsePane, setSidebarToggleComputer]);
452
496
 
497
+ // Memoized full context value for ShellProvider to prevent unnecessary effect re-runs
498
+ const shellContextValue = React.useMemo(
499
+ () => ({
500
+ ...baseContextValue,
501
+ peekTarget,
502
+ setPeekTarget,
503
+ peekPane,
504
+ clearPeek,
505
+ }),
506
+ [baseContextValue, peekTarget, setPeekTarget, peekPane, clearPeek],
507
+ );
508
+
453
509
  return (
454
510
  <div {...props} ref={ref} className={classNames('rt-ShellRoot', className)} style={{ ...heightStyle, ...props.style }}>
455
- <ShellProvider
456
- value={{
457
- ...baseContextValue,
458
- peekTarget,
459
- setPeekTarget,
460
- peekPane,
461
- clearPeek,
462
- }}
463
- >
511
+ <ShellProvider value={shellContextValue}>
464
512
  <PresentationContext.Provider value={presentationCtxValue}>
465
513
  <LeftModeContext.Provider value={leftModeCtxValue}>
466
514
  <PanelModeContext.Provider value={panelModeCtxValue}>