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
2 changes: 1 addition & 1 deletion apps/storybook/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const parameters = {
order: [
'Get Started',
'Foundations',
['Colors', 'Spacing', 'Typography', 'Icons'],
['Colors', 'Theme', 'Spacing', 'Typography', 'Icons'],
'Components',
'Site'
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script setup>
import { ref } from 'vue'

defineProps({
title: { type: String, required: true },
description: { type: String, default: '' },
/** Array of { name, description, kind, on }. */
items: { type: Array, default: () => [] }
})

const copiedKey = ref(null)
let copyTimeout = null

function copyToClipboard(value) {
if (!value) return
navigator.clipboard?.writeText(value).catch(() => {})
copiedKey.value = value
if (copyTimeout) clearTimeout(copyTimeout)
copyTimeout = setTimeout(() => {
copiedKey.value = null
}, 1000)
}

// The live token is passed as a CSS custom property, then consumed by the
// token utilities below. Storybook's Tailwind runs with `important: true`, so
// the color has to be a utility (bg-/text-/border-[var(--…)]) — inline styles
// would lose to the !important utilities on the same element.
function swatchVars(item) {
return {
'--swatch-color': `var(${item.name})`,
'--swatch-on': item.on ?? 'var(--bg-surface)'
}
}

// Swatch renders the live token, so it follows the Storybook theme toggle.
function swatchKindClass(item) {
if (item.kind === 'text') {
return 'bg-[var(--swatch-on)] text-[var(--swatch-color)] border-2 border-solid border-[var(--border-muted)]'
}
if (item.kind === 'border') {
return 'bg-[var(--bg-canvas)] border-4 border-solid border-[var(--swatch-color)]'
}
return 'bg-[var(--swatch-color)] border-2 border-solid border-[var(--border-muted)]'
}
</script>

<template>
<section class="mb-[var(--spacing-xxl)]">
<div class="mb-[var(--spacing-md)]">
<h2
class="m-0 mb-[var(--spacing-xs)] border-b border-solid border-[var(--border-default)] pb-[var(--spacing-xs)] !text-overline-md text-[var(--text-muted)]"
>
{{ title }}
</h2>
<p v-if="description" class="m-0 max-w-[var(--container-3xl)] text-body-sm text-[var(--text-muted)]">
{{ description }}
</p>
</div>

<div class="overflow-hidden rounded-[var(--shape-card)] border border-solid border-[var(--border-default)] bg-[var(--bg-surface)]">
<button
v-for="item in items"
:key="item.name"
type="button"
class="flex w-full items-center gap-[var(--spacing-md)] border-b border-solid border-[var(--border-muted)] px-[var(--spacing-md)] py-[var(--spacing-sm)] text-left last:border-b-0 hover:bg-[var(--bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring-color)] focus-visible:ring-inset"
:title="copiedKey === item.name ? 'Copied!' : 'Copy CSS variable'"
@click="copyToClipboard(item.name)"
>
<span
:style="swatchVars(item)"
:class="[
'flex h-9 w-14 shrink-0 items-center justify-center rounded-[var(--shape-elements)] font-code text-body-xs',
swatchKindClass(item)
]"
>
<span v-if="item.kind === 'text'">Aa</span>
</span>

<code class="w-[var(--container-3xs)] shrink-0 truncate font-code text-body-sm text-[var(--text-default)]">
{{ copiedKey === item.name ? 'Copied!' : item.name }}
</code>

<span class="text-body-sm text-[var(--text-muted)]">{{ item.description }}</span>
</button>
</div>
</section>
</template>
6 changes: 6 additions & 0 deletions apps/storybook/src/foundations/data/theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Semantic theme color catalog — generated from the `@aziontech/theme`
* token resolvers (see ../utils/theme-tokens.js), so the page always reflects
* every color that ships in globals.css.
*/
export { themeColorGroups, buildThemeColorGroups } from '../utils/theme-tokens.js'
192 changes: 192 additions & 0 deletions apps/storybook/src/foundations/utils/theme-tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/**
* Semantic theme color catalog — generated from the `theme/*` token layer
* (resolved against `primitives/`), so it can never drift from what ships in
* `@aziontech/theme/globals.css`.
*
* `compileThemeVars()` resolves `tokens/theme/*` (background, border, text,
* primary, secondary, accent, ring, code-sintax, feedback/*, surfaces) against
* the color primitives, producing the component-facing theme set: `--bg-*`,
* `--primary*`, `--accent*`, `--secondary*`, `--border-*`, `--text-*`,
* `--ring-color`, feedback `--{info,success,warning,danger}{,-contrast,-border}`,
* and `--code-sintax-*`. We keep only color-valued tokens, drop the `--surface-*`
* primitive scale (documented on the Colors page), and group by role. Each token
* carries its resolved `light` and `dark` value.
*
* Tokens from `tokens/semantic/*` are intentionally NOT sourced here.
*/
import { compileThemeVars } from '@aziontech/theme/theme-colors'

const isColor = (value) => typeof value === 'string' && /^#([0-9a-f]{3,8})$/i.test(value.trim())

// The surface scale is a primitive-level scale documented on the Colors page.
const isExcluded = (name) => /^--surface-/.test(name)

/** Resolve the theme layer into one { name: { light, dark } } map. */
function buildResolvedMap() {
const { light, dark } = compileThemeVars()

const map = {}
for (const name of new Set([...Object.keys(light), ...Object.keys(dark)])) {
if (isExcluded(name)) continue
const l = light[name]
const d = dark[name]
if (!isColor(l) && !isColor(d)) continue
map[name] = { light: l ?? d, dark: d ?? l }
}

return map
}

// Ordered sections. First matching predicate wins; the trailing catch-all
// guarantees a newly-added token always surfaces somewhere.
const SECTIONS = [
{
id: 'backgrounds',
title: 'Backgrounds',
description:
'Surface and page backgrounds. Use --bg-canvas for pages and --bg-surface for elements on top; -raised / -overlay add elevation, and the state tokens (hover, active, selected, disabled) layer on interaction.',
match: (n) => /^--bg-/.test(n)
},
{
id: 'text',
title: 'Text & Icons',
description: 'Accessible foreground colors for text and icons.',
match: (n) => /^--text-/.test(n)
},
{
id: 'borders',
title: 'Borders',
description: 'Border colors for component outlines, separators and status surfaces.',
match: (n) => /^--border-/.test(n)
},
{
id: 'brand',
title: 'Brand & Interactive',
description:
'Brand and interactive fills. Each role pairs a base color with a -contrast foreground for accessible content on top, plus -mask / -selected tints.',
match: (n) => /^--(primary|secondary|accent)(-|$)/.test(n)
},
{
id: 'feedback',
title: 'Feedback Colors',
description:
'Status colors for info, success, warning and danger. Each set pairs a subtle background with an accessible -contrast foreground and a -border.',
match: (n) => /^--(info|success|warning|danger)(-|$)/.test(n)
},
{
id: 'focus',
title: 'Focus',
description: 'Focus-ring color for keyboard navigation.',
match: (n) => /^--ring(-|$)/.test(n)
},
{
id: 'code',
title: 'Code Syntax',
description: 'Syntax-highlighting colors for the CodeBlock component.',
match: (n) => /^--code-sintax-/.test(n)
},
{
id: 'other',
title: 'Other',
description: 'Additional semantic color tokens.',
match: () => true
}
]

// Human-readable notes, keyed by token name. Missing entries render without a note.
const DESCRIPTIONS = {
'--bg-canvas': 'Body / page background.',
'--bg-surface': 'Primary element background (cards, panels).',
'--bg-surface-raised': 'Raised element background — one step above the surface.',
'--bg-surface-overlay': 'Floating surface for menus, popovers and dropdowns.',
'--bg-hover': 'Hover overlay for interactive surfaces.',
'--bg-active': 'Active / pressed overlay for interactive surfaces.',
'--bg-selected': 'Background for a selected element.',
'--bg-disabled': 'Background for a disabled element.',
'--bg-mask': 'Scrim placed over content to dim it.',
'--bg-backdrop': 'Dialog / modal backdrop.',
'--bg-contrast': 'High-contrast inverse background.',

'--text-default': 'Primary text and icons.',
'--text-muted': 'Secondary text and icons.',
'--text-disabled': 'Disabled text and icons.',
'--text-link': 'Interactive link text.',
'--text-contrast': 'Text and icons on a high-contrast surface.',

'--border-default': 'Default border for UI components.',
'--border-muted': 'Subtle, low-emphasis border.',
'--border-strong': 'High-emphasis border.',
'--border-selected': 'Border for a selected / active element.',

'--primary': 'Primary action / brand fill.',
'--primary-contrast': 'Text and icons on a primary fill.',
'--primary-mask': 'Translucent primary tint for hover / wash.',
'--primary-selected': 'Primary tint for a selected state.',
'--secondary': 'Secondary fill.',
'--secondary-contrast': 'Text and icons on a secondary fill.',
'--secondary-mask': 'Translucent secondary tint for hover / wash.',
'--secondary-selected': 'Secondary tint for a selected state.',
'--accent': 'Accent fill.',
'--accent-contrast': 'Text and icons on an accent fill.',
'--accent-mask': 'Translucent accent tint for hover / wash.',
'--accent-selected': 'Accent tint for a selected state.',

'--info': 'Informational background.',
'--info-contrast': 'Text and icons on an info background.',
'--info-border': 'Border for info surfaces.',
'--success': 'Success background.',
'--success-contrast': 'Text and icons on a success background.',
'--success-border': 'Border for success surfaces.',
'--warning': 'Warning background.',
'--warning-contrast': 'Text and icons on a warning background.',
'--warning-border': 'Border for warning surfaces.',
'--danger': 'Danger / error background.',
'--danger-contrast': 'Text and icons on a danger background.',
'--danger-border': 'Border for danger surfaces.',

'--ring-color': 'Focus ring around a focused element.',

'--code-sintax-identifier': 'Identifiers (variables, properties).',
'--code-sintax-line-number': 'Gutter line numbers.',
'--code-sintax-keyword': 'Language keywords.',
'--code-sintax-punctuation': 'Punctuation and operators.',
'--code-sintax-function': 'Function and method names.',
'--code-sintax-type': 'Types and classes.',
'--code-sintax-string': 'String literals.'
}

// A token's swatch style: filled by default; borders draw an outline; foreground
// (text / -contrast) roles render an "Aa" glyph on the surface (or on their base).
function swatchKind(name) {
if (/^--border-/.test(name) || /-border$/.test(name) || name === '--ring-color') return 'border'
if (/^--text-/.test(name) || /-contrast$/.test(name) || /^--code-sintax-/.test(name)) return 'text'
return 'fill'
}

// For -contrast foregrounds, back the glyph with the base fill it sits on.
function backing(name) {
const base = name.replace(/-contrast$/, '')
if (base !== name) return `var(${base})`
return null
}

export function buildThemeColorGroups() {
const resolved = buildResolvedMap()
const groups = SECTIONS.map((s) => ({ ...s, items: [] }))

for (const name of Object.keys(resolved).sort()) {
const section = groups.find((g) => g.match(name))
section.items.push({
name,
light: resolved[name].light,
dark: resolved[name].dark,
description: DESCRIPTIONS[name] ?? '',
kind: swatchKind(name),
on: backing(name)
})
}

return groups.filter((g) => g.items.length > 0).map(({ match, ...g }) => g)
}

export const themeColorGroups = buildThemeColorGroups()
48 changes: 48 additions & 0 deletions apps/storybook/src/stories/foundations/Theme.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import SemanticSwatchGroup from '../../foundations/components/SemanticSwatchGroup.vue'
import { PageContainer, PageHeader } from '../../foundations/components/layout/index.js'
import { themeColorGroups } from '../../foundations/data/theme.js'

export default {
title: 'Foundations/Theme',
parameters: {
options: { showPanel: false },
controls: { disable: true },
actions: { disable: true },
docs: {
description: {
component:
'Semantic theme colors — every mode-aware color token that ships in `@aziontech/theme/globals.css`, generated straight from the token source so the catalog can never drift. Each swatch renders the live token, so toggle the theme in the toolbar to see light and dark. Click a row to copy its CSS variable.'
}
}
}
}

export const Overview = {
name: 'Overview',
render: () => ({
components: { PageContainer, PageHeader, SemanticSwatchGroup },
setup() {
return { themeColorGroups }
},
template: /* html */ `
<PageContainer>
<PageHeader title="Theme">
Semantic color tokens layered on top of the primitive palette. Consume these
<code class="font-code text-code">var(--*)</code> tokens — or the matching Tailwind
<code class="font-code text-code">bg-*</code> / <code class="font-code text-code">text-*</code> /
<code class="font-code text-code">border-*</code> utilities — never a raw hex, so a single theme
drives both light and dark — toggle the theme in the toolbar to preview each token.
Click any row to copy its CSS variable.
</PageHeader>

<SemanticSwatchGroup
v-for="group in themeColorGroups"
:key="group.id"
:title="group.title"
:description="group.description"
:items="group.items"
/>
</PageContainer>
`
})
}
1 change: 1 addition & 0 deletions packages/theme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
".": "./default.js",
"./animations": "./src/tokens/primitives/animations/animate.js",
"./colors": "./src/tokens/primitives/colors/colors.js",
"./theme-colors": "./src/scripts/compile-theme.js",
"./texts": "./src/tokens/semantic/texts.data.js",
"./tailwind-preset": "./dist/v3/tailwind-preset.js",
"./tailwind-preset/v3": "./dist/v3/tailwind-preset.js",
Expand Down
Loading