@tcn/ui 0.16.0 → 0.17.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 (185) hide show
  1. package/dist/card.css +1 -0
  2. package/dist/column.css +1 -1
  3. package/dist/containers.css +1 -1
  4. package/dist/containers.module-BmICKsOK.js +5 -0
  5. package/dist/containers.module-BmICKsOK.js.map +1 -0
  6. package/dist/form/field/field.js +11 -8
  7. package/dist/form/field/field.js.map +1 -1
  8. package/dist/inputs/color_input/color_picker.js +5 -2
  9. package/dist/inputs/color_input/color_picker.js.map +1 -1
  10. package/dist/inputs/combo_box/combo_box.js +18 -15
  11. package/dist/inputs/combo_box/combo_box.js.map +1 -1
  12. package/dist/inputs/date_picker/date_picker.js +13 -10
  13. package/dist/inputs/date_picker/date_picker.js.map +1 -1
  14. package/dist/inputs/date_picker/date_picker_input.js +20 -17
  15. package/dist/inputs/date_picker/date_picker_input.js.map +1 -1
  16. package/dist/inputs/date_picker/date_picker_year_selector.js +5 -2
  17. package/dist/inputs/date_picker/date_picker_year_selector.js.map +1 -1
  18. package/dist/inputs/mask_input/key_capture_input.js +26 -23
  19. package/dist/inputs/mask_input/key_capture_input.js.map +1 -1
  20. package/dist/inputs/mask_input/mask_input.js +5 -2
  21. package/dist/inputs/mask_input/mask_input.js.map +1 -1
  22. package/dist/inputs/multiselect/multiselect.js +22 -19
  23. package/dist/inputs/multiselect/multiselect.js.map +1 -1
  24. package/dist/inputs/phone_number_input/phone_number_context.js +7 -4
  25. package/dist/inputs/phone_number_input/phone_number_context.js.map +1 -1
  26. package/dist/inputs/select/select.js +5 -2
  27. package/dist/inputs/select/select.js.map +1 -1
  28. package/dist/inputs/slider/slider.js +19 -16
  29. package/dist/inputs/slider/slider.js.map +1 -1
  30. package/dist/inputs/suggestions/suggestion_list.js +5 -2
  31. package/dist/inputs/suggestions/suggestion_list.js.map +1 -1
  32. package/dist/inputs/switch/switch.js +18 -15
  33. package/dist/inputs/switch/switch.js.map +1 -1
  34. package/dist/inputs/unit_input/unit_input.js +15 -12
  35. package/dist/inputs/unit_input/unit_input.js.map +1 -1
  36. package/dist/layouts/containers/columns/columns.d.ts +6 -1
  37. package/dist/layouts/containers/columns/columns.d.ts.map +1 -1
  38. package/dist/layouts/containers/columns/columns.js +30 -7
  39. package/dist/layouts/containers/columns/columns.js.map +1 -1
  40. package/dist/layouts/containers/rail.d.ts +2 -5
  41. package/dist/layouts/containers/rail.d.ts.map +1 -1
  42. package/dist/layouts/containers/rail.js +17 -55
  43. package/dist/layouts/containers/rail.js.map +1 -1
  44. package/dist/layouts/containers/rows/index.d.ts +3 -0
  45. package/dist/layouts/containers/rows/index.d.ts.map +1 -0
  46. package/dist/layouts/containers/rows/index.js +7 -0
  47. package/dist/layouts/containers/rows/index.js.map +1 -0
  48. package/dist/layouts/containers/rows/row.d.ts +6 -0
  49. package/dist/layouts/containers/rows/row.d.ts.map +1 -0
  50. package/dist/layouts/containers/rows/row.js +20 -0
  51. package/dist/layouts/containers/rows/row.js.map +1 -0
  52. package/dist/layouts/containers/rows/rows.d.ts +11 -0
  53. package/dist/layouts/containers/rows/rows.d.ts.map +1 -0
  54. package/dist/layouts/containers/rows/rows.js +34 -0
  55. package/dist/layouts/containers/rows/rows.js.map +1 -0
  56. package/dist/layouts/containers/scaffold.d.ts +2 -5
  57. package/dist/layouts/containers/scaffold.d.ts.map +1 -1
  58. package/dist/layouts/containers/scaffold.js +17 -55
  59. package/dist/layouts/containers/scaffold.js.map +1 -1
  60. package/dist/layouts/index.d.ts +2 -0
  61. package/dist/layouts/index.d.ts.map +1 -1
  62. package/dist/layouts/index.js +26 -22
  63. package/dist/layouts/index.js.map +1 -1
  64. package/dist/mobile/inputs/date_picker/mobile_date_picker_header.js +5 -2
  65. package/dist/mobile/inputs/date_picker/mobile_date_picker_header.js.map +1 -1
  66. package/dist/mobile/inputs/date_picker/mobile_date_picker_input.js +5 -2
  67. package/dist/mobile/inputs/date_picker/mobile_date_picker_input.js.map +1 -1
  68. package/dist/mobile/inputs/date_picker/mobile_date_picker_year_selector.js +8 -5
  69. package/dist/mobile/inputs/date_picker/mobile_date_picker_year_selector.js.map +1 -1
  70. package/dist/navigation/tabs/state/link/tab_link.js +9 -6
  71. package/dist/navigation/tabs/state/link/tab_link.js.map +1 -1
  72. package/dist/overlay/menu/menu.js +3 -0
  73. package/dist/overlay/menu/menu.js.map +1 -1
  74. package/dist/overlay/popper/context_popper.js +8 -5
  75. package/dist/overlay/popper/context_popper.js.map +1 -1
  76. package/dist/overlay/popper/element_popper.js +9 -6
  77. package/dist/overlay/popper/element_popper.js.map +1 -1
  78. package/dist/overlay/popper/legacy/popper.js +13 -10
  79. package/dist/overlay/popper/legacy/popper.js.map +1 -1
  80. package/dist/overlay/popper/preview_popper.js +10 -7
  81. package/dist/overlay/popper/preview_popper.js.map +1 -1
  82. package/dist/overlay/tethered/tethered.js +11 -8
  83. package/dist/overlay/tethered/tethered.js.map +1 -1
  84. package/dist/resizable.css +1 -0
  85. package/dist/resizable.module-I6iyBAvM.js +5 -0
  86. package/dist/resizable.module-I6iyBAvM.js.map +1 -0
  87. package/dist/resize_handle.css +1 -0
  88. package/dist/row.css +1 -0
  89. package/dist/stacks/box/box.js +12 -9
  90. package/dist/stacks/box/box.js.map +1 -1
  91. package/dist/stacks/box/detect_resize_bounds.d.ts +1 -0
  92. package/dist/stacks/box/detect_resize_bounds.d.ts.map +1 -1
  93. package/dist/stacks/box/detect_resize_bounds.js +22 -20
  94. package/dist/stacks/box/detect_resize_bounds.js.map +1 -1
  95. package/dist/stacks/h_collapsible_box.js +17 -14
  96. package/dist/stacks/h_collapsible_box.js.map +1 -1
  97. package/dist/stacks/v_collapsible_box.js +19 -16
  98. package/dist/stacks/v_collapsible_box.js.map +1 -1
  99. package/dist/surfaces/card/card.d.ts.map +1 -1
  100. package/dist/surfaces/card/card.js +14 -6
  101. package/dist/surfaces/card/card.js.map +1 -1
  102. package/dist/surfaces/pop_confirm/pop_confirm.js +4 -2
  103. package/dist/surfaces/pop_confirm/pop_confirm.js.map +1 -1
  104. package/dist/test-setup.d.ts +2 -0
  105. package/dist/test-setup.d.ts.map +1 -0
  106. package/dist/test-setup.js +10 -0
  107. package/dist/test-setup.js.map +1 -0
  108. package/dist/themes/theme.d.ts.map +1 -1
  109. package/dist/themes/theme.js +17 -22
  110. package/dist/themes/theme.js.map +1 -1
  111. package/dist/themes/themes/ergo/ergo_theme.css +1 -1
  112. package/dist/themes/themes/ergo/ergo_theme.js +201 -21
  113. package/dist/themes/themes/ergo/ergo_theme.js.map +1 -1
  114. package/dist/utils/index.d.ts +1 -0
  115. package/dist/utils/index.d.ts.map +1 -1
  116. package/dist/utils/index.js +39 -26
  117. package/dist/utils/index.js.map +1 -1
  118. package/dist/utils/resize/context.d.ts +4 -0
  119. package/dist/utils/resize/context.d.ts.map +1 -0
  120. package/dist/utils/resize/context.js +10 -0
  121. package/dist/utils/resize/context.js.map +1 -0
  122. package/dist/utils/resize/handle_config.d.ts +32 -0
  123. package/dist/utils/resize/handle_config.d.ts.map +1 -0
  124. package/dist/utils/resize/handle_config.js +85 -0
  125. package/dist/utils/resize/handle_config.js.map +1 -0
  126. package/dist/utils/resize/index.d.ts +10 -0
  127. package/dist/utils/resize/index.d.ts.map +1 -0
  128. package/dist/utils/resize/index.js +16 -0
  129. package/dist/utils/resize/index.js.map +1 -0
  130. package/dist/utils/resize/resizable.d.ts +11 -0
  131. package/dist/utils/resize/resizable.d.ts.map +1 -0
  132. package/dist/utils/resize/resizable.js +52 -0
  133. package/dist/utils/resize/resizable.js.map +1 -0
  134. package/dist/utils/resize/resize_handle.d.ts +7 -0
  135. package/dist/utils/resize/resize_handle.d.ts.map +1 -0
  136. package/dist/utils/resize/resize_handle.js +100 -0
  137. package/dist/utils/resize/resize_handle.js.map +1 -0
  138. package/dist/utils/resize/resize_strategy.d.ts +47 -0
  139. package/dist/utils/resize/resize_strategy.d.ts.map +1 -0
  140. package/dist/utils/resize/resize_strategy.js +108 -0
  141. package/dist/utils/resize/resize_strategy.js.map +1 -0
  142. package/dist/utils/resize/types.d.ts +28 -0
  143. package/dist/utils/resize/types.d.ts.map +1 -0
  144. package/dist/utils/resize/types.js +2 -0
  145. package/dist/utils/resize/types.js.map +1 -0
  146. package/package.json +3 -3
  147. package/src/layouts/__stories__/columns.stories.tsx +31 -0
  148. package/src/layouts/__stories__/composed.stories.tsx +77 -8
  149. package/src/layouts/__stories__/rows.stories.tsx +77 -0
  150. package/src/layouts/__stories__/utils.tsx +2 -84
  151. package/src/layouts/containers/columns/column.module.css +3 -2
  152. package/src/layouts/containers/columns/columns.tsx +29 -3
  153. package/src/layouts/containers/containers.module.css +27 -29
  154. package/src/layouts/containers/rail.tsx +9 -51
  155. package/src/layouts/containers/rows/index.ts +2 -0
  156. package/src/layouts/containers/rows/row.module.css +15 -0
  157. package/src/layouts/containers/rows/row.tsx +22 -0
  158. package/src/layouts/containers/rows/rows.tsx +42 -0
  159. package/src/layouts/containers/scaffold.tsx +9 -49
  160. package/src/layouts/index.ts +2 -0
  161. package/src/stacks/box/detect_resize_bounds.ts +5 -1
  162. package/src/surfaces/card/card.module.css +5 -0
  163. package/src/surfaces/card/card.stories.tsx +66 -8
  164. package/src/surfaces/card/card.tsx +6 -2
  165. package/src/surfaces/page/page.stories.tsx +84 -4
  166. package/src/surfaces/panel/__stories__/panel.stories.tsx +84 -9
  167. package/src/test-setup.ts +11 -0
  168. package/src/themes/theme.tsx +6 -16
  169. package/src/themes/themes/ergo/ergo_theme.css +199 -19
  170. package/src/utils/index.ts +2 -0
  171. package/src/utils/resize/__stories__/resizable.stories.tsx +214 -0
  172. package/src/utils/resize/__stories__/resizable_stories.module.css +47 -0
  173. package/src/utils/resize/__tests__/handle_config.test.ts +269 -0
  174. package/src/utils/resize/__tests__/resize_strategy.test.ts +163 -0
  175. package/src/utils/resize/context.ts +9 -0
  176. package/src/utils/resize/handle_config.ts +142 -0
  177. package/src/utils/resize/index.ts +37 -0
  178. package/src/utils/resize/resizable.module.css +5 -0
  179. package/src/utils/resize/resizable.tsx +97 -0
  180. package/src/utils/resize/resize_handle.module.css +146 -0
  181. package/src/utils/resize/resize_handle.tsx +165 -0
  182. package/src/utils/resize/resize_strategy.ts +190 -0
  183. package/src/utils/resize/types.ts +64 -0
  184. package/dist/containers.module-DlGySre0.js +0 -5
  185. package/dist/containers.module-DlGySre0.js.map +0 -1
