@proveanything/smartlinks 2.1.7 → 2.1.8

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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.1.7 | Generated: 2026-11-01T12:48:45.040Z
3
+ Version: 2.1.8 | Generated: 2026-11-02T13:05:01.425Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -7539,6 +7539,7 @@ interface IframeResponderOptions {
7539
7539
  ) => Promise<void>;
7540
7540
  onAuthLogout?: () => Promise<void>;
7541
7541
  onRouteChange?: (path: string, state: Record<string, string>) => void;
7542
+ autoHeight?: boolean | { minHeight?: number };
7542
7543
  onResize?: (height: number) => void;
7543
7544
  onError?: (error: Error) => void;
7544
7545
  onReady?: () => void;
@@ -35,10 +35,8 @@ const responder = new smartlinks.IframeResponder({
35
35
  smartlinks.setBearerToken(token);
36
36
  console.log('User logged in:', user);
37
37
  },
38
- onResize: (height) => {
39
- // Update iframe height
40
- iframeElement.style.height = `${height}px`;
41
- },
38
+ // An app in a page: the iframe is as tall as the app's content — the page scrolls, never the iframe.
39
+ autoHeight: { minHeight: 480 },
42
40
  });
43
41
 
44
42
  // Attach to iframe element
@@ -115,9 +113,24 @@ interface IframeResponderOptions {
115
113
  onResize?: (height: number) => void;
116
114
  onError?: (error: Error) => void;
117
115
  onReady?: () => void;
116
+
117
+ // Height
118
+ autoHeight?: boolean | { minHeight?: number }; // size the iframe to the app's content (an app in a page)
118
119
  }
119
120
  ```
120
121
 
122
+ **Height: `autoHeight` or `onResize`.**
123
+
124
+ - **`autoHeight`** — for an app placed in a page (a website, a section of a dashboard). The responder sets
125
+ the iframe's height from the height the app reports, adds the iframe's own border and padding (an
126
+ element with `box-sizing: border-box`, Tailwind's default, otherwise ends up short by its border — a
127
+ pointless scrollbar), and never uses the window-filling height. The iframe's own scrolling stays on as a
128
+ fallback for an app on an older SDK that under-reports its height. `minHeight`
129
+ is the least height it gives. `onResize` still hears each content height.
130
+ - **`onResize`** without `autoHeight` — for a full-screen host that sizes the iframe itself. It receives
131
+ the app's content height whenever the app reports one, **and** on every window resize the window's
132
+ height below the iframe's container.
133
+
121
134
  ## Advanced Examples
122
135
 
123
136
  ### Deep Linking / Route Synchronization
@@ -175,6 +188,8 @@ const responder = new smartlinks.IframeResponder({
175
188
 
176
189
  ### Responsive Height Management
177
190
 
191
+ An app in a page: `autoHeight` (above) does it all. A full-screen host that manages the height itself:
192
+
178
193
  ```typescript
