@bagelink/vue 1.10.35 → 1.10.37

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.10.35",
4
+ "version": "1.10.37",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -90,7 +90,7 @@
90
90
  "signature_pad": "^5.0.9",
91
91
  "vue-i18n": "^11.2.8",
92
92
  "vue-toastification": "^2.0.0-rc.5",
93
- "@bagelink/utils": "1.10.35"
93
+ "@bagelink/utils": "1.10.37"
94
94
  },
95
95
  "scripts": {
96
96
  "dev": "tsx watch src/index.ts",
@@ -423,7 +423,7 @@ onMounted(() => {
423
423
  }
424
424
 
425
425
  .selectinput .bagel-input.mb-0 input {
426
- background: transparent !important;
426
+ /* background: transparent !important; */
427
427
  }
428
428
 
429
429
  .selectinput-btn:disabled {
@@ -0,0 +1,278 @@
1
+ <script lang="ts" setup>
2
+ import type { PanelConfig, PanelState } from '../../composables/useResizableLayout'
3
+ import { computed, inject, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
4
+ import { RESIZABLE_KEY, nextPanelId } from '../../composables/useResizableLayout'
5
+
6
+ const props = withDefaults(defineProps<Props>(), {
7
+ defaultSize: 200,
8
+ min: 0,
9
+ max: Number.POSITIVE_INFINITY,
10
+ collapsible: false,
11
+ collapsed: undefined,
12
+ flex: false,
13
+ handle: true,
14
+ })
15
+
16
+ const emit = defineEmits<{
17
+ 'update:collapsed': [value: boolean]
18
+ 'update:size': [value: number]
19
+ }>()
20
+
21
+ interface Props {
22
+ id?: string
23
+ /** Initial size in pixels. @default 200 */
24
+ defaultSize?: number
25
+ /** Controlled size — use with v-model:size to drive from outside (e.g. device presets). */
26
+ size?: number
27
+ /** Minimum size in pixels. @default 0 */
28
+ min?: number
29
+ /** Maximum size in pixels. @default Infinity */
30
+ max?: number
31
+ /** Allows the panel to be collapsed. Exposes toggleCollapse via slot + v-model:collapsed. */
32
+ collapsible?: boolean
33
+ /** Controlled collapsed state — use with v-model:collapsed. */
34
+ collapsed?: boolean
35
+ /** Fill all remaining space in the layout. */
36
+ flex?: boolean
37
+ /** Collapse this panel when the layout switches to mobile. */
38
+ collapseOnMobile?: boolean
39
+ /** Hide this panel entirely when the layout switches to mobile. */
40
+ hideOnMobile?: boolean
41
+ /** Show a drag handle on the trailing edge of this panel. @default true */
42
+ handle?: boolean
43
+ }
44
+
45
+ const ctx = inject(RESIZABLE_KEY)!
46
+ if (!ctx) throw new Error('<Panel> must be used inside a <Resizable>')
47
+
48
+ // Stable ID for the lifetime of this instance
49
+ const panelId = props.id ?? nextPanelId()
50
+
51
+ const panelState = shallowRef<PanelState | null>(null)
52
+ const panelEl = ref<HTMLElement | null>(null)
53
+
54
+ // ── Collapsed (controlled / uncontrolled) ──────────────────────────────────
55
+
56
+ const collapsedInternal = computed({
57
+ get: () => props.collapsed ?? panelState.value?.collapsed.value ?? false,
58
+ set(val) {
59
+ if (panelState.value) panelState.value.collapsed.value = val
60
+ emit('update:collapsed', val)
61
+ },
62
+ })
63
+
64
+ watch(() => props.collapsed, (val) => {
65
+ if (val !== undefined && panelState.value) panelState.value.collapsed.value = val
66
+ })
67
+
68
+ // ── Size (controlled / uncontrolled) ──────────────────────────────────────
69
+
70
+ // Prop → internal (button presets, external control)
71
+ watch(() => props.size, (val) => {
72
+ if (val === undefined || !panelState.value) return
73
+ const clamped = Math.max(props.min, Math.min(props.max, val))
74
+ if (clamped !== panelState.value.size.value) panelState.value.size.value = clamped
75
+ })
76
+
77
+ // Internal → prop (drag end syncs back up via v-model:size)
78
+ // Skip the initial mount transition (undefined → number) to avoid spurious emits
79
+ watch(
80
+ () => panelState.value?.size.value,
81
+ (newSize, oldSize) => {
82
+ if (newSize !== undefined && oldSize !== undefined) emit('update:size', newSize)
83
+ },
84
+ )
85
+
86
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
87
+
88
+ onMounted(() => {
89
+ const config: PanelConfig = {
90
+ id: panelId,
91
+ defaultSize: props.size ?? props.defaultSize,
92
+ min: props.min,
93
+ max: props.max,
94
+ flex: props.flex,
95
+ collapseOnMobile: props.collapseOnMobile ?? false,
96
+ hideOnMobile: props.hideOnMobile ?? false,
97
+ }
98
+ panelState.value = ctx.registerPanel(config)
99
+ panelState.value.el.value = panelEl.value
100
+ if (props.collapsed !== undefined) panelState.value.collapsed.value = props.collapsed
101
+ })
102
+
103
+ onUnmounted(() => { ctx.unregisterPanel(panelId) })
104
+
105
+ // ── Computed state ─────────────────────────────────────────────────────────
106
+
107
+ const size = computed(() => panelState.value?.size.value ?? props.defaultSize)
108
+ const isCollapsed = computed(() => collapsedInternal.value)
109
+ const isHorizontal = computed(() => ctx.direction.value === 'horizontal')
110
+ const isHidden = computed(() => ctx.isMobile.value && props.hideOnMobile)
111
+
112
+ // Default to false (show handle) before mount so handles appear on first render.
113
+ // After registration the correct last-panel is identified reactively.
114
+ const isLastPanel = computed(() => {
115
+ const ps = ctx.panels.value
116
+ const idx = ps.findIndex(p => p.id === panelId)
117
+ return idx >= 0 && idx === ps.length - 1
118
+ })
119
+
120
+ const panelStyle = computed(() => {
121
+ if (isCollapsed.value) {
122
+ return isHorizontal.value
123
+ ? { flex: '0 0 0px', width: '0px', minWidth: '0', overflow: 'hidden' }
124
+ : { flex: '0 0 0px', height: '0px', minHeight: '0', overflow: 'hidden' }
125
+ }
126
+ if (props.flex) {
127
+ return isHorizontal.value
128
+ ? { flex: '1 1 0', minWidth: `${props.min}px`, maxWidth: props.max === Number.POSITIVE_INFINITY ? undefined : `${props.max}px`, overflow: 'hidden' }
129
+ : { flex: '1 1 0', minHeight: `${props.min}px`, maxHeight: props.max === Number.POSITIVE_INFINITY ? undefined : `${props.max}px`, overflow: 'hidden' }
130
+ }
131
+ return isHorizontal.value
132
+ ? { flex: `0 0 ${size.value}px`, width: `${size.value}px`, overflow: 'hidden' }
133
+ : { flex: `0 0 ${size.value}px`, height: `${size.value}px`, overflow: 'hidden' }
134
+ })
135
+
136
+ // ── Exposed / slot API ─────────────────────────────────────────────────────
137
+
138
+ function toggleCollapse() {
139
+ collapsedInternal.value = !collapsedInternal.value
140
+ }
141
+
142
+ defineExpose({ toggleCollapse, size, isCollapsed })
143
+ </script>
144
+
145
+ <template>
146
+ <div
147
+ v-if="!isHidden" ref="panelEl" class="panel"
148
+ :class="[isHorizontal ? 'panel--h' : 'panel--v', isCollapsed && 'panel--collapsed']" :style="panelStyle"
149
+ >
150
+ <div class="panel__content">
151
+ <slot :collapsed="isCollapsed" :size="size" :toggle-collapse="toggleCollapse" />
152
+ </div>
153
+
154
+ <div
155
+ v-if="props.handle && !isLastPanel && !isCollapsed" class="panel__handle"
156
+ :class="isHorizontal ? 'panel__handle--h' : 'panel__handle--v'"
157
+ @pointerdown="ctx.onHandlePointerDown(panelId, $event)"
158
+ >
159
+ <div class="panel__handle-bar" />
160
+ </div>
161
+ </div>
162
+ </template>
163
+
164
+ <style scoped>
165
+ .panel {
166
+ position: relative;
167
+ display: flex;
168
+ overflow: hidden;
169
+ /* transition: flex-basis 0.2s ease, width 0.2s ease, height 0.2s ease; */
170
+ }
171
+
172
+ /* Disable transitions while dragging so the handle tracks the pointer exactly */
173
+ :global(.resizable[data-resizing]) .panel {
174
+ transition: none !important;
175
+ }
176
+
177
+ /* Prevent iframes from absorbing pointer events and triggering expensive
178
+ cross-process reflows on every resize tick */
179
+ :global(.resizable[data-resizing]) iframe,
180
+ :global(.resizable[data-resizing]) video {
181
+ pointer-events: none !important;
182
+ }
183
+
184
+ .panel--h {
185
+ flex-direction: row;
186
+ }
187
+
188
+ .panel--v {
189
+ flex-direction: column;
190
+ }
191
+
192
+ .panel__content {
193
+ flex: 1 1 0;
194
+ overflow: hidden;
195
+ min-width: 0;
196
+ min-height: 0;
197
+ }
198
+
199
+ /* ─── Handle ─────────────────────────────────────────────────────────────── */
200
+
201
+ .panel__handle {
202
+ flex-shrink: 0;
203
+ position: relative;
204
+ z-index: 10;
205
+ touch-action: none;
206
+ user-select: none;
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ }
211
+
212
+ .panel__handle--h {
213
+ width: var(--resizable-handle-size, 9px);
214
+ cursor: ew-resize;
215
+ }
216
+
217
+ .panel__handle--v {
218
+ height: var(--resizable-handle-size, 9px);
219
+ cursor: ns-resize;
220
+ }
221
+
222
+ /* Persistent 1px divider */
223
+ .panel__handle::before {
224
+ content: '';
225
+ position: absolute;
226
+ border-radius: 9999px;
227
+ background: var(--resizable-divider-color, color-mix(in srgb, currentColor 14%, transparent));
228
+ transition: background 0.15s;
229
+ }
230
+
231
+ .panel__handle--h::before {
232
+ top: 0;
233
+ bottom: 0;
234
+ left: 50%;
235
+ transform: translateX(-50%);
236
+ width: 1px;
237
+ height: 100%;
238
+ }
239
+
240
+ .panel__handle--v::before {
241
+ left: 0;
242
+ right: 0;
243
+ top: 50%;
244
+ transform: translateY(-50%);
245
+ height: 1px;
246
+ width: 100%;
247
+ }
248
+
249
+ /* Grip pill — fades in on hover */
250
+ .panel__handle-bar {
251
+ position: relative;
252
+ z-index: 1;
253
+ border-radius: 9999px;
254
+ background: var(--resizable-grip-color, var(--color-primary, #3b82f6));
255
+ opacity: 0;
256
+ transition: opacity 0.15s, transform 0.15s;
257
+ transform: scale(0.8);
258
+ }
259
+
260
+ .panel__handle--h .panel__handle-bar {
261
+ width: 3px;
262
+ height: 24px;
263
+ }
264
+
265
+ .panel__handle--v .panel__handle-bar {
266
+ height: 3px;
267
+ width: 24px;
268
+ }
269
+
270
+ .panel__handle:hover::before {
271
+ background: var(--resizable-divider-active-color, var(--color-primary, #3b82f6));
272
+ }
273
+
274
+ .panel__handle:hover .panel__handle-bar {
275
+ opacity: 1;
276
+ transform: scale(1);
277
+ }
278
+ </style>
@@ -0,0 +1,64 @@
1
+ <script lang="ts" setup>
2
+ import { computed, onMounted, onUnmounted, provide, ref } from 'vue'
3
+ import { RESIZABLE_KEY, useResizableLayoutProvider } from '../../composables/useResizableLayout'
4
+
5
+ interface Props {
6
+ /** Lay panels side-by-side. Default: vertical (stacked). */
7
+ horizontal?: boolean
8
+ /**
9
+ * Stack panels vertically when the container is narrower than `breakpoint`.
10
+ * Only applies when `horizontal` is set. Default: true.
11
+ * Set `:breakpoint="0"` to disable responsive switching entirely.
12
+ */
13
+ mobileVertical?: boolean
14
+ /** Width (px) below which the mobile layout activates. @default 768 */
15
+ breakpoint?: number
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ horizontal: false,
20
+ mobileVertical: true,
21
+ breakpoint: 768,
22
+ })
23
+
24
+ const containerEl = ref<HTMLElement>()
25
+ const directionProp = computed(() => props.horizontal ? 'horizontal' as const : 'vertical' as const)
26
+ const mobileDirection = computed(() => props.mobileVertical || !props.horizontal ? 'vertical' as const : 'horizontal' as const,
27
+ )
28
+
29
+ const { context, observe, disconnect } = useResizableLayoutProvider({
30
+ directionProp,
31
+ mobileDirection: mobileDirection.value,
32
+ breakpoint: props.breakpoint,
33
+ containerEl,
34
+ })
35
+
36
+ provide(RESIZABLE_KEY, context)
37
+ onMounted(observe)
38
+ onUnmounted(disconnect)
39
+
40
+ const isHorizontal = computed(() => context.direction.value === 'horizontal')
41
+ </script>
42
+
43
+ <template>
44
+ <div ref="containerEl" class="resizable" :class="isHorizontal ? 'resizable--row' : 'resizable--col'">
45
+ <slot />
46
+ </div>
47
+ </template>
48
+
49
+ <style scoped>
50
+ .resizable {
51
+ display: flex;
52
+ width: 100%;
53
+ height: 100%;
54
+ overflow: hidden;
55
+ }
56
+
57
+ .resizable--row {
58
+ flex-direction: row;
59
+ }
60
+
61
+ .resizable--col {
62
+ flex-direction: column;
63
+ }
64
+ </style>
@@ -3,6 +3,8 @@ export { default as AppLayout } from './AppLayout.vue'
3
3
  export { default as AppSidebar } from './AppSidebar.vue'
4
4
  export { default as BottomMenu } from './BottomMenu.vue'
5
5
  export { default as Layout } from './Layout.vue'
6
+ export { default as Panel } from './Panel.vue'
7
+ export { default as Resizable } from './Resizable.vue'
6
8
  export { default as SidebarMenu } from './SidebarMenu.vue'
7
9
  export { default as Skeleton } from './Skeleton.vue'
8
10
  export { default as TabbedLayout } from './TabbedLayout.vue'
@@ -9,6 +9,8 @@ export { useDevice } from './useDevice'
9
9
  export { useExcel } from './useExcel'
10
10
  export { useLocalStore } from './useLocalStore'
11
11
  export { usePolling } from './usePolling'
12
+ export { useResizableLayoutProvider } from './useResizableLayout'
13
+ export type { PanelConfig, PanelState, ResizableContext } from './useResizableLayout'
12
14
  export { useResizeObserver } from './useResizeObserver'
13
15
  export { useTheme } from './useTheme'
14
16
  interface UseBglSchemaParamsT<T> {
@@ -0,0 +1,252 @@
1
+ import type { InjectionKey, Ref } from 'vue'
2
+ import { computed, ref, shallowRef, watch } from 'vue'
3
+
4
+ export interface PanelConfig {
5
+ id: string
6
+ defaultSize: number
7
+ min: number
8
+ max: number
9
+ flex: boolean
10
+ collapseOnMobile: boolean
11
+ hideOnMobile: boolean
12
+ }
13
+
14
+ export interface PanelState {
15
+ id: string
16
+ config: PanelConfig
17
+ size: Ref<number>
18
+ collapsed: Ref<boolean>
19
+ /** DOM reference set by Panel.vue for direct style mutation during drag. */
20
+ el: Ref<HTMLElement | null>
21
+ }
22
+
23
+ export interface ResizableContext {
24
+ direction: Ref<'horizontal' | 'vertical'>
25
+ isMobile: Ref<boolean>
26
+ panels: Ref<PanelState[]>
27
+ registerPanel: (config: PanelConfig) => PanelState
28
+ unregisterPanel: (id: string) => void
29
+ onHandlePointerDown: (panelId: string, event: PointerEvent) => void
30
+ }
31
+
32
+ export const RESIZABLE_KEY: InjectionKey<ResizableContext> = Symbol('Resizable')
33
+
34
+ let _panelUid = 0
35
+ export function nextPanelId(): string {
36
+ return `panel-${++_panelUid}`
37
+ }
38
+
39
+ // ─── Internal drag state ───────────────────────────────────────────────────
40
+
41
+ interface DragState {
42
+ panelId: string
43
+ pointerId: number
44
+ isHorizontal: boolean
45
+ isRtl: boolean
46
+ startPos: number
47
+ startSizeA: number
48
+ startSizeB: number
49
+ currentSizeA: number
50
+ currentSizeB: number
51
+ indexA: number
52
+ indexB: number
53
+ isFlexA: boolean
54
+ isFlexB: boolean
55
+ elA: HTMLElement | null
56
+ elB: HTMLElement | null
57
+ }
58
+
59
+ // ─── Provider ──────────────────────────────────────────────────────────────
60
+
61
+ export function useResizableLayoutProvider(options: {
62
+ directionProp: Ref<'horizontal' | 'vertical'>
63
+ mobileDirection: 'horizontal' | 'vertical'
64
+ breakpoint: number
65
+ containerEl: Ref<HTMLElement | undefined>
66
+ }) {
67
+ const { containerEl, breakpoint, mobileDirection } = options
68
+
69
+ const isMobile = ref(false)
70
+ const panels = shallowRef<PanelState[]>([])
71
+
72
+ const direction = computed<'horizontal' | 'vertical'>(() => isMobile.value ? mobileDirection : options.directionProp.value,
73
+ )
74
+
75
+ // ── Breakpoint detection ──
76
+ let ro: ResizeObserver | null = null
77
+
78
+ function observe() {
79
+ if (!containerEl.value) return
80
+ ro?.disconnect()
81
+ ro = new ResizeObserver((entries) => {
82
+ const entry = entries[0]
83
+ if (entry) isMobile.value = entry.contentRect.width < breakpoint
84
+ })
85
+ ro.observe(containerEl.value)
86
+ isMobile.value = containerEl.value.clientWidth < breakpoint
87
+ }
88
+
89
+ function disconnect() {
90
+ ro?.disconnect()
91
+ ro = null
92
+ }
93
+
94
+ // On mobile switch: apply collapsed state overrides
95
+ watch(isMobile, (mobile) => {
96
+ for (const panel of panels.value) {
97
+ const { config } = panel
98
+ panel.collapsed.value = mobile
99
+ ? (config.collapseOnMobile || false)
100
+ : false
101
+ }
102
+ })
103
+
104
+ // ── Panel registry ──
105
+ function registerPanel(config: PanelConfig): PanelState {
106
+ const state: PanelState = {
107
+ id: config.id,
108
+ config,
109
+ size: ref(config.defaultSize),
110
+ collapsed: ref(isMobile.value ? config.collapseOnMobile : false),
111
+ el: ref(null),
112
+ }
113
+ panels.value = [...panels.value, state]
114
+ return state
115
+ }
116
+
117
+ function unregisterPanel(id: string) {
118
+ panels.value = panels.value.filter(p => p.id !== id)
119
+ }
120
+
121
+ function getPanelIndex(id: string) {
122
+ return panels.value.findIndex(p => p.id === id)
123
+ }
124
+
125
+ // ── Drag ──
126
+ // Cleanup function for the current drag's document-level listeners.
127
+ // Called at the start of every pointerdown to clear any previously stuck drag.
128
+ let dragCleanup: (() => void) | null = null
129
+
130
+ function flushResize(ds: DragState, currentPos: number) {
131
+ const rawDelta = currentPos - ds.startPos
132
+ const delta = ds.isRtl ? -rawDelta : rawDelta
133
+
134
+ const panelA = panels.value[ds.indexA]
135
+ const panelB = panels.value[ds.indexB]
136
+ const minA = panelA.config.min
137
+ const maxA = panelA.config.max
138
+ const minB = panelB.config.min
139
+ const maxB = panelB.config.max
140
+
141
+ let newA = ds.currentSizeA
142
+ let newB = ds.currentSizeB
143
+
144
+ if (ds.isFlexB) {
145
+ newA = Math.max(minA, Math.min(maxA, ds.startSizeA + delta))
146
+ }
147
+ else if (ds.isFlexA) {
148
+ newB = Math.max(minB, Math.min(maxB, ds.startSizeB - delta))
149
+ }
150
+ else {
151
+ const proposed = Math.max(minA, Math.min(maxA, ds.startSizeA + delta))
152
+ const actualDelta = proposed - ds.startSizeA
153
+ newB = Math.max(minB, Math.min(maxB, ds.startSizeB - actualDelta))
154
+ newA = ds.startSizeA + (ds.startSizeB - newB)
155
+ }
156
+
157
+ ds.currentSizeA = newA
158
+ ds.currentSizeB = newB
159
+
160
+ // Direct DOM mutation — bypasses Vue scheduler for zero-overhead drag
161
+ const dim = ds.isHorizontal ? 'width' : 'height'
162
+ if (ds.elA && !ds.isFlexA) {
163
+ ds.elA.style.flex = `0 0 ${newA}px`
164
+ ds.elA.style[dim] = `${newA}px`
165
+ }
166
+ if (ds.elB && !ds.isFlexB) {
167
+ ds.elB.style.flex = `0 0 ${newB}px`
168
+ ds.elB.style[dim] = `${newB}px`
169
+ }
170
+ }
171
+
172
+ function onHandlePointerDown(panelId: string, event: PointerEvent) {
173
+ // Always clean up any previously stuck drag before starting a new one
174
+ dragCleanup?.()
175
+
176
+ const indexA = getPanelIndex(panelId)
177
+ if (indexA < 0 || indexA >= panels.value.length - 1) return
178
+
179
+ const panelA = panels.value[indexA]
180
+ const panelB = panels.value[indexA + 1]
181
+ if (panelA.collapsed.value || panelB.collapsed.value) return
182
+
183
+ event.preventDefault()
184
+
185
+ const isHorizontal = direction.value === 'horizontal'
186
+ const pos = isHorizontal ? event.clientX : event.clientY
187
+ const isRtl = isHorizontal
188
+ && !!containerEl.value
189
+ && getComputedStyle(containerEl.value).direction === 'rtl'
190
+
191
+ const ds: DragState = {
192
+ panelId,
193
+ pointerId: event.pointerId,
194
+ isHorizontal,
195
+ isRtl,
196
+ startPos: pos,
197
+ startSizeA: panelA.size.value,
198
+ startSizeB: panelB.size.value,
199
+ currentSizeA: panelA.size.value,
200
+ currentSizeB: panelB.size.value,
201
+ indexA,
202
+ indexB: indexA + 1,
203
+ isFlexA: panelA.config.flex,
204
+ isFlexB: panelB.config.flex,
205
+ elA: panelA.el.value,
206
+ elB: panelB.el.value,
207
+ }
208
+ containerEl.value?.setAttribute('data-resizing', '')
209
+ document.body.style.userSelect = 'none'
210
+ document.body.style.cursor = isHorizontal ? 'ew-resize' : 'ns-resize'
211
+
212
+ function onMove(e: PointerEvent) {
213
+ if (e.pointerId !== ds.pointerId) return
214
+ flushResize(ds, ds.isHorizontal ? e.clientX : e.clientY)
215
+ }
216
+
217
+ function onUp(e: PointerEvent) {
218
+ if (e.pointerId !== ds.pointerId) return
219
+ flushResize(ds, ds.isHorizontal ? e.clientX : e.clientY)
220
+ // Single Vue update at drag end — syncs DOM state back to reactive refs
221
+ panels.value[ds.indexA].size.value = ds.currentSizeA
222
+ panels.value[ds.indexB].size.value = ds.currentSizeB
223
+ cleanup()
224
+ }
225
+
226
+ function cleanup() {
227
+ dragCleanup = null
228
+ containerEl.value?.removeAttribute('data-resizing')
229
+ document.body.style.userSelect = ''
230
+ document.body.style.cursor = ''
231
+ document.removeEventListener('pointermove', onMove)
232
+ document.removeEventListener('pointerup', onUp)
233
+ document.removeEventListener('pointercancel', cleanup)
234
+ }
235
+
236
+ document.addEventListener('pointermove', onMove)
237
+ document.addEventListener('pointerup', onUp)
238
+ document.addEventListener('pointercancel', cleanup)
239
+ dragCleanup = cleanup
240
+ }
241
+
242
+ const context: ResizableContext = {
243
+ direction,
244
+ isMobile,
245
+ panels,
246
+ registerPanel,
247
+ unregisterPanel,
248
+ onHandlePointerDown,
249
+ }
250
+
251
+ return { context, observe, disconnect }
252
+ }