@juspay/blend-design-system 0.0.37-beta.4 → 0.0.37-beta.5

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 (72) hide show
  1. package/dist/components/Breadcrumb/Breadcrumb.d.ts +2 -5
  2. package/dist/components/Breadcrumb/types.d.ts +6 -0
  3. package/dist/components/Charts/ChartUtils.d.ts +2 -0
  4. package/dist/components/Charts/types.d.ts +2 -2
  5. package/dist/components/DateRangePicker/types.d.ts +1 -1
  6. package/dist/components/DateRangePicker/utils.d.ts +2 -0
  7. package/dist/components/Directory/Directory.d.ts +1 -1
  8. package/dist/components/Directory/types.d.ts +1 -1
  9. package/dist/components/Directory/utils.d.ts +2 -0
  10. package/dist/components/Radio/StyledRadio.d.ts +0 -1
  11. package/dist/components/Sidebar/SidebarContent.d.ts +1 -1
  12. package/dist/components/Sidebar/types.d.ts +10 -1
  13. package/dist/components/Sidebar/utils.d.ts +1 -1
  14. package/dist/components/SidebarV2/SidebarV2Panel.d.ts +1 -1
  15. package/dist/components/SidebarV2/index.d.ts +1 -1
  16. package/dist/components/SidebarV2/types.d.ts +3 -0
  17. package/dist/components/Stepper/types.d.ts +2 -0
  18. package/dist/main.js +27657 -27314
  19. package/dist/tokens.js +17 -16
  20. package/lib/components/Avatar/Avatar.tsx +6 -1
  21. package/lib/components/AvatarGroup/AvatarGroup.tsx +1 -1
  22. package/lib/components/AvatarV2/AvatarV2.tsx +10 -1
  23. package/lib/components/Breadcrumb/Breadcrumb.tsx +9 -8
  24. package/lib/components/Breadcrumb/types.ts +12 -0
  25. package/lib/components/Button/ButtonBase.tsx +1 -1
  26. package/lib/components/Card/CardComponents.tsx +52 -17
  27. package/lib/components/Charts/ChartUtils.tsx +7 -0
  28. package/lib/components/Charts/Charts.tsx +4 -2
  29. package/lib/components/Charts/CoreChart.tsx +4 -2
  30. package/lib/components/Charts/types.tsx +2 -2
  31. package/lib/components/ChartsV2/ChartV2.tsx +1 -1
  32. package/lib/components/Checkbox/Checkbox.tsx +29 -7
  33. package/lib/components/CodeBlock/CodeBlock.tsx +47 -1
  34. package/lib/components/CodeBlock/codeBlock.token.ts +5 -5
  35. package/lib/components/CodeEditor/CodeEditor.tsx +26 -4
  36. package/lib/components/CodeEditor/MonacoEditorWrapper.tsx +13 -1
  37. package/lib/components/DataTable/DataTable.tsx +8 -0
  38. package/lib/components/DataTable/TableHeader/FilterComponents.tsx +4 -0
  39. package/lib/components/DateRangePicker/DateRangePicker.tsx +34 -17
  40. package/lib/components/DateRangePicker/types.ts +5 -5
  41. package/lib/components/DateRangePicker/utils.ts +5 -0
  42. package/lib/components/Directory/Directory.tsx +3 -2
  43. package/lib/components/Directory/types.ts +1 -1
  44. package/lib/components/Directory/utils.ts +6 -0
  45. package/lib/components/Drawer/components/DrawerBase.tsx +16 -0
  46. package/lib/components/Drawer/components/NestedSelectDrawer.tsx +13 -1
  47. package/lib/components/Drawer/components/SelectDrawer.tsx +9 -1
  48. package/lib/components/Inputs/OTPInput/OTPInput.tsx +5 -3
  49. package/lib/components/Menu/Menu.tsx +9 -1
  50. package/lib/components/Modal/useModal.ts +7 -0
  51. package/lib/components/Radio/Radio.tsx +12 -5
  52. package/lib/components/Radio/StyledRadio.tsx +33 -17
  53. package/lib/components/Sidebar/Sidebar.tsx +11 -1
  54. package/lib/components/Sidebar/SidebarContent.tsx +5 -2
  55. package/lib/components/Sidebar/TenantPanel.tsx +52 -34
  56. package/lib/components/Sidebar/types.ts +11 -1
  57. package/lib/components/Sidebar/utils.ts +1 -1
  58. package/lib/components/SidebarV2/SecondarySidebar.tsx +86 -44
  59. package/lib/components/SidebarV2/SidebarV2Panel.tsx +4 -2
  60. package/lib/components/SidebarV2/index.ts +1 -0
  61. package/lib/components/SidebarV2/types.ts +4 -0
  62. package/lib/components/StatCard/statcard.tokens.ts +1 -1
  63. package/lib/components/Stepper/VerticalStepper.tsx +209 -171
  64. package/lib/components/Stepper/types.ts +2 -0
  65. package/lib/components/StepperV2/Stepper/Steps.tsx +15 -1
  66. package/lib/components/Text/Text.tsx +1 -0
  67. package/lib/components/Upload/Upload.tsx +6 -0
  68. package/lib/components/Upload/components/FileListDisplay.tsx +159 -16
  69. package/lib/components/Upload/utils.ts +10 -2
  70. package/lib/context/ThemeProvider.tsx +19 -8
  71. package/lib/hooks/useDebounce.ts +9 -1
  72. package/package.json +1 -1