179
194
  const responder = new smartlinks.IframeResponder({
180
195
  collectionId: 'acme-wines',
@@ -442,7 +457,7 @@ import type {
442
457
  3. **Clean up** responder with `destroy()` when unmounting
443
458
  4. **Use version control** to test against development versions
444
459
  5. **Validate tokens** server-side for security
445
- 6. **Set height dynamically** based on content using `onResize`
460
+ 6. **Size the iframe to its content** with `autoHeight` (an app in a page) or `onResize` (a full-screen host)
446
461
  7. **Sync routes** with parent navigation for better UX
447
462
  8. **Set `parentPath`** for routes that have a meaningful parent screen in the app hierarchy
448
463
 
@@ -458,8 +473,9 @@ import type {
458
473
  - Verify token is being set with `setBearerToken()`
459
474
  - Check that auth messages are being received
460
475
 
461
- ### Height not updating
462
- - Implement `onResize` callback
476
+ ### Height not updating, or a scrollbar inside the iframe
477
+ - An app in a page: use `autoHeight` instead of setting the height yourself
478
+ - A full-screen host: implement the `onResize` callback
463
479
  - Ensure iframe has CSS styling for height changes
464
480
  - Check that iframe is sending resize messages
465
481
 
@@ -80,11 +80,17 @@ const responder = new SL.IframeResponder({
80
80
  appId: 'appointments', // the app's id on this collection
81
81
  initialPath: '/my-bookings', // the page's `path`
82
82
  params: { service: 'svc_123' }, // what the page `accepts`
83
- onResize: (h) => { iframe.style.height = `${h}px` },
83
+ autoHeight: { minHeight: 480 }, // the iframe is as tall as the page: your page scrolls, never the iframe
84
84
  })
85
85
  iframe.src = await responder.attach(iframe)
86
86
  ```
87
87
 
88
+ - **Size it with `autoHeight`, not by hand.** The responder sets the iframe's height from the page's own
89
+ height and counts the iframe's border and padding, so there's nothing left to scroll. Setting
90
+ `iframe.style.height` from `onResize` yourself goes wrong in a site: `onResize` also fires on every
91
+ window resize with "the window below the iframe" (for a full-screen host), and a bordered iframe
92
+ (Tailwind's border-box) ends up a few pixels short — a scrollbar inside the frame.
93
+
88
94
  - **The visitor's session goes with it.** The page's API calls are proxied through the site, so it
89
95
  acts as whoever is signed in to the site (or as a guest). Never pass a token in the URL.
90
96
  - **The app must be installed** on the site's collection; its address comes from there.
package/dist/iframe.js CHANGED
@@ -36,16 +36,29 @@ export var iframe;
36
36
  function sendHeight(height, extra) {
37
37
  if (!inIframe())
38
38
  return;
39
- const h = height !== null && height !== void 0 ? height : document.documentElement.scrollHeight;
39
+ const h = height !== null && height !== void 0 ? height : measureHeight();
40
40
  postParentMessage((resizeOptions === null || resizeOptions === void 0 ? void 0 : resizeOptions.messageType) || 'smartlinks:resize', Object.assign(Object.assign({ height: h }, resizeOptions === null || resizeOptions === void 0 ? void 0 : resizeOptions.extra), extra));
41
41
  }
42
42
  iframe.sendHeight = sendHeight;
43
+ /**
44
+ * The page's own height — not the frame's. scrollHeight and clientHeight are never less than the iframe's
45
+ * viewport, so a page that got shorter (a step done, a list emptied) was still reported at the frame's old
46
+ * height and the frame never shrank. When the page overflows the frame, its scrollHeight is the real
47
+ * height (absolutely positioned content included); when it fits, the document's own box is.
48
+ */
43
49
  function measureHeight() {
44
50
  if (!isBrowser())
45
51
  return 0;
46
52
  const doc = document.documentElement;
47
- // Use max of several properties for robustness
48
- return Math.max(doc.scrollHeight, doc.offsetHeight, doc.clientHeight, document.body ? document.body.scrollHeight : 0, document.body ? document.body.offsetHeight : 0);
53
+ if (doc.scrollHeight > doc.clientHeight + 1)
54
+ return doc.scrollHeight;
55
+ const body = document.body;
56
+ let bodyHeight = 0;
57
+ if (body) {
58
+ const cs = window.getComputedStyle(body);
59
+ bodyHeight = body.offsetHeight + (parseFloat(cs.marginTop) || 0) + (parseFloat(cs.marginBottom) || 0);
60
+ }
61
+ return Math.ceil(Math.max(doc.offsetHeight, bodyHeight));
49
62
  }
50
63
  function scheduleManualPolling() {
51
64
  var _a;
@@ -61,33 +74,63 @@ export var iframe;
61
74
  }
62
75
  }, interval);
63
76
  }
77
+ let measureQueued = false;
78
+ /**
79
+ * Measure once, a moment after a burst of changes, however many asked for it. A timer, not
80
+ * requestAnimationFrame: frames are paused in a hidden page and in an off-screen cross-origin iframe (an
81
+ * app below the fold), so the height wasn't reported until it was scrolled into view; timers only slow down.
82
+ */
83
+ function queueMeasure() {
84
+ if (measureQueued || !isBrowser())
85
+ return;
86
+ measureQueued = true;
87
+ const run = () => {
88
+ measureQueued = false;
89
+ if (!resizeOptions)
90
+ return;
91
+ const h = measureHeight();
92
+ if (resizeOptions.alwaysSend || h !== lastHeight) {
93
+ lastHeight = h;
94
+ sendHeight(h);
95
+ }
96
+ };
97
+ setTimeout(run, 30);
98
+ }
99
+ let lastWidth = 0;
100
+ /** The frame got wider or narrower: the page reflows. (Its height is ours to set — not a reason.) */
101
+ function onWindowResize() {
102
+ if (window.innerWidth === lastWidth)
103
+ return;
104
+ lastWidth = window.innerWidth;
105
+ queueMeasure();
106
+ }
107
+ const CONTENT_EVENTS = ['load', 'transitionend', 'animationend'];
108
+ /**
109
+ * Re-measure when something that changes the page's height happens: its content (any DOM change — a step
110
+ * rendered, times loaded), an image or font arriving, a transition or animation ending, the frame's width.
111
+ * Never the frame's own height: an app held at the frame's height (html/body/root at 100%, a 100vh
112
+ * wrapper — common in app templates) changes size whenever the host resizes the frame, so watching it
113
+ * re-measured after every resize and, with a few px beyond 100vh, grew the frame step by step. Content
114
+ * growing INSIDE such a pinned body never resized it either, so it was never reported (the booking app
115
+ * said "700" once while loading and nothing after).
116
+ */
64
117
  function setupObservers() {
65
118
  if (!isBrowser())
66
119
  return;
67
- // Prefer ResizeObserver for layout changes
68
- if (typeof ResizeObserver !== 'undefined') {
69
- resizeObserver = new ResizeObserver(() => {
70
- const h = measureHeight();
71
- if ((resizeOptions === null || resizeOptions === void 0 ? void 0 : resizeOptions.alwaysSend) || h !== lastHeight) {
72
- lastHeight = h;
73
- sendHeight(h);
74
- }
75
- });
76
- resizeObserver.observe(document.body);
120
+ if (typeof MutationObserver !== 'undefined') {
121
+ mutationObserver = new MutationObserver(queueMeasure);
122
+ mutationObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });
77
123
  }
78
124
  else {
79
- // Fallback: MutationObserver for DOM changes
80
- mutationObserver = new MutationObserver(() => {
81
- const h = measureHeight();
82
- if ((resizeOptions === null || resizeOptions === void 0 ? void 0 : resizeOptions.alwaysSend) || h !== lastHeight) {
83
- lastHeight = h;
84
- sendHeight(h);
85
- }
86
- });
87
- mutationObserver.observe(document.body, { childList: true, subtree: true, attributes: true });
88
- // Manual polling as additional safeguard
89
125
  scheduleManualPolling();
90
126
  }
127
+ for (const e of CONTENT_EVENTS)
128
+ document.addEventListener(e, queueMeasure, true);
129
+ const fonts = document.fonts;
130
+ if (fonts && typeof fonts.addEventListener === 'function')
131
+ fonts.addEventListener('loadingdone', queueMeasure);
132
+ lastWidth = window.innerWidth;
133
+ window.addEventListener('resize', onWindowResize);
91
134
  }
92
135
  /** Enable automatic height reporting to parent iframe. */
93
136
  function enableAutoIframeResize(options) {
@@ -97,10 +140,6 @@ export var iframe;
97
140
  lastHeight = measureHeight();
98
141
  sendHeight(lastHeight);
99
142
  setupObservers();
100
- if (!resizeObserver) {
101
- // If no ResizeObserver, MutationObserver is active and we also poll
102
- scheduleManualPolling();
103
- }
104
143
  }
105
144
  iframe.enableAutoIframeResize = enableAutoIframeResize;
106
145
  /** Disable automatic height reporting. */
@@ -109,8 +148,15 @@ export var iframe;
109
148
  resizeObserver.disconnect();
110
149
  if (mutationObserver)
111
150
  mutationObserver.disconnect();
112
- if (isBrowser())
151
+ if (isBrowser()) {
152
+ for (const e of CONTENT_EVENTS)
153
+ document.removeEventListener(e, queueMeasure, true);
154
+ const fonts = document.fonts;
155
+ if (fonts && typeof fonts.removeEventListener === 'function')
156
+ fonts.removeEventListener('loadingdone', queueMeasure);
157
+ window.removeEventListener('resize', onWindowResize);
113
158
  clearInterval(autoResizeTimer);
159
+ }
114
160
  resizeObserver = undefined;
115
161
  mutationObserver = undefined;
116
162
  autoResizeTimer = undefined;
@@ -61,6 +61,12 @@ export declare class IframeResponder {
61
61
  private resolveAppUrl;
62
62
  private getVersionUrl;
63
63
  private buildIframeSrc;
64
+ /**
65
+ * autoHeight: the iframe exactly as tall as the app's content. The height set on an element with
66
+ * box-sizing: border-box (Tailwind's default) includes its border and padding, so those are added — a
67
+ * 1px border otherwise left the page 2px short and the iframe showed a pointless scrollbar.
68
+ */
69
+ private applyContentHeight;
64
70
  private calculateViewportHeight;
65
71
  private handleMessage;
66
72
  private handleRouteChange;
@@ -77,10 +77,15 @@ export class IframeResponder {
77
77
  // Set up message listener
78
78
  this.messageHandler = this.handleMessage.bind(this);
79
79
  window.addEventListener('message', this.messageHandler);
80
- // Set up resize listener for viewport-based calculations
81
- this.resizeHandler = this.calculateViewportHeight.bind(this);
82
- window.addEventListener('resize', this.resizeHandler);
83
- window.addEventListener('orientationchange', this.resizeHandler);
80
+ // autoHeight: sized to the app's content, so the iframe has nothing to scroll. Its own scrolling is
81
+ // left on: an app on an older SDK may still under-report its height, and a scrollbar beats content
82
+ // cut off.
83
+ if (!this.options.autoHeight) {
84
+ // Set up resize listener for viewport-based calculations (a full-screen host)
85
+ this.resizeHandler = this.calculateViewportHeight.bind(this);
86
+ window.addEventListener('resize', this.resizeHandler);
87
+ window.addEventListener('orientationchange', this.resizeHandler);
88
+ }
84
89
  const src = this.buildIframeSrc();
85
90
  return src;
86
91
  }
@@ -239,6 +244,30 @@ export class IframeResponder {
239
244
  // ===========================================================================
240
245
  // Viewport Resize Calculation
241
246
  // ===========================================================================
247
+ /**
248
+ * autoHeight: the iframe exactly as tall as the app's content. The height set on an element with
249
+ * box-sizing: border-box (Tailwind's default) includes its border and padding, so those are added — a
250
+ * 1px border otherwise left the page 2px short and the iframe showed a pointless scrollbar.
251
+ */
252
+ applyContentHeight(contentHeight) {
253
+ var _a;
254
+ const iframe = this.iframe;
255
+ if (!iframe)
256
+ return;
257
+ const min = typeof this.options.autoHeight === 'object' ? (_a = this.options.autoHeight.minHeight) !== null && _a !== void 0 ? _a : 0 : 0;
258
+ let extra = 0;
259
+ try {
260
+ const cs = window.getComputedStyle(iframe);
261
+ if (cs.boxSizing === 'border-box') {
262
+ extra = ['borderTopWidth', 'borderBottomWidth', 'paddingTop', 'paddingBottom']
263
+ .reduce((n, k) => n + (parseFloat(cs[k]) || 0), 0);
264
+ }
265
+ }
266
+ catch (_b) {
267
+ /* no computed style (not in a document): the content height as it is */
268
+ }
269
+ iframe.style.height = `${Math.ceil(Math.max(min, contentHeight + extra))}px`;
270
+ }
242
271
  calculateViewportHeight() {
243
272
  var _a, _b;
244
273
  if (!this.iframe)
@@ -332,6 +361,8 @@ export class IframeResponder {
332
361
  case 'smartlinks:resize': {
333
362
  const contentHeight = (_a = data.payload) === null || _a === void 0 ? void 0 : _a.height;
334
363
  if (typeof contentHeight === 'number' && contentHeight > 0) {
364
+ if (this.options.autoHeight)
365
+ this.applyContentHeight(contentHeight);
335
366
  (_c = (_b = this.options).onResize) === null || _c === void 0 ? void 0 : _c.call(_b, contentHeight);
336
367
  }
337
368
  break;
package/dist/openapi.yaml CHANGED
@@ -27729,6 +27729,9 @@ components:
27729
27729
  accountData:
27730
27730
  type: object
27731
27731
  additionalProperties: true
27732
+ autoHeight:
27733
+ type: object
27734
+ additionalProperties: true
27732
27735
  required:
27733
27736
  - collectionId
27734
27737
  - appId
@@ -36,6 +36,21 @@ export interface IframeResponderOptions {
36
36
  onAuthLogin?: (token: string, user: any, accountData?: Record<string, any>) => Promise<void>;
37
37
  onAuthLogout?: () => Promise<void>;
38
38
  onRouteChange?: (path: string, state: Record<string, string>) => void;
39
+ /**
40
+ * Size the iframe to the app's content, for an app placed in a page (a booking form in a website, a
41
+ * widget in a section): the page scrolls, never the iframe. The responder sets the iframe's height from
42
+ * the height the app reports, adding the iframe's own border and padding, and never uses the
43
+ * window-filling height. `minHeight`: the least height (px) it's given.
44
+ * Leave off for a full-screen host, which sizes the iframe itself from `onResize`.
45
+ */
46
+ autoHeight?: boolean | {
47
+ minHeight?: number;
48
+ };
49
+ /**
50
+ * A height for the iframe: the app's content height whenever the app reports one, and — unless
51
+ * `autoHeight` is on — the window's height below the iframe's container on every window resize (for a
52
+ * full-screen host). A page that embeds an app should use `autoHeight` instead.
53
+ */
39
54
  onResize?: (height: number) => void;
40
55
  onError?: (error: Error) => void;
41
56
  onReady?: () => void;
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.1.7 | Generated: 2026-11-01T12:48:45.040Z
3
+ Version: 2.1.8 | Generated: 2026-11-02T13:05:01.425Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -7539,6 +7539,7 @@ interface IframeResponderOptions {
7539
7539
  ) => Promise<void>;
7540
7540
  onAuthLogout?: () => Promise<void>;
7541
7541
  onRouteChange?: (path: string, state: Record<string, string>) => void;
7542
+ autoHeight?: boolean | { minHeight?: number };
7542
7543
  onResize?: (height: number) => void;
7543
7544
  onError?: (error: Error) => void;
7544
7545
  onReady?: () => void;
@@ -35,10 +35,8 @@ const responder = new smartlinks.IframeResponder({
35
35
  smartlinks.setBearerToken(token);
36
36
  console.log('User logged in:', user);
37
37
  },
38
- onResize: (height) => {
39
- // Update iframe height
40
- iframeElement.style.height = `${height}px`;
41
- },
38
+ // An app in a page: the iframe is as tall as the app's content — the page scrolls, never the iframe.
39
+ autoHeight: { minHeight: 480 },
42
40
  });
43
41
 
44
42
  // Attach to iframe element
@@ -115,9 +113,24 @@ interface IframeResponderOptions {
115
113
  onResize?: (height: number) => void;
116
114
  onError?: (error: Error) => void;
117
115
  onReady?: () => void;
116
+
117
+ // Height
118
+ autoHeight?: boolean | { minHeight?: number }; // size the iframe to the app's content (an app in a page)
118
119
  }
119
120
  ```
120
121
 
122
+ **Height: `autoHeight` or `onResize`.**
123
+
124
+ - **`autoHeight`** — for an app placed in a page (a website, a section of a dashboard). The responder sets
125
+ the iframe's height from the height the app reports, adds the iframe's own border and padding (an
126
+ element with `box-sizing: border-box`, Tailwind's default, otherwise ends up short by its border — a
127
+ pointless scrollbar), and never uses the window-filling height. The iframe's own scrolling stays on as a
128
+ fallback for an app on an older SDK that under-reports its height. `minHeight`
129
+ is the least height it gives. `onResize` still hears each content height.
130
+ - **`onResize`** without `autoHeight` — for a full-screen host that sizes the iframe itself. It receives
131
+ the app's content height whenever the app reports one, **and** on every window resize the window's
132
+ height below the iframe's container.
133
+
121
134
  ## Advanced Examples
122
135
 
123
136
  ### Deep Linking / Route Synchronization
@@ -175,6 +188,8 @@ const responder = new smartlinks.IframeResponder({
175
188
 
176
189
  ### Responsive Height Management
177
190
 
191
+ An app in a page: `autoHeight` (above) does it all. A full-screen host that manages the height itself:
192
+
178
193
  ```typescript
179
194
  const responder = new smartlinks.IframeResponder({
180
195
  collectionId: 'acme-wines',
@@ -442,7 +457,7 @@ import type {
442
457
  3. **Clean up** responder with `destroy()` when unmounting
443
458
  4. **Use version control** to test against development versions
444
459
  5. **Validate tokens** server-side for security
445
- 6. **Set height dynamically** based on content using `onResize`
460
+ 6. **Size the iframe to its content** with `autoHeight` (an app in a page) or `onResize` (a full-screen host)
446
461
  7. **Sync routes** with parent navigation for better UX
447
462
  8. **Set `parentPath`** for routes that have a meaningful parent screen in the app hierarchy
448
463
 
@@ -458,8 +473,9 @@ import type {
458
473
  - Verify token is being set with `setBearerToken()`
459
474
  - Check that auth messages are being received
460
475
 
461
- ### Height not updating
462
- - Implement `onResize` callback
476
+ ### Height not updating, or a scrollbar inside the iframe
477
+ - An app in a page: use `autoHeight` instead of setting the height yourself
478
+ - A full-screen host: implement the `onResize` callback
463
479
  - Ensure iframe has CSS styling for height changes
464
480
  - Check that iframe is sending resize messages
465
481
 
package/docs/websites.md CHANGED
@@ -80,11 +80,17 @@ const responder = new SL.IframeResponder({
80
80
  appId: 'appointments', // the app's id on this collection
81
81
  initialPath: '/my-bookings', // the page's `path`
82
82
  params: { service: 'svc_123' }, // what the page `accepts`
83
- onResize: (h) => { iframe.style.height = `${h}px` },
83
+ autoHeight: { minHeight: 480 }, // the iframe is as tall as the page: your page scrolls, never the iframe
84
84
  })
85
85
  iframe.src = await responder.attach(iframe)
86
86
  ```
87
87
 
88
+ - **Size it with `autoHeight`, not by hand.** The responder sets the iframe's height from the page's own
89
+ height and counts the iframe's border and padding, so there's nothing left to scroll. Setting
90
+ `iframe.style.height` from `onResize` yourself goes wrong in a site: `onResize` also fires on every
91
+ window resize with "the window below the iframe" (for a full-screen host), and a bordered iframe
92
+ (Tailwind's border-box) ends up a few pixels short — a scrollbar inside the frame.
93
+
88
94
  - **The visitor's session goes with it.** The page's API calls are proxied through the site, so it
89
95
  acts as whoever is signed in to the site (or as a guest). Never pass a token in the URL.
90
96
  - **The app must be installed** on the site's collection; its address comes from there.
package/openapi.yaml CHANGED
@@ -27729,6 +27729,9 @@ components:
27729
27729
  accountData:
27730
27730
  type: object
27731
27731
  additionalProperties: true
27732
+ autoHeight:
27733
+ type: object
27734
+ additionalProperties: true
27732
27735
  required:
27733
27736
  - collectionId
27734
27737
  - appId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.1.7",
3
+ "version": "2.1.8",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",