Add responsive branded header and sound controls
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-09 06:54:47 +01:00
parent a65037e3b3
commit 9d76eabb39
16 changed files with 616 additions and 136 deletions
+246
View File
@@ -0,0 +1,246 @@
---
name: mohfarawati-frontend-system
description: Enforce the project's frontend architecture and UI system when implementing or modifying frontend code.
---
# Frontend System
This skill ensures that all frontend work follows the established UI architecture and design system used in this project.
The system is defined by the real implementation in the codebase, not by external design assumptions.
## Source of truth
The current system is defined by:
- `docs/frontend-system-current-state.md`
- `components/ui/*`
- `components/layout/*`
- `lib/utils.ts`
- `app/globals.css`
- `tailwind.config.ts`
These files define the project's UI primitives, tokens, layout rules, and styling system.
## Primitive architecture
All shared UI must originate from the local primitive layer.
Primary primitives live in:
`components/ui`
Examples include:
- Button
- Card
- AppCard
- Input
- Textarea
- Select
- Dialog
- Sheet
- Tabs
- DropdownMenu
- Accordion
- Badge
- Checkbox
- Table
- Label
- Separator
Feature code must reuse these primitives rather than reimplementing their behavior.
If new shared UI behavior is required, extend the primitive layer instead of bypassing it.
## Class composition
Use the project's shared helper:
`cn()`
from:
`lib/utils.ts`
for class composition.
Variant logic should use:
`class-variance-authority (cva)`
when the primitive already exposes variants.
## Allowed libraries
Frontend code may use only the libraries already present in the system.
UI primitives:
- `@radix-ui/react-*` through local primitives
Styling:
- `tailwindcss`
Class composition:
- `class-variance-authority`
- `clsx`
- `tailwind-merge`
Motion:
- `framer-motion`
- `tailwindcss-animate`
Icons:
- `lucide-react`
Do not introduce additional UI frameworks or icon libraries.
## Token system
Styling must rely on the semantic token layer defined in:
- `app/globals.css`
- `tailwind.config.ts`
Prefer semantic tokens over raw color values.
Shared UI must not introduce new arbitrary color values when tokens already exist.
## Radius system
The project uses a shared radius token system.
Approved radius token categories include:
- surface radius
- nested element radius
- pill radius
These tokens are mapped in Tailwind and must be used for shared UI.
Shared components should not introduce arbitrary radius values.
## Surface and card system
Shared surfaces should follow the established card system.
Primary shared surfaces originate from:
- `Card`
- `AppCard`
These primitives define the canonical surface styles used throughout the application.
Feature code should not duplicate card styling directly.
Instead, reuse these primitives or extend them where necessary.
## Layout system
Shared layout constraints should use the project's container system.
The container primitive defines width limits and horizontal padding behavior.
Container variants include categories such as:
- default layout width
- narrow content width
- wide layout width
- admin layout width
Layout code should reuse the container primitive rather than defining ad-hoc layout wrappers.
## Spacing rhythm
The project uses a consistent spacing rhythm.
Common spacing categories include:
- surface padding
- component internal spacing
- layout gaps
- vertical content spacing
Spacing values should remain consistent with the existing system used in primitives and layout components.
Avoid introducing arbitrary spacing values for shared components.
## Motion system
Motion is implemented through two layers.
Custom motion:
- `framer-motion`
Primitive state transitions:
- `tailwindcss-animate`
Before introducing new motion patterns, inspect existing motion components and reuse their behavior when possible.
## Icon system
The project uses a single icon library:
- `lucide-react`
Icons should be imported from this library only.
Do not introduce additional icon libraries.
## Arbitrary Tailwind values
Arbitrary Tailwind values are not globally forbidden.
They may be used when required for:
- layout-specific grid definitions
- rare content sizing constraints
- typography fitting adjustments
- one-off layout edge cases not covered by tokens
However, arbitrary values must not replace the shared token system.
They should not be used for shared-system styling when project tokens already exist, especially for categories such as:
- radius
- colors
- shadows
- borders
- shared surface styling
When arbitrary values are used, they should remain narrow in scope and justified by layout or typography needs.
## Forbidden patterns
The following patterns must be avoided:
- introducing a new UI framework
- bypassing the local primitive layer when an equivalent primitive already exists
- creating feature-level implementations of components that already exist in `components/ui`
- importing Radix primitives directly in feature code when a local wrapper exists
- introducing raw styling for shared UI surfaces when tokenized styles already exist
- creating duplicate design systems
- adding styling abstractions that conflict with the existing token system
## Transitional areas
Some files still contain transitional visual code.
These areas must not be treated as canonical UI system references for new work.
Examples include decorative hero components, experimental visual effects, or legacy route-level styling.
When implementing new UI, prefer primitives and tokens defined in the main system rather than copying patterns from transitional files.
## Implementation guidance for Codex
When implementing frontend code:
1. Inspect existing primitives before writing new UI.
2. Reuse primitives from `components/ui`.
3. Prefer semantic tokens over raw styling.
4. Follow the established radius, surface, spacing, and container systems.
5. Reuse motion patterns before introducing new ones.
6. Keep new UI aligned with the current frontend architecture.
If a requested UI does not fit the current primitives, extend the primitive layer rather than bypassing it.
@@ -0,0 +1,3 @@
display_name: MohFarawati Frontend System
short_description: Enforce the project's frontend architecture and UI primitives
default_prompt: Use $frontend-system when creating, editing, or refactoring frontend UI in this project.
+4 -1
View File
@@ -3,6 +3,7 @@ import { unstable_noStore as noStore } from "next/cache";
import localFont from "next/font/local";
import Script from "next/script";
import { getLocale } from "next-intl/server";
import { SoundProvider } from "@/components/sound-provider";
import { ThemeProvider } from "@/components/theme-provider";
import { buildAppMetadata } from "@/lib/metadata";
import { getDirection } from "@/lib/locale";
@@ -93,7 +94,9 @@ export default async function RootLayout({
return (
<html lang={locale} dir={getDirection(locale)} suppressHydrationWarning>
<body className={`${museo.variable} ${dubai.variable}`}>
<ThemeProvider>{children}</ThemeProvider>
<ThemeProvider>
<SoundProvider>{children}</SoundProvider>
</ThemeProvider>
<Script
src="https://plausible.mohfarawati.de/js/pa-H7WDubj39eeDW3cfzeXhU.js"
strategy="afterInteractive"
@@ -3,6 +3,7 @@
import { useTranslations } from "next-intl";
import { LocaleToggle } from "@/components/layout/locale-toggle";
import { SoundToggle } from "@/components/sound-toggle";
import { ThemeToggle } from "@/components/theme-toggle";
import { cn } from "@/lib/utils";
@@ -21,6 +22,12 @@ export function FloatingPreferences({
<div className={cn("fixed inset-x-0 top-0 z-50 px-4 pt-4 sm:px-6", className)}>
<div className="mx-auto flex w-full justify-center">
<div className="inline-flex items-center justify-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1 shadow-panel backdrop-blur-chrome">
<SoundToggle
ariaLabel={t("soundMute")}
mutedAriaLabel={t("soundUnmute")}
variant="ghost"
className="h-9 w-9 rounded-pill border border-transparent bg-transparent text-foreground/80 hover:bg-accent hover:text-foreground"
/>
<ThemeToggle
ariaLabel={t("themeToggle")}
variant="ghost"
+95 -10
View File
@@ -10,6 +10,7 @@ import { useState } from "react";
import { Container } from "@/components/layout/container";
import { LocaleToggle } from "@/components/layout/locale-toggle";
import { SiteLogo } from "@/components/layout/site-logo";
import { SoundToggle } from "@/components/sound-toggle";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import { getLocalizedPath, stripLocalePrefix } from "@/lib/locale";
@@ -23,6 +24,41 @@ const navItems = [
{ key: "contact", path: "/contact" },
];
const brandTitleCharacters = [
...Array.from("MOH").map((character) => ({ character, tone: "base" as const })),
{ character: " ", tone: "space" as const },
...Array.from("FARAWATI").map((character) => ({ character, tone: "accent" as const })),
];
const brandShellVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.035,
delayChildren: 0.18,
},
},
} as const;
const brandCharacterVariants = {
hidden: {
opacity: 0,
y: 18,
rotateX: -80,
filter: "blur(8px)",
},
visible: {
opacity: 1,
y: 0,
rotateX: 0,
filter: "blur(0px)",
transition: {
type: "spring" as const,
stiffness: 280,
damping: 20,
mass: 0.8,
},
},
} as const;
type SiteHeaderProps = {
isAdmin?: boolean;
lightLogoUrl?: string | null;
@@ -109,15 +145,52 @@ export function SiteHeader({
<div className="col-start-1 flex items-center justify-self-start">
<Link
href={getLocalizedPath(locale)}
className="inline-flex items-center rounded-pill px-1 py-1 sm:py-1.5 lg:py-2"
className="inline-flex items-center gap-2.5 rounded-pill px-1 py-1 sm:gap-3 lg:py-2"
>
<SiteLogo
lightLogoUrl={lightLogoUrl}
darkLogoUrl={darkLogoUrl}
alt="mohfarawati"
priority
className="h-7 opacity-88 sm:h-8 lg:h-9"
/>
<motion.span
initial={{ opacity: 0, x: -18, scale: 0.82, rotate: -14, filter: "blur(10px)" }}
animate={{ opacity: 1, x: 0, scale: 1, rotate: 0, filter: "blur(0px)" }}
transition={{ type: "spring", stiffness: 240, damping: 20, mass: 0.9 }}
className="relative inline-flex items-center justify-center [transform-style:preserve-3d]"
>
<SiteLogo
lightLogoUrl={lightLogoUrl}
darkLogoUrl={darkLogoUrl}
alt="mohfarawati"
priority
className="relative h-10 opacity-95"
/>
</motion.span>
<motion.span
initial="hidden"
animate="visible"
variants={brandShellVariants}
className="flex flex-col justify-center"
>
<span className="flex flex-wrap items-center text-[1.28rem] font-black leading-[1.2]">
{brandTitleCharacters.map(({ character, tone }, index) => (
<motion.span
key={`${character}-${index}`}
variants={brandCharacterVariants}
className={cn(
character === " " ? "mr-[0.11em] w-[0.11em]" : "mr-[0.005em] inline-block",
tone === "base" ? "text-[hsl(var(--hero-ink))] dark:text-foreground" : "",
tone === "accent" ? "text-primary" : "",
)}
>
{character === " " ? "\u00A0" : character}
</motion.span>
))}
</span>
<motion.span
initial={{ opacity: 0, y: 20, clipPath: "inset(0 0 100% 0)", filter: "blur(10px)" }}
animate={{ opacity: 1, y: 0, clipPath: "inset(0 0 0% 0)", filter: "blur(0px)" }}
transition={{ duration: 0.7, delay: 0.34, ease: [0.22, 1, 0.36, 1] }}
className="-mt-0.5 text-[0.65rem] font-light leading-[1.2] tracking-[0.008em] text-foreground/78"
>
Webdesigner Bremen | Freelancer
</motion.span>
</motion.span>
</Link>
</div>
@@ -146,6 +219,12 @@ export function SiteHeader({
locale={locale}
className={desktopControlButtonClassName}
/>
<SoundToggle
ariaLabel={t("soundMute")}
mutedAriaLabel={t("soundUnmute")}
variant="ghost"
className={desktopControlButtonClassName}
/>
<ThemeToggle
ariaLabel={t("themeToggle")}
variant="ghost"
@@ -154,7 +233,7 @@ export function SiteHeader({
{isAdmin ? (
<Button asChild variant="ghost" size="icon" className={desktopControlButtonClassName}>
<Link href="/root" aria-label={t("root")}>
<LayoutDashboard className="h-4 w-4" />
<LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" />
</Link>
</Button>
) : null}
@@ -263,6 +342,12 @@ export function SiteHeader({
className={mobileControlButtonClassName}
/>
<div className="flex items-center gap-2">
<SoundToggle
ariaLabel={t("soundMute")}
mutedAriaLabel={t("soundUnmute")}
variant="ghost"
className={mobileControlButtonClassName}
/>
<ThemeToggle
ariaLabel={t("themeToggle")}
variant="ghost"
@@ -271,7 +356,7 @@ export function SiteHeader({
{isAdmin ? (
<Button asChild variant="ghost" size="icon" className={mobileControlButtonClassName}>
<Link href="/root" aria-label={t("root")} onClick={() => setIsOpen(false)}>
<LayoutDashboard className="h-4 w-4" />
<LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" />
</Link>
</Button>
) : null}
+6 -3
View File
@@ -36,9 +36,12 @@ export function SiteLogo({
return (
<motion.span
className="block"
whileHover={{ scale: 1.04, y: -1 }}
transition={{ duration: 0.28, ease: "easeOut" }}
className="block [transform-style:preserve-3d]"
initial={{ opacity: 0, x: -42, scale: 0.88, rotate: -360 }}
animate={{ opacity: 1, x: 0, scale: 1, rotate: 0 }}
whileHover={{ scale: 1.05, y: -2, rotate: -2, filter: "drop-shadow(0 16px 28px rgba(15,23,42,0.14))" }}
whileTap={{ scale: 0.9, rotate: 6, y: 1, filter: "drop-shadow(0 8px 18px rgba(15,23,42,0.18))" }}
transition={{ x: { duration: 0.78, ease: [0.22, 1, 0.36, 1] }, opacity: { duration: 0.46 }, rotate: { duration: 0.56, ease: "easeOut" }, scale: { duration: 0.72, ease: [0.22, 1, 0.36, 1] } }}
>
<Image
src={src}
+2
View File
@@ -17,6 +17,7 @@ import { DashboardLayout } from "@/components/dashboard/dashboard-layout";
import { MotionFade } from "@/components/motion-fade";
import { FormSaveButton } from "@/components/root/form-save-button";
import { SidebarMaintenanceControl } from "@/components/root/sidebar-maintenance-control";
import { SoundToggle } from "@/components/sound-toggle";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config";
@@ -119,6 +120,7 @@ export async function RootDashboardShell({
label={saveButtonLabel ?? "Speichern"}
reloadDocumentOnSuccess={reloadDocumentOnSave}
/>
<SoundToggle ariaLabel="Mute sounds" mutedAriaLabel="Unmute sounds" />
<ThemeToggle ariaLabel="Theme wechseln" />
</>
);
+67
View File
@@ -0,0 +1,67 @@
"use client";
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
type SoundContextValue = {
isMuted: boolean;
toggleMuted: () => void;
playSound: (src: string) => void;
};
const SOUND_MUTED_STORAGE_KEY = "mohfarawati-sound-muted";
const SoundContext = createContext<SoundContextValue | null>(null);
export function SoundProvider({ children }: { children: ReactNode }) {
const [isMuted, setIsMuted] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
const storedValue = window.localStorage.getItem(SOUND_MUTED_STORAGE_KEY);
if (storedValue === "true") {
setIsMuted(true);
}
}, []);
useEffect(() => {
if (!mounted) {
return;
}
window.localStorage.setItem(SOUND_MUTED_STORAGE_KEY, String(isMuted));
}, [isMuted, mounted]);
const value = useMemo<SoundContextValue>(
() => ({
isMuted,
toggleMuted: () => {
setIsMuted((currentValue) => !currentValue);
},
playSound: (src: string) => {
if (!mounted || isMuted) {
return;
}
const audio = new Audio(src);
audio.preload = "auto";
void audio.play().catch(() => {});
},
}),
[isMuted, mounted],
);
return <SoundContext.Provider value={value}>{children}</SoundContext.Provider>;
}
export function useSound() {
const context = useContext(SoundContext);
if (!context) {
throw new Error("useSound must be used within SoundProvider");
}
return context;
}
+49
View File
@@ -0,0 +1,49 @@
"use client";
import { Volume2, VolumeX } from "lucide-react";
import { useSound } from "@/components/sound-provider";
import { Button, type ButtonProps } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type SoundToggleProps = {
ariaLabel?: string;
mutedAriaLabel?: string;
variant?: ButtonProps["variant"];
className?: string;
iconClassName?: string;
};
export function SoundToggle({
ariaLabel = "Mute sounds",
mutedAriaLabel = "Unmute sounds",
variant = "outline",
className,
iconClassName,
}: SoundToggleProps) {
const { isMuted, toggleMuted } = useSound();
return (
<Button
type="button"
variant={variant}
size="icon"
onClick={toggleMuted}
aria-label={isMuted ? mutedAriaLabel : ariaLabel}
aria-pressed={isMuted}
className={cn("group cursor-pointer", className)}
>
{isMuted ? (
<VolumeX
strokeWidth={1.6}
className={cn("h-[1.125rem] w-[1.125rem] transition-transform duration-300 ease-out group-hover:scale-110", iconClassName)}
/>
) : (
<Volume2
strokeWidth={1.6}
className={cn("h-[1.125rem] w-[1.125rem] transition-transform duration-300 ease-out group-hover:scale-110", iconClassName)}
/>
)}
</Button>
);
}
+13 -4
View File
@@ -4,6 +4,7 @@ import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { useSound } from "@/components/sound-provider";
import { Button, type ButtonProps } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -23,6 +24,7 @@ export function ThemeToggle({
iconClassName,
}: ThemeToggleProps) {
const { setTheme, theme, resolvedTheme } = useTheme();
const { playSound } = useSound();
const [mounted, setMounted] = useState(false);
useEffect(() => {
@@ -38,7 +40,7 @@ export function ThemeToggle({
aria-label={ariaLabel}
className={cn("group cursor-pointer", label ? "justify-start" : undefined, className)}
>
<Moon className={cn("h-4 w-4 transition-transform duration-700 ease-out group-hover:scale-110 group-hover:rotate-[360deg]", iconClassName)} />
<Moon className={cn("h-4 w-4 transition-transform duration-300 ease-out group-hover:scale-110", iconClassName)} />
{label ? <span>{label}</span> : null}
</Button>
);
@@ -47,19 +49,26 @@ export function ThemeToggle({
const activeTheme = theme === "system" ? resolvedTheme : theme;
const isDark = activeTheme === "dark";
const handleThemeToggle = () => {
const nextTheme = isDark ? "light" : "dark";
setTheme(nextTheme);
playSound(nextTheme === "dark" ? "/audio/dark.mp3" : "/audio/light.mp3");
};
return (
<Button
type="button"
variant={variant}
size={label ? "sm" : "icon"}
onClick={() => setTheme(isDark ? "light" : "dark")}
onClick={handleThemeToggle}
aria-label={ariaLabel}
className={cn("group cursor-pointer", label ? "justify-start" : undefined, className)}
>
{isDark ? (
<Sun className={cn("h-4 w-4 transition-transform duration-700 ease-out group-hover:scale-110 group-hover:rotate-[360deg]", iconClassName)} />
<Sun className={cn("h-4 w-4 transition-transform duration-300 ease-out group-hover:scale-110", iconClassName)} />
) : (
<Moon className={cn("h-4 w-4 transition-transform duration-700 ease-out group-hover:scale-110 group-hover:rotate-[360deg]", iconClassName)} />
<Moon className={cn("h-4 w-4 transition-transform duration-300 ease-out group-hover:scale-110", iconClassName)} />
)}
{label ? <span>{label}</span> : null}
</Button>
+115 -115
View File
@@ -2,7 +2,13 @@
## 1. UI primitive source of truth
The current primitive source of truth is:
The current primitive source of truth is the local
`components/ui`
layer.
Current core primitive files:
- `components/ui/button.tsx`
- `components/ui/card.tsx`
@@ -21,11 +27,11 @@ The current primitive source of truth is:
- `components/ui/label.tsx`
- `components/ui/separator.tsx`
Feature code currently reuses these primitives directly.
Feature code currently imports these local primitives rather than importing Radix primitives directly.
## 2. Libraries currently used for UI, motion, icons, and class composition
UI primitives and wrappers:
UI primitives and wrappers currently used:
- `@radix-ui/react-accordion`
- `@radix-ui/react-checkbox`
@@ -34,16 +40,16 @@ UI primitives and wrappers:
- `@radix-ui/react-select`
- `@radix-ui/react-slot`
Motion:
Motion currently used:
- `framer-motion`
- `tailwindcss-animate`
Icons:
Icons currently used:
- `lucide-react`
Class composition:
Class composition currently used:
- `class-variance-authority`
- `clsx`
@@ -52,21 +58,19 @@ Class composition:
Shared helper:
- `lib/utils.ts`
- `cn(...inputs)`
Source files:
Current helper function:
- `package.json`
- `lib/utils.ts`
`cn()`
## 3. Current token system in use
The active semantic token layer is defined in:
The active token system is defined in:
- `app/globals.css`
- `tailwind.config.ts`
Current color token groups exposed in Tailwind:
Current semantic color tokens exposed in Tailwind:
- `background`
- `foreground`
@@ -100,7 +104,7 @@ Current color token groups exposed in Tailwind:
- `status.warning-soft`
- `sidebar.*`
Current tokenized utility usage in shared UI is based on:
Current shared utility usage is centered on:
- `bg-card`
- `bg-background`
@@ -111,46 +115,56 @@ Current tokenized utility usage in shared UI is based on:
- `border-border/80`
- `border-border/70`
- `border-input`
- `hover:bg-accent`
There are also decorative hero tokens still present in current code:
- `--hero-base`
- `--hero-ink`
- `--hero-left`
- `--hero-right`
- `--hero-bottom`
## 4. Current radius system in use
Defined in:
Current radius tokens in:
- `app/globals.css`
- `tailwind.config.ts`
Current radius tokens:
are:
- `--radius-surface: 24px`
- `--radius-nested: 16px`
- `--radius-pill: 9999px`
Current mapped utilities:
Current Tailwind mappings in:
- `tailwind.config.ts`
are:
- `rounded-surface`
- `rounded-nested`
- `rounded-pill`
Current shared primitive usage:
Current usage pattern:
- surface containers:
- `rounded-surface`
- `rounded-surface`
- outer surfaces and primitive containers
- `components/ui/card.tsx`
- `components/ui/app-card.tsx`
- `components/ui/dialog.tsx`
- `components/ui/select.tsx`
- `components/ui/tabs.tsx`
- controls and nested blocks:
- `rounded-nested`
- `rounded-nested`
- controls and inner blocks
- `components/ui/button.tsx`
- `components/ui/input.tsx`
- `components/ui/textarea.tsx`
- `components/ui/select.tsx`
- `components/ui/dialog.tsx`
- `components/ui/sheet.tsx`
- pill controls:
- `rounded-pill`
- `rounded-pill`
- top-level chrome controls
- `components/layout/site-header.tsx`
- `components/layout/floating-preferences.tsx`
@@ -162,11 +176,9 @@ Source:
- `components/ui/card.tsx`
Current base rule:
Current base surface rule:
```tsx
rounded-surface border border-border/80 bg-card text-card-foreground shadow-card
```
`rounded-surface border border-border/80 bg-card text-card-foreground shadow-card`
### `AppCard`
@@ -199,9 +211,9 @@ Current interactive rule:
- `hover:bg-accent/30 hover:text-accent-foreground`
### Current surface usage in feature code
Current feature code now reuses these levels in shared admin surfaces.
Nested surface blocks currently reuse `AppCard` directly in:
Examples:
- `components/root/media-field-picker.tsx`
- `components/root/portfolio-projects-overview.tsx`
@@ -217,11 +229,9 @@ Source:
- `components/ui/button.tsx`
Base:
Current base rule:
```tsx
inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-nested text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50
```
`inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-nested text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50`
Current variants:
@@ -238,17 +248,6 @@ Current variants:
- `destructive`
- `bg-destructive text-destructive-foreground hover:bg-destructive/90`
Current sizes:
- `default`
- `h-10 px-4 py-2`
- `sm`
- `h-9 px-3`
- `lg`
- `h-11 px-6`
- `icon`
- `h-10 w-10`
### Input
Source:
@@ -257,9 +256,7 @@ Source:
Current rule:
```tsx
flex h-10 w-full rounded-nested border border-input bg-background px-3 py-2 text-base text-foreground shadow-xs ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:ring-offset-0
```
`flex h-10 w-full rounded-nested border border-input bg-background px-3 py-2 text-base text-foreground shadow-xs ... focus-visible:border-ring ... focus-visible:ring-ring/30`
### Select
@@ -269,21 +266,15 @@ Source:
Current trigger rule:
```tsx
flex h-10 w-full items-center justify-between rounded-nested border border-input bg-background px-3 py-2 text-sm shadow-xs
```
`flex h-10 w-full items-center justify-between rounded-nested border border-input bg-background px-3 py-2 text-sm shadow-xs`
Current content rule:
```tsx
rounded-surface border border-input bg-background text-foreground shadow-panel
```
`rounded-surface border border-input bg-background text-foreground shadow-panel`
Current item rule:
```tsx
rounded-nested ... focus:bg-accent focus:text-accent-foreground
```
`rounded-nested ... focus:bg-accent focus:text-accent-foreground`
### Dialog
@@ -293,21 +284,15 @@ Source:
Current overlay rule:
```tsx
fixed inset-0 z-50 bg-foreground/30 backdrop-blur-sm
```
`fixed inset-0 z-50 bg-foreground/30 backdrop-blur-sm`
Current content rule:
```tsx
rounded-surface border border-border/80 bg-card p-6 text-card-foreground shadow-panel
```
`rounded-surface border border-border/80 bg-card p-6 text-card-foreground shadow-panel`
Current close button rule:
```tsx
inline-flex h-8 w-8 items-center justify-center rounded-nested text-muted-foreground hover:bg-accent hover:text-foreground
```
`inline-flex h-8 w-8 items-center justify-center rounded-nested text-muted-foreground hover:bg-accent hover:text-foreground`
### Sheet
@@ -317,15 +302,11 @@ Source:
Current overlay rule matches `Dialog`:
```tsx
fixed inset-0 z-50 bg-foreground/30 backdrop-blur-sm
```
`fixed inset-0 z-50 bg-foreground/30 backdrop-blur-sm`
Current content rule:
```tsx
border-border/80 bg-card p-6 text-card-foreground shadow-panel
```
`border-border/80 bg-card p-6 text-card-foreground shadow-panel`
Current side radius rules:
@@ -342,21 +323,15 @@ Source:
Current list rule:
```tsx
inline-flex ... gap-2 rounded-surface border border-border/80 bg-muted/40 p-1.5
```
`inline-flex ... gap-2 rounded-surface border border-border/80 bg-muted/40 p-1.5`
Current trigger rule:
```tsx
rounded-nested px-3 py-2 text-sm font-medium text-muted-foreground
```
`rounded-nested px-3 py-2 text-sm font-medium text-muted-foreground`
Current active rule:
Current active trigger rule:
```tsx
border border-border/80 bg-background text-foreground shadow-xs
```
`border border-border/80 bg-background text-foreground shadow-xs`
## 7. Current layout/container rules in use
@@ -366,11 +341,9 @@ Source:
- `components/layout/container.tsx`
Current base container rule:
Current base rule:
```tsx
mx-auto w-full px-4 sm:px-6 lg:px-8
```
`mx-auto w-full px-4 sm:px-6 lg:px-8`
Current size variants:
@@ -383,7 +356,18 @@ Current size variants:
- `admin`
- `max-w-admin`
### Shared public chrome
Mapped widths from:
- `app/globals.css`
are:
- `--container-narrow: 48rem`
- `--container-default: 72rem`
- `--container-wide: 80rem`
- `--container-admin: 88rem`
### Shared public top chrome
Current shared top controls use:
@@ -404,7 +388,7 @@ Current admin layout still uses:
- `min-h-screen bg-muted/30`
- `grid min-h-screen lg:grid-cols-[280px_minmax(0,1fr)]`
- `sticky top-0`
- sticky sidebar and header
- `p-4 lg:p-6`
Source:
@@ -413,7 +397,7 @@ Source:
## 8. Current spacing rhythm in use
Current repeated spacing values across primitives and layout:
Current repeated spacing values:
- `p-4`
- `p-6`
@@ -446,6 +430,15 @@ Current primitive spacing sources:
- `Container`
- `px-4 sm:px-6 lg:px-8`
Current layout spacing is also built around:
- `gap-section`
- `gap-content`
from:
- `tailwind.config.ts`
## 9. Current motion rules in use
Primary motion library:
@@ -460,16 +453,16 @@ Current motion patterns:
- reveal-on-scroll wrapper
- `components/motion-fade.tsx`
- `initial: { opacity: 0, y: 20 }`
- `whileInView: { opacity: 1, y: 0 }`
- `transition: { duration: 0.45, ease: "easeOut" }`
- shared overlay open/close classes
- `dialog`
- `sheet`
- `select`
- `dropdown-menu`
- `accordion`
- animated nav active pill and mobile menu
- `initial={{ opacity: 0, y: 20 }}`
- `whileInView={{ opacity: 1, y: 0 }}`
- `transition={{ duration: 0.45, delay, ease: "easeOut" }}`
- shared overlay and disclosure state classes
- `components/ui/dialog.tsx`
- `components/ui/sheet.tsx`
- `components/ui/select.tsx`
- `components/ui/dropdown-menu.tsx`
- `components/ui/accordion.tsx`
- animated navigation chrome
- `components/layout/site-header.tsx`
## 10. Current icon rules in use
@@ -478,9 +471,13 @@ Current icon library:
- `lucide-react`
Current shared rule:
Current rule:
Icons are imported directly from `lucide-react` and passed through local primitives or feature components.
icons are imported directly from
`lucide-react`
and used through local primitives or feature components.
Examples:
@@ -490,47 +487,50 @@ Examples:
- `components/theme-toggle.tsx`
- `components/root/portfolio-project-form.tsx`
No second React icon library is currently present in `package.json`.
No second React icon library is present in:
- `package.json`
## 11. Current forbidden drift or remaining transitional areas, if any
The shared primitive system is currently tokenized.
The shared primitive layer is currently tokenized.
Remaining transitional areas still present in the codebase:
Remaining transitional or non-shared drift still present in the codebase:
- public hero / atmospheric visuals still use raw gradients and `rgba(...)` decorative backgrounds
- public atmospheric and hero visuals still use route-specific raw gradients and `rgba(...)`
- `components/layout/home-hero.tsx`
- `components/layout/site-ambient-backdrop.tsx`
- `components/layout/hero-atmosphere.tsx`
- `components/layout/site-logo.tsx`
- `coming-soon` page still uses older raw translucent chrome styles
- `coming-soon` still uses older translucent raw chrome rules
- `app/[locale]/coming-soon/page.tsx`
- some public route-level content blocks still use direct local classes instead of shared primitive wrappers
- some public route-level content blocks still use route-local classes instead of shared primitives
- `app/[locale]/(site)/portfolio/page.tsx`
- `app/[locale]/(site)/portfolio/[slug]/page.tsx`
- one preview block in site settings still uses a custom dark preview surface
- one custom dark preview surface remains in current admin code
- `components/root/site-settings-form.tsx`
- `bg-slate-950`
- `text-white/55`
- `text-white/60`
- some admin and public cards still use `Card` with local class overrides instead of `AppCard`
- `app/root/page.tsx`
- `components/dashboard/dashboard-card.tsx`
These files exist in the current codebase and should be treated as remaining transitional areas, not as shared-system rules.
These are current areas in the repository, but they are not the shared-system rules.
## 12. Codex rules derived from the current system
- Use `components/ui` as the primitive source of truth.
- Use `cn()` from `lib/utils.ts` for class composition.
- Use `cva()` where the primitive already exposes variants.
- Use semantic tokens already defined in `app/globals.css` and mapped in `tailwind.config.ts`.
- Use `Card` for the base surface:
- `rounded-surface border border-border/80 bg-card text-card-foreground shadow-card`
- Use `Card` as the base surface primitive.
- Use `AppCard` when feature code needs explicit shared surface levels.
- Use `AppCard level={1}` for primary card surfaces.
- Use `AppCard level={2}` for nested panels and inner blocks.
- Use `AppCard level={3}` for elevated grouped panels.
- Use `rounded-surface`, `rounded-nested`, and `rounded-pill` as the shared radius system.
- Use `Input`, `Textarea`, `Select`, `Dialog`, `Sheet`, and `Tabs` from `components/ui` instead of re-creating those patterns in feature code.
- Use `Input`, `Textarea`, `Select`, `Dialog`, `Sheet`, and `Tabs` from `components/ui` instead of rebuilding those patterns in feature code.
- Use `Container` for width and horizontal padding rules.
- Use `lucide-react` for icons.
- Use `framer-motion` for custom motion and `tailwindcss-animate` for primitive state transitions.
- Do not treat transitional decorative hero files or remaining route-local raw blocks as shared-system primitives.
- Do not treat route-specific hero visuals or transitional raw blocks as shared-system primitives.
+3 -1
View File
@@ -8,7 +8,9 @@
"root": "Root",
"openMenu": "فتح القائمة",
"closeMenu": "إغلاق القائمة",
"themeToggle": "تبديل المظهر"
"themeToggle": "تبديل المظهر",
"soundMute": "كتم الأصوات",
"soundUnmute": "تشغيل الأصوات"
},
"footer": {
"copyright": "© {year} moh-sass. جميع الحقوق محفوظة."
+3 -1
View File
@@ -8,7 +8,9 @@
"root": "Root",
"openMenu": "Menue oeffnen",
"closeMenu": "Menue schliessen",
"themeToggle": "Theme wechseln"
"themeToggle": "Theme wechseln",
"soundMute": "Sounds stummschalten",
"soundUnmute": "Sounds aktivieren"
},
"footer": {
"copyright": "© {year} moh-sass. Alle Rechte vorbehalten."
+3 -1
View File
@@ -8,7 +8,9 @@
"root": "Root",
"openMenu": "Open menu",
"closeMenu": "Close menu",
"themeToggle": "Toggle theme"
"themeToggle": "Toggle theme",
"soundMute": "Mute sounds",
"soundUnmute": "Unmute sounds"
},
"footer": {
"copyright": "© {year} moh-sass. All rights reserved."
Binary file not shown.
Binary file not shown.