basefn 1.9.1 → 1.11.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 (98) hide show
  1. package/package.json +4 -4
  2. package/rescript.json +1 -1
  3. package/src/Basefn.res +5 -0
  4. package/src/Basefn.res.mjs +4 -0
  5. package/src/Basefn__Responsive.res.mjs +5 -5
  6. package/src/Basefn__Utils.res.mjs +2 -2
  7. package/src/Demo.res +59 -59
  8. package/src/Demo.res.mjs +630 -628
  9. package/src/Example.res +3 -3
  10. package/src/Example.res.mjs +48 -47
  11. package/src/components/Basefn__Accordion.res +5 -5
  12. package/src/components/Basefn__Accordion.res.mjs +25 -26
  13. package/src/components/Basefn__Alert.res +4 -4
  14. package/src/components/Basefn__Alert.res.mjs +20 -18
  15. package/src/components/Basefn__AlertDialog.res +5 -5
  16. package/src/components/Basefn__AlertDialog.res.mjs +23 -21
  17. package/src/components/Basefn__AppLayout.res +3 -3
  18. package/src/components/Basefn__AppLayout.res.mjs +25 -24
  19. package/src/components/Basefn__AspectRatio.res +1 -1
  20. package/src/components/Basefn__AspectRatio.res.mjs +2 -2
  21. package/src/components/Basefn__Avatar.res.mjs +3 -3
  22. package/src/components/Basefn__Badge.res +1 -1
  23. package/src/components/Basefn__Badge.res.mjs +2 -2
  24. package/src/components/Basefn__Breadcrumb.res +5 -5
  25. package/src/components/Basefn__Breadcrumb.res.mjs +14 -14
  26. package/src/components/Basefn__Button.res +1 -1
  27. package/src/components/Basefn__Button.res.mjs +10 -9
  28. package/src/components/Basefn__ButtonGroup.res +1 -1
  29. package/src/components/Basefn__ButtonGroup.res.mjs +2 -2
  30. package/src/components/Basefn__Card.res +3 -3
  31. package/src/components/Basefn__Card.res.mjs +11 -11
  32. package/src/components/Basefn__Checkbox.res +1 -1
  33. package/src/components/Basefn__Checkbox.res.mjs +7 -7
  34. package/src/components/Basefn__ContextMenu.res +4 -4
  35. package/src/components/Basefn__ContextMenu.res.mjs +22 -20
  36. package/src/components/Basefn__Drawer.res +5 -5
  37. package/src/components/Basefn__Drawer.res.mjs +22 -20
  38. package/src/components/Basefn__Dropdown.res +4 -4
  39. package/src/components/Basefn__Dropdown.res.mjs +20 -18
  40. package/src/components/Basefn__Grid.res.mjs +15 -14
  41. package/src/components/Basefn__HoverCard.res +3 -3
  42. package/src/components/Basefn__HoverCard.res.mjs +14 -12
  43. package/src/components/Basefn__Icon.res.mjs +8 -7
  44. package/src/components/Basefn__Input.res.mjs +4 -4
  45. package/src/components/Basefn__Kbd.res +2 -2
  46. package/src/components/Basefn__Kbd.res.mjs +5 -5
  47. package/src/components/Basefn__Label.res +1 -1
  48. package/src/components/Basefn__Label.res.mjs +4 -4
  49. package/src/components/Basefn__Modal.res +5 -5
  50. package/src/components/Basefn__Modal.res.mjs +20 -18
  51. package/src/components/Basefn__Popover.res +3 -3
  52. package/src/components/Basefn__Popover.res.mjs +15 -13
  53. package/src/components/Basefn__Progress.res +2 -2
  54. package/src/components/Basefn__Progress.res.mjs +16 -14
  55. package/src/components/Basefn__Radio.res +1 -1
  56. package/src/components/Basefn__Radio.res.mjs +7 -7
  57. package/src/components/Basefn__Resizable.css +156 -0
  58. package/src/components/Basefn__Resizable.res +318 -0
  59. package/src/components/Basefn__Resizable.res.mjs +343 -0
  60. package/src/components/Basefn__ScrollArea.res +1 -1
  61. package/src/components/Basefn__ScrollArea.res.mjs +2 -2
  62. package/src/components/Basefn__Select.res +2 -2
  63. package/src/components/Basefn__Select.res.mjs +7 -6
  64. package/src/components/Basefn__Separator.res +1 -1
  65. package/src/components/Basefn__Separator.res.mjs +9 -9
  66. package/src/components/Basefn__Sidebar.res +7 -7
  67. package/src/components/Basefn__Sidebar.res.mjs +28 -24
  68. package/src/components/Basefn__Skeleton.res.mjs +2 -2
  69. package/src/components/Basefn__Slider.res +4 -4
  70. package/src/components/Basefn__Slider.res.mjs +21 -20
  71. package/src/components/Basefn__Spinner.res +1 -1
  72. package/src/components/Basefn__Spinner.res.mjs +8 -8
  73. package/src/components/Basefn__Spotlight.res +22 -14
  74. package/src/components/Basefn__Spotlight.res.mjs +79 -65
  75. package/src/components/Basefn__Stepper.res +6 -6
  76. package/src/components/Basefn__Stepper.res.mjs +20 -24
  77. package/src/components/Basefn__Switch.res +1 -1
  78. package/src/components/Basefn__Switch.res.mjs +16 -14
  79. package/src/components/Basefn__Tabs.res +4 -4
  80. package/src/components/Basefn__Tabs.res.mjs +19 -17
  81. package/src/components/Basefn__Textarea.res.mjs +4 -4
  82. package/src/components/Basefn__ThemeToggle.res +1 -1
  83. package/src/components/Basefn__ThemeToggle.res.mjs +13 -10
  84. package/src/components/Basefn__Timeline.res +6 -6
  85. package/src/components/Basefn__Timeline.res.mjs +21 -21
  86. package/src/components/Basefn__Toast.res +4 -4
  87. package/src/components/Basefn__Toast.res.mjs +23 -20
  88. package/src/components/Basefn__Toggle.res +1 -1
  89. package/src/components/Basefn__Toggle.res.mjs +8 -7
  90. package/src/components/Basefn__ToggleGroup.res +2 -2
  91. package/src/components/Basefn__ToggleGroup.res.mjs +12 -10
  92. package/src/components/Basefn__Tooltip.res +3 -3
  93. package/src/components/Basefn__Tooltip.res.mjs +14 -12
  94. package/src/components/Basefn__Topbar.res +7 -7
  95. package/src/components/Basefn__Topbar.res.mjs +22 -22
  96. package/src/components/Basefn__Typography.res +2 -2
  97. package/src/components/Basefn__Typography.res.mjs +13 -13
  98. package/src/styles/Basefn__Theme.res.mjs +5 -5
