@drivenets/design-system 0.18.1 → 0.20.0

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 (216) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/index.cjs +17038 -10114
  3. package/dist/index.d.cts +1355 -444
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +1354 -443
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +17073 -10172
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +41 -41
  11. package/src/components/ds-bot-button/ds-bot-button.figma.ts +38 -0
  12. package/src/components/ds-bot-button/ds-bot-button.module.scss +41 -0
  13. package/src/components/ds-bot-button/ds-bot-button.tsx +48 -0
  14. package/src/components/ds-bot-button/ds-bot-button.types.ts +33 -0
  15. package/src/components/ds-bot-button/index.ts +2 -0
  16. package/src/components/ds-bot-button/net-gen-logo.tsx +46 -0
  17. package/src/components/ds-bulk-actions/components/action-button.tsx +42 -0
  18. package/src/components/ds-bulk-actions/components/menu-entries.tsx +76 -0
  19. package/src/components/ds-bulk-actions/components/overflow.tsx +75 -0
  20. package/src/components/ds-bulk-actions/ds-bulk-actions.figma.ts +28 -0
  21. package/src/components/ds-bulk-actions/ds-bulk-actions.module.scss +199 -0
  22. package/src/components/ds-bulk-actions/ds-bulk-actions.tsx +220 -0
  23. package/src/components/ds-bulk-actions/ds-bulk-actions.types.ts +170 -0
  24. package/src/components/ds-bulk-actions/ds-bulk-actions.unit.test.ts +195 -0
  25. package/src/components/ds-bulk-actions/ds-bulk-actions.utils.ts +91 -0
  26. package/src/components/ds-bulk-actions/ds-bulk-actions.utils.unit.test.ts +134 -0
  27. package/src/components/ds-bulk-actions/hooks/compute-bulk-actions-overflow.ts +85 -0
  28. package/src/components/ds-bulk-actions/hooks/use-bulk-actions-overflow.ts +153 -0
  29. package/src/components/ds-bulk-actions/index.ts +18 -0
  30. package/src/components/ds-button-v3/ds-button-v3.module.scss +2 -2
  31. package/src/components/ds-catalog-layout/ds-catalog-layout.figma.ts +1 -1
  32. package/src/components/ds-checkbox/ds-checkbox.figma.ts +3 -0
  33. package/src/components/ds-checkbox/ds-checkbox.module.scss +102 -20
  34. package/src/components/ds-checkbox/ds-checkbox.tsx +30 -2
  35. package/src/components/ds-checkbox/ds-checkbox.types.ts +10 -0
  36. package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.module.scss +169 -0
  37. package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.tsx +92 -0
  38. package/src/components/ds-code-input/collapsed-code-field/index.ts +1 -0
  39. package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.module.scss +110 -0
  40. package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.tsx +61 -0
  41. package/src/components/ds-code-input/ds-code-input-panel/index.ts +1 -0
  42. package/src/components/ds-code-input/ds-code-input.module.scss +25 -0
  43. package/src/components/ds-code-input/ds-code-input.tsx +401 -0
  44. package/src/components/ds-code-input/ds-code-input.types.ts +166 -0
  45. package/src/components/ds-code-input/ds-code-input.utils.ts +94 -0
  46. package/src/components/ds-code-input/ds-code-input.utils.unit.test.ts +166 -0
  47. package/src/components/ds-code-input/index.ts +2 -0
  48. package/src/components/ds-code-input/use-collapsed-viewport.ts +289 -0
  49. package/src/components/ds-code-input/use-editor-host.ts +87 -0
  50. package/src/components/ds-dropdown-menu/ds-dropdown-menu.tsx +2 -0
  51. package/src/components/ds-empty-state/ds-empty-state.module.scss +16 -0
  52. package/src/components/ds-empty-state/ds-empty-state.tsx +45 -0
  53. package/src/components/ds-empty-state/ds-empty-state.types.ts +38 -0
  54. package/src/components/ds-empty-state/index.ts +3 -0
  55. package/src/components/ds-file-upload/ds-file-upload.tsx +4 -1
  56. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.ts +11 -0
  57. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.types.ts +41 -0
  58. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/index.ts +1 -0
  59. package/src/components/ds-filters-bar/components/ds-filters-bar-clear-all.ts +11 -0
  60. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.ts +11 -0
  61. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.types.ts +30 -0
  62. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/index.ts +1 -0
  63. package/src/components/ds-filters-bar/components/ds-filters-bar-pinned.ts +31 -0
  64. package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.ts +11 -0
  65. package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.types.ts +22 -0
  66. package/src/components/ds-filters-bar/components/ds-filters-bar-query/index.ts +1 -0
  67. package/src/components/ds-filters-bar/components/ds-filters-bar-saved-filters.ts +19 -0
  68. package/src/components/ds-filters-bar/components/ds-filters-bar-search.ts +11 -0
  69. package/src/components/ds-filters-bar/components/ds-filters-bar-summary.ts +11 -0
  70. package/src/components/ds-filters-bar/components/ds-filters-bar-toolbar.ts +11 -0
  71. package/src/components/ds-filters-bar/components/ds-filters-bar-view-switch.ts +11 -0
  72. package/src/components/ds-filters-bar/components/ds-filters-bar-view.ts +11 -0
  73. package/src/components/ds-filters-bar/components/index.ts +11 -0
  74. package/src/components/ds-filters-bar/ds-filters-bar.context.ts +69 -0
  75. package/src/components/ds-filters-bar/ds-filters-bar.tsx +120 -0
  76. package/src/components/ds-filters-bar/ds-filters-bar.types.ts +440 -0
  77. package/src/components/ds-filters-bar/ds-filters-bar.unit.test.ts +216 -0
  78. package/src/components/ds-filters-bar/ds-filters-bar.utils.ts +141 -0
  79. package/src/components/ds-filters-bar/index.ts +1 -0
  80. package/src/components/ds-form-control/ds-form-control.module.scss +18 -0
  81. package/src/components/ds-form-control/ds-form-control.tsx +61 -19
  82. package/src/components/ds-form-control/ds-form-control.types.ts +2 -1
  83. package/src/components/ds-icon/custom-icons/index.ts +2 -0
  84. package/src/components/ds-icon/custom-icons/special-adv-filters.tsx +28 -0
  85. package/src/components/ds-illustration/ds-illustration.figma.ts +34 -0
  86. package/src/components/ds-illustration/ds-illustration.module.scss +6 -0
  87. package/src/components/ds-illustration/ds-illustration.tsx +29 -0
  88. package/src/components/ds-illustration/ds-illustration.types.ts +45 -0
  89. package/src/components/ds-illustration/illustrations/configuration.tsx +51 -0
  90. package/src/components/ds-illustration/illustrations/device.tsx +133 -0
  91. package/src/components/ds-illustration/illustrations/done.tsx +107 -0
  92. package/src/components/{ds-catalog-layout/catalog-layout-empty-illustration.tsx → ds-illustration/illustrations/empty-inbox.tsx} +10 -14
  93. package/src/components/ds-illustration/illustrations/error.tsx +86 -0
  94. package/src/components/ds-illustration/illustrations/go-filter.tsx +51 -0
  95. package/src/components/ds-illustration/illustrations/illustration-clouds.tsx +10 -0
  96. package/src/components/ds-illustration/illustrations/index.ts +39 -0
  97. package/src/components/ds-illustration/illustrations/no-connection.tsx +54 -0
  98. package/src/components/ds-illustration/illustrations/no-credit-card.tsx +48 -0
  99. package/src/components/ds-illustration/illustrations/no-device.tsx +61 -0
  100. package/src/components/ds-illustration/illustrations/no-documents.tsx +52 -0
  101. package/src/components/ds-illustration/illustrations/no-gps.tsx +57 -0
  102. package/src/components/ds-illustration/illustrations/no-images.tsx +82 -0
  103. package/src/components/ds-illustration/illustrations/no-items-cart.tsx +86 -0
  104. package/src/components/ds-illustration/illustrations/no-messages.tsx +51 -0
  105. package/src/components/ds-illustration/illustrations/no-search-result.tsx +76 -0
  106. package/src/components/ds-illustration/illustrations/no-tasks.tsx +61 -0
  107. package/src/components/ds-illustration/illustrations/site.tsx +80 -0
  108. package/src/components/ds-illustration/index.ts +3 -0
  109. package/src/components/ds-main-menu/components/expanded-item/expanded-item.tsx +108 -0
  110. package/src/components/ds-main-menu/components/expanded-item/expanded-item.types.ts +8 -0
  111. package/src/components/ds-main-menu/components/expanded-item/index.ts +1 -0
  112. package/src/components/ds-main-menu/components/menu-graphic/index.ts +1 -0
  113. package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.tsx +16 -0
  114. package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.types.ts +7 -0
  115. package/src/components/ds-main-menu/components/tile-link/index.ts +1 -0
  116. package/src/components/ds-main-menu/components/tile-link/tile-link.tsx +85 -0
  117. package/src/components/ds-main-menu/components/tile-link/tile-link.types.ts +8 -0
  118. package/src/components/ds-main-menu/components/utility-link/index.ts +1 -0
  119. package/src/components/ds-main-menu/components/utility-link/utility-link.tsx +37 -0
  120. package/src/components/ds-main-menu/components/utility-link/utility-link.types.ts +7 -0
  121. package/src/components/ds-main-menu/ds-main-menu.figma.ts +17 -0
  122. package/src/components/ds-main-menu/ds-main-menu.module.scss +349 -0
  123. package/src/components/ds-main-menu/ds-main-menu.tsx +102 -0
  124. package/src/components/ds-main-menu/ds-main-menu.types.ts +86 -0
  125. package/src/components/ds-main-menu/ds-main-menu.utils.ts +40 -0
  126. package/src/components/ds-main-menu/index.ts +9 -0
  127. package/src/components/ds-modal/figma/ds-modal-footer.figma.ts +1 -1
  128. package/src/components/ds-pin-toggle/ds-pin-toggle.figma.ts +27 -0
  129. package/src/components/ds-pin-toggle/ds-pin-toggle.module.scss +41 -0
  130. package/src/components/ds-pin-toggle/ds-pin-toggle.tsx +43 -0
  131. package/src/components/ds-pin-toggle/ds-pin-toggle.types.ts +29 -0
  132. package/src/components/ds-pin-toggle/index.ts +2 -0
  133. package/src/components/ds-popover/components/popover-trigger/index.ts +1 -0
  134. package/src/components/ds-popover/components/popover-trigger/popover-trigger.tsx +15 -0
  135. package/src/components/ds-popover/ds-popover.context.ts +18 -0
  136. package/src/components/ds-popover/ds-popover.hover-intent.ts +65 -0
  137. package/src/components/ds-popover/ds-popover.tsx +164 -41
  138. package/src/components/ds-popover/ds-popover.types.ts +65 -0
  139. package/src/components/ds-popover/ds-popover.utils.ts +60 -0
  140. package/src/components/ds-popover/ds-popover.utils.unit.test.ts +25 -0
  141. package/src/components/ds-saved-filters/components/ds-saved-filters-delete-confirm.tsx +53 -0
  142. package/src/components/ds-saved-filters/components/ds-saved-filters-name-entry.tsx +64 -0
  143. package/src/components/ds-saved-filters/components/ds-saved-filters-name-modal.tsx +50 -0
  144. package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.module.scss +68 -0
  145. package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.tsx +83 -0
  146. package/src/components/ds-saved-filters/components/ds-saved-filters-row/index.ts +1 -0
  147. package/src/components/ds-saved-filters/components/ds-saved-filters-save.tsx +164 -0
  148. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.module.scss +21 -0
  149. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.tsx +237 -0
  150. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/index.ts +1 -0
  151. package/src/components/ds-saved-filters/components/index.ts +2 -0
  152. package/src/components/ds-saved-filters/ds-saved-filters.ts +6 -0
  153. package/src/components/ds-saved-filters/ds-saved-filters.types.ts +106 -0
  154. package/src/components/ds-saved-filters/ds-saved-filters.utils.ts +35 -0
  155. package/src/components/ds-saved-filters/hooks/index.ts +1 -0
  156. package/src/components/ds-saved-filters/hooks/use-picker-outside-dismiss.ts +42 -0
  157. package/src/components/ds-saved-filters/index.ts +2 -0
  158. package/src/components/ds-segment-group/ds-segment-group.module.scss +65 -37
  159. package/src/components/ds-segment-group/ds-segment-group.tsx +17 -5
  160. package/src/components/ds-segment-group/ds-segment-group.types.ts +20 -5
  161. package/src/components/ds-segment-group/figma/ds-segment-group-v2-item.figma.ts +109 -0
  162. package/src/components/ds-segment-group/figma/ds-segment-group-v2.figma.ts +31 -0
  163. package/src/components/ds-site-node/ds-site-node.tsx +2 -2
  164. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +12 -2
  165. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +2 -3
  166. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +1 -6
  167. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +6 -3
  168. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +18 -7
  169. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +27 -21
  170. package/src/components/ds-table/components/ds-table-header-label.module.scss +12 -0
  171. package/src/components/ds-table/components/ds-table-header-label.tsx +28 -0
  172. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +0 -1
  173. package/src/components/ds-table/context/ds-table-context.tsx +8 -0
  174. package/src/components/ds-table/ds-table.module.scss +26 -16
  175. package/src/components/ds-table/ds-table.tsx +46 -14
  176. package/src/components/ds-table/ds-table.types.ts +33 -3
  177. package/src/components/ds-table/hooks/use-column-resize.ts +114 -11
  178. package/src/components/ds-table/hooks/use-scrollbar-spacer.ts +108 -0
  179. package/src/components/ds-table/hooks/use-scrollbar-spacer.unit.test.ts +26 -0
  180. package/src/components/ds-table/styles/_variables.scss +0 -2
  181. package/src/components/ds-table/utils/column-size.ts +274 -1
  182. package/src/components/ds-table/utils/column-size.unit.test.ts +157 -0
  183. package/src/components/ds-table/utils/constants.ts +13 -5
  184. package/src/components/ds-table/utils/table-columns.tsx +27 -13
  185. package/src/components/ds-table/utils/table-columns.unit.test.ts +44 -0
  186. package/src/components/ds-tag/ds-tag.figma.ts +23 -3
  187. package/src/components/ds-tag/ds-tag.module.scss +71 -7
  188. package/src/components/ds-tag/ds-tag.tsx +115 -14
  189. package/src/components/ds-tag/ds-tag.types.ts +70 -3
  190. package/src/components/ds-tag/ds-tag.unit.test.ts +23 -4
  191. package/src/components/ds-tag/index.ts +2 -2
  192. package/src/components/ds-text-input/ds-input-field-v02.figma.ts +2 -0
  193. package/src/components/ds-toggle/ds-toggle.module.scss +18 -0
  194. package/src/components/ds-toggle/ds-toggle.tsx +15 -9
  195. package/src/components/ds-toggle/ds-toggle.types.ts +6 -0
  196. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.module.scss +69 -0
  197. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.tsx +55 -0
  198. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.types.ts +43 -0
  199. package/src/components/ds-toggle-filter-data/index.ts +1 -0
  200. package/src/components/ds-tooltip/ds-tooltip.module.scss +8 -0
  201. package/src/components/ds-tooltip/ds-tooltip.tsx +7 -3
  202. package/src/components/ds-tooltip/ds-tooltip.types.ts +17 -0
  203. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.figma.ts +35 -0
  204. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.module.scss +150 -0
  205. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.tsx +161 -0
  206. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.types.ts +117 -0
  207. package/src/components/ds-top-bar-navigation/index.ts +2 -0
  208. package/src/components/ds-tree/ds-tree.tsx +10 -10
  209. package/src/components/ds-tree/ds-tree.types.ts +40 -7
  210. package/src/index.ts +8 -0
  211. package/src/utils/use-is-horizontally-overflowing.ts +35 -0
  212. package/src/components/ds-site-node/hooks/use-is-overflowing.ts +0 -31
  213. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +0 -112
  214. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.tsx +0 -67
  215. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.types.ts +0 -41
  216. package/src/components/ds-table/components/ds-table-bulk-actions/index.ts +0 -2
