@drivenets/design-system 0.19.0 → 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 (110) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/index.cjs +12409 -10548
  3. package/dist/index.d.cts +678 -312
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +679 -313
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +12404 -10550
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +29 -29
  11. package/src/components/ds-checkbox/ds-checkbox.figma.ts +3 -0
  12. package/src/components/ds-checkbox/ds-checkbox.module.scss +102 -20
  13. package/src/components/ds-checkbox/ds-checkbox.tsx +30 -2
  14. package/src/components/ds-checkbox/ds-checkbox.types.ts +10 -0
  15. package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.module.scss +169 -0
  16. package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.tsx +92 -0
  17. package/src/components/ds-code-input/collapsed-code-field/index.ts +1 -0
  18. package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.module.scss +110 -0
  19. package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.tsx +61 -0
  20. package/src/components/ds-code-input/ds-code-input-panel/index.ts +1 -0
  21. package/src/components/ds-code-input/ds-code-input.module.scss +25 -0
  22. package/src/components/ds-code-input/ds-code-input.tsx +401 -0
  23. package/src/components/ds-code-input/ds-code-input.types.ts +166 -0
  24. package/src/components/ds-code-input/ds-code-input.utils.ts +94 -0
  25. package/src/components/ds-code-input/ds-code-input.utils.unit.test.ts +166 -0
  26. package/src/components/ds-code-input/index.ts +2 -0
  27. package/src/components/ds-code-input/use-collapsed-viewport.ts +289 -0
  28. package/src/components/ds-code-input/use-editor-host.ts +87 -0
  29. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.ts +11 -0
  30. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.types.ts +41 -0
  31. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/index.ts +1 -0
  32. package/src/components/ds-filters-bar/components/ds-filters-bar-clear-all.ts +11 -0
  33. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.ts +11 -0
  34. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.types.ts +30 -0
  35. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/index.ts +1 -0
  36. package/src/components/ds-filters-bar/components/ds-filters-bar-pinned.ts +31 -0
  37. package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.ts +11 -0
  38. package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.types.ts +22 -0
  39. package/src/components/ds-filters-bar/components/ds-filters-bar-query/index.ts +1 -0
  40. package/src/components/ds-filters-bar/components/ds-filters-bar-saved-filters.ts +19 -0
  41. package/src/components/ds-filters-bar/components/ds-filters-bar-search.ts +11 -0
  42. package/src/components/ds-filters-bar/components/ds-filters-bar-summary.ts +11 -0
  43. package/src/components/ds-filters-bar/components/ds-filters-bar-toolbar.ts +11 -0
  44. package/src/components/ds-filters-bar/components/ds-filters-bar-view-switch.ts +11 -0
  45. package/src/components/ds-filters-bar/components/ds-filters-bar-view.ts +11 -0
  46. package/src/components/ds-filters-bar/components/index.ts +11 -0
  47. package/src/components/ds-filters-bar/ds-filters-bar.context.ts +69 -0
  48. package/src/components/ds-filters-bar/ds-filters-bar.tsx +120 -0
  49. package/src/components/ds-filters-bar/ds-filters-bar.types.ts +440 -0
  50. package/src/components/ds-filters-bar/ds-filters-bar.unit.test.ts +216 -0
  51. package/src/components/ds-filters-bar/ds-filters-bar.utils.ts +141 -0
  52. package/src/components/ds-filters-bar/index.ts +1 -0
  53. package/src/components/ds-form-control/ds-form-control.module.scss +18 -0
  54. package/src/components/ds-form-control/ds-form-control.tsx +43 -9
  55. package/src/components/ds-form-control/ds-form-control.types.ts +2 -1
  56. package/src/components/ds-icon/custom-icons/index.ts +2 -0
  57. package/src/components/ds-icon/custom-icons/special-adv-filters.tsx +28 -0
  58. package/src/components/ds-pin-toggle/ds-pin-toggle.figma.ts +27 -0
  59. package/src/components/ds-pin-toggle/ds-pin-toggle.module.scss +41 -0
  60. package/src/components/ds-pin-toggle/ds-pin-toggle.tsx +43 -0
  61. package/src/components/ds-pin-toggle/ds-pin-toggle.types.ts +29 -0
  62. package/src/components/ds-pin-toggle/index.ts +2 -0
  63. package/src/components/ds-popover/components/popover-trigger/index.ts +1 -0
  64. package/src/components/ds-popover/components/popover-trigger/popover-trigger.tsx +15 -0
  65. package/src/components/ds-popover/ds-popover.context.ts +18 -0
  66. package/src/components/ds-popover/ds-popover.hover-intent.ts +65 -0
  67. package/src/components/ds-popover/ds-popover.tsx +163 -41
  68. package/src/components/ds-popover/ds-popover.types.ts +59 -1
  69. package/src/components/ds-popover/ds-popover.utils.ts +60 -0
  70. package/src/components/ds-popover/ds-popover.utils.unit.test.ts +25 -0
  71. package/src/components/ds-saved-filters/components/ds-saved-filters-delete-confirm.tsx +53 -0
  72. package/src/components/ds-saved-filters/components/ds-saved-filters-name-entry.tsx +64 -0
  73. package/src/components/ds-saved-filters/components/ds-saved-filters-name-modal.tsx +50 -0
  74. package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.module.scss +68 -0
  75. package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.tsx +83 -0
  76. package/src/components/ds-saved-filters/components/ds-saved-filters-row/index.ts +1 -0
  77. package/src/components/ds-saved-filters/components/ds-saved-filters-save.tsx +164 -0
  78. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.module.scss +21 -0
  79. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.tsx +237 -0
  80. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/index.ts +1 -0
  81. package/src/components/ds-saved-filters/components/index.ts +2 -0
  82. package/src/components/ds-saved-filters/ds-saved-filters.ts +6 -0
  83. package/src/components/ds-saved-filters/ds-saved-filters.types.ts +106 -0
  84. package/src/components/ds-saved-filters/ds-saved-filters.utils.ts +35 -0
  85. package/src/components/ds-saved-filters/hooks/index.ts +1 -0
  86. package/src/components/ds-saved-filters/hooks/use-picker-outside-dismiss.ts +42 -0
  87. package/src/components/ds-saved-filters/index.ts +2 -0
  88. package/src/components/ds-segment-group/ds-segment-group.module.scss +65 -37
  89. package/src/components/ds-segment-group/ds-segment-group.tsx +17 -5
  90. package/src/components/ds-segment-group/ds-segment-group.types.ts +20 -5
  91. package/src/components/ds-segment-group/figma/ds-segment-group-v2-item.figma.ts +109 -0
  92. package/src/components/ds-segment-group/figma/ds-segment-group-v2.figma.ts +31 -0
  93. package/src/components/ds-tag/ds-tag.figma.ts +23 -3
  94. package/src/components/ds-tag/ds-tag.module.scss +71 -7
  95. package/src/components/ds-tag/ds-tag.tsx +115 -14
  96. package/src/components/ds-tag/ds-tag.types.ts +70 -3
  97. package/src/components/ds-tag/ds-tag.unit.test.ts +23 -4
  98. package/src/components/ds-tag/index.ts +2 -2
  99. package/src/components/ds-text-input/ds-input-field-v02.figma.ts +2 -0
  100. package/src/components/ds-toggle/ds-toggle.tsx +19 -15
  101. package/src/components/ds-toggle/ds-toggle.types.ts +6 -0
  102. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.module.scss +69 -0
  103. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.tsx +55 -0
  104. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.types.ts +43 -0
  105. package/src/components/ds-toggle-filter-data/index.ts +1 -0
  106. package/src/components/ds-tooltip/ds-tooltip.module.scss +8 -0
  107. package/src/components/ds-tooltip/ds-tooltip.tsx +1 -1
  108. package/src/components/ds-tree/ds-tree.tsx +10 -10
  109. package/src/components/ds-tree/ds-tree.types.ts +40 -7
  110. package/src/index.ts +2 -0
