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
105 changes: 105 additions & 0 deletions .specs/brand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
name: brand
category: content
structure: monolithic
status: implemented
spec_version: 1
figma:
url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=587-3946
node_id: 587:3946
checksum: 62d98081182fc1883def54d3bfaea330a30141becc0ecc3e31cdca35e26e7f8d
created: 2026-07-13
last_updated: 2026-07-13
---

# Brand — Component Spec

## Purpose

The Azion brand logo, rendered as an inline SVG lockup. A single `kind` prop selects the lockup: `default` (the AZION wordmark) for standard surfaces, `reduced` (the "A" glyph) for compact headers and tight spaces, and `extended` (the AZION wordmark with the "move to the edge technologies®" tagline) for marketing-oriented layouts. It consolidates the loose `@aziontech/webkit/svg/azion/*` assets — which remain individually importable — behind one component.

## Usage

```vue
<script setup>
import Brand from '@aziontech/webkit/brand'
</script>

<template>
<Brand kind="default" />
</template>
```

## Props

| Prop | Type | Default | Required | JSDoc |
|---|---|---|---|---|
| `kind` | `'default' \| 'reduced' \| 'extended'` | `'default'` | no | Brand lockup to render: `default` (AZION wordmark), `reduced` (the "A" glyph for tight spaces), `extended` (wordmark with the "move to the edge technologies®" tagline). |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | no | Size token; sets the lockup height (16 / 24 / 32 px) while width scales automatically so each `kind` keeps its own aspect ratio. |

## Events

| _none_ | — | — |

## Slots

| _none_ | — | — |

## States

- Visual states: `default`
- `data-kind` mirrors the `kind` prop
- `data-size` mirrors the `size` prop

## Motion & Animations

_none_

## Tokens

| Region | Token (DESIGN.md) |
|---|---|
| brand mark (SVG fill, asset-owned) | `var(--primary)` |

## Theme gaps

| Figma variable | Temporary primitive | Follow-up |
|---|---|---|
| brand orange (`#F3652B`) | hardcoded HEX inside the `svg/azion/*` assets | `TODO: tokenizar the brand assets to `var(--primary)`` |

## Accessibility (WCAG 2.1 AA)

- The root carries `role="img"` and a default `aria-label` (`Azion`) so the logo has an accessible name; consumers may override the label via a passed `aria-label`.
- Non-interactive: the logo is not focusable and emits no events; the inner SVG is presentational.
- Contrast ≥3:1 for the brand mark against its background (large graphic).
- `motion-reduce` not applicable — the component is static.

## Stories (Storybook)

- Default
- Types — composite story rendering every `kind` value (`default`, `reduced`, `extended`) side-by-side.
- Sizes — composite story rendering every `size` value (`small`, `medium`, `large`) side-by-side.

## Constraints — DO NOT

<!-- This block is injected VERBATIM into every sub-agent prompt.
spec-validator rejects the spec if this block is missing or shorter than the template. -->

