vue-dockable-desktop 1.1.1 → 1.1.2

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,106 @@ correspondence lives, alongside the feature-by-feature map in [docs/PARITY.md](d
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.1.2] — 2026-09-24
15
+
16
+ **Parity: react-dockable-desktop 6.3.1.**
17
+
18
+ Fixes from a user bug report. Every item was reproduced before it was fixed, and each fix has a
19
+ regression test that fails against 1.1.1. No public API changes: the export list in
20
+ `api-surface.json` is unchanged.
21
+
22
+ ### Fixed
23
+
24
+ - **One Escape could close several things.** Modals, drawers, the context menu, the toolbar
25
+ flyout and the toolbar search all listen for Escape, most of them on `document`, and the
26
+ answering modal's `stopPropagation()` never stopped the other listeners there. So a drawer
27
+ opened *after* a modal closed along with it; a context menu, toolbar flyout or toolbar search
28
+ inside a modal or drawer closed its host too; and with a drawer on each side, one press
29
+ closed both. Whoever acts on Escape now claims the event, and every other listener checks for
30
+ a claim first. Transient UI answers before overlays: the menu and flyout listen in the
31
+ capture phase, and the search claims on its own input. With two drawers open, the one opened
32
+ last closes first. An application widget inside a modal can keep the modal open by calling
33
+ `event.preventDefault()` on its own Escape. [ADR 0016](docs/decisions/0016-escape-claiming.md),
34
+ divergence D17 in [PARITY.md](docs/PARITY.md). Covered by `test/components/escapeRouting.test.ts`.
35
+
36
+ - **A custom context-menu slot closed before its items could be clicked.** In slot mode the
37
+ outside-press check had no element for the slot, so the `pointerdown` of any click inside it
38
+ counted as outside and closed the menu; the `click` then never reached the item. The slot is
39
+ now wrapped in a `display: contents` element that counts as inside. The docs said dismissal
40
+ "becomes yours" in slot mode; it does not — Escape and an outside press still close the menu.
41
+
42
+ - **`usePanel()` did not work inside a modal or side panel.** It looked the panel up in the
43
+ docked-panel state, which holds no overlays. So `onBeforeClose` registered nothing, and Escape
44
+ or `close()` closed the overlay regardless; `onSaveState` and `minimize` warned that the panel
45
+ was "rendered standalone"; `title` returned the instance id, and `dirty` stayed `false` after
46
+ `setDirty(true)`. The overlay now supplies its title, dirty state and guard registration, and
47
+ `onSaveState` and `minimize` say accurately why they do nothing there.
48
+
49
+ - **A toast adapter's `show` was never called.** A new toast only went into the built-in queue;
50
+ only a repeat of its id reached the adapter, as `update`. The queued records were never
51
+ removed either, so they piled up and all appeared at once if the adapter was later unset.
52
+ In adapter mode nothing is queued now: a new id goes to `show` — with `type` and the
53
+ container's `defaultDuration` and `defaultClosable` filled in — a repeated id to `update`,
54
+ and a dismissed id used again is a new `show`. Toasts raised before the adapter mounts are
55
+ handed to it. A toast's `onClose` is not called in adapter mode, and the docs now say so.
56
+
57
+ - **`loadLayout` accepted a partial layout and cleared the workspace.** Valid JSON with a
58
+ `gridRoot` but without `floating`, `minimized` or `panels` parsed to an *empty* workspace,
59
+ which `loadLayout` applied and reported as success — every open panel closed, and `true`
60
+ returned. It now returns `false` with a warning and leaves the layout unchanged.
61
+
62
+ - **Right-to-left gestures went the wrong way.** A pointer delta is physical, while the sizes,
63
+ sides and tab indexes it changes are logical, and a flex row reverses under RTL:
64
+ - a horizontal split divider moved away from the pointer;
65
+ - a sidebar resizer shrank the drawer when dragged away from its edge;
66
+ - a toolbar flyout was placed by the workspace's direction, though the strip sits outside the
67
+ workspace and follows the page — so an RTL workspace in an LTR page opened a left strip's
68
+ flyout off-screen, and the viewport clamp pulled it back over the strip;
69
+ - a tab dropped on another tab's left half was inserted on its right, and vice versa.
70
+
71
+ Each gesture now reads its own element's computed direction when it starts. The manual's RTL
72
+ section says that `<VddSidebar>` and `<VddToolbar>` follow the page's `dir`, not the
73
+ workspace's.
74
+
75
+ - **`.vdd-fill-viewport` did nothing on `<VddDesktop>` itself.** There the class shares an
76
+ element with `.vdd-workspace`, whose `height: 100%` has equal specificity and comes later. A
77
+ compound selector now outranks it. The zero-height warning, which recommends the class, says
78
+ where it can go.
79
+
80
+ - **A quick click on a taskbar icon could land on its preview.** The preview rose into place
81
+ from below — starting at 90% of its lift, over the icon — so for its first frames a click or
82
+ right-click meant for the icon hit the preview; and its hover bridge reached 4px past the gap
83
+ even at rest. It now drifts down into place from just above, so neither the preview nor its
84
+ bridge ever covers the icon, and the bridge spans exactly the 8px gap. Measured frame by
85
+ frame in Chrome: a start below the resting place, even by 6px, still put the bridge over the
86
+ top of the icon.
87
+
88
+ - **A collapsed toolbar left a 1px line and kept its buttons in the Tab order.** Collapsing only
89
+ set the strip's size to zero. It now also removes the edge border and makes the strip `inert`.
90
+
91
+ - **Invalid ARIA.** Toolbar flyout tools were `role="menuitem"` with `aria-pressed`; they are
92
+ now `menuitemradio` with `aria-checked`. Checkbox context-menu items carried `aria-checked` on
93
+ a plain `menuitem`; they are now `menuitemcheckbox`. The toast container's `aria-label` sat on
94
+ a `<div>` with no role, where it is ignored; it now has `role="region"`.
95
+
96
+ - **An event map declared as an `interface` failed to compile.** The manual shows
97
+ `interface MyEvents { … }`, but the type parameter was constrained to
98
+ `Record<string, unknown>`, which an interface does not satisfy (TS2344). The constraint on
99
+ `createWorkspace`, `Workspace`, `useWorkspace` and `EventBus` is now `object`, so interfaces
100
+ and type aliases both work. Payloads are still checked against the map.
101
+
102
+ ### Documentation
103
+
104
+ - Removed the `#panel-actions` slot on `<VddDesktop>`, which was documented but never existed
105
+ (manual and PARITY.md); the manual now says where a panel's own actions belong.
106
+ - `updateSplitSizes`: the root branch's path is `[]`; `[0]` is its first child.
107
+ - `<VddFloatingWidget>` takes `widget-id`, not `id`, in the migration guide.
108
+ - A close guard does not replace the unsaved-changes question: it runs first, and a dirty panel
109
+ is still asked about.
110
+ - Only `useSidebar()` works from a panel in the workspace; `useSidebarTab()` needs a drawer tab.
111
+ - Filled in the empty table of theme tokens that have no base value, and corrected the prose
112
+ around it: every rule that reads them has a fallback, and no built-in skin sets them.
113
+
14
114
  ## [1.1.1] — 2026-09-19
15
115
 
16
116
  **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[];
@@ -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;
@@ -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>;