@proveanything/smartlinks 2.1.7 → 2.1.10
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/dist/docs/API_SUMMARY.md +2 -1
- package/dist/docs/iframe-responder.md +23 -7
- package/dist/docs/theme-tokens.md +8 -0
- package/dist/docs/websites.md +7 -1
- package/dist/idbConnection.d.ts +14 -0
- package/dist/idbConnection.js +91 -0
- package/dist/iframe.js +74 -28
- package/dist/iframeResponder.d.ts +6 -0
- package/dist/iframeResponder.js +35 -4
- package/dist/openapi.yaml +3 -0
- package/dist/persistentCache.js +36 -95
- package/dist/theme.css +20 -0
- package/dist/translationCache.js +49 -114
- package/dist/types/iframeResponder.d.ts +15 -0
- package/docs/API_SUMMARY.md +2 -1
- package/docs/iframe-responder.md +23 -7
- package/docs/theme-tokens.md +8 -0
- package/docs/websites.md +7 -1
- package/openapi.yaml +3 -0
- package/package.json +87 -87
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.10 | Generated: 2026-11-03T16:21:02.896Z
|
|
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
|
-
|
|
39
|
-
|
|
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. **
|
|
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
|
-
-
|
|
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
|
|
|
@@ -99,6 +99,14 @@ renders correctly even against a host that sets none of them.
|
|
|
99
99
|
|
|
100
100
|
Dark mode is a *theme* (a different value-set), not a separate token set — the host swaps the values.
|
|
101
101
|
|
|
102
|
+
- **Host:** in dark mode, send a dark value-set — in the iframe address (`#slt=` with `theme: "dark"`, and the
|
|
103
|
+
legacy `theme=` payload with `m: "d"`) or on the mount root. Sending only the hook (`dark=1`, `.dark`)
|
|
104
|
+
leaves the app to guess.
|
|
105
|
+
- **App:** `theme.css` has dark fallbacks under `.dark` / `[data-theme="dark"]`, so an app built on it stays
|
|
106
|
+
readable even when a host sends the hook and no values. An app with its own CSS variables should do the
|
|
107
|
+
same: a light fallback at the root, a dark one under the hook — never a light colour that also stands
|
|
108
|
+
in for dark.
|
|
109
|
+
|
|
102
110
|
---
|
|
103
111
|
|
|
104
112
|
## 3. Theme value-sets (what the AI emits, what the lookbook stores)
|
package/dist/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
|
-
|
|
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.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface IdbConnection {
|
|
2
|
+
/** The open connection, opening one if needed. */
|
|
3
|
+
open(): Promise<IDBDatabase>;
|
|
4
|
+
/** Forget the current connection so the next call re-opens. */
|
|
5
|
+
drop(closed?: IDBDatabase): void;
|
|
6
|
+
/**
|
|
7
|
+
* Run one transaction against the object store. If the connection is
|
|
8
|
+
* closed, re-open it and try once more. Errors from `body` are not retried.
|
|
9
|
+
*/
|
|
10
|
+
run<T>(mode: IDBTransactionMode, body: (store: IDBObjectStore, tx: IDBTransaction) => T | Promise<T>): Promise<T>;
|
|
11
|
+
}
|
|
12
|
+
/** True for the error a transaction throws on a connection the browser closed. */
|
|
13
|
+
export declare function isClosedConnectionError(error: unknown): boolean;
|
|
14
|
+
export declare function createIdbConnection(dbName: string, storeName: string, version?: number): IdbConnection;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/idbConnection.ts
|
|
2
|
+
// Shared IndexedDB connection handling for the SDK's caches.
|
|
3
|
+
//
|
|
4
|
+
// The browser may close an IndexedDB connection behind our back: iOS Safari
|
|
5
|
+
// does it when the tab goes to the background, and any browser does it on
|
|
6
|
+
// storage eviction or a version change. A transaction on a closed connection
|
|
7
|
+
// throws "The database connection is closing", so a cache that keeps one
|
|
8
|
+
// connection for the life of the page silently stops working after that.
|
|
9
|
+
//
|
|
10
|
+
// createIdbConnection() forgets a connection the browser closes and re-opens
|
|
11
|
+
// on the next call; run() retries a transaction once when the connection
|
|
12
|
+
// turns out to be closed.
|
|
13
|
+
/** True for the error a transaction throws on a connection the browser closed. */
|
|
14
|
+
export function isClosedConnectionError(error) {
|
|
15
|
+
const e = error;
|
|
16
|
+
if (!e)
|
|
17
|
+
return false;
|
|
18
|
+
if (e.name === 'InvalidStateError')
|
|
19
|
+
return true;
|
|
20
|
+
const msg = String(e.message || '');
|
|
21
|
+
return /connection is closing|database connection|closed/i.test(msg);
|
|
22
|
+
}
|
|
23
|
+
export function createIdbConnection(dbName, storeName, version = 1) {
|
|
24
|
+
let db = null;
|
|
25
|
+
let opening = null;
|
|
26
|
+
const drop = (closed) => {
|
|
27
|
+
// A newer connection may already have replaced the one that closed.
|
|
28
|
+
if (closed && db && db !== closed)
|
|
29
|
+
return;
|
|
30
|
+
db = null;
|
|
31
|
+
opening = null;
|
|
32
|
+
};
|
|
33
|
+
const open = () => {
|
|
34
|
+
if (db)
|
|
35
|
+
return Promise.resolve(db);
|
|
36
|
+
if (opening)
|
|
37
|
+
return opening;
|
|
38
|
+
opening = new Promise((resolve, reject) => {
|
|
39
|
+
try {
|
|
40
|
+
const req = indexedDB.open(dbName, version);
|
|
41
|
+
req.onupgradeneeded = (event) => {
|
|
42
|
+
const upgrading = event.target.result;
|
|
43
|
+
if (!upgrading.objectStoreNames.contains(storeName)) {
|
|
44
|
+
upgrading.createObjectStore(storeName);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
req.onsuccess = () => {
|
|
48
|
+
const opened = req.result;
|
|
49
|
+
opened.onclose = () => drop(opened);
|
|
50
|
+
opened.onversionchange = () => {
|
|
51
|
+
opened.close();
|
|
52
|
+
drop(opened);
|
|
53
|
+
};
|
|
54
|
+
db = opened;
|
|
55
|
+
resolve(opened);
|
|
56
|
+
};
|
|
57
|
+
req.onerror = () => {
|
|
58
|
+
opening = null;
|
|
59
|
+
reject(req.error);
|
|
60
|
+
};
|
|
61
|
+
req.onblocked = () => {
|
|
62
|
+
opening = null;
|
|
63
|
+
reject(new Error(`[smartlinks] IndexedDB open blocked (${dbName})`));
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
opening = null;
|
|
68
|
+
reject(err);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return opening;
|
|
72
|
+
};
|
|
73
|
+
const run = async (mode, body) => {
|
|
74
|
+
for (let attempt = 0;; attempt++) {
|
|
75
|
+
const current = await open();
|
|
76
|
+
let tx;
|
|
77
|
+
try {
|
|
78
|
+
tx = current.transaction(storeName, mode);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (attempt === 0 && isClosedConnectionError(err)) {
|
|
82
|
+
drop(current);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
return await body(tx.objectStore(storeName), tx);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return { open, drop, run };
|
|
91
|
+
}
|
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 :
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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;
|
package/dist/iframeResponder.js
CHANGED
|
@@ -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
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
package/dist/persistentCache.js
CHANGED
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
// isIdbAvailable() and wrapped in try/catch so that failures in private-
|
|
6
6
|
// browsing, quota-exceeded situations, or server-side environments are always
|
|
7
7
|
// silent — they never propagate errors to the caller.
|
|
8
|
+
//
|
|
9
|
+
// The connection itself is managed by idbConnection.ts, which re-opens it when
|
|
10
|
+
// the browser closes it behind our back (iOS Safari does this when the tab is
|
|
11
|
+
// backgrounded) instead of leaving the cache dead for the rest of the page.
|
|
12
|
+
import { createIdbConnection } from './idbConnection.js';
|
|
8
13
|
const DB_NAME = 'smartlinks-sdk-cache';
|
|
9
14
|
const STORE_NAME = 'responses';
|
|
10
15
|
const DB_VERSION = 1;
|
|
11
|
-
|
|
16
|
+
const connection = createIdbConnection(DB_NAME, STORE_NAME, DB_VERSION);
|
|
12
17
|
/**
|
|
13
18
|
* Returns true only in environments where IndexedDB is genuinely available.
|
|
14
19
|
* False in Node.js, and in some private-browsing contexts that stub indexedDB
|
|
@@ -22,36 +27,13 @@ export function isIdbAvailable() {
|
|
|
22
27
|
return false;
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
req.onupgradeneeded = (event) => {
|
|
32
|
-
const db = event.target.result;
|
|
33
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
34
|
-
db.createObjectStore(STORE_NAME);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
req.onsuccess = (event) => {
|
|
38
|
-
resolve(event.target.result);
|
|
39
|
-
};
|
|
40
|
-
req.onerror = () => {
|
|
41
|
-
dbPromise = null;
|
|
42
|
-
reject(req.error);
|
|
43
|
-
};
|
|
44
|
-
req.onblocked = () => {
|
|
45
|
-
dbPromise = null;
|
|
46
|
-
reject(new Error('[smartlinks] IndexedDB open blocked'));
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
dbPromise = null;
|
|
51
|
-
reject(err);
|
|
52
|
-
}
|
|
30
|
+
/** Resolve when the transaction settles, whatever the outcome. */
|
|
31
|
+
function settled(tx) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
tx.oncomplete = () => resolve();
|
|
34
|
+
tx.onerror = () => resolve(); // quota exceeded etc — fail silently
|
|
35
|
+
tx.onabort = () => resolve();
|
|
53
36
|
});
|
|
54
|
-
return dbPromise;
|
|
55
37
|
}
|
|
56
38
|
/**
|
|
57
39
|
* Read an entry from IndexedDB.
|
|
@@ -62,18 +44,11 @@ export async function idbGet(key) {
|
|
|
62
44
|
if (!isIdbAvailable())
|
|
63
45
|
return null;
|
|
64
46
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
|
|
71
|
-
req.onerror = () => resolve(null);
|
|
72
|
-
}
|
|
73
|
-
catch (_a) {
|
|
74
|
-
resolve(null);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
47
|
+
return await connection.run('readonly', (store) => new Promise((resolve) => {
|
|
48
|
+
const req = store.get(key);
|
|
49
|
+
req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
|
|
50
|
+
req.onerror = () => resolve(null);
|
|
51
|
+
}));
|
|
77
52
|
}
|
|
78
53
|
catch (_a) {
|
|
79
54
|
return null;
|
|
@@ -88,18 +63,9 @@ export async function idbSet(key, entry) {
|
|
|
88
63
|
if (!isIdbAvailable())
|
|
89
64
|
return;
|
|
90
65
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
95
|
-
tx.objectStore(STORE_NAME).put(entry, key);
|
|
96
|
-
tx.oncomplete = () => resolve();
|
|
97
|
-
tx.onerror = () => resolve(); // quota exceeded etc — fail silently
|
|
98
|
-
tx.onabort = () => resolve();
|
|
99
|
-
}
|
|
100
|
-
catch (_a) {
|
|
101
|
-
resolve();
|
|
102
|
-
}
|
|
66
|
+
await connection.run('readwrite', (store, tx) => {
|
|
67
|
+
store.put(entry, key);
|
|
68
|
+
return settled(tx);
|
|
103
69
|
});
|
|
104
70
|
}
|
|
105
71
|
catch (_a) {
|
|
@@ -114,18 +80,9 @@ export async function idbDelete(key) {
|
|
|
114
80
|
if (!isIdbAvailable())
|
|
115
81
|
return;
|
|
116
82
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
121
|
-
tx.objectStore(STORE_NAME).delete(key);
|
|
122
|
-
tx.oncomplete = () => resolve();
|
|
123
|
-
tx.onerror = () => resolve();
|
|
124
|
-
tx.onabort = () => resolve();
|
|
125
|
-
}
|
|
126
|
-
catch (_a) {
|
|
127
|
-
resolve();
|
|
128
|
-
}
|
|
83
|
+
await connection.run('readwrite', (store, tx) => {
|
|
84
|
+
store.delete(key);
|
|
85
|
+
return settled(tx);
|
|
129
86
|
});
|
|
130
87
|
}
|
|
131
88
|
catch (_a) { }
|
|
@@ -138,41 +95,25 @@ export async function idbClear(pattern) {
|
|
|
138
95
|
if (!isIdbAvailable())
|
|
139
96
|
return;
|
|
140
97
|
try {
|
|
141
|
-
const db = await openDb();
|
|
142
98
|
if (!pattern) {
|
|
143
|
-
await
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
tx.objectStore(STORE_NAME).clear();
|
|
147
|
-
tx.oncomplete = () => resolve();
|
|
148
|
-
tx.onerror = () => resolve();
|
|
149
|
-
tx.onabort = () => resolve();
|
|
150
|
-
}
|
|
151
|
-
catch (_a) {
|
|
152
|
-
resolve();
|
|
153
|
-
}
|
|
99
|
+
await connection.run('readwrite', (store, tx) => {
|
|
100
|
+
store.clear();
|
|
101
|
+
return settled(tx);
|
|
154
102
|
});
|
|
155
103
|
return;
|
|
156
104
|
}
|
|
157
105
|
// Pattern-based: enumerate all keys and delete matching ones.
|
|
158
|
-
await new Promise((resolve) => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (key.includes(pattern))
|
|
166
|
-
store.delete(key);
|
|
167
|
-
}
|
|
168
|
-
resolve();
|
|
169
|
-
};
|
|
170
|
-
req.onerror = () => resolve();
|
|
171
|
-
}
|
|
172
|
-
catch (_a) {
|
|
106
|
+
await connection.run('readwrite', (store) => new Promise((resolve) => {
|
|
107
|
+
const req = store.getAllKeys();
|
|
108
|
+
req.onsuccess = () => {
|
|
109
|
+
for (const key of req.result) {
|
|
110
|
+
if (key.includes(pattern))
|
|
111
|
+
store.delete(key);
|
|
112
|
+
}
|
|
173
113
|
resolve();
|
|
174
|
-
}
|
|
175
|
-
|
|
114
|
+
};
|
|
115
|
+
req.onerror = () => resolve();
|
|
116
|
+
}));
|
|
176
117
|
}
|
|
177
118
|
catch (_a) { }
|
|
178
119
|
}
|
package/dist/theme.css
CHANGED
|
@@ -41,6 +41,26 @@
|
|
|
41
41
|
--shadow-md: var(--sl-shadow-md, 0 0 #0000);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/* Dark mode without values. A host in dark mode should send a dark value-set (the --sl-* tokens); one
|
|
45
|
+
* that only sets the hook (.dark / data-theme="dark") left every colour on its light fallback — white
|
|
46
|
+
* cards and near-black text on a dark page. Under the hook the fallbacks are dark too; whatever the host
|
|
47
|
+
* does send still wins. (Re-declared on the hook's element, so it works wherever the host puts it.) */
|
|
48
|
+
.dark,
|
|
49
|
+
[data-theme="dark"] {
|
|
50
|
+
--color-background: var(--sl-color-bg, #09090b);
|
|
51
|
+
--color-surface: var(--sl-color-surface, #18181b);
|
|
52
|
+
--color-surface-raised:var(--sl-color-surface-raised, var(--sl-color-surface, #27272a));
|
|
53
|
+
--color-foreground: var(--sl-color-fg, #fafafa);
|
|
54
|
+
--color-muted: var(--sl-color-muted, #a1a1aa);
|
|
55
|
+
--color-border: var(--sl-color-border, #3f3f46);
|
|
56
|
+
--color-primary: var(--sl-color-accent, #6366f1);
|
|
57
|
+
--color-on-primary: var(--sl-color-on-accent, #ffffff);
|
|
58
|
+
--color-primary-soft: var(--sl-color-accent-soft, color-mix(in oklab, var(--sl-color-accent, #6366f1) 20%, transparent));
|
|
59
|
+
--color-success: var(--sl-color-success, #22c55e);
|
|
60
|
+
--color-danger: var(--sl-color-danger, #f87171);
|
|
61
|
+
color-scheme: dark;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
/* shadcn bridge (optional): if the app uses shadcn's --primary/--background/etc. HSL variables,
|
|
45
65
|
* uncomment to drive them from the SmartLinks tokens too. Left commented so it never overrides an
|
|
46
66
|
* app that has deliberately customised its shadcn palette — opt in per app (see migration step 17).
|
package/dist/translationCache.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { createIdbConnection } from './idbConnection.js';
|
|
1
2
|
const DB_NAME = 'smartlinks-sdk-translations';
|
|
2
3
|
const STORE_NAME = 'translations';
|
|
3
4
|
const DB_VERSION = 1;
|
|
4
5
|
const DEFAULT_LOCAL_TRANSLATION_TTL_MS = 90 * 24 * 60 * 60000;
|
|
5
|
-
|
|
6
|
+
const connection = createIdbConnection(DB_NAME, STORE_NAME, DB_VERSION);
|
|
6
7
|
const memoryTranslationCache = new Map();
|
|
7
8
|
export function getDefaultTranslationCacheTtlMs() {
|
|
8
9
|
return DEFAULT_LOCAL_TRANSLATION_TTL_MS;
|
|
@@ -15,36 +16,13 @@ function isIndexedDbAvailable() {
|
|
|
15
16
|
return false;
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
request.onupgradeneeded = (event) => {
|
|
25
|
-
const db = event.target.result;
|
|
26
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
27
|
-
db.createObjectStore(STORE_NAME);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
request.onsuccess = (event) => {
|
|
31
|
-
resolve(event.target.result);
|
|
32
|
-
};
|
|
33
|
-
request.onerror = () => {
|
|
34
|
-
dbPromise = null;
|
|
35
|
-
reject(request.error);
|
|
36
|
-
};
|
|
37
|
-
request.onblocked = () => {
|
|
38
|
-
dbPromise = null;
|
|
39
|
-
reject(new Error('[smartlinks] Translation IndexedDB open blocked'));
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
dbPromise = null;
|
|
44
|
-
reject(error);
|
|
45
|
-
}
|
|
19
|
+
/** Resolve when the transaction settles, whatever the outcome. */
|
|
20
|
+
function settled(tx) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
tx.oncomplete = () => resolve();
|
|
23
|
+
tx.onerror = () => resolve();
|
|
24
|
+
tx.onabort = () => resolve();
|
|
46
25
|
});
|
|
47
|
-
return dbPromise;
|
|
48
26
|
}
|
|
49
27
|
function stableContextKey(context) {
|
|
50
28
|
if (!context)
|
|
@@ -123,33 +101,26 @@ async function getEntry(key) {
|
|
|
123
101
|
if (!isIndexedDbAvailable())
|
|
124
102
|
return null;
|
|
125
103
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
};
|
|
147
|
-
request.onerror = () => resolve(null);
|
|
148
|
-
}
|
|
149
|
-
catch (_a) {
|
|
150
|
-
resolve(null);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
104
|
+
return await connection.run('readonly', (store) => new Promise((resolve) => {
|
|
105
|
+
const request = store.get(key);
|
|
106
|
+
request.onsuccess = () => {
|
|
107
|
+
var _a;
|
|
108
|
+
const result = (_a = request.result) !== null && _a !== void 0 ? _a : null;
|
|
109
|
+
if (!result) {
|
|
110
|
+
resolve(null);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (isExpired(result)) {
|
|
114
|
+
memoryTranslationCache.delete(key);
|
|
115
|
+
void deleteEntries([key]);
|
|
116
|
+
resolve(null);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
memoryTranslationCache.set(key, result);
|
|
120
|
+
resolve(result);
|
|
121
|
+
};
|
|
122
|
+
request.onerror = () => resolve(null);
|
|
123
|
+
}));
|
|
153
124
|
}
|
|
154
125
|
catch (_a) {
|
|
155
126
|
return null;
|
|
@@ -170,21 +141,11 @@ async function setEntries(entries) {
|
|
|
170
141
|
if (!isIndexedDbAvailable())
|
|
171
142
|
return;
|
|
172
143
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
177
|
-
const store = tx.objectStore(STORE_NAME);
|
|
178
|
-
for (const [key, entry] of entries) {
|
|
179
|
-
store.put(entry, key);
|
|
180
|
-
}
|
|
181
|
-
tx.oncomplete = () => resolve();
|
|
182
|
-
tx.onerror = () => resolve();
|
|
183
|
-
tx.onabort = () => resolve();
|
|
184
|
-
}
|
|
185
|
-
catch (_a) {
|
|
186
|
-
resolve();
|
|
144
|
+
await connection.run('readwrite', (store, tx) => {
|
|
145
|
+
for (const [key, entry] of entries) {
|
|
146
|
+
store.put(entry, key);
|
|
187
147
|
}
|
|
148
|
+
return settled(tx);
|
|
188
149
|
});
|
|
189
150
|
}
|
|
190
151
|
catch (_a) {
|
|
@@ -226,21 +187,11 @@ async function deleteEntries(keys) {
|
|
|
226
187
|
if (!isIndexedDbAvailable() || keys.length === 0)
|
|
227
188
|
return;
|
|
228
189
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
233
|
-
const store = tx.objectStore(STORE_NAME);
|
|
234
|
-
for (const key of keys) {
|
|
235
|
-
store.delete(key);
|
|
236
|
-
}
|
|
237
|
-
tx.oncomplete = () => resolve();
|
|
238
|
-
tx.onerror = () => resolve();
|
|
239
|
-
tx.onabort = () => resolve();
|
|
240
|
-
}
|
|
241
|
-
catch (_a) {
|
|
242
|
-
resolve();
|
|
190
|
+
await connection.run('readwrite', (store, tx) => {
|
|
191
|
+
for (const key of keys) {
|
|
192
|
+
store.delete(key);
|
|
243
193
|
}
|
|
194
|
+
return settled(tx);
|
|
244
195
|
});
|
|
245
196
|
}
|
|
246
197
|
catch (_a) {
|
|
@@ -256,41 +207,25 @@ export async function clearCachedTranslations(collectionId) {
|
|
|
256
207
|
if (!isIndexedDbAvailable())
|
|
257
208
|
return;
|
|
258
209
|
try {
|
|
259
|
-
const db = await openDb();
|
|
260
210
|
if (!collectionId) {
|
|
261
|
-
await
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
tx.objectStore(STORE_NAME).clear();
|
|
265
|
-
tx.oncomplete = () => resolve();
|
|
266
|
-
tx.onerror = () => resolve();
|
|
267
|
-
tx.onabort = () => resolve();
|
|
268
|
-
}
|
|
269
|
-
catch (_a) {
|
|
270
|
-
resolve();
|
|
271
|
-
}
|
|
211
|
+
await connection.run('readwrite', (store, tx) => {
|
|
212
|
+
store.clear();
|
|
213
|
+
return settled(tx);
|
|
272
214
|
});
|
|
273
215
|
return;
|
|
274
216
|
}
|
|
275
|
-
await new Promise((resolve) => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
for (const key of request.result) {
|
|
282
|
-
if (key.startsWith(`${collectionId}::`)) {
|
|
283
|
-
store.delete(key);
|
|
284
|
-
}
|
|
217
|
+
await connection.run('readwrite', (store) => new Promise((resolve) => {
|
|
218
|
+
const request = store.getAllKeys();
|
|
219
|
+
request.onsuccess = () => {
|
|
220
|
+
for (const key of request.result) {
|
|
221
|
+
if (key.startsWith(`${collectionId}::`)) {
|
|
222
|
+
store.delete(key);
|
|
285
223
|
}
|
|
286
|
-
|
|
287
|
-
};
|
|
288
|
-
request.onerror = () => resolve();
|
|
289
|
-
}
|
|
290
|
-
catch (_a) {
|
|
224
|
+
}
|
|
291
225
|
resolve();
|
|
292
|
-
}
|
|
293
|
-
|
|
226
|
+
};
|
|
227
|
+
request.onerror = () => resolve();
|
|
228
|
+
}));
|
|
294
229
|
}
|
|
295
230
|
catch (_a) {
|
|
296
231
|
// Ignore deletion failures.
|
|
@@ -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;
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.10 | Generated: 2026-11-03T16:21:02.896Z
|
|
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;
|
package/docs/iframe-responder.md
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
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. **
|
|
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
|
-
-
|
|
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/theme-tokens.md
CHANGED
|
@@ -99,6 +99,14 @@ renders correctly even against a host that sets none of them.
|
|
|
99
99
|
|
|
100
100
|
Dark mode is a *theme* (a different value-set), not a separate token set — the host swaps the values.
|
|
101
101
|
|
|
102
|
+
- **Host:** in dark mode, send a dark value-set — in the iframe address (`#slt=` with `theme: "dark"`, and the
|
|
103
|
+
legacy `theme=` payload with `m: "d"`) or on the mount root. Sending only the hook (`dark=1`, `.dark`)
|
|
104
|
+
leaves the app to guess.
|
|
105
|
+
- **App:** `theme.css` has dark fallbacks under `.dark` / `[data-theme="dark"]`, so an app built on it stays
|
|
106
|
+
readable even when a host sends the hook and no values. An app with its own CSS variables should do the
|
|
107
|
+
same: a light fallback at the root, a dark one under the hook — never a light colour that also stands
|
|
108
|
+
in for dark.
|
|
109
|
+
|
|
102
110
|
---
|
|
103
111
|
|
|
104
112
|
## 3. Theme value-sets (what the AI emits, what the lookbook stores)
|
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
|
-
|
|
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,87 +1,87 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@proveanything/smartlinks",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"default": "./dist/index.js"
|
|
13
|
-
},
|
|
14
|
-
"./testing": {
|
|
15
|
-
"types": "./dist/testing/index.d.ts",
|
|
16
|
-
"import": "./dist/testing/index.js",
|
|
17
|
-
"default": "./dist/testing/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./*": {
|
|
20
|
-
"types": "./dist/*.d.ts",
|
|
21
|
-
"import": "./dist/*.js",
|
|
22
|
-
"default": "./dist/*.js"
|
|
23
|
-
},
|
|
24
|
-
"./baseline.css": "./dist/baseline.css",
|
|
25
|
-
"./baseline.classes.json": "./dist/baseline.classes.json",
|
|
26
|
-
"./theme.css": "./dist/theme.css",
|
|
27
|
-
"./theme-boot.js": "./dist/theme-boot.js",
|
|
28
|
-
"./package.json": "./package.json"
|
|
29
|
-
},
|
|
30
|
-
"bin": {
|
|
31
|
-
"smartlinks-register-release": "scripts/register-release.mjs",
|
|
32
|
-
"smartlinks-publish": "scripts/publish.mjs",
|
|
33
|
-
"smartlinks-doctor": "scripts/doctor.mjs",
|
|
34
|
-
"smartlinks-headless": "scripts/headless-check.mjs"
|
|
35
|
-
},
|
|
36
|
-
"files": [
|
|
37
|
-
"dist/",
|
|
38
|
-
"docs/",
|
|
39
|
-
"scripts/register-release.mjs",
|
|
40
|
-
"scripts/publish.mjs",
|
|
41
|
-
"scripts/doctor.mjs",
|
|
42
|
-
"scripts/headless-check.mjs",
|
|
43
|
-
"scripts/lib/",
|
|
44
|
-
"openapi.yaml",
|
|
45
|
-
"README.md"
|
|
46
|
-
],
|
|
47
|
-
"scripts": {
|
|
48
|
-
"test": "node test/run.cjs",
|
|
49
|
-
"build": "tsc && node scripts/fix-esm-extensions.mjs && node generate-api-summary.cjs && node generate-openapi.cjs && node scripts/copy-docs-to-dist.cjs && node scripts/build-baseline.mjs",
|
|
50
|
-
"docs": "typedoc",
|
|
51
|
-
"docs:summary": "node generate-api-summary.cjs",
|
|
52
|
-
"docs:openapi": "node generate-openapi.cjs",
|
|
53
|
-
"docs:openapi:lint": "npx --yes @redocly/cli@latest lint openapi.yaml",
|
|
54
|
-
"docs:openapi:check": "node generate-openapi.cjs && npx --yes @redocly/cli@latest lint openapi.yaml",
|
|
55
|
-
"build:docs": "tsc build-docs.ts --outDir dist && node dist/build-docs.js",
|
|
56
|
-
"prepublishOnly": "npm run build"
|
|
57
|
-
},
|
|
58
|
-
"keywords": [
|
|
59
|
-
"smartlinks",
|
|
60
|
-
"api",
|
|
61
|
-
"authentication",
|
|
62
|
-
"traceability",
|
|
63
|
-
"blockchain",
|
|
64
|
-
"typescript",
|
|
65
|
-
"sdk"
|
|
66
|
-
],
|
|
67
|
-
"author": "Glenn Shoosmith",
|
|
68
|
-
"license": "MIT",
|
|
69
|
-
"publishConfig": {
|
|
70
|
-
"access": "public"
|
|
71
|
-
},
|
|
72
|
-
"dependencies": {
|
|
73
|
-
"cross-fetch": "^3.1.5",
|
|
74
|
-
"es-module-lexer": "^1.7.0"
|
|
75
|
-
},
|
|
76
|
-
"devDependencies": {
|
|
77
|
-
"ts-node": "^10.9.1",
|
|
78
|
-
"typedoc": "^0.25.13",
|
|
79
|
-
"typedoc-plugin-markdown": "^3.17.1",
|
|
80
|
-
"typescript": "^4.9.0"
|
|
81
|
-
},
|
|
82
|
-
"repository": {
|
|
83
|
-
"type": "git",
|
|
84
|
-
"url": "https://github.com/Prove-Anything/smartlinks.git"
|
|
85
|
-
},
|
|
86
|
-
"homepage": "https://smartlinks.app"
|
|
87
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@proveanything/smartlinks",
|
|
3
|
+
"version": "2.1.10",
|
|
4
|
+
"description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./testing": {
|
|
15
|
+
"types": "./dist/testing/index.d.ts",
|
|
16
|
+
"import": "./dist/testing/index.js",
|
|
17
|
+
"default": "./dist/testing/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./*": {
|
|
20
|
+
"types": "./dist/*.d.ts",
|
|
21
|
+
"import": "./dist/*.js",
|
|
22
|
+
"default": "./dist/*.js"
|
|
23
|
+
},
|
|
24
|
+
"./baseline.css": "./dist/baseline.css",
|
|
25
|
+
"./baseline.classes.json": "./dist/baseline.classes.json",
|
|
26
|
+
"./theme.css": "./dist/theme.css",
|
|
27
|
+
"./theme-boot.js": "./dist/theme-boot.js",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"smartlinks-register-release": "scripts/register-release.mjs",
|
|
32
|
+
"smartlinks-publish": "scripts/publish.mjs",
|
|
33
|
+
"smartlinks-doctor": "scripts/doctor.mjs",
|
|
34
|
+
"smartlinks-headless": "scripts/headless-check.mjs"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist/",
|
|
38
|
+
"docs/",
|
|
39
|
+
"scripts/register-release.mjs",
|
|
40
|
+
"scripts/publish.mjs",
|
|
41
|
+
"scripts/doctor.mjs",
|
|
42
|
+
"scripts/headless-check.mjs",
|
|
43
|
+
"scripts/lib/",
|
|
44
|
+
"openapi.yaml",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "node test/run.cjs",
|
|
49
|
+
"build": "tsc && node scripts/fix-esm-extensions.mjs && node generate-api-summary.cjs && node generate-openapi.cjs && node scripts/copy-docs-to-dist.cjs && node scripts/build-baseline.mjs",
|
|
50
|
+
"docs": "typedoc",
|
|
51
|
+
"docs:summary": "node generate-api-summary.cjs",
|
|
52
|
+
"docs:openapi": "node generate-openapi.cjs",
|
|
53
|
+
"docs:openapi:lint": "npx --yes @redocly/cli@latest lint openapi.yaml",
|
|
54
|
+
"docs:openapi:check": "node generate-openapi.cjs && npx --yes @redocly/cli@latest lint openapi.yaml",
|
|
55
|
+
"build:docs": "tsc build-docs.ts --outDir dist && node dist/build-docs.js",
|
|
56
|
+
"prepublishOnly": "npm run build"
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"smartlinks",
|
|
60
|
+
"api",
|
|
61
|
+
"authentication",
|
|
62
|
+
"traceability",
|
|
63
|
+
"blockchain",
|
|
64
|
+
"typescript",
|
|
65
|
+
"sdk"
|
|
66
|
+
],
|
|
67
|
+
"author": "Glenn Shoosmith",
|
|
68
|
+
"license": "MIT",
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"cross-fetch": "^3.1.5",
|
|
74
|
+
"es-module-lexer": "^1.7.0"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"ts-node": "^10.9.1",
|
|
78
|
+
"typedoc": "^0.25.13",
|
|
79
|
+
"typedoc-plugin-markdown": "^3.17.1",
|
|
80
|
+
"typescript": "^4.9.0"
|
|
81
|
+
},
|
|
82
|
+
"repository": {
|
|
83
|
+
"type": "git",
|
|
84
|
+
"url": "https://github.com/Prove-Anything/smartlinks.git"
|
|
85
|
+
},
|
|
86
|
+
"homepage": "https://smartlinks.app"
|
|
87
|
+
}
|