@marianmeres/widget-provider 1.0.2 → 1.1.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/AGENTS.md CHANGED
@@ -1,45 +1,62 @@
1
1
  # @marianmeres/widget-provider — Agent Guide
2
2
 
3
3
  ## Quick Reference
4
+
4
5
  - **Stack**: Deno, TypeScript, browser DOM APIs
5
6
  - **Runtime**: Deno (primary), npm (secondary via build)
6
- - **Dependency**: `@marianmeres/store` (reactive state)
7
+ - **Dependencies**: `@marianmeres/store` (reactive state), `@marianmeres/pubsub` (internal message dispatch), `@marianmeres/clog` (debug logging)
7
8
  - **Test**: `deno test` | **Build**: `deno task npm:build` | **Publish**: `deno task publish`
8
9
 
9
10
  ## Project Structure
11
+
10
12
  ```
11
13
  /src
12
14
  mod.ts — Public entry point (re-exports)
13
15
  types.ts — All type definitions and constants
14
16
  style-presets.ts — CSS preset configs and apply functions
15
17
  widget-provider.ts — Core provideWidget() implementation
18
+ draggable.ts — makeDraggable() for float preset drag-and-drop
19
+ resizable.ts — makeResizable() for float preset resize
20
+ iconGrip.ts — SVG icon for drag handle
21
+ iconResize.ts — SVG icon for resize handle
16
22
  /tests — Deno tests (unit tests for pure functions)
17
23
  /scripts — npm build script
18
24
  /example — Dev example app
19
25
  ```
20
26
 
21
27
  ## What This Library Does
28
+
22
29
  Embeds an iframe-based widget into a host page with:
30
+
23
31
  - Style presets (float, fullscreen, inline) for positioning
24
32
  - postMessage-based bidirectional communication (namespaced with `@@__widget_provider__@@`)
25
33
  - Show/hide animations (fade-scale, slide-up)
34
+ - Height and width control (maximize/minimize/reset for each axis — no-op when preset is inline)
26
35
  - Optional trigger button (auto-toggles with widget visibility)
36
+ - Drag-and-drop with edge-snap (float preset only, via handle bar)
37
+ - Free-resize with corner handle (float preset only)
38
+ - Detach/dock workflow (inline preset only — float the widget, leave placeholder)
39
+ - Small-screen detection with auto-maximize on `open()`
27
40
  - Reactive state via `@marianmeres/store` (Svelte-compatible subscribe)
28
41
 
29
42
  ## Critical Conventions
43
+
30
44
  1. All message types are prefixed with `MSG_PREFIX` (`@@__widget_provider__@@`)
31
45
  2. Types live in `types.ts`, style logic in `style-presets.ts`, core logic in `widget-provider.ts`
32
46
  3. `mod.ts` is the sole public entry point — all public exports go through it
33
47
  4. Use Deno formatting: tabs, 90 char line width (`deno fmt`)
34
48
  5. `provideWidget()` is the only user-facing factory — returns `WidgetProviderApi`
49
+ 6. Preset-specific guards: actions that don't apply to a preset silently no-op (e.g. dimension actions when inline, detach when not inline, draggable/resizable when not float)
35
50
 
36
51
  ## Before Making Changes
52
+
37
53
  - [ ] Check existing patterns in similar files
38
54
  - [ ] Run `deno test`
39
55
  - [ ] Run `deno fmt`
40
56
  - [ ] Ensure all public exports are re-exported from `mod.ts`
41
57
 
42
58
  ## Documentation Index
59
+
43
60
  - [Architecture](./docs/architecture.md)
44
61
  - [Conventions](./docs/conventions.md)
45
62
  - [Tasks](./docs/tasks.md)
package/API.md CHANGED
@@ -7,40 +7,97 @@
7
7
  Create and embed an iframe-based widget. Returns a control API object.
8
8
 
9
9
  **Parameters:**
10
+
10
11
  - `options` (`WidgetProviderOptions`) — Configuration object (see below)
11
12
 
12
13
  **Returns:** `WidgetProviderApi`
13
14
 
14
15
  **Example:**