- Do not add props beyond the Props table above. If you need a prop that is not listed, emit `BLOCKED: missing prop <name>` and stop — do not invent.
- Do not add events beyond the Events table above. Same rule for slots and sub-components.
- Do not invent imports. Every `@aziontech/webkit/*` path must exist in `packages/webkit/package.json#exports`. Every relative import must resolve to a real file. Every npm package must be installed.
- Do not use HEX/RGB/HSL colors, Tailwind palette names (e.g. `bg-blue-500`), raw typography classes (e.g. `text-sm`), `any`, `@ts-ignore`, or `class` inside `defineProps`.
- Do not install or import positioning/animation libraries (`@floating-ui/*`, `popper.js`, `tippy.js`, `gsap`, `framer-motion`, `motion`, `@vueuse/motion`, `@formkit/auto-animate`, drag-drop runtimes, scroll virtualization libs). Use CSS + Vue primitives (`<Teleport>`, `<Transition>`). See `.claude/rules/dependencies.md`.
- Do not improvise animations. Every `animate-*` / `transition-*` class must come from `packages/theme/src/tokens/semantic/animations.js`; every motion-bearing class pairs with `motion-reduce:*` on the same class string; no component-local `@keyframes`.
- Do not create class presets in JavaScript (`const kindClasses = {...}`, `const sharedClasses = [...]`, `const sizeClasses = {...}`, `const rootClasses = computed(...)`). Variants live on `data-*` attributes consumed by Tailwind `data-[attr=value]:`. All utilities live inline on the root element's `class` attribute. No `<style>` block, no component-local `.css`/`.scss`. See `.claude/rules/styling.md`.
- Do not inherit artifacts as-is from another design system, Figma file, library, or pre-existing `CONTRACT.md` / `README.md`. Rewrite to our conventions. See `.claude/rules/migration.md`.
- Do not add Figma references to Storybook stories. No `parameters.design`, no `parameters.figma`, no Figma URLs in `docs.description.*`, no `@storybook/addon-designs` import. The Figma link is owned by `<name>.figma.ts` (Code Connect). See `.claude/docs/COMPONENT_REQUIREMENTS.md`.
- Do not use `parameters.actions.argTypesRegex` (deprecated in Storybook 8 and silently misroutes Vue 3 emits) or `parameters.actions.handles` (DOM-only). Declare every event explicitly in `argTypes` with a camelCase `on<Event>` key and `{ action: '<emitted-name>' }`. Do not use the legacy CSF2 `Name.args = {...}` form — always object-style CSF3.
- Do not add bespoke Storybook stories beyond Default + Types + Sizes for the props the component actually declares, unless the spec's "Stories (Storybook)" section explicitly justifies the addition. Do not split Types/Sizes into one-story-per-variant — the composite stories are the canonical pattern.
- Do not duplicate the `## Usage` block from the spec inside the Storybook story body. The block is injected once into `parameters.docs.description.component` by the storybook-write skill; copy it nowhere else.
- Do not edit `.claude/docs/DESIGN.md`, `.claude/docs/COMPONENT_REQUIREMENTS.md`, or `.claude/docs/PRIMEVUE_ABSTRACTION.md`.
- Do not edit the root `package.json` or `.github/workflows/*`.
- Do not change `structure` after `status: approved`. To change structure, bump `spec_version` and re-author the spec.
- Do not create files outside the paths declared by your task (the orchestrator tells you exactly which files to write).
- Do not run `git` commands, `pnpm install`, or any command that changes the lockfile.
- If anything in the spec is ambiguous or contradicts the rules, emit `BLOCKED: <one-sentence reason>` and write nothing.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
118 changes: 118 additions & 0 deletions apps/storybook/src/stories/components/content/brand/Brand.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Brand from '@aziontech/webkit/brand'

import { toSfc } from '../../../_shared/story-source'

const IMPORT = "import Brand from '@aziontech/webkit/brand'"

/** @type {import('@storybook/vue3').Meta<typeof Brand>} */
const meta = {
title: 'Components/Content/Brand',
component: Brand,
tags: ['autodocs'],
parameters: {
layout: 'centered',
backgrounds: {
default: 'dark'
},
a11y: {
config: {
rules: [
{ id: 'color-contrast', enabled: true },
{ id: 'focus-order-semantics', enabled: true }
]
}
},
docs: {
description: {
component:
'The Azion brand logo, rendered as an inline SVG lockup. A single `kind` prop selects the lockup: `default` (the AZION wordmark) for standard surfaces, `reduced` (the "A" glyph) for compact headers and tight spaces, and `extended` (the AZION wordmark with the "move to the edge technologies®" tagline) for marketing-oriented layouts. It consolidates the loose `@aziontech/webkit/svg/azion/*` assets — which remain individually importable — behind one component.'
},
canvas: { sourceState: 'shown' }
}
},
argTypes: {
kind: {
control: 'select',
options: ['default', 'reduced', 'extended'],
description: 'Brand lockup to render.',
table: {
category: 'props',
type: { summary: "'default' | 'reduced' | 'extended'" },
defaultValue: { summary: "'default'" }
}
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: 'Size token; sets the lockup height while width scales to preserve each kind’s aspect ratio.',
table: {
category: 'props',
type: { summary: "'small' | 'medium' | 'large'" },
defaultValue: { summary: "'medium'" }
}
}
},
args: { kind: 'default', size: 'medium' }
}

export default meta

const Template = (args) => ({
components: { Brand },
setup: () => ({ props: args }),
template: '<Brand v-bind="props" />'
})

const DEFAULT_MARKUP = '<Brand kind="default" />'

/** @type {import('@storybook/vue3').StoryObj<typeof Brand>} */
export const Default = {
render: Template,
parameters: {
docs: {
description: { story: 'The default AZION wordmark lockup.' },
source: { code: toSfc(IMPORT, DEFAULT_MARKUP) }
}
}
}

