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
13 changes: 13 additions & 0 deletions .claude/docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Reference: `packages/webkit/src/components/webkit/actions/button/button.vue`.
| `text-label-sm` / `text-label-md` / `text-label-lg` | Labels, compact UI text |
| `text-overline-md` / `text-overline-sm` / `text-overline-xs` | Overlines (uppercase, tracking baked in) |
| `text-button-lg` / `text-button-md` | Button labels |
| `text-link` | Inline `<a>` inside body/heading copy (inherits parent size; hover underline) |

These classes are **mobile-first**: font sizes can change at `sm`, `md`, etc. Do not duplicate breakpoint logic in components.

Expand All @@ -69,6 +70,18 @@ Font family, line height, letter spacing, and responsive font size live in `text
<i class="shrink-0 text-[length:inherit] leading-none" />
```

### Inline links vs navigation `Link`

For anchors **inside** body or heading copy, use the `text-link` typography class on a plain `<a>` (inherits size from the parent `text-body-*` / `text-heading-*` class):

```html
<p class="text-body-md">
Read the <a href="/docs" class="text-link">documentation</a>.
</p>
```

For **standalone** navigation links (icon, ghost hover surface, fixed height), use `@aziontech/webkit/navigation/link` — not `text-link` alone.

---

## Spacing
Expand Down
1 change: 1 addition & 0 deletions .specs/_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"type": "string",
"enum": [
"actions",
"code",
"content",
"data",
"feedback",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script setup>
import { ref } from 'vue'

defineProps({
title: { type: String, required: true },
items: { type: Array, default: () => [] }
})

const copiedKey = ref(null)
let copyTimeout = null

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

function isCopied(key) {
return copiedKey.value === key
}

function labelColor(hex) {
const value = (hex ?? '').replace('#', '').slice(0, 6)
if (value.length < 6) return '#fff'
const r = parseInt(value.slice(0, 2), 16)
const g = parseInt(value.slice(2, 4), 16)
const b = parseInt(value.slice(4, 6), 16)
return 0.299 * r + 0.587 * g + 0.114 * b > 140 ? '#000' : '#fff'
}
</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-[var(--border-default)] pb-[var(--spacing-xs)] !text-overline-md text-[var(--text-muted)]"
>
{{ title }}
</h2>
</div>

<div class="flex w-full flex-row flex-wrap gap-[var(--spacing-sm)]">
<article
v-for="item in items"
:key="item.id"
class="w-full min-w-[var(--container-3xs)] flex-1 cursor-pointer overflow-hidden rounded-[var(--shape-card)] border border-[var(--border-default)] bg-[var(--bg-surface)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring-color)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-canvas)]"
role="button"
tabindex="0"
:title="isCopied(`${item.id}-value`) ? 'Copied!' : 'Copy variable name'"
@click="copyToClipboard(item.meta ?? item.value, `${item.id}-value`)"
@keydown.enter.prevent="copyToClipboard(item.meta ?? item.value, `${item.id}-value`)"
@keydown.space.prevent="copyToClipboard(item.meta ?? item.value, `${item.id}-value`)"
>
<div
class="flex h-24 flex-col px-[var(--spacing-sm)] py-[var(--spacing-xs)] font-code"
:style="{ background: item.preview ?? item.value ?? 'transparent' }"
>
<span
class="font-code text-body-xs font-semibold"
:style="{ color: labelColor(item.preview ?? item.value) }"
>
{{ item.label }}
</span>
<span
class="font-code text-body-xs"
:style="{ color: labelColor(item.preview ?? item.value) }"
>
{{ isCopied(`${item.id}-value`) ? 'Copied' : item.value }}
</span>
</div>

<div
class="flex items-center justify-between gap-[var(--spacing-xs)] bg-[var(--bg-mask)] px-[var(--spacing-sm)] py-[var(--spacing-xs)] font-code"
>
<span class="truncate text-body-xs text-[var(--text-muted)]">{{ item.meta }}</span>
</div>
</article>
</div>
</section>
</template>
68 changes: 36 additions & 32 deletions apps/storybook/src/foundations/components/ColorPlayground.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { backgroundTokens, textTokens, borderTokens } from '../data/colors.js';
import { playgroundColorTokens } from '../data/colors.js';

const tokens = Array.isArray(playgroundColorTokens) ? playgroundColorTokens : [];

const CATEGORIES = [
{ key: 'background', label: 'Background', tokens: backgroundTokens, prefix: 'bg' },
{ key: 'text', label: 'Text', tokens: textTokens, prefix: 'text' },
{ key: 'border', label: 'Border', tokens: borderTokens, prefix: 'border' },
{ key: 'theme', label: 'Theme', filter: (t) => t.category === 'theme' },
{ key: 'background', label: 'Background', filter: (t) => t.category === 'background' },
{ key: 'text', label: 'Text', filter: (t) => t.category === 'text' },
{ key: 'border', label: 'Border', filter: (t) => t.category === 'border' },
];

const selectedCategory = ref('background');
const selectedTokenName = ref(backgroundTokens[0].name);
const selectedCategory = ref('theme');
const selectedTokenName = ref('primary');

// Detect global theme from Storybook's theme addon (azion-light/azion-dark class)
const isDark = ref(true);
let observer = null;

Expand All @@ -22,38 +24,41 @@ function updateTheme() {

onMounted(() => {
updateTheme();
// Watch for class changes on document body or html element
observer = new MutationObserver(updateTheme);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });

const first = tokens.find((t) => t.name === 'primary') ?? tokens[0];
if (first) selectedTokenName.value = first.name;
});

