vue-dockable-desktop 1.1.1 → 1.2.0

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/CHANGELOG.md CHANGED
@@ -11,6 +11,154 @@ correspondence lives, alongside the feature-by-feature map in [docs/PARITY.md](d
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.2.0] — 2026-09-24
15
+
16
+ **Parity: react-dockable-desktop 6.3.1.**
17
+
18
+ The remaining items from the user bug report that 1.1.2 left for a minor release: they add
19
+ behaviour, change a default, or extend a public type.
20
+
21
+ ### Added
22
+
23
+ - **Tabs work from the keyboard.** Each tab group is one stop in the Tab order — its selected
24
+ tab. ArrowLeft and ArrowRight move to the previous or next tab and show it, wrapping, and
25
+ mirrored under RTL; Delete closes the focused tab, asking first if it has unsaved changes.
26
+ The tab strip is a `tablist` and the body a `tabpanel`; a tab's close × is hidden from
27
+ assistive technology, since Delete and the menu's Close cover it. A focus ring shows for
28
+ keyboard focus only.
29
+
30
+ - **The context menu works from the keyboard.** It takes focus when it opens; ArrowDown and
31
+ ArrowUp move between enabled items, Enter or Space runs one, and Tab or Escape closes it.
32
+ Closing from the keyboard, or by running an item, hands focus back to whatever had it. A
33
+ submenu now also opens on click, which helps on touch screens. A custom menu slot manages
34
+ its own focus.
35
+
36
+ - **`--vdd-font-family`, one token for the font of all library chrome.** Set it to change the
37
+ font of the workspace, sidebar, toolbar, modals, side panels, toasts, menus and flyouts in
38
+ one place, or to `inherit` to use your page's font. See the theming chapter.
39
+
40
+ - **Two message keys: `notifications` and `closeNotification`.** The toast region's
41
+ accessible name and a toast's close button were hard-coded English, as was the sidebar
42
+ drawer's close button (now the existing `close` key). They go through the message table and
43
+ formatter like every other string, and fall back to English where there is no workspace.
44
+
45
+ - **`<VddDesktop>` can be set up without a DOM.** Server-rendering it threw
46
+ `document is not defined` and took the whole page render down. It now renders the chrome and
47
+ leaves the panels to the client. `<ClientOnly>` remains the supported setup under Nuxt or
48
+ other SSR, and the getting-started chapter says so.
49
+
50
+ ### Changed
51
+
52
+ - **The chrome's default font.** The context menu used the system font stack while the rest of
53
+ the chrome used the workspace's, and everything rendered outside the workspace — the sidebar
54
+ and toolbar around it, modals, side panels, toasts, menus and flyouts — took the host page's
55
+ body font. All of it now uses `--vdd-font-family`, which defaults to the workspace's stack.
56
+ To keep the page's font, set `--vdd-font-family: inherit`.
57
+
58
+ - **`MessageKey` has two more keys.** A table typed `Record<MessageKey, string>`, as the i18n
59
+ chapter recommends, fails to compile until it adds `notifications` and `closeNotification`.
60
+ That is the check doing its job; partial `messages` overrides are unaffected.
61
+
62
+ ## [1.1.2] — 2026-09-24
63
+
64
+ **Parity: react-dockable-desktop 6.3.1.**
65
+
66
+ Fixes from a user bug report. Every item was reproduced before it was fixed, and each fix has a
67
+ regression test that fails against 1.1.1. No public API changes: the export list in
68
+ `api-surface.json` is unchanged.
69
+
70
+ ### Fixed
71
+
72
+ - **One Escape could close several things.** Modals, drawers, the context menu, the toolbar
73
+ flyout and the toolbar search all listen for Escape, most of them on `document`, and the
74
+ answering modal's `stopPropagation()` never stopped the other listeners there. So a drawer
75
+ opened *after* a modal closed along with it; a context menu, toolbar flyout or toolbar search
76
+ inside a modal or drawer closed its host too; and with a drawer on each side, one press
77
+ closed both. Whoever acts on Escape now claims the event, and every other listener checks for
78
+ a claim first. Transient UI answers before overlays: the menu and flyout listen in the
79
+ capture phase, and the search claims on its own input. With two drawers open, the one opened
80
+ last closes first. An application widget inside a modal can keep the modal open by calling
81
+ `event.preventDefault()` on its own Escape. [ADR 0016](docs/decisions/0016-escape-claiming.md),
82
+ divergence D17 in [PARITY.md](docs/PARITY.md). Covered by `test/components/escapeRouting.test.ts`.
83
+
84
+ - **A custom context-menu slot closed before its items could be clicked.** In slot mode the
85
+ outside-press check had no element for the slot, so the `pointerdown` of any click inside it
86
+ counted as outside and closed the menu; the `click` then never reached the item. The slot is
87
+ now wrapped in a `display: contents` element that counts as inside. The docs said dismissal
88
+ "becomes yours" in slot mode; it does not — Escape and an outside press still close the menu.
89
+
90
+ - **`usePanel()` did not work inside a modal or side panel.** It looked the panel up in the
91
+ docked-panel state, which holds no overlays. So `onBeforeClose` registered nothing, and Escape
92
+ or `close()` closed the overlay regardless; `onSaveState` and `minimize` warned that the panel
93
+ was "rendered standalone"; `title` returned the instance id, and `dirty` stayed `false` after
94
+ `setDirty(true)`. The overlay now supplies its title, dirty state and guard registration, and
95
+ `onSaveState` and `minimize` say accurately why they do nothing there.
96
+
97
+ - **A toast adapter's `show` was never called.** A new toast only went into the built-in queue;
98
+ only a repeat of its id reached the adapter, as `update`. The queued records were never
99
+ removed either, so they piled up and all appeared at once if the adapter was later unset.
100
+ In adapter mode nothing is queued now: a new id goes to `show` — with `type` and the
101
+ container's `defaultDuration` and `defaultClosable` filled in — a repeated id to `update`,
102
+ and a dismissed id used again is a new `show`. Toasts raised before the adapter mounts are
103
+ handed to it. A toast's `onClose` is not called in adapter mode, and the docs now say so.
104
+
105
+ - **`loadLayout` accepted a partial layout and cleared the workspace.** Valid JSON with a
106
+ `gridRoot` but without `floating`, `minimized` or `panels` parsed to an *empty* workspace,
107
+ which `loadLayout` applied and reported as success — every open panel closed, and `true`
108
+ returned. It now returns `false` with a warning and leaves the layout unchanged.
109
+
110
+ - **Right-to-left gestures went the wrong way.** A pointer delta is physical, while the sizes,
111
+ sides and tab indexes it changes are logical, and a flex row reverses under RTL:
112
+ - a horizontal split divider moved away from the pointer;
113
+ - a sidebar resizer shrank the drawer when dragged away from its edge;
114
+ - a toolbar flyout was placed by the workspace's direction, though the strip sits outside the
115
+ workspace and follows the page — so an RTL workspace in an LTR page opened a left strip's
116
+ flyout off-screen, and the viewport clamp pulled it back over the strip;
117
+ - a tab dropped on another tab's left half was inserted on its right, and vice versa.
118
+
119
+ Each gesture now reads its own element's computed direction when it starts. The manual's RTL
120
+ section says that `<VddSidebar>` and `<VddToolbar>` follow the page's `dir`, not the
121
+ workspace's.
122
+
123
+ - **`.vdd-fill-viewport` did nothing on `<VddDesktop>` itself.** There the class shares an
124
+ element with `.vdd-workspace`, whose `height: 100%` has equal specificity and comes later. A
125
+ compound selector now outranks it. The zero-height warning, which recommends the class, says
126
+ where it can go.
127
+
128
+ - **A quick click on a taskbar icon could land on its preview.** The preview rose into place
129
+ from below — starting at 90% of its lift, over the icon — so for its first frames a click or
130
+ right-click meant for the icon hit the preview; and its hover bridge reached 4px past the gap
131
+ even at rest. It now drifts down into place from just above, so neither the preview nor its
132
+ bridge ever covers the icon, and the bridge spans exactly the 8px gap. Measured frame by
133
+ frame in Chrome: a start below the resting place, even by 6px, still put the bridge over the
134
+ top of the icon.
135
+
136
+ - **A collapsed toolbar left a 1px line and kept its buttons in the Tab order.** Collapsing only
137
+ set the strip's size to zero. It now also removes the edge border and makes the strip `inert`.
138
+
139
+ - **Invalid ARIA.** Toolbar flyout tools were `role="menuitem"` with `aria-pressed`; they are
140
+ now `menuitemradio` with `aria-checked`. Checkbox context-menu items carried `aria-checked` on
141
+ a plain `menuitem`; they are now `menuitemcheckbox`. The toast container's `aria-label` sat on
142
+ a `<div>` with no role, where it is ignored; it now has `role="region"`.
143
+
144
+ - **An event map declared as an `interface` failed to compile.** The manual shows
145
+ `interface MyEvents { … }`, but the type parameter was constrained to
146
+ `Record<string, unknown>`, which an interface does not satisfy (TS2344). The constraint on
147
+ `createWorkspace`, `Workspace`, `useWorkspace` and `EventBus` is now `object`, so interfaces
148
+ and type aliases both work. Payloads are still checked against the map.
149
+
150
+ ### Documentation
151
+
152
+ - Removed the `#panel-actions` slot on `<VddDesktop>`, which was documented but never existed
153
+ (manual and PARITY.md); the manual now says where a panel's own actions belong.
154
+ - `updateSplitSizes`: the root branch's path is `[]`; `[0]` is its first child.
155
+ - `<VddFloatingWidget>` takes `widget-id`, not `id`, in the migration guide.
156
+ - A close guard does not replace the unsaved-changes question: it runs first, and a dirty panel
157
+ is still asked about.
158
+ - Only `useSidebar()` works from a panel in the workspace; `useSidebarTab()` needs a drawer tab.
159
+ - Filled in the empty table of theme tokens that have no base value, and corrected the prose
160
+ around it: every rule that reads them has a fallback, and no built-in skin sets them.
161
+
14
162
  ## [1.1.1] — 2026-09-19
