From 8e28aec92762af093a53efa35a8a635056393b48 Mon Sep 17 00:00:00 2001 From: MykolaGolubyev Date: Sun, 5 Apr 2026 20:25:01 -0400 Subject: [PATCH 1/2] iframe: zoom and new tab --- .../extensions/html/IframeIncludePlugin.java | 2 + .../add-2026-04-03-iframe-zoom-new-tab.md | 1 + znai-docs/znai/visuals/iframe.md | 28 +++++++ .../src/doc-elements/container/Container.css | 1 + .../src/doc-elements/container/Container.tsx | 3 + .../doc-elements/container/ContainerTitle.css | 23 +++++ .../doc-elements/container/ContainerTitle.tsx | 3 + .../src/doc-elements/iframe/Iframe.css | 43 +++++++++- .../src/doc-elements/iframe/Iframe.tsx | 83 +++++++++++++++---- znai-reactjs/src/doc-elements/zoom/Zoom.ts | 6 +- .../src/doc-elements/zoom/ZoomOverlay.tsx | 2 +- 11 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 znai-docs/znai/release-notes/1.88/add-2026-04-03-iframe-zoom-new-tab.md diff --git a/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/html/IframeIncludePlugin.java b/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/html/IframeIncludePlugin.java index 63f436988..d2f6df2a2 100644 --- a/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/html/IframeIncludePlugin.java +++ b/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/html/IframeIncludePlugin.java @@ -59,6 +59,8 @@ public PluginParamsDefinition parameters() { params.add("aspectRatio", PluginParamType.STRING, "aspect ratio for video embedding", "\"16:9\""); params.add("light", PluginParamType.OBJECT, "CSS properties override for light theme", "{ \"--color\": \"#333\" }"); params.add("dark", PluginParamType.OBJECT, "CSS properties override for dark theme", "{ \"--color\": \"#eee\" }"); + params.add("zoomEnabled", PluginParamType.BOOLEAN, "enable full-screen zoom button in the title bar", "true"); + params.add("newTabEnabled", PluginParamType.BOOLEAN, "enable open in new tab button in the title bar", "true"); return params; } diff --git a/znai-docs/znai/release-notes/1.88/add-2026-04-03-iframe-zoom-new-tab.md b/znai-docs/znai/release-notes/1.88/add-2026-04-03-iframe-zoom-new-tab.md new file mode 100644 index 000000000..1a97f09d3 --- /dev/null +++ b/znai-docs/znai/release-notes/1.88/add-2026-04-03-iframe-zoom-new-tab.md @@ -0,0 +1 @@ +* Add: iframe zoom and open in new tab \ No newline at end of file diff --git a/znai-docs/znai/visuals/iframe.md b/znai-docs/znai/visuals/iframe.md index fb19cab92..be18d83a2 100644 --- a/znai-docs/znai/visuals/iframe.md +++ b/znai-docs/znai/visuals/iframe.md @@ -107,6 +107,34 @@ Use `wide` to take all the available horizontal space: wide: true } +# Zoom And New Tab + +Use `zoomEnabled` to add a full-screen zoom button to the title bar. +Clicking the button opens the iframe in a full-screen overlay with a close button. Press `Escape` or click the close button to exit. + +Use `newTabEnabled` to add an open-in-new-tab button to the title bar. + +Note: Requires `title` to be set. + +```markdown {highlight: ["zoomEnabled", "newTabEnabled"]} +:include-iframe: iframe/custom-multi-line.html { + title: "parameters reference", + fit: true, + maxHeight: 120, + zoomEnabled: true, + newTabEnabled: true +} +``` + +:include-iframe: iframe/custom-multi-line.html { + title: "parameters reference", + fit: true, + maxHeight: 120, + zoomEnabled: true, + newTabEnabled: true +} + + # Embedding Video Use `include-iframe` to embed media from other places. By default, aspect ratio is set to `16:9`. diff --git a/znai-reactjs/src/doc-elements/container/Container.css b/znai-reactjs/src/doc-elements/container/Container.css index f4fb2eff3..c26145c69 100644 --- a/znai-reactjs/src/doc-elements/container/Container.css +++ b/znai-reactjs/src/doc-elements/container/Container.css @@ -28,6 +28,7 @@ .znai-container.wide .znai-container-title-wrapper { border: none; + padding-right: 0; } .znai-container.wide .znai-container-title { diff --git a/znai-reactjs/src/doc-elements/container/Container.tsx b/znai-reactjs/src/doc-elements/container/Container.tsx index 20d44d1d4..d00f31d98 100644 --- a/znai-reactjs/src/doc-elements/container/Container.tsx +++ b/znai-reactjs/src/doc-elements/container/Container.tsx @@ -38,6 +38,7 @@ interface Props extends ContainerCommonProps { onCollapseToggle?(): void; additionalTitleClassNames?: string; additionalTitleContainerClassNames?: string; + titleActions?: React.ReactNode; titleContainerStyle?: CSSProperties; style?: CSSProperties; onClick?(): void; @@ -68,6 +69,7 @@ export function Container({ onCollapseToggle, additionalTitleClassNames, additionalTitleContainerClassNames, + titleActions, titleContainerStyle, style, onClick, @@ -96,6 +98,7 @@ export function Container({ onCollapseToggle={onCollapseToggle} additionalTitleClassNames={additionalTitleClassNames} additionalContainerClassNames={additionalTitleContainerClassNames} + titleActions={titleActions} containerStyle={titleContainerStyle} /> ) : null; diff --git a/znai-reactjs/src/doc-elements/container/ContainerTitle.css b/znai-reactjs/src/doc-elements/container/ContainerTitle.css index bc5a898ef..ca1dd7a79 100644 --- a/znai-reactjs/src/doc-elements/container/ContainerTitle.css +++ b/znai-reactjs/src/doc-elements/container/ContainerTitle.css @@ -25,6 +25,7 @@ .znai-container-title { display: flex; align-items: center; + flex: 1; color: var(--znai-snippets-title-color); padding: 8px 0 8px 16px; } @@ -62,4 +63,26 @@ .znai-container-title-anchor .znai-icon svg { width: 14px; height: 14px; +} + +.znai-container-title-actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 4px; +} + +.znai-container-title-actions .znai-icon { + cursor: pointer; + opacity: 0.6; +} + +.znai-container-title-actions .znai-icon:hover { + opacity: 1; +} + +.znai-container-title-actions .znai-icon, +.znai-container-title-actions .znai-icon svg { + width: 14px; + height: 14px; } \ No newline at end of file diff --git a/znai-reactjs/src/doc-elements/container/ContainerTitle.tsx b/znai-reactjs/src/doc-elements/container/ContainerTitle.tsx index 5af17118a..23f591013 100644 --- a/znai-reactjs/src/doc-elements/container/ContainerTitle.tsx +++ b/znai-reactjs/src/doc-elements/container/ContainerTitle.tsx @@ -30,6 +30,7 @@ interface Props extends ContainerTitleCommonProps { additionalContainerClassNames?: string; additionalTitleClassNames?: string; containerStyle?: React.CSSProperties; + titleActions?: React.ReactNode; onCollapseToggle?(): void; } @@ -45,6 +46,7 @@ export function ContainerTitle({ containerStyle, collapsed, anchorId, + titleActions, onCollapseToggle, }: Props) { const collapsible = collapsed !== undefined; @@ -77,6 +79,7 @@ export function ContainerTitle({ )} + {titleActions &&
{titleActions}
} ); diff --git a/znai-reactjs/src/doc-elements/iframe/Iframe.css b/znai-reactjs/src/doc-elements/iframe/Iframe.css index e60e75809..0e67caa63 100644 --- a/znai-reactjs/src/doc-elements/iframe/Iframe.css +++ b/znai-reactjs/src/doc-elements/iframe/Iframe.css @@ -24,6 +24,47 @@ } .znai-iframe-title { - padding: 4px 16px; + padding-top: 4px; + padding-bottom: 4px; font-size: var(--znai-smaller-text-size); +} + +.znai-iframe-zoomed { + display: flex; + flex-direction: column; + width: calc(100vw - 32px); + height: calc(100vh - 32px); + background: var(--znai-background-color); +} + +.znai-iframe-zoomed-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 4px 8px 16px; + background: var(--znai-snippets-title-background-color); + color: var(--znai-snippets-title-color); + font-size: var(--znai-smaller-text-size); +} + +.znai-iframe-zoomed-close { + cursor: pointer; + opacity: 0.6; +} + +.znai-iframe-zoomed-close:hover { + opacity: 1; +} + +.znai-iframe-zoomed-close.znai-icon, +.znai-iframe-zoomed-close.znai-icon svg { + width: 18px; + height: 18px; +} + +.znai-iframe-zoomed-content { + flex: 1; + border: 0; + width: 100%; + height: 100%; } \ No newline at end of file diff --git a/znai-reactjs/src/doc-elements/iframe/Iframe.tsx b/znai-reactjs/src/doc-elements/iframe/Iframe.tsx index 9ea6460a9..a0d661f9e 100644 --- a/znai-reactjs/src/doc-elements/iframe/Iframe.tsx +++ b/znai-reactjs/src/doc-elements/iframe/Iframe.tsx @@ -16,6 +16,8 @@ import React, { useEffect, useRef, useState } from "react"; import { Container } from "../container/Container"; +import { Icon } from "../icons/Icon"; +import { zoom } from "../zoom/Zoom"; import "./Iframe.css"; @@ -29,6 +31,8 @@ interface Props { wide?: boolean; height?: number; maxHeight?: number; + zoomEnabled?: boolean; + newTabEnabled?: boolean; // changes on every page regen to force iframe reload previewMarker?: string; } @@ -44,7 +48,7 @@ export function Iframe(props: Props) { const initialIframeHeight = 14; let activeElement: any = null; -export function IframeFit({ src, title, wide, height, maxHeight, light, dark, previewMarker }: Props) { +export function IframeFit({ src, title, wide, height, maxHeight, light, dark, zoomEnabled, newTabEnabled, previewMarker }: Props) { const containerRef = useRef(null); const iframeRef = useRef(null); const mutationObserverRef = useRef(null); @@ -60,20 +64,9 @@ export function IframeFit({ src, title, wide, height, maxHeight, light, dark, pr iframeRef!.current!.src += ""; }, [previewMarker]); - // handle site theme switching - useEffect(() => { - // TODO theme integration via context - // @ts-ignore - window.znaiTheme.addChangeHandler(onThemeChange); - - // @ts-ignore - return () => window.znaiTheme.removeChangeHandler(onThemeChange); - - function onThemeChange() { - injectCssProperties(iframeRef, dark, light); - updateScrollBarToMatch(containerRef, iframeRef); - } - }, [dark, light]); + const { syncTheme } = useIframeThemeSync(iframeRef, dark, light, () => { + updateScrollBarToMatch(containerRef, iframeRef); + }); useEffect(() => { return () => { @@ -89,8 +82,15 @@ export function IframeFit({ src, title, wide, height, maxHeight, light, dark, pr activeElement = document.activeElement; } + const titleActions = (zoomEnabled || newTabEnabled) ? ( + <> + {zoomEnabled && } + {newTabEnabled && } + + ) : undefined; + return ( - +