onUnmounted(() => {
observer?.disconnect();
});

const activeTokens = computed(() =>
CATEGORIES.find((c) => c.key === selectedCategory.value)?.tokens ?? []
);
const activeTokens = computed(() => {
const cat = CATEGORIES.find((c) => c.key === selectedCategory.value);
return cat ? tokens.filter(cat.filter) : tokens;
});

const selectedToken = computed(() =>
activeTokens.value.find((t) => t.name === selectedTokenName.value) ?? activeTokens.value[0]
const selectedToken = computed(
() => activeTokens.value.find((t) => t.name === selectedTokenName.value) ?? activeTokens.value[0]
);

const currentHex = computed(() =>
isDark.value ? selectedToken.value?.darkHex : selectedToken.value?.lightHex
);

const modeLabel = computed(() => isDark.value ? 'dark' : 'light');
const modeLabel = computed(() => (isDark.value ? 'dark' : 'light'));

const previewStyle = computed(() => {
const hex = currentHex.value;
if (!hex) return {};
const cat = selectedCategory.value;
if (cat === 'background') return { backgroundColor: hex };
if (cat === 'text') return { color: hex };
if (cat === 'border') return { borderColor: hex, borderWidth: '2px', borderStyle: 'solid' };
return {};
if (cat === 'background' || cat === 'theme') return { backgroundColor: hex };
if (cat === 'text') return { color: hex };
if (cat === 'border') return { borderColor: hex, borderWidth: '2px', borderStyle: 'solid' };
return { backgroundColor: hex };
});

const previewTextStyle = computed(() => {
Expand All @@ -74,7 +79,6 @@ function onCategoryChange() {
selectedTokenName.value = activeTokens.value[0]?.name ?? '';
}

// Track copied state
const copiedKey = ref(null);
let copyTimeout = null;

Expand All @@ -94,7 +98,6 @@ function isCopied(key) {

<template>
<div class="flex flex-col gap-6">
<!-- Controls ─────────────────────────────────────────────────────────── -->
<div class="flex gap-5 items-end flex-wrap">
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-semibold uppercase tracking-wider text-muted">Category</label>
Expand All @@ -106,10 +109,15 @@ function isCopied(key) {
'px-3.5 py-1.5 text-xs font-medium cursor-pointer bg-transparent transition-all duration-100',
selectedCategory === cat.key
? 'bg-primary text-white font-semibold'
: 'text-muted border-r border-default last:border-r-0'
: 'text-muted border-r border-default last:border-r-0',
]"
@click="selectedCategory = cat.key; onCategoryChange()"
>{{ cat.label }}</button>
@click="
selectedCategory = cat.key;
onCategoryChange();
"
>
{{ cat.label }}
</button>
</div>
</div>

Expand All @@ -126,17 +134,13 @@ function isCopied(key) {
</div>
</div>

<!-- Preview ───────────────────────────────────────────────────────────── -->
<div class="flex gap-5 items-start flex-wrap">
<div
class="flex-1 min-w-[220px] min-h-[160px] rounded-[10px] flex items-center justify-center transition-all duration-200"
:class="selectedCategory !== 'border' ? 'border border-default' : ''"
:style="previewStyle"
>
<span
class="font-code text-[15px] font-semibold"
:style="previewTextStyle"
>
<span class="font-code text-[15px] font-semibold" :style="previewTextStyle">
{{ selectedToken?.tailwindClass }}
</span>
</div>
Expand All @@ -149,7 +153,7 @@ function isCopied(key) {
@click="copyToClipboard(selectedToken?.tailwindClass, 'tw')"
>
<code class="font-code text-[11px] border bg-white/10 border-white/15 text-code px-1.5 py-0.5 rounded">{{ selectedToken?.tailwindClass }}</code>
<i :class="['pi text-[11px] opacity-0 group-hover:!opacity-50', isCopied('css') ? 'pi-check text-success' : 'pi-copy']" />
<i :class="['pi text-[11px] opacity-0 group-hover:!opacity-50', isCopied('tw') ? 'pi-check text-success' : 'pi-copy']" />
</button>
</div>

Expand All @@ -168,7 +172,7 @@ function isCopied(key) {
<span class="text-[10px] font-semibold uppercase tracking-wider text-muted">Resolved ({{ modeLabel }})</span>
<span class="flex items-center gap-1.5 flex-wrap">
<span
class="w-3.5 h-3.5 rounded flex-shrink-0 border border-gray-500/30"
class="w-3.5 h-3.5 rounded flex-shrink-0 border border-default"
:style="{ background: currentHex ?? 'transparent' }"
/>
<code class="font-code text-[11px] border bg-white/10 border-white/15 text-code px-1.5 py-0.5 rounded">{{ currentHex }}</code>
Expand All @@ -184,13 +188,13 @@ function isCopied(key) {
<span class="text-[10px] font-semibold uppercase tracking-wider text-muted">Light → Dark</span>
<span class="flex items-center gap-1.5 flex-wrap">
<span
class="w-3.5 h-3.5 rounded flex-shrink-0 border border-gray-500/30"
class="w-3.5 h-3.5 rounded flex-shrink-0 border border-default"
:style="{ background: selectedToken?.lightHex ?? 'transparent' }"
/>
<code class="font-code text-[11px] border bg-white/10 border-white/15 text-code px-1.5 py-0.5 rounded">{{ selectedToken?.lightHex }}</code>
<span class="opacity-40 text-[11px]">→</span>
<span
class="w-3.5 h-3.5 rounded flex-shrink-0 border border-gray-500/30"
class="w-3.5 h-3.5 rounded flex-shrink-0 border border-default"
:style="{ background: selectedToken?.darkHex ?? 'transparent' }"
/>
<code class="font-code text-[11px] border bg-white/10 border-white/15 text-code px-1.5 py-0.5 rounded">{{ selectedToken?.darkHex }}</code>
Expand Down
4 changes: 2 additions & 2 deletions apps/storybook/src/foundations/components/ColorSwatch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const labelColor = computed(() => {
</script>

<template>
<div class="flex flex-col rounded-md overflow-hidden border border-gray-500/15 flex-1 min-w-[60px] max-w-[88px] cursor-default">
<div class="flex flex-col rounded-md overflow-hidden border border-default flex-1 min-w-[60px] max-w-[88px] cursor-default">
<div
class="h-[52px] flex items-end px-1.5 py-1"
:style="{ background: hex ?? 'transparent' }"
Expand All @@ -33,7 +33,7 @@ const labelColor = computed(() => {
>{{ shade }}</span>
</div>
<div class="px-1.5 py-[5px] bg-black/25">
<span class="text-[9px] font-code text-gray-400 block break-all leading-tight">{{ hex }}</span>
<span class="text-[9px] font-code text-muted block break-all leading-tight">{{ hex }}</span>
</div>
</div>
</template>
Loading
Loading