@@ -1,7 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Xote from "xote/src/Xote.res.mjs";
4
- import * as Xote__JSX from "xote/src/Xote__JSX.res.mjs";
3
+ import * as Node$Xote from "xote/src/Node.res.mjs";
4
+ import * as Signal$Xote from "xote/src/Signal.res.mjs";
5
+ import * as XoteJSX$Xote from "xote/src/XoteJSX.res.mjs";
6
+ import * as Computed$Xote from "xote/src/Computed.res.mjs";
5
7
 
6
8
  import './Basefn__Popover.css'
7
9
  ;
@@ -42,14 +44,14 @@ function Basefn__Popover(props) {
42
44
  let align = __align !== undefined ? __align : "Center";
43
45
  let closeOnClickOutside = __closeOnClickOutside !== undefined ? __closeOnClickOutside : true;
44
46
  let handleTriggerClick = param => {
45
- let newValue = !Xote.Signal.get(isOpen);
46
- Xote.Signal.set(isOpen, newValue);
47
+ let newValue = !Signal$Xote.get(isOpen);
48
+ Signal$Xote.set(isOpen, newValue);
47
49
  if (onOpenChange !== undefined) {
48
50
  return onOpenChange(newValue);
49
51
  }
50
52
  };
51
53
  let handleClose = () => {
52
- Xote.Signal.set(isOpen, false);
54
+ Signal$Xote.set(isOpen, false);
53
55
  if (onOpenChange !== undefined) {
54
56
  return onOpenChange(false);
55
57
  }
@@ -68,14 +70,14 @@ function Basefn__Popover(props) {
68
70
  let alignClass = " basefn-popover__content--align-" + alignToString(align);
69
71
  return "basefn-popover__content" + positionClass + alignClass;
70
72
  };
71
- let popoverContent = Xote.Computed.make(() => {
72
- if (Xote.Signal.get(isOpen)) {
73
+ let popoverContent = Computed$Xote.make(() => {
74
+ if (Signal$Xote.get(isOpen)) {
73
75
  return [
74
- Xote__JSX.Elements.jsx("div", {
76
+ XoteJSX$Xote.Elements.jsx("div", {
75
77
  class: "basefn-popover__backdrop",
76
78
  onClick: handleBackdropClick
77
79
  }),
78
- Xote__JSX.Elements.jsx("div", {
80
+ XoteJSX$Xote.Elements.jsx("div", {
79
81
  class: getContentClassName(),
80
82
  children: content
81
83
  })
@@ -84,15 +86,15 @@ function Basefn__Popover(props) {
84
86
  return [];
85
87
  }
86
88
  }, undefined);
87
- return Xote__JSX.Elements.jsxs("div", {
89
+ return XoteJSX$Xote.Elements.jsxs("div", {
88
90
  class: getPopoverClassName(),
89
- children: Xote__JSX.array([
90
- Xote__JSX.Elements.jsx("div", {
91
+ children: XoteJSX$Xote.array([
92
+ XoteJSX$Xote.Elements.jsx("div", {
91
93
  class: "basefn-popover__trigger",
92
94
  onClick: handleTriggerClick,
93
95
  children: props.trigger
94
96
  }),
95
- Xote.Component.signalFragment(popoverContent)
97
+ Node$Xote.signalFragment(popoverContent)
96
98
  ])
97
99
  });
98
100
  }
@@ -74,14 +74,14 @@ let make = (
74
74
  {showLabel || label->Option.isSome
75
75
  ? <div class="basefn-progress__label">
76
76
  <span>
77
- {Component.text(
77
+ {Node.text(
78
78
  switch label {
79
79
  | Some(labelText) => labelText
80
80
  | None => "Progress"
81
81
  },
82
82
  )}
83
83
  </span>
84
- {!indeterminate ? {Component.textSignal(() => getPercentage() ++ "%")} : <> </>}
84
+ {!indeterminate ? {Node.signalText(() => getPercentage() ++ "%")} : <> </>}
85
85
  </div>
86
86
  : <> </>}
87
87
  </>
@@ -1,8 +1,10 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Xote from "xote/src/Xote.res.mjs";
4
- import * as Xote__JSX from "xote/src/Xote__JSX.res.mjs";
3
+ import * as Node$Xote from "xote/src/Node.res.mjs";
4
+ import * as Signal$Xote from "xote/src/Signal.res.mjs";
5
5
  import * as Core__Option from "@rescript/core/src/Core__Option.res.mjs";
6
+ import * as XoteJSX$Xote from "xote/src/XoteJSX.res.mjs";
7
+ import * as Computed$Xote from "xote/src/Computed.res.mjs";
6
8
 
7
9
  import './Basefn__Progress.css'
8
10
  ;
@@ -56,20 +58,20 @@ function Basefn__Progress(props) {
56
58
  return "basefn-progress__bar " + variantClass + animatedClass;
57
59
  };
58
60
  let getPercentage = () => {
59
- let currentValue = Xote.Signal.get(value);
61
+ let currentValue = Signal$Xote.get(value);
60
62
  let percent = currentValue / max * 100.0;
61
63
  let clamped = percent > 100.0 ? 100.0 : (
62
64
  percent < 0.0 ? 0.0 : percent
63
65
  );
64
66
  return clamped.toString();
65
67
  };
66
- return Xote__JSX.jsxs(Xote__JSX.jsxFragment, {
67
- children: Xote__JSX.array([
68
- Xote__JSX.Elements.jsx("div", {
68
+ return XoteJSX$Xote.jsxs(XoteJSX$Xote.jsxFragment, {
69
+ children: XoteJSX$Xote.array([
70
+ XoteJSX$Xote.Elements.jsx("div", {
69
71
  class: getProgressClass(),
70
- children: Xote__JSX.Elements.jsx("div", {
72
+ children: XoteJSX$Xote.Elements.jsx("div", {
71
73
  class: getBarClass(),
72
- style: Xote.Computed.make(() => {
74
+ style: Computed$Xote.make(() => {
73
75
  if (indeterminate) {
74
76
  return "width: 30%";
75
77
  } else {
@@ -78,15 +80,15 @@ function Basefn__Progress(props) {
78
80
  }, undefined)
79
81
  })
80
82
  }),
81
- showLabel || Core__Option.isSome(label) ? Xote__JSX.Elements.jsxs("div", {
83
+ showLabel || Core__Option.isSome(label) ? XoteJSX$Xote.Elements.jsxs("div", {
82
84
  class: "basefn-progress__label",
83
- children: Xote__JSX.array([
84
- Xote__JSX.Elements.jsx("span", {
85
- children: Xote.Component.text(label !== undefined ? label : "Progress")
85
+ children: XoteJSX$Xote.array([
86
+ XoteJSX$Xote.Elements.jsx("span", {
87
+ children: Node$Xote.text(label !== undefined ? label : "Progress")
86
88
  }),
87
- indeterminate ? Xote__JSX.jsx(Xote__JSX.jsxFragment, {}) : Xote.Component.textSignal(() => getPercentage() + "%")
89
+ indeterminate ? XoteJSX$Xote.jsx(XoteJSX$Xote.jsxFragment, {}) : Node$Xote.signalText(() => getPercentage() + "%")
88
90
  ])
89
- }) : Xote__JSX.jsx(Xote__JSX.jsxFragment, {})
91
+ }) : XoteJSX$Xote.jsx(XoteJSX$Xote.jsxFragment, {})
90
92
  ])
91
93
  });
92
94
  }
@@ -30,6 +30,6 @@ let make = (
30
30
  disabled={disabled}
31
31
  ?onChange
32
32
  />
33
- <span class="basefn-radio-label"> {Component.text(label)} </span>
33
+ <span class="basefn-radio-label"> {Node.text(label)} </span>
34
34
  </label>
35
35
  }
@@ -1,7 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Xote from "xote/src/Xote.res.mjs";
4
- import * as Xote__JSX from "xote/src/Xote__JSX.res.mjs";
3
+ import * as Node$Xote from "xote/src/Node.res.mjs";
4
+ import * as XoteJSX$Xote from "xote/src/XoteJSX.res.mjs";
5
5
 
6
6
  import './Basefn__Radio.css'
7
7
  ;
@@ -10,10 +10,10 @@ function Basefn__Radio(props) {
10
10
  let __disabled = props.disabled;
11
11
  let disabled = __disabled !== undefined ? __disabled : false;
12
12
  let base = "basefn-radio-wrapper";
13
- return Xote__JSX.Elements.jsxs("label", {
13
+ return XoteJSX$Xote.Elements.jsxs("label", {
14
14
  class: disabled ? base + " basefn-radio-wrapper--disabled" : base,
15
- children: Xote__JSX.array([
16
- Xote__JSX.Elements.jsx("input", {
15
+ children: XoteJSX$Xote.array([
16
+ XoteJSX$Xote.Elements.jsx("input", {
17
17
  class: "basefn-radio-input",
18
18
  type: "radio",
19
19
  name: props.name,
@@ -22,9 +22,9 @@ function Basefn__Radio(props) {
22
22
  checked: props.checked,
23
23
  onChange: props.onChange
24
24
  }),
25
- Xote__JSX.Elements.jsx("span", {
25
+ XoteJSX$Xote.Elements.jsx("span", {
26
26
  class: "basefn-radio-label",
27
- children: Xote.Component.text(props.label)
27
+ children: Node$Xote.text(props.label)
28
28
  })
29
29
  ])
30
30
  });
@@ -0,0 +1,156 @@
1
+ @import "../styles/variables.css";
2
+
3
+ /* Panel Group Container */
4
+ .basefn-resizable {
5
+ display: flex;
6
+ width: 100%;
7
+ height: 100%;
8
+ overflow: hidden;
9
+ }
10
+
11
+ .basefn-resizable--horizontal {
12
+ flex-direction: row;
13
+ }
14
+
15
+ .basefn-resizable--vertical {
16
+ flex-direction: column;
17
+ }
18
+
19
+ /* Panel */
20
+ .basefn-resizable__panel {
21
+ overflow: auto;
22
+ }
23
+
24
+ .basefn-resizable__panel--dragging {
25
+ pointer-events: none;
26
+ user-select: none;
27
+ }
28
+
29
+ /* Handle */
30
+ .basefn-resizable__handle {
31
+ flex-shrink: 0;
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ position: relative;
36
+ outline: none;
37
+ background: transparent;
38
+ border: 0;
39
+ padding: 0;
40
+ }
41
+
42
+ .basefn-resizable__handle--horizontal {
43
+ width: 8px;
44
+ cursor: col-resize;
45
+ }
46
+
47
+ .basefn-resizable__handle--vertical {
48
+ height: 8px;
49
+ cursor: row-resize;
50
+ }
51
+
52
+ /* Handle visible line */
53
+ .basefn-resizable__handle::after {
54
+ content: "";
55
+ position: absolute;
56
+ background-color: var(--basefn-border-primary);
57
+ transition: background-color var(--basefn-transition-fast);
58
+ }
59
+
60
+ .basefn-resizable__handle--horizontal::after {
61
+ width: 1px;
62
+ height: 100%;
63
+ left: 50%;
64
+ transform: translateX(-50%);
65
+ }
66
+
67
+ .basefn-resizable__handle--vertical::after {
68
+ height: 1px;
69
+ width: 100%;
70
+ top: 50%;
71
+ transform: translateY(-50%);
72
+ }
73
+
74
+ .basefn-resizable__handle:hover::after,
75
+ .basefn-resizable__handle:focus-visible::after {
76
+ background-color: var(--basefn-color-primary);
77
+ }
78
+
79
+ /* Grip indicator */
80
+ .basefn-resizable__handle-grip {
81
+ position: relative;
82
+ z-index: 1;
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ border-radius: var(--basefn-radius-sm);
87
+ background-color: var(--basefn-border-primary);
88
+ border: 1px solid var(--basefn-border-secondary);
89
+ transition:
90
+ background-color var(--basefn-transition-fast),
91
+ border-color var(--basefn-transition-fast);
92
+ }
93
+
94
+ .basefn-resizable__handle--horizontal .basefn-resizable__handle-grip {
95
+ width: 12px;
96
+ height: 24px;
97
+ }
98
+
99
+ .basefn-resizable__handle--vertical .basefn-resizable__handle-grip {
100
+ width: 24px;
101
+ height: 12px;
102
+ }
103
+
104
+ /* Grip dots pattern */
105
+ .basefn-resizable__handle-grip::after {
106
+ content: "";
107
+ display: block;
108
+ border-radius: var(--basefn-radius-full);
109
+ }
110
+
111
+ .basefn-resizable__handle--horizontal .basefn-resizable__handle-grip::after {
112
+ width: 4px;
113
+ height: 16px;
114
+ background-image: radial-gradient(
115
+ circle,
116
+ var(--basefn-text-muted) 1px,
117
+ transparent 1px
118
+ );
119
+ background-size: 4px 4px;
120
+ background-position: center;
121
+ background-repeat: repeat;
122
+ }
123
+
124
+ .basefn-resizable__handle--vertical .basefn-resizable__handle-grip::after {
125
+ width: 16px;
126
+ height: 4px;
127
+ background-image: radial-gradient(
128
+ circle,
129
+ var(--basefn-text-muted) 1px,
130
+ transparent 1px
131
+ );
132
+ background-size: 4px 4px;
133
+ background-position: center;
134
+ background-repeat: repeat;
135
+ }
136
+
137
+ /* Hover and focus states */
138
+ .basefn-resizable__handle:hover .basefn-resizable__handle-grip,
139
+ .basefn-resizable__handle:focus-visible .basefn-resizable__handle-grip {
140
+ background-color: var(--basefn-bg-tertiary);
141
+ border-color: var(--basefn-color-primary);
142
+ }
143
+
144
+ .basefn-resizable__handle:focus-visible .basefn-resizable__handle-grip {
145
+ box-shadow: 0 0 0 var(--basefn-focus-ring-width) var(--basefn-focus-ring-color);
146
+ }
147
+
148
+ /* Hide line when grip is present */
149
+ .basefn-resizable__handle--with-grip::after {
150
+ background-color: transparent;
151
+ }
152
+
153
+ .basefn-resizable__handle--with-grip:hover::after,
154
+ .basefn-resizable__handle--with-grip:focus-visible::after {
155
+ background-color: transparent;
156
+ }
@@ -0,0 +1,318 @@
1
+ %%raw(`import './Basefn__Resizable.css'`)
2
+
3
+ open Xote
4
+
5
+ type direction = Horizontal | Vertical
6
+
7
+ type panel = {
8
+ content: Node.node,
9
+ defaultSize: float,
10
+ minSize?: float,
11
+ maxSize?: float,
12
+ }
13
+
14
+ let directionToString = (d: direction) =>
15
+ switch d {
16
+ | Horizontal => "horizontal"
17
+ | Vertical => "vertical"
18
+ }
19
+
20
+ // DOM helpers
21
+ let getClientX: Dom.event => float = %raw(`function(e) {
22
+ var t = e.touches ? e.touches[0] : e;
23
+ return t.clientX;
24
+ }`)
25
+
26
+ let getClientY: Dom.event => float = %raw(`function(e) {
27
+ var t = e.touches ? e.touches[0] : e;
28
+ return t.clientY;
29
+ }`)
30
+
31
+ let addDocListener: (string, Dom.event => unit) => unit = %raw(`function(ev, fn) {
32
+ document.addEventListener(ev, fn);
33
+ }`)
34
+
35
+ let removeDocListener: (string, Dom.event => unit) => unit = %raw(`function(ev, fn) {
36
+ document.removeEventListener(ev, fn);
37
+ }`)
38
+
39
+ let disableSelect: unit => unit = %raw(`function() {
40
+ document.body.style.userSelect = "none";
41
+ document.body.style.webkitUserSelect = "none";
42
+ }`)
43
+
44
+ let enableSelect: unit => unit = %raw(`function() {
45
+ document.body.style.userSelect = "";
46
+ document.body.style.webkitUserSelect = "";
47
+ }`)
48
+
49
+ let findClosest: (Dom.element, string) => Nullable.t<Dom.element> = %raw(`function(el, sel) {
50
+ return el.closest(sel);
51
+ }`)
52
+
53
+ let getElementRect: Dom.element => {..} = %raw(`function(el) {
54
+ return el.getBoundingClientRect();
55
+ }`)
56
+
57
+ let getKey: Dom.event => string = %raw(`function(e) { return e.key || "" }`)
58
+
59
+ let getEventTarget: Dom.event => Dom.element = %raw(`function(e) {
60
+ return e.target;
61
+ }`)
62
+
63
+ let preventDefault: Dom.event => unit = %raw(`function(e) { e.preventDefault() }`)
64
+
65
+ let genId: unit => string = %raw(`function() {
66
+ return "basefn-resizable-" + Math.random().toString(36).substr(2, 9);
67
+ }`)
68
+
69
+ // Global event delegation: register callbacks by container ID, one document
70
+ // listener handles all instances. No timing dependency on DOM rendering.
71
+ let registerInstance: (string, (int, Dom.event) => unit) => unit = %raw(`function(id, cb) {
72
+ if (!window.__basefn_resizable) {
73
+ window.__basefn_resizable = {};
74
+
75
+ function handler(e) {
76
+ var target = e.target;
77
+ if (!target || !target.closest) return;
78
+ var handle = target.closest(".basefn-resizable__handle");
79
+ if (!handle) return;
80
+ var container = handle.closest(".basefn-resizable");
81
+ if (!container || !container.id) return;
82
+ var entry = window.__basefn_resizable[container.id];
83
+ if (!entry) return;
84
+ // Find the handle index among direct children of the container
85
+ var children = container.children;
86
+ var handleIndex = 0;
87
+ for (var i = 0; i < children.length; i++) {
88
+ if (children[i].classList.contains("basefn-resizable__handle")) {
89
+ if (children[i] === handle) {
90
+ entry(handleIndex, e);
91
+ return;
92
+ }
93
+ handleIndex++;
94
+ }
95
+ }
96
+ }
97
+
98
+ document.addEventListener("mousedown", handler);
99
+ document.addEventListener("touchstart", handler, { passive: false });
100
+ }
101
+
102
+ window.__basefn_resizable[id] = cb;
103
+ }`)
104
+
105
+ // Lazily attach mousedown/touchstart directly to a handle element.
106
+ // Called from onMouseEnter so the element is guaranteed to exist.
107
+ let initHandle: (Dom.element, int, (int, Dom.event) => unit) => unit = %raw(`function(el, idx, cb) {
108
+ if (el._basefnInit) return;
109
+ el._basefnInit = true;
110
+ el.addEventListener("mousedown", function(e) { cb(idx, e); });
111
+ el.addEventListener("touchstart", function(e) { cb(idx, e); }, { passive: false });
112
+ }`)
113
+
114
+ let getCurrentTarget: Dom.event => Dom.element = %raw(`function(e) {
115
+ return e.currentTarget;
116
+ }`)
117
+
118
+ type dragInfo = {
119
+ handleIndex: int,
120
+ startPos: float,
121
+ startSizes: array<float>,
122
+ container: Dom.element,
123
+ }
124
+
125
+ @jsx.component
126
+ let make = (
127
+ ~panels: array<panel>,
128
+ ~direction: direction=Horizontal,
129
+ ~withHandle: bool=true,
130
+ ~onResize: option<array<float> => unit>=?,
131
+ ~class: string="",
132
+ ) => {
133
+ let containerId = genId()
134
+ let sizes = Signal.make(panels->Array.map(p => p.defaultSize))
135
+ let isDragging = Signal.make(false)
136
+ let dragRef: ref<option<dragInfo>> = ref(None)
137
+
138
+ let getPos = (evt: Dom.event) =>
139
+ switch direction {
140
+ | Horizontal => getClientX(evt)
141
+ | Vertical => getClientY(evt)
142
+ }
143
+
144
+ let getContainerDimension = (container: Dom.element) => {
145
+ let rect = getElementRect(container)
146
+ switch direction {
147
+ | Horizontal => Obj.magic(rect)["width"]
148
+ | Vertical => Obj.magic(rect)["height"]
149
+ }
150
+ }
151
+
152
+ let clampSizes = (leftIdx: int, rightIdx: int, newLeft: float, newRight: float) => {
153
+ let lp = panels->Array.getUnsafe(leftIdx)
154
+ let rp = panels->Array.getUnsafe(rightIdx)
155
+ let lMin = lp.minSize->Option.getOr(0.0)
156
+ let lMax = lp.maxSize->Option.getOr(100.0)
157
+ let rMin = rp.minSize->Option.getOr(0.0)
158
+ let rMax = rp.maxSize->Option.getOr(100.0)
159
+ let total = newLeft +. newRight
160
+
161
+ if newLeft < lMin {
162
+ (lMin, total -. lMin)
163
+ } else if newLeft > lMax {
164
+ (lMax, total -. lMax)
165
+ } else if newRight < rMin {
166
+ (total -. rMin, rMin)
167
+ } else if newRight > rMax {
168
+ (total -. rMax, rMax)
169
+ } else {
170
+ (newLeft, newRight)
171
+ }
172
+ }
173
+
174
+ let applySizes = (newSizes: array<float>) => {
175
+ Signal.set(sizes, newSizes)
176
+ switch onResize {
177
+ | Some(cb) => cb(newSizes)
178
+ | None => ()
179
+ }
180
+ }
181
+
182
+ let onMouseMove = (evt: Dom.event) => {
183
+ switch dragRef.contents {
184
+ | None => ()
185
+ | Some(info) =>
186
+ let containerSize = getContainerDimension(info.container)
187
+ if containerSize > 0.0 {
188
+ let delta = getPos(evt) -. info.startPos
189
+ let deltaPercent = delta /. containerSize *. 100.0
190
+ let li = info.handleIndex
191
+ let ri = info.handleIndex + 1
192
+ let origLeft = info.startSizes->Array.getUnsafe(li)
193
+ let origRight = info.startSizes->Array.getUnsafe(ri)
194
+ let (nl, nr) = clampSizes(li, ri, origLeft +. deltaPercent, origRight -. deltaPercent)
195
+ let newSizes = Signal.get(sizes)->Array.copy
196
+ newSizes->Array.setUnsafe(li, nl)
197
+ newSizes->Array.setUnsafe(ri, nr)
198
+ applySizes(newSizes)
199
+ }
200
+ }
201
+ }
202
+
203
+ let rec onMouseUp = (_: Dom.event) => {
204
+ dragRef := None
205
+ Signal.set(isDragging, false)
206
+ enableSelect()
207
+ removeDocListener("mousemove", onMouseMove)
208
+ removeDocListener("mouseup", onMouseUp)
209
+ removeDocListener("touchmove", onMouseMove)
210
+ removeDocListener("touchend", onMouseUp)
211
+ }
212
+
213
+ let startDrag = (handleIndex: int, evt: Dom.event) => {
214
+ preventDefault(evt)
215
+ let target = getEventTarget(evt)
216
+ let maybeContainer = findClosest(target, ".basefn-resizable")
217
+ switch Nullable.toOption(maybeContainer) {
218
+ | None => ()
219
+ | Some(container) =>
220
+ dragRef := Some({
221
+ handleIndex,
222
+ startPos: getPos(evt),
223
+ startSizes: Signal.get(sizes)->Array.copy,
224
+ container,
225
+ })
226
+ Signal.set(isDragging, true)
227
+ disableSelect()
228
+ addDocListener("mousemove", onMouseMove)
229
+ addDocListener("mouseup", onMouseUp)
230
+ addDocListener("touchmove", onMouseMove)
231
+ addDocListener("touchend", onMouseUp)
232
+ }
233
+ }
234
+
235
+ let handleKeyDown = (handleIndex: int, evt: Dom.event) => {
236
+ let key = getKey(evt)
237
+ let step = 1.0
238
+ let li = handleIndex
239
+ let ri = handleIndex + 1
240
+ let currentSizes = Signal.get(sizes)
241
+ let left = currentSizes->Array.getUnsafe(li)
242
+ let right = currentSizes->Array.getUnsafe(ri)
243
+
244
+ let delta = switch (direction, key) {
245
+ | (Horizontal, "ArrowLeft") | (Vertical, "ArrowUp") => Some(-.step)
246
+ | (Horizontal, "ArrowRight") | (Vertical, "ArrowDown") => Some(step)
247
+ | _ => None
248
+ }
249
+
250
+ switch delta {
251
+ | Some(d) =>
252
+ preventDefault(evt)
253
+ let (nl, nr) = clampSizes(li, ri, left +. d, right -. d)
254
+ let newSizes = currentSizes->Array.copy
255
+ newSizes->Array.setUnsafe(li, nl)
256
+ newSizes->Array.setUnsafe(ri, nr)
257
+ applySizes(newSizes)
258
+ | None => ()
259
+ }
260
+ }
261
+
262
+ // Register this instance for global event delegation
263
+ registerInstance(containerId, startDrag)
264
+
265
+ let containerClass =
266
+ "basefn-resizable basefn-resizable--" ++
267
+ directionToString(direction) ++
268
+ (class !== "" ? " " ++ class : "")
269
+
270
+ let elements: array<Node.node> = []
271
+
272
+ panels->Array.forEachWithIndex((panel, index) => {
273
+ let panelStyle = Computed.make(() => {
274
+ let size = Signal.get(sizes)->Array.getUnsafe(index)
275
+ "flex-basis:" ++ Float.toString(size) ++ "%;flex-grow:0;flex-shrink:0"
276
+ })
277
+
278
+ let panelClass = Computed.make(() => {
279
+ let base = "basefn-resizable__panel"
280
+ if Signal.get(isDragging) {
281
+ base ++ " basefn-resizable__panel--dragging"
282
+ } else {
283
+ base
284
+ }
285
+ })
286
+
287
+ elements->Array.push(
288
+ <div key={"panel-" ++ Int.toString(index)} class={panelClass} style={panelStyle}>
289
+ {panel.content}
290
+ </div>,
291
+ )
292
+
293
+ if index < Array.length(panels) - 1 {
294
+ let handleClass =
295
+ "basefn-resizable__handle basefn-resizable__handle--" ++
296
+ directionToString(direction) ++
297
+ (withHandle ? " basefn-resizable__handle--with-grip" : "")
298
+
299
+ elements->Array.push(
300
+ <div
301
+ key={"handle-" ++ Int.toString(index)}
302
+ class={handleClass}
303
+ onMouseEnter={evt => {
304
+ let el = getCurrentTarget(evt)
305
+ initHandle(el, index, startDrag)
306
+ }}
307
+ onKeyDown={evt => handleKeyDown(index, evt)}
308
+ role="separator"
309
+ tabIndex={0}
310
+ >
311
+ {withHandle ? <div class="basefn-resizable__handle-grip" /> : <> </>}
312
+ </div>,
313
+ )
314
+ }
315
+ })
316
+
317
+ <div id={containerId} class={containerClass}> {elements->Node.fragment} </div>
318
+ }