+
@@ -22,12 +25,39 @@ export default {
title: 'Public/Blocks/CodeBlock',
component: CodeView,
decorators: [withWrapper],
+ parameters: {
+ controls: { expanded: true },
+ docs: {
+ description: {
+ component: `
+# Code Block
+
+Display syntax-highlighted code snippets in your Plone pages.
+
+## Features
+
+- **Multiple languages**: Python, JavaScript, TypeScript, Docker, and more
+- **Syntax highlighting**: Dark and light themes
+- **Line numbers**: Optional line numbering
+- **Long lines**: Option to wrap long lines
+- **Captions**: Add title and description to code blocks
+
+## How to use
+
+1. Add the Code block to your page
+2. Write or paste your code
+3. Select the programming language
+4. Configure style and display options
+ `,
+ },
+ },
+ },
argTypes: {
language: {
name: 'Language',
- description: 'Language to be used for syntax hightlighting',
+ description: 'Language to be used for syntax highlighting',
control: 'select',
- options: ['python', 'javascript', 'dockerfile'],
+ options: ['python', 'javascript', 'typescript', 'dockerfile', 'json'],
},
style: {
name: 'Style',
@@ -50,6 +80,16 @@ export default {
description: 'Wrap long lines in the code block',
control: 'boolean',
},
+ caption_title: {
+ name: 'Caption Title',
+ description: 'Title for the code block caption',
+ control: 'text',
+ },
+ caption_description: {
+ name: 'Caption Description',
+ description: 'Description for the code block caption',
+ control: 'text',
+ },
},
args: {
language: 'python',
@@ -60,34 +100,429 @@ export default {
},
};
+/**
+ * Default story showing Python code with dark theme
+ */
+export const Default = {
+ name: '🎯 Default (Python Dark)',
+ args: {
+ language: 'python',
+ style: 'dark',
+ code: codePython,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Recommended default block configuration: Python code with dark theme and line numbers. Ideal for code documentation.',
+ },
+ },
+ },
+};
+
+/**
+ * Style examples
+ */
export const DarkStyle = {
+ name: '🌙 Dark Style',
args: {
language: 'python',
style: 'dark',
code: codePython,
},
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Dark theme for code blocks. Popular choice for reducing eye strain and modern aesthetics.',
+ },
+ },
+ },
};
export const LightStyle = {
+ name: '☀️ Light Style',
args: {
language: 'python',
style: 'light',
code: codePython,
},
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Light theme for code blocks. Great for printing and bright environments.',
+ },
+ },
+ },
+};
+
+/**
+ * Language examples
+ */
+export const PythonCode = {
+ name: '🐍 Python',
+ args: {
+ language: 'python',
+ style: 'dark',
+ code: codePython,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Python code with proper syntax highlighting.',
+ },
+ },
+ },
+};
+
+export const JavaScriptCode = {
+ name: '📜 JavaScript',
+ args: {
+ language: 'javascript',
+ style: 'dark',
+ code: codeJavaScript,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'JavaScript/JSX code with React syntax highlighting.',
+ },
+ },
+ },
+};
+
+/**
+ * Feature examples
+ */
+export const WithLineNumbers = {
+ name: '🔢 With Line Numbers',
+ args: {
+ language: 'python',
+ style: 'dark',
+ code: codePython,
+ showLineNumbers: true,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Code block with line numbers (default). Useful for referencing specific lines in documentation.',
+ },
+ },
+ },
+};
+
+export const WithoutLineNumbers = {
+ name: '📄 Without Line Numbers',
+ args: {
+ language: 'python',
+ style: 'dark',
+ code: codePython,
+ showLineNumbers: false,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Code block without line numbers. Cleaner look for simple snippets.',
+ },
+ },
+ },
};
export const LongLines = {
+ name: '📏 Long Lines Wrapped',
+ args: {
+ language: 'python',
+ style: 'dark',
+ code: codeLongLines,
+ wrapLongLines: true,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Long lines are wrapped to fit the container. Prevents horizontal scrolling.',
+ },
+ },
+ },
+};
+
+export const LongLinesNoWrap = {
+ name: '📏 Long Lines No Wrap',
args: {
language: 'python',
style: 'dark',
+ showLineNumbers: false,
code: codeLongLines,
+ wrapLongLines: false,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Long lines are not wrapped, requiring horizontal scroll. Preserves original formatting.',
+ },
+ },
+ },
+};
+
+/**
+ * Caption examples
+ */
+export const WithCaption = {
+ name: '📝 With Caption',
+ args: {
+ language: 'python',
+ style: 'dark',
+ code: codePython,
+ caption_title: 'Plone Admin Password Update',
+ caption_description:
+ 'This script demonstrates how to update the admin password in a Plone instance using the makerequest utility.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Code block with title and description caption. Perfect for adding context to code examples.',
+ },
+ },
+ },
+};
+
+export const WithLongCaption = {
+ name: '📝 With Long Caption',
+ args: {
+ language: 'javascript',
+ style: 'dark',
+ code: codeJavaScript,
+ caption_title: 'React Component with Redux',
+ caption_description:
+ 'This React component demonstrates the use of Redux hooks.\nIt uses useSelector to access the user state from the Redux store.\nThe component is written using modern React functional components and JSX syntax.\nIdeal for modern React applications with state management.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Code block with multi-line caption. Shows how to document complex code with detailed explanations.',
+ },
+ },
},
};
-export const LongLinesNoLineNumbers = {
+
+/**
+ * Practical combinations
+ */
+export const LightStyleWithCaption = {
+ name: '💡 Light Style with Caption',
+ args: {
+ language: 'javascript',
+ style: 'light',
+ code: codeJavaScript,
+ caption_title: 'React Functional Component',
+ caption_description:
+ 'Example of a modern React component using hooks and Redux.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Useful combination for documentation with light background. The caption adds context to the code.',
+ },
+ },
+ },
+};
+
+export const MinimalNoLineNumbers = {
+ name: '💡 Minimal (No Line Numbers)',
args: {
language: 'python',
style: 'dark',
+ code: codePython,
showLineNumbers: false,
+ caption_title: 'Quick Example',
+ caption_description: 'Simple code snippet without line numbers.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Minimalist configuration for simple code snippets. Clean and focused on the code itself.',
+ },
+ },
+ },
+};
+
+/**
+ * Size examples
+ */
+export const SizeSmall = {
+ name: '📐 Size Small',
+ args: {
+ language: 'javascript',
+ style: 'dark',
+ code: codeJavaScript,
+ styles: {
+ align: 'center',
+ size: 's',
+ },
+ caption_title: 'Small Code Block',
+ caption_description: 'Max width of 400px. Perfect for small snippets.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Small size (400px max-width). Ideal for short code snippets or when space is limited.',
+ },
+ },
+ },
+};
+
+export const SizeMedium = {
+ name: '📐 Size Medium',
+ args: {
+ language: 'python',
+ style: 'dark',
+ code: codePython,
+ styles: {
+ align: 'center',
+ size: 'm',
+ },
+ caption_title: 'Medium Code Block',
+ caption_description: 'Max width of 600px. Good for most code examples.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Medium size (600px max-width). The default and most versatile size for code blocks.',
+ },
+ },
+ },
+};
+
+export const SizeLarge = {
+ name: '📐 Size Large',
+ args: {
+ language: 'python',
+ style: 'dark',
+ code: codePython,
+ styles: {
+ align: 'center',
+ size: 'l',
+ },
+ caption_title: 'Large Code Block',
+ caption_description: 'Max width of 900px, or 100% when centered.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Large size (900px max-width, or 100% when centered). Use for complex code that needs more horizontal space.',
+ },
+ },
+ },
+};
+
+export const SizeLargeCentered = {
+ name: '📐 Size Large + Centered (Full Width)',
+ args: {
+ language: 'python',
+ style: 'dark',
code: codeLongLines,
+ showLineNumbers: true,
+ wrapLongLines: false,
+ styles: {
+ align: 'center',
+ size: 'l',
+ },
+ caption_title: 'Full Width Code Block',
+ caption_description:
+ 'When large size is combined with center alignment, the block uses 100% width for maximum space.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Large + Centered = Full Width (100%). Perfect for code with long lines that needs maximum horizontal space.',
+ },
+ },
+ },
+};
+
+/**
+ * Alignment examples with different sizes
+ */
+export const LeftAlignedSmall = {
+ name: '↔️ Left Aligned + Small',
+ args: {
+ language: 'javascript',
+ style: 'dark',
+ code: codeJavaScript,
+ styles: {
+ align: 'left',
+ size: 's',
+ },
+ caption_title: 'Left-Aligned Small Block',
+ caption_description:
+ 'Text flows around on the right. Great for inline examples.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Small block aligned to the left with text wrapping around it. Useful for documentation with code examples.',
+ },
+ },
+ },
+};
+
+export const RightAlignedMedium = {
+ name: '↔️ Right Aligned + Medium',
+ args: {
+ language: 'python',
+ style: 'light',
+ code: codePython,
+ styles: {
+ align: 'right',
+ size: 'm',
+ },
+ caption_title: 'Right-Aligned Medium Block',
+ caption_description: 'Text flows around on the left.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Medium block aligned to the right. Creates interesting layouts with text on the left side.',
+ },
+ },
+ },
+};
+
+export const CenteredLarge = {
+ name: '↔️ Centered + Large (Full Width)',
+ args: {
+ language: 'python',
+ style: 'dark',
+ code: codePython,
+ showLineNumbers: true,
+ styles: {
+ align: 'center',
+ size: 'l',
+ },
+ caption_title: 'Centered Large Block',
+ caption_description:
+ 'Uses full width (100%) when centered with large size.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Large size + Center alignment = 100% width. Maximizes space for code that needs it.',
+ },
+ },
},
};
diff --git a/packages/volto-code-block/src/components/Blocks/Code/Edit.jsx b/packages/volto-code-block/src/components/Blocks/Code/Edit.jsx
index acbba85..99a3e1f 100644
--- a/packages/volto-code-block/src/components/Blocks/Code/Edit.jsx
+++ b/packages/volto-code-block/src/components/Blocks/Code/Edit.jsx
@@ -10,6 +10,9 @@ import { highlight } from 'prismjs/components/prism-core';
const CodeBlockEdit = (props) => {
const { data, selected, block, onChangeBlock } = props;
const [code, setCode] = React.useState(data.code || '');
+ const styles = data.styles || {};
+ const align = styles?.align || 'center';
+ const size = styles?.size || 'l';
const className = `code-block-wrapper edit ${data.style}`;
const codeBlockConfig = config.blocks?.blocksConfig?.codeBlock;
const defaultLanguage = codeBlockConfig?.defaultLanguage;
@@ -27,22 +30,24 @@ const CodeBlockEdit = (props) => {
};
return (
-
-
-
handleChange(code)}
- highlight={(code) => highlight(code, language)}
- padding={10}
- preClassName={`code-block-wrapper ${data.style} language-${data.language}`}
- />
+
+
+
+ handleChange(code)}
+ highlight={(code) => highlight(code, language)}
+ padding={10}
+ preClassName={`code-block-wrapper ${data.style} language-${data.language}`}
+ />
+
{caption_title && (
)}
-
-
-
+
+
+
);
};
diff --git a/packages/volto-code-block/src/components/Blocks/Code/schema.js b/packages/volto-code-block/src/components/Blocks/Code/schema.js
index 54bd760..a72b264 100644
--- a/packages/volto-code-block/src/components/Blocks/Code/schema.js
+++ b/packages/volto-code-block/src/components/Blocks/Code/schema.js
@@ -1,4 +1,5 @@
import { defineMessages } from 'react-intl';
+import { addStyling } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
import STYLES from '../../SyntaxHighlighter/Styles';
import config from '@plone/volto/registry';
@@ -39,6 +40,10 @@ const messages = defineMessages({
id: 'Caption',
defaultMessage: 'Caption',
},
+ size: {
+ id: 'Size',
+ defaultMessage: 'Size',
+ },
});
export const codeSchema = (props) => {
@@ -61,7 +66,7 @@ export const codeSchema = (props) => {
return STYLES.map((item) => [item[0], props.intl.formatMessage(item[1])]);
};
- return {
+ const schema = {
title: props.intl.formatMessage(messages.codeBlock),
fieldsets: [
{
@@ -120,4 +125,20 @@ export const codeSchema = (props) => {
},
required: ['language'],
};
+ // Add styling with alignment
+ addStyling({ schema, intl: props.intl });
+ schema.properties.styles.schema.properties.align = {
+ widget: 'align',
+ title: 'Alignment',
+ actions: ['left', 'center', 'right'],
+ default: 'center',
+ };
+ schema.properties.styles.schema.properties.size = {
+ title: props.intl.formatMessage(messages.size),
+ widget: 'image_size',
+ default: 'l',
+ };
+ schema.properties.styles.schema.fieldsets[0].fields.push('align');
+ schema.properties.styles.schema.fieldsets[0].fields.push('size');
+ return schema;
};
diff --git a/packages/volto-code-block/src/components/Blocks/Gist/DefaultView.jsx b/packages/volto-code-block/src/components/Blocks/Gist/DefaultView.jsx
index 2b83914..2143728 100644
--- a/packages/volto-code-block/src/components/Blocks/Gist/DefaultView.jsx
+++ b/packages/volto-code-block/src/components/Blocks/Gist/DefaultView.jsx
@@ -1,16 +1,31 @@
import React from 'react';
import Gist from 'react-gist';
import Caption from '../../Caption/Caption';
+import cx from 'classnames';
const GistView = (props) => {
- const { file, gistId, caption_title, caption_description } = props;
+ const {
+ file,
+ gistId,
+ caption_title,
+ caption_description,
+ className,
+ styles = {},
+ } = props;
+ const align = styles?.align || 'center';
+ const size = styles?.size || 'l';
+
return (
- <>
- {gistId && }
- {caption_title && (
-
- )}
- >
+
+
+
+ {gistId && }
+
+ {caption_title && (
+
+ )}
+
+
);
};
diff --git a/packages/volto-code-block/src/components/Blocks/Gist/DefaultView.stories.jsx b/packages/volto-code-block/src/components/Blocks/Gist/DefaultView.stories.jsx
index beccee7..800bb74 100644
--- a/packages/volto-code-block/src/components/Blocks/Gist/DefaultView.stories.jsx
+++ b/packages/volto-code-block/src/components/Blocks/Gist/DefaultView.stories.jsx
@@ -5,7 +5,7 @@ import Wrapper from '@plone/volto/storybook';
const withWrapper = (Story, { args }) => {
return (
-
+
@@ -16,20 +16,54 @@ export default {
title: 'Public/Blocks/GistBlock',
component: GistView,
decorators: [withWrapper],
+ parameters: {
+ controls: { expanded: true },
+ docs: {
+ description: {
+ component: `
+# Gist Block
+
+Embed GitHub Gists directly into your Plone pages.
+
+## Features
+
+- **GitHub integration**: Directly embed public Gists
+- **File selection**: Choose specific files from multi-file Gists
+- **Flexible alignment**: Left, center, or right alignment
+- **Size control**: Small, medium, or large display
+- **Captions**: Add title and description to Gists
+- **Automatic styling**: Gists maintain GitHub's native styling
+
+## How to use
+
+1. Add the Gist block to your page
+2. Paste the Gist ID or URL
+3. Optionally select a specific file
+4. Configure alignment, size, and caption
+ `,
+ },
+ },
+ },
argTypes: {
gistId: {
name: 'Gist ID',
- description: 'Path of the Gist to be displayed',
+ description: 'Path of the Gist to be displayed (username/gist-id)',
+ control: 'text',
},
file: {
name: 'File',
- description: 'File, in the Gist, to be displayed',
+ description: 'Specific file in the Gist to be displayed',
+ control: 'text',
},
caption_title: {
name: 'Caption Title',
+ description: 'Title for the Gist caption',
+ control: 'text',
},
caption_description: {
name: 'Caption Description',
+ description: 'Description for the Gist caption',
+ control: 'text',
},
},
args: {
@@ -40,20 +74,240 @@ export default {
},
};
-export const DisplayGist = {
+/**
+ * Default story showing a complete Gist
+ */
+export const Default = {
+ name: '🎯 Default (Complete Gist)',
+ args: {},
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Recommended default configuration: displays all files in the Gist with centered alignment and large size.',
+ },
+ },
+ },
+};
+
+/**
+ * File selection examples
+ */
+export const CompleteGist = {
+ name: '📦 Complete Gist (All Files)',
args: {},
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Displays all files from the Gist. Useful when you want to show the entire content of a multi-file Gist.',
+ },
+ },
+ },
};
-export const DisplayGistFile = {
+export const SingleFile = {
+ name: '📄 Single File',
args: {
file: 'docker-compose.yml',
},
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Displays only a specific file from the Gist. Perfect when you want to highlight a particular file from a multi-file Gist.',
+ },
+ },
+ },
};
+/**
+ * Caption examples
+ */
export const WithCaption = {
+ name: '📝 With Caption',
args: {
file: 'docker-compose.yml',
- caption_title: 'docker-compose.yml',
- caption_description: 'This is the main docker-compose.yml for this project',
+ caption_title: 'Docker Compose Configuration',
+ caption_description:
+ 'This is the main docker-compose.yml for this project.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Gist with title and description caption. Perfect for adding context and explanations.',
+ },
+ },
+ },
+};
+
+export const WithLongCaption = {
+ name: '📝 With Long Caption',
+ args: {
+ caption_title: 'Complete Plone Configuration',
+ caption_description:
+ 'This Gist contains multiple configuration files for a Plone project.\nIncludes Docker setup, environment variables, and deployment scripts.\nEach file serves a specific purpose in the project architecture.\nUse this as a reference for setting up similar projects.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Gist with multi-line caption. Shows how to document complex configurations with detailed explanations.',
+ },
+ },
+ },
+};
+
+/**
+ * Size examples
+ */
+export const SizeSmall = {
+ name: '📐 Size Small',
+ args: {
+ file: 'docker-compose.yml',
+ styles: {
+ align: 'center',
+ size: 's',
+ },
+ caption_title: 'Small Gist',
+ caption_description: 'Max width of 400px. Perfect for small code snippets.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Small size (400px max-width). Ideal for short Gist files or when space is limited.',
+ },
+ },
+ },
+};
+
+export const SizeMedium = {
+ name: '📐 Size Medium',
+ args: {
+ file: 'docker-compose.yml',
+ styles: {
+ align: 'center',
+ size: 'm',
+ },
+ caption_title: 'Medium Gist',
+ caption_description: 'Max width of 600px. Good for most Gist embeds.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Medium size (600px max-width). The default and most versatile size for Gist blocks.',
+ },
+ },
+ },
+};
+
+export const SizeLarge = {
+ name: '📐 Size Large',
+ args: {
+ styles: {
+ align: 'center',
+ size: 'l',
+ },
+ caption_title: 'Large Gist',
+ caption_description: 'Max width of 900px, or 100% when centered.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Large size (900px max-width, or 100% when centered). Use for complex Gists that need more horizontal space.',
+ },
+ },
+ },
+};
+
+export const SizeLargeCentered = {
+ name: '📐 Size Large + Centered (Full Width)',
+ args: {
+ styles: {
+ align: 'center',
+ size: 'l',
+ },
+ caption_title: 'Full Width Gist',
+ caption_description:
+ 'When large size is combined with center alignment, the Gist uses 100% width for maximum space.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Large + Centered = Full Width (100%). Perfect for multi-file Gists or wide code that needs maximum horizontal space.',
+ },
+ },
+ },
+};
+
+/**
+ * Alignment examples with different sizes
+ */
+export const LeftAlignedSmall = {
+ name: '↔️ Left Aligned + Small',
+ args: {
+ file: 'docker-compose.yml',
+ styles: {
+ align: 'left',
+ size: 's',
+ },
+ caption_title: 'Left-Aligned Small Gist',
+ caption_description:
+ 'Text flows around on the right. Great for inline examples.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Small Gist aligned to the left with text wrapping around it. Useful for documentation with code examples.',
+ },
+ },
+ },
+};
+
+export const RightAlignedMedium = {
+ name: '↔️ Right Aligned + Medium',
+ args: {
+ file: 'docker-compose.yml',
+ styles: {
+ align: 'right',
+ size: 'm',
+ },
+ caption_title: 'Right-Aligned Medium Gist',
+ caption_description: 'Text flows around on the left.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Medium Gist aligned to the right. Creates interesting layouts with text on the left side.',
+ },
+ },
+ },
+};
+
+export const CenteredLarge = {
+ name: '↔️ Centered + Large (Full Width)',
+ args: {
+ styles: {
+ align: 'center',
+ size: 'l',
+ },
+ caption_title: 'Centered Large Gist',
+ caption_description:
+ 'Uses full width (100%) when centered with large size.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Large size + Center alignment = 100% width. Maximizes space for Gists that need it.',
+ },
+ },
},
};
diff --git a/packages/volto-code-block/src/components/Blocks/Gist/Edit.jsx b/packages/volto-code-block/src/components/Blocks/Gist/Edit.jsx
index a9e531a..03391ff 100644
--- a/packages/volto-code-block/src/components/Blocks/Gist/Edit.jsx
+++ b/packages/volto-code-block/src/components/Blocks/Gist/Edit.jsx
@@ -5,9 +5,9 @@ import GistBlockData from './Data';
import View from './DefaultView';
const GistBlockEdit = (props) => {
- const { data, selected, block } = props;
- const { gistId, file } = data;
- const { caption_title, caption_description } = data;
+ const { data, selected, block, className } = props;
+ const { gistId, file, styles, caption_title, caption_description } = data;
+
return (
{data && (
@@ -18,6 +18,8 @@ const GistBlockEdit = (props) => {
file={file}
caption_title={caption_title}
caption_description={caption_description}
+ className={className}
+ styles={styles}
/>
>
)}
diff --git a/packages/volto-code-block/src/components/Blocks/Gist/View.jsx b/packages/volto-code-block/src/components/Blocks/Gist/View.jsx
index 98ff247..c6761ac 100644
--- a/packages/volto-code-block/src/components/Blocks/Gist/View.jsx
+++ b/packages/volto-code-block/src/components/Blocks/Gist/View.jsx
@@ -3,10 +3,11 @@ import { withBlockExtensions } from '@plone/volto/helpers/Extensions';
import GistView from './DefaultView';
const GistBlockView = (props) => {
- const { data, block } = props;
- const { gistId, file, caption_title, caption_description } = data;
+ const { data, block, className } = props;
+ const { gistId, file, caption_title, caption_description, styles } = data;
+
return (
-
+
{data && (
{
gistId={gistId}
file={file}
block={block}
+ className={className}
+ styles={styles}
/>
)}
diff --git a/packages/volto-code-block/src/components/Blocks/Gist/schema.js b/packages/volto-code-block/src/components/Blocks/Gist/schema.js
index cadc1d0..b78a584 100644
--- a/packages/volto-code-block/src/components/Blocks/Gist/schema.js
+++ b/packages/volto-code-block/src/components/Blocks/Gist/schema.js
@@ -1,4 +1,5 @@
import { defineMessages } from 'react-intl';
+import { addStyling } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
const messages = defineMessages({
gistBlock: {
@@ -21,10 +22,18 @@ const messages = defineMessages({
id: 'Description',
defaultMessage: 'Description',
},
+ align: {
+ id: 'Alignment',
+ defaultMessage: 'Alignment',
+ },
+ size: {
+ id: 'Size',
+ defaultMessage: 'Size',
+ },
});
export const gistSchema = (props) => {
- return {
+ const schema = {
title: props.intl.formatMessage(messages.gistBlock),
fieldsets: [
{
@@ -56,4 +65,20 @@ export const gistSchema = (props) => {
},
required: ['gistId'],
};
+ // Add styling with alignment and size
+ addStyling({ schema, intl: props.intl });
+ schema.properties.styles.schema.properties.align = {
+ widget: 'align',
+ title: props.intl.formatMessage(messages.align),
+ actions: ['left', 'center', 'right'],
+ default: 'center',
+ };
+ schema.properties.styles.schema.properties.size = {
+ title: props.intl.formatMessage(messages.size),
+ widget: 'image_size',
+ default: 'l',
+ };
+ schema.properties.styles.schema.fieldsets[0].fields.push('align');
+ schema.properties.styles.schema.fieldsets[0].fields.push('size');
+ return schema;
};
diff --git a/packages/volto-code-block/src/components/Blocks/Mermaid/Data.jsx b/packages/volto-code-block/src/components/Blocks/Mermaid/Data.jsx
new file mode 100644
index 0000000..16bfdcd
--- /dev/null
+++ b/packages/volto-code-block/src/components/Blocks/Mermaid/Data.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { BlockDataForm } from '@plone/volto/components/manage/Form';
+import { mermaidSchema } from './schema';
+import { useIntl } from 'react-intl';
+
+const MermaidBlockData = (props) => {
+ const { block, blocksConfig, data, onChangeBlock, navRoot, contentType } =
+ props;
+ const intl = useIntl();
+ const schema = mermaidSchema({ ...props, intl });
+
+ return (
+
{
+ onChangeBlock(block, {
+ ...data,
+ [id]: value,
+ });
+ }}
+ onChangeBlock={onChangeBlock}
+ formData={data}
+ block={block}
+ blocksConfig={blocksConfig}
+ navRoot={navRoot}
+ contentType={contentType}
+ />
+ );
+};
+
+export default MermaidBlockData;
diff --git a/packages/volto-code-block/src/components/Blocks/Mermaid/DefaultView.jsx b/packages/volto-code-block/src/components/Blocks/Mermaid/DefaultView.jsx
index 29c698e..8451244 100644
--- a/packages/volto-code-block/src/components/Blocks/Mermaid/DefaultView.jsx
+++ b/packages/volto-code-block/src/components/Blocks/Mermaid/DefaultView.jsx
@@ -1,7 +1,17 @@
import React, { useEffect, useState } from 'react';
+import Caption from '../../Caption/Caption';
+import cx from 'classnames';
const MermaidView = (props) => {
- const { code, block } = props;
+ const {
+ code,
+ block,
+ styles = {},
+ caption_title,
+ caption_description,
+ className,
+ } = props;
+ const align = styles?.align || 'center';
const elementId = `mermaid-${block}`;
const [mermaid, setMermaid] = useState(null);
const [svg, setSVG] = useState('');
@@ -34,9 +44,21 @@ const MermaidView = (props) => {
}, [mermaid, elementId, code]);
return (
-
+ <>
+ {code && (
+
+
+ {svg &&
}
+ {caption_title && (
+
+ )}
+
+
+ )}
+ >
);
};
diff --git a/packages/volto-code-block/src/components/Blocks/Mermaid/DefaultView.stories.jsx b/packages/volto-code-block/src/components/Blocks/Mermaid/DefaultView.stories.jsx
index fcb8713..c7e5ada 100644
--- a/packages/volto-code-block/src/components/Blocks/Mermaid/DefaultView.stories.jsx
+++ b/packages/volto-code-block/src/components/Blocks/Mermaid/DefaultView.stories.jsx
@@ -15,7 +15,7 @@ const codePieChart =
const withWrapper = (Story, { args }) => {
return (
-
+
@@ -26,6 +26,31 @@ export default {
title: 'Public/Blocks/MermaidBlock',
component: MermaidView,
decorators: [withWrapper],
+ parameters: {
+ controls: { expanded: true },
+ docs: {
+ description: {
+ component: `
+# Mermaid Block
+
+Embed Mermaid diagrams directly into your Plone pages.
+
+## Features
+
+- **Multiple diagram types**: Flowcharts, sequence diagrams, class diagrams, and more
+- **Flexible alignment**: Left, center, or right alignment
+- **Responsive**: Diagrams adapt to their container
+
+## How to use
+
+1. Add the Mermaid block to your page
+2. Write your Mermaid diagram code
+3. Configure alignment
+4. The diagram will be automatically rendered
+ `,
+ },
+ },
+ },
argTypes: {
code: {
name: 'Code',
@@ -36,34 +61,271 @@ export default {
name: 'Block ID',
description: 'Id of the block being used',
},
+ 'styles.align': {
+ name: 'Alignment',
+ description: 'Horizontal alignment of the diagram',
+ control: { type: 'select' },
+ options: ['left', 'center', 'right'],
+ },
+ caption_title: {
+ name: 'Caption Title',
+ description: 'Title for the diagram caption',
+ control: 'text',
+ },
+ caption_description: {
+ name: 'Caption Description',
+ description: 'Description for the diagram caption',
+ control: 'text',
+ },
},
args: {
code: '',
block: '12345',
+ styles: {
+ align: 'center',
+ },
},
};
+/**
+ * Default story showing a sequence diagram centered
+ */
+export const Default = {
+ name: '🎯 Default (Sequence Diagram Centered)',
+ args: {
+ code: codeSequence,
+ block: 'default-123',
+ styles: {
+ align: 'center',
+ },
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Recommended default block configuration: sequence diagram with centered alignment. Ideal for documenting communication flows.',
+ },
+ },
+ },
+};
+
+/**
+ * Examples of different diagram types
+ */
export const SequenceDiagram = {
+ name: '📊 Sequence Diagram',
args: {
code: codeSequence,
block: 'sequence-123',
},
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Sequence diagram showing message exchanges between participants. Perfect for documenting interactions between systems or components.',
+ },
+ },
+ },
};
+
export const FlowChart = {
+ name: '📊 Flow Chart',
args: {
code: codeFlowChart,
- block: 'flowchar-123',
+ block: 'flowchart-123',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Flow chart with nested subgraphs. Useful for representing complex processes with multiple decision paths.',
+ },
+ },
},
};
+
export const ClassDiagram = {
+ name: '📊 Class Diagram',
args: {
code: codeClassDiagram,
block: 'classdiagram-123',
},
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'UML class diagram showing inheritance and relationships. Ideal for documenting software architecture.',
+ },
+ },
+ },
};
+
export const PieChart = {
+ name: '📊 Pie Chart',
args: {
code: codePieChart,
block: 'piechart-123',
},
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Pie chart for representing proportions and percentages. Great for visualizing data distribution.',
+ },
+ },
+ },
+};
+
+/**
+ * Alignment examples
+ */
+export const AlignLeft = {
+ name: '◀️ Left Alignment',
+ args: {
+ code: codeSequence,
+ block: 'align-left-123',
+ styles: {
+ align: 'left',
+ },
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Diagram aligned to the left. Useful in layouts with text on the right or in multi-column pages.',
+ },
+ },
+ },
+};
+
+export const AlignCenter = {
+ name: '▪️ Center Alignment',
+ args: {
+ code: codeFlowChart,
+ block: 'align-center-123',
+ styles: {
+ align: 'center',
+ },
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Centered diagram (default configuration). Offers a balanced visual and is the most common option.',
+ },
+ },
+ },
+};
+
+export const AlignRight = {
+ name: '▶️ Right Alignment',
+ args: {
+ code: codeClassDiagram,
+ block: 'align-right-123',
+ styles: {
+ align: 'right',
+ },
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Diagram aligned to the right. Useful in special layouts or to create visual contrast with other elements.',
+ },
+ },
+ },
+};
+
+/**
+ * Caption examples
+ */
+export const WithCaption = {
+ name: '📝 With Caption',
+ args: {
+ code: codeSequence,
+ block: 'caption-123',
+ styles: {
+ align: 'center',
+ },
+ caption_title: 'Communication Flow',
+ caption_description:
+ 'This diagram shows the message exchange between Alice and John.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Diagram with title and description caption. Perfect for adding context and explanations to your diagrams.',
+ },
+ },
+ },
+};
+
+export const WithLongCaption = {
+ name: '📝 With Long Caption',
+ args: {
+ code: codeFlowChart,
+ block: 'long-caption-123',
+ styles: {
+ align: 'center',
+ },
+ caption_title: 'Nested Subgraphs Example',
+ caption_description:
+ 'This flowchart demonstrates the use of nested subgraphs with different directions.\nThe TOP subgraph contains two nested subgraphs (B1 and B2).\nB1 has a right-to-left (RL) direction, while B2 has a bottom-to-top (BT) direction.\nThis structure allows for complex hierarchical representations.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Diagram with multi-line caption. Shows how to document complex diagrams with detailed explanations.',
+ },
+ },
+ },
+};
+
+/**
+ * Practical combinations
+ */
+export const LeftAlignedWithCaption = {
+ name: '💡 Left-Aligned with Caption',
+ args: {
+ code: codePieChart,
+ block: 'left-caption-123',
+ styles: {
+ align: 'left',
+ },
+ caption_title: 'Pet Adoption Statistics',
+ caption_description:
+ 'Distribution of pets adopted by volunteers in 2024. Dogs are the most adopted, followed by cats.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Useful combination for sidebars or when you want text to wrap around the diagram on the right side.',
+ },
+ },
+ },
+};
+
+export const RightAlignedWithCaption = {
+ name: '💡 Right-Aligned with Caption',
+ args: {
+ code: codeClassDiagram,
+ block: 'right-caption-123',
+ styles: {
+ align: 'right',
+ },
+ caption_title: 'Animal Class Hierarchy',
+ caption_description:
+ 'Object-oriented design showing inheritance relationships between animal classes.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Interesting combination for layouts with text on the left. The diagram complements the content without dominating the page.',
+ },
+ },
+ },
};
diff --git a/packages/volto-code-block/src/components/Blocks/Mermaid/Edit.jsx b/packages/volto-code-block/src/components/Blocks/Mermaid/Edit.jsx
index 61ad2cb..8ad7a0d 100644
--- a/packages/volto-code-block/src/components/Blocks/Mermaid/Edit.jsx
+++ b/packages/volto-code-block/src/components/Blocks/Mermaid/Edit.jsx
@@ -1,11 +1,13 @@
import React from 'react';
import { withBlockExtensions } from '@plone/volto/helpers/Extensions';
+import SidebarPortal from '@plone/volto/components/manage/Sidebar/SidebarPortal';
import config from '@plone/volto/registry';
import Editor from '../../Editor/Editor.tsx';
import { highlight } from 'prismjs/components/prism-core';
+import MermaidBlockData from './Data';
const MermaidBlockEdit = (props) => {
- const { data, block, onChangeBlock } = props;
+ const { data, block, onChangeBlock, selected } = props;
const [code, setCode] = React.useState(data.code || '');
const className = `code-block-wrapper edit light`;
const allLanguages = config.settings.codeBlock.languages;
@@ -17,17 +19,22 @@ const MermaidBlockEdit = (props) => {
};
return (
-
-
-
handleChange(code)}
- highlight={(code) => highlight(code, language)}
- padding={10}
- preClassName={`code-block-wrapper ${data.style} language-mermaid`}
- />
+ <>
+
+
+ handleChange(code)}
+ highlight={(code) => highlight(code, language)}
+ padding={10}
+ preClassName={`code-block-wrapper ${data.style} language-mermaid`}
+ />
+
-
+
+
+
+ >
);
};
diff --git a/packages/volto-code-block/src/components/Blocks/Mermaid/View.jsx b/packages/volto-code-block/src/components/Blocks/Mermaid/View.jsx
index 1c38154..58740fd 100644
--- a/packages/volto-code-block/src/components/Blocks/Mermaid/View.jsx
+++ b/packages/volto-code-block/src/components/Blocks/Mermaid/View.jsx
@@ -4,8 +4,7 @@ import { withBlockExtensions } from '@plone/volto/helpers/Extensions';
const MermaidBlockView = (props) => {
const { block, data } = props;
- const code = data.code || '';
- return <>{code &&
}>;
+ return <>{data &&
}>;
};
export default withBlockExtensions(MermaidBlockView);
diff --git a/packages/volto-code-block/src/components/Blocks/Mermaid/schema.js b/packages/volto-code-block/src/components/Blocks/Mermaid/schema.js
new file mode 100644
index 0000000..1c4e5ac
--- /dev/null
+++ b/packages/volto-code-block/src/components/Blocks/Mermaid/schema.js
@@ -0,0 +1,79 @@
+import { defineMessages } from 'react-intl';
+import { addStyling } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
+
+const messages = defineMessages({
+ mermaidBlock: {
+ id: 'Mermaid Diagram',
+ defaultMessage: 'Mermaid Diagram',
+ },
+ align: {
+ id: 'Alignment',
+ defaultMessage: 'Alignment',
+ },
+ alignLeft: {
+ id: 'Left',
+ defaultMessage: 'Left',
+ },
+ alignCenter: {
+ id: 'Center',
+ defaultMessage: 'Center',
+ },
+ alignRight: {
+ id: 'Right',
+ defaultMessage: 'Right',
+ },
+ caption_title: {
+ id: 'Title',
+ defaultMessage: 'Title',
+ },
+ caption_description: {
+ id: 'Description',
+ defaultMessage: 'Description',
+ },
+ caption: {
+ id: 'Caption',
+ defaultMessage: 'Caption',
+ },
+ size: {
+ id: 'Size',
+ defaultMessage: 'Size',
+ },
+});
+
+export const mermaidSchema = (props) => {
+ const schema = {
+ title: props.intl.formatMessage(messages.mermaidBlock),
+ fieldsets: [
+ {
+ id: 'default',
+ title: 'Default',
+ fields: [],
+ },
+ {
+ id: 'caption',
+ title: props.intl.formatMessage(messages.caption),
+ fields: ['caption_title', 'caption_description'],
+ },
+ ],
+ properties: {
+ caption_title: {
+ title: props.intl.formatMessage(messages.caption_title),
+ },
+ caption_description: {
+ title: props.intl.formatMessage(messages.caption_description),
+ widget: 'textarea',
+ },
+ },
+ required: [],
+ };
+ // Add styling with alignment
+ addStyling({ schema, intl: props.intl });
+ schema.properties.styles.schema.properties.align = {
+ widget: 'align',
+ title: props.intl.formatMessage(messages.align),
+ actions: ['left', 'center', 'right'],
+ default: 'center',
+ };
+ schema.properties.styles.schema.fieldsets[0].fields.push('align');
+ return schema;
+};
diff --git a/packages/volto-code-block/src/index.js b/packages/volto-code-block/src/index.js
index de2281b..0b7d129 100644
--- a/packages/volto-code-block/src/index.js
+++ b/packages/volto-code-block/src/index.js
@@ -5,14 +5,17 @@ import showcaseSVG from '@plone/volto/icons/showcase.svg';
// Blocks - CodeBlock
import CodeBlockView from './components/Blocks/Code/View';
import CodeBlockEdit from './components/Blocks/Code/Edit';
+import { codeSchema } from './components/Blocks/Code/schema';
// Blocks - MermaidBlock
import MermaidBlockEdit from './components/Blocks/Mermaid/Edit';
import MermaidBlockView from './components/Blocks/Mermaid/View';
+import { mermaidSchema } from './components/Blocks/Mermaid/schema';
// Blocks - GistBlock
import GistBlockEdit from './components/Blocks/Gist/Edit';
import GistBlockView from './components/Blocks/Gist/View';
+import { gistSchema } from './components/Blocks/Gist/schema';
import './theme/main.less';
import './theme/theme-dark.less';
@@ -53,6 +56,7 @@ const applyConfig = (config) => {
blockHasOwnFocusManagement: true,
defaultLanguage: 'python',
defaultStyle: 'dark',
+ blockSchema: codeSchema,
};
config.blocks.blocksConfig.mermaidBlock = {
@@ -64,8 +68,9 @@ const applyConfig = (config) => {
edit: MermaidBlockEdit,
restricted: false,
mostUsed: false,
- sidebarTab: 0,
+ sidebarTab: 1,
blockHasOwnFocusManagement: true,
+ blockSchema: mermaidSchema,
};
config.blocks.blocksConfig.gistBlock = {
@@ -79,6 +84,7 @@ const applyConfig = (config) => {
mostUsed: false,
sidebarTab: 1,
blockHasOwnFocusManagement: false,
+ blockSchema: gistSchema,
};
config.settings['codeBlock'] = {
diff --git a/packages/volto-code-block/src/theme/main.less b/packages/volto-code-block/src/theme/main.less
index e099892..3ec5248 100644
--- a/packages/volto-code-block/src/theme/main.less
+++ b/packages/volto-code-block/src/theme/main.less
@@ -2,7 +2,62 @@
Code Block styles
*/
.block.code {
- > .code-block-wrapper {
+ clear: both;
+
+ &.align-left {
+ .code-content-wrapper {
+ margin: 0 1rem 1rem 0 !important;
+ float: left;
+ }
+ }
+
+ &.align-right {
+ .code-content-wrapper {
+ margin: 0 0 1rem 1rem !important;
+ float: right;
+ }
+ }
+
+ &.align-center,
+ &.align-full {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .code-content-wrapper {
+ &.block-l {
+ width: 100%;
+ max-width: 900px;
+ }
+ &.block-m {
+ width: 100%;
+ max-width: 600px;
+ }
+ &.block-s {
+ width: 100%;
+ max-width: 400px;
+ }
+
+ .code-block-wrapper {
+ width: 100%;
+ overflow-x: auto;
+ }
+
+ .codeBlockCaption {
+ clear: both;
+ }
+ }
+
+ // When centered and large, use full width
+ &.align-center,
+ &.align-full {
+ .code-content-wrapper.block-l {
+ max-width: 100%;
+ }
+ }
+
+ > .code-content-wrapper {
&.edit {
&.dark {
> textarea {
@@ -48,26 +103,6 @@
}
}
-.block.gist {
- &.edit {
- position: relative;
-
- iframe {
- width: 100%;
- height: 100%;
- }
-
- .gist.editLayer {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.1);
- }
- }
-}
-
div.codeBlockCaption {
margin: 0 5px;
color: #000;
@@ -93,3 +128,138 @@ div.codeBlockCaption {
}
}
}
+
+/*
+ Mermaid Block styles
+*/
+.block.mermaid {
+ clear: both;
+
+ &.align-left {
+ .mermaidWrapper {
+ margin: 0 1rem 1rem 0 !important;
+ float: left;
+ }
+ }
+
+ &.align-right {
+ .mermaidWrapper {
+ margin: 0 0 1rem 1rem !important;
+ float: right;
+ }
+ }
+
+ &.align-center,
+ &.align-full {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .mermaidWrapper {
+ svg {
+ max-width: 100%;
+ height: auto;
+ }
+ }
+
+ // When centered and large, use full width
+ &.align-center,
+ &.align-full {
+ .mermaidWrapper {
+ max-width: 100%;
+ }
+ }
+
+ .codeBlockCaption {
+ margin-top: 0.5rem;
+ clear: both;
+ }
+}
+
+/*
+ Gist Block styles
+*/
+.block.gist {
+ clear: both;
+
+ &.align-left {
+ .gist-content-wrapper {
+ margin: 0 1rem 1rem 0 !important;
+ float: left;
+ }
+ }
+
+ &.align-right {
+ .gist-content-wrapper {
+ margin: 0 0 1rem 1rem !important;
+ float: right;
+ }
+ }
+
+ &.align-center,
+ &.align-full {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .gist-content-wrapper {
+ &.block-l {
+ width: 100%;
+ max-width: 900px;
+ }
+ &.block-m {
+ width: 100%;
+ max-width: 600px;
+ }
+ &.block-s {
+ width: 100%;
+ max-width: 400px;
+ }
+
+ .gistWrapper {
+ width: 100%;
+ overflow-x: auto;
+ }
+
+ .codeBlockCaption {
+ margin-top: 0.5rem;
+ clear: both;
+ }
+ }
+
+ // When centered and large, use full width
+ &.align-center,
+ &.align-full {
+ .gist-content-wrapper.block-l {
+ max-width: 100%;
+ }
+ }
+
+ &.edit {
+ .gist.editLayer {
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+
+ .gist-content-wrapper {
+ position: relative;
+ }
+ }
+}
+
+/*
+ Accordion compatibility: prevent floats from leaking outside accordion panels
+*/
+.accordion-block .ui.accordion .content.active::after,
+.accordion-block .ui.accordion .content::after {
+ display: table;
+ clear: both;
+ content: '';
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fc7b266..10c2027 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24309,7 +24309,11 @@ snapshots:
pretty-format: 26.6.2
throat: 5.0.0
transitivePeerDependencies:
+ - bufferutil
+ - canvas
- supports-color
+ - ts-node
+ - utf-8-validate
jest-leak-detector@26.6.2:
dependencies: