@svgrid/grid 2.1.21 → 2.1.23
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/README.md +9 -2
- package/dist/DockNodeView.svelte +306 -0
- package/dist/DockNodeView.svelte.d.ts +7 -0
- package/dist/DockPopoutHost.svelte +17 -0
- package/dist/DockPopoutHost.svelte.d.ts +14 -0
- package/dist/GridMenus.svelte +83 -3
- package/dist/SvAutoComplete.svelte +5 -3
- package/dist/SvCarousel.svelte +20 -42
- package/dist/SvColorInput.svelte +4 -3
- package/dist/SvColorInput.svelte.d.ts +1 -1
- package/dist/SvComboBox.svelte +7 -3
- package/dist/SvCommand.svelte +28 -85
- package/dist/SvCountryInput.svelte +1 -1
- package/dist/SvDateRangeInput.svelte +1 -1
- package/dist/SvDateTimePicker.svelte +9 -3
- package/dist/SvDateTimePicker.svelte.d.ts +3 -0
- package/dist/SvDockLayout.svelte +244 -0
- package/dist/SvDockLayout.svelte.d.ts +77 -0
- package/dist/SvDockManager.svelte +922 -0
- package/dist/SvDockManager.svelte.d.ts +106 -0
- package/dist/SvDrawer.svelte +69 -47
- package/dist/SvDrawer.svelte.d.ts +4 -0
- package/dist/SvDropDownList.svelte +85 -13
- package/dist/SvDropDownList.svelte.d.ts +3 -0
- package/dist/SvField.svelte +21 -4
- package/dist/SvField.svelte.d.ts +1 -0
- package/dist/SvForm.svelte +201 -91
- package/dist/SvForm.svelte.d.ts +15 -22
- package/dist/SvGrid.controller.svelte.d.ts +32 -1
- package/dist/SvGrid.controller.svelte.js +156 -27
- package/dist/SvGrid.css +193 -5
- package/dist/SvGrid.svelte +357 -12
- package/dist/SvGrid.types.d.ts +452 -1
- package/dist/SvGridDropdown.svelte +8 -1
- package/dist/SvGridSelect.svelte +1 -0
- package/dist/SvListBox.svelte +36 -14
- package/dist/SvMaskedInput.svelte +4 -3
- package/dist/SvMaskedInput.svelte.d.ts +1 -1
- package/dist/SvMenu.svelte +1 -1
- package/dist/SvMenuList.svelte +23 -71
- package/dist/SvMenuList.svelte.d.ts +2 -13
- package/dist/SvModal.svelte +12 -29
- package/dist/SvMultiSelect.svelte +1 -0
- package/dist/SvNumberInput.svelte +4 -3
- package/dist/SvNumberInput.svelte.d.ts +1 -1
- package/dist/SvPagination.svelte +18 -18
- package/dist/SvPasswordInput.svelte +3 -2
- package/dist/SvPasswordInput.svelte.d.ts +1 -1
- package/dist/SvPhoneInput.svelte +3 -2
- package/dist/SvPhoneInput.svelte.d.ts +1 -1
- package/dist/SvPopover.svelte +1 -1
- package/dist/SvStepper.svelte +6 -9
- package/dist/SvTagsInput.svelte +3 -2
- package/dist/SvTagsInput.svelte.d.ts +1 -1
- package/dist/SvTooltip.svelte +12 -23
- package/dist/SvTour.svelte +32 -4
- package/dist/SvTreeSelect.svelte +1 -0
- package/dist/build-api.js +76 -54
- package/dist/cdn/svgrid.js +10907 -6444
- package/dist/cdn/svgrid.svelte-external.js +9622 -5475
- package/dist/columns.js +6 -0
- package/dist/conditional-formatting.d.ts +67 -3
- package/dist/conditional-formatting.js +124 -17
- package/dist/createCarousel.svelte.d.ts +72 -0
- package/dist/createCarousel.svelte.js +80 -0
- package/dist/createCommand.svelte.d.ts +62 -0
- package/dist/createCommand.svelte.js +109 -0
- package/dist/createForm.svelte.d.ts +62 -0
- package/dist/createForm.svelte.js +374 -0
- package/dist/createMenu.svelte.d.ts +88 -0
- package/dist/createMenu.svelte.js +153 -0
- package/dist/createOverlay.svelte.d.ts +38 -0
- package/dist/createOverlay.svelte.js +68 -0
- package/dist/createPagination.d.ts +74 -0
- package/dist/createPagination.js +71 -0
- package/dist/createPopoverSelect.svelte.js +6 -12
- package/dist/createStepper.d.ts +44 -0
- package/dist/createStepper.js +26 -0
- package/dist/createTooltip.svelte.d.ts +32 -0
- package/dist/createTooltip.svelte.js +61 -0
- package/dist/datetime/timezone.d.ts +36 -0
- package/dist/datetime/timezone.js +110 -0
- package/dist/dock-context.d.ts +52 -0
- package/dist/dock-context.js +1 -0
- package/dist/dock-manager-model.d.ts +136 -0
- package/dist/dock-manager-model.js +514 -0
- package/dist/dock-model.d.ts +105 -0
- package/dist/dock-model.js +288 -0
- package/dist/dock-popout.d.ts +30 -0
- package/dist/dock-popout.js +81 -0
- package/dist/editor-contract.d.ts +2 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +35 -2
- package/dist/filtering/excel-filters.d.ts +18 -1
- package/dist/filtering/excel-filters.js +56 -0
- package/dist/form-field.d.ts +73 -0
- package/dist/form-field.js +15 -0
- package/dist/index.d.ts +25 -5
- package/dist/index.js +40 -4
- package/dist/list-nav.d.ts +18 -0
- package/dist/list-nav.js +30 -0
- package/dist/list-option.d.ts +2 -0
- package/dist/menu-item.d.ts +20 -0
- package/dist/menu-item.js +1 -0
- package/dist/menus.d.ts +5 -0
- package/dist/menus.js +68 -16
- package/dist/panel-resize.d.ts +25 -0
- package/dist/panel-resize.js +40 -0
- package/dist/popover.d.ts +29 -3
- package/dist/popover.js +22 -6
- package/dist/recurrence.d.ts +33 -7
- package/dist/recurrence.js +151 -13
- package/dist/row-drag.d.ts +8 -0
- package/dist/row-drag.js +11 -0
- package/dist/scheduler-ical.d.ts +30 -0
- package/dist/scheduler-ical.js +180 -0
- package/dist/scheduler-model.d.ts +269 -0
- package/dist/scheduler-model.js +602 -0
- package/dist/scheduler-view.svelte.d.ts +22 -0
- package/dist/scheduler-view.svelte.js +13 -0
- package/dist/spreadsheet.js +20 -12
- package/dist/ui-app.types.d.ts +3 -0
- package/package.json +5 -1
- package/src/DockNodeView.svelte +306 -0
- package/src/DockPopoutHost.svelte +17 -0
- package/src/GridMenus.svelte +83 -3
- package/src/SvAutoComplete.svelte +5 -3
- package/src/SvCarousel.svelte +20 -42
- package/src/SvCarousel.test.ts +68 -0
- package/src/SvColorInput.svelte +4 -3
- package/src/SvComboBox.svelte +7 -3
- package/src/SvCommand.svelte +28 -85
- package/src/SvCommand.test.ts +78 -0
- package/src/SvCountryInput.svelte +1 -1
- package/src/SvDateRangeInput.svelte +1 -1
- package/src/SvDateTimePicker.svelte +9 -3
- package/src/SvDockLayout.svelte +244 -0
- package/src/SvDockManager.svelte +922 -0
- package/src/SvDrawer.svelte +69 -47
- package/src/SvDropDownList.svelte +85 -13
- package/src/SvField.svelte +21 -4
- package/src/SvForm.svelte +201 -91
- package/src/SvForm.test.ts +299 -0
- package/src/SvGrid.controller.svelte.ts +152 -28
- package/src/SvGrid.css +193 -5
- package/src/SvGrid.svelte +357 -12
- package/src/SvGrid.types.ts +463 -1
- package/src/SvGridDropdown.svelte +8 -1
- package/src/SvGridSelect.svelte +1 -0
- package/src/SvListBox.svelte +36 -14
- package/src/SvMaskedInput.svelte +4 -3
- package/src/SvMenu.svelte +1 -1
- package/src/SvMenu.test.ts +24 -0
- package/src/SvMenuList.svelte +23 -71
- package/src/SvModal.svelte +12 -29
- package/src/SvModal.test.ts +82 -0
- package/src/SvMultiSelect.svelte +1 -0
- package/src/SvNumberInput.svelte +4 -3
- package/src/SvPagination.svelte +18 -18
- package/src/SvPasswordInput.svelte +3 -2
- package/src/SvPhoneInput.svelte +3 -2
- package/src/SvPopover.svelte +1 -1
- package/src/SvStepper.svelte +6 -9
- package/src/SvTagsInput.svelte +3 -2
- package/src/SvTooltip.svelte +12 -23
- package/src/SvTooltip.test.ts +73 -0
- package/src/SvTour.svelte +32 -4
- package/src/SvTreeSelect.svelte +1 -0
- package/src/build-api.ts +74 -51
- package/src/columns.ts +5 -0
- package/src/conditional-formatting.test.ts +104 -0
- package/src/conditional-formatting.ts +176 -19
- package/src/createCarousel.svelte.ts +104 -0
- package/src/createCommand.svelte.ts +125 -0
- package/src/createForm.svelte.ts +333 -0
- package/src/createMenu.svelte.ts +202 -0
- package/src/createOverlay.svelte.ts +101 -0
- package/src/createPagination.test.ts +65 -0
- package/src/createPagination.ts +84 -0
- package/src/createPopoverSelect.svelte.ts +5 -9
- package/src/createStepper.test.ts +62 -0
- package/src/createStepper.ts +61 -0
- package/src/createTooltip.svelte.ts +86 -0
- package/src/datetime/timezone.test.ts +69 -0
- package/src/datetime/timezone.ts +134 -0
- package/src/dock-context.ts +47 -0
- package/src/dock-manager-model.test.ts +275 -0
- package/src/dock-manager-model.ts +593 -0
- package/src/dock-model.test.ts +163 -0
- package/src/dock-model.ts +368 -0
- package/src/dock-popout.ts +94 -0
- package/src/editor-contract.ts +2 -0
- package/src/filter-operators.test.ts +59 -0
- package/src/filter-operators.ts +35 -2
- package/src/filtering/excel-filters.test.ts +86 -1
- package/src/filtering/excel-filters.ts +65 -0
- package/src/form-field.ts +87 -0
- package/src/index.ts +158 -4
- package/src/list-nav.test.ts +49 -0
- package/src/list-nav.ts +29 -0
- package/src/list-option.ts +2 -0
- package/src/menu-item.ts +21 -0
- package/src/menus.test.ts +48 -0
- package/src/menus.ts +73 -16
- package/src/panel-resize.ts +52 -0
- package/src/popover.bounds.test.ts +93 -0
- package/src/popover.ts +51 -7
- package/src/recurrence.test.ts +88 -1
- package/src/recurrence.ts +166 -15
- package/src/row-drag.ts +10 -0
- package/src/scheduler-ical.test.ts +68 -0
- package/src/scheduler-ical.ts +180 -0
- package/src/scheduler-model.test.ts +546 -0
- package/src/scheduler-model.ts +853 -0
- package/src/scheduler-view.svelte.ts +33 -0
- package/src/spreadsheet.ts +20 -12
- package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
- package/src/ui-app.types.ts +3 -0
- package/src/virtual.test.ts +6 -4
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvDockManager - a full docking manager built on <SvDockLayout>. It adds the
|
|
4
|
+
* three "pro" features a tiled layout alone does not have:
|
|
5
|
+
*
|
|
6
|
+
* - Floating / pop-out windows: drag a tab out of the dock (or hit the float
|
|
7
|
+
* button) to pop it into a movable, resizable window; drag it back to redock.
|
|
8
|
+
* - Tab reordering: drag a tab along its own strip to reorder it.
|
|
9
|
+
* - Pinning / auto-hide: collapse a panel to an edge strip; a fly-out reveals
|
|
10
|
+
* it on hover, and "pin" docks it back.
|
|
11
|
+
*
|
|
12
|
+
* The docked area is an <SvDockLayout> in "manager mode" (it delegates every
|
|
13
|
+
* gesture here); floating windows are each a small <SvDockLayout> too. The
|
|
14
|
+
* whole workspace is the serializable `DockManagerState` from `dock-manager-model`.
|
|
15
|
+
*
|
|
16
|
+
* Build the initial workspace with the base `dockGroup`/`dockTabs`/`dockPane`
|
|
17
|
+
* helpers:
|
|
18
|
+
*
|
|
19
|
+
* let workspace = $state({
|
|
20
|
+
* main: dockGroup('row', [dockTabs([dockPane('a','A')]), dockTabs([dockPane('b','B')])]),
|
|
21
|
+
* floating: [], autoHide: [],
|
|
22
|
+
* })
|
|
23
|
+
* <SvDockManager bind:workspace>{#snippet pane(p)} ... {/snippet}</SvDockManager>
|
|
24
|
+
*/
|
|
25
|
+
import { onDestroy, type Snippet } from 'svelte'
|
|
26
|
+
import { fly } from 'svelte/transition'
|
|
27
|
+
import SvDockLayout from './SvDockLayout.svelte'
|
|
28
|
+
import type { DockPane, DockTabs, DockZone } from './dock-model'
|
|
29
|
+
import {
|
|
30
|
+
reorderTab,
|
|
31
|
+
setManagerActive,
|
|
32
|
+
resizeGroup,
|
|
33
|
+
closePane,
|
|
34
|
+
dockPaneOnto,
|
|
35
|
+
floatPane,
|
|
36
|
+
dockPaneToEmptyMain,
|
|
37
|
+
addPaneToMain,
|
|
38
|
+
dockWindowOnto,
|
|
39
|
+
moveWindow,
|
|
40
|
+
resizeWindow,
|
|
41
|
+
bringToFront,
|
|
42
|
+
setWindowMinimized,
|
|
43
|
+
toggleWindowMaximized,
|
|
44
|
+
closeWindow,
|
|
45
|
+
autoHideWindow,
|
|
46
|
+
autoHideLeaf,
|
|
47
|
+
autoHidePaneToSide,
|
|
48
|
+
pinAutoHidden,
|
|
49
|
+
setAutoHideSize,
|
|
50
|
+
toggleMaximizeLeaf,
|
|
51
|
+
findLeafById,
|
|
52
|
+
allManagerIds,
|
|
53
|
+
type DockManagerState,
|
|
54
|
+
type DockSide,
|
|
55
|
+
type AutoHideEntry,
|
|
56
|
+
} from './dock-manager-model'
|
|
57
|
+
import { openPopout, type Popout } from './dock-popout'
|
|
58
|
+
|
|
59
|
+
/** Imperative handle exposed via `onReady`. */
|
|
60
|
+
export type DockManagerApi = {
|
|
61
|
+
getState: () => DockManagerState
|
|
62
|
+
setState: (s: DockManagerState) => void
|
|
63
|
+
float: (paneId: string) => void
|
|
64
|
+
popout: (paneId: string) => void
|
|
65
|
+
autoHide: (paneId: string) => void
|
|
66
|
+
maximize: (tabsId: string) => void
|
|
67
|
+
close: (paneId: string) => void
|
|
68
|
+
focus: (paneId: string) => void
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Granular lifecycle events (in addition to `onChange`). */
|
|
72
|
+
export type DockEvent =
|
|
73
|
+
| { type: 'activePane'; paneId: string; tabsId: string }
|
|
74
|
+
| { type: 'close'; paneId: string }
|
|
75
|
+
| { type: 'float'; paneId: string }
|
|
76
|
+
| { type: 'popout'; paneId: string }
|
|
77
|
+
| { type: 'autoHide'; paneId: string; side: DockSide }
|
|
78
|
+
| { type: 'pin'; paneId: string }
|
|
79
|
+
| { type: 'maximize'; tabsId: string; maximized: boolean }
|
|
80
|
+
| { type: 'window'; windowId: string; action: 'minimize' | 'maximize' | 'close' | 'autoHide' }
|
|
81
|
+
| { type: 'focus'; paneId: string }
|
|
82
|
+
|
|
83
|
+
type Props = {
|
|
84
|
+
/** The whole workspace: tiled `main` + `floating` windows + `autoHide` edges. Bindable. */
|
|
85
|
+
workspace: DockManagerState
|
|
86
|
+
/** Renders each pane's content, keyed off the `DockPane`. */
|
|
87
|
+
pane: Snippet<[DockPane]>
|
|
88
|
+
/** Notified after any change. */
|
|
89
|
+
onChange?: (workspace: DockManagerState) => void
|
|
90
|
+
/** Granular lifecycle events (activePane / close / float / popout / ...). */
|
|
91
|
+
onEvent?: (event: DockEvent) => void
|
|
92
|
+
/** Minimum pane size in px along a split. Default 80. */
|
|
93
|
+
minSize?: number
|
|
94
|
+
/** Allow dragging tabs to reorder them within a strip. Default `true`. */
|
|
95
|
+
reorderEnabled?: boolean
|
|
96
|
+
/** Which side of each panel its tab strip sits on. Default `'top'`. */
|
|
97
|
+
headerPosition?: 'top' | 'bottom' | 'left' | 'right'
|
|
98
|
+
/** Keep inactive tabs mounted (hidden) so their content state persists.
|
|
99
|
+
* Default `false` (only the active tab renders). */
|
|
100
|
+
keepAlive?: boolean
|
|
101
|
+
/** Hide the tab button on a single-pane leaf (no redundant title bar; the panel
|
|
102
|
+
* actions still show). Applies to the docked area, floating windows, and fly-outs. */
|
|
103
|
+
hideSingleTab?: boolean
|
|
104
|
+
/** Offer the "pop out to a new browser window" panel action. Default `false` -
|
|
105
|
+
* most apps only need in-app floating windows, so the OS pop-out stays hidden
|
|
106
|
+
* unless you opt in. The `popout()` API still works regardless. */
|
|
107
|
+
allowPopout?: boolean
|
|
108
|
+
/** Locked / split mode: a fixed set of resizable panes. Tabs can't be dragged
|
|
109
|
+
* (no reorder / float / dock) and the panel action buttons are hidden - only
|
|
110
|
+
* splitter resize + tab switching remain. Turns the manager into a plain
|
|
111
|
+
* split-view workspace. Default `false`. */
|
|
112
|
+
locked?: boolean
|
|
113
|
+
/** Receive the imperative API once, on mount. */
|
|
114
|
+
onReady?: (api: DockManagerApi) => void
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let {
|
|
118
|
+
workspace = $bindable(),
|
|
119
|
+
pane,
|
|
120
|
+
onChange,
|
|
121
|
+
onEvent,
|
|
122
|
+
minSize = 80,
|
|
123
|
+
reorderEnabled = true,
|
|
124
|
+
headerPosition = 'top',
|
|
125
|
+
keepAlive = false,
|
|
126
|
+
hideSingleTab = false,
|
|
127
|
+
allowPopout = false,
|
|
128
|
+
locked = false,
|
|
129
|
+
onReady,
|
|
130
|
+
}: Props = $props()
|
|
131
|
+
const emit = (e: DockEvent) => onEvent?.(e)
|
|
132
|
+
|
|
133
|
+
let rootEl = $state<HTMLElement | null>(null)
|
|
134
|
+
// Runtime-only: OS window handles for popped-out panes (not serializable).
|
|
135
|
+
const popouts = new Map<string, Popout>()
|
|
136
|
+
let nextId = mgrBase()
|
|
137
|
+
// Never mint an id that already lives in the workspace. The base counter is
|
|
138
|
+
// module-scoped and resets on reload, so a workspace restored from storage can
|
|
139
|
+
// already hold the `dm-N` we're about to generate - skip past any collision so
|
|
140
|
+
// keyed lists never see a duplicate.
|
|
141
|
+
const genId = (): string => {
|
|
142
|
+
const used = new Set(allManagerIds(workspace))
|
|
143
|
+
let id = `dm-${nextId++}`
|
|
144
|
+
while (used.has(id)) id = `dm-${nextId++}`
|
|
145
|
+
return id
|
|
146
|
+
}
|
|
147
|
+
function commit(next: DockManagerState) { workspace = next; onChange?.(next) }
|
|
148
|
+
|
|
149
|
+
// Focus tracking (which pane the user last engaged) for the active-panel ring.
|
|
150
|
+
let focusedPaneId = $state<string | null>(null)
|
|
151
|
+
function setFocus(paneId: string | null) {
|
|
152
|
+
if (paneId === focusedPaneId) return
|
|
153
|
+
focusedPaneId = paneId
|
|
154
|
+
if (paneId) emit({ type: 'focus', paneId })
|
|
155
|
+
}
|
|
156
|
+
const focusedLeafId = $derived.by(() => {
|
|
157
|
+
if (!focusedPaneId) return null
|
|
158
|
+
const scan = (n: any): string | null => {
|
|
159
|
+
if (n.type === 'tabs') return n.panes.some((p: DockPane) => p.id === focusedPaneId) ? n.id : null
|
|
160
|
+
for (const c of n.children) { const id = scan(c); if (id) return id }
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
if (workspace.main) { const id = scan(workspace.main); if (id) return id }
|
|
164
|
+
const w = workspace.floating.find((f) => f.leaf.panes.some((p) => p.id === focusedPaneId))
|
|
165
|
+
return w ? w.leaf.id : null
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Controlled handlers each SvDockLayout routes back to the manager.
|
|
169
|
+
const onActivate = (tabsId: string, i: number) => {
|
|
170
|
+
const paneId = findLeafById(workspace, tabsId)?.panes[i]?.id
|
|
171
|
+
commit(setManagerActive(workspace, tabsId, i))
|
|
172
|
+
if (paneId) { setFocus(paneId); emit({ type: 'activePane', tabsId, paneId }) }
|
|
173
|
+
}
|
|
174
|
+
// Reveal an auto-hidden leaf's flyout on a specific pane (clicked from the
|
|
175
|
+
// edge rail). Toggles closed when the already-open pane is clicked again.
|
|
176
|
+
function revealAutoHidePane(e: AutoHideEntry, pi: number) {
|
|
177
|
+
if (revealed === e.id && e.leaf.active === pi) { revealed = null; return }
|
|
178
|
+
if (e.leaf.active !== pi) onActivate(e.leaf.id, pi)
|
|
179
|
+
revealed = e.id
|
|
180
|
+
}
|
|
181
|
+
const onClose = (paneId: string) => { commit(closePane(workspace, paneId)); emit({ type: 'close', paneId }) }
|
|
182
|
+
const onResize = (groupId: string, sizes: number[]) => commit(resizeGroup(workspace, groupId, sizes))
|
|
183
|
+
|
|
184
|
+
function paneTitle(paneId: string): string {
|
|
185
|
+
const inNode = (n: any): string | null => {
|
|
186
|
+
if (n.type === 'tabs') return n.panes.find((p: DockPane) => p.id === paneId)?.title ?? null
|
|
187
|
+
for (const c of n.children) { const t = inNode(c); if (t) return t }
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
if (workspace.main) { const t = inNode(workspace.main); if (t) return t }
|
|
191
|
+
for (const w of workspace.floating) { const p = w.leaf.panes.find((p) => p.id === paneId); if (p) return p.title }
|
|
192
|
+
for (const e of workspace.autoHide) { const p = e.leaf.panes.find((p) => p.id === paneId); if (p) return p.title }
|
|
193
|
+
return ''
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ------------------------------------------------------------------ tab drag
|
|
197
|
+
type Target =
|
|
198
|
+
| { kind: 'reorder'; tabsId: string; index: number }
|
|
199
|
+
| { kind: 'dock'; surface: string; tabsId: string; zone: DockZone }
|
|
200
|
+
| { kind: 'autohide'; side: DockSide }
|
|
201
|
+
| { kind: 'empty-main' }
|
|
202
|
+
| { kind: 'float' }
|
|
203
|
+
let ghost = $state<{ title: string; x: number; y: number } | null>(null)
|
|
204
|
+
let target = $state<Target | null>(null)
|
|
205
|
+
let src: { paneId: string; sourceTabsId: string; sx: number; sy: number } | null = null
|
|
206
|
+
let draggingWindow = $state<string | null>(null)
|
|
207
|
+
// The leaf under the pointer while dragging, for the dock guide. `zone` is the
|
|
208
|
+
// guide chip the pointer is on (drop docks there) or null (off the guide -> the
|
|
209
|
+
// drop floats). Shown whether or not a chip is hit so the user can aim.
|
|
210
|
+
let hoverLeaf = $state<{ tabsId: string; surface: string; zone: DockZone | null } | null>(null)
|
|
211
|
+
|
|
212
|
+
// Drop indicators handed to every surface's SvDockLayout (ids are unique, so
|
|
213
|
+
// only the matching leaf paints it).
|
|
214
|
+
const guideGlobal = $derived.by(() =>
|
|
215
|
+
hoverLeaf ? { tabsId: hoverLeaf.tabsId, zone: hoverLeaf.zone, centerOnly: hoverLeaf.surface !== 'main' } : null,
|
|
216
|
+
)
|
|
217
|
+
const reorderTargetGlobal = $derived.by(() =>
|
|
218
|
+
target && target.kind === 'reorder' ? { tabsId: target.tabsId, index: target.index } : null,
|
|
219
|
+
)
|
|
220
|
+
const autohideSide = $derived.by(() => (target && target.kind === 'autohide' ? target.side : null))
|
|
221
|
+
|
|
222
|
+
/** Which guide chip the pointer is over on a leaf, or null (-> float). The
|
|
223
|
+
* geometry mirrors the guide layout in DockNodeView (26px chips, 30px apart). */
|
|
224
|
+
function zoneFromGuide(r: DOMRect, x: number, y: number, edges: boolean): DockZone | null {
|
|
225
|
+
const dx = x - (r.left + r.right) / 2
|
|
226
|
+
const dy = y - (r.top + r.bottom) / 2
|
|
227
|
+
const H = 15, O = 30
|
|
228
|
+
const box = (bx: number, by: number) => Math.abs(dx - bx) <= H && Math.abs(dy - by) <= H
|
|
229
|
+
if (box(0, 0)) return 'center'
|
|
230
|
+
if (!edges) return null
|
|
231
|
+
if (box(-O, 0)) return 'left'
|
|
232
|
+
if (box(O, 0)) return 'right'
|
|
233
|
+
if (box(0, -O)) return 'top'
|
|
234
|
+
if (box(0, O)) return 'bottom'
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function beginTabDrag(e: PointerEvent, paneId: string, tabsId: string) {
|
|
239
|
+
setFocus(paneId)
|
|
240
|
+
src = { paneId, sourceTabsId: tabsId, sx: e.clientX, sy: e.clientY }
|
|
241
|
+
window.addEventListener('pointermove', onTabMove)
|
|
242
|
+
window.addEventListener('pointerup', onTabUp)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const verticalHeader = $derived(headerPosition === 'left' || headerPosition === 'right')
|
|
246
|
+
|
|
247
|
+
function insertIndex(strip: Element, x: number, y: number): number {
|
|
248
|
+
const tabsEls = Array.from(strip.querySelectorAll('[data-dock-tab]')) as HTMLElement[]
|
|
249
|
+
for (let i = 0; i < tabsEls.length; i++) {
|
|
250
|
+
const r = tabsEls[i]!.getBoundingClientRect()
|
|
251
|
+
const before = verticalHeader ? y < r.top + r.height / 2 : x < r.left + r.width / 2
|
|
252
|
+
if (before) return i
|
|
253
|
+
}
|
|
254
|
+
return tabsEls.length
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const EDGE_BAND = 22
|
|
258
|
+
|
|
259
|
+
/** Compute the drop action AND set `hoverLeaf` (which drives the dock guide). */
|
|
260
|
+
function computeTarget(x: number, y: number): Target {
|
|
261
|
+
hoverLeaf = null
|
|
262
|
+
const el = document.elementFromPoint(x, y) as HTMLElement | null
|
|
263
|
+
if (!el || !rootEl?.contains(el)) {
|
|
264
|
+
// Outside the manager entirely -> float.
|
|
265
|
+
return { kind: 'float' }
|
|
266
|
+
}
|
|
267
|
+
const leafEl = el.closest('[data-dock-tabs]') as HTMLElement | null
|
|
268
|
+
const strip = el.closest('.sv-dock__tabstrip')
|
|
269
|
+
// Reorder wins over everything when over the source leaf's own strip.
|
|
270
|
+
if (reorderEnabled && leafEl && strip && leafEl.dataset.dockTabs === src!.sourceTabsId) {
|
|
271
|
+
return { kind: 'reorder', tabsId: src!.sourceTabsId, index: insertIndex(strip, x, y) }
|
|
272
|
+
}
|
|
273
|
+
// Near the manager's outer border -> auto-hide to that side. Checked AFTER
|
|
274
|
+
// the source-strip reorder (above) so reordering still wins, but before the
|
|
275
|
+
// guide - so all four edges work, including the top where tab strips live.
|
|
276
|
+
{
|
|
277
|
+
const rr = rootEl.getBoundingClientRect()
|
|
278
|
+
const dl = x - rr.left, dr = rr.right - x, dt = y - rr.top, db = rr.bottom - y
|
|
279
|
+
const mn = Math.min(dl, dr, dt, db)
|
|
280
|
+
if (mn >= 0 && mn <= EDGE_BAND) {
|
|
281
|
+
return { kind: 'autohide', side: mn === dl ? 'left' : mn === dr ? 'right' : mn === dt ? 'top' : 'bottom' }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (leafEl) {
|
|
285
|
+
const surface = (leafEl.closest('[data-dock-surface]') as HTMLElement | null)?.dataset.dockSurface ?? 'main'
|
|
286
|
+
const edges = surface === 'main'
|
|
287
|
+
const zone = zoneFromGuide(leafEl.getBoundingClientRect(), x, y, edges)
|
|
288
|
+
// Always show the guide on the hovered leaf; only a chip hit docks.
|
|
289
|
+
hoverLeaf = { tabsId: leafEl.dataset.dockTabs!, surface, zone }
|
|
290
|
+
return zone
|
|
291
|
+
? { kind: 'dock', surface, tabsId: leafEl.dataset.dockTabs!, zone }
|
|
292
|
+
: { kind: 'float' } // over the leaf but off the guide -> float
|
|
293
|
+
}
|
|
294
|
+
if (el.closest('[data-dock-empty]')) return { kind: 'empty-main' }
|
|
295
|
+
return { kind: 'float' }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function onTabMove(e: PointerEvent) {
|
|
299
|
+
if (!src) return
|
|
300
|
+
if (!ghost) {
|
|
301
|
+
if (Math.hypot(e.clientX - src.sx, e.clientY - src.sy) < 5) return
|
|
302
|
+
ghost = { title: paneTitle(src.paneId), x: e.clientX, y: e.clientY }
|
|
303
|
+
document.body.style.userSelect = 'none'
|
|
304
|
+
document.body.style.cursor = 'grabbing'
|
|
305
|
+
}
|
|
306
|
+
ghost = { ...ghost, x: e.clientX, y: e.clientY }
|
|
307
|
+
target = computeTarget(e.clientX, e.clientY)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function onTabUp(e: PointerEvent) {
|
|
311
|
+
window.removeEventListener('pointermove', onTabMove)
|
|
312
|
+
window.removeEventListener('pointerup', onTabUp)
|
|
313
|
+
if (src && ghost && target) {
|
|
314
|
+
const { paneId } = src
|
|
315
|
+
if (target.kind === 'reorder') {
|
|
316
|
+
const from = tabIndexOf(target.tabsId, paneId)
|
|
317
|
+
if (from >= 0) commit(reorderTab(workspace, target.tabsId, from, target.index > from ? target.index - 1 : target.index))
|
|
318
|
+
} else if (target.kind === 'dock') {
|
|
319
|
+
commit(dockPaneOnto(workspace, paneId, target.tabsId, target.zone, genId))
|
|
320
|
+
} else if (target.kind === 'autohide') {
|
|
321
|
+
commit(autoHidePaneToSide(workspace, paneId, target.side, genId))
|
|
322
|
+
} else if (target.kind === 'empty-main') {
|
|
323
|
+
commit(dockPaneToEmptyMain(workspace, paneId, genId))
|
|
324
|
+
} else {
|
|
325
|
+
// Float at the drop point, sized from the manager rect.
|
|
326
|
+
const r = rootEl?.getBoundingClientRect()
|
|
327
|
+
const x = (e.clientX - (r?.left ?? 0)) - 40
|
|
328
|
+
const y = (e.clientY - (r?.top ?? 0)) - 12
|
|
329
|
+
commit(floatPane(workspace, paneId, { x: Math.max(0, x), y: Math.max(0, y), width: 340, height: 240 }, genId))
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
src = null; ghost = null; target = null; hoverLeaf = null
|
|
333
|
+
document.body.style.userSelect = ''
|
|
334
|
+
document.body.style.cursor = ''
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function tabIndexOf(tabsId: string, paneId: string): number {
|
|
338
|
+
const find = (n: any): number => {
|
|
339
|
+
if (n.type === 'tabs') return n.id === tabsId ? n.panes.findIndex((p: DockPane) => p.id === paneId) : -1
|
|
340
|
+
for (const c of n.children) { const i = find(c); if (i >= 0) return i }
|
|
341
|
+
return -1
|
|
342
|
+
}
|
|
343
|
+
if (workspace.main) { const i = find(workspace.main); if (i >= 0) return i }
|
|
344
|
+
const w = workspace.floating.find((w) => w.leaf.id === tabsId)
|
|
345
|
+
return w ? w.leaf.panes.findIndex((p) => p.id === paneId) : -1
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// -------------------------------------------------- floating window move/resize
|
|
349
|
+
function windowDragStart(e: PointerEvent, windowId: string) {
|
|
350
|
+
if (e.button !== 0) return
|
|
351
|
+
commit(bringToFront(workspace, windowId))
|
|
352
|
+
const w = workspace.floating.find((w) => w.id === windowId)
|
|
353
|
+
if (!w) return
|
|
354
|
+
const ox = e.clientX - w.x
|
|
355
|
+
const oy = e.clientY - w.y
|
|
356
|
+
// Make the dragged window transparent to hit-testing so elementFromPoint
|
|
357
|
+
// sees the main leaf underneath (for redock), not the window itself.
|
|
358
|
+
draggingWindow = windowId
|
|
359
|
+
let redock: { tabsId: string; zone: DockZone } | null = null
|
|
360
|
+
let autoHide: DockSide | null = null
|
|
361
|
+
const move = (ev: PointerEvent) => {
|
|
362
|
+
commit(moveWindow(workspace, windowId, ev.clientX - ox, ev.clientY - oy))
|
|
363
|
+
// Near the manager's outer border -> auto-hide to that edge, mirroring the
|
|
364
|
+
// tab-drag path (computeTarget). Checked BEFORE redock so an edge wins over
|
|
365
|
+
// a guide chip that sits near the same border; `target` drives the edge hint.
|
|
366
|
+
const rr = rootEl?.getBoundingClientRect()
|
|
367
|
+
if (rr) {
|
|
368
|
+
const dl = ev.clientX - rr.left, dr = rr.right - ev.clientX, dt = ev.clientY - rr.top, db = rr.bottom - ev.clientY
|
|
369
|
+
const mn = Math.min(dl, dr, dt, db)
|
|
370
|
+
if (mn >= 0 && mn <= EDGE_BAND) {
|
|
371
|
+
autoHide = mn === dl ? 'left' : mn === dr ? 'right' : mn === dt ? 'top' : 'bottom'
|
|
372
|
+
target = { kind: 'autohide', side: autoHide }
|
|
373
|
+
hoverLeaf = null
|
|
374
|
+
redock = null
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
autoHide = null
|
|
379
|
+
target = null
|
|
380
|
+
// Over a main leaf's guide chip -> arm a redock (else the window floats).
|
|
381
|
+
const el = document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement | null
|
|
382
|
+
const leafEl = el?.closest('[data-dock-tabs]') as HTMLElement | null
|
|
383
|
+
const surface = leafEl ? (leafEl.closest('[data-dock-surface]') as HTMLElement | null)?.dataset.dockSurface : undefined
|
|
384
|
+
if (leafEl && surface === 'main') {
|
|
385
|
+
const zone = zoneFromGuide(leafEl.getBoundingClientRect(), ev.clientX, ev.clientY, true)
|
|
386
|
+
hoverLeaf = { tabsId: leafEl.dataset.dockTabs!, surface: 'main', zone }
|
|
387
|
+
redock = zone ? { tabsId: leafEl.dataset.dockTabs!, zone } : null
|
|
388
|
+
} else {
|
|
389
|
+
hoverLeaf = null
|
|
390
|
+
redock = null
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const up = () => {
|
|
394
|
+
window.removeEventListener('pointermove', move)
|
|
395
|
+
window.removeEventListener('pointerup', up)
|
|
396
|
+
if (autoHide) {
|
|
397
|
+
const size = autoHide === 'left' || autoHide === 'right' ? w.width : w.height
|
|
398
|
+
commit(autoHideWindow(workspace, windowId, autoHide, genId, Math.round(size)))
|
|
399
|
+
} else if (redock) {
|
|
400
|
+
commit(dockWindowOnto(workspace, windowId, redock.tabsId, redock.zone, genId))
|
|
401
|
+
}
|
|
402
|
+
draggingWindow = null
|
|
403
|
+
target = null
|
|
404
|
+
hoverLeaf = null
|
|
405
|
+
document.body.style.userSelect = ''
|
|
406
|
+
}
|
|
407
|
+
document.body.style.userSelect = 'none'
|
|
408
|
+
window.addEventListener('pointermove', move)
|
|
409
|
+
window.addEventListener('pointerup', up)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function windowResizeStart(e: PointerEvent, windowId: string) {
|
|
413
|
+
if (e.button !== 0) return
|
|
414
|
+
e.stopPropagation()
|
|
415
|
+
const w = workspace.floating.find((w) => w.id === windowId)
|
|
416
|
+
if (!w) return
|
|
417
|
+
const sx = e.clientX, sy = e.clientY, sw = w.width, sh = w.height
|
|
418
|
+
const move = (ev: PointerEvent) => commit(resizeWindow(workspace, windowId, sw + (ev.clientX - sx), sh + (ev.clientY - sy)))
|
|
419
|
+
const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); document.body.style.userSelect = '' }
|
|
420
|
+
document.body.style.userSelect = 'none'
|
|
421
|
+
window.addEventListener('pointermove', move)
|
|
422
|
+
window.addEventListener('pointerup', up)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ------------------------------------------------------------- auto-hide fly-out
|
|
426
|
+
let revealed = $state<string | null>(null) // entry id currently flown out
|
|
427
|
+
const flyoutStyle = (e: { side: DockSide; size: number }) =>
|
|
428
|
+
(e.side === 'left' || e.side === 'right' ? 'width' : 'height') + `: ${e.size}px`
|
|
429
|
+
|
|
430
|
+
// Dismiss an open fly-out on an outside click or Escape (click a strip button
|
|
431
|
+
// to toggle; the fly-out stays put while you interact inside it).
|
|
432
|
+
$effect(() => {
|
|
433
|
+
if (!revealed) return
|
|
434
|
+
const onDown = (ev: PointerEvent) => {
|
|
435
|
+
const el = ev.target as HTMLElement | null
|
|
436
|
+
if (el?.closest('.sv-dockmgr__flyout') || el?.closest('.sv-dockmgr__strip')) return
|
|
437
|
+
revealed = null
|
|
438
|
+
}
|
|
439
|
+
const onKey = (ev: KeyboardEvent) => { if (ev.key === 'Escape') revealed = null }
|
|
440
|
+
window.addEventListener('pointerdown', onDown, true)
|
|
441
|
+
window.addEventListener('keydown', onKey)
|
|
442
|
+
return () => {
|
|
443
|
+
window.removeEventListener('pointerdown', onDown, true)
|
|
444
|
+
window.removeEventListener('keydown', onKey)
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
// Inset the tiled area so edge strips sit beside content, not over it. Must
|
|
449
|
+
// match the fixed strip thickness in CSS (.sv-dockmgr__strip--*).
|
|
450
|
+
const STRIP = 34
|
|
451
|
+
const mainPad = $derived({
|
|
452
|
+
left: workspace.autoHide.some((e) => e.side === 'left') ? STRIP : 0,
|
|
453
|
+
right: workspace.autoHide.some((e) => e.side === 'right') ? STRIP : 0,
|
|
454
|
+
top: workspace.autoHide.some((e) => e.side === 'top') ? STRIP : 0,
|
|
455
|
+
bottom: workspace.autoHide.some((e) => e.side === 'bottom') ? STRIP : 0,
|
|
456
|
+
})
|
|
457
|
+
// The maximized tiled leaf (shown alone), or null. Guarded so a stale id noops.
|
|
458
|
+
const maxLeaf = $derived(workspace.maximizedLeaf ? findLeafById(workspace, workspace.maximizedLeaf) : null)
|
|
459
|
+
|
|
460
|
+
// -------------------------------------------------------- tab-action helpers
|
|
461
|
+
function floatOne(paneId: string) {
|
|
462
|
+
const r = rootEl?.getBoundingClientRect()
|
|
463
|
+
commit(floatPane(workspace, paneId, { x: (r ? r.width : 400) * 0.3, y: 60, width: 340, height: 240 }, genId))
|
|
464
|
+
emit({ type: 'float', paneId })
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function findPaneObject(paneId: string): DockPane | null {
|
|
468
|
+
const scan = (n: any): DockPane | null => {
|
|
469
|
+
if (n.type === 'tabs') return n.panes.find((p: DockPane) => p.id === paneId) ?? null
|
|
470
|
+
for (const c of n.children) { const hit = scan(c); if (hit) return hit }
|
|
471
|
+
return null
|
|
472
|
+
}
|
|
473
|
+
if (workspace.main) { const m = scan(workspace.main); if (m) return m }
|
|
474
|
+
for (const w of workspace.floating) { const p = w.leaf.panes.find((x) => x.id === paneId); if (p) return p }
|
|
475
|
+
for (const e of workspace.autoHide) { const p = e.leaf.panes.find((x) => x.id === paneId); if (p) return p }
|
|
476
|
+
return null
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Pop a pane out into a REAL browser window. Falls back to a floating window
|
|
480
|
+
* if the browser blocks the pop-up. */
|
|
481
|
+
function popoutPane(paneId: string) {
|
|
482
|
+
if (popouts.has(paneId) || !rootEl) return
|
|
483
|
+
const paneObj = findPaneObject(paneId)
|
|
484
|
+
if (!paneObj) return
|
|
485
|
+
const removed = closePane(workspace, paneId) // pull it out of the layout
|
|
486
|
+
const handle = openPopout({
|
|
487
|
+
pane: paneObj,
|
|
488
|
+
paneSnippet: pane,
|
|
489
|
+
themeSource: rootEl,
|
|
490
|
+
onClose: (id) => popInPane(id),
|
|
491
|
+
})
|
|
492
|
+
if (!handle) { floatOne(paneId); return } // blocked -> float instead
|
|
493
|
+
popouts.set(paneId, handle)
|
|
494
|
+
commit(removed)
|
|
495
|
+
emit({ type: 'popout', paneId })
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Dock a popped-out pane back into the layout (window closed / API call). */
|
|
499
|
+
function popInPane(paneId: string) {
|
|
500
|
+
const handle = popouts.get(paneId)
|
|
501
|
+
if (!handle) return
|
|
502
|
+
popouts.delete(paneId)
|
|
503
|
+
handle.destroy()
|
|
504
|
+
commit(addPaneToMain(workspace, handle.pane, genId))
|
|
505
|
+
}
|
|
506
|
+
function leafIdOfPane(paneId: string): string | null {
|
|
507
|
+
const findLeaf = (n: any): string | null => {
|
|
508
|
+
if (n.type === 'tabs') return n.panes.some((p: DockPane) => p.id === paneId) ? n.id : null
|
|
509
|
+
for (const c of n.children) { const id = findLeaf(c); if (id) return id }
|
|
510
|
+
return null
|
|
511
|
+
}
|
|
512
|
+
return workspace.main ? findLeaf(workspace.main) : null
|
|
513
|
+
}
|
|
514
|
+
const mainRect = () => (rootEl?.querySelector('.sv-dockmgr__main') as HTMLElement | null)?.getBoundingClientRect()
|
|
515
|
+
const leafRect = (tabsId: string) =>
|
|
516
|
+
(rootEl?.querySelector(`[data-dock-tabs="${CSS.escape(tabsId)}"]`) as HTMLElement | null)?.getBoundingClientRect()
|
|
517
|
+
|
|
518
|
+
/** Which edge a tiled leaf is nearest to - so the auto-hide button sends it to
|
|
519
|
+
* the side it already lives on. */
|
|
520
|
+
function nearestSide(tabsId: string): DockSide {
|
|
521
|
+
const lr = leafRect(tabsId), mr = mainRect()
|
|
522
|
+
if (!lr || !mr || mr.width <= 0 || mr.height <= 0) return 'left'
|
|
523
|
+
const cx = (lr.left + lr.right) / 2, cy = (lr.top + lr.bottom) / 2
|
|
524
|
+
const dl = (cx - mr.left) / mr.width, dr = (mr.right - cx) / mr.width
|
|
525
|
+
const dt = (cy - mr.top) / mr.height, db = (mr.bottom - cy) / mr.height
|
|
526
|
+
const m = Math.min(dl, dr, dt, db)
|
|
527
|
+
return m === dl ? 'left' : m === dr ? 'right' : m === dt ? 'top' : 'bottom'
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function autoHidePane(paneId: string) {
|
|
531
|
+
const tabsId = leafIdOfPane(paneId)
|
|
532
|
+
if (!tabsId) return
|
|
533
|
+
const side = nearestSide(tabsId)
|
|
534
|
+
const lr = leafRect(tabsId)
|
|
535
|
+
const size = lr ? Math.round(side === 'left' || side === 'right' ? lr.width : lr.height) : 260
|
|
536
|
+
commit(autoHideLeaf(workspace, tabsId, side, size))
|
|
537
|
+
emit({ type: 'autoHide', paneId, side })
|
|
538
|
+
}
|
|
539
|
+
function pinPane(paneId: string) {
|
|
540
|
+
const entry = workspace.autoHide.find((e) => e.leaf.panes.some((p) => p.id === paneId))
|
|
541
|
+
if (!entry) return
|
|
542
|
+
const mr = mainRect()
|
|
543
|
+
const dim = mr ? (entry.side === 'left' || entry.side === 'right' ? mr.width : mr.height) : 1000
|
|
544
|
+
const frac = Math.min(0.5, Math.max(0.12, entry.size / (dim || 1000)))
|
|
545
|
+
commit(pinAutoHidden(workspace, entry.id, genId, frac))
|
|
546
|
+
emit({ type: 'pin', paneId })
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Auto-hide a floating window to the manager edge nearest its position. */
|
|
550
|
+
function autoHideWin(windowId: string) {
|
|
551
|
+
const w = workspace.floating.find((f) => f.id === windowId)
|
|
552
|
+
const mr = mainRect()
|
|
553
|
+
if (!w || !mr) return
|
|
554
|
+
const cx = w.x + w.width / 2, cy = w.y + w.height / 2
|
|
555
|
+
const dl = cx, dr = mr.width - cx, dt = cy, db = mr.height - cy
|
|
556
|
+
const m = Math.min(dl, dr, dt, db)
|
|
557
|
+
const side: DockSide = m === dl ? 'left' : m === dr ? 'right' : m === dt ? 'top' : 'bottom'
|
|
558
|
+
const size = side === 'left' || side === 'right' ? w.width : w.height
|
|
559
|
+
commit(autoHideWindow(workspace, windowId, side, genId, Math.round(size)))
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Slide-in params for a fly-out, by which edge it lives on.
|
|
563
|
+
const flyIn = (side: DockSide) => ({
|
|
564
|
+
duration: 160,
|
|
565
|
+
x: side === 'left' ? -20 : side === 'right' ? 20 : 0,
|
|
566
|
+
y: side === 'top' ? -20 : side === 'bottom' ? 20 : 0,
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
// Drag the fly-out's inner edge to resize it; size is stored on the entry.
|
|
570
|
+
function flyoutResizeStart(e: PointerEvent, entry: AutoHideEntry) {
|
|
571
|
+
if (e.button !== 0) return
|
|
572
|
+
e.preventDefault(); e.stopPropagation()
|
|
573
|
+
const start = entry.size
|
|
574
|
+
const sx = e.clientX, sy = e.clientY
|
|
575
|
+
const move = (ev: PointerEvent) => {
|
|
576
|
+
const d =
|
|
577
|
+
entry.side === 'left' ? ev.clientX - sx
|
|
578
|
+
: entry.side === 'right' ? sx - ev.clientX
|
|
579
|
+
: entry.side === 'top' ? ev.clientY - sy
|
|
580
|
+
: sy - ev.clientY
|
|
581
|
+
commit(setAutoHideSize(workspace, entry.id, start + d))
|
|
582
|
+
}
|
|
583
|
+
const up = () => {
|
|
584
|
+
window.removeEventListener('pointermove', move)
|
|
585
|
+
window.removeEventListener('pointerup', up)
|
|
586
|
+
document.body.style.userSelect = ''
|
|
587
|
+
}
|
|
588
|
+
document.body.style.userSelect = 'none'
|
|
589
|
+
window.addEventListener('pointermove', move)
|
|
590
|
+
window.addEventListener('pointerup', up)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Imperative API, handed to the consumer once.
|
|
594
|
+
onReady?.({
|
|
595
|
+
getState: () => workspace,
|
|
596
|
+
setState: (s) => commit(s),
|
|
597
|
+
float: (id) => floatOne(id),
|
|
598
|
+
popout: (id) => popoutPane(id),
|
|
599
|
+
autoHide: (id) => autoHidePane(id),
|
|
600
|
+
maximize: (tabsId) => { if (findLeafById(workspace, tabsId)) commit(toggleMaximizeLeaf(workspace, tabsId)) },
|
|
601
|
+
close: (id) => commit(closePane(workspace, id)),
|
|
602
|
+
focus: (id) => setFocus(id),
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
// Close every pop-out window when the manager itself is torn down.
|
|
606
|
+
onDestroy(() => { for (const h of popouts.values()) h.destroy(); popouts.clear() })
|
|
607
|
+
</script>
|
|
608
|
+
|
|
609
|
+
<div class="sv-dockmgr" bind:this={rootEl}>
|
|
610
|
+
<!-- Auto-hide edge strips. Vertical strips inset past the horizontal ones so
|
|
611
|
+
the corners belong to top/bottom and nothing overlaps. -->
|
|
612
|
+
{#each (['left', 'right', 'top', 'bottom'] as DockSide[]) as side (side)}
|
|
613
|
+
{@const entries = workspace.autoHide.filter((e) => e.side === side)}
|
|
614
|
+
{#if entries.length}
|
|
615
|
+
<div
|
|
616
|
+
class="sv-dockmgr__strip sv-dockmgr__strip--{side}"
|
|
617
|
+
style:top={side === 'left' || side === 'right' ? `${mainPad.top}px` : undefined}
|
|
618
|
+
style:bottom={side === 'left' || side === 'right' ? `${mainPad.bottom}px` : undefined}
|
|
619
|
+
>
|
|
620
|
+
{#each entries as e (e.id)}
|
|
621
|
+
<!-- Auto-hide applies to the whole leaf, so surface one edge tab per
|
|
622
|
+
pane (not one per entry). Clicking a pane's tab reveals the flyout
|
|
623
|
+
with that pane active. -->
|
|
624
|
+
{#each e.leaf.panes as p, pi (p.id)}
|
|
625
|
+
<button
|
|
626
|
+
type="button"
|
|
627
|
+
class="sv-dockmgr__stab"
|
|
628
|
+
class:is-open={revealed === e.id && e.leaf.active === pi}
|
|
629
|
+
title={p.title}
|
|
630
|
+
onclick={() => revealAutoHidePane(e, pi)}
|
|
631
|
+
>{p.title}</button>
|
|
632
|
+
{/each}
|
|
633
|
+
{/each}
|
|
634
|
+
</div>
|
|
635
|
+
{/if}
|
|
636
|
+
{/each}
|
|
637
|
+
|
|
638
|
+
<!-- Docked (tiled) area. When a leaf is maximized, only that leaf is shown. -->
|
|
639
|
+
<div
|
|
640
|
+
class="sv-dockmgr__main"
|
|
641
|
+
style:padding={`${mainPad.top}px ${mainPad.right}px ${mainPad.bottom}px ${mainPad.left}px`}
|
|
642
|
+
>
|
|
643
|
+
{#if maxLeaf}
|
|
644
|
+
<SvDockLayout
|
|
645
|
+
layout={maxLeaf}
|
|
646
|
+
{pane}
|
|
647
|
+
{minSize}
|
|
648
|
+
{headerPosition}
|
|
649
|
+
focusedLeaf={focusedLeafId}
|
|
650
|
+
{keepAlive}
|
|
651
|
+
{hideSingleTab}
|
|
652
|
+
{locked}
|
|
653
|
+
surface="main"
|
|
654
|
+
onBeginDrag={beginTabDrag}
|
|
655
|
+
externalDrop={guideGlobal}
|
|
656
|
+
externalReorder={reorderTargetGlobal}
|
|
657
|
+
{onActivate}
|
|
658
|
+
{onClose}
|
|
659
|
+
{onResize}
|
|
660
|
+
leafActions={mainLeafActions}
|
|
661
|
+
/>
|
|
662
|
+
{:else if workspace.main}
|
|
663
|
+
<SvDockLayout
|
|
664
|
+
layout={workspace.main}
|
|
665
|
+
{pane}
|
|
666
|
+
{minSize}
|
|
667
|
+
{headerPosition}
|
|
668
|
+
focusedLeaf={focusedLeafId}
|
|
669
|
+
{keepAlive}
|
|
670
|
+
{hideSingleTab}
|
|
671
|
+
{locked}
|
|
672
|
+
surface="main"
|
|
673
|
+
onBeginDrag={beginTabDrag}
|
|
674
|
+
externalDrop={guideGlobal}
|
|
675
|
+
externalReorder={reorderTargetGlobal}
|
|
676
|
+
{onActivate}
|
|
677
|
+
{onClose}
|
|
678
|
+
{onResize}
|
|
679
|
+
leafActions={mainLeafActions}
|
|
680
|
+
/>
|
|
681
|
+
{:else}
|
|
682
|
+
<div class="sv-dockmgr__empty" data-dock-empty>Drag a panel here</div>
|
|
683
|
+
{/if}
|
|
684
|
+
</div>
|
|
685
|
+
|
|
686
|
+
<!-- Auto-hide fly-out -->
|
|
687
|
+
{#each workspace.autoHide as e (e.id)}
|
|
688
|
+
{#if revealed === e.id}
|
|
689
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
690
|
+
<div
|
|
691
|
+
class="sv-dockmgr__flyout sv-dockmgr__flyout--{e.side}"
|
|
692
|
+
style={flyoutStyle(e)}
|
|
693
|
+
transition:fly={flyIn(e.side)}
|
|
694
|
+
>
|
|
695
|
+
<SvDockLayout
|
|
696
|
+
layout={e.leaf}
|
|
697
|
+
{pane}
|
|
698
|
+
{minSize}
|
|
699
|
+
{headerPosition}
|
|
700
|
+
focusedLeaf={focusedLeafId}
|
|
701
|
+
{keepAlive}
|
|
702
|
+
hideSingleTab={true}
|
|
703
|
+
surface={e.id}
|
|
704
|
+
onBeginDrag={beginTabDrag}
|
|
705
|
+
externalDrop={guideGlobal}
|
|
706
|
+
externalReorder={reorderTargetGlobal}
|
|
707
|
+
{onActivate}
|
|
708
|
+
{onClose}
|
|
709
|
+
{onResize}
|
|
710
|
+
leafActions={flyoutLeafActions}
|
|
711
|
+
/>
|
|
712
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
713
|
+
<div
|
|
714
|
+
class="sv-dockmgr__flyresize sv-dockmgr__flyresize--{e.side}"
|
|
715
|
+
onpointerdown={(ev) => flyoutResizeStart(ev, e)}
|
|
716
|
+
></div>
|
|
717
|
+
</div>
|
|
718
|
+
{/if}
|
|
719
|
+
{/each}
|
|
720
|
+
|
|
721
|
+
<!-- Floating windows -->
|
|
722
|
+
{#each workspace.floating as w (w.id)}
|
|
723
|
+
<div
|
|
724
|
+
class="sv-dockmgr__window"
|
|
725
|
+
class:is-minimized={w.minimized}
|
|
726
|
+
class:is-maximized={w.maximized}
|
|
727
|
+
style:left={w.maximized ? undefined : `${w.x}px`}
|
|
728
|
+
style:top={w.maximized ? undefined : `${w.y}px`}
|
|
729
|
+
style:width={w.maximized ? undefined : `${w.width}px`}
|
|
730
|
+
style:height={w.maximized || w.minimized ? undefined : `${w.height}px`}
|
|
731
|
+
style:z-index={100 + w.z}
|
|
732
|
+
style:pointer-events={draggingWindow === w.id ? 'none' : undefined}
|
|
733
|
+
onpointerdown={() => commit(bringToFront(workspace, w.id))}
|
|
734
|
+
>
|
|
735
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
736
|
+
<div
|
|
737
|
+
class="sv-dockmgr__winbar"
|
|
738
|
+
ondblclick={() => commit(toggleWindowMaximized(workspace, w.id))}
|
|
739
|
+
onpointerdown={(e) => { if (!w.maximized) windowDragStart(e, w.id) }}
|
|
740
|
+
>
|
|
741
|
+
<span class="sv-dockmgr__wintitle">{w.leaf.panes[w.leaf.active]?.title ?? 'Window'}</span>
|
|
742
|
+
<div class="sv-dockmgr__winctl">
|
|
743
|
+
<button type="button" title="Auto-hide" aria-label="Auto-hide window"
|
|
744
|
+
onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); autoHideWin(w.id); emit({ type: 'window', windowId: w.id, action: 'autoHide' }) }}>▿</button>
|
|
745
|
+
<button type="button" title={w.minimized ? 'Restore' : 'Minimize'} aria-label="Minimize window"
|
|
746
|
+
onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); commit(setWindowMinimized(workspace, w.id, !w.minimized)); emit({ type: 'window', windowId: w.id, action: 'minimize' }) }}>–</button>
|
|
747
|
+
<button type="button" title={w.maximized ? 'Restore' : 'Maximize'} aria-label="Maximize window"
|
|
748
|
+
onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); commit(toggleWindowMaximized(workspace, w.id)); emit({ type: 'window', windowId: w.id, action: 'maximize' }) }}>{w.maximized ? '❐' : '□'}</button>
|
|
749
|
+
<button type="button" class="sv-dockmgr__winclose" title="Close" aria-label="Close window"
|
|
750
|
+
onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); commit(closeWindow(workspace, w.id)); emit({ type: 'window', windowId: w.id, action: 'close' }) }}>×</button>
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
{#if !w.minimized}
|
|
754
|
+
<div class="sv-dockmgr__winbody">
|
|
755
|
+
<SvDockLayout
|
|
756
|
+
layout={w.leaf}
|
|
757
|
+
{pane}
|
|
758
|
+
{minSize}
|
|
759
|
+
{headerPosition}
|
|
760
|
+
focusedLeaf={focusedLeafId}
|
|
761
|
+
{keepAlive}
|
|
762
|
+
{hideSingleTab}
|
|
763
|
+
surface={w.id}
|
|
764
|
+
onBeginDrag={beginTabDrag}
|
|
765
|
+
externalDrop={guideGlobal}
|
|
766
|
+
externalReorder={reorderTargetGlobal}
|
|
767
|
+
{onActivate}
|
|
768
|
+
{onClose}
|
|
769
|
+
{onResize}
|
|
770
|
+
/>
|
|
771
|
+
</div>
|
|
772
|
+
{#if !w.maximized}
|
|
773
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
774
|
+
<div class="sv-dockmgr__winresize" onpointerdown={(e) => windowResizeStart(e, w.id)}></div>
|
|
775
|
+
{/if}
|
|
776
|
+
{/if}
|
|
777
|
+
</div>
|
|
778
|
+
{/each}
|
|
779
|
+
|
|
780
|
+
<!-- Auto-hide edge drop indicator (drag a tab to the manager border). -->
|
|
781
|
+
{#if autohideSide}
|
|
782
|
+
<div class="sv-dockmgr__edgehint sv-dockmgr__edgehint--{autohideSide}" aria-hidden="true"></div>
|
|
783
|
+
{/if}
|
|
784
|
+
|
|
785
|
+
{#if ghost}
|
|
786
|
+
<div class="sv-dockmgr__ghost" style:left={`${ghost.x + 12}px`} style:top={`${ghost.y + 12}px`}>{ghost.title}</div>
|
|
787
|
+
{/if}
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
{#snippet mainLeafActions(leaf: DockTabs)}
|
|
791
|
+
{@const active = leaf.panes[leaf.active] ?? leaf.panes[0]}
|
|
792
|
+
<button type="button" class="sv-dockmgr__pact" title={workspace.maximizedLeaf === leaf.id ? 'Restore' : 'Maximize'} aria-label="Maximize panel"
|
|
793
|
+
onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); const on = workspace.maximizedLeaf !== leaf.id; commit(toggleMaximizeLeaf(workspace, leaf.id)); emit({ type: 'maximize', tabsId: leaf.id, maximized: on }) }}>{workspace.maximizedLeaf === leaf.id ? '❐' : '⤢'}</button>
|
|
794
|
+
<button type="button" class="sv-dockmgr__pact" title="Auto-hide" aria-label="Auto-hide panel"
|
|
795
|
+
onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); if (active) autoHidePane(active.id) }}>▿</button>
|
|
796
|
+
{#if active}
|
|
797
|
+
<button type="button" class="sv-dockmgr__pact" title="Float" aria-label="Float {active.title}"
|
|
798
|
+
onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); floatOne(active.id) }}>▢</button>
|
|
799
|
+
{#if allowPopout}
|
|
800
|
+
<button type="button" class="sv-dockmgr__pact" title="Pop out to a new window" aria-label="Pop out {active.title}"
|
|
801
|
+
onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); popoutPane(active.id) }}>⧉</button>
|
|
802
|
+
{/if}
|
|
803
|
+
{/if}
|
|
804
|
+
{/snippet}
|
|
805
|
+
|
|
806
|
+
{#snippet flyoutLeafActions(leaf: DockTabs)}
|
|
807
|
+
{@const active = leaf.panes[leaf.active] ?? leaf.panes[0]}
|
|
808
|
+
{#if active}
|
|
809
|
+
<button type="button" class="sv-dockmgr__pact" title="Pin" aria-label="Pin {active.title}"
|
|
810
|
+
onpointerdown={(e) => e.stopPropagation()} onclick={(e) => { e.stopPropagation(); pinPane(active.id) }}>📌</button>
|
|
811
|
+
{/if}
|
|
812
|
+
{/snippet}
|
|
813
|
+
|
|
814
|
+
<script lang="ts" module>
|
|
815
|
+
let MGR = 0
|
|
816
|
+
function mgrBase(): number { MGR += 1; return MGR * 1_000_000 }
|
|
817
|
+
</script>
|
|
818
|
+
|
|
819
|
+
<style>
|
|
820
|
+
.sv-dockmgr { position: relative; width: 100%; height: 100%; min-width: 0; min-height: 0; display: flex; }
|
|
821
|
+
.sv-dockmgr__main { position: relative; flex: 1; min-width: 0; min-height: 0; display: flex; }
|
|
822
|
+
.sv-dockmgr__main > :global(*) { flex: 1; min-width: 0; min-height: 0; }
|
|
823
|
+
.sv-dockmgr__empty {
|
|
824
|
+
flex: 1; display: flex; align-items: center; justify-content: center;
|
|
825
|
+
color: var(--sg-muted, #94a3b8); font-size: 13px; border: 2px dashed var(--sg-border, #e2e8f0);
|
|
826
|
+
border-radius: 10px; margin: 8px;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/* Auto-hide edge strips. Fixed 34px thickness == the main-area inset, so a
|
|
830
|
+
strip sits BESIDE the tiled content instead of overlapping it. */
|
|
831
|
+
.sv-dockmgr__strip {
|
|
832
|
+
position: absolute; z-index: 40; display: flex; gap: 4px; padding: 4px;
|
|
833
|
+
background: var(--sg-header-bg, #f6f7f9); box-sizing: border-box;
|
|
834
|
+
}
|
|
835
|
+
.sv-dockmgr__strip--left { left: 0; top: 0; bottom: 0; width: 34px; flex-direction: column; align-items: center; border-right: 1px solid var(--sg-border, #e2e8f0); }
|
|
836
|
+
.sv-dockmgr__strip--right { right: 0; top: 0; bottom: 0; width: 34px; flex-direction: column; align-items: center; border-left: 1px solid var(--sg-border, #e2e8f0); }
|
|
837
|
+
.sv-dockmgr__strip--top { top: 0; left: 0; right: 0; height: 34px; align-items: center; border-bottom: 1px solid var(--sg-border, #e2e8f0); }
|
|
838
|
+
.sv-dockmgr__strip--bottom { bottom: 0; left: 0; right: 0; height: 34px; align-items: center; border-top: 1px solid var(--sg-border, #e2e8f0); }
|
|
839
|
+
.sv-dockmgr__stab {
|
|
840
|
+
font: inherit; font-size: 12px; font-weight: 600; padding: 3px 8px; cursor: pointer;
|
|
841
|
+
color: var(--sg-muted, #64748b); background: var(--sg-bg, #fff);
|
|
842
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 6px; white-space: nowrap;
|
|
843
|
+
display: inline-flex; align-items: center; max-width: 100%;
|
|
844
|
+
}
|
|
845
|
+
.sv-dockmgr__strip--left .sv-dockmgr__stab, .sv-dockmgr__strip--right .sv-dockmgr__stab { writing-mode: vertical-rl; padding: 8px 3px; max-height: 60%; overflow: hidden; }
|
|
846
|
+
.sv-dockmgr__strip--top .sv-dockmgr__stab, .sv-dockmgr__strip--bottom .sv-dockmgr__stab { max-width: 40%; overflow: hidden; text-overflow: ellipsis; }
|
|
847
|
+
.sv-dockmgr__stab:hover, .sv-dockmgr__stab.is-open { color: var(--sg-accent, #2563eb); border-color: var(--sg-accent, #2563eb); }
|
|
848
|
+
|
|
849
|
+
/* Auto-hide fly-out. Capped so revealing never covers the whole workspace. */
|
|
850
|
+
.sv-dockmgr__flyout {
|
|
851
|
+
position: absolute; z-index: 60; display: flex;
|
|
852
|
+
background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
|
|
853
|
+
border-radius: 8px; box-shadow: 0 12px 40px -8px rgba(15,23,42,0.45); overflow: hidden;
|
|
854
|
+
}
|
|
855
|
+
.sv-dockmgr__flyout > :global(*) { flex: 1; }
|
|
856
|
+
.sv-dockmgr__flyout--left { left: 35px; top: 4px; bottom: 4px; max-width: 62%; }
|
|
857
|
+
.sv-dockmgr__flyout--right { right: 35px; top: 4px; bottom: 4px; max-width: 62%; }
|
|
858
|
+
.sv-dockmgr__flyout--top { top: 35px; left: 4px; right: 4px; max-height: 62%; }
|
|
859
|
+
.sv-dockmgr__flyout--bottom { bottom: 35px; left: 4px; right: 4px; max-height: 62%; }
|
|
860
|
+
|
|
861
|
+
/* Fly-out resize handle on the panel's inner edge. */
|
|
862
|
+
.sv-dockmgr__flyresize { position: absolute; z-index: 2; touch-action: none; }
|
|
863
|
+
.sv-dockmgr__flyresize--left { top: 0; bottom: 0; right: -3px; width: 7px; cursor: ew-resize; }
|
|
864
|
+
.sv-dockmgr__flyresize--right { top: 0; bottom: 0; left: -3px; width: 7px; cursor: ew-resize; }
|
|
865
|
+
.sv-dockmgr__flyresize--top { left: 0; right: 0; bottom: -3px; height: 7px; cursor: ns-resize; }
|
|
866
|
+
.sv-dockmgr__flyresize--bottom { left: 0; right: 0; top: -3px; height: 7px; cursor: ns-resize; }
|
|
867
|
+
.sv-dockmgr__flyresize:hover { background: color-mix(in srgb, var(--sg-accent, #2563eb) 40%, transparent); }
|
|
868
|
+
|
|
869
|
+
/* Floating windows */
|
|
870
|
+
.sv-dockmgr__window {
|
|
871
|
+
position: absolute; display: flex; flex-direction: column; min-width: 140px; min-height: 90px;
|
|
872
|
+
background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
873
|
+
box-shadow: 0 18px 50px -12px rgba(15,23,42,0.5); overflow: hidden;
|
|
874
|
+
}
|
|
875
|
+
.sv-dockmgr__window.is-maximized { inset: 4px; width: auto; height: auto; border-radius: 8px; }
|
|
876
|
+
.sv-dockmgr__window.is-minimized { min-height: 0; height: auto; width: 220px; }
|
|
877
|
+
.sv-dockmgr__winbar {
|
|
878
|
+
flex: none; height: 28px; display: flex; align-items: center; gap: 8px; padding: 0 4px 0 10px; cursor: grab;
|
|
879
|
+
background: var(--sg-header-bg, #f6f7f9); border-bottom: 1px solid var(--sg-border, #e2e8f0);
|
|
880
|
+
user-select: none; touch-action: none;
|
|
881
|
+
}
|
|
882
|
+
.sv-dockmgr__window.is-maximized .sv-dockmgr__winbar { cursor: default; }
|
|
883
|
+
.sv-dockmgr__wintitle { flex: 1; min-width: 0; font-size: 12px; font-weight: 700; color: var(--sg-fg, #0f172a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
884
|
+
.sv-dockmgr__winctl { flex: none; display: flex; align-items: center; gap: 1px; }
|
|
885
|
+
.sv-dockmgr__winctl button {
|
|
886
|
+
display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 20px;
|
|
887
|
+
padding: 0; font-size: 13px; line-height: 1; color: var(--sg-muted, #64748b); cursor: pointer;
|
|
888
|
+
background: none; border: none; border-radius: 5px;
|
|
889
|
+
}
|
|
890
|
+
.sv-dockmgr__winctl button:hover { background: color-mix(in srgb, var(--sg-fg, #0f172a) 8%, transparent); color: var(--sg-fg, #0f172a); }
|
|
891
|
+
.sv-dockmgr__winclose:hover { background: var(--sg-danger, #dc2626) !important; color: #fff !important; }
|
|
892
|
+
.sv-dockmgr__winbody { flex: 1; min-height: 0; display: flex; }
|
|
893
|
+
.sv-dockmgr__winbody > :global(*) { flex: 1; min-width: 0; min-height: 0; }
|
|
894
|
+
.sv-dockmgr__winresize {
|
|
895
|
+
position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; touch-action: none;
|
|
896
|
+
background: linear-gradient(135deg, transparent 50%, var(--sg-border, #cbd5e1) 50%);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.sv-dockmgr__pact {
|
|
900
|
+
display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px;
|
|
901
|
+
padding: 0; font-size: 11px; line-height: 1; color: var(--sg-muted, #94a3b8); cursor: pointer;
|
|
902
|
+
background: none; border: none; border-radius: 4px;
|
|
903
|
+
}
|
|
904
|
+
.sv-dockmgr__pact:hover { background: var(--sg-row-hover-bg, #f1f5f9); color: var(--sg-accent, #2563eb); }
|
|
905
|
+
|
|
906
|
+
/* Auto-hide edge drop indicator: a bar along the manager border. */
|
|
907
|
+
.sv-dockmgr__edgehint {
|
|
908
|
+
position: absolute; z-index: 70; pointer-events: none;
|
|
909
|
+
background: color-mix(in srgb, var(--sg-accent, #2563eb) 30%, transparent);
|
|
910
|
+
border: 2px solid var(--sg-accent, #2563eb); border-radius: 6px;
|
|
911
|
+
}
|
|
912
|
+
.sv-dockmgr__edgehint--left { left: 2px; top: 2px; bottom: 2px; width: 46px; }
|
|
913
|
+
.sv-dockmgr__edgehint--right { right: 2px; top: 2px; bottom: 2px; width: 46px; }
|
|
914
|
+
.sv-dockmgr__edgehint--top { top: 2px; left: 2px; right: 2px; height: 40px; }
|
|
915
|
+
.sv-dockmgr__edgehint--bottom { bottom: 2px; left: 2px; right: 2px; height: 40px; }
|
|
916
|
+
|
|
917
|
+
.sv-dockmgr__ghost {
|
|
918
|
+
position: fixed; z-index: 2147483646; pointer-events: none; padding: 5px 10px; font-size: 12.5px;
|
|
919
|
+
font-weight: 600; color: var(--sg-on-accent, #fff); background: var(--sg-accent, #2563eb);
|
|
920
|
+
border-radius: 6px; box-shadow: 0 8px 24px -6px rgba(15,23,42,0.5);
|
|
921
|
+
}
|
|
922
|
+
</style>
|