basefn 1.9.1 → 1.10.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basefn",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/brnrdog/basefn.git"
@@ -80,7 +80,7 @@
80
80
  "@semantic-release/github": "^12.0.2",
81
81
  "@semantic-release/npm": "^13.1.3",
82
82
  "rescript": "^12.0.1",
83
- "vite": "^5.0.0",
83
+ "vite": "^8.0.0",
84
84
  "xote": "^4.12.0"
85
85
  }
86
86
  }
package/src/Basefn.res CHANGED
@@ -72,6 +72,8 @@ type gridJustifyContent = Basefn__Grid.justifyContent
72
72
  type gridAlignContent = Basefn__Grid.alignContent
73
73
  type gridItemColumnSpan = Basefn__Grid.Item.columnSpan
74
74
  type gridItemRowSpan = Basefn__Grid.Item.rowSpan
75
+ type resizableDirection = Basefn__Resizable.direction
76
+ type resizablePanel = Basefn__Resizable.panel
75
77
  type breakpoint = Basefn__Responsive.breakpoint
76
78
  type currentBreakpoint = Basefn__Responsive.currentBreakpoint
77
79
  type responsiveValue<'a> = Basefn__Responsive.responsiveValue<'a>
@@ -231,6 +233,9 @@ module ContextMenu = {
231
233
  module Spotlight = {
232
234
  include Basefn__Spotlight
233
235
  }
236
+ module Resizable = {
237
+ include Basefn__Resizable
238
+ }
234
239
 
235
240
  // Responsive Utilities
236
241
  module Responsive = {
@@ -35,6 +35,7 @@ import * as Basefn__Timeline from "./components/Basefn__Timeline.res.mjs";
35
35
  import * as Basefn__Accordion from "./components/Basefn__Accordion.res.mjs";
36
36
  import * as Basefn__AppLayout from "./components/Basefn__AppLayout.res.mjs";
37
37
  import * as Basefn__HoverCard from "./components/Basefn__HoverCard.res.mjs";
38
+ import * as Basefn__Resizable from "./components/Basefn__Resizable.res.mjs";
38
39
  import * as Basefn__Separator from "./components/Basefn__Separator.res.mjs";
39
40
  import * as Basefn__Spotlight from "./components/Basefn__Spotlight.res.mjs";
40
41
  import * as Basefn__Breadcrumb from "./components/Basefn__Breadcrumb.res.mjs";
@@ -143,6 +144,8 @@ let ContextMenu = Basefn__ContextMenu;
143
144
 
144
145
  let Spotlight = Basefn__Spotlight;
145
146
 
147
+ let Resizable = Basefn__Resizable;
148
+
146
149
  let Responsive = Basefn__Responsive;
147
150
 
148
151
  export {
@@ -192,6 +195,7 @@ export {
192
195
  AlertDialog,
193
196
  ContextMenu,
194
197
  Spotlight,
198
+ Resizable,
195
199
  Responsive,
196
200
  }
197
201
  /* Not a pure module */
@@ -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,289 @@
1
+ %%raw(`import './Basefn__Resizable.css'`)
2
+
3
+ open Xote
4
+
5
+ type direction = Horizontal | Vertical
6
+
7
+ type panel = {
8
+ content: Component.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
+ // Set up mousedown/touchstart delegation on a container element by ID
70
+ let setupDragListeners: (string, (int, Dom.event) => unit) => unit = %raw(`function(containerId, onStart) {
71
+ requestAnimationFrame(function() {
72
+ var container = document.getElementById(containerId);
73
+ if (!container) return;
74
+
75
+ function handler(e) {
76
+ var target = e.target;
77
+ var handle = target.closest ? target.closest(".basefn-resizable__handle") : null;
78
+ if (!handle || !container.contains(handle)) return;
79
+ var handles = container.querySelectorAll(".basefn-resizable__handle");
80
+ for (var i = 0; i < handles.length; i++) {
81
+ if (handles[i] === handle) {
82
+ onStart(i, e);
83
+ return;
84
+ }
85
+ }
86
+ }
87
+
88
+ container.addEventListener("mousedown", handler);
89
+ container.addEventListener("touchstart", handler, { passive: false });
90
+ });
91
+ }`)
92
+
93
+ type dragInfo = {
94
+ handleIndex: int,
95
+ startPos: float,
96
+ startSizes: array<float>,
97
+ container: Dom.element,
98
+ }
99
+
100
+ @jsx.component
101
+ let make = (
102
+ ~panels: array<panel>,
103
+ ~direction: direction=Horizontal,
104
+ ~withHandle: bool=true,
105
+ ~onResize: option<array<float> => unit>=?,
106
+ ~class: string="",
107
+ ) => {
108
+ let containerId = genId()
109
+ let sizes = Signal.make(panels->Array.map(p => p.defaultSize))
110
+ let isDragging = Signal.make(false)
111
+ let dragRef: ref<option<dragInfo>> = ref(None)
112
+
113
+ let getPos = (evt: Dom.event) =>
114
+ switch direction {
115
+ | Horizontal => getClientX(evt)
116
+ | Vertical => getClientY(evt)
117
+ }
118
+
119
+ let getContainerDimension = (container: Dom.element) => {
120
+ let rect = getElementRect(container)
121
+ switch direction {
122
+ | Horizontal => Obj.magic(rect)["width"]
123
+ | Vertical => Obj.magic(rect)["height"]
124
+ }
125
+ }
126
+
127
+ let clampSizes = (leftIdx: int, rightIdx: int, newLeft: float, newRight: float) => {
128
+ let lp = panels->Array.getUnsafe(leftIdx)
129
+ let rp = panels->Array.getUnsafe(rightIdx)
130
+ let lMin = lp.minSize->Option.getOr(0.0)
131
+ let lMax = lp.maxSize->Option.getOr(100.0)
132
+ let rMin = rp.minSize->Option.getOr(0.0)
133
+ let rMax = rp.maxSize->Option.getOr(100.0)
134
+ let total = newLeft +. newRight
135
+
136
+ if newLeft < lMin {
137
+ (lMin, total -. lMin)
138
+ } else if newLeft > lMax {
139
+ (lMax, total -. lMax)
140
+ } else if newRight < rMin {
141
+ (total -. rMin, rMin)
142
+ } else if newRight > rMax {
143
+ (total -. rMax, rMax)
144
+ } else {
145
+ (newLeft, newRight)
146
+ }
147
+ }
148
+
149
+ let applySizes = (newSizes: array<float>) => {
150
+ Signal.set(sizes, newSizes)
151
+ switch onResize {
152
+ | Some(cb) => cb(newSizes)
153
+ | None => ()
154
+ }
155
+ }
156
+
157
+ let onMouseMove = (evt: Dom.event) => {
158
+ switch dragRef.contents {
159
+ | None => ()
160
+ | Some(info) =>
161
+ let containerSize = getContainerDimension(info.container)
162
+ if containerSize > 0.0 {
163
+ let delta = getPos(evt) -. info.startPos
164
+ let deltaPercent = delta /. containerSize *. 100.0
165
+ let li = info.handleIndex
166
+ let ri = info.handleIndex + 1
167
+ let origLeft = info.startSizes->Array.getUnsafe(li)
168
+ let origRight = info.startSizes->Array.getUnsafe(ri)
169
+ let (nl, nr) = clampSizes(li, ri, origLeft +. deltaPercent, origRight -. deltaPercent)
170
+ let newSizes = Signal.get(sizes)->Array.copy
171
+ newSizes->Array.setUnsafe(li, nl)
172
+ newSizes->Array.setUnsafe(ri, nr)
173
+ applySizes(newSizes)
174
+ }
175
+ }
176
+ }
177
+
178
+ let rec onMouseUp = (_: Dom.event) => {
179
+ dragRef := None
180
+ Signal.set(isDragging, false)
181
+ enableSelect()
182
+ removeDocListener("mousemove", onMouseMove)
183
+ removeDocListener("mouseup", onMouseUp)
184
+ removeDocListener("touchmove", onMouseMove)
185
+ removeDocListener("touchend", onMouseUp)
186
+ }
187
+
188
+ let startDrag = (handleIndex: int, evt: Dom.event) => {
189
+ preventDefault(evt)
190
+ let target = getEventTarget(evt)
191
+ let maybeContainer = findClosest(target, ".basefn-resizable")
192
+ switch Nullable.toOption(maybeContainer) {
193
+ | None => ()
194
+ | Some(container) =>
195
+ dragRef := Some({
196
+ handleIndex,
197
+ startPos: getPos(evt),
198
+ startSizes: Signal.get(sizes)->Array.copy,
199
+ container,
200
+ })
201
+ Signal.set(isDragging, true)
202
+ disableSelect()
203
+ addDocListener("mousemove", onMouseMove)
204
+ addDocListener("mouseup", onMouseUp)
205
+ addDocListener("touchmove", onMouseMove)
206
+ addDocListener("touchend", onMouseUp)
207
+ }
208
+ }
209
+
210
+ let handleKeyDown = (handleIndex: int, evt: Dom.event) => {
211
+ let key = getKey(evt)
212
+ let step = 1.0
213
+ let li = handleIndex
214
+ let ri = handleIndex + 1
215
+ let currentSizes = Signal.get(sizes)
216
+ let left = currentSizes->Array.getUnsafe(li)
217
+ let right = currentSizes->Array.getUnsafe(ri)
218
+
219
+ let delta = switch (direction, key) {
220
+ | (Horizontal, "ArrowLeft") | (Vertical, "ArrowUp") => Some(-.step)
221
+ | (Horizontal, "ArrowRight") | (Vertical, "ArrowDown") => Some(step)
222
+ | _ => None
223
+ }
224
+
225
+ switch delta {
226
+ | Some(d) =>
227
+ preventDefault(evt)
228
+ let (nl, nr) = clampSizes(li, ri, left +. d, right -. d)
229
+ let newSizes = currentSizes->Array.copy
230
+ newSizes->Array.setUnsafe(li, nl)
231
+ newSizes->Array.setUnsafe(ri, nr)
232
+ applySizes(newSizes)
233
+ | None => ()
234
+ }
235
+ }
236
+
237
+ // Set up mousedown/touchstart via event delegation
238
+ setupDragListeners(containerId, startDrag)
239
+
240
+ let containerClass =
241
+ "basefn-resizable basefn-resizable--" ++
242
+ directionToString(direction) ++
243
+ (class !== "" ? " " ++ class : "")
244
+
245
+ let elements: array<Component.node> = []
246
+
247
+ panels->Array.forEachWithIndex((panel, index) => {
248
+ let panelStyle = Computed.make(() => {
249
+ let size = Signal.get(sizes)->Array.getUnsafe(index)
250
+ "flex-basis:" ++ Float.toString(size) ++ "%;flex-grow:0;flex-shrink:0"
251
+ })
252
+
253
+ let panelClass = Computed.make(() => {
254
+ let base = "basefn-resizable__panel"
255
+ if Signal.get(isDragging) {
256
+ base ++ " basefn-resizable__panel--dragging"
257
+ } else {
258
+ base
259
+ }
260
+ })
261
+
262
+ elements->Array.push(
263
+ <div key={"panel-" ++ Int.toString(index)} class={panelClass} style={panelStyle}>
264
+ {panel.content}
265
+ </div>,
266
+ )
267
+
268
+ if index < Array.length(panels) - 1 {
269
+ let handleClass =
270
+ "basefn-resizable__handle basefn-resizable__handle--" ++
271
+ directionToString(direction) ++
272
+ (withHandle ? " basefn-resizable__handle--with-grip" : "")
273
+
274
+ elements->Array.push(
275
+ <div
276
+ key={"handle-" ++ Int.toString(index)}
277
+ class={handleClass}
278
+ onKeyDown={evt => handleKeyDown(index, evt)}
279
+ role="separator"
280
+ tabIndex={0}
281
+ >
282
+ {withHandle ? <div class="basefn-resizable__handle-grip" /> : <> </>}
283
+ </div>,
284
+ )
285
+ }
286
+ })
287
+
288
+ <div id={containerId} class={containerClass}> {elements->Component.fragment} </div>
289
+ }
@@ -0,0 +1,313 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Xote from "xote/src/Xote.res.mjs";
4
+ import * as Xote__JSX from "xote/src/Xote__JSX.res.mjs";
5
+ import * as Core__Option from "@rescript/core/src/Core__Option.res.mjs";
6
+
7
+ import './Basefn__Resizable.css'
8
+ ;
9
+
10
+ function directionToString(d) {
11
+ if (d === "Horizontal") {
12
+ return "horizontal";
13
+ } else {
14
+ return "vertical";
15
+ }
16
+ }
17
+
18
+ let getClientX = (function(e) {
19
+ var t = e.touches ? e.touches[0] : e;
20
+ return t.clientX;
21
+ });
22
+
23
+ let getClientY = (function(e) {
24
+ var t = e.touches ? e.touches[0] : e;
25
+ return t.clientY;
26
+ });
27
+
28
+ let addDocListener = (function(ev, fn) {
29
+ document.addEventListener(ev, fn);
30
+ });
31
+
32
+ let removeDocListener = (function(ev, fn) {
33
+ document.removeEventListener(ev, fn);
34
+ });
35
+
36
+ let disableSelect = (function() {
37
+ document.body.style.userSelect = "none";
38
+ document.body.style.webkitUserSelect = "none";
39
+ });
40
+
41
+ let enableSelect = (function() {
42
+ document.body.style.userSelect = "";
43
+ document.body.style.webkitUserSelect = "";
44
+ });
45
+
46
+ let findClosest = (function(el, sel) {
47
+ return el.closest(sel);
48
+ });
49
+
50
+ let getElementRect = (function(el) {
51
+ return el.getBoundingClientRect();
52
+ });
53
+
54
+ let getKey = (function(e) { return e.key || "" });
55
+
56
+ let getEventTarget = (function(e) {
57
+ return e.target;
58
+ });
59
+
60
+ let preventDefault = (function(e) { e.preventDefault() });
61
+
62
+ let genId = (function() {
63
+ return "basefn-resizable-" + Math.random().toString(36).substr(2, 9);
64
+ });
65
+
66
+ let setupDragListeners = (function(containerId, onStart) {
67
+ requestAnimationFrame(function() {
68
+ var container = document.getElementById(containerId);
69
+ if (!container) return;
70
+
71
+ function handler(e) {
72
+ var target = e.target;
73
+ var handle = target.closest ? target.closest(".basefn-resizable__handle") : null;
74
+ if (!handle || !container.contains(handle)) return;
75
+ var handles = container.querySelectorAll(".basefn-resizable__handle");
76
+ for (var i = 0; i < handles.length; i++) {
77
+ if (handles[i] === handle) {
78
+ onStart(i, e);
79
+ return;
80
+ }
81
+ }
82
+ }
83
+
84
+ container.addEventListener("mousedown", handler);
85
+ container.addEventListener("touchstart", handler, { passive: false });
86
+ });
87
+ });
88
+
89
+ function Basefn__Resizable(props) {
90
+ let __class = props.class;
91
+ let onResize = props.onResize;
92
+ let __withHandle = props.withHandle;
93
+ let __direction = props.direction;
94
+ let panels = props.panels;
95
+ let direction = __direction !== undefined ? __direction : "Horizontal";
96
+ let withHandle = __withHandle !== undefined ? __withHandle : true;
97
+ let $$class = __class !== undefined ? __class : "";
98
+ let containerId = genId();
99
+ let sizes = Xote.Signal.make(panels.map(p => p.defaultSize), undefined, undefined);
100
+ let isDragging = Xote.Signal.make(false, undefined, undefined);
101
+ let dragRef = {
102
+ contents: undefined
103
+ };
104
+ let getPos = evt => {
105
+ if (direction === "Horizontal") {
106
+ return getClientX(evt);
107
+ } else {
108
+ return getClientY(evt);
109
+ }
110
+ };
111
+ let getContainerDimension = container => {
112
+ let rect = getElementRect(container);
113
+ if (direction === "Horizontal") {
114
+ return rect.width;
115
+ } else {
116
+ return rect.height;
117
+ }
118
+ };
119
+ let clampSizes = (leftIdx, rightIdx, newLeft, newRight) => {
120
+ let lp = panels[leftIdx];
121
+ let rp = panels[rightIdx];
122
+ let lMin = Core__Option.getOr(lp.minSize, 0.0);
123
+ let lMax = Core__Option.getOr(lp.maxSize, 100.0);
124
+ let rMin = Core__Option.getOr(rp.minSize, 0.0);
125
+ let rMax = Core__Option.getOr(rp.maxSize, 100.0);
126
+ let total = newLeft + newRight;
127
+ if (newLeft < lMin) {
128
+ return [
129
+ lMin,
130
+ total - lMin
131
+ ];
132
+ } else if (newLeft > lMax) {
133
+ return [
134
+ lMax,
135
+ total - lMax
136
+ ];
137
+ } else if (newRight < rMin) {
138
+ return [
139
+ total - rMin,
140
+ rMin
141
+ ];
142
+ } else if (newRight > rMax) {
143
+ return [
144
+ total - rMax,
145
+ rMax
146
+ ];
147
+ } else {
148
+ return [
149
+ newLeft,
150
+ newRight
151
+ ];
152
+ }
153
+ };
154
+ let applySizes = newSizes => {
155
+ Xote.Signal.set(sizes, newSizes);
156
+ if (onResize !== undefined) {
157
+ return onResize(newSizes);
158
+ }
159
+ };
160
+ let onMouseMove = evt => {
161
+ let info = dragRef.contents;
162
+ if (info === undefined) {
163
+ return;
164
+ }
165
+ let containerSize = getContainerDimension(info.container);
166
+ if (containerSize <= 0.0) {
167
+ return;
168
+ }
169
+ let delta = getPos(evt) - info.startPos;
170
+ let deltaPercent = delta / containerSize * 100.0;
171
+ let li = info.handleIndex;
172
+ let ri = info.handleIndex + 1 | 0;
173
+ let origLeft = info.startSizes[li];
174
+ let origRight = info.startSizes[ri];
175
+ let match = clampSizes(li, ri, origLeft + deltaPercent, origRight - deltaPercent);
176
+ let newSizes = Xote.Signal.get(sizes).slice();
177
+ newSizes[li] = match[0];
178
+ newSizes[ri] = match[1];
179
+ applySizes(newSizes);
180
+ };
181
+ let onMouseUp = param => {
182
+ dragRef.contents = undefined;
183
+ Xote.Signal.set(isDragging, false);
184
+ enableSelect();
185
+ removeDocListener("mousemove", onMouseMove);
186
+ removeDocListener("mouseup", onMouseUp);
187
+ removeDocListener("touchmove", onMouseMove);
188
+ removeDocListener("touchend", onMouseUp);
189
+ };
190
+ let startDrag = (handleIndex, evt) => {
191
+ preventDefault(evt);
192
+ let target = getEventTarget(evt);
193
+ let maybeContainer = findClosest(target, ".basefn-resizable");
194
+ if (!(maybeContainer == null)) {
195
+ dragRef.contents = {
196
+ handleIndex: handleIndex,
197
+ startPos: getPos(evt),
198
+ startSizes: Xote.Signal.get(sizes).slice(),
199
+ container: maybeContainer
200
+ };
201
+ Xote.Signal.set(isDragging, true);
202
+ disableSelect();
203
+ addDocListener("mousemove", onMouseMove);
204
+ addDocListener("mouseup", onMouseUp);
205
+ addDocListener("touchmove", onMouseMove);
206
+ return addDocListener("touchend", onMouseUp);
207
+ }
208
+ };
209
+ setupDragListeners(containerId, startDrag);
210
+ let containerClass = "basefn-resizable basefn-resizable--" + directionToString(direction) + (
211
+ $$class !== "" ? " " + $$class : ""
212
+ );
213
+ let elements = [];
214
+ panels.forEach((panel, index) => {
215
+ let panelStyle = Xote.Computed.make(() => {
216
+ let size = Xote.Signal.get(sizes)[index];
217
+ return "flex-basis:" + size.toString() + "%;flex-grow:0;flex-shrink:0";
218
+ }, undefined);
219
+ let panelClass = Xote.Computed.make(() => {
220
+ let base = "basefn-resizable__panel";
221
+ if (Xote.Signal.get(isDragging)) {
222
+ return base + " basefn-resizable__panel--dragging";
223
+ } else {
224
+ return base;
225
+ }
226
+ }, undefined);
227
+ elements.push(Xote__JSX.Elements.jsxKeyed("div", {
228
+ class: panelClass,
229
+ style: panelStyle,
230
+ children: panel.content
231
+ }, "panel-" + index.toString(), undefined));
232
+ if (index >= (panels.length - 1 | 0)) {
233
+ return;
234
+ }
235
+ let handleClass = "basefn-resizable__handle basefn-resizable__handle--" + directionToString(direction) + (
236
+ withHandle ? " basefn-resizable__handle--with-grip" : ""
237
+ );
238
+ elements.push(Xote__JSX.Elements.jsxKeyed("div", {
239
+ class: handleClass,
240
+ role: "separator",
241
+ tabIndex: 0,
242
+ onKeyDown: evt => {
243
+ let key = getKey(evt);
244
+ let ri = index + 1 | 0;
245
+ let currentSizes = Xote.Signal.get(sizes);
246
+ let left = currentSizes[index];
247
+ let right = currentSizes[ri];
248
+ let delta;
249
+ if (direction === "Horizontal") {
250
+ switch (key) {
251
+ case "ArrowLeft" :
252
+ delta = - 1.0;
253
+ break;
254
+ case "ArrowRight" :
255
+ delta = 1.0;
256
+ break;
257
+ default:
258
+ delta = undefined;
259
+ }
260
+ } else {
261
+ switch (key) {
262
+ case "ArrowDown" :
263
+ delta = 1.0;
264
+ break;
265
+ case "ArrowUp" :
266
+ delta = - 1.0;
267
+ break;
268
+ default:
269
+ delta = undefined;
270
+ }
271
+ }
272
+ if (delta === undefined) {
273
+ return;
274
+ }
275
+ preventDefault(evt);
276
+ let match = clampSizes(index, ri, left + delta, right - delta);
277
+ let newSizes = currentSizes.slice();
278
+ newSizes[index] = match[0];
279
+ newSizes[ri] = match[1];
280
+ applySizes(newSizes);
281
+ },
282
+ children: withHandle ? Xote__JSX.Elements.jsx("div", {
283
+ class: "basefn-resizable__handle-grip"
284
+ }) : Xote__JSX.jsx(Xote__JSX.jsxFragment, {})
285
+ }, "handle-" + index.toString(), undefined));
286
+ });
287
+ return Xote__JSX.Elements.jsx("div", {
288
+ id: containerId,
289
+ class: containerClass,
290
+ children: Xote.Component.fragment(elements)
291
+ });
292
+ }
293
+
294
+ let make = Basefn__Resizable;
295
+
296
+ export {
297
+ directionToString,
298
+ getClientX,
299
+ getClientY,
300
+ addDocListener,
301
+ removeDocListener,
302
+ disableSelect,
303
+ enableSelect,
304
+ findClosest,
305
+ getElementRect,
306
+ getKey,
307
+ getEventTarget,
308
+ preventDefault,
309
+ genId,
310
+ setupDragListeners,
311
+ make,
312
+ }
313
+ /* Not a pure module */
@@ -65,10 +65,18 @@ let make = (
65
65
  | "ArrowDown" => {
66
66
  let _ = Basefn__Dom.preventDefault(evt)
67
67
  Signal.update(activeIndex, i => mod(i + 1, max(len, 1)))
68
+ let _ = %raw(`requestAnimationFrame(() => {
69
+ const el = document.querySelector(".basefn-spotlight__item--active");
70
+ if (el) el.scrollIntoView({ block: "nearest" });
71
+ })`)
68
72
  }
69
73
  | "ArrowUp" => {
70
74
  let _ = Basefn__Dom.preventDefault(evt)
71
75
  Signal.update(activeIndex, i => mod(i - 1 + max(len, 1), max(len, 1)))
76
+ let _ = %raw(`requestAnimationFrame(() => {
77
+ const el = document.querySelector(".basefn-spotlight__item--active");
78
+ if (el) el.scrollIntoView({ block: "nearest" });
79
+ })`)
72
80
  }
73
81
  | "Enter" =>
74
82
  if len > 0 {
@@ -54,10 +54,20 @@ function Basefn__Spotlight(props) {
54
54
  switch (k) {
55
55
  case "ArrowDown" :
56
56
  Basefn__Dom.preventDefault(evt);
57
- return Xote.Signal.update(activeIndex, i => Primitive_int.mod_(i + 1 | 0, Primitive_int.max(len, 1)));
57
+ Xote.Signal.update(activeIndex, i => Primitive_int.mod_(i + 1 | 0, Primitive_int.max(len, 1)));
58
+ ((requestAnimationFrame(() => {
59
+ const el = document.querySelector(".basefn-spotlight__item--active");
60
+ if (el) el.scrollIntoView({ block: "nearest" });
61
+ })));
62
+ return;
58
63
  case "ArrowUp" :
59
64
  Basefn__Dom.preventDefault(evt);
60
- return Xote.Signal.update(activeIndex, i => Primitive_int.mod_((i - 1 | 0) + Primitive_int.max(len, 1) | 0, Primitive_int.max(len, 1)));
65
+ Xote.Signal.update(activeIndex, i => Primitive_int.mod_((i - 1 | 0) + Primitive_int.max(len, 1) | 0, Primitive_int.max(len, 1)));
66
+ ((requestAnimationFrame(() => {
67
+ const el = document.querySelector(".basefn-spotlight__item--active");
68
+ if (el) el.scrollIntoView({ block: "nearest" });
69
+ })));
70
+ return;
61
71
  case "Enter" :
62
72
  if (len <= 0) {
63
73
  return;