diff --git a/docs/customizing-volto-light-theme/advanced-components-bm3.md b/docs/customizing-volto-light-theme/advanced-components-bm3.md new file mode 100644 index 000000000..6558137e1 --- /dev/null +++ b/docs/customizing-volto-light-theme/advanced-components-bm3.md @@ -0,0 +1,1152 @@ +--- +myst: + html_meta: + "description": "Advanced Components, Slots & Block Model v3" + "property=og:description": "Advanced Components, Slots & Block Model v3" + "property=og:title": "Advanced Components, Slots & Block Model v3" + "keywords": "Plone, Volto, Training, Volto Light Theme, Integrate, block" +--- + +# Advanced Components, Slots & Block Model v3 + +## Understanding the Card Primitive + +The Card primitive is VLT's reusable component for displaying content in card layouts. It has three configurable slots: image, summary, and actions. + +### Card Structure + +```jsx + + + +

ARM-7 Bench Arm

+

Six-axis arm for precision bench work.

+
+ + + +
+``` + +### Making a Card Clickable + +A card becomes a link when you give it either an `item` or an `href`. The two are mutually exclusive, and `item` is the one to reach for when you have a content object: + +```jsx +... // preferred: a Plone content object +... // for arbitrary destinations +``` + +The link wraps the title inside the summary, and a CSS overlay stretches its clickable area across the whole card. + +Passing `item` lets the underlying link inspect the content type. +For visitors who are not logged in, a File links to its download URL, and a Link item links to its target URL. +Passing `href={item['@id']}` skips that, and those items link to their own page instead. +Use `href` when you genuinely have only a URL. + +Pass `null` to make the card non-interactive, which is what listing templates do in edit mode. + +### Card Variations + +The layout of a card depends on where it is rendered: + +- **Vertical** (default): the image is on top. +- **Horizontal**: the image is on the left or on the right, when the surrounding block is aligned left or right, as a Teaser block can be. +- **Contained**: inside a container block, such as a Grid, the summary gets side padding, and the card takes its background color from the current theme. +- **Listing**: inside a `.card-listing` wrapper, the image is on the left, with a fixed width set by `--card-listing-image-size`, 220px by default. + +### Card.Image Slot + +Point the slot at a content object and it resolves the image from that item: + +```tsx + +``` + +A `src` prop works too, for an image that is not a content object. To control how the image is rendered, pass your own component: + +```tsx + +``` + +### Card.Summary Slot + +Recommended structure using VLT's Summary component: + +```tsx +import config from '@plone/volto/registry'; +import DefaultSummary from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary'; + +const Summary = config.getComponent({ + name: 'Summary', + dependencies: [item['@type']], +}).component || DefaultSummary; + + + + +``` + +`Card` generates an id for the card's accessible name and builds a link component for the item, then passes both down to its slots as the `a11yLabelId` and `LinkToItem` props. +`Card.Summary` forwards them to its children, so a Summary rendered inside it receives them without you wiring anything up. +Using them is what gives the card an accessible name and a real link—see the next section. + +## Creating Custom Summary Components + +The Summary component displays content metadata in listings, teasers, and cards. +VLT includes these implementations, and registers all but the first for their content types: + +- `DefaultSummary`: the kicker, title, and description. It is the fallback for all other types. +- `NewsItemSummary`, for News Items: the publication date and the kicker, then the title and description. +- `EventSummary`, for Events: the start and end dates and the kicker, then the title and description. +- `FileSummary`, for Files: the file size, file type, and kicker, then the title and description. +- `PersonSummary`, for the Person type: the kicker, title, and description, plus the email address, phone number, and room. + +### The Summary Contract + +Every Summary receives the same props. Getting this shape right matters, because three of them are easy to miss: + +```ts +type DefaultSummaryProps = { + item: Partial; // the content object + LinkToItem?: React.ElementType; // wraps the title in the card's link + HeadingTag?: React.ElementType; // the heading level to render + a11yLabelId?: string; // id that names the card + hide_description?: boolean; +}; +``` + +`item` is the content object. The other three are **siblings of `item`, not fields inside it**. A common mistake is to read them from `item`, where they will always be `undefined`. + +`DefaultSummary` shows the pattern to follow: + +```tsx +const DefaultSummary = (props) => { + const { + item, + LinkToItem = React.Fragment, + HeadingTag = 'div', + a11yLabelId, + } = props; + + return ( + <> + {item?.head_title &&
{item.head_title}
} + + {item.title ? item.title : item.id} + + {/* ... */} + + ); +}; +``` + +The `id={a11yLabelId}` and the `` wrapper are not decoration. +Together they give the card an accessible name and put a real link in the heading, which is how a keyboard or screen reader user reaches the item. +A Summary that omits them renders a card that looks right but cannot be navigated. + +### Step 1: Create the Robot Content Type + +The Robotarium lends robots, so it needs a content type for them. Create it through the Plone UI: + +1. Go to http://localhost:3000/controlpanel/dexterity-types. +2. Select the **Add** button in the toolbar. +3. In the **Add new content type** form, fill in: + - **Title**: Robot + - **Description**: A robot available for booking +4. Select **Save**. +5. Select **Robot** in the list of content types. +6. In the **Behaviors** tab, enable: + - **Kicker field**, which adds a field named `head_title`. The Robotarium uses it to store the robot's charge level. + - **Preview Image**, so that robots can show a photo in listings. + - Any other behaviors you want. +7. Select **Save**. + +```{note} +The form asks only for a title and a description. Plone derives the type's id from the title by normalizing it, so **Robot** becomes `robot`, and a title like **Charging Station** would become `charging_station`. + +That id is what the REST API returns as the content's `@type`, and it is the value you register components against later in this chapter. +It is worth confirming rather than assuming: open http://localhost:3000/++api++/ followed by the path of a robot you created, and check its `@type`. + +Plone's built-in types predate this rule and keep their historical ids, which is why VLT registers its news summary against `News Item`, with a space and capitals, rather than `news_item`. +``` + +### Step 2: Create a Custom Summary Component + +Create {file}`src/components/Summary/RobotSummary.tsx`: + +```tsx +import * as React from 'react'; +import { FormattedNumber } from 'react-intl'; +import type { DefaultSummaryProps } from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary'; + +const RobotSummary = (props: DefaultSummaryProps) => { + const { + item, + LinkToItem = React.Fragment, + HeadingTag = 'div', + a11yLabelId, + } = props; + const { title, description, head_title } = item; + const charge = parseFloat(head_title); + + return ( + <> + + {title ? title : item.id} + + {description &&