@@ -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
+ };
@@ -0,0 +1,166 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ applyTextSelection,
4
+ canonicalizeLf,
5
+ getAdditionalLineCount,
6
+ getLineAtIndex,
7
+ getLineIndexAtOffset,
8
+ getLogicalLineCount,
9
+ getSelectedLineCount,
10
+ type TextSelection,
11
+ } from './ds-code-input.utils';
12
+
13
+ const multiline = 'SELECT *\nFROM devices\nWHERE id = 1';
14
+
15
+ const stubTextarea = (value: string) => {
16
+ const setSelectionRange = vi.fn();
17
+
18
+ return {
19
+ element: { value, setSelectionRange } as unknown as HTMLTextAreaElement,
20
+ setSelectionRange,
21
+ };
22
+ };
23
+
24
+ describe('canonicalizeLf', () => {
25
+ it('rewrites CRLF to LF', () => {
26
+ expect(canonicalizeLf('a\r\nb')).toBe('a\nb');
27
+ });
28
+
29
+ it('rewrites a lone CR to LF', () => {
30
+ expect(canonicalizeLf('a\rb')).toBe('a\nb');
31
+ });
32
+
33
+ it('leaves LF unchanged', () => {
34
+ expect(canonicalizeLf('a\nb')).toBe('a\nb');
35
+ });
36
+
37
+ it('normalizes mixed endings in one string', () => {
38
+ expect(canonicalizeLf('a\r\nb\rc\nd')).toBe('a\nb\nc\nd');
39
+ });
40
+ });
41
+
42
+ describe('getLogicalLineCount', () => {
43
+ it('counts an empty string as one logical line', () => {
44
+ expect(getLogicalLineCount('')).toBe(1);
45
+ });
46
+
47
+ it('counts a trailing newline as an extra empty line', () => {
48
+ expect(getLogicalLineCount('a\n')).toBe(2);
49
+ });
50
+
51
+ it('counts each logical line in multiline text', () => {
52
+ expect(getLogicalLineCount(multiline)).toBe(3);
53
+ });
54
+ });
55
+
56
+ describe('getAdditionalLineCount', () => {
57
+ it('is zero for a single line, including empty text', () => {
58
+ expect(getAdditionalLineCount('')).toBe(0);
59
+ expect(getAdditionalLineCount('SELECT *')).toBe(0);
60
+ });
61
+
62
+ it('is the number of lines beyond the first', () => {
63
+ expect(getAdditionalLineCount('a\n')).toBe(1);
64
+ expect(getAdditionalLineCount(multiline)).toBe(2);
65
+ });
66
+ });
67
+
68
+ describe('getLineAtIndex', () => {
69
+ it('returns the line at a valid index', () => {
70
+ expect(getLineAtIndex(multiline, 0)).toBe('SELECT *');
71
+ expect(getLineAtIndex(multiline, 1)).toBe('FROM devices');
72
+ expect(getLineAtIndex(multiline, 2)).toBe('WHERE id = 1');
73
+ });
74
+
75
+ it('returns an empty string when the index is out of range', () => {
76
+ expect(getLineAtIndex(multiline, -1)).toBe('');
77
+ expect(getLineAtIndex(multiline, 3)).toBe('');
78
+ });
79
+
80
+ it('splits CRLF text on logical lines', () => {
81
+ expect(getLineAtIndex('hello\r\nworld', 0)).toBe('hello');
82
+ expect(getLineAtIndex('hello\r\nworld', 1)).toBe('world');
83
+ });
84
+ });
85
+
86
+ describe('getLineIndexAtOffset', () => {
87
+ const text = 'hello\nworld';
88
+
89
+ it('returns the first line at the start of the text', () => {
90
+ expect(getLineIndexAtOffset(text, 0)).toBe(0);
91
+ });
92
+
93
+ it('stays on the previous line when the offset sits on a newline', () => {
94
+ expect(getLineIndexAtOffset(text, 5)).toBe(0);
95
+ });
96
+
97
+ it('moves to the next line once the offset is past the newline', () => {
98
+ expect(getLineIndexAtOffset(text, 6)).toBe(1);
99
+ });
100
+
101
+ it('clamps offsets below 0 and past the text length', () => {
102
+ expect(getLineIndexAtOffset(text, -4)).toBe(0);
103
+ expect(getLineIndexAtOffset(text, 100)).toBe(1);
104
+ });
105
+
106
+ it('maps offsets in CRLF text onto the same logical lines as LF', () => {
107
+ const crlf = 'hello\r\nworld';
108
+
109
+ expect(getLineIndexAtOffset(crlf, 5)).toBe(0);
110
+ expect(getLineIndexAtOffset(crlf, 6)).toBe(1);
111
+ expect(getLineIndexAtOffset(crlf, crlf.length)).toBe(1);
112
+ });
113
+ });
114
+
115
+ describe('getSelectedLineCount', () => {
116
+ const text = 'a\nb\nc';
117
+
118
+ it('is zero when the range is collapsed, even in multiline text', () => {
119
+ expect(getSelectedLineCount(text, 0, 0)).toBe(0);
120
+ expect(getSelectedLineCount(text, 2, 2)).toBe(0);
121
+ });
122
+
123
+ it('is one when the range stays on a single line', () => {
124
+ expect(getSelectedLineCount(text, 0, 1)).toBe(1);
125
+ });
126
+
127
+ it('counts each line the range crosses', () => {
128
+ expect(getSelectedLineCount(text, 0, 2)).toBe(2);
129
+ expect(getSelectedLineCount(text, 0, text.length)).toBe(3);
130
+ });
131
+
132
+ it('treats a reversed range the same as start-to-end', () => {
133
+ expect(getSelectedLineCount(text, 2, 0)).toBe(getSelectedLineCount(text, 0, 2));
134
+ });
135
+
136
+ it('stays on one line when the range ends on a newline', () => {
137
+ expect(getSelectedLineCount('hello\nworld', 0, 5)).toBe(1);
138
+ });
139
+ });
140
+
141
+ describe('applyTextSelection', () => {
142
+ it('restores the range and direction on the element', () => {
143
+ const { element, setSelectionRange } = stubTextarea('abc');
144
+ const selection: TextSelection = { start: 1, end: 2, direction: 'forward' };
145
+
146
+ applyTextSelection(element, selection);
147
+
148
+ expect(setSelectionRange).toHaveBeenCalledWith(1, 2, 'forward');
149
+ });
150
+
151
+ it('clamps the range to the current value length', () => {
152
+ const { element, setSelectionRange } = stubTextarea('abc');
153
+
154
+ applyTextSelection(element, { start: 10, end: 20, direction: 'none' });
155
+
156
+ expect(setSelectionRange).toHaveBeenCalledWith(3, 3, 'none');
157
+ });
158
+
159
+ it('clamps only the end when start is still in range', () => {
160
+ const { element, setSelectionRange } = stubTextarea('abc');
161
+
162
+ applyTextSelection(element, { start: 1, end: 99, direction: 'backward' });
163
+
164
+ expect(setSelectionRange).toHaveBeenCalledWith(1, 3, 'backward');
165
+ });
166
+ });
@@ -0,0 +1,2 @@
1
+ export { default as DsCodeInput } from './ds-code-input';
2
+ export * from './ds-code-input.types';