@@ -943,6 +943,14 @@ const DataTable = forwardRef(
943
943
 
944
944
  const sortTimeoutRef = useRef<NodeJS.Timeout | null>(null)
945
945
 
946
+ useEffect(() => {
947
+ return () => {
948
+ if (sortTimeoutRef.current) {
949
+ clearTimeout(sortTimeoutRef.current)
950
+ }
951
+ }
952
+ }, [])
953
+
946
954
  const applySortConfig = (
947
955
  field: keyof T,
948
956
  newSortConfig: SortConfig | null
@@ -618,6 +618,8 @@ export const SingleSelectItems: React.FC<{
618
618
  .header.filter
619
619
  .sortOption.fontWeight,
620
620
  flexGrow: 1,
621
+ overflowWrap: 'break-word',
622
+ minWidth: 0,
621
623
  }}
622
624
  >
623
625
  {label}
@@ -824,6 +826,8 @@ export const MultiSelectItems: React.FC<{
824
826
  .header.filter
825
827
  .sortOption.fontWeight,
826
828
  flexGrow: 1,
829
+ overflowWrap: 'break-word',
830
+ minWidth: 0,
827
831
  }}
828
832
  >
829
833
  {label}
@@ -25,6 +25,8 @@ import {
25
25
  getPresetLabel,
26
26
  getTodayInTimezone,
27
27
  validateDateInput,
28
+ isControlledDateRange,
29
+ isValidDate,
28
30
  } from './utils'
29
31
  import CalendarGrid from './CalendarGrid'
30
32
  import QuickRangeSelector from './QuickRangeSelector'
@@ -408,7 +410,7 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
408
410
 
409
411
  const [selectedRange, setSelectedRange] = useState<
410
412
  DateRange | undefined
