dockview-svelte 0.0.1
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 +661 -0
- package/README.md +480 -0
- package/dist/components/DefaultTab.svelte +55 -0
- package/dist/components/DefaultTab.svelte.d.ts +7 -0
- package/dist/components/Dockview.svelte +517 -0
- package/dist/components/Dockview.svelte.d.ts +113 -0
- package/dist/components/DvWidget.svelte +120 -0
- package/dist/components/DvWidget.svelte.d.ts +41 -0
- package/dist/components/Gridview.svelte +341 -0
- package/dist/components/Gridview.svelte.d.ts +73 -0
- package/dist/components/Paneview.svelte +307 -0
- package/dist/components/Paneview.svelte.d.ts +64 -0
- package/dist/components/Splitview.svelte +332 -0
- package/dist/components/Splitview.svelte.d.ts +70 -0
- package/dist/core/context.d.ts +62 -0
- package/dist/core/context.js +1 -0
- package/dist/core/effect-helpers.d.ts +7 -0
- package/dist/core/effect-helpers.js +23 -0
- package/dist/core/factory.svelte.d.ts +16 -0
- package/dist/core/factory.svelte.js +248 -0
- package/dist/core/gridview.svelte.d.ts +31 -0
- package/dist/core/gridview.svelte.js +177 -0
- package/dist/core/paneview-test-helpers.js +28 -0
- package/dist/core/paneview.svelte.d.ts +29 -0
- package/dist/core/paneview.svelte.js +186 -0
- package/dist/core/registry.d.ts +88 -0
- package/dist/core/registry.js +143 -0
- package/dist/core/splitview.svelte.d.ts +32 -0
- package/dist/core/splitview.svelte.js +179 -0
- package/dist/core/types.d.ts +424 -0
- package/dist/core/types.js +1 -0
- package/dist/core/utils.d.ts +12 -0
- package/dist/core/utils.js +59 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +11 -0
- package/package.json +66 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { DEV } from 'esm-env'
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
|
+
import { getContext } from 'svelte'
|
|
5
|
+
import { DOCKVIEW_CONTEXT_KEY } from '../core/context.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Props for `<DvWidget>`.
|
|
9
|
+
*
|
|
10
|
+
* The `children` snippet receives the panel's shared `state` object — the same
|
|
11
|
+
* object a `widgets`-registry component receives as its `state` prop — and its
|
|
12
|
+
* output is rendered into the panel body (and tab/header, when given).
|
|
13
|
+
* `tab` / `header` snippets receive the same `state`.
|
|
14
|
+
*
|
|
15
|
+
* > **Type note:** the snippet parameter defaults to `never` on purpose — it
|
|
16
|
+
* > cannot be inferred per-widget, so annotate it with your panel's state type
|
|
17
|
+
* > (e.g. `{#snippet children(state: PanelState<{ id: number }>)}`). The
|
|
18
|
+
* > annotation is what gives you typed `state.params`.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
*
|
|
22
|
+
* ```svelte
|
|
23
|
+
* <Dockview>
|
|
24
|
+
* <DvWidget name="chat" title="Chat">
|
|
25
|
+
* {#snippet children(state)}
|
|
26
|
+
* <ChatWidget {state} />
|
|
27
|
+
* {/snippet}
|
|
28
|
+
* </DvWidget>
|
|
29
|
+
* </Dockview>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
interface Props {
|
|
33
|
+
/** Widget key this instance registers under. */
|
|
34
|
+
name: string
|
|
35
|
+
/** Body content, called with the shared `state`. */
|
|
36
|
+
children: Snippet<[state: never]>
|
|
37
|
+
/** Dockview tab override, called with the shared `PanelState` (Dockview only). */
|
|
38
|
+
tab?: Snippet<[state: never]>
|
|
39
|
+
/** Paneview header override, called with the shared `PaneviewState` (Paneview only). */
|
|
40
|
+
header?: Snippet<[state: never]>
|
|
41
|
+
/** Default title for this widget (tier 2 in the resolution chain). */
|
|
42
|
+
title?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let { name, children, tab, header, title }: Props = $props()
|
|
46
|
+
|
|
47
|
+
const ctx = getContext<{
|
|
48
|
+
kind: 'dockview' | 'splitview' | 'gridview' | 'paneview'
|
|
49
|
+
registerWidget: (key: string, def: Record<string, unknown>) => void
|
|
50
|
+
unregisterWidget: (key: string) => void
|
|
51
|
+
} | null>(DOCKVIEW_CONTEXT_KEY)
|
|
52
|
+
if (!ctx) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'dockview-svelte: <DvWidget> must be a child of Dockview/Splitview/Gridview/Paneview'
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Snippets only apply to specific layouts; warn on inapplicable ones instead
|
|
59
|
+
// of silently dropping them (the per-layout registry ignores unknown keys).
|
|
60
|
+
$effect(() => {
|
|
61
|
+
if (!DEV) return
|
|
62
|
+
if (tab && ctx.kind !== 'dockview') {
|
|
63
|
+
console.warn(
|
|
64
|
+
`dockview-svelte: <DvWidget tab> is only supported by <Dockview>; ignored inside <${ctx.kind}>`
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
if (header && ctx.kind !== 'paneview') {
|
|
68
|
+
console.warn(
|
|
69
|
+
`dockview-svelte: <DvWidget header> is only supported by <Paneview>; ignored inside <${ctx.kind}>`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
if (title !== undefined && ctx.kind !== 'dockview' && ctx.kind !== 'paneview') {
|
|
73
|
+
console.warn(
|
|
74
|
+
`dockview-svelte: <DvWidget title> is only supported by <Dockview>/<Paneview>; ignored inside <${ctx.kind}>`
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build a plain-function component around a snippet. The factories call
|
|
81
|
+
* `mount(component, { target, props: { state } })`, so the wrapper only needs
|
|
82
|
+
* to accept `{ state }` and invoke the snippet with the dockview-owned anchor.
|
|
83
|
+
* A plain function works because Svelte 5 `mount` invokes
|
|
84
|
+
* `Component(anchor, props)` directly (see `_mount` in `svelte/internal/client`).
|
|
85
|
+
*
|
|
86
|
+
* Compiled snippets are `(anchor, ...args)` functions where each arg is a
|
|
87
|
+
* getter thunk (see `svelte/compiler` output: `children($$anchor, () =>
|
|
88
|
+
* myState)`). Passing a thunk over the live `state` keeps reactive reads
|
|
89
|
+
* tracked.
|
|
90
|
+
*/
|
|
91
|
+
function snippetComponent(snippet: Snippet<[state: never]>) {
|
|
92
|
+
return (anchor: Node, props: { state: never }) => {
|
|
93
|
+
;(snippet as unknown as (node: Node, getState: () => never) => void)(anchor, () => props.state)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const component = $derived(snippetComponent(children as Snippet<[state: never]>))
|
|
98
|
+
const tabComponent = $derived(tab ? snippetComponent(tab as Snippet<[state: never]>) : undefined)
|
|
99
|
+
const headerComponent = $derived(
|
|
100
|
+
header ? snippetComponent(header as Snippet<[state: never]>) : undefined
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Register on mount and on every prop change; the cleanup unregisters the
|
|
104
|
+
// current key first, so a changed `name` never leaves a stale entry behind.
|
|
105
|
+
// `widgets`-prop entries with the same key win on re-seed — declarative
|
|
106
|
+
// children are the override path, not the base.
|
|
107
|
+
// Only include snippets that apply to the parent layout (`tab` → Dockview,
|
|
108
|
+
// `header` → Paneview, `title` → Dockview/Paneview) so the dev warning above is
|
|
109
|
+
// accurate and the inapplicable keys never reach the registry.
|
|
110
|
+
$effect(() => {
|
|
111
|
+
const def: Record<string, unknown> = { component }
|
|
112
|
+
if (tabComponent && ctx.kind === 'dockview') def.tab = tabComponent
|
|
113
|
+
if (headerComponent && ctx.kind === 'paneview') def.header = headerComponent
|
|
114
|
+
if (title !== undefined && (ctx.kind === 'dockview' || ctx.kind === 'paneview')) {
|
|
115
|
+
def.title = title
|
|
116
|
+
}
|
|
117
|
+
ctx.registerWidget(name, def)
|
|
118
|
+
return () => ctx.unregisterWidget(name)
|
|
119
|
+
})
|
|
120
|
+
</script>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Props for `<DvWidget>`.
|
|
4
|
+
*
|
|
5
|
+
* The `children` snippet receives the panel's shared `state` object — the same
|
|
6
|
+
* object a `widgets`-registry component receives as its `state` prop — and its
|
|
7
|
+
* output is rendered into the panel body (and tab/header, when given).
|
|
8
|
+
* `tab` / `header` snippets receive the same `state`.
|
|
9
|
+
*
|
|
10
|
+
* > **Type note:** the snippet parameter defaults to `never` on purpose — it
|
|
11
|
+
* > cannot be inferred per-widget, so annotate it with your panel's state type
|
|
12
|
+
* > (e.g. `{#snippet children(state: PanelState<{ id: number }>)}`). The
|
|
13
|
+
* > annotation is what gives you typed `state.params`.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
*
|
|
17
|
+
* ```svelte
|
|
18
|
+
* <Dockview>
|
|
19
|
+
* <DvWidget name="chat" title="Chat">
|
|
20
|
+
* {#snippet children(state)}
|
|
21
|
+
* <ChatWidget {state} />
|
|
22
|
+
* {/snippet}
|
|
23
|
+
* </DvWidget>
|
|
24
|
+
* </Dockview>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
interface Props {
|
|
28
|
+
/** Widget key this instance registers under. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Body content, called with the shared `state`. */
|
|
31
|
+
children: Snippet<[state: never]>;
|
|
32
|
+
/** Dockview tab override, called with the shared `PanelState` (Dockview only). */
|
|
33
|
+
tab?: Snippet<[state: never]>;
|
|
34
|
+
/** Paneview header override, called with the shared `PaneviewState` (Paneview only). */
|
|
35
|
+
header?: Snippet<[state: never]>;
|
|
36
|
+
/** Default title for this widget (tier 2 in the resolution chain). */
|
|
37
|
+
title?: string;
|
|
38
|
+
}
|
|
39
|
+
declare const DvWidget: import("svelte").Component<Props, {}, "">;
|
|
40
|
+
type DvWidget = ReturnType<typeof DvWidget>;
|
|
41
|
+
export default DvWidget;
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
<script lang="ts" generics="const W extends GridviewWidgets">
|
|
2
|
+
import type {
|
|
3
|
+
GridviewApi,
|
|
4
|
+
GridviewComponentOptions,
|
|
5
|
+
IGridviewPanel,
|
|
6
|
+
SerializedGridviewComponent,
|
|
7
|
+
} from 'dockview'
|
|
8
|
+
import { createGridview, Orientation } from 'dockview'
|
|
9
|
+
import { onMount, type Snippet, setContext } from 'svelte'
|
|
10
|
+
import { DOCKVIEW_CONTEXT_KEY, type GridviewContext } from '../core/context.js'
|
|
11
|
+
import { createGridviewFactory } from '../core/gridview.svelte.js'
|
|
12
|
+
import { GridviewWidgetRegistry } from '../core/registry.js'
|
|
13
|
+
import type {
|
|
14
|
+
GridviewHandle,
|
|
15
|
+
GridviewMoveOptions,
|
|
16
|
+
GridviewOpenPanelOptions,
|
|
17
|
+
GridviewPanelHandle,
|
|
18
|
+
GridviewParamsOf,
|
|
19
|
+
GridviewWidgetDefinition,
|
|
20
|
+
GridviewWidgets,
|
|
21
|
+
} from '../core/types.js'
|
|
22
|
+
import { deepEqual } from '../core/utils.js'
|
|
23
|
+
|
|
24
|
+
interface Props<W extends GridviewWidgets> {
|
|
25
|
+
/** Widget registry: `{ [key]: { component } }` — no tabs or titles, grid cells have no headers. */
|
|
26
|
+
widgets?: W
|
|
27
|
+
/**
|
|
28
|
+
* `GridviewComponentOptions` passthrough (proportionalLayout, …).
|
|
29
|
+
* `orientation` defaults to `HORIZONTAL` (required by dockview).
|
|
30
|
+
*
|
|
31
|
+
* `createComponent` is owned by the library (Svelte `mount` factory
|
|
32
|
+
* backed by the `widgets` registry) and cannot be overridden.
|
|
33
|
+
*/
|
|
34
|
+
options?: Omit<GridviewComponentOptions, 'createComponent'> & { orientation?: Orientation }
|
|
35
|
+
/** `bind:layout` — gridview JSON (`toJSON`/`fromJSON` shape). */
|
|
36
|
+
layout?: SerializedGridviewComponent
|
|
37
|
+
/** `bind:handle` — `{ api, openPanel, registerWidget, removePanel, movePanel, setVisible, setActive }`. */
|
|
38
|
+
handle?: GridviewHandle<W>
|
|
39
|
+
/**
|
|
40
|
+
* `bind:panels` — reactive list of current cells (`api.panels` snapshot).
|
|
41
|
+
* Refreshed on layout/add/remove/FromJSON, so `panels.length` is the
|
|
42
|
+
* sveltish cell count — no manual `onDidAddPanel` counting needed.
|
|
43
|
+
*/
|
|
44
|
+
panels?: IGridviewPanel[]
|
|
45
|
+
/**
|
|
46
|
+
* `bind:activePanel` — the currently active cell. Derived from the
|
|
47
|
+
* per-panel `api.onDidActiveChange` (via the factory) plus explicit sync
|
|
48
|
+
* on the open/remove paths — `GridviewApi.onDidActivePanelChange` only
|
|
49
|
+
* fires on focus-driven activation, never on programmatic adds.
|
|
50
|
+
*/
|
|
51
|
+
activePanel?: IGridviewPanel | undefined
|
|
52
|
+
/** Extra classes for the root container. */
|
|
53
|
+
class?: string
|
|
54
|
+
/** Declarative widgets (`<DvWidget>` children) — alternative to the `widgets` prop. */
|
|
55
|
+
children?: Snippet
|
|
56
|
+
/** Fired once the component is mounted and `api` is ready. */
|
|
57
|
+
onReady?: (event: { api: GridviewApi; handle: GridviewHandle<W> }) => void
|
|
58
|
+
onDidLayoutChange?: () => void
|
|
59
|
+
onDidLayoutFromJSON?: () => void
|
|
60
|
+
onDidAddPanel?: (panel: IGridviewPanel) => void
|
|
61
|
+
onDidRemovePanel?: (panel: IGridviewPanel) => void
|
|
62
|
+
onDidActivePanelChange?: (panel: IGridviewPanel | undefined) => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let {
|
|
66
|
+
widgets = {} as W,
|
|
67
|
+
options = {} as Omit<GridviewComponentOptions, 'createComponent'> & {
|
|
68
|
+
orientation?: Orientation
|
|
69
|
+
},
|
|
70
|
+
layout = $bindable(),
|
|
71
|
+
handle = $bindable(),
|
|
72
|
+
panels = $bindable([]),
|
|
73
|
+
activePanel = $bindable(undefined),
|
|
74
|
+
class: className = '',
|
|
75
|
+
children,
|
|
76
|
+
onReady,
|
|
77
|
+
onDidLayoutChange,
|
|
78
|
+
onDidLayoutFromJSON,
|
|
79
|
+
onDidAddPanel,
|
|
80
|
+
onDidRemovePanel,
|
|
81
|
+
onDidActivePanelChange,
|
|
82
|
+
}: Props<W> = $props()
|
|
83
|
+
|
|
84
|
+
let container: HTMLElement
|
|
85
|
+
let api = $state<GridviewApi | undefined>(undefined)
|
|
86
|
+
|
|
87
|
+
const registry = new GridviewWidgetRegistry()
|
|
88
|
+
const counters = new Map<string, number>()
|
|
89
|
+
|
|
90
|
+
// Stable `registerWidget`, shared by context and handle.
|
|
91
|
+
function registerWidget(key: string, def: GridviewWidgetDefinition): void {
|
|
92
|
+
registry.register(key, def)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function unregisterWidget(key: string): void {
|
|
96
|
+
registry.unregister(key)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Context for descendant widgets (api is populated after mount).
|
|
100
|
+
const context = $state<GridviewContext>({
|
|
101
|
+
api: undefined,
|
|
102
|
+
registerWidget,
|
|
103
|
+
unregisterWidget,
|
|
104
|
+
kind: 'gridview',
|
|
105
|
+
})
|
|
106
|
+
setContext(DOCKVIEW_CONTEXT_KEY, context)
|
|
107
|
+
|
|
108
|
+
// Seed the registry from the `widgets` prop (additive).
|
|
109
|
+
$effect(() => {
|
|
110
|
+
registry.seed(widgets)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
function nextId(key: string): string {
|
|
114
|
+
const n = (counters.get(key) ?? 0) + 1
|
|
115
|
+
counters.set(key, n)
|
|
116
|
+
return `${key}-${n}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** `bind:layout` loop-break: the JSON we last emitted to the parent. */
|
|
120
|
+
let lastEmitted: SerializedGridviewComponent | undefined
|
|
121
|
+
|
|
122
|
+
/** Serialize the current layout and push it to the bound `layout`. */
|
|
123
|
+
function emitLayout(): void {
|
|
124
|
+
if (!api) return
|
|
125
|
+
const json = api.toJSON()
|
|
126
|
+
lastEmitted = json
|
|
127
|
+
layout = json
|
|
128
|
+
refreshPanels()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Refresh `bind:panels` from the live cells. */
|
|
132
|
+
function refreshPanels(): void {
|
|
133
|
+
if (!api) return
|
|
134
|
+
panels = [...api.panels]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Set `bind:activePanel` + fire `onDidActivePanelChange`, deduped on panel
|
|
139
|
+
* id. Per-panel `onDidActiveChange` fires on both the deactivated and the
|
|
140
|
+
* activated cell, so the guard keeps a single update per activation.
|
|
141
|
+
*/
|
|
142
|
+
function setActivePanel(panel: IGridviewPanel | undefined): void {
|
|
143
|
+
if (activePanel?.id === panel?.id) return
|
|
144
|
+
activePanel = panel
|
|
145
|
+
onDidActivePanelChange?.(panel)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getPanelOrThrow(id: string): import('dockview').GridviewPanel {
|
|
149
|
+
const panel = api!.getPanel(id)
|
|
150
|
+
if (!panel) {
|
|
151
|
+
throw new Error(`dockview-svelte: unknown panel "${id}"`)
|
|
152
|
+
}
|
|
153
|
+
return panel as import('dockview').GridviewPanel
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function openPanel<K extends keyof W & string>(
|
|
157
|
+
key: K,
|
|
158
|
+
opts?: GridviewOpenPanelOptions<GridviewParamsOf<W[K]['component']>>
|
|
159
|
+
): GridviewPanelHandle<GridviewParamsOf<W[K]['component']>> {
|
|
160
|
+
if (!api) {
|
|
161
|
+
throw new Error('dockview-svelte: Gridview is not mounted yet')
|
|
162
|
+
}
|
|
163
|
+
const def = registry.get(key)
|
|
164
|
+
if (!def) {
|
|
165
|
+
throw new Error(`dockview-svelte: unknown widget "${key}"`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const id = opts?.id ?? nextId(key)
|
|
169
|
+
const panel = api.addPanel({
|
|
170
|
+
...(opts ?? {}),
|
|
171
|
+
id,
|
|
172
|
+
component: key,
|
|
173
|
+
params: opts?.params ?? {},
|
|
174
|
+
})
|
|
175
|
+
// `addPanel` fires no event the component observes for the open path
|
|
176
|
+
// itself (events cover external mutations) — refresh the bound
|
|
177
|
+
// panels/layout directly so `bind:panels` reflects the new cell.
|
|
178
|
+
// `doSetGroupActive` inside `addPanel` fires per-panel
|
|
179
|
+
// `onDidActiveChange`, which the factory mirrors to `state.active`
|
|
180
|
+
// but the component does not observe — so sync `bind:activePanel`
|
|
181
|
+
// explicitly.
|
|
182
|
+
emitLayout()
|
|
183
|
+
setActivePanel(panel)
|
|
184
|
+
|
|
185
|
+
const state = factory?.getState(id)
|
|
186
|
+
// `state` is `GridviewState<Record<string, unknown>>` at the factory
|
|
187
|
+
// boundary; `GridviewState<P>` is invariant in `P` (params is read/write),
|
|
188
|
+
// so the runtime-known `P` requires an `unknown` bridge.
|
|
189
|
+
return { id, panel, api: panel.api, state: state! } as unknown as GridviewPanelHandle<
|
|
190
|
+
GridviewParamsOf<W[K]['component']>
|
|
191
|
+
>
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Remove a cell by panel id. */
|
|
195
|
+
function removePanel(id: string): void {
|
|
196
|
+
if (!api) {
|
|
197
|
+
throw new Error('dockview-svelte: Gridview is not mounted yet')
|
|
198
|
+
}
|
|
199
|
+
const removed = getPanelOrThrow(id)
|
|
200
|
+
const wasActive = activePanel?.id === removed.id
|
|
201
|
+
api.removePanel(removed)
|
|
202
|
+
// Removal fires no active event either — fall back to the last
|
|
203
|
+
// remaining cell when the active one was removed.
|
|
204
|
+
if (wasActive) setActivePanel(api.panels.at(-1))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Move a cell relative to a reference panel. */
|
|
208
|
+
function movePanel(id: string, move: GridviewMoveOptions): void {
|
|
209
|
+
if (!api) {
|
|
210
|
+
throw new Error('dockview-svelte: Gridview is not mounted yet')
|
|
211
|
+
}
|
|
212
|
+
api.movePanel(getPanelOrThrow(id), {
|
|
213
|
+
direction: move.direction,
|
|
214
|
+
reference: move.reference,
|
|
215
|
+
size: move.size,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Show or hide a cell by id (via the panel api — `panel.setVisible` only fires `onDidVisibilityChange` without touching the layout). */
|
|
220
|
+
function setVisible(id: string, visible: boolean): void {
|
|
221
|
+
if (!api) {
|
|
222
|
+
throw new Error('dockview-svelte: Gridview is not mounted yet')
|
|
223
|
+
}
|
|
224
|
+
getPanelOrThrow(id).api.setVisible(visible)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Activate a cell by id (via the panel api — `panel.setActive` only fires `onDidActiveChange`). */
|
|
228
|
+
function setActive(id: string): void {
|
|
229
|
+
if (!api) {
|
|
230
|
+
throw new Error('dockview-svelte: Gridview is not mounted yet')
|
|
231
|
+
}
|
|
232
|
+
getPanelOrThrow(id).api.setActive()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let factory: ReturnType<typeof createGridviewFactory> | undefined
|
|
236
|
+
|
|
237
|
+
// External `layout` changes → apply via `fromJSON` (loop-break via `lastEmitted`).
|
|
238
|
+
$effect(() => {
|
|
239
|
+
const current = api
|
|
240
|
+
if (!current || !layout) return
|
|
241
|
+
if (deepEqual(lastEmitted, layout)) return
|
|
242
|
+
current.fromJSON(layout)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// Live `options` changes (orientation, etc.) → apply via `updateOptions`.
|
|
246
|
+
// NOTE: `opts` must be read before the `api` guard — otherwise the
|
|
247
|
+
// initial run returns early without tracking `options`, and later changes
|
|
248
|
+
// never re-trigger the effect.
|
|
249
|
+
$effect(() => {
|
|
250
|
+
const opts = options
|
|
251
|
+
if (!api) return
|
|
252
|
+
api.updateOptions(opts)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
onMount(() => {
|
|
256
|
+
factory = createGridviewFactory(registry, context, {
|
|
257
|
+
onDidActivePanelChange: (panel) => setActivePanel(panel as unknown as IGridviewPanel),
|
|
258
|
+
})
|
|
259
|
+
const createdApi = createGridview(container, {
|
|
260
|
+
...options,
|
|
261
|
+
orientation: options.orientation ?? Orientation.HORIZONTAL,
|
|
262
|
+
createComponent: factory.createComponent,
|
|
263
|
+
})
|
|
264
|
+
// Apply a pre-existing `bind:layout` before exposing the api.
|
|
265
|
+
if (layout) {
|
|
266
|
+
createdApi.fromJSON(layout)
|
|
267
|
+
lastEmitted = createdApi.toJSON()
|
|
268
|
+
layout = lastEmitted
|
|
269
|
+
}
|
|
270
|
+
api = createdApi
|
|
271
|
+
context.api = createdApi
|
|
272
|
+
|
|
273
|
+
const createdHandle: GridviewHandle<W> = {
|
|
274
|
+
api: createdApi,
|
|
275
|
+
openPanel,
|
|
276
|
+
registerWidget,
|
|
277
|
+
removePanel,
|
|
278
|
+
movePanel,
|
|
279
|
+
setVisible,
|
|
280
|
+
setActive,
|
|
281
|
+
}
|
|
282
|
+
handle = createdHandle
|
|
283
|
+
|
|
284
|
+
// Bridge gridview events to callbacks, disposing on teardown.
|
|
285
|
+
const subscriptions = [
|
|
286
|
+
createdApi.onDidLayoutChange(() => {
|
|
287
|
+
emitLayout()
|
|
288
|
+
onDidLayoutChange?.()
|
|
289
|
+
}),
|
|
290
|
+
createdApi.onDidLayoutFromJSON(() => {
|
|
291
|
+
refreshPanels()
|
|
292
|
+
onDidLayoutFromJSON?.()
|
|
293
|
+
}),
|
|
294
|
+
createdApi.onDidAddPanel((panel: IGridviewPanel) => {
|
|
295
|
+
refreshPanels()
|
|
296
|
+
// External adds (e.g. `api.addPanel` escape hatch) activate the
|
|
297
|
+
// new cell without a component-level event — same sync as the
|
|
298
|
+
// `openPanel` path.
|
|
299
|
+
setActivePanel(panel)
|
|
300
|
+
onDidAddPanel?.(panel)
|
|
301
|
+
}),
|
|
302
|
+
createdApi.onDidRemovePanel((panel: IGridviewPanel) => {
|
|
303
|
+
refreshPanels()
|
|
304
|
+
if (activePanel?.id === panel.id) setActivePanel(createdApi.panels.at(-1))
|
|
305
|
+
onDidRemovePanel?.(panel)
|
|
306
|
+
}),
|
|
307
|
+
createdApi.onDidActivePanelChange((panel: IGridviewPanel | undefined) => {
|
|
308
|
+
// Focus-driven activation path (`registerPanel` wires
|
|
309
|
+
// `onDidFocusChange` → `doSetGroupActive`). Programmatic
|
|
310
|
+
// activation (`api.setActive`, `addPanel`) only fires the
|
|
311
|
+
// per-panel event, handled via the factory hook below.
|
|
312
|
+
setActivePanel(panel)
|
|
313
|
+
}),
|
|
314
|
+
]
|
|
315
|
+
// Seed the initial panels before exposing the handle.
|
|
316
|
+
refreshPanels()
|
|
317
|
+
onReady?.({ api: createdApi, handle: createdHandle })
|
|
318
|
+
|
|
319
|
+
return () => {
|
|
320
|
+
for (const disposable of subscriptions) disposable.dispose()
|
|
321
|
+
createdApi.dispose()
|
|
322
|
+
api = undefined
|
|
323
|
+
factory = undefined
|
|
324
|
+
lastEmitted = undefined
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
</script>
|
|
328
|
+
|
|
329
|
+
<div class="dv-svelte-gridview-root {className}" bind:this={container}>
|
|
330
|
+
{#if children}
|
|
331
|
+
{@render children()}
|
|
332
|
+
{/if}
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<style>
|
|
336
|
+
.dv-svelte-gridview-root {
|
|
337
|
+
width: 100%;
|
|
338
|
+
height: 100%;
|
|
339
|
+
position: relative;
|
|
340
|
+
}
|
|
341
|
+
</style>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { GridviewApi, GridviewComponentOptions, IGridviewPanel, SerializedGridviewComponent } from 'dockview';
|
|
2
|
+
import { Orientation } from 'dockview';
|
|
3
|
+
import { type Snippet } from 'svelte';
|
|
4
|
+
import type { GridviewHandle, GridviewWidgets } from '../core/types.js';
|
|
5
|
+
interface Props<W extends GridviewWidgets> {
|
|
6
|
+
/** Widget registry: `{ [key]: { component } }` — no tabs or titles, grid cells have no headers. */
|
|
7
|
+
widgets?: W;
|
|
8
|
+
/**
|
|
9
|
+
* `GridviewComponentOptions` passthrough (proportionalLayout, …).
|
|
10
|
+
* `orientation` defaults to `HORIZONTAL` (required by dockview).
|
|
11
|
+
*
|
|
12
|
+
* `createComponent` is owned by the library (Svelte `mount` factory
|
|
13
|
+
* backed by the `widgets` registry) and cannot be overridden.
|
|
14
|
+
*/
|
|
15
|
+
options?: Omit<GridviewComponentOptions, 'createComponent'> & {
|
|
16
|
+
orientation?: Orientation;
|
|
17
|
+
};
|
|
18
|
+
/** `bind:layout` — gridview JSON (`toJSON`/`fromJSON` shape). */
|
|
19
|
+
layout?: SerializedGridviewComponent;
|
|
20
|
+
/** `bind:handle` — `{ api, openPanel, registerWidget, removePanel, movePanel, setVisible, setActive }`. */
|
|
21
|
+
handle?: GridviewHandle<W>;
|
|
22
|
+
/**
|
|
23
|
+
* `bind:panels` — reactive list of current cells (`api.panels` snapshot).
|
|
24
|
+
* Refreshed on layout/add/remove/FromJSON, so `panels.length` is the
|
|
25
|
+
* sveltish cell count — no manual `onDidAddPanel` counting needed.
|
|
26
|
+
*/
|
|
27
|
+
panels?: IGridviewPanel[];
|
|
28
|
+
/**
|
|
29
|
+
* `bind:activePanel` — the currently active cell. Derived from the
|
|
30
|
+
* per-panel `api.onDidActiveChange` (via the factory) plus explicit sync
|
|
31
|
+
* on the open/remove paths — `GridviewApi.onDidActivePanelChange` only
|
|
32
|
+
* fires on focus-driven activation, never on programmatic adds.
|
|
33
|
+
*/
|
|
34
|
+
activePanel?: IGridviewPanel | undefined;
|
|
35
|
+
/** Extra classes for the root container. */
|
|
36
|
+
class?: string;
|
|
37
|
+
/** Declarative widgets (`<DvWidget>` children) — alternative to the `widgets` prop. */
|
|
38
|
+
children?: Snippet;
|
|
39
|
+
/** Fired once the component is mounted and `api` is ready. */
|
|
40
|
+
onReady?: (event: {
|
|
41
|
+
api: GridviewApi;
|
|
42
|
+
handle: GridviewHandle<W>;
|
|
43
|
+
}) => void;
|
|
44
|
+
onDidLayoutChange?: () => void;
|
|
45
|
+
onDidLayoutFromJSON?: () => void;
|
|
46
|
+
onDidAddPanel?: (panel: IGridviewPanel) => void;
|
|
47
|
+
onDidRemovePanel?: (panel: IGridviewPanel) => void;
|
|
48
|
+
onDidActivePanelChange?: (panel: IGridviewPanel | undefined) => void;
|
|
49
|
+
}
|
|
50
|
+
declare function $$render<const W extends GridviewWidgets>(): {
|
|
51
|
+
props: Props<W>;
|
|
52
|
+
exports: {};
|
|
53
|
+
bindings: "layout" | "handle" | "panels" | "activePanel";
|
|
54
|
+
slots: {};
|
|
55
|
+
events: {};
|
|
56
|
+
};
|
|
57
|
+
declare class __sveltets_Render<const W extends GridviewWidgets> {
|
|
58
|
+
props(): ReturnType<typeof $$render<W>>['props'];
|
|
59
|
+
events(): ReturnType<typeof $$render<W>>['events'];
|
|
60
|
+
slots(): ReturnType<typeof $$render<W>>['slots'];
|
|
61
|
+
bindings(): "layout" | "handle" | "panels" | "activePanel";
|
|
62
|
+
exports(): {};
|
|
63
|
+
}
|
|
64
|
+
interface $$IsomorphicComponent {
|
|
65
|
+
new <const W extends GridviewWidgets>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<W>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<W>['props']>, ReturnType<__sveltets_Render<W>['events']>, ReturnType<__sveltets_Render<W>['slots']>> & {
|
|
66
|
+
$$bindings?: ReturnType<__sveltets_Render<W>['bindings']>;
|
|
67
|
+
} & ReturnType<__sveltets_Render<W>['exports']>;
|
|
68
|
+
<const W extends GridviewWidgets>(internal: unknown, props: ReturnType<__sveltets_Render<W>['props']> & {}): ReturnType<__sveltets_Render<W>['exports']>;
|
|
69
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
70
|
+
}
|
|
71
|
+
declare const Gridview: $$IsomorphicComponent;
|
|
72
|
+
type Gridview<const W extends GridviewWidgets> = InstanceType<typeof Gridview<W>>;
|
|
73
|
+
export default Gridview;
|