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.
Files changed (36) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +480 -0
  3. package/dist/components/DefaultTab.svelte +55 -0
  4. package/dist/components/DefaultTab.svelte.d.ts +7 -0
  5. package/dist/components/Dockview.svelte +517 -0
  6. package/dist/components/Dockview.svelte.d.ts +113 -0
  7. package/dist/components/DvWidget.svelte +120 -0
  8. package/dist/components/DvWidget.svelte.d.ts +41 -0
  9. package/dist/components/Gridview.svelte +341 -0
  10. package/dist/components/Gridview.svelte.d.ts +73 -0
  11. package/dist/components/Paneview.svelte +307 -0
  12. package/dist/components/Paneview.svelte.d.ts +64 -0
  13. package/dist/components/Splitview.svelte +332 -0
  14. package/dist/components/Splitview.svelte.d.ts +70 -0
  15. package/dist/core/context.d.ts +62 -0
  16. package/dist/core/context.js +1 -0
  17. package/dist/core/effect-helpers.d.ts +7 -0
  18. package/dist/core/effect-helpers.js +23 -0
  19. package/dist/core/factory.svelte.d.ts +16 -0
  20. package/dist/core/factory.svelte.js +248 -0
  21. package/dist/core/gridview.svelte.d.ts +31 -0
  22. package/dist/core/gridview.svelte.js +177 -0
  23. package/dist/core/paneview-test-helpers.js +28 -0
  24. package/dist/core/paneview.svelte.d.ts +29 -0
  25. package/dist/core/paneview.svelte.js +186 -0
  26. package/dist/core/registry.d.ts +88 -0
  27. package/dist/core/registry.js +143 -0
  28. package/dist/core/splitview.svelte.d.ts +32 -0
  29. package/dist/core/splitview.svelte.js +179 -0
  30. package/dist/core/types.d.ts +424 -0
  31. package/dist/core/types.js +1 -0
  32. package/dist/core/utils.d.ts +12 -0
  33. package/dist/core/utils.js +59 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.js +11 -0
  36. package/package.json +66 -0
