@deot/docs-playground 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,6 +70,66 @@ const files = ref({
70
70
 
71
71
  Playground 只接受来自自身 iframe 的导航消息。宿主应用应监听 `navigate` 并交给自己的 Router 处理。
72
72
 
73
+ ## 预览内的临时高度
74
+
75
+ Playground 会在预览应用中注入 `docs:playground`,也可通过 iframe 内的 `window.$docsPlayground` 访问同一服务。适用于 ActionSheet、Modal、Drawer、Message 等依赖视口高度的示例,无需自行撑高 DOM 或轮询 `window.innerHeight`。
76
+
77
+ ```ts
78
+ import { inject, ref } from 'vue';
79
+ import type { PlaygroundService } from '@deot/docs-playground';
80
+
81
+ const playground = inject<PlaygroundService>('docs:playground')!;
82
+ const visible = ref(false);
83
+
84
+ // 定义时不执行,点击后先等待高度就绪,再打开弹层;visible 关闭时恢复。
85
+ // visible 是 ref 且省略 handler 时,默认执行 visible.value = true。
86
+ const handleOpen = playground.run(560, { visible });
87
+
88
+ // 也可生成 boolean 状态处理函数:true 扩展高度,false 恢复高度。
89
+ const handleVisibleChange = playground.run(560);
90
+
91
+ // 支持 getter 和 computed;不能传入普通 boolean 快照。
92
+ const openDrawer = playground.run(560, { visible: () => state.drawerVisible }, () => {
93
+ state.drawerVisible = true;
94
+ });
95
+
96
+ // 回调返回的 Promise 表示弹层关闭时,可省略 options。
97
+ const openActionSheet = playground.run(560, async () => {
98
+ await MActionSheet.open({ title: '请选择操作' });
99
+ });
100
+ ```
101
+
102
+ `run(height)`、`run(height, handler)` 和 `run(height, options, handler)` 均返回事件处理函数。传入 handler 时会透传调用参数和回调结果,最后一个参数始终为 handler。`options` 当前只包含 `visible?: Ref<boolean> | (() => boolean)`;传入可写 `Ref<boolean>` 时可以省略 handler,默认将其设为 `true`。getter 和只读 computed 需要显式 handler。示例中的 `state`、`MActionSheet` 由业务代码提供。
103
+
104
+ | 接口 | 行为 |
105
+ | --- | --- |
106
+ | `enable(height): Promise<boolean>` | 设置手动临时高度,等待 iframe 实际高度达到要求后返回 `true`。连续调用替换旧的手动请求。 |
107
+ | `disable(): void` | 释放手动请求;重复调用无副作用,不影响 `run` 的请求。 |
108
+ | `run(height)` | 返回 boolean 事件处理函数;收到 `true` 时调用 `enable(height)`,收到 `false` 时调用 `disable()`。 |
109
+ | `run(height, handler)` | 等待高度后执行;同步返回或 Promise 完成时恢复高度,异常也恢复并继续抛出。 |
110
+ | `run(height, { visible: ref })` | 等待高度后默认执行 `visible.value = true`,并在状态关闭时恢复高度。 |
111
+ | `run(height, { visible }, handler)` | 执行前监听状态,关闭时恢复;回调完成后未打开或回调抛错时也释放。异步打开流程应由回调返回的 Promise 覆盖。 |
112
+
113
+ 同一个 `run` 返回函数在回调与弹层均结束前会复用当前 Promise,避免重复点击打开;不同函数的请求独立管理并取最大高度。Message / Toast 等立即返回的 API 应通过 `visible` 或关闭回调恢复高度,不能把函数返回当成弹层关闭。
114
+
115
+ ```ts
116
+ const handleMessage = async () => {
117
+ if (!await playground.enable(240)) return;
118
+ try {
119
+ Message.info({ content: '已保存', onClose: () => playground.disable() });
120
+ } catch (error) {
121
+ playground.disable();
122
+ throw error;
123
+ }
124
+ };
125
+ ```
126
+
127
+ 高度为正有限 CSS px,表示 iframe 内部可视高度,不包含 `previewInset` 与边框。普通预览取原有高度与临时请求的最大值,释放后恢复当前视口规则;独立窗口受可用屏幕空间限制。等待超时(2 秒)、取消或空间不足时返回 `false`;`run` 在这些情况下不执行回调并返回 `undefined`。
128
+
129
+ 在 `setup` 中创建的 `run` 会随当前 Vue scope 自动取消等待、移除监听并释放高度;其他位置创建时绑定预览应用。手动 `enable/disable` 绑定整个预览应用,子组件单独卸载时按需自行 `disable()`。刷新、重新运行及应用卸载会统一释放请求。服务不取消已经启动的业务任务,也不调用组件的 `destroy()`;示例保留必要的业务 `onUnmounted`,只需删除高度等待使用的 `disposed` 标记。
130
+
131
+ 类型导出为 `PlaygroundService`、`PlaygroundRunOptions` 和 `PlaygroundWindow`。Window 类型可通过 `(window as PlaygroundWindow).$docsPlayground` 显式使用;服务仅存在于 Playground 预览应用,不向普通页面声明必然存在的全局变量。接入项目需使用包含该接口的新版 Playground。
132
+
73
133
  ## 其他公共导出
74
134
 
75
135
  | 导出 | 说明 |
package/dist/index.cjs CHANGED
@@ -896,6 +896,8 @@ var PREVIEW_SCROLLER_USE_CODE = [
896
896
  "if(app._context&&next._context){",
897
897
  "if(app._context.components)Object.assign(next._context.components,app._context.components);",
898
898
  "if(app._context.directives)Object.assign(next._context.directives,app._context.directives);",
899
+ "Object.assign(next._context.provides,app._context.provides);",
900
+ "next._context.mixins.push(...app._context.mixins);",
899
901
  "}",
900
902
  "window.__app__=next;",
901
903
  `document.documentElement.classList.add("${PREVIEW_SCROLL_HTML_CLASS}");`,
@@ -942,6 +944,7 @@ var useSandboxAutoHeight = (sandboxRef) => {
942
944
  const collapsedTop = Math.max(0, bodyRect.top - rootTop);
943
945
  let childrenBottom = 0;
944
946
  for (const child of body.children) {
947
+ if (iframeWindow.getComputedStyle(child).position === "fixed") continue;
945
948
  const rect = child.getBoundingClientRect();
946
949
  const marginBottom = Number.parseFloat(iframeWindow.getComputedStyle(child).marginBottom) || 0;
947
950
  childrenBottom = Math.max(childrenBottom, rect.bottom + marginBottom - rootTop);
@@ -962,9 +965,14 @@ var useSandboxAutoHeight = (sandboxRef) => {
962
965
  }
963
966
  return Math.ceil(Math.max(scrollContent.scrollHeight + margins, scrollContent.offsetHeight + margins, childrenBottom + margins));
964
967
  };
965
- const isEmptyPreviewApp = (iframeDocument) => {
968
+ const isPendingPreviewApp = (iframeDocument) => {
966
969
  const app = iframeDocument.getElementById("app");
967
- return !!app && app.childElementCount === 0;
970
+ return app ? app.childElementCount === 0 : !!iframe?.srcdoc;
971
+ };
972
+ const releaseBootstrapHeight = (target) => {
973
+ (0, vue.nextTick)(() => {
974
+ if (iframe === target) target.style.height = "100%";
975
+ });
968
976
  };
969
977
  const measure = () => {
970
978
  frameId = 0;
@@ -974,10 +982,11 @@ var useSandboxAutoHeight = (sandboxRef) => {
974
982
  const iframeDocument = iframe.contentDocument;
975
983
  if (!iframeWindow || !iframeDocument?.body) return;
976
984
  const scrollContent = resolveScrollContent(iframeDocument);
977
- if (!scrollContent && isEmptyPreviewApp(iframeDocument)) return;
985
+ if (!scrollContent && isPendingPreviewApp(iframeDocument)) return;
978
986
  const contentHeight = scrollContent ? measureScrollContent(scrollContent) : measureDocumentContent(iframeWindow, iframeDocument);
979
987
  const nextHeight = Math.max(contentHeight, 24);
980
988
  if (height.value !== nextHeight) height.value = nextHeight;
989
+ releaseBootstrapHeight(iframe);
981
990
  } catch {
982
991
  if (height.value !== 24) height.value = 24;
983
992
  }
@@ -1046,7 +1055,7 @@ var useSandboxAutoHeight = (sandboxRef) => {
1046
1055
  return;
1047
1056
  }
1048
1057
  iframe.addEventListener("load", handleIframeLoad);
1049
- iframe.style.height = "100%";
1058
+ iframe.style.height = `${Math.max(window.innerHeight, 24)}px`;
1050
1059
  observeContent();
1051
1060
  };
1052
1061
  const syncIframe = () => setIframe(container?.querySelector("iframe") || null);
@@ -1396,6 +1405,73 @@ var useSandboxTheme = (sandboxRef) => {
1396
1405
  });
1397
1406
  };
1398
1407
  //#endregion
1408
+ //#region packages/playground/src/core/runtime/height-bridge.ts
1409
+ /**
1410
+ * 只接收本宿主当前 iframe 的高度请求,应用重评时切换会话。
1411
+ * @param sandbox 预览容器。
1412
+ * @param maxHeight 当前可用高度,普通预览默认不限制。
1413
+ * @returns 所有有效请求的最大高度。
1414
+ */
1415
+ function usePreviewHeightBridge(sandbox, maxHeight = () => Infinity) {
1416
+ const height = (0, vue.ref)(0);
1417
+ const requests = /* @__PURE__ */ new Map();
1418
+ let source = null;
1419
+ let session = "";
1420
+ let disposed = false;
1421
+ const sync = () => {
1422
+ height.value = Math.max(0, ...requests.values());
1423
+ };
1424
+ const reset = () => {
1425
+ requests.clear();
1426
+ session = "";
1427
+ source = null;
1428
+ sync();
1429
+ };
1430
+ const handleMessage = async (event) => {
1431
+ const iframe = resolveSandboxContainer(sandbox.value)?.querySelector("iframe");
1432
+ const data = event.data;
1433
+ if (!iframe?.contentWindow || event.source !== iframe.contentWindow || data?.action !== "docs:height" || typeof data.session !== "string") return;
1434
+ if (data.operation === "start") {
1435
+ reset();
1436
+ source = iframe.contentWindow;
1437
+ session = data.session;
1438
+ return;
1439
+ }
1440
+ if (source !== iframe.contentWindow || session !== data.session) return;
1441
+ if (data.operation === "stop") {
1442
+ reset();
1443
+ return;
1444
+ }
1445
+ if (!Number.isSafeInteger(data.id) || data.id <= 0) return;
1446
+ if (data.operation === "release") {
1447
+ requests.delete(data.id);
1448
+ sync();
1449
+ return;
1450
+ }
1451
+ if (data.operation !== "enable" || !Number.isFinite(data.height) || data.height <= 0) return;
1452
+ const accepted = data.height <= maxHeight();
1453
+ if (accepted) requests.set(data.id, data.height);
1454
+ sync();
1455
+ const receiver = source;
1456
+ await (0, vue.nextTick)();
1457
+ if (disposed || source !== receiver || session !== data.session) return;
1458
+ receiver?.postMessage({
1459
+ action: "docs:height:applied",
1460
+ session,
1461
+ id: data.id,
1462
+ accepted
1463
+ }, "*");
1464
+ };
1465
+ (0, vue.watch)(() => resolveSandboxContainer(sandbox.value), reset, { flush: "sync" });
1466
+ if (typeof window !== "undefined") window.addEventListener("message", handleMessage);
1467
+ (0, vue.onBeforeUnmount)(() => {
1468
+ disposed = true;
1469
+ if (typeof window !== "undefined") window.removeEventListener("message", handleMessage);
1470
+ reset();
1471
+ });
1472
+ return height;
1473
+ }
1474
+ //#endregion
1399
1475
  //#region packages/playground/src/core/runtime/toolbar.vue?vue&type=script&setup=true&lang.ts
1400
1476
  var _hoisted_1$3 = { class: "docs-playground__tools" };
1401
1477
  var _hoisted_2$3 = {
@@ -1640,7 +1716,9 @@ var BUILTIN_IMPORT_ASSETS = {
1640
1716
  "@deot/helper": "@deot/helper/dist/index.js",
1641
1717
  "normalize-wheel": "normalize-wheel-es/dist/index.mjs",
1642
1718
  "photoswipe": "photoswipe/dist/photoswipe.esm.js",
1643
- "photoswipe/lightbox": "photoswipe/dist/photoswipe-lightbox.esm.js"
1719
+ "photoswipe/lightbox": "photoswipe/dist/photoswipe-lightbox.esm.js",
1720
+ "vue": "vue/dist/vue.runtime.esm-browser.js",
1721
+ "vue/server-renderer": "vue/dist/server-renderer.esm-browser.js"
1644
1722
  };
1645
1723
  var normalizeCdnURL = (cdnURL = DEFAULT_CDN_URL) => {
1646
1724
  return cdnURL.trim().replace(/\/+$/u, "") || "https://cdn.jsdelivr.net/npm";
@@ -1651,9 +1729,7 @@ var SASS_CDN_URL = jsdelivrEsmURL("sass/+esm");
1651
1729
  var cdnAsset = (cdnURL, asset) => cdnJoin(normalizeCdnURL(cdnURL), asset);
1652
1730
  var createBuiltinImports = (cdnURL = DEFAULT_CDN_URL) => ({
1653
1731
  ...Object.fromEntries(Object.entries(BUILTIN_IMPORT_ASSETS).map(([specifier, asset]) => [specifier, cdnAsset(cdnURL, asset)])),
1654
- "lodash-es": jsdelivrEsmURL("lodash-es/+esm"),
1655
- "vue": "https://play.vuejs.org/vue.runtime.esm-browser.js",
1656
- "vue/server-renderer": "https://play.vuejs.org/server-renderer.esm-browser.js"
1732
+ "lodash-es": jsdelivrEsmURL("lodash-es/+esm")
1657
1733
  });
1658
1734
  var createBuiltinStyles = (cdnURL = DEFAULT_CDN_URL) => Object.fromEntries(PREVIEW_STYLE_ASSETS.map((asset) => [asset, cdnAsset(cdnURL, asset)]));
1659
1735
  //#endregion
@@ -1733,6 +1809,227 @@ var clearPlaygroundMaps = () => {
1733
1809
  ].forEach((store) => store.set());
1734
1810
  };
1735
1811
  //#endregion
1812
+ //#region packages/playground/src/preview-service.ts
1813
+ /** Preview iframe 内的高度服务;class 会被序列化到 iframe 执行。 */
1814
+ var PlaygroundService = class {
1815
+ target;
1816
+ vue;
1817
+ session;
1818
+ /* istanbul ignore next -- class 会被序列化到没有覆盖率运行时的 iframe。 */
1819
+ sequence = 0;
1820
+ /* istanbul ignore next -- class 会被序列化到没有覆盖率运行时的 iframe。 */
1821
+ disposed = false;
1822
+ manual;
1823
+ /* istanbul ignore next -- class 会被序列化到没有覆盖率运行时的 iframe。 */
1824
+ requests = /* @__PURE__ */ new Set();
1825
+ /* istanbul ignore next -- class 会被序列化到没有覆盖率运行时的 iframe。 */
1826
+ owners = /* @__PURE__ */ new Set();
1827
+ /**
1828
+ * @param target 当前预览窗口。
1829
+ * @param vue 与预览应用共用的 Vue 实例。
1830
+ */
1831
+ /* istanbul ignore next -- class 会被序列化到没有覆盖率运行时的 iframe。 */
1832
+ constructor(target, vue) {
1833
+ this.target = target;
1834
+ this.vue = vue;
1835
+ this.session = `${Date.now()}-${Math.random()}`;
1836
+ this.dispose = this.dispose.bind(this);
1837
+ this.post("start");
1838
+ this.target.addEventListener("pagehide", this.dispose);
1839
+ }
1840
+ /**
1841
+ * 申请手动临时高度;新请求会替换上一个手动请求。
1842
+ * @param height iframe 最小高度。
1843
+ * @returns iframe 是否在超时前应用目标高度。
1844
+ */
1845
+ enable(height) {
1846
+ this.validateHeight(height);
1847
+ this.manual?.release();
1848
+ this.manual = this.createRequest(height);
1849
+ return this.manual.ready;
1850
+ }
1851
+ /** 释放手动临时高度,不影响 `run` 创建的请求。 */
1852
+ disable() {
1853
+ this.manual?.release();
1854
+ this.manual = void 0;
1855
+ }
1856
+ /**
1857
+ * 创建等待高度后执行的事件处理函数。
1858
+ * @param height iframe 最小高度。
1859
+ * @param optionsOrHandler 可见状态配置,或最后一个 handler。
1860
+ * @param lastHandler 使用配置时的最后一个 handler。
1861
+ * @returns 保留 handler 参数和结果的异步事件处理函数。
1862
+ */
1863
+ run(height, optionsOrHandler, lastHandler) {
1864
+ this.validateHeight(height);
1865
+ if (optionsOrHandler === void 0) return (visible) => {
1866
+ if (visible) return this.enable(height);
1867
+ this.disable();
1868
+ return Promise.resolve(void 0);
1869
+ };
1870
+ const visible = (typeof optionsOrHandler === "function" ? {} : optionsOrHandler).visible;
1871
+ if (visible !== void 0 && !this.vue.isRef(visible) && typeof visible !== "function") throw new TypeError("Playground visible must be a ref or getter");
1872
+ const handler = typeof optionsOrHandler === "function" ? optionsOrHandler : lastHandler ?? (this.vue.isRef(visible) ? () => {
1873
+ visible.value = true;
1874
+ } : void 0);
1875
+ if (typeof handler !== "function") throw new TypeError("Playground run requires a handler");
1876
+ let ownerDisposed = false;
1877
+ let current;
1878
+ let pending;
1879
+ const disposeOwner = () => {
1880
+ ownerDisposed = true;
1881
+ current?.release();
1882
+ this.owners.delete(disposeOwner);
1883
+ };
1884
+ this.owners.add(disposeOwner);
1885
+ if (this.vue.getCurrentScope()) this.vue.onScopeDispose(disposeOwner);
1886
+ return (...args) => {
1887
+ if (this.disposed || ownerDisposed) return Promise.resolve(void 0);
1888
+ if (pending) return pending;
1889
+ const request = current = this.createRequest(height);
1890
+ let executionFinished = false;
1891
+ const unlock = () => {
1892
+ if (executionFinished && request.released && current === request) {
1893
+ pending = void 0;
1894
+ current = void 0;
1895
+ }
1896
+ };
1897
+ request.closed.then(unlock);
1898
+ const execution = (async () => {
1899
+ try {
1900
+ if (!await request.ready || ownerDisposed || this.disposed) return void 0;
1901
+ if (visible) request.watch(visible);
1902
+ const result = await handler(...args);
1903
+ if (!visible || !this.vue.toValue(visible)) request.release();
1904
+ return result;
1905
+ } catch (error) {
1906
+ request.release();
1907
+ throw error;
1908
+ } finally {
1909
+ executionFinished = true;
1910
+ unlock();
1911
+ }
1912
+ })();
1913
+ pending = execution;
1914
+ return execution;
1915
+ };
1916
+ }
1917
+ /** 释放当前预览应用持有的全部高度和监听。 */
1918
+ /* istanbul ignore next -- 注入测试会从序列化后的 iframe class 调用该方法。 */
1919
+ dispose() {
1920
+ if (this.disposed) return;
1921
+ this.disposed = true;
1922
+ for (const disposeOwner of this.owners) disposeOwner();
1923
+ for (const request of this.requests) request.release();
1924
+ this.post("stop");
1925
+ this.target.removeEventListener("pagehide", this.dispose);
1926
+ if (this.target.$docsPlayground === this) delete this.target.$docsPlayground;
1927
+ }
1928
+ /** @param height iframe 最小高度。 */
1929
+ validateHeight(height) {
1930
+ if (!Number.isFinite(height) || height <= 0) throw new TypeError("Playground height must be a positive finite number");
1931
+ }
1932
+ /**
1933
+ * @param operation 请求操作。
1934
+ * @param id 请求标识。
1935
+ * @param height iframe 最小高度。
1936
+ */
1937
+ /* istanbul ignore next -- 构造和销毁时会从序列化后的 iframe class 调用该方法。 */
1938
+ post(operation, id = 0, height = 0) {
1939
+ this.target.parent.postMessage({
1940
+ action: "docs:height",
1941
+ session: this.session,
1942
+ operation,
1943
+ id,
1944
+ height
1945
+ }, "*");
1946
+ }
1947
+ /**
1948
+ * @param height iframe 最小高度。
1949
+ * @returns 内部等待与释放句柄。
1950
+ */
1951
+ createRequest(height) {
1952
+ this.validateHeight(height);
1953
+ const id = ++this.sequence;
1954
+ let released = false;
1955
+ let settled = false;
1956
+ let frame = 0;
1957
+ let timer = 0;
1958
+ let stopWatch;
1959
+ let resolveReady;
1960
+ let resolveClosed;
1961
+ const ready = new Promise((resolve) => {
1962
+ resolveReady = resolve;
1963
+ });
1964
+ const closed = new Promise((resolve) => {
1965
+ resolveClosed = resolve;
1966
+ });
1967
+ const stopWaiting = () => {
1968
+ this.target.cancelAnimationFrame(frame);
1969
+ if (timer) {
1970
+ this.target.clearTimeout(timer);
1971
+ timer = 0;
1972
+ }
1973
+ this.target.removeEventListener("message", onMessage);
1974
+ };
1975
+ const release = () => {
1976
+ if (released) return;
1977
+ released = true;
1978
+ stopWaiting();
1979
+ stopWatch?.();
1980
+ resolveReady(false);
1981
+ resolveClosed();
1982
+ this.requests.delete(request);
1983
+ this.post("release", id);
1984
+ };
1985
+ const check = () => {
1986
+ if (released) return;
1987
+ if (this.target.innerHeight >= height) {
1988
+ settled = true;
1989
+ stopWaiting();
1990
+ resolveReady(true);
1991
+ } else frame = this.target.requestAnimationFrame(check);
1992
+ };
1993
+ const onMessage = (event) => {
1994
+ const data = event.data;
1995
+ if (event.source !== this.target.parent || data?.action !== "docs:height:applied" || data.session !== this.session || data.id !== id || settled) return;
1996
+ if (!data.accepted) release();
1997
+ else check();
1998
+ };
1999
+ const request = {
2000
+ ready,
2001
+ closed,
2002
+ get released() {
2003
+ return released;
2004
+ },
2005
+ release,
2006
+ watch: (visible) => {
2007
+ stopWatch = this.vue.watch(() => this.vue.toValue(visible), (value) => {
2008
+ if (!value) release();
2009
+ }, { flush: "sync" });
2010
+ }
2011
+ };
2012
+ this.requests.add(request);
2013
+ if (this.disposed) release();
2014
+ else {
2015
+ this.target.addEventListener("message", onMessage);
2016
+ timer = this.target.setTimeout(() => {
2017
+ timer = 0;
2018
+ release();
2019
+ }, 2e3);
2020
+ this.post("enable", id, height);
2021
+ }
2022
+ return request;
2023
+ }
2024
+ };
2025
+ var PREVIEW_SERVICE_IMPORT_CODE = "import * as __docsPreviewVue from \"vue\"";
2026
+ var PREVIEW_SERVICE_USE_CODE = [
2027
+ `;const __docsPlayground=new (${PlaygroundService.toString()})(window,__docsPreviewVue);`,
2028
+ "app.provide(\"docs:playground\",__docsPlayground);",
2029
+ "window.$docsPlayground=__docsPlayground;",
2030
+ "app.mixin({beforeUnmount(){if(this===this.$root)__docsPlayground.dispose()}});"
2031
+ ].join("");
2032
+ //#endregion
1736
2033
  //#region packages/playground/src/core/scss.ts
1737
2034
  var SCHEME = "docs-playground";
1738
2035
  var STYLE_EXTS = [".scss", ".sass"];
@@ -1990,8 +2287,8 @@ var createRuntimePreviewOptions = (cdnURL = DEFAULT_CDN_URL, options = {}) => {
1990
2287
  "</style>"
1991
2288
  ].join("\n"),
1992
2289
  customCode: {
1993
- importCode: DOCS_LINK_IMPORT_CODE,
1994
- useCode: DOCS_LINK_USE_CODE
2290
+ importCode: `${DOCS_LINK_IMPORT_CODE}\n${PREVIEW_SERVICE_IMPORT_CODE}`,
2291
+ useCode: `${DOCS_LINK_USE_CODE}\n${PREVIEW_SERVICE_USE_CODE}`
1995
2292
  }
1996
2293
  };
1997
2294
  };
@@ -2072,6 +2369,8 @@ var wrapper_default = /* @__PURE__ */ (0, vue.defineComponent)({
2072
2369
  const { t } = (0, _deot_docs_locale.useLocale)();
2073
2370
  const currentViewport = (0, vue.ref)(props.viewport);
2074
2371
  const sandboxRef = (0, vue.ref)(null);
2372
+ const availableHeight = () => Math.max(0, getWindowInnerSize().height - 40 - 44);
2373
+ const temporaryHeight = usePreviewHeightBridge(sandboxRef, availableHeight);
2075
2374
  const sandboxKey = (0, vue.ref)(0);
2076
2375
  const layoutTick = (0, vue.ref)(0);
2077
2376
  const runtimeError = useSandboxRuntimeErrorGuard(sandboxRef);
@@ -2083,7 +2382,16 @@ var wrapper_default = /* @__PURE__ */ (0, vue.defineComponent)({
2083
2382
  });
2084
2383
  const layout = (0, vue.computed)(() => {
2085
2384
  layoutTick.value;
2086
- return resolvePopupLayout(currentViewport.value);
2385
+ const base = resolvePopupLayout(currentViewport.value);
2386
+ if (!temporaryHeight.value) return base;
2387
+ const canvasHeight = Math.min(availableHeight(), Math.max(base.canvasHeight, temporaryHeight.value));
2388
+ const shellHeight = Math.max(base.shellHeight, canvasHeight + 44);
2389
+ return {
2390
+ ...base,
2391
+ canvasHeight,
2392
+ shellHeight,
2393
+ bodyHeight: shellHeight - 44
2394
+ };
2087
2395
  });
2088
2396
  const shellStyle = (0, vue.computed)(() => ({
2089
2397
  width: `${layout.value.shellWidth}px`,
@@ -2294,7 +2602,7 @@ var runtime_default = /* @__PURE__ */ (0, vue.defineComponent)({
2294
2602
  extraHeadHTML
2295
2603
  ].filter(Boolean).join("\n"),
2296
2604
  customCode: {
2297
- importCode: joinCode(DOCS_LINK_IMPORT_CODE, props.previewOptions?.customCode?.importCode, props.previewScroller ? PREVIEW_SCROLL_RESET_CODE : void 0, props.previewScroller ? PREVIEW_SCROLLER_IMPORT_CODE : void 0),
2605
+ importCode: joinCode(runtimePreviewOptions.customCode?.importCode, props.previewOptions?.customCode?.importCode, props.previewScroller ? PREVIEW_SCROLL_RESET_CODE : void 0, props.previewScroller ? PREVIEW_SCROLLER_IMPORT_CODE : void 0),
2298
2606
  useCode: joinCode(runtimePreviewOptions.customCode?.useCode, props.previewOptions?.customCode?.useCode, props.previewScroller ? PREVIEW_SCROLLER_USE_CODE : void 0)
2299
2607
  }
2300
2608
  };
@@ -2311,6 +2619,7 @@ var runtime_default = /* @__PURE__ */ (0, vue.defineComponent)({
2311
2619
  const copyValue = (0, vue.computed)(() => props.files[props.entry] || "");
2312
2620
  const store = createRuntimeStore(props.files, props.entry, props.options);
2313
2621
  const sandboxRef = (0, vue.ref)(null);
2622
+ const temporaryHeight = usePreviewHeightBridge(sandboxRef);
2314
2623
  const sandboxKey = (0, vue.ref)(0);
2315
2624
  const runtimeHeight = useSandboxAutoHeight(sandboxRef);
2316
2625
  const runtimeError = useSandboxRuntimeErrorGuard(sandboxRef);
@@ -2364,13 +2673,14 @@ var runtime_default = /* @__PURE__ */ (0, vue.defineComponent)({
2364
2673
  }
2365
2674
  const canExpandPreview = (0, vue.computed)(() => !props.styleless && isPlaygroundExpandable(props.expandable));
2366
2675
  const expandLabel = (0, vue.computed)(() => t(previewExpanded.value ? "playground.runtime.collapsePreview" : "playground.runtime.expandPreview"));
2367
- const desiredViewportHeight = (0, vue.computed)(() => {
2676
+ const baseViewportHeight = (0, vue.computed)(() => {
2368
2677
  if (previewExpanded.value && props.expandable === true) return frozenExpandedHeight.value;
2369
2678
  if (previewExpanded.value && isPlaygroundExpandable(props.expandable)) return resolveExpandedPreviewHeight(props.expandable, 0);
2370
2679
  const fixedHeight = getViewportHeight(props.viewport);
2371
2680
  if (fixedHeight) return fixedHeight;
2372
2681
  return runtimeHeight.value;
2373
2682
  });
2683
+ const desiredViewportHeight = (0, vue.computed)(() => Math.max(baseViewportHeight.value, temporaryHeight.value));
2374
2684
  const scrollExpandedPreviewIntoView = () => {
2375
2685
  scrollPlaygroundToViewportStart(runtimeRoot.value);
2376
2686
  };
@@ -2892,6 +3202,7 @@ exports.PREVIEW_SCROLLER_USE_CODE = PREVIEW_SCROLLER_USE_CODE;
2892
3202
  exports.PREVIEW_SCROLL_CONTENT_CLASS = PREVIEW_SCROLL_CONTENT_CLASS;
2893
3203
  exports.PREVIEW_SCROLL_HTML_CLASS = PREVIEW_SCROLL_HTML_CLASS;
2894
3204
  exports.Playground = playground_default;
3205
+ exports.PlaygroundService = PlaygroundService;
2895
3206
  exports.applyPlaygroundImportMapOverride = applyPlaygroundImportMapOverride;
2896
3207
  exports.applyPlaygroundStyleOverride = applyPlaygroundStyleOverride;
2897
3208
  exports.clearPlaygroundMaps = clearPlaygroundMaps;
package/dist/index.d.ts CHANGED
@@ -7,8 +7,10 @@ import { Language } from '@deot/docs-locale';
7
7
  import type { LanguageFn } from 'highlight.js';
8
8
  import { Portal } from '@deot/vc';
9
9
  import { PublicProps } from 'vue';
10
+ import type { Ref } from 'vue';
10
11
  import type { SandboxProps } from '@vue/repl';
11
12
  import type { StoreState } from '@vue/repl';
13
+ import { vue } from 'vue';
12
14
 
13
15
  declare const __VLS_export: DefineComponent<__VLS_Props, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
14
16
  language: string;
@@ -187,6 +189,34 @@ export declare type PlaygroundPreviewOptions = SandboxProps['previewOptions'];
187
189
 
188
190
  export declare type PlaygroundPreviewScroller = boolean;
189
191
 
192
+ export declare interface PlaygroundRunOptions {
193
+ visible?: Ref<boolean> | (() => boolean);
194
+ }
195
+
196
+ export declare class PlaygroundService {
197
+ private readonly target;
198
+ private readonly vue;
199
+ private readonly session;
200
+ private sequence;
201
+ private disposed;
202
+ private manual?;
203
+ private readonly requests;
204
+ private readonly owners;
205
+ constructor(target: Window, vue: PreviewVue);
206
+ enable(height: number): Promise<boolean>;
207
+ disable(): void;
208
+ run(height: number): (visible: boolean) => Promise<boolean | undefined>;
209
+ run<Args extends unknown[], Result>(height: number, handler: (...args: Args) => Result): (...args: Args) => Promise<Awaited<Result> | undefined>;
210
+ run<Args extends unknown[], Result>(height: number, options: PlaygroundRunOptions, handler: (...args: Args) => Result): (...args: Args) => Promise<Awaited<Result> | undefined>;
211
+ run(height: number, options: PlaygroundRunOptions & {
212
+ visible: Ref<boolean>;
213
+ }): () => Promise<void | undefined>;
214
+ dispose(): void;
215
+ private validateHeight;
216
+ private post;
217
+ private createRequest;
218
+ }
219
+
190
220
  export declare type PlaygroundView = 'runtime' | 'files';
191
221
 
192
222
  export declare type PlaygroundViewport = 'auto' | number | [width: number, height: number];
@@ -196,12 +226,18 @@ export declare interface PlaygroundViewsProps {
196
226
  activeView: PlaygroundView;
197
227
  }
198
228
 
229
+ export declare interface PlaygroundWindow extends Window {
230
+ $docsPlayground: PlaygroundService;
231
+ }
232
+
199
233
  export declare const PREVIEW_SCROLL_CONTENT_CLASS = "docs-playground-preview-scroll__content";
200
234
 
201
235
  export declare const PREVIEW_SCROLL_HTML_CLASS = "docs-playground-preview-scroll";
202
236
 
203
237
  export declare const PREVIEW_SCROLLER_USE_CODE: string;
204
238
 
239
+ declare type PreviewVue = Pick<vue, 'getCurrentScope' | 'onScopeDispose' | 'watch' | 'toValue' | 'isRef'>;
240
+
205
241
  export declare const registerVueHighlight: (api?: HLJSApi) => HLJSApi;
206
242
 
207
243
  export declare const removePlaygroundImportMapOverride: (alias: string) => {
package/dist/index.js CHANGED
@@ -869,6 +869,8 @@ var PREVIEW_SCROLLER_USE_CODE = [
869
869
  "if(app._context&&next._context){",
870
870
  "if(app._context.components)Object.assign(next._context.components,app._context.components);",
871
871
  "if(app._context.directives)Object.assign(next._context.directives,app._context.directives);",
872
+ "Object.assign(next._context.provides,app._context.provides);",
873
+ "next._context.mixins.push(...app._context.mixins);",
872
874
  "}",
873
875
  "window.__app__=next;",
874
876
  `document.documentElement.classList.add("${PREVIEW_SCROLL_HTML_CLASS}");`,
@@ -915,6 +917,7 @@ var useSandboxAutoHeight = (sandboxRef) => {
915
917
  const collapsedTop = Math.max(0, bodyRect.top - rootTop);
916
918
  let childrenBottom = 0;
917
919
  for (const child of body.children) {
920
+ if (iframeWindow.getComputedStyle(child).position === "fixed") continue;
918
921
  const rect = child.getBoundingClientRect();
919
922
  const marginBottom = Number.parseFloat(iframeWindow.getComputedStyle(child).marginBottom) || 0;
920
923
  childrenBottom = Math.max(childrenBottom, rect.bottom + marginBottom - rootTop);
@@ -935,9 +938,14 @@ var useSandboxAutoHeight = (sandboxRef) => {
935
938
  }
936
939
  return Math.ceil(Math.max(scrollContent.scrollHeight + margins, scrollContent.offsetHeight + margins, childrenBottom + margins));
937
940
  };
938
- const isEmptyPreviewApp = (iframeDocument) => {
941
+ const isPendingPreviewApp = (iframeDocument) => {
939
942
  const app = iframeDocument.getElementById("app");
940
- return !!app && app.childElementCount === 0;
943
+ return app ? app.childElementCount === 0 : !!iframe?.srcdoc;
944
+ };
945
+ const releaseBootstrapHeight = (target) => {
946
+ nextTick(() => {
947
+ if (iframe === target) target.style.height = "100%";
948
+ });
941
949
  };
942
950
  const measure = () => {
943
951
  frameId = 0;
@@ -947,10 +955,11 @@ var useSandboxAutoHeight = (sandboxRef) => {
947
955
  const iframeDocument = iframe.contentDocument;
948
956
  if (!iframeWindow || !iframeDocument?.body) return;
949
957
  const scrollContent = resolveScrollContent(iframeDocument);
950
- if (!scrollContent && isEmptyPreviewApp(iframeDocument)) return;
958
+ if (!scrollContent && isPendingPreviewApp(iframeDocument)) return;
951
959
  const contentHeight = scrollContent ? measureScrollContent(scrollContent) : measureDocumentContent(iframeWindow, iframeDocument);
952
960
  const nextHeight = Math.max(contentHeight, 24);
953
961
  if (height.value !== nextHeight) height.value = nextHeight;
962
+ releaseBootstrapHeight(iframe);
954
963
  } catch {
955
964
  if (height.value !== 24) height.value = 24;
956
965
  }
@@ -1019,7 +1028,7 @@ var useSandboxAutoHeight = (sandboxRef) => {
1019
1028
  return;
1020
1029
  }
1021
1030
  iframe.addEventListener("load", handleIframeLoad);
1022
- iframe.style.height = "100%";
1031
+ iframe.style.height = `${Math.max(window.innerHeight, 24)}px`;
1023
1032
  observeContent();
1024
1033
  };
1025
1034
  const syncIframe = () => setIframe(container?.querySelector("iframe") || null);
@@ -1369,6 +1378,73 @@ var useSandboxTheme = (sandboxRef) => {
1369
1378
  });
1370
1379
  };
1371
1380
  //#endregion
1381
+ //#region packages/playground/src/core/runtime/height-bridge.ts
1382
+ /**
1383
+ * 只接收本宿主当前 iframe 的高度请求,应用重评时切换会话。
1384
+ * @param sandbox 预览容器。
1385
+ * @param maxHeight 当前可用高度,普通预览默认不限制。
1386
+ * @returns 所有有效请求的最大高度。
1387
+ */
1388
+ function usePreviewHeightBridge(sandbox, maxHeight = () => Infinity) {
1389
+ const height = ref(0);
1390
+ const requests = /* @__PURE__ */ new Map();
1391
+ let source = null;
1392
+ let session = "";
1393
+ let disposed = false;
1394
+ const sync = () => {
1395
+ height.value = Math.max(0, ...requests.values());
1396
+ };
1397
+ const reset = () => {
1398
+ requests.clear();
1399
+ session = "";
1400
+ source = null;
1401
+ sync();
1402
+ };
1403
+ const handleMessage = async (event) => {
1404
+ const iframe = resolveSandboxContainer(sandbox.value)?.querySelector("iframe");
1405
+ const data = event.data;
1406
+ if (!iframe?.contentWindow || event.source !== iframe.contentWindow || data?.action !== "docs:height" || typeof data.session !== "string") return;
1407
+ if (data.operation === "start") {
1408
+ reset();
1409
+ source = iframe.contentWindow;
1410
+ session = data.session;
1411
+ return;
1412
+ }
1413
+ if (source !== iframe.contentWindow || session !== data.session) return;
1414
+ if (data.operation === "stop") {
1415
+ reset();
1416
+ return;
1417
+ }
1418
+ if (!Number.isSafeInteger(data.id) || data.id <= 0) return;
1419
+ if (data.operation === "release") {
1420
+ requests.delete(data.id);
1421
+ sync();
1422
+ return;
1423
+ }
1424
+ if (data.operation !== "enable" || !Number.isFinite(data.height) || data.height <= 0) return;
1425
+ const accepted = data.height <= maxHeight();
1426
+ if (accepted) requests.set(data.id, data.height);
1427
+ sync();
1428
+ const receiver = source;
1429
+ await nextTick();
1430
+ if (disposed || source !== receiver || session !== data.session) return;
1431
+ receiver?.postMessage({
1432
+ action: "docs:height:applied",
1433
+ session,
1434
+ id: data.id,
1435
+ accepted
1436
+ }, "*");
1437
+ };
1438
+ watch(() => resolveSandboxContainer(sandbox.value), reset, { flush: "sync" });
1439
+ if (typeof window !== "undefined") window.addEventListener("message", handleMessage);
1440
+ onBeforeUnmount(() => {
1441
+ disposed = true;
1442
+ if (typeof window !== "undefined") window.removeEventListener("message", handleMessage);
1443
+ reset();
1444
+ });
1445
+ return height;
1446
+ }
1447
+ //#endregion
1372
1448
  //#region packages/playground/src/core/runtime/toolbar.vue?vue&type=script&setup=true&lang.ts
1373
1449
  var _hoisted_1$3 = { class: "docs-playground__tools" };
1374
1450
  var _hoisted_2$3 = {
@@ -1613,7 +1689,9 @@ var BUILTIN_IMPORT_ASSETS = {
1613
1689
  "@deot/helper": "@deot/helper/dist/index.js",
1614
1690
  "normalize-wheel": "normalize-wheel-es/dist/index.mjs",
1615
1691
  "photoswipe": "photoswipe/dist/photoswipe.esm.js",
1616
- "photoswipe/lightbox": "photoswipe/dist/photoswipe-lightbox.esm.js"
1692
+ "photoswipe/lightbox": "photoswipe/dist/photoswipe-lightbox.esm.js",
1693
+ "vue": "vue/dist/vue.runtime.esm-browser.js",
1694
+ "vue/server-renderer": "vue/dist/server-renderer.esm-browser.js"
1617
1695
  };
1618
1696
  var normalizeCdnURL = (cdnURL = DEFAULT_CDN_URL) => {
1619
1697
  return cdnURL.trim().replace(/\/+$/u, "") || "https://cdn.jsdelivr.net/npm";
@@ -1624,9 +1702,7 @@ var SASS_CDN_URL = jsdelivrEsmURL("sass/+esm");
1624
1702
  var cdnAsset = (cdnURL, asset) => cdnJoin(normalizeCdnURL(cdnURL), asset);
1625
1703
  var createBuiltinImports = (cdnURL = DEFAULT_CDN_URL) => ({
1626
1704
  ...Object.fromEntries(Object.entries(BUILTIN_IMPORT_ASSETS).map(([specifier, asset]) => [specifier, cdnAsset(cdnURL, asset)])),
1627
- "lodash-es": jsdelivrEsmURL("lodash-es/+esm"),
1628
- "vue": "https://play.vuejs.org/vue.runtime.esm-browser.js",
1629
- "vue/server-renderer": "https://play.vuejs.org/server-renderer.esm-browser.js"
1705
+ "lodash-es": jsdelivrEsmURL("lodash-es/+esm")
1630
1706
  });
1631
1707
  var createBuiltinStyles = (cdnURL = DEFAULT_CDN_URL) => Object.fromEntries(PREVIEW_STYLE_ASSETS.map((asset) => [asset, cdnAsset(cdnURL, asset)]));
1632
1708
  //#endregion
@@ -1706,6 +1782,227 @@ var clearPlaygroundMaps = () => {
1706
1782
  ].forEach((store) => store.set());
1707
1783
  };
1708
1784
  //#endregion
1785
+ //#region packages/playground/src/preview-service.ts
1786
+ /** Preview iframe 内的高度服务;class 会被序列化到 iframe 执行。 */
1787
+ var PlaygroundService = class {
1788
+ target;
1789
+ vue;
1790
+ session;
1791
+ /* istanbul ignore next -- class 会被序列化到没有覆盖率运行时的 iframe。 */
1792
+ sequence = 0;
1793
+ /* istanbul ignore next -- class 会被序列化到没有覆盖率运行时的 iframe。 */
1794
+ disposed = false;
1795
+ manual;
1796
+ /* istanbul ignore next -- class 会被序列化到没有覆盖率运行时的 iframe。 */
1797
+ requests = /* @__PURE__ */ new Set();
1798
+ /* istanbul ignore next -- class 会被序列化到没有覆盖率运行时的 iframe。 */
1799
+ owners = /* @__PURE__ */ new Set();
1800
+ /**
1801
+ * @param target 当前预览窗口。
1802
+ * @param vue 与预览应用共用的 Vue 实例。
1803
+ */
1804
+ /* istanbul ignore next -- class 会被序列化到没有覆盖率运行时的 iframe。 */
1805
+ constructor(target, vue) {
1806
+ this.target = target;
1807
+ this.vue = vue;
1808
+ this.session = `${Date.now()}-${Math.random()}`;
1809
+ this.dispose = this.dispose.bind(this);
1810
+ this.post("start");
1811
+ this.target.addEventListener("pagehide", this.dispose);
1812
+ }
1813
+ /**
1814
+ * 申请手动临时高度;新请求会替换上一个手动请求。
1815
+ * @param height iframe 最小高度。
1816
+ * @returns iframe 是否在超时前应用目标高度。
1817
+ */
1818
+ enable(height) {
1819
+ this.validateHeight(height);
1820
+ this.manual?.release();
1821
+ this.manual = this.createRequest(height);
1822
+ return this.manual.ready;
1823
+ }
1824
+ /** 释放手动临时高度,不影响 `run` 创建的请求。 */
1825
+ disable() {
1826
+ this.manual?.release();
1827
+ this.manual = void 0;
1828
+ }
1829
+ /**
1830
+ * 创建等待高度后执行的事件处理函数。
1831
+ * @param height iframe 最小高度。
1832
+ * @param optionsOrHandler 可见状态配置,或最后一个 handler。
1833
+ * @param lastHandler 使用配置时的最后一个 handler。
1834
+ * @returns 保留 handler 参数和结果的异步事件处理函数。
1835
+ */
1836
+ run(height, optionsOrHandler, lastHandler) {
1837
+ this.validateHeight(height);
1838
+ if (optionsOrHandler === void 0) return (visible) => {
1839
+ if (visible) return this.enable(height);
1840
+ this.disable();
1841
+ return Promise.resolve(void 0);
1842
+ };
1843
+ const visible = (typeof optionsOrHandler === "function" ? {} : optionsOrHandler).visible;
1844
+ if (visible !== void 0 && !this.vue.isRef(visible) && typeof visible !== "function") throw new TypeError("Playground visible must be a ref or getter");
1845
+ const handler = typeof optionsOrHandler === "function" ? optionsOrHandler : lastHandler ?? (this.vue.isRef(visible) ? () => {
1846
+ visible.value = true;
1847
+ } : void 0);
1848
+ if (typeof handler !== "function") throw new TypeError("Playground run requires a handler");
1849
+ let ownerDisposed = false;
1850
+ let current;
1851
+ let pending;
1852
+ const disposeOwner = () => {
1853
+ ownerDisposed = true;
1854
+ current?.release();
1855
+ this.owners.delete(disposeOwner);
1856
+ };
1857
+ this.owners.add(disposeOwner);
1858
+ if (this.vue.getCurrentScope()) this.vue.onScopeDispose(disposeOwner);
1859
+ return (...args) => {
1860
+ if (this.disposed || ownerDisposed) return Promise.resolve(void 0);
1861
+ if (pending) return pending;
1862
+ const request = current = this.createRequest(height);
1863
+ let executionFinished = false;
1864
+ const unlock = () => {
1865
+ if (executionFinished && request.released && current === request) {
1866
+ pending = void 0;
1867
+ current = void 0;
1868
+ }
1869
+ };
1870
+ request.closed.then(unlock);
1871
+ const execution = (async () => {
1872
+ try {
1873
+ if (!await request.ready || ownerDisposed || this.disposed) return void 0;
1874
+ if (visible) request.watch(visible);
1875
+ const result = await handler(...args);
1876
+ if (!visible || !this.vue.toValue(visible)) request.release();
1877
+ return result;
1878
+ } catch (error) {
1879
+ request.release();
1880
+ throw error;
1881
+ } finally {
1882
+ executionFinished = true;
1883
+ unlock();
1884
+ }
1885
+ })();
1886
+ pending = execution;
1887
+ return execution;
1888
+ };
1889
+ }
1890
+ /** 释放当前预览应用持有的全部高度和监听。 */
1891
+ /* istanbul ignore next -- 注入测试会从序列化后的 iframe class 调用该方法。 */
1892
+ dispose() {
1893
+ if (this.disposed) return;
1894
+ this.disposed = true;
1895
+ for (const disposeOwner of this.owners) disposeOwner();
1896
+ for (const request of this.requests) request.release();
1897
+ this.post("stop");
1898
+ this.target.removeEventListener("pagehide", this.dispose);
1899
+ if (this.target.$docsPlayground === this) delete this.target.$docsPlayground;
1900
+ }
1901
+ /** @param height iframe 最小高度。 */
1902
+ validateHeight(height) {
1903
+ if (!Number.isFinite(height) || height <= 0) throw new TypeError("Playground height must be a positive finite number");
1904
+ }
1905
+ /**
1906
+ * @param operation 请求操作。
1907
+ * @param id 请求标识。
1908
+ * @param height iframe 最小高度。
1909
+ */
1910
+ /* istanbul ignore next -- 构造和销毁时会从序列化后的 iframe class 调用该方法。 */
1911
+ post(operation, id = 0, height = 0) {
1912
+ this.target.parent.postMessage({
1913
+ action: "docs:height",
1914
+ session: this.session,
1915
+ operation,
1916
+ id,
1917
+ height
1918
+ }, "*");
1919
+ }
1920
+ /**
1921
+ * @param height iframe 最小高度。
1922
+ * @returns 内部等待与释放句柄。
1923
+ */
1924
+ createRequest(height) {
1925
+ this.validateHeight(height);
1926
+ const id = ++this.sequence;
1927
+ let released = false;
1928
+ let settled = false;
1929
+ let frame = 0;
1930
+ let timer = 0;
1931
+ let stopWatch;
1932
+ let resolveReady;
1933
+ let resolveClosed;
1934
+ const ready = new Promise((resolve) => {
1935
+ resolveReady = resolve;
1936
+ });
1937
+ const closed = new Promise((resolve) => {
1938
+ resolveClosed = resolve;
1939
+ });
1940
+ const stopWaiting = () => {
1941
+ this.target.cancelAnimationFrame(frame);
1942
+ if (timer) {
1943
+ this.target.clearTimeout(timer);
1944
+ timer = 0;
1945
+ }
1946
+ this.target.removeEventListener("message", onMessage);
1947
+ };
1948
+ const release = () => {
1949
+ if (released) return;
1950
+ released = true;
1951
+ stopWaiting();
1952
+ stopWatch?.();
1953
+ resolveReady(false);
1954
+ resolveClosed();
1955
+ this.requests.delete(request);
1956
+ this.post("release", id);
1957
+ };
1958
+ const check = () => {
1959
+ if (released) return;
1960
+ if (this.target.innerHeight >= height) {
1961
+ settled = true;
1962
+ stopWaiting();
1963
+ resolveReady(true);
1964
+ } else frame = this.target.requestAnimationFrame(check);
1965
+ };
1966
+ const onMessage = (event) => {
1967
+ const data = event.data;
1968
+ if (event.source !== this.target.parent || data?.action !== "docs:height:applied" || data.session !== this.session || data.id !== id || settled) return;
1969
+ if (!data.accepted) release();
1970
+ else check();
1971
+ };
1972
+ const request = {
1973
+ ready,
1974
+ closed,
1975
+ get released() {
1976
+ return released;
1977
+ },
1978
+ release,
1979
+ watch: (visible) => {
1980
+ stopWatch = this.vue.watch(() => this.vue.toValue(visible), (value) => {
1981
+ if (!value) release();
1982
+ }, { flush: "sync" });
1983
+ }
1984
+ };
1985
+ this.requests.add(request);
1986
+ if (this.disposed) release();
1987
+ else {
1988
+ this.target.addEventListener("message", onMessage);
1989
+ timer = this.target.setTimeout(() => {
1990
+ timer = 0;
1991
+ release();
1992
+ }, 2e3);
1993
+ this.post("enable", id, height);
1994
+ }
1995
+ return request;
1996
+ }
1997
+ };
1998
+ var PREVIEW_SERVICE_IMPORT_CODE = "import * as __docsPreviewVue from \"vue\"";
1999
+ var PREVIEW_SERVICE_USE_CODE = [
2000
+ `;const __docsPlayground=new (${PlaygroundService.toString()})(window,__docsPreviewVue);`,
2001
+ "app.provide(\"docs:playground\",__docsPlayground);",
2002
+ "window.$docsPlayground=__docsPlayground;",
2003
+ "app.mixin({beforeUnmount(){if(this===this.$root)__docsPlayground.dispose()}});"
2004
+ ].join("");
2005
+ //#endregion
1709
2006
  //#region packages/playground/src/core/scss.ts
1710
2007
  var SCHEME = "docs-playground";
1711
2008
  var STYLE_EXTS = [".scss", ".sass"];
@@ -1963,8 +2260,8 @@ var createRuntimePreviewOptions = (cdnURL = DEFAULT_CDN_URL, options = {}) => {
1963
2260
  "</style>"
1964
2261
  ].join("\n"),
1965
2262
  customCode: {
1966
- importCode: DOCS_LINK_IMPORT_CODE,
1967
- useCode: DOCS_LINK_USE_CODE
2263
+ importCode: `${DOCS_LINK_IMPORT_CODE}\n${PREVIEW_SERVICE_IMPORT_CODE}`,
2264
+ useCode: `${DOCS_LINK_USE_CODE}\n${PREVIEW_SERVICE_USE_CODE}`
1968
2265
  }
1969
2266
  };
1970
2267
  };
@@ -2045,6 +2342,8 @@ var Alone = new Portal(/* @__PURE__ */ defineComponent({
2045
2342
  const { t } = useLocale();
2046
2343
  const currentViewport = ref(props.viewport);
2047
2344
  const sandboxRef = ref(null);
2345
+ const availableHeight = () => Math.max(0, getWindowInnerSize().height - 40 - 44);
2346
+ const temporaryHeight = usePreviewHeightBridge(sandboxRef, availableHeight);
2048
2347
  const sandboxKey = ref(0);
2049
2348
  const layoutTick = ref(0);
2050
2349
  const runtimeError = useSandboxRuntimeErrorGuard(sandboxRef);
@@ -2056,7 +2355,16 @@ var Alone = new Portal(/* @__PURE__ */ defineComponent({
2056
2355
  });
2057
2356
  const layout = computed(() => {
2058
2357
  layoutTick.value;
2059
- return resolvePopupLayout(currentViewport.value);
2358
+ const base = resolvePopupLayout(currentViewport.value);
2359
+ if (!temporaryHeight.value) return base;
2360
+ const canvasHeight = Math.min(availableHeight(), Math.max(base.canvasHeight, temporaryHeight.value));
2361
+ const shellHeight = Math.max(base.shellHeight, canvasHeight + 44);
2362
+ return {
2363
+ ...base,
2364
+ canvasHeight,
2365
+ shellHeight,
2366
+ bodyHeight: shellHeight - 44
2367
+ };
2060
2368
  });
2061
2369
  const shellStyle = computed(() => ({
2062
2370
  width: `${layout.value.shellWidth}px`,
@@ -2264,7 +2572,7 @@ var runtime_default = /* @__PURE__ */ defineComponent({
2264
2572
  extraHeadHTML
2265
2573
  ].filter(Boolean).join("\n"),
2266
2574
  customCode: {
2267
- importCode: joinCode(DOCS_LINK_IMPORT_CODE, props.previewOptions?.customCode?.importCode, props.previewScroller ? PREVIEW_SCROLL_RESET_CODE : void 0, props.previewScroller ? PREVIEW_SCROLLER_IMPORT_CODE : void 0),
2575
+ importCode: joinCode(runtimePreviewOptions.customCode?.importCode, props.previewOptions?.customCode?.importCode, props.previewScroller ? PREVIEW_SCROLL_RESET_CODE : void 0, props.previewScroller ? PREVIEW_SCROLLER_IMPORT_CODE : void 0),
2268
2576
  useCode: joinCode(runtimePreviewOptions.customCode?.useCode, props.previewOptions?.customCode?.useCode, props.previewScroller ? PREVIEW_SCROLLER_USE_CODE : void 0)
2269
2577
  }
2270
2578
  };
@@ -2281,6 +2589,7 @@ var runtime_default = /* @__PURE__ */ defineComponent({
2281
2589
  const copyValue = computed(() => props.files[props.entry] || "");
2282
2590
  const store = createRuntimeStore(props.files, props.entry, props.options);
2283
2591
  const sandboxRef = ref(null);
2592
+ const temporaryHeight = usePreviewHeightBridge(sandboxRef);
2284
2593
  const sandboxKey = ref(0);
2285
2594
  const runtimeHeight = useSandboxAutoHeight(sandboxRef);
2286
2595
  const runtimeError = useSandboxRuntimeErrorGuard(sandboxRef);
@@ -2334,13 +2643,14 @@ var runtime_default = /* @__PURE__ */ defineComponent({
2334
2643
  }
2335
2644
  const canExpandPreview = computed(() => !props.styleless && isPlaygroundExpandable(props.expandable));
2336
2645
  const expandLabel = computed(() => t(previewExpanded.value ? "playground.runtime.collapsePreview" : "playground.runtime.expandPreview"));
2337
- const desiredViewportHeight = computed(() => {
2646
+ const baseViewportHeight = computed(() => {
2338
2647
  if (previewExpanded.value && props.expandable === true) return frozenExpandedHeight.value;
2339
2648
  if (previewExpanded.value && isPlaygroundExpandable(props.expandable)) return resolveExpandedPreviewHeight(props.expandable, 0);
2340
2649
  const fixedHeight = getViewportHeight(props.viewport);
2341
2650
  if (fixedHeight) return fixedHeight;
2342
2651
  return runtimeHeight.value;
2343
2652
  });
2653
+ const desiredViewportHeight = computed(() => Math.max(baseViewportHeight.value, temporaryHeight.value));
2344
2654
  const scrollExpandedPreviewIntoView = () => {
2345
2655
  scrollPlaygroundToViewportStart(runtimeRoot.value);
2346
2656
  };
@@ -2855,4 +3165,4 @@ var playground_default = /* @__PURE__ */ defineComponent({
2855
3165
  }
2856
3166
  });
2857
3167
  //#endregion
2858
- export { code_preview_default as CodePreview, DEFAULT_CDN_URL, Editor, PREVIEW_SCROLLER_USE_CODE, PREVIEW_SCROLL_CONTENT_CLASS, PREVIEW_SCROLL_HTML_CLASS, playground_default as Playground, applyPlaygroundImportMapOverride, applyPlaygroundStyleOverride, clearPlaygroundMaps, createBuiltinImports, createBuiltinStyles, createRuntimePreviewOptions, getPlaygroundImportMapOverrides, getPlaygroundSiteModules, getPlaygroundSiteStyles, getPlaygroundStyleOverrides, highlightCode, highlightCodeByLanguage, isSafePlaygroundHref, normalizeCdnURL, registerVueHighlight, removePlaygroundImportMapOverride, removePlaygroundStyleOverride, resolveHighlightLanguage, resolvePlaygroundTitleId, setPlaygroundImportMapOverrides, setPlaygroundSiteModules, setPlaygroundSiteStyles, setPlaygroundStyleOverrides, slugifyPlaygroundTitle, vueHighlight };
3168
+ export { code_preview_default as CodePreview, DEFAULT_CDN_URL, Editor, PREVIEW_SCROLLER_USE_CODE, PREVIEW_SCROLL_CONTENT_CLASS, PREVIEW_SCROLL_HTML_CLASS, playground_default as Playground, PlaygroundService, applyPlaygroundImportMapOverride, applyPlaygroundStyleOverride, clearPlaygroundMaps, createBuiltinImports, createBuiltinStyles, createRuntimePreviewOptions, getPlaygroundImportMapOverrides, getPlaygroundSiteModules, getPlaygroundSiteStyles, getPlaygroundStyleOverrides, highlightCode, highlightCodeByLanguage, isSafePlaygroundHref, normalizeCdnURL, registerVueHighlight, removePlaygroundImportMapOverride, removePlaygroundStyleOverride, resolveHighlightLanguage, resolvePlaygroundTitleId, setPlaygroundImportMapOverrides, setPlaygroundSiteModules, setPlaygroundSiteStyles, setPlaygroundStyleOverrides, slugifyPlaygroundTitle, vueHighlight };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deot/docs-playground",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "exports": {