diff --git a/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/attention/AttentionSignCustomFencePlugin.java b/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/attention/AttentionSignCustomFencePlugin.java new file mode 100644 index 000000000..cf82d1fe5 --- /dev/null +++ b/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/attention/AttentionSignCustomFencePlugin.java @@ -0,0 +1,52 @@ +/* + * Copyright 2026 znai maintainers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.testingisdocumenting.znai.extensions.attention; + +import org.testingisdocumenting.znai.extensions.PluginParamType; +import org.testingisdocumenting.znai.extensions.PluginParams; +import org.testingisdocumenting.znai.extensions.PluginParamsDefinition; +import org.testingisdocumenting.znai.extensions.fence.FencePlugin; + +public class AttentionSignCustomFencePlugin extends AttentionSignFencePluginBase { + @Override + protected String type() { + return "custom"; + } + + @Override + public PluginParamsDefinition parameters() { + return super.parameters() + .add("icon", PluginParamType.STRING, "optional icon id to display next to the content, " + + "no icon is used when not specified", "\"info\""); + } + + @Override + protected String attentionType(PluginParams pluginParams) { + String customType = pluginParams.getFreeParam(); + if (customType == null || customType.isBlank()) { + throw new IllegalArgumentException("attention-custom requires a type as the first free form parameter, " + + "e.g. ```attention-custom my-type"); + } + + return customType.trim(); + } + + @Override + public FencePlugin create() { + return new AttentionSignCustomFencePlugin(); + } +} diff --git a/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/attention/AttentionSignFencePluginBase.java b/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/attention/AttentionSignFencePluginBase.java index 83ad89946..7bf4fe0fe 100644 --- a/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/attention/AttentionSignFencePluginBase.java +++ b/znai-core/src/main/java/org/testingisdocumenting/znai/extensions/attention/AttentionSignFencePluginBase.java @@ -43,6 +43,10 @@ public String id() { abstract protected String type(); + protected String attentionType(PluginParams pluginParams) { + return type(); + } + @Override public PluginParamsDefinition parameters() { return new PluginParamsDefinition() @@ -55,7 +59,7 @@ public PluginResult process(ComponentsRegistry componentsRegistry, Path markupPa parserResult = markupParser.parse(markupPath, content); Map props = pluginParams.getOpts().toMap(); - props.put("attentionType", type()); + props.put("attentionType", attentionType(pluginParams)); props.put("content", parserResult.docElement().contentToListOfMaps()); return PluginResult.docElement("AttentionBlock", props); diff --git a/znai-core/src/main/resources/META-INF/services/org.testingisdocumenting.znai.extensions.fence.FencePlugin b/znai-core/src/main/resources/META-INF/services/org.testingisdocumenting.znai.extensions.fence.FencePlugin index bc121a74f..6ab92f13a 100644 --- a/znai-core/src/main/resources/META-INF/services/org.testingisdocumenting.znai.extensions.fence.FencePlugin +++ b/znai-core/src/main/resources/META-INF/services/org.testingisdocumenting.znai.extensions.fence.FencePlugin @@ -22,5 +22,6 @@ org.testingisdocumenting.znai.extensions.attention.AttentionSignQuestionFencePlu org.testingisdocumenting.znai.extensions.attention.AttentionSignWarningFencePlugin org.testingisdocumenting.znai.extensions.attention.AttentionSignAvoidFencePlugin org.testingisdocumenting.znai.extensions.attention.AttentionSignRecommendationFencePlugin +org.testingisdocumenting.znai.extensions.attention.AttentionSignCustomFencePlugin org.testingisdocumenting.znai.extensions.json.JsonFencePlugin org.testingisdocumenting.znai.extensions.latex.LatexFencePlugin diff --git a/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/attention/AttentionSignCustomFencePluginTest.groovy b/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/attention/AttentionSignCustomFencePluginTest.groovy new file mode 100644 index 000000000..34589a8c6 --- /dev/null +++ b/znai-core/src/test/groovy/org/testingisdocumenting/znai/extensions/attention/AttentionSignCustomFencePluginTest.groovy @@ -0,0 +1,73 @@ +/* + * Copyright 2026 znai maintainers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.testingisdocumenting.znai.extensions.attention + +import org.junit.Test +import org.testingisdocumenting.znai.extensions.PluginParamsFactory +import org.testingisdocumenting.znai.extensions.include.PluginsTestUtils + +import static org.testingisdocumenting.webtau.Matchers.code +import static org.testingisdocumenting.webtau.Matchers.throwException +import static org.testingisdocumenting.znai.parser.TestComponentsRegistry.TEST_COMPONENTS_REGISTRY + +class AttentionSignCustomFencePluginTest { + static PluginParamsFactory pluginParamsFactory = TEST_COMPONENTS_REGISTRY.pluginParamsFactory() + + @Test + void "uses free form parameter as the attention type"() { + def props = process("my-type", [:], "hello world") + props.attentionType.should == "my-type" + } + + @Test + void "trims the free form type"() { + def props = process(" my-type ", [:], "hello world") + props.attentionType.should == "my-type" + } + + @Test + void "supports optional label"() { + def props = process("my-type", [label: "Consider"], "hello world") + props.attentionType.should == "my-type" + props.label.should == "Consider" + } + + @Test + void "supports optional icon"() { + def props = process("my-type", [icon: "zap"], "hello world") + props.attentionType.should == "my-type" + props.icon.should == "zap" + } + + @Test + void "has no icon by default"() { + def props = process("my-type", [:], "hello world") + props.containsKey("icon").should == false + } + + @Test + void "fails when type is not provided"() { + code { + process("", [:], "hello world") + } should throwException(~/attention-custom requires a type/) + } + + private static def process(String type, Map params, String content) { + return PluginsTestUtils.processFenceAndGetProps( + pluginParamsFactory.create("attention-custom", type, params), content) + } +} diff --git a/znai-docs/znai/attention-custom.css b/znai-docs/znai/attention-custom.css new file mode 100644 index 000000000..ca4ee5580 --- /dev/null +++ b/znai-docs/znai/attention-custom.css @@ -0,0 +1,17 @@ +.znai-attention-block.my-type { + border-left: 3px solid #6f42c1; + background: #f3effb; +} + +.znai-attention-block.my-type .znai-attention-block-icon { + color: #6f42c1; +} + +.theme-znai-dark .znai-attention-block.my-type { + border-left-color: #b794f6; + background: rgba(159, 122, 234, 0.12); +} + +.theme-znai-dark .znai-attention-block.my-type .znai-attention-block-icon { + color: #b794f6; +} diff --git a/znai-docs/znai/extensions.json b/znai-docs/znai/extensions.json index 7fbadba80..8f3b77594 100644 --- a/znai-docs/znai/extensions.json +++ b/znai-docs/znai/extensions.json @@ -1,7 +1,19 @@ { - "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"], + "cssResources": [ + "custom.css", + "attention-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"] -} \ No newline at end of file + "plugins": [ + "plugins/themed-box-plugin.json", + "plugins/custom-fence-block-plugin.json" + ] +} diff --git a/znai-docs/znai/release-notes/1.91/add-2026-06-02-custom-attention-block.md b/znai-docs/znai/release-notes/1.91/add-2026-06-02-custom-attention-block.md new file mode 100644 index 000000000..3e890c385 --- /dev/null +++ b/znai-docs/znai/release-notes/1.91/add-2026-06-02-custom-attention-block.md @@ -0,0 +1 @@ +* Add: [Attention Signs](visuals/attention-signs) `attention-custom` block diff --git a/znai-docs/znai/visuals/attention-signs.md b/znai-docs/znai/visuals/attention-signs.md index 511ce4a14..303952696 100644 --- a/znai-docs/znai/visuals/attention-signs.md +++ b/znai-docs/znai/visuals/attention-signs.md @@ -133,3 +133,53 @@ attention- ```attention-recommendation `recommendation` ``` + +# Custom Attention Block + +Use `attention-custom` when the built-in types are not enough. The free form parameter defines +the type. It is used as a CSS class name, exactly like `note`, `warning`, and the other built-in types. + + +`````markdown +```attention-custom my-type +hello world +``` +````` + +```attention-custom my-type +hello world +``` + +`attention-custom` only provides the markup placeholders. Each guide is responsible for implementing +the CSS for its own types. + +Use in combination with `style.css`. Scope rules under `.theme-znai-dark` to define dark mode colors. + +:include-file: attention-custom.css {title: "style.css"} + +# Custom Icon + +Unlike the built-in types, a custom type has no icon by default. Use the `icon` parameter to display one. + +To pick an icons to use go to [Feather icons](https://feathericons.com/). +`````markdown +```attention-custom my-type {icon: "zap"} +hello world +``` +````` + +```attention-custom my-type {icon: "zap"} +hello world +``` + +Combine `icon` with the optional `label` + +`````markdown +```attention-custom my-type {icon: "zap", label: "Consider"} +hello world +``` +````` + +```attention-custom my-type {icon: "zap", label: "Consider"} +hello world +``` diff --git a/znai-reactjs/src/doc-elements/paragraph/AttentionBlock.demo.tsx b/znai-reactjs/src/doc-elements/paragraph/AttentionBlock.demo.tsx index 9dcdec0aa..79aac99df 100644 --- a/znai-reactjs/src/doc-elements/paragraph/AttentionBlock.demo.tsx +++ b/znai-reactjs/src/doc-elements/paragraph/AttentionBlock.demo.tsx @@ -48,4 +48,18 @@ export function attentionBlockDemo(registry: Registry) { elementsLibrary={elementsLibrary} /> )); + + registry.add("custom type without icon", () => ( + + )); + + registry.add("custom type with icon and label", () => ( + + )); } diff --git a/znai-reactjs/src/doc-elements/paragraph/AttentionBlock.tsx b/znai-reactjs/src/doc-elements/paragraph/AttentionBlock.tsx index becfaa074..b06cdca98 100644 --- a/znai-reactjs/src/doc-elements/paragraph/AttentionBlock.tsx +++ b/znai-reactjs/src/doc-elements/paragraph/AttentionBlock.tsx @@ -24,6 +24,7 @@ import "./AttentionBlock.css"; interface Props extends DocElementProps { attentionType: string; label?: string; + icon?: string; iconTooltip?: string; content: DocElementContent; } @@ -36,14 +37,16 @@ const iconByType: Record = { recommendation: "check-circle", }; -export function AttentionBlock({ attentionType, label, iconTooltip, content, elementsLibrary }: Props) { - const iconId = iconByType[attentionType] || "square"; +export function AttentionBlock({ attentionType, label, icon, iconTooltip, content, elementsLibrary }: Props) { + const iconId = icon ?? iconByType[attentionType]; return (
- - - {label && {label}:} - + {(iconId || label) && ( + + {iconId && } + {label && {label}:} + + )}