Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions znai-docs/znai/extensions.json
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
11 changes: 8 additions & 3 deletions znai-docs/znai/llm.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -162,7 +162,7 @@ answer-link: znai-from-export/introduction/getting-started#maven-plugin
<plugin>
<groupId>org.testingisdocumenting.znai</groupId>
<artifactId>znai-maven-plugin</artifactId>
<version>1.90</version>
<version>1.90.1</version>
</plugin>
```

Expand Down Expand Up @@ -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

Expand Down
75 changes: 73 additions & 2 deletions znai-docs/znai/plugins/javascript-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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}
47 changes: 47 additions & 0 deletions znai-docs/znai/plugins/javascript/activity-feed.css
Original file line number Diff line number Diff line change
@@ -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);
}
43 changes: 43 additions & 0 deletions znai-docs/znai/plugins/javascript/activity-feed.js
Original file line number Diff line number Diff line change
@@ -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);
};
})();
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions znai-docs/znai/release-notes/2026.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.91

:include-markdowns: 1.91

# 1.90.1

:include-markdowns: 1.90.1
Expand Down
60 changes: 27 additions & 33 deletions znai-reactjs/src/doc-elements/javascript/JavascriptFunction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -26,8 +26,6 @@ interface Props {
args?: Record<string, unknown>;
}

const CONTAINER_ARG_NAMES = new Set(["title", "wide", "className", "anchorId"]);

type ThemeName = "light" | "dark";
type ThemeListener = (themeName: ThemeName) => void;

Expand All @@ -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<string, unknown>,
Expand All @@ -50,25 +44,31 @@ export function JavascriptFunction({ functionName, args }: Props) {
const nodeRef = useRef<HTMLDivElement>(null);
const listenersRef = useRef<ThemeListener[]>([]);

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;
Expand All @@ -79,7 +79,7 @@ export function JavascriptFunction({ functionName, args }: Props) {
get current() {
return isZnaiDarkTheme() ? "dark" : "light";
},
subscribe(listener: ThemeListener) {
subscribe(listener) {
listeners.push(listener);
},
};
Expand All @@ -105,36 +105,30 @@ export function JavascriptFunction({ functionName, args }: Props) {
};
}, [functionName, userArgs]);

const containerClassName = `znai-javascript-function${userClassName ? ` ${userClassName}` : ""}`;

return (
<Container
wide={wide}
title={title}
anchorId={anchorId}
className={containerClassName}
className={userClassName ? `znai-javascript-function ${userClassName}` : "znai-javascript-function"}
additionalTitleClassNames="znai-javascript-function-title"
>
<div ref={nodeRef} />
<div ref={nodeRef} style={nodeStyle} onClick={(e) => e.stopPropagation()} />
</Container>
);
}

function extractUserArgs(args: Record<string, unknown> | undefined): Record<string, unknown> {
if (!args) {
return {};
}
function asString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}

const result: Record<string, unknown> = {};
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;
Expand Down
Loading