@juspay/blend-design-system 0.0.37-beta.7 → 0.0.37-beta.8

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 (59) hide show
  1. package/dist/components/CodeEditor/types.d.ts +22 -2
  2. package/dist/components/CodeEditorV2/CodeEditorV2.d.ts +1 -1
  3. package/dist/components/CodeEditorV2/codeEditorV2.types.d.ts +23 -2
  4. package/dist/components/MenuV2/MenuV2Content.d.ts +1 -0
  5. package/dist/components/MenuV2/menuV2.types.d.ts +5 -0
  6. package/dist/components/MenuV2/menuV2.utils.d.ts +10 -2
  7. package/dist/components/MultiSelect/MultiSelect.d.ts +1 -1
  8. package/dist/components/MultiSelect/MultiSelectMenu.d.ts +1 -1
  9. package/dist/components/MultiSelect/types.d.ts +2 -0
  10. package/dist/components/MultiSelectV2/MultiSelectV2.d.ts +1 -1
  11. package/dist/components/MultiSelectV2/MultiSelectV2Menu.d.ts +1 -1
  12. package/dist/components/MultiSelectV2/multiSelectV2.types.d.ts +2 -0
  13. package/dist/components/SingleSelect/SingleSelect.d.ts +1 -1
  14. package/dist/components/SingleSelect/SingleSelectMenu.d.ts +2 -1
  15. package/dist/components/SingleSelect/types.d.ts +1 -0
  16. package/dist/components/SingleSelectV2/SingleSelectV2.d.ts +1 -1
  17. package/dist/components/SingleSelectV2/SingleSelectV2Menu.d.ts +1 -1
  18. package/dist/components/SingleSelectV2/singleSelectV2.tokens.types.d.ts +8 -0
  19. package/dist/components/SingleSelectV2/singleSelectV2.types.d.ts +2 -0
  20. package/dist/components/Snackbar/Snackbar.d.ts +1 -1
  21. package/dist/components/Snackbar/snackbar.tokens.d.ts +1 -0
  22. package/dist/components/Snackbar/types.d.ts +2 -0
  23. package/dist/main.js +18043 -17940
  24. package/dist/{node-C_DmV86X.js → node-CovJEwot.js} +16 -0
  25. package/dist/node.js +1 -1
  26. package/dist/{tokens.js → token-engine.js} +1 -1
  27. package/lib/components/CodeEditor/MonacoEditorWrapper.tsx +6 -2
  28. package/lib/components/CodeEditor/types.ts +52 -2
  29. package/lib/components/CodeEditorV2/MonacoEditor/MonacoEditorWrapper.tsx +6 -2
  30. package/lib/components/CodeEditorV2/codeEditorV2.types.ts +44 -5
  31. package/lib/components/DateRangePicker/CalendarGrid.tsx +3 -0
  32. package/lib/components/DateRangePicker/DateRangePicker.tsx +5 -1
  33. package/lib/components/MenuV2/MenuV2.tsx +9 -2
  34. package/lib/components/MenuV2/MenuV2Content.tsx +6 -0
  35. package/lib/components/MenuV2/MenuV2SubMenu.tsx +16 -2
  36. package/lib/components/MenuV2/menuV2.types.ts +11 -0
  37. package/lib/components/MenuV2/menuV2.utils.ts +45 -3
  38. package/lib/components/MultiSelect/MultiSelect.tsx +2 -0
  39. package/lib/components/MultiSelect/MultiSelectMenu.tsx +8 -0
  40. package/lib/components/MultiSelect/types.ts +2 -0
  41. package/lib/components/MultiSelectV2/MultiSelectV2.tsx +2 -0
  42. package/lib/components/MultiSelectV2/MultiSelectV2Menu.tsx +8 -0
  43. package/lib/components/MultiSelectV2/multiSelectV2.types.ts +2 -0
  44. package/lib/components/SingleSelect/SingleSelect.tsx +2 -0
  45. package/lib/components/SingleSelect/SingleSelectMenu.tsx +171 -138
  46. package/lib/components/SingleSelect/types.ts +1 -0
  47. package/lib/components/SingleSelectV2/SingleSelectV2.tsx +2 -0
  48. package/lib/components/SingleSelectV2/SingleSelectV2Menu.tsx +84 -58
  49. package/lib/components/SingleSelectV2/SingleSelectV2Search.tsx +1 -0
  50. package/lib/components/SingleSelectV2/singleSelectV2.dark.tokens.ts +8 -0
  51. package/lib/components/SingleSelectV2/singleSelectV2.light.tokens.ts +8 -0
  52. package/lib/components/SingleSelectV2/singleSelectV2.tokens.types.ts +8 -0
  53. package/lib/components/SingleSelectV2/singleSelectV2.types.ts +2 -0
  54. package/lib/components/Snackbar/Snackbar.tsx +18 -8
  55. package/lib/components/Snackbar/snackbar.tokens.ts +6 -0
  56. package/lib/components/Snackbar/types.ts +2 -0
  57. package/lib/components/StatCard/StatCard.tsx +29 -0
  58. package/package.json +5 -4
  59. package/dist/tokens.d.ts +0 -2
