Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .storybook/data-themes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Auto-generated theme configuration from *.ui_skins.themes.yml files
// This file is automatically generated - do not edit manually

// Theme target mapping
const themeTargets = {
'cyberpunk': 'body',
'forest': 'html',
'garden': 'html'
};

// Add a decorator to set data-theme on the correct target element
export const themeDecorators = [
(storyFn, context) => {
// Wait for the target element to be available
setTimeout(() => {
const selectedTheme = context.globals.theme || 'cyberpunk';
const targetSelector = themeTargets[selectedTheme] || 'body';

// Remove data-theme from both html and body to clean previous themes
const html = document.querySelector('html');
const body = document.querySelector('body');
if (html) html.removeAttribute('data-theme');
if (body) body.removeAttribute('data-theme');

// Apply the new theme to the correct target
const target = document.querySelector(targetSelector);
if (target) {
target.setAttribute('data-theme', selectedTheme);
}
}, 0);
return storyFn();
},
];

export const themeGlobalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'cyberpunk',
toolbar: {
icon: 'paintbrush',
items: [
{ value: 'cyberpunk', title: 'Cyberpunk' },
{ value: 'forest', title: 'Forest' },
{ value: 'garden', title: 'Garden' }
],
},
},
};
12 changes: 12 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import type { Preview } from '@storybook/html'

import { themeDecorators, themeGlobalTypes } from './data-themes'

export const decorators = [
...themeDecorators, // Theme decorators
// Your other decorators
]

export const globalTypes = {
...themeGlobalTypes, // Theme global types
// Your other global types
}

