@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 +18 -1
- package/API.md +287 -54
- package/README.md +48 -17
- package/dist/draggable.d.ts +14 -0
- package/dist/draggable.js +289 -0
- package/dist/iconGrip.d.ts +2 -0
- package/dist/iconGrip.js +24 -0
- package/dist/iconResize.d.ts +2 -0
- package/dist/iconResize.js +18 -0
- package/dist/mod.d.ts +12 -3
- package/dist/mod.js +11 -2
- package/dist/resizable.d.ts +9 -0
- package/dist/resizable.js +138 -0
- package/dist/style-presets.d.ts +9 -0
- package/dist/style-presets.js +23 -1
- package/dist/types.d.ts +154 -0
- package/dist/widget-provider.js +467 -34
- package/docs/architecture.md +36 -10
- package/docs/conventions.md +27 -2
- package/docs/tasks.md +14 -4
- package/package.json +3 -1
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
|
-
- **
|
|
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
|
|
18
|
+
import { provideWidget } from "@marianmeres/widget-provider";
|
|
17
19
|
|
|
18
20
|
const widget = provideWidget({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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,
|
|
97
|
+
resolveAllowedOrigins(undefined, "https://example.com/app");
|
|
41
98
|
// => ['https://example.com']
|
|
42
99
|
|
|
43
|
-
resolveAllowedOrigins([
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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)
|
|
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
|
|
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
|
|
26
|
+
import { provideWidget } from "@marianmeres/widget-provider";
|
|
26
27
|
|
|
27
28
|
const widget = provideWidget({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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(
|
|
51
|
+
widget.send("greet", { name: "World" });
|
|
41
52
|
|
|
42
53
|
// Listen for messages from the iframe
|
|
43
|
-
const unsub = widget.onMessage(
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
| `"inline"`
|
|
61
|
-
| `"float"`
|
|
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`, `
|
|
68
|
-
`
|
|
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;
|