stencil-atomic-design-system
8
总安装量
4
周安装量
#34467
全站排名
安装命令
npx skills add https://github.com/corlab-tech/skills --skill stencil-atomic-design-system
Agent 安装分布
opencode
3
claude-code
3
windsurf
3
gemini-cli
3
github-copilot
2
junie
2
Skill 文档
Stencil.js Atomic Design System
Build a scalable, themeable design system using Stencil.js with Atomic Design principles, design tokens, and slot-based composition patterns for multi-client deployments.
Project Structure
design-respec/
âââ src/
â âââ components/
â â # Atoms - Basic building blocks
â â âââ cor-button/
â â â âââ cor-button.tsx
â â â âââ cor-button.css
â â â âââ cor-button.constants.ts
â â â âââ cor-button.enums.ts
â â â âââ cor-button.stories.ts
â â â âââ readme.md
â â â âââ test/
â â â âââ cor-button.spec.tsx
â â âââ cor-icon/
â â âââ cor-typography/
â â âââ cor-separator/
â â # Molecules - Combined components
â â âââ cor-input-field/
â â âââ cor-toggle/
â â âââ cor-search-bar/
â â # Organisms - Complex UI structures
â â âââ cor-card/
â â âââ cor-navbar/
â â âââ cor-form/
â â # Templates - Page structures
â â âââ cor-page-layout/
â â âââ cor-grid/
â âââ assets/
â â âââ css/
â â âââ index.css # Main CSS imports
â â âââ base/ # Base HTML styles
â â âââ utilities/ # Utility classes
â âââ utils/
â âââ theme-utils.ts
âââ tokens/
â âââ core/ # Core design tokens
â â âââ color.tokens.json
â â âââ font.tokens.json
â â âââ space.tokens.json
â â âââ radius.tokens.json
â â âââ shadow.tokens.json
â â âââ border.tokens.json
â â âââ components/ # Component-specific tokens
â â âââ style-dictionary.config.json
â âââ respec/ # RESPEC-specific tokens
â â âââ [theme files]
â âââ [client-name]/ # Client-specific tokens
â â âââ [theme overrides]
â âââ generated/ # Auto-generated CSS
â âââ *.css
âââ stencil.config.ts
âââ package.json
1. Atomic Design Implementation
Atoms – Foundational Elements
// src/components/cor-button/cor-button.tsx
import { Component, Prop, h, Host } from '@stencil/core';
@Component({
tag: 'cor-button',
styleUrl: 'cor-button.css',
shadow: true,
})
export class CorButton {
/**
* Button variant - defines the visual style
*/
@Prop() variant: 'primary' | 'secondary' | 'tertiary' = 'primary';
/**
* Button size
*/
@Prop() size: 'small' | 'medium' | 'large' = 'medium';
/**
* Disabled state
*/
@Prop() disabled: boolean = false;
render() {
return (
<Host
class={{
[`button--${this.variant}`]: true,
[`button--${this.size}`]: true,
'button--disabled': this.disabled,
}}
>
<button disabled={this.disabled} class="button">
<slot name="icon-left"></slot>
<span class="button__content">
<slot></slot>
</span>
<slot name="icon-right"></slot>
</button>
</Host>
);
}
}
/* cor-button.css - Component-scoped tokens */
:host {
/* Component-level tokens */
--button-padding-small: var(--spacing-xs) var(--spacing-sm);
--button-padding-medium: var(--spacing-sm) var(--spacing-md);
--button-padding-large: var(--spacing-md) var(--spacing-lg);
display: inline-block;
}
.button {
/* Use semantic tokens from generated CSS */
font-family: var(--font-family-pro-display);
border-radius: var(--radius-md);
transition: 250ms ease;
cursor: pointer;
border: none;
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
}
/* Size variants */
:host(.button--small) .button {
padding: var(--button-padding-small);
font-size: var(--font-size-sm);
}
:host(.button--medium) .button {
padding: var(--button-padding-medium);
font-size: var(--font-size-md);
}
:host(.button--large) .button {
padding: var(--button-padding-large);
font-size: var(--font-size-lg);
}
/* Variant styles using semantic tokens */
:host(.button--primary) .button {
background: var(--color-primary-background-default);
color: var(--color-primary-text-default);
border: var(--spacing-px) solid var(--color-primary-border-default);
}
:host(.button--primary) .button:hover {
background: var(--color-primary-background-hover);
border-color: var(--color-primary-border-hover);
}
:host(.button--secondary) .button {
background: var(--color-secondary-background-default);
color: var(--color-secondary-text-default);
border: var(--spacing-px) solid var(--color-secondary-border-default);
}
:host(.button--disabled) .button {
opacity: 0.38;
cursor: not-allowed;
}
Molecules – Combined Elements
// src/components/cor-input-field/cor-input-field.tsx
import { Component, Prop, State, Event, EventEmitter, h, Host } from '@stencil/core';
@Component({
tag: 'cor-input-field',
styleUrl: 'cor-input-field.css',
shadow: true,
})
export class CorInputField {
@Prop() label: string;
@Prop() value: string = '';
@Prop() error: string;
@Prop() helperText: string;
@Prop() required: boolean = false;
@Prop() disabled: boolean = false;
@State() focused: boolean = false;
@Event() valueChange: EventEmitter<string>;
private handleInput = (event: Event) => {
const value = (event.target as HTMLInputElement).value;
this.valueChange.emit(value);
};
render() {
return (
<Host
class={{
'input-field--focused': this.focused,
'input-field--error': !!this.error,
'input-field--disabled': this.disabled,
}}
>
<div class="input-field">
{this.label && (
<label class="input-field__label">
{this.label}
{this.required && <span class="input-field__required">*</span>}
</label>
)}
<div class="input-field__wrapper">
<slot name="prefix"></slot>
<input
class="input-field__input"
value={this.value}
onInput={this.handleInput}
onFocus={() => this.focused = true}
onBlur={() => this.focused = false}
disabled={this.disabled}
/>
<slot name="suffix"></slot>
</div>
{(this.error || this.helperText) && (
<div class="input-field__message">
{this.error ? this.error : this.helperText}
</div>
)}
</div>
</Host>
);
}
}
Organisms – Complex Structures
// src/components/cor-card/cor-card.tsx
import { Component, Prop, h, Host } from '@stencil/core';
@Component({
tag: 'cor-card',
styleUrl: 'cor-card.css',
shadow: true,
})
export class CorCard {
@Prop() elevated: boolean = false;
@Prop() interactive: boolean = false;
render() {
return (
<Host
class={{
'card--elevated': this.elevated,
'card--interactive': this.interactive,
}}
>
<article class="card">
<slot name="header"></slot>
<div class="card__body">
<slot></slot>
</div>
<slot name="footer"></slot>
</article>
</Host>
);
}
}
Templates – Page Structures
// src/components/cor-page-layout/cor-page-layout.tsx
import { Component, Prop, h, Host } from '@stencil/core';
@Component({
tag: 'cor-page-layout',
styleUrl: 'cor-page-layout.css',
shadow: true,
})
export class CorPageLayout {
@Prop() sidebarPosition: 'left' | 'right' = 'left';
@Prop() hasHeader: boolean = true;
@Prop() hasFooter: boolean = false;
render() {
return (
<Host class={`layout--sidebar-${this.sidebarPosition}`}>
<div class="layout">
{this.hasHeader && (
<header class="layout__header">
<slot name="header"></slot>
</header>
)}
<div class="layout__container">
<aside class="layout__sidebar">
<slot name="sidebar"></slot>
</aside>
<main class="layout__main">
<slot></slot>
</main>
</div>
{this.hasFooter && (
<footer class="layout__footer">
<slot name="footer"></slot>
</footer>
)}
</div>
</Host>
);
}
}
2. Design Tokens Framework
Three-Tier Token Hierarchy
Tokens are defined in JSON format and processed by Style Dictionary to generate CSS variables.
Core Tokens (Foundation)
/* tokens/core/color.tokens.json */
{
"color": {
"palette": {
"blue": {
"50": { "value": "#e3f2fd" },
"100": { "value": "#bbdefb" },
"200": { "value": "#90caf9" },
"300": { "value": "#64b5f6" },
"400": { "value": "#42a5f5" },
"500": { "value": "#2196f3" },
"600": { "value": "#1e88e5" },
"700": { "value": "#1976d2" },
"800": { "value": "#1565c0" },
"900": { "value": "#0d47a1" }
},
"neutral": {
"50": { "value": "#fafafa" },
"100": { "value": "#f5f5f5" },
"200": { "value": "#eeeeee" },
"300": { "value": "#e0e0e0" },
"400": { "value": "#bdbdbd" },
"500": { "value": "#9e9e9e" },
"600": { "value": "#757575" },
"700": { "value": "#616161" },
"800": { "value": "#424242" },
"900": { "value": "#212121" }
}
}
}
}
/* tokens/core/font.tokens.json */
{
"font": {
"family": {
"pro-display": {
"value": "SF Pro Display, -apple-system, BlinkMacSystemFont, sans-serif"
},
"pro-text": {
"value": "SF Pro Text, -apple-system, BlinkMacSystemFont, sans-serif"
},
"mono": {
"value": "SF Mono, Monaco, monospace"
}
},
"size": {
"xs": { "value": "0.75rem" },
"sm": { "value": "0.875rem" },
"md": { "value": "1rem" },
"lg": { "value": "1.125rem" },
"xl": { "value": "1.25rem" },
"2xl": { "value": "1.5rem" },
"3xl": { "value": "1.875rem" },
"4xl": { "value": "2.25rem" }
},
"weight": {
"light": { "value": "300" },
"regular": { "value": "400" },
"medium": { "value": "500" },
"semi-bold": { "value": "600" },
"bold": { "value": "700" }
}
},
"lineHeight": {
"tight": { "value": "1.25" },
"base": { "value": "1.5" },
"relaxed": { "value": "1.75" }
}
}
/* tokens/core/space.tokens.json */
{
"space": {
"px": { "value": "1px" },
"2xs": { "value": "0.125rem" },
"xs": { "value": "0.25rem" },
"sm": { "value": "0.5rem" },
"md": { "value": "0.75rem" },
"lg": { "value": "1rem" },
"xl": { "value": "1.5rem" },
"2xl": { "value": "2rem" },
"3xl": { "value": "2.5rem" },
"4xl": { "value": "3rem" },
"5xl": { "value": "4rem" },
"6xl": { "value": "5rem" },
"7xl": { "value": "6rem" }
}
}
/* tokens/core/radius.tokens.json */
{
"radius": {
"none": { "value": "0" },
"sm": { "value": "0.25rem" },
"md": { "value": "0.5rem" },
"lg": { "value": "0.75rem" },
"xl": { "value": "1rem" },
"2xl": { "value": "1.5rem" },
"3xl": { "value": "2rem" },
"full": { "value": "9999px" }
}
}
Component Tokens
/* tokens/core/components/button.tokens.json */
{
"button": {
"primary": {
"background": {
"default": { "value": "{color.palette.blue.600}" },
"hover": { "value": "{color.palette.blue.700}" },
"active": { "value": "{color.palette.blue.800}" },
"disabled": { "value": "{color.palette.blue.200}" }
},
"text": {
"default": { "value": "#ffffff" },
"disabled": { "value": "{color.palette.neutral.400}" }
},
"border": {
"default": { "value": "{color.palette.blue.600}" },
"hover": { "value": "{color.palette.blue.700}" }
}
},
"secondary": {
"background": {
"default": { "value": "transparent" },
"hover": { "value": "{color.palette.neutral.100}" },
"active": { "value": "{color.palette.neutral.200}" }
},
"text": {
"default": { "value": "{color.palette.neutral.900}" },
"disabled": { "value": "{color.palette.neutral.400}" }
},
"border": {
"default": { "value": "{color.palette.neutral.300}" },
"hover": { "value": "{color.palette.neutral.400}" }
}
}
}
}
Theme Support
/* src/tokens/semantic/themes.css */
/* Light theme (default) */
:root,
[data-theme="light"] {
--ds-color-background: white;
--ds-color-surface: white;
--ds-color-text-primary: var(--ds-color-gray-900);
--ds-color-text-secondary: var(--ds-color-gray-600);
}
/* Dark theme */
[data-theme="dark"] {
--ds-color-background: var(--ds-color-gray-900);
--ds-color-surface: var(--ds-color-gray-800);
--ds-color-text-primary: white;
--ds-color-text-secondary: var(--ds-color-gray-300);
--ds-border-color: var(--ds-color-gray-700);
}
Global CSS Setup
/* src/assets/css/index.css */
@import 'base/html.css';
@import 'utilities/scrollbars.css';
@import 'utilities/overlays.css';
/* Import generated tokens - these are auto-generated from JSON */
/* Tokens will be available as CSS variables like:
--color-primary-background-default
--font-family-pro-display
--spacing-md
--radius-lg
etc.
*/
Token Processing with Style Dictionary
/* tokens/core/style-dictionary.config.json */
{
"source": ["tokens/core/**/*.json"],
"platforms": {
"css": {
"transformGroup": "css",
"buildPath": "../generated/",
"files": [
{
"destination": "core.css",
"format": "css/variables"
}
]
}
}
}
3. Slot-Based Component Architecture
Why Slots Over Props
Slots provide better flexibility, composability, and performance for content projection:
// â Prop-based (avoid)
@Component({ tag: 'ds-button-bad' })
export class BadButton {
@Prop() label: string;
@Prop() iconLeft: string;
@Prop() iconRight: string;
render() {
return (
<button>
{this.iconLeft && <ds-icon name={this.iconLeft} />}
{this.label}
{this.iconRight && <ds-icon name={this.iconRight} />}
</button>
);
}
}
// â
Slot-based (preferred)
@Component({ tag: 'ds-button' })
export class Button {
render() {
return (
<button>
<slot name="icon-left"></slot>
<slot></slot>
<slot name="icon-right"></slot>
</button>
);
}
}
Slot Patterns
Default and Named Slots
@Component({ tag: 'ds-modal' })
export class Modal {
render() {
return (
<div class="modal">
<div class="modal__header">
<slot name="header"></slot>
</div>
<div class="modal__body">
<slot></slot> {/* Default slot */}
</div>
<div class="modal__footer">
<slot name="footer"></slot>
</div>
</div>
);
}
}
Usage:
<ds-modal>
<h2 slot="header">Modal Title</h2>
<p>This is the body content in the default slot</p>
<div slot="footer">
<ds-button>Cancel</ds-button>
<ds-button variant="primary">Confirm</ds-button>
</div>
</ds-modal>
Conditional Slot Rendering
@Component({ tag: 'ds-alert' })
export class Alert {
@Element() el: HTMLElement;
@State() hasIcon: boolean = false;
componentWillLoad() {
// Check if slot has content
this.hasIcon = !!this.el.querySelector('[slot="icon"]');
}
render() {
return (
<div class={{ 'alert': true, 'alert--has-icon': this.hasIcon }}>
{this.hasIcon && (
<div class="alert__icon">
<slot name="icon"></slot>
</div>
)}
<div class="alert__content">
<slot></slot>
</div>
</div>
);
}
}
Slot Fallback Content
@Component({ tag: 'ds-avatar' })
export class Avatar {
@Prop() name: string;
render() {
const initials = this.name
?.split(' ')
.map(word => word[0])
.join('')
.toUpperCase();
return (
<div class="avatar">
<slot>
{/* Fallback content when slot is empty */}
<span class="avatar__initials">{initials}</span>
</slot>
</div>
);
}
}
4. Multi-Client Theming Architecture
Theme Structure
tokens/
âââ core/ # Base design tokens
âââ respec/ # RESPEC-specific overrides
âââ [client-name]/ # Client-specific tokens
âââ generated/ # Auto-generated CSS from Style Dictionary
âââ core.css
âââ respec.css
âââ [client-name].css
Build Configuration
// stencil.config.ts
import { Config } from '@stencil/core';
import { reactOutputTarget as react } from '@stencil/react-output-target';
import { postcss } from '@stencil/postcss';
import postcssNested from 'postcss-nested';
export const config: Config = {
namespace: 'design-system',
srcDir: 'src',
globalStyle: 'src/assets/css/index.css',
outputTargets: [
{
type: 'dist',
esmLoaderPath: '../loader',
copy: [
{
src: '../tokens/generated/*.css',
dest: 'tokens',
warn: false,
},
],
},
{
type: 'dist-custom-elements',
copy: [
{
src: '**/*.{jpg,png}',
dest: 'dist/components/assets',
warn: true,
},
],
},
{
type: 'docs-readme',
},
{
type: 'www',
serviceWorker: null,
},
// Conditional React output
react({
outDir: '../react-design-system/src/components/stencil-generated/',
}),
],
plugins: [
postcss({
plugins: [postcssNested()],
}),
],
};
Build Scripts
// package.json
{
"scripts": {
"build": "npm run tokens:build && stencil build",
"build:react": "npm run tokens:build && stencil build --react",
"start": "stencil build --dev --watch --serve",
"test": "stencil test --spec --e2e",
"test:watch": "stencil test --spec --e2e --watchAll",
"tokens:build": "style-dictionary build --config ./tokens/core/style-dictionary.config.json",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
}
}
5. Component File Structure
Each component follows this structure:
src/components/cor-[component-name]/
âââ cor-[component-name].tsx # Component implementation
âââ cor-[component-name].css # Component styles
âââ cor-[component-name].enums.ts # Enums for props
âââ cor-[component-name].constants.ts # Constants
âââ cor-[component-name].stories.ts # Storybook stories
âââ readme.md # Auto-generated docs
âââ test/
âââ cor-[component-name].spec.tsx # Unit tests
âââ cor-[component-name].e2e.ts # E2E tests
6. CSS Variable Naming Convention
The project uses flat CSS variable naming without prefixes:
/* Generated from tokens */
--color-primary-background-default
--color-primary-background-hover
--font-family-pro-display
--font-size-md
--spacing-lg
--radius-md
--shadow-sm
--line-height-base
--font-weight-semi-bold
7. Best Practices
Component Development
- Use cor- prefix for all components
- Slot-based composition over props for content
- Enums for prop values in separate .enums.ts files
- Constants in separate .constants.ts files
- TypeScript strict mode for type safety
- CSS variables for all design tokens
- PostCSS with nested syntax for styles
Token Management
- JSON format for token definitions
- Style Dictionary for token processing
- Three-tier hierarchy: Core â Theme â Component
- Reference tokens using
{token.path}syntax - Auto-generate CSS from JSON tokens
Testing
- Unit tests with
@stencil/core/testing - E2E tests for user interactions
- Storybook stories for visual testing
- Test file naming:
*.spec.tsxfor unit,*.e2e.tsfor E2E
Documentation
- JSDoc comments for public APIs
- Auto-generated readme from component metadata
- Storybook for interactive documentation
- Component stories showing all variations