const preview: Preview = {
parameters: {
controls: {
Expand Down
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This addon streamlines the integration of Drupal Single Directory Components (SD
- [Features of the Addon](#features-of-the-addon)
- [Quickstart Guide](#quickstart-guide)
- [Configuration](#configuration)
- [Automatic Theme Generation](#automatic-theme-generation)
- [Creating Experimental Stories](#creating-experimental-stories)
- [Support for Single Story Files (\*.story.yml)](#support-for-single-story-files-storyyml)
- [Namespaces](#namespaces)
Expand Down Expand Up @@ -645,6 +646,79 @@ Addon supports UI Patterns `library_wrapper` [for wrapping stories](https://proj

- Custom Twig filters and functions are not supported ([UI Patterns TwigExtension](https://git.drupalcode.org/project/ui_patterns/-/blob/8.x-1.x/src/Template/TwigExtension.php)).

## UI Skins

The addon supports ui_skins yml definition and creates Storybook theme decorators and global types from `*.ui_skins.themes.yml` files. This allows you to easily switch between different data-themes in Storybook.

### How it works

The addon automatically scans for `*.ui_skins.themes.yml` files and generates a `data-themes.ts` file in your `.storybook` directory. This file contains theme decorators and global types that can be imported into your `preview.ts`.

### Theme Configuration

Create a `*.ui_skins.themes.yml` file in your project root with the following structure:

```yaml
# Theme configuration example
cyberpunk:
label: "Cyberpunk"
label_context: "color"
key: "data-theme"
target: body

forest:
label: "Forest"
label_context: "color"
key: "data-theme"
target: html

garden:
label: "Garden"
label_context: "color"
key: "data-theme"
target: html
```

### Configuration Options

See UI skins for options

### Using Themes

After the 'data-themes.ts' is generated, you can import them into your `.storybook/preview.ts`:

```typescript
import type { Preview } from '@storybook/html'
import { themeDecorators, themeGlobalTypes } from './data-themes'

export const decorators = [
...themeDecorators, // Theme decorators
// Your other decorators
]

export const globalTypes = {
...themeGlobalTypes, // Theme global types
// Your other global types
}

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
}

export default preview
```

### CSS Integration

The data-themes work with CSS custom properties. You can define theme-specific styles in your component CSS. See card component for exemple

## Why Choose SDC Storybook Over Alternatives?

While solutions like [SDC Styleguide](https://www.drupal.org/project/sdc_styleguide) and [Drupal Storybook](https://www.drupal.org/project/storybook) are valuable, the SDC Storybook addon offers distinct advantages:
Expand Down
17 changes: 15 additions & 2 deletions components/card/card.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
[data-theme="cyberpunk"] {
--color-primary: rgb(0, 255, 255);
--color-primary-content: rgb(0, 0, 0);
}
[data-theme="forest"] {
--color-primary: rgb(2, 39, 2);
--color-primary-content: rgb(255, 255, 255);
}
[data-theme="garden"] {
--color-primary: rgb(50, 205, 50);
--color-primary-content: rgb(0, 0, 0);
}
.umami-card {
padding: 1rem;
border: 1px solid lightgray;
background-color: #fff;
border: 1px solid var(--color-primary-content, lightgray);
background-color: var(--color-primary, #fff );
color: var(--color-primary-content);
border-radius: 0.5rem;
}

Expand Down
20 changes: 20 additions & 0 deletions sdc-addon.ui_skins.themes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# No value because same as plugin ID.
cyberpunk:
label: "Cyberpunk"
label_context: "color"
key: "data-theme"
target: body

# No value because same as plugin ID.
forest:
label: "Forest"
label_context: "color"
key: "data-theme"
target: html

# No value because same as plugin ID.
garden:
label: "Garden"
label_context: "color"
key: "data-theme"
target: html
142 changes: 142 additions & 0 deletions src/dataThemesGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { readFile, writeFile } from 'node:fs/promises'
import { glob } from 'glob'
import { parse } from 'yaml'
import { join } from 'node:path'

export interface ThemeConfig {
key: string
label: string
value: string
target: string
}

/**
* Generate Storybook data-themes.ts configuration from *.ui_skins.themes.yml files
*/
export async function generateThemes(): Promise<void> {
console.log('🎨 Generating theme configuration from *.ui_skins.themes.yml files...')

try {
// Find all theme files
const themeFiles = await glob('**/*.ui_skins.themes.yml', { cwd: process.cwd() })

if (themeFiles.length === 0) {
console.log('⚠️ No *.ui_skins.themes.yml files found')
return
}

console.log(`📁 Found ${themeFiles.length} theme file(s):`, themeFiles)

const themes: ThemeConfig[] = []

// Parse each theme file
for (const themeFile of themeFiles) {
try {
const content = await readFile(themeFile, 'utf8')
const themeData = parse(content)

// Extract theme information
for (const [themeKey, themeConfig] of Object.entries(themeData)) {
if (typeof themeConfig === 'object' && themeConfig !== null) {
themes.push({
key: themeKey,
label: (themeConfig as any).label || themeKey,
value: themeKey,
target: (themeConfig as any).target || 'body'
})
}
}

console.log(`✅ Parsed theme file: ${themeFile}`)
} catch (error) {
console.error(`❌ Error parsing ${themeFile}:`, (error as Error).message)
}
}

if (themes.length === 0) {
console.log('⚠️ No themes found in the files')
return
}

console.log(`🎯 Found ${themes.length} theme(s):`, themes.map(t => t.key))

// Generate the themes configuration file
const themesConfigContent = generateThemesConfig(themes)
const themesConfigPath = join(process.cwd(), '.storybook', 'data-themes.ts')
await writeFile(themesConfigPath, themesConfigContent, 'utf8')

console.log(`✅ Generated data-themes.ts with ${themes.length} theme(s)`)
console.log(`📝 Updated: ${themesConfigPath}`)
console.log(`💡 To use themes, add to your preview.ts:`)
console.log(` import { themeDecorators, themeGlobalTypes } from './data-themes'`)
console.log(` Then add ...themeDecorators to decorators and ...themeGlobalTypes to globalTypes`)

} catch (error) {
console.error('❌ Error generating themes:', error)
throw error
}
}

/**
* Generate the themes configuration file
*/
export function generateThemesConfig(themes: ThemeConfig[]): string {
const themeItems = themes.map(theme =>
` { value: '${theme.value}', title: '${theme.label}' }`
).join(',\n')

const defaultTheme = themes.length > 0 ? themes[0].value : 'default'
const defaultTarget = themes.length > 0 ? themes[0].target : 'body'

// Create theme target mapping
const themeTargets = themes.map(theme =>
` '${theme.value}': '${theme.target}'`
).join(',\n')

return `// Auto-generated theme configuration from *.ui_skins.themes.yml files
// This file is automatically generated - do not edit manually

// Theme target mapping
const themeTargets = {
${themeTargets}
};

// Add a decorator to set data-theme on the correct target element
export const themeDecorators = [
(storyFn, context) => {
// Wait for the target element to be available
setTimeout(() => {
const selectedTheme = context.globals.theme || '${defaultTheme}';
const targetSelector = themeTargets[selectedTheme] || '${defaultTarget}';

// Remove data-theme from both html and body to clean previous themes
const html = document.querySelector('html');
const body = document.querySelector('body');
if (html) html.removeAttribute('data-theme');
if (body) body.removeAttribute('data-theme');

// Apply the new theme to the correct target
const target = document.querySelector(targetSelector);
if (target) {
target.setAttribute('data-theme', selectedTheme);
}
}, 0);
return storyFn();
},
];

export const themeGlobalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: '${defaultTheme}',
toolbar: {
icon: 'paintbrush',
items: [
${themeItems}
],
},
},
};
`
}
2 changes: 2 additions & 0 deletions src/preset.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import YamlStoriesPlugin, {
yamlStoriesIndexer,
} from './vite-plugin-storybook-yaml-stories.ts'
import vitePluginThemeGenerator from './vite-plugin-theme-generator.ts'
import { mergeConfig } from 'vite'
import type { UserConfig } from 'vite'
import type { Indexer } from 'storybook/internal/types'
Expand Down Expand Up @@ -52,6 +53,7 @@ export async function viteFinal(config: UserConfig, options: SDCAddonOptions) {
include: ['buffer', 'stream', 'path'],
}),
...(twigPlugin ? [twigPlugin] : []),
vitePluginThemeGenerator(),
YamlStoriesPlugin({ ...options, globalDefs, namespaces }),
],
optimizeDeps: {
Expand Down
Loading