15
163
 
16
164
  **Parity: react-dockable-desktop 6.3.1.**
@@ -12,8 +12,9 @@ type __VLS_Slots = {
12
12
  * and it needs no provider, no ref handshake and no adapter object.
13
13
  *
14
14
  * `items` is the pending menu, `x`/`y` the requested position, and `close` dismisses it.
15
- * Positioning and dismissal are then yours: the built-in clamping, Escape handling and
16
- * outside-click handling belong to the built-in markup this replaces.
15
+ * Positioning and keyboard navigation are then yours: the built-in viewport clamping
16
+ * belongs to the markup this replaces. Dismissal is not — Escape and a press outside the
17
+ * slot still close the menu, and a press inside it does not.
17
18
  */
18
19
  default?: (props: {
19
20
  items: ContextMenuItem[];
@@ -0,0 +1,10 @@
1
+ import type { MessageKey } from '../core/messages';
2
+ /**
3
+ * One of the library's own strings, for a component that may render with no workspace.
4
+ *
5
+ * Toasts work from anywhere and a sidebar can stand alone, so these cannot `useWorkspace()`,
6
+ * which throws without one. With a workspace the string goes through its message table and
7
+ * formatter, exactly as every other library label does; without one it is the English
8
+ * default. Call the returned function during render, so a locale change is followed. @internal
9
+ */
10
+ export declare function useLibraryMessage(): (key: MessageKey) => string;
@@ -12,7 +12,8 @@ import type { OverlayInstance } from '../core/overlays';
12
12
  * **Escape routing** lives here too, and is the reason it can be stated once: Escape belongs
13
13
  * to the topmost modal, and reaches a drawer only when no modal is open. rdd expressed the
14
14
  * same rule twice, in two different shapes (`modals.length === 0` in one,
15
- * `isTopmost` in the other).
15
+ * `isTopmost` in the other). With a drawer on each side and no modal, the one opened last
16
+ * answers. Whoever answers claims the event (`../core/escape`), so one Escape closes one thing.
16
17
  */
17
18
  export declare function useOverlayHost(instance: MaybeRefOrGetter<OverlayInstance>): {
18
19
  /** The instance's title, resolved through the formatter, with a trailing `*` when dirty. */
@@ -15,6 +15,16 @@ export interface PanelContext {
15
15
  width: number;
16
16
  height: number;
17
17
  } | null>;
18
+ /** Live title and dirty state, for a container whose panel is not in `ws.state.panels`. */
19
+ title?: Ref<Label>;
20
+ dirty?: Ref<boolean>;
21
+ /** Register a close guard with the container; returns its disposer. */
22
+ onBeforeClose?: (guard: () => boolean | Promise<boolean>) => () => void;
23
+ /**
24
+ * Set by a modal or side panel: it is not part of the layout, so it has nothing to save
25
+ * and nowhere to minimise to. Lets `usePanel()` say so rather than call it standalone.
26
+ */
27
+ overlay?: boolean;
18
28
  }
19
29
  export declare const PANEL_KEY: InjectionKey<PanelContext>;
20
30
  /** @internal — used by containers to tell their content which panel it is. */
@@ -15,4 +15,4 @@ import type { Workspace } from '../core/workspace';
15
15
  * `subscribe()` is wrapped so a subscription made during `setup` is disposed with the
16
16
  * component. Outside a scope it behaves exactly like `workspace.subscribe`.
17
17
  */
18
- export declare function useWorkspace<TEvents extends Record<string, unknown> = Record<string, unknown>>(): Workspace<TEvents>;
18
+ export declare function useWorkspace<TEvents extends object = Record<string, unknown>>(): Workspace<TEvents>;
@@ -35,6 +35,25 @@ export interface PointerDragConfig<TStart> {
35
35
  * cleans up automatically on release or cancel.
36
36
  */
37
37
  export declare function startPointerDrag<TStart>(config: PointerDragConfig<TStart>): void;
38
+ /**
39
+ * Whether `el` lays out right to left, as the browser computed it.
40
+ *
41
+ * A pointer delta is physical, but a flex row reverses under RTL, so a gesture that changes a
42
+ * logical size or index has to know which way its own element runs. The element's computed
43
+ * direction is the only source that is right everywhere: the sidebar and toolbar sit
44
+ * *outside* the workspace, and take their direction from the host page, not from the
45
+ * workspace's `dir`. Read it once, when the gesture starts.
46
+ */
47
+ export declare function isRtlElement(el: Element | null | undefined): boolean;
48
+ /**
49
+ * Which logical side of a tab the pointer is over: `'left'` means before it in tab order,
50
+ * `'right'` after it — the meaning the insertion index and the RTL indicator CSS both use.
51
+ * Under RTL the physical left half is the logical end.
52
+ */
53
+ export declare function tabSide(clientX: number, rect: {
54
+ left: number;
55
+ width: number;
56
+ }, rtl: boolean): 'left' | 'right';
38
57
  export type ResizeDir = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
39
58
  export interface ResizeRect {
40
59
  x: number;
@@ -0,0 +1,4 @@
1
+ /** Mark an Escape as answered, so no other listener in the library acts on it. */
2
+ export declare function claimEscape(event: Event): void;
3
+ /** Whether a listener in the library, or the application via `preventDefault()`, already answered it. */
4
+ export declare function isEscapeClaimed(event: Event): boolean;
@@ -50,7 +50,7 @@ export interface BuiltInEvents {
50
50
  }[];
51
51
  };
52
52
  }
53
- export declare class EventBus<TEvents extends Record<string, unknown> = Record<string, unknown>> {
53
+ export declare class EventBus<TEvents extends object = Record<string, unknown>> {
54
54
  private listeners;
55
55
  subscribe<K extends keyof (TEvents & BuiltInEvents) & string>(event: K, callback: (data: (TEvents & BuiltInEvents)[K]) => void): () => void;
56
56
  publish<K extends keyof (TEvents & BuiltInEvents) & string>(event: K, data: (TEvents & BuiltInEvents)[K]): void;
@@ -107,6 +107,14 @@ export declare const defaultMessages: {
107
107
  readonly id: "vdd.search";
108
108
  readonly defaultMessage: "Search";
109
109
  };
110
+ readonly notifications: {
111
+ readonly id: "vdd.notifications";
112
+ readonly defaultMessage: "Notifications";
113
+ };
114
+ readonly closeNotification: {
115
+ readonly id: "vdd.closeNotification";
116
+ readonly defaultMessage: "Close notification";
117
+ };
110
118
  };
111
119
  /**
112
120
  * Every message key. Import it in your own message table to get a compile-time guarantee
@@ -13,7 +13,7 @@
13
13
  * workspaces on a page shared panel DOM.
14
14
  */
15
15
  export declare class PanelDomCache {
16
- private readonly doc;
16
+ private readonly ownerDocument?;
17
17
  private elements;
18
18
  /**
19
19
  * Preserved state, **owned per panel and outliving the hidden period**.
@@ -34,7 +34,13 @@ export declare class PanelDomCache {
34
34
  */
35
35
  private sizes;
36
36
  private hidden;
37
- constructor(doc?: Document);
37
+ /**
38
+ * The document is looked up on first use, not in a parameter default: constructing the cache
39
+ * then touches nothing, so `<VddDesktop>` can be set up where there is no DOM (server
40
+ * rendering) as long as no panel element is asked for.
41
+ */
42
+ constructor(ownerDocument?: Document | undefined);
43
+ private get doc();
38
44
  /** The off-screen store. Panels live here while minimised, and before their first host. */
39
45
  hiddenStore(): HTMLElement;
40
46
  /**
@@ -58,6 +58,11 @@ export interface ToastAdapter {
58
58
  dismiss(id?: string): void;
59
59
  component: Component | null;
60
60
  }
61
+ /** What `<VddToasts>` would fill in for an option the caller left out. */
62
+ export interface ToastDefaults {
63
+ duration: number;
64
+ closable: boolean;
65
+ }
61
66
  /** The live queue. Exported for `<VddToasts>` and for tests; applications use `toast`. */
62
67
  export declare const toastQueue: {
63
68
  items: {
@@ -438,6 +443,10 @@ export declare const toastQueue: {
438
443
  __name?: string | undefined;
439
444
  } | null;
440
445
  } | null;
446
+ defaults: {
447
+ duration: number;
448
+ closable: boolean;
449
+ };
441
450
  };
442
451
  /** Begin the exit transition. `<VddToasts>` removes the record when it finishes. */
443
452
  export declare function startExit(id: string): void;
@@ -472,3 +481,11 @@ export interface ToastFunction {
472
481
  export declare const toast: ToastFunction;
473
482
  /** Empty the queue with no animation. For tests and for a full application teardown. */
474
483
  export declare function resetToasts(): void;
484
+ /**
485
+ * Hand `toast.*` to an adapter, or take it back with `null`. Used by `<VddToasts>`.
486
+ *
487
+ * Toasts raised before an adapter arrives are waiting in `items`; they are handed to it, so a
488
+ * toast raised during start-up is not lost. The adapter's ids are forgotten when it changes —
489
+ * a new adapter has shown none of them.
490
+ */
491
+ export declare function setToastAdapter(adapter: ToastAdapter | null, defaults: ToastDefaults): void;
@@ -95,7 +95,7 @@ export interface WorkspaceState {
95
95
  edgeSplitRatio: number;
96
96
  }
97
97
  /** A workspace instance: reactive state, actions, registry, and a Vue plugin. */
98
- export interface Workspace<TEvents extends Record<string, unknown> = Record<string, unknown>> {
98
+ export interface Workspace<TEvents extends object = Record<string, unknown>> {
99
99
  /** The whole state, read-only. Reactive — use it in templates and `computed`. */
100
100
  readonly state: Readonly<WorkspaceState>;
101
101
  /** The panel catalogue. */
@@ -219,4 +219,4 @@ export declare const WORKSPACE_KEY: InjectionKey<Workspace<never>>;
219
219
  * The returned object is both the imperative API and a Vue plugin, matching the
220
220
  * `createPinia()` / `createRouter()` shape a Vue developer already knows.
221
221
  */
222
- export declare function createWorkspace<TEvents extends Record<string, unknown> = Record<string, unknown>>(config?: WorkspaceConfig): Workspace<TEvents>;
222
+ export declare function createWorkspace<TEvents extends object = Record<string, unknown>>(config?: WorkspaceConfig): Workspace<TEvents>;