@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
@@ -634,20 +634,19 @@
634
634
  .tcn-scaffold {
635
635
  --scaffold-divide-footer: var(--divide-footer, 1);
636
636
  --scaffold-divide-header: var(--divide-header, 1);
637
- --pad-inline: var(--padding-large);
637
+ /* Since Scaffold now acts as body too - there is no pad inline default - each outer container should set it */
638
+ /* --pad-inline: var(--padding-large); */
638
639
 
639
640
  /* Border appears on nested scaffold/rail when it follows header or utility-bar */
640
- :where(.tcn-header) + :where(.tcn-scaffold),
641
- :where(.tcn-header) + :where(.tcn-rail),
642
- :where(.tcn-utility-bar) + :where(.tcn-scaffold),
643
- :where(.tcn-utility-bar) + :where(.tcn-rail) {
644
- border-top: calc(var(--scaffold-divide-header) * 1px) solid
641
+ :where(.tcn-header) {
642
+ border-bottom: calc(var(--scaffold-divide-header) * 1px) solid
645
643
  var(--ergo-material-border);
646
644
  }
645
+ :where(.tcn-utility-bar) {
646
+ border-bottom: 1px solid var(--ergo-material-border);
647
+ }
647
648
 
648
- /* Border appears on footer when it follows scaffold/rail */
649
- :where(.tcn-scaffold) + :where(.tcn-footer),
650
- :where(.tcn-rail) + :where(.tcn-footer) {
649
+ :where(.tcn-footer) {
651
650
  border-top: calc(var(--scaffold-divide-footer) * 1px) solid
652
651
  var(--ergo-material-border);
653
652
  }
@@ -661,6 +660,7 @@
661
660
  .tcn-page {
662
661
  --divide-header: 0;
663
662
  --divide-footer: 0;
663
+ --pad-inline: var(--padding-large);
664
664
  --material: var(--background-color-tertiary);
665
665
  background-color: var(--material);
666
666
  }
@@ -668,6 +668,7 @@
668
668
  /* MODAL: */
669
669
  .tcn-modal {
670
670
  --divide-header: 0;
671
+ --pad-inline: var(--padding-large);
671
672
  box-shadow: 0px 4px 34px 0px #00000096;
672
673
  border-radius: var(--ergo-shape-radius-medium);
673
674
  background-color: var(--background-color-primary);
@@ -691,6 +692,7 @@
691
692
  /* DRAWER: */
692
693
  .tcn-drawer {
693
694
  --divide-header: 0;
695
+ --pad-inline: var(--padding-large);
694
696
  box-shadow: 0px 4px 34px 0px #00000096;
695
697
  background-color: var(--background-color-primary);
696
698
  overflow: hidden;
@@ -713,6 +715,7 @@
713
715
  .tcn-window {
714
716
  box-shadow: 0px 4px 34px 0px #00000096;
715
717
  --divide-header: 0;
718
+ --pad-inline: var(--padding-large);
716
719
  border-radius: var(--ergo-shape-radius-medium);
717
720
  background-color: var(--background-color-primary);
718
721
  overflow: hidden;
@@ -737,7 +740,7 @@
737
740
  --tether-pad-size: 16px;
738
741
  --pad-inline: var(--padding-medium);
739
742
 
740
- .tcn-pop-confirm-scaffold > .tcn-scaffold-stack {
743
+ .tcn-pop-confirm-scaffold {
741
744
  border-radius: var(--ergo-shape-radius-medium);
742
745
  box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.4);
743
746
  background-color: var(--background-color-primary);
@@ -794,6 +797,7 @@
794
797
  .tcn-panel {
795
798
  --divide-header: 1;
796
799
  --divide-footer: 1;
800
+ --pad-inline: var(--padding-large);
797
801
  --material: var(--background-color-primary);
798
802
  background-color: var(--material);
799
803
  border-radius: var(--ergo-shape-radius-medium);
@@ -808,6 +812,7 @@
808
812
  .tcn-aside {
809
813
  --divide-header: 1;
810
814
  --divide-footer: 1;
815
+ --pad-inline: var(--padding-large);
811
816
 
812
817
  --material: var(--background-color-secondary);
813
818
  background-color: var(--material);
@@ -855,13 +860,6 @@
855
860
  :where(.tcn-utility-bar) {
856
861
  min-height: var(--ergo-sizing-bar-sm);
857
862
  }
858
-
859
- :where(.tcn-scaffold-stack) > :where(.tcn-scaffold) > :where(.tcn-scaffold-stack),
860
- :where(.tcn-scaffold-stack) > :where(.tcn-rail) > :where(.tcn-rail-stack),
861
- :where(.tcn-rail-stack) > :where(.tcn-scaffold) > :where(.tcn-scaffold-stack),
862
- :where(.tcn-rail-stack) > :where(.tcn-rail) > :where(.tcn-rail-stack) {
863
- padding-inline: var(--padding-medium);
864
- }
865
863
  }
866
864
 
867
865
  /* Columns: */
@@ -870,6 +868,7 @@
870
868
  padding: var(--padding-large);
871
869
 
872
870
  :where(.tcn-column) {
871
+ gap: var(--gap-medium);
873
872
  border-right: 1px solid var(--ergo-material-border);
874
873
  padding-inline-end: var(--padding-medium);
875
874
  }
@@ -880,16 +879,54 @@
880
879
  }
881
880
  }
882
881
 
882
+ /* Resize handle offsets */
883
+ :where(.tcn-rail.tcn-columns) {
884
+ --resize-offset: calc(-1 * var(--padding-medium));
885
+ }
886
+
887
+ :where(.tcn-rail):not(.tcn-columns) {
888
+ --resize-offset: 0px;
889
+ }
890
+
883
891
  .tcn-page {
884
892
  :where(.tcn-columns) {
885
893
  padding: var(--padding-medium);
886
894
 
887
- :where(.tcn-column):not(:last-child) {
895
+ :where(.tcn-column) {
896
+ gap: var(--gap-medium);
897
+ }
898
+
899
+ > .tcn-column:not(:last-child) {
888
900
  padding-inline-end: var(--padding-medium);
889
901
  }
890
902
  }
891
903
  }
892
904
 
905
+ /* Rows: */
906
+ .tcn-panel,
907
+ .tcn-card {
908
+ :where(.tcn-rows) {
909
+ :where(.tcn-row):not(:last-child) {
910
+ border-bottom: 1px solid var(--ergo-material-border);
911
+ padding-block-end: var(--padding-medium);
912
+ }
913
+
914
+ :where(.tcn-row):not(:first-child) {
915
+ padding-block-start: var(--padding-medium);
916
+ }
917
+ }
918
+ }
919
+
920
+ .tcn-page {
921
+ :where(.tcn-rows) {
922
+ padding: var(--padding-medium);
923
+
924
+ :where(.tcn-row):not(:last-child) {
925
+ padding-block-end: var(--padding-medium);
926
+ }
927
+ }
928
+ }
929
+
893
930
  /* SECTION */
894
931
  .tcn-section {
895
932
  --max-section-depth: 4;
@@ -938,7 +975,7 @@
938
975
  padding-inline-end: var(--pad-inline, var(--padding-medium));
939
976
  }
940
977
 
941
- .tcn-scaffold-stack > .tcn-detail {
978
+ .tcn-scaffold > .tcn-detail {
942
979
  padding-inline: var(--pad-inline, var(--padding-medium));
943
980
  }
944
981
 
@@ -1423,4 +1460,147 @@
1423
1460
  --accent-color: var(--ergo-accent-blue);
1424
1461
  }
1425
1462
  }
1463
+
1464
+ /* ===== Resize Handle ===== */
1465
+
1466
+ /* Edge indicators: centered pill chip */
1467
+ .tcn-resize-handle[data-axis="horizontal"] {
1468
+ width: 8px;
1469
+ }
1470
+
1471
+ .tcn-resize-handle[data-axis="vertical"] {
1472
+ height: 8px;
1473
+ }
1474
+
1475
+ .tcn-resize-handle[data-axis="horizontal"]::after,
1476
+ .tcn-resize-handle[data-axis="vertical"]::after {
1477
+ content: "";
1478
+ position: absolute;
1479
+ border-radius: var(--ergo-shape-radius-medium);
1480
+ background-color: var(--ergo-grey-light);
1481
+ pointer-events: none;
1482
+ transition: background-color 0.3s;
1483
+ top: 50%;
1484
+ left: 50%;
1485
+ transform: translate(-50%, -50%);
1486
+ }
1487
+
1488
+ .tcn-resize-handle[data-axis="horizontal"]::after {
1489
+ width: 4px;
1490
+ height: 16px;
1491
+ }
1492
+
1493
+ .tcn-resize-handle[data-axis="vertical"]::after {
1494
+ width: 16px;
1495
+ height: 4px;
1496
+ }
1497
+
1498
+ /* Corner L-shape indicator */
1499
+ .tcn-resize-handle[data-axis="corner"]::after {
1500
+ content: "";
1501
+ position: absolute;
1502
+ width: 8px;
1503
+ height: 8px;
1504
+ border-style: solid;
1505
+ border-color: var(--ergo-grey-light);
1506
+ border-width: 0;
1507
+ pointer-events: none;
1508
+ transition: border-color 0.15s;
1509
+ }
1510
+
1511
+ /* Physical corners */
1512
+ .tcn-resize-handle[data-position="top-left"]::after {
1513
+ top: 0;
1514
+ left: 0;
1515
+ border-top-width: 4px;
1516
+ border-left-width: 4px;
1517
+ }
1518
+
1519
+ .tcn-resize-handle[data-position="top-right"]::after {
1520
+ top: 0;
1521
+ right: 0;
1522
+ border-top-width: 4px;
1523
+ border-right-width: 4px;
1524
+ }
1525
+
1526
+ .tcn-resize-handle[data-position="bottom-left"]::after {
1527
+ bottom: 0;
1528
+ left: 0;
1529
+ border-bottom-width: 4px;
1530
+ border-left-width: 4px;
1531
+ }
1532
+
1533
+ .tcn-resize-handle[data-position="bottom-right"]::after {
1534
+ bottom: 0;
1535
+ right: 0;
1536
+ border-bottom-width: 4px;
1537
+ border-right-width: 4px;
1538
+ }
1539
+
1540
+ /* Logical corners (RTL-aware) */
1541
+ .tcn-resize-handle[data-position="top-start"]:dir(ltr)::after {
1542
+ top: 0;
1543
+ left: 0;
1544
+ border-top-width: 4px;
1545
+ border-left-width: 4px;
1546
+ }
1547
+
1548
+ .tcn-resize-handle[data-position="top-start"]:dir(rtl)::after {
1549
+ top: 0;
1550
+ right: 0;
1551
+ border-top-width: 4px;
1552
+ border-right-width: 4px;
1553
+ }
1554
+
1555
+ .tcn-resize-handle[data-position="top-end"]:dir(ltr)::after {
1556
+ top: 0;
1557
+ right: 0;
1558
+ border-top-width: 4px;
1559
+ border-right-width: 4px;
1560
+ }
1561
+
1562
+ .tcn-resize-handle[data-position="top-end"]:dir(rtl)::after {
1563
+ top: 0;
1564
+ left: 0;
1565
+ border-top-width: 4px;
1566
+ border-left-width: 4px;
1567
+ }
1568
+
1569
+ .tcn-resize-handle[data-position="bottom-start"]:dir(ltr)::after {
1570
+ bottom: 0;
1571
+ left: 0;
1572
+ border-bottom-width: 4px;
1573
+ border-left-width: 4px;
1574
+ }
1575
+
1576
+ .tcn-resize-handle[data-position="bottom-start"]:dir(rtl)::after {
1577
+ bottom: 0;
1578
+ right: 0;
1579
+ border-bottom-width: 4px;
1580
+ border-right-width: 4px;
1581
+ }
1582
+
1583
+ .tcn-resize-handle[data-position="bottom-end"]:dir(ltr)::after {
1584
+ bottom: 0;
1585
+ right: 0;
1586
+ border-bottom-width: 4px;
1587
+ border-right-width: 4px;
1588
+ }
1589
+
1590
+ .tcn-resize-handle[data-position="bottom-end"]:dir(rtl)::after {
1591
+ bottom: 0;
1592
+ left: 0;
1593
+ border-bottom-width: 4px;
1594
+ border-left-width: 4px;
1595
+ }
1596
+
1597
+ /* Hover */
1598
+ .tcn-resize-handle[data-axis="horizontal"]:hover::after,
1599
+ .tcn-resize-handle[data-axis="vertical"]:hover::after {
1600
+ background-color: var(--ergo-grey-dark);
1601
+ }
1602
+
1603
+ .tcn-resize-handle:hover::after {
1604
+ border-color: var(--ergo-grey-dark);
1605
+ }
1426
1606
  }
@@ -21,3 +21,5 @@ export * from './types/variations.js';
21
21
 
22
22
  export * from './dnd/draggable/draggable.js';
23
23
  export * from './dnd/handle.js';
24
+
25
+ export * from './resize/index.js';
@@ -0,0 +1,214 @@
1
+ import { useRef, useState } from 'react';
2
+ import { Resizable } from '../resizable.js';
3
+ import { ResizeHandle } from '../resize_handle.js';
4
+ import { Box } from '../../../stacks/box/box.js';
5
+ import { VStack } from '../../../stacks/v_stack.js';
6
+ import { BodyText } from '../../../typography/index.js';
7
+ import styles from './resizable_stories.module.css';
8
+
9
+ export default {
10
+ title: 'Utils/Resizable',
11
+ component: Resizable,
12
+ tags: ['autodocs'],
13
+ };
14
+
15
+ export const HorizontalResize = () => {
16
+ const [width, setWidth] = useState(300);
17
+
18
+ return (
19
+ <VStack gap="8px" padding="16px" minHeight="400px">
20
+ <BodyText>Drag the right edge to resize horizontally.</BodyText>
21
+ <Resizable onWidthResize={({ width: w }) => setWidth(Math.round(w))}>
22
+ <Box width={`${width}px`} height="200px" className={styles['demo-box']}>
23
+ <BodyText>Resizable content</BodyText>
24
+ <div className={styles['size-display']}>width: {width}px</div>
25
+ </Box>
26
+ <ResizeHandle position="right" className={styles['demo-handle']} />
27
+ </Resizable>
28
+ </VStack>
29
+ );
30
+ };
31
+
32
+ export const VerticalResize = () => {
33
+ const [height, setHeight] = useState(200);
34
+
35
+ return (
36
+ <VStack gap="8px" padding="16px" minHeight="600px">
37
+ <BodyText>Drag the bottom edge to resize vertically.</BodyText>
38
+ <Resizable onHeightResize={({ height: h }) => setHeight(Math.round(h))}>
39
+ <Box width="400px" height={`${height}px`} className={styles['demo-box']}>
40
+ <BodyText>Resizable content</BodyText>
41
+ <div className={styles['size-display']}>height: {height}px</div>
42
+ </Box>
43
+ <ResizeHandle position="bottom" className={styles['demo-handle']} />
44
+ </Resizable>
45
+ </VStack>
46
+ );
47
+ };
48
+
49
+ export const CornerResize = () => {
50
+ const [width, setWidth] = useState(300);
51
+ const [height, setHeight] = useState(200);
52
+
53
+ return (
54
+ <VStack gap="8px" padding="16px" minHeight="600px">
55
+ <BodyText>
56
+ Drag the bottom-right corner to resize both axes simultaneously.
57
+ </BodyText>
58
+ <Resizable
59
+ onWidthResize={({ width: w }) => setWidth(Math.round(w))}
60
+ onHeightResize={({ height: h }) => setHeight(Math.round(h))}
61
+ >
62
+ <Box width={`${width}px`} height={`${height}px`} className={styles['demo-box']}>
63
+ <BodyText>Resizable content</BodyText>
64
+ <div className={styles['size-display']}>
65
+ {width}px × {height}px
66
+ </div>
67
+ </Box>
68
+ <ResizeHandle position="bottom-right" className={styles['demo-handle']} />
69
+ </Resizable>
70
+ </VStack>
71
+ );
72
+ };
73
+
74
+ export const MultipleHandles = () => {
75
+ const [width, setWidth] = useState(400);
76
+ const [height, setHeight] = useState(300);
77
+
78
+ return (
79
+ <VStack gap="8px" padding="16px" minHeight="600px">
80
+ <BodyText>Left, right, bottom edges and bottom-right corner — all active.</BodyText>
81
+ <Resizable
82
+ onWidthResize={({ width: w }) => setWidth(Math.round(w))}
83
+ onHeightResize={({ height: h }) => setHeight(Math.round(h))}
84
+ >
85
+ <Box width={`${width}px`} height={`${height}px`} className={styles['demo-box']}>
86
+ <BodyText>Resizable content</BodyText>
87
+ <div className={styles['size-display']}>
88
+ {width}px × {height}px
89
+ </div>
90
+ </Box>
91
+ <ResizeHandle position="left" className={styles['demo-handle']} />
92
+ <ResizeHandle position="right" className={styles['demo-handle']} />
93
+ <ResizeHandle position="bottom" className={styles['demo-handle']} />
94
+ <ResizeHandle
95
+ position="bottom-right"
96
+ className={styles['demo-handle']}
97
+ style={{
98
+ zIndex: 2,
99
+ }}
100
+ />
101
+ </Resizable>
102
+ </VStack>
103
+ );
104
+ };
105
+
106
+ export const RTL = () => {
107
+ const [widthStart, setWidthStart] = useState(300);
108
+ const [widthLeft, setWidthLeft] = useState(300);
109
+
110
+ return (
111
+ <VStack gap="16px" padding="16px" minHeight="400px">
112
+ <BodyText>
113
+ RTL context: &quot;start&quot; flips to the right edge, &quot;left&quot; stays
114
+ physical left.
115
+ </BodyText>
116
+ <div className={styles['rtl-container']}>
117
+ <VStack gap="16px">
118
+ <BodyText>position=&quot;start&quot; (logical — flips in RTL)</BodyText>
119
+ <Resizable onWidthResize={({ width: w }) => setWidthStart(Math.round(w))}>
120
+ <Box width={`${widthStart}px`} height="100px" className={styles['demo-box']}>
121
+ <div className={styles['size-display']}>start handle: {widthStart}px</div>
122
+ </Box>
123
+ <ResizeHandle position="start" className={styles['demo-handle']} />
124
+ </Resizable>
125
+
126
+ <BodyText>position=&quot;left&quot; (physical — stays on left in RTL)</BodyText>
127
+ <Resizable onWidthResize={({ width: w }) => setWidthLeft(Math.round(w))}>
128
+ <Box width={`${widthLeft}px`} height="100px" className={styles['demo-box']}>
129
+ <div className={styles['size-display']}>left handle: {widthLeft}px</div>
130
+ </Box>
131
+ <ResizeHandle position="left" className={styles['demo-handle']} />
132
+ </Resizable>
133
+ </VStack>
134
+ </div>
135
+ </VStack>
136
+ );
137
+ };
138
+
139
+ export const NestedResizable = () => {
140
+ const [outerWidth, setOuterWidth] = useState(500);
141
+ const [innerWidth, setInnerWidth] = useState(250);
142
+
143
+ return (
144
+ <VStack gap="8px" padding="16px" minHeight="400px">
145
+ <BodyText>Nested: each handle binds to its nearest Resizable provider.</BodyText>
146
+ <Resizable onWidthResize={({ width: w }) => setOuterWidth(Math.round(w))}>
147
+ <Box width={`${outerWidth}px`} height="300px" className={styles['nested-outer']}>
148
+ <BodyText>Outer ({outerWidth}px)</BodyText>
149
+ <Resizable onWidthResize={({ width: w }) => setInnerWidth(Math.round(w))}>
150
+ <Box
151
+ width={`${innerWidth}px`}
152
+ height="150px"
153
+ className={styles['nested-inner']}
154
+ >
155
+ <BodyText>Inner ({innerWidth}px)</BodyText>
156
+ </Box>
157
+ <ResizeHandle position="right" className={styles['demo-handle']} />
158
+ </Resizable>
159
+ </Box>
160
+ <ResizeHandle position="right" className={styles['demo-handle']} />
161
+ </Resizable>
162
+ </VStack>
163
+ );
164
+ };
165
+
166
+ export const OverflowHidden = () => {
167
+ const [width, setWidth] = useState(300);
168
+
169
+ return (
170
+ <VStack gap="8px" padding="16px" minHeight="400px">
171
+ <BodyText>
172
+ Outer box is the resize target (minWidth: 100px). Inner box clips content with
173
+ overflow:hidden. Shrink below 200px to clip the purple box. It stops at 100px due
174
+ to the min-width constraint.
175
+ </BodyText>
176
+ <Resizable onWidthResize={({ width: w }) => setWidth(Math.round(w))}>
177
+ <Box width={`${width}px`} minWidth="100px" height="200px">
178
+ <Box height="100%" overflow="hidden" className={styles['demo-box']}>
179
+ <Box minWidth="200px" height="80px" className={styles['clip-target']} />
180
+ <div className={styles['size-display']}>width: {width}px</div>
181
+ </Box>
182
+ </Box>
183
+ <ResizeHandle position="right" className={styles['demo-handle']} />
184
+ </Resizable>
185
+ </VStack>
186
+ );
187
+ };
188
+
189
+ export const CustomHandle = () => {
190
+ const handleRef = useRef<HTMLDivElement>(null);
191
+ const [width, setWidth] = useState(300);
192
+
193
+ return (
194
+ <VStack gap="8px" padding="16px" minHeight="400px">
195
+ <BodyText>Custom styled handle with forwarded ref and aria attributes.</BodyText>
196
+ <Resizable onWidthResize={({ width: w }) => setWidth(Math.round(w))}>
197
+ <Box width={`${width}px`} height="200px" className={styles['demo-box']}>
198
+ <BodyText>Resizable content</BodyText>
199
+ <div className={styles['size-display']}>width: {width}px</div>
200
+ </Box>
201
+ <ResizeHandle
202
+ ref={handleRef}
203
+ position="right"
204
+ style={{
205
+ background: 'rgba(234, 67, 53, 0.4)',
206
+ width: '8px',
207
+ }}
208
+ aria-label="Resize handle"
209
+ data-testid="custom-handle"
210
+ />
211
+ </Resizable>
212
+ </VStack>
213
+ );
214
+ };
@@ -0,0 +1,47 @@
1
+ @layer tcn-system {
2
+ :where(.demo-box) {
3
+ background: var(--tcn-color-surface-secondary, #f0f0f0);
4
+ border: 1px solid var(--tcn-color-border, #ccc);
5
+ padding: 16px;
6
+ position: relative;
7
+ }
8
+
9
+ :where(.demo-handle) {
10
+ background: rgba(0, 120, 212, 0.3);
11
+ }
12
+
13
+ :where(.demo-handle:hover) {
14
+ background: rgba(0, 120, 212, 0.6);
15
+ }
16
+
17
+ :where(.size-display) {
18
+ font-family: monospace;
19
+ font-size: 12px;
20
+ color: var(--tcn-color-text-secondary, #666);
21
+ margin-top: 8px;
22
+ }
23
+
24
+ :where(.rtl-container) {
25
+ direction: rtl;
26
+ border: 2px dashed #999;
27
+ padding: 16px;
28
+ }
29
+
30
+ :where(.clip-target) {
31
+ background: #7b1fa2;
32
+ border-radius: 4px;
33
+ flex-shrink: 0;
34
+ }
35
+
36
+ :where(.nested-outer) {
37
+ background: #e8f0fe;
38
+ border: 2px solid #4285f4;
39
+ padding: 24px;
40
+ }
41
+
42
+ :where(.nested-inner) {
43
+ background: #fce8e6;
44
+ border: 2px solid #ea4335;
45
+ padding: 16px;
46
+ }
47
+ }