Skip to content
Merged
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
222 changes: 222 additions & 0 deletions .plans/storybook-foundations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# Foundations Documentation Layer — Storybook

## Context

The webkit monorepo has 81 component stories but zero design system foundations documentation. The theme package has a complete 3-layer token system (Primitives → Semantic → CSS Vars → Tailwind), and the icons package has 402 icons with a catalog.json. This plan adds a `Foundations/` category in Storybook documenting Colors, Typography, Spacing, and Iconography — sourced dynamically from the existing packages, with reusable Vue 3 documentation components.

---

## Architecture Decisions

- **CSF2 `.stories.js` over MDX** — consistent with the 81 existing stories; MDX has no established pattern in this codebase
- **Dynamic token data** — `createCssVars()` from `@aziontech/theme/tokens` is called once at module load to get all resolved hex values; no hardcoding
- **Vue SFC components** — all documentation components are `.vue` files in `src/components/foundations/`
- **No new dependencies** — zero new `npm install` operations

---

## File Structure

### New files to create

```
apps/storybook/src/
├── foundations-data/
│ ├── tokens.js ← derives color data from @aziontech/theme/tokens
│ ├── typography.js ← mirrors semantic-texts-plugin.js values as JS array
│ └── spacing.js ← mirrors semantic-spacing-plugin.js values as JS array
├── components/foundations/
│ ├── ColorSwatch.vue ← single color chip (inline style for bg)
│ ├── ColorPalette.vue ← grid of swatches under a heading
│ ├── TokenTable.vue ← generic table, reused across all sections
│ ├── TypographyPreview.vue
│ ├── SpacingPreview.vue
│ └── IconGrid.vue ← searchable icon gallery
└── stories/foundations/
├── Colors.stories.js
├── Typography.stories.js
├── Spacing.stories.js
└── Iconography.stories.js
```

### Existing files to modify

```
apps/storybook/.storybook/preview.js → storySort.order: add 'Foundations'
```

---

## Data Extraction Strategy

### Colors
```js
// In foundations-data/tokens.js
import {
createCssVars,
primitives,
brandPrimitives,
surfacePrimitives,
textSemantic,
backgroundSemantic,
borderSemantic
} from '@aziontech/theme/tokens'

const { light, dark } = createCssVars()
// light/dark are flat objects: { '--text-default': '#111827', ... }
```

- **Primitive groups**: Walk `primitives` (gray, orange, violet, etc.) and `surfacePrimitives`
- **Brand groups**: Walk `brandPrimitives.primary`, `.accent`, `.absolute`
- **Semantic groups**: Walk `textSemantic.light` keys → look up `light['--text-' + key]` and `dark['--text-' + key]`

### Typography
Static JS array in `typography.js` mirroring the CSS values from `packages/theme/src/tailwind/semantic-texts-plugin.js`. Class name strings must appear as literals (not dynamically constructed) so Tailwind's scanner includes them.

### Spacing
Same approach as Typography — static array mirroring `semantic-spacing-plugin.js`.

### Icons
```js
import icons from '@aziontech/icons/catalog'
// [{ icon: 'ai ai-angular', name: 'ai-angular', keywords: '' }, ...]
```
Passed directly as a prop to `IconGrid.vue`. Filtering done via `computed` inside the component.

---

## Component APIs

### `ColorSwatch.vue`
```js
props: {
name: String, // 'gray-500'
value: String, // '#6b7280'
darkValue: String, // optional, for semantic tokens
cssVar: String, // optional, '--text-default'
size: { type: String, default: 'md' } // 'sm' | 'md' | 'lg'
}
```
Renders a color chip using **inline `style`** binding (not Tailwind class) for background. Shows both light/dark values simultaneously — no dependency on Storybook theme toggle.

### `ColorPalette.vue`
```js
props: {
title: String,
description: String,
tokens: Array // [{ name, value, darkValue?, cssVar? }]
}
```
Wraps `ColorSwatch` in a responsive grid (`grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3`).

### `TokenTable.vue`
```js
props: {
columns: Array, // [{ key, label, type? }] — type: 'swatch' renders inline ColorSwatch
rows: Array, // array of objects matching column keys
caption: String // optional
}
```
Generic table. The `type: 'swatch'` column type triggers inline `ColorSwatch` rendering via `v-if="col.type === 'swatch'"`. Used by: Colors (semantic section), Typography, Spacing.

### `TypographyPreview.vue`
```js
props: {
name, className, fontSize, lineHeight,
fontFamily, letterSpacing, textTransform, sample
}
```
Two-column row: left = metadata in `font-mono text-body-xs`, right = sample text with `:class="className"` applied.

