Maestro is a library for building front-end component libraries for Drupal using Storybook.
It includes a collection of utilities for bundling Twig templates and JavaScript files so that you can develop components in isolation using the same syntax and conventions you'd use in a Drupal theme.
- Maestro makes a smaller, more flexible, but lower-level alternative to Wingsuit
- It takes a lot of inspiration from vite-plugin-twig-drupal
Before using Maestro, you should have basic knowledge of the following:
npm i @ilo-org/maestro
The Maestro package has three major exports.
@ilo-org/maestro
- The main export that introduces aMaestro
object@ilo-org/maestro/storybook
- The storybook decorators/helpers@ilo-org/maestro/plugin
- A vite plugin that helps to compile the twig components
To start with Maestro, you need a html
storybook template using Vite as a builder tool
npx storybook@latest init --type="html"
After your Storybook project is scaffolded, you can start adding Maestro.
First, we need to add the vite plugin from Maestro to Storybook's vite.config.js
// vite.config.js
import { defineConfig } from "vite";
import { join } from "node:path";
import { UIPatterns } from "@ilo-org/maestro/plugin";
export default defineConfig({
plugins: UIPatterns({
namespaces: {
components: join(__dirname, "..", "src/components"),
},
}),
});
The Vite plugin takes the following properties.
property | description | required |
---|---|---|
namespaces |
A key-value pair of the namespace for your components and the directory it should point to | Yes |
globals |
A key-value pair of global variables that should be available to the Twig templates | No |
dynamics |
An array of Twig templates names that use dynamic includes. See more info below. | No |
To enable Drupal's JS API in the storybook, you need first to add Behavior Decorator
to the storybook's preview file
// .storybook/preview.js
import { BehaviorDecorator } from "@ilo-org/maestro/storybook";
/** @type { import('@storybook/html').Preview } */
const preview = {
decorators: [BehaviorDecorator],
};
export default preview;
and then you need to inject the head
property to the storybook config and merge the vite config with the plugin mentioned above
// .storybook/vite.config.js
import { mergeConfig } from "vite";
import alternate from "./vite.config.js";
import { head } from "@ilo-org/maestro/storybook";
/** @type { import('@storybook/html-vite').StorybookConfig } */
const config = {
stories: [
"../stories/**/*.mdx",
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
],
staticDirs: ["../public"],
core: {
builder: "@storybook/builder-vite",
},
addons: [...],
framework: {
name: getAbsolutePath("@storybook/html-vite"),
options: {},
},
viteFinal: (config) => {
return mergeConfig(alternate, config);
},
previewHead: head,
};
export default config;
Let's assume that your components are structured something like this:
📦accordion
┣ 📜accordion.behavior.js
┣ 📜accordion.component.yml
┣ 📜accordion.js
┣ 📜accordion-item.twig
┗ 📜accordion.twig
Here we have a Twig template, a JS file for interactivity, a behaviors file for attaching the JavaScript to the component instance and a pattern definitions file accordion.component.yml
that determines the fields and settings that can be passed from Drupal to the component.
To create a story you need to use the main Maestro
object. Maestro has a create
method that accepts the following:
- component (actual twig component)
- pattern definition (ui pattern definition in yaml)
And in return, you get the object.
{
meta: {...}
stories: [...],
}
The stories key will always have 1 element, which is the Default story, and if the pattern definition has any variants, you will get all other stories in an array
Here's an example of how to create a story
import Accordion from "../components/accordion/accordion.twig";
import AccordionPatterns from "../components/accordion/accordion.pattern.yml";
import { Maestro } from "@ilo-org/maestro";
const story = Maestro.create(Accordion, AccordionPatterns);
const Meta = {
title: "Components/Content/Accordion",
tags: ["autodocs"],
...story.meta,
};
const [Default, Scrollable, Focus] = story.stories;
export default Meta;
export { Default, Scrollable, Focus };
In this example, Maestro builds a component using the Twig template and then derives stories to create based on the pattern definitions file.
Any JavaScript is loaded and attached to the component automatically.
Twig includes work automatically. For example, assuming our accordion.twig
component uses an accordion-item.twig
component, then it would look like this:
<ul>
{% for item in items %} {% include "@components/accordion/accordion-item.twig"
with { label: item.label ... } %} {% endfor %}
</ul>
In this example, Maestro will import the accordion-item.twig
template into the accordion.twig
template so that it can be rendered correctly.
Dynamic includes are small caveat to this rule and require some additional setup. Consider the following example.
{# my-dynamic-component.twig #}
<div>
{% for item in items %}
{% include '@components/' ~ item.component ~ '/' ~ item.component ~ '.twig' with item.componentdata %}
{% endfor %}
</div>
Here, the component is importing sub-component dynamically based on the arguments passed to it. Maestro can handle this situation as well with some additional configuration to its Vite plugin.
// vite.config.js
export default defineConfig({
plugins: UIPatterns({
...
dynamics: ["my-dynamic-component"]
}),
});
Simply pass an array of components that use dynamic imports to the dynamics
property of the Vite plugin and you're off to the races.
There are a few rules for variants:
- Variants will inherit any setting from the default definition
- Variants named default will be ignored because Default is generated by the pattern definition itself
Maestro is a package of the ILO Design System, which is an open-source project. We welcome your contributions! Before submitting a pull request, please take a moment to review our Contribution Guidelines.
Maestro is licensed under the Apache 2.0 License.