hd-garden 0.3.12 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/a2/[slug]/page.tsx +61 -0
- package/app/a2/page.tsx +27 -0
- package/app/admin/page.tsx +11 -15
- package/app/admin/ui/page.tsx +1 -1
- package/app/blog/[...path]/page.tsx +33 -22
- package/app/blog/page.tsx +6 -7
- package/app/columns/[column]/[...path]/page.tsx +33 -25
- package/app/columns/page.tsx +38 -52
- package/app/components/AdminDashboard.tsx +24 -34
- package/app/components/AdminUiPanels.tsx +5 -5
- package/app/components/AdminUiShell.tsx +7 -7
- package/app/components/ArticleMeasure.tsx +95 -0
- package/app/components/ArticleTitle.tsx +20 -0
- package/app/components/BlogArchive.tsx +106 -102
- package/app/components/GardenAdminGuide.tsx +38 -45
- package/app/components/GardenContentGuide.tsx +22 -106
- package/app/components/GardenMediaGuide.tsx +24 -45
- package/app/components/MarkdownArticle.tsx +145 -66
- package/app/components/MermaidDiagram.tsx +44 -71
- package/app/components/PageIntro.tsx +2 -2
- package/app/components/SiteFooter.tsx +3 -7
- package/app/components/SiteHeader.tsx +55 -41
- package/app/components/TableOfContents.tsx +3 -2
- package/app/components/TagIndex.tsx +97 -0
- package/app/components/UiStudio.tsx +2 -2
- package/app/components/garden/GardenBlock.tsx +45 -0
- package/app/components/garden/blocks.tsx +530 -0
- package/app/components/garden/chrome.tsx +350 -0
- package/app/components/garden/sandbox-message.ts +16 -0
- package/app/garden/page.tsx +85 -223
- package/app/garden-modules.d.ts +2 -0
- package/app/globals.css +947 -88
- package/app/layout.tsx +9 -0
- package/app/lib/a2-meta.ts +22 -0
- package/app/lib/a2-preview.ts +50 -0
- package/app/lib/content.ts +48 -5
- package/app/lib/fallback-site-config.ts +1 -1
- package/app/lib/markdown-plugins.ts +2 -0
- package/app/lib/remark-a2.mjs +208 -0
- package/app/lib/remark-garden.mjs +88 -0
- package/app/page.tsx +122 -93
- package/app/styles/a2.css +1383 -0
- package/app/styles/shell.css +438 -0
- package/app/styles/tokens.css +111 -0
- package/app/tags/page.tsx +28 -0
- package/app/ui-parity/page.tsx +3 -2
- package/package.json +2 -2
- package/public/images/garden/admin-studio.png +0 -0
- package/scripts/build-content.mjs +11 -1
- package/scripts/check-post.mjs +18 -4
- package/scripts/new-post.mjs +3 -0
- package/site.config.yaml +2 -2
- package/templates/garden-blocks.md +128 -0
- package/templates/garden-snippets.plist +121 -0
- package/templates/site/site.config.yaml +1 -1
- package/ui/garden/catalog.mjs +196 -0
- package/ui/garden/check.mjs +103 -0
- package/ui/garden/enhance.mjs +89 -0
- package/ui/garden/parse.mjs +198 -0
- package/ui/garden/snippets.mjs +109 -0
- package/ui/garden/title.mjs +37 -0
- package/ui/garden/word-count.mjs +266 -0
- package/ui/host.mjs +45 -28
- package/ui/parity-fixtures.mjs +1 -1
- package/ui/preview/dist/garden-ui.css +81 -77
- package/ui/preview/dist/garden-ui.esm.js +124 -89
- package/ui/preview/dist/garden-ui.iife.js +124 -89
- package/ui/prompt/rules.md +17 -1
- package/ui/react/CatalogPage.tsx +12 -0
- package/ui/react/components/display.tsx +1 -1
- package/ui/react/components/media.tsx +2 -2
- package/ui/react/ui.css +81 -77
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { acceptSandboxSizeMessage } from "./sandbox-message";
|
|
11
|
+
|
|
12
|
+
const COPY_ICON = (
|
|
13
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
14
|
+
<rect x="5.25" y="5.25" width="8.5" height="8.5" rx="1.5" />
|
|
15
|
+
<path d="M10.75 5.25V3.75a1.5 1.5 0 0 0-1.5-1.5h-5.5a1.5 1.5 0 0 0-1.5 1.5v5.5a1.5 1.5 0 0 0 1.5 1.5h1.5" />
|
|
16
|
+
</svg>
|
|
17
|
+
);
|
|
18
|
+
const ENTER_ICON = (
|
|
19
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
20
|
+
<path d="M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10" />
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
const EXIT_ICON = (
|
|
24
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
25
|
+
<path d="M6 2.5V6H2.5M13.5 6H10V2.5M10 13.5V10h3.5M2.5 10H6v3.5" />
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
function writeClipboard(text: string) {
|
|
30
|
+
if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(text);
|
|
31
|
+
return new Promise<void>((resolve, reject) => {
|
|
32
|
+
const area = document.createElement("textarea");
|
|
33
|
+
area.value = text;
|
|
34
|
+
area.setAttribute("readonly", "");
|
|
35
|
+
area.style.position = "fixed";
|
|
36
|
+
area.style.opacity = "0";
|
|
37
|
+
document.body.appendChild(area);
|
|
38
|
+
area.select();
|
|
39
|
+
try {
|
|
40
|
+
if (document.execCommand("copy")) resolve();
|
|
41
|
+
else reject(new Error("copy"));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
reject(error);
|
|
44
|
+
} finally {
|
|
45
|
+
area.remove();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function CopyButton({ getText, label = "复制代码" }: { getText: () => string; label?: string }) {
|
|
51
|
+
const [done, setDone] = useState(false);
|
|
52
|
+
const timer = useRef<number>(0);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
className={`ux-btn ux-copy${done ? " ux-done" : ""}`}
|
|
59
|
+
aria-label={done ? "已复制" : label}
|
|
60
|
+
title={done ? "已复制" : label}
|
|
61
|
+
onClick={() => {
|
|
62
|
+
const text = getText();
|
|
63
|
+
writeClipboard(text).then(() => {
|
|
64
|
+
setDone(true);
|
|
65
|
+
window.clearTimeout(timer.current);
|
|
66
|
+
timer.current = window.setTimeout(() => setDone(false), 1500);
|
|
67
|
+
}).catch(() => setDone(false));
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{done ? "已复制" : COPY_ICON}
|
|
71
|
+
</button>
|
|
72
|
+
<span className="ux-live" aria-live="polite">{done ? "已复制到剪贴板" : ""}</span>
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function CodeCopy({ children }: { children: ReactNode }) {
|
|
78
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
79
|
+
return (
|
|
80
|
+
<div className="ux-cb" ref={ref}>
|
|
81
|
+
{children}
|
|
82
|
+
<CopyButton getText={() => {
|
|
83
|
+
const pre = ref.current?.querySelector("pre");
|
|
84
|
+
const code = pre?.querySelector("code") ?? pre;
|
|
85
|
+
return (code?.textContent ?? "").replace(/\n$/u, "");
|
|
86
|
+
}} />
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function diffLines(source: string) {
|
|
92
|
+
return source.replace(/\n$/u, "").split("\n").map((line) => {
|
|
93
|
+
const kind = line.startsWith("+") ? "add" : line.startsWith("-") ? "del" : "ctx";
|
|
94
|
+
const body = kind === "ctx" ? line.replace(/^ /u, "") : line.slice(1);
|
|
95
|
+
const mark = kind === "add" ? "+" : kind === "del" ? "−" : " ";
|
|
96
|
+
const spoken = kind === "add" ? "(新增)" : kind === "del" ? "(删除)" : "";
|
|
97
|
+
return { kind, body, mark, spoken };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function CodeDiff({
|
|
102
|
+
code,
|
|
103
|
+
diff,
|
|
104
|
+
next,
|
|
105
|
+
}: {
|
|
106
|
+
code: ReactNode;
|
|
107
|
+
diff: string;
|
|
108
|
+
next: string;
|
|
109
|
+
}) {
|
|
110
|
+
const [tab, setTab] = useState(0);
|
|
111
|
+
const base = useId();
|
|
112
|
+
const tabs = ["代码", "对比上一版"];
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="g-tabs g-code-group g-codediff" data-g="tabs">
|
|
116
|
+
<div role="tablist" aria-label="代码">
|
|
117
|
+
{tabs.map((label, index) => (
|
|
118
|
+
<button
|
|
119
|
+
key={label}
|
|
120
|
+
type="button"
|
|
121
|
+
role="tab"
|
|
122
|
+
id={`${base}-t${index}`}
|
|
123
|
+
aria-controls={`${base}-p${index}`}
|
|
124
|
+
aria-selected={tab === index}
|
|
125
|
+
tabIndex={tab === index ? 0 : -1}
|
|
126
|
+
onClick={() => setTab(index)}
|
|
127
|
+
onKeyDown={(event) => {
|
|
128
|
+
const nextIndex = event.key === "ArrowRight" || event.key === "ArrowLeft" ? (index + 1) % 2 : -1;
|
|
129
|
+
if (nextIndex < 0) return;
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
setTab(nextIndex);
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{label}
|
|
135
|
+
</button>
|
|
136
|
+
))}
|
|
137
|
+
<CopyButton getText={() => {
|
|
138
|
+
if (tab === 1) return next;
|
|
139
|
+
const root = document.getElementById(`${base}-p0`);
|
|
140
|
+
return (root?.innerText ?? "").replace(/\n$/u, "");
|
|
141
|
+
}} />
|
|
142
|
+
</div>
|
|
143
|
+
<div role="tabpanel" id={`${base}-p0`} aria-labelledby={`${base}-t0`} hidden={tab !== 0}>
|
|
144
|
+
{code}
|
|
145
|
+
</div>
|
|
146
|
+
<div role="tabpanel" id={`${base}-p1`} aria-labelledby={`${base}-t1`} hidden={tab !== 1}>
|
|
147
|
+
<pre className="code g-diff">
|
|
148
|
+
<code>
|
|
149
|
+
{diffLines(diff).map((line, index) => (
|
|
150
|
+
<span className={`d-${line.kind}`} key={index}>
|
|
151
|
+
<span className="d-m" aria-hidden="true">{line.mark}</span>
|
|
152
|
+
{line.body}
|
|
153
|
+
{line.spoken ? <span className="vh">{line.spoken}</span> : null}
|
|
154
|
+
</span>
|
|
155
|
+
))}
|
|
156
|
+
</code>
|
|
157
|
+
</pre>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function fullscreenElement() {
|
|
164
|
+
return document.fullscreenElement || (document as Document & { webkitFullscreenElement?: Element }).webkitFullscreenElement || null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function FullscreenFrame({ children, label = "演示" }: { children: ReactNode; label?: string }) {
|
|
168
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
169
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
170
|
+
const restoreRef = useRef<HTMLElement | null>(null);
|
|
171
|
+
const [overlay, setOverlay] = useState(false);
|
|
172
|
+
const [active, setActive] = useState(false);
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const onChange = () => {
|
|
176
|
+
const on = fullscreenElement() === ref.current;
|
|
177
|
+
setActive(on || overlay);
|
|
178
|
+
if (!on && !overlay && restoreRef.current) {
|
|
179
|
+
restoreRef.current.focus();
|
|
180
|
+
restoreRef.current = null;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
document.addEventListener("fullscreenchange", onChange);
|
|
184
|
+
document.addEventListener("webkitfullscreenchange", onChange);
|
|
185
|
+
return () => {
|
|
186
|
+
document.removeEventListener("fullscreenchange", onChange);
|
|
187
|
+
document.removeEventListener("webkitfullscreenchange", onChange);
|
|
188
|
+
};
|
|
189
|
+
}, [overlay]);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (!overlay) return undefined;
|
|
193
|
+
const root = ref.current;
|
|
194
|
+
document.documentElement.classList.add("ux-lock");
|
|
195
|
+
const trap = (event: KeyboardEvent) => {
|
|
196
|
+
if (event.key === "Escape") {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
setOverlay(false);
|
|
199
|
+
setActive(false);
|
|
200
|
+
document.documentElement.classList.remove("ux-lock");
|
|
201
|
+
restoreRef.current?.focus();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (event.key !== "Tab" || !root) return;
|
|
205
|
+
const items = [...root.querySelectorAll<HTMLElement>("button, a[href], input, select, textarea, iframe, [tabindex]:not([tabindex='-1'])")]
|
|
206
|
+
.filter((item) => !item.hasAttribute("disabled") && item.getClientRects().length > 0);
|
|
207
|
+
if (!items.length) return;
|
|
208
|
+
const index = items.indexOf(document.activeElement as HTMLElement);
|
|
209
|
+
const last = items.length - 1;
|
|
210
|
+
if (index < 0 || (event.shiftKey && index === 0) || (!event.shiftKey && index === last)) {
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
items[index < 0 ? 0 : event.shiftKey ? last : 0].focus();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const pull = (event: FocusEvent) => {
|
|
216
|
+
if (root && !root.contains(event.target as Node)) buttonRef.current?.focus();
|
|
217
|
+
};
|
|
218
|
+
document.addEventListener("keydown", trap, true);
|
|
219
|
+
document.addEventListener("focusin", pull);
|
|
220
|
+
buttonRef.current?.focus();
|
|
221
|
+
return () => {
|
|
222
|
+
document.documentElement.classList.remove("ux-lock");
|
|
223
|
+
document.removeEventListener("keydown", trap, true);
|
|
224
|
+
document.removeEventListener("focusin", pull);
|
|
225
|
+
};
|
|
226
|
+
}, [overlay]);
|
|
227
|
+
|
|
228
|
+
const open = () => {
|
|
229
|
+
const node = ref.current;
|
|
230
|
+
if (!node) return;
|
|
231
|
+
restoreRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
232
|
+
const request = node.requestFullscreen || (node as HTMLElement & { webkitRequestFullscreen?: () => Promise<void> }).webkitRequestFullscreen;
|
|
233
|
+
if (request && (document.fullscreenEnabled || (document as Document & { webkitFullscreenEnabled?: boolean }).webkitFullscreenEnabled)) {
|
|
234
|
+
void request.call(node);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
setOverlay(true);
|
|
238
|
+
setActive(true);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const close = () => {
|
|
242
|
+
if (fullscreenElement()) {
|
|
243
|
+
void (document.exitFullscreen?.() || (document as Document & { webkitExitFullscreen?: () => Promise<void> }).webkitExitFullscreen?.());
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
setOverlay(false);
|
|
247
|
+
setActive(false);
|
|
248
|
+
restoreRef.current?.focus();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div ref={ref} className={`ux-fs${overlay ? " ux-fs-on" : ""}`} role={overlay ? "dialog" : undefined} aria-modal={overlay ? true : undefined} aria-label={overlay ? label : undefined}>
|
|
253
|
+
<div className="ux-fsbar">
|
|
254
|
+
<button
|
|
255
|
+
ref={buttonRef}
|
|
256
|
+
type="button"
|
|
257
|
+
className="ux-btn ux-fsb"
|
|
258
|
+
aria-label={active ? "退出全屏" : "全屏"}
|
|
259
|
+
title={active ? "退出全屏" : "全屏"}
|
|
260
|
+
onClick={() => (active ? close() : open())}
|
|
261
|
+
>
|
|
262
|
+
{active ? EXIT_ICON : ENTER_ICON}
|
|
263
|
+
</button>
|
|
264
|
+
</div>
|
|
265
|
+
{children}
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const FRAME_BOOT = `<style>
|
|
271
|
+
html,body{background:transparent!important;height:auto!important;min-height:0!important;margin:0;max-width:100%}
|
|
272
|
+
body>*{max-width:100%;box-sizing:border-box}
|
|
273
|
+
svg,img,canvas,video,table{max-width:100%;height:auto}
|
|
274
|
+
#r>style:first-child+*,#r>:first-child:not(style),body>main:first-child>:first-child:not(style){padding-right:44px;box-sizing:border-box}
|
|
275
|
+
</style><script>(function(){
|
|
276
|
+
var last=0,lock=0;
|
|
277
|
+
function fit(){var b=document.body,root=document.documentElement;if(!b)return 1;b.style.zoom="";b.style.transform="";b.style.transformOrigin="";b.style.height="";root.style.height="";var cw=root.clientWidth||b.clientWidth;if(!(cw>0))return 1;var sw=Math.max(root.scrollWidth,b.scrollWidth);if(!(sw>cw+1))return 1;var z=cw/sw;b.style.zoom=String(z);var again=root.scrollWidth;if(again>cw+1&&again<sw-1){z=z*cw/again;b.style.zoom=String(z);again=root.scrollWidth}if(again>cw+1){b.style.zoom="";var h=Math.max(root.scrollHeight,b.scrollHeight);b.style.transformOrigin="0 0";b.style.transform="scale("+z+")";b.style.height=Math.ceil(h*z)+"px";root.style.height=Math.ceil(h*z)+"px"}return z}
|
|
278
|
+
function m(force){if(lock)return;var b=document.body;if(!b)return;lock=1;fit();var h=0,cs=getComputedStyle(b);[].forEach.call(b.children,function(e){if(e.tagName==="SCRIPT"||e.tagName==="STYLE")return;var r=e.getBoundingClientRect(),st=getComputedStyle(e);h=Math.max(h,r.bottom+parseFloat(st.marginBottom||0))});h=Math.ceil(h+parseFloat(cs.paddingBottom||0)+parseFloat(cs.marginBottom||0)+2);if(h>0&&(force||h!==last)){last=h;parent.postMessage({type:"ux:size",height:h,fid:"__UX_FID__"},"*")}lock=0}
|
|
279
|
+
var q=0,pending=0;function s(force){if(force)pending=1;if(q)return;q=1;requestAnimationFrame(function(){q=0;var run=pending;pending=0;m(run)})}
|
|
280
|
+
function watch(){[].forEach.call(document.images||[],function(img){if(img.dataset.uxSized)return;img.dataset.uxSized="1";img.addEventListener("load",s);img.addEventListener("error",s)})}
|
|
281
|
+
var started=0;function boot(){watch();s();if(started||!window.ResizeObserver)return;started=1;var ro=new ResizeObserver(s);ro.observe(document.documentElement);if(document.body)ro.observe(document.body)}
|
|
282
|
+
addEventListener("DOMContentLoaded",boot);addEventListener("load",boot);addEventListener("resize",s);
|
|
283
|
+
var acked=0;
|
|
284
|
+
addEventListener("message",function(e){var d=e.data;if(!d)return;if(d.type==="ux:ack"&&d.fid==="__UX_FID__"){acked=1;return}if(d.type!=="ux:measure"||(d.fid&&d.fid!=="__UX_FID__"))return;last=0;m(1)});
|
|
285
|
+
var tries=0,tick=setInterval(function(){if(acked||++tries>30){clearInterval(tick);return}last=0;m(1)},200);
|
|
286
|
+
setTimeout(function(){last=0;m(1)},0);
|
|
287
|
+
})()<\/script>`;
|
|
288
|
+
|
|
289
|
+
export function frameSrcdoc(html: string, title: string, fid = "ux") {
|
|
290
|
+
const csp = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:;">`;
|
|
291
|
+
const safe = fid.replace(/[^A-Za-z0-9_-]/g, "") || "ux";
|
|
292
|
+
const boot = FRAME_BOOT.replaceAll("__UX_FID__", safe);
|
|
293
|
+
if (/<head[^>]*>/iu.test(html)) return html.replace(/<head[^>]*>/iu, (head) => `${head}${csp}${boot}`);
|
|
294
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${title}</title>${csp}${boot}</head><body>${html}</body></html>`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function SandboxFrame({ html, title }: { html: string; title: string }) {
|
|
298
|
+
const ref = useRef<HTMLIFrameElement>(null);
|
|
299
|
+
const [height, setHeight] = useState(80);
|
|
300
|
+
const fid = useId().replace(/[^A-Za-z0-9_-]/g, "") || "ux";
|
|
301
|
+
const srcDoc = frameSrcdoc(html, title, fid);
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
const frame = ref.current;
|
|
305
|
+
let settled = false;
|
|
306
|
+
let tries = 0;
|
|
307
|
+
const ask = () => frame?.contentWindow?.postMessage({ type: "ux:measure", fid }, "*");
|
|
308
|
+
const onMessage = (event: MessageEvent) => {
|
|
309
|
+
if (!frame) return;
|
|
310
|
+
const data = event.data;
|
|
311
|
+
if (!acceptSandboxSizeMessage(data, fid, event.source, frame.contentWindow)) return;
|
|
312
|
+
const next = Number(data.height);
|
|
313
|
+
if (!Number.isFinite(next) || next < 48) return;
|
|
314
|
+
if (frame.closest(".ux-fs:fullscreen, .ux-fs-on")) return;
|
|
315
|
+
settled = true;
|
|
316
|
+
frame.contentWindow?.postMessage({ type: "ux:ack", fid }, "*");
|
|
317
|
+
setHeight(Math.max(60, Math.min(1600, next)));
|
|
318
|
+
};
|
|
319
|
+
window.addEventListener("message", onMessage);
|
|
320
|
+
frame?.addEventListener("load", ask);
|
|
321
|
+
ask();
|
|
322
|
+
const timer = window.setInterval(() => {
|
|
323
|
+
if (settled || tries++ > 30) {
|
|
324
|
+
window.clearInterval(timer);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
ask();
|
|
328
|
+
}, 200);
|
|
329
|
+
return () => {
|
|
330
|
+
window.clearInterval(timer);
|
|
331
|
+
window.removeEventListener("message", onMessage);
|
|
332
|
+
frame?.removeEventListener("load", ask);
|
|
333
|
+
};
|
|
334
|
+
}, [fid]);
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<FullscreenFrame label={title}>
|
|
338
|
+
<div className="g-sandbox">
|
|
339
|
+
<iframe
|
|
340
|
+
ref={ref}
|
|
341
|
+
title={title}
|
|
342
|
+
sandbox="allow-scripts"
|
|
343
|
+
referrerPolicy="no-referrer"
|
|
344
|
+
srcDoc={srcDoc}
|
|
345
|
+
style={{ height, overflow: "hidden", background: "transparent" }}
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
</FullscreenFrame>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SandboxSizeData = {
|
|
2
|
+
type?: string;
|
|
3
|
+
fid?: string;
|
|
4
|
+
height?: unknown;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/** A resize is from this frame only when both the window and the frame id match. */
|
|
8
|
+
export function acceptSandboxSizeMessage(
|
|
9
|
+
data: SandboxSizeData | null | undefined,
|
|
10
|
+
fid: string,
|
|
11
|
+
source: unknown,
|
|
12
|
+
contentWindow: unknown,
|
|
13
|
+
): boolean {
|
|
14
|
+
if (!data || (data.type !== "ux:size" && data.type !== "garden:resize")) return false;
|
|
15
|
+
return contentWindow != null && source === contentWindow && data.fid === fid;
|
|
16
|
+
}
|