Skip to content

Commit 634933b

Browse files
javascriptfunction: height control and presentation stopPropagation (#1538)
1 parent 23b7432 commit 634933b

7 files changed

Lines changed: 202 additions & 40 deletions

File tree

znai-docs/znai/extensions.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"cssResources": ["custom.css", "plugins/javascript/theme-box.css"],
3-
"jsResources": ["custom.js", "plugins/javascript/theme-box.js"],
2+
"cssResources": ["custom.css", "plugins/javascript/theme-box.css", "plugins/javascript/activity-feed.css"],
3+
"jsResources": ["custom.js", "plugins/javascript/theme-box.js", "plugins/javascript/activity-feed.js"],
44
"htmlResources": ["custom.html"],
55
"htmlHeadResources": ["tracking.html"],
66
"plugins": ["plugins/themed-box-plugin.json", "plugins/custom-fence-block-plugin.json"]

znai-docs/znai/llm.txt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ answer-link: znai-from-export/introduction/getting-started#command-line
146146
## CLI download
147147

148148
Download and unzip
149-
[znai](https://repo.maven.apache.org/maven2/org/testingisdocumenting/znai/znai-dist/1.90/znai-dist-1.90-znai.zip). Add
150-
it to your `PATH`.
149+
[znai](https://repo.maven.apache.org/maven2/org/testingisdocumenting/znai/znai-dist/1.90.1/znai-dist-1.90.1-znai.zip).
150+
Add it to your `PATH`.
151151

152152
## Brew
153153

@@ -162,7 +162,7 @@ answer-link: znai-from-export/introduction/getting-started#maven-plugin
162162
<plugin>
163163
<groupId>org.testingisdocumenting.znai</groupId>
164164
<artifactId>znai-maven-plugin</artifactId>
165-
<version>1.90</version>
165+
<version>1.90.1</version>
166166
</plugin>
167167
```
168168

@@ -9941,6 +9941,11 @@ export PATH=$(pwd)/dist:$PATH
99419941
znai --version
99429942
```
99439943

9944+
# Release Notes :: 2026 :: 1.90.1
9945+
answer-link: znai-from-export/release-notes/2026#1901
9946+
9947+
9948+
99449949
# Release Notes :: 2026 :: 1.90
99459950
answer-link: znai-from-export/release-notes/2026#190
99469951

znai-docs/znai/plugins/javascript-plugin.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Your function receives three arguments:
4848

4949
* `node` — the parent `div` to append content to.
5050
* `args` — the parameters passed from markdown. Framework-level keys (`title`,
51-
`wide`, `className`, `anchorId`) are handled by znai and not forwarded.
51+
`wide`, `className`, `anchorId`, `height`) are handled by znai and not forwarded.
5252
* `themeObservable` — live access to the current znai theme.
5353

5454
```javascript {title: "themeObservable shape"}
@@ -129,6 +129,76 @@ iframes.
129129
metrics: {"requests": 1024, "latency_ms": 42, "error_rate": 0.003}
130130
}
131131

132+
# Height
133+
134+
By default the block grows to fit whatever the function renders into it. Pass
135+
`height` to pin the block to a fixed size — content past it scrolls inside the
136+
viewport znai gives the function.
137+
138+
`height` accepts either a number (treated as pixels) or any CSS length string
139+
like `"320px"` or `"30vh"`.
140+
141+
The `activityFeed` function below appends one row per event. Without `height`,
142+
all twelve rows render and the block grows to fit them:
143+
144+
```markdown
145+
:include-javascript-function: activityFeed {
146+
title: "deploys",
147+
events: [
148+
{time: "09:14", action: "build started", detail: "commit a31f9b on main"},
149+
{time: "09:17", action: "tests passed", detail: "248 / 248 green"},
150+
...
151+
]
152+
}
153+
```
154+
155+
:include-javascript-function: activityFeed {
156+
title: "deploys",
157+
events: [
158+
{time: "09:14", action: "build started", detail: "commit a31f9b on main"},
159+
{time: "09:17", action: "tests passed", detail: "248 / 248 green"},
160+
{time: "09:18", action: "image pushed", detail: "registry.io/web@sha256:c2..."},
161+
{time: "09:19", action: "deploy started", detail: "rolling out to canary"},
162+
{time: "09:21", action: "canary healthy", detail: "p99 42ms, error rate 0.0%"},
163+
{time: "09:24", action: "promoted", detail: "100% of fleet now on a31f9b"},
164+
{time: "09:31", action: "alert fired", detail: "latency spike on shard 4"},
165+
{time: "09:33", action: "shard restarted", detail: "shard 4 back to baseline"},
166+
{time: "09:40", action: "config reload", detail: "feature flag billing-v2 → on"},
167+
{time: "09:47", action: "backfill queued", detail: "1.2M rows over 6 batches"},
168+
{time: "09:58", action: "backfill complete", detail: "elapsed 11m02s"},
169+
{time: "10:05", action: "deploy started", detail: "rolling out to prod-eu"}
170+
]
171+
}
172+
173+
Add `height` and the same twelve events scroll inside a fixed-size box instead:
174+
175+
```markdown
176+
:include-javascript-function: activityFeed {
177+
title: "deploys",
178+
height: 220,
179+
events: [/* same twelve events */]
180+
}
181+
```
182+
183+
:include-javascript-function: activityFeed {
184+
title: "deploys",
185+
height: 220,
186+
events: [
187+
{time: "09:14", action: "build started", detail: "commit a31f9b on main"},
188+
{time: "09:17", action: "tests passed", detail: "248 / 248 green"},
189+
{time: "09:18", action: "image pushed", detail: "registry.io/web@sha256:c2..."},
190+
{time: "09:19", action: "deploy started", detail: "rolling out to canary"},
191+
{time: "09:21", action: "canary healthy", detail: "p99 42ms, error rate 0.0%"},
192+
{time: "09:24", action: "promoted", detail: "100% of fleet now on a31f9b"},
193+
{time: "09:31", action: "alert fired", detail: "latency spike on shard 4"},
194+
{time: "09:33", action: "shard restarted", detail: "shard 4 back to baseline"},
195+
{time: "09:40", action: "config reload", detail: "feature flag billing-v2 → on"},
196+
{time: "09:47", action: "backfill queued", detail: "1.2M rows over 6 batches"},
197+
{time: "09:58", action: "backfill complete", detail: "elapsed 11m02s"},
198+
{time: "10:05", action: "deploy started", detail: "rolling out to prod-eu"}
199+
]
200+
}
201+
132202
# Styling With A Class Name
133203

134204
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.
161231
The function below powers the examples on this page. It reads the initial
162232
theme and subscribes to future changes.
163233

164-
:include-file: javascript/theme-box.js {title: "plugins/javascript/theme-box.js"}
234+
:include-file: javascript/theme-box.js {title: "plugins/javascript/theme-box.js", collapsed: true, noGap: true}
235+
:include-file: javascript/activity-feed.js {title: "plugins/javascript/activity-feed.js", collapsed: true, noGap: true}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.activity-feed {
2+
box-sizing: border-box;
3+
display: flex;
4+
flex-direction: column;
5+
width: 100%;
6+
border: 1px solid var(--znai-snippets-outer-border-color, #dedede);
7+
border-radius: 4px;
8+
background-color: var(--znai-snippets-title-background-color, #efeff1);
9+
color: var(--znai-regular-text-color);
10+
font-family: var(--znai-regular-font-family);
11+
}
12+
13+
.activity-feed-row {
14+
display: grid;
15+
grid-template-columns: 80px 140px 1fr;
16+
column-gap: 12px;
17+
padding: 6px 12px;
18+
border-bottom: 1px solid var(--znai-snippets-outer-border-color, #dedede);
19+
font-family: var(--znai-code-font-family, monospace);
20+
font-size: 0.85rem;
21+
}
22+
23+
.activity-feed-row:last-child {
24+
border-bottom: none;
25+
}
26+
27+
.activity-feed-time {
28+
color: var(--znai-regular-text-light-color);
29+
}
30+
31+
.activity-feed-action {
32+
color: var(--znai-brand-primary-color);
33+
font-weight: 600;
34+
}
35+
36+
.activity-feed-action::before {
37+
content: "▸ ";
38+
}
39+
40+
.activity-feed-detail {
41+
color: var(--znai-regular-text-color);
42+
word-break: break-word;
43+
}
44+
45+
.activity-feed.activity-feed-dark .activity-feed-action {
46+
color: var(--znai-color-yellow);
47+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* sample plugin showing a list of rows that grows to fit its content.
3+
*
4+
* the parent node sized by znai (`height` arg) is what constrains us — when
5+
* unset, the feed renders all rows tall enough to show them all; when set,
6+
* the same content scrolls inside the fixed viewport znai gave us.
7+
*/
8+
(function () {
9+
function createElement(tagName, className, textContent) {
10+
var el = document.createElement(tagName);
11+
el.className = className;
12+
if (textContent !== undefined) {
13+
el.textContent = textContent;
14+
}
15+
return el;
16+
}
17+
18+
function buildRow(event) {
19+
var row = createElement("div", "activity-feed-row");
20+
row.appendChild(createElement("span", "activity-feed-time", event.time || ""));
21+
row.appendChild(createElement("span", "activity-feed-action", event.action || ""));
22+
row.appendChild(createElement("span", "activity-feed-detail", event.detail || ""));
23+
return row;
24+
}
25+
26+
window.activityFeed = function (node, args, themeObservable) {
27+
var events = Array.isArray(args.events) ? args.events : [];
28+
29+
var feed = createElement("div", "activity-feed");
30+
events.forEach(function (event) {
31+
feed.appendChild(buildRow(event));
32+
});
33+
34+
node.appendChild(feed);
35+
36+
function applyTheme(themeName) {
37+
feed.classList.toggle("activity-feed-dark", themeName === "dark");
38+
}
39+
40+
applyTheme(themeObservable.current);
41+
themeObservable.subscribe(applyTheme);
42+
};
43+
})();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
* Add: [Javascript Plugin](plugins/javascript-plugin) `height` config to pin a block to a fixed size with overflow scrolling
2+
* Add: Clicks inside [Javascript Plugin](plugins/javascript-plugin) blocks no longer advance [Presentation](flow/presentation) slides

znai-reactjs/src/doc-elements/javascript/JavascriptFunction.tsx

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import React, { useEffect, useMemo, useRef } from "react";
17+
import React, { CSSProperties, useEffect, useMemo, useRef } from "react";
1818

1919
import { Container } from "../container/Container";
2020
import { isZnaiDarkTheme, useZnaiThemeChange, ZNAI_DARK_THEME_NAME } from "../../theme/znaiTheme";
@@ -26,8 +26,6 @@ interface Props {
2626
args?: Record<string, unknown>;
2727
}
2828

29-
const CONTAINER_ARG_NAMES = new Set(["title", "wide", "className", "anchorId"]);
30-
3129
type ThemeName = "light" | "dark";
3230
type ThemeListener = (themeName: ThemeName) => void;
3331

@@ -36,10 +34,6 @@ export interface ThemeObservable {
3634
subscribe(listener: ThemeListener): void;
3735
}
3836

39-
function toPublicThemeName(znaiThemeName: string): ThemeName {
40-
return znaiThemeName === ZNAI_DARK_THEME_NAME ? "dark" : "light";
41-
}
42-
4337
type JavascriptPluginFunction = (
4438
node: HTMLDivElement,
4539
args: Record<string, unknown>,
@@ -50,25 +44,31 @@ export function JavascriptFunction({ functionName, args }: Props) {
5044
const nodeRef = useRef<HTMLDivElement>(null);
5145
const listenersRef = useRef<ThemeListener[]>([]);
5246

53-
const title = typeof args?.title === "string" ? args.title : undefined;
47+
const title = asString(args?.title);
48+
const anchorId = asString(args?.anchorId);
49+
const userClassName = asString(args?.className);
5450
const wide = args?.wide === true;
55-
const anchorId = typeof args?.anchorId === "string" ? args.anchorId : undefined;
56-
const userClassName = typeof args?.className === "string" ? args.className : "";
51+
const height = asHeight(args?.height);
5752

58-
const userArgs = useMemo(() => extractUserArgs(args), [args]);
53+
const userArgs = useMemo(() => {
54+
if (!args) return {};
55+
const { title: _t, wide: _w, className: _c, anchorId: _a, height: _h, ...rest } = args;
56+
return rest;
57+
}, [args]);
58+
59+
const nodeStyle: CSSProperties | undefined =
60+
height !== undefined ? { height, overflow: "auto" } : undefined;
5961

6062
useZnaiThemeChange((znaiThemeName) => {
61-
const publicName = toPublicThemeName(znaiThemeName);
63+
const publicName: ThemeName = znaiThemeName === ZNAI_DARK_THEME_NAME ? "dark" : "light";
6264
listenersRef.current.slice().forEach((l) => l(publicName));
6365
});
6466

6567
useEffect(() => {
6668
const node = nodeRef.current;
67-
if (!node) {
68-
return;
69-
}
69+
if (!node) return;
7070

71-
const fn = lookupFunction(functionName);
71+
const fn = lookupWindowFunction(functionName);
7272
if (!fn) {
7373
renderError(node, `javascript function "${functionName}" was not found on window`);
7474
return;
@@ -79,7 +79,7 @@ export function JavascriptFunction({ functionName, args }: Props) {
7979
get current() {
8080
return isZnaiDarkTheme() ? "dark" : "light";
8181
},
82-
subscribe(listener: ThemeListener) {
82+
subscribe(listener) {
8383
listeners.push(listener);
8484
},
8585
};
@@ -105,36 +105,30 @@ export function JavascriptFunction({ functionName, args }: Props) {
105105
};
106106
}, [functionName, userArgs]);
107107

108-
const containerClassName = `znai-javascript-function${userClassName ? ` ${userClassName}` : ""}`;
109-
110108
return (
111109
<Container
112110
wide={wide}
113111
title={title}
114112
anchorId={anchorId}
115-
className={containerClassName}
113+
className={userClassName ? `znai-javascript-function ${userClassName}` : "znai-javascript-function"}
116114
additionalTitleClassNames="znai-javascript-function-title"
117115
>
118-
<div ref={nodeRef} />
116+
<div ref={nodeRef} style={nodeStyle} onClick={(e) => e.stopPropagation()} />
119117
</Container>
120118
);
121119
}
122120

123-
function extractUserArgs(args: Record<string, unknown> | undefined): Record<string, unknown> {
124-
if (!args) {
125-
return {};
126-
}
121+
function asString(value: unknown): string | undefined {
122+
return typeof value === "string" ? value : undefined;
123+
}
127124

128-
const result: Record<string, unknown> = {};
129-
for (const key of Object.keys(args)) {
130-
if (!CONTAINER_ARG_NAMES.has(key)) {
131-
result[key] = args[key];
132-
}
133-
}
134-
return result;
125+
function asHeight(value: unknown): string | number | undefined {
126+
if (typeof value === "number") return value;
127+
if (typeof value === "string" && value.length > 0) return value;
128+
return undefined;
135129
}
136130

137-
function lookupFunction(functionName: string): JavascriptPluginFunction | undefined {
131+
function lookupWindowFunction(functionName: string): JavascriptPluginFunction | undefined {
138132
// @ts-ignore
139133
const candidate = window[functionName];
140134
return typeof candidate === "function" ? (candidate as JavascriptPluginFunction) : undefined;

0 commit comments

Comments
 (0)