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-docs/znai/visuals/iframe/custom-multi-line.html b/znai-docs/znai/visuals/iframe/custom-multi-line.html
new file mode 100644
index 000000000..348405b3a
--- /dev/null
+++ b/znai-docs/znai/visuals/iframe/custom-multi-line.html
@@ -0,0 +1,52 @@
+
+
+
+ multi line content
+
+
+
+
+
This is an example with more content to demonstrate zoom functionality.
+
+ | Parameter | Type | Description |
+ | src | string | URL of the content to embed |
+ | fit | boolean | Auto resize iframe to fit content |
+ | title | string | Title bar text |
+ | wide | boolean | Take all available horizontal space |
+ | zoomEnabled | boolean | Show full-screen zoom button |
+ | newTabEnabled | boolean | Show open in new tab button |
+
+
+
+
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 (
-
+