@kushagradhawan/kookie-ui 0.1.123 → 0.1.125

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 (66) hide show
  1. package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -1
  2. package/dist/cjs/components/_internal/shell-bottom.js +1 -1
  3. package/dist/cjs/components/_internal/shell-bottom.js.map +2 -2
  4. package/dist/cjs/components/_internal/shell-handles.js +1 -1
  5. package/dist/cjs/components/_internal/shell-handles.js.map +2 -2
  6. package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
  7. package/dist/cjs/components/_internal/shell-inspector.js +1 -1
  8. package/dist/cjs/components/_internal/shell-inspector.js.map +2 -2
  9. package/dist/cjs/components/_internal/shell-prop-helpers.js.map +2 -2
  10. package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
  11. package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
  12. package/dist/cjs/components/_internal/shell-sidebar.js.map +2 -2
  13. package/dist/cjs/components/combobox.d.ts.map +1 -1
  14. package/dist/cjs/components/combobox.js +1 -1
  15. package/dist/cjs/components/combobox.js.map +3 -3
  16. package/dist/cjs/components/shell.context.d.ts +2 -0
  17. package/dist/cjs/components/shell.context.d.ts.map +1 -1
  18. package/dist/cjs/components/shell.context.js +1 -1
  19. package/dist/cjs/components/shell.context.js.map +2 -2
  20. package/dist/cjs/components/shell.d.ts.map +1 -1
  21. package/dist/cjs/components/shell.js +1 -1
  22. package/dist/cjs/components/shell.js.map +3 -3
  23. package/dist/cjs/components/shell.types.d.ts +11 -0
  24. package/dist/cjs/components/shell.types.d.ts.map +1 -1
  25. package/dist/cjs/components/shell.types.js +1 -1
  26. package/dist/cjs/components/shell.types.js.map +1 -1
  27. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
  28. package/dist/esm/components/_internal/shell-bottom.js +1 -1
  29. package/dist/esm/components/_internal/shell-bottom.js.map +2 -2
  30. package/dist/esm/components/_internal/shell-handles.js +1 -1
  31. package/dist/esm/components/_internal/shell-handles.js.map +2 -2
  32. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
  33. package/dist/esm/components/_internal/shell-inspector.js +1 -1
  34. package/dist/esm/components/_internal/shell-inspector.js.map +2 -2
  35. package/dist/esm/components/_internal/shell-prop-helpers.js.map +2 -2
  36. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
  37. package/dist/esm/components/_internal/shell-sidebar.js +1 -1
  38. package/dist/esm/components/_internal/shell-sidebar.js.map +2 -2
  39. package/dist/esm/components/combobox.d.ts.map +1 -1
  40. package/dist/esm/components/combobox.js +1 -1
  41. package/dist/esm/components/combobox.js.map +3 -3
  42. package/dist/esm/components/shell.context.d.ts +2 -0
  43. package/dist/esm/components/shell.context.d.ts.map +1 -1
  44. package/dist/esm/components/shell.context.js.map +2 -2
  45. package/dist/esm/components/shell.d.ts.map +1 -1
  46. package/dist/esm/components/shell.js +1 -1
  47. package/dist/esm/components/shell.js.map +3 -3
  48. package/dist/esm/components/shell.types.d.ts +11 -0
  49. package/dist/esm/components/shell.types.d.ts.map +1 -1
  50. package/dist/esm/components/shell.types.js.map +1 -1
  51. package/package.json +1 -1
  52. package/schemas/base-button.json +1 -1
  53. package/schemas/button.json +1 -1
  54. package/schemas/icon-button.json +1 -1
  55. package/schemas/index.json +6 -6
  56. package/schemas/toggle-button.json +1 -1
  57. package/schemas/toggle-icon-button.json +1 -1
  58. package/src/components/_internal/shell-bottom.tsx +5 -5
  59. package/src/components/_internal/shell-handles.tsx +26 -26
  60. package/src/components/_internal/shell-inspector.tsx +5 -5
  61. package/src/components/_internal/shell-prop-helpers.ts +1 -1
  62. package/src/components/_internal/shell-sidebar.tsx +10 -10
  63. package/src/components/combobox.tsx +176 -80
  64. package/src/components/shell.context.tsx +12 -10
  65. package/src/components/shell.tsx +21 -20
  66. package/src/components/shell.types.ts +13 -0
@@ -27,6 +27,13 @@ import type { MarginProps } from '../props/margin.props.js';
27
27
  import type { ComponentPropsWithout, RemovedProps } from '../helpers/component-props.js';
28
28
  import type { GetPropDefTypes } from '../props/prop-def.js';
29
29
 
