Compare commits

..

11 Commits

Author SHA1 Message Date
hsiegeln
a3afe3cb1b feat(assets): add high-detail cameleer3-logo.svg traced from PNG via Inkscape
All checks were successful
Build & Publish / publish (push) Successful in 1m4s
32-scan potrace vector trace with transparent background. Added to brand
assets inventory, documentation, and package exports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:58:44 +02:00
hsiegeln
4841a7ad7c refactor(assets): replace named exports with wildcard ./assets/* export
All checks were successful
Build & Publish / publish (push) Successful in 1m10s
Consumers now import via @cameleer/design-system/assets/<filename> instead
of named aliases like /logo-32. Simpler, more flexible, and supports any
future assets without adding new export entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:43:01 +02:00
hsiegeln
32a49690fa style(assets): make all logo PNGs transparent using Inkscape trace
All checks were successful
Build & Publish / publish (push) Successful in 1m7s
Traced the original logo with Inkscape (16-scan potrace, background removal)
and re-exported all PNG variants with transparent backgrounds. Also reduces
total asset size from ~1.7MB to ~630KB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:14:50 +02:00
hsiegeln
20f7b2f5aa feat(assets): ship Cameleer3 logo and favicon variants with the design system
All checks were successful
Build & Publish / publish (push) Successful in 1m5s
Add brand assets as static package exports (logo, logo-16 through logo-512, logo-svg)
with pre-generated PNG sizes for favicons, PWA icons, Apple touch icons, and social images.
Includes inventory showcase section and updated documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:17:36 +02:00
hsiegeln
5cb51e65be style(topbar): make .env badge match neutral button style of liveToggle/themeToggle
All checks were successful
Build & Publish / publish (push) Successful in 1m42s
SonarQube Analysis / sonarqube (push) Successful in 2m26s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:59:00 +02:00
hsiegeln
4dcd4aaa27 feat(topbar): change environment prop from string to ReactNode
All checks were successful
Build & Publish / publish (push) Successful in 1m21s
Allows consuming apps to pass a custom dropdown or any interactive
element instead of a static string label. Rendering changed from
<span> to <div> to support block-level children.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:25:20 +02:00
hsiegeln
58320b9762 fix(topbar): rename refresh toggle labels from LIVE/PAUSED to AUTO/MANUAL
All checks were successful
Build & Publish / publish (push) Successful in 54s
SonarQube Analysis / sonarqube (push) Successful in 3m3s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:30:17 +02:00
hsiegeln
c48dffaef2 feat(global-filter): add refreshTimeRange() for manual refresh in paused mode
All checks were successful
Build & Publish / publish (push) Successful in 55s
Revert auto-sliding when paused — time range only advances with
auto-refresh on. Add refreshTimeRange() to useGlobalFilters for
on-demand recomputation from the active preset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:01:47 +02:00
hsiegeln
3ef4c5686e fix(global-filter): keep time range sliding when auto-refresh is paused
All checks were successful
Build & Publish / publish (push) Successful in 1m6s
The preset time window now advances on a 10s interval regardless of
auto-refresh state. Pausing only stops query polling — the window
stays current so manual refreshes see up-to-date data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:16:11 +02:00
hsiegeln
78e28789a5 docs: update CommandPalette entry for open SearchCategory type
All checks were successful
Build & Publish / publish (push) Successful in 1m36s
SonarQube Analysis / sonarqube (push) Successful in 2m52s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:23:32 +02:00
hsiegeln
03ec34bb5c feat(command-palette): open SearchCategory to arbitrary strings
All checks were successful
Build & Publish / publish (push) Successful in 1m33s
Widen SearchCategory from a closed union to string. Known categories
(application, exchange, attribute, route, agent) keep their labels.
Unknown categories render with title-cased labels and appear as
dynamic tabs derived from the data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:21:28 +02:00
22 changed files with 559 additions and 23 deletions

View File

@@ -118,4 +118,13 @@ import type { ChartSeries, DataPoint } from '@cameleer/design-system'
// Styles (once, at app root) // Styles (once, at app root)
import '@cameleer/design-system/style.css' import '@cameleer/design-system/style.css'
// Brand assets (static files via ./assets/* export)
import logo from '@cameleer/design-system/assets/cameleer3-logo.png' // full resolution
import logo32 from '@cameleer/design-system/assets/cameleer3-32.png' // 32×32 favicon
import logo180 from '@cameleer/design-system/assets/cameleer3-180.png' // Apple touch icon
import logo192 from '@cameleer/design-system/assets/cameleer3-192.png' // Android/PWA icon
import logo512 from '@cameleer/design-system/assets/cameleer3-512.png' // PWA splash, og:image
import logoSvg from '@cameleer/design-system/assets/cameleer3-logo.svg' // high-detail SVG logo
import camelSvg from '@cameleer/design-system/assets/camel-logo.svg' // simplified camel SVG
``` ```

View File

@@ -246,7 +246,7 @@ import {
| Checkbox | primitive | Boolean input with label | | Checkbox | primitive | Boolean input with label |
| CodeBlock | primitive | Syntax-highlighted code/JSON display | | CodeBlock | primitive | Syntax-highlighted code/JSON display |
| Collapsible | primitive | Single expand/collapse section | | Collapsible | primitive | Single expand/collapse section |
| CommandPalette | composite | Full-screen search and command interface | | CommandPalette | composite | Full-screen search and command interface. `SearchCategory` is an open `string` type — known categories (application, exchange, attribute, route, agent) have built-in labels; custom categories render with title-cased labels and appear as dynamic tabs. |
| ConfirmDialog | composite | Type-to-confirm destructive action dialog built on Modal. Props: open, onClose, onConfirm, title, message, confirmText, confirmLabel, cancelLabel, variant, loading, className | | ConfirmDialog | composite | Type-to-confirm destructive action dialog built on Modal. Props: open, onClose, onConfirm, title, message, confirmText, confirmLabel, cancelLabel, variant, loading, className |
| DataTable | composite | Sortable, paginated data table with row actions. Use `flush` prop when embedded inside a container that provides its own border/radius | | DataTable | composite | Sortable, paginated data table with row actions. Use `flush` prop when embedded inside a container that provides its own border/radius |
| DateRangePicker | primitive | Date range selection with presets | | DateRangePicker | primitive | Date range selection with presets |
@@ -306,7 +306,7 @@ import {
| Sidebar | Composable compound sidebar shell with icon-rail collapse mode. Sub-components: `Sidebar.Header`, `Sidebar.Section`, `Sidebar.Footer`, `Sidebar.FooterLink`. The app controls all content via children — the DS provides the frame. | | Sidebar | Composable compound sidebar shell with icon-rail collapse mode. Sub-components: `Sidebar.Header`, `Sidebar.Section`, `Sidebar.Footer`, `Sidebar.FooterLink`. The app controls all content via children — the DS provides the frame. |
| SidebarTree | Data-driven tree for sidebar sections. Accepts `nodes: SidebarTreeNode[]` with expand/collapse, starring, keyboard nav, search filter, and path-based selection highlighting. | | SidebarTree | Data-driven tree for sidebar sections. Accepts `nodes: SidebarTreeNode[]` with expand/collapse, starring, keyboard nav, search filter, and path-based selection highlighting. |
| useStarred | Hook for localStorage-backed starred item IDs. Returns `{ starredIds, isStarred, toggleStar }`. | | useStarred | Hook for localStorage-backed starred item IDs. Returns `{ starredIds, isStarred, toggleStar }`. |
| TopBar | Header bar with breadcrumb, search trigger, ButtonGroup status filters, time range selector, theme toggle, environment badge, user avatar | | TopBar | Header bar with breadcrumb, search trigger, ButtonGroup status filters, time range selector, theme toggle, environment slot (`ReactNode` — pass a string for a static label or a custom dropdown for interactive selection), user avatar |
## Import Paths ## Import Paths
@@ -334,6 +334,35 @@ import type { Column, DataTableProps, SearchResult } from '@cameleer/design-syst
See `CLAUDE.md` "Using This Design System in Other Apps" for full setup instructions. See `CLAUDE.md` "Using This Design System in Other Apps" for full setup instructions.
## Brand Assets
The design system ships logo assets as static files via the `./assets/*` package export. These are not React components — they resolve to file URLs when imported via a bundler. All PNGs have transparent backgrounds.
| File | Size | Use case |
|------|------|----------|
| `cameleer3-logo.png` | Original | Full resolution for print/marketing |
| `cameleer3-16.png` | 16×16 | Browser tab favicon |
| `cameleer3-32.png` | 32×32 | Standard favicon, bookmarks |
| `cameleer3-48.png` | 48×48 | Windows taskbar |
| `cameleer3-180.png` | 180×180 | Apple touch icon |
| `cameleer3-192.png` | 192×192 | Android/PWA icon |
| `cameleer3-512.png` | 512×512 | PWA splash, og:image |
| `cameleer3-logo.svg` | Vector | High-detail SVG logo (traced from PNG, transparent) |
| `camel-logo.svg` | Vector | Simplified camel SVG logo |
### Usage
```tsx
import logo from '@cameleer/design-system/assets/cameleer3-512.png'
<img src={logo} alt="Cameleer3" />
```
```html
<!-- Favicons in index.html -->
<link rel="icon" type="image/png" sizes="32x32" href="/cameleer3-32.png">
<link rel="apple-touch-icon" sizes="180x180" href="/cameleer3-180.png">
```
## Styling Rules ## Styling Rules
- **CSS Modules only** — no inline styles except dynamic values (width, color from props) - **CSS Modules only** — no inline styles except dynamic values (width, color from props)

3
assets/camel-logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
assets/cameleer3-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

BIN
assets/cameleer3-180.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/cameleer3-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/cameleer3-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
assets/cameleer3-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
assets/cameleer3-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

BIN
assets/cameleer3-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

144
assets/cameleer3-logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -10,10 +10,12 @@
"types": "./dist/index.es.d.ts", "types": "./dist/index.es.d.ts",
"import": "./dist/index.es.js" "import": "./dist/index.es.js"
}, },
"./style.css": "./dist/style.css" "./style.css": "./dist/style.css",
"./assets/*": "./assets/*"
}, },
"files": [ "files": [
"dist" "dist",
"assets"
], ],
"sideEffects": [ "sideEffects": [
"*.css" "*.css"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -19,8 +19,7 @@ interface CommandPaletteProps {
onSubmit?: (query: string) => void onSubmit?: (query: string) => void
} }
const CATEGORY_LABELS: Record<SearchCategory | 'all', string> = { const KNOWN_CATEGORY_LABELS: Record<string, string> = {
all: 'All',
application: 'Applications', application: 'Applications',
exchange: 'Exchanges', exchange: 'Exchanges',
attribute: 'Attributes', attribute: 'Attributes',
@@ -28,8 +27,8 @@ const CATEGORY_LABELS: Record<SearchCategory | 'all', string> = {
agent: 'Agents', agent: 'Agents',
} }
const ALL_CATEGORIES: Array<SearchCategory | 'all'> = [ /** Preferred display order for known categories */
'all', const KNOWN_CATEGORY_ORDER: string[] = [
'application', 'application',
'exchange', 'exchange',
'attribute', 'attribute',
@@ -37,6 +36,13 @@ const ALL_CATEGORIES: Array<SearchCategory | 'all'> = [
'agent', 'agent',
] ]
function categoryLabel(cat: string): string {
if (cat === 'all') return 'All'
if (KNOWN_CATEGORY_LABELS[cat]) return KNOWN_CATEGORY_LABELS[cat]
// Title-case unknown categories: "my-thing" → "My Thing", "foo_bar" → "Foo Bar"
return cat.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
}
function highlightText(text: string, query: string, matchRanges?: [number, number][]): ReactNode { function highlightText(text: string, query: string, matchRanges?: [number, number][]): ReactNode {
if (!query && (!matchRanges || matchRanges.length === 0)) return text if (!query && (!matchRanges || matchRanges.length === 0)) return text
@@ -69,7 +75,7 @@ function highlightText(text: string, query: string, matchRanges?: [number, numbe
export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryChange, onSubmit }: CommandPaletteProps) { export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryChange, onSubmit }: CommandPaletteProps) {
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const [activeCategory, setActiveCategory] = useState<SearchCategory | 'all'>('all') const [activeCategory, setActiveCategory] = useState<string>('all')
const [scopeFilters, setScopeFilters] = useState<ScopeFilter[]>([]) const [scopeFilters, setScopeFilters] = useState<ScopeFilter[]>([])
const [focusedIdx, setFocusedIdx] = useState(0) const [focusedIdx, setFocusedIdx] = useState(0)
const [expandedId, setExpandedId] = useState<string | null>(null) const [expandedId, setExpandedId] = useState<string | null>(null)
@@ -128,7 +134,7 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
// Group results by category // Group results by category
const grouped = useMemo(() => { const grouped = useMemo(() => {
const map = new Map<SearchCategory, SearchResult[]>() const map = new Map<string, SearchResult[]>()
for (const r of filtered) { for (const r of filtered) {
if (!map.has(r.category)) map.set(r.category, []) if (!map.has(r.category)) map.set(r.category, [])
map.get(r.category)!.push(r) map.get(r.category)!.push(r)
@@ -148,6 +154,19 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
return counts return counts
}, [queryFiltered]) }, [queryFiltered])
// Build tab list dynamically: 'all' + known categories (in order) + any unknown categories found in data
const visibleCategories = useMemo(() => {
const dataCategories = new Set(data.map((r) => r.category))
const tabs: string[] = ['all']
for (const cat of KNOWN_CATEGORY_ORDER) {
if (dataCategories.has(cat)) tabs.push(cat)
}
for (const cat of dataCategories) {
if (!tabs.includes(cat)) tabs.push(cat)
}
return tabs
}, [data])
function handleKeyDown(e: React.KeyboardEvent) { function handleKeyDown(e: React.KeyboardEvent) {
switch (e.key) { switch (e.key) {
case 'Escape': case 'Escape':
@@ -246,7 +265,7 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
{/* Category tabs */} {/* Category tabs */}
<div className={styles.tabs} role="tablist"> <div className={styles.tabs} role="tablist">
{ALL_CATEGORIES.map((cat) => ( {visibleCategories.map((cat) => (
<button <button
key={cat} key={cat}
role="tab" role="tab"
@@ -262,7 +281,7 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
setFocusedIdx(0) setFocusedIdx(0)
}} }}
> >
{CATEGORY_LABELS[cat]} {categoryLabel(cat)}
{categoryCounts[cat] != null && ( {categoryCounts[cat] != null && (
<span className={styles.tabCount}>{categoryCounts[cat]}</span> <span className={styles.tabCount}>{categoryCounts[cat]}</span>
)} )}
@@ -283,7 +302,7 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
Array.from(grouped.entries()).map(([category, items]) => ( Array.from(grouped.entries()).map(([category, items]) => (
<div key={category} className={styles.group}> <div key={category} className={styles.group}>
<div className={styles.groupHeader}> <div className={styles.groupHeader}>
<SectionHeader>{CATEGORY_LABELS[category]}</SectionHeader> <SectionHeader>{categoryLabel(category)}</SectionHeader>
</div> </div>
{items.map((result) => { {items.map((result) => {
const flatIdx = flatResults.indexOf(result) const flatIdx = flatResults.indexOf(result)

View File

@@ -1,6 +1,7 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
export type SearchCategory = 'application' | 'exchange' | 'attribute' | 'route' | 'agent' /** Known categories: 'application' | 'exchange' | 'attribute' | 'route' | 'agent'. Custom categories are rendered with title-cased labels and a default icon. */
export type SearchCategory = string
export interface SearchResult { export interface SearchResult {
id: string id: string

View File

@@ -153,14 +153,17 @@
} }
.env { .env {
display: flex;
align-items: center;
height: 30px;
padding: 4px 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-raised);
color: var(--text-muted);
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 600;
padding: 3px 10px;
border-radius: 10px;
background: var(--success-bg);
color: var(--success);
border: 1px solid var(--success-border);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }

View File

@@ -1,3 +1,4 @@
import { type ReactNode } from 'react'
import { Search, Moon, Sun, Power } from 'lucide-react' import { Search, Moon, Sun, Power } from 'lucide-react'
import styles from './TopBar.module.css' import styles from './TopBar.module.css'
import { Breadcrumb } from '../../composites/Breadcrumb/Breadcrumb' import { Breadcrumb } from '../../composites/Breadcrumb/Breadcrumb'
@@ -14,7 +15,7 @@ import type { BreadcrumbItem } from '../../providers/BreadcrumbProvider'
interface TopBarProps { interface TopBarProps {
breadcrumb: BreadcrumbItem[] breadcrumb: BreadcrumbItem[]
environment?: string environment?: ReactNode
user?: { name: string } user?: { name: string }
onLogout?: () => void onLogout?: () => void
className?: string className?: string
@@ -90,7 +91,7 @@ export function TopBar({
title={globalFilters.autoRefresh ? 'Auto-refresh is on — click to pause' : 'Auto-refresh is paused — click to resume'} title={globalFilters.autoRefresh ? 'Auto-refresh is on — click to pause' : 'Auto-refresh is paused — click to resume'}
> >
<span className={styles.liveDot} /> <span className={styles.liveDot} />
{globalFilters.autoRefresh ? 'LIVE' : 'PAUSED'} {globalFilters.autoRefresh ? 'AUTO' : 'MANUAL'}
</button> </button>
<button <button
className={styles.themeToggle} className={styles.themeToggle}
@@ -102,7 +103,7 @@ export function TopBar({
{theme === 'light' ? <Moon size={16} /> : <Sun size={16} />} {theme === 'light' ? <Moon size={16} /> : <Sun size={16} />}
</button> </button>
{environment && ( {environment && (
<span className={styles.env}>{environment}</span> <div className={styles.env}>{environment}</div>
)} )}
{user && ( {user && (
<Dropdown <Dropdown

View File

@@ -12,6 +12,7 @@ export type ExchangeStatus = 'completed' | 'failed' | 'running' | 'warning'
interface GlobalFilterContextValue { interface GlobalFilterContextValue {
timeRange: TimeRange timeRange: TimeRange
setTimeRange: (range: TimeRange) => void setTimeRange: (range: TimeRange) => void
refreshTimeRange: () => void
statusFilters: Set<ExchangeStatus> statusFilters: Set<ExchangeStatus>
toggleStatus: (status: ExchangeStatus) => void toggleStatus: (status: ExchangeStatus) => void
clearStatusFilters: () => void clearStatusFilters: () => void
@@ -76,6 +77,14 @@ export function GlobalFilterProvider({ children }: { children: ReactNode }) {
return () => clearInterval(id) return () => clearInterval(id)
}, [autoRefresh, timeRange.preset]) }, [autoRefresh, timeRange.preset])
// Recompute time range from preset on demand (for manual refresh in PAUSED mode)
const refreshTimeRange = useCallback(() => {
if (timeRange.preset) {
const { start, end } = computePresetRange(timeRange.preset)
setTimeRangeState({ start, end, preset: timeRange.preset })
}
}, [timeRange.preset])
const isInTimeRange = useCallback( const isInTimeRange = useCallback(
(timestamp: Date) => { (timestamp: Date) => {
if (timeRange.preset) { if (timeRange.preset) {
@@ -90,7 +99,7 @@ export function GlobalFilterProvider({ children }: { children: ReactNode }) {
return ( return (
<GlobalFilterContext.Provider <GlobalFilterContext.Provider
value={{ timeRange, setTimeRange, statusFilters, toggleStatus, clearStatusFilters, isInTimeRange, autoRefresh, setAutoRefresh }} value={{ timeRange, setTimeRange, refreshTimeRange, statusFilters, toggleStatus, clearStatusFilters, isInTimeRange, autoRefresh, setAutoRefresh }}
> >
{children} {children}
</GlobalFilterContext.Provider> </GlobalFilterContext.Provider>

View File

@@ -3,6 +3,7 @@ import styles from './Inventory.module.css'
import { PrimitivesSection } from './sections/PrimitivesSection' import { PrimitivesSection } from './sections/PrimitivesSection'
import { CompositesSection } from './sections/CompositesSection' import { CompositesSection } from './sections/CompositesSection'
import { LayoutSection } from './sections/LayoutSection' import { LayoutSection } from './sections/LayoutSection'
import { BrandAssetsSection } from './sections/BrandAssetsSection'
const NAV_SECTIONS = [ const NAV_SECTIONS = [
{ {
@@ -93,6 +94,13 @@ const NAV_SECTIONS = [
{ label: 'TopBar', href: '#topbar' }, { label: 'TopBar', href: '#topbar' },
], ],
}, },
{
label: 'Brand Assets',
href: '#brand-assets',
components: [
{ label: 'Cameleer3 Logo', href: '#brand-assets' },
],
},
] ]
export function Inventory() { export function Inventory() {
@@ -121,6 +129,7 @@ export function Inventory() {
<PrimitivesSection /> <PrimitivesSection />
<CompositesSection /> <CompositesSection />
<LayoutSection /> <LayoutSection />
<BrandAssetsSection />
</main> </main>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,86 @@
.section {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 22px;
font-weight: 700;
color: var(--text-primary);
margin: 0 0 24px;
}
.componentCard {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 16px;
box-shadow: var(--shadow-sm);
}
.componentTitle {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 4px;
}
.componentDesc {
font-size: 12px;
color: var(--text-muted);
margin: 0 0 16px;
}
.demoArea {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-start;
}
.logoGrid {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-end;
}
.logoItem {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.logoPreview {
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-body);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 12px;
min-width: 48px;
min-height: 48px;
}
.logoPreview img {
display: block;
image-rendering: auto;
}
.logoLabel {
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
}
.logoExport {
font-size: 10px;
font-family: var(--font-mono);
color: var(--text-muted);
background: var(--bg-body);
padding: 2px 6px;
border-radius: var(--radius-sm);
white-space: nowrap;
}

View File

@@ -0,0 +1,77 @@
import styles from './BrandAssetsSection.module.css'
import camelLogoSvg from '../../../assets/camel-logo.svg'
import cameleer3Logo from '../../../assets/cameleer3-logo.png'
import cameleer3LogoSvg from '../../../assets/cameleer3-logo.svg'
const LOGO_SIZES = [16, 32, 48, 180, 192, 512] as const
export function BrandAssetsSection() {
return (
<section id="brand-assets" className={styles.section}>
<h2 className={styles.sectionTitle}>Brand Assets</h2>
<div className={styles.componentCard}>
<h3 className={styles.componentTitle}>Cameleer3 Logo (PNG)</h3>
<p className={styles.componentDesc}>
Full-resolution logo and pre-generated size variants for favicons, PWA icons, and social images.
Shipped as static assets via <code>@cameleer/design-system/assets/*</code> export.
</p>
<div className={styles.demoArea}>
<div className={styles.logoGrid}>
{LOGO_SIZES.map((size) => (
<div key={size} className={styles.logoItem}>
<div className={styles.logoPreview}>
<img
src={cameleer3Logo}
alt={`Logo ${size}×${size}`}
width={Math.min(size, 96)}
height={Math.min(size, 96)}
/>
</div>
<span className={styles.logoLabel}>{size}×{size}</span>
<code className={styles.logoExport}>assets/cameleer3-{size}.png</code>
</div>
))}
<div className={styles.logoItem}>
<div className={styles.logoPreview}>
<img
src={cameleer3Logo}
alt="Full resolution logo"
width={96}
height={96}
/>
</div>
<span className={styles.logoLabel}>Original</span>
<code className={styles.logoExport}>assets/cameleer3-logo.png</code>
</div>
</div>
</div>
</div>
<div className={styles.componentCard}>
<h3 className={styles.componentTitle}>SVG Logos</h3>
<p className={styles.componentDesc}>
Vector logos for scalable usage. Transparent backgrounds, infinitely scalable.
</p>
<div className={styles.demoArea}>
<div className={styles.logoGrid}>
<div className={styles.logoItem}>
<div className={styles.logoPreview}>
<img src={cameleer3LogoSvg} alt="Cameleer3 SVG logo" width={96} height={96} />
</div>
<span className={styles.logoLabel}>Cameleer3 SVG</span>
<code className={styles.logoExport}>assets/cameleer3-logo.svg</code>
</div>
<div className={styles.logoItem}>
<div className={styles.logoPreview}>
<img src={camelLogoSvg} alt="Camel SVG logo" width={96} height={96} />
</div>
<span className={styles.logoLabel}>Camel SVG</span>
<code className={styles.logoExport}>assets/camel-logo.svg</code>
</div>
</div>
</div>
</div>
</section>
)
}