### `SpacingPreview.vue`
```js
props: {
name, className, property,
desktopValue, tabletValue, mobileValue, description
}
```
Visual demo using **inline `style`** (not the semantic class) to isolate the spacing value. Below it, a 3-column responsive values table.

### `IconGrid.vue`
```js
props: {
icons: Array, // from @aziontech/icons/catalog
initialSize: { type: Number, default: 24 }
}
```
Internal state: `searchQuery ref`, `iconSize ref`. `filteredIcons` computed. Split into `azionIcons` (ai prefix) and `primeIcons` (pi prefix). Plain `<input type="text">` for search, `<input type="range" min="12" max="64" step="4">` for size. Grid: `grid-cols-4 md:grid-cols-6 lg:grid-cols-8`. No copy or download in Phase 1.

---

## Story File Structure

Each story uses `title: 'Foundations/[Name]'` and `parameters: { layout: 'fullscreen' }`.

| Story file | Named exports (sidebar entries) |
|---|---|
| `Colors.stories.js` | `PrimitiveColors`, `BrandColors`, `SemanticColors` |
| `Typography.stories.js` | `TypeScale` |
| `Spacing.stories.js` | `SemanticSpacing` |
| `Iconography.stories.js` | `AllIcons` (with `argTypes.initialSize` control) |

---

## `preview.js` Change

```js
// Line 79 — current:
order: ['Introduction', 'Core', 'Components']

// Updated:
order: ['Introduction', 'Foundations', 'Core', 'Components', 'Site']
```

---

## Delivery Phases

### Phase 1 — Colors ← START HERE
Creates the entire infrastructure. All subsequent phases reuse it.

1. `foundations-data/tokens.js`
2. `components/foundations/ColorSwatch.vue`
3. `components/foundations/ColorPalette.vue`
4. `components/foundations/TokenTable.vue`
5. `stories/foundations/Colors.stories.js`
6. Modify `preview.js` → update `storySort.order`

### Phase 2 — Typography
1. `foundations-data/typography.js`
2. `components/foundations/TypographyPreview.vue`
3. `stories/foundations/Typography.stories.js`

### Phase 3 — Spacing
1. `foundations-data/spacing.js`
2. `components/foundations/SpacingPreview.vue`
3. `stories/foundations/Spacing.stories.js`

### Phase 4 — Iconography
1. `components/foundations/IconGrid.vue`
2. `stories/foundations/Iconography.stories.js`

---

## Verification

- `pnpm dev` inside `apps/storybook/` → navigate to Foundations/ in sidebar
- Colors: verify primitive palettes render, semantic table shows correct hex values for both light/dark
- Toggle light/dark theme → ColorSwatch values remain static (by design), Storybook UI theme changes
- Typography: verify sample text renders in correct style (check font size/weight visually)
- Iconography: search filters icons reactively; size slider updates all icons
- No new lint errors; no new npm packages added

---

## Critical Files Reference

- `apps/storybook/.storybook/preview.js` — modify `storySort.order`
- `packages/theme/src/tokens/build/css-vars.js` — `createCssVars()` source
- `packages/theme/src/tokens/index.js` — re-exports all token primitives and utilities
- `packages/theme/src/tailwind/semantic-texts-plugin.js` — typography CSS values
- `packages/theme/src/tailwind/semantic-spacing-plugin.js` — spacing CSS values
- `packages/icons/dist/catalog.json` — icon manifest (87 ai + 315 pi)
4 changes: 2 additions & 2 deletions apps/storybook/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ export const parameters = {
},
options: {
storySort: {
method: 'alphabetical',
order: ['Introduction', 'Core', 'Components']
order: ['Foundations', ['Colors', 'Spacing', 'Typography', 'Icons'], 'Primevue', 'Primevue', 'Components', 'Site']
}
}
};
Expand All @@ -90,3 +89,4 @@ export const decorators = [
defaultTheme: 'dark',
})
]

157 changes: 157 additions & 0 deletions apps/storybook/src/foundations-data/tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* Color token data for Foundations/Colors documentation.
* Dynamically sourced from @aziontech/theme/tokens — no hardcoded values.
*/

import {
createCssVars,
primitives,
brandPrimitives,
surfacePrimitives,
textSemantic,
backgroundSemantic,
borderSemantic,
} from '@aziontech/theme/tokens'

const { light, dark } = createCssVars()

// ─── Usage descriptions ──────────────────────────────────────────────────────