@@ -0,0 +1,25 @@
1
+ .root {
2
+ display: block;
3
+ width: 100%;
4
+ }
5
+
6
+ .panel {
7
+ overflow: hidden;
8
+
9
+ &.invalid {
10
+ outline: 1px solid var(--background-error);
11
+ outline-offset: -1px;
12
+ }
13
+ }
14
+
15
+ .visuallyHidden {
16
+ position: absolute;
17
+ width: 1px;
18
+ height: 1px;
19
+ margin: -1px;
20
+ padding: 0;
21
+ border: 0;
22
+ overflow: hidden;
23
+ white-space: nowrap;
24
+ clip-path: inset(50%);
25
+ }
@@ -0,0 +1,401 @@
1
+ import { type ChangeEvent, type FocusEvent, useEffect, useId, useRef, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import classNames from 'classnames';
4
+ import { DsButtonV3, type ButtonV3Size } from '../ds-button-v3';
5
+ import { DsPopover } from '../ds-popover';
6
+ import { mergeRefs } from '../../utils/merge-refs';
7
+ import { CollapsedCodeField } from './collapsed-code-field';
8
+ import collapsedStyles from './collapsed-code-field/collapsed-code-field.module.scss';
9
+ import { DsCodeInputPanel } from './ds-code-input-panel';
10
+ import panelStyles from './ds-code-input-panel/ds-code-input-panel.module.scss';
11
+ import styles from './ds-code-input.module.scss';
12
+ import type { CodeInputSize, DsCodeInputProps } from './ds-code-input.types';
13
+ import {
14
+ applyTextSelection,
15
+ canonicalizeLf,
16
+ getAdditionalLineCount,
17
+ getLogicalLineCount,
18
+ readTextSelection,
19
+ type TextSelection,
20
+ } from './ds-code-input.utils';
21
+ import { useCollapsedViewport } from './use-collapsed-viewport';
22
+ import { useEditorHost } from './use-editor-host';
23
+
24
+ const toggleSizes: Record<CodeInputSize, ButtonV3Size> = Object.freeze({
25
+ small: 'tiny',
26
+ default: 'small',
27
+ large: 'small',
28
+ });
29
+
30
+ const OVERLAY_GUTTER_PX = 4;
31
+
32
+ const defaultLocale = Object.freeze({
33
+ expand: 'Expand code editor',
34
+ collapse: 'Collapse code editor',
35
+ searchPlaceholder: 'Search in code',
36
+ codeLabel: 'Code',
37
+ additionalLines: (count: number) => `↵ +${String(count)}`,
38
+ additionalLinesLabel: (count: number) => `${String(count)} additional code lines`,
39
+ linePosition: (current: number, total: number) => `Ln ${String(current)}/${String(total)}`,
40
+ multilineDescription: (total: number) =>
41
+ `Multiline code, ${String(total)} lines. Only one line is visible. Expand to view the complete code.`,
42
+ linePositionDescription: (current: number, total: number) =>
43
+ `Line ${String(current)} of ${String(total)}; other code lines are hidden.`,
44
+ selectionStatus: (selected: number, total: number) =>
45
+ `${String(selected)} lines selected · ${String(total)} total`,
46
+ expandedAnnouncement: (total: number) => `Code editor expanded, ${String(total)} lines.`,
47
+ collapsedAnnouncement: 'Code editor collapsed',
48
+ });
49
+
50
+ /**
51
+ * Multiline editor for code, such as a query expression. The collapsed field is a
52
+ * one-row viewport of the same document; expanding opens a larger overlay with search.
53
+ * There is one textarea: collapsed and expanded are two homes it moves between.
54
+ *
55
+ * @summary multiline code field that expands into a searchable overlay
56
+ */
57
+ const DsCodeInput = ({
58
+ id,
59
+ ref,
60
+ name,
61
+ size = 'default',
62
+ value,
63
+ defaultValue,
64
+ placeholder,
65
+ disabled = false,
66
+ readOnly = false,
67
+ invalid = false,
68
+ 'aria-describedby': ariaDescribedBy,
69
+ expanded,
70
+ defaultExpanded = false,
71
+ onExpandChange,
72
+ onChange,
73
+ onValueChange,
74
+ onFocus,
75
+ onBlur,
76
+ maxLength,
77
+ locale,
78
+ slots,
79
+ className,
80
+ style,
81
+ }: DsCodeInputProps) => {
82
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? '');
83
+ const [uncontrolledExpanded, setUncontrolledExpanded] = useState(defaultExpanded);
84
+ // True when the user opened the overlay and should keep typing in it.
85
+ const [focusOnOpen, setFocusOnOpen] = useState(false);
86
+ const [announcement, setAnnouncement] = useState('');
87
+
88
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
89
+ const collapsedSlotRef = useRef<HTMLDivElement>(null);
90
+ const overlaySlotRef = useRef<HTMLDivElement>(null);
91
+ // Caret/selection saved before the textarea moves to the other slot.
92
+ const pendingSelectionRef = useRef<TextSelection | null>(null);
93
+ // True when the user closed the overlay and should land back in the field.
94
+ const focusOnCloseRef = useRef(false);
95
+ // Skip the first expand/collapse announcement so mount is silent.
96
+ const skipAnnouncementRef = useRef(true);
97
+
98
+ const panelId = useId();
99
+ const descriptionId = useId();
100
+ const announcementId = useId();
101
+
102
+ const strings = { ...defaultLocale, ...locale };
103
+
104
+ const isValueControlled = value !== undefined;
105
+ const currentValue = isValueControlled ? value : uncontrolledValue;
106
+
107
+ const isExpandedControlled = expanded !== undefined;
108
+ const isExpanded = isExpandedControlled ? expanded : uncontrolledExpanded;
109
+
110
+ const handleOpenChange = (open: boolean) => {
111
+ if (open) {
112
+ focusOnCloseRef.current = false;
113
+
114
+ // User-open already saved the caret. If the overlay opened some other
115
+ // way, snapshot it now so it still comes back after the move.
116
+ if (!pendingSelectionRef.current) {
117
+ const textarea = textareaRef.current;
118
+
119
+ if (textarea) {
120
+ pendingSelectionRef.current = readTextSelection(textarea);
121
+ }
122
+ }
123
+ } else {
124
+ setFocusOnOpen(false);
125
+ }
126
+
127
+ if (!isExpandedControlled) {
128
+ setUncontrolledExpanded(open);
129
+ }
130
+
131
+ onExpandChange?.(open);
132
+ };
133
+
134
+ const { host, hostReady, isMovingHostRef } = useEditorHost({
135
+ isExpanded,
136
+ collapsedSlotRef,
137
+ overlaySlotRef,
138
+ hostClassName: isExpanded ? panelStyles.editorHost : collapsedStyles.editorHost,
139
+ textareaRef,
140
+ pendingSelectionRef,
141
+ shouldFocus: isExpanded && focusOnOpen,
142
+ });
143
+ const viewport = useCollapsedViewport(textareaRef, currentValue, hostReady, !isExpanded);
144
+ const revealCaretLineRef = useRef(viewport.revealCaretLine);
145
+
146
+ revealCaretLineRef.current = viewport.revealCaretLine;
147
+
148
+ useEffect(() => {
149
+ if (skipAnnouncementRef.current) {
150
+ skipAnnouncementRef.current = false;
151
+
152
+ return;
153
+ }
154
+
155
+ setAnnouncement(
156
+ isExpanded
157
+ ? strings.expandedAnnouncement(getLogicalLineCount(currentValue))
158
+ : strings.collapsedAnnouncement,
159
+ );
160
+ // Announce presentation changes only — not every keystroke in the document.
161
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- currentValue/strings would re-announce while typing
162
+ }, [isExpanded]);
163
+
164
+ const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
165
+ // Some browsers insert \r\n. Rewrite to \n and put the caret back so
166
+ // typing does not jump.
167
+ const newValue = canonicalizeLf(event.target.value);
168
+
169
+ if (newValue !== event.target.value) {
170
+ const selection = readTextSelection(event.target);
171
+
172
+ event.target.value = newValue;
173
+ applyTextSelection(event.target, selection);
174
+ }
175
+
176
+ if (!isValueControlled) {
177
+ setUncontrolledValue(newValue);
178
+ }
179
+
180
+ onChange?.(event);
181
+ onValueChange?.(newValue);
182
+ };
183
+
184
+ const handleFocus = (event: FocusEvent<HTMLTextAreaElement>) => {
185
+ // Moving the host fires fake focus/blur. Ignore those so callers do not
186
+ // think the user left the field.
187
+ if (isMovingHostRef.current) {
188
+ return;
189
+ }
190
+
191
+ onFocus?.(event);
192
+ };
193
+
194
+ const handleBlur = (event: FocusEvent<HTMLTextAreaElement>) => {
195
+ if (isMovingHostRef.current) {
196
+ return;
197
+ }
198
+
199
+ onBlur?.(event);
200
+ };
201
+
202
+ const handleUserOpen = () => {
203
+ const textarea = textareaRef.current;
204
+
205
+ if (textarea) {
206
+ pendingSelectionRef.current = readTextSelection(textarea);
207
+ }
208
+
209
+ setFocusOnOpen(true);
210
+ handleOpenChange(true);
211
+ };
212
+
213
+ const additionalLines = getAdditionalLineCount(currentValue);
214
+ const description =
215
+ !isExpanded && additionalLines > 0
216
+ ? viewport.focused
217
+ ? strings.linePositionDescription(viewport.visibleLine, viewport.totalLines)
218
+ : strings.multilineDescription(viewport.totalLines)
219
+ : undefined;
220
+
221
+ // One textarea, rendered once and portaled into the host. rows / wrap /
222
+ // className flip when it moves between the collapsed slot and the overlay.
223
+ const editor = host
224
+ ? createPortal(
225
+ <textarea
226
+ id={id}
227
+ ref={mergeRefs(ref, textareaRef)}
228
+ name={name}
229
+ className={isExpanded ? panelStyles.codeArea : collapsedStyles.input}
230
+ rows={isExpanded ? undefined : 1}
231
+ wrap={isExpanded ? 'soft' : 'off'}
232
+ value={currentValue}
233
+ placeholder={placeholder}
234
+ disabled={disabled}
235
+ readOnly={readOnly}
236
+ maxLength={maxLength}
237
+ spellCheck={false}
238
+ autoComplete="off"
239
+ aria-multiline="true"
240
+ aria-label={isExpanded && !id ? strings.codeLabel : undefined}
241
+ aria-invalid={invalid || undefined}
242
+ aria-describedby={
243
+ [description && descriptionId, ariaDescribedBy].filter(Boolean).join(' ') || undefined
244
+ }
245
+ onChange={handleChange}
246
+ onSelect={viewport.onSelect}
247
+ onKeyUp={viewport.onKeyUp}
248
+ onClick={viewport.onClick}
249
+ onScroll={viewport.onScroll}
250
+ onBeforeInput={viewport.onBeforeInput}
251
+ onFocus={(event) => {
252
+ if (!isMovingHostRef.current) {
253
+ viewport.onFocus();
254
+ }
255
+
256
+ handleFocus(event);
257
+ }}
258
+ onBlur={(event) => {
259
+ if (!isMovingHostRef.current) {
260
+ viewport.onBlur();
261
+ }
262
+
263
+ handleBlur(event);
264
+ }}
265
+ onKeyDown={(event) => {
266
+ if (event.key === 'Escape' && isExpanded) {
267
+ event.stopPropagation();
268
+ focusOnCloseRef.current = true;
269
+ handleOpenChange(false);
270
+
271
+ return;
272
+ }
273
+
274
+ if (!isExpanded && event.altKey && event.key === 'ArrowDown') {
275
+ event.preventDefault();
276
+ event.stopPropagation();
277
+ handleUserOpen();
278
+ }
279
+ }}
280
+ />,
281
+ host,
282
+ )
283
+ : null;
284
+
285
+ return (
286
+ <DsPopover.Root
287
+ open={isExpanded}
288
+ side="bottom"
289
+ align="start"
290
+ gutter={OVERLAY_GUTTER_PX}
291
+ matchAnchorWidth
292
+ // We own focus so the expand button does not steal it.
293
+ restoreFocus={false}
294
+ onOpenAutoFocus={(event) => {
295
+ event.preventDefault();
296
+
297
+ if (focusOnOpen) {
298
+ textareaRef.current?.focus({ preventScroll: true });
299
+ }
300
+ }}
301
+ onCloseAutoFocus={(event) => {
302
+ event.preventDefault();
303
+
304
+ // Put focus back in the collapsed field and scroll to the caret line.
305
+ if (focusOnCloseRef.current) {
306
+ textareaRef.current?.focus({ preventScroll: true });
307
+ revealCaretLineRef.current();
308
+ focusOnCloseRef.current = false;
309
+ }
310
+ }}
311
+ onOpenChange={handleOpenChange}
312
+ >
313
+ <DsPopover.Anchor>
314
+ <div className={classNames(styles.root, className)} style={style}>
315
+ <CollapsedCodeField
316
+ size={size}
317
+ placeholder={placeholder}
318
+ disabled={disabled}
319
+ invalid={invalid}
320
+ showEcho={isExpanded}
321
+ echoText={viewport.echoText}
322
+ fadeStart={viewport.fadeStart}
323
+ fadeEnd={viewport.fadeEnd}
324
+ showEllipsis={viewport.showEllipsis}
325
+ indicator={
326
+ isExpanded && additionalLines > 0
327
+ ? { kind: 'line', current: viewport.caretLine, total: viewport.totalLines }
328
+ : isExpanded
329
+ ? null
330
+ : viewport.indicator
331
+ }
332
+ additionalLinesLabel={strings.additionalLinesLabel}
333
+ additionalLines={strings.additionalLines}
334
+ linePosition={strings.linePosition}
335
+ selectionStatus={strings.selectionStatus}
336
+ editorSlotRef={collapsedSlotRef}
337
+ onExpand={handleUserOpen}
338
+ adornment={
339
+ <>
340
+ <DsPopover.Trigger>
341
+ <DsButtonV3
342
+ variant="tertiary"
343
+ size={toggleSizes[size]}
344
+ icon={isExpanded ? 'collapse_content' : 'expand_content'}
345
+ aria-label={isExpanded ? strings.collapse : strings.expand}
346
+ aria-expanded={isExpanded}
347
+ aria-controls={panelId}
348
+ onPointerDown={(event) => {
349
+ // Snapshot the caret and request focus before the
350
+ // button would steal it.
351
+ event.preventDefault();
352
+
353
+ if (isExpanded) {
354
+ focusOnCloseRef.current = true;
355
+ } else {
356
+ const textarea = textareaRef.current;
357
+
358
+ if (textarea) {
359
+ pendingSelectionRef.current = readTextSelection(textarea);
360
+ }
361
+
362
+ setFocusOnOpen(true);
363
+ }
364
+ }}
365
+ />
366
+ </DsPopover.Trigger>
367
+ {slots?.endAdornment}
368
+ </>
369
+ }
370
+ />
371
+ {description && (
372
+ <span id={descriptionId} className={styles.visuallyHidden}>
373
+ {description}
374
+ </span>
375
+ )}
376
+ <div id={announcementId} className={styles.visuallyHidden} aria-live="polite" aria-atomic="true">
377
+ {announcement}
378
+ </div>
379
+ </div>
380
+ </DsPopover.Anchor>
381
+
382
+ <DsPopover.Panel
383
+ id={panelId}
384
+ className={classNames(styles.panel, invalid && styles.invalid)}
385
+ aria-label={strings.codeLabel}
386
+ >
387
+ <DsCodeInputPanel
388
+ value={currentValue}
389
+ isOpen={isExpanded}
390
+ searchPlaceholder={strings.searchPlaceholder}
391
+ editorSlotRef={overlaySlotRef}
392
+ />
393
+ </DsPopover.Panel>
394
+ {editor}
395
+ </DsPopover.Root>
396
+ );
397
+ };
398
+
399
+ DsCodeInput.displayName = 'DsCodeInput';
400
+
401
+ export default DsCodeInput;
@@ -0,0 +1,166 @@
1
+ import type React from 'react';
2
+
3
+ export const codeInputSizes = ['small', 'default', 'large'] as const;
4
+ export type CodeInputSize = (typeof codeInputSizes)[number];
5
+
6
+ export interface DsCodeInputProps {
7
+ /**
8
+ * Associates the editor with a `<label htmlFor>`. When set, the expanded
9
+ * textarea keeps that name instead of `locale.codeLabel`.
10
+ */
11
+ id?: string;
12
+ /**
13
+ * The ref to the editor textarea
14
+ */
15
+ ref?: React.Ref<HTMLTextAreaElement>;
16
+ /**
17
+ * The name of the input
18
+ */
19
+ name?: string;
20
+ /**
21
+ * @default default
22
+ */
23
+ size?: CodeInputSize;
24
+ /**
25
+ * The current value. Shared by the collapsed viewport and the overlay.
26
+ */
27
+ value?: string;
28
+ /**
29
+ * The initial value when rendered.
30
+ * Use when you don't need to control the value of the input.
31
+ */
32
+ defaultValue?: string;
33
+ /**
34
+ * The placeholder text
35
+ */
36
+ placeholder?: string;
37
+ /**
38
+ * Whether the input is disabled. The overlay still opens so long values stay searchable.
39
+ * @default false
40
+ */
41
+ disabled?: boolean;
42
+ /**
43
+ * Whether the value is read only. The overlay still opens so long values stay searchable.
44
+ * @default false
45
+ */
46
+ readOnly?: boolean;
47
+ /**
48
+ * Shows the error border and sets `aria-invalid`. Point `aria-describedby` at the message.
49
+ * @default false
50
+ */
51
+ invalid?: boolean;
52
+ /**
53
+ * Id of an element that describes the value, such as an error message
54
+ */
55
+ 'aria-describedby'?: string;
56
+ /**
57
+ * Whether the overlay is open. Pair with `onExpandChange`.
58
+ */
59
+ expanded?: boolean;
60
+ /**
61
+ * Initial overlay state when uncontrolled.
62
+ * @default false
63
+ */
64
+ defaultExpanded?: boolean;
65
+ /**
66
+ * Fires when the overlay opens or closes
67
+ */
68
+ onExpandChange?: (expanded: boolean) => void;
69
+ /**
70
+ * Callback when the value changes
71
+ */
72
+ onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
73
+ /**
74
+ * Value change event handler (provides just the value)
75
+ */
76
+ onValueChange?: (value: string) => void;
77
+ /**
78
+ * Event handler called when the editor receives focus
79
+ */
80
+ onFocus?: (event: React.FocusEvent<HTMLTextAreaElement>) => void;
81
+ /**
82
+ * Event handler called when the editor loses focus
83
+ */
84
+ onBlur?: (event: React.FocusEvent<HTMLTextAreaElement>) => void;
85
+ /**
86
+ * The maximum number of characters
87
+ */
88
+ maxLength?: number;
89
+ /**
90
+ * User-facing strings
91
+ */
92
+ locale?: DsCodeInputLocale;
93
+ /**
94
+ * Optional render slots for customizing parts of the collapsed field.
95
+ */
96
+ slots?: {
97
+ /**
98
+ * Adornment to display after the expand button
99
+ */
100
+ endAdornment?: React.ReactNode;
101
+ };
102
+ className?: string;
103
+ style?: React.CSSProperties;
104
+ }
105
+
106
+ export interface DsCodeInputLocale {
107
+ /**
108
+ * Accessible name of the button that opens the overlay
109
+ * @default 'Expand code editor'
110
+ */
111
+ expand?: string;
112
+ /**
113
+ * Accessible name of the button that closes the overlay
114
+ * @default 'Collapse code editor'
115
+ */
116
+ collapse?: string;
117
+ /**
118
+ * @default 'Search in code'
119
+ */
120
+ searchPlaceholder?: string;
121
+ /**
122
+ * Accessible name of the overlay when the compact field is inert
123
+ * @default 'Code'
124
+ */
125
+ codeLabel?: string;
126
+ /**
127
+ * Visual label for hidden logical lines. Receives the additional-line count.
128
+ * @default (count) => `↵ +${count}`
129
+ */
130
+ additionalLines?: (count: number) => string;
131
+ /**
132
+ * Accessible name for hidden logical lines. Receives the additional-line count.
133
+ * @default (count) => `${count} additional code lines`
134
+ */
135
+ additionalLinesLabel?: (count: number) => string;
136
+ /**
137
+ * Visual focused line indicator. Receives the visible line and the total.
138
+ * @default (current, total) => `Ln ${current}/${total}`
139
+ */
140
+ linePosition?: (current: number, total: number) => string;
141
+ /**
142
+ * Accessible description while collapsed and multiline.
143
+ * @default (total) => `Multiline code, ${total} lines. Only one line is visible. Expand to view the complete code.`
144
+ */
145
+ multilineDescription?: (total: number) => string;
146
+ /**
147
+ * Accessible description while focused on a hidden line.
148
+ * @default (current, total) => `Line ${current} of ${total}; other code lines are hidden.`
149
+ */
150
+ linePositionDescription?: (current: number, total: number) => string;
151
+ /**
152
+ * Visual status when a selection spans hidden lines.
153
+ * @default (selected, total) => `${selected} lines selected · ${total} total`
154
+ */
155
+ selectionStatus?: (selected: number, total: number) => string;
156
+ /**
157
+ * Announced once when the overlay opens. Receives the logical line count.
158
+ * @default (total) => `Code editor expanded, ${total} lines.`
159
+ */
160
+ expandedAnnouncement?: (total: number) => string;
161
+ /**
162
+ * Announced once when the overlay closes
163
+ * @default 'Code editor collapsed'
164
+ */
165
+ collapsedAnnouncement?: string;
166
+ }
@@ -0,0 +1,94 @@
1
+ export type TextSelectionDirection = 'forward' | 'backward' | 'none';
2
+
3
+ export interface TextSelection {
4
+ start: number;
5
+ end: number;
6
+ direction: TextSelectionDirection;
7
+ }
8
+
9
+ const CANVAS_TAB_SIZE_FALLBACK = 2;
10
+
11
+ let measureContext: CanvasRenderingContext2D | null | undefined;
12
+
13
+ export const canonicalizeLf = (text: string) => text.replace(/\r\n?/g, '\n');
14
+
15
+ const getLogicalLines = (text: string) => canonicalizeLf(text).split('\n');
16
+
17
+ export const getLogicalLineCount = (text: string) => getLogicalLines(text).length;
18
+
19
+ export const getAdditionalLineCount = (text: string) => Math.max(0, getLogicalLineCount(text) - 1);
20
+
21
+ export const getLineAtIndex = (text: string, lineIndex: number) => getLogicalLines(text)[lineIndex] ?? '';
22
+
23
+ export const getLineIndexAtOffset = (text: string, offset: number) => {
24
+ const lf = canonicalizeLf(text);
25
+ const clamped = Math.min(Math.max(offset, 0), lf.length);
26
+
27
+ return lf.slice(0, clamped).split('\n').length - 1;
28
+ };
29
+
30
+ export const getSelectedLineCount = (text: string, start: number, end: number) => {
31
+ if (start === end) {
32
+ return 0;
33
+ }
34
+
35
+ const from = Math.min(start, end);
36
+ const to = Math.max(start, end);
37
+
38
+ return getLineIndexAtOffset(text, to) - getLineIndexAtOffset(text, from) + 1;
39
+ };
40
+
41
+ export const readTextSelection = (element: HTMLTextAreaElement): TextSelection => ({
42
+ start: element.selectionStart,
43
+ end: element.selectionEnd,
44
+ direction: element.selectionDirection,
45
+ });
46
+
47
+ export const applyTextSelection = (element: HTMLTextAreaElement, selection: TextSelection) => {
48
+ const length = element.value.length;
49
+ const start = Math.min(selection.start, length);
50
+ const end = Math.min(selection.end, length);
51
+
52
+ element.setSelectionRange(start, end, selection.direction);
53
+ };
54
+
55
+ const getLineHeight = (element: HTMLTextAreaElement) => {
56
+ const parsed = Number.parseFloat(getComputedStyle(element).lineHeight);
57
+
58
+ if (Number.isFinite(parsed)) {
59
+ return parsed;
60
+ }
61
+
62
+ return Number.parseFloat(getComputedStyle(element).fontSize);
63
+ };
64
+
65
+ export const scrollToLogicalLine = (element: HTMLTextAreaElement, lineIndex: number) => {
66
+ element.scrollTop = Math.max(0, lineIndex) * getLineHeight(element);
67
+ };
68
+
69
+ export const getVisibleLineIndex = (element: HTMLTextAreaElement) => {
70
+ const lineHeight = getLineHeight(element);
71
+
72
+ if (lineHeight <= 0) {
73
+ return 0;
74
+ }
75
+
76
+ return Math.round(element.scrollTop / lineHeight);
77
+ };
78
+
79
+ export const measureTextWidth = (element: HTMLElement, text: string) => {
80
+ if (measureContext === undefined) {
81
+ measureContext = document.createElement('canvas').getContext('2d');
82
+ }
83
+
84
+ if (!measureContext) {
85
+ return 0;
86
+ }
87
+
88
+ const computed = getComputedStyle(element);
89
+ const tabSize = Number.parseInt(computed.tabSize, 10) || CANVAS_TAB_SIZE_FALLBACK;
90
+
91
+ measureContext.font = `${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`;
92
+
93
+ return measureContext.measureText(text.replaceAll('\t', ' '.repeat(tabSize))).width;
94
+ };