This commit is contained in:
@@ -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
@@ -3,6 +3,7 @@ import { unstable_noStore as noStore } from "next/cache";
|
|||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import { getLocale } from "next-intl/server";
|
import { getLocale } from "next-intl/server";
|
||||||
|
import { SoundProvider } from "@/components/sound-provider";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { buildAppMetadata } from "@/lib/metadata";
|
import { buildAppMetadata } from "@/lib/metadata";
|
||||||
import { getDirection } from "@/lib/locale";
|
import { getDirection } from "@/lib/locale";
|
||||||
@@ -93,7 +94,9 @@ export default async function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang={locale} dir={getDirection(locale)} suppressHydrationWarning>
|
<html lang={locale} dir={getDirection(locale)} suppressHydrationWarning>
|
||||||
<body className={`${museo.variable} ${dubai.variable}`}>
|
<body className={`${museo.variable} ${dubai.variable}`}>
|
||||||
<ThemeProvider>{children}</ThemeProvider>
|
<ThemeProvider>
|
||||||
|
<SoundProvider>{children}</SoundProvider>
|
||||||
|
</ThemeProvider>
|
||||||
<Script
|
<Script
|
||||||
src="https://plausible.mohfarawati.de/js/pa-H7WDubj39eeDW3cfzeXhU.js"
|
src="https://plausible.mohfarawati.de/js/pa-H7WDubj39eeDW3cfzeXhU.js"
|
||||||
strategy="afterInteractive"
|
strategy="afterInteractive"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { LocaleToggle } from "@/components/layout/locale-toggle";
|
import { LocaleToggle } from "@/components/layout/locale-toggle";
|
||||||
|
import { SoundToggle } from "@/components/sound-toggle";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { cn } from "@/lib/utils";
|
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={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="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">
|
<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
|
<ThemeToggle
|
||||||
ariaLabel={t("themeToggle")}
|
ariaLabel={t("themeToggle")}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useState } from "react";
|
|||||||
import { Container } from "@/components/layout/container";
|
import { Container } from "@/components/layout/container";
|
||||||
import { LocaleToggle } from "@/components/layout/locale-toggle";
|
import { LocaleToggle } from "@/components/layout/locale-toggle";
|
||||||
import { SiteLogo } from "@/components/layout/site-logo";
|
import { SiteLogo } from "@/components/layout/site-logo";
|
||||||
|
import { SoundToggle } from "@/components/sound-toggle";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getLocalizedPath, stripLocalePrefix } from "@/lib/locale";
|
import { getLocalizedPath, stripLocalePrefix } from "@/lib/locale";
|
||||||
@@ -23,6 +24,41 @@ const navItems = [
|
|||||||
{ key: "contact", path: "/contact" },
|
{ 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 = {
|
type SiteHeaderProps = {
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
lightLogoUrl?: string | null;
|
lightLogoUrl?: string | null;
|
||||||
@@ -109,15 +145,52 @@ export function SiteHeader({
|
|||||||
<div className="col-start-1 flex items-center justify-self-start">
|
<div className="col-start-1 flex items-center justify-self-start">
|
||||||
<Link
|
<Link
|
||||||
href={getLocalizedPath(locale)}
|
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"
|
||||||
|
>
|
||||||
|
<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
|
<SiteLogo
|
||||||
lightLogoUrl={lightLogoUrl}
|
lightLogoUrl={lightLogoUrl}
|
||||||
darkLogoUrl={darkLogoUrl}
|
darkLogoUrl={darkLogoUrl}
|
||||||
alt="mohfarawati"
|
alt="mohfarawati"
|
||||||
priority
|
priority
|
||||||
className="h-7 opacity-88 sm:h-8 lg:h-9"
|
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -146,6 +219,12 @@ export function SiteHeader({
|
|||||||
locale={locale}
|
locale={locale}
|
||||||
className={desktopControlButtonClassName}
|
className={desktopControlButtonClassName}
|
||||||
/>
|
/>
|
||||||
|
<SoundToggle
|
||||||
|
ariaLabel={t("soundMute")}
|
||||||
|
mutedAriaLabel={t("soundUnmute")}
|
||||||
|
variant="ghost"
|
||||||
|
className={desktopControlButtonClassName}
|
||||||
|
/>
|
||||||
<ThemeToggle
|
<ThemeToggle
|
||||||
ariaLabel={t("themeToggle")}
|
ariaLabel={t("themeToggle")}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -154,7 +233,7 @@ export function SiteHeader({
|
|||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<Button asChild variant="ghost" size="icon" className={desktopControlButtonClassName}>
|
<Button asChild variant="ghost" size="icon" className={desktopControlButtonClassName}>
|
||||||
<Link href="/root" aria-label={t("root")}>
|
<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>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -263,6 +342,12 @@ export function SiteHeader({
|
|||||||
className={mobileControlButtonClassName}
|
className={mobileControlButtonClassName}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<SoundToggle
|
||||||
|
ariaLabel={t("soundMute")}
|
||||||
|
mutedAriaLabel={t("soundUnmute")}
|
||||||
|
variant="ghost"
|
||||||
|
className={mobileControlButtonClassName}
|
||||||
|
/>
|
||||||
<ThemeToggle
|
<ThemeToggle
|
||||||
ariaLabel={t("themeToggle")}
|
ariaLabel={t("themeToggle")}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -271,7 +356,7 @@ export function SiteHeader({
|
|||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<Button asChild variant="ghost" size="icon" className={mobileControlButtonClassName}>
|
<Button asChild variant="ghost" size="icon" className={mobileControlButtonClassName}>
|
||||||
<Link href="/root" aria-label={t("root")} onClick={() => setIsOpen(false)}>
|
<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>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -36,9 +36,12 @@ export function SiteLogo({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.span
|
<motion.span
|
||||||
className="block"
|
className="block [transform-style:preserve-3d]"
|
||||||
whileHover={{ scale: 1.04, y: -1 }}
|
initial={{ opacity: 0, x: -42, scale: 0.88, rotate: -360 }}
|
||||||
transition={{ duration: 0.28, ease: "easeOut" }}
|
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
|
<Image
|
||||||
src={src}
|
src={src}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { DashboardLayout } from "@/components/dashboard/dashboard-layout";
|
|||||||
import { MotionFade } from "@/components/motion-fade";
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { FormSaveButton } from "@/components/root/form-save-button";
|
import { FormSaveButton } from "@/components/root/form-save-button";
|
||||||
import { SidebarMaintenanceControl } from "@/components/root/sidebar-maintenance-control";
|
import { SidebarMaintenanceControl } from "@/components/root/sidebar-maintenance-control";
|
||||||
|
import { SoundToggle } from "@/components/sound-toggle";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config";
|
import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config";
|
||||||
@@ -119,6 +120,7 @@ export async function RootDashboardShell({
|
|||||||
label={saveButtonLabel ?? "Speichern"}
|
label={saveButtonLabel ?? "Speichern"}
|
||||||
reloadDocumentOnSuccess={reloadDocumentOnSave}
|
reloadDocumentOnSuccess={reloadDocumentOnSave}
|
||||||
/>
|
/>
|
||||||
|
<SoundToggle ariaLabel="Mute sounds" mutedAriaLabel="Unmute sounds" />
|
||||||
<ThemeToggle ariaLabel="Theme wechseln" />
|
<ThemeToggle ariaLabel="Theme wechseln" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { Moon, Sun } from "lucide-react";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useSound } from "@/components/sound-provider";
|
||||||
import { Button, type ButtonProps } from "@/components/ui/button";
|
import { Button, type ButtonProps } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export function ThemeToggle({
|
|||||||
iconClassName,
|
iconClassName,
|
||||||
}: ThemeToggleProps) {
|
}: ThemeToggleProps) {
|
||||||
const { setTheme, theme, resolvedTheme } = useTheme();
|
const { setTheme, theme, resolvedTheme } = useTheme();
|
||||||
|
const { playSound } = useSound();
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -38,7 +40,7 @@ export function ThemeToggle({
|
|||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
className={cn("group cursor-pointer", label ? "justify-start" : undefined, className)}
|
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}
|
{label ? <span>{label}</span> : null}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -47,19 +49,26 @@ export function ThemeToggle({
|
|||||||
const activeTheme = theme === "system" ? resolvedTheme : theme;
|
const activeTheme = theme === "system" ? resolvedTheme : theme;
|
||||||
const isDark = activeTheme === "dark";
|
const isDark = activeTheme === "dark";
|
||||||
|
|
||||||
|
const handleThemeToggle = () => {
|
||||||
|
const nextTheme = isDark ? "light" : "dark";
|
||||||
|
|
||||||
|
setTheme(nextTheme);
|
||||||
|
playSound(nextTheme === "dark" ? "/audio/dark.mp3" : "/audio/light.mp3");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={label ? "sm" : "icon"}
|
size={label ? "sm" : "icon"}
|
||||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
onClick={handleThemeToggle}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
className={cn("group cursor-pointer", label ? "justify-start" : undefined, className)}
|
className={cn("group cursor-pointer", label ? "justify-start" : undefined, className)}
|
||||||
>
|
>
|
||||||
{isDark ? (
|
{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}
|
{label ? <span>{label}</span> : null}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
|
|
||||||
## 1. UI primitive source of truth
|
## 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/button.tsx`
|
||||||
- `components/ui/card.tsx`
|
- `components/ui/card.tsx`
|
||||||
@@ -21,11 +27,11 @@ The current primitive source of truth is:
|
|||||||
- `components/ui/label.tsx`
|
- `components/ui/label.tsx`
|
||||||
- `components/ui/separator.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
|
## 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-accordion`
|
||||||
- `@radix-ui/react-checkbox`
|
- `@radix-ui/react-checkbox`
|
||||||
@@ -34,16 +40,16 @@ UI primitives and wrappers:
|
|||||||
- `@radix-ui/react-select`
|
- `@radix-ui/react-select`
|
||||||
- `@radix-ui/react-slot`
|
- `@radix-ui/react-slot`
|
||||||
|
|
||||||
Motion:
|
Motion currently used:
|
||||||
|
|
||||||
- `framer-motion`
|
- `framer-motion`
|
||||||
- `tailwindcss-animate`
|
- `tailwindcss-animate`
|
||||||
|
|
||||||
Icons:
|
Icons currently used:
|
||||||
|
|
||||||
- `lucide-react`
|
- `lucide-react`
|
||||||
|
|
||||||
Class composition:
|
Class composition currently used:
|
||||||
|
|
||||||
- `class-variance-authority`
|
- `class-variance-authority`
|
||||||
- `clsx`
|
- `clsx`
|
||||||
@@ -52,21 +58,19 @@ Class composition:
|
|||||||
Shared helper:
|
Shared helper:
|
||||||
|
|
||||||
- `lib/utils.ts`
|
- `lib/utils.ts`
|
||||||
- `cn(...inputs)`
|
|
||||||
|
|
||||||
Source files:
|
Current helper function:
|
||||||
|
|
||||||
- `package.json`
|
`cn()`
|
||||||
- `lib/utils.ts`
|
|
||||||
|
|
||||||
## 3. Current token system in use
|
## 3. Current token system in use
|
||||||
|
|
||||||
The active semantic token layer is defined in:
|
The active token system is defined in:
|
||||||
|
|
||||||
- `app/globals.css`
|
- `app/globals.css`
|
||||||
- `tailwind.config.ts`
|
- `tailwind.config.ts`
|
||||||
|
|
||||||
Current color token groups exposed in Tailwind:
|
Current semantic color tokens exposed in Tailwind:
|
||||||
|
|
||||||
- `background`
|
- `background`
|
||||||
- `foreground`
|
- `foreground`
|
||||||
@@ -100,7 +104,7 @@ Current color token groups exposed in Tailwind:
|
|||||||
- `status.warning-soft`
|
- `status.warning-soft`
|
||||||
- `sidebar.*`
|
- `sidebar.*`
|
||||||
|
|
||||||
Current tokenized utility usage in shared UI is based on:
|
Current shared utility usage is centered on:
|
||||||
|
|
||||||
- `bg-card`
|
- `bg-card`
|
||||||
- `bg-background`
|
- `bg-background`
|
||||||
@@ -111,46 +115,56 @@ Current tokenized utility usage in shared UI is based on:
|
|||||||
- `border-border/80`
|
- `border-border/80`
|
||||||
- `border-border/70`
|
- `border-border/70`
|
||||||
- `border-input`
|
- `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
|
## 4. Current radius system in use
|
||||||
|
|
||||||
Defined in:
|
Current radius tokens in:
|
||||||
|
|
||||||
- `app/globals.css`
|
- `app/globals.css`
|
||||||
- `tailwind.config.ts`
|
|
||||||
|
|
||||||
Current radius tokens:
|
are:
|
||||||
|
|
||||||
- `--radius-surface: 24px`
|
- `--radius-surface: 24px`
|
||||||
- `--radius-nested: 16px`
|
- `--radius-nested: 16px`
|
||||||
- `--radius-pill: 9999px`
|
- `--radius-pill: 9999px`
|
||||||
|
|
||||||
Current mapped utilities:
|
Current Tailwind mappings in:
|
||||||
|
|
||||||
|
- `tailwind.config.ts`
|
||||||
|
|
||||||
|
are:
|
||||||
|
|
||||||
- `rounded-surface`
|
- `rounded-surface`
|
||||||
- `rounded-nested`
|
- `rounded-nested`
|
||||||
- `rounded-pill`
|
- `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/card.tsx`
|
||||||
- `components/ui/app-card.tsx`
|
- `components/ui/app-card.tsx`
|
||||||
- `components/ui/dialog.tsx`
|
- `components/ui/dialog.tsx`
|
||||||
- `components/ui/select.tsx`
|
- `components/ui/select.tsx`
|
||||||
- `components/ui/tabs.tsx`
|
- `components/ui/tabs.tsx`
|
||||||
- controls and nested blocks:
|
- `rounded-nested`
|
||||||
- `rounded-nested`
|
- controls and inner blocks
|
||||||
- `components/ui/button.tsx`
|
- `components/ui/button.tsx`
|
||||||
- `components/ui/input.tsx`
|
- `components/ui/input.tsx`
|
||||||
- `components/ui/textarea.tsx`
|
- `components/ui/textarea.tsx`
|
||||||
- `components/ui/select.tsx`
|
- `components/ui/select.tsx`
|
||||||
- `components/ui/dialog.tsx`
|
- `components/ui/dialog.tsx`
|
||||||
- `components/ui/sheet.tsx`
|
- `components/ui/sheet.tsx`
|
||||||
- pill controls:
|
- `rounded-pill`
|
||||||
- `rounded-pill`
|
- top-level chrome controls
|
||||||
- `components/layout/site-header.tsx`
|
- `components/layout/site-header.tsx`
|
||||||
- `components/layout/floating-preferences.tsx`
|
- `components/layout/floating-preferences.tsx`
|
||||||
|
|
||||||
@@ -162,11 +176,9 @@ Source:
|
|||||||
|
|
||||||
- `components/ui/card.tsx`
|
- `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`
|
### `AppCard`
|
||||||
|
|
||||||
@@ -199,9 +211,9 @@ Current interactive rule:
|
|||||||
|
|
||||||
- `hover:bg-accent/30 hover:text-accent-foreground`
|
- `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/media-field-picker.tsx`
|
||||||
- `components/root/portfolio-projects-overview.tsx`
|
- `components/root/portfolio-projects-overview.tsx`
|
||||||
@@ -217,11 +229,9 @@ Source:
|
|||||||
|
|
||||||
- `components/ui/button.tsx`
|
- `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:
|
Current variants:
|
||||||
|
|
||||||
@@ -238,17 +248,6 @@ Current variants:
|
|||||||
- `destructive`
|
- `destructive`
|
||||||
- `bg-destructive text-destructive-foreground hover:bg-destructive/90`
|
- `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
|
### Input
|
||||||
|
|
||||||
Source:
|
Source:
|
||||||
@@ -257,9 +256,7 @@ Source:
|
|||||||
|
|
||||||
Current rule:
|
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 ... focus-visible:border-ring ... focus-visible:ring-ring/30`
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Select
|
### Select
|
||||||
|
|
||||||
@@ -269,21 +266,15 @@ Source:
|
|||||||
|
|
||||||
Current trigger rule:
|
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:
|
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:
|
Current item rule:
|
||||||
|
|
||||||
```tsx
|
`rounded-nested ... focus:bg-accent focus:text-accent-foreground`
|
||||||
rounded-nested ... focus:bg-accent focus:text-accent-foreground
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dialog
|
### Dialog
|
||||||
|
|
||||||
@@ -293,21 +284,15 @@ Source:
|
|||||||
|
|
||||||
Current overlay rule:
|
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:
|
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:
|
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
|
### Sheet
|
||||||
|
|
||||||
@@ -317,15 +302,11 @@ Source:
|
|||||||
|
|
||||||
Current overlay rule matches `Dialog`:
|
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:
|
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:
|
Current side radius rules:
|
||||||
|
|
||||||
@@ -342,21 +323,15 @@ Source:
|
|||||||
|
|
||||||
Current list rule:
|
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:
|
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
|
## 7. Current layout/container rules in use
|
||||||
|
|
||||||
@@ -366,11 +341,9 @@ Source:
|
|||||||
|
|
||||||
- `components/layout/container.tsx`
|
- `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:
|
Current size variants:
|
||||||
|
|
||||||
@@ -383,7 +356,18 @@ Current size variants:
|
|||||||
- `admin`
|
- `admin`
|
||||||
- `max-w-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:
|
Current shared top controls use:
|
||||||
|
|
||||||
@@ -404,7 +388,7 @@ Current admin layout still uses:
|
|||||||
|
|
||||||
- `min-h-screen bg-muted/30`
|
- `min-h-screen bg-muted/30`
|
||||||
- `grid min-h-screen lg:grid-cols-[280px_minmax(0,1fr)]`
|
- `grid min-h-screen lg:grid-cols-[280px_minmax(0,1fr)]`
|
||||||
- `sticky top-0`
|
- sticky sidebar and header
|
||||||
- `p-4 lg:p-6`
|
- `p-4 lg:p-6`
|
||||||
|
|
||||||
Source:
|
Source:
|
||||||
@@ -413,7 +397,7 @@ Source:
|
|||||||
|
|
||||||
## 8. Current spacing rhythm in use
|
## 8. Current spacing rhythm in use
|
||||||
|
|
||||||
Current repeated spacing values across primitives and layout:
|
Current repeated spacing values:
|
||||||
|
|
||||||
- `p-4`
|
- `p-4`
|
||||||
- `p-6`
|
- `p-6`
|
||||||
@@ -446,6 +430,15 @@ Current primitive spacing sources:
|
|||||||
- `Container`
|
- `Container`
|
||||||
- `px-4 sm:px-6 lg:px-8`
|
- `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
|
## 9. Current motion rules in use
|
||||||
|
|
||||||
Primary motion library:
|
Primary motion library:
|
||||||
@@ -460,16 +453,16 @@ Current motion patterns:
|
|||||||
|
|
||||||
- reveal-on-scroll wrapper
|
- reveal-on-scroll wrapper
|
||||||
- `components/motion-fade.tsx`
|
- `components/motion-fade.tsx`
|
||||||
- `initial: { opacity: 0, y: 20 }`
|
- `initial={{ opacity: 0, y: 20 }}`
|
||||||
- `whileInView: { opacity: 1, y: 0 }`
|
- `whileInView={{ opacity: 1, y: 0 }}`
|
||||||
- `transition: { duration: 0.45, ease: "easeOut" }`
|
- `transition={{ duration: 0.45, delay, ease: "easeOut" }}`
|
||||||
- shared overlay open/close classes
|
- shared overlay and disclosure state classes
|
||||||
- `dialog`
|
- `components/ui/dialog.tsx`
|
||||||
- `sheet`
|
- `components/ui/sheet.tsx`
|
||||||
- `select`
|
- `components/ui/select.tsx`
|
||||||
- `dropdown-menu`
|
- `components/ui/dropdown-menu.tsx`
|
||||||
- `accordion`
|
- `components/ui/accordion.tsx`
|
||||||
- animated nav active pill and mobile menu
|
- animated navigation chrome
|
||||||
- `components/layout/site-header.tsx`
|
- `components/layout/site-header.tsx`
|
||||||
|
|
||||||
## 10. Current icon rules in use
|
## 10. Current icon rules in use
|
||||||
@@ -478,9 +471,13 @@ Current icon library:
|
|||||||
|
|
||||||
- `lucide-react`
|
- `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:
|
Examples:
|
||||||
|
|
||||||
@@ -490,47 +487,50 @@ Examples:
|
|||||||
- `components/theme-toggle.tsx`
|
- `components/theme-toggle.tsx`
|
||||||
- `components/root/portfolio-project-form.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
|
## 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/home-hero.tsx`
|
||||||
- `components/layout/site-ambient-backdrop.tsx`
|
- `components/layout/site-ambient-backdrop.tsx`
|
||||||
- `components/layout/hero-atmosphere.tsx`
|
- `components/layout/hero-atmosphere.tsx`
|
||||||
- `components/layout/site-logo.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`
|
- `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/page.tsx`
|
||||||
- `app/[locale]/(site)/portfolio/[slug]/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`
|
- `components/root/site-settings-form.tsx`
|
||||||
- `bg-slate-950`
|
- `bg-slate-950`
|
||||||
- `text-white/55`
|
- `text-white/55`
|
||||||
- `text-white/60`
|
- `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
|
## 12. Codex rules derived from the current system
|
||||||
|
|
||||||
- Use `components/ui` as the primitive source of truth.
|
- Use `components/ui` as the primitive source of truth.
|
||||||
- Use `cn()` from `lib/utils.ts` for class composition.
|
- 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 semantic tokens already defined in `app/globals.css` and mapped in `tailwind.config.ts`.
|
||||||
- Use `Card` for the base surface:
|
- Use `Card` as the base surface primitive.
|
||||||
- `rounded-surface border border-border/80 bg-card text-card-foreground shadow-card`
|
|
||||||
- Use `AppCard` when feature code needs explicit shared surface levels.
|
- Use `AppCard` when feature code needs explicit shared surface levels.
|
||||||
- Use `AppCard level={1}` for primary card surfaces.
|
- Use `AppCard level={1}` for primary card surfaces.
|
||||||
- Use `AppCard level={2}` for nested panels and inner blocks.
|
- Use `AppCard level={2}` for nested panels and inner blocks.
|
||||||
- Use `AppCard level={3}` for elevated grouped panels.
|
- Use `AppCard level={3}` for elevated grouped panels.
|
||||||
- Use `rounded-surface`, `rounded-nested`, and `rounded-pill` as the shared radius system.
|
- 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 `Container` for width and horizontal padding rules.
|
||||||
- Use `lucide-react` for icons.
|
- Use `lucide-react` for icons.
|
||||||
- Use `framer-motion` for custom motion and `tailwindcss-animate` for primitive state transitions.
|
- 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
@@ -8,7 +8,9 @@
|
|||||||
"root": "Root",
|
"root": "Root",
|
||||||
"openMenu": "فتح القائمة",
|
"openMenu": "فتح القائمة",
|
||||||
"closeMenu": "إغلاق القائمة",
|
"closeMenu": "إغلاق القائمة",
|
||||||
"themeToggle": "تبديل المظهر"
|
"themeToggle": "تبديل المظهر",
|
||||||
|
"soundMute": "كتم الأصوات",
|
||||||
|
"soundUnmute": "تشغيل الأصوات"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} moh-sass. جميع الحقوق محفوظة."
|
"copyright": "© {year} moh-sass. جميع الحقوق محفوظة."
|
||||||
|
|||||||
+3
-1
@@ -8,7 +8,9 @@
|
|||||||
"root": "Root",
|
"root": "Root",
|
||||||
"openMenu": "Menue oeffnen",
|
"openMenu": "Menue oeffnen",
|
||||||
"closeMenu": "Menue schliessen",
|
"closeMenu": "Menue schliessen",
|
||||||
"themeToggle": "Theme wechseln"
|
"themeToggle": "Theme wechseln",
|
||||||
|
"soundMute": "Sounds stummschalten",
|
||||||
|
"soundUnmute": "Sounds aktivieren"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} moh-sass. Alle Rechte vorbehalten."
|
"copyright": "© {year} moh-sass. Alle Rechte vorbehalten."
|
||||||
|
|||||||
+3
-1
@@ -8,7 +8,9 @@
|
|||||||
"root": "Root",
|
"root": "Root",
|
||||||
"openMenu": "Open menu",
|
"openMenu": "Open menu",
|
||||||
"closeMenu": "Close menu",
|
"closeMenu": "Close menu",
|
||||||
"themeToggle": "Toggle theme"
|
"themeToggle": "Toggle theme",
|
||||||
|
"soundMute": "Mute sounds",
|
||||||
|
"soundUnmute": "Unmute sounds"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} moh-sass. All rights reserved."
|
"copyright": "© {year} moh-sass. All rights reserved."
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user