@@ -7082,6 +7082,14 @@ const f = (r) => {
7082
7082
  color: r.colors.gray[600]
7083
7083
  },
7084
7084
  iconColor: r.colors.gray[400]
7085
+ },
7086
+ footer: {
7087
+ paddingTop: r.unit[16],
7088
+ paddingRight: r.unit[16],
7089
+ paddingBottom: r.unit[16],
7090
+ paddingLeft: r.unit[16],
7091
+ backgroundColor: r.colors.gray[0],
7092
+ borderTop: `${r.border.width[1]} solid ${r.colors.gray[200]}`
7085
7093
  }
7086
7094
  },
7087
7095
  mobilePanel: {
@@ -7428,6 +7436,14 @@ const f = (r) => {
7428
7436
  color: r.colors.gray[200]
7429
7437
  },
7430
7438
  iconColor: r.colors.gray[500]
7439
+ },
7440
+ footer: {
7441
+ paddingTop: r.unit[16],
7442
+ paddingRight: r.unit[16],
7443
+ paddingBottom: r.unit[16],
7444
+ paddingLeft: r.unit[16],
7445
+ backgroundColor: r.colors.gray[800],
7446
+ borderTop: `${r.border.width[1]} solid ${r.colors.gray[700]}`
7431
7447
  }
7432
7448
  },
