mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:01:10 +02:00
feat: add shared-branding package and extend shared-utils
NEW PACKAGE: @manacore/shared-branding - AppLogo: SVG logo component for any Mana app - AppLogoWithName: Logo with app name for headers - ManaIcon: Universal Mana drop icon for credits - Branding config: Centralized colors, names, taglines - Support for memoro, manacore, manadeck, maerchenzauber ENHANCED: @manacore/shared-utils - formatTimestamp: Relative day labels (Today/Yesterday) with i18n - Re-export isToday, isYesterday from date-fns App branding centralized: - memoro: Gold (#f8d62b), AI Voice Memos - manacore: Indigo (#6366f1), Central Hub - manadeck: Purple (#8b5cf6), AI Flashcards - maerchenzauber: Pink (#ec4899), AI Story Creator 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
afdc30bd5f
commit
7d426d57fd
10 changed files with 493 additions and 1 deletions
129
packages/shared-branding/README.md
Normal file
129
packages/shared-branding/README.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# @manacore/shared-branding
|
||||
|
||||
Shared branding components and configuration for the Mana ecosystem.
|
||||
|
||||
## Features
|
||||
|
||||
- **AppLogo**: SVG logo component for any Mana app
|
||||
- **AppLogoWithName**: Logo with app name combination
|
||||
- **ManaIcon**: Universal Mana drop icon for credits display
|
||||
- **Branding Config**: Centralized colors, names, and taglines
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pnpm add @manacore/shared-branding
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### AppLogo
|
||||
|
||||
Display an app's logo:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { AppLogo } from '@manacore/shared-branding';
|
||||
</script>
|
||||
|
||||
<AppLogo app="memoro" size={32} />
|
||||
<AppLogo app="manacore" size={32} />
|
||||
<AppLogo app="manadeck" size={32} />
|
||||
<AppLogo app="maerchenzauber" size={32} />
|
||||
```
|
||||
|
||||
### AppLogoWithName
|
||||
|
||||
Display logo with app name (perfect for headers):
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { AppLogoWithName } from '@manacore/shared-branding';
|
||||
</script>
|
||||
|
||||
<AppLogoWithName app="memoro" size={28} />
|
||||
<AppLogoWithName app="manacore" showName={false} />
|
||||
```
|
||||
|
||||
### ManaIcon
|
||||
|
||||
Universal Mana drop icon:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { ManaIcon } from '@manacore/shared-branding';
|
||||
</script>
|
||||
|
||||
<ManaIcon size={24} color="#4287f5" />
|
||||
```
|
||||
|
||||
### Branding Configuration
|
||||
|
||||
Access branding config programmatically:
|
||||
|
||||
```typescript
|
||||
import { getAppBranding, APP_BRANDING } from '@manacore/shared-branding';
|
||||
|
||||
const memoro = getAppBranding('memoro');
|
||||
console.log(memoro.name); // "Memoro"
|
||||
console.log(memoro.tagline); // "AI Voice Memos"
|
||||
console.log(memoro.primaryColor); // "#f8d62b"
|
||||
```
|
||||
|
||||
## App Branding
|
||||
|
||||
| App | Name | Primary Color | Tagline |
|
||||
|-----|------|---------------|---------|
|
||||
| `memoro` | Memoro | #f8d62b (Gold) | AI Voice Memos |
|
||||
| `manacore` | ManaCore | #6366f1 (Indigo) | Central Hub |
|
||||
| `manadeck` | ManaDeck | #8b5cf6 (Purple) | AI Flashcards |
|
||||
| `maerchenzauber` | Märchenzauber | #ec4899 (Pink) | AI Story Creator |
|
||||
|
||||
## Props
|
||||
|
||||
### AppLogo
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `app` | `AppId` | required | App identifier |
|
||||
| `size` | `number` | `32` | Size in pixels |
|
||||
| `color` | `string` | App primary color | Override color |
|
||||
| `class` | `string` | `''` | Additional CSS classes |
|
||||
|
||||
### AppLogoWithName
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `app` | `AppId` | required | App identifier |
|
||||
| `size` | `number` | `28` | Logo size in pixels |
|
||||
| `color` | `string` | App primary color | Override color |
|
||||
| `showName` | `boolean` | `true` | Show app name |
|
||||
| `nameFontSize` | `string` | `'1.25rem'` | Name font size |
|
||||
| `gap` | `string` | `'0.5rem'` | Gap between logo and name |
|
||||
| `class` | `string` | `''` | Additional CSS classes |
|
||||
|
||||
### ManaIcon
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `size` | `number` | `24` | Size in pixels |
|
||||
| `color` | `string` | `'#4287f5'` | Icon color |
|
||||
| `class` | `string` | `''` | Additional CSS classes |
|
||||
|
||||
## Types
|
||||
|
||||
```typescript
|
||||
type AppId = 'memoro' | 'manacore' | 'manadeck' | 'maerchenzauber';
|
||||
|
||||
interface AppBranding {
|
||||
id: AppId;
|
||||
name: string;
|
||||
tagline: string;
|
||||
primaryColor: string;
|
||||
secondaryColor?: string;
|
||||
logoPath: string;
|
||||
logoViewBox?: string;
|
||||
logoStroke?: boolean;
|
||||
logoStrokeWidth?: number;
|
||||
}
|
||||
```
|
||||
23
packages/shared-branding/package.json
Normal file
23
packages/shared-branding/package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "@manacore/shared-branding",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"svelte": "./src/index.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
}
|
||||
}
|
||||
50
packages/shared-branding/src/AppLogo.svelte
Normal file
50
packages/shared-branding/src/AppLogo.svelte
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">
|
||||
import type { AppId } from './types';
|
||||
import { APP_BRANDING } from './config';
|
||||
|
||||
interface Props {
|
||||
/** App to show logo for */
|
||||
app: AppId;
|
||||
/** Size in pixels */
|
||||
size?: number;
|
||||
/** Override color */
|
||||
color?: string;
|
||||
/** Additional CSS classes */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
app,
|
||||
size = 32,
|
||||
color,
|
||||
class: className = ''
|
||||
}: Props = $props();
|
||||
|
||||
const branding = $derived(APP_BRANDING[app]);
|
||||
const fillColor = $derived(color ?? branding.primaryColor);
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={branding.logoViewBox ?? '0 0 24 24'}
|
||||
class={className}
|
||||
aria-label="{branding.name} logo"
|
||||
>
|
||||
{#if branding.logoStroke}
|
||||
<path
|
||||
d={branding.logoPath}
|
||||
fill="none"
|
||||
stroke={fillColor}
|
||||
stroke-width={branding.logoStrokeWidth ?? 2}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
{:else}
|
||||
<path
|
||||
d={branding.logoPath}
|
||||
fill={fillColor}
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
59
packages/shared-branding/src/AppLogoWithName.svelte
Normal file
59
packages/shared-branding/src/AppLogoWithName.svelte
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<script lang="ts">
|
||||
import type { AppId } from './types';
|
||||
import { APP_BRANDING } from './config';
|
||||
import AppLogo from './AppLogo.svelte';
|
||||
|
||||
interface Props {
|
||||
/** App to show logo for */
|
||||
app: AppId;
|
||||
/** Logo size in pixels */
|
||||
size?: number;
|
||||
/** Override color */
|
||||
color?: string;
|
||||
/** Show app name */
|
||||
showName?: boolean;
|
||||
/** Name font size */
|
||||
nameFontSize?: string;
|
||||
/** Gap between logo and name */
|
||||
gap?: string;
|
||||
/** Additional CSS classes */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
app,
|
||||
size = 28,
|
||||
color,
|
||||
showName = true,
|
||||
nameFontSize = '1.25rem',
|
||||
gap = '0.5rem',
|
||||
class: className = ''
|
||||
}: Props = $props();
|
||||
|
||||
const branding = $derived(APP_BRANDING[app]);
|
||||
</script>
|
||||
|
||||
<a href="/" class="app-logo-with-name {className}" style="gap: {gap};">
|
||||
<AppLogo {app} {size} {color} />
|
||||
{#if showName}
|
||||
<span
|
||||
class="app-logo-with-name__text"
|
||||
style="font-size: {nameFontSize}; color: {color ?? 'inherit'};"
|
||||
>
|
||||
{branding.name}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.app-logo-with-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.app-logo-with-name__text {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
32
packages/shared-branding/src/ManaIcon.svelte
Normal file
32
packages/shared-branding/src/ManaIcon.svelte
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Mana drop icon - used across all apps for credits/mana display
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
/** Size in pixels */
|
||||
size?: number;
|
||||
/** Color */
|
||||
color?: string;
|
||||
/** Additional CSS classes */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
size = 24,
|
||||
color = '#4287f5',
|
||||
class: className = ''
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill={color}
|
||||
class={className}
|
||||
aria-label="Mana"
|
||||
>
|
||||
<path d="M12.3047 1C12.3392 1.04573 19.608 10.6706 19.6084 14.6953C19.6084 18.7293 16.3386 21.9998 12.3047 22C8.27061 22 5 18.7294 5 14.6953C5.00041 10.661 12.3047 1 12.3047 1ZM12.3047 7.3916C12.2811 7.42276 8.65234 12.2288 8.65234 14.2393C8.65241 16.2562 10.2877 17.8916 12.3047 17.8916C14.3217 17.8916 15.957 16.2562 15.957 14.2393C15.957 12.2301 12.3331 7.42917 12.3047 7.3916Z" />
|
||||
</svg>
|
||||
68
packages/shared-branding/src/config.ts
Normal file
68
packages/shared-branding/src/config.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import type { AppBranding, AppId } from './types';
|
||||
|
||||
/**
|
||||
* Branding configuration for all Mana ecosystem apps
|
||||
*/
|
||||
export const APP_BRANDING: Record<AppId, AppBranding> = {
|
||||
memoro: {
|
||||
id: 'memoro',
|
||||
name: 'Memoro',
|
||||
tagline: 'AI Voice Memos',
|
||||
primaryColor: '#f8d62b',
|
||||
secondaryColor: '#f7d44c',
|
||||
// Memoro smile/face logo
|
||||
logoPath: 'M280 140C280 217.32 217.32 280 140 280C62.6801 280 0 217.32 0 140C0 62.6801 62.6801 0 140 0C217.32 0 280 62.6801 280 140ZM247.988 140C247.988 199.64 199.64 241.988 140 241.988C80.3598 241.988 32.0118 199.64 32.0118 140C32.0118 111.918 36.7308 95.3397 54.3005 76.1331C58.5193 71.5212 70.5 63 79.3937 74.511L119.781 131.788C134.5 149 149 147 160.218 131.788L200.605 74.5101C208 64 221.48 71.5203 225.699 76.1321C243.269 95.3388 247.988 111.918 247.988 140Z',
|
||||
logoViewBox: '0 0 280 280',
|
||||
logoStroke: false,
|
||||
},
|
||||
manacore: {
|
||||
id: 'manacore',
|
||||
name: 'ManaCore',
|
||||
tagline: 'Central Hub',
|
||||
primaryColor: '#6366f1',
|
||||
secondaryColor: '#818cf8',
|
||||
// Hexagon/Core icon
|
||||
logoPath: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5',
|
||||
logoViewBox: '0 0 24 24',
|
||||
logoStroke: true,
|
||||
logoStrokeWidth: 2,
|
||||
},
|
||||
manadeck: {
|
||||
id: 'manadeck',
|
||||
name: 'ManaDeck',
|
||||
tagline: 'AI Flashcards',
|
||||
primaryColor: '#8b5cf6',
|
||||
secondaryColor: '#a78bfa',
|
||||
// Cards/Deck icon
|
||||
logoPath: 'M2 4h20v16H2zM6 2v2M18 2v2M6 20v2M18 20v2M2 10h20',
|
||||
logoViewBox: '0 0 24 24',
|
||||
logoStroke: true,
|
||||
logoStrokeWidth: 1.5,
|
||||
},
|
||||
maerchenzauber: {
|
||||
id: 'maerchenzauber',
|
||||
name: 'Märchenzauber',
|
||||
tagline: 'AI Story Creator',
|
||||
primaryColor: '#ec4899',
|
||||
secondaryColor: '#f472b6',
|
||||
// Book/Story icon
|
||||
logoPath: 'M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25',
|
||||
logoViewBox: '0 0 24 24',
|
||||
logoStroke: true,
|
||||
logoStrokeWidth: 1.5,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get branding config for an app
|
||||
*/
|
||||
export function getAppBranding(appId: AppId): AppBranding {
|
||||
return APP_BRANDING[appId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all app brandings
|
||||
*/
|
||||
export function getAllAppBrandings(): AppBranding[] {
|
||||
return Object.values(APP_BRANDING);
|
||||
}
|
||||
24
packages/shared-branding/src/index.ts
Normal file
24
packages/shared-branding/src/index.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Shared branding components and configuration for the Mana ecosystem
|
||||
*
|
||||
* This package provides:
|
||||
* - App logos (AppLogo, AppLogoWithName)
|
||||
* - Mana icon (ManaIcon)
|
||||
* - Branding configuration (colors, names, taglines)
|
||||
*/
|
||||
|
||||
// Components
|
||||
export { default as AppLogo } from './AppLogo.svelte';
|
||||
export { default as AppLogoWithName } from './AppLogoWithName.svelte';
|
||||
export { default as ManaIcon } from './ManaIcon.svelte';
|
||||
|
||||
// Configuration
|
||||
export { APP_BRANDING, getAppBranding, getAllAppBrandings } from './config';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
AppId,
|
||||
AppBranding,
|
||||
LogoProps,
|
||||
AppLogoWithNameProps,
|
||||
} from './types';
|
||||
52
packages/shared-branding/src/types.ts
Normal file
52
packages/shared-branding/src/types.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* App identifiers for branding
|
||||
*/
|
||||
export type AppId = 'memoro' | 'manacore' | 'manadeck' | 'maerchenzauber';
|
||||
|
||||
/**
|
||||
* App branding configuration
|
||||
*/
|
||||
export interface AppBranding {
|
||||
/** Unique app identifier */
|
||||
id: AppId;
|
||||
/** Display name */
|
||||
name: string;
|
||||
/** Short description/tagline */
|
||||
tagline: string;
|
||||
/** Primary brand color (hex) */
|
||||
primaryColor: string;
|
||||
/** Secondary brand color (hex) */
|
||||
secondaryColor?: string;
|
||||
/** SVG path data for the logo icon */
|
||||
logoPath: string;
|
||||
/** Logo viewBox (default: "0 0 24 24") */
|
||||
logoViewBox?: string;
|
||||
/** Whether the logo uses stroke instead of fill */
|
||||
logoStroke?: boolean;
|
||||
/** Logo stroke width (if logoStroke is true) */
|
||||
logoStrokeWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logo component props
|
||||
*/
|
||||
export interface LogoProps {
|
||||
/** Size in pixels */
|
||||
size?: number;
|
||||
/** Override color (uses app primary color if not provided) */
|
||||
color?: string;
|
||||
/** Additional CSS classes */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* App logo with name props
|
||||
*/
|
||||
export interface AppLogoWithNameProps extends LogoProps {
|
||||
/** Show app name next to logo */
|
||||
showName?: boolean;
|
||||
/** Font size for name (CSS value) */
|
||||
nameFontSize?: string;
|
||||
/** Gap between logo and name */
|
||||
gap?: string;
|
||||
}
|
||||
18
packages/shared-branding/tsconfig.json
Normal file
18
packages/shared-branding/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
* Date utility functions
|
||||
*/
|
||||
|
||||
import { format, formatDistanceToNow, parseISO } from 'date-fns';
|
||||
import { format, formatDistanceToNow, parseISO, isToday, isYesterday } from 'date-fns';
|
||||
import { de, enUS } from 'date-fns/locale';
|
||||
|
||||
const locales = {
|
||||
|
|
@ -41,3 +41,40 @@ export function formatRelativeTime(date: string | Date, locale: LocaleKey = 'de'
|
|||
export function toISOString(date: Date): string {
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp with relative day labels (Today, Yesterday, or full date)
|
||||
*
|
||||
* Examples:
|
||||
* - Today → "Today, 14:30" / "Heute, 14:30"
|
||||
* - Yesterday → "Yesterday, 14:30" / "Gestern, 14:30"
|
||||
* - Other → "15. März 2024, 14:30" / "March 15, 2024, 2:30 PM"
|
||||
*/
|
||||
export function formatTimestamp(
|
||||
date: string | Date,
|
||||
locale: LocaleKey = 'de'
|
||||
): string {
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date;
|
||||
const timeFormat = locale === 'de' ? 'HH:mm' : 'h:mm a';
|
||||
|
||||
const labels = {
|
||||
de: { today: 'Heute', yesterday: 'Gestern' },
|
||||
en: { today: 'Today', yesterday: 'Yesterday' },
|
||||
};
|
||||
|
||||
if (isToday(dateObj)) {
|
||||
return `${labels[locale].today}, ${format(dateObj, timeFormat)}`;
|
||||
}
|
||||
|
||||
if (isYesterday(dateObj)) {
|
||||
return `${labels[locale].yesterday}, ${format(dateObj, timeFormat)}`;
|
||||
}
|
||||
|
||||
const dateFormat = locale === 'de' ? 'd. MMMM yyyy' : 'MMMM d, yyyy';
|
||||
return `${format(dateObj, dateFormat, { locale: locales[locale] })}, ${format(dateObj, timeFormat)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a date is today
|
||||
*/
|
||||
export { isToday, isYesterday } from 'date-fns';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue