feat(branding): scaffold Logo component + Sumi icons + brand assets pipeline (Sprint 3)
Sprint 3 = production assets (logo, icons, hero, textures). Most deliverables
are physical artistic work (artist Renaud + Nikola scans). This commit lays
the CODE scaffold so assets drop in without friction when delivered.
New : apps/web/src/components/branding/
- Logo.tsx — single source of truth for Talas / Veza brand rendering.
Replaces ad-hoc inline wordmarks (Sidebar/Navbar/Footer/landing each had
their own VEZA <h2>). Variants: wordmark / symbol / lockup. Sizes xs..xl.
Colors auto/ink/cyan/inverse. Optional tagline. Horizontal/vertical orient.
- assets/SymbolPlaceholder.tsx — geometric ink stroke + arc + dot, monochrome,
currentColor inheritance, scalable. Mirrors charte §3.1 brief. Replaced by
artist's hand-drawn mark in P0.1 of BRIEF_ARTISTE.
- Logo.stories.tsx — full Storybook coverage: variants, sizes, colors,
orientation, Talas vs Veza, all-sizes ladder.
- index.ts — barrel exports.
New : apps/web/src/components/icons/sumi/
- Play.tsx — first calligraphic icon stub (programmatic approximation per
charte §6.3). 9 more to come (Pause, Search, Profile, Chat, Upload,
Settings, Home, Close, Volume).
- index.ts — barrel + commented TODO list per priority.
- Used via existing components/icons/SumiIcon.tsx wrapper which falls back to
Lucide when no Sumi version exists.
Brand alignment of platform metadata :
- public/favicon.svg — Mizu cyan placeholder (#0098B5) replacing default
vite.svg. Mirrors SymbolPlaceholder geometry.
- public/manifest.json — theme_color #1a1a1a -> #0098B5 (SUMI accent),
background_color #ffffff -> #0D0D0F (charte §4.4 rule 1: no pure white).
- index.html — theme-color meta + msapplication-TileColor aligned to SUMI.
Favicon link points to /favicon.svg.
New doc : apps/web/docs/BRANDING.md
- Architecture map of brand assets in apps/web.
- Logo component API + usage examples.
- Asset deliverables status table (P0/P1/P2 from brief artiste, all 🟡 placeholders).
- Naming convention for raw scans + processed SVGs.
- Step-by-step "how to integrate a delivered asset" for wordmark and Sumi icon.
- Brand color guard (ESLint rule pointer).
Build OK (vite 12.6s). Typecheck clean. No visual regression — Sidebar/Navbar
inline wordmarks intentionally NOT migrated yet (they use fontWeight 300 which
contradicts charte's Bold requirement; a per-screen migration call later).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cb511afa6e
commit
33fcd7d1bd
10 changed files with 584 additions and 5 deletions
149
apps/web/docs/BRANDING.md
Normal file
149
apps/web/docs/BRANDING.md
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# Branding & assets pipeline — apps/web
|
||||||
|
|
||||||
|
Single source of truth for how Talas / Veza brand assets enter the codebase.
|
||||||
|
Reference brand spec : [`CHARTE_GRAPHIQUE_TALAS.md`](../../../../Documents/TG__Talas_Group/05_EXPERIENCE_UTILISATEUR/CHARTE_GRAPHIQUE_TALAS.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/web/
|
||||||
|
├── public/
|
||||||
|
│ ├── favicon.svg # SVG favicon (Mizu cyan placeholder)
|
||||||
|
│ ├── icons/ # PWA icons (PNG, 72x72 to 512x512)
|
||||||
|
│ ├── fonts/ # Self-hosted woff2 (Space Grotesk, Inter, JetBrains Mono)
|
||||||
|
│ └── manifest.json # PWA manifest (theme_color = #0098B5 SUMI accent)
|
||||||
|
└── src/
|
||||||
|
├── components/
|
||||||
|
│ ├── branding/
|
||||||
|
│ │ ├── Logo.tsx # SOLE entry point for Talas / Veza wordmark + symbol
|
||||||
|
│ │ ├── Logo.stories.tsx
|
||||||
|
│ │ ├── assets/
|
||||||
|
│ │ │ ├── SymbolPlaceholder.tsx # Geometric placeholder, swap for hand-drawn
|
||||||
|
│ │ │ ├── TalasWordmark.tsx # (P0.1 artist deliverable — 3 variants)
|
||||||
|
│ │ │ └── VezaWordmark.tsx # (P1.1 artist deliverable — 1 variant)
|
||||||
|
│ │ └── index.ts
|
||||||
|
│ └── icons/
|
||||||
|
│ ├── SumiIcon.tsx # Wrapper : prefers hand-drawn, falls back to Lucide
|
||||||
|
│ └── sumi/ # Hand-drawn calligraphic icons (10 prioritaires)
|
||||||
|
│ ├── Play.tsx
|
||||||
|
│ ├── Pause.tsx (TODO)
|
||||||
|
│ └── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logo component
|
||||||
|
|
||||||
|
**Always use `<Logo />`** instead of inline `<h2>VEZA</h2>` style markup.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Logo } from '@/components/branding';
|
||||||
|
|
||||||
|
// Default (wordmark, md, theme-aware color)
|
||||||
|
<Logo brand="veza" />
|
||||||
|
|
||||||
|
// Lockup with tagline
|
||||||
|
<Logo brand="veza" variant="lockup" size="lg" tagline="STREAMING" />
|
||||||
|
|
||||||
|
// Symbol only (favicon-style usage)
|
||||||
|
<Logo brand="talas" variant="symbol" size="sm" />
|
||||||
|
|
||||||
|
// Cyan accent
|
||||||
|
<Logo brand="veza" variant="lockup" color="cyan" />
|
||||||
|
```
|
||||||
|
|
||||||
|
API : see [Logo.stories.tsx](../src/components/branding/Logo.stories.tsx) for all variants in Storybook.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Asset deliverables — current status
|
||||||
|
|
||||||
|
Per `BRIEF_ARTISTE_IDENTITE_VISUELLE.md` (artist Renaud, 15 avril 2026) and Sprint 3 :
|
||||||
|
|
||||||
|
| Asset | Priority | Status | Location |
|
||||||
|
|-------|----------|--------|----------|
|
||||||
|
| TALAS wordmark × 3 (propre, sauvage, vertical) | P0.1 | ⏳ awaiting artist | `branding/assets/TalasWordmark.tsx` (pending) |
|
||||||
|
| Hero image post-apo | P0.2 | ⏳ awaiting artist | `public/hero/` (pending) |
|
||||||
|
| VEZA wordmark × 1 (tag fluide) | P1.1 | ⏳ awaiting artist | `branding/assets/VezaWordmark.tsx` (pending) |
|
||||||
|
| 3-5 textures de liaison | P1.2 | ⏳ awaiting artist | `public/textures/` (pending) |
|
||||||
|
| 3 symboles iconiques (enso, onde, libre) | P1.3 | ⏳ awaiting artist | `branding/assets/Symbol.tsx` (pending) |
|
||||||
|
| Talas symbole (calligraphique) | — | 🟡 placeholder | `branding/assets/SymbolPlaceholder.tsx` |
|
||||||
|
| Favicon SVG | — | 🟡 placeholder | `public/favicon.svg` |
|
||||||
|
| 10 Sumi icons (play/pause/search/...) | — | 🟡 1/10 stubbed | `components/icons/sumi/` |
|
||||||
|
| washi.png texture | — | ✅ inline SVG (feTurbulence) | `src/index.css:456` (no external file) |
|
||||||
|
| Fonts (Space Grotesk + Inter + JetBrains Mono) | — | ✅ self-hosted | `public/fonts/*.woff2` |
|
||||||
|
| PWA icons (PNG, 9 sizes) | — | 🟡 generic placeholders | `public/icons/icon-*.png` |
|
||||||
|
|
||||||
|
### Naming convention
|
||||||
|
|
||||||
|
- Wordmarks : `{brand}_wordmark_{variant}.svg` then exported as React component
|
||||||
|
- Example : `talas_wordmark_propre.svg` → `TalasWordmarkPropre.tsx`
|
||||||
|
- Symbols : `{brand}_symbol_{type}.svg`
|
||||||
|
- Hero / textures : `{kind}_{number}.png` (raw scans), processed to `webp` for prod
|
||||||
|
- Always store source SVGs (vectorized) ; processed bitmaps in build
|
||||||
|
|
||||||
|
### Format requirements (per BRIEF_ARTISTE §5)
|
||||||
|
|
||||||
|
- **Scan minimum 600 DPI** (1200 if available). PNG/TIFF only — no JPG (bleeding edges on ink).
|
||||||
|
- **One artwork per file**. Naming : `talas_wordmark_sauvage_01.png` etc.
|
||||||
|
- **No retouching** before delivery — clean fond, niveaux, détourage handled in apps/web preprocessing.
|
||||||
|
- **Paper white** (not cream) ; **encre de Chine** (not brown-tinted black) ; aquarelle limited to terreuse palette.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to integrate a delivered asset
|
||||||
|
|
||||||
|
### Wordmark (e.g. TALAS propre)
|
||||||
|
|
||||||
|
1. Receive `talas_wordmark_propre_01.png` (scan 600+ DPI).
|
||||||
|
2. Clean fond + isolate ink in Inkscape : `File → Import → Select-by-color (white) → Delete → Trace bitmap`.
|
||||||
|
3. Export SVG with `currentColor` fills + transparent background.
|
||||||
|
4. Save as `apps/web/src/components/branding/assets/TalasWordmark.tsx` :
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import type { SVGProps } from 'react';
|
||||||
|
export default function TalasWordmark(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 240 60" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
{/* Pasted SVG paths here, fills set to currentColor */}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Update `Logo.tsx` to use `<TalasWordmark />` for `brand='talas'` instead of the
|
||||||
|
text fallback. (Detect via prop or via fallback chain.)
|
||||||
|
6. Storybook will show it automatically.
|
||||||
|
|
||||||
|
### Sumi icon (e.g. Pause)
|
||||||
|
|
||||||
|
1. Receive `pause_01.png` from artist.
|
||||||
|
2. Vectorize manually in Inkscape (no auto-trace — preserves irregularity).
|
||||||
|
3. Save as `apps/web/src/components/icons/sumi/Pause.tsx`.
|
||||||
|
4. Add export to `components/icons/sumi/index.ts`.
|
||||||
|
5. At call site :
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { SumiIcon } from '@/components/icons/SumiIcon';
|
||||||
|
import { PauseIcon } from '@/components/icons/sumi';
|
||||||
|
import { Pause } from 'lucide-react';
|
||||||
|
|
||||||
|
<SumiIcon sumi={PauseIcon} fallback={Pause} size={24} />
|
||||||
|
```
|
||||||
|
|
||||||
|
The `SumiIcon` wrapper handles the "use hand-drawn if available, else Lucide
|
||||||
|
fallback" logic, so you can drop hand-drawn icons in progressively.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Brand color guard
|
||||||
|
|
||||||
|
ESLint rule (`eslint.config.js` `no-restricted-syntax` for hex literals) blocks
|
||||||
|
new hardcoded colors. To fix a warning :
|
||||||
|
|
||||||
|
- CSS context (JSX style/className/template literal) : use `var(--sumi-*)`.
|
||||||
|
- TS / canvas context : `import { ColorVizIndigo } from '@veza/design-system/tokens-generated';`.
|
||||||
|
|
||||||
|
Source of truth for all colors : `packages/design-system/tokens/primitive/color.json`.
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<title>Veza - Plateforme de streaming musical</title>
|
<title>Veza - Plateforme de streaming musical</title>
|
||||||
|
|
||||||
|
|
@ -18,7 +18,8 @@
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
|
||||||
<!-- PWA Meta Tags -->
|
<!-- PWA Meta Tags -->
|
||||||
<meta name="theme-color" content="#1a1a1a" />
|
<!-- theme-color = SUMI Mizu cyan (charte §4.1 — sole UI accent) -->
|
||||||
|
<meta name="theme-color" content="#0098B5" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
|
@ -36,7 +37,7 @@
|
||||||
<link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512x512.png" />
|
<link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512x512.png" />
|
||||||
|
|
||||||
<!-- Microsoft Tiles -->
|
<!-- Microsoft Tiles -->
|
||||||
<meta name="msapplication-TileColor" content="#1a1a1a" />
|
<meta name="msapplication-TileColor" content="#0D0D0F" />
|
||||||
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
|
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
|
||||||
|
|
||||||
<!-- SEO and Social -->
|
<!-- SEO and Social -->
|
||||||
|
|
|
||||||
13
apps/web/public/favicon.svg
Normal file
13
apps/web/public/favicon.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<!--
|
||||||
|
Veza / Talas favicon — placeholder.
|
||||||
|
Awaits the artist's hand-drawn calligraphic mark (P0.1 of BRIEF_ARTISTE_IDENTITE_VISUELLE).
|
||||||
|
Mirrors apps/web/src/components/branding/assets/SymbolPlaceholder.tsx.
|
||||||
|
Mizu cyan (#0098B5) per CHARTE_GRAPHIQUE_TALAS §4.1.
|
||||||
|
-->
|
||||||
|
<path d="M7 4 Q 8.5 9, 7.5 14 T 8 20"
|
||||||
|
stroke="#0098B5" stroke-width="2.5" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M11 5 Q 17 12, 12 19"
|
||||||
|
stroke="#0098B5" stroke-width="1.8" stroke-linecap="round" fill="none" opacity="0.85"/>
|
||||||
|
<circle cx="18.5" cy="6" r="1.0" fill="#0098B5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 666 B |
|
|
@ -2,8 +2,8 @@
|
||||||
"name": "Veza Platform",
|
"name": "Veza Platform",
|
||||||
"short_name": "Veza",
|
"short_name": "Veza",
|
||||||
"description": "Plateforme de streaming, collaboration et distribution musicale moderne",
|
"description": "Plateforme de streaming, collaboration et distribution musicale moderne",
|
||||||
"theme_color": "#1a1a1a",
|
"theme_color": "#0098B5",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#0D0D0F",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
|
|
|
||||||
125
apps/web/src/components/branding/Logo.stories.tsx
Normal file
125
apps/web/src/components/branding/Logo.stories.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { Logo } from './Logo';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Branding/Logo',
|
||||||
|
component: Logo,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
'Single source of truth for Talas / Veza brand rendering. Use this component everywhere a wordmark, symbol, or lockup is needed. See `CHARTE_GRAPHIQUE_TALAS.md §3` for the logo specifications. The current SVG symbol is a placeholder until the artist (Renaud, P0.1) delivers the hand-drawn calligraphic mark.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
brand: {
|
||||||
|
control: 'inline-radio',
|
||||||
|
options: ['talas', 'veza'],
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
control: 'inline-radio',
|
||||||
|
options: ['wordmark', 'symbol', 'lockup'],
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: 'inline-radio',
|
||||||
|
options: ['xs', 'sm', 'md', 'lg', 'xl'],
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
control: 'inline-radio',
|
||||||
|
options: ['auto', 'ink', 'cyan', 'inverse'],
|
||||||
|
},
|
||||||
|
orientation: {
|
||||||
|
control: 'inline-radio',
|
||||||
|
options: ['horizontal', 'vertical'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
brand: 'veza',
|
||||||
|
variant: 'wordmark',
|
||||||
|
size: 'md',
|
||||||
|
color: 'auto',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Logo>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const VezaWordmark: Story = {
|
||||||
|
args: { brand: 'veza', variant: 'wordmark', size: 'lg' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TalasWordmark: Story = {
|
||||||
|
args: { brand: 'talas', variant: 'wordmark', size: 'lg' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Symbol: Story = {
|
||||||
|
args: { brand: 'talas', variant: 'symbol', size: 'xl' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LockupHorizontal: Story = {
|
||||||
|
args: { brand: 'veza', variant: 'lockup', size: 'lg', tagline: 'STREAMING' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LockupVertical: Story = {
|
||||||
|
args: { brand: 'talas', variant: 'lockup', size: 'lg', orientation: 'vertical' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Cyan: Story = {
|
||||||
|
args: { brand: 'veza', variant: 'lockup', size: 'lg', color: 'cyan', tagline: 'BETA' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Inverse: Story = {
|
||||||
|
args: { brand: 'talas', variant: 'wordmark', size: 'xl', color: 'inverse' },
|
||||||
|
parameters: { backgrounds: { default: 'dark' } },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllSizes: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="flex flex-col gap-6 items-start">
|
||||||
|
{(['xs', 'sm', 'md', 'lg', 'xl'] as const).map((size) => (
|
||||||
|
<div key={size} className="flex items-center gap-4">
|
||||||
|
<span className="text-xs text-muted-foreground tracking-widest uppercase w-8">{size}</span>
|
||||||
|
<Logo brand="veza" variant="lockup" size={size} tagline="STREAMING" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllVariants: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="grid grid-cols-3 gap-8 items-start">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-xs text-muted-foreground uppercase">Wordmark</span>
|
||||||
|
<Logo brand="veza" variant="wordmark" size="lg" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-xs text-muted-foreground uppercase">Symbol</span>
|
||||||
|
<Logo brand="talas" variant="symbol" size="lg" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-xs text-muted-foreground uppercase">Lockup</span>
|
||||||
|
<Logo brand="veza" variant="lockup" size="lg" tagline="STREAMING" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TalasVsVeza: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div className="grid grid-cols-2 gap-8 items-start">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-xs text-muted-foreground uppercase">Talas (mother brand)</span>
|
||||||
|
<Logo brand="talas" variant="lockup" size="xl" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-xs text-muted-foreground uppercase">Veza (sub-brand)</span>
|
||||||
|
<Logo brand="veza" variant="lockup" size="xl" tagline="STREAMING" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
170
apps/web/src/components/branding/Logo.tsx
Normal file
170
apps/web/src/components/branding/Logo.tsx
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import SymbolPlaceholder from './assets/SymbolPlaceholder';
|
||||||
|
|
||||||
|
export type LogoBrand = 'talas' | 'veza';
|
||||||
|
export type LogoVariant = 'wordmark' | 'symbol' | 'lockup';
|
||||||
|
export type LogoSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
export type LogoColor = 'auto' | 'ink' | 'cyan' | 'inverse';
|
||||||
|
|
||||||
|
export interface LogoProps {
|
||||||
|
/** Which brand to render. Default 'veza' (sub-brand for the web app). */
|
||||||
|
brand?: LogoBrand;
|
||||||
|
/** Visual variant : wordmark only, symbol only, or both. Default 'wordmark'. */
|
||||||
|
variant?: LogoVariant;
|
||||||
|
/** Size — drives the wordmark font-size and symbol box. Default 'md'. */
|
||||||
|
size?: LogoSize;
|
||||||
|
/** Color treatment :
|
||||||
|
* - 'auto' : inherits text-foreground (default, theme-aware)
|
||||||
|
* - 'ink' : forced black ink (--sumi-text-primary)
|
||||||
|
* - 'cyan' : Mizu cyan accent (--sumi-accent)
|
||||||
|
* - 'inverse' : washi paper (--sumi-text-inverse — light text on dark bg)
|
||||||
|
*/
|
||||||
|
color?: LogoColor;
|
||||||
|
/** Optional tagline rendered below the wordmark (e.g. "Spectre Astral"). */
|
||||||
|
tagline?: string;
|
||||||
|
/** Layout direction for lockup variant. Default 'horizontal'. */
|
||||||
|
orientation?: 'horizontal' | 'vertical';
|
||||||
|
className?: string;
|
||||||
|
/** Override the accessible label. Default = brand name. */
|
||||||
|
'aria-label'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZE_TO_TEXT_CLASS: Record<LogoSize, string> = {
|
||||||
|
xs: 'text-xs',
|
||||||
|
sm: 'text-sm',
|
||||||
|
md: 'text-base',
|
||||||
|
lg: 'text-xl',
|
||||||
|
xl: 'text-3xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIZE_TO_SYMBOL_PX: Record<LogoSize, number> = {
|
||||||
|
xs: 14,
|
||||||
|
sm: 18,
|
||||||
|
md: 24,
|
||||||
|
lg: 32,
|
||||||
|
xl: 48,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIZE_TO_TAGLINE_CLASS: Record<LogoSize, string> = {
|
||||||
|
xs: 'text-[8px]',
|
||||||
|
sm: 'text-[9px]',
|
||||||
|
md: 'text-[10px]',
|
||||||
|
lg: 'text-xs',
|
||||||
|
xl: 'text-sm',
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLOR_TO_CLASS: Record<LogoColor, string> = {
|
||||||
|
auto: 'text-foreground',
|
||||||
|
ink: 'text-[var(--sumi-text-primary)]',
|
||||||
|
cyan: 'text-[var(--sumi-accent)]',
|
||||||
|
inverse: 'text-[var(--sumi-text-inverse)]',
|
||||||
|
};
|
||||||
|
|
||||||
|
const BRAND_TO_LABEL: Record<LogoBrand, string> = {
|
||||||
|
talas: 'TALAS',
|
||||||
|
veza: 'VEZA',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logo — single source of truth for rendering the Talas / Veza brand.
|
||||||
|
*
|
||||||
|
* Replaces ad-hoc inline wordmarks scattered across Sidebar, Navbar, Footer,
|
||||||
|
* landing pages, etc. Aligns with CHARTE_GRAPHIQUE_TALAS §3 (logo specs).
|
||||||
|
*
|
||||||
|
* Currently the wordmark renders as Space Grotesk Bold + uppercase + wide
|
||||||
|
* letter-spacing, per the charte. When the artist's hand-drawn wordmarks
|
||||||
|
* arrive (P0.1), this component will swap to consume the SVG assets via
|
||||||
|
* imports from './assets/{Brand}Wordmark.tsx'.
|
||||||
|
*
|
||||||
|
* The symbol uses a placeholder (assets/SymbolPlaceholder.tsx) until the real
|
||||||
|
* calligraphic mark is delivered.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <Logo brand="veza" variant="lockup" size="md" tagline="STREAMING" />
|
||||||
|
* <Logo brand="talas" variant="symbol" size="lg" color="cyan" />
|
||||||
|
* <Logo variant="wordmark" size="xl" color="inverse" />
|
||||||
|
*/
|
||||||
|
export function Logo({
|
||||||
|
brand = 'veza',
|
||||||
|
variant = 'wordmark',
|
||||||
|
size = 'md',
|
||||||
|
color = 'auto',
|
||||||
|
tagline,
|
||||||
|
orientation = 'horizontal',
|
||||||
|
className,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
}: LogoProps) {
|
||||||
|
const label = BRAND_TO_LABEL[brand];
|
||||||
|
const accessibleLabel = ariaLabel ?? `${label} logo`;
|
||||||
|
const colorClass = COLOR_TO_CLASS[color];
|
||||||
|
|
||||||
|
const wordmark = (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'font-heading font-bold leading-none tracking-[0.12em] uppercase',
|
||||||
|
SIZE_TO_TEXT_CLASS[size],
|
||||||
|
colorClass,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const symbol = (
|
||||||
|
<SymbolPlaceholder
|
||||||
|
width={SIZE_TO_SYMBOL_PX[size]}
|
||||||
|
height={SIZE_TO_SYMBOL_PX[size]}
|
||||||
|
className={cn('shrink-0', colorClass)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const taglineEl = tagline && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'font-heading font-light leading-none tracking-[0.2em] uppercase opacity-60',
|
||||||
|
SIZE_TO_TAGLINE_CLASS[size],
|
||||||
|
colorClass,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tagline}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variant === 'symbol') {
|
||||||
|
return (
|
||||||
|
<span role="img" aria-label={accessibleLabel} className={cn('inline-flex', className)}>
|
||||||
|
{symbol}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variant === 'wordmark') {
|
||||||
|
return (
|
||||||
|
<span role="img" aria-label={accessibleLabel} className={cn('inline-flex flex-col', className)}>
|
||||||
|
{wordmark}
|
||||||
|
{taglineEl}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// variant === 'lockup'
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
role="img"
|
||||||
|
aria-label={accessibleLabel}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center',
|
||||||
|
orientation === 'vertical' ? 'flex-col gap-1' : 'gap-2',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{symbol}
|
||||||
|
<span className="inline-flex flex-col">
|
||||||
|
{wordmark}
|
||||||
|
{taglineEl}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logo;
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import type { SVGProps } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Talas symbol — placeholder SVG until the artist's hand-drawn version arrives.
|
||||||
|
*
|
||||||
|
* The real symbol (per CHARTE_GRAPHIQUE_TALAS §3.1) is a calligraphic gesture
|
||||||
|
* evoking both an audio waveform and a brush stroke. It will be:
|
||||||
|
* - Hand-drawn (paper or tablet, then vectorized in Inkscape)
|
||||||
|
* - Irregular (imperfections preserved, no auto-smoothing)
|
||||||
|
* - Monochrome (currentColor)
|
||||||
|
* - Functional from 16x16 (favicon) to engraving size
|
||||||
|
*
|
||||||
|
* This placeholder is geometric: an asymmetric ink stroke crossed by a single
|
||||||
|
* fluid arc — strict monochrome, currentColor inheritance, scalable. It exists
|
||||||
|
* to keep the Logo component working until P0.1 of BRIEF_ARTISTE delivers.
|
||||||
|
*
|
||||||
|
* To replace : drop the artist's vectorized SVG at
|
||||||
|
* apps/web/src/components/branding/assets/Symbol.tsx
|
||||||
|
* exporting a default React component with the same SVGProps signature, and
|
||||||
|
* update Logo.tsx to import from './assets/Symbol' instead.
|
||||||
|
*/
|
||||||
|
export default function SymbolPlaceholder(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{/* Vertical brush stroke — ink */}
|
||||||
|
<path
|
||||||
|
d="M7 4 Q 8.5 9, 7.5 14 T 8 20"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
{/* Curved arc — sound wave */}
|
||||||
|
<path
|
||||||
|
d="M11 5 Q 17 12, 12 19"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
opacity="0.85"
|
||||||
|
/>
|
||||||
|
{/* Ink dot — punctuation */}
|
||||||
|
<circle cx="18.5" cy="6" r="0.9" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
apps/web/src/components/branding/index.ts
Normal file
2
apps/web/src/components/branding/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { Logo, type LogoBrand, type LogoColor, type LogoProps, type LogoSize, type LogoVariant } from './Logo';
|
||||||
|
export { default as SymbolPlaceholder } from './assets/SymbolPlaceholder';
|
||||||
25
apps/web/src/components/icons/sumi/Play.tsx
Normal file
25
apps/web/src/components/icons/sumi/Play.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import type { SVGProps } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play — Sumi calligraphic icon (placeholder).
|
||||||
|
*
|
||||||
|
* Per CHARTE_GRAPHIQUE_TALAS §6.3 : "Triangle en un seul trait rapide".
|
||||||
|
*
|
||||||
|
* This file's geometry is a programmatic approximation. The artist (P3 of
|
||||||
|
* BRIEF_ARTISTE_IDENTITE_VISUELLE) should replace it with a scanned + vectorized
|
||||||
|
* hand-drawn version that preserves the irregularity of a real brush stroke
|
||||||
|
* (variable thickness, no auto-smoothing).
|
||||||
|
*/
|
||||||
|
export default function PlayIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{/* Single closed brush stroke approximating a play triangle */}
|
||||||
|
<path d="M7 5.5 L 7.5 18.5 L 18.2 12.3 Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
apps/web/src/components/icons/sumi/index.ts
Normal file
42
apps/web/src/components/icons/sumi/index.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* Sumi calligraphic icon set — placeholders + delivered.
|
||||||
|
*
|
||||||
|
* Per CHARTE_GRAPHIQUE_TALAS §6.3, the design system specifies 10 priority
|
||||||
|
* icons drawn as brush gestures (variable stroke, irregular, monochrome,
|
||||||
|
* 24x24 viewBox). Each icon here is an SVG React component consumed via
|
||||||
|
* SumiIcon (`../SumiIcon.tsx`) which falls back to a Lucide icon when no
|
||||||
|
* Sumi version exists yet.
|
||||||
|
*
|
||||||
|
* Workflow to add a new icon :
|
||||||
|
* 1. Artist draws the icon on paper (or tablet). Hi-res scan, transparent
|
||||||
|
* background, monochrome.
|
||||||
|
* 2. Vectorize manually in Inkscape (no auto-trace — preserves irregularity).
|
||||||
|
* Export as SVG with currentColor and 24x24 viewBox.
|
||||||
|
* 3. Save as `apps/web/src/components/icons/sumi/{Name}.tsx`, exporting
|
||||||
|
* default a React functional component receiving SVGProps<SVGSVGElement>.
|
||||||
|
* 4. Add to the barrel below.
|
||||||
|
* 5. At call sites : `<SumiIcon sumi={PlayIcon} fallback={Play} />`.
|
||||||
|
*
|
||||||
|
* Priority list (CHARTE_GRAPHIQUE §6.3) :
|
||||||
|
* ✓ Play — triangle en un seul trait rapide
|
||||||
|
* ☐ Pause — deux traits verticaux paralleles
|
||||||
|
* ☐ Search — enso (cercle zen ouvert, non ferme)
|
||||||
|
* ☐ Profile — capsule de micro (ovale + trait de base)
|
||||||
|
* ☐ Chat — onde sonore (trois arcs concentriques)
|
||||||
|
* ☐ Upload — trait ascendant avec goutte au sommet
|
||||||
|
* ☐ Settings — ensui (cercle + trait directionnel)
|
||||||
|
* ☐ Home — triangle inverse, montagne minimaliste
|
||||||
|
* ☐ Close — deux traits croises d'un seul geste
|
||||||
|
* ☐ Volume — arc de cercle avec diffusion
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as PlayIcon } from './Play';
|
||||||
|
// export { default as PauseIcon } from './Pause';
|
||||||
|
// export { default as SearchIcon } from './Search';
|
||||||
|
// export { default as ProfileIcon } from './Profile';
|
||||||
|
// export { default as ChatIcon } from './Chat';
|
||||||
|
// export { default as UploadIcon } from './Upload';
|
||||||
|
// export { default as SettingsIcon } from './Settings';
|
||||||
|
// export { default as HomeIcon } from './Home';
|
||||||
|
// export { default as CloseIcon } from './Close';
|
||||||
|
// export { default as VolumeIcon } from './Volume';
|
||||||
Loading…
Reference in a new issue