16
+
15
17
  ```typescript
16
- import { provideWidget } from '@marianmeres/widget-provider';
18
+ import { provideWidget } from "@marianmeres/widget-provider";
17
19
 
18
20
  const widget = provideWidget({
19
- widgetUrl: 'https://example.com/widget',
20
- stylePreset: 'float',
21
- animate: 'slide-up',
22
- trigger: { content: '<span>Chat</span>' },
21
+ widgetUrl: "https://example.com/widget",
22
+ stylePreset: "float",
23
+ animate: "slide-up",
24
+ trigger: { content: "<span>Chat</span>" },
25
+ draggable: true,
23
26
  });
24
27
  ```
25
28
 
26
29
  ---
27
30
 
31
+ ### `makeDraggable(container, iframe, options?)`
32
+
33
+ Make a fixed-position container draggable via a handle bar inserted at the top.
34
+ Uses the Pointer Events API for unified mouse + touch support.
35
+
36
+ Typically used internally by `provideWidget()` when `draggable` option is set
37
+ and preset is `"float"`. Can also be used standalone.
38
+
39
+ **Parameters:**
40
+
41
+ - `container` (`HTMLElement`) — The fixed-position container element
42
+ - `iframe` (`HTMLIFrameElement`) — The iframe inside the container
43
+ - `options` (`DraggableOptions`, optional) — Drag behavior configuration
44
+
45
+ **Returns:** `DraggableHandle`
46
+
47
+ ---
48
+
49
+ ### `makeResizable(container, iframe, options?)`
50
+
51
+ Make a fixed-position container resizable via a corner handle at the bottom-right.
52
+ Uses the Pointer Events API for unified mouse + touch support.
53
+
54
+ Typically used internally by `provideWidget()` when `resizable` option is set
55
+ and preset is `"float"`. Can also be used standalone.
56
+
57
+ **Parameters:**
58
+
59
+ - `container` (`HTMLElement`) — The fixed-position container element
60
+ - `iframe` (`HTMLIFrameElement`) — The iframe inside the container
61
+ - `options` (`ResizableOptions`, optional) — Resize behavior configuration
62
+
63
+ **Returns:** `ResizableHandle`
64
+
65
+ ---
66
+
67
+ ### `resolveEdge(atLeft, atRight, atTop, atBottom)`
68
+
69
+ Pure edge-resolution logic: given which viewport edges are touched,
70
+ return the single active edge, a corner, or `null` if none or ambiguous.
71
+
72
+ **Parameters:**
73
+
74
+ - `atLeft` (`boolean`) — Whether the element is touching the left edge
75
+ - `atRight` (`boolean`) — Whether the element is touching the right edge
76
+ - `atTop` (`boolean`) — Whether the element is touching the top edge
77
+ - `atBottom` (`boolean`) — Whether the element is touching the bottom edge
78
+
79
+ **Returns:** `SnapEdge | null`
80
+
81
+ ---
82
+
28
83
  ### `resolveAllowedOrigins(explicit, widgetUrl)`
29
84
 
30
85
  Resolve the list of allowed origins for postMessage validation.
31
86
 
32
87
  **Parameters:**
88
+
33
89
  - `explicit` (`string | string[] | undefined`) — Explicitly configured origin(s)
34
90
  - `widgetUrl` (`string`) — The widget URL to derive origin from
35
91
 
36
92
  **Returns:** `string[]` — Array of allowed origin strings
37
93
 
38
94
  **Example:**
95
+
39
96
  ```typescript
40
- resolveAllowedOrigins(undefined, 'https://example.com/app');
97
+ resolveAllowedOrigins(undefined, "https://example.com/app");
41
98
  // => ['https://example.com']
42
99
 
43
- resolveAllowedOrigins(['https://a.com', 'https://b.com'], 'https://c.com/app');
100
+ resolveAllowedOrigins(["https://a.com", "https://b.com"], "https://c.com/app");
44
101
  // => ['https://a.com', 'https://b.com']
45
102
  ```
46
103
 
@@ -51,6 +108,7 @@ resolveAllowedOrigins(['https://a.com', 'https://b.com'], 'https://c.com/app');
51
108
  Check whether a given origin is in the allowed list.
52
109
 
53
110
  **Parameters:**
111
+
54
112
  - `origin` (`string`) — Origin to check
