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,70 @@
1
+ import type { ISplitviewPanel, IView, SerializedSplitview, SplitviewApi, SplitviewComponentOptions } from 'dockview';
2
+ import { type Snippet } from 'svelte';
3
+ import type { SplitviewHandle, SplitviewWidgets } from '../core/types.js';
4
+ interface Props<W extends SplitviewWidgets> {
5
+ /** Widget registry: `{ [key]: { component } }` — no tabs or titles, splitviews have no headers. */
6
+ widgets?: W;
7
+ /**
8
+ * `SplitviewComponentOptions` passthrough (orientation, proportionalLayout, …).
9
+ *
10
+ * `createComponent` is owned by the library (Svelte `mount` factory
11
+ * backed by the `widgets` registry) and cannot be overridden.
12
+ */
13
+ options?: Omit<SplitviewComponentOptions, 'createComponent'>;
14
+ /** `bind:layout` — splitview JSON (`toJSON`/`fromJSON` shape). */
15
+ layout?: SerializedSplitview;
16
+ /** `bind:handle` — `{ api, openPanel, registerWidget, removePanel, movePanel, setVisible, setActive }`. */
17
+ handle?: SplitviewHandle<W>;
18
+ /**
19
+ * `bind:views` — reactive list of current views (`api.panels` snapshot).
20
+ * Refreshed on layout/add/remove/FromJSON, so `views.length` is the
21
+ * sveltish view count — no manual `onDidAddView` counting needed.
22
+ */
23
+ views?: ISplitviewPanel[];
24
+ /**
25
+ * `bind:activeView` — the currently active pane. `SplitviewApi` exposes
26
+ * no active event (activation only fires per-panel
27
+ * `api.onDidActiveChange`), so this derives from those events plus
28
+ * explicit sync on the open/remove paths (dockview activates the new
29
+ * pane on `addPanel` and the last pane on `removePanel`).
30
+ */
31
+ activeView?: ISplitviewPanel | undefined;
32
+ /** Extra classes for the root container. */
33
+ class?: string;
34
+ /** Declarative widgets (`<DvWidget>` children) — alternative to the `widgets` prop. */
35
+ children?: Snippet;
36
+ /** Fired once the component is mounted and `api` is ready. */
37
+ onReady?: (event: {
38
+ api: SplitviewApi;
39
+ handle: SplitviewHandle<W>;
40
+ }) => void;
41
+ onDidLayoutChange?: () => void;
42
+ onDidLayoutFromJSON?: () => void;
43
+ onDidAddView?: (view: IView) => void;
44
+ onDidRemoveView?: (view: IView) => void;
45
+ onDidActiveViewChange?: (view: ISplitviewPanel | undefined) => void;
46
+ }
47
+ declare function $$render<const W extends SplitviewWidgets>(): {
48
+ props: Props<W>;
49
+ exports: {};
50
+ bindings: "layout" | "handle" | "views" | "activeView";
51
+ slots: {};
52
+ events: {};
53
+ };
54
+ declare class __sveltets_Render<const W extends SplitviewWidgets> {
55
+ props(): ReturnType<typeof $$render<W>>['props'];
56
+ events(): ReturnType<typeof $$render<W>>['events'];
57
+ slots(): ReturnType<typeof $$render<W>>['slots'];
58
+ bindings(): "layout" | "handle" | "views" | "activeView";
59
+ exports(): {};
60
+ }
61
+ interface $$IsomorphicComponent {
62
+ new <const W extends SplitviewWidgets>(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']>> & {
63
+ $$bindings?: ReturnType<__sveltets_Render<W>['bindings']>;
64
+ } & ReturnType<__sveltets_Render<W>['exports']>;
65
+ <const W extends SplitviewWidgets>(internal: unknown, props: ReturnType<__sveltets_Render<W>['props']> & {}): ReturnType<__sveltets_Render<W>['exports']>;
66
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
67
+ }
68
+ declare const Splitview: $$IsomorphicComponent;
69
+ type Splitview<const W extends SplitviewWidgets> = InstanceType<typeof Splitview<W>>;
70
+ export default Splitview;
@@ -0,0 +1,62 @@
1
+ import type { DockviewApi, GridviewApi, PaneviewApi, SplitviewApi } from 'dockview';
2
+ import type { WidgetRegistry } from './registry.js';
3
+ import type { GridviewWidgetDefinition, PaneviewWidgetDefinition, SplitviewWidgetDefinition } from './types.js';
4
+ /**
5
+ * Context provided by `<Dockview>` to descendant widgets.
6
+ *
7
+ * `api` is populated once the component mounts; read it reactively. The registry
8
+ * and `registerWidget` are stable for the lifetime of the component.
9
+ */
10
+ export interface DockviewContext {
11
+ /** The parent {@link DockviewApi}, available after mount. */
12
+ api: DockviewApi | undefined;
13
+ /** Register (or override) a widget type at runtime. */
14
+ registerWidget: (key: string, def: Parameters<WidgetRegistry['register']>[1]) => void;
15
+ /** Remove a widget type (used when a `<DvWidget>` child is destroyed). */
16
+ unregisterWidget: (key: string) => void;
17
+ /** The layout this context belongs to (used by `<DvWidget>` to validate snippets). */
18
+ kind: 'dockview';
19
+ }
20
+ /**
21
+ * Context provided by `<Splitview>` to descendant widgets.
22
+ * Same shape as {@link DockviewContext} but typed for the splitview api.
23
+ */
24
+ export interface SplitviewContext {
25
+ /** The parent {@link SplitviewApi}, available after mount. */
26
+ api: SplitviewApi | undefined;
27
+ /** Register (or override) a widget type at runtime. */
28
+ registerWidget: (key: string, def: SplitviewWidgetDefinition) => void;
29
+ /** Remove a widget type (used when a `<DvWidget>` child is destroyed). */
30
+ unregisterWidget: (key: string) => void;
31
+ /** The layout this context belongs to (used by `<DvWidget>` to validate snippets). */
32
+ kind: 'splitview';
33
+ }
34
+ /**
35
+ * Context provided by `<Gridview>` to descendant widgets.
36
+ * Same shape as {@link DockviewContext} but typed for the gridview api.
37
+ */
38
+ export interface GridviewContext {
39
+ /** The parent {@link GridviewApi}, available after mount. */
40
+ api: GridviewApi | undefined;
41
+ /** Register (or override) a widget type at runtime. */
42
+ registerWidget: (key: string, def: GridviewWidgetDefinition) => void;
43
+ /** Remove a widget type (used when a `<DvWidget>` child is destroyed). */
44
+ unregisterWidget: (key: string) => void;
45
+ /** The layout this context belongs to (used by `<DvWidget>` to validate snippets). */
46
+ kind: 'gridview';
47
+ }
48
+ /**
49
+ * Context provided by `<Paneview>` to descendant widgets.
50
+ * Same shape as {@link DockviewContext} but typed for the paneview api.
51
+ */
52
+ export interface PaneviewContext {
53
+ /** The parent {@link PaneviewApi}, available after mount. */
54
+ api: PaneviewApi | undefined;
55
+ /** Register (or override) a widget type at runtime. */
56
+ registerWidget: (key: string, def: PaneviewWidgetDefinition) => void;
57
+ /** Remove a widget type (used when a `<DvWidget>` child is destroyed). */
58
+ unregisterWidget: (key: string) => void;
59
+ /** The layout this context belongs to (used by `<DvWidget>` to validate snippets). */
60
+ kind: 'paneview';
61
+ }
62
+ export declare const DOCKVIEW_CONTEXT_KEY: unique symbol;
@@ -0,0 +1 @@
1
+ export const DOCKVIEW_CONTEXT_KEY = Symbol('dockview-svelte.context');
@@ -0,0 +1,7 @@
1
+ import { type vi } from 'vitest';
2
+ /** Await pending `$effect`s so widget → dockview pushes have run. */
3
+ export declare function flushEffects(rounds?: number): Promise<void>;
4
+ /** Assert `updateParameters` was called with a snapshot equal to `expected`. */
5
+ export declare function expectUpdateParameters(api: {
6
+ updateParameters: ReturnType<typeof vi.fn>;
7
+ }, expected: unknown): void;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared helpers for factory double-bind tests.
3
+ *
4
+ * The factories wire widget → dockview pushes through `$effect`s created inside
5
+ * `$effect.root()` during `init()`. Those effects run on Svelte's microtask
6
+ * queue, so tests must `await tick()` after mutating `state` before asserting
7
+ * on the api mocks. `flushSync` does NOT work here — the effects are scheduled
8
+ * async, not batched sync updates.
9
+ */
10
+ import { tick } from 'svelte';
11
+ import { expect } from 'vitest';
12
+ /** Await pending `$effect`s so widget → dockview pushes have run. */
13
+ export async function flushEffects(rounds = 2) {
14
+ for (let i = 0; i < rounds; i++) {
15
+ await tick();
16
+ }
17
+ }
18
+ /** Assert `updateParameters` was called with a snapshot equal to `expected`. */
19
+ export function expectUpdateParameters(api, expected) {
20
+ expect(api.updateParameters).toHaveBeenCalled();
21
+ const last = api.updateParameters.mock.calls.at(-1)[0];
22
+ expect(last).toEqual(expected);
23
+ }
@@ -0,0 +1,16 @@
1
+ import type { CreateComponentOptions, DockviewGroupPanel, IContentRenderer, IHeaderActionsRenderer, ITabRenderer } from 'dockview';
2
+ import { type DockviewContext } from './context.js';
3
+ import type { WidgetRegistry } from './registry.js';
4
+ import type { HeaderActionComponent, PanelState } from './types.js';
5
+ /** Header slot served by {@link createHeaderActionFactories}. */
6
+ export type HeaderActionSlot = 'left' | 'right' | 'prefix';
7
+ /** The renderer factories returned by {@link createDockviewFactory}. */
8
+ export interface DockviewFactory {
9
+ createComponent: (options: CreateComponentOptions) => IContentRenderer;
10
+ createTabComponent: (options: CreateComponentOptions) => ITabRenderer;
11
+ /** Svelte-backed group-header action factories (left/right/prefix slots). */
12
+ createHeaderActionFactories: Record<HeaderActionSlot, ((group: DockviewGroupPanel) => IHeaderActionsRenderer) | undefined>;
13
+ /** Access a panel's shared state by id (used by `openPanel` to build the handle). */
14
+ getState: (id: string) => PanelState | undefined;
15
+ }
16
+ export declare function createDockviewFactory(registry: WidgetRegistry, context?: DockviewContext, headerActions?: Record<HeaderActionSlot, HeaderActionComponent | undefined>): DockviewFactory;
@@ -0,0 +1,248 @@
1
+ import { mount, unmount } from 'svelte';
2
+ import DefaultTab from '../components/DefaultTab.svelte';
3
+ import { DOCKVIEW_CONTEXT_KEY } from './context.js';
4
+ import { deepEqual, mergeInto } from './utils.js';
5
+ /**
6
+ * Creates the content/tab renderer factories backed by a shared widget registry.
7
+ *
8
+ * The content renderer owns the {@link PanelState} (created here as `$state`) and
9
+ * its reactivity wiring: the title mirror and the params double-bind. The tab
10
+ * renderer only mounts its header component against the same state object.
11
+ *
12
+ * Both mounts forward the parent `DockviewContext` via Svelte's `mount`
13
+ * `context` option, so widgets can `getContext(DOCKVIEW_CONTEXT_KEY)`.
14
+ *
15
+ * The empty-state `watermark` is NOT a factory: `Dockview.svelte` renders it
16
+ * as a plain Svelte child overlay when empty, so it gets context automatically.
17
+ */
18
+ /** Create a `$state`-wrapped panel state. `$state` must be a declaration initializer. */
19
+ function createPanelState(api, params, title) {
20
+ const state = $state({
21
+ params: params ?? {},
22
+ size: { width: 0, height: 0 },
23
+ shown: true,
24
+ visible: api.isVisible,
25
+ active: api.isActive,
26
+ focused: api.isFocused,
27
+ pinned: api.isPinned,
28
+ groupActive: api.isGroupActive,
29
+ api,
30
+ title,
31
+ custom: {},
32
+ });
33
+ return state;
34
+ }
35
+ export function createDockviewFactory(registry, context, headerActions) {
36
+ const panels = new Map();
37
+ function getOrCreateState(api, params, title) {
38
+ let entry = panels.get(api.id);
39
+ if (!entry) {
40
+ entry = {
41
+ state: createPanelState(api, params, title),
42
+ contentDisposed: false,
43
+ tabDisposed: false,
44
+ };
45
+ panels.set(api.id, entry);
46
+ }
47
+ return entry;
48
+ }
49
+ function createContentRenderer(options) {
50
+ const element = document.createElement('div');
51
+ element.className = 'dv-svelte-content';
52
+ let instance;
53
+ let entry;
54
+ let state;
55
+ let raf = 0;
56
+ let disposables = [];
57
+ let effectDestroy;
58
+ let lastParams;
59
+ let lastActive = false;
60
+ let lastPinned = false;
61
+ return {
62
+ element,
63
+ init(params) {
64
+ const def = registry.get(options.name);
65
+ if (!def) {
66
+ throw new Error(`dockview-svelte: unknown widget "${options.name}"`);
67
+ }
68
+ entry = getOrCreateState(params.api, params.params, params.title);
69
+ state = entry.state;
70
+ instance = mount(def.component, {
71
+ target: element,
72
+ props: { state },
73
+ ...(context ? { context: new Map([[DOCKVIEW_CONTEXT_KEY, context]]) } : {}),
74
+ });
75
+ lastParams = $state.snapshot(state.params);
76
+ lastActive = state.active;
77
+ lastPinned = state.pinned;
78
+ // Title mirror: dockview owns the title; reflect changes into state.
79
+ disposables.push(params.api.onDidTitleChange((event) => {
80
+ state.title = event.title;
81
+ }));
82
+ // Read-only mirrors of dockview's per-panel booleans.
83
+ disposables.push(params.api.onDidActiveChange((event) => {
84
+ state.active = event.isActive;
85
+ lastActive = event.isActive;
86
+ }), params.api.onDidFocusChange((event) => {
87
+ state.focused = event.isFocused;
88
+ }), params.api.onDidVisibilityChange((event) => {
89
+ state.visible = event.isVisible;
90
+ }), params.api.onDidChangePinned((event) => {
91
+ state.pinned = event.isPinned;
92
+ lastPinned = event.isPinned;
93
+ }), params.api.onDidActiveGroupChange((event) => {
94
+ state.groupActive = event.isActive;
95
+ }));
96
+ // Widget → dockview (params double-bind, activation, pin toggle).
97
+ effectDestroy = $effect.root(() => {
98
+ $effect(() => {
99
+ const snap = $state.snapshot(state.params);
100
+ if (deepEqual(snap, lastParams))
101
+ return;
102
+ lastParams = snap;
103
+ state.api.updateParameters(snap);
104
+ });
105
+ $effect(() => {
106
+ // Only activation is meaningful from the widget side.
107
+ if (state.active && !lastActive) {
108
+ state.api.setActive();
109
+ }
110
+ });
111
+ $effect(() => {
112
+ if (state.pinned !== lastPinned) {
113
+ lastPinned = state.pinned;
114
+ state.api.setPinned(state.pinned);
115
+ }
116
+ });
117
+ });
118
+ },
119
+ update(event) {
120
+ // dockview → widget
121
+ if (!state)
122
+ return;
123
+ mergeInto(state.params, event.params);
124
+ lastParams = $state.snapshot(state.params);
125
+ },
126
+ layout(width, height) {
127
+ cancelAnimationFrame(raf);
128
+ raf = requestAnimationFrame(() => {
129
+ if (!state?.shown)
130
+ return;
131
+ state.size.width = width;
132
+ state.size.height = height;
133
+ });
134
+ },
135
+ onShow() {
136
+ if (state)
137
+ state.shown = true;
138
+ },
139
+ onHide() {
140
+ if (state)
141
+ state.shown = false;
142
+ },
143
+ dispose() {
144
+ cancelAnimationFrame(raf);
145
+ effectDestroy?.();
146
+ effectDestroy = undefined;
147
+ for (const disposable of disposables)
148
+ disposable.dispose();
149
+ disposables = [];
150
+ if (instance) {
151
+ unmount(instance);
152
+ instance = undefined;
153
+ }
154
+ if (entry) {
155
+ entry.contentDisposed = true;
156
+ if (entry.tabDisposed)
157
+ panels.delete(options.id);
158
+ }
159
+ },
160
+ };
161
+ }
162
+ function createTabRenderer(options) {
163
+ const element = document.createElement('div');
164
+ element.className = 'dv-svelte-tab';
165
+ let instance;
166
+ let entry;
167
+ let state;
168
+ return {
169
+ element,
170
+ init(params) {
171
+ const def = registry.get(options.name);
172
+ // Content owns the state; the tab just shares it. Use the default
173
+ // header when the widget has no custom `tab`.
174
+ const tabComponent = def?.tab ?? DefaultTab;
175
+ entry = getOrCreateState(params.api, params.params, params.title);
176
+ state = entry.state;
177
+ instance = mount(tabComponent, {
178
+ target: element,
179
+ props: { state },
180
+ ...(context ? { context: new Map([[DOCKVIEW_CONTEXT_KEY, context]]) } : {}),
181
+ });
182
+ },
183
+ dispose() {
184
+ if (instance) {
185
+ unmount(instance);
186
+ instance = undefined;
187
+ }
188
+ if (entry) {
189
+ entry.tabDisposed = true;
190
+ if (entry.contentDisposed)
191
+ panels.delete(options.id);
192
+ }
193
+ },
194
+ };
195
+ }
196
+ function createHeaderActionFactory(slot, slotComponent) {
197
+ if (!slotComponent)
198
+ return undefined;
199
+ return (group) => {
200
+ const element = document.createElement('div');
201
+ element.className = `dv-svelte-header-actions dv-svelte-header-actions--${slot}`;
202
+ let instance;
203
+ let disposables = [];
204
+ // Reactive group-state mirror — the same idiom as PanelState.
205
+ const state = $state({
206
+ isCollapsed: group.api.isCollapsed(),
207
+ isPeeking: group.api.isPeeking(),
208
+ location: group.api.location,
209
+ });
210
+ return {
211
+ element,
212
+ init(params) {
213
+ disposables.push(group.api.onDidCollapsedChange((event) => {
214
+ state.isCollapsed = event.isCollapsed;
215
+ }), group.api.onDidPeekChange((event) => {
216
+ state.isPeeking = event.isPeeking;
217
+ }), group.api.onDidLocationChange((event) => {
218
+ state.location = event.location;
219
+ }));
220
+ instance = mount(slotComponent, {
221
+ target: element,
222
+ props: { containerApi: params.containerApi, group, state },
223
+ ...(context ? { context: new Map([[DOCKVIEW_CONTEXT_KEY, context]]) } : {}),
224
+ });
225
+ },
226
+ dispose() {
227
+ for (const disposable of disposables)
228
+ disposable.dispose();
229
+ disposables = [];
230
+ if (instance) {
231
+ unmount(instance);
232
+ instance = undefined;
233
+ }
234
+ },
235
+ };
236
+ };
237
+ }
238
+ return {
239
+ createComponent: createContentRenderer,
240
+ createTabComponent: createTabRenderer,
241
+ createHeaderActionFactories: {
242
+ left: createHeaderActionFactory('left', headerActions?.left),
243
+ right: createHeaderActionFactory('right', headerActions?.right),
244
+ prefix: createHeaderActionFactory('prefix', headerActions?.prefix),
245
+ },
246
+ getState: (id) => panels.get(id)?.state,
247
+ };
248
+ }
@@ -0,0 +1,31 @@
1
+ import type { CreateComponentOptions, GridviewPanel as GridviewPanelType } from 'dockview';
2
+ import { type GridviewContext } from './context.js';
3
+ import type { GridviewWidgetRegistry } from './registry.js';
4
+ import type { GridviewState } from './types.js';
5
+ /** The renderer factory returned by {@link createGridviewFactory}. */
6
+ export interface GridviewFactory {
7
+ createComponent: (options: CreateComponentOptions) => GridviewPanelType;
8
+ /** Access a cell's state by id (used by `openPanel` to build the handle). */
9
+ getState: (id: string) => GridviewState | undefined;
10
+ /**
11
+ * Fired when a cell's `api.onDidActiveChange` reports `isActive: true`.
12
+ * The component derives `bind:activePanel` from this — `GridviewApi`'s
13
+ * own `onDidActivePanelChange` only fires on focus-driven activation.
14
+ */
15
+ onDidActivePanelChange: (panel: GridviewPanelType) => void;
16
+ }
17
+ /**
18
+ * Creates the gridview renderer factory backed by a shared widget registry.
19
+ *
20
+ * A gridview cell is a single `GridviewPanel` subclass instance: `getComponent()`
21
+ * returns the `IFrameworkPart` (`{ update, dispose }`) that mounts the Svelte
22
+ * widget into the panel element. `init()` runs before the cell is added to the
23
+ * grid, so the mount happens there; `size` is written via the api's
24
+ * `onDidDimensionsChange` mirror.
25
+ *
26
+ * The mount forwards the parent context via Svelte's `mount` `context` option,
27
+ * so widgets can `getContext(DOCKVIEW_CONTEXT_KEY)`.
28
+ */
29
+ export declare function createGridviewFactory(registry: GridviewWidgetRegistry, context?: GridviewContext, hooks?: {
30
+ onDidActivePanelChange?: (panel: GridviewPanelType) => void;
31
+ }): GridviewFactory;
@@ -0,0 +1,177 @@
1
+ import { GridviewPanel as GridviewPanelBase } from 'dockview';
2
+ import { mount, unmount } from 'svelte';
3
+ import { DOCKVIEW_CONTEXT_KEY } from './context.js';
4
+ import { deepEqual, mergeInto } from './utils.js';
5
+ /** Create a `$state`-wrapped gridview state. `$state` must be a declaration initializer. */
6
+ function createGridviewState(api, params) {
7
+ const state = $state({
8
+ params: params ?? {},
9
+ size: { width: 0, height: 0 },
10
+ visible: api.isVisible,
11
+ active: api.isActive,
12
+ focused: api.isFocused,
13
+ api,
14
+ custom: {},
15
+ });
16
+ return state;
17
+ }
18
+ /**
19
+ * A gridview cell that mounts a Svelte widget into `this.element` and wires
20
+ * the reactive {@link GridviewState}.
21
+ *
22
+ * Same pattern as {@link SvelteSplitviewPanel}: `GridviewPanel` carries its
23
+ * view through the abstract `getComponent(): IFrameworkPart`. The subclass
24
+ * overrides `init()` to mount (once `this.api`/`this.element` exist and params
25
+ * arrive) and `update()` for the dockview → widget params merge.
26
+ *
27
+ * `getComponent().update` is deliberately a no-op: params flow through the
28
+ * `update(event)` override instead — a single, well-typed path.
29
+ */
30
+ class SvelteGridviewPanel extends GridviewPanelBase {
31
+ deps;
32
+ instance;
33
+ entry;
34
+ state;
35
+ disposables = [];
36
+ effectDestroy;
37
+ lastParams;
38
+ lastActive = false;
39
+ lastVisible = true;
40
+ raf = 0;
41
+ constructor(id, name, deps) {
42
+ super(id, name);
43
+ this.deps = deps;
44
+ }
45
+ init(parameters) {
46
+ super.init(parameters);
47
+ const { widget, context, getOrCreateState } = this.deps;
48
+ this.entry = getOrCreateState(this.api, parameters.params);
49
+ this.state = this.entry.state;
50
+ this.instance = mount(widget, {
51
+ target: this.element,
52
+ props: { state: this.state },
53
+ ...(context ? { context: new Map([[DOCKVIEW_CONTEXT_KEY, context]]) } : {}),
54
+ });
55
+ this.lastParams = $state.snapshot(this.state.params);
56
+ this.lastActive = this.state.active;
57
+ this.lastVisible = this.state.visible;
58
+ // Read-only mirrors of the api flags. `size` is rAF-throttled —
59
+ // `onDidDimensionsChange` fires per-pixel on sash drags, exactly like the
60
+ // Dockview `layout()` hook.
61
+ this.disposables.push(this.api.onDidActiveChange((event) => {
62
+ this.state.active = event.isActive;
63
+ this.lastActive = event.isActive;
64
+ // Activation (button, `setActive`, focus, programmatic add)
65
+ // lands here on the newly-active cell — the component derives
66
+ // `bind:activePanel` from it. Deactivation (`false`) is
67
+ // ignored; the incoming cell's `true` wins without ordering
68
+ // hazards.
69
+ if (event.isActive)
70
+ this.deps.notifyActive(this);
71
+ }), this.api.onDidFocusChange((event) => {
72
+ this.state.focused = event.isFocused;
73
+ }), this.api.onDidVisibilityChange((event) => {
74
+ this.state.visible = event.isVisible;
75
+ this.lastVisible = event.isVisible;
76
+ }), this.api.onDidDimensionsChange((event) => {
77
+ cancelAnimationFrame(this.raf);
78
+ this.raf = requestAnimationFrame(() => {
79
+ this.state.size.width = event.width;
80
+ this.state.size.height = event.height;
81
+ });
82
+ }));
83
+ // Widget → dockview (params double-bind, activation).
84
+ this.effectDestroy = $effect.root(() => {
85
+ $effect(() => {
86
+ const snap = $state.snapshot(this.state.params);
87
+ if (deepEqual(snap, this.lastParams))
88
+ return;
89
+ this.lastParams = snap;
90
+ this.state.api.updateParameters(snap);
91
+ });
92
+ $effect(() => {
93
+ // Only activation is meaningful from the widget side.
94
+ if (this.state.active && !this.lastActive) {
95
+ this.state.api.setActive();
96
+ }
97
+ });
98
+ $effect(() => {
99
+ if (this.state.visible !== this.lastVisible) {
100
+ this.lastVisible = this.state.visible;
101
+ this.state.api.setVisible(this.state.visible);
102
+ }
103
+ });
104
+ });
105
+ }
106
+ update(event) {
107
+ super.update(event);
108
+ if (!this.state)
109
+ return;
110
+ mergeInto(this.state.params, event.params);
111
+ this.lastParams = $state.snapshot(this.state.params);
112
+ }
113
+ getComponent() {
114
+ return {
115
+ update: () => {
116
+ // Params flow through `update(event)` above, not here (see class doc).
117
+ },
118
+ dispose: () => {
119
+ cancelAnimationFrame(this.raf);
120
+ this.effectDestroy?.();
121
+ this.effectDestroy = undefined;
122
+ for (const disposable of this.disposables)
123
+ disposable.dispose();
124
+ this.disposables = [];
125
+ if (this.instance) {
126
+ unmount(this.instance);
127
+ this.instance = undefined;
128
+ }
129
+ if (this.entry && !this.entry.disposed) {
130
+ this.entry.disposed = true;
131
+ this.deps.releaseState(this.id);
132
+ }
133
+ },
134
+ };
135
+ }
136
+ }
137
+ /**
138
+ * Creates the gridview renderer factory backed by a shared widget registry.
139
+ *
140
+ * A gridview cell is a single `GridviewPanel` subclass instance: `getComponent()`
141
+ * returns the `IFrameworkPart` (`{ update, dispose }`) that mounts the Svelte
142
+ * widget into the panel element. `init()` runs before the cell is added to the
143
+ * grid, so the mount happens there; `size` is written via the api's
144
+ * `onDidDimensionsChange` mirror.
145
+ *
146
+ * The mount forwards the parent context via Svelte's `mount` `context` option,
147
+ * so widgets can `getContext(DOCKVIEW_CONTEXT_KEY)`.
148
+ */
149
+ export function createGridviewFactory(registry, context, hooks) {
150
+ const panels = new Map();
151
+ function getOrCreateState(api, params) {
152
+ let entry = panels.get(api.id);
153
+ if (!entry) {
154
+ entry = { state: createGridviewState(api, params), disposed: false };
155
+ panels.set(api.id, entry);
156
+ }
157
+ return entry;
158
+ }
159
+ function createComponent(options) {
160
+ const def = registry.get(options.name);
161
+ if (!def) {
162
+ throw new Error(`dockview-svelte: unknown widget "${options.name}"`);
163
+ }
164
+ return new SvelteGridviewPanel(options.id, options.name, {
165
+ widget: def.component,
166
+ context,
167
+ getOrCreateState,
168
+ releaseState: (id) => panels.delete(id),
169
+ notifyActive: (panel) => hooks?.onDidActivePanelChange?.(panel),
170
+ });
171
+ }
172
+ return {
173
+ createComponent,
174
+ getState: (id) => panels.get(id)?.state,
175
+ onDidActivePanelChange: (panel) => hooks?.onDidActivePanelChange?.(panel),
176
+ };
177
+ }
@@ -0,0 +1,28 @@
1
+ import { vi } from 'vitest';
2
+ /**
3
+ * Fake {@link PaneviewPanelApi} for tests that need a real `init()` without
4
+ * importing the paneview factory test's hoisted mock.
5
+ */
6
+ export function makePaneviewFakeApi(id) {
7
+ const listeners = {};
8
+ const on = (key) => (cb) => {
9
+ ;
10
+ (listeners[key] ??= []).push(cb);
11
+ return { dispose: vi.fn() };
12
+ };
13
+ return {
14
+ id,
15
+ isVisible: true,
16
+ isActive: false,
17
+ isFocused: false,
18
+ isExpanded: true,
19
+ onDidActiveChange: vi.fn(on('active')),
20
+ onDidFocusChange: vi.fn(on('focus')),
21
+ onDidVisibilityChange: vi.fn(on('visibility')),
22
+ onDidDimensionsChange: vi.fn(on('dimensions')),
23
+ onDidExpansionChange: vi.fn(on('expansion')),
24
+ updateParameters: vi.fn(),
25
+ setActive: vi.fn(),
26
+ setExpanded: vi.fn(),
27
+ };
28
+ }