const TYPES_TEMPLATE = `<div class="flex flex-wrap items-center gap-8">
<Brand kind="default" />
<Brand kind="reduced" />
<Brand kind="extended" />
</div>`

/** @type {import('@storybook/vue3').StoryObj<typeof Brand>} */
export const Types = {
render: () => ({ components: { Brand }, template: TYPES_TEMPLATE }),
parameters: {
docs: {
controls: { disable: true },
description: {
story:
'All brand lockups side by side: default (AZION wordmark), reduced (the "A" glyph), and extended (wordmark with the "move to the edge technologies®" tagline).'
},
source: { code: toSfc(IMPORT, TYPES_TEMPLATE) }
}
}
}

const SIZES_TEMPLATE = `<div class="flex flex-wrap items-center gap-8">
<Brand kind="default" size="small" />
<Brand kind="default" size="medium" />
<Brand kind="default" size="large" />
</div>`

/** @type {import('@storybook/vue3').StoryObj<typeof Brand>} */
export const Sizes = {
render: () => ({ components: { Brand }, template: SIZES_TEMPLATE }),
parameters: {
docs: {
controls: { disable: true },
description: {
story: 'The lockup at each size token — small (16px), medium (24px), and large (32px) tall — with width scaling to preserve the aspect ratio.'
},
source: { code: toSfc(IMPORT, SIZES_TEMPLATE) }
}
}
}
26 changes: 26 additions & 0 deletions packages/webkit/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -2226,6 +2226,32 @@
"Drive color from `kind`, never a hardcoded color or a raw palette class."
]
},
"brand": {
"import": "@aziontech/webkit/brand",
"target": "./src/components/content/brand/brand.vue",
"kind": "component",
"treeShakeableImport": "@aziontech/webkit/brand",
"category": "content",
"structure": "monolithic",
"status": "implemented",
"props": [
{
"name": "kind",
"type": "'default' | 'reduced' | 'extended'",
"default": "'default'",
"required": "no",
"doc": "Brand lockup to render: `default` (AZION wordmark), `reduced` (the \"A\" glyph for tight spaces), `extended` (wordmark with the \"move to the edge technologies®\" tagline)."
},
{
"name": "size",
"type": "'small' | 'medium' | 'large'",
"default": "'medium'",
"required": "no",
"doc": "Size token; sets the lockup height (16 / 24 / 32 px) while width scales automatically so each `kind` keeps its own aspect ratio."
}
],
"purpose": "The Azion brand logo, rendered as an inline SVG lockup. A single `kind` prop selects the lockup: `default` (the AZION wordmark) for standard surfaces, `reduced` (the \"A\" glyph) for compact headers and tight spaces, and `extended` (the AZION wordmark with the \"move to the edge technologies®\" tagline) for marketing-oriented layouts. It consolidates the loose `@aziontech/webkit/svg/azion/*` assets — which remain individually importable — behind one component."
},
"accordion": {
"import": "@aziontech/webkit/accordion",
"target": "./src/components/content/accordion/index.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/webkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
"./tag": "./src/components/tag/tag.vue",
"./avatar": "./src/components/avatar/avatar.vue",
"./badge": "./src/components/content/badge/badge.vue",
"./brand": "./src/components/content/brand/brand.vue",
"./accordion": "./src/components/content/accordion/index.ts",
"./accordion-root": "./src/components/content/accordion/accordion.vue",
"./accordion-item": "./src/components/content/accordion/accordion-item/accordion-item.vue",
Expand Down
24 changes: 24 additions & 0 deletions packages/webkit/src/components/content/brand/brand.figma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { figma } from '@figma/code-connect'

import Brand from './brand.vue'

// The Figma "Brand" component exposes a `type` variant (Default | Min | Move to the Edge | Full).
// We surface the three lockups this component ships: Default → default, Min → reduced, and
// Full (the tagline lockup) → extended. The "Move to the Edge" Figma variant is not exposed.
// `size` has no Figma counterpart — it is a code-only prop, so it is not mapped here.
figma.connect(
Brand,
'https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=587-3946',
{
props: {
kind: figma.enum('type', {
Default: 'default',
Min: 'reduced',
Full: 'extended'
})
},
example: (props) => /* html */ `
<Brand kind="${props.kind}" size="medium" />
`
}
)
Loading
Loading