55
113
  - `allowed` (`string[]`) — List of allowed origins (use `"*"` to allow any)
56
114
 
@@ -63,6 +121,7 @@ Check whether a given origin is in the allowed list.
63
121
  Resolve animation option into a concrete `AnimateConfig` or `null`.
64
122
 
65
123
  **Parameters:**
124
+
66
125
  - `opt` (`boolean | AnimatePreset | { preset?: AnimatePreset; transition?: string } | undefined`)
67
126
 
68
127
  **Returns:** `AnimateConfig | null`
@@ -75,26 +134,34 @@ Resolve animation option into a concrete `AnimateConfig` or `null`.
75
134
 
76
135
  ```typescript
77
136
  interface WidgetProviderOptions {
78
- /** The URL of the SPA to embed (required) */
79
- widgetUrl: string;
80
- /** DOM element to append the widget into. Default: document.body */
81
- parentContainer?: HTMLElement;
82
- /** Positioning mode. Default: "inline" */
83
- stylePreset?: StylePreset;
84
- /** CSS overrides applied to the container wrapper div */
85
- styleOverrides?: StyleOverrides;
86
- /** Allowed origin(s) for postMessage validation. Derived from widgetUrl if omitted */
87
- allowedOrigin?: string | string[];
88
- /** Whether the widget starts visible. Default: true */
89
- visible?: boolean;
90
- /** Iframe sandbox attribute. Default: "allow-scripts allow-same-origin" */
91
- sandbox?: string;
92
- /** Additional iframe attributes (e.g. allow, referrerpolicy) */
93
- iframeAttrs?: Record<string, string>;
94
- /** Opt-in show/hide animation: true | AnimatePreset | { preset?, transition? } */
95
- animate?: boolean | AnimatePreset | { preset?: AnimatePreset; transition?: string };
96
- /** Built-in floating trigger button: true | { content?, style? } */
97
- trigger?: boolean | { content?: string; style?: Partial<CSSStyleDeclaration> };
137
+ /** The URL of the SPA to embed (required) */
138
+ widgetUrl: string;
139
+ /** DOM element to append the widget into. Default: document.body */
140
+ parentContainer?: HTMLElement;
141
+ /** Positioning mode. Default: "inline" */
142
+ stylePreset?: StylePreset;
143
+ /** CSS overrides applied to the container wrapper div */
144
+ styleOverrides?: StyleOverrides;
145
+ /** Allowed origin(s) for postMessage validation. Derived from widgetUrl if omitted */
146
+ allowedOrigin?: string | string[];
147
+ /** Whether the widget starts visible. Default: true */
148
+ visible?: boolean;
149
+ /** Iframe sandbox attribute. Default: "allow-scripts allow-same-origin" */
150
+ sandbox?: string;
151
+ /** Additional iframe attributes (e.g. allow, referrerpolicy) */
152
+ iframeAttrs?: Record<string, string>;
153
+ /** Opt-in show/hide animation: true | AnimatePreset | { preset?, transition? } */
154
+ animate?: boolean | AnimatePreset | { preset?: AnimatePreset; transition?: string };
155
+ /** Built-in floating trigger button: true | { content?, style? } */
156
+ trigger?: boolean | { content?: string; style?: Partial<CSSStyleDeclaration> };
157
+ /** Enable drag-and-drop for float preset: true | DraggableOptions */
158
+ draggable?: boolean | DraggableOptions;
159
+ /** Enable free-resize for float preset: true | ResizableOptions */
160
+ resizable?: boolean | ResizableOptions;
161
+ /** Placeholder config for detach(). Only relevant for inline preset with parentContainer */
162
+ placeholder?: boolean | PlaceholderOptions;
163
+ /** Viewport width threshold (px) below which open() auto-maximizes. Default: 640. Set to 0 to disable */
164
+ smallScreenBreakpoint?: number;
98
165
  }
99
166
  ```
100
167
 