const textUsage = {
default: 'Primary text content',
muted: 'Secondary / supporting text',
link: 'Interactive link text',
code: 'Code and monospace text',
linkHover: 'Hovered link text',
primary: 'Brand primary — CTAs and highlights',
primaryHover: 'Hovered primary text',
accent: 'Accent / violet highlight',
accentHover: 'Hovered accent text',
danger: 'Error and destructive states',
dangerHover: 'Hovered danger text',
warning: 'Warning / caution states',
warningHover: 'Hovered warning text',
success: 'Success and positive states',
successHover: 'Hovered success text',
}

const backgroundUsage = {
surfaceRaised: 'Elevated surface — cards, panels',
surfaceOverlay: 'Floating overlay — tooltips, dropdowns',
surface: 'Base surface — page body',
canvas: 'Outermost page canvas',
primary: 'Brand primary background',
primaryHover: 'Hovered primary background',
danger: 'Error / danger background',
dangerHover: 'Hovered danger background',
warning: 'Warning background',
warningHover: 'Hovered warning background',
success: 'Success background',
successHover: 'Hovered success background',
backdrop: 'Modal / dialog backdrop overlay',
}

const borderUsage = {
default: 'Standard divider border',
subtle: 'Subtle / low-contrast border',
strong: 'High-contrast border',
primary: 'Brand primary border',
primaryHover: 'Hovered primary border',
accent: 'Accent brand border',
accentHover: 'Hovered accent border',
danger: 'Error / destructive border',
dangerHover: 'Hovered danger border',
warning: 'Warning border',
warningHover: 'Hovered warning border',
success: 'Success border',
successHover: 'Hovered success border',
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

function buildSemanticRows(semanticMap, cssVarPrefix, tailwindPrefix, usageMap) {
return Object.keys(semanticMap.light).map((key) => ({
name: `${tailwindPrefix}${key}`,
cssVar: `--${cssVarPrefix}${key}`,
tailwindClass: `${tailwindPrefix}${key}`,
lightValue: light[`--${cssVarPrefix}${key}`] ?? 'N/A',
darkValue: dark[`--${cssVarPrefix}${key}`] ?? 'N/A',
usage: usageMap[key] ?? '',
}))
}

// ─── Primitive color groups ───────────────────────────────────────────────────
// Ordered for display (most commonly used first)

const PRIMITIVE_ORDER = ['neutral', 'orange', 'violet', 'red', 'green', 'blue', 'yellow', 'gray', 'slate']

export const primitiveColorGroups = PRIMITIVE_ORDER
.filter((family) => primitives[family])
.map((family) => ({
group: family.charAt(0).toUpperCase() + family.slice(1),
tokens: Object.entries(primitives[family]).map(([shade, value]) => ({
name: `${family}-${shade}`,
value,
})),
}))

// ─── Surface scale ────────────────────────────────────────────────────────────

export const surfaceColorGroup = {
group: 'Surface (Neutral aliases)',
description: 'Surface tokens alias the neutral palette and form the layered elevation system.',
tokens: Object.entries(surfacePrimitives.surface).map(([shade, value]) => ({
name: `surface-${shade}`,
value,
})),
}

// ─── Brand color groups ───────────────────────────────────────────────────────

export const brandColorGroups = [
{
group: 'Primary (Orange)',
description: 'Used for interactive elements, CTAs, and brand accents.',
tokens: Object.entries(brandPrimitives.primary).map(([shade, value]) => ({
name: `primary-${shade}`,
value,
})),
},
{
group: 'Accent (Violet)',
description: 'Used for secondary highlights and accent states.',
tokens: Object.entries(brandPrimitives.accent).map(([shade, value]) => ({
name: `accent-${shade}`,
value,
})),
},
{
group: 'Absolute',
description: 'Fixed white/black extremes — always the same in both themes.',
tokens: Object.entries(brandPrimitives.absolute).map(([name, value]) => ({
name,
value,
})),
},
]

// ─── Semantic color groups ────────────────────────────────────────────────────
// Resolved hex values for both light and dark themes.

export const semanticColorGroups = [
{
group: 'Text',
description: 'Consumed via Tailwind as `text-{name}`. Controls font color.',
rows: buildSemanticRows(textSemantic, 'text-', 'text-', textUsage),
},
{
group: 'Background',
description: 'Consumed via Tailwind as `bg-{name}`. Controls background color.',
rows: buildSemanticRows(backgroundSemantic, 'background-', 'bg-', backgroundUsage),
},
{
group: 'Border',
description: 'Consumed via Tailwind as `border-{name}`. Controls border color.',
rows: buildSemanticRows(borderSemantic, 'border-', 'border-', borderUsage),
},
]
Loading
Loading