diff --git a/znai-docs/znai/extensions.json b/znai-docs/znai/extensions.json index 0e25621ae..7fbadba80 100644 --- a/znai-docs/znai/extensions.json +++ b/znai-docs/znai/extensions.json @@ -1,6 +1,6 @@ { - "cssResources": ["custom.css", "plugins/javascript/theme-box.css"], - "jsResources": ["custom.js", "plugins/javascript/theme-box.js"], + "cssResources": ["custom.css", "plugins/javascript/theme-box.css", "plugins/javascript/activity-feed.css"], + "jsResources": ["custom.js", "plugins/javascript/theme-box.js", "plugins/javascript/activity-feed.js"], "htmlResources": ["custom.html"], "htmlHeadResources": ["tracking.html"], "plugins": ["plugins/themed-box-plugin.json", "plugins/custom-fence-block-plugin.json"] diff --git a/znai-docs/znai/llm.txt b/znai-docs/znai/llm.txt index a90658054..9071bff5e 100644 --- a/znai-docs/znai/llm.txt +++ b/znai-docs/znai/llm.txt @@ -146,8 +146,8 @@ answer-link: znai-from-export/introduction/getting-started#command-line ## CLI download Download and unzip -[znai](https://repo.maven.apache.org/maven2/org/testingisdocumenting/znai/znai-dist/1.90/znai-dist-1.90-znai.zip). Add -it to your `PATH`. +[znai](https://repo.maven.apache.org/maven2/org/testingisdocumenting/znai/znai-dist/1.90.1/znai-dist-1.90.1-znai.zip). +Add it to your `PATH`. ## Brew @@ -162,7 +162,7 @@ answer-link: znai-from-export/introduction/getting-started#maven-plugin org.testingisdocumenting.znai znai-maven-plugin - 1.90 + 1.90.1 ``` @@ -9941,6 +9941,11 @@ export PATH=$(pwd)/dist:$PATH znai --version ``` +# Release Notes :: 2026 :: 1.90.1 +answer-link: znai-from-export/release-notes/2026#1901 + + + # Release Notes :: 2026 :: 1.90 answer-link: znai-from-export/release-notes/2026#190 diff --git a/znai-docs/znai/plugins/javascript-plugin.md b/znai-docs/znai/plugins/javascript-plugin.md index ea4f2514a..8a4a27c18 100644 --- a/znai-docs/znai/plugins/javascript-plugin.md +++ b/znai-docs/znai/plugins/javascript-plugin.md @@ -48,7 +48,7 @@ Your function receives three arguments: * `node` — the parent `div` to append content to. * `args` — the parameters passed from markdown. Framework-level keys (`title`, - `wide`, `className`, `anchorId`) are handled by znai and not forwarded. + `wide`, `className`, `anchorId`, `height`) are handled by znai and not forwarded. * `themeObservable` — live access to the current znai theme. ```javascript {title: "themeObservable shape"} @@ -129,6 +129,76 @@ iframes. metrics: {"requests": 1024, "latency_ms": 42, "error_rate": 0.003} } +# Height + +By default the block grows to fit whatever the function renders into it. Pass +`height` to pin the block to a fixed size — content past it scrolls inside the +viewport znai gives the function. + +`height` accepts either a number (treated as pixels) or any CSS length string +like `"320px"` or `"30vh"`. + +The `activityFeed` function below appends one row per event. Without `height`, +all twelve rows render and the block grows to fit them: + +```markdown +:include-javascript-function: activityFeed { + title: "deploys", + events: [ + {time: "09:14", action: "build started", detail: "commit a31f9b on main"}, + {time: "09:17", action: "tests passed", detail: "248 / 248 green"}, + ... + ] +} +``` + +:include-javascript-function: activityFeed { + title: "deploys", + events: [ + {time: "09:14", action: "build started", detail: "commit a31f9b on main"}, + {time: "09:17", action: "tests passed", detail: "248 / 248 green"}, + {time: "09:18", action: "image pushed", detail: "registry.io/web@sha256:c2..."}, + {time: "09:19", action: "deploy started", detail: "rolling out to canary"}, + {time: "09:21", action: "canary healthy", detail: "p99 42ms, error rate 0.0%"}, + {time: "09:24", action: "promoted", detail: "100% of fleet now on a31f9b"}, + {time: "09:31", action: "alert fired", detail: "latency spike on shard 4"}, + {time: "09:33", action: "shard restarted", detail: "shard 4 back to baseline"}, + {time: "09:40", action: "config reload", detail: "feature flag billing-v2 → on"}, + {time: "09:47", action: "backfill queued", detail: "1.2M rows over 6 batches"}, + {time: "09:58", action: "backfill complete", detail: "elapsed 11m02s"}, + {time: "10:05", action: "deploy started", detail: "rolling out to prod-eu"} + ] +} + +Add `height` and the same twelve events scroll inside a fixed-size box instead: + +```markdown +:include-javascript-function: activityFeed { + title: "deploys", + height: 220, + events: [/* same twelve events */] +} +``` + +:include-javascript-function: activityFeed { + title: "deploys", + height: 220, + events: [ + {time: "09:14", action: "build started", detail: "commit a31f9b on main"}, + {time: "09:17", action: "tests passed", detail: "248 / 248 green"}, + {time: "09:18", action: "image pushed", detail: "registry.io/web@sha256:c2..."}, + {time: "09:19", action: "deploy started", detail: "rolling out to canary"}, + {time: "09:21", action: "canary healthy", detail: "p99 42ms, error rate 0.0%"}, + {time: "09:24", action: "promoted", detail: "100% of fleet now on a31f9b"}, + {time: "09:31", action: "alert fired", detail: "latency spike on shard 4"}, + {time: "09:33", action: "shard restarted", detail: "shard 4 back to baseline"}, + {time: "09:40", action: "config reload", detail: "feature flag billing-v2 → on"}, + {time: "09:47", action: "backfill queued", detail: "1.2M rows over 6 batches"}, + {time: "09:58", action: "backfill complete", detail: "elapsed 11m02s"}, + {time: "10:05", action: "deploy started", detail: "rolling out to prod-eu"} + ] +} + # Styling With A Class Name Pass `className` to attach an arbitrary class to the parent node znai creates. @@ -161,4 +231,5 @@ inside the parameters surfaces the page and the rendered block. The function below powers the examples on this page. It reads the initial theme and subscribes to future changes. -:include-file: javascript/theme-box.js {title: "plugins/javascript/theme-box.js"} +:include-file: javascript/theme-box.js {title: "plugins/javascript/theme-box.js", collapsed: true, noGap: true} +:include-file: javascript/activity-feed.js {title: "plugins/javascript/activity-feed.js", collapsed: true, noGap: true} diff --git a/znai-docs/znai/plugins/javascript/activity-feed.css b/znai-docs/znai/plugins/javascript/activity-feed.css new file mode 100644 index 000000000..ecbbb99a4 --- /dev/null +++ b/znai-docs/znai/plugins/javascript/activity-feed.css @@ -0,0 +1,47 @@ +.activity-feed { + box-sizing: border-box; + display: flex; + flex-direction: column; + width: 100%; + border: 1px solid var(--znai-snippets-outer-border-color, #dedede); + border-radius: 4px; + background-color: var(--znai-snippets-title-background-color, #efeff1); + color: var(--znai-regular-text-color); + font-family: var(--znai-regular-font-family); +} + +.activity-feed-row { + display: grid; + grid-template-columns: 80px 140px 1fr; + column-gap: 12px; + padding: 6px 12px; + border-bottom: 1px solid var(--znai-snippets-outer-border-color, #dedede); + font-family: var(--znai-code-font-family, monospace); + font-size: 0.85rem; +} + +.activity-feed-row:last-child { + border-bottom: none; +} + +.activity-feed-time { + color: var(--znai-regular-text-light-color); +} + +.activity-feed-action { + color: var(--znai-brand-primary-color); + font-weight: 600; +} + +.activity-feed-action::before { + content: "▸ "; +} + +.activity-feed-detail { + color: var(--znai-regular-text-color); + word-break: break-word; +} + +.activity-feed.activity-feed-dark .activity-feed-action { + color: var(--znai-color-yellow); +} diff --git a/znai-docs/znai/plugins/javascript/activity-feed.js b/znai-docs/znai/plugins/javascript/activity-feed.js new file mode 100644 index 000000000..c1ce97eec --- /dev/null +++ b/znai-docs/znai/plugins/javascript/activity-feed.js @@ -0,0 +1,43 @@ +/* + * sample plugin showing a list of rows that grows to fit its content. + * + * the parent node sized by znai (`height` arg) is what constrains us — when + * unset, the feed renders all rows tall enough to show them all; when set, + * the same content scrolls inside the fixed viewport znai gave us. + */ +(function () { + function createElement(tagName, className, textContent) { + var el = document.createElement(tagName); + el.className = className; + if (textContent !== undefined) { + el.textContent = textContent; + } + return el; + } + + function buildRow(event) { + var row = createElement("div", "activity-feed-row"); + row.appendChild(createElement("span", "activity-feed-time", event.time || "")); + row.appendChild(createElement("span", "activity-feed-action", event.action || "")); + row.appendChild(createElement("span", "activity-feed-detail", event.detail || "")); + return row; + } + + window.activityFeed = function (node, args, themeObservable) { + var events = Array.isArray(args.events) ? args.events : []; + + var feed = createElement("div", "activity-feed"); + events.forEach(function (event) { + feed.appendChild(buildRow(event)); + }); + + node.appendChild(feed); + + function applyTheme(themeName) { + feed.classList.toggle("activity-feed-dark", themeName === "dark"); + } + + applyTheme(themeObservable.current); + themeObservable.subscribe(applyTheme); + }; +})(); diff --git a/znai-docs/znai/release-notes/1.91/add-2026-05-28-javascript-plugin-height-and-presentation-clicks.md b/znai-docs/znai/release-notes/1.91/add-2026-05-28-javascript-plugin-height-and-presentation-clicks.md new file mode 100644 index 000000000..0caefaa5b --- /dev/null +++ b/znai-docs/znai/release-notes/1.91/add-2026-05-28-javascript-plugin-height-and-presentation-clicks.md @@ -0,0 +1,2 @@ +* Add: [Javascript Plugin](plugins/javascript-plugin) `height` config to pin a block to a fixed size with overflow scrolling +* Add: Clicks inside [Javascript Plugin](plugins/javascript-plugin) blocks no longer advance [Presentation](flow/presentation) slides diff --git a/znai-docs/znai/release-notes/2026.md b/znai-docs/znai/release-notes/2026.md index 4142c17c8..351aaaa0c 100644 --- a/znai-docs/znai/release-notes/2026.md +++ b/znai-docs/znai/release-notes/2026.md @@ -1,3 +1,7 @@ +# 1.91 + +:include-markdowns: 1.91 + # 1.90.1 :include-markdowns: 1.90.1 diff --git a/znai-reactjs/src/doc-elements/javascript/JavascriptFunction.tsx b/znai-reactjs/src/doc-elements/javascript/JavascriptFunction.tsx index b07269fdb..fd148689f 100644 --- a/znai-reactjs/src/doc-elements/javascript/JavascriptFunction.tsx +++ b/znai-reactjs/src/doc-elements/javascript/JavascriptFunction.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useEffect, useMemo, useRef } from "react"; +import React, { CSSProperties, useEffect, useMemo, useRef } from "react"; import { Container } from "../container/Container"; import { isZnaiDarkTheme, useZnaiThemeChange, ZNAI_DARK_THEME_NAME } from "../../theme/znaiTheme"; @@ -26,8 +26,6 @@ interface Props { args?: Record; } -const CONTAINER_ARG_NAMES = new Set(["title", "wide", "className", "anchorId"]); - type ThemeName = "light" | "dark"; type ThemeListener = (themeName: ThemeName) => void; @@ -36,10 +34,6 @@ export interface ThemeObservable { subscribe(listener: ThemeListener): void; } -function toPublicThemeName(znaiThemeName: string): ThemeName { - return znaiThemeName === ZNAI_DARK_THEME_NAME ? "dark" : "light"; -} - type JavascriptPluginFunction = ( node: HTMLDivElement, args: Record, @@ -50,25 +44,31 @@ export function JavascriptFunction({ functionName, args }: Props) { const nodeRef = useRef(null); const listenersRef = useRef([]); - const title = typeof args?.title === "string" ? args.title : undefined; + const title = asString(args?.title); + const anchorId = asString(args?.anchorId); + const userClassName = asString(args?.className); const wide = args?.wide === true; - const anchorId = typeof args?.anchorId === "string" ? args.anchorId : undefined; - const userClassName = typeof args?.className === "string" ? args.className : ""; + const height = asHeight(args?.height); - const userArgs = useMemo(() => extractUserArgs(args), [args]); + const userArgs = useMemo(() => { + if (!args) return {}; + const { title: _t, wide: _w, className: _c, anchorId: _a, height: _h, ...rest } = args; + return rest; + }, [args]); + + const nodeStyle: CSSProperties | undefined = + height !== undefined ? { height, overflow: "auto" } : undefined; useZnaiThemeChange((znaiThemeName) => { - const publicName = toPublicThemeName(znaiThemeName); + const publicName: ThemeName = znaiThemeName === ZNAI_DARK_THEME_NAME ? "dark" : "light"; listenersRef.current.slice().forEach((l) => l(publicName)); }); useEffect(() => { const node = nodeRef.current; - if (!node) { - return; - } + if (!node) return; - const fn = lookupFunction(functionName); + const fn = lookupWindowFunction(functionName); if (!fn) { renderError(node, `javascript function "${functionName}" was not found on window`); return; @@ -79,7 +79,7 @@ export function JavascriptFunction({ functionName, args }: Props) { get current() { return isZnaiDarkTheme() ? "dark" : "light"; }, - subscribe(listener: ThemeListener) { + subscribe(listener) { listeners.push(listener); }, }; @@ -105,36 +105,30 @@ export function JavascriptFunction({ functionName, args }: Props) { }; }, [functionName, userArgs]); - const containerClassName = `znai-javascript-function${userClassName ? ` ${userClassName}` : ""}`; - return ( -
+
e.stopPropagation()} /> ); } -function extractUserArgs(args: Record | undefined): Record { - if (!args) { - return {}; - } +function asString(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} - const result: Record = {}; - for (const key of Object.keys(args)) { - if (!CONTAINER_ARG_NAMES.has(key)) { - result[key] = args[key]; - } - } - return result; +function asHeight(value: unknown): string | number | undefined { + if (typeof value === "number") return value; + if (typeof value === "string" && value.length > 0) return value; + return undefined; } -function lookupFunction(functionName: string): JavascriptPluginFunction | undefined { +function lookupWindowFunction(functionName: string): JavascriptPluginFunction | undefined { // @ts-ignore const candidate = window[functionName]; return typeof candidate === "function" ? (candidate as JavascriptPluginFunction) : undefined;