diff --git a/.stylelintignore b/.stylelintignore
index 4b2be235a0..f52d70d0db 100644
--- a/.stylelintignore
+++ b/.stylelintignore
@@ -2,3 +2,4 @@ dist
docs
node_modules
packages/registry/lib
+build
diff --git a/PLIPs.md b/PLIPs.md
new file mode 100644
index 0000000000..669bdccfb1
--- /dev/null
+++ b/PLIPs.md
@@ -0,0 +1,8 @@
+# Plone Improvement Proposals (PLIPs)
+
+For details of the PLIP process, read the following.
+
+- [Plone Improvement Proposals (PLIPs)](https://6.docs.plone.org/contributing/core/plips.html)
+- [PLIP review](https://6.docs.plone.org/contributing/core/plip-review.html)
+
+You can also [browse the current list of open PLIPs for Volto](https://github.com/plone/volto/labels/03%20type%3A%20feature%20(plip)).
diff --git a/packages/cmsui/.eslintrc.cjs b/packages/cmsui/.eslintrc.cjs
new file mode 100644
index 0000000000..b3a71fc44c
--- /dev/null
+++ b/packages/cmsui/.eslintrc.cjs
@@ -0,0 +1,11 @@
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ overrides: [
+ {
+ files: ['**/*.ts', '**/*.tsx'],
+ extends: [
+ 'plugin:react/jsx-runtime', // We only want this for non-library code (eg. volto add-ons)
+ ],
+ },
+ ],
+};
diff --git a/packages/cmsui/.release-it.json b/packages/cmsui/.release-it.json
new file mode 100644
index 0000000000..507f953edc
--- /dev/null
+++ b/packages/cmsui/.release-it.json
@@ -0,0 +1,29 @@
+{
+ "plugins": {
+ "../scripts/prepublish.js": {}
+ },
+ "hooks": {
+ "after:bump": [
+ "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft",
+ "pipx run towncrier build --yes --version ${version}"
+ ],
+ "after:release": "rm .changelog.draft"
+ },
+ "npm": {
+ "publish": false
+ },
+ "git": {
+ "commitArgs": ["--no-verify"],
+ "changelog": "pipx run towncrier build --draft --yes --version 0.0.0",
+ "requireUpstream": false,
+ "requireCleanWorkingDir": false,
+ "commitMessage": "Release @plone/cmsui ${version}",
+ "tagName": "plone-cmsui-${version}",
+ "tagAnnotation": "Release @plone/cmsui ${version}"
+ },
+ "github": {
+ "release": true,
+ "releaseName": "@plone/cmsui ${version}",
+ "releaseNotes": "cat .changelog.draft"
+ }
+}
diff --git a/packages/cmsui/.storybook/Logo.svg b/packages/cmsui/.storybook/Logo.svg
new file mode 100644
index 0000000000..5a7ba56902
--- /dev/null
+++ b/packages/cmsui/.storybook/Logo.svg
@@ -0,0 +1,21 @@
+
diff --git a/packages/cmsui/.storybook/main.ts b/packages/cmsui/.storybook/main.ts
new file mode 100644
index 0000000000..cf9f4ddec3
--- /dev/null
+++ b/packages/cmsui/.storybook/main.ts
@@ -0,0 +1,41 @@
+import type { StorybookConfig } from '@storybook/react-vite';
+import { mergeConfig } from 'vite';
+
+const config: StorybookConfig = {
+ // For some reason the property does not allow negation
+ // https://github.com/storybookjs/storybook/issues/11181#issuecomment-1535288804
+ stories: [
+ '../components/**/*.mdx',
+ '../components/**/*.stories.@(js|jsx|ts|tsx)',
+ ],
+ addons: [
+ '@storybook/addon-links',
+ '@storybook/addon-essentials',
+ '@storybook/addon-interactions',
+ ],
+ framework: {
+ name: '@storybook/react-vite',
+ options: {},
+ },
+ docs: {
+ autodocs: 'tag',
+ },
+ typescript: {
+ reactDocgen: 'react-docgen-typescript',
+ reactDocgenTypescriptOptions: {
+ compilerOptions: {
+ allowSyntheticDefaultImports: false,
+ esModuleInterop: false,
+ },
+ propFilter: () => true,
+ },
+ },
+ async viteFinal(config) {
+ return mergeConfig(config, {
+ build: {
+ minify: false,
+ },
+ });
+ },
+};
+export default config;
diff --git a/packages/cmsui/.storybook/manager.js b/packages/cmsui/.storybook/manager.js
new file mode 100644
index 0000000000..2e62084432
--- /dev/null
+++ b/packages/cmsui/.storybook/manager.js
@@ -0,0 +1,6 @@
+import { addons } from '@storybook/manager-api';
+import theme from './theme';
+
+addons.setConfig({
+ theme,
+});
diff --git a/packages/cmsui/.storybook/preview-head.html b/packages/cmsui/.storybook/preview-head.html
new file mode 100644
index 0000000000..05da1e9dfb
--- /dev/null
+++ b/packages/cmsui/.storybook/preview-head.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/packages/cmsui/.storybook/preview.ts b/packages/cmsui/.storybook/preview.ts
new file mode 100644
index 0000000000..362843c9bc
--- /dev/null
+++ b/packages/cmsui/.storybook/preview.ts
@@ -0,0 +1,24 @@
+import './storybook-base.css';
+import '@plone/components/dist/basic.css';
+import '../main.css';
+import config from '@plone/registry';
+import installSlots from '../config';
+import installBlocks from '@plone/blocks';
+
+config.set('slots', {});
+config.set('utilities', {});
+installSlots(config);
+installBlocks(config);
+
+export const parameters = {
+ backgrounds: {
+ default: 'light',
+ },
+ actions: { argTypesRegex: '^on[A-Z].*' },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+};
diff --git a/packages/cmsui/.storybook/storybook-base.css b/packages/cmsui/.storybook/storybook-base.css
new file mode 100644
index 0000000000..f55ba4bf27
--- /dev/null
+++ b/packages/cmsui/.storybook/storybook-base.css
@@ -0,0 +1,19 @@
+/* Base styles */
+:root {
+ --basic-font-family: system-ui;
+ --basic-font-size: 16px;
+ background: var(--background-color);
+ font-family: var(--basic-font-family);
+ font-size: var(--basic-font-size);
+ line-height: 1.5;
+}
+
+.sbdocs.sbdocs-content {
+ p {
+ font-size: 16px;
+ }
+}
+
+#storybook-root {
+ width: 100vw;
+}
diff --git a/packages/cmsui/.storybook/theme.ts b/packages/cmsui/.storybook/theme.ts
new file mode 100644
index 0000000000..3262e1f700
--- /dev/null
+++ b/packages/cmsui/.storybook/theme.ts
@@ -0,0 +1,10 @@
+import { create } from '@storybook/theming/create';
+import logo from './Logo.svg';
+
+export default create({
+ base: 'light',
+ brandTitle: '@plone/components StoryBook',
+ brandUrl: 'https://plone-components.netlify.app/',
+ brandImage: logo,
+ brandTarget: '_self',
+});
diff --git a/packages/cmsui/.stylelintrc b/packages/cmsui/.stylelintrc
new file mode 100644
index 0000000000..8ac62f8d0f
--- /dev/null
+++ b/packages/cmsui/.stylelintrc
@@ -0,0 +1,14 @@
+{
+ "extends": ["stylelint-config-idiomatic-order"],
+ "plugins": ["stylelint-prettier"],
+ "overrides": [
+ {
+ "files": ["**/*.scss"],
+ "customSyntax": "postcss-scss"
+ }
+ ],
+ "rules": {
+ "prettier/prettier": true,
+ "order/properties-alphabetical-order": null
+ }
+}
diff --git a/packages/cmsui/CHANGELOG.md b/packages/cmsui/CHANGELOG.md
new file mode 100644
index 0000000000..2969638257
--- /dev/null
+++ b/packages/cmsui/CHANGELOG.md
@@ -0,0 +1,11 @@
+# @plone/cmsui Release Notes
+
+
+
+
+
+## 1.0.0 (unreleased)
diff --git a/packages/cmsui/README.md b/packages/cmsui/README.md
new file mode 100644
index 0000000000..ee5d757cd6
--- /dev/null
+++ b/packages/cmsui/README.md
@@ -0,0 +1,8 @@
+# `@plone/cmsui`
+
+This package provides default structural slots for Plone 7 and the API-first story.
+
+> [!WARNING]
+> This package or app is experimental.
+> The community offers no support whatsoever for it.
+> Breaking changes may occur without notice.
diff --git a/packages/cmsui/components/Field/Field.tsx b/packages/cmsui/components/Field/Field.tsx
new file mode 100644
index 0000000000..9e91e94eb0
--- /dev/null
+++ b/packages/cmsui/components/Field/Field.tsx
@@ -0,0 +1,185 @@
+import config from '@plone/registry';
+import type {
+ WidgetsConfigById,
+ WidgetsConfigByFactory,
+ WidgetsConfigByType,
+ WidgetsConfigByVocabulary,
+ WidgetsConfigByWidget,
+} from '@plone/types';
+
+type FieldProps = {
+ id: keyof WidgetsConfigById;
+ widget: keyof WidgetsConfigByWidget;
+ vocabulary: { '@id': keyof WidgetsConfigByVocabulary };
+ choices: string;
+ type: keyof WidgetsConfigByType;
+ focus: boolean;
+ mode: string;
+ widgetOptions: any;
+ factory: keyof WidgetsConfigByFactory;
+ onChange: (id: string, value: any) => void;
+ placeholder: string;
+ title: string;
+};
+
+const MODE_HIDDEN = 'hidden'; //hidden mode. If mode is hidden, field is not rendered
+
+/**
+ * Get default widget
+ */
+const getWidgetDefault = (): React.ComponentType => config.widgets.default;
+
+/**
+ * Get widget by field's `id` attribute
+ */
+const getWidgetByFieldId = (
+ id: FieldProps['id'],
+): React.ComponentType | null => config.widgets.id[id] || null;
+
+/**
+ * Get widget by factory attribute
+ */
+const getWidgetByFactory = (
+ factory: FieldProps['factory'],
+): React.ComponentType | null => config.widgets.factory?.[factory] || null;
+
+/**
+ * Get widget by field's `widget` attribute
+ */
+const getWidgetByName = (
+ widget: FieldProps['widget'],
+): React.ComponentType | null =>
+ typeof widget === 'string'
+ ? config.widgets.widget[widget] || getWidgetDefault()
+ : null;
+
+/**
+ * Get widget by tagged values
+ *
+
+directives.widget(
+ 'fieldname',
+ frontendOptions={
+ "widget": 'specialwidget',
+ "version": 'extra'
+ })
+
+ */
+const getWidgetFromTaggedValues = (widgetOptions: {
+ frontendOptions: { widget: FieldProps['widget']; widgetProps: any };
+}): React.ComponentType | null =>
+ typeof widgetOptions?.frontendOptions?.widget === 'string'
+ ? config.widgets.widget[widgetOptions.frontendOptions.widget]
+ : null;
+
+/**
+ * Get widget props from tagged values
+ *
+
+directives.widget(
+ "fieldname",
+ frontendOptions={
+ "widget": "specialwidget",
+ "widgetProps": {"prop1": "specialprop"}
+ })
+
+ */
+const getWidgetPropsFromTaggedValues = (widgetOptions: {
+ frontendOptions: { widget: string; widgetProps: any };
+}): React.ComponentType | null =>
+ typeof widgetOptions?.frontendOptions?.widgetProps === 'object'
+ ? widgetOptions.frontendOptions.widgetProps
+ : null;
+
+/**
+ * Get widget by field's `vocabulary` attribute
+ */
+const getWidgetByVocabulary = (
+ vocabulary: FieldProps['vocabulary'],
+): React.ComponentType | null =>
+ vocabulary && vocabulary['@id']
+ ? config.widgets.vocabulary[
+ vocabulary['@id'].replace(
+ /^.*\/@vocabularies\//,
+ '',
+ ) as keyof WidgetsConfigByVocabulary
+ ]
+ : null;
+
+/**
+ * Get widget by field's hints `vocabulary` attribute in widgetOptions
+ */
+const getWidgetByVocabularyFromHint = (
+ props: FieldProps,
+): React.ComponentType | null =>
+ props.widgetOptions && props.widgetOptions.vocabulary
+ ? config.widgets.vocabulary[
+ props.widgetOptions.vocabulary['@id'].replace(
+ /^.*\/@vocabularies\//,
+ '',
+ ) as keyof WidgetsConfigByVocabulary
+ ]
+ : null;
+
+/**
+ * Get widget by field's `choices` attribute
+ */
+const getWidgetByChoices = (
+ props: FieldProps,
+): React.ComponentType | null => {
+ if (props.choices) {
+ return config.widgets.choices;
+ }
+
+ if (props.vocabulary) {
+ // If vocabulary exists, then it means it's a choice field in disguise with
+ // no widget specified that probably contains a string then we force it
+ // to be a select widget instead
+ return config.widgets.choices;
+ }
+
+ return null;
+};
+
+/**
+ * Get widget by field's `type` attribute
+ */
+const getWidgetByType = (type: FieldProps['type']): React.ComponentType =>
+ config.widgets.type[type] || null;
+
+const Field = (props: FieldProps) => {
+ const Widget =
+ getWidgetByFieldId(props.id) ||
+ getWidgetFromTaggedValues(props.widgetOptions) ||
+ getWidgetByName(props.widget) ||
+ getWidgetByChoices(props) ||
+ getWidgetByVocabulary(props.vocabulary) ||
+ getWidgetByVocabularyFromHint(props) ||
+ getWidgetByFactory(props.factory) ||
+ getWidgetByType(props.type) ||
+ getWidgetDefault();
+
+ if (props.mode === MODE_HIDDEN) {
+ return null;
+ }
+
+ // Adding the widget props from tagged values (if any)
+ const widgetProps = {
+ ...props,
+ label: props.title,
+ placeholder: props.placeholder || 'Type something...',
+ // Temporary translator from the old widget signature (id, value) to the new one (value)
+ onChange: (arg1: string, arg2: string | undefined) => {
+ if (!arg2 === undefined) {
+ props.onChange(props.id, arg1);
+ } else {
+ props.onChange(props.id, arg2);
+ }
+ },
+ ...getWidgetPropsFromTaggedValues(props.widgetOptions),
+ };
+
+ return ;
+};
+
+export default Field;
diff --git a/packages/cmsui/config/widgets.ts b/packages/cmsui/config/widgets.ts
new file mode 100644
index 0000000000..3ff5ba82fb
--- /dev/null
+++ b/packages/cmsui/config/widgets.ts
@@ -0,0 +1,9 @@
+import type { ConfigType } from '@plone/registry';
+import { QuantaTextAreaField, QuantaTextField } from '@plone/components';
+
+export default function install(config: ConfigType) {
+ config.widgets.default = QuantaTextField;
+ config.widgets.widget.textarea = QuantaTextAreaField;
+
+ return config;
+}
diff --git a/packages/cmsui/index.ts b/packages/cmsui/index.ts
new file mode 100644
index 0000000000..4804a02ba9
--- /dev/null
+++ b/packages/cmsui/index.ts
@@ -0,0 +1,9 @@
+import type { ConfigType } from '@plone/registry';
+import installWidgets from './config/widgets';
+import '@plone/components/dist/basic.css';
+import '@plone/components/dist/quanta.css';
+
+export default function install(config: ConfigType) {
+ installWidgets(config);
+ return config;
+}
diff --git a/packages/cmsui/main.css b/packages/cmsui/main.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/cmsui/news/.gitkeep b/packages/cmsui/news/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/cmsui/package.json b/packages/cmsui/package.json
new file mode 100644
index 0000000000..d397a3f337
--- /dev/null
+++ b/packages/cmsui/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "@plone/cmsui",
+ "description": "Plone CMSUI components",
+ "maintainers": [
+ {
+ "name": "Plone Foundation",
+ "url": "https://plone.org"
+ }
+ ],
+ "funding": "https://github.com/sponsors/plone",
+ "license": "MIT",
+ "version": "1.0.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/plone/volto.git"
+ },
+ "bugs": {
+ "url": "https://github.com/plone/volto/issues"
+ },
+ "homepage": "https://plone.org",
+ "keywords": [
+ "volto",
+ "plone",
+ "plone6",
+ "react",
+ "helpers"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "type": "module",
+ "main": "index.ts",
+ "scripts": {
+ "test": "vitest",
+ "dry-release": "release-it --dry-run",
+ "release": "release-it",
+ "release-major-alpha": "release-it major --preRelease=alpha",
+ "release-alpha": "release-it --preRelease=alpha",
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ },
+ "dependencies": {
+ "@plone/client": "workspace:*",
+ "@plone/components": "workspace:*",
+ "@plone/registry": "workspace:*",
+ "react-aria-components": "^1.5.0"
+ },
+ "devDependencies": {
+ "@plone/types": "workspace:*",
+ "@storybook/addon-essentials": "^8.0.4",
+ "@storybook/addon-interactions": "^8.0.4",
+ "@storybook/addon-links": "^8.0.4",
+ "@storybook/addon-mdx-gfm": "^8.0.4",
+ "@storybook/blocks": "^8.0.4",
+ "@storybook/manager-api": "^8.0.4",
+ "@storybook/react": "^8.0.4",
+ "@storybook/react-vite": "^8.0.4",
+ "@storybook/theming": "^8.0.4",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "eslint-plugin-storybook": "^0.8.0",
+ "jest-axe": "^8.0.0",
+ "release-it": "17.1.1",
+ "storybook": "^8.0.4",
+ "tsconfig": "workspace:*",
+ "typescript": "^5.6.3",
+ "vite": "^5.4.8",
+ "vitest": "^2.1.3",
+ "vitest-axe": "^0.1.0"
+ }
+}
diff --git a/packages/cmsui/setupTesting.ts b/packages/cmsui/setupTesting.ts
new file mode 100644
index 0000000000..8bc87fa36e
--- /dev/null
+++ b/packages/cmsui/setupTesting.ts
@@ -0,0 +1,3 @@
+import '@testing-library/jest-dom';
+import { toHaveNoViolations } from 'jest-axe';
+expect.extend(toHaveNoViolations);
diff --git a/packages/cmsui/stories.ts b/packages/cmsui/stories.ts
new file mode 100644
index 0000000000..6a668df163
--- /dev/null
+++ b/packages/cmsui/stories.ts
@@ -0,0 +1,22 @@
+export const storyData = {
+ blocks: {
+ '7ab29abe-b38c-406b-94d7-b270e544a998': {
+ '@type': 'slate',
+ value: [
+ {
+ type: 'p',
+ children: [
+ {
+ text: 'Lorem ipsum dolor sit amet eu tempus ornare elit. Curabitur egestas quisque molestie pellentesque nunc imperdiet posuere morbi nunc eleifend. Volutpat enim augue blandit aliquam interdum pulvinar eu mattis congue. Eleifend mauris ut fermentum egestas mi faucibus adipiscing arcu nibh scelerisque justo habitasse. Mi consectetur hac maecenas leo dictumst vitae phasellus quam praesent vivamus nullam imperdiet integer mauris.',
+ },
+ ],
+ },
+ ],
+ plaintext:
+ 'Lorem ipsum dolor sit amet eu tempus ornare elit. Curabitur egestas quisque molestie pellentesque nunc imperdiet posuere morbi nunc eleifend. Volutpat enim augue blandit aliquam interdum pulvinar eu mattis congue. Eleifend mauris ut fermentum egestas mi faucibus adipiscing arcu nibh scelerisque justo habitasse. Mi consectetur hac maecenas leo dictumst vitae phasellus quam praesent vivamus nullam imperdiet integer mauris.',
+ },
+ },
+ blocks_layout: {
+ items: ['7ab29abe-b38c-406b-94d7-b270e544a998'],
+ },
+};
diff --git a/packages/cmsui/towncrier.toml b/packages/cmsui/towncrier.toml
new file mode 100644
index 0000000000..3ef721f378
--- /dev/null
+++ b/packages/cmsui/towncrier.toml
@@ -0,0 +1,33 @@
+[tool.towncrier]
+filename = "CHANGELOG.md"
+directory = "news/"
+title_format = "## {version} ({project_date})"
+underlines = ["", "", ""]
+template = "../scripts/templates/towncrier_template.jinja"
+start_string = "\n"
+issue_format = "[#{issue}](https://github.com/plone/volto/issues/{issue})"
+
+[[tool.towncrier.type]]
+directory = "breaking"
+name = "Breaking"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "feature"
+name = "Feature"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "bugfix"
+name = "Bugfix"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "internal"
+name = "Internal"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "documentation"
+name = "Documentation"
+showcontent = true
diff --git a/packages/cmsui/tsconfig.json b/packages/cmsui/tsconfig.json
new file mode 100644
index 0000000000..2965402840
--- /dev/null
+++ b/packages/cmsui/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "tsconfig/react-library.json",
+ "include": ["**/*.ts", "**/*.tsx"],
+ "exclude": [
+ "node_modules",
+ "build",
+ "public",
+ "coverage",
+ "src/**/*.test.{js,jsx,ts,tsx}",
+ "src/**/*.spec.{js,jsx,ts,tsx}",
+ "src/**/*.stories.{js,jsx,ts,tsx}"
+ ]
+}
diff --git a/packages/cmsui/vitest.config.ts b/packages/cmsui/vitest.config.ts
new file mode 100644
index 0000000000..fddf5f61f7
--- /dev/null
+++ b/packages/cmsui/vitest.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from 'vitest/config';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: './setupTesting.ts',
+ // you might want to disable it, if you don't have tests that rely on CSS
+ // since parsing CSS is slow
+ css: true,
+ exclude: ['**/node_modules/**', '**/lib/**'],
+ },
+});
diff --git a/packages/volto/.gitignore b/packages/volto/.gitignore
index 6d1cc045b4..fdb83bb779 100644
--- a/packages/volto/.gitignore
+++ b/packages/volto/.gitignore
@@ -10,6 +10,7 @@ eslint.xml
yarn-error.log
build
.changelog.draft
+.registry.loader.js
# yarn 3
.pnp.*
diff --git a/packages/volto/package.json b/packages/volto/package.json
index 64d9b1c45b..6ad031b51e 100644
--- a/packages/volto/package.json
+++ b/packages/volto/package.json
@@ -30,6 +30,9 @@
"coreAddons": {
"volto-slate": {
"package": "@plone/volto-slate"
+ },
+ "cmsui": {
+ "package": "@plone/cmsui"
}
},
"main": "src/index.js",
@@ -179,6 +182,7 @@
"dependencies": {
"@loadable/component": "5.14.1",
"@loadable/server": "5.14.0",
+ "@plone/cmsui": "workspace:*",
"@plone/registry": "workspace:*",
"@plone/scripts": "workspace:*",
"@plone/volto-slate": "workspace:*",
diff --git a/packages/volto/src/components/manage/Form/Form.jsx b/packages/volto/src/components/manage/Form/Form.jsx
index d19b68a603..e5b358b3e0 100644
--- a/packages/volto/src/components/manage/Form/Form.jsx
+++ b/packages/volto/src/components/manage/Form/Form.jsx
@@ -5,7 +5,8 @@
import Icon from '@plone/volto/components/theme/Icon/Icon';
import Toast from '@plone/volto/components/manage/Toast/Toast';
-import { Field, BlocksForm } from '@plone/volto/components/manage/Form';
+import { BlocksForm } from '@plone/volto/components/manage/Form';
+import Field from '@plone/cmsui/components/Field/Field';
import BlocksToolbar from '@plone/volto/components/manage/Form/BlocksToolbar';
import UndoToolbar from '@plone/volto/components/manage/Form/UndoToolbar';
import { difference } from '@plone/volto/helpers/Utils/Utils';
diff --git a/packages/volto/src/components/manage/Form/InlineForm.jsx b/packages/volto/src/components/manage/Form/InlineForm.jsx
index a92f6aaba2..6e132a9f74 100644
--- a/packages/volto/src/components/manage/Form/InlineForm.jsx
+++ b/packages/volto/src/components/manage/Form/InlineForm.jsx
@@ -14,7 +14,7 @@ import {
arrayRange,
} from '@plone/volto/helpers/Utils/Utils';
import Icon from '@plone/volto/components/theme/Icon/Icon';
-import { Field } from '@plone/volto/components/manage/Form';
+import Field from '@plone/cmsui/components/Field/Field';
import { applySchemaDefaults } from '@plone/volto/helpers/Blocks/Blocks';
import upSVG from '@plone/volto/icons/up-key.svg';
diff --git a/packages/volto/src/config/index.js b/packages/volto/src/config/index.js
index 48ab6e0159..f4a497b546 100644
--- a/packages/volto/src/config/index.js
+++ b/packages/volto/src/config/index.js
@@ -177,6 +177,14 @@ let config = {
includeSiteTitle: false,
titleAndSiteTitleSeparator: '-',
},
+ cssLayers: [
+ 'reset',
+ 'semanticUI',
+ 'plone-components',
+ 'layout',
+ 'addons',
+ 'theme',
+ ],
},
experimental: {
addBlockButton: {
diff --git a/packages/volto/src/loadSemanticUI.less b/packages/volto/src/loadSemanticUI.less
new file mode 100644
index 0000000000..be2ef838ac
--- /dev/null
+++ b/packages/volto/src/loadSemanticUI.less
@@ -0,0 +1,4 @@
+@layer semanticUI {
+ @import 'semantic-ui-less/semantic.less';
+ @import '@plone/volto/../theme/themes/pastanaga/extras/extras.less';
+}
diff --git a/packages/volto/src/styles/main.css b/packages/volto/src/styles/main.css
new file mode 100644
index 0000000000..eb3c025cb8
--- /dev/null
+++ b/packages/volto/src/styles/main.css
@@ -0,0 +1,2 @@
+@import 'variables.css';
+@import 'sidebar.css';
diff --git a/packages/volto/src/styles/sidebar.css b/packages/volto/src/styles/sidebar.css
new file mode 100644
index 0000000000..588b700570
--- /dev/null
+++ b/packages/volto/src/styles/sidebar.css
@@ -0,0 +1,19 @@
+/* Todo: Better class naming to identify things in the sidebar? */
+.sidebar-container {
+ @layer plone-components {
+ /* ToDo: Infer a `field` classname? */
+ .q.react-aria-TextField {
+ width: 100%;
+ }
+
+ .q.react-aria-TextField,
+ .q.react-aria-TextArea {
+ margin-bottom: var(--form-spacing-small);
+ }
+
+ .q.react-aria-TextField:first-child,
+ .q.react-aria-TextArea:first-child {
+ margin-top: var(--form-spacing-small);
+ }
+ }
+}
diff --git a/packages/volto/src/styles/variables.css b/packages/volto/src/styles/variables.css
new file mode 100644
index 0000000000..b7cf02e909
--- /dev/null
+++ b/packages/volto/src/styles/variables.css
@@ -0,0 +1,3 @@
+:root {
+ --form-spacing-small: 25px;
+}
diff --git a/packages/volto/src/theme.js b/packages/volto/src/theme.js
index 44b93a05b8..b14b184861 100644
--- a/packages/volto/src/theme.js
+++ b/packages/volto/src/theme.js
@@ -1,12 +1,3 @@
-/**
- * If you want to apply semantic ui only to the toolbar
- * and all administrative or editorial views,
- * uncomment the following line and comment the line:
- * `import 'semantic-ui-less/semantic.less';`
- *
- * Then, in your `theme.config` file, change the following variable:
- * `@container : 'pastanaga-cms-ui'`
- */
-// import '../theme/themes/pastanaga-cms-ui/extras/cms-ui.semantic.less';
-import 'semantic-ui-less/semantic.less';
-import '../theme/themes/pastanaga/extras/extras.less';
+// Load SemanticUI behind a layer
+import './loadSemanticUI.less';
+import './styles/main.css';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ae6dfec91f..72835d24c2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -517,6 +517,91 @@ importers:
specifier: ^7.2.0
version: 7.2.0(debug@4.3.4)
+ packages/cmsui:
+ dependencies:
+ '@plone/client':
+ specifier: workspace:*
+ version: link:../client
+ '@plone/components':
+ specifier: workspace:*
+ version: link:../components
+ '@plone/registry':
+ specifier: workspace:*
+ version: link:../registry
+ react:
+ specifier: ^16.8.0 || ^17.0.0 || ^18.0.0
+ version: 18.2.0
+ react-aria-components:
+ specifier: ^1.5.0
+ version: 1.5.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ react-dom:
+ specifier: ^16.8.0 || ^17.0.0 || ^18.0.0
+ version: 18.2.0(react@18.2.0)
+ devDependencies:
+ '@plone/types':
+ specifier: workspace:*
+ version: link:../types
+ '@storybook/addon-essentials':
+ specifier: ^8.0.4
+ version: 8.0.8(@types/react@18.3.12)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@storybook/addon-interactions':
+ specifier: ^8.0.4
+ version: 8.0.8(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@2.1.3(@types/node@20.12.7)(jsdom@22.1.0)(less@3.11.1)(lightningcss@1.28.1)(sass@1.75.0)(terser@5.30.3))
+ '@storybook/addon-links':
+ specifier: ^8.0.4
+ version: 8.0.8(react@18.2.0)
+ '@storybook/addon-mdx-gfm':
+ specifier: ^8.0.4
+ version: 8.0.8
+ '@storybook/blocks':
+ specifier: ^8.0.4
+ version: 8.0.8(@types/react@18.3.12)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@storybook/manager-api':
+ specifier: ^8.0.4
+ version: 8.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@storybook/react':
+ specifier: ^8.0.4
+ version: 8.0.8(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.6.3)
+ '@storybook/react-vite':
+ specifier: ^8.0.4
+ version: 8.0.8(@preact/preset-vite@2.8.2(@babel/core@7.25.8)(vite@5.4.9(@types/node@20.12.7)(less@3.11.1)(lightningcss@1.28.1)(sass@1.75.0)(terser@5.30.3)))(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.9(@types/node@20.12.7)(less@3.11.1)(lightningcss@1.28.1)(sass@1.75.0)(terser@5.30.3))
+ '@storybook/theming':
+ specifier: ^8.0.4
+ version: 8.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@types/react':
+ specifier: ^18
+ version: 18.3.12
+ '@types/react-dom':
+ specifier: ^18
+ version: 18.3.1
+ eslint-plugin-storybook:
+ specifier: ^0.8.0
+ version: 0.8.0(eslint@8.57.0)(typescript@5.6.3)
+ jest-axe:
+ specifier: ^8.0.0
+ version: 8.0.0
+ release-it:
+ specifier: 17.1.1
+ version: 17.1.1(typescript@5.6.3)
+ storybook:
+ specifier: ^8.0.4
+ version: 8.0.8(@babel/preset-env@7.24.4(@babel/core@7.25.8))(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ tsconfig:
+ specifier: workspace:*
+ version: link:../tsconfig
+ typescript:
+ specifier: ^5.6.3
+ version: 5.6.3
+ vite:
+ specifier: ^5.4.8
+ version: 5.4.9(@types/node@20.12.7)(less@3.11.1)(lightningcss@1.28.1)(sass@1.75.0)(terser@5.30.3)
+ vitest:
+ specifier: ^2.1.3
+ version: 2.1.3(@types/node@20.12.7)(jsdom@22.1.0)(less@3.11.1)(lightningcss@1.28.1)(sass@1.75.0)(terser@5.30.3)
+ vitest-axe:
+ specifier: ^0.1.0
+ version: 0.1.0(vitest@2.1.3(@types/node@20.12.7)(jsdom@22.1.0)(less@3.11.1)(lightningcss@1.28.1)(sass@1.75.0)(terser@5.30.3))
+
packages/components:
dependencies:
'@react-aria/utils':
@@ -1157,6 +1242,9 @@ importers:
'@loadable/server':
specifier: 5.14.0
version: 5.14.0(@loadable/component@5.14.1(react@18.2.0))(react@18.2.0)
+ '@plone/cmsui':
+ specifier: workspace:*
+ version: link:../cmsui
'@plone/registry':
specifier: workspace:*
version: link:../registry
@@ -18340,7 +18428,7 @@ snapshots:
dependencies:
'@babel/core': 7.25.8
'@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.8)
- '@babel/helper-plugin-utils': 7.24.0
+ '@babel/helper-plugin-utils': 7.25.7
'@babel/plugin-transform-arrow-functions@7.24.1(@babel/core@7.24.4)':
dependencies:
@@ -18378,9 +18466,11 @@ snapshots:
'@babel/plugin-transform-async-to-generator@7.24.1(@babel/core@7.25.8)':
dependencies:
'@babel/core': 7.25.8
- '@babel/helper-module-imports': 7.24.3
+ '@babel/helper-module-imports': 7.25.7
'@babel/helper-plugin-utils': 7.25.7
'@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.25.8)
+ transitivePeerDependencies:
+ - supports-color
'@babel/plugin-transform-block-scoped-functions@7.24.1(@babel/core@7.24.4)':
dependencies:
@@ -23526,12 +23616,12 @@ snapshots:
'@storybook/react-vite@8.0.8(@preact/preset-vite@2.8.2(@babel/core@7.25.8)(vite@5.4.8(@types/node@20.12.7)(less@3.11.1)(lightningcss@1.24.1)(sass@1.75.0)(terser@5.30.3)))(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.8(@types/node@20.12.7)(less@3.11.1)(lightningcss@1.24.1)(sass@1.75.0)(terser@5.30.3))':
dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.6.3)(vite@5.4.8(@types/node@20.12.7)(less@3.11.1)(lightningcss@1.24.1)(sass@1.75.0)(terser@5.30.3))
- '@rollup/pluginutils': 5.1.0(rollup@4.24.0)
+ '@rollup/pluginutils': 5.1.2(rollup@4.24.0)
'@storybook/builder-vite': 8.0.8(@preact/preset-vite@2.8.2(@babel/core@7.25.8)(vite@5.4.8(@types/node@20.12.7)(less@3.11.1)(lightningcss@1.24.1)(sass@1.75.0)(terser@5.30.3)))(encoding@0.1.13)(typescript@5.6.3)(vite@5.4.8(@types/node@20.12.7)(less@3.11.1)(lightningcss@1.24.1)(sass@1.75.0)(terser@5.30.3))
'@storybook/node-logger': 8.0.8
'@storybook/react': 8.0.8(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.6.3)
find-up: 5.0.0
- magic-string: 0.30.10
+ magic-string: 0.30.11
react: 18.2.0
react-docgen: 7.0.3
react-dom: 18.2.0(react@18.2.0)