411
- >(value)
413
+ >(() => (isControlledDateRange(value) ? value : undefined))
412
414
  const lastExternalValueRef = React.useRef<{
413
415
  start: number | null
414
416
  end: number | null
@@ -524,32 +526,44 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
524
526
 
525
527
  const resetValues = useCallback(
526
528
  (dateRangeObj?: DateRange) => {
527
- setSelectedRange(dateRangeObj)
529
+ const normalizedRange = isControlledDateRange(dateRangeObj)
530
+ ? dateRangeObj
531
+ : undefined
532
+
533
+ setSelectedRange(normalizedRange)
528
534
  setActivePreset(
529
- dateRangeObj
530
- ? detectPresetFromRange(dateRangeObj, timezone)
535
+ normalizedRange
536
+ ? detectPresetFromRange(normalizedRange, timezone)
531
537
  : DateRangePreset.CUSTOM
532
538
  )
533
539
  setStartDate(
534
- dateRangeObj &&
535
- formatDate(dateRangeObj.startDate, dateFormat, timezone)
540
+ normalizedRange &&
541
+ formatDate(
542
+ normalizedRange.startDate,
543
+ dateFormat,
544
+ timezone
545
+ )
536
546
  )
537
- if (dateRangeObj && dateRangeObj.endDate) {
547
+ if (normalizedRange?.endDate) {
538
548
  setEndDate(
539
- formatDate(dateRangeObj.endDate, dateFormat, timezone)
549
+ formatDate(
550
+ normalizedRange.endDate,
551
+ dateFormat,
552
+ timezone
553
+ )
540
554
  )
541
- } else if (!dateRangeObj) {
555
+ } else if (!normalizedRange) {
542
556
  setEndDate(undefined)
543
557
  }
544
558
  setStartTime(
545
- dateRangeObj &&
546
- formatDate(dateRangeObj.startDate, 'HH:mm', timezone)
559
+ normalizedRange &&
560
+ formatDate(normalizedRange.startDate, 'HH:mm', timezone)
547
561
  )
548
- if (dateRangeObj && dateRangeObj.endDate) {
562
+ if (normalizedRange?.endDate) {
549
563
  setEndTime(
550
- formatDate(dateRangeObj.endDate, 'HH:mm', timezone)
564
+ formatDate(normalizedRange.endDate, 'HH:mm', timezone)
551
565
  )
552
- } else if (!dateRangeObj) {
566
+ } else if (!normalizedRange) {
553
567
  setEndTime(undefined)
554
568
  }
555
569
  setStartDateValidation({ isValid: true, error: 'none' })
@@ -562,7 +576,7 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
562
576
  )
563
577
 
564
578
  useEffect(() => {
565
- if (!value) {
579
+ if (!isControlledDateRange(value)) {
566
580
  if (lastExternalValueRef.current !== null) {
567
581
  lastExternalValueRef.current = null
568
582
  resetValues(undefined)
@@ -571,8 +585,11 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
571
585
  }
572
586
 
573
587
  const nextSignature = {
574
- start: value.startDate.getTime() ?? null,
575
- end: value.endDate?.getTime() ?? null,
588
+ start: value.startDate.getTime(),
589
+ end:
590
+ value.endDate && isValidDate(value.endDate)
591
+ ? value.endDate.getTime()
592
+ : null,
576
593
  dateFormat,
577
594
  }
578
595
 
@@ -194,11 +194,11 @@ export type CustomPresetDefinition = {
194
194
  /**
195
195
  * Presets configuration - can be predefined presets, custom configs, or custom definitions
196
196
  */
197
- export type PresetsConfig =
198
- | DateRangePreset[]
199
- | CustomPresetConfig[]
200
- | CustomPresetDefinition[]
201
- | (DateRangePreset | CustomPresetConfig | CustomPresetDefinition)[]
197
+ export type PresetsConfig = (
198
+ | DateRangePreset
199
+ | CustomPresetConfig
200
+ | CustomPresetDefinition
201
+ )[]
202
202
 
203
203
  // =============================================================================
204
204
  // COMPONENT PROPS
@@ -256,6 +256,11 @@ export const isValidDate = (date: Date): boolean => {
256
256
  return date instanceof Date && !isNaN(date.getTime())
257
257
  }
258
258
 
259
+ /** True when a controlled `value` has a usable start date (avoids `.getTime()` on undefined). */
260
+ export const isControlledDateRange = (
261
+ value?: DateRange | null
262
+ ): value is DateRange => !!value && isValidDate(value.startDate)
263
+
259
264
  /**
260
265
  * Formats time in 12-hour format
261
266
  * @param date The date to format
@@ -5,19 +5,20 @@ import { createRef, useEffect, useRef } from 'react'
5
5
  import type { DirectoryProps } from './types'
6
6
  import Section from './Section'
7
7
  import Block from '../Primitives/Block/Block'
8
- import { handleSectionNavigation } from './utils'
8
+ import { handleSectionNavigation, normalizeDirectoryData } from './utils'
9
9
  import { ActiveItemProvider } from './NavItem'
10
10
  import { useResponsiveTokens } from '../../hooks/useResponsiveTokens'
11
11
  import { DirectoryTokenType } from './directory.tokens'
12
12
 
13
13
  const Directory = ({
14
- directoryData,
14
+ directoryData: directoryDataProp,
15
15
  idPrefix,
16
16
  activeItem,
17
17
  onActiveItemChange,
18
18
  defaultActiveItem,
19
19
  iconOnlyMode = false,
20
20
  }: DirectoryProps) => {
21
+ const directoryData = normalizeDirectoryData(directoryDataProp)
21
22
  const sectionRefs = useRef<Array<React.RefObject<HTMLDivElement | null>>>(
22
23
  []
23
24
  )
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
3
  export type DirectoryProps = {
4
- directoryData: DirectoryData[]
4
+ directoryData: DirectoryData[] | null
5
5
  idPrefix?: string
6
6
  activeItem?: string | null
7
7
  onActiveItemChange?: (item: string | null) => void
@@ -1,3 +1,9 @@
1
+ import type { DirectoryData } from './types'
2
+
3
+ export const normalizeDirectoryData = (
4
+ directoryData: DirectoryData[] | null
5
+ ): DirectoryData[] => (Array.isArray(directoryData) ? directoryData : [])
6
+
1
7
  export const handleSectionNavigation = (
2
8
  direction: 'up' | 'down',
3
9
  currentIndex: number,
@@ -299,6 +299,22 @@ export const Drawer = ({
299
299
  string | undefined
300
300
  >(undefined)
301
301
 
302
+ // Prevent focus from escaping to underlying content when a nested drawer is open
303
+ React.useLayoutEffect(() => {
304
+ const stopFocus = (e: FocusEvent) => {
305
+ // only intercept when a nested modal is open
306
+ if (document.querySelector('[data-modal]')) {
307
+ e.stopImmediatePropagation()
308
+ }
309
+ }
310
+ document.addEventListener('focusin', stopFocus)
311
+ document.addEventListener('focusout', stopFocus)
312
+ return () => {
313
+ document.removeEventListener('focusin', stopFocus)
314
+ document.removeEventListener('focusout', stopFocus)
315
+ }
316
+ }, [])
317
+
302
318
  // Screen reader announcement for drawer state changes
303
319
  React.useEffect(() => {
304
320
  if (open) {
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import React, { useState, useMemo, createContext } from 'react'
3
+ import React, { useState, useMemo, createContext, useEffect } from 'react'
4
4
  import {
5
5
  Drawer,
6
6
  DrawerPortal,
@@ -249,6 +249,11 @@ export const NestedMultiSelectDrawer = ({
249
249
  }>
250
250
  >([{ title: heading, items, selectedValues: internalSelectedValues }])
251
251
 
252
+ useEffect(() => {
253
+ setInternalSelectedValues(selectedValues)
254
+ setNavigationStack([{ title: heading, items, selectedValues }])
255
+ }, [selectedValues])
256
+
252
257
  const selectMobileOffset = {
253
258
  top: '74px',
254
259
  bottom: '0px',
@@ -579,6 +584,13 @@ export const NestedSingleSelectDrawer = ({
579
584
  }>
580
585
  >([{ title: heading, items, selectedValue: internalSelectedValue }])
581
586
 
587
+ useEffect(() => {
588
+ setInternalSelectedValue(selectedValue || '')
589
+ setNavigationStack([
590
+ { title: heading, items, selectedValue: selectedValue || '' },
591
+ ])
592
+ }, [selectedValue])
593
+
582
594
  const selectMobileOffset = {
583
595
  top: '74px',
584
596
  bottom: '16px',
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import React, { useState, useMemo } from 'react'
3
+ import React, { useState, useMemo, useEffect } from 'react'
4
4
  import {
5
5
  Drawer,
6
6
  DrawerPortal,
@@ -46,6 +46,10 @@ export const MultiSelectDrawer = ({
46
46
  const [internalSelectedValues, setInternalSelectedValues] =
47
47
  useState<string[]>(selectedValues)
48
48
 
49
+ useEffect(() => {
50
+ setInternalSelectedValues(selectedValues)
51
+ }, [selectedValues])
52
+
49
53
  const selectMobileOffset = {
50
54
  top: '74px',
51
55
  bottom: '16px',
@@ -386,6 +390,10 @@ export const SingleSelectDrawer = ({
386
390
  selectedValue || ''
387
391
  )
388
392
 
393
+ useEffect(() => {
394
+ setInternalSelectedValue(selectedValue || '')
395
+ }, [selectedValue])
396
+
389
397
  const selectMobileOffset = {
390
398
  top: '74px',
391
399
  bottom: '0px',
@@ -74,15 +74,17 @@ const OTPInput = ({
74
74
  .join(' ') || undefined
75
75
 
76
76
  useEffect(() => {
77
- if (!disabled) return
78
77
  const val = value || ''
79
78
  const otpArray = val.split('').slice(0, length)
80
79
  const paddedOtp = [
81
80
  ...otpArray,
82
81
  ...new Array(Math.max(length - otpArray.length, 0)).fill(''),
83
82
  ]
84
- setOtp(paddedOtp)
85
- }, [disabled, value, length])
83
+ setOtp((prevOtp) => {
84
+ if (prevOtp.join('') === paddedOtp.join('')) return prevOtp
85
+ return paddedOtp
86
+ })
87
+ }, [value, length])
86
88
 
87
89
  useEffect(() => {
88
90
  setOtp((prevOtp) => {
@@ -7,7 +7,7 @@ import {
7
7
  MenuSide,
8
8
  type MenuItemType,
9
9
  } from './types'
10
- import React, { useState, useRef, useMemo, useCallback } from 'react'
10
+ import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react'
11
11
  import { filterMenuGroups } from './utils'
12
12
  import MenuItem from './MenuItem'
13
13
  import Block from '../Primitives/Block/Block'
@@ -115,6 +115,14 @@ const Menu = ({
115
115
  onOpenChange?.(newOpen)
116
116
  }
117
117
 
118
+ useEffect(() => {
119
+ return () => {
120
+ if (timeoutRef.current) {
121
+ clearTimeout(timeoutRef.current)
122
+ }
123
+ }
124
+ }, [])
125
+
118
126
  const handleOutsideInteraction = useCallback((e: Event) => {
119
127
  if (justOpenedRef.current) {
120
128
  e.preventDefault()
@@ -22,6 +22,12 @@ export const useModal = (isOpen: boolean, onClose: () => void) => {
22
22
  const container = getPortalContainer()
23
23
  setPortalContainer(container)
24
24
 
25
+ document.body.style.setProperty(
26
+ 'pointer-events',
27
+ 'auto',
28
+ 'important'
29
+ )
30
+
25
31
  let animationFrame2: number | null = null
26
32
  const animationFrame1 = requestAnimationFrame(() => {
27
33
  animationFrame2 = requestAnimationFrame(() => {
@@ -44,6 +50,7 @@ export const useModal = (isOpen: boolean, onClose: () => void) => {
44
50
  cancelAnimationFrame(animationFrame2)
45
51
  }
46
52
  document.removeEventListener('keydown', handleEscapeKey)
53
+ document.body.style.removeProperty('pointer-events')
47
54
  }
48
55
  } else {
49
56
  // Start exit animation
@@ -16,7 +16,7 @@ import { getTruncatedText } from '../../global-utils/GlobalUtils'
16
16
  export const Radio = ({
17
17
  id,
18
18
  checked,
19
- defaultChecked = false,
19
+ defaultChecked,
20
20
  onChange,
21
21
  disabled = false,
22
22
  required = false,
@@ -45,12 +45,20 @@ export const Radio = ({
45
45
  subtextId && customAriaDescribedBy
46
46
  ? `${customAriaDescribedBy} ${subtextId}`
47
47
  : subtextId || customAriaDescribedBy
48
+ const isControlled = checked !== undefined
49
+ const checkedProps = isControlled
50
+ ? { checked }
51
+ : { defaultChecked: defaultChecked ?? false }
48
52
 
49
53
  return (
50
54
  <Block
51
55
  data-radio={children ?? 'radio'}
52
56
  data-status={disabled ? 'disabled' : 'enabled'}
53
- data-state={checked ? 'checked' : 'unchecked'}
57
+ data-state={
58
+ (isControlled ? checked : defaultChecked)
59
+ ? 'checked'
60
+ : 'unchecked'
61
+ }
54
62
  data-id={value ?? ''}
55
63
  display="flex"
56
64
  alignItems={subtext ? 'flex-start' : 'center'}
@@ -60,14 +68,13 @@ export const Radio = ({
60
68
  type="radio"
61
69
  id={uniqueId}
62
70
  name={name}
63
- checked={checked}
64
- defaultChecked={defaultChecked}
71
+ {...checkedProps}
65
72
  disabled={disabled}
66
73
  required={required}
74
+ readOnly={isControlled && !onChange ? true : undefined}
67
75
  onChange={onChange}
68
76
  size={size}
69
77
  $isDisabled={disabled}
70
- $isChecked={checked || false}
71
78
  $error={error}
72
79
  $tokens={radioTokens}
73
80
  style={getErrorShakeStyle(shouldShake)}
@@ -7,7 +7,6 @@ import { radioAnimations } from './radio.animations'
7
7
  export const StyledRadioInput = styled.input<{
8
8
  size: RadioSize
9
9
  $isDisabled: boolean
10
- $isChecked: boolean
11
10
  $error?: boolean
12
11
  $tokens: RadioTokensType
13
12
  }>`
@@ -22,17 +21,17 @@ export const StyledRadioInput = styled.input<{
22
21
  padding: 0;
23
22
  flex-shrink: 0;
24
23
 
25
- ${({ size, $isChecked, $isDisabled, $tokens }) => {
24
+ ${({ size, $isDisabled, $tokens }) => {
26
25
  const state = $isDisabled ? 'disabled' : 'default'
27
- const indicatorState = $isChecked ? 'active' : 'inactive'
26
+ const inactiveIndicator = $tokens.indicator.inactive
27
+ const activeIndicator = $tokens.indicator.active
28
28
 
29
29
  return css`
30
30
  ${radioAnimations}
31
31
 
32
- background-color: ${$tokens.indicator[indicatorState]
33
- .backgroundColor[state]};
34
- border: ${$tokens.borderWidth[indicatorState][state]}px solid
35
- ${$tokens.indicator[indicatorState].borderColor[state]};
32
+ background-color: ${inactiveIndicator.backgroundColor[state]};
33
+ border: ${$tokens.borderWidth.inactive[state]}px solid
34
+ ${inactiveIndicator.borderColor[state]};
36
35
  width: ${$tokens.height[size]};
37
36
  height: ${$tokens.height[size]};
38
37
 
@@ -41,28 +40,45 @@ export const StyledRadioInput = styled.input<{
41
40
  width: 50%;
42
41
  height: 50%;
43
42
  border-radius: 50%;
44
- background-color: ${$isChecked
45
- ? $tokens.activeIndicator.active.backgroundColor[state]
46
- : 'transparent'};
47
- transform: ${$isChecked ? 'scale(1)' : 'scale(0)'};
43
+ background-color: transparent;
44
+ transform: scale(0);
48
45
  transition:
49
46
  transform 250ms cubic-bezier(0.4, 0, 0.2, 1),
50
47
  background-color 200ms cubic-bezier(0.4, 0, 0.2, 1);
51
48
  }
49
+
50
+ &:checked {
51
+ background-color: ${activeIndicator.backgroundColor[state]};
52
+ border: ${$tokens.borderWidth.active[state]}px solid
53
+ ${activeIndicator.borderColor[state]};
54
+
55
+ &::after {
56
+ background-color: ${$tokens.activeIndicator.active
57
+ .backgroundColor[state]};
58
+ transform: scale(1);
59
+ }
60
+ }
61
+
52
62
  &:focus-visible {
53
- outline: 2px solid
54
- ${$tokens.indicator[indicatorState].borderColor[state]};
63
+ outline: 2px solid ${inactiveIndicator.borderColor[state]};
55
64
  outline-offset: 2px;
56
65
  /* WCAG 2.4.7 Focus Visible (AA): Focus indicator must be visible
57
66
  * WCAG 1.4.11 Non-text Contrast (AA): Focus outline must have contrast ratio ≥3:1 against adjacent colors
58
67
  * Manual verification recommended for all states */
59
68
  }
60
69
 
70
+ &:checked:focus-visible {
71
+ outline-color: ${activeIndicator.borderColor[state]};
72
+ }
73
+
61
74
  &:not(:disabled):hover {
62
- background-color: ${$tokens.indicator[indicatorState]
63
- .backgroundColor.hover};
64
- border-color: ${$tokens.indicator[indicatorState].borderColor
65
- .hover};
75
+ background-color: ${inactiveIndicator.backgroundColor.hover};
76
+ border-color: ${inactiveIndicator.borderColor.hover};
77
+ }
78
+
79
+ &:checked:not(:disabled):hover {
80
+ background-color: ${activeIndicator.backgroundColor.hover};
81
+ border-color: ${activeIndicator.borderColor.hover};
66
82
  }
67
83
 
68
84
  cursor: ${$isDisabled ? 'not-allowed' : 'pointer'};
@@ -323,12 +323,22 @@ const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
323
323
  passive: true,
324
324
  })
325
325
 
326
- const handleResize = () => setTimeout(updateBlurState, 50)
326
+ let resizeTimeoutId: ReturnType<typeof setTimeout> | null = null
327
+
328
+ const handleResize = () => {
329
+ if (resizeTimeoutId) {
330
+ clearTimeout(resizeTimeoutId)
331
+ }
332
+ resizeTimeoutId = setTimeout(updateBlurState, 50)
333
+ }
327
334
  window.addEventListener('resize', handleResize, { passive: true })
328
335
 
329
336
  return () => {
330
337
  scrollingElement.removeEventListener('scroll', updateBlurState)
331
338
  window.removeEventListener('resize', handleResize)
339
+ if (resizeTimeoutId) {
340
+ clearTimeout(resizeTimeoutId)
341
+ }
332
342
  }
333
343
  }, [isExpanded, data, isHovering])
334
344
 
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import styled from 'styled-components'
3
3
  import Block from '../Primitives/Block/Block'
4
4
  import Directory from '../Directory/Directory'
5
+ import { normalizeDirectoryData } from '../Directory/utils'
5
6
  import SidebarHeader from './SidebarHeader'
6
7
  import SidebarFooter from './SidebarFooter'
7
8
  import type { DirectoryData } from '../Directory/types'
@@ -35,7 +36,7 @@ export type SidebarContentProps = {
35
36
  sidebarNavId?: string
36
37
  showTopBlur: boolean
37
38
  showBottomBlur: boolean
38
- data: DirectoryData[]
39
+ data: DirectoryData[] | null
39
40
  idPrefix: string
40
41
  activeItem?: string | null
41
42
  onActiveItemChange?: (item: string | null) => void
@@ -66,6 +67,8 @@ const SidebarContent: React.FC<SidebarContentProps> = ({
66
67
  setIsHovering,
67
68
  sidebarState = 'expanded',
68
69
  }) => {
70
+ const directoryData = normalizeDirectoryData(data)
71
+
69
72
  return (
70
73
  <Block
71
74
  data-element="sidebar-content"
@@ -104,7 +107,7 @@ const SidebarContent: React.FC<SidebarContentProps> = ({
104
107
  onMouseEnter={() => setIsHovering?.(true)}
105
108
  >
106
109
  <Directory
107
- directoryData={data}
110
+ directoryData={directoryData}
108
111
  idPrefix={idPrefix}
109
112
  activeItem={activeItem}
110
113
  onActiveItemChange={onActiveItemChange}
@@ -13,6 +13,7 @@ import { arrangeTenants } from './utils'
13
13
  import type { TenantItem } from './types'
14
14
  import { Tooltip } from '../Tooltip'
15
15
  import { TooltipSide, TooltipSize } from '../Tooltip/types'
16
+ import { Badge, BadgeColor, BadgeSize } from '../Badge'
16
17
  import { useResponsiveTokens } from '../../hooks/useResponsiveTokens'
17
18
  import type { SidebarTokenType } from './sidebar.tokens'
18
19
 
@@ -125,6 +126,56 @@ const TenantItem: React.FC<{
125
126
  }> = ({ tenant, isSelected, onSelect }) => {
126
127
  const tokens = useResponsiveTokens<SidebarTokenType>('SIDEBAR')
127
128
 
129
+ const tenantButton = (
130
+ <PrimitiveButton
131
+ data-element="sidebar-section"
132
+ data-id={tenant.label}
133
+ data-status={isSelected ? 'selected' : 'not selected'}
134
+ type="button"
135
+ onClick={onSelect}
136
+ backgroundColor={tokens.leftPanel.item.backgroundColor.default}
137
+ width={tokens.leftPanel.item.width}
138
+ height={tokens.leftPanel.item.width}
139
+ borderRadius={tokens.leftPanel.item.borderRadius}
140
+ display="flex"
141
+ alignItems="center"
142
+ justifyContent="center"
143
+ cursor="pointer"
144
+ border={
145
+ isSelected
146
+ ? tokens.leftPanel.item.border.active
147
+ : tokens.leftPanel.item.border.default
148
+ }
149
+ aria-label={`Select tenant: ${tenant.label}`}
150
+ aria-pressed={isSelected}
151
+ style={{
152
+ transition: 'all 75ms ease',
153
+ }}
154
+ _hover={{
155
+ backgroundColor: tokens.leftPanel.item.backgroundColor.hover,
156
+ outline: isSelected
157
+ ? tokens.leftPanel.item.border.active
158
+ : tokens.leftPanel.item.border.hover,
159
+ }}
160
+ >
161
+ <span aria-hidden="true">{tenant.icon}</span>
162
+ </PrimitiveButton>
163
+ )
164
+
165
+ const trigger = tenant.badge ? (
166
+ <Badge
167
+ text={tenant.badge.text.slice(0, 2)}
168
+ size={BadgeSize.SM}
169
+ color={BadgeColor.NEUTRAL}
170
+ position={'bottom-right'}
171
+ isCircular
172
+ >
173
+ {tenantButton}
174
+ </Badge>
175
+ ) : (
176
+ tenantButton
177
+ )
178
+
128
179
  return (
129
180
  <Tooltip
130
181
  content={tenant.label}
@@ -132,40 +183,7 @@ const TenantItem: React.FC<{
132
183
  delayDuration={500}
133
184
  size={TooltipSize.SMALL}
134
185
  >
135
- <PrimitiveButton
136
- data-element="sidebar-section"
137
- data-id={tenant.label}
138
- data-status={isSelected ? 'selected' : 'not selected'}
139
- type="button"
140
- onClick={onSelect}
141
- backgroundColor={tokens.leftPanel.item.backgroundColor.default}
142
- width={tokens.leftPanel.item.width}
143
- height={tokens.leftPanel.item.width}
144
- borderRadius={tokens.leftPanel.item.borderRadius}
145
- display="flex"
146
- alignItems="center"
147
- justifyContent="center"
148
- cursor="pointer"
149
- border={
150
- isSelected
151
- ? tokens.leftPanel.item.border.active
152
- : tokens.leftPanel.item.border.default
153
- }
154
- aria-label={`Select tenant: ${tenant.label}`}
155
- aria-pressed={isSelected}
156
- style={{
157
- transition: 'all 75ms ease',
158
- }}
159
- _hover={{
160
- backgroundColor:
161
- tokens.leftPanel.item.backgroundColor.hover,
162
- outline: isSelected
163
- ? tokens.leftPanel.item.border.active
164
- : tokens.leftPanel.item.border.hover,
165
- }}
166
- >
167
- <span aria-hidden="true">{tenant.icon}</span>
168
- </PrimitiveButton>
186
+ {trigger}
169
187
  </Tooltip>
170
188
  )
171
189
  }