@@ -0,0 +1,307 @@
1
+ <script lang="ts" generics="const W extends PaneviewWidgets">
2
+ import type {
3
+ IPaneviewPanel,
4
+ PaneviewApi,
5
+ PaneviewComponentOptions,
6
+ PaneviewDidDropEvent,
7
+ PaneviewDndOverlayEvent,
8
+ SerializedPaneview,
9
+ } from 'dockview'
10
+ import { createPaneview } from 'dockview'
11
+ import { onMount, type Snippet, setContext } from 'svelte'
12
+ import { DOCKVIEW_CONTEXT_KEY, type PaneviewContext } from '../core/context.js'
13
+ import { createPaneviewFactory } from '../core/paneview.svelte.js'
14
+ import { PaneviewWidgetRegistry } from '../core/registry.js'
15
+ import type {
16
+ PaneviewHandle,
17
+ PaneviewOpenPanelOptions,
18
+ PaneviewPanelHandle,
19
+ PaneviewParamsOf,
20
+ PaneviewWidgetDefinition,
21
+ PaneviewWidgets,
22
+ } from '../core/types.js'
23
+ import { deepEqual } from '../core/utils.js'
24
+
25
+ interface Props<W extends PaneviewWidgets> {
26
+ /** Widget registry: `{ [key]: { component, header?, title? } }`. */
27
+ widgets?: W
28
+ /**
29
+ * `PaneviewComponentOptions` passthrough (disableDnd, …).
30
+ *
31
+ * `createComponent` / `createHeaderComponent` are owned by the library
32
+ * (Svelte `mount` factories backed by the `widgets` registry) and cannot
33
+ * be overridden.
34
+ */
35
+ options?: Omit<PaneviewComponentOptions, 'createComponent' | 'createHeaderComponent'>
36
+ /** `bind:layout` — paneview JSON (`toJSON`/`fromJSON` shape). */
37
+ layout?: SerializedPaneview
38
+ /** `bind:handle` — `{ api, openPanel, registerWidget, removePanel, movePanel, setVisible, setExpanded }`. */
39
+ handle?: PaneviewHandle<W>
40
+ /**
41
+ * `bind:panels` — reactive list of current panes (`api.panels` snapshot).
42
+ * Refreshed on layout/add/remove/FromJSON, so `panels.length` is the
43
+ * sveltish pane count — no manual `onDidAddView` counting needed.
44
+ */
45
+ panels?: IPaneviewPanel[]
46
+ /** Extra classes for the root container. */
47
+ class?: string
48
+ /** Declarative widgets (`<DvWidget>` children) — alternative to the `widgets` prop. */
49
+ children?: Snippet
50
+ /** Fired once the component is mounted and `api` is ready. */
51
+ onReady?: (event: { api: PaneviewApi; handle: PaneviewHandle<W> }) => void
52
+ onDidLayoutChange?: () => void
53
+ onDidLayoutFromJSON?: () => void
54
+ onDidAddView?: (panel: IPaneviewPanel) => void
55
+ onDidRemoveView?: (panel: IPaneviewPanel) => void
56
+ onDidDrop?: (event: PaneviewDidDropEvent) => void
57
+ onUnhandledDragOver?: (event: PaneviewDndOverlayEvent) => void
58
+ }
59
+
60
+ let {
61
+ widgets = {} as W,
62
+ options = {},
63
+ layout = $bindable(),
64
+ handle = $bindable(),
65
+ panels = $bindable([]),
66
+ class: className = '',
67
+ children,
68
+ onReady,
69
+ onDidLayoutChange,
70
+ onDidLayoutFromJSON,
71
+ onDidAddView,
72
+ onDidRemoveView,
73
+ onDidDrop,
74
+ onUnhandledDragOver,
75
+ }: Props<W> = $props()
76
+
77
+ let container: HTMLElement
78
+ let api = $state<PaneviewApi | undefined>(undefined)
79
+
80
+ const registry = new PaneviewWidgetRegistry()
81
+ const counters = new Map<string, number>()
82
+
83
+ // Stable `registerWidget`, shared by context and handle.
84
+ function registerWidget(key: string, def: PaneviewWidgetDefinition): void {
85
+ registry.register(key, def)
86
+ }
87
+
88
+ function unregisterWidget(key: string): void {
89
+ registry.unregister(key)
90
+ }
91
+
92
+ // Context for descendant widgets (api is populated after mount).
93
+ const context = $state<PaneviewContext>({
94
+ api: undefined,
95
+ registerWidget,
96
+ unregisterWidget,
97
+ kind: 'paneview',
98
+ })
99
+ setContext(DOCKVIEW_CONTEXT_KEY, context)
100
+
101
+ // Seed the registry from the `widgets` prop (additive).
102
+ $effect(() => {
103
+ registry.seed(widgets)
104
+ })
105
+
106
+ function nextId(key: string): string {
107
+ const n = (counters.get(key) ?? 0) + 1
108
+ counters.set(key, n)
109
+ return `${key}-${n}`
110
+ }
111
+
112
+ /** `bind:layout` loop-break: the JSON we last emitted to the parent. */
113
+ let lastEmitted: SerializedPaneview | undefined
114
+
115
+ /** Serialize the current layout and push it to the bound `layout`. */
116
+ function emitLayout(): void {
117
+ if (!api) return
118
+ const json = api.toJSON()
119
+ lastEmitted = json
120
+ layout = json
121
+ refreshPanels()
122
+ }
123
+
124
+ /** Refresh `bind:panels` from the live panes. */
125
+ function refreshPanels(): void {
126
+ if (!api) return
127
+ panels = [...api.panels]
128
+ }
129
+
130
+ function getPanelOrThrow(id: string): import('dockview').PaneviewPanel {
131
+ const panel = api!.getPanel(id)
132
+ if (!panel) {
133
+ throw new Error(`dockview-svelte: unknown panel "${id}"`)
134
+ }
135
+ return panel as import('dockview').PaneviewPanel
136
+ }
137
+
138
+ function openPanel<K extends keyof W & string>(
139
+ key: K,
140
+ opts?: PaneviewOpenPanelOptions<PaneviewParamsOf<W[K]['component']>>
141
+ ): PaneviewPanelHandle<PaneviewParamsOf<W[K]['component']>> {
142
+ if (!api) {
143
+ throw new Error('dockview-svelte: Paneview is not mounted yet')
144
+ }
145
+ const def = registry.get(key)
146
+ if (!def) {
147
+ throw new Error(`dockview-svelte: unknown widget "${key}"`)
148
+ }
149
+
150
+ const id = opts?.id ?? nextId(key)
151
+ // Title resolves through the same three-tier chain as Dockview
152
+ // (explicit → per-widget → widget key), evaluated once at open time.
153
+ // `headerComponent` reuses the widget key so `createHeaderComponent`
154
+ // can resolve the header off the same registry entry; omit it when the
155
+ // widget defines no header so dockview falls back to `DefaultHeader`.
156
+ const panel = api.addPanel({
157
+ ...(opts ?? {}),
158
+ id,
159
+ component: key,
160
+ headerComponent: def.header ? key : undefined,
161
+ title: opts?.title ?? def.title ?? key,
162
+ params: opts?.params ?? {},
163
+ })
164
+ // `addPanel` fires no observed event for the open path itself — refresh the
165
+ // bound panels/layout directly so `bind:panels` reflects the new pane.
166
+ emitLayout()
167
+
168
+ const state = factory?.getState(id)
169
+ // `state` is `PaneviewState<Record<string, unknown>>` at the factory
170
+ // boundary; `PaneviewState<P>` is invariant in `P` (params is read/write),
171
+ // so the runtime-known `P` requires an `unknown` bridge.
172
+ return { id, panel, api: panel.api, state: state! } as unknown as PaneviewPanelHandle<
173
+ PaneviewParamsOf<W[K]['component']>
174
+ >
175
+ }
176
+
177
+ /** Remove a pane by panel id. */
178
+ function removePanel(id: string): void {
179
+ if (!api) {
180
+ throw new Error('dockview-svelte: Paneview is not mounted yet')
181
+ }
182
+ api.removePanel(getPanelOrThrow(id))
183
+ }
184
+
185
+ /** Move a pane from one index to another. */
186
+ function movePanel(from: number, to: number): void {
187
+ if (!api) {
188
+ throw new Error('dockview-svelte: Paneview is not mounted yet')
189
+ }
190
+ api.movePanel(from, to)
191
+ }
192
+
193
+ /** Show or hide a pane by id (via the panel api — `panel.setVisible` only fires `onDidVisibilityChange` without touching the layout). */
194
+ function setVisible(id: string, visible: boolean): void {
195
+ if (!api) {
196
+ throw new Error('dockview-svelte: Paneview is not mounted yet')
197
+ }
198
+ getPanelOrThrow(id).api.setVisible(visible)
199
+ }
200
+
201
+ /** Expand or collapse a pane by id. */
202
+ function setExpanded(id: string, expanded: boolean): void {
203
+ if (!api) {
204
+ throw new Error('dockview-svelte: Paneview is not mounted yet')
205
+ }
206
+ getPanelOrThrow(id).setExpanded(expanded)
207
+ }
208
+
209
+ let factory: ReturnType<typeof createPaneviewFactory> | undefined
210
+
211
+ // External `layout` changes → apply via `fromJSON` (loop-break via `lastEmitted`).
212
+ $effect(() => {
213
+ const current = api
214
+ if (!current || !layout) return
215
+ if (deepEqual(lastEmitted, layout)) return
216
+ current.fromJSON(layout)
217
+ })
218
+
219
+ // Live `options` changes → apply via `updateOptions`.
220
+ // NOTE: `opts` must be read before the `api` guard — otherwise the
221
+ // initial run returns early without tracking `options`, and later changes
222
+ // never re-trigger the effect.
223
+ $effect(() => {
224
+ const opts = options
225
+ if (!api) return
226
+ api.updateOptions(opts)
227
+ })
228
+
229
+ onMount(() => {
230
+ factory = createPaneviewFactory(registry, context)
231
+ const createdApi = createPaneview(container, {
232
+ ...options,
233
+ createComponent: factory.createComponent,
234
+ createHeaderComponent: factory.createHeaderComponent,
235
+ })
236
+ // Apply a pre-existing `bind:layout` before exposing the api.
237
+ if (layout) {
238
+ createdApi.fromJSON(layout)
239
+ lastEmitted = createdApi.toJSON()
240
+ layout = lastEmitted
241
+ }
242
+ api = createdApi
243
+ context.api = createdApi
244
+
245
+ const createdHandle: PaneviewHandle<W> = {
246
+ api: createdApi,
247
+ openPanel,
248
+ registerWidget,
249
+ removePanel,
250
+ movePanel,
251
+ setVisible,
252
+ setExpanded,
253
+ }
254
+ handle = createdHandle
255
+
256
+ // Bridge paneview events to callbacks, disposing on teardown.
257
+ const subscriptions = [
258
+ createdApi.onDidLayoutChange(() => {
259
+ emitLayout()
260
+ onDidLayoutChange?.()
261
+ }),
262
+ createdApi.onDidLayoutFromJSON(() => {
263
+ refreshPanels()
264
+ onDidLayoutFromJSON?.()
265
+ }),
266
+ createdApi.onDidAddView((panel: IPaneviewPanel) => {
267
+ refreshPanels()
268
+ onDidAddView?.(panel)
269
+ }),
270
+ createdApi.onDidRemoveView((panel: IPaneviewPanel) => {
271
+ refreshPanels()
272
+ onDidRemoveView?.(panel)
273
+ }),
274
+ createdApi.onDidDrop((event: PaneviewDidDropEvent) => {
275
+ onDidDrop?.(event)
276
+ }),
277
+ createdApi.onUnhandledDragOver((event: PaneviewDndOverlayEvent) => {
278
+ onUnhandledDragOver?.(event)
279
+ }),
280
+ ]
281
+ // Seed the initial panels before exposing the handle.
282
+ refreshPanels()
283
+ onReady?.({ api: createdApi, handle: createdHandle })
284
+
285
+ return () => {
286
+ for (const disposable of subscriptions) disposable.dispose()
287
+ createdApi.dispose()
288
+ api = undefined
289
+ factory = undefined
290
+ lastEmitted = undefined
291
+ }
292
+ })
293
+ </script>
294
+
295
+ <div class="dv-svelte-paneview-root {className}" bind:this={container}>
296
+ {#if children}
297
+ {@render children()}
298
+ {/if}
299
+ </div>
300
+
301
+ <style>
302
+ .dv-svelte-paneview-root {
303
+ width: 100%;
304
+ height: 100%;
305
+ position: relative;
306
+ }
307
+ </style>
@@ -0,0 +1,64 @@
1
+ import type { IPaneviewPanel, PaneviewApi, PaneviewComponentOptions, PaneviewDidDropEvent, PaneviewDndOverlayEvent, SerializedPaneview } from 'dockview';
2
+ import { type Snippet } from 'svelte';
3
+ import type { PaneviewHandle, PaneviewWidgets } from '../core/types.js';
4
+ interface Props<W extends PaneviewWidgets> {
5
+ /** Widget registry: `{ [key]: { component, header?, title? } }`. */
6
+ widgets?: W;
7
+ /**
8
+ * `PaneviewComponentOptions` passthrough (disableDnd, …).
9
+ *
10
+ * `createComponent` / `createHeaderComponent` are owned by the library
11
+ * (Svelte `mount` factories backed by the `widgets` registry) and cannot
12
+ * be overridden.
13
+ */
14
+ options?: Omit<PaneviewComponentOptions, 'createComponent' | 'createHeaderComponent'>;
15
+ /** `bind:layout` — paneview JSON (`toJSON`/`fromJSON` shape). */
16
+ layout?: SerializedPaneview;
17
+ /** `bind:handle` — `{ api, openPanel, registerWidget, removePanel, movePanel, setVisible, setExpanded }`. */
18
+ handle?: PaneviewHandle<W>;
19
+ /**
20
+ * `bind:panels` — reactive list of current panes (`api.panels` snapshot).
21
+ * Refreshed on layout/add/remove/FromJSON, so `panels.length` is the
22
+ * sveltish pane count — no manual `onDidAddView` counting needed.
23
+ */
24
+ panels?: IPaneviewPanel[];
25
+ /** Extra classes for the root container. */
26
+ class?: string;
27
+ /** Declarative widgets (`<DvWidget>` children) — alternative to the `widgets` prop. */
28
+ children?: Snippet;
29
+ /** Fired once the component is mounted and `api` is ready. */
30
+ onReady?: (event: {
31
+ api: PaneviewApi;
32
+ handle: PaneviewHandle<W>;
33
+ }) => void;
34
+ onDidLayoutChange?: () => void;
35
+ onDidLayoutFromJSON?: () => void;
36
+ onDidAddView?: (panel: IPaneviewPanel) => void;
37
+ onDidRemoveView?: (panel: IPaneviewPanel) => void;
38
+ onDidDrop?: (event: PaneviewDidDropEvent) => void;
39
+ onUnhandledDragOver?: (event: PaneviewDndOverlayEvent) => void;
40
+ }
41
+ declare function $$render<const W extends PaneviewWidgets>(): {
42
+ props: Props<W>;
43
+ exports: {};
44
+ bindings: "layout" | "handle" | "panels";
45
+ slots: {};
46
+ events: {};
47
+ };
48
+ declare class __sveltets_Render<const W extends PaneviewWidgets> {
49
+ props(): ReturnType<typeof $$render<W>>['props'];
50
+ events(): ReturnType<typeof $$render<W>>['events'];
51
+ slots(): ReturnType<typeof $$render<W>>['slots'];
52
+ bindings(): "layout" | "handle" | "panels";
53
+ exports(): {};
54
+ }
55
+ interface $$IsomorphicComponent {
56
+ new <const W extends PaneviewWidgets>(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']>> & {
57
+ $$bindings?: ReturnType<__sveltets_Render<W>['bindings']>;
58
+ } & ReturnType<__sveltets_Render<W>['exports']>;
59
+ <const W extends PaneviewWidgets>(internal: unknown, props: ReturnType<__sveltets_Render<W>['props']> & {}): ReturnType<__sveltets_Render<W>['exports']>;
60
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
61
+ }
62
+ declare const Paneview: $$IsomorphicComponent;
63
+ type Paneview<const W extends PaneviewWidgets> = InstanceType<typeof Paneview<W>>;
64
+ export default Paneview;
@@ -0,0 +1,332 @@
1
+ <script lang="ts" generics="const W extends SplitviewWidgets">
2
+ import type {
3
+ ISplitviewPanel,
4
+ IView,
5
+ SerializedSplitview,
6
+ SplitviewApi,
7
+ SplitviewComponentOptions,
8
+ } from 'dockview'
9
+ import { createSplitview } from 'dockview'
10
+ import { onMount, type Snippet, setContext } from 'svelte'
11
+ import { DOCKVIEW_CONTEXT_KEY, type SplitviewContext } from '../core/context.js'
12
+ import { SplitviewWidgetRegistry } from '../core/registry.js'
13
+ import { createSplitviewFactory } from '../core/splitview.svelte.js'
14
+ import type {
15
+ SplitviewHandle,
16
+ SplitviewOpenPanelOptions,
17
+ SplitviewPanelHandle,
18
+ SplitviewParamsOf,
19
+ SplitviewWidgetDefinition,
20
+ SplitviewWidgets,
21
+ } from '../core/types.js'
22
+ import { deepEqual } from '../core/utils.js'
23
+
24
+ interface Props<W extends SplitviewWidgets> {
25
+ /** Widget registry: `{ [key]: { component } }` — no tabs or titles, splitviews have no headers. */
26
+ widgets?: W
27
+ /**
28
+ * `SplitviewComponentOptions` passthrough (orientation, proportionalLayout, …).
29
+ *
30
+ * `createComponent` is owned by the library (Svelte `mount` factory
31
+ * backed by the `widgets` registry) and cannot be overridden.
32
+ */
33
+ options?: Omit<SplitviewComponentOptions, 'createComponent'>
34
+ /** `bind:layout` — splitview JSON (`toJSON`/`fromJSON` shape). */
35
+ layout?: SerializedSplitview
36
+ /** `bind:handle` — `{ api, openPanel, registerWidget, removePanel, movePanel, setVisible, setActive }`. */
37
+ handle?: SplitviewHandle<W>
38
+ /**
39
+ * `bind:views` — reactive list of current views (`api.panels` snapshot).
40
+ * Refreshed on layout/add/remove/FromJSON, so `views.length` is the
41
+ * sveltish view count — no manual `onDidAddView` counting needed.
42
+ */
43
+ views?: ISplitviewPanel[]
44
+ /**
45
+ * `bind:activeView` — the currently active pane. `SplitviewApi` exposes
46
+ * no active event (activation only fires per-panel
47
+ * `api.onDidActiveChange`), so this derives from those events plus
48
+ * explicit sync on the open/remove paths (dockview activates the new
49
+ * pane on `addPanel` and the last pane on `removePanel`).
50
+ */
51
+ activeView?: ISplitviewPanel | 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: SplitviewApi; handle: SplitviewHandle<W> }) => void
58
+ onDidLayoutChange?: () => void
59
+ onDidLayoutFromJSON?: () => void
60
+ onDidAddView?: (view: IView) => void
61
+ onDidRemoveView?: (view: IView) => void
62
+ onDidActiveViewChange?: (view: ISplitviewPanel | undefined) => void
63
+ }
64
+
65
+ let {
66
+ widgets = {} as W,
67
+ options = {},
68
+ layout = $bindable(),
69
+ handle = $bindable(),
70
+ views = $bindable([]),
71
+ activeView = $bindable(undefined),
72
+ class: className = '',
73
+ children,
74
+ onReady,
75
+ onDidLayoutChange,
76
+ onDidLayoutFromJSON,
77
+ onDidAddView,
78
+ onDidRemoveView,
79
+ onDidActiveViewChange,
80
+ }: Props<W> = $props()
81
+
82
+ let container: HTMLElement
83
+ let api = $state<SplitviewApi | undefined>(undefined)
84
+
85
+ const registry = new SplitviewWidgetRegistry()
86
+ const counters = new Map<string, number>()
87
+
88
+ // Stable `registerWidget`, shared by context and handle.
89
+ function registerWidget(key: string, def: SplitviewWidgetDefinition): void {
90
+ registry.register(key, def)
91
+ }
92
+
93
+ function unregisterWidget(key: string): void {
94
+ registry.unregister(key)
95
+ }
96
+
97
+ // Context for descendant widgets (api is populated after mount).
98
+ const context = $state<SplitviewContext>({
99
+ api: undefined,
100
+ registerWidget,
101
+ unregisterWidget,
102
+ kind: 'splitview',
103
+ })
104
+ setContext(DOCKVIEW_CONTEXT_KEY, context)
105
+
106
+ // Seed the registry from the `widgets` prop (additive).
107
+ $effect(() => {
108
+ registry.seed(widgets)
109
+ })
110
+
111
+ function nextId(key: string): string {
112
+ const n = (counters.get(key) ?? 0) + 1
113
+ counters.set(key, n)
114
+ return `${key}-${n}`
115
+ }
116
+
117
+ /** `bind:layout` loop-break: the JSON we last emitted to the parent. */
118
+ let lastEmitted: SerializedSplitview | undefined
119
+
120
+ /** Serialize the current layout and push it to the bound `layout`. */
121
+ function emitLayout(): void {
122
+ if (!api) return
123
+ const json = api.toJSON()
124
+ lastEmitted = json
125
+ layout = json
126
+ refreshViews()
127
+ }
128
+
129
+ /** Refresh `bind:views` from the live panels. */
130
+ function refreshViews(): void {
131
+ if (!api) return
132
+ views = [...api.panels]
133
+ }
134
+
135
+ /**
136
+ * Set `bind:activeView` + fire `onDidActiveViewChange`, deduped on panel id.
137
+ * Per-panel `onDidActiveChange` fires on both the deactivated and the
138
+ * activated pane, so the guard keeps a single update per activation.
139
+ */
140
+ function setActiveView(view: ISplitviewPanel | undefined): void {
141
+ if (activeView?.id === view?.id) return
142
+ activeView = view
143
+ onDidActiveViewChange?.(view)
144
+ }
145
+
146
+ function openPanel<K extends keyof W & string>(
147
+ key: K,
148
+ opts?: SplitviewOpenPanelOptions<SplitviewParamsOf<W[K]['component']>>
149
+ ): SplitviewPanelHandle<SplitviewParamsOf<W[K]['component']>> {
150
+ if (!api) {
151
+ throw new Error('dockview-svelte: Splitview is not mounted yet')
152
+ }
153
+ const def = registry.get(key)
154
+ if (!def) {
155
+ throw new Error(`dockview-svelte: unknown widget "${key}"`)
156
+ }
157
+
158
+ const id = opts?.id ?? nextId(key)
159
+ const panel = api.addPanel({
160
+ ...(opts ?? {}),
161
+ id,
162
+ component: key,
163
+ params: opts?.params ?? {},
164
+ })
165
+ // `addPanel` is synchronous but fires no event the component observes for
166
+ // the open path itself (events cover external mutations) — refresh the
167
+ // bound views/layout directly so `bind:views` reflects the new panel.
168
+ // `setActive` inside `addPanel` fires per-panel `onDidActiveChange`,
169
+ // which the factory mirrors to `state.active` but the component does
170
+ // not observe — so sync `bind:activeView` explicitly.
171
+ emitLayout()
172
+ setActiveView(panel)
173
+
174
+ const state = factory?.getState(id)
175
+ // `state` is `SplitviewState<Record<string, unknown>>` at the factory
176
+ // boundary; `SplitviewState<P>` is invariant in `P` (params is read/write),
177
+ // so the runtime-known `P` requires an `unknown` bridge.
178
+ return { id, panel, api: panel.api, state: state! } as unknown as SplitviewPanelHandle<
179
+ SplitviewParamsOf<W[K]['component']>
180
+ >
181
+ }
182
+
183
+ /** Remove a view by panel id. */
184
+ function removePanel(id: string): void {
185
+ if (!api) {
186
+ throw new Error('dockview-svelte: Splitview is not mounted yet')
187
+ }
188
+ const panel = api.getPanel(id)
189
+ if (!panel) {
190
+ throw new Error(`dockview-svelte: unknown panel "${id}"`)
191
+ }
192
+ const wasActive = activeView?.id === panel.id
193
+ api.removePanel(panel)
194
+ // `removePanel` activates the last pane without any active event —
195
+ // fall back to it when the active one was removed.
196
+ if (wasActive) setActiveView(api.panels.at(-1))
197
+ }
198
+
199
+ /** Move a view from one index to another. */
200
+ function movePanel(from: number, to: number): void {
201
+ if (!api) {
202
+ throw new Error('dockview-svelte: Splitview is not mounted yet')
203
+ }
204
+ api.movePanel(from, to)
205
+ }
206
+
207
+ function getPanelOrThrow(id: string): import('dockview').SplitviewPanel {
208
+ const panel = api!.getPanel(id)
209
+ if (!panel) {
210
+ throw new Error(`dockview-svelte: unknown panel "${id}"`)
211
+ }
212
+ return panel as import('dockview').SplitviewPanel
213
+ }
214
+
215
+ /** Show or hide a split pane by id (via the panel api — `panel.setVisible` only fires `onDidVisibilityChange` without touching the layout). */
216
+ function setVisible(id: string, visible: boolean): void {
217
+ if (!api) {
218
+ throw new Error('dockview-svelte: Splitview is not mounted yet')
219
+ }
220
+ getPanelOrThrow(id).api.setVisible(visible)
221
+ }
222
+
223
+ /** Activate a split pane by id (via the panel api — `panel.setActive` only fires `onDidActiveChange`). */
224
+ function setActive(id: string): void {
225
+ if (!api) {
226
+ throw new Error('dockview-svelte: Splitview is not mounted yet')
227
+ }
228
+ getPanelOrThrow(id).api.setActive()
229
+ }
230
+
231
+ let factory: ReturnType<typeof createSplitviewFactory> | undefined
232
+
233
+ // External `layout` changes → apply via `fromJSON` (loop-break via `lastEmitted`).
234
+ $effect(() => {
235
+ const current = api
236
+ if (!current || !layout) return
237
+ if (deepEqual(lastEmitted, layout)) return
238
+ current.fromJSON(layout)
239
+ })
240
+
241
+ // Live `options` changes (orientation, etc.) → apply via `updateOptions`.
242
+ // NOTE: `opts` must be read before the `api` guard — otherwise the
243
+ // initial run returns early without tracking `options`, and later changes
244
+ // never re-trigger the effect.
245
+ $effect(() => {
246
+ const opts = options
247
+ if (!api) return
248
+ api.updateOptions(opts)
249
+ })
250
+
251
+ onMount(() => {
252
+ factory = createSplitviewFactory(registry, context, {
253
+ onDidActiveViewChange: (view) => setActiveView(view as unknown as ISplitviewPanel),
254
+ })
255
+ const createdApi = createSplitview(container, {
256
+ ...options,
257
+ createComponent: factory.createComponent,
258
+ })
259
+ // Apply a pre-existing `bind:layout` before exposing the api.
260
+ if (layout) {
261
+ createdApi.fromJSON(layout)
262
+ lastEmitted = createdApi.toJSON()
263
+ layout = lastEmitted
264
+ }
265
+ api = createdApi
266
+ context.api = createdApi
267
+
268
+ const createdHandle: SplitviewHandle<W> = {
269
+ api: createdApi,
270
+ openPanel,
271
+ registerWidget,
272
+ removePanel,
273
+ movePanel,
274
+ setVisible,
275
+ setActive,
276
+ }
277
+ handle = createdHandle
278
+
279
+ // Bridge splitview events to callbacks, disposing on teardown.
280
+ const subscriptions = [
281
+ createdApi.onDidLayoutChange(() => {
282
+ emitLayout()
283
+ onDidLayoutChange?.()
284
+ }),
285
+ createdApi.onDidLayoutFromJSON(() => {
286
+ refreshViews()
287
+ onDidLayoutFromJSON?.()
288
+ }),
289
+ createdApi.onDidAddView((view: IView) => {
290
+ refreshViews()
291
+ // External adds (e.g. `api.addPanel` escape hatch) activate the
292
+ // new pane without a component-level event — same sync as the
293
+ // `openPanel` path.
294
+ setActiveView(view as unknown as ISplitviewPanel)
295
+ onDidAddView?.(view)
296
+ }),
297
+ createdApi.onDidRemoveView((view: IView) => {
298
+ const removedId = (view as unknown as ISplitviewPanel | undefined)?.id
299
+ refreshViews()
300
+ if (removedId !== undefined && activeView?.id === removedId) {
301
+ setActiveView(createdApi.panels.at(-1))
302
+ }
303
+ onDidRemoveView?.(view)
304
+ }),
305
+ ]
306
+ // Seed the initial views before exposing the handle.
307
+ refreshViews()
308
+ onReady?.({ api: createdApi, handle: createdHandle })
309
+
310
+ return () => {
311
+ for (const disposable of subscriptions) disposable.dispose()
312
+ createdApi.dispose()
313
+ api = undefined
314
+ factory = undefined
315
+ lastEmitted = undefined
316
+ }
317
+ })
318
+ </script>
319
+
320
+ <div class="dv-svelte-splitview-root {className}" bind:this={container}>
321
+ {#if children}
322
+ {@render children()}
323
+ {/if}
324
+ </div>
325
+
326
+ <style>
327
+ .dv-svelte-splitview-root {
328
+ width: 100%;
329
+ height: 100%;
330
+ position: relative;
331
+ }
332
+ </style>