@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
package/dist/tokens.js CHANGED
@@ -1,5 +1,5 @@
1
- import { F as u, T as d, g as k, a as E, b as V, c as g, d as v, e as S, f as p, h, i as R, j as A, k as y, l as C, m as N, n as I, o as P, p as _, q as m, r as B, s as L, t as M, u as w, v as j, w as U, x as D, y as G, z as H } from "./node-C2uf3sNA.js";
2
- import { BRANCH_COLLECTION as ie, BRANCH_ID_PATTERN as ae, PRESETS as ce, PRESET_BLEND_DEFAULT as fe, PRESET_GREEN as le, PRESET_JUSPAY as Te, PRESET_ORANGE as ue, PRESET_PURPLE as de, RADIUS_PRESETS as Oe, SNAPSHOT_SUBCOLLECTION as ke, STORAGE_KEYS as Ee, TEAM_ROLE_PERMISSIONS as Ve, VERSION_PATTERN as ge, VERSION_SUBCOLLECTION as ve, analyzeContrast as Se, canUserPerformAction as pe, diffBrandConfigs as he, extractOverridePaths as Re, generateBranchId as Ae, generateColorScale as ye, getContrastRatio as Ce, getContrastRatioHex as Ne, getDefaultOnboardingState as Ie, getDefaultPreferences as Pe, getPreset as _e, hexToRgb as me, incrementVersion as Be, isValidHexColor as Le, listPresets as Me, meetsWCAG as we, parseBranchId as je, relativeLuminance as Ue, resolveWithInheritance as De, snapshotsPath as Ge, suggestForeground as He, validateAgainstLocks as xe, validateBranchId as Ke, validateBrandConfig as Fe, validatePaletteContrast as Je, validateVersion as $e, versionsPath as Ye } from "./tokens-server.js";
1
+ import { F as u, T as d, g as k, a as E, b as V, c as g, d as v, e as p, f as S, h, i as R, j as A, k as y, l as C, m as N, n as I, o as P, p as m, q as _, r as B, s as L, t as w, u as M, v as j, w as U, x as D, y as G, z as H } from "./node-C2uf3sNA.js";
2
+ import { BRANCH_COLLECTION as ie, BRANCH_ID_PATTERN as ae, PRESETS as ce, PRESET_BLEND_DEFAULT as fe, PRESET_GREEN as le, PRESET_JUSPAY as Te, PRESET_ORANGE as ue, PRESET_PURPLE as de, RADIUS_PRESETS as Oe, SNAPSHOT_SUBCOLLECTION as ke, STORAGE_KEYS as Ee, TEAM_ROLE_PERMISSIONS as Ve, VERSION_PATTERN as ge, VERSION_SUBCOLLECTION as ve, analyzeContrast as pe, canUserPerformAction as Se, diffBrandConfigs as he, extractOverridePaths as Re, generateBranchId as Ae, generateColorScale as ye, getContrastRatio as Ce, getContrastRatioHex as Ne, getDefaultOnboardingState as Ie, getDefaultPreferences as Pe, getPreset as me, hexToRgb as _e, incrementVersion as Be, isValidHexColor as Le, listPresets as we, meetsWCAG as Me, parseBranchId as je, relativeLuminance as Ue, resolveWithInheritance as De, snapshotsPath as Ge, suggestForeground as He, validateAgainstLocks as xe, validateBranchId as Ke, validateBrandConfig as Fe, validatePaletteContrast as Je, validateVersion as $e, versionsPath as Ye } from "./tokens-server.js";
3
3
  function O(e) {
4
4
  const o = typeof structuredClone == "function" ? structuredClone(u) : JSON.parse(JSON.stringify(u));
5
5
  return x(o, e), K(o, e), F(o, e), J(o, e), o;
@@ -30,8 +30,9 @@ function F(e, o) {
30
30
  function J(e, o) {
31
31
  if (o.font) {
32
32
  if (o.font.family) {
33
- const t = e.font;
34
- "family" in t && (t.family = o.font.family);
33
+ const t = e.font.family, n = o.font.family.trim(), s = n.toLowerCase() === "system ui" ? "system-ui, sans-serif" : n;
34
+ for (const r of Object.keys(t))
35
+ r !== "mono" && (t[r] = s);
35
36
  }
36
37
  if (o.font.weight) {
37
38
  const t = e.font.weight;
@@ -46,12 +47,12 @@ const l = {
46
47
  ALERTV2: D,
47
48
  AVATARV2: U,
48
49
  BREADCRUMBV2: j,
49
- CHARTSV2: w,
50
- CHECKBOXV2: M,
50
+ CHARTSV2: M,
51
+ CHECKBOXV2: w,
51
52
  CODEEDITORV2: L,
52
53
  KEYVALUEPAIRV2: B,
53
- MENU_V2: m,
54
- MULTI_SELECT_V2: _,
54
+ MENU_V2: _,
55
+ MULTI_SELECT_V2: m,
55
56
  POPOVERV2: P,
56
57
  PROGRESS_BARV2: I,
57
58
  RADIOV2: N,
@@ -60,8 +61,8 @@ const l = {
60
61
  SNACKBARV2: A,
61
62
  STATCARDV2: R,
62
63
  TABSV2: h,
63
- TAGV2: p,
64
- TEXT_INPUTV2: S,
64
+ TAGV2: S,
65
+ TEXT_INPUTV2: p,
65
66
  TIMELINE: v,
66
67
  TOOLTIPV2: g,
67
68
  TOPBARV2: V,
@@ -198,9 +199,9 @@ export {
198
199
  te as V2_COMPONENT_KEYS,
199
200
  ge as VERSION_PATTERN,
200
201
  ve as VERSION_SUBCOLLECTION,
201
- Se as analyzeContrast,
202
+ pe as analyzeContrast,
202
203
  O as buildBrandFoundation,
203
- pe as canUserPerformAction,
204
+ Se as canUserPerformAction,
204
205
  z as clearTokenCache,
205
206
  he as diffBrandConfigs,
206
207
  Re as extractOverridePaths,
@@ -210,12 +211,12 @@ export {
210
211
  Ne as getContrastRatioHex,
211
212
  Ie as getDefaultOnboardingState,
212
213
  Pe as getDefaultPreferences,
213
- _e as getPreset,
214
- me as hexToRgb,
214
+ me as getPreset,
215
+ _e as hexToRgb,
215
216
  Be as incrementVersion,
216
217
  Le as isValidHexColor,
217
- Me as listPresets,
218
- we as meetsWCAG,
218
+ we as listPresets,
219
+ Me as meetsWCAG,
219
220
  je as parseBranchId,
220
221
  oe as registerResolver,
221
222
  Ue as relativeLuminance,
@@ -1,4 +1,4 @@
1
- import { forwardRef, useState } from 'react'
1
+ import { forwardRef, useEffect, useState } from 'react'
2
2
  import {
3
3
  type AvatarProps,
4
4
  AvatarSize,
@@ -33,6 +33,11 @@ const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
33
33
  ref
34
34
  ) => {
35
35
  const [imageError, setImageError] = useState(false)
36
+
37
+ useEffect(() => {
38
+ setImageError(false)
39
+ }, [src])
40
+
36
41
  const hasImage = src && !imageError
37
42
  const shouldShowSkeleton = skeleton?.show
38
43
  const variant = hasImage ? 'withImage' : 'withoutImage'
@@ -17,7 +17,7 @@ import { AvatarTokensType } from '../Avatar/avatar.tokens'
17
17
  const AvatarGroup = forwardRef<HTMLDivElement, AvatarGroupProps>(
18
18
  (
19
19
  {
20
- avatars,
20
+ avatars = [],
21
21
  maxCount = 5,
22
22
  size = AvatarSize.MD,
23
23
  shape = AvatarShape.CIRCULAR,
@@ -1,4 +1,9 @@
1
- import React, { forwardRef, useState, type ReactElement } from 'react'
1
+ import React, {
2
+ forwardRef,
3
+ useEffect,
4
+ useState,
5
+ type ReactElement,
6
+ } from 'react'
2
7
  import {
3
8
  AvatarV2Props,
4
9
  AvatarV2Size,
@@ -176,6 +181,10 @@ const AvatarV2 = forwardRef<HTMLDivElement, AvatarV2Props>(
176
181
  ) => {
177
182
  const [imageError, setImageError] = useState(false)
178
183
 
184
+ useEffect(() => {
185
+ setImageError(false)
186
+ }, [src])
187
+
179
188
  const tokens = useResponsiveTokens<AvatarV2TokensType>('AVATARV2')
180
189
 
181
190
  const hasImage = src && !imageError
@@ -5,7 +5,12 @@ import PrimitiveButton from '../Primitives/PrimitiveButton/PrimitiveButton'
5
5
  import PrimitiveText from '../Primitives/PrimitiveText/PrimitiveText'
6
6
  import { FOUNDATION_THEME } from '../../tokens'
7
7
  import type { BreadcrumbTokenType } from './breadcrumb.tokens'
8
- import type { BreadcrumbItemType, BreadcrumbSkeletonProps } from './types'
8
+ import type {
9
+ BreadcrumbItemType,
10
+ BreadcrumbProps,
11
+ BreadcrumbSkeletonProps,
12
+ } from './types'
13
+ import { normalizeBreadcrumbItems } from './types'
9
14
  import { useResponsiveTokens } from '../../hooks/useResponsiveTokens'
10
15
  import { SkeletonVariant } from '../Skeleton'
11
16
  import BreadcrumbSkeleton from './BreadcrumbSkeleton'
@@ -111,13 +116,9 @@ const BreadcrumbItem = ({
111
116
  )
112
117
  }
113
118
 
114
- const Breadcrumb = ({
115
- items,
116
- skeleton,
117
- }: {
118
- items: BreadcrumbItemType[]
119
- skeleton?: BreadcrumbSkeletonProps
120
- }) => {
119
+ const Breadcrumb = ({ items: itemsProp, skeleton }: BreadcrumbProps) => {
120
+ const items = normalizeBreadcrumbItems(itemsProp)
121
+
121
122
  const breadcrumbTokens =
122
123
  useResponsiveTokens<BreadcrumbTokenType>('BREADCRUMB')
123
124
  if (items.length === 0) return null
@@ -5,6 +5,18 @@ export type BreadcrumbSkeletonProps = {
5
5
  variant: SkeletonVariant
6
6
  }
7
7
 
8
+ /** Replace null, undefined, or non-array `items` with `[]`. */
9
+ export function normalizeBreadcrumbItems(
10
+ items: BreadcrumbItemType[] | null | undefined
11
+ ): BreadcrumbItemType[] {
12
+ return Array.isArray(items) ? items : []
13
+ }
14
+
15
+ export type BreadcrumbProps = {
16
+ items: BreadcrumbItemType[] | null
17
+ skeleton?: BreadcrumbSkeletonProps
18
+ }
19
+
8
20
  export type BreadcrumbItemType = {
9
21
  leftSlot?: React.ReactNode
10
22
  rightSlot?: React.ReactNode
@@ -11,7 +11,7 @@ import type { ButtonTokensType } from './button.tokens'
11
11
  import { LoaderCircle } from 'lucide-react'
12
12
  import { useResponsiveTokens } from '../../hooks/useResponsiveTokens'
13
13
  import { FOUNDATION_THEME } from '../../tokens'
14
- const StyledButtonText = styled(Text)`
14
+ const StyledButtonText = styled.span`
15
15
  display: flex;
16
16
  align-items: center;
17
17
  justify-content: center;
@@ -44,6 +44,37 @@ type CustomCardComponentProps = {
44
44
  baseId?: string
45
45
  }
46
46
 
47
+ const isPlainTextContent = (content: React.ReactNode) =>
48
+ typeof content === 'string' || typeof content === 'number'
49
+
50
+ const CardBodyContent = ({
51
+ content,
52
+ contentId,
53
+ cardToken,
54
+ describedBy,
55
+ }: {
56
+ content: React.ReactNode
57
+ contentId: string
58
+ cardToken: CardTokenType
59
+ describedBy?: string
60
+ }) => {
61
+ const sharedProps = {
62
+ id: contentId,
63
+ style: getBodyContentStyles(cardToken),
64
+ 'aria-describedby': describedBy,
65
+ }
66
+
67
+ if (isPlainTextContent(content)) {
68
+ return (
69
+ <Text as="p" {...sharedProps}>
70
+ {content}
71
+ </Text>
72
+ )
73
+ }
74
+
75
+ return <Block {...sharedProps}>{content}</Block>
76
+ }
77
+
47
78
  export const DefaultCard: React.FC<CardComponentProps> = ({
48
79
  props,
49
80
  cardToken,
@@ -80,6 +111,9 @@ export const DefaultCard: React.FC<CardComponentProps> = ({
80
111
  const subHeaderId = `${baseId}-subheader`
81
112
  const bodyTitleId = `${baseId}-body-title`
82
113
  const contentId = `${baseId}-content`
114
+ const contentDataId = isPlainTextContent(content)
115
+ ? String(content)
116
+ : undefined
83
117
 
84
118
  return (
85
119
  <>
@@ -228,22 +262,20 @@ export const DefaultCard: React.FC<CardComponentProps> = ({
228
262
  ),
229
263
  }}
230
264
  data-element="card-body-content"
231
- data-id={content}
265
+ data-id={contentDataId}
232
266
  >
233
- <Text
234
- as="p"
235
- id={contentId}
236
- style={getBodyContentStyles(cardToken)}
237
- aria-describedby={
267
+ <CardBodyContent
268
+ content={content}
269
+ contentId={contentId}
270
+ cardToken={cardToken}
271
+ describedBy={
238
272
  bodyTitle
239
273
  ? bodyTitleId
240
274
  : headerTitle
241
275
  ? headerTitleId
242
276
  : undefined
243
277
  }
244
- >
245
- {content}
246
- </Text>
278
+ />
247
279
  </Block>
248
280
  )}
249
281
 
@@ -461,20 +493,18 @@ const CardContent: React.FC<{
461
493
  }
462
494
  >
463
495
  {hasContent && (
464
- <Text
465
- as="p"
466
- id={contentId}
467
- style={getBodyContentStyles(cardToken)}
468
- aria-describedby={
496
+ <CardBodyContent
497
+ content={content}
498
+ contentId={contentId}
499
+ cardToken={cardToken}
500
+ describedBy={
469
501
  bodyTitle
470
502
  ? bodyTitleId
471
503
  : headerTitle
472
504
  ? headerTitleId
473
505
  : undefined
474
506
  }
475
- >
476
- {content}
477
- </Text>
507
+ />
478
508
  )}
479
509
 
480
510
  {hasActionButton && (
@@ -612,7 +642,12 @@ export const CustomCard: React.FC<CustomCardComponentProps> = ({
612
642
 
613
643
  return (
614
644
  <Block
645
+ display="flex"
646
+ flexDirection="column"
647
+ flexGrow={1}
615
648
  style={{
649
+ minHeight: 0,
650
+ width: '100%',
616
651
  ...(maxHeight && {
617
652
  overflowY: 'auto',
618
653
  overflowX: 'hidden',
@@ -6,6 +6,13 @@ import {
6
6
  } from './types'
7
7
  import { parseTimestamp, dateFormat } from './DateTimeFormatter'
8
8
 
9
+ /** Replace null, undefined, or non-array `data` with `[]` (empty / no-data state). */
10
+ export function normalizeChartData(
11
+ data: NewNestedDataPoint[] | null | undefined
12
+ ): NewNestedDataPoint[] {
13
+ return Array.isArray(data) ? data : []
14
+ }
15
+
9
16
  export function transformNestedData(
10
17
  data: NewNestedDataPoint[],
11
18
  selectedKeys: string[] = []
@@ -5,7 +5,7 @@ import ChartHeader from './ChartHeader'
5
5
  import ChartLegends from './ChartLegend'
6
6
  import { useRef, useState, useEffect, useCallback, useId, useMemo } from 'react'
7
7
  import { renderChart } from './renderChart'
8
- import { transformNestedData } from './ChartUtils'
8
+ import { normalizeChartData, transformNestedData } from './ChartUtils'
9
9
  import Block from '../../components/Primitives/Block/Block'
10
10
  import { ChartTokensType } from './chart.tokens'
11
11
  import { FOUNDATION_THEME } from '../../tokens'
@@ -20,7 +20,7 @@ import ChartsSkeleton from './ChartsSkeleton'
20
20
 
21
21
  const Charts: React.FC<ChartsProps> = ({
22
22
  chartType = ChartType.LINE,
23
- data,
23
+ data: dataProp,
24
24
  colors,
25
25
  slot1,
26
26
  slot2,
@@ -46,6 +46,8 @@ const Charts: React.FC<ChartsProps> = ({
46
46
  lineSeriesKeys,
47
47
  ...props
48
48
  }) => {
49
+ const data = normalizeChartData(dataProp)
50
+
49
51
  const { breakPointLabel } = useBreakpoints(BREAKPOINTS)
50
52
  const isSmallScreen = breakPointLabel === 'sm'
51
53
  const chartTokens = useResponsiveTokens<ChartTokensType>('CHARTS')
@@ -3,11 +3,11 @@ import { ResponsiveContainer } from 'recharts'
3
3
  import { ChartType, CoreChartProps } from './types'
4
4
  import { DEFAULT_COLORS } from './utils'
5
5
  import { renderChart } from './renderChart'
6
- import { transformNestedData } from './ChartUtils'
6
+ import { normalizeChartData, transformNestedData } from './ChartUtils'
7
7
 
8
8
  export const CoreChart: React.FC<CoreChartProps> = ({
9
9
  chartType = ChartType.LINE,
10
- data,
10
+ data: dataProp,
11
11
  colors = DEFAULT_COLORS,
12
12
  barsize,
13
13
  xAxis,
@@ -22,6 +22,8 @@ export const CoreChart: React.FC<CoreChartProps> = ({
22
22
  enableHover = false,
23
23
  lineSeriesKeys,
24
24
  }) => {
25
+ const data = normalizeChartData(dataProp)
26
+
25
27
  const [internalHoveredKey, setInternalHoveredKey] = useState<string | null>(
26
28
  null
27
29
  )
@@ -151,7 +151,7 @@ export type RenderChartProps = {
151
151
 
152
152
  export type CoreChartProps = {
153
153
  chartType?: ChartType
154
- data: NewNestedDataPoint[]
154
+ data?: NewNestedDataPoint[] | null
155
155
  colors?: { key: string; color: string }[]
156
156
  barsize?: number
157
157
  xAxis?: XAxisConfig
@@ -175,7 +175,7 @@ export type ChartsSkeletonProps = {
175
175
 
176
176
  export type ChartsProps = {
177
177
  chartType?: ChartType
178
- data: NewNestedDataPoint[]
178
+ data?: NewNestedDataPoint[] | null
179
179
  colors?: { key: string; color: string }[]
180
180
  slot1?: ReactNode
181
181
  slot2?: ReactNode
@@ -32,7 +32,7 @@ const ChartV2 = forwardRef<ChartV2ReactRefObject, ChartV2Props>(
32
32
  ref
33
33
  ) => {
34
34
  const tokens = useResponsiveTokens<ChartV2TokensType>('CHARTSV2')
35
- const { options, ...restProps } = props
35
+ const { options = {}, ...restProps } = props
36
36
 
37
37
  const hasSeriesData =
38
38
  (options.series as ChartV2SeriesOptionsType[] | undefined)?.some(
@@ -1,4 +1,4 @@
1
- import { forwardRef, useId } from 'react'
1
+ import { forwardRef, useEffect, useId, useState } from 'react'
2
2
  import { Check, Minus } from 'lucide-react'
3
3
  import type { CheckboxProps } from './types'
4
4
  import { CheckboxSize } from './types'
@@ -46,6 +46,26 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
46
46
  const generatedId = useId()
47
47
  const uniqueId = id || generatedId
48
48
  const shouldShake = useErrorShake(error)
49
+ const isControlled = checked !== undefined
50
+ const [uncontrolledChecked, setUncontrolledChecked] = useState<
51
+ boolean | 'indeterminate'
52
+ >(defaultChecked ?? false)
53
+ const resolvedChecked = isControlled ? checked : uncontrolledChecked
54
+
55
+ useEffect(() => {
56
+ if (!isControlled) {
57
+ setUncontrolledChecked(defaultChecked ?? false)
58
+ }
59
+ }, [defaultChecked, isControlled])
60
+
61
+ const handleCheckedChange = (
62
+ nextChecked: boolean | 'indeterminate'
63
+ ) => {
64
+ if (!isControlled) {
65
+ setUncontrolledChecked(nextChecked)
66
+ }
67
+ onCheckedChange?.(nextChecked)
68
+ }
49
69
 
50
70
  const labelMaxLength = maxLength?.label
51
71
  const subtextMaxLength = maxLength?.subtext
@@ -76,28 +96,30 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
76
96
  id={uniqueId}
77
97
  name={name}
78
98
  ref={ref}
79
- checked={checked ?? defaultChecked ?? false}
80
- onCheckedChange={onCheckedChange}
99
+ {...(isControlled
100
+ ? { checked }
101
+ : { defaultChecked: defaultChecked ?? false })}
102
+ onCheckedChange={handleCheckedChange}
81
103
  disabled={disabled}
82
104
  required={required}
83
105
  size={size}
84
106
  $isDisabled={disabled}
85
- $checked={checked ?? defaultChecked ?? false}
107
+ $checked={resolvedChecked}
86
108
  $error={error}
87
109
  style={getErrorShakeStyle(shouldShake)}
88
110
  {...ariaAttributes}
89
111
  {...restProps}
90
112
  data-element="checkbox"
91
113
  data-state={
92
- checked === 'indeterminate'
114
+ resolvedChecked === 'indeterminate'
93
115
  ? 'indeterminate'
94
- : checked
116
+ : resolvedChecked
95
117
  ? 'checked'
96
118
  : 'unchecked'
97
119
  }
98
120
  >
99
121
  <CheckboxIndicator
100
- checked={checked ?? defaultChecked ?? false}
122
+ checked={resolvedChecked}
101
123
  size={size}
102
124
  tokens={tokens}
103
125
  disabled={disabled}
@@ -7,6 +7,7 @@ import {
7
7
  useCallback,
8
8
  useMemo,
9
9
  } from 'react'
10
+ import styled from 'styled-components'
10
11
  import { Check, Copy, FileCode } from 'lucide-react'
11
12
  import Block from '../Primitives/Block/Block'
12
13
  import Button from '../Button/Button'
@@ -33,6 +34,21 @@ import {
33
34
  buildDiffViewSegments,
34
35
  } from './utils'
35
36
 
37
+ const CopyOverlay = styled.div`
38
+ position: absolute;
39
+ top: 12px;
40
+ right: 12px;
41
+ z-index: 10;
42
+ opacity: 0;
43
+ pointer-events: none;
44
+ transition: opacity 0.15s ease;
45
+
46
+ .code-body:hover & {
47
+ opacity: 1;
48
+ pointer-events: auto;
49
+ }
50
+ `
51
+
36
52
  const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
37
53
  (
38
54
  {
@@ -255,7 +271,7 @@ const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
255
271
  type="button"
256
272
  onClick={copyToClipboard}
257
273
  buttonType={ButtonType.SECONDARY}
258
- subType={ButtonSubType.ICON_ONLY}
274
+ subType={ButtonSubType.INLINE}
259
275
  size={ButtonSize.SMALL}
260
276
  aria-label={
261
277
  isCopied
@@ -284,8 +300,38 @@ const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
284
300
  }
285
301
  backgroundColor={tokens.body.backgroundColor}
286
302
  overflow="auto"
303
+ position={
304
+ !showHeader && showCopyButton ? 'relative' : undefined
305
+ }
306
+ className={
307
+ !showHeader && showCopyButton ? 'code-body' : undefined
308
+ }
287
309
  style={{ maxHeight: maxHeight || 'none' }}
288
310
  >
311
+ {!showHeader && showCopyButton && (
312
+ <CopyOverlay>
313
+ <Button
314
+ data-element="copy-button"
315
+ type="button"
316
+ onClick={copyToClipboard}
317
+ buttonType={ButtonType.SECONDARY}
318
+ subType={ButtonSubType.ICON_ONLY}
319
+ size={ButtonSize.SMALL}
320
+ aria-label={
321
+ isCopied
322
+ ? 'Code copied to clipboard'
323
+ : 'Copy code'
324
+ }
325
+ leadingIcon={
326
+ isCopied ? (
327
+ <Check size={16} aria-hidden="true" />
328
+ ) : (
329
+ <Copy size={16} aria-hidden="true" />
330
+ )
331
+ }
332
+ />
333
+ </CopyOverlay>
334
+ )}
289
335
  {isDiffMode && diffLines?.length ? (
290
336
  <CodeBlockDiffView
291
337
  diffLines={diffLines}
@@ -130,9 +130,9 @@ export const getCodeBlockTokens = (
130
130
  borderBottom: `1px solid ${foundationToken.colors.gray[200]}`,
131
131
  padding: {
132
132
  x: foundationToken.unit[12],
133
- y: foundationToken.unit[10],
133
+ y: foundationToken.unit[8],
134
134
  },
135
- gap: foundationToken.unit[8],
135
+ gap: foundationToken.unit[6],
136
136
  icon: {
137
137
  width: 16,
138
138
  },
@@ -229,11 +229,11 @@ export const getCodeBlockTokens = (
229
229
  borderBottom: `1px solid ${foundationToken.colors.gray[200]}`,
230
230
  padding: {
231
231
  x: foundationToken.unit[16],
232
- y: foundationToken.unit[10],
232
+ y: foundationToken.unit[8],
233
233
  },
234
- gap: foundationToken.unit[12],
234
+ gap: foundationToken.unit[8],
235
235
  icon: {
236
- width: 20,
236
+ width: 16,
237
237
  },
238
238
  text: {
239
239
  fontSize: foundationToken.font.size.body.md.fontSize,
@@ -1,9 +1,8 @@
1
- import { forwardRef, useState } from 'react'
1
+ import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
2
2
  import Block from '../Primitives/Block/Block'
3
3
  import { useResponsiveTokens } from '../../hooks/useResponsiveTokens'
4
4
  import type { CodeBlockTokenType } from '../CodeBlock/codeBlock.token'
5
5
  import { CodeEditorVariant, type CodeEditorProps } from './types'
6
- import { createCopyToClipboard } from '../CodeBlock/utils'
7
6
  import { shouldShowLineNumbers, getContainerStyles } from './utils'
8
7
  import { CodeEditorHeader } from './CodeEditorHeader'
9
8
  import { MonacoEditorWrapper } from './MonacoEditorWrapper'
@@ -37,6 +36,31 @@ const CodeEditor = forwardRef<HTMLDivElement, CodeEditorProps>(
37
36
  ) => {
38
37
  const tokens = useResponsiveTokens<CodeBlockTokenType>('CODE_BLOCK')
39
38
  const [isCopied, setIsCopied] = useState(false)
39
+ const copyFeedbackTimeoutRef = useRef<ReturnType<
40
+ typeof setTimeout
41
+ > | null>(null)
42
+
43
+ useEffect(() => {
44
+ return () => {
45
+ if (copyFeedbackTimeoutRef.current !== null) {
46
+ clearTimeout(copyFeedbackTimeoutRef.current)
47
+ }
48
+ }
49
+ }, [])
50
+
51
+ const copyToClipboard = useCallback(() => {
52
+ navigator.clipboard.writeText(value)
53
+ setIsCopied(true)
54
+
55
+ if (copyFeedbackTimeoutRef.current !== null) {
56
+ clearTimeout(copyFeedbackTimeoutRef.current)
57
+ }
58
+
59
+ copyFeedbackTimeoutRef.current = setTimeout(() => {
60
+ setIsCopied(false)
61
+ copyFeedbackTimeoutRef.current = null
62
+ }, 2000)
63
+ }, [value])
40
64
 
41
65
  // Determine if line numbers should be shown
42
66
  const shouldShowLineNumbersValue = shouldShowLineNumbers(
@@ -44,8 +68,6 @@ const CodeEditor = forwardRef<HTMLDivElement, CodeEditorProps>(
44
68
  variant
45
69
  )
46
70
 
47
- // Handlers
48
- const copyToClipboard = createCopyToClipboard(value, setIsCopied)
49
71
  const containerStyles = getContainerStyles(minHeight, maxHeight)
50
72
 
51
73
  return (
@@ -342,6 +342,7 @@ export const MonacoEditorWrapper = ({
342
342
  const editorRef = useRef<Monaco.editor.IStandaloneCodeEditor | null>(null)
343
343
  const monacoRef = useRef<typeof import('monaco-editor') | null>(null)
344
344
  const shortcutDisposables = useRef<Monaco.IDisposable[]>([])
345
+ const focusTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
345
346
  const [isEditorReady, setIsEditorReady] = useState(false)
346
347
  const monacoLanguage = useMemo(() => mapLanguage(language), [language])
347
348
 
@@ -449,6 +450,14 @@ export const MonacoEditorWrapper = ({
449
450
  }
450
451
  }, [disposeShortcuts, isEditorReady, registerKeyboardShortcuts])
451
452
 
453
+ useEffect(() => {
454
+ return () => {
455
+ if (focusTimeoutRef.current !== null) {
456
+ clearTimeout(focusTimeoutRef.current)
457
+ }
458
+ }
459
+ }, [])
460
+
452
461
  const handleEditorDidMount: OnMount = (editor, monacoInstance) => {
453
462
  editorRef.current = editor
454
463
  monacoRef.current = monacoInstance
@@ -502,7 +511,10 @@ export const MonacoEditorWrapper = ({
502
511
  editor.onDidBlurEditorText(() => onBlur?.())
503
512
 
504
513
  if (autoFocus && !disabled && !readOnly) {
505
- setTimeout(() => editor.focus(), EDITOR_FOCUS_DELAY_MS)
514
+ focusTimeoutRef.current = setTimeout(
515
+ () => editor.focus(),
516
+ EDITOR_FOCUS_DELAY_MS
517
+ )
506
518
  }
507
519
  }
508
520