@@ -0,0 +1,269 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ getHandleConfig,
4
+ resolveDirection,
5
+ resolveHandleConfig,
6
+ computeResizeState,
7
+ } from '../handle_config.js';
8
+
9
+ describe('getHandleConfig', () => {
10
+ describe('edge positions', () => {
11
+ it('left → horizontal, origin left, invert true, disableDirection true', () => {
12
+ const config = getHandleConfig('left');
13
+ expect(config.horizontal).toEqual({
14
+ origin: 'left',
15
+ invert: true,
16
+ disableDirection: true,
17
+ });
18
+ expect(config.vertical).toBeUndefined();
19
+ });
20
+
21
+ it('right → horizontal, origin right, invert false, disableDirection true', () => {
22
+ const config = getHandleConfig('right');
23
+ expect(config.horizontal).toEqual({
24
+ origin: 'right',
25
+ invert: false,
26
+ disableDirection: true,
27
+ });
28
+ expect(config.vertical).toBeUndefined();
29
+ });
30
+
31
+ it('start → horizontal, origin left, invert true, disableDirection false', () => {
32
+ const config = getHandleConfig('start');
33
+ expect(config.horizontal).toEqual({
34
+ origin: 'left',
35
+ invert: true,
36
+ disableDirection: false,
37
+ });
38
+ expect(config.vertical).toBeUndefined();
39
+ });
40
+
41
+ it('end → horizontal, origin right, invert false, disableDirection false', () => {
42
+ const config = getHandleConfig('end');
43
+ expect(config.horizontal).toEqual({
44
+ origin: 'right',
45
+ invert: false,
46
+ disableDirection: false,
47
+ });
48
+ expect(config.vertical).toBeUndefined();
49
+ });
50
+
51
+ it('top → vertical, origin top, invert true', () => {
52
+ const config = getHandleConfig('top');
53
+ expect(config.vertical).toEqual({
54
+ origin: 'top',
55
+ invert: true,
56
+ disableDirection: true,
57
+ });
58
+ expect(config.horizontal).toBeUndefined();
59
+ });
60
+
61
+ it('bottom → vertical, origin bottom, invert false', () => {
62
+ const config = getHandleConfig('bottom');
63
+ expect(config.vertical).toEqual({
64
+ origin: 'bottom',
65
+ invert: false,
66
+ disableDirection: true,
67
+ });
68
+ expect(config.horizontal).toBeUndefined();
69
+ });
70
+ });
71
+
72
+ describe('corner positions (physical)', () => {
73
+ it('top-left → both axes', () => {
74
+ const config = getHandleConfig('top-left');
75
+ expect(config.horizontal).toEqual({
76
+ origin: 'left',
77
+ invert: true,
78
+ disableDirection: true,
79
+ });
80
+ expect(config.vertical).toEqual({
81
+ origin: 'top',
82
+ invert: true,
83
+ disableDirection: true,
84
+ });
85
+ });
86
+
87
+ it('top-right → both axes', () => {
88
+ const config = getHandleConfig('top-right');
89
+ expect(config.horizontal).toEqual({
90
+ origin: 'right',
91
+ invert: false,
92
+ disableDirection: true,
93
+ });
94
+ expect(config.vertical).toEqual({
95
+ origin: 'top',
96
+ invert: true,
97
+ disableDirection: true,
98
+ });
99
+ });
100
+
101
+ it('bottom-left → both axes', () => {
102
+ const config = getHandleConfig('bottom-left');
103
+ expect(config.horizontal).toEqual({
104
+ origin: 'left',
105
+ invert: true,
106
+ disableDirection: true,
107
+ });
108
+ expect(config.vertical).toEqual({
109
+ origin: 'bottom',
110
+ invert: false,
111
+ disableDirection: true,
112
+ });
113
+ });
114
+
115
+ it('bottom-right → both axes', () => {
116
+ const config = getHandleConfig('bottom-right');
117
+ expect(config.horizontal).toEqual({
118
+ origin: 'right',
119
+ invert: false,
120
+ disableDirection: true,
121
+ });
122
+ expect(config.vertical).toEqual({
123
+ origin: 'bottom',
124
+ invert: false,
125
+ disableDirection: true,
126
+ });
127
+ });
128
+ });
129
+
130
+ describe('corner positions (logical, RTL-aware)', () => {
131
+ it('top-start → horizontal disableDirection false', () => {
132
+ const config = getHandleConfig('top-start');
133
+ expect(config.horizontal?.disableDirection).toBe(false);
134
+ expect(config.vertical?.disableDirection).toBe(true);
135
+ });
136
+
137
+ it('top-end → horizontal disableDirection false', () => {
138
+ const config = getHandleConfig('top-end');
139
+ expect(config.horizontal?.disableDirection).toBe(false);
140
+ expect(config.vertical?.disableDirection).toBe(true);
141
+ });
142
+
143
+ it('bottom-start → horizontal disableDirection false', () => {
144
+ const config = getHandleConfig('bottom-start');
145
+ expect(config.horizontal?.disableDirection).toBe(false);
146
+ expect(config.vertical?.disableDirection).toBe(true);
147
+ });
148
+
149
+ it('bottom-end → horizontal disableDirection false', () => {
150
+ const config = getHandleConfig('bottom-end');
151
+ expect(config.horizontal?.disableDirection).toBe(false);
152
+ expect(config.vertical?.disableDirection).toBe(true);
153
+ });
154
+ });
155
+ });
156
+
157
+ describe('resolveDirection', () => {
158
+ describe('LTR', () => {
159
+ it('invert=false → +1', () => {
160
+ expect(resolveDirection('ltr', false, false)).toBe(1);
161
+ });
162
+
163
+ it('invert=true → -1', () => {
164
+ expect(resolveDirection('ltr', true, false)).toBe(-1);
165
+ });
166
+
167
+ it('disableDirection has no effect in LTR', () => {
168
+ expect(resolveDirection('ltr', false, true)).toBe(1);
169
+ expect(resolveDirection('ltr', true, true)).toBe(-1);
170
+ });
171
+ });
172
+
173
+ describe('RTL', () => {
174
+ it('invert=false, disableDirection=false → flipped to -1', () => {
175
+ expect(resolveDirection('rtl', false, false)).toBe(-1);
176
+ });
177
+
178
+ it('invert=true, disableDirection=false → flipped to +1', () => {
179
+ expect(resolveDirection('rtl', true, false)).toBe(1);
180
+ });
181
+
182
+ it('disableDirection=true → no flip, same as LTR', () => {
183
+ expect(resolveDirection('rtl', false, true)).toBe(1);
184
+ expect(resolveDirection('rtl', true, true)).toBe(-1);
185
+ });
186
+ });
187
+ });
188
+
189
+ describe('computeResizeState', () => {
190
+ it('computes positive delta when moving in direction', () => {
191
+ const result = computeResizeState(200, 100, 150, 1, 200);
192
+ expect(result.newSize).toBe(250);
193
+ expect(result.totalDelta).toBe(50);
194
+ expect(result.currentDelta).toBe(50);
195
+ });
196
+
197
+ it('computes negative delta when moving against direction', () => {
198
+ const result = computeResizeState(200, 100, 50, 1, 200);
199
+ expect(result.newSize).toBe(150);
200
+ expect(result.totalDelta).toBe(-50);
201
+ expect(result.currentDelta).toBe(-50);
202
+ });
203
+
204
+ it('respects direction multiplier', () => {
205
+ const result = computeResizeState(200, 100, 150, -1, 200);
206
+ expect(result.newSize).toBe(150);
207
+ expect(result.totalDelta).toBe(-50);
208
+ expect(result.currentDelta).toBe(-50);
209
+ });
210
+
211
+ it('tracks currentDelta relative to previousSize', () => {
212
+ // First move: 200 → 250
213
+ const first = computeResizeState(200, 100, 150, 1, 200);
214
+ expect(first.newSize).toBe(250);
215
+ expect(first.currentDelta).toBe(50);
216
+
217
+ // Second move: 200 → 260 (10px further), previous was 250
218
+ const second = computeResizeState(200, 100, 160, 1, 250);
219
+ expect(second.newSize).toBe(260);
220
+ expect(second.totalDelta).toBe(60);
221
+ expect(second.currentDelta).toBe(10);
222
+ });
223
+
224
+ it('handles zero movement', () => {
225
+ const result = computeResizeState(200, 100, 100, 1, 200);
226
+ expect(result.newSize).toBe(200);
227
+ expect(result.totalDelta).toBe(0);
228
+ expect(result.currentDelta).toBe(0);
229
+ });
230
+ });
231
+
232
+ describe('resolveHandleConfig', () => {
233
+ it('resolves edge position with direction in LTR', () => {
234
+ const config = resolveHandleConfig('right', 'ltr');
235
+ expect(config.horizontal).toEqual({ origin: 'right', direction: 1 });
236
+ expect(config.vertical).toBeUndefined();
237
+ });
238
+
239
+ it('resolves edge position with direction in LTR (inverted)', () => {
240
+ const config = resolveHandleConfig('left', 'ltr');
241
+ expect(config.horizontal).toEqual({ origin: 'left', direction: -1 });
242
+ });
243
+
244
+ it('resolves logical position — start flips in RTL', () => {
245
+ const ltr = resolveHandleConfig('start', 'ltr');
246
+ const rtl = resolveHandleConfig('start', 'rtl');
247
+ expect(ltr.horizontal?.direction).toBe(-1);
248
+ expect(rtl.horizontal?.direction).toBe(1);
249
+ });
250
+
251
+ it('resolves physical position — left ignores RTL', () => {
252
+ const ltr = resolveHandleConfig('left', 'ltr');
253
+ const rtl = resolveHandleConfig('left', 'rtl');
254
+ expect(ltr.horizontal?.direction).toBe(-1);
255
+ expect(rtl.horizontal?.direction).toBe(-1);
256
+ });
257
+
258
+ it('resolves corner with both axes', () => {
259
+ const config = resolveHandleConfig('bottom-right', 'ltr');
260
+ expect(config.horizontal).toEqual({ origin: 'right', direction: 1 });
261
+ expect(config.vertical).toEqual({ origin: 'bottom', direction: 1 });
262
+ });
263
+
264
+ it('resolves logical corner — horizontal flips in RTL, vertical does not', () => {
265
+ const config = resolveHandleConfig('bottom-start', 'rtl');
266
+ expect(config.horizontal?.direction).toBe(1);
267
+ expect(config.vertical?.direction).toBe(1);
268
+ });
269
+ });
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ResizeHandleStrategy } from '../resize_strategy.js';
3
+ import type { StartResizeParams } from '../resize_strategy.js';
4
+
5
+ const defaults: StartResizeParams = {
6
+ rectangle: {
7
+ dimensions: { width: 300, height: 200 },
8
+ position: { x: 100, y: 100 },
9
+ },
10
+ languageDirection: 'ltr',
11
+ };
12
+
13
+ function start(
14
+ strategy: ResizeHandleStrategy,
15
+ overrides: Partial<StartResizeParams> = {}
16
+ ) {
17
+ strategy.startResize({ ...defaults, ...overrides });
18
+ }
19
+
20
+ describe('ResizeHandleStrategy', () => {
21
+ describe('horizontal edge (right)', () => {
22
+ it('computes positive resize when dragging right', () => {
23
+ const strategy = new ResizeHandleStrategy('right');
24
+ start(strategy);
25
+
26
+ const result = strategy.resize({ x: 150, y: 100 });
27
+ expect(result.horizontal).toBeDefined();
28
+ expect(result.horizontal?.newSize).toBe(350);
29
+ expect(result.horizontal?.origin).toBe('right');
30
+ expect(result.horizontal?.totalDelta).toBe(50);
31
+ expect(result.horizontal?.currentDelta).toBe(50);
32
+ expect(result.vertical).toBeUndefined();
33
+ });
34
+
35
+ it('computes negative resize when dragging left', () => {
36
+ const strategy = new ResizeHandleStrategy('right');
37
+ start(strategy);
38
+
39
+ const result = strategy.resize({ x: 50, y: 100 });
40
+ expect(result.horizontal?.newSize).toBe(250);
41
+ expect(result.horizontal?.totalDelta).toBe(-50);
42
+ });
43
+ });
44
+
45
+ describe('horizontal edge (left, inverted)', () => {
46
+ it('shrinks when dragging right', () => {
47
+ const strategy = new ResizeHandleStrategy('left');
48
+ start(strategy);
49
+
50
+ const result = strategy.resize({ x: 150, y: 100 });
51
+ expect(result.horizontal?.newSize).toBe(250);
52
+ expect(result.horizontal?.origin).toBe('left');
53
+ });
54
+
55
+ it('grows when dragging left', () => {
56
+ const strategy = new ResizeHandleStrategy('left');
57
+ start(strategy);
58
+
59
+ const result = strategy.resize({ x: 50, y: 100 });
60
+ expect(result.horizontal?.newSize).toBe(350);
61
+ });
62
+ });
63
+
64
+ describe('vertical edge (bottom)', () => {
65
+ it('computes vertical resize only', () => {
66
+ const strategy = new ResizeHandleStrategy('bottom');
67
+ start(strategy);
68
+
69
+ const result = strategy.resize({ x: 100, y: 150 });
70
+ expect(result.horizontal).toBeUndefined();
71
+ expect(result.vertical).toBeDefined();
72
+ expect(result.vertical?.newSize).toBe(250);
73
+ expect(result.vertical?.origin).toBe('bottom');
74
+ });
75
+ });
76
+
77
+ describe('corner (bottom-right)', () => {
78
+ it('computes both axes simultaneously', () => {
79
+ const strategy = new ResizeHandleStrategy('bottom-right');
80
+ start(strategy);
81
+
82
+ const result = strategy.resize({ x: 160, y: 140 });
83
+ expect(result.horizontal?.newSize).toBe(360);
84
+ expect(result.horizontal?.origin).toBe('right');
85
+ expect(result.vertical?.newSize).toBe(240);
86
+ expect(result.vertical?.origin).toBe('bottom');
87
+ });
88
+ });
89
+
90
+ describe('RTL — logical positions', () => {
91
+ it('start handle flips direction in RTL', () => {
92
+ const strategy = new ResizeHandleStrategy('start');
93
+ start(strategy, { languageDirection: 'rtl' });
94
+
95
+ const result = strategy.resize({ x: 150, y: 100 });
96
+ expect(result.horizontal?.newSize).toBe(350);
97
+ });
98
+
99
+ it('start handle does not flip in LTR', () => {
100
+ const strategy = new ResizeHandleStrategy('start');
101
+ start(strategy);
102
+
103
+ const result = strategy.resize({ x: 150, y: 100 });
104
+ expect(result.horizontal?.newSize).toBe(250);
105
+ });
106
+
107
+ it('left handle ignores RTL (physical)', () => {
108
+ const strategy = new ResizeHandleStrategy('left');
109
+ start(strategy, { languageDirection: 'rtl' });
110
+
111
+ const result = strategy.resize({ x: 150, y: 100 });
112
+ expect(result.horizontal?.newSize).toBe(250);
113
+ });
114
+ });
115
+
116
+ describe('commitResize — tracks applied dimensions', () => {
117
+ it('currentDelta tracks committed size', () => {
118
+ const strategy = new ResizeHandleStrategy('right');
119
+ start(strategy);
120
+
121
+ strategy.resize({ x: 150, y: 100 });
122
+ strategy.commitResize({ width: 350, height: 200 });
123
+
124
+ const second = strategy.resize({ x: 170, y: 100 });
125
+ expect(second.horizontal?.newSize).toBe(370);
126
+ expect(second.horizontal?.currentDelta).toBe(20);
127
+ });
128
+
129
+ it('currentDelta is correct when previous resize was clamped', () => {
130
+ const strategy = new ResizeHandleStrategy('right');
131
+ start(strategy);
132
+
133
+ strategy.resize({ x: 150, y: 100 });
134
+ strategy.commitResize({ width: 320, height: 200 });
135
+
136
+ const result = strategy.resize({ x: 160, y: 100 });
137
+ expect(result.horizontal?.newSize).toBe(360);
138
+ expect(result.horizontal?.currentDelta).toBe(40);
139
+ });
140
+ });
141
+
142
+ describe('endResize', () => {
143
+ it('returns committed sizes and origins', () => {
144
+ const strategy = new ResizeHandleStrategy('bottom-right');
145
+ start(strategy);
146
+
147
+ strategy.resize({ x: 180, y: 160 });
148
+ strategy.commitResize({ width: 380, height: 260 });
149
+
150
+ const result = strategy.endResize();
151
+ expect(result.horizontal).toEqual({ width: 380, origin: 'right' });
152
+ expect(result.vertical).toEqual({ height: 260, origin: 'bottom' });
153
+ });
154
+
155
+ it('returns start sizes if no resize occurred', () => {
156
+ const strategy = new ResizeHandleStrategy('right');
157
+ start(strategy);
158
+
159
+ const result = strategy.endResize();
160
+ expect(result.horizontal).toEqual({ width: 300, origin: 'right' });
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,9 @@
1
+ import { createContext } from 'react';
2
+ import { makeContextHook } from '../hooks/make_context_hook.js';
3
+ import type { ResizableContextValue } from './types.js';
4
+
5
+ export const ResizableContext = createContext<ResizableContextValue | undefined>(
6
+ undefined
7
+ );
8
+
9
+ export const useResizable = makeContextHook(ResizableContext, 'ResizableContext');
@@ -0,0 +1,142 @@
1
+ import type {
2
+ AxisConfig,
3
+ HandleConfig,
4
+ ResolvedHandleConfig,
5
+ ResizeHandlePosition,
6
+ } from './types.js';
7
+
8
+ /**
9
+ * Pure position → config lookup. No DOM, no React.
10
+ *
11
+ * Edges populate one axis, corners populate both.
12
+ * The React adapter reads DOM state (getBoundingClientRect, getComputedStyle)
13
+ * and feeds resolved values into resolveDirection / computeResizeState.
14
+ */
15
+
16
+ const configs: Record<ResizeHandlePosition, HandleConfig> = {
17
+ // Edges — single axis
18
+ left: {
19
+ horizontal: { origin: 'left', invert: true, disableDirection: true },
20
+ },
21
+ right: {
22
+ horizontal: { origin: 'right', invert: false, disableDirection: true },
23
+ },
24
+ start: {
25
+ horizontal: { origin: 'left', invert: true, disableDirection: false },
26
+ },
27
+ end: {
28
+ horizontal: { origin: 'right', invert: false, disableDirection: false },
29
+ },
30
+ top: {
31
+ vertical: { origin: 'top', invert: true, disableDirection: true },
32
+ },
33
+ bottom: {
34
+ vertical: { origin: 'bottom', invert: false, disableDirection: true },
35
+ },
36
+
37
+ // Corners — dual axis (physical)
38
+ 'top-left': {
39
+ horizontal: { origin: 'left', invert: true, disableDirection: true },
40
+ vertical: { origin: 'top', invert: true, disableDirection: true },
41
+ },
42
+ 'top-right': {
43
+ horizontal: { origin: 'right', invert: false, disableDirection: true },
44
+ vertical: { origin: 'top', invert: true, disableDirection: true },
45
+ },
46
+ 'bottom-left': {
47
+ horizontal: { origin: 'left', invert: true, disableDirection: true },
48
+ vertical: { origin: 'bottom', invert: false, disableDirection: true },
49
+ },
50
+ 'bottom-right': {
51
+ horizontal: { origin: 'right', invert: false, disableDirection: true },
52
+ vertical: { origin: 'bottom', invert: false, disableDirection: true },
53
+ },
54
+
55
+ // Corners — dual axis (logical, RTL-aware)
56
+ 'top-start': {
57
+ horizontal: { origin: 'left', invert: true, disableDirection: false },
58
+ vertical: { origin: 'top', invert: true, disableDirection: true },
59
+ },
60
+ 'top-end': {
61
+ horizontal: { origin: 'right', invert: false, disableDirection: false },
62
+ vertical: { origin: 'top', invert: true, disableDirection: true },
63
+ },
64
+ 'bottom-start': {
65
+ horizontal: { origin: 'left', invert: true, disableDirection: false },
66
+ vertical: { origin: 'bottom', invert: false, disableDirection: true },
67
+ },
68
+ 'bottom-end': {
69
+ horizontal: { origin: 'right', invert: false, disableDirection: false },
70
+ vertical: { origin: 'bottom', invert: false, disableDirection: true },
71
+ },
72
+ };
73
+
74
+ export function getHandleConfig(position: ResizeHandlePosition): HandleConfig {
75
+ return configs[position];
76
+ }
77
+
78
+ /**
79
+ * Resolve the direction multiplier for a resize axis.
80
+ *
81
+ * @param languageDirection - 'ltr' or 'rtl' from getComputedStyle (passed by React adapter)
82
+ * @param invert - whether the axis inverts the delta (e.g. dragging left-handle right shrinks)
83
+ * @param disableDirection - if true, ignore RTL (physical positions like left/right)
84
+ * @returns +1 or -1
85
+ */
86
+ export function resolveDirection(
87
+ languageDirection: string,
88
+ invert: boolean,
89
+ disableDirection: boolean
90
+ ): number {
91
+ const finalInvert = languageDirection === 'rtl' && !disableDirection ? !invert : invert;
92
+ return finalInvert ? -1 : 1;
93
+ }
94
+
95
+ function resolveAxis(axis: AxisConfig, languageDirection: string) {
96
+ return {
97
+ origin: axis.origin,
98
+ direction: resolveDirection(languageDirection, axis.invert, axis.disableDirection),
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Combines position lookup with direction resolution.
104
+ * The caller only needs to provide the language direction from the DOM —
105
+ * all config internals (invert, disableDirection) are resolved here.
106
+ */
107
+ export function resolveHandleConfig(
108
+ position: ResizeHandlePosition,
109
+ languageDirection: string
110
+ ): ResolvedHandleConfig {
111
+ const config = configs[position];
112
+ return {
113
+ horizontal: config.horizontal
114
+ ? resolveAxis(config.horizontal, languageDirection)
115
+ : undefined,
116
+ vertical: config.vertical
117
+ ? resolveAxis(config.vertical, languageDirection)
118
+ : undefined,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Pure resize state computation. Called per-axis on each mousemove.
124
+ *
125
+ * @param startSize - element width/height at mousedown (from getBoundingClientRect)
126
+ * @param startCoord - clientX/clientY at mousedown
127
+ * @param currentCoord - clientX/clientY at this mousemove
128
+ * @param direction - +1 or -1 from resolveDirection
129
+ * @param previousSize - the size after the last mousemove (for currentDelta)
130
+ */
131
+ export function computeResizeState(
132
+ startSize: number,
133
+ startCoord: number,
134
+ currentCoord: number,
135
+ direction: number,
136
+ previousSize: number
137
+ ): { newSize: number; totalDelta: number; currentDelta: number } {
138
+ const totalDelta = direction * (currentCoord - startCoord);
139
+ const newSize = startSize + totalDelta;
140
+ const currentDelta = newSize - previousSize;
141
+ return { newSize, totalDelta, currentDelta };
142
+ }
@@ -0,0 +1,37 @@
1
+ export { Resizable } from './resizable.js';
2
+ export type { ResizableProps } from './resizable.js';
3
+
4
+ export { ResizeHandle } from './resize_handle.js';
5
+ export type { ResizeHandleProps } from './resize_handle.js';
6
+
7
+ export { useResizable } from './context.js';
8
+
9
+ export {
10
+ getHandleConfig,
11
+ resolveDirection,
12
+ resolveHandleConfig,
13
+ computeResizeState,
14
+ } from './handle_config.js';
15
+ export { ResizeHandleStrategy } from './resize_strategy.js';
16
+ export type {
17
+ StartResizeParams,
18
+ HorizontalResizeResult,
19
+ VerticalResizeResult,
20
+ ResizeResult,
21
+ EndResizeResult,
22
+ } from './resize_strategy.js';
23
+
24
+ export type {
25
+ ResizeHandlePosition,
26
+ HandleConfig,
27
+ AxisConfig,
28
+ ResizableContextValue,
29
+ ResolvedAxisConfig,
30
+ ResolvedHandleConfig,
31
+ OnWidthResize,
32
+ OnWidthResizeEnd,
33
+ OnHeightResize,
34
+ OnHeightResizeEnd,
35
+ WidthResizeOrigin,
36
+ HeightResizeOrigin,
37
+ } from './types.js';
@@ -0,0 +1,5 @@
1
+ @layer tcn-system {
2
+ :where(.resizable-target) {
3
+ position: relative;
4
+ }
5
+ }