Compare commits
8
Commits
b8a02f9e90
...
0f48381894
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f48381894 | ||
|
|
59094eba85 | ||
|
|
dd72f013d4 | ||
|
|
86243b79c2 | ||
|
|
4892f27006 | ||
|
|
f7355b5356 | ||
|
|
dcffa87be7 | ||
|
|
d1a1a67b84 |
@@ -180,6 +180,7 @@ export default async function HomePage({ params }: HomePageProps) {
|
|||||||
String(project.projectYear),
|
String(project.projectYear),
|
||||||
],
|
],
|
||||||
featured: index === 0,
|
featured: index === 0,
|
||||||
|
coverImagePath: project.coverImagePath,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const fallbackProjectCards: ProjectCardCopy[] = projectsCopy.fallbackItems.map((item) => ({
|
const fallbackProjectCards: ProjectCardCopy[] = projectsCopy.fallbackItems.map((item) => ({
|
||||||
@@ -203,6 +204,9 @@ export default async function HomePage({ params }: HomePageProps) {
|
|||||||
kicker={t("heroVisual.kicker")}
|
kicker={t("heroVisual.kicker")}
|
||||||
title={t("heroVisual.title")}
|
title={t("heroVisual.title")}
|
||||||
description={t("heroVisual.description")}
|
description={t("heroVisual.description")}
|
||||||
|
availability={contactCtaCopy.availabilityValue}
|
||||||
|
primaryCta={{ label: t("heroVisual.primaryCta"), href: portfolioHref }}
|
||||||
|
secondaryCta={{ label: t("heroVisual.secondaryCta"), href: contactHref }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Container
|
<Container
|
||||||
|
|||||||
@@ -58,10 +58,18 @@ export default async function PortfolioItemPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const t = await getTranslations({ locale: localeKey, namespace: "portfolioDetail" });
|
const t = await getTranslations({ locale: localeKey, namespace: "portfolioDetail" });
|
||||||
|
const title = getLocalizedValue(item.title, localeKey);
|
||||||
|
const category = getLocalizedValue(item.category.name, localeKey);
|
||||||
|
const summary = getLocalizedValue(item.summary, localeKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHero />
|
<PageHero
|
||||||
|
locale={localeKey}
|
||||||
|
badge={category}
|
||||||
|
lines={[{ text: title, tone: "soft", align: "center" }]}
|
||||||
|
description={summary}
|
||||||
|
/>
|
||||||
|
|
||||||
<Container size="wide" className="pb-12 lg:pb-16">
|
<Container size="wide" className="pb-12 lg:pb-16">
|
||||||
<PortfolioProjectDetail
|
<PortfolioProjectDetail
|
||||||
|
|||||||
@@ -168,6 +168,18 @@ html[lang="ar"] {
|
|||||||
font-family: var(--font-dubai), sans-serif;
|
font-family: var(--font-dubai), sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Eyebrow / overline label. Latin gets uppercase + tracking; Arabic keeps
|
||||||
|
normal case and spacing so joined letterforms are not broken. */
|
||||||
|
.eyebrow {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[lang="ar"] .eyebrow {
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Bot, Database, Globe, ServerCog } from "lucide-react";
|
import { Bot, Database, Globe, ServerCog } from "lucide-react";
|
||||||
|
|
||||||
import { AppCard } from "@/components/ui/app-card";
|
|
||||||
import { SectionHeading } from "./section-heading";
|
import { SectionHeading } from "./section-heading";
|
||||||
import type { CapabilitiesSectionCopy } from "./types";
|
import type { CapabilitiesSectionCopy } from "./types";
|
||||||
|
|
||||||
@@ -19,29 +18,25 @@ export function CapabilitiesSection({ copy }: CapabilitiesSectionProps) {
|
|||||||
description={copy.description}
|
description={copy.description}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4 xl:gap-6">
|
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-4">
|
||||||
{copy.items.map((item, index) => {
|
{copy.items.map((item, index) => {
|
||||||
const Icon = capabilityIcons[index] ?? Globe;
|
const Icon = capabilityIcons[index] ?? Globe;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppCard
|
<div
|
||||||
key={item.title}
|
key={item.title}
|
||||||
layer="single"
|
className="group flex h-full flex-col gap-5 rounded-surface border border-border bg-surface-1 p-6 shadow-card transition duration-300 ease-out hover:-translate-y-1 hover:shadow-panel"
|
||||||
interactive
|
|
||||||
className="group border-border/80 bg-surface-2 transition-all duration-300 hover:-translate-y-1 hover:border-border-strong hover:shadow-panel"
|
|
||||||
>
|
>
|
||||||
<div className="flex h-full flex-col gap-5 p-6">
|
<div className="flex h-12 w-12 items-center justify-center rounded-nested bg-brand-primary/10 text-brand-primary transition-transform duration-300 group-hover:-translate-y-0.5">
|
||||||
<div className="flex h-11 w-11 items-center justify-center rounded-nested border border-border/70 bg-background text-foreground transition-transform duration-300 group-hover:-translate-y-0.5">
|
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<h3 className="text-xl font-semibold tracking-[-0.04em] text-foreground">
|
<h3 className="text-title font-semibold text-foreground">
|
||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm leading-7 text-muted-foreground">{item.description}</p>
|
<p className="text-small leading-relaxed text-muted-foreground">{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppCard>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,26 +19,26 @@ export function ContactCtaSection({
|
|||||||
<AppCard className="overflow-hidden">
|
<AppCard className="overflow-hidden">
|
||||||
<div className="grid gap-6 rounded-nested border border-border/70 bg-background p-6 sm:p-8 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end lg:p-10">
|
<div className="grid gap-6 rounded-nested border border-border/70 bg-background p-6 sm:p-8 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end lg:p-10">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
<p className="eyebrow text-caption font-medium text-brand-primary">
|
||||||
{copy.eyebrow}
|
{copy.eyebrow}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="max-w-3xl text-balance text-3xl font-semibold tracking-[-0.05em] text-foreground sm:text-4xl lg:text-5xl">
|
<h2 className="max-w-3xl text-balance text-h2 text-foreground">
|
||||||
{copy.title}
|
{copy.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="max-w-2xl text-sm leading-7 text-muted-foreground sm:text-base">
|
<p className="max-w-2xl text-body-lg text-muted-foreground">
|
||||||
{copy.description}
|
{copy.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-6 text-sm">
|
<div className="flex flex-wrap gap-8 pt-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="font-semibold text-foreground">{copy.emailLabel}</p>
|
<p className="text-caption font-medium text-muted-foreground">{copy.emailLabel}</p>
|
||||||
<a href={`mailto:${copy.emailValue}`} className="text-muted-foreground hover:text-foreground">
|
<a href={`mailto:${copy.emailValue}`} className="text-small text-foreground transition-colors hover:text-brand-primary">
|
||||||
{copy.emailValue}
|
{copy.emailValue}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="font-semibold text-foreground">{copy.availabilityLabel}</p>
|
<p className="text-caption font-medium text-muted-foreground">{copy.availabilityLabel}</p>
|
||||||
<p className="text-muted-foreground">{copy.availabilityValue}</p>
|
<p className="text-small text-foreground">{copy.availabilityValue}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AppCard } from "@/components/ui/app-card";
|
|
||||||
import { SectionHeading } from "./section-heading";
|
import { SectionHeading } from "./section-heading";
|
||||||
import type { ProcessSectionCopy } from "./types";
|
import type { ProcessSectionCopy } from "./types";
|
||||||
|
|
||||||
@@ -8,35 +7,28 @@ type ProcessSectionProps = {
|
|||||||
|
|
||||||
export function ProcessSection({ copy }: ProcessSectionProps) {
|
export function ProcessSection({ copy }: ProcessSectionProps) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-8">
|
<section className="space-y-10">
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
eyebrow={copy.eyebrow}
|
eyebrow={copy.eyebrow}
|
||||||
title={copy.title}
|
title={copy.title}
|
||||||
description={copy.description}
|
description={copy.description}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4 xl:gap-6">
|
<ol className="grid grid-cols-1 gap-x-8 gap-y-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{copy.steps.map((step, index) => (
|
{copy.steps.map((step, index) => (
|
||||||
<AppCard
|
<li key={step.title} className="border-t border-border pt-5">
|
||||||
key={step.title}
|
<span className="block text-[2.75rem] font-semibold leading-none tracking-tight text-brand-primary/25">
|
||||||
layer="single"
|
|
||||||
interactive
|
|
||||||
className="group border-border/80 bg-surface-2 transition-all duration-300 hover:-translate-y-1 hover:border-border-strong hover:shadow-panel"
|
|
||||||
>
|
|
||||||
<div className="flex h-full flex-col gap-5 p-6">
|
|
||||||
<p className="text-sm font-semibold tracking-[0.18em] text-muted-foreground">
|
|
||||||
{String(index + 1).padStart(2, "0")}
|
{String(index + 1).padStart(2, "0")}
|
||||||
</p>
|
</span>
|
||||||
<div className="space-y-3">
|
<h3 className="mt-5 text-title font-semibold text-foreground">
|
||||||
<h3 className="text-2xl font-semibold tracking-[-0.05em] text-foreground">
|
|
||||||
{step.title}
|
{step.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm leading-7 text-muted-foreground">{step.description}</p>
|
<p className="mt-2 text-small leading-relaxed text-muted-foreground">
|
||||||
</div>
|
{step.description}
|
||||||
</div>
|
</p>
|
||||||
</AppCard>
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ArrowUpRight } from "lucide-react";
|
import { ArrowUpRight } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { PortfolioCover } from "@/components/site/portfolio-cover";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { AppCard } from "@/components/ui/app-card";
|
import { cn } from "@/lib/utils";
|
||||||
import { SectionHeading } from "./section-heading";
|
import { SectionHeading } from "./section-heading";
|
||||||
import type { ProjectCardCopy, ProjectsSectionCopy } from "./types";
|
import type { ProjectCardCopy, ProjectsSectionCopy } from "./types";
|
||||||
|
|
||||||
@@ -20,66 +21,85 @@ export function ProjectsSection({ copy, items }: ProjectsSectionProps) {
|
|||||||
description={copy.description}
|
description={copy.description}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3 xl:gap-6">
|
<div className="grid grid-cols-1 gap-5 lg:grid-cols-3">
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => {
|
||||||
<AppCard
|
const featured = Boolean(item.featured);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
key={`${item.title}-${index}`}
|
key={`${item.title}-${index}`}
|
||||||
layer="single"
|
href={item.href}
|
||||||
interactive
|
className={cn(
|
||||||
className={[
|
"group flex h-full flex-col overflow-hidden rounded-surface border border-border bg-surface-1 shadow-card transition duration-300 ease-out hover:-translate-y-1 hover:shadow-panel focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
|
||||||
"group h-full border-border/80 bg-surface-2 transition-all duration-300 hover:-translate-y-1 hover:border-border-strong hover:bg-background/80 hover:shadow-lg",
|
featured && "lg:col-span-2",
|
||||||
index === 0 ? "lg:col-span-2" : "",
|
)}
|
||||||
].join(" ")}
|
|
||||||
>
|
>
|
||||||
<div className="flex h-full flex-col gap-6 p-6">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative overflow-hidden",
|
||||||
|
featured ? "aspect-[16/10]" : "aspect-[4/3]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PortfolioCover
|
||||||
|
src={item.coverImagePath ?? null}
|
||||||
|
title={item.title}
|
||||||
|
category={item.meta[0]}
|
||||||
|
sizes={
|
||||||
|
featured
|
||||||
|
? "(min-width: 1024px) 66vw, 100vw"
|
||||||
|
: "(min-width: 1024px) 33vw, 100vw"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col p-6">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{item.meta.map((metaItem) => (
|
{item.meta.map((metaItem) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={metaItem}
|
key={metaItem}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-border/80 bg-background text-muted-foreground transition-colors duration-300 group-hover:border-border-strong group-hover:text-foreground/80"
|
className="border-border text-caption text-muted-foreground"
|
||||||
>
|
>
|
||||||
{metaItem}
|
{metaItem}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<h3
|
||||||
<h3 className="text-2xl font-semibold tracking-[-0.05em] text-foreground transition-colors duration-300 group-hover:text-foreground/95">
|
className={cn(
|
||||||
|
"mt-4 font-semibold tracking-tight text-foreground",
|
||||||
|
featured ? "text-h3 sm:text-h2" : "text-h3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm leading-7 text-muted-foreground">{item.summary}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<p className="mt-2 line-clamp-2 text-small leading-relaxed text-muted-foreground">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
{item.summary}
|
||||||
{copy.stackLabel}
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
|
{item.stack.length > 0 ? (
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
{item.stack.map((stackItem) => (
|
{item.stack.map((stackItem) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={stackItem}
|
key={stackItem}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="bg-secondary/70 text-foreground transition-colors duration-300 group-hover:bg-secondary"
|
className="bg-secondary/70 text-caption text-foreground"
|
||||||
>
|
>
|
||||||
{stackItem}
|
{stackItem}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
<div className="mt-auto">
|
<span className="mt-auto inline-flex items-center gap-2 pt-6 text-caption font-medium text-foreground/70 transition-colors group-hover:text-brand-primary">
|
||||||
<Link
|
|
||||||
href={item.href}
|
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium text-foreground transition-all duration-200 group-hover:translate-x-0.5"
|
|
||||||
>
|
|
||||||
{item.cta}
|
{item.cta}
|
||||||
<ArrowUpRight className="h-4 w-4 transition-transform duration-200 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
|
<ArrowUpRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
);
|
||||||
</div>
|
})}
|
||||||
</AppCard>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ export function SectionHeading({
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
<p className="eyebrow text-caption font-medium text-brand-primary">
|
||||||
{eyebrow}
|
{eyebrow}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="max-w-4xl text-balance text-3xl font-semibold tracking-[-0.05em] text-foreground sm:text-4xl lg:text-5xl">
|
<h2 className="max-w-4xl text-balance text-h2 text-foreground">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="max-w-3xl text-sm leading-7 text-muted-foreground sm:text-[1.02rem]">
|
<p className={cn("text-body-lg text-muted-foreground", align === "center" ? "mx-auto" : "max-w-2xl")}>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export type ProjectCardCopy = {
|
|||||||
cta: string;
|
cta: string;
|
||||||
meta: string[];
|
meta: string[];
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
|
coverImagePath?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectsSectionCopy = {
|
export type ProjectsSectionCopy = {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ArrowLeft, ArrowRight, Mail } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import { HeroBadge } from "@/components/layout/hero-badge";
|
import { HeroBadge } from "@/components/layout/hero-badge";
|
||||||
import {
|
import {
|
||||||
HeroContentMotion,
|
HeroContentMotion,
|
||||||
@@ -7,12 +10,21 @@ import {
|
|||||||
HeroShell,
|
HeroShell,
|
||||||
HeroTitle,
|
HeroTitle,
|
||||||
} from "@/components/layout/site-hero";
|
} from "@/components/layout/site-hero";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
type HeroCta = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
type HomeHeroProps = {
|
type HomeHeroProps = {
|
||||||
locale: string;
|
locale: string;
|
||||||
kicker: string;
|
kicker: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
availability?: string;
|
||||||
|
primaryCta?: HeroCta;
|
||||||
|
secondaryCta?: HeroCta;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function HomeHero({
|
export function HomeHero({
|
||||||
@@ -20,9 +32,13 @@ export function HomeHero({
|
|||||||
kicker,
|
kicker,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
availability,
|
||||||
|
primaryCta,
|
||||||
|
secondaryCta,
|
||||||
}: HomeHeroProps) {
|
}: HomeHeroProps) {
|
||||||
const titleLines = title.split("\n").filter(Boolean);
|
const titleLines = title.split("\n").filter(Boolean);
|
||||||
const isArabic = locale === "ar";
|
const isArabic = locale === "ar";
|
||||||
|
const DirectionIcon = isArabic ? ArrowLeft : ArrowRight;
|
||||||
const lines = isArabic
|
const lines = isArabic
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -67,13 +83,22 @@ export function HomeHero({
|
|||||||
return (
|
return (
|
||||||
<HeroShell showBridges>
|
<HeroShell showBridges>
|
||||||
<HeroContentMotion variant="home">
|
<HeroContentMotion variant="home">
|
||||||
|
{availability ? (
|
||||||
|
<HeroMotionItem variant="home">
|
||||||
|
<span className="inline-flex items-center gap-2.5 rounded-pill border border-foreground/10 bg-background/55 px-3.5 py-1.5 text-caption font-medium text-foreground/70 shadow-[var(--shadow-sm)] backdrop-blur-[18px]">
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-status-success opacity-70" />
|
||||||
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-status-success" />
|
||||||
|
</span>
|
||||||
|
{availability}
|
||||||
|
</span>
|
||||||
|
</HeroMotionItem>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<HeroMotionItem variant="home">
|
<HeroMotionItem variant="home">
|
||||||
<HeroBadge
|
<HeroBadge
|
||||||
trailing={
|
trailing={
|
||||||
<span
|
<span aria-hidden="true" className="text-[0.95rem] leading-none">
|
||||||
aria-hidden="true"
|
|
||||||
className="text-[0.95rem] leading-none"
|
|
||||||
>
|
|
||||||
👋
|
👋
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -81,12 +106,37 @@ export function HomeHero({
|
|||||||
{kicker}
|
{kicker}
|
||||||
</HeroBadge>
|
</HeroBadge>
|
||||||
</HeroMotionItem>
|
</HeroMotionItem>
|
||||||
|
|
||||||
<HeroTitle locale={locale} lines={lines} />
|
<HeroTitle locale={locale} lines={lines} />
|
||||||
|
|
||||||
<HeroMotionItem variant="home">
|
<HeroMotionItem variant="home">
|
||||||
<p className="mx-auto mt-7 max-w-[33rem] whitespace-pre-line text-sm leading-7 text-foreground/68 sm:text-base sm:leading-8">
|
<p className="mx-auto mt-7 max-w-[33rem] whitespace-pre-line text-body text-foreground/68">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</HeroMotionItem>
|
</HeroMotionItem>
|
||||||
|
|
||||||
|
{primaryCta || secondaryCta ? (
|
||||||
|
<HeroMotionItem variant="home">
|
||||||
|
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||||||
|
{primaryCta ? (
|
||||||
|
<Button asChild size="lg" className="w-full sm:w-auto">
|
||||||
|
<Link href={primaryCta.href}>
|
||||||
|
{primaryCta.label}
|
||||||
|
<DirectionIcon className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{secondaryCta ? (
|
||||||
|
<Button asChild variant="outline" size="lg" className="w-full sm:w-auto">
|
||||||
|
<Link href={secondaryCta.href}>
|
||||||
|
<Mail className="h-4 w-4" />
|
||||||
|
{secondaryCta.label}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</HeroMotionItem>
|
||||||
|
) : null}
|
||||||
</HeroContentMotion>
|
</HeroContentMotion>
|
||||||
</HeroShell>
|
</HeroShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { ImageIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type PortfolioCoverProps = {
|
||||||
|
src: string | null;
|
||||||
|
title: string;
|
||||||
|
category?: string;
|
||||||
|
sizes?: string;
|
||||||
|
priority?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SIZES =
|
||||||
|
"(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project cover. Renders the stored image when present, otherwise a restrained
|
||||||
|
* branded placeholder (owner will replace placeholders with real artwork).
|
||||||
|
* Fills a positioned, aspect-sized parent (`relative` + `aspect-*`).
|
||||||
|
*/
|
||||||
|
export function PortfolioCover({
|
||||||
|
src,
|
||||||
|
title,
|
||||||
|
category,
|
||||||
|
sizes = DEFAULT_SIZES,
|
||||||
|
priority = false,
|
||||||
|
className,
|
||||||
|
}: PortfolioCoverProps) {
|
||||||
|
if (src) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={title}
|
||||||
|
fill
|
||||||
|
sizes={sizes}
|
||||||
|
priority={priority}
|
||||||
|
className={cn(
|
||||||
|
"object-cover transition-transform duration-500 ease-out group-hover:scale-[1.04]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const monogram = title.trim().charAt(0).toUpperCase() || "•";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-full w-full flex-col items-center justify-center gap-3 bg-surface-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute -top-1/3 left-1/2 h-2/3 w-2/3 -translate-x-1/2 rounded-full bg-[radial-gradient(closest-side,hsl(var(--brand-primary)/0.12),transparent)]" />
|
||||||
|
<span className="text-[clamp(3rem,7vw,4.5rem)] font-semibold leading-none text-foreground/12">
|
||||||
|
{monogram}
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-caption text-muted-foreground/70">
|
||||||
|
<ImageIcon className="h-3.5 w-3.5" />
|
||||||
|
{category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -235,14 +235,7 @@ function ProjectHeader({
|
|||||||
<Link href={getLocalizedPath(locale, "/portfolio", defaultLocale)}>{t("back")}</Link>
|
<Link href={getLocalizedPath(locale, "/portfolio", defaultLocale)}>{t("back")}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<h1 className="mt-5 text-3xl font-semibold text-foreground sm:text-4xl lg:text-5xl">
|
<div className="mt-6">
|
||||||
{getLocalizedValue(item.title, locale)}
|
|
||||||
</h1>
|
|
||||||
<p className="mt-4 max-w-3xl text-base leading-8 text-muted-foreground sm:text-lg">
|
|
||||||
{getLocalizedValue(item.summary, locale)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-8">
|
|
||||||
<ProjectMeta item={item} locale={locale} />
|
<ProjectMeta item={item} locale={locale} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { ArrowUpRight, CalendarDays } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { MotionFade } from "@/components/motion-fade";
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { AppCard } from "@/components/ui/app-card";
|
import { PortfolioCover } from "@/components/site/portfolio-cover";
|
||||||
import { CardContent } from "@/components/ui/card";
|
|
||||||
import { getLocalizedPath, type AppLocale } from "@/lib/locale";
|
import { getLocalizedPath, type AppLocale } from "@/lib/locale";
|
||||||
import { getLocalizedValue, type PortfolioProjectView } from "@/lib/portfolio";
|
import { getLocalizedValue, type PortfolioProjectView } from "@/lib/portfolio";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type PortfolioProjectGridProps = {
|
type PortfolioProjectGridProps = {
|
||||||
locale: AppLocale;
|
locale: AppLocale;
|
||||||
@@ -22,48 +22,85 @@ export function PortfolioProjectGrid({
|
|||||||
emptyLabel,
|
emptyLabel,
|
||||||
openLabel,
|
openLabel,
|
||||||
}: PortfolioProjectGridProps) {
|
}: PortfolioProjectGridProps) {
|
||||||
|
if (projects.length === 0) {
|
||||||
return (
|
return (
|
||||||
<section className="grid gap-4 md:grid-cols-2">
|
<section>
|
||||||
{projects.map((item, index) => (
|
<div className="rounded-surface border border-border bg-surface-1 px-6 py-16 text-center text-small text-muted-foreground">
|
||||||
<MotionFade key={item.slug} delay={index * 0.05}>
|
|
||||||
<AppCard interactive>
|
|
||||||
<CardContent className="p-5">
|
|
||||||
<Link
|
|
||||||
href={getLocalizedPath(locale, `/portfolio/${item.slug}`, defaultLocale)}
|
|
||||||
className="group block"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<p className="text-sm text-muted-foreground/80">
|
|
||||||
{getLocalizedValue(item.category.name, locale)}
|
|
||||||
</p>
|
|
||||||
<p className="inline-flex items-center gap-1 text-xs text-muted-foreground/80">
|
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
|
||||||
{item.projectYear}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<h2 className="mt-3 text-xl font-semibold text-foreground">
|
|
||||||
{getLocalizedValue(item.title, locale)}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
|
||||||
{getLocalizedValue(item.summary, locale)}
|
|
||||||
</p>
|
|
||||||
<p className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-foreground/80 group-hover:text-foreground">
|
|
||||||
{openLabel}
|
|
||||||
<ArrowUpRight className="h-4 w-4" />
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</CardContent>
|
|
||||||
</AppCard>
|
|
||||||
</MotionFade>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{projects.length === 0 ? (
|
|
||||||
<AppCard className="md:col-span-2">
|
|
||||||
<CardContent className="p-6 text-sm text-muted-foreground">
|
|
||||||
{emptyLabel}
|
{emptyLabel}
|
||||||
</CardContent>
|
</div>
|
||||||
</AppCard>
|
</section>
|
||||||
) : null}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{projects.map((item, index) => {
|
||||||
|
const featured = item.isFeatured;
|
||||||
|
const title = getLocalizedValue(item.title, locale);
|
||||||
|
const category = getLocalizedValue(item.category.name, locale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionFade
|
||||||
|
key={item.slug}
|
||||||
|
delay={Math.min(index * 0.05, 0.3)}
|
||||||
|
className={cn(featured && "sm:col-span-2 lg:col-span-2")}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={getLocalizedPath(locale, `/portfolio/${item.slug}`, defaultLocale)}
|
||||||
|
className="group flex h-full flex-col overflow-hidden rounded-surface border border-border bg-surface-1 shadow-card transition duration-300 ease-out hover:-translate-y-1 hover:shadow-panel focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative overflow-hidden",
|
||||||
|
featured ? "aspect-[16/10]" : "aspect-[4/3]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PortfolioCover
|
||||||
|
src={item.coverImagePath}
|
||||||
|
title={title}
|
||||||
|
category={category}
|
||||||
|
priority={index === 0}
|
||||||
|
sizes={
|
||||||
|
featured
|
||||||
|
? "(min-width: 1024px) 66vw, 100vw"
|
||||||
|
: "(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col p-5 sm:p-6">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="eyebrow text-caption text-muted-foreground">
|
||||||
|
{category}
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-caption text-muted-foreground">
|
||||||
|
<CalendarDays className="h-3.5 w-3.5" />
|
||||||
|
{item.projectYear}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
className={cn(
|
||||||
|
"mt-3 font-semibold tracking-tight text-foreground",
|
||||||
|
featured ? "text-h3 sm:text-h2" : "text-h3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="mt-2 line-clamp-2 text-small leading-relaxed text-muted-foreground">
|
||||||
|
{getLocalizedValue(item.summary, locale)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<span className="mt-5 inline-flex items-center gap-2 text-caption font-medium text-foreground/70 transition-colors group-hover:text-brand-primary">
|
||||||
|
{openLabel}
|
||||||
|
<ArrowUpRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</MotionFade>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# Design System — mohfarawati.de
|
||||||
|
|
||||||
|
The visual language for the public site and admin. This documents the token
|
||||||
|
system that already lives in `app/globals.css` and `tailwind.config.ts`, and
|
||||||
|
defines the layers that are still missing (type scale, spacing rhythm) so the
|
||||||
|
site has one consistent, premium base before any page is redesigned.
|
||||||
|
|
||||||
|
Source of truth for values: `app/globals.css` (`:root` + `.dark`). Tailwind
|
||||||
|
mappings: `tailwind.config.ts`. This file is the *usage guide* for those tokens.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Direction: Warm Premium (work-forward)
|
||||||
|
|
||||||
|
Chosen visual direction. Every design decision serves these principles:
|
||||||
|
|
||||||
|
1. **The work is the hero.** Layouts lead with project imagery and a strong
|
||||||
|
title hierarchy; chrome stays quiet.
|
||||||
|
2. **One decisive accent.** The warm red-orange brand color appears sparingly —
|
||||||
|
primary CTAs, one highlighted word, active/selected states, focus. Everything
|
||||||
|
else is neutral ink on light/dark surfaces. Accent is a punctuation mark, not
|
||||||
|
a background.
|
||||||
|
3. **Generous whitespace + restraint.** Space and type hierarchy carry the
|
||||||
|
premium feel — not borders, gradients, or decoration. "Too busy" is the
|
||||||
|
default failure mode; cut before adding.
|
||||||
|
4. **Depth, softly.** Elevation via one soft shadow layer and the large 24px
|
||||||
|
radius already in the tokens. Avoid stacking shadows or heavy borders.
|
||||||
|
5. **Light + dark parity.** Every token has a dark value; never hardcode a hex.
|
||||||
|
6. **Arabic is first-class.** RTL layout, no uppercase or letter-spacing on
|
||||||
|
Arabic text, slightly looser line-height. Latin uses Museo, Arabic uses Dubai.
|
||||||
|
|
||||||
|
Colors and fonts are fixed by the owner and are **not** changed by this system —
|
||||||
|
it only organizes and documents them and adds the missing scale layers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Color tokens
|
||||||
|
|
||||||
|
All colors are HSL channel triplets consumed as `hsl(var(--token))`. Light is
|
||||||
|
`:root`, dark is `.dark`.
|
||||||
|
|
||||||
|
### Base
|
||||||
|
|
||||||
|
| Token | Light | Dark | Use |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--background` | `0 0% 100%` | `222 22% 8%` | Page canvas |
|
||||||
|
| `--foreground` | `222 24% 10%` | `210 20% 96%` | Primary ink |
|
||||||
|
| `--muted` | `220 14% 96%` | `222 16% 14%` | Quiet fills |
|
||||||
|
| `--muted-foreground` | `220 10% 42%` | `217 10% 64%` | Secondary text |
|
||||||
|
|
||||||
|
### Surfaces (elevation ramp)
|
||||||
|
|
||||||
|
| Token | Light | Dark | Use |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--surface-1` | `0 0% 100%` | `222 22% 9%` | Base card |
|
||||||
|
| `--surface-2` | `220 16% 97%` | `222 18% 12%` | Raised card / section tint |
|
||||||
|
| `--surface-3` | `220 14% 94%` | `222 16% 15%` | Highest / hover |
|
||||||
|
| `--surface-inverse` | `222 24% 12%` | `210 20% 96%` | Inverted blocks |
|
||||||
|
|
||||||
|
Step surfaces 1 → 2 → 3 for hierarchy. Do not invent intermediate greys.
|
||||||
|
|
||||||
|
### Brand + semantic (use sparingly)
|
||||||
|
|
||||||
|
| Token | Light | Dark | Use |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--brand-primary` / `--primary` / `--ring` | `9 73% 50%` | `9 78% 56%` | The one accent: CTAs, highlight word, focus |
|
||||||
|
| `--brand-secondary` | `12 64% 58%` | `12 74% 64%` | Accent gradient partner, live dots |
|
||||||
|
| `--primary-foreground` | `210 40% 98%` | `210 40% 98%` | Text on accent |
|
||||||
|
| `--destructive` | `0 84% 60%` | `0 63% 31%` | Errors only |
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
| Token | Light | Dark |
|
||||||
|
|---|---|---|
|
||||||
|
| `--status-success` / `--status-success-soft` | `145 63% 42%` / `145 58% 92%` | `145 68% 66%` / `145 36% 18%` |
|
||||||
|
| `--status-warning` / `--status-warning-soft` | `36 92% 44%` / `41 96% 90%` | `42 96% 66%` / `35 40% 18%` |
|
||||||
|
|
||||||
|
Reserved for real state (success/warning feedback), never decoration.
|
||||||
|
|
||||||
|
### Lines + hero
|
||||||
|
|
||||||
|
| Token | Light | Dark | Use |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--border` | `220 13% 90%` | `222 14% 18%` | Default hairline |
|
||||||
|
| `--border-strong` | `220 11% 82%` | `222 12% 26%` | Emphasis divider |
|
||||||
|
| `--hero-base` / `--hero-ink` | `260 22% 95%` / `226 43% 11%` | `222 24% 10%` / `210 20% 96%` | Hero backdrop base + ink |
|
||||||
|
|
||||||
|
Tailwind exposes these as `bg-background`, `text-foreground`, `bg-surface-2`,
|
||||||
|
`text-brand-primary`, `border-border`, `bg-status-success-soft`, etc.
|
||||||
|
|
||||||
|
**Accent budget:** at most one accent-filled element per view (usually the
|
||||||
|
primary CTA). Additional emphasis uses ink weight/size, not more color.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Typography
|
||||||
|
|
||||||
|
### Fonts (fixed)
|
||||||
|
|
||||||
|
| Script | Family | Variable | Utility | Weights |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Latin (default) | Museo Sans Rounded | `--font-museo` | `.font-latin` | 300, 500, 700, 900 |
|
||||||
|
| Arabic | Dubai | `--font-dubai` | `.font-arabic` | 300, 400, 700 |
|
||||||
|
|
||||||
|
Applied on `body` (Latin) and `html[lang="ar"]` (Arabic) in `globals.css`.
|
||||||
|
|
||||||
|
### Type scale (to add as tokens in 2.3)
|
||||||
|
|
||||||
|
A single documented scale replaces ad-hoc sizes. Latin values; Arabic overrides
|
||||||
|
noted below.
|
||||||
|
|
||||||
|
| Role | Size (clamp) | Weight | Line-height | Tracking |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `display` | `clamp(2.75rem, 8vw, 5.5rem)` | 900 | 0.95 | -0.03em |
|
||||||
|
| `h1` | `clamp(2.25rem, 5vw, 3.5rem)` | 700 | 1.05 | -0.02em |
|
||||||
|
| `h2` | `clamp(1.75rem, 3.5vw, 2.5rem)` | 700 | 1.1 | -0.015em |
|
||||||
|
| `h3` | `1.375rem` (22px) | 700 | 1.2 | -0.01em |
|
||||||
|
| `title` | `1.125rem` (18px) | 500 | 1.3 | 0 |
|
||||||
|
| `body-lg` | `1.125rem` (18px) | 400 | 1.7 | 0 |
|
||||||
|
| `body` | `1rem` (16px) | 400 | 1.7 | 0 |
|
||||||
|
| `small` | `0.875rem` (14px) | 400 / 500 | 1.6 | 0 |
|
||||||
|
| `caption` | `0.8125rem` (13px) | 500 | 1.5 | 0 |
|
||||||
|
| `overline` | `0.75rem` (12px) | 500 | 1.4 | 0.14em, uppercase |
|
||||||
|
|
||||||
|
**Arabic overrides:** never `uppercase`, never letter-spacing (breaks joins);
|
||||||
|
add ~0.1 to line-height on display/h1/h2; cap weight at 700 (Dubai has no 900).
|
||||||
|
The `overline` role for Arabic uses normal tracking and normal case.
|
||||||
|
|
||||||
|
Only two Latin weights per view in body copy (regular 400 + one bold). Reserve
|
||||||
|
900 for `display` only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Radius
|
||||||
|
|
||||||
|
| Token | Value | Use |
|
||||||
|
|---|---|---|
|
||||||
|
| `--radius-surface` | `24px` | Cards, panels, large containers (`rounded-surface` / `lg` / `xl`) |
|
||||||
|
| `--radius-nested` | `16px` | Nested elements, inputs, buttons (`rounded-nested` / `sm` / `md`) |
|
||||||
|
| `--radius-pill` | `9999px` | Pills, badges, toggles (`rounded-pill`) |
|
||||||
|
|
||||||
|
The generous 24px surface radius is part of the premium feel — keep it
|
||||||
|
consistent; do not mix arbitrary radii.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Elevation (shadows)
|
||||||
|
|
||||||
|
| Token | Use |
|
||||||
|
|---|---|
|
||||||
|
| `--shadow-xs` / `shadow-xs` | Subtle lift (hairline cards) |
|
||||||
|
| `--shadow-sm` / `shadow-sm` | Small controls, pills |
|
||||||
|
| `--shadow-card` / `shadow-card` | Default raised card |
|
||||||
|
| `--shadow-panel` / `shadow-panel` | Floating panel / glass surface |
|
||||||
|
| `--shadow-md`, `--shadow-lg` | Reserved for overlays; use rarely |
|
||||||
|
|
||||||
|
Discipline: most surfaces are flat with a hairline border. Raise with **one**
|
||||||
|
shadow layer at a time. Never stack `shadow-lg` on nested elements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Spacing + layout
|
||||||
|
|
||||||
|
### Containers
|
||||||
|
|
||||||
|
| Token | Value | Use |
|
||||||
|
|---|---|---|
|
||||||
|
| `--container-narrow` | `48rem` | Reading width, forms |
|
||||||
|
| `--container-default` | `72rem` | Standard page content |
|
||||||
|
| `--container-wide` | `80rem` | Galleries, wide grids |
|
||||||
|
| `--container-admin` | `88rem` | Admin workspace |
|
||||||
|
|
||||||
|
Header: `--header-height: 4.5rem`.
|
||||||
|
|
||||||
|
### Vertical rhythm (to formalize in 2.3)
|
||||||
|
|
||||||
|
Existing: `--section-space: 3.5rem`, `--content-space: 1.5rem`
|
||||||
|
(`spacing.section`, `spacing.content`).
|
||||||
|
|
||||||
|
Premium layouts need more air. Recommended section rhythm to add:
|
||||||
|
|
||||||
|
| Token | Value | Use |
|
||||||
|
|---|---|---|
|
||||||
|
| `section-sm` | `3rem` | Tight sections |
|
||||||
|
| `section` | `5rem` (mobile) → `7rem` (lg) | Default between major sections |
|
||||||
|
| `section-lg` | `8rem`+ | Hero-adjacent breathing room |
|
||||||
|
|
||||||
|
Internal gaps use rem steps: `1rem`, `1.5rem`, `2rem`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Motion
|
||||||
|
|
||||||
|
framer-motion, restrained. Entrance pattern (see `components/layout/site-hero.tsx`):
|
||||||
|
- item: `opacity 0→1`, `y 16→0`, `duration 0.54`, ease `[0.22, 1, 0.36, 1]`
|
||||||
|
- container stagger: `0.13`, delay `0.08`
|
||||||
|
|
||||||
|
Rules: entrance + hover only; respect `prefers-reduced-motion`; no looping or
|
||||||
|
attention-grabbing motion on content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Component conventions
|
||||||
|
|
||||||
|
- **Cards** route through `components/ui/app-card.tsx` (`AppCard` with
|
||||||
|
`level`/`layer`/`padding`). Use `level` to express hierarchy — do not restyle
|
||||||
|
cards ad hoc.
|
||||||
|
- **Buttons**: one `primary` (accent) per view; others `outline`/`ghost`.
|
||||||
|
- **Badges**: `success`/`warning` variants map to status tokens; `outline` for
|
||||||
|
neutral tags.
|
||||||
|
- **Inputs**: `--radius-nested`, `--border`, focus uses `--focus-ring`.
|
||||||
|
- **Inline feedback** (admin): `components/admin/admin-flash.tsx`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Do / Don't
|
||||||
|
|
||||||
|
**Do**
|
||||||
|
|
||||||
|
- Lead with imagery and whitespace; let type hierarchy do the work.
|
||||||
|
- Use the accent once per view, deliberately.
|
||||||
|
- Keep one radius language (24 / 16 / pill) and one shadow layer.
|
||||||
|
- Test every screen in light and dark, and Arabic RTL.
|
||||||
|
|
||||||
|
**Don't**
|
||||||
|
|
||||||
|
- Fill backgrounds with brand color or gradients.
|
||||||
|
- Add a second accent hue.
|
||||||
|
- Apply uppercase/letter-spacing to Arabic text.
|
||||||
|
- Stack shadows or mix arbitrary radii/greys.
|
||||||
|
- Put decoration where whitespace would do.
|
||||||
@@ -44,6 +44,8 @@
|
|||||||
},
|
},
|
||||||
"heroVisual": {
|
"heroVisual": {
|
||||||
"kicker": "مرحبا، أنا محمد",
|
"kicker": "مرحبا، أنا محمد",
|
||||||
|
"primaryCta": "شاهد الأعمال",
|
||||||
|
"secondaryCta": "تواصل معي",
|
||||||
"title": "مطوّر واجهات ويب\nومصمّم جرافيك",
|
"title": "مطوّر واجهات ويب\nومصمّم جرافيك",
|
||||||
"description": "أبني حلول رقمية لمشاكل حقيقية\nتبدو قوية وتعمل بكفاءة."
|
"description": "أبني حلول رقمية لمشاكل حقيقية\nتبدو قوية وتعمل بكفاءة."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,6 +44,8 @@
|
|||||||
},
|
},
|
||||||
"heroVisual": {
|
"heroVisual": {
|
||||||
"kicker": "Hi, ich bin Moh",
|
"kicker": "Hi, ich bin Moh",
|
||||||
|
"primaryCta": "Arbeiten ansehen",
|
||||||
|
"secondaryCta": "Kontakt aufnehmen",
|
||||||
"title": "frontend\ndeveloper\n& designer",
|
"title": "frontend\ndeveloper\n& designer",
|
||||||
"description": "Ich entwickle digitale Loesungen fuer echte Probleme,\ndie gut aussehen und sauber funktionieren."
|
"description": "Ich entwickle digitale Loesungen fuer echte Probleme,\ndie gut aussehen und sauber funktionieren."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,6 +44,8 @@
|
|||||||
},
|
},
|
||||||
"heroVisual": {
|
"heroVisual": {
|
||||||
"kicker": "Hi, I am Moh",
|
"kicker": "Hi, I am Moh",
|
||||||
|
"primaryCta": "View work",
|
||||||
|
"secondaryCta": "Get in touch",
|
||||||
"title": "frontend\ndeveloper\n& designer",
|
"title": "frontend\ndeveloper\n& designer",
|
||||||
"description": "I craft digital solutions for human problems\nthat look good and work well."
|
"description": "I craft digital solutions for human problems\nthat look good and work well."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,6 +74,23 @@ const config: Config = {
|
|||||||
ring: "hsl(var(--sidebar-ring))",
|
ring: "hsl(var(--sidebar-ring))",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["var(--font-museo)", "Arial", "Helvetica", "sans-serif"],
|
||||||
|
latin: ["var(--font-museo)", "Arial", "Helvetica", "sans-serif"],
|
||||||
|
arabic: ["var(--font-dubai)", "Tahoma", "Arial", "sans-serif"],
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
display: ["clamp(2.75rem, 8vw, 5.5rem)", { lineHeight: "0.95", letterSpacing: "-0.03em", fontWeight: "900" }],
|
||||||
|
h1: ["clamp(2.25rem, 5vw, 3.5rem)", { lineHeight: "1.05", letterSpacing: "-0.02em", fontWeight: "700" }],
|
||||||
|
h2: ["clamp(1.75rem, 3.5vw, 2.5rem)", { lineHeight: "1.1", letterSpacing: "-0.015em", fontWeight: "700" }],
|
||||||
|
h3: ["1.375rem", { lineHeight: "1.2", letterSpacing: "-0.01em", fontWeight: "700" }],
|
||||||
|
title: ["1.125rem", { lineHeight: "1.3", fontWeight: "500" }],
|
||||||
|
"body-lg": ["1.125rem", { lineHeight: "1.7", fontWeight: "400" }],
|
||||||
|
body: ["1rem", { lineHeight: "1.7", fontWeight: "400" }],
|
||||||
|
small: ["0.875rem", { lineHeight: "1.6" }],
|
||||||
|
caption: ["0.8125rem", { lineHeight: "1.5", fontWeight: "500" }],
|
||||||
|
overline: ["0.75rem", { lineHeight: "1.4", letterSpacing: "0.14em", fontWeight: "500" }],
|
||||||
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
sm: "var(--radius-nested)",
|
sm: "var(--radius-nested)",
|
||||||
md: "var(--radius-nested)",
|
md: "var(--radius-nested)",
|
||||||
@@ -106,6 +123,8 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
section: "var(--section-space)",
|
section: "var(--section-space)",
|
||||||
|
"section-sm": "3rem",
|
||||||
|
"section-lg": "8rem",
|
||||||
content: "var(--content-space)",
|
content: "var(--content-space)",
|
||||||
},
|
},
|
||||||
backdropBlur: {
|
backdropBlur: {
|
||||||
|
|||||||
Reference in New Issue
Block a user