{description}

} + {head_title && !isNaN(charge) && ( +
+
+ Charge + + + +
+ {/* The bar repeats the value the label already states, so it is + hidden from assistive technology rather than announced twice. */} + + )} + + ); +}; + +RobotSummary.hideLink = false; +export default RobotSummary; +``` + +The `HeadingTag` default is `'div'` rather than a fixed heading level. The caller decides the level, because the right one depends on where the listing sits in the page's heading outline. + +```{note} +The example reuses the kicker (`head_title`) to hold the charge level, so that it needs no new field. +A real Robotarium would add a proper numeric field to the Robot type instead, and reserve the kicker for what it is meant for: a line of text above the title. +``` + +### Step 3: Add Styles for the Robot Summary + +The charge is the one thing VLT has no styling for, so it is the one thing this partial has to describe: a labelled row above a slim track whose fill is as wide as the charge. + +Create {file}`src/theme/_robotSummary.scss`: + +```scss +.robot-charge { + margin-top: $spacing-small; + + .charge-label { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + letter-spacing: 0.14em; + text-transform: uppercase; + @include headtitle2(); + } + + .charge-track { + overflow: hidden; + height: 5px; + border-radius: 999px; + background: color-mix( + in oklab, + var(--theme-foreground-color) 12%, + transparent + ); + } + + .charge-level { + height: 100%; + border-radius: 999px; + background: linear-gradient( + 90deg, + color-mix(in oklab, var(--accent-color) 60%, #fff), + var(--accent-color) + ); + } +} + +// Cards that carry a charge read as instrument panels, so give them an edge. +.card:has(.robot-charge) { + border: 1px solid oklch(1 0 0 / 0.28); +} +``` + +Three decisions here are worth carrying to your own work. + +**Nothing is scoped to the listing.** A Summary is rendered in listings, in teasers, and in bare cards, and the same component should look the same in all of them. Scoping these rules to the fleet listing, with a selector such as `.robots .card .card-summary .robot-charge`, would style the listing and leave the same robot in a teaser looking like a different component. Selecting on what the Summary itself renders keeps the contexts in step. + +**The colors come from tokens.** The track mixes the theme's foreground color with transparency, so it stays visible on any block theme, and the fill follows `--accent-color`, so it changes with the design. + +**The typography comes from VLT.** The label uses VLT's `headtitle2()` mixin, one of the theme's text styles for small labels, instead of a font size of its own. + +Import the partial at the end of {file}`src/theme/_main.scss`: + +```scss +@import './robotSummary'; +``` + +### Step 4: Register the Summary Component + +In {file}`src/config/settings.ts`: + +```typescript +import RobotSummary from '../components/Summary/RobotSummary'; + +export default function install(config: ConfigType) { + // ... previous config ... + + config.registerComponent({ + name: 'Summary', + component: RobotSummary, + dependencies: ['robot'], + }); + + return config; +} +``` + +### Step 5: Test the Robot Summary + +1. Add a Robot to your site, for example **ARM-7 Bench Arm**. +2. Fill in the title, a short description, and the kicker with a plain number such as `87`. +3. Add the robot to a Listing or Teaser block. +4. `RobotSummary` renders the title, the description, and a charge level of 87%. + +## Creating Custom Listing Variations with Card Actions + +Listing variations customize how a Listing block displays its content. This section builds a `RobotsTemplate` that presents the fleet and uses the Card.Actions slot for a booking button. + +### Card.Actions Slot + +```{important} +`Card.Actions` only renders what a template puts in it, and **none of VLT's built-in listing variations put anything there**. +Registering an `Actions` component is not enough on its own: the List, List with images, and Grid variations, and the Teaser block, all ignore the slot, so a card rendered by any of them shows no actions. + +That is why the fleet listing below needs its own template. If you register an Actions component and see nothing, check which variation the listing is using before suspecting the registration. +``` + +The Card.Actions slot provides interactive elements beyond the main card link: + +- "Book this unit" for a robot that is available +- "Download specifications" for its documentation +- "Reserve a slot" for a robot that is currently out + +The card's link overlay covers the whole card, so the actions have to sit above it. Otherwise, selecting an action would open the card's link instead. +The stylesheet in Step 5 takes care of that. + +### Step 1: Create RobotActions Component + +Create {file}`src/components/Actions/RobotActions.tsx`: + +```tsx +const RobotActions = ({ item }) => { + return ( + + ); +}; + +export default RobotActions; +``` + +The button carries `type="button"` so it does not submit a surrounding form, and names the robot it belongs to. In a fleet listing, four buttons all reading "Book" are indistinguishable to someone moving through the page control by control; "Book ARM-7 Bench Arm" is not. + +### Step 2: Register RobotActions + +Update {file}`src/config/settings.ts`: + +```typescript +import RobotActions from '../components/Actions/RobotActions'; + +export default function install(config: ConfigType) { + // ... previous configuration ... + + // Register RobotActions component + config.registerComponent({ + name: 'Actions', + component: RobotActions, + dependencies: ['robot'], + }); + + return config; +} +``` + +### Step 3: Create RobotsTemplate Listing Variation + +Create {file}`src/components/blocks/Listing/RobotsTemplate.tsx`: + +```tsx +import React from 'react'; +import PropTypes from 'prop-types'; +import ConditionalLink from '@plone/volto/components/manage/ConditionalLink/ConditionalLink'; +import Card from '@kitconcept/volto-light-theme/primitives/Card/Card'; +import { flattenToAppURL, isInternalURL } from '@plone/volto/helpers/Url/Url'; +import config from '@plone/volto/registry'; +import DefaultSummary from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary'; +import cx from 'classnames'; + +const RobotsTemplate = ({ items, linkTitle, linkHref, isEditMode }) => { + let link = null; + let href = linkHref?.[0]?.['@id'] || ''; + const PreviewImageComponent = config.getComponent('PreviewImage').component; + + if (isInternalURL(href)) { + link = ( + + {linkTitle || href} + + ); + } else if (href) { + link = {linkTitle || href}; + } + + return ( + <> +
    + {items.map((item) => { + const Summary = + config.getComponent({ + name: 'Summary', + dependencies: [item['@type']], + }).component || DefaultSummary; + + const Actions = config.getComponent({ + name: 'Actions', + dependencies: [item['@type']], + }).component; + + const showLink = !Summary.hideLink && !isEditMode; + const placeholderSrc = + config.settings.placeholderImages?.[item['@type']]; + + return ( +
  • + + + + + + + {Actions && } + + +
  • + ); + })} +
+ + {link &&
{link}
} + + ); +}; + +RobotsTemplate.propTypes = { + items: PropTypes.arrayOf(PropTypes.any).isRequired, + linkTitle: PropTypes.string, + linkHref: PropTypes.any, + isEditMode: PropTypes.bool, +}; + +export default RobotsTemplate; +``` + +**Key Features:** + +- Uses `config.getComponent()` to fetch the Summary and Actions components registered for each content type +- Renders actions only for content types that have an Actions component registered +- Always renders the image slot, so a robot without a photo shows a placeholder, and every card in a row has the same shape +- Passes `null` to `Card` in edit mode, or when the Summary sets `hideLink`, so that the card is not a link there +- Passes `item` to `Card` rather than a bare URL, so that content types with their own link behavior, such as File, resolve correctly +- Renders a `
    ` of `
  • ` elements, matching VLT's own listing templates, so assistive technology announces the number of items + +```{important} +Write a variation by starting from the VLT variation closest to what you want—here that is `GridTemplate`, since the fleet is a two-column card grid—and change only what has to change. + +The image props illustrate why. `sizes` tells the browser how wide the image will actually render, so that it downloads the right scale; omit it and the cards look soft for no visible reason. It is not obvious from the outside, and it comes for free by following the shape of the template you are adapting. + +The same applies to the stylesheet in Step 5. +``` + +The fleet deviates from `GridTemplate` in one deliberate place. +`GridTemplate` renders `Card.Image` only when an item has an image, or when a placeholder is registered for its content type. +`RobotsTemplate` always renders it, with `showPlaceholderImage`, so a robot without a photo shows Volto's default placeholder image instead of an empty space. + +To show a robot-specific placeholder instead, add an image to your add-on, for example {file}`src/assets/robot-placeholder.svg`, and register it for the `robot` type in {file}`src/config/settings.ts`: + +```typescript +import robotPlaceholderImage from '../assets/robot-placeholder.svg'; + +export default function install(config: ConfigType) { + // ... previous configuration ... + + config.settings.placeholderImages = { + ...config.settings.placeholderImages, + robot: robotPlaceholderImage, + }; + + return config; +} +``` + +The key is the content type id, the same one you registered the Summary and Actions components against. +VLT's Teaser block and its own listing variations read the same setting, so a robot without a photo shows the same placeholder there. + +```{note} +Do not put the placeholder in a folder named {file}`icons`. +Volto loads SVG files from {file}`icons` folders as inline icons rather than as image files, so they cannot be used as the source of an image. +``` + +### Step 4: Register RobotsTemplate + +Update {file}`src/config/blocks.ts`: + +```typescript +import RobotsTemplate from '../components/blocks/Listing/RobotsTemplate'; + +export default function install(config: ConfigType) { + // ... previous configuration ... + + // Register RobotsTemplate listing variation + config.blocks.blocksConfig.listing.variations = [ + ...(config.blocks.blocksConfig.listing.variations || []), + { + id: 'robots', + title: 'Robot Fleet', + template: RobotsTemplate, + }, + ]; + + return config; +} +``` + +The Listing block adds the id of the selected variation to its classes, so the block renders as `.block.listing.robots`, which the stylesheet in the next step targets. + +### Step 5: Add Styles + +The fleet is a two-column card grid, which is what VLT's own `grid` variation already is. +The stylesheet repeats that variation's rules for `robots`, and adds the two things the grid variation has no equivalent for: the card surface, and the actions row. + +Create {file}`src/theme/blocks/_listing.scss`: + +```scss +// The rules below mirror `.block.listing.grid` in VLT's +// `theme/blocks/_listing.scss` and `theme/_layout.scss`. + +// VLT constrains most listing variations per item, and the grid variation as a +// whole. A card grid needs the second treatment, so repeat for `robots` what +// VLT does for `& > .block.listing.grid`. View mode only — in edit mode +// `.block-editor-listing .items` is already the constrained element, and adding +// a second cap on the block is what makes the fleet listing sit off-center there. +#page-document .blocks-group-wrapper > .block.listing.robots { + @include default-container-width(); + @include adjustMarginsToContainer($default-container-width); +} + +.block.listing.robots { + &.next--has--same--backgroundColor.next--is--same--block-type, + &.next--is--__button { + .listing-item:last-child { + padding-bottom: 0 !important; + border-bottom: none !important; + } + } + + .items { + display: flex; + flex-wrap: wrap; + + @media only screen and (max-width: $largest-mobile-screen) { + flex-direction: column; + + .listing-item { + padding-bottom: $spacing-small !important; + } + } + } + + .listing-item { + // Cards in a row are as tall as the tallest of them, and the actions have + // to sit on the bottom edge of each. That needs an unbroken chain of + // stretching boxes from the row down to `.card-inner` — an `align-self` on + // the actions alone does nothing while these are all still block boxes. + display: flex; + align-items: normal; + border-bottom: none; + margin: 0 !important; + + @media only screen and (min-width: $tablet-breakpoint) { + width: 50%; + padding-top: 10px; + padding-bottom: 10px !important; + + &:nth-child(2n) { + padding-left: 10px !important; + } + + &:nth-child(2n + 1) { + padding-right: 10px !important; + } + + &:last-child, + &:nth-last-child(2):not(:nth-child(2n)) { + padding-bottom: 0 !important; + } + + &:first-child, + &:nth-child(2) { + padding-top: 0 !important; + } + } + + &:last-child:nth-child(2n + 1) { + @media only screen and (min-width: $largest-mobile-screen) { + margin-left: 0 !important; + } + } + + .card { + display: flex; + flex-direction: column; + flex-grow: 1; + + // VLT gives the grid variation's cards the theme's secondary surface from + // `use-theme-colors()`; `.robots` is not `.grid`, so say it here. + background-color: var(--theme-high-contrast-color); + + .card-inner { + display: flex; + flex-direction: column; + flex-grow: 1; + padding-bottom: $spacing-medium !important; + + .image-wrapper img { + margin: 0; + } + + .card-summary { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: $spacing-medium $spacing-small 0 $spacing-small; + + .title { + margin: 0 0 $spacing-small 0 !important; + @include text-heading-h3(); + } + + // As in the reference design: the charge bar sits on the bottom edge + // of the summary, so the bars line up across a row however long the + // descriptions are. + .robot-charge { + padding-top: $spacing-small; + margin-top: auto; + } + } + } + + // The fleet's own addition: the Card.Actions slot, which none of VLT's + // variations fill. + .actions-wrapper { + // Raise the actions above the card's link overlay, which covers the + // whole card, so that the buttons receive clicks. VLT does the same + // for additional links inside a card. + position: relative; + z-index: 1; + // The last of the stretching boxes: `auto` here is what pins the + // actions to the bottom of a card that is taller than its content. + margin-top: auto; + padding: $spacing-medium $spacing-small 0 $spacing-small; + text-align: right; + + .book-unit-button { + padding: 8px 20px; + border: 1px solid var(--accent-color); + background: var(--accent-color); + color: var(--accent-foreground-color); + cursor: pointer; + transition: + background 0.2s ease, + color 0.2s ease, + border-color 0.2s ease; + @include body-text-bold(); + + // The inverse of the filled state, matching how the Button block + // inverts on hover. Both values are read from the theme's foreground, + // which is always a color — `--theme-color` may be a gradient. + &:hover, + &:focus { + border-color: var(--theme-foreground-color); + background: none; + color: var(--theme-foreground-color); + } + } + } + } + } +} +``` + +Import the partial in {file}`src/theme/_main.scss`, next to the other block partials: + +```scss +@import './blocks/listing'; +``` + +### Step 6: Test the Fleet Listing + +1. Add a few robots, such as **ARM-7 Bench Arm**, **ROVER-2 Terrain Scout**, **QUAD-4 Walker**, and **DRONE-1 Surveyor**, each with a description, a charge level in the kicker, and an image. +2. Add a Listing block to a page. +3. Select the **Robot Fleet** variation in the block settings. +4. Configure the block to list the Robot content type. +5. Each robot appears with its image, summary, charge level, and a Book button, and selecting a Book button does not open the robot's page. +6. Add a robot without an image. Its card shows the placeholder, and has the same shape as the others. + +(light-theme-slots-label)= + +## Working with Slots + +VLT provides slots for extending the layout without component shadowing. This section adds a practical example: a sign-up form for **Signal**, the Robotarium's workshop bulletin, in the `preFooter` slot. + +### Available Slots + +Volto renders four slots itself, and VLT's header and footer add the others. +Several of them already contain components when you start: + +| Slot | Where it renders | Rendered by | Registered by default | +| --- | --- | --- | --- | +| `aboveApp` | Around the whole app | Volto | `plone-components-css`, in the CMS UI only | +| `aboveContent` | Above the content | Volto | — | +| `belowContent` | Below the content | Volto | `tags`, `relatedItems` | +| `aboveListingItems` | Inside a Listing block, above the items | Volto and VLT | — | +| `aboveHeader` | Above the header | VLT | `Theming`, `StickyMenu` | +| `belowHeader` | Below the header | VLT | — | +| `headerTools` | Top right of the header, and in the mobile menu | VLT | `Anontools` | +| `preFooter` | Top of the footer | VLT | `footerLogos`, `MobileStickyMenu` | +| `footer` | Main footer area | VLT | `coreFooter`, only with the `kitconcept.footer` behavior | +| `postFooter` | Bottom of the footer | VLT | `PostFooterFollowUsLogoAndLinks`, `Colophon` | +| `followUs` | Social media links, inside `postFooter` | VLT | `FollowUs`, from `@plonegovbr/volto-social-media` | +| `footerLinks` | Footer links, inside `postFooter` | VLT | — | + +The `followUs` slot only renders when the site has social media links, and `footerLinks` only when it has footer links. + +### Step 1: Create the Signal Sign-up Component + +Create {file}`src/components/SignalSignup/SignalSignup.tsx`: + +```tsx +import React, { useState } from 'react'; + +const SignalSignup = () => { + const [email, setEmail] = useState(''); + + const handleSubmit = (e) => { + e.preventDefault(); + // In a real implementation, this would submit to a mailing service + console.log('Signal sign-up:', email); + alert(`Thanks, ${email}. You are on the Signal list.`); + setEmail(''); + }; + + return ( +
    +
    +

    Signal

    +

    + Workshop notes, new arrivals, and downtime warnings. One message a month. +

    +
    + setEmail(e.target.value)} + aria-label="Email address" + placeholder="you@example.com" + required + className="signal-input" + /> + +
    +
    +
    + ); +}; + +export default SignalSignup; +``` + +### Step 2: Add Styles + +Create {file}`src/theme/_signalSignup.scss`: + +```scss +// VLT pads every non-empty container in the footer, with +// `#footer > .container:not(:empty)`. The sign-up band brings its own padding +// and its own background, so that padding would show up as a gap in the +// footer's gradient above it. Both selectors have the same specificity, and +// this one wins because it loads later. +#footer > .pre-footer:has(.signal-signup) { + padding: 0; +} + +.signal-signup { + background-color: var(--secondary-color); + color: var(--secondary-foreground-color); + padding: 4rem 2rem; + + .signal-container { + max-width: var(--default-container-width); + margin: 0 auto; + text-align: center; + } + + .signal-title { + font-size: 2rem; + margin-bottom: 1rem; + } + + .signal-description { + font-size: 1.125rem; + margin-bottom: 2rem; + opacity: 0.9; + } + + .signal-form { + display: flex; + gap: 1rem; + max-width: 500px; + margin: 0 auto; + flex-wrap: wrap; + justify-content: center; + + .signal-input { + flex: 1; + min-width: 250px; + padding: 0.75rem 1rem; + border: 1px solid var(--secondary-foreground-color); + font-size: 1rem; + background-color: var(--primary-color); + color: var(--primary-foreground-color); + + &:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + } + } + + .signal-button { + padding: 0.75rem 2rem; + background-color: var(--accent-color); + color: var(--accent-foreground-color); + border: none; + font-size: 1rem; + cursor: pointer; + transition: opacity 0.2s; + + &:hover { + opacity: 0.9; + } + + &:focus { + outline: 2px solid var(--accent-foreground-color); + outline-offset: 2px; + } + } + } +} +``` + +Import the partial at the end of {file}`src/theme/_main.scss`: + +```scss +@import './signalSignup'; +``` + +### Step 3: Register the Component to the Slot + +In {file}`src/config/settings.ts`: + +```typescript +import SignalSignup from '../components/SignalSignup/SignalSignup'; + +export default function install(config: ConfigType) { + // ... previous config ... + + config.registerSlotComponent({ + name: 'SignalSignup', + slot: 'preFooter', + component: SignalSignup, + }); + + return config; +} +``` + +The Signal sign-up now appears at the top of the footer on every page, after any footer logos, which shows how slots let you extend the layout without shadowing core components. + +(light-theme-swap-components-label)= + +## Swapping Structural Components + +Slots let you *add* to the layout. Sooner or later you will want to *replace* part of it—a navigation that your design calls for, a footer that VLT's does not cover. + +The old answer was to shadow the component by dropping a file into {file}`customizations`. VLT 8 offers a better one: its structural components are resolved through the registry at render time, so a project can substitute its own by registering it and naming it in configuration. + +### How It Works + +VLT registers each of its structural components as a utility under the name `vlt`, with a `type` naming the role: + +```typescript +config.registerUtility({ name: 'vlt', type: 'navigation', method: Navigation }); +``` + +A setting then decides which registered name renders for each role: + +```typescript +config.settings.vlt = { + components: { + breadcrumbs: 'vlt', + footer: 'vlt', + header: 'vlt', + languageSelector: 'vlt', + logo: 'vlt', + mobileNavigation: 'vlt', + navigation: 'vlt', + searchWidget: 'vlt', + tags: 'vlt', + }, +}; +``` + +These are the defaults, so out of the box nothing changes. The nine roles above are the ones you can swap. + +### Making the Swap + +Swapping a role takes two steps, and the Robotarium does not need either of them yet—the mechanism is worth knowing before you reach for it. + +First, register your implementation under a name of your own, against the role's `type`, in {file}`src/config/settings.ts`: + +```typescript +config.registerUtility({ + name: 'robotarium', + type: 'navigation', + method: MyNavigation, +}); +``` + +Both implementations now sit in the registry, and nothing renders differently yet. The second step selects yours: + +```typescript +config.settings.vlt.components.navigation = 'robotarium'; +``` + +That is the whole change. VLT's header resolves the navigation through the registry when it renders, so it picks up your component. Every other role keeps its `vlt` default, and VLT's own navigation stays registered under `vlt`, so reverting is a one-line edit, not a file deletion. + +The same two steps swap any of the nine roles. + +### Why Prefer This Over Shadowing + +- **Explicit.** The active component is named in configuration, not implied by a file's path. +- **Composable.** Several implementations can coexist under different names, and you choose which one renders. +- **Decoupled.** You bind to a role name, not to an internal module path that can move between VLT releases. +- **Safe.** If a setting names a component that was never registered, VLT falls back to its own implementation rather than rendering nothing. +- **Typed.** The keys of `config.settings.vlt.components` are a fixed set, so a misspelled role is a compile error rather than a silent no-op. + +```{note} +Your add-on's configuration must run after VLT's, so that `config.settings.vlt` exists when you assign to it. +It does, because VLT is listed in your add-on's `addons`, and Volto applies the add-ons that an add-on declares before the add-on itself. +``` + +(light-theme-block-model-v3-label)= + +## Block Model v3 (opt-in) + +Block Model v3 gives every block the same two containers in view mode and in edit mode, so a block looks the same while you edit it as it does on the published page. +VLT 8 ships it as an opt-in. +The Robotarium keeps the default, Block Model v2, and this section explains what changes when you switch. + +(bm3-two-container-label)= + +### The Two-Container System + +Every block in Block Model v3 follows this structure: + +``` +┌────────────────────────────────────────────────────────────┐ +│ Main/Outer Container (.block.${type}.category-${category}) │ +│ • Full width (edge to edge) │ +│ • Background color & theme variables │ +│ • Vertical spacing via padding on BG color changes │ +│ │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ Secondary/Inner Container (.block-inner-container) │ │ +│ │ • Content width & horizontal centering │ │ +│ │ • Default vertical spacing between blocks │ │ +│ │ • Content alignment via CSS Grid │ │ +│ │ │ │ +│ │ [Block Content Here] │ │ +│ │ │ │ +│ └───────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────┘ +``` + +The outer container uses padding rather than margin for vertical spacing, so that its background has no gaps. +In edit mode, VLT renders the same two containers, and adds the editing controls after the inner container. + +### Enable Block Model v3 + +VLT sets `config.settings.blockModel = 2`, and copies that value to each block it has migrated: `slate`, `title`, `gridBlock`, and the Button block, `__button`. +Those blocks keep the value they had when VLT's configuration ran, which is before your add-on runs. +To switch, set the flag, and copy it to those blocks again. + +In your project's {file}`src/config/settings.ts`: + +```typescript +export default function install(config: ConfigType) { + // ... previous configuration ... + + // Enable Block Model v3 globally + config.settings.blockModel = 3; + + // Re-apply it to the blocks VLT has migrated. + // The Button block only exists if its add-on is installed. + for (const type of ['slate', 'title', 'gridBlock', '__button']) { + if (config.blocks.blocksConfig[type]) { + config.blocks.blocksConfig[type].blockModel = config.settings.blockModel; + } + } + + return config; +} +``` + +```{important} +A block that stays on v2 still renders, but without the two containers, so it can look different from its neighbors. +Enable v3 only when every block your site uses supports it. +Block repositories that support it show a "BMv3 ready" badge. +``` + +(light-theme-block-categories-label)= + +### Block Categories + +Under v3, VLT's stylesheet sets the width of a block's inner container, and the space before the next block, according to the block's category. +The category is set at `config.blocks.blocksConfig[type].category`, and it becomes a `category-${category}` class on the outer container. +Vertical spacing between blocks comes from the **upper** block: its content sits flush with the top of its container, and the bottom padding creates the space before the next block. + +VLT already assigns categories to the blocks it has migrated, so you do not need to repeat them: + +| Category | Assigned to | Width of the inner container | Spacing adjustments, by the category of the neighboring block | +| --- | --- | --- | --- | +| `inline` | `slate` | narrow | no space before `separator`, more space before `action` or `heading` | +| `title` | `title` | default | no top padding on a `cards` block that follows | +| `action` | `__button` | the width chosen for the block | no space before `separator`, more space before `cards` or `inline` | +| `cards` | `gridBlock` | default | more space before `action` or `inline` | + +VLT's stylesheet also has rules for a few categories that no block in VLT or in the recommended add-ons assigns yet, such as `heading` and `separator`. + +Your own blocks can have a category too. +Reuse one of the four only if both its width rule and its spacing fit the block. +The Cover block is a self-contained unit, like the cards in a Grid, but `cards` fixes the inner container at the default width. +Under v3, that rule is more specific than the Cover's own `max-width`, so the Block Width control would stop working. + +When none of the categories fits, add your own. +Name it for the family of blocks it describes rather than for the block that prompted it, and give it rules, because a category only means something if your stylesheets act on the `category-*` class it produces. +The Robotarium adds a `showcase` category, for full-width sections that bring their own background and padding, such as the Cover. + +Add these two lines to {file}`src/config/blocks.ts`, after the Cover registration from the previous chapter: + +```typescript +config.blocks.blocksConfig.cover.category = 'showcase'; +config.blocks.blocksConfig.cover.blockModel = config.settings.blockModel; +``` + +They are worth adding even with the flag left at `2`: the Cover then follows whatever `config.settings.blockModel` holds, and `BlockWrapper` in its view reads the same value. +Under v2, the category has no effect, because only v3 renders the `category-*` class. + +```{warning} +The file matters here, and so does the position within it. + +{file}`src/index.ts` calls `installSettings` before `installBlocks`, so at the time {file}`config/settings.ts` runs, `config.blocks.blocksConfig.cover` does not exist yet, and the lines above would throw an error there. +They have to run after the block is registered, and after the flag is set, which {file}`config/blocks.ts` satisfies on both counts. +``` + +Then give the category its rules. +Create {file}`src/theme/_categories.scss`: + +```scss +// Block Model v3 spacing for the `showcase` category: full-width sections +// with their own background and padding, such as the Cover block. +// VLT writes its category rules for `:not(.blocks-group-wrapper) > .block`, +// which matches the blocks that Block Model v3 renders, and these rules +// follow the same pattern. +:not(.blocks-group-wrapper) > .block.category-showcase { + // A showcase keeps its padding inside its own background, so the space + // before the next block goes on the outer container. + padding-bottom: $block-vertical-space; + + // Leave more space before text and buttons, and none before a separator. + &:has(+ .category-inline), + &:has(+ .category-action) { + padding-bottom: $spacing-xlarge; + } + + &:has(+ .category-separator) { + padding-bottom: 0; + } +} +``` + +Import the partial at the end of {file}`src/theme/_main.scss`: + +```scss +@import './categories'; +``` + +Unlike `cards`, `showcase` sets no width, so the Cover's own stylesheet still decides it, and the Block Width control keeps working. + +### The `volto-bm3-compat` add-on + +`@kitconcept/volto-bm3-compat` provides the `BlockWrapper` component that you used in the Cover view. +It lets one view work under both block models, rendering the wrappers that v2 needs and leaving them out under v3. +VLT depends on it and declares it as an add-on, and you also listed it in your project add-on in the first chapter, because your view imports from it. + +## Checkpoint + +- The Signal sign-up form appears at the top of the footer on every page. +- A Listing block set to the **Robot Fleet** variation shows each charge level as a labelled bar, the Book buttons line up along the bottom of the cards whatever the description lengths, selecting a Book button does not open the robot's page, a robot without a photo shows a placeholder, and tabbing through the page reaches every robot title as a link. +- The same robot in a Teaser block renders the same title, description, and charge bar as it does in the fleet listing. +- If—and only if—you enabled Block Model v3, a Text block renders inside a `.block-inner-container` and looks the same in edit mode as in view mode, and the Cover renders with the `category-showcase` class and still follows its Block Width control. With the flag left at `2`, as the Robotarium leaves it, blocks render as before. + +## Further Reading + +- [Swap structural components](https://volto-light-theme.readthedocs.io/how-to-guides/swap-structural-components.html) +- [Slots reference](https://volto-light-theme.readthedocs.io/reference/slots.html) +- [Card primitive reference](https://volto-light-theme.readthedocs.io/reference/card.html) +- [Summary components](https://volto-light-theme.readthedocs.io/how-to-guides/summary.html) +- [Block Model v3](https://volto-light-theme.readthedocs.io/conceptual-guides/block-model-v3.html) diff --git a/docs/customizing-volto-light-theme/block-development-widgets.md b/docs/customizing-volto-light-theme/block-development-widgets.md new file mode 100644 index 000000000..f5e199dfd --- /dev/null +++ b/docs/customizing-volto-light-theme/block-development-widgets.md @@ -0,0 +1,915 @@ +--- +myst: + html_meta: + "description": "Block Development, Widgets & Integration" + "property=og:description": "Block Development, Widgets & Integration" + "property=og:title": "Block Development, Widgets & Integration" + "keywords": "Plone, Volto, Training, Volto Light Theme" +--- + +# Block Development, Widgets & Integration + +## Widgets for Block Styling + +A block's styling options are ordinary schema fields, rendered by a handful of widgets. +Some of these widgets come with Volto, and some with VLT. +This section introduces the ones you will meet in this chapter and in VLT's own blocks. +The [VLT widgets reference](https://volto-light-theme.readthedocs.io/reference/widgets.html) covers them in more detail. + +| Widget | Provided by | Stores | Used for | +| --- | --- | --- | --- | +| `blockWidth` | Volto | a width token, such as `default` | the width of a block, resolved through `config.blocks.widths` | +| `blockAlignment` | Volto | an alignment token, such as `center` | the alignment of a block's content, resolved through `config.blocks.alignments` | +| `size` | Volto | a size token: `s`, `m`, or `l` | the size of media, resolved through `config.blocks.sizes` | +| `colorSwatch`, also registered as `color_picker` | VLT | the name of a palette entry | block themes and other curated palettes | +| `colorPicker` | VLT | a hex color | a free choice of color, as in the theme behavior | +| `object_list` | VLT, replacing Volto's widget of the same name | a list of objects | repeating items, such as header actions and footer links | +| `softTextWidget` and `softTextareaWidget` | VLT | text | text with a recommended maximum length | + +### Width, Alignment, and Size + +`blockWidth` and `blockAlignment` let editors pick one of a few buttons: + +```javascript +{ + widget: 'blockWidth', + title: 'Block Width', + default: 'default', + actions: config.blocks.widths.map((width) => width.name), +} +``` + +```javascript +{ + widget: 'blockAlignment', + title: 'Alignment', + default: 'center', + actions: config.blocks.alignments.map((alignment) => alignment.name), +} +``` + +Always pass the token names as `actions`, as shown above. +Without `actions`, both widgets use a default list whose entries carry their own style objects, and the widget then stores the style object instead of the token. +{ref}`Step 6 of the Cover block ` explains the consequences. + +VLT resolves the tokens only for fields named `blockWidth:noprefix` and `align:noprefix`, as {ref}`the previous chapter ` explains. + +`size` selects a size from a default list of three: + +```javascript +{ + widget: 'size', + title: 'Size', + default: 'm', +} +``` + +The stored values are the tokens `s`, `m`, and `l`. +The names Small, Medium, and Large are only their labels, so `default` must be one of the tokens. +The default entries of `size` carry no styles, so it stores the tokens even without `actions`. +When the field is named `size:noprefix`, VLT maps each token to the `--media-size` custom property through `config.blocks.sizes`: + +```typescript +config.blocks.sizes = [ + { style: { '--media-size': 'var(--size-small)' }, name: 's', label: 'Small' }, + { style: { '--media-size': 'var(--size-medium)' }, name: 'm', label: 'Medium' }, + { style: { '--media-size': 'var(--size-large)' }, name: 'l', label: 'Large' }, +]; +``` + +All three widgets are built on Volto's `buttons` widget, which you can also use for your own single-choice fields. +It accepts a list of `actions`, an `actionsInfoMap` with the icon and the label of each action, and `filterActions` to show only some of the actions. + +### Palettes: `colorSwatch` + +`colorSwatch` lets editors pick from a curated palette instead of entering free-form values. +Each entry follows the `StyleDefinition` type from `@plone/types`. +Always provide a `default` entry, so that the field has a predictable fallback. + +The following schema enhancer adds such a field to the `styles` object of a block. +Like the Cover block's enhancer later in this chapter, it only works when it runs after VLT's `defaultStylingSchema`: + +```javascript +const colors = [ + { + name: 'default', + label: 'Default', + style: { + '--theme-color': '#fff', + '--theme-foreground-color': '#000', + }, + }, + { + name: 'grey', + label: 'Grey', + style: { + '--theme-color': '#ecebeb', + '--theme-foreground-color': '#000', + }, + }, +]; + +const myColorSchemaEnhancer = ({ schema }) => { + schema.properties.styles.schema.fieldsets[0].fields.push('myColorField'); + schema.properties.styles.schema.properties.myColorField = { + widget: 'colorSwatch', + title: 'My color', + default: 'default', + colors, + }; + return schema; +}; +``` + +The widget stores the `name` of the chosen entry, and the StyleWrapper turns it into a class on the block, such as `has--myColorField--grey`, which you can target in your stylesheets. +To also inject the entry's custom properties as inline styles, register a `styleFieldDefinition` utility under the field name: + +```javascript +config.registerUtility({ + name: 'myColorField', + type: 'styleFieldDefinition', + method: () => colors, +}); +``` + +VLT only looks up these utilities for the fields inside a block's `styles` object, which is why the example adds the field there. + +```{note} +This is the recommended way to use this widget, because the same `colors` list feeds both the widget and the styles, which keeps a single source of truth for the palette. +``` + +VLT registers the same component as `color_picker`. +That is the widget `defaultStylingSchema` uses for the **Background color** field, with the palettes passed in a `themes` prop instead of `colors`. +VLT also registers `themeColorSwatch`, a variant that always shows the palettes in `config.blocks.themes`. + +### Free Colors: `colorPicker` + +`colorPicker` is a color picker with a visual chooser and a hex input. +The theme behavior uses it for its five color fields. + +For fields listed in `config.settings.colorMap`, `colorPicker` also renders `ColorContrastChecker`. +This component warns the editor when the contrast between the field and its paired color is below 4.5:1, following the WCAG guidelines. +The pairs and their default values are defined like this: + +```javascript +config.settings.colorMap = { + primary_color: { + colorPair: 'primary_foreground_color', + default: '#ffffff', + }, + primary_foreground_color: { + colorPair: 'primary_color', + default: '#000000', + }, + // ... and the same for the secondary and accent pairs +}; +``` + +You can reuse the checker in your own color widget. +Render it after the field, and only for fields listed in `colorMap`, as VLT's `colorPicker` does, because it throws an error for any other field. +It compares hex colors with the paired field of the content being edited, so it is meant for content fields such as the theme behavior's, not for block settings. + +```jsx +import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper'; +import config from '@plone/volto/registry'; +import ColorContrastChecker from '@kitconcept/volto-light-theme/components/Widgets/ColorContrastChecker'; + +const MyColorWidget = (props) => { + const { id, value, onChange } = props; + + return ( + <> + + onChange(id, event.target.value)} + /> + + {config.settings.colorMap[id] && } + + ); +}; + +export default MyColorWidget; +``` + +### Lists: `object_list` + +VLT replaces Volto's `object_list` widget with one that supports reordering by drag and drop. +It stores a list of objects, each with a generated `@id`. +The shape of each object comes from a `schema` prop, or from `schemaName`, the name of a schema registered as a utility. +The schema can be a schema object, or a function that returns one: + +```javascript +config.registerUtility({ + name: 'mySchemaName', + type: 'schema', + method: mySchema, +}); +``` + +```javascript +{ + widget: 'object_list', + title: 'Items', + schemaName: 'mySchemaName', +} +``` + +VLT's header actions, footer links, and footer logos fields work this way, with the schemas `headerActions`, `footerLinks`, and `footerLogos`. + +### Text With a Soft Limit + +`softTextWidget` and `softTextareaWidget` behave like the `text` and `textarea` widgets, but they display a character count while the editor types. +When the count exceeds the limit set in `softMaxLength`, a warning appears, but the editor can still save the content. + +These widgets are configured from the backend, with `directives.widget` and its `frontendOptions`: + +```python +directives.widget( + "seo_title", + frontendOptions={ + "widget": "softTextWidget", + "widgetProps": {"softMaxLength": "55"}, + }, +) +seo_title = schema.TextLine( + title="SEO Title", + description="Override the meta title. Use maximum 55 characters.", + required=False, +) +``` + +(cover-block-label)= + +## Creating a Custom Cover Block + +This section builds a cover block step by step, starting with a basic implementation and then adding VLT's styling fields to it. + +### Step 1: Create Basic Block Schema + +Create {file}`src/components/blocks/Cover/schema.ts`: + +```typescript +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + cover: { + id: 'Cover', + defaultMessage: 'Cover', + }, + title: { + id: 'Title', + defaultMessage: 'Title', + }, + subtitle: { + id: 'Subtitle', + defaultMessage: 'Subtitle', + }, + backgroundImage: { + id: 'Background Image', + defaultMessage: 'Background Image', + }, +}); + +const coverBlockSchema = (props) => { + const { intl } = props; + + return { + title: intl.formatMessage(messages.cover), + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['title', 'subtitle'], + }, + { + id: 'design', + title: 'Design', + fields: ['backgroundImage'], + }, + ], + properties: { + title: { + title: intl.formatMessage(messages.title), + type: 'string', + }, + subtitle: { + title: intl.formatMessage(messages.subtitle), + type: 'string', + }, + backgroundImage: { + title: intl.formatMessage(messages.backgroundImage), + widget: 'object_browser', + mode: 'image', + allowExternals: false, + }, + }, + required: [], + }; +}; + +export { coverBlockSchema }; +``` + +### Step 2: Create View Component + +Before writing any markup, note which wrappers the view is responsible for. +VLT supports two block models, and they wrap a block differently: + +- Under **Block Model v2**, the default, the block view renders its own outer element, `div.block.cover`. The renderer passes it the block's classes and inline styles. +- Under **Block Model v3**, VLT's renderer adds two wrappers itself, and renders the view inside them: + +```html +
    +
    + ...your markup starts here... +``` + +The outer element carries the block's identity and its theme; the inner container carries the width and the alignment. +This split is what lets a block have a background that behaves independently of its content width, and it is described in full in {ref}`the two-container system `. + +A view that always renders its own `div.block` would be wrapped twice under v3. +`BlockWrapper` from `@kitconcept/volto-bm3-compat` avoids that. +It renders the outer element, and an optional inner element that you pass as `ExtraWrapper`, only when the block uses v2. +With it, the view produces the same structure under both models. + +Create {file}`src/components/blocks/Cover/View.tsx`: + +```tsx +import config from '@plone/volto/registry'; +import { BlockWrapper } from '@kitconcept/volto-bm3-compat'; +import type { BlockViewProps } from '@plone/types'; +import type { ReactNode } from 'react'; + +// Under Block Model v2, BlockWrapper renders `.block.cover` and then this +// inner container. Under v3 it renders neither, because VLT's renderer adds +// both. Either way the result is `.block.cover > .block-inner-container > ...`. +const InnerContainer = (props: { children: ReactNode }) => ( +
    {props.children}
    +); + +const CoverView = (props: BlockViewProps) => { + const { title, subtitle, backgroundImage } = props?.data || {}; + const imageItem = backgroundImage?.[0]; + const Image = config.getComponent('Image').component; + + return ( + + {imageItem?.['@id'] && ( +
    + +
    + )} + +
    + {title &&

    {title}

    } + {subtitle &&

    {subtitle}

    } +
    +
    + ); +}; + +export default CoverView; +``` + +The view renders the image with Volto's `Image` component, which picks a suitable image scale for the space available. + +### Step 3: Create Edit Component + +Create {file}`src/components/blocks/Cover/Edit.tsx`: + +```tsx +import { useIntl } from 'react-intl'; +import SidebarPortal from '@plone/volto/components/manage/Sidebar/SidebarPortal'; +import { BlockDataForm } from '@plone/volto/components/manage/Form'; +import { coverBlockSchema } from './schema'; +import CoverView from './View'; +import type { BlockEditProps } from '@plone/types'; + +const CoverEdit = (props: BlockEditProps) => { + const { selected, onChangeBlock, block, data } = props; + const intl = useIntl(); + + return ( + <> + + + { + onChangeBlock(block, { + ...data, + [id]: value, + }); + }} + /> + + + ); +}; + +export default CoverEdit; +``` + +`BlockDataForm` applies the block's `schemaEnhancer` from its configuration to the schema you pass, which is what makes the styling fields from Step 6 appear in the sidebar. + +### Step 4: Register the Basic Block + +Edit {file}`src/config/blocks.ts`. Do not replace the file: it already holds the block themes from the previous chapter. +Add the imports at the top of the file, and the registration inside the existing `install` function: + +```typescript +import type { ConfigType } from '@plone/registry'; +import CoverView from '../components/blocks/Cover/View'; +import CoverEdit from '../components/blocks/Cover/Edit'; +import { coverBlockSchema } from '../components/blocks/Cover/schema'; +import coverSVG from '@plone/volto/icons/hero.svg'; + +export default function install(config: ConfigType) { + // ... block themes configuration ... + + // Register Cover Block + config.blocks.blocksConfig.cover = { + id: 'cover', + title: 'Cover', + icon: coverSVG, + group: 'common', + view: CoverView, + edit: CoverEdit, + restricted: false, + mostUsed: true, + blockSchema: coverBlockSchema, + sidebarTab: 1, + }; + + return config; +} +``` + +```{note} +Give project blocks their own key, and follow the convention every other block in the registry uses: a lowercase single word such as `teaser`, `banner`, or `slider`, reserving camelCase for genuinely multi-word names like `gridBlock`. + +The icon is a separate matter. Icon assets are not tied to block names, and {file}`hero.svg` is the one that depicts this layout, so this example uses it. +``` + +### Step 5: Add Basic Block Styles + +The block has no styling fields yet, so this step sets up **structure only**. +Everything that depends on a widget—the width, the alignment, the theme color—arrives in Step 8, once the fields that produce those properties exist. + +Create {file}`src/theme/blocks/_cover.scss`: + +```scss +.block.cover { + .block-inner-container { + position: relative; + display: flex; + overflow: hidden; + min-height: 60vh; + max-width: var(--default-container-width); + align-items: center; + padding: 4rem 2rem; + margin-inline: auto; + } + + .cover-text { + // Above the background image. + position: relative; + z-index: 1; + display: flex; + width: 100%; + flex-direction: column; + gap: 1rem; + + .cover-title { + margin-bottom: $spacing-small; + font-size: 5rem; + line-height: 1.1; + } + + .cover-subtitle { + margin-bottom: $spacing-small; + font-size: 2rem; + line-height: 1.3; + opacity: 0.9; + } + } + + &:has(.cover-image-wrapper) { + color: #fff; + } + + .cover-image-wrapper { + position: absolute; + z-index: 0; + inset: 0; + + img { + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0.7; + } + + &::after { + position: absolute; + background: rgb(0 0 0 / 40%); + content: ''; + inset: 0; + } + } +} +``` + +Two decisions are worth calling out. + +**The width goes on `.block-inner-container`, not on `.block`.** +That is the two-container split: the outer element is the block's full extent, the inner one is where content is constrained. Putting `max-width` on the outer element instead fights VLT, which already constrains inner containers, and it makes a full-width block impossible. + +**`min-height` is load-bearing.** +The image sits in an absolutely positioned wrapper and contributes no height. +Without `min-height`, the cover is only as tall as its text, and the image is cropped to that strip. + +Import the partial in {file}`src/theme/_main.scss`, next to the other block partials: + +```scss +@import './blocks/cover'; +``` + +(light-theme-cover-actions-label)= + +### Step 6: Add Width and Alignment Fields + +Now add width and alignment fields to the block, with a schema enhancer next to the block schema. VLT resolves both fields, as described in {ref}`the previous chapter `. + +:::{important} +The enhancer below **extends** the styling fieldset; it does not create one. +`schema.properties.styles` only exists once VLT's `defaultStylingSchema` has run, so this enhancer only works when it is composed after it, which is what Step 7 does: + +```typescript +schemaEnhancer: composeSchema(defaultStylingSchema, coverSchemaEnhancer), +``` + +Registered on its own, it throws an error, because `schema.properties.styles` is `undefined`. +This is also why the block gets its **Background color** control for free: that field comes from `defaultStylingSchema`, not from anything you write here. +::: + +Make three changes to {file}`src/components/blocks/Cover/schema.ts`. + +First, import the registry at the top of the file: + +```typescript +import config from '@plone/volto/registry'; +``` + +Second, add two messages to the `messages` object: + +```typescript + blockWidth: { + id: 'Block Width', + defaultMessage: 'Block Width', + }, + textAlignment: { + id: 'Text Alignment', + defaultMessage: 'Text Alignment', + }, +``` + +Third, add the enhancer after `coverBlockSchema`, and export it as well, by replacing the `export` line at the end of the file: + +```typescript +// Schema enhancer that adds the width and alignment styling fields. +// Only meaningful when composed *after* VLT's `defaultStylingSchema`, which is +// what creates `schema.properties.styles`. +const coverSchemaEnhancer = ({ schema, intl }) => { + // Add custom fields to the styling schema at the beginning + schema.properties.styles.schema.fieldsets[0].fields = [ + 'align:noprefix', + 'blockWidth:noprefix', + ...schema.properties.styles.schema.fieldsets[0].fields, + ]; + + schema.properties.styles.schema.properties['align:noprefix'] = { + widget: 'blockAlignment', + title: intl.formatMessage(messages.textAlignment), + default: 'center', + actions: config.blocks.alignments.map((alignment) => alignment.name), + }; + + schema.properties.styles.schema.properties['blockWidth:noprefix'] = { + widget: 'blockWidth', + title: intl.formatMessage(messages.blockWidth), + default: 'default', + actions: config.blocks.widths.map((width) => width.name), + }; + + return schema; +}; + +export { coverBlockSchema, coverSchemaEnhancer }; +``` + +::::{dropdown} The complete schema.ts after this step +```typescript +import { defineMessages } from 'react-intl'; +import config from '@plone/volto/registry'; + +const messages = defineMessages({ + cover: { + id: 'Cover', + defaultMessage: 'Cover', + }, + title: { + id: 'Title', + defaultMessage: 'Title', + }, + subtitle: { + id: 'Subtitle', + defaultMessage: 'Subtitle', + }, + backgroundImage: { + id: 'Background Image', + defaultMessage: 'Background Image', + }, + blockWidth: { + id: 'Block Width', + defaultMessage: 'Block Width', + }, + textAlignment: { + id: 'Text Alignment', + defaultMessage: 'Text Alignment', + }, +}); + +const coverBlockSchema = (props) => { + const { intl } = props; + + return { + title: intl.formatMessage(messages.cover), + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['title', 'subtitle'], + }, + { + id: 'design', + title: 'Design', + fields: ['backgroundImage'], + }, + ], + properties: { + title: { + title: intl.formatMessage(messages.title), + type: 'string', + }, + subtitle: { + title: intl.formatMessage(messages.subtitle), + type: 'string', + }, + backgroundImage: { + title: intl.formatMessage(messages.backgroundImage), + widget: 'object_browser', + mode: 'image', + allowExternals: false, + }, + }, + required: [], + }; +}; + +// Schema enhancer that adds the width and alignment styling fields. +// Only meaningful when composed *after* VLT's `defaultStylingSchema`, which is +// what creates `schema.properties.styles`. +const coverSchemaEnhancer = ({ schema, intl }) => { + // Add custom fields to the styling schema at the beginning + schema.properties.styles.schema.fieldsets[0].fields = [ + 'align:noprefix', + 'blockWidth:noprefix', + ...schema.properties.styles.schema.fieldsets[0].fields, + ]; + + schema.properties.styles.schema.properties['align:noprefix'] = { + widget: 'blockAlignment', + title: intl.formatMessage(messages.textAlignment), + default: 'center', + actions: config.blocks.alignments.map((alignment) => alignment.name), + }; + + schema.properties.styles.schema.properties['blockWidth:noprefix'] = { + widget: 'blockWidth', + title: intl.formatMessage(messages.blockWidth), + default: 'default', + actions: config.blocks.widths.map((width) => width.name), + }; + + return schema; +}; + +export { coverBlockSchema, coverSchemaEnhancer }; +``` +:::: + +Two details in that enhancer decide whether it works at all. + +**The field names must end in `:noprefix`.** +As covered in {ref}`the previous chapter `, `align:noprefix` and `align` are different field names. +VLT registers its style definitions under the literal names `align:noprefix` and `blockWidth:noprefix`, so a field called `align` matches none of them. +The `--block-alignment` property is then never injected, and the styles in Step 8 that read it have no effect. +The mistake is easy to miss: the buttons still appear in the sidebar, but the text does not move. + +**Pass the token names as `actions`.** +Without `actions`, the widgets fall back to Volto's default list, and each default entry carries its own style object, such as `{ '--block-width': 'unset' }` for Full. +When the chosen entry has a style object, the widget stores that object instead of the token. +The block would then save a style object rather than `"full"`, skip VLT's definitions in `config.blocks.widths`, and get the class `has--block-width--[object Object]`. +Mapping `config.blocks.alignments` and `config.blocks.widths` to their names makes the widgets store tokens, and keeps the block in step with the theme, including any width or alignment your project adds. + +### Step 7: Update Block Registration with Schema Enhancer + +Back in {file}`src/config/blocks.ts`, add the two new imports and the `schemaEnhancer` key to the registration you wrote in Step 4: + +```typescript +import type { ConfigType } from '@plone/registry'; +import CoverView from '../components/blocks/Cover/View'; +import CoverEdit from '../components/blocks/Cover/Edit'; +import { + coverBlockSchema, + coverSchemaEnhancer, +} from '../components/blocks/Cover/schema'; +import { composeSchema } from '@plone/volto/helpers/Extensions'; +import { defaultStylingSchema } from '@kitconcept/volto-light-theme/components/Blocks/schema'; +import coverSVG from '@plone/volto/icons/hero.svg'; + +export default function install(config: ConfigType) { + // ... block themes configuration ... + + // Register Cover Block + config.blocks.blocksConfig.cover = { + id: 'cover', + title: 'Cover', + icon: coverSVG, + group: 'common', + view: CoverView, + edit: CoverEdit, + restricted: false, + mostUsed: true, + blockSchema: coverBlockSchema, + schemaEnhancer: composeSchema(defaultStylingSchema, coverSchemaEnhancer), + sidebarTab: 1, + }; + + return config; +} +``` + +`composeSchema` runs its arguments in order, so `defaultStylingSchema` creates the styling fieldset and `coverSchemaEnhancer` then extends it. Reverse them and the second enhancer runs against a schema that has no `styles` property yet. + +(light-theme-third-party-styling-label)= + +:::{tip} +The same technique adds VLT's theming to a block **you did not write**—one from a third-party add-on, or a core Volto block. +The only difference is that such a block may already have a `schemaEnhancer` of its own, which you must preserve. +`composeSchema` skips an enhancer that is `undefined`, so this works whether or not the block has one: + +```typescript +config.blocks.blocksConfig. = { + ...config.blocks.blocksConfig., + schemaEnhancer: composeSchema( + config.blocks.blocksConfig..schemaEnhancer, + defaultStylingSchema, + ), +}; +``` +::: + +### Step 8: Update Styles to Use Widget Values + +The schema enhancers from Steps 6 and 7 now provide the custom properties that the styles can read: `--block-width` and `--block-alignment` from the new fields, and the theme properties, such as `--theme-color`, from the **Background color** control. + +Replace the whole of {file}`src/theme/blocks/_cover.scss` with the following version. +Compared with Step 5, it adds the five numbered changes: + +```scss +// 1. Lift VLT's layout-width cap for this block. See the explanation below. +#page-document .blocks-group-wrapper > .block.cover { + max-width: 100%; +} + +.block.cover { + // 2. The text color of the selected theme. + color: var(--theme-foreground-color); + + .block-inner-container { + position: relative; + display: flex; + overflow: hidden; + min-height: 60vh; + + // 3. The width chosen in Block Width, with the site default as a + // fallback, so the block stays sane if the field is ever removed. + max-width: var(--block-width, var(--default-container-width)); + align-items: center; + padding: 4rem 2rem; + margin-inline: auto; + + // 4. The background of the selected theme. Use `background`, not + // `background-color`, so that gradient themes work as well. + background: var(--theme-color); + } + + .cover-text { + // Above the background image. + position: relative; + z-index: 1; + display: flex; + width: 100%; + flex-direction: column; + + // 5. The alignment chosen in Text Alignment. + align-items: var(--block-alignment, start); + gap: 1rem; + text-align: var(--block-alignment, start); + + .cover-title { + margin-bottom: $spacing-small; + font-size: 5rem; + line-height: 1.1; + } + + .cover-subtitle { + margin-bottom: $spacing-small; + font-size: 2rem; + line-height: 1.3; + opacity: 0.9; + } + } + + &:has(.cover-image-wrapper) { + color: #fff; + } + + .cover-image-wrapper { + position: absolute; + z-index: 0; + inset: 0; + + img { + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0.7; + } + + &::after { + position: absolute; + background: rgb(0 0 0 / 40%); + content: ''; + inset: 0; + } + } +} +``` + +The first rule is needed because VLT limits every block inside a block group to the layout width, with the selector `#page-document .blocks-group-wrapper > *`. +Lifting that limit for the Cover lets a Full Width cover reach the edges of the page, and the inner container's `max-width` then decides the actual width. + +The image and the text both sit inside `.block-inner-container`, so the Block Width control resizes them together. +The theme color behind the Cover still spans the whole width of the page, because VLT groups consecutive blocks that share a theme and paints the theme on the group. +Change 4 repeats the theme background inside the Cover, where it shows through the partly transparent image. +For why gradient themes need `background`, see {ref}`the warning about gradient palettes `. + +## Checkpoint + +Restart the frontend, then add a Cover block to the Robotarium landing page. Give it the title **Book a robot. Build something.**, a subtitle such as *Twelve units, one afternoon at a time*, and a workshop photo as the background image. Confirm that: + +- The block appears in the block chooser, listed as **Cover**. +- The **Styling** section of the block settings shows the **Text Alignment**, **Block Width**, and **Background color** controls. +- Changing the alignment moves the title and subtitle, and changing the width resizes the image and the text. If the controls appear but nothing moves, check that the field names end in `:noprefix`. +- On the published page, setting the width to **Full Width** takes the image from edge to edge. + +```{seealso} +A block can also offer more than one rendering of the same data, as the Listing block does with its variations. +That is covered in the next chapter, where the Robotarium gets a **Robot Fleet** listing variation with its own card layout and per-item actions. +``` + +## Further Reading + +- [Widgets reference](https://volto-light-theme.readthedocs.io/reference/widgets.html) +- [Develop add-ons for VLT](https://volto-light-theme.readthedocs.io/how-to-guides/develop-add-ons.html) diff --git a/docs/customizing-volto-light-theme/concepts.md b/docs/customizing-volto-light-theme/concepts.md index 91eba4414..ef8675e61 100644 --- a/docs/customizing-volto-light-theme/concepts.md +++ b/docs/customizing-volto-light-theme/concepts.md @@ -1,35 +1,47 @@ --- myst: html_meta: - "description": "Concepts" - "property=og:description": "Concepts" - "property=og:title": "Concepts" + "description": "Foundation, Concepts & Project Setup" + "property=og:description": "Foundation, Concepts & Project Setup" + "property=og:title": "Foundation, Concepts & Project Setup" "keywords": "Plone, Volto, Training, Volto Light Theme" --- -# Volto Light Theme Concepts +# Foundation, Concepts & Project Setup -Volto Light Theme (VLT) is a customizable theme built for the Volto frontend of the Plone CMS. It provides a foundation that aims to solve many common design challenges, while remaining flexible enough for customization. It's particularly valuable because it is based on real-world experience, while simultaneously embodying the Volto vision for the future. This module will help you understand the core concepts in VLT and create a mental map of its parts. +## The Project -## Base Styling +Rather than theme an abstract site, this training builds one: **the Robotarium**, a neighborhood workshop that lends robots the way a library lends books. Members browse the fleet, check what is charged and available, and book a unit for the afternoon. -VLT is designed with simplicity and a minimal aesthetic in mind. Consistency, accessibility, and intuitiveness are what drive the development and improvements for VLT. +Every VLT feature the training covers earns its place by solving something the Robotarium actually needs: -## Customizable Variables +| The site needs | You will learn | Covered in | +| --- | --- | --- | +| A look that is its own, not the default | Design tokens, the color system, and block themes | {doc}`design-system-implementation` | +| A full-width opening on the landing page | Building a custom block with VLT's widgets | {doc}`block-development-widgets` | +| A fleet listing showing charge levels | Summary components and listing variations | {doc}`advanced-components-bm3` | +| A booking button on each robot | The Card primitive and its Actions slot | {doc}`advanced-components-bm3` | +| A newsletter sign-up at the top of the footer | Slots | {doc}`advanced-components-bm3` | -VLT offers a set of CSS custom properties (variables) that allow developers to customize various design elements, such as: +The project is called `robotarium`, and Cookieplone names its frontend add-on `volto-robotarium`. +If you would rather build something else, every step works the same with your own names substituted. -- Colors -- Spatial relationships -- Layouts +## Volto Light Theme Core Concepts -These variables can be easily overridden in your project to match the desired visual identity. +Volto Light Theme (VLT) is a customizable theme for the Volto frontend of Plone. +It provides a foundation that solves many common design challenges, while remaining flexible enough for customization. +It is based on real-world projects, and it follows the direction that Volto is taking. -## Colors +Most of VLT's design decisions are CSS custom properties that your project can override: the color pairs, the container widths, and the palettes that editors apply to blocks. +This section introduces each of them, so that you have a mental map of VLT's parts before you start building. -The color system is designed so that colors work in couples: a "background color" and a "foreground color". The "foreground color" is often called "text color" in other systems, but since we want to use this value for more than text—like icons or borders, this works better as a generic term. Colors that do not specify "foreground" in the name are meant to be background colors. +### Color System -The main color properties for a project using VLT are the following: +The color system is designed so that colors work in pairs: a background color and a foreground color. +Other systems often call the foreground color the text color, but VLT uses it for more than text, such as icons and borders, so it uses the more generic term. +Colors that do not have "foreground" in their name are background colors. + +The main color properties for a project using VLT are: ```scss --primary-color: #fff; @@ -42,20 +54,42 @@ The main color properties for a project using VLT are the following: --accent-foreground-color: #000; ``` -### Semantic color properties +These six properties are not declared as plain custom properties. +VLT registers them with the CSS [`@property`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@property) at-rule, which declares their type and their default value in one place: + +```scss +@property --primary-color { + inherits: true; + initial-value: #fff; + syntax: ''; +} +``` + +Registering them has three consequences that matter for a theme: -As an additional layer on top of the main color properties, we have set in place some semantic custom properties for the basic layout sections. As a default they use the values from the main color variables, but they can be detached if desired by setting new color values. However, we feel that leaving these color relationships as they are helps create a cohesive final design. The semantic layer of properties includes the following: +- The `initial-value` is the default, so the property always has a valid color, even when nothing sets it. For the same reason, a fallback such as `var(--accent-color, teal)` is never used. +- The `` syntax makes the value typed, so an invalid value is rejected instead of silently cascading. +- The browser can animate a typed property, so a transition from one color to another is smooth instead of jumping. + +VLT registers `--background` the same way, with white as its default, and uses it as the default value of `--primary-color`. + +You override these properties the same way as any other custom property, by assigning to them in `:root`. + +### Semantic Color Properties + +On top of the main color properties, VLT declares semantic properties for the main layout sections. +By default they take their values from the main color properties. +You can set them to other colors, but keeping these relationships helps create a cohesive design: ```scss // Header --header-background: var(--primary-color); ---header-foreground: var(--primary-foreground-color); -//Footer +// Footer --footer-background: var(--secondary-color); --footer-foreground: var(--secondary-foreground-color); -// Fat Menu +// Fat menu --fatmenu-background: var(--accent-color); --fatmenu-foreground: var(--accent-foreground-color); @@ -68,35 +102,109 @@ As an additional layer on top of the main color properties, we have set in place --search-foreground: var(--accent-foreground-color); // Link color ---link-foreground-color: var(--link-color); +--link-color: #0070a2; ``` -## Block width - -VLT now uses a standard block width definition as well. Currently, this is done via a `widget` component, `BlockWidthWidget`, that will be ported to Volto core in the near future. This component stores the value of the custom property `--block-width` so that it can be used by the StyleWrapper when injecting styles into the markup. +VLT does not declare `--header-foreground` by default. +The navigation items fall back to `--primary-foreground-color`, and the {ref}`theme behavior ` sets `--header-foreground` when an editor picks a navigation text color. -The three-width layout system considers the following variables: +Links are the one exception to the pattern above. +VLT declares `--link-color`, but does not use it by default. +Instead, links use `--link-foreground-color` when it is set, and otherwise the foreground color of the block theme they sit in, `--theme-foreground-color`. +To give links a distinct color everywhere, set `--link-foreground-color` in your project: ```scss ---layout-container-width: #{$layout-container-width}; // for major elements like headers, & large Blocks like the `volto-slider-block`. ---default-container-width: #{$default-container-width}; // a balanced content presentation for most Blocks. ---narrow-container-width: #{$narrow-container-width}; // optimal readability. +:root { + // Turn links into a different color than the theme foreground color + --link-foreground-color: var(--link-color); +} ``` -The default values map the container width SCSS variables, which have existed in the VLT ecosystem since versions < 6.0.0-alpha.0: +### Block Themes + +VLT includes a block theme system that lets individual blocks use their own color palette. +The themes are configured in `config.blocks.themes`, and applied through the StyleWrapper when a block renders. + +**The four core block theme variables:** + +- `--theme-color`: primary background color, the most visible color in the block +- `--theme-high-contrast-color`: secondary background color for nested elements, such as cards within a block, to separate them from the main background +- `--theme-foreground-color`: default text and icon color +- `--theme-low-contrast-foreground-color`: subdued text color for secondary content, such as placeholders or helper text + +A theme can also set other CSS properties, but these four variables are the color foundation of each theme. + +**Example configuration:** + +```typescript +config.blocks.themes = [ + { + style: { + "--theme-color": "#fff", + "--theme-high-contrast-color": "#ecebeb", + "--theme-foreground-color": "#000", + "--theme-low-contrast-foreground-color": "#555555", + }, + name: "default", + label: "Default", + }, + { + style: { + "--theme-color": "#ecebeb", + "--theme-high-contrast-color": "#fff", + "--theme-foreground-color": "#000", + "--theme-low-contrast-foreground-color": "#555555", + }, + name: "grey", + label: "Grey", + }, +]; +``` + +Editors select a block theme from the **Background color** control, in the **Styling** section of the block settings in the sidebar. +VLT's `defaultStylingSchema` adds a `theme` field to the block's schema and renders it with the `color_picker` widget, which draws one swatch per configured theme, colored with that theme's `--theme-color`. +The block stores the name of the selected theme in its `theme` field. + +```{note} +`color_picker` is a legacy name for what is really a swatch picker. +VLT registers the same `ColorSwatch` component under both `color_picker` and `colorSwatch`, and a comment in VLT's source notes that the widget should be renamed. + +Do not confuse it with two similarly named widgets: + +- `colorPicker` is a different widget, a free color input with a contrast checker, used for the site-wide colors that the backend behaviors expose. +- `themeColorSwatch` is registered by VLT, but currently no schema that VLT ships uses it. +``` + +### Container Width System + +VLT uses three container widths: ```scss -// Container widths -$layout-container-width: 1440px !default; -$default-container-width: 940px !default; -$narrow-container-width: 620px !default; +// Three-width layout system +--layout-container-width: 1440px; // for major elements like headers and large blocks +--default-container-width: 940px; // balanced content presentation for most blocks +--narrow-container-width: 620px; // optimal readability for text ``` -## Block alignment +VLT declares these three custom properties from SCSS variables of the same name, such as `$default-container-width`. +To change them, set the SCSS variables, as explained in {ref}`light-theme-custom-properties-label`. + +Those three are the site-wide scale. Individual blocks never read them directly. Instead, each block carries a `--block-width` property that points at one of them, and the `blockWidth` widget is what chooses the target: -As part of the effort to generalize behaviors, another VLT `widget` is the `BlockAlignmentWidget`, which also takes advantage of the StyleWrapper by setting the `--block-alignment` property. +| Token stored | `--block-width` becomes | Resulting width | +| --- | --- | --- | +| `narrow` | `var(--narrow-container-width)` | 620px | +| `default` | `var(--default-container-width)` | 940px | +| `layout` | `var(--layout-container-width)` | 1440px | +| `full` | `100%` | edge to edge | -The three default options are: +The indirection is what makes the scale worth having. A block's stylesheet only ever writes `max-width: var(--block-width)`, so it never needs to know which of the three it was handed, and changing `--default-container-width` in one place moves every block set to `default`. Only `full` steps outside the scale, which is why it resolves to a plain `100%` rather than to a container width. + +The section on style fields below explains how the stored token becomes that value. + +### Block Alignment + +The `blockAlignment` widget works the same way, storing one of `left`, `center`, or `right` and resolving it to `--block-alignment`. The three tokens map to these values: ```scss --align-left: start; @@ -104,6 +212,462 @@ The three default options are: --align-right: end; ``` -## Conclusion +(light-theme-style-fields-label)= + +### Style Fields and the `:noprefix` Convention + +Width and alignment are both **style fields**, and every style field follows the same two-step process: store a token, then resolve that token to a style object at render time. + +What a block actually saves is short. A block set to narrow width and left alignment stores this in its `styles` object: + +```json +{ + "blockWidth:noprefix": "narrow", + "align:noprefix": "left" +} +``` + +Only the tokens are stored—no CSS, no custom property values. +This relies on the widget storing the token rather than a style object, which {ref}`the Cover block ` in chapter 3 shows how to ensure. +Resolution happens when the block renders: + +1. For each key in `styles`, VLT looks up a `styleFieldDefinition` utility registered under **that exact key**. +2. The utility returns a list of style definitions. For `blockWidth:noprefix` that list is `config.blocks.widths`. +3. VLT finds the definition whose `name` matches the stored token and takes its `style` object. For `narrow`, that is `{ '--block-width': 'var(--narrow-container-width)' }`. +4. The StyleWrapper injects that object as an inline style on the block. + +Storing tokens rather than values is what makes the system re-themeable. Change what `narrow` means in `config.blocks.widths` and every block already set to `narrow` follows, because the content only ever recorded the word. + +Separately, the StyleWrapper turns each style field into a CSS class. +By default it prefixes the class with the field name, so a field named `align` with the value `center` produces `has--align--center`. +A field name ending in `:noprefix` opts out of that prefixing, so `align:noprefix` with the value `center` produces the class `center`. + +VLT uses this suffix for its own style fields, and registers the matching style definitions under the same literal name: + +```typescript +config.registerUtility({ + name: 'align:noprefix', + type: 'styleFieldDefinition', + method: ({ data }) => + config.blocks.blocksConfig?.[data?.['@type'] ?? '']?.alignments || + config.blocks.alignments, +}); +``` + +The suffix is part of the field name, not decoration. +A field named `align` and a field named `align:noprefix` are two different fields: only the second one matches the utility above, and only the second one gets its CSS custom properties injected. +The three style fields VLT defines this way are `align:noprefix`, `blockWidth:noprefix`, and `size:noprefix`. + +(light-theme-generated-classes-label)= + +### Generated Block Classes + +Beyond the style fields, VLT adds a set of functions to `config.settings.styleClassNameExtenders`. +They inspect each block's position in the page and inject extra classes on it. +This is what lets the theme control vertical spacing between blocks from CSS alone, without a block needing to know anything about its neighbors. + +The classes available on every block are: + +| Class | Injected when | +| --- | --- | +| `next--is--${type}` | there is a following block, and `${type}` is its type | +| `previous--is--same--block-type` | the preceding block has the same type | +| `next--is--same--block-type` | the following block has the same type | +| `is--first--of--block-type` | the preceding block has a different type, or there is none | +| `is--last--of--block-type` | the following block has a different type, or there is none | +| `has--headline` | the block has a headline, or follows a Heading block | +| `previous--has--same--backgroundColor` | the preceding block uses the same theme | +| `next--has--same--backgroundColor` | the following block uses the same theme | +| `previous--has--different--backgroundColor` | the preceding block uses a different theme | +| `next--has--different--backgroundColor` | the following block uses a different theme | +| `has--block-width--${width}` | always, from the `blockWidth:noprefix` field, or `default` when it is empty | +| `has--block-alignment--${alignment}` | always, from the `align:noprefix` field, or `center` when it is empty | +| `has--background-color--${theme}` | always, from the block's theme, or `default` when it has none | + +When VLT compares themes, a block without a theme, or a missing neighbor, counts as `default`. + +You will see these classes throughout VLT's own stylesheets, and in the examples later in this training. +For example, VLT's listing styles use two of them to remove the space below a Grid listing when the next block is another listing with the same background, so the pair reads as one band of color: + +```scss +.block.listing.grid { + &.next--has--same--backgroundColor.next--is--same--block-type { + .listing-item:last-child { + padding-bottom: 0 !important; + border-bottom: none !important; + } + } +} +``` + +Because the extenders are registered in configuration, a project can add its own to inject any class it needs. + +## Create a New Project with Cookieplone + +This training uses Cookieplone 2.0 to generate the project. +If you have not used Cookieplone before, the [Cookieplone guide](https://6.docs.plone.org/install/create-project-cookieplone.html) lists the tools it needs. + +From the folder where you keep your projects, run: + +```shell +uvx cookieplone project +``` + +The `project` argument selects the **Plone 6 Project** template. +Without it, Cookieplone first asks you to choose a category and a template. + +Cookieplone then asks a series of questions. +Accept the defaults, except for the project title: + +| Question | Default | Answer | +| --- | --- | --- | +| Project Title | `Project Title` | `Robotarium` | +| Project Slug (Used for repository id) | `robotarium` | accept | +| Python Package Name | `robotarium` | accept | +| Use Volto as frontend? | Yes | accept | +| all other questions | | accept | + +The title determines the other names. +Cookieplone derives the slug `robotarium` from it, and uses the slug for the output folder and the Python package name. +It also derives the name of the frontend add-on, `volto-robotarium`, without asking, and it picks the Plone and Volto versions for you. +At the time of writing, it generated a project with Plone 6.2.2 and Volto 19.4.1. + +```{important} +Every path in this training of the form {file}`frontend/packages/volto-robotarium/…` uses that add-on name. +If you build something other than a Robotarium, the add-on is called `volto-` followed by your project slug, and you should adjust the paths accordingly. +``` + +You should end up with this shape: + +```console +robotarium/ +├── Makefile +├── backend/ +│ ├── pyproject.toml +│ └── src/robotarium/ +└── frontend/ + ├── mrs.developer.json + ├── volto.config.js + └── packages/volto-robotarium/ <- your project add-on +``` + +## Installing Volto Light Theme + +VLT is shipped as two add-ons that you must install together: + +- the frontend Volto add-on `@kitconcept/volto-light-theme`, which is also a theme add-on, +- the backend Plone add-on `kitconcept.voltolighttheme`, which provides the site customization behaviors. + +```{note} +This training uses VLT 8.0.0, which requires Volto 19. +The [VLT compatibility table](https://volto-light-theme.readthedocs.io/reference/compatibility.html) lists the Volto versions that each VLT version supports. +``` + +You first declare both add-ons in your project, and then install everything with a single command. + +### Step 1: Declare the Frontend Packages + +VLT supports all core blocks, and it also supports blocks from a set of recommended add-ons that provide the basic blocks for your website. +Including them is not required, and you can pick only the ones you want to use. + +VLT does not install the recommended block add-ons for you, so you add them to your project add-on yourself, together with VLT. +Open {file}`frontend/packages/volto-robotarium/package.json` and add them to its `dependencies`: + +```json +{ + "dependencies": { + "@eeacms/volto-accordion-block": "^12.0.0", + "@kitconcept/volto-banner-block": "^1.2.1", + "@kitconcept/volto-bm3-compat": "^1.0.0-alpha.1", + "@kitconcept/volto-button-block": "^5.0.0", + "@kitconcept/volto-carousel-block": "^3.0.0", + "@kitconcept/volto-dsgvo-banner": "^4.0.0", + "@kitconcept/volto-heading-block": "^2.5.0", + "@kitconcept/volto-highlight-block": "^5.0.0", + "@kitconcept/volto-introduction-block": "^1.4.1", + "@kitconcept/volto-light-theme": "^8.0.0", + "@kitconcept/volto-logos-block": "^4.0.0", + "@kitconcept/volto-separator-block": "^5.0.0", + "@kitconcept/volto-slider-block": "^7.0.0", + "@plonegovbr/volto-social-media": "^3.0.0-alpha.0" + } +} +``` + +```{note} +These are the known good versions for VLT 8.0.0. The up-to-date list lives in the [recommended add-ons reference](https://volto-light-theme.readthedocs.io/reference/recommended-addons.html) of the VLT documentation. +``` + +`@kitconcept/volto-bm3-compat` is not a block add-on. +VLT already depends on it, and you list it here because the block you build in chapter 3 imports from it. + +Installing a package is not enough: Volto also has to load it as an add-on. +In the same {file}`package.json`, Cookieplone generated two empty keys, `"addons": []` and `"theme": ""`. +List the add-ons in `addons`, and declare VLT as the `theme`: + +```json +{ + "addons": [ + "@eeacms/volto-accordion-block", + "@kitconcept/volto-banner-block", + "@kitconcept/volto-bm3-compat", + "@kitconcept/volto-button-block", + "@kitconcept/volto-carousel-block", + "@kitconcept/volto-dsgvo-banner", + "@kitconcept/volto-heading-block", + "@kitconcept/volto-highlight-block", + "@kitconcept/volto-introduction-block", + "@kitconcept/volto-logos-block", + "@kitconcept/volto-separator-block", + "@kitconcept/volto-slider-block", + "@plonegovbr/volto-social-media", + "@kitconcept/volto-light-theme" + ], + "theme": "@kitconcept/volto-light-theme" +} +``` + +VLT needs both keys, because it is both a regular add-on and a theme add-on. + +```{important} +VLT must be the last entry in `addons`. +Volto applies add-ons in the order you list them, and VLT's configuration extends the blocks that the other add-ons register, so it has to run after theirs. + +Your project add-on is applied after all of them, because Volto applies the add-ons that an add-on declares before the add-on itself. +``` + +### Step 2: Declare the Python Package + +Edit {file}`backend/pyproject.toml` and add `kitconcept.voltolighttheme` to the `dependencies` array that Cookieplone generated: + +```toml +dependencies = [ + "Products.CMFPlone==6.2.2", + "plone.api", + "plone.restapi", + "plone.volto", + "kitconcept.voltolighttheme==8.0.0", +] +``` + +Keep the Plone version that Cookieplone generated for you, if it differs from the one shown here. + +### Step 3: Install Everything + +Both manifests are now edited, so install the whole project in one step, from the project root: + +```shell +make install +``` + +This installs the backend and the frontend, and creates a Plone site with the id `Plone`. + +### Step 4: Start the Servers and Install VLT in Plone + +Start your development environment, from the project root, in two terminals: + +```shell +# Terminal 1 - Backend +make backend-start + +# Terminal 2 - Frontend +make frontend-start +``` + +Once the frontend is running, open http://localhost:3000. +The first time you open the site, a cookie consent dialog from `@kitconcept/volto-dsgvo-banner` covers the page. +Choose one of its options to close it. + +Then log in at http://localhost:3000/login with the user `admin` and the password `admin`, and install the backend add-on: + +1. Go to http://localhost:3000/controlpanel/addons. +2. In the list of available add-ons, select **Volto Light Theme: Install** to show its details. +3. Select **Install**. + +### Step 5: Activate Behaviors for Plone Site + +The backend add-on ships five behaviors. +None of them is enabled automatically, because each one adds fields to the edit form of the content type you apply it to, and you should opt into the ones you want. + +| Behavior | Shown in the Behaviors tab as | What it adds | +| --- | --- | --- | +| `voltolighttheme.header` | Header customizations for sites/subsites | Site logo, complementary logo, fat menu switch, intranet header switch, site flag, and site actions | +| `voltolighttheme.theme` | Theme colors customizations for sites/subsites | Five color fields for the navigation, fat menu, breadcrumbs, and footer. See {ref}`light-theme-behavior-colors-label`. | +| `voltolighttheme.footer` | Footer customizations for sites/subsites | Footer logos with their size and container width, footer links, and the footer colophon text | +| `kitconcept.footer` | kitconcept specific footer customizations | The footer layout of kitconcept distributions: a footer logo, an address, three link columns, and a sponsor logo with its link | +| `kitconcept.sticky_menu` | Sticky menu | Icon links fixed to the right edge of the screen, their colors, and a switch to show them on mobile | + +The header fields work as follows: + +Site logo +: The main logo, at the top left of the header. + +Complementary logo +: A second logo on the right side of the header. Only the intranet header shows it. + +Fat menu switch +: The fat menu is the panel that opens below the navigation when you select a main section. It is enabled by default. + +Intranet header switch +: Replaces the default header with a layout intended for intranet sites. + +Site flag +: A short text shown in a colored pill at the top of the header. + +Site actions +: Links shown at the top right of the header, each with a title, a target URL, and an option to open it in a new tab. + +To activate the behaviors: + +1. Go to http://localhost:3000/controlpanel/dexterity-types/Plone%20Site. +2. In the **Behaviors** tab, select the behaviors from the table above that you want. +3. Select **Save**. + +This training uses the first three. +Leave `kitconcept.footer` disabled while you follow it: when it is active, VLT renders kitconcept's footer layout, which sets its own footer background colors and replaces the footer gradient that you define in the next chapter. + +The behaviors add their fields to the content type they are applied to. +Applied to the Plone Site, they configure the whole site. +Applied to a subsite, they configure that branch instead, and each page uses the settings of its nearest ancestor that has them. + +(light-theme-behavior-colors-label)= + +#### What the theme behavior does + +The behavior adds five empty, optional fields to the **Theming** tab of the edit form, each rendered with the `colorPicker` widget. +When an editor fills in a field, VLT sets the matching custom property: + +| Field | Label in the form | Custom property it sets | +| --- | --- | --- | +| `header_foreground` | Navigation Text Color | `--header-foreground` | +| `accent_foreground_color` | Fat Menu / Breadcrumbs Text Color | `--accent-foreground-color` | +| `accent_color` | Fat Menu Background Color | `--accent-color` | +| `secondary_foreground_color` | Footer Font Color | `--secondary-foreground-color` | +| `secondary_color` | Footer Background Color | `--secondary-color` | + +There is no field for the primary color pair. +The header background follows `--primary-color`, which no field of the behavior sets, and the navigation text color has its own field, `header_foreground`. + +VLT writes the values that editors choose into a `:root` rule at the top of the document head. +Your project's stylesheet comes after that rule, and the design tokens that you will define in the next chapter use the same `:root` selector. +When both set the same property, your stylesheet wins, and the value that the editor chose has no effect. + +```{important} +Decide who owns each of these five properties. +If editors should be able to change a color, leave its property out of your stylesheet. + +The Robotarium's stylesheet sets `--accent-color`, `--accent-foreground-color`, and `--secondary-color` in the next chapter. +On the Robotarium, the Fat Menu Background Color, Fat Menu / Breadcrumbs Text Color, and Footer Background Color fields therefore have no effect, while Navigation Text Color and Footer Font Color still work. +``` + +```{note} +Behaviors can be added in new releases, and this table reflects `kitconcept.voltolighttheme` 8.0.0. +The authoritative list is the behavior registration in the add-on itself, at {file}`backend/src/kitconcept/voltolighttheme/behaviors/configure.zcml` in the [VLT repository](https://github.com/kitconcept/volto-light-theme). +See the [site customization guide](https://volto-light-theme.readthedocs.io/conceptual-guides/site-customization.html) for what each field does. +``` + +Your site now has the VLT site customization fields available. + +## File Structure Setup + +Set up the recommended file structure in your project add-on's `src` folder. +Cookieplone already created part of it, so you are filling in the gaps rather than starting from nothing: + +```console +src/ +├── components/ # exists, empty +│ └── blocks/ # new +├── config/ +│ ├── settings.ts # exists, sets the default language +│ └── blocks.ts # new +├── index.ts # exists, calls installSettings() +└── theme/ # new + ├── blocks/ + ├── _variables.scss + └── _main.scss +``` + +From {file}`frontend/packages/volto-robotarium/src`: + +```shell +mkdir -p components/blocks config theme/blocks +touch config/blocks.ts theme/_variables.scss theme/_main.scss +``` + +```{warning} +Do not overwrite the generated {file}`index.ts` and {file}`config/settings.ts`. +The generated `settings.ts` sets your site's `defaultLanguage`, and `index.ts` already calls it. +Every later step in this training that shows one of these two files is showing an **edit**, not a replacement. +``` + +Whenever you add new files to your project, restart the frontend, because hot reload does not pick them up. + +(light-theme-insertion-points-label)= + +### The Two Theme Insertion Points + +Two of those filenames are load-bearing. +Volto scans every add-on for {file}`theme/_variables.scss` and {file}`theme/_main.scss`, and injects whichever it finds into the theme's stylesheet at two different points: + +| File | Injected | Use it for | +| --- | --- | --- | +| {file}`theme/_variables.scss` | **before** VLT's own variables | the few SCSS variables VLT compiles with | +| {file}`theme/_main.scss` | **after** all of VLT's styles | everything else, including all your custom properties | + +The order is what makes each one useful. +No other filename is special: `theme/blocks/` and anything else you add are ordinary partials that become part of the build only because {file}`_main.scss` imports them. + +```{note} +Volto skips these two files for whichever add-on is registered as the `theme`. +They work for {file}`volto-robotarium` precisely because VLT, not your project, is the theme add-on. +``` + +(light-theme-custom-properties-label)= + +#### Prefer CSS custom properties + +Almost all of your theming should be CSS custom properties written in {file}`_main.scss`, not SCSS variables. + +VLT is built as a custom-property system: the color pairs, the semantic properties, the container widths, and the block themes are all custom properties, resolved in the browser. That is what lets you override them per block, per section, and at runtime—a block theme works by re-declaring `--theme-color` on one block, which no build-time variable could do. Setting your design tokens as custom properties in `:root` is the normal path, and the next chapter sets these, among others: + +```scss +// src/theme/_main.scss +:root { + --accent-color: #3b5759; + --accent-foreground-color: #fff; + --link-foreground-color: #157a7a; +} +``` + +{file}`_variables.scss` is the narrow exception, for values VLT **computes with at build time**. VLT declares its SCSS variables with `!default`, so each one applies only when nothing has already set it. Since {file}`_variables.scss` is injected first, anything you assign there wins—and it is the *only* place they can be changed, because VLT's stylesheets have already used the values by the time {file}`_main.scss` is injected: + +```scss +// src/theme/_variables.scss +$default-container-width: 1120px; +``` + +Two kinds of value belong there: + +- **Breakpoints**, which cannot be custom properties at all, since media queries cannot read them. +- **The three container widths.** VLT declares `--layout-container-width`, `--default-container-width`, and `--narrow-container-width` from the SCSS variables of the same name, and also uses the SCSS values in media and container queries. Setting the SCSS variable updates both, so you never set the custom property yourself. + +The rule of thumb: custom properties for anything meant to vary per block, per section, or at runtime—which is nearly everything, and all of the color system. SCSS variables only for the handful of values VLT compiles into media queries and mixins. + +## Checkpoint + +Restart both servers and confirm the install landed before moving on. Most trouble in later chapters traces back to one of these: + +- The site at http://localhost:3000 renders in VLT's styling, not Volto's default Pastanaga theme. If it looks unchanged, VLT is listed in `addons` but not declared as the `theme`. +- Editing a page and selecting a Text block shows a **Background color** control in the **Styling** section of the block settings. This confirms VLT's block configuration is applied. +- The add-ons control panel lists **Volto Light Theme: Install** among the installed add-ons, and the Plone Site content type shows the `voltolighttheme` behaviors as enabled. +- Adding a temporary rule to {file}`src/theme/_main.scss`, such as `body { border-top: 4px solid red; }`, shows that border after a restart. If it does not, your add-on is not being picked up, and no amount of CSS in later chapters will apply. Remove the rule afterward. + +## Further Reading + +This training covers the parts of VLT you need to build a project. The theme's own documentation goes deeper on each topic: -VLT provides a robust foundation for Plone CMS frontend development through this framework of customizable variables and standardized block controls. Through these core concepts, VLT strikes a balance between maintaining consistency and flexibility, allowing developers to create cohesive designs while still having the freedom to customize elements to match their specific project needs. +- [Color system](https://volto-light-theme.readthedocs.io/conceptual-guides/color-system.html) +- [Layout](https://volto-light-theme.readthedocs.io/conceptual-guides/layout.html) +- [Vertical spacing](https://volto-light-theme.readthedocs.io/conceptual-guides/vertical-spacing.html) +- [Install guide](https://volto-light-theme.readthedocs.io/how-to-guides/install.html) +- [Site customization](https://volto-light-theme.readthedocs.io/conceptual-guides/site-customization.html) diff --git a/docs/customizing-volto-light-theme/creating-new-project.md b/docs/customizing-volto-light-theme/creating-new-project.md deleted file mode 100644 index 9cc7fef69..000000000 --- a/docs/customizing-volto-light-theme/creating-new-project.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -myst: - html_meta: - "description": "Create a new project with Plone and Volto" - "property=og:description": "Create a new project with Plone and Volto" - "property=og:title": "Create a new project with Plone and Volto" - "keywords": "Plone, Volto, Training, Theme, Footer" ---- - -# Create a new project with Plone and Volto -We recommend creating your Plone project with **Cookieplone**. Our comprehensive documentation provides step-by-step guidance to help you get started. For detailed installation instructions, visit our [Cookieplone guide](https://6.docs.plone.org/install/create-project-cookieplone.html). \ No newline at end of file diff --git a/docs/customizing-volto-light-theme/design-system-implementation.md b/docs/customizing-volto-light-theme/design-system-implementation.md new file mode 100644 index 000000000..564ea9c6b --- /dev/null +++ b/docs/customizing-volto-light-theme/design-system-implementation.md @@ -0,0 +1,400 @@ +--- +myst: + html_meta: + "description": "Design system implementation & theming" + "property=og:description": "Design system implementation & theming" + "property=og:title": "Design system implementation & theming" + "keywords": "Plone, Volto, Training, Theme, Footer" +--- + +# Design System Implementation & Theming + +The Robotarium has a look: workshop teal, a technical font, and surfaces that read like painted metal. This chapter turns that into a working theme. + +Nothing here is specific to robots. The steps are the ones you follow for any design handed to you—pull the decisions out of it, express them as tokens, and let VLT's components pick them up. + +## Extracting Design Tokens + +Before writing any code, list the decisions that the design makes: + +- **Color pairs**: the primary, secondary, and accent background colors, and the text color used on each. +- **Link color**, if links should stand out from the surrounding text. +- **Typography**: the font families, and any line heights or font weights that differ from VLT's. +- **Block palettes**: the backgrounds that editors should be able to choose for a section of a page. + +For the Robotarium, that list comes down to a dark teal accent with white text, a pale teal secondary color, a teal link color, the Chakra Petch font, and one extra block palette. + +## Implementing Your Design System + +### Step 1: Add Font Files + +If you're using custom fonts, add the font files to your theme directory. +The Robotarium uses [Chakra Petch](https://fonts.google.com/specimen/Chakra+Petch). +From {file}`frontend/packages/volto-robotarium/src/theme`, create a folder for it: + +```shell +mkdir -p fonts/Chakra_Petch +``` + +Download the font family from Google Fonts, and copy its regular and bold files into that folder: + +```console +src/theme/fonts/Chakra_Petch/ +├── ChakraPetch-Regular.ttf +└── ChakraPetch-Bold.ttf +``` + +Keeping the files inside your theme bundles them with it, so your stylesheets can refer to them with relative paths. + +### Step 2: Override SCSS Variables + +Set the build-time values in {file}`src/theme/_variables.scss`, as described in {ref}`light-theme-custom-properties-label`: + +```scss +// src/theme/_variables.scss +$default-container-width: 1120px; +$spacing-large: 80px; +``` + +The Robotarium widens the default container from 940px to 1120px, and increases VLT's large spacing step from 60px to 80px. +Setting `$default-container-width` also updates `--default-container-width`, so you do not need to set both. + +### Step 3: Define Your Design Tokens + +In {file}`src/theme/_main.scss`, declare the fonts and the CSS custom properties that carry your design: + +```scss +@font-face { + font-family: 'Chakra Petch'; + src: url('./fonts/Chakra_Petch/ChakraPetch-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Chakra Petch'; + src: url('./fonts/Chakra_Petch/ChakraPetch-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +:root { + // Extract these from your design + --accent-color: #3b5759; + --accent-foreground-color: #fff; + --secondary-color: #afcac8; + + // Typography. VLT reads the font through + // `$page-font: var(--custom-main-font, $page-font-template)`. + --custom-main-font: 'Chakra Petch', sans-serif; + + // Give links a color of their own instead of the theme foreground color + --link-foreground-color: #157a7a; + + // Breadcrumbs: a white bar, with text in the footer text color + --breadcrumbs-background: var(--background); + --breadcrumbs-foreground: var(--secondary-foreground-color); + + // Gradients for header and footer. + // `--background` is VLT's base color, white by default. + --header-background: linear-gradient( + -3deg, + var(--background) 0%, + color-mix(in oklab, var(--secondary-color) 1%, var(--background)) 20%, + color-mix(in oklab, var(--secondary-color) 3%, var(--background)) 35%, + color-mix(in oklab, var(--secondary-color) 10%, var(--background)) 50%, + color-mix(in oklab, var(--secondary-color) 30%, var(--background)) 65%, + color-mix(in oklab, var(--secondary-color) 65%, var(--background)) 80%, + color-mix(in oklab, var(--secondary-color) 90%, var(--background)) 92%, + var(--secondary-color) 100% + ); + + --footer-background: radial-gradient( + ellipse 100% 100% at 50% 100%, + var(--secondary-color) 0%, + var(--secondary-color) 20%, + color-mix(in oklab, var(--secondary-color) 85%, var(--background)) 30%, + color-mix(in oklab, var(--secondary-color) 65%, var(--background)) 40%, + color-mix(in oklab, var(--secondary-color) 45%, var(--background)) 50%, + color-mix(in oklab, var(--secondary-color) 30%, var(--background)) 60%, + color-mix(in oklab, var(--secondary-color) 18%, var(--background)) 70%, + color-mix(in oklab, var(--secondary-color) 10%, var(--background)) 80%, + color-mix(in oklab, var(--secondary-color) 4%, var(--background)) 90%, + var(--background) 100% + ); +} +``` + +The breadcrumbs tokens replace VLT's defaults, which use the accent colors. +The Robotarium's accent text color is white, for the teal fat menu, and it would disappear on the white breadcrumbs bar, so the breadcrumbs take the text color of the footer instead. +As a result, the Footer Font Color field of the theme behavior also changes the color of the breadcrumbs. + +The header and footer gradients are assigned to the same `--header-background` and `--footer-background` properties you saw in the previous chapter. +VLT paints the header and the footer with `background`, not `background-color`, so a gradient works there as well as a flat color, with no component changes. + +Setting a font is a good example of the {ref}`prefer custom properties ` rule from the previous chapter: you do not shadow VLT's typography stylesheets, you set the one property they already read. + +Site-wide rules that are not tokens go in the same file, outside `:root`: + +```scss +.breadcrumbs { + border-bottom: 1px solid var(--secondary-color); +} + +// Let the first block sit flush with the header +#page-document, +#page-edit, +#page-add { + .blocks-group-wrapper:first-child { + padding-top: 0; + } +} +``` + +```{warning} +Declare custom properties inside `:root`, but keep normal rules outside it. +Nesting a selector such as `#page-document` inside `:root` compiles to `:root #page-document`, which adds specificity you will have to fight later. +``` + +### Step 4: Add Block-Specific Styles + +Keep per-block styles in their own partials under {file}`src/theme/blocks/`, one file per block. +This keeps them easy to find, and stops {file}`_main.scss` from growing too large. +The Robotarium adds four of them. + +The button partial replaces the colors of buttons on hover and focus with the accent colors: + +```scss +// src/theme/blocks/_button.scss +body .block.__button { + > .button.container, + > .block-inner-container { + a, + button { + padding: 1rem; + transition: + background 0.2s ease, + color 0.2s ease; + + &:hover, + &:active, + &:focus { + background: var(--accent-color); + color: var(--accent-foreground-color); + } + } + } +} +``` + +VLT colors buttons inside themed blocks with rules that start with `body`. +Starting your rule with `body` as well gives it the same specificity, and it wins because your stylesheet loads after VLT's. + +The hover colors also matter for the gradient palette that you add in Step 5. +VLT's own hover state uses `--theme-color` as the text color. +A gradient is not a valid text color, so in a section with a gradient palette, the button text would inherit the dark foreground color and disappear against the dark hover background. +The accent colors avoid that. + +The slider and grid partials give text a frosted-glass panel, on the slide titles and in four-column grids: + +```scss +// src/theme/blocks/_slider.scss +.block.slider .teaser-item .teaser-item-title { + background: rgb(255 255 255 / 10%); + backdrop-filter: blur(20px) saturate(110%); + box-shadow: + 0 8px 32px 0 rgb(31 135 125 / 10%), + inset 0 0 0 1px rgb(255 255 255 / 10%); + color: var(--theme-foreground-color); +} +``` + +```scss +// src/theme/blocks/_grid.scss +.block.gridBlock .four .slate:not(.inner) { + padding: 2.5rem; + padding-top: 4rem; + backdrop-filter: blur(20px) saturate(110%); + box-shadow: + 0 8px 32px 0 rgb(31 135 125 / 10%), + inset 0 0 0 1px rgb(255 255 255 / 10%); +} +``` + +The teaser partial adds space around the text of Teaser blocks: + +```scss +// src/theme/blocks/_teaser.scss +.block.teaser .card .card-inner .card-summary { + padding: $spacing-large; +} +``` + +The teaser rule uses `$spacing-large`, one of VLT's SCSS variables, which you set to 80px in Step 2. +VLT's variables and mixins are in scope for your stylesheets because {file}`_main.scss` is compiled after them, so you can reuse the theme's spacing scale, breakpoints, and typography mixins instead of inventing new values. + +(charging-bay-label)= + +### Step 5: Configure Block Themes + +Block themes are the palettes an editor can choose between on any block. The Robotarium gets two: the plain default, and **Charging Bay**, a soft teal wash for sections that should feel like the lit alcove where the robots dock. + +In {file}`src/config/blocks.ts`, define them: + +```typescript +import type { ConfigType } from '@plone/registry'; + +export default function install(config: ConfigType) { + // Block palettes + config.blocks.themes = [ + { + style: { + '--theme-color': 'white', + '--theme-high-contrast-color': '#bbd1d0', + '--theme-foreground-color': 'black', + '--theme-low-contrast-foreground-color': '#555555', + }, + name: 'default', + label: 'Default', + }, + { + style: { + '--theme-color': `linear-gradient(180deg, oklab(1 0 0 / 0.9) 0%, transparent 15%, transparent 85%, oklab(1 0 0 / 0.9) 100%), + radial-gradient(ellipse 850px 700px at 12% 15%, oklab(1 0 0 / 0.4) 0%, transparent 70%), + radial-gradient(ellipse 900px 650px at 85% 85%, oklab(1 0 0 / 0.4) 0%, transparent 70%), + radial-gradient(ellipse 850px 750px at 70% 35%, oklab(0.805 -0.030 -0.003 / 0.5) 0%, oklab(0.835 -0.025 -0.002 / 0.2) 50%, transparent 85%), + radial-gradient(ellipse 920px 800px at 25% 65%, oklab(0.780 -0.032 -0.004 / 0.45) 0%, oklab(0.825 -0.027 -0.003 / 0.2) 50%, transparent 87%), + radial-gradient(ellipse 1100px 900px at 50% 50%, oklab(0.805 -0.030 -0.003 / 0.25) 0%, oklab(0.850 -0.025 -0.002 / 0.1) 60%, transparent 90%), + linear-gradient(182deg, oklab(1 0 0) 0%, oklab(0.988 -0.006 0) 10%, oklab(0.958 -0.016 -0.001) 22%, oklab(0.910 -0.025 -0.003) 35%, oklab(0.860 -0.030 -0.003) 45%, oklab(0.820 -0.032 -0.004) 52%, oklab(0.860 -0.030 -0.003) 59%, oklab(0.910 -0.025 -0.003) 69%, oklab(0.958 -0.016 -0.001) 82%, oklab(0.988 -0.006 0) 92%, oklab(1 0 0) 100%)`, + '--theme-high-contrast-color': 'oklab(1 0 0 / 0.1)', + '--theme-foreground-color': 'black', + '--theme-low-contrast-foreground-color': '#555555', + }, + name: 'charging-bay', + label: 'Charging Bay', + }, + ]; + + // Re-point the Grid block at the new palettes. See below. + config.blocks.blocksConfig.gridBlock.themes = config.blocks.themes; + + return config; +} +``` + +```{note} +VLT copies the default palettes to the Grid block when its own configuration runs, which is before yours. +The Grid block is the only block in VLT, or in any of the recommended add-ons, that keeps its own copy of the palettes, so this is the only place where the extra line is needed. +``` + +(light-theme-gradient-themes-label)= + +```{warning} +`--theme-color` is usually a flat color, but Charging Bay sets it to a gradient. +A gradient only works where a stylesheet reads the property with `background`, as VLT does for block backgrounds. +Where a stylesheet reads it with `background-color` or `color`, the browser ignores the declaration: + +- The Charging Bay swatch in the block settings would look empty, which the rule below fixes. +- The Button block would lose its hover text color, which the button partial from Step 4 fixes. + +In your own styles, read `--theme-color` with `background`, never with `background-color`. +``` + +VLT styles the swatches in the **Background color** control with `background-color: var(--theme-color)`. +Add this rule to {file}`src/theme/_main.scss`, next to the other site-wide rules, to give the Charging Bay swatch a gradient of its own: + +```scss +// Match the theme swatch in the sidebar to the custom "charging bay" theme +#sidebar { + .color-swatch-widget .color-swatch-option-handler.charging-bay { + background: linear-gradient(135deg, var(--secondary-color) 0%, #fff 100%); + } +} +``` + +### Step 6: Keep the Layout Settings in Sync + +Widening the container in SCSS is only half the change. +VLT also reads the container width from configuration, in {file}`src/config/settings.ts`. +Add the `layout` block to the file that Cookieplone generated. +Do not replace the file, or you will drop your language settings: + +```typescript +import type { ConfigType } from '@plone/registry'; + +export default function install(config: ConfigType) { + // ... the language settings generated by Cookieplone ... + + // Added in this step + config.settings.layout = { + ...config.settings.layout, + defaultContainerWidth: 1120, + }; + + return config; +} +``` + +This value is not used for layout—the CSS custom property does that. +It is used to compute the `sizes` attribute of responsive images in teasers and listings, which tells the browser how wide an image will actually be, so that it can pick the right scale to download. + +If the two values disagree, nothing breaks visibly, but the browser picks image scales for the wrong width. +When the configured width is too small, images look soft; when it is too large, pages download bigger images than they need. +Whenever you change `$default-container-width`, change `defaultContainerWidth` to match. + +```{note} +`config.settings.layout` also carries `tabletBreakpoint`, 768 by default, which is used in the same calculation. +It matches VLT's `$largest-mobile-screen` SCSS variable, so if you change VLT's breakpoints, update it as well. +``` + +### Step 7: Main Index Configuration + +The generated {file}`src/index.ts` already calls `installSettings`. Add the two lines that wire up your block configuration: + +```typescript +import type { ConfigType } from '@plone/registry'; +import installSettings from './config/settings'; +import installBlocks from './config/blocks'; + +function applyConfig(config: ConfigType) { + installSettings(config); + installBlocks(config); + return config; +} + +export default applyConfig; +``` + +### Step 8: Import Your Stylesheets + +{file}`_main.scss` is the entry point that Volto injects after all of VLT's styles. +Import your partials there, in the order you want them applied: + +```scss +// src/theme/_main.scss +// ... your @font-face rules, :root tokens, and site-wide rules above ... + +@import './blocks/button'; +@import './blocks/grid'; +@import './blocks/slider'; +@import './blocks/teaser'; +``` + +{file}`_variables.scss` needs no import, because Volto injects it on its own. + +## Checkpoint + +Restart the frontend and confirm the following before moving on: + +- The site renders in Chakra Petch, and the header and footer show their gradients. +- Links in body text are teal, rather than the color of the surrounding text. +- Opening a page in edit mode and selecting a Text block shows **Default** and **Charging Bay** in the **Background color** control, and the Charging Bay swatch shows a gradient. +- Choosing **Charging Bay** for a block gives it the teal wash. + +## Further Reading + +- [Color system](https://volto-light-theme.readthedocs.io/conceptual-guides/color-system.html) +- [Colors reference](https://volto-light-theme.readthedocs.io/reference/colors.html) +- [Layout](https://volto-light-theme.readthedocs.io/conceptual-guides/layout.html) diff --git a/docs/customizing-volto-light-theme/index.md b/docs/customizing-volto-light-theme/index.md index 74e398160..e72d036af 100644 --- a/docs/customizing-volto-light-theme/index.md +++ b/docs/customizing-volto-light-theme/index.md @@ -14,23 +14,29 @@ myst: Level : Intermediate -Customizing Volto Light Theme aims to provide developers with comprehensive knowledge and practical skills for theming in Plone 6's Volto frontend by using and extending Volto Light Theme. +This training teaches you how to theme the Volto frontend of Plone 6 with Volto Light Theme (VLT). +You will learn how VLT's design tokens, block themes, and widgets fit together, and how to extend them with your own blocks, listing variations, slots, and components. -The goal of this training is to guide developers in understanding and effectively working with Volto Light Theme to create visually compelling and consistent user interfaces. Due to the scope of the content, the training follows a structured "show and tell" approach, presenting concepts and explaining their usage, while also encouraging participants to put their learning into practice. - -Participants will explore essential aspects of Volto Light Theme such as setting up theme providers, customizing styles, and understanding how Volto's theming system functions. The training also covers advanced tips, best practices, and lesser-known features to extend Volto Light Theme and create more tailored solutions. +The training follows a "show and tell" approach. +Each chapter explains a concept, then applies it to a site you build along the way for **the Robotarium**, a workshop that lends robots the way a library lends books. This training is best suited for developers who have prior experience with Volto and want to deepen their theming knowledge. +It was written for the following versions: + +- Volto Light Theme 8.0.0, both the frontend package `@kitconcept/volto-light-theme` and the backend package `kitconcept.voltolighttheme` +- Volto 19.4 +- Plone 6.2 +- Cookieplone 2.0 + ```{toctree} :caption: Volto Light Theme :maxdepth: 1 :numbered: concepts -creating-new-project -installing-vlt -integrate-new-block -theming +design-system-implementation +block-development-widgets +advanced-components-bm3 question-answer ``` \ No newline at end of file diff --git a/docs/customizing-volto-light-theme/installing-vlt.md b/docs/customizing-volto-light-theme/installing-vlt.md deleted file mode 100644 index 32cc59343..000000000 --- a/docs/customizing-volto-light-theme/installing-vlt.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -myst: - html_meta: - "description": "Installing Volto Light Theme" - "property=og:description": "Installing Volto Light Theme" - "property=og:title": "Installing Volto Light Theme" - "keywords": "Plone, Volto, Training, Volto Light Theme" ---- - -# Installing Volto Light Theme - -Follow the steps below to install and configure VLT in your project. VLT provides a clean and modern design with ready-to-use blocks and components. - -## Step 1: Install Volto Light Theme - -To install VLT, navigate to the {file}`frontend/packages/volto-my-project` folder and run the following command: - -```{note} -For now we'll be using an `alpha` release, so we need to specify the correct version. -``` - -```shell -pnpm install @kitconcept/volto-light-theme@6.0.0-alpha.2 -``` - -While in your project package folder, add VLT to the `addons` list in your {file}`package.json`, as follows: - -```json -"addons": ["@kitconcept/volto-light-theme"], -``` - -## Step 2: install block add-ons - -Volto Light Theme comes with several pre-configured add-ons that provide basic blocks for your website. If you'd like to include them, you can add them in the `addons` section in your {file}`package.json`, but this is not required. - -Here is the list of recommended addons to install, including VLT, which should be the last element: - -```json -"addons": [ - "@eeacms/volto-accordion-block", - "@kitconcept/volto-button-block", - "@kitconcept/volto-heading-block", - "@kitconcept/volto-highlight-block", - "@kitconcept/volto-introduction-block", - "@kitconcept/volto-separator-block", - "@kitconcept/volto-slider-block", - "@kitconcept/volto-light-theme" -], -``` - -## Step 3: configure Volto Light Theme as the theme provider - -To leverage a cohesive set of styles, components, and design patterns that align with Volto's best practices, you need to set VLT as your theme provider. - -Open the {file}`volto.config.js` file in your {file}`frontend` folder, and modify it as shown below. - -```{code-block} js -:emphasize-lines: 2 - -const addons = ['volto-project-title']; -const theme = '@kitconcept/volto-light-theme'; - -module.exports = { - addons, - theme -}; -``` - -You'll need to restart your Plone frontend to see the changes. - -That's it! Your project should now be using Volto Light Theme with its additional blocks and components. diff --git a/docs/customizing-volto-light-theme/integrate-new-block.md b/docs/customizing-volto-light-theme/integrate-new-block.md deleted file mode 100644 index 7086e08b4..000000000 --- a/docs/customizing-volto-light-theme/integrate-new-block.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -myst: - html_meta: - "description": "Adding New Block" - "property=og:description": "Adding New Block" - "property=og:title": "Adding New Block" - "keywords": "Plone, Volto, Training, Volto Light Theme, Integrate, block" ---- - -# Integrate a new block - -In this training module, we'll learn how to integrate the `@plone-collective/volto-relateditems-block` into VLT. This block allows you to create links to related content in your Plone site. - -## Install `@plone-collective/volto-relateditems-block` - -To install the related items block, make sure you are in the {file}`frontend/packages/volto-my-project` folder, and use the following command: - -```{note} -For now we'll be using an `alpha` release, so we need to specify the correct version. -``` - -```shell -pnpm install @plone-collective/volto-relateditems-block@1.0.0-alpha.1 -``` - -After installation, ensure that the add-on is included in the `addons` key of your project's {file}`package.json`: - -```json -"addons": [ - "@eeacms/volto-accordion-block", - "@kitconcept/volto-button-block", - "@kitconcept/volto-heading-block", - "@kitconcept/volto-highlight-block", - "@kitconcept/volto-introduction-block", - "@kitconcept/volto-separator-block", - "@kitconcept/volto-slider-block", - "@plone-collective/volto-relateditems-block", - "@kitconcept/volto-light-theme" -], -``` - -You'll need to restart your Plone frontend to see the changes. - -That's it! Your project should now be using `@plone-collective/volto-relateditems-block`, which shows related items for the content as a list of links. diff --git a/docs/customizing-volto-light-theme/question-answer.md b/docs/customizing-volto-light-theme/question-answer.md index 1c711fda9..4297e39c2 100644 --- a/docs/customizing-volto-light-theme/question-answer.md +++ b/docs/customizing-volto-light-theme/question-answer.md @@ -7,4 +7,99 @@ myst: "keywords": "Plone, Volto, Training, Volto Light Theme" --- -# Questions and answers +# Questions and Answers + +This chapter collects the questions that come up most often while working through the training. +Each answer is short, and links to the section that explains it in full. + +## Setup + +### My styles do not appear at all + +Check three things, in order: + +1. VLT is declared as the `theme` in your add-on's {file}`package.json`, not only listed in `addons`. It needs both entries. +2. Your stylesheet is {file}`src/theme/_main.scss`, or is imported from it. Volto only picks up that exact filename. +3. You restarted the frontend after adding the file. Hot reload does not pick up new files. + +See {ref}`light-theme-insertion-points-label`. + +### Where do I override `$spacing-large` or a breakpoint? + +In {file}`src/theme/_variables.scss`. +Assigning SCSS variables in {file}`_main.scss` has no effect, because VLT has already used their values by then. +See {ref}`light-theme-custom-properties-label`. + +## Styling + +### Should I use `--theme-color` or `--primary-color`? + +`--theme-color` and its companions are set per block by the selected block theme, and change as the editor picks a different one. The `--primary-color` family is site-wide. Inside a block, read the theme properties so the block adapts to whichever palette it is given; outside blocks, read the site-wide ones. + +### My links are the same color as the surrounding text + +That is the default: links use the foreground color of the block theme they sit in. +Set `--link-foreground-color` in your project to give them a color of their own. + +### An editor changed a color in the Theming tab, but the site ignores it + +Your stylesheet probably sets the same property. +See {ref}`light-theme-behavior-colors-label` for how to decide who owns each color. + +### My gradient palette shows an empty swatch, or unreadable buttons + +Some VLT styles read `--theme-color` with `background-color` or `color`, which ignore a gradient. +See {ref}`the warning about gradient palettes `. + +### Where do the `next--is--...` classes in VLT's CSS come from? + +From functions that VLT adds to `config.settings.styleClassNameExtenders`. +They describe each block's relationship to its neighbors, so that vertical spacing can be expressed in CSS instead of in the components. +See {ref}`light-theme-generated-classes-label` for the full list. + +## Blocks and Widgets + +### My alignment or width buttons appear but do nothing + +Check two things in the field definition: + +- The field name must end in `:noprefix`, as in `align:noprefix` and `blockWidth:noprefix`. Only those names match the style definitions that inject `--block-alignment` and `--block-width`. +- The `actions` must be the token names from `config.blocks`. Otherwise the widget stores a style object instead of a token. + +See {ref}`light-theme-style-fields-label` and {ref}`Step 6 of the Cover block `. + +### Can I reuse a Volto block id for my own block? + +Avoid it. +Registering under an existing key replaces that block's configuration instead of adding a new block, and the result then depends on the order in which the add-ons are applied. +Give project blocks their own key. + +### How do I add the background color control to a third-party block? + +Compose VLT's `defaultStylingSchema` with the block's existing `schemaEnhancer`. +See {ref}`the tip at the end of Step 7 of the Cover block `. + +## Components + +### Should I shadow VLT's header, or swap it? + +Swap it. +VLT 8 resolves its structural components through the registry, so registering your own and naming it in `config.settings.vlt.components` replaces it without shadowing. +Shadowing still works, but it binds you to an internal module path and hides which component is actually active. +See {ref}`light-theme-swap-components-label`. + +### Slot or component swap? + +Use a slot to add something to the layout. Swap a structural component to replace one. Slots are additive and several components can occupy one; a swap substitutes a single named role. + +## Block Model v3 + +### Should I enable it? + +Only once every block that your site uses supports it. +See {ref}`light-theme-block-model-v3-label`. + +### Do I need to set categories on VLT's blocks? + +No. VLT already assigns them to the blocks it has migrated. +For your own blocks, reuse a category only when its rules fit the block, or add a category of your own, as explained in {ref}`light-theme-block-categories-label`. diff --git a/docs/customizing-volto-light-theme/theming.md b/docs/customizing-volto-light-theme/theming.md deleted file mode 100644 index 5e3d94699..000000000 --- a/docs/customizing-volto-light-theme/theming.md +++ /dev/null @@ -1,342 +0,0 @@ ---- -myst: - html_meta: - "description": "Extend the Volto Light Theme" - "property=og:description": "Extend the Volto Light Theme" - "property=og:title": "Extend the Volto Light Theme" - "keywords": "Plone, Volto, Training, Extend, Volto Light Theme" ---- - -# Extend VLT - -In this section we'll extend the VLT to create a "dark" aesthetic for our project. The same patterns can be applied for other visual identity cases. - -## File structure - -Let us start by setting up the recommended file structure. In your project add-on's {file}`src` folder, create a subfolder named {file}`theme`. -Inside {file}`theme` create two empty files named {file}`_main.scss` and {file}`_variables.scss`. Refer to the following file system diagram: - -```console -src/ -├── components -├── index.js -└── theme - ├── _main.scss - └── _variables.scss -``` - -Remember that if you add new files to your project, it will be necessary to restart your Plone frontend. - -### `_variables.scss` - -{file}`_variables.scss` is where you can override the base theme SCSS variables. - -```scss -:root { - --primary-color: black; - --primary-foreground-color: lemonchiffon; - - --secondary-color: darkslategrey; - --secondary-foreground-color: lemonchiffon; - - --accent-color: darkslategrey; - --accent-foreground-color: lemonchiffon; - - --link-color: lightblue; -} -``` - -### `_main.scss` - -{file}`_main.scss` is where you should put any custom styles. You can also include other SCSS or CSS files, as follows: - -```scss -@import 'variables'; -``` - -## Block themes - -Now we need to change the available themes for the blocks by adding the following definition inside the `applyConfig` function in our project's {file}`index.js`: - -```js -config.blocks.themes = [ - { - style: { - '--theme-color': 'black', - '--theme-high-contrast-color': 'darkslategrey', - '--theme-foreground-color': 'lemonchiffon', - '--theme-low-contrast-foreground-color': 'lightgrey', - }, - name: 'default', - label: 'Default', - }, - { - style: { - '--theme-color': 'darkslategrey', - '--theme-high-contrast-color': 'black', - '--theme-foreground-color': 'lemonchiffon', - '--theme-low-contrast-foreground-color': 'lightgrey', - }, - name: 'green', - label: 'Green', - }, -]; -``` - -## Extend add-on styles - -The theme provides the ability to extend or modify existing components. Let's create one more file named {file}`relatedItems.scss` to add specific styles for the add-on, as follows: - -```console -src/ -├── components -├── index.js -└── theme - ├── blocks - └── _relatedItems.scss - ├── _main.scss - └── _variables.scss -``` - -In the new {file}`_relatedItems.scss`, let's fix the text color, and add background color to the `.inner-container` element with the variables `--primary-foreground-color` and `--theme-high-contrast-color`. Lastly, let's use `--link-foreground-color` for the related items links: - -```scss -.block.relatedItems { - .inner-container { - background: var(--theme-high-contrast-color); - padding: 3rem; - - h2.headline { - color: var(--primary-foreground-color); - } - - ul.items-list { - color: var(--primary-foreground-color); - li a { - color: var(--link-foreground-color); - } - } - } -} -``` - -And now we need to add {file}`_relatedItems.scss` in {file}`_main_.scss`: - -```scss -@import 'blocks/relatedItems'; -@import 'variables'; -``` - -## Enhancing a block schema - -To be able to use the Block Width widget with our Related Items block, we need to add it to the block schema. -To do this we'll use a `schemaEnhancer`. A schema enhancer is a function that receives an object with `formData` (the block `data`), the `schema` (the original schema that we want to tweak), and the injected `intl` (to aid with internationalization). - -Usually we would want to keep the schema enhancers in individual files per block, after which they can be imported to the {file}`index.js`. For this example we'll leave everything in the same file: - -```js -import { defineMessages } from 'react-intl'; -import { composeSchema } from '@plone/volto/helpers/Extensions'; -import { defaultStylingSchema } from '@kitconcept/volto-light-theme/components/Blocks/schema'; -import { addStyling } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer'; - -const messages = defineMessages({ - BlockWidth: { - id: 'Block Width', - defaultMessage: 'Block Width', - }, -}); - -const applyConfig = (config) => { - config.settings = { - ...config.settings, - isMultilingual: false, - supportedLanguages: ['en'], - defaultLanguage: 'en', - }; - - config.blocks.themes = [ - { - style: { - '--theme-color': 'black', - '--theme-high-contrast-color': 'darkslategrey', - '--theme-foreground-color': 'lemonchiffon', - '--theme-low-contrast-foreground-color': 'lightgrey', - }, - name: 'default', - label: 'Default', - }, - { - style: { - '--theme-color': 'darkslategrey', - '--theme-high-contrast-color': 'black', - '--theme-foreground-color': 'lemonchiffon', - '--theme-low-contrast-foreground-color': 'lightgrey', - }, - name: 'green', - label: 'Green', - }, - ]; - - const relatedItemsEnhancer = ({ formData, schema, intl }) => { - addStyling({ schema, intl }); - - schema.properties.styles.schema.fieldsets[0].fields = [ - 'blockWidth:noprefix', - ...schema.properties.styles.schema.fieldsets[0].fields, - ]; - schema.properties.styles.schema.properties['blockWidth:noprefix'] = { - widget: 'blockWidth', - title: intl.formatMessage(messages.BlockWidth), - default: 'default', - filterActions: ['narrow', 'default'], - }; - return schema; - }; - - config.blocks.blocksConfig.relatedItems = { - ...config.blocks.blocksConfig.relatedItems, - schemaEnhancer: composeSchema(defaultStylingSchema, relatedItemsEnhancer), - }; - - return config; -}; - -export default applyConfig; -``` - -Finally, let's wire the width classes for our block in the file {file}`_relatedItems.scss`: - -```scss -.block.relatedItems { - margin-right: auto; - margin-left: auto; - - .inner-container { - background: var(--theme-high-contrast-color); - padding: 3rem; - - h2.headline { - color: var(--primary-foreground-color); - } - - ul { - color: var(--primary-foreground-color); - li a { - color: var(--link-foreground-color); - } - } - } - - &.has--block-width--narrow { - max-width: var(--narrow-container-width) !important; - } - - &.has--block-width--default { - max-width: var(--default-container-width) !important; - } -} - -.block-editor-relatedItems { - &.has--block-width--narrow .block .block .block { - max-width: var(--narrow-container-width) !important; - } - - &.has--block-width--default .block .block .block { - max-width: var(--default-container-width) !important; - } -} -``` - - -## Set themes for one block - -To demonstrate this feature, we'll add a custom list of themes just for the Related Items block, which will include one more theme called `Blue`. Add the property `themes` to the `relatedItems` block in the `blocksConfig` object: - -```js -import { defineMessages } from 'react-intl'; -import { composeSchema } from '@plone/volto/helpers/Extensions'; -import { defaultStylingSchema } from '@kitconcept/volto-light-theme/components/Blocks/schema'; -import { addStyling } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer'; - -const messages = defineMessages({ - BlockWidth: { - id: 'Block Width', - defaultMessage: 'Block Width', - }, -}); - -const applyConfig = (config) => { - config.settings = { - ...config.settings, - isMultilingual: false, - supportedLanguages: ['en'], - defaultLanguage: 'en', - }; - - config.blocks.themes = [ - { - style: { - '--theme-color': 'black', - '--theme-high-contrast-color': 'darkslategrey', - '--theme-foreground-color': 'lemonchiffon', - '--theme-low-contrast-foreground-color': 'lightgrey', - }, - name: 'default', - label: 'Default', - }, - { - style: { - '--theme-color': 'darkslategrey', - '--theme-high-contrast-color': 'black', - '--theme-foreground-color': 'lemonchiffon', - '--theme-low-contrast-foreground-color': 'lightgrey', - }, - name: 'green', - label: 'Green', - }, - ]; - - const relatedItemsEnhancer = ({ formData, schema, intl }) => { - addStyling({ schema, intl }); - - schema.properties.styles.schema.fieldsets[0].fields = [ - 'blockWidth:noprefix', - ...schema.properties.styles.schema.fieldsets[0].fields, - ]; - schema.properties.styles.schema.properties['blockWidth:noprefix'] = { - widget: 'blockWidth', - title: intl.formatMessage(messages.BlockWidth), - default: 'default', - filterActions: ['narrow', 'default'], - }; - return schema; - }; - - config.blocks.blocksConfig.relatedItems = { - ...config.blocks.blocksConfig.relatedItems, - schemaEnhancer: composeSchema(defaultStylingSchema, relatedItemsEnhancer), - themes: [ - ...config.blocks.themes, - { - style: { - '--theme-color': 'midnightblue', - '--theme-high-contrast-color': 'black', - '--theme-foreground-color': 'lemonchiffon', - '--theme-low-contrast-foreground-color': 'lightgrey', - }, - name: 'blue', - label: 'Blue', - }, - ], - }; - - return config; -}; - -export default applyConfig; -``` - -## Conclusion - -Understanding how to extend VLT will help you take advantage of the system and quickly create consistent and flexible designs. diff --git a/docs/mastering-plone/events.md b/docs/mastering-plone/events.md index 3d4fe99bd..7d40ed9df 100644 --- a/docs/mastering-plone/events.md +++ b/docs/mastering-plone/events.md @@ -218,7 +218,7 @@ This trick does not yet work in Volto because some CSS classes are still missing Modify {file}`frontend/theme/extras/custom.overrides` and add: -```less +```css /* Hide date fields from contributors */ body.userrole-contributor { #default-start.field, diff --git a/styles/config/vocabularies/Plone/accept.txt b/styles/config/vocabularies/Plone/accept.txt index 52aeee0e9..3b8bae882 100644 --- a/styles/config/vocabularies/Plone/accept.txt +++ b/styles/config/vocabularies/Plone/accept.txt @@ -17,6 +17,7 @@ Brehault buildout bundler Casali +Chakra CMFCore configlet Cookiecutter @@ -117,6 +118,7 @@ pdb pdbpp PDF Pellegrini +Petch PhantomJS Philipp piwik @@ -165,6 +167,7 @@ REST reusability reutilization RichText +Robotarium Rohberg Rossum Runyan @@ -181,6 +184,8 @@ Steffen subclassing subcomponent subfolders +subsite +subsites subtemplates sudo superset @@ -220,6 +225,7 @@ viewlet Virtualbox virtualenv VMWare +VLT Volto von VSCode