30
+ /**
31
+ * Pre-compiled regex for className sanitization.
32
+ * Matches size classes like "rt-r-size-1", "rt-r-size-2", etc.
33
+ * Pre-compiling avoids regex compilation on every render.
34
+ */
35
+ const SIZE_CLASS_REGEX = /^rt-r-size-\d$/;
36
+
30
37
  type TextFieldVariant = (typeof textFieldRootPropDefs.variant.values)[number];
31
38
  type ComboboxValue = string | null;
32
39
  /**
@@ -97,15 +104,33 @@ type ComboboxRootOwnProps = GetPropDefTypes<typeof comboboxRootPropDefs> & {
97
104
  };
98
105
 
99
106
  /**
100
- * Internal context shared by all sub-components to avoid prop drilling.
107
+ * Split contexts to minimize re-renders. Each context changes independently:
108
+ * - ConfigContext: Static config that rarely changes (size, color, placeholders, etc.)
109
+ * - SelectionContext: Changes when user selects an item
110
+ * - SearchContext: Changes on every keystroke in the search input
111
+ * - NavigationContext: Changes during keyboard navigation
101
112
  */
102
- interface ComboboxContextValue extends ComboboxRootOwnProps {
113
+
114
+ /** Static configuration - rarely changes after mount */
115
+ interface ComboboxConfigContextValue {
116
+ size?: ComboboxRootOwnProps['size'];
117
+ highContrast?: boolean;
118
+ placeholder?: string;
119
+ searchPlaceholder?: string;
120
+ filter?: CommandFilter;
121
+ shouldFilter?: boolean;
122
+ loop?: boolean;
123
+ disabled?: boolean;
124
+ resetSearchOnSelect?: boolean;
125
+ color?: ComboboxRootOwnProps['color'];
126
+ listboxId: string;
127
+ }
128
+
129
+ /** Selection state - changes when user picks an option */
130
+ interface ComboboxSelectionContextValue {
103
131
  open: boolean;
104
132
  setOpen: (open: boolean) => void;
105
133
  value: ComboboxValue;
106
- setValue: (value: ComboboxValue) => void;
107
- searchValue: string;
108
- setSearchValue: (value: string) => void;
109
134
  /** Label registered by the selected item */
110
135
  selectedLabel?: string;
111
136
  /** Resolved display value (already computed from string or function) */
@@ -113,24 +138,73 @@ interface ComboboxContextValue extends ComboboxRootOwnProps {
113
138
  registerItemLabel: (value: string, label: string) => void;
114
139
  unregisterItemLabel: (value: string) => void;
115
140
  handleSelect: (value: string) => void;
116
- listboxId: string;
141
+ }
142
+
143
+ /** Search state - changes on every keystroke */
144
+ interface ComboboxSearchContextValue {
145
+ searchValue: string;
146
+ setSearchValue: (value: string) => void;
147
+ }
148
+
149
+ /** Navigation state - changes during keyboard navigation */
150
+ interface ComboboxNavigationContextValue {
117
151
  activeDescendantId: string | undefined;
118
152
  setActiveDescendantId: (id: string | undefined) => void;
119
153
  }
120
154
 
121
- const ComboboxContext = React.createContext<ComboboxContextValue | null>(null);
155
+ const ComboboxConfigContext = React.createContext<ComboboxConfigContextValue | null>(null);
156
+ const ComboboxSelectionContext = React.createContext<ComboboxSelectionContextValue | null>(null);
157
+ const ComboboxSearchContext = React.createContext<ComboboxSearchContextValue | null>(null);
158
+ const ComboboxNavigationContext = React.createContext<ComboboxNavigationContextValue | null>(null);
122
159
 
123
160
  /**
124
- * Utility hook that ensures consumers are wrapped in Combobox.Root.
161
+ * Utility hooks that ensure consumers are wrapped in Combobox.Root.
162
+ * Components should use only the contexts they need to minimize re-renders.
125
163
  */
126
- const useComboboxContext = (caller: string) => {
127
- const ctx = React.useContext(ComboboxContext);
164
+ const useComboboxConfigContext = (caller: string) => {
165
+ const ctx = React.useContext(ComboboxConfigContext);
166
+ if (!ctx) {
167
+ throw new Error(`${caller} must be used within Combobox.Root`);
168
+ }
169
+ return ctx;
170
+ };
171
+
172
+ const useComboboxSelectionContext = (caller: string) => {
173
+ const ctx = React.useContext(ComboboxSelectionContext);
174
+ if (!ctx) {
175
+ throw new Error(`${caller} must be used within Combobox.Root`);
176
+ }
177
+ return ctx;
178
+ };
179
+
180
+ const useComboboxSearchContext = (caller: string) => {
181
+ const ctx = React.useContext(ComboboxSearchContext);
182
+ if (!ctx) {
183
+ throw new Error(`${caller} must be used within Combobox.Root`);
184
+ }
185
+ return ctx;
186
+ };
187
+
188
+ const useComboboxNavigationContext = (caller: string) => {
189
+ const ctx = React.useContext(ComboboxNavigationContext);
128
190
  if (!ctx) {
129
191
  throw new Error(`${caller} must be used within Combobox.Root`);
130
192
  }
131
193
  return ctx;
132
194
  };
133
195
 
196
+ /**
197
+ * Combined context hook for components that need multiple contexts.
198
+ * Use sparingly - prefer individual context hooks when possible.
199
+ */
200
+ const useComboboxContext = (caller: string) => {
201
+ const config = useComboboxConfigContext(caller);
202
+ const selection = useComboboxSelectionContext(caller);
203
+ const search = useComboboxSearchContext(caller);
204
+ const navigation = useComboboxNavigationContext(caller);
205
+ return { ...config, ...selection, ...search, ...navigation };
206
+ };
207
+
134
208
  /**
135
209
  * Context for values that are only available inside Content (e.g., variant, color)
136
210
  * so that Input/Item can style themselves consistently.
@@ -224,6 +298,9 @@ const ComboboxRoot: React.FC<ComboboxRootProps> = (props) => {
224
298
 
225
299
  const handleSelect = React.useCallback(
226
300
  (nextValue: string) => {
301
+ // Batch state updates to minimize re-renders
302
+ // React 18+ automatically batches these, but we use flushSync-free pattern
303
+ // to ensure predictable update order
227
304
  setValue(nextValue);
228
305
  setOpen(false);
229
306
  if (resetSearchOnSelect) {
@@ -258,7 +335,10 @@ const ComboboxRoot: React.FC<ComboboxRootProps> = (props) => {
258
335
  return displayValueProp;
259
336
  }, [displayValueProp, value]);
260
337
 
261
- const contextValue = React.useMemo<ComboboxContextValue>(
338
+ // Split context values for optimal re-render performance
339
+ // Each context only triggers re-renders for components that use it
340
+
341
+ const configContextValue = React.useMemo<ComboboxConfigContextValue>(
262
342
  () => ({
263
343
  size,
264
344
  highContrast,
@@ -270,55 +350,53 @@ const ComboboxRoot: React.FC<ComboboxRootProps> = (props) => {
270
350
  disabled,
271
351
  resetSearchOnSelect,
272
352
  color,
273
- resolvedDisplayValue,
353
+ listboxId,
354
+ }),
355
+ [size, highContrast, placeholder, searchPlaceholder, filter, shouldFilter, loop, disabled, resetSearchOnSelect, color, listboxId],
356
+ );
357
+
358
+ const selectionContextValue = React.useMemo<ComboboxSelectionContextValue>(
359
+ () => ({
274
360
  open,
275
361
  setOpen,
276
362
  value,
277
- setValue,
278
- searchValue,
279
- setSearchValue,
280
363
  selectedLabel,
364
+ resolvedDisplayValue,
281
365
  registerItemLabel,
282
366
  unregisterItemLabel,
283
367
  handleSelect,
284
- listboxId,
285
- activeDescendantId,
286
- setActiveDescendantId,
287
368
  }),
288
- [
289
- size,
290
- highContrast,
291
- placeholder,
292
- searchPlaceholder,
293
- filter,
294
- shouldFilter,
295
- loop,
296
- disabled,
297
- resetSearchOnSelect,
298
- color,
299
- resolvedDisplayValue,
300
- open,
301
- setOpen,
302
- value,
303
- setValue,
369
+ [open, setOpen, value, selectedLabel, resolvedDisplayValue, registerItemLabel, unregisterItemLabel, handleSelect],
370
+ );
371
+
372
+ const searchContextValue = React.useMemo<ComboboxSearchContextValue>(
373
+ () => ({
304
374
  searchValue,
305
375
  setSearchValue,
306
- selectedLabel,
307
- registerItemLabel,
308
- unregisterItemLabel,
309
- handleSelect,
310
- listboxId,
376
+ }),
377
+ [searchValue, setSearchValue],
378
+ );
379
+
380
+ const navigationContextValue = React.useMemo<ComboboxNavigationContextValue>(
381
+ () => ({
311
382
  activeDescendantId,
312
383
  setActiveDescendantId,
313
- ],
384
+ }),
385
+ [activeDescendantId, setActiveDescendantId],
314
386
  );
315
387
 
316
388
  return (
317
- <ComboboxContext.Provider value={contextValue}>
318
- <Popover.Root open={open} onOpenChange={setOpen} {...rootProps}>
319
- {children}
320
- </Popover.Root>
321
- </ComboboxContext.Provider>
389
+ <ComboboxConfigContext.Provider value={configContextValue}>
390
+ <ComboboxSelectionContext.Provider value={selectionContextValue}>
391
+ <ComboboxSearchContext.Provider value={searchContextValue}>
392
+ <ComboboxNavigationContext.Provider value={navigationContextValue}>
393
+ <Popover.Root open={open} onOpenChange={setOpen} {...rootProps}>
394
+ {children}
395
+ </Popover.Root>
396
+ </ComboboxNavigationContext.Provider>
397
+ </ComboboxSearchContext.Provider>
398
+ </ComboboxSelectionContext.Provider>
399
+ </ComboboxConfigContext.Provider>
322
400
  );
323
401
  };
324
402
  ComboboxRoot.displayName = 'Combobox.Root';
@@ -332,9 +410,13 @@ interface ComboboxTriggerProps extends NativeTriggerProps, MarginProps, Combobox
332
410
  * syncing size/highContrast from Root while exposing select-like states.
333
411
  */
334
412
  const ComboboxTrigger = React.forwardRef<ComboboxTriggerElement, ComboboxTriggerProps>((props, forwardedRef) => {
335
- const context = useComboboxContext('Combobox.Trigger');
413
+ // Use specific contexts to minimize re-renders
414
+ const configContext = useComboboxConfigContext('Combobox.Trigger');
415
+ const selectionContext = useComboboxSelectionContext('Combobox.Trigger');
416
+ const navigationContext = useComboboxNavigationContext('Combobox.Trigger');
417
+
336
418
  const { children, className, placeholder, disabled, readOnly, error, loading, color, radius, ...triggerProps } = extractProps(
337
- { size: context.size, highContrast: context.highContrast, ...props },
419
+ { size: configContext.size, highContrast: configContext.highContrast, ...props },
338
420
  { size: comboboxRootPropDefs.size, highContrast: comboboxRootPropDefs.highContrast },
339
421
  comboboxTriggerPropDefs,
340
422
  marginPropDefs,
@@ -343,29 +425,29 @@ const ComboboxTrigger = React.forwardRef<ComboboxTriggerElement, ComboboxTrigger
343
425
  // Extract material and panelBackground separately since they need to be passed as data attributes
344
426
  const { material, panelBackground } = props;
345
427
 
346
- const isDisabled = disabled ?? context.disabled;
428
+ const isDisabled = disabled ?? configContext.disabled;
347
429
 
348
430
  // Use color from props or fall back to context color
349
- const resolvedColor = color ?? context.color;
431
+ const resolvedColor = color ?? configContext.color;
350
432
 
351
433
  // Comprehensive ARIA attributes for combobox pattern (WAI-ARIA 1.2)
352
434
  const ariaProps = React.useMemo(
353
435
  () => ({
354
436
  role: 'combobox' as const,
355
- 'aria-expanded': context.open,
437
+ 'aria-expanded': selectionContext.open,
356
438
  'aria-disabled': isDisabled || undefined,
357
439
  'aria-haspopup': 'listbox' as const,
358
- 'aria-controls': context.open ? context.listboxId : undefined,
359
- 'aria-activedescendant': context.open ? context.activeDescendantId : undefined,
440
+ 'aria-controls': selectionContext.open ? configContext.listboxId : undefined,
441
+ 'aria-activedescendant': selectionContext.open ? navigationContext.activeDescendantId : undefined,
360
442
  'aria-autocomplete': 'list' as const,
361
443
  }),
362
- [context.open, context.listboxId, context.activeDescendantId, isDisabled],
444
+ [selectionContext.open, configContext.listboxId, navigationContext.activeDescendantId, isDisabled],
363
445
  );
364
446
 
365
447
  const defaultContent = (
366
448
  <>
367
449
  <span className="rt-SelectTriggerInner">
368
- <ComboboxValue placeholder={placeholder ?? context.placeholder} />
450
+ <ComboboxValue placeholder={placeholder ?? configContext.placeholder} />
369
451
  </span>
370
452
  {loading ? (
371
453
  <div className="rt-SelectIcon rt-SelectLoadingIcon" aria-hidden="true">
@@ -414,14 +496,16 @@ interface ComboboxValueProps extends React.ComponentPropsWithoutRef<'span'> {
414
496
  /**
415
497
  * Value mirrors Select.Value by showing the selected item's label
416
498
  * or falling back to placeholder text supplied by the consumer or context.
417
- *
499
+ *
418
500
  * Priority: resolvedDisplayValue (explicit) > selectedLabel (from items) > raw value > children > placeholder
419
501
  */
420
502
  const ComboboxValue = React.forwardRef<ComboboxValueElement, ComboboxValueProps>(({ placeholder, children, className, ...valueProps }, forwardedRef) => {
421
- const context = useComboboxContext('Combobox.Value');
503
+ // Only use the contexts we need - config for placeholder, selection for value display
504
+ const configContext = useComboboxConfigContext('Combobox.Value');
505
+ const selectionContext = useComboboxSelectionContext('Combobox.Value');
422
506
  // Priority: explicit displayValue (resolved) > registered label > raw value
423
- const displayValue = context.resolvedDisplayValue ?? context.selectedLabel ?? context.value ?? undefined;
424
- const fallback = placeholder ?? context.placeholder;
507
+ const displayValue = selectionContext.resolvedDisplayValue ?? selectionContext.selectedLabel ?? selectionContext.value ?? undefined;
508
+ const fallback = placeholder ?? configContext.placeholder;
425
509
  return (
426
510
  <span {...valueProps} ref={forwardedRef} className={classNames('rt-ComboboxValue', className)}>
427
511
  {displayValue ?? children ?? fallback}
@@ -440,27 +524,29 @@ interface ComboboxContentProps extends Omit<ComponentPropsWithout<typeof Popover
440
524
  * and instantiating cmdk's Command list for roving focus + filtering.
441
525
  */
442
526
  const ComboboxContent = React.forwardRef<ComboboxContentElement, ComboboxContentProps>((props, forwardedRef) => {
443
- const context = useComboboxContext('Combobox.Content');
527
+ // Only use config context - Content doesn't need selection/search/navigation state
528
+ const configContext = useComboboxConfigContext('Combobox.Content');
444
529
  const themeContext = useThemeContext();
445
530
  const effectiveMaterial = themeContext.panelBackground;
446
531
 
447
- const sizeProp = props.size ?? context.size ?? comboboxContentPropDefs.size.default;
532
+ const sizeProp = props.size ?? configContext.size ?? comboboxContentPropDefs.size.default;
448
533
  const variantProp = props.variant ?? comboboxContentPropDefs.variant.default;
449
- const highContrastProp = props.highContrast ?? context.highContrast ?? comboboxContentPropDefs.highContrast.default;
534
+ const highContrastProp = props.highContrast ?? configContext.highContrast ?? comboboxContentPropDefs.highContrast.default;
450
535
 
451
536
  const { className, children, color, forceMount, container, ...contentProps } = extractProps(
452
537
  { ...props, size: sizeProp, variant: variantProp, highContrast: highContrastProp },
453
538
  comboboxContentPropDefs,
454
539
  );
455
- const resolvedColor = color || context.color || themeContext.accentColor;
540
+ const resolvedColor = color || configContext.color || themeContext.accentColor;
456
541
 
457
542
  // Memoize className sanitization to avoid string operations on every render
543
+ // Uses pre-compiled SIZE_CLASS_REGEX for better performance
458
544
  const sanitizedClassName = React.useMemo(() => {
459
545
  if (typeof sizeProp !== 'string') return className;
460
546
  return className
461
547
  ?.split(/\s+/)
462
548
  .filter(Boolean)
463
- .filter((token) => !/^rt-r-size-\d$/.test(token))
549
+ .filter((token) => !SIZE_CLASS_REGEX.test(token))
464
550
  .join(' ') || undefined;
465
551
  }, [className, sizeProp]);
466
552
 
@@ -493,9 +579,9 @@ const ComboboxContent = React.forwardRef<ComboboxContentElement, ComboboxContent
493
579
  <Theme asChild>
494
580
  <ComboboxContentContext.Provider value={{ variant: variantProp, size: String(sizeProp), color: resolvedColor, material: effectiveMaterial, highContrast: highContrastProp }}>
495
581
  <CommandPrimitive
496
- loop={context.loop}
497
- shouldFilter={context.shouldFilter}
498
- filter={context.filter}
582
+ loop={configContext.loop}
583
+ shouldFilter={configContext.shouldFilter}
584
+ filter={configContext.filter}
499
585
  className="rt-ComboboxCommand"
500
586
  >
501
587
  {children}
@@ -522,7 +608,9 @@ interface ComboboxInputProps extends Omit<React.ComponentPropsWithoutRef<typeof
522
608
  * automatic focus management and optional adornments.
523
609
  */
524
610
  const ComboboxInput = React.forwardRef<ComboboxInputElement, ComboboxInputProps>(({ className, startAdornment, endAdornment, placeholder, variant: inputVariant, value, onValueChange, ...inputProps }, forwardedRef) => {
525
- const context = useComboboxContext('Combobox.Input');
611
+ // Use specific contexts - config for size/placeholder, search for search state
612
+ const configContext = useComboboxConfigContext('Combobox.Input');
613
+ const searchContext = useComboboxSearchContext('Combobox.Input');
526
614
  const contentContext = useComboboxContentContext();
527
615
  const contentVariant = contentContext?.variant ?? 'solid';
528
616
  const color = contentContext?.color;
@@ -537,12 +625,12 @@ const ComboboxInput = React.forwardRef<ComboboxInputElement, ComboboxInputProps>
537
625
  const textFieldVariant = inputVariant ?? (contentVariant === 'solid' ? 'surface' : 'soft');
538
626
 
539
627
  // Use controlled search value from context, allow override via props
540
- const searchValue = value ?? context.searchValue;
541
- const handleSearchChange = onValueChange ?? context.setSearchValue;
628
+ const searchValue = value ?? searchContext.searchValue;
629
+ const handleSearchChange = onValueChange ?? searchContext.setSearchValue;
542
630
 
543
631
  const inputField = (
544
632
  <div
545
- className={classNames('rt-TextFieldRoot', 'rt-ComboboxInputRoot', `rt-r-size-${context.size}`, `rt-variant-${textFieldVariant}`)}
633
+ className={classNames('rt-TextFieldRoot', 'rt-ComboboxInputRoot', `rt-r-size-${configContext.size}`, `rt-variant-${textFieldVariant}`)}
546
634
  data-accent-color={color}
547
635
  data-material={material}
548
636
  data-panel-background={material}
@@ -553,7 +641,7 @@ const ComboboxInput = React.forwardRef<ComboboxInputElement, ComboboxInputProps>
553
641
  ref={forwardedRef}
554
642
  value={searchValue}
555
643
  onValueChange={handleSearchChange}
556
- placeholder={placeholder ?? context.searchPlaceholder}
644
+ placeholder={placeholder ?? configContext.searchPlaceholder}
557
645
  className={classNames('rt-reset', 'rt-TextFieldInput', className)}
558
646
  />
559
647
  {endAdornment ? (
@@ -579,7 +667,9 @@ interface ComboboxListProps extends React.ComponentPropsWithoutRef<typeof Comman
579
667
  * Also handles aria-activedescendant tracking via a single MutationObserver for all items.
580
668
  */
581
669
  const ComboboxList = React.forwardRef<ComboboxListElement, ComboboxListProps>(({ className, ...listProps }, forwardedRef) => {
582
- const context = useComboboxContext('Combobox.List');
670
+ // Use specific contexts - config for listboxId, navigation for active descendant
671
+ const configContext = useComboboxConfigContext('Combobox.List');
672
+ const navigationContext = useComboboxNavigationContext('Combobox.List');
583
673
  const listRef = React.useRef<HTMLDivElement | null>(null);
584
674
 
585
675
  // Combined ref handling
@@ -595,6 +685,9 @@ const ComboboxList = React.forwardRef<ComboboxListElement, ComboboxListProps>(({
595
685
  [forwardedRef],
596
686
  );
597
687
 
688
+ // Destructure to get stable reference for effect dependency
689
+ const { setActiveDescendantId } = navigationContext;
690
+
598
691
  /**
599
692
  * Single MutationObserver at List level to track aria-activedescendant.
600
693
  * This replaces per-item observers for better performance with large lists.
@@ -606,7 +699,7 @@ const ComboboxList = React.forwardRef<ComboboxListElement, ComboboxListProps>(({
606
699
  const updateActiveDescendant = () => {
607
700
  const selectedItem = listNode.querySelector('[data-selected="true"], [aria-selected="true"]');
608
701
  const itemId = selectedItem?.id;
609
- context.setActiveDescendantId(itemId || undefined);
702
+ setActiveDescendantId(itemId || undefined);
610
703
  };
611
704
 
612
705
  // Initial check
@@ -621,7 +714,7 @@ const ComboboxList = React.forwardRef<ComboboxListElement, ComboboxListProps>(({
621
714
  });
622
715
 
623
716
  return () => observer.disconnect();
624
- }, [context.setActiveDescendantId]);
717
+ }, [setActiveDescendantId]);
625
718
 
626
719
  return (
627
720
  <ScrollArea type="auto" className="rt-ComboboxScrollArea" scrollbars="vertical" size="1">
@@ -629,7 +722,7 @@ const ComboboxList = React.forwardRef<ComboboxListElement, ComboboxListProps>(({
629
722
  <CommandPrimitive.List
630
723
  {...listProps}
631
724
  ref={combinedRef}
632
- id={context.listboxId}
725
+ id={configContext.listboxId}
633
726
  role="listbox"
634
727
  aria-label="Options"
635
728
  className={classNames('rt-ComboboxList', className)}
@@ -709,13 +802,16 @@ function extractTextFromChildren(children: React.ReactNode): string {
709
802
  }
710
803
 
711
804
  const ComboboxItem = React.forwardRef<ComboboxItemElement, ComboboxItemProps>(({ className, children, label, value, disabled, onSelect, keywords, ...itemProps }, forwardedRef) => {
712
- const context = useComboboxContext('Combobox.Item');
805
+ // Use specific contexts - config for disabled, selection for value/registration
806
+ // This means items only re-render when selection changes, not on search or navigation
807
+ const configContext = useComboboxConfigContext('Combobox.Item');
808
+ const selectionContext = useComboboxSelectionContext('Combobox.Item');
713
809
  const contentContext = useComboboxContentContext();
714
-
810
+
715
811
  // Memoize label extraction to avoid recursive traversal on every render
716
812
  const extractedLabel = React.useMemo(() => extractTextFromChildren(children), [children]);
717
813
  const itemLabel = label ?? (extractedLabel || String(value));
718
- const isSelected = value != null && context.value === value;
814
+ const isSelected = value != null && selectionContext.value === value;
719
815
  const sizeClass = contentContext?.size ? `rt-r-size-${contentContext.size}` : undefined;
720
816
 
721
817
  // Use provided keywords, or default to the item label for search
@@ -727,7 +823,7 @@ const ComboboxItem = React.forwardRef<ComboboxItemElement, ComboboxItemProps>(({
727
823
  const itemId = `combobox-item-${generatedId}`;
728
824
 
729
825
  // Destructure stable references to avoid effect re-runs when unrelated context values change
730
- const { registerItemLabel, unregisterItemLabel, handleSelect: contextHandleSelect } = context;
826
+ const { registerItemLabel, unregisterItemLabel, handleSelect: contextHandleSelect } = selectionContext;
731
827
 
732
828
  // Register/unregister label for display in trigger
733
829
  React.useEffect(() => {
@@ -745,7 +841,7 @@ const ComboboxItem = React.forwardRef<ComboboxItemElement, ComboboxItemProps>(({
745
841
  [contextHandleSelect, onSelect],
746
842
  );
747
843
 
748
- const isDisabled = disabled ?? context.disabled ?? false;
844
+ const isDisabled = disabled ?? configContext.disabled ?? false;
749
845
 
750
846
  /**
751
847
  * Performance notes:
@@ -41,6 +41,8 @@ export interface ShellContextValue {
41
41
  onLeftPres?: (p: PresentationValue) => void;
42
42
  // Sizing info for overlay grouping
43
43
  onLeftDefaults?: (size: number) => void;
44
+ onRailDefaults?: (size: number) => void;
45
+ onPanelDefaults?: (size: number) => void;
44
46
  }
45
47
 
46
48
  const ShellContext = React.createContext<ShellContextValue | null>(null);
@@ -61,35 +63,35 @@ export function ShellProvider({ value, children }: { value: ShellContextValue; c
61
63
  // Pane mode slice contexts
62
64
  type ModeSetter<T> = (mode: T) => void;
63
65
 
64
- export const LeftModeContext = React.createContext<{ leftMode: PaneMode; setLeftMode: ModeSetter<PaneMode> } | null>(null as any);
66
+ export const LeftModeContext = React.createContext<{ leftMode: PaneMode; setLeftMode: ModeSetter<PaneMode> } | null>(null);
65
67
  export function useLeftMode() {
66
68
  const ctx = React.useContext(LeftModeContext);
67
69
  if (!ctx) throw new Error('useLeftMode must be used within Shell.Root');
68
70
  return ctx;
69
71
  }
70
72
 
71
- export const PanelModeContext = React.createContext<{ panelMode: PaneMode; setPanelMode: ModeSetter<PaneMode> } | null>(null as any);
73
+ export const PanelModeContext = React.createContext<{ panelMode: PaneMode; setPanelMode: ModeSetter<PaneMode> } | null>(null);
72
74
  export function usePanelMode() {
73
75
  const ctx = React.useContext(PanelModeContext);
74
76
  if (!ctx) throw new Error('usePanelMode must be used within Shell.Root');
75
77
  return ctx;
76
78
  }
77
79
 
78
- export const SidebarModeContext = React.createContext<{ sidebarMode: SidebarMode; setSidebarMode: ModeSetter<SidebarMode> } | null>(null as any);
80
+ export const SidebarModeContext = React.createContext<{ sidebarMode: SidebarMode; setSidebarMode: ModeSetter<SidebarMode> } | null>(null);
79
81
  export function useSidebarMode() {
80
82
  const ctx = React.useContext(SidebarModeContext);
81
83
  if (!ctx) throw new Error('useSidebarMode must be used within Shell.Root');
82
84
  return ctx;
83
85
  }
84
86
 
85
- export const InspectorModeContext = React.createContext<{ inspectorMode: PaneMode; setInspectorMode: ModeSetter<PaneMode> } | null>(null as any);
87
+ export const InspectorModeContext = React.createContext<{ inspectorMode: PaneMode; setInspectorMode: ModeSetter<PaneMode> } | null>(null);
86
88
  export function useInspectorMode() {
87
89
  const ctx = React.useContext(InspectorModeContext);
88
90
  if (!ctx) throw new Error('useInspectorMode must be used within Shell.Root');
89
91
  return ctx;
90
92
  }
91
93
 
92
- export const BottomModeContext = React.createContext<{ bottomMode: PaneMode; setBottomMode: ModeSetter<PaneMode> } | null>(null as any);
94
+ export const BottomModeContext = React.createContext<{ bottomMode: PaneMode; setBottomMode: ModeSetter<PaneMode> } | null>(null);
93
95
  export function useBottomMode() {
94
96
  const ctx = React.useContext(BottomModeContext);
95
97
  if (!ctx) throw new Error('useBottomMode must be used within Shell.Root');
@@ -97,7 +99,7 @@ export function useBottomMode() {
97
99
  }
98
100
 
99
101
  // Presentation slice
100
- export const PresentationContext = React.createContext<{ currentBreakpoint: Breakpoint; currentBreakpointReady: boolean; leftResolvedPresentation?: PresentationValue } | null>(null as any);
102
+ export const PresentationContext = React.createContext<{ currentBreakpoint: Breakpoint; currentBreakpointReady: boolean; leftResolvedPresentation?: PresentationValue } | null>(null);
101
103
  export function usePresentation() {
102
104
  const ctx = React.useContext(PresentationContext);
103
105
  if (!ctx) throw new Error('usePresentation must be used within Shell.Root');
@@ -106,7 +108,7 @@ export function usePresentation() {
106
108
 
107
109
  // Peek slice
108
110
  export const PeekContext = React.createContext<{ peekTarget: PaneTarget | null; setPeekTarget: (t: PaneTarget | null) => void; peekPane: (t: PaneTarget) => void; clearPeek: () => void } | null>(
109
- null as any,
111
+ null,
110
112
  );
111
113
  export function usePeek() {
112
114
  const ctx = React.useContext(PeekContext);
@@ -120,7 +122,7 @@ export const ActionsContext = React.createContext<{
120
122
  expandPane: (t: PaneTarget) => void;
121
123
  collapsePane: (t: PaneTarget) => void;
122
124
  setSidebarToggleComputer?: (fn: (current: SidebarMode) => SidebarMode) => void;
123
- } | null>(null as any);
125
+ } | null>(null);
124
126
  export function useShellActions() {
125
127
  const ctx = React.useContext(ActionsContext);
126
128
  if (!ctx) throw new Error('useShellActions must be used within Shell.Root');
@@ -128,7 +130,7 @@ export function useShellActions() {
128
130
  }
129
131
 
130
132
  // Composition slice
131
- export const CompositionContext = React.createContext<{ hasLeft: boolean; setHasLeft: (v: boolean) => void; hasSidebar: boolean; setHasSidebar: (v: boolean) => void } | null>(null as any);
133
+ export const CompositionContext = React.createContext<{ hasLeft: boolean; setHasLeft: (v: boolean) => void; hasSidebar: boolean; setHasSidebar: (v: boolean) => void } | null>(null);
132
134
  export function useComposition() {
133
135
  const ctx = React.useContext(CompositionContext);
134
136
  if (!ctx) throw new Error('useComposition must be used within Shell.Root');
@@ -142,7 +144,7 @@ export const InsetContext = React.createContext<{
142
144
  registerInset: (id: InsetPaneId) => void;
143
145
  unregisterInset: (id: InsetPaneId) => void;
144
146
  hasAnyInset: boolean;
145
- } | null>(null as any);
147
+ } | null>(null);
146
148
  export function useInset() {
147
149
  const ctx = React.useContext(InsetContext);
148
150
  if (!ctx) throw new Error('useInset must be used within Shell.Root');