@@ -104,24 +171,33 @@ interface WidgetProviderOptions {
104
171
 
105
172
  The object returned by `provideWidget()`.
106
173
 
107
- | Method / Property | Signature | Description |
108
- |-------------------|-----------|-------------|
109
- | `show()` | `() => void` | Show the widget container |
110
- | `hide()` | `() => void` | Hide the widget container |
111
- | `toggle()` | `() => void` | Toggle visibility |
112
- | `destroy()` | `() => void` | Remove iframe, listeners, DOM elements. Irreversible |
113
- | `setPreset(preset)` | `(preset: StylePreset) => void` | Switch style preset at runtime |
114
- | `maximize()` | `() => void` | Switch to fullscreen preset |
115
- | `minimize()` | `() => void` | Switch back to initial preset |
116
- | `requestNativeFullscreen()` | `() => Promise<void>` | Browser fullscreen for iframe |
117
- | `exitNativeFullscreen()` | `() => Promise<void>` | Exit browser fullscreen |
118
- | `send(type, payload?)` | `<T>(type: string, payload?: T) => void` | Send message to iframe |
119
- | `onMessage(type, handler)` | `<T>(type: string, handler: (payload: T) => void) => Unsubscribe` | Listen for iframe messages |
120
- | `subscribe(cb)` | `(cb: (state: WidgetState) => void) => Unsubscribe` | Reactive state subscription |
121
- | `get()` | `() => WidgetState` | Get current state snapshot |
122
- | `iframe` | `readonly HTMLIFrameElement` | Direct iframe element reference |
123
- | `container` | `readonly HTMLElement` | Direct container div reference |
124
- | `trigger` | `readonly HTMLElement \| null` | Trigger button reference, or null |
174
+ | Method / Property | Signature | Description |
175
+ | --------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------- |
176
+ | `open()` | `() => void` | Show and auto-maximize on small screens, or minimize on first open |
177
+ | `show()` | `() => void` | Show the widget container |
178
+ | `hide()` | `() => void` | Hide the widget container |
179
+ | `toggle()` | `() => void` | Toggle visibility |
180
+ | `destroy()` | `() => void` | Remove iframe, listeners, DOM elements. Irreversible |
181
+ | `setPreset(preset)` | `(preset: StylePreset) => void` | Switch style preset at runtime |
182
+ | `maximize()` | `() => void` | Switch to fullscreen preset |
183
+ | `minimize()` | `() => void` | Switch back to initial preset |
184
+ | `maximizeHeight(offset?)` | `(offset?: number) => void` | Maximize height keeping width/position. No-op when inline |
185
+ | `minimizeHeight(height?)` | `(height?: number) => void` | Collapse to minimal height (default 48px). No-op when inline |
186
+ | `maximizeWidth(offset?)` | `(offset?: number) => void` | Maximize width keeping height/position. No-op when inline |
187
+ | `minimizeWidth(width?)` | `(width?: number) => void` | Collapse to minimal width (default 48px). No-op when inline |
188
+ | `reset()` | `() => void` | Reset both height and width to current preset's defaults. No-op when inline |
189
+ | `requestNativeFullscreen()` | `() => Promise<void>` | Browser fullscreen for iframe |
190
+ | `exitNativeFullscreen()` | `() => Promise<void>` | Exit browser fullscreen |
191
+ | `detach()` | `() => void` | Float an inline widget to document.body, leaving a placeholder. Inline only |
192
+ | `dock()` | `() => void` | Return a detached widget to its original parentContainer |
193
+ | `send(type, payload?)` | `<T>(type: string, payload?: T) => void` | Send message to iframe |
194
+ | `onMessage(type, handler)` | `<T>(type: string, handler: (payload: T) => void) => Unsubscribe` | Listen for iframe messages |
195
+ | `subscribe(cb)` | `(cb: (state: WidgetState) => void) => Unsubscribe` | Reactive state subscription |
196
+ | `get()` | `() => WidgetState` | Get current state snapshot |
197
+ | `iframe` | `readonly HTMLIFrameElement` | Direct iframe element reference |
198
+ | `container` | `readonly HTMLElement` | Direct container div reference |
199
+ | `trigger` | `readonly HTMLElement \| null` | Trigger button reference, or null |
200
+ | `placeholder` | `readonly HTMLElement \| null` | Placeholder element reference (only present while detached), or null |
125
201
 
126
202
  ---
127
203
 
@@ -129,10 +205,163 @@ The object returned by `provideWidget()`.
129
205
 
130
206
  ```typescript
131
207
  interface WidgetState {
132
- visible: boolean;
133
- ready: boolean;
134
- destroyed: boolean;
135
- preset: StylePreset;
208
+ visible: boolean;
209
+ ready: boolean;
210
+ destroyed: boolean;
211
+ preset: StylePreset;
212
+ heightState: HeightState;
213
+ widthState: WidthState;
214
+ /** Whether the widget has been detached from its parentContainer */
215
+ detached: boolean;
216
+ /** Whether the viewport width is below the configured smallScreenBreakpoint */
217
+ isSmallScreen: boolean;
218
+ }
219
+ ```
220
+
221
+ ---
222
+
223
+ ### `DimensionState`
224
+
225
+ ```typescript
226
+ type DimensionState = "normal" | "minimized" | "maximized";
227
+ ```
228
+
229
+ ---
230
+
231
+ ### `HeightState`
232
+
233
+ ```typescript
234
+ type HeightState = DimensionState;
235
+ ```
236
+
237
+ ---
238
+
239
+ ### `WidthState`
240
+
241
+ ```typescript
242
+ type WidthState = DimensionState;
243
+ ```
244
+
245
+ ---
246
+
247
+ ### `DraggableOptions`
248
+
249
+ ```typescript
250
+ interface DraggableOptions {
251
+ /** Height of the drag handle bar in pixels. Default: 24 */
252
+ handleHeight?: number;
253
+ /** CSS overrides for the drag handle element */
254
+ handleStyle?: Partial<CSSStyleDeclaration>;
255
+ /** Minimum gap (px) between widget edge and viewport edge. Default: 20 */
256
+ boundaryPadding?: number;
257
+ /** Enable edge-snap (ghost preview + maximize on release). true | EdgeSnapOptions | false */
258
+ edgeSnap?: boolean | EdgeSnapOptions;
259
+ /** Called when pointer is released while the edge-snap ghost is showing */
260
+ onEdgeSnap?: (edge: SnapEdge) => void;
261
+ /** Reset-snap: shows a ghost and resets dimensions when dragging away from edges */
262
+ resetSnap?: {
263
+ isActive: () => boolean;
264
+ createGhost: () => HTMLElement;
265
+ };
266
+ /** Called when pointer is released while the reset-snap ghost is showing */
267
+ onResetSnap?: () => void;
268
+ }
269
+ ```
270
+
271
+ ---
272
+
273
+ ### `DraggableHandle`
274
+
275
+ ```typescript
276
+ interface DraggableHandle {
277
+ /** The drag handle DOM element */
278
+ readonly handleEl: HTMLElement;
279
+ /** Remove all event listeners and the handle element */
280
+ destroy(): void;
281
+ /** Reset position to the preset default (clears top/left, restores bottom/right) */
282
+ resetPosition(): void;
283
+ }
284
+ ```
285
+
286
+ ---
287
+
288
+ ### `EdgeSnapOptions`
289
+
290
+ ```typescript
291
+ interface EdgeSnapOptions {
292
+ /** Dwell time (ms) at edge before ghost appears. Default: 500 */
293
+ dwellMs?: number;
294
+ /** CSS overrides for the ghost preview element */
295
+ ghostStyle?: Partial<CSSStyleDeclaration>;
296
+ }
297
+ ```
298
+
299
+ ---
300
+
301
+ ### `SnapEdge`
302
+
303
+ ```typescript
304
+ type SnapEdge =
305
+ | "left"
306
+ | "right"
307
+ | "top"
308
+ | "bottom"
309
+ | "top-left"
310
+ | "top-right"
311
+ | "bottom-left"
312
+ | "bottom-right";
313
+ ```
314
+
315
+ ---
316
+
317
+ ### `ResizableOptions`
318
+
319
+ ```typescript
320
+ interface ResizableOptions {
321
+ /** Size of the resize handle area in pixels. Default: 20 */
322
+ handleSize?: number;
323
+ /** CSS overrides for the resize handle element */
324
+ handleStyle?: Partial<CSSStyleDeclaration>;
325
+ /** Minimum gap (px) between widget edge and viewport edge. Default: 20 */
326
+ boundaryPadding?: number;
327
+ /** Minimum width in pixels. Default: 200 */
328
+ minWidth?: number;
329
+ /** Minimum height in pixels. Default: 150 */
330
+ minHeight?: number;
331
+ /** Maximum width in pixels. Default: viewport width minus padding */
332
+ maxWidth?: number;
333
+ /** Maximum height in pixels. Default: viewport height minus padding */
334
+ maxHeight?: number;
335
+ /** Called when a manual resize interaction ends */
336
+ onResizeEnd?: () => void;
337
+ }
338
+ ```
339
+
340
+ ---
341
+
342
+ ### `ResizableHandle`
343
+
344
+ ```typescript
345
+ interface ResizableHandle {
346
+ /** The resize handle DOM element */
347
+ readonly handleEl: HTMLElement;
348
+ /** Remove all event listeners and the handle element */
349
+ destroy(): void;
350
+ /** Reset size to the preset default (clears inline width/height) */
351
+ resetSize(): void;
352
+ }
353
+ ```
354
+
355
+ ---
356
+
357
+ ### `PlaceholderOptions`
358
+
359
+ ```typescript
360
+ interface PlaceholderOptions {
361
+ /** CSS overrides for the placeholder div */
362
+ style?: Partial<CSSStyleDeclaration>;
363
+ /** HTML content inside the placeholder (e.g., informational text) */
364
+ content?: string;
136
365
  }
137
366
  ```
138
367
 
@@ -142,8 +371,8 @@ interface WidgetState {
142
371
 
143
372
  ```typescript
144
373
  interface WidgetMessage<T = unknown> {
145
- type: string;
146
- payload?: T;
374
+ type: string;
375
+ payload?: T;
147
376
  }
148
377
  ```
149
378
 
@@ -177,9 +406,9 @@ type StyleOverrides = Partial<CSSStyleDeclaration>;
177
406
 
178
407
  ```typescript
179
408
  interface AnimateConfig {
180
- transition: string;
181
- hidden: Partial<CSSStyleDeclaration>;
182
- visible: Partial<CSSStyleDeclaration>;
409
+ transition: string;
410
+ hidden: Partial<CSSStyleDeclaration>;
411
+ visible: Partial<CSSStyleDeclaration>;
183
412
  }
184
413
  ```
185
414
 
@@ -202,3 +431,7 @@ interface AnimateConfig {
202
431
  ### `IFRAME_BASE`
203
432
 
204
433
  `Partial<CSSStyleDeclaration>` — Base CSS applied to all iframes (100% width/height, no border).
434
+
435
+ ### `PLACEHOLDER_BASE`
436
+
437
+ `Partial<CSSStyleDeclaration>` — Default CSS for the detach placeholder element (dashed border, centered content).
package/README.md CHANGED
@@ -5,7 +5,8 @@
5
5
  [![License](https://img.shields.io/npm/l/@marianmeres/widget-provider)](LICENSE)
6
6
 
7
7
  Embed an iframe-based widget into a host page with built-in positioning presets,
8
- bidirectional postMessage communication, show/hide animations, and reactive state.
8
+ bidirectional postMessage communication, show/hide animations, drag-and-drop,
9
+ resize, detach/dock workflow, and reactive state.
9
10
 
10
11
  ## Installation
11
12
 
@@ -22,31 +23,41 @@ deno add jsr:@marianmeres/widget-provider
22
23
  ## Usage
23
24
 
24
25
  ```typescript
25
- import { provideWidget } from '@marianmeres/widget-provider';
26
+ import { provideWidget } from "@marianmeres/widget-provider";
26
27
 
27
28
  const widget = provideWidget({
28
- widgetUrl: 'https://example.com/my-widget',
29
- stylePreset: 'float', // "float" | "fullscreen" | "inline"
30
- animate: true, // fade-scale animation
31
- trigger: true, // show floating trigger button when hidden
29
+ widgetUrl: "https://example.com/my-widget",
30
+ stylePreset: "float", // "float" | "fullscreen" | "inline"
31
+ animate: true, // fade-scale animation
32
+ trigger: true, // show floating trigger button when hidden
33
+ draggable: true, // drag handle for float preset
34
+ resizable: true, // resize handle for float preset
32
35
  });
33
36
 
34
37
  // Control visibility
38
+ widget.open(); // show + auto-maximize on small screens
35
39
  widget.show();
36
40
  widget.hide();
37
41
  widget.toggle();
38
42
 
43
+ // Dimension control (float/fullscreen only — no-op when inline)
44
+ widget.maximizeHeight();
45
+ widget.minimizeHeight();
46
+ widget.maximizeWidth();
47
+ widget.minimizeWidth();
48
+ widget.reset();
49
+
39
50
  // Send messages to the iframe
40
- widget.send('greet', { name: 'World' });
51
+ widget.send("greet", { name: "World" });
41
52
 
42
53
  // Listen for messages from the iframe
43
- const unsub = widget.onMessage('response', (payload) => {
44
- console.log(payload);
54
+ const unsub = widget.onMessage("response", (payload) => {
55
+ console.log(payload);
45
56
  });
46
57
 
47
58
  // Subscribe to reactive state changes
48
59
  widget.subscribe((state) => {
49
- console.log(state.visible, state.ready);
60
+ console.log(state.visible, state.ready, state.heightState, state.detached);
50
61
  });
51
62
 
52
63
  // Clean up
@@ -55,17 +66,37 @@ widget.destroy();
55
66
 
56
67
  ### Style Presets
57
68
 
58
- | Preset | Description |
59
- |--------|-------------|
60
- | `"inline"` | Flows within parent container (default) |
61
- | `"float"` | Fixed bottom-right chat-widget style |
62
- | `"fullscreen"` | Covers viewport with backdrop overlay |
69
+ | Preset | Description |
70
+ | -------------- | --------------------------------------- |
71
+ | `"inline"` | Flows within parent container (default) |
72
+ | `"float"` | Fixed bottom-right chat-widget style |
73
+ | `"fullscreen"` | Covers viewport with backdrop overlay |
74
+
75
+ ### Detach / Dock (inline only)
76
+
77
+ An inline widget can be temporarily detached from its parent container and floated
78
+ on `document.body`, leaving a placeholder behind. Dock returns it to the original
79
+ position.
80
+
81
+ ```typescript
82
+ const widget = provideWidget({
83
+ widgetUrl: "https://example.com/my-widget",
84
+ parentContainer: document.getElementById("sidebar")!,
85
+ stylePreset: "inline",
86
+ placeholder: { content: "Widget is floating..." },
87
+ });
88
+
89
+ widget.detach(); // moves to body, switches to float style
90
+ widget.dock(); // returns to sidebar, restores inline style
91
+ ```
63
92
 
64
93
  ### Message Protocol
65
94
 
66
95
  Messages between the host and iframe are namespaced with `@@__widget_provider__@@`
67
- prefix. The iframe can send built-in control messages: `ready`, `maximize`, `minimize`,
68
- `hide`, `close`, `setPreset`, `nativeFullscreen`, `exitNativeFullscreen`.
96
+ prefix. The iframe can send built-in control messages: `ready`, `open`, `maximize`,
97
+ `minimize`, `maximizeHeight`, `minimizeHeight`, `maximizeWidth`, `minimizeWidth`,
98
+ `reset`, `hide`, `close`, `setPreset`, `detach`, `dock`, `nativeFullscreen`,
99
+ `exitNativeFullscreen`.
69
100
 
70
101
  ## API
71
102
 
@@ -0,0 +1,14 @@
1
+ import type { DraggableHandle, DraggableOptions, SnapEdge } from "./types.js";
2
+ /**
3
+ * Pure edge-resolution logic: given which viewport edges are touched,
4
+ * return the single active edge or `null` if ambiguous (corner) or none.
5
+ */
6
+ export declare function resolveEdge(atLeft: boolean, atRight: boolean, atTop: boolean, atBottom: boolean): SnapEdge | null;
7
+ /**
8
+ * Make a fixed-position container draggable via a handle bar inserted at the top.
9
+ *
10
+ * Uses the Pointer Events API for unified mouse + touch support.
11
+ * The handle uses `setPointerCapture` so all move/up events are reliably delivered
12
+ * even when the pointer leaves the element.
13
+ */
14
+ export declare function makeDraggable(container: HTMLElement, iframe: HTMLIFrameElement, options?: DraggableOptions): DraggableHandle;