@lijian-ui/dsh-file-manager 0.1.20
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/LICENSE +38 -0
- package/README.i18n.yaml +6 -0
- package/README.md +66 -0
- package/README.zh.md +66 -0
- package/cordis.patch.yml +13 -0
- package/lib/assets/mermaid.min.js +3587 -0
- package/lib/client.js +7621 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +3207 -0
- package/lib/tsconfig.client.tsbuildinfo +1 -0
- package/lib/tsconfig.host.tsbuildinfo +1 -0
- package/lib/types/client/FileManagerSettingsCard.d.ts +56 -0
- package/lib/types/client/FileManagerSettingsCard.d.ts.map +1 -0
- package/lib/types/client/PluginSettingsCard.d.ts +104 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
- package/lib/types/client/api.d.ts +75 -0
- package/lib/types/client/api.d.ts.map +1 -0
- package/lib/types/client/chat/AttachedFilesDock.d.ts +51 -0
- package/lib/types/client/chat/AttachedFilesDock.d.ts.map +1 -0
- package/lib/types/client/chat/file-ref.d.ts +35 -0
- package/lib/types/client/chat/file-ref.d.ts.map +1 -0
- package/lib/types/client/chat/mermaid-chat.d.ts +30 -0
- package/lib/types/client/chat/mermaid-chat.d.ts.map +1 -0
- package/lib/types/client/chat/placeholder-hint.d.ts +16 -0
- package/lib/types/client/chat/placeholder-hint.d.ts.map +1 -0
- package/lib/types/client/components/ExplorerPanel.d.ts +26 -0
- package/lib/types/client/components/ExplorerPanel.d.ts.map +1 -0
- package/lib/types/client/components/FileIcon.d.ts +15 -0
- package/lib/types/client/components/FileIcon.d.ts.map +1 -0
- package/lib/types/client/components/ScmPanel.d.ts +23 -0
- package/lib/types/client/components/ScmPanel.d.ts.map +1 -0
- package/lib/types/client/components/a11y.d.ts +12 -0
- package/lib/types/client/components/a11y.d.ts.map +1 -0
- package/lib/types/client/components/icons.d.ts +41 -0
- package/lib/types/client/components/icons.d.ts.map +1 -0
- package/lib/types/client/components/overlay.d.ts +45 -0
- package/lib/types/client/components/overlay.d.ts.map +1 -0
- package/lib/types/client/drag/DragFileInlay.d.ts +31 -0
- package/lib/types/client/drag/DragFileInlay.d.ts.map +1 -0
- package/lib/types/client/drag/file-drag.d.ts +47 -0
- package/lib/types/client/drag/file-drag.d.ts.map +1 -0
- package/lib/types/client/drag.d.ts +31 -0
- package/lib/types/client/drag.d.ts.map +1 -0
- package/lib/types/client/file-source.d.ts +29 -0
- package/lib/types/client/file-source.d.ts.map +1 -0
- package/lib/types/client/fileType.d.ts +31 -0
- package/lib/types/client/fileType.d.ts.map +1 -0
- package/lib/types/client/floating.d.ts +27 -0
- package/lib/types/client/floating.d.ts.map +1 -0
- package/lib/types/client/hooks/useResizableSplit.d.ts +37 -0
- package/lib/types/client/hooks/useResizableSplit.d.ts.map +1 -0
- package/lib/types/client/hooks/useStore.d.ts +10 -0
- package/lib/types/client/hooks/useStore.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +57 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/layout.d.ts +104 -0
- package/lib/types/client/layout.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +125 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/maximize.d.ts +24 -0
- package/lib/types/client/maximize.d.ts.map +1 -0
- package/lib/types/client/mention.d.ts +44 -0
- package/lib/types/client/mention.d.ts.map +1 -0
- package/lib/types/client/mount.d.ts +22 -0
- package/lib/types/client/mount.d.ts.map +1 -0
- package/lib/types/client/persist.d.ts +55 -0
- package/lib/types/client/persist.d.ts.map +1 -0
- package/lib/types/client/picker/FilePickerModal.d.ts +47 -0
- package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -0
- package/lib/types/client/picker/file-picker.d.ts +62 -0
- package/lib/types/client/picker/file-picker.d.ts.map +1 -0
- package/lib/types/client/preview/PreviewPanel.d.ts +17 -0
- package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -0
- package/lib/types/client/preview/PreviewTabs.d.ts +30 -0
- package/lib/types/client/preview/PreviewTabs.d.ts.map +1 -0
- package/lib/types/client/preview/PreviewToolbar.d.ts +37 -0
- package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -0
- package/lib/types/client/preview/content.d.ts +43 -0
- package/lib/types/client/preview/content.d.ts.map +1 -0
- package/lib/types/client/preview/markdown.d.ts +60 -0
- package/lib/types/client/preview/markdown.d.ts.map +1 -0
- package/lib/types/client/preview/mermaid.d.ts +78 -0
- package/lib/types/client/preview/mermaid.d.ts.map +1 -0
- package/lib/types/client/preview/selection.d.ts +26 -0
- package/lib/types/client/preview/selection.d.ts.map +1 -0
- package/lib/types/client/reference.d.ts +75 -0
- package/lib/types/client/reference.d.ts.map +1 -0
- package/lib/types/client/settings-form.d.ts +206 -0
- package/lib/types/client/settings-form.d.ts.map +1 -0
- package/lib/types/client/store.d.ts +279 -0
- package/lib/types/client/store.d.ts.map +1 -0
- package/lib/types/core/types.d.ts +132 -0
- package/lib/types/core/types.d.ts.map +1 -0
- package/lib/types/host/fs-service.d.ts +96 -0
- package/lib/types/host/fs-service.d.ts.map +1 -0
- package/lib/types/host/gate.d.ts +45 -0
- package/lib/types/host/gate.d.ts.map +1 -0
- package/lib/types/host/git-runner.d.ts +80 -0
- package/lib/types/host/git-runner.d.ts.map +1 -0
- package/lib/types/host/git-service.d.ts +130 -0
- package/lib/types/host/git-service.d.ts.map +1 -0
- package/lib/types/host/loopback.d.ts +25 -0
- package/lib/types/host/loopback.d.ts.map +1 -0
- package/lib/types/host/poll-guard.d.ts +55 -0
- package/lib/types/host/poll-guard.d.ts.map +1 -0
- package/lib/types/host/routes.d.ts +54 -0
- package/lib/types/host/routes.d.ts.map +1 -0
- package/lib/types/index.d.ts +35 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/mount-once.d.ts +25 -0
- package/lib/types/mount-once.d.ts.map +1 -0
- package/package.json +85 -0
- package/src/client/FileManagerSettingsCard.tsx +117 -0
- package/src/client/PluginSettingsCard.tsx +337 -0
- package/src/client/api.ts +141 -0
- package/src/client/chat/file-ref.ts +144 -0
- package/src/client/chat/mermaid-chat.tsx +102 -0
- package/src/client/chat/placeholder-hint.tsx +46 -0
- package/src/client/components/ExplorerPanel.tsx +534 -0
- package/src/client/components/FileIcon.tsx +46 -0
- package/src/client/components/ScmPanel.tsx +480 -0
- package/src/client/components/a11y.ts +20 -0
- package/src/client/components/icons.tsx +274 -0
- package/src/client/components/overlay.tsx +225 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/drag/DragFileInlay.tsx +89 -0
- package/src/client/drag/file-drag.ts +67 -0
- package/src/client/drag.ts +154 -0
- package/src/client/file-source.ts +58 -0
- package/src/client/fileType.ts +129 -0
- package/src/client/floating.ts +63 -0
- package/src/client/hooks/useResizableSplit.ts +91 -0
- package/src/client/hooks/useStore.ts +15 -0
- package/src/client/index.ts +389 -0
- package/src/client/layout.ts +546 -0
- package/src/client/locales.ts +247 -0
- package/src/client/maximize.ts +34 -0
- package/src/client/mount.tsx +74 -0
- package/src/client/persist.ts +194 -0
- package/src/client/picker/FilePickerModal.tsx +506 -0
- package/src/client/picker/file-picker.ts +86 -0
- package/src/client/preview/PreviewPanel.tsx +233 -0
- package/src/client/preview/PreviewTabs.tsx +159 -0
- package/src/client/preview/PreviewToolbar.tsx +160 -0
- package/src/client/preview/content.tsx +617 -0
- package/src/client/preview/markdown.ts +351 -0
- package/src/client/preview/mermaid.ts +274 -0
- package/src/client/preview/selection.ts +82 -0
- package/src/client/reference.ts +161 -0
- package/src/client/settings-card.module.css +316 -0
- package/src/client/settings-form.ts +451 -0
- package/src/client/store.ts +1290 -0
- package/src/client/styles/chip.module.css +38 -0
- package/src/client/styles/drag.module.css +30 -0
- package/src/client/styles/explorer.module.css +371 -0
- package/src/client/styles/picker.module.css +344 -0
- package/src/client/styles/preview.module.css +904 -0
- package/src/client/styles/scm.module.css +380 -0
- package/src/client/styles/tokens.module.css +480 -0
- package/src/core/types.ts +162 -0
- package/src/host/fs-service.ts +513 -0
- package/src/host/gate.ts +74 -0
- package/src/host/git-runner.ts +116 -0
- package/src/host/git-service.ts +395 -0
- package/src/host/loopback.ts +63 -0
- package/src/host/poll-guard.ts +109 -0
- package/src/host/routes.ts +695 -0
- package/src/index.ts +97 -0
- package/src/mount-once.ts +48 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DOM layout controller: extends the web shell's three-column frame
|
|
3
|
+
* (`[data-dsh-frame]`, a grid) with two trailing grid tracks — the preview
|
|
4
|
+
* region and the explorer column — by mirroring the shell's own inline
|
|
5
|
+
* grid-template-columns string and re-appending the two panel tracks on every
|
|
6
|
+
* shell update (MutationObserver, same frame before paint). Also owns the
|
|
7
|
+
* absolute drag handles (12px explorer / 20px preview hit zones), the
|
|
8
|
+
* floating expand button (docked at the top-right corner, just below the
|
|
9
|
+
* shell header's divider — issues #374 / #292), the collapse-as-width-0
|
|
10
|
+
* keep-mounted behavior, and the transient
|
|
11
|
+
* maximize mode (issue #315): while a panel is maximized the target column
|
|
12
|
+
* takes over the whole frame row (or renders as a fixed full-screen overlay
|
|
13
|
+
* on narrow viewports), and Esc / the header button restore the layout.
|
|
14
|
+
*
|
|
15
|
+
* The shell's inline style is the source of truth for the sidebar and details
|
|
16
|
+
* tracks; this controller never guesses their widths. Handles are out-of-flow
|
|
17
|
+
* (absolute), so appending tracks never disturbs the shell's own children.
|
|
18
|
+
*
|
|
19
|
+
* FileManager Layout architecture (Apache-2.0, re-implemented): the explorer
|
|
20
|
+
* column collapses to width 0 while staying mounted; the preview region keeps
|
|
21
|
+
* a 1px left border only (no outer margins — gaps would expose the window
|
|
22
|
+
* background, jarring in dark mode).
|
|
23
|
+
* @module dsh-filemgr/client/layout
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { handlePointerDragStart } from './drag.ts'
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_PREVIEW_REGION_PX, DEFAULT_WORKSPACE_PANEL_PX,
|
|
29
|
+
MAX_PREVIEW_REGION_PX, MAX_WORKSPACE_PANEL_PX,
|
|
30
|
+
MIN_PREVIEW_PANEL_PX, MIN_WORKSPACE_PANEL_PX,
|
|
31
|
+
KEY_EXPLORER_WIDTH, KEY_PREVIEW_WIDTH,
|
|
32
|
+
clampExplorerWidth, clampPreviewWidth,
|
|
33
|
+
type MaximizeTarget,
|
|
34
|
+
} from './store.ts'
|
|
35
|
+
import { writeStoredNumber } from './persist.ts'
|
|
36
|
+
import { maximizedGridTracks, maximizedOverlay } from './maximize.ts'
|
|
37
|
+
import {
|
|
38
|
+
FLOATING_BUTTON_HEIGHT_PX, FLOATING_HEADER_GAP_PX,
|
|
39
|
+
clampFloatingTop, titlebarAreaHeight, topAlignedFloatingTop,
|
|
40
|
+
} from './floating.ts'
|
|
41
|
+
import type { LayoutStore } from './store.ts'
|
|
42
|
+
|
|
43
|
+
/** The frame grid element (portals target it). */
|
|
44
|
+
let frameElement: HTMLElement | null = null
|
|
45
|
+
|
|
46
|
+
/** Read the current frame element (undefined while the shell is not mounted). */
|
|
47
|
+
export function getFrameElement(): HTMLElement | null {
|
|
48
|
+
return frameElement
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Locate the frame grid element the two panel columns append into. The web-ui
|
|
53
|
+
* aggregate's compat shim stamps `data-dsh-frame` onto the grid, but a
|
|
54
|
+
* STANDALONE install of this package has no shim (the attribute never
|
|
55
|
+
* appears), so the panel would wait forever and never mount (issue #56). Fall
|
|
56
|
+
* back to the rc.6-native structure: the frame grid is the parent of the
|
|
57
|
+
* sidebar column, exactly the element the shim would stamp.
|
|
58
|
+
*/
|
|
59
|
+
function findFrame(): HTMLElement | null {
|
|
60
|
+
const stamped = document.querySelector<HTMLElement>('[data-dsh-frame]')
|
|
61
|
+
if (stamped !== null) return stamped
|
|
62
|
+
return document.querySelector<HTMLElement>('[class*="sidebarCol"]')?.parentElement ?? null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse an inline grid-template-columns string into its tracks. Handles
|
|
67
|
+
* "minmax(0, 1fr)" (spaces inside parens must not split). Empty on failure.
|
|
68
|
+
*/
|
|
69
|
+
export function parseGridTracks(input: string): string[] {
|
|
70
|
+
const tracks: string[] = []
|
|
71
|
+
let depth = 0
|
|
72
|
+
let current = ''
|
|
73
|
+
for (const char of input) {
|
|
74
|
+
if (char === '(') depth += 1
|
|
75
|
+
if (char === ')') depth = Math.max(0, depth - 1)
|
|
76
|
+
if (char === ' ' && depth === 0) {
|
|
77
|
+
if (current !== '') {
|
|
78
|
+
tracks.push(current)
|
|
79
|
+
current = ''
|
|
80
|
+
}
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
current += char
|
|
84
|
+
}
|
|
85
|
+
if (current !== '') tracks.push(current)
|
|
86
|
+
return tracks
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Extract a px width from one track (0 for fr/minmax/non-px tracks). */
|
|
90
|
+
export function trackPx(track: string): number {
|
|
91
|
+
const match = /^(-?[\d.]+)px$/.exec(track.trim())
|
|
92
|
+
return match === null ? 0 : Number(match[1])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One drag handle's geometry (hit zone + visual line) — pure CSS in the module. */
|
|
96
|
+
export const EXPLORER_HANDLE_WIDTH = 12
|
|
97
|
+
export const PREVIEW_HANDLE_WIDTH = 20
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Drag target width: apply the hard px bounds (the same min/max the handle
|
|
101
|
+
* always enforced), then the store's ordered container-aware clamp so the
|
|
102
|
+
* grid never re-clamps a width the drag showed.
|
|
103
|
+
*/
|
|
104
|
+
export function dragTargetWidth(
|
|
105
|
+
kind: 'explorer' | 'preview',
|
|
106
|
+
startWidth: number,
|
|
107
|
+
deltaX: number,
|
|
108
|
+
snapshot: { availableWidth: number; previewOpen: boolean; explorerWidth: number },
|
|
109
|
+
): number {
|
|
110
|
+
const requested = startWidth + deltaX
|
|
111
|
+
if (kind === 'explorer') {
|
|
112
|
+
const bounded = Math.min(MAX_WORKSPACE_PANEL_PX, Math.max(MIN_WORKSPACE_PANEL_PX, requested))
|
|
113
|
+
return clampExplorerWidth(bounded, snapshot.availableWidth, snapshot.previewOpen)
|
|
114
|
+
}
|
|
115
|
+
const bounded = Math.min(MAX_PREVIEW_REGION_PX, Math.max(MIN_PREVIEW_PANEL_PX, requested))
|
|
116
|
+
return clampPreviewWidth(bounded, snapshot.availableWidth, snapshot.explorerWidth)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The layout controller: frame sync, handles, floating button, width math. */
|
|
120
|
+
export class PanelLayoutController {
|
|
121
|
+
private frame: HTMLElement | null = null
|
|
122
|
+
private previewCol: HTMLDivElement | null = null
|
|
123
|
+
private explorerCol: HTMLDivElement | null = null
|
|
124
|
+
private explorerHandle: HTMLDivElement | null = null
|
|
125
|
+
private previewHandle: HTMLDivElement | null = null
|
|
126
|
+
private floatingButton: HTMLButtonElement | null = null
|
|
127
|
+
private styleObserver: MutationObserver | null = null
|
|
128
|
+
private sizeObserver: ResizeObserver | null = null
|
|
129
|
+
private waitObserver: MutationObserver | null = null
|
|
130
|
+
private frameWidth = 0
|
|
131
|
+
/** Cached shell details handle (re-resolved when the shell rebuilds it). */
|
|
132
|
+
private detailsHandle: HTMLElement | null = null
|
|
133
|
+
/** The shell's own 3 tracks (sidebar, center, details) — mirror of its inline style. */
|
|
134
|
+
private shellTracks: string[] = []
|
|
135
|
+
private instantTimer: ReturnType<typeof setTimeout> | undefined
|
|
136
|
+
private disposers: Array<() => void> = []
|
|
137
|
+
constructor(private readonly layout: LayoutStore) {}
|
|
138
|
+
|
|
139
|
+
/** Start watching for the frame and attach once it appears. */
|
|
140
|
+
mount(): void {
|
|
141
|
+
const tryAttach = (): void => {
|
|
142
|
+
if (this.frame !== null) return
|
|
143
|
+
const frame = findFrame()
|
|
144
|
+
if (frame === null) return
|
|
145
|
+
this.attach(frame)
|
|
146
|
+
}
|
|
147
|
+
this.waitObserver = new MutationObserver(() => { tryAttach() })
|
|
148
|
+
this.waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
149
|
+
tryAttach()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Attach to the frame: columns, handles, observers, store subscription. */
|
|
153
|
+
private attach(frame: HTMLElement): void {
|
|
154
|
+
this.frame = frame
|
|
155
|
+
frameElement = frame
|
|
156
|
+
// The wait observer's only job was finding the frame; a document-wide
|
|
157
|
+
// MutationObserver left running would fire on every chat render for the
|
|
158
|
+
// rest of the session.
|
|
159
|
+
this.waitObserver?.disconnect()
|
|
160
|
+
this.waitObserver = null
|
|
161
|
+
this.detailsHandle = null
|
|
162
|
+
|
|
163
|
+
// The two panel columns: trailing grid items (tracks 4 and 5).
|
|
164
|
+
const previewCol = document.createElement('div')
|
|
165
|
+
previewCol.dataset.filemgrPreviewCol = ''
|
|
166
|
+
previewCol.className = 'filemgr-preview-col'
|
|
167
|
+
previewCol.style.minWidth = '0'
|
|
168
|
+
previewCol.style.overflow = 'hidden'
|
|
169
|
+
previewCol.style.display = 'flex'
|
|
170
|
+
previewCol.style.flexDirection = 'column'
|
|
171
|
+
previewCol.style.borderLeft = '1px solid var(--aion-bg-3, #e5e6eb)'
|
|
172
|
+
|
|
173
|
+
const explorerCol = document.createElement('div')
|
|
174
|
+
explorerCol.dataset.filemgrExplorerCol = ''
|
|
175
|
+
explorerCol.className = 'filemgr-explorer-col'
|
|
176
|
+
explorerCol.style.minWidth = '0'
|
|
177
|
+
explorerCol.style.overflow = 'hidden'
|
|
178
|
+
explorerCol.style.display = 'flex'
|
|
179
|
+
explorerCol.style.flexDirection = 'column'
|
|
180
|
+
explorerCol.style.borderLeft = '1px solid var(--aion-bg-3, #e5e6eb)'
|
|
181
|
+
|
|
182
|
+
frame.appendChild(previewCol)
|
|
183
|
+
frame.appendChild(explorerCol)
|
|
184
|
+
this.previewCol = previewCol
|
|
185
|
+
this.explorerCol = explorerCol
|
|
186
|
+
|
|
187
|
+
// The absolute drag handles (out of the grid flow). Both sit on the left
|
|
188
|
+
// edge of their panel: dragging left widens (reverse).
|
|
189
|
+
this.explorerHandle = this.createHandle('filemgr-explorer-handle', EXPLORER_HANDLE_WIDTH, true, 'explorer')
|
|
190
|
+
this.previewHandle = this.createHandle('filemgr-preview-handle', PREVIEW_HANDLE_WIDTH, true, 'preview')
|
|
191
|
+
frame.appendChild(this.explorerHandle)
|
|
192
|
+
frame.appendChild(this.previewHandle)
|
|
193
|
+
|
|
194
|
+
// The floating expand button (fixed, top-right corner) — DOM-level,
|
|
195
|
+
// no React. Docked exactly where the explorer's collapse chevron sits,
|
|
196
|
+
// so collapsing and re-expanding toggle in place (issue #374 follow-up);
|
|
197
|
+
// a click toggles the explorer.
|
|
198
|
+
this.floatingButton = document.createElement('button')
|
|
199
|
+
this.floatingButton.type = 'button'
|
|
200
|
+
this.floatingButton.className = 'filemgr-floating-expand'
|
|
201
|
+
this.floatingButton.setAttribute('aria-label', 'Expand explorer')
|
|
202
|
+
this.floatingButton.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>'
|
|
203
|
+
this.floatingButton.addEventListener('click', () => { this.toggleExplorer() })
|
|
204
|
+
document.body.appendChild(this.floatingButton)
|
|
205
|
+
|
|
206
|
+
// Window Controls Overlay (dsh-desktop, issue #292): re-position when
|
|
207
|
+
// the titlebar area changes (button must stay below the window buttons).
|
|
208
|
+
const overlay = (navigator as Navigator & { windowControlsOverlay?: EventTarget }).windowControlsOverlay
|
|
209
|
+
if (overlay !== undefined) {
|
|
210
|
+
const onGeometryChange = (): void => { this.positionFloatingButton() }
|
|
211
|
+
overlay.addEventListener('geometrychange', onGeometryChange)
|
|
212
|
+
this.disposers.push(() => overlay.removeEventListener('geometrychange', onGeometryChange))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Esc restores a maximized panel (issue #315). Editing surfaces own Esc:
|
|
216
|
+
// while an input/textarea/contenteditable is focused, leave it alone.
|
|
217
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
218
|
+
if (event.key !== 'Escape') return
|
|
219
|
+
const target = event.target instanceof Element ? event.target : null
|
|
220
|
+
if (target !== null && target.closest('input, textarea, [contenteditable="true"]') !== null) return
|
|
221
|
+
this.layout.update((prev) => (prev.maximized === null ? prev : { ...prev, maximized: null }))
|
|
222
|
+
}
|
|
223
|
+
window.addEventListener('keydown', onKeyDown)
|
|
224
|
+
this.disposers.push(() => window.removeEventListener('keydown', onKeyDown))
|
|
225
|
+
|
|
226
|
+
// Sync the shell's inline grid: any shell write re-appends our tracks.
|
|
227
|
+
const syncGrid = (): void => {
|
|
228
|
+
const el = this.frame
|
|
229
|
+
if (el === null) return
|
|
230
|
+
const inline = el.style.gridTemplateColumns
|
|
231
|
+
if (inline === '') return
|
|
232
|
+
const tracks = parseGridTracks(inline)
|
|
233
|
+
if (tracks.length >= 2 && tracks.length <= 3) {
|
|
234
|
+
// The shell's own write (3 tracks) — remember it and re-append ours.
|
|
235
|
+
this.shellTracks = tracks
|
|
236
|
+
this.applyGrid()
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
if (tracks.length === 5 && this.shellTracks.length === 3) {
|
|
240
|
+
// Our own write — keep it (the shell tracks are already mirrored).
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
this.styleObserver = new MutationObserver(syncGrid)
|
|
245
|
+
this.styleObserver.observe(frame, { attributes: true, attributeFilter: ['style'] })
|
|
246
|
+
|
|
247
|
+
// Measure the row width: frame minus the shell's sidebar + details tracks.
|
|
248
|
+
const measure = (): void => {
|
|
249
|
+
if (this.frame === null) return
|
|
250
|
+
this.frameWidth = this.frame.getBoundingClientRect().width
|
|
251
|
+
const sidebar = this.shellTracks.length >= 1 ? trackPx(this.shellTracks[0]) : 0
|
|
252
|
+
const details = this.shellTracks.length >= 3 ? trackPx(this.shellTracks[2]) : 0
|
|
253
|
+
const available = Math.max(0, this.frameWidth - sidebar - details)
|
|
254
|
+
const state = this.layout.getSnapshot()
|
|
255
|
+
if (Math.abs(state.availableWidth - available) > 0.5) {
|
|
256
|
+
this.layout.update((prev) => ({ ...prev, availableWidth: available }))
|
|
257
|
+
}
|
|
258
|
+
this.layout.shrinkToFit(this.layout.getSnapshot())
|
|
259
|
+
}
|
|
260
|
+
this.sizeObserver = new ResizeObserver(() => {
|
|
261
|
+
measure()
|
|
262
|
+
this.applyGrid()
|
|
263
|
+
})
|
|
264
|
+
this.sizeObserver.observe(frame)
|
|
265
|
+
|
|
266
|
+
// Store -> DOM: grid, handles, floating button.
|
|
267
|
+
this.disposers.push(this.layout.subscribe(() => this.applyGrid()))
|
|
268
|
+
|
|
269
|
+
// Initial sync: read the shell's inline style (it is already applied).
|
|
270
|
+
const initial = frame.style.gridTemplateColumns
|
|
271
|
+
if (initial !== '') {
|
|
272
|
+
const tracks = parseGridTracks(initial)
|
|
273
|
+
if (tracks.length >= 2 && tracks.length <= 3) {
|
|
274
|
+
this.shellTracks = tracks
|
|
275
|
+
} else if (tracks.length === 5 && trackPx(tracks[0]) > 0) {
|
|
276
|
+
// The frame may already carry our own previous 5-track write: the
|
|
277
|
+
// plugin hot-reloads in place (client HMR re-materializes the fiber),
|
|
278
|
+
// and the shell's grid is then the first three tracks. A zero-width
|
|
279
|
+
// first track is never a shell write (the shell collapses to the 56px
|
|
280
|
+
// rail, drags clamp at 280px) — treat it as untrusted and wait for
|
|
281
|
+
// the next shell write instead of mirroring the damage.
|
|
282
|
+
this.shellTracks = tracks.slice(0, 3)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
measure()
|
|
286
|
+
this.applyGrid()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Create one drag handle element with its pointer wiring. */
|
|
290
|
+
private createHandle(
|
|
291
|
+
className: string,
|
|
292
|
+
hitWidth: number,
|
|
293
|
+
reverse: boolean,
|
|
294
|
+
kind: 'explorer' | 'preview',
|
|
295
|
+
): HTMLDivElement {
|
|
296
|
+
const el = document.createElement('div')
|
|
297
|
+
el.className = className
|
|
298
|
+
el.style.position = 'absolute'
|
|
299
|
+
el.style.top = '0'
|
|
300
|
+
el.style.bottom = '0'
|
|
301
|
+
// Same layer as the columns (z 30): the handle strips overlap the
|
|
302
|
+
// column tracks, so anything lower would be painted under the opaque
|
|
303
|
+
// columns and stop receiving pointer events (issue #234 follow-up).
|
|
304
|
+
// Full-screen overlay drawers must render at the ROOT stacking context
|
|
305
|
+
// (z 100~1000) to cover both the columns and the handles — see the
|
|
306
|
+
// columns' stacking-contract note in tokens.module.css.
|
|
307
|
+
el.style.zIndex = '30'
|
|
308
|
+
el.style.cursor = 'col-resize'
|
|
309
|
+
el.style.width = `${hitWidth}px`
|
|
310
|
+
if (reverse) {
|
|
311
|
+
// The preview handle extends LEFT of the preview region's left edge.
|
|
312
|
+
el.style.marginLeft = `-${hitWidth}px`
|
|
313
|
+
}
|
|
314
|
+
el.addEventListener('pointerdown', (event: PointerEvent) => {
|
|
315
|
+
const isExplorer = kind === 'explorer'
|
|
316
|
+
handlePointerDragStart(event, el, {
|
|
317
|
+
reverse,
|
|
318
|
+
getStartWidth: () => {
|
|
319
|
+
const state = this.layout.getSnapshot()
|
|
320
|
+
return isExplorer ? state.explorerWidth : state.previewWidth
|
|
321
|
+
},
|
|
322
|
+
compute: (startWidth, deltaX) => dragTargetWidth(
|
|
323
|
+
isExplorer ? 'explorer' : 'preview',
|
|
324
|
+
startWidth,
|
|
325
|
+
deltaX,
|
|
326
|
+
this.layout.getSnapshot(),
|
|
327
|
+
),
|
|
328
|
+
onFrame: (width) => {
|
|
329
|
+
// layout.update notifies the subscribers (subscribe -> applyGrid),
|
|
330
|
+
// so no explicit applyGrid here — double-writing every frame.
|
|
331
|
+
this.layout.update((prev) => (
|
|
332
|
+
isExplorer ? { ...prev, explorerWidth: width } : { ...prev, previewWidth: width }
|
|
333
|
+
))
|
|
334
|
+
},
|
|
335
|
+
onEnd: (width) => {
|
|
336
|
+
writeStoredNumber(isExplorer ? KEY_EXPLORER_WIDTH : KEY_PREVIEW_WIDTH, width)
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
// Double-click resets THIS panel to its default width (instant, like
|
|
341
|
+
// every panel width change — the shell's grid transition must not lag).
|
|
342
|
+
el.addEventListener('dblclick', () => {
|
|
343
|
+
this.instant(() => {
|
|
344
|
+
const width = kind === 'explorer' ? DEFAULT_WORKSPACE_PANEL_PX : DEFAULT_PREVIEW_REGION_PX
|
|
345
|
+
this.layout.update((prev) => (
|
|
346
|
+
kind === 'explorer' ? { ...prev, explorerWidth: width } : { ...prev, previewWidth: width }
|
|
347
|
+
))
|
|
348
|
+
writeStoredNumber(kind === 'explorer' ? KEY_EXPLORER_WIDTH : KEY_PREVIEW_WIDTH, width)
|
|
349
|
+
this.applyGrid()
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
// The visual line is drawn by CSS (::after) — the hit zone is transparent.
|
|
353
|
+
return el
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Toggle explorer collapse (width 0, kept mounted; no transition). */
|
|
357
|
+
toggleExplorer(): void {
|
|
358
|
+
const state = this.layout.getSnapshot()
|
|
359
|
+
const next = !state.explorerCollapsed
|
|
360
|
+
this.instant(() => {
|
|
361
|
+
this.layout.update((prev) => ({ ...prev, explorerCollapsed: next }))
|
|
362
|
+
try {
|
|
363
|
+
localStorage.setItem(`project-panel-collapse:${state.root}`, next ? 'collapsed' : 'expanded')
|
|
364
|
+
} catch {
|
|
365
|
+
// best-effort
|
|
366
|
+
}
|
|
367
|
+
this.applyGrid()
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Toggle the preview region (open = tabs exist; close keeps tabs). */
|
|
372
|
+
setPreviewOpen(open: boolean): void {
|
|
373
|
+
this.instant(() => {
|
|
374
|
+
this.layout.update((prev) => ({ ...prev, previewOpen: open }))
|
|
375
|
+
this.applyGrid()
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Locate the shell conversation header: its bottom border is the
|
|
381
|
+
* horizontal divider under the "Session log" row the button should
|
|
382
|
+
* sit below. Resolved per call (the shell may mount it late); null when
|
|
383
|
+
* the shell has no header (standalone installs, desktop variants).
|
|
384
|
+
*/
|
|
385
|
+
private findHeaderBottom(): number | null {
|
|
386
|
+
const frame = this.frame
|
|
387
|
+
if (frame === null) return null
|
|
388
|
+
const header = frame.querySelector<HTMLElement>(
|
|
389
|
+
'[data-pane="conversation"] header, [class*="centerCol"] header',
|
|
390
|
+
)
|
|
391
|
+
if (header === null) return null
|
|
392
|
+
const bottom = header.getBoundingClientRect().bottom
|
|
393
|
+
return Number.isFinite(bottom) ? bottom : null
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Position the floating button: docked at the top-right corner, just
|
|
397
|
+
* below the shell header's bottom divider (fallback: the chevron row). */
|
|
398
|
+
private positionFloatingButton(): void {
|
|
399
|
+
const el = this.floatingButton
|
|
400
|
+
if (el === null) return
|
|
401
|
+
const height = window.innerHeight
|
|
402
|
+
const titlebar = titlebarAreaHeight()
|
|
403
|
+
const headerBottom = this.findHeaderBottom()
|
|
404
|
+
const top = headerBottom !== null
|
|
405
|
+
? clampFloatingTop(headerBottom + FLOATING_HEADER_GAP_PX, height, FLOATING_BUTTON_HEIGHT_PX, titlebar)
|
|
406
|
+
: topAlignedFloatingTop(height, FLOATING_BUTTON_HEIGHT_PX, titlebar)
|
|
407
|
+
el.style.top = `${Math.round(top)}px`
|
|
408
|
+
el.style.transform = 'none'
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Apply one store update with transitions disabled for exactly one frame. */
|
|
412
|
+
private instant(fn: () => void): void {
|
|
413
|
+
const frame = this.frame
|
|
414
|
+
if (frame === null) {
|
|
415
|
+
fn()
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
frame.setAttribute('data-filemgr-instant', '')
|
|
419
|
+
if (this.instantTimer !== undefined) clearTimeout(this.instantTimer)
|
|
420
|
+
this.instantTimer = setTimeout(() => {
|
|
421
|
+
this.instantTimer = undefined
|
|
422
|
+
frame.removeAttribute('data-filemgr-instant')
|
|
423
|
+
}, 0)
|
|
424
|
+
fn()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Re-write the frame grid and reposition handles + floating button. */
|
|
428
|
+
private applyGrid(): void {
|
|
429
|
+
const frame = this.frame
|
|
430
|
+
if (frame === null) return
|
|
431
|
+
// Never guess the shell tracks: without a mirrored shell write, the old
|
|
432
|
+
// fallback zeroed the sidebar track (the bug where the left sidebar
|
|
433
|
+
// vanished after a hot reload). Skip the write until syncGrid observes
|
|
434
|
+
// the shell's own 3-track grid.
|
|
435
|
+
if (this.shellTracks.length !== 3) return
|
|
436
|
+
const state = this.layout.getSnapshot()
|
|
437
|
+
const width = this.frameWidth > 0 ? this.frameWidth : frame.getBoundingClientRect().width
|
|
438
|
+
|
|
439
|
+
if (state.maximized !== null) {
|
|
440
|
+
this.applyMaximized(frame, state.maximized, width)
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
this.clearMaximizedChrome()
|
|
444
|
+
|
|
445
|
+
const explorer = this.layout.explorerWidthPx(state)
|
|
446
|
+
const preview = this.layout.previewWidthPx(state)
|
|
447
|
+
|
|
448
|
+
// Five tracks: shell sidebar, center, shell details, preview, explorer.
|
|
449
|
+
frame.style.gridTemplateColumns =
|
|
450
|
+
`${this.shellTracks[0]} minmax(0, 1fr) ${this.shellTracks[2]} ${Math.round(preview)}px ${Math.round(explorer)}px`
|
|
451
|
+
|
|
452
|
+
// Column contents follow the tracks (both columns always mounted).
|
|
453
|
+
if (this.explorerCol !== null) {
|
|
454
|
+
this.explorerCol.style.visibility = explorer > 0 ? 'visible' : 'hidden'
|
|
455
|
+
}
|
|
456
|
+
if (this.previewCol !== null) {
|
|
457
|
+
this.previewCol.style.visibility = preview > 0 ? 'visible' : 'hidden'
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Handles: at the left edge of each panel.
|
|
461
|
+
if (this.explorerHandle !== null) {
|
|
462
|
+
const left = Math.round(width - explorer)
|
|
463
|
+
this.explorerHandle.style.left = `${left}px`
|
|
464
|
+
this.explorerHandle.style.marginLeft = `${-EXPLORER_HANDLE_WIDTH / 2}px`
|
|
465
|
+
this.explorerHandle.style.display = explorer > 0 && state.root !== '' ? 'block' : 'none'
|
|
466
|
+
}
|
|
467
|
+
if (this.previewHandle !== null) {
|
|
468
|
+
const left = Math.round(width - explorer - preview)
|
|
469
|
+
this.previewHandle.style.left = `${left}px`
|
|
470
|
+
this.previewHandle.style.display = preview > 0 && state.root !== '' ? 'block' : 'none'
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// The official shell renders its own details drag handle at
|
|
474
|
+
// `viewport - details` (details treated as the last track). Once this
|
|
475
|
+
// panel extends the grid with preview/explorer tracks, the real
|
|
476
|
+
// center/details boundary shifts left by preview + explorer — re-derive
|
|
477
|
+
// the handle position from the actual tracks so it stays on the details
|
|
478
|
+
// column's left edge (degenerates to the official value when both panels
|
|
479
|
+
// are closed).
|
|
480
|
+
const detailsTrack = trackPx(this.shellTracks[2])
|
|
481
|
+
// applyGrid runs on every drag frame: resolve the shell handle once and
|
|
482
|
+
// cache it instead of scanning the whole frame subtree each call. The
|
|
483
|
+
// cache resets on attach (the shell may rebuild its chrome on HMR).
|
|
484
|
+
if (this.detailsHandle === null || !this.detailsHandle.isConnected) {
|
|
485
|
+
this.detailsHandle = frame.querySelector<HTMLElement>('[data-side="details"]')
|
|
486
|
+
}
|
|
487
|
+
if (this.detailsHandle !== null) {
|
|
488
|
+
this.detailsHandle.style.left = `${Math.round(width - detailsTrack - preview - explorer)}px`
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Floating expand button: visible only when the explorer is collapsed.
|
|
492
|
+
if (this.floatingButton !== null) {
|
|
493
|
+
const show = state.root !== '' && state.explorerCollapsed
|
|
494
|
+
this.floatingButton.style.display = show ? 'flex' : 'none'
|
|
495
|
+
this.positionFloatingButton()
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Maximize layout: the target column takes over the whole frame row (the
|
|
501
|
+
* other tracks collapse to 0px). On narrow viewports the takeover grid is
|
|
502
|
+
* skipped and the column renders as a fixed full-screen overlay instead
|
|
503
|
+
* (issue #315). Everything stays mounted — only geometry changes.
|
|
504
|
+
*/
|
|
505
|
+
private applyMaximized(frame: HTMLElement, target: MaximizeTarget, width: number): void {
|
|
506
|
+
const overlay = maximizedOverlay(this.layout.getSnapshot().availableWidth)
|
|
507
|
+
if (!overlay) {
|
|
508
|
+
frame.style.gridTemplateColumns = maximizedGridTracks(target, width)
|
|
509
|
+
}
|
|
510
|
+
if (this.explorerCol !== null) {
|
|
511
|
+
this.explorerCol.style.visibility = target === 'explorer' ? 'visible' : 'hidden'
|
|
512
|
+
this.explorerCol.classList.toggle('filemgr-maximized', target === 'explorer' && overlay)
|
|
513
|
+
}
|
|
514
|
+
if (this.previewCol !== null) {
|
|
515
|
+
this.previewCol.style.visibility = target === 'preview' ? 'visible' : 'hidden'
|
|
516
|
+
this.previewCol.classList.toggle('filemgr-maximized', target === 'preview' && overlay)
|
|
517
|
+
}
|
|
518
|
+
// No drag chrome while maximized: nothing to resize, and the floating
|
|
519
|
+
// button only makes sense for the collapsed explorer.
|
|
520
|
+
if (this.explorerHandle !== null) this.explorerHandle.style.display = 'none'
|
|
521
|
+
if (this.previewHandle !== null) this.previewHandle.style.display = 'none'
|
|
522
|
+
if (this.floatingButton !== null) this.floatingButton.style.display = 'none'
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Remove the narrow-screen overlay class from both columns. */
|
|
526
|
+
private clearMaximizedChrome(): void {
|
|
527
|
+
this.explorerCol?.classList.remove('filemgr-maximized')
|
|
528
|
+
this.previewCol?.classList.remove('filemgr-maximized')
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Detach everything (plugin unload). */
|
|
532
|
+
dispose(): void {
|
|
533
|
+
this.waitObserver?.disconnect()
|
|
534
|
+
this.styleObserver?.disconnect()
|
|
535
|
+
this.sizeObserver?.disconnect()
|
|
536
|
+
for (const dispose of this.disposers) dispose()
|
|
537
|
+
this.previewCol?.remove()
|
|
538
|
+
this.explorerCol?.remove()
|
|
539
|
+
this.explorerHandle?.remove()
|
|
540
|
+
this.previewHandle?.remove()
|
|
541
|
+
this.floatingButton?.remove()
|
|
542
|
+
if (this.instantTimer !== undefined) clearTimeout(this.instantTimer)
|
|
543
|
+
if (frameElement === this.frame) frameElement = null
|
|
544
|
+
this.frame = null
|
|
545
|
+
}
|
|
546
|
+
}
|