7433
7449
  mobilePanel: {
package/dist/node.js CHANGED
@@ -1,4 +1,4 @@
1
- import { F as a, T as o, y as t, x as T, w as n, v as g, z as k, u as V, t as r, s as i, r as l, q as c, g as b, p as d, o as u, n as S, m as p, a as h, l as m, j as v, i as x, k as A, h as C, f as M, e as B, d as E, c as N, b as P } from "./node-C_DmV86X.js";
1
+ import { F as a, T as o, y as t, x as T, w as n, v as g, z as k, u as V, t as r, s as i, r as l, q as c, g as b, p as d, o as u, n as S, m as p, a as h, l as m, j as v, i as x, k as A, h as C, f as M, e as B, d as E, c as N, b as P } from "./node-CovJEwot.js";
2
2
  export {
3
3
  a as FOUNDATION_THEME,
4
4
  o as Theme,
@@ -1,4 +1,4 @@
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-C_DmV86X.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-CovJEwot.js";
2
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,9 +5,13 @@ import Block from '../Primitives/Block/Block'
5
5
  import type { CodeBlockTokenType } from '../CodeBlock/codeBlock.token'
6
6
  import './monaco-editor.css'
7
7
 
8
+ // Monaco registers only `javascript`/`typescript` (VS Code's
9
+ // `javascriptreact`/`typescriptreact` IDs are not registered, so they would
10
+ // fall back to plaintext with no highlighting). Monaco's JS/TS tokenizers
11
+ // handle JSX/TSX syntax fine, so map the ergonomic aliases onto them.
8
12
  const LANGUAGE_MAP: Record<string, string> = {
9
- jsx: 'javascriptreact',
10
- tsx: 'typescriptreact',
13
+ jsx: 'javascript',
14
+ tsx: 'typescript',
11
15
  }
12
16
 
13
17
  const mapLanguage = (value: string) => LANGUAGE_MAP[value] ?? value
@@ -1,11 +1,61 @@
1
1
  import type { ReactNode } from 'react'
2
- import type { SupportedLanguage } from '../CodeBlock/types'
3
2
 
4
3
  export enum CodeEditorVariant {
5
4
  DEFAULT = 'default',
6
5
  NO_GUTTER = 'no-gutter',
7
6
  }
8
7
 
8
+ /**
9
+ * Common Monaco language IDs, provided purely for IDE autocomplete on the
10
+ * `language` prop — NOT a runtime constraint. Every entry is verified against
11
+ * the language IDs monaco-editor actually registers (Monaco does not export a
12
+ * static union of its built-in IDs; they are only enumerable at runtime via
13
+ * `monaco.languages.getLanguages()`). Note Monaco's plain-text ID is
14
+ * `plaintext` and shell scripts are `shell` (there is no `bash` ID). Monaco
15
+ * also has no distinct `jsx`/`tsx` IDs — those still work through the open
16
+ * string arm of {@link CodeEditorLanguage}: the editor maps them onto the
17
+ * `javascript`/`typescript` tokenizers.
18
+ */
19
+ export type KnownCodeEditorLanguage =
20
+ | 'javascript'
21
+ | 'typescript'
22
+ | 'json'
23
+ | 'css'
24
+ | 'scss'
25
+ | 'less'
26
+ | 'html'
27
+ | 'xml'
28
+ | 'markdown'
29
+ | 'yaml'
30
+ | 'ini'
31
+ | 'graphql'
32
+ | 'sql'
33
+ | 'python'
34
+ | 'rust'
35
+ | 'go'
36
+ | 'java'
37
+ | 'kotlin'
38
+ | 'swift'
39
+ | 'c'
40
+ | 'cpp'
41
+ | 'csharp'
42
+ | 'php'
43
+ | 'ruby'
44
+ | 'shell'
45
+ | 'powershell'
46
+ | 'dockerfile'
47
+ | 'plaintext'
48
+
49
+ /**
50
+ * Language accepted by {@link CodeEditorProps.language}: autocomplete for
51
+ * {@link KnownCodeEditorLanguage}, while `(string & {})` keeps the prop open
52
+ * to every other Monaco language ID (the component forwards to Monaco, whose
53
+ * own `language` option is a plain `string`). Decoupled from `CodeBlock`'s
54
+ * `SupportedLanguage`, which is scoped to CodeBlock's hand-rolled tokenizer
55
+ * rather than Monaco.
56
+ */
57
+ export type CodeEditorLanguage = KnownCodeEditorLanguage | (string & {})
58
+
9
59
  export type CodeEditorProps = {
10
60
  value: string
11
61
  onChange?: (value: string) => void
@@ -30,7 +80,7 @@ export type CodeEditorProps = {
30
80
  */
31
81
  showLeftIcon?: boolean
32
82
  showCopyButton?: boolean
33
- language?: SupportedLanguage
83
+ language?: CodeEditorLanguage
34
84
  placeholder?: string
35
85
  readOnly?: boolean
36
86
  disabled?: boolean
@@ -25,9 +25,13 @@ import { CSSObject } from 'styled-components'
25
25
  // Language mapping
26
26
  // ---------------------------------------------------------------------------
27
27
 
28
+ // Monaco registers only `javascript`/`typescript` (VS Code's
29
+ // `javascriptreact`/`typescriptreact` IDs are not registered, so they would
30
+ // fall back to plaintext with no highlighting). Monaco's JS/TS tokenizers
31
+ // handle JSX/TSX syntax fine, so map the ergonomic aliases onto them.
28
32
  const LANGUAGE_MAP: Record<string, string> = {
29
- jsx: 'javascriptreact',
30
- tsx: 'typescriptreact',
33
+ jsx: 'javascript',
34
+ tsx: 'typescript',
31
35
  }
32
36
 
33
37
  const mapLanguage = (lang: string) => LANGUAGE_MAP[lang] ?? lang
@@ -21,19 +21,58 @@ export type CodeEditorV2DiffLine = {
21
21
  type: CodeEditorV2DiffLineType
22
22
  }
23
23
 
24
- export type CodeEditorV2SupportedLanguage =
24
+ /**
25
+ * Common Monaco language IDs, provided purely for IDE autocomplete on the
26
+ * `language` prop — NOT a runtime constraint. Every entry is verified against
27
+ * the language IDs monaco-editor actually registers (Monaco does not export a
28
+ * static union of its built-in IDs; they are only enumerable at runtime via
29
+ * `monaco.languages.getLanguages()`). Note Monaco's plain-text ID is
30
+ * `plaintext` and shell scripts are `shell` (there is no `bash` ID). Monaco
31
+ * also has no distinct `jsx`/`tsx` IDs — those still work through the open
32
+ * string arm of {@link CodeEditorV2Language}: the editor maps them onto the
33
+ * `javascript`/`typescript` tokenizers.
34
+ */
35
+ export type KnownCodeEditorV2Language =
25
36
  | 'javascript'
26
37
  | 'typescript'
27
- | 'jsx'
28
- | 'tsx'
29
38
  | 'json'
30
39
  | 'css'
40
+ | 'scss'
41
+ | 'less'
31
42
  | 'html'
43
+ | 'xml'
32
44
  | 'markdown'
33
45
  | 'yaml'
46
+ | 'ini'
47
+ | 'graphql'
48
+ | 'sql'
34
49
  | 'python'
35
50
  | 'rust'
36
- | 'haskell'
51
+ | 'go'
52
+ | 'java'
53
+ | 'kotlin'
54
+ | 'swift'
55
+ | 'c'
56
+ | 'cpp'
57
+ | 'csharp'
58
+ | 'php'
59
+ | 'ruby'
60
+ | 'shell'
61
+ | 'powershell'
62
+ | 'dockerfile'
63
+ | 'plaintext'
64
+
65
+ /**
66
+ * Language accepted by {@link CodeEditorV2Props.language}: autocomplete for
67
+ * {@link KnownCodeEditorV2Language}, while `(string & {})` keeps the prop open
68
+ * to every other Monaco language ID (matching the internal
69
+ * `MonacoEditorWrapper`'s `language: string` — Monaco itself types the option
70
+ * as a plain `string`).
71
+ */
72
+ export type CodeEditorV2Language = KnownCodeEditorV2Language | (string & {})
73
+
74
+ /** @deprecated Use {@link CodeEditorV2Language} instead. */
75
+ export type CodeEditorV2SupportedLanguage = CodeEditorV2Language
37
76
 
38
77
  export type CodeEditorV2Dimensions = {
39
78
  width?: CSSObject['width']
@@ -56,7 +95,7 @@ export type CodeEditorV2Props = {
56
95
  rightSlot?: ReactNode
57
96
  showCopyButton?: boolean
58
97
  }
59
- language?: CodeEditorV2SupportedLanguage
98
+ language?: CodeEditorV2Language
60
99
  placeholder?: string
61
100
  readOnly?: boolean
62
101
  disabled?: boolean
@@ -129,6 +129,7 @@ const CalendarSkeleton = ({
129
129
  <Block
130
130
  style={{
131
131
  maxHeight: CONTAINER_HEIGHT,
132
+ height: '100%',
132
133
  padding: FOUNDATION_THEME.unit[16],
133
134
  }}
134
135
  >
@@ -918,6 +919,7 @@ const CalendarGrid = forwardRef<HTMLDivElement, CalendarGridProps>(
918
919
  <MotionBlock
919
920
  style={{
920
921
  maxHeight: CONTAINER_HEIGHT,
922
+ height: '100%',
921
923
  overflowY: 'auto',
922
924
  overflow: 'auto',
923
925
  position: 'relative',
@@ -985,6 +987,7 @@ const CalendarGrid = forwardRef<HTMLDivElement, CalendarGridProps>(
985
987
  }}
986
988
  style={{
987
989
  maxHeight: CONTAINER_HEIGHT,
990
+ height: '100%',
988
991
  overflowY: 'auto',
989
992
  overflow: 'auto',
990
993
  position: 'relative',
@@ -292,7 +292,7 @@ const CalendarSection: React.FC<
292
292
  isSingleDatePicker,
293
293
  maxYearOffset,
294
294
  }) => (
295
- <Block>
295
+ <Block flexGrow={1} minHeight={0} overflow="auto">
296
296
  <CalendarGrid
297
297
  selectedRange={selectedRange}
298
298
  onDateSelect={onDateSelect}
@@ -1228,6 +1228,10 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
1228
1228
  style={{
1229
1229
  ...calendarToken.calendar,
1230
1230
  }}
1231
+ maxHeight="var(--radix-popper-available-height)"
1232
+ display="flex"
1233
+ flexDirection="column"
1234
+ overflow="hidden"
1231
1235
  >
1232
1236
  {showDateInput && (
1233
1237
  <DateInputsSection
@@ -23,6 +23,8 @@ const MenuV2 = React.forwardRef<HTMLDivElement, MenuV2Props>(
23
23
  dimensions = {} as MenuV2Dimensions,
24
24
  enableSearch = false,
25
25
  searchPlaceholder = 'Search',
26
+ searchSortFn,
27
+ onEnter,
26
28
  enableVirtualScrolling = false,
27
29
  virtualScrolling,
28
30
  open: controlledOpen,
@@ -61,10 +63,14 @@ const MenuV2 = React.forwardRef<HTMLDivElement, MenuV2Props>(
61
63
  )
62
64
 
63
65
  const filteredItems = useMemo(
64
- () => filterMenuV2Groups(items, searchText),
65
- [items, searchText]
66
+ () => filterMenuV2Groups(items, searchText, searchSortFn),
67
+ [items, searchText, searchSortFn]
66
68
  )
67
69
 
70
+ const handleSearchEnter = useCallback(() => {
71
+ onEnter?.(searchText, filteredItems)
72
+ }, [onEnter, searchText, filteredItems])
73
+
68
74
  const handleInteractOutside = useCallback((e: unknown) => {
69
75
  const event = e as {
70
76
  target: HTMLElement | null
@@ -104,6 +110,7 @@ const MenuV2 = React.forwardRef<HTMLDivElement, MenuV2Props>(
104
110
  searchPlaceholder={searchPlaceholder}
105
111
  searchText={searchText}
106
112
  onSearchTextChange={setSearchText}
113
+ onEnter={handleSearchEnter}
107
114
  maxHeight={
108
115
  dimensions.maxHeight as CSSObject['maxHeight']
109
116
  }
@@ -104,6 +104,7 @@ export type MenuV2ContentProps = {
104
104
  searchPlaceholder: string
105
105
  searchText: string
106
106
  onSearchTextChange: (value: string) => void
107
+ onEnter?: () => void
107
108
  maxHeight?: CSSObject['maxHeight']
108
109
  minHeight?: CSSObject['minHeight']
109
110
  minWidth?: CSSObject['minWidth']
@@ -130,6 +131,7 @@ const MenuV2Content = React.forwardRef<HTMLDivElement, MenuV2ContentProps>(
130
131
  searchPlaceholder,
131
132
  searchText,
132
133
  onSearchTextChange,
134
+ onEnter,
133
135
  maxHeight,
134
136
  minHeight,
135
137
  minWidth: minWidthProp,
@@ -250,6 +252,10 @@ const MenuV2Content = React.forwardRef<HTMLDivElement, MenuV2ContentProps>(
250
252
  onChange={(e) => onSearchTextChange(e.target.value)}
251
253
  onKeyDown={(e) => {
252
254
  e.stopPropagation()
255
+ if (e.key === 'Enter') {
256
+ e.preventDefault()
257
+ onEnter?.()
258
+ }
253
259
  }}
254
260
  aria-label={
255
261
  searchPlaceholder
@@ -14,6 +14,7 @@ import {
14
14
  getMenuItemDescriptionColor,
15
15
  filterMenuV2Item,
16
16
  getItemSlots,
17
+ defaultSearchSortFn,
17
18
  } from './menuV2.utils'
18
19
  import { menuV2SubmenuContentAnimations } from './menuV2.animations'
19
20
  import { useResponsiveTokens } from '../../hooks/useResponsiveTokens'
@@ -64,12 +65,15 @@ const MenuV2SubMenu = ({ item, index, maxHeight }: MenuV2SubMenuProps) => {
64
65
 
65
66
  const lower = searchText.toLowerCase()
66
67
 
67
- return item.subMenu.reduce<MenuV2ItemType[]>((acc, sub) => {
68
+ const matched = item.subMenu.reduce<MenuV2ItemType[]>((acc, sub) => {
68
69
  const result = filterMenuV2Item(sub, lower)
69
70
  if (result) acc.push(result)
70
71
  return acc
71
72
  }, [])
72
- }, [item.subMenu, searchText])
73
+
74
+ const sortFn = item.subMenuSearchSortFn ?? defaultSearchSortFn
75
+ return sortFn(matched, searchText)
76
+ }, [item.subMenu, searchText, item.subMenuSearchSortFn])
73
77
 
74
78
  const {
75
79
  bgDefault,
@@ -262,6 +266,16 @@ const MenuV2SubMenu = ({ item, index, maxHeight }: MenuV2SubMenuProps) => {
262
266
  }
263
267
  value={searchText}
264
268
  onChange={(e) => setSearchText(e.target.value)}
269
+ onKeyDown={(e) => {
270
+ e.stopPropagation()
271
+ if (e.key === 'Enter') {
272
+ e.preventDefault()
273
+ item.onSubMenuSearchEnter?.(
274
+ searchText,
275
+ filteredSubMenuItems
276
+ )
277
+ }
278
+ }}
265
279
  aria-label="Search submenu"
266
280
  />
267
281
  </Block>
@@ -38,6 +38,10 @@ export type MenuV2ItemLabel = {
38
38
  text: string
39
39
  leftSlot?: React.ReactElement
40
40
  }
41
+ export type MenuV2SearchSortFn = (
42
+ items: MenuV2ItemType[],
43
+ searchText: string
44
+ ) => MenuV2ItemType[]
41
45
  export type MenuV2ItemType = {
42
46
  id?: string
43
47
  label: MenuV2ItemLabel
@@ -49,6 +53,11 @@ export type MenuV2ItemType = {
49
53
  subMenu?: MenuV2ItemType[]
50
54
  enableSubMenuSearch?: boolean
51
55
  subMenuSearchPlaceholder?: string
56
+ subMenuSearchSortFn?: MenuV2SearchSortFn
57
+ onSubMenuSearchEnter?: (
58
+ searchText: string,
59
+ filteredResults: MenuV2ItemType[]
60
+ ) => void
52
61
  tooltip?: string | ReactNode
53
62
  tooltipProps?: MenuV2ItemTooltipProps
54
63
  }
@@ -82,6 +91,8 @@ export type MenuV2Props = {
82
91
  dimensions?: MenuV2Dimensions
83
92
  enableSearch?: boolean
84
93
  searchPlaceholder?: string
94
+ searchSortFn?: MenuV2SearchSortFn
95
+ onEnter?: (searchText: string, filteredGroups: MenuV2GroupType[]) => void
85
96
  enableVirtualScrolling?: boolean
86
97
  virtualScrolling?: MenuV2VirtualScrollingConfig
87
98
  open?: boolean
@@ -1,5 +1,9 @@
1
1
  import type { ReactNode } from 'react'
2
- import type { MenuV2GroupType, MenuV2ItemType } from './menuV2.types'
2
+ import type {
3
+ MenuV2GroupType,
4
+ MenuV2ItemType,
5
+ MenuV2SearchSortFn,
6
+ } from './menuV2.types'
3
7
  import type { MenuV2TokensType } from './menuV2.tokens'
4
8
 
5
9
  import { MenuV2ItemActionType, MenuV2ItemVariant } from './menuV2.types'
@@ -11,6 +15,43 @@ export const getItemSlots = (item: MenuV2ItemType): [ReactNode?] => {
11
15
  return [item.label.leftSlot]
12
16
  }
13
17
 
18
+ export enum MenuV2MatchRank {
19
+ EXACT = 0,
20
+ PREFIX = 1,
21
+ SUBSTRING = 2,
22
+ NONE = 3,
23
+ }
24
+
25
+ const getFieldMatchRank = (
26
+ fieldValue: string | undefined,
27
+ lower: string
28
+ ): MenuV2MatchRank => {
29
+ if (!fieldValue) return MenuV2MatchRank.NONE
30
+ const value = fieldValue.toLowerCase()
31
+ if (value === lower) return MenuV2MatchRank.EXACT
32
+ if (value.startsWith(lower)) return MenuV2MatchRank.PREFIX
33
+ if (value.includes(lower)) return MenuV2MatchRank.SUBSTRING
34
+ return MenuV2MatchRank.NONE
35
+ }
36
+
37
+ export const getItemMatchRank = (
38
+ item: MenuV2ItemType,
39
+ lower: string
40
+ ): MenuV2MatchRank => {
41
+ return Math.min(
42
+ getFieldMatchRank(item.label.text, lower),
43
+ getFieldMatchRank(item.subLabel, lower)
44
+ ) as MenuV2MatchRank
45
+ }
46
+
47
+ export const defaultSearchSortFn: MenuV2SearchSortFn = (items, searchText) => {
48
+ if (!searchText.trim()) return items
49
+ const lower = searchText.toLowerCase()
50
+ return [...items].sort(
51
+ (a, b) => getItemMatchRank(a, lower) - getItemMatchRank(b, lower)
52
+ )
53
+ }
54
+
14
55
  export const filterMenuV2Item = (
15
56
  item: MenuV2ItemType,
16
57
  lower: string
@@ -140,7 +181,8 @@ export const flattenMenuV2Groups = (
140
181
 
141
182
  export const filterMenuV2Groups = (
142
183
  groups: MenuV2GroupType[],
143
- searchText: string
184
+ searchText: string,
185
+ searchSortFn: MenuV2SearchSortFn = defaultSearchSortFn
144
186
  ): MenuV2GroupType[] => {
145
187
  if (!searchText) return groups
146
188
  const lower = searchText.toLowerCase()
@@ -150,7 +192,7 @@ export const filterMenuV2Groups = (
150
192
  .map((item) => filterMenuV2Item(item, lower))
151
193
  .filter(Boolean) as MenuV2ItemType[]
152
194
  if (filteredItems.length === 0) return null
153
- return { ...group, items: filteredItems }
195
+ return { ...group, items: searchSortFn(filteredItems, searchText) }
154
196
  })
155
197
  .filter(Boolean) as MenuV2GroupType[]
156
198
  }
@@ -101,6 +101,7 @@ const MultiSelect = ({
101
101
  onClearAllClick,
102
102
  onOpenChange,
103
103
  multiSelectGroupPosition,
104
+ menuFooter,
104
105
  ...rest
105
106
  }: MultiSelectProps) => {
106
107
  const { onFocus, onBlur, ...buttonRest } = rest
@@ -356,6 +357,7 @@ const MultiSelect = ({
356
357
  hasMore={hasMore}
357
358
  loadingComponent={loadingComponent}
358
359
  menuId={menuId}
360
+ menuFooter={menuFooter}
359
361
  trigger={
360
362
  customTrigger || (
361
363
  <PrimitiveButton
@@ -86,6 +86,10 @@ const FixedActionButtons = styled(Block)(() => ({
86
86
  flexShrink: 0,
87
87
  }))
88
88
 
89
+ const MenuFooter = styled(Block)(() => ({
90
+ flexShrink: 0,
91
+ }))
92
+
89
93
  type FlattenedMultiSelectItem = VirtualListItem & {
90
94
  type: 'item' | 'label' | 'separator'
91
95
  item?: MultiSelectMenuItemType
@@ -171,6 +175,7 @@ const MultiSelectMenu = ({
171
175
  allowCustomValue = false,
172
176
  customValueLabel = 'Specify',
173
177
  menuId,
178
+ menuFooter,
174
179
  }: MultiSelectMenuProps) => {
175
180
  const multiSelectTokens =
176
181
  useResponsiveTokens<MultiSelectTokensType>('MULTI_SELECT')
@@ -892,6 +897,9 @@ const MultiSelectMenu = ({
892
897
  )}
893
898
  </FixedActionButtons>
894
899
  )}
900
+ {menuFooter && (
901
+ <MenuFooter>{menuFooter}</MenuFooter>
902
+ )}
895
903
  </>
896
904
  )}
897
905
  </Content>
@@ -157,6 +157,7 @@ export type MultiSelectProps = {
157
157
  showClearButton?: boolean
158
158
  onClearAllClick?: () => void
159
159
  multiSelectGroupPosition?: 'center' | 'left' | 'right'
160
+ menuFooter?: React.ReactNode
160
161
  } & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'slot' | 'onChange'>
161
162
 
162
163
  // Multi Select Menu Dropdpown
@@ -225,4 +226,5 @@ export type MultiSelectMenuProps = {
225
226
  customValueLabel?: string
226
227
  menuId?: string
227
228
  collisionBoundary?: Element | null | Array<Element | null>
229
+ menuFooter?: React.ReactNode
228
230
  }
@@ -91,6 +91,7 @@ const MultiSelectV2 = ({
91
91
  onClearAllClick,
92
92
  onOpenChange,
93
93
  multiSelectGroupPosition,
94
+ menuFooter,
94
95
  ...rest
95
96
  }: MultiSelectV2Props) => {
96
97
  const { disabled, name, ...buttonRest } = rest as {
@@ -366,6 +367,7 @@ const MultiSelectV2 = ({
366
367
  }
367
368
  allowCustomValue={allowCustomValue}
368
369
  customValueLabel={customValueLabel}
370
+ menuFooter={menuFooter}
369
371
  />
370
372
 
371
373
  {shouldShowClearButton && (
@@ -69,6 +69,10 @@ const ScrollableContent = styled(Block)(() => ({
69
69
  },
70
70
  }))
71
71
 
72
+ const MenuFooter = styled(Block)`
73
+ flex-shrink: 0;
74
+ `
75
+
72
76
  const MultiSelectV2Menu = ({
73
77
  items,
74
78
  selected,
@@ -104,6 +108,7 @@ const MultiSelectV2Menu = ({
104
108
  allowCustomValue = false,
105
109
  customValueLabel = 'Specify',
106
110
  menuId,
111
+ menuFooter,
107
112
  }: MultiSelectV2MenuProps) => {
108
113
  const multiSelectTokens =
109
114
  useResponsiveTokens<MultiSelectV2TokensType>('MULTI_SELECT_V2')
@@ -467,6 +472,9 @@ const MultiSelectV2Menu = ({
467
472
  onClose={() => handleOpenChange(false)}
468
473
  />
469
474
  )}
475
+ {menuFooter && (
476
+ <MenuFooter>{menuFooter}</MenuFooter>
477
+ )}
470
478
  </>
471
479
  )}
472
480
  </Content>
@@ -97,6 +97,7 @@ export type MultiSelectV2MenuProps = {
97
97
  allowCustomValue?: boolean
98
98
  customValueLabel?: string
99
99
  menuId?: string
100
+ menuFooter?: ReactNode
100
101
  }
101
102
 
102
103
  export type MultiSelectV2MenuRootProps = SelectV2MenuRootPropsBase & {
@@ -170,4 +171,5 @@ export type MultiSelectV2Props = Omit<
170
171
  showClearButton?: boolean
171
172
  onClearAllClick?: () => void
172
173
  multiSelectGroupPosition?: 'center' | 'left' | 'right'
174
+ menuFooter?: ReactNode
173
175
  }
@@ -104,6 +104,7 @@ const SingleSelect = ({
104
104
  customValueLabel = 'Specify',
105
105
  singleSelectGroupPosition,
106
106
  allowDeselect = false,
107
+ menuFooter,
107
108
  ...rest
108
109
  }: SingleSelectProps) => {
109
110
  const { onFocus, onBlur, ...buttonRest } = rest
@@ -298,6 +299,7 @@ const SingleSelect = ({
298
299
  allowCustomValue={allowCustomValue}
299
300
  customValueLabel={customValueLabel}
300
301
  menuId={menuId}
302
+ menuFooter={menuFooter}
301
303
  trigger={
302
304
  customTrigger || (
303
305
  <PrimitiveButton