refactor: refresh marketing theme with brand blue (#573)
This commit is contained in:
parent
da9ca0858f
commit
9d69aab8b5
@ -54,17 +54,17 @@ export default function DocCollectionCard({ collection, meta }: DocCollectionCar
|
||||
const tags = activeResource?.tags?.length ? activeResource.tags : collection.tags
|
||||
|
||||
return (
|
||||
<article className="group relative flex h-full flex-col overflow-hidden rounded-3xl border border-gray-200 bg-white shadow-sm transition duration-200 hover:-translate-y-1 hover:shadow-lg">
|
||||
<div className="relative h-32 w-full overflow-hidden bg-gradient-to-br from-purple-50 via-white to-purple-100">
|
||||
<div className="absolute inset-0 flex flex-col justify-between p-4">
|
||||
<article className="group relative flex h-full flex-col overflow-hidden rounded-2xl border border-brand-border bg-white shadow-[0_4px_20px_rgba(0,0,0,0.04)] transition duration-200 hover:-translate-y-1 hover:border-brand hover:shadow-[0_8px_28px_rgba(51,102,255,0.18)]">
|
||||
<div className="relative h-28 w-full bg-brand/10">
|
||||
<div className="absolute inset-0 flex flex-col justify-between p-4 text-xs text-brand-heading/70">
|
||||
<div>
|
||||
{meta && (
|
||||
<span className="inline-flex items-center rounded-full bg-white/80 px-3 py-1 text-xs font-medium text-purple-700 shadow-sm">
|
||||
<span className="inline-flex items-center rounded-full border border-brand-border bg-white/90 px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-brand">
|
||||
{meta}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-purple-500">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{updatedAt && (
|
||||
<span suppressHydrationWarning>
|
||||
Updated <ClientTime isoString={updatedAt} />
|
||||
@ -75,19 +75,19 @@ export default function DocCollectionCard({ collection, meta }: DocCollectionCar
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-4 p-6">
|
||||
<div className="flex flex-1 flex-col gap-4 p-6 text-brand-heading">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900 transition group-hover:text-purple-700">{collection.title}</h2>
|
||||
<p className="text-sm text-gray-600">{description}</p>
|
||||
<h2 className="text-lg font-semibold text-brand-navy transition group-hover:text-brand">{collection.title}</h2>
|
||||
<p className="text-sm text-brand-heading/80">{description}</p>
|
||||
</div>
|
||||
|
||||
{versions.length > 0 && (
|
||||
<label className="flex flex-col gap-1 text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||||
<label className="flex flex-col gap-1 text-xs font-semibold uppercase tracking-[0.2em] text-brand-heading/70">
|
||||
<span>Version</span>
|
||||
<select
|
||||
value={selectedVersionId}
|
||||
onChange={(event) => setSelectedVersionId(event.target.value)}
|
||||
className="rounded-full border border-gray-300 px-3 py-1 text-sm font-medium text-gray-700 focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500"
|
||||
className="rounded-full border border-brand-border px-3 py-1 text-sm font-medium text-brand-heading focus:border-brand focus:outline-none focus:ring-1 focus:ring-brand/30"
|
||||
>
|
||||
{versions.map((version) => (
|
||||
<option key={version.id} value={version.id}>
|
||||
@ -101,23 +101,23 @@ export default function DocCollectionCard({ collection, meta }: DocCollectionCar
|
||||
{tags && tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
<span key={tag} className="rounded-full bg-purple-50 px-3 py-1 text-xs font-medium text-purple-700">
|
||||
<span key={tag} className="rounded-full border border-brand-border bg-brand-surface px-3 py-1 text-xs font-medium text-brand-heading">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-auto flex items-center justify-between text-sm font-medium text-purple-600">
|
||||
<div className="flex flex-col">
|
||||
<div className="mt-auto flex items-center justify-between text-sm font-medium text-brand">
|
||||
<div className="flex flex-col text-brand-heading">
|
||||
<span>Open reader</span>
|
||||
{activeVersion && (
|
||||
<span className="text-xs text-gray-500">{resolveVersionLabel(activeVersion)}</span>
|
||||
<span className="text-xs text-brand-heading/70">{resolveVersionLabel(activeVersion)}</span>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
href={href}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-transparent bg-purple-50 px-3 py-1 text-purple-700 transition hover:border-purple-200 hover:bg-white"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-brand-border bg-brand-surface px-3 py-1 text-brand transition hover:border-brand hover:bg-white"
|
||||
>
|
||||
<span>Open</span>
|
||||
<ArrowUpRight className="h-4 w-4 transition group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
|
||||
@ -6,6 +6,7 @@ import type { DocCollection } from './types'
|
||||
import { getDocResources } from './resources.server'
|
||||
import { isFeatureEnabled } from '@lib/featureToggles'
|
||||
import DocCollectionCard from './DocCollectionCard'
|
||||
import Footer from '@components/Footer'
|
||||
|
||||
function formatMeta(resource: DocCollection) {
|
||||
const parts: string[] = []
|
||||
@ -36,20 +37,20 @@ export default async function DocsHome() {
|
||||
})
|
||||
|
||||
return (
|
||||
<main className="px-4 py-8 md:px-8">
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-8">
|
||||
<header className="space-y-3">
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-purple-600">Knowledge Base</p>
|
||||
<h1 className="text-3xl font-bold md:text-4xl">Documentation Library</h1>
|
||||
<p className="max-w-3xl text-sm text-gray-600 md:text-base">
|
||||
Browse curated implementation guides, architecture notes, and runbooks from dl.svc.plus. Select a resource to
|
||||
open the focused reading workspace.
|
||||
<main className="bg-brand-surface px-6 py-12 sm:px-8">
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-10">
|
||||
<header className="space-y-4 text-brand-heading">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.32em] text-brand">Knowledge Base</p>
|
||||
<h1 className="text-[32px] font-bold text-brand md:text-[36px]">Documentation Library</h1>
|
||||
<p className="max-w-3xl text-sm text-brand-heading/80 md:text-base">
|
||||
Browse curated implementation guides, architecture notes, and runbooks from dl.svc.plus. Select a resource to open
|
||||
the focused reading workspace.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
{resources.length === 0 ? (
|
||||
<div className="rounded-3xl border border-dashed border-gray-300 bg-white/70 p-10 text-center text-sm text-gray-500">
|
||||
<div className="rounded-2xl border border-dashed border-brand-border bg-white p-10 text-center text-sm text-brand-heading/70">
|
||||
Documentation resources are not available at the moment. Please check back later.
|
||||
</div>
|
||||
) : (
|
||||
@ -62,6 +63,7 @@ export default async function DocsHome() {
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@ -11,28 +11,28 @@
|
||||
--app-shell-nav-offset: 5.5rem;
|
||||
|
||||
/* Light theme defaults */
|
||||
--color-background: #f8fafc;
|
||||
--color-background-muted: #f1f5f9;
|
||||
--color-background: #f4f6fb;
|
||||
--color-background-muted: #e7ecf6;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-elevated: rgba(255, 255, 255, 0.92);
|
||||
--color-surface-translucent: rgba(255, 255, 255, 0.85);
|
||||
--color-surface-muted: #f1f5f9;
|
||||
--color-surface-hover: #f3f4f6;
|
||||
--color-surface-border: #e2e8f0;
|
||||
--color-surface-border-strong: #cbd5e1;
|
||||
--color-text: #111827;
|
||||
--color-heading: #0f172a;
|
||||
--color-text-muted: #4b5563;
|
||||
--color-text-subtle: #6b7280;
|
||||
--color-text-inverse: #f8fafc;
|
||||
--color-primary: #7c3aed;
|
||||
--color-primary-hover: #6d28d9;
|
||||
--color-primary-muted: #ede9fe;
|
||||
--color-primary-border: #c4b5fd;
|
||||
--color-primary-foreground: #f9fafb;
|
||||
--color-accent: #6366f1;
|
||||
--color-accent-muted: #e0e7ff;
|
||||
--color-accent-foreground: #312e81;
|
||||
--color-surface-elevated: rgba(255, 255, 255, 0.96);
|
||||
--color-surface-translucent: rgba(255, 255, 255, 0.88);
|
||||
--color-surface-muted: #f1f4fb;
|
||||
--color-surface-hover: #f0f4ff;
|
||||
--color-surface-border: #d6e0ff;
|
||||
--color-surface-border-strong: #b4c5ff;
|
||||
--color-text: #1e2e55;
|
||||
--color-heading: #2e3a59;
|
||||
--color-text-muted: #4a5672;
|
||||
--color-text-subtle: #61708c;
|
||||
--color-text-inverse: #f8fbff;
|
||||
--color-primary: #3366ff;
|
||||
--color-primary-hover: #4d7aff;
|
||||
--color-primary-muted: #f0f4ff;
|
||||
--color-primary-border: #d6e0ff;
|
||||
--color-primary-foreground: #ffffff;
|
||||
--color-accent: #254edb;
|
||||
--color-accent-muted: #e3e9ff;
|
||||
--color-accent-foreground: #162a6b;
|
||||
--color-success: #16a34a;
|
||||
--color-success-muted: #dcfce7;
|
||||
--color-success-foreground: #166534;
|
||||
@ -42,25 +42,25 @@
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-muted: #fee2e2;
|
||||
--color-danger-foreground: #7f1d1d;
|
||||
--color-info: #2563eb;
|
||||
--color-info-muted: #dbeafe;
|
||||
--color-info-foreground: #1d4ed8;
|
||||
--color-overlay: rgba(15, 23, 42, 0.45);
|
||||
--color-ring: #c4b5fd;
|
||||
--color-focus: rgba(124, 58, 237, 0.35);
|
||||
--color-info: #3366ff;
|
||||
--color-info-muted: #f0f4ff;
|
||||
--color-info-foreground: #254edb;
|
||||
--color-overlay: rgba(30, 46, 85, 0.45);
|
||||
--color-ring: #d6e0ff;
|
||||
--color-focus: rgba(51, 102, 255, 0.35);
|
||||
--color-divider: rgba(15, 23, 42, 0.08);
|
||||
--color-badge-surface: #e5e7eb;
|
||||
--color-badge-muted: #f3f4f6;
|
||||
--color-badge-foreground: #1f2937;
|
||||
|
||||
--gradient-app-from: #ede9fe;
|
||||
--gradient-app-via: #f5f3ff;
|
||||
--gradient-app-to: #e0f2fe;
|
||||
--gradient-primary-from: #7c3aed;
|
||||
--gradient-primary-to: #4c1d95;
|
||||
--gradient-app-from: #f5f8ff;
|
||||
--gradient-app-via: #eef3ff;
|
||||
--gradient-app-to: #f4f9ff;
|
||||
--gradient-primary-from: #3366ff;
|
||||
--gradient-primary-to: #254edb;
|
||||
|
||||
--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.08), 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--shadow-md: 0 12px 32px rgba(15, 23, 42, 0.12);
|
||||
--shadow-sm: 0 1px 3px rgba(30, 46, 85, 0.08), 0 1px 2px rgba(30, 46, 85, 0.04);
|
||||
--shadow-md: 0 12px 32px rgba(30, 46, 85, 0.12);
|
||||
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.5rem;
|
||||
|
||||
@ -1,30 +1,22 @@
|
||||
import type { CommonHomeLayoutConfig } from '@templates/layouts/commonHome'
|
||||
|
||||
export const defaultHomeLayoutConfig: CommonHomeLayoutConfig = {
|
||||
rootClassName:
|
||||
'relative min-h-screen bg-gradient-to-b from-[#EAF3FF] to-[#CFE4FF] text-slate-900 antialiased',
|
||||
rootClassName: 'relative min-h-screen bg-brand-surface text-brand-navy antialiased',
|
||||
hero: {
|
||||
sectionClassName: 'relative isolate overflow-hidden py-24',
|
||||
overlays: [
|
||||
'absolute inset-0 bg-gradient-to-b from-white/90 via-white/70 to-transparent',
|
||||
'absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.18),_transparent_65%)]',
|
||||
],
|
||||
containerClassName: 'relative px-6 sm:px-8',
|
||||
contentClassName: 'mx-auto max-w-7xl',
|
||||
sectionClassName: 'relative isolate overflow-hidden py-24 sm:py-28',
|
||||
overlays: ['absolute inset-0 bg-gradient-to-b from-white via-white/80 to-transparent'],
|
||||
containerClassName: 'relative px-8',
|
||||
contentClassName: 'mx-auto w-full max-w-6xl',
|
||||
slot: {
|
||||
key: 'ProductMatrix',
|
||||
},
|
||||
},
|
||||
content: {
|
||||
sectionClassName: 'relative isolate py-20',
|
||||
overlays: [
|
||||
'absolute inset-x-0 top-0 h-px bg-blue-100/70',
|
||||
'absolute inset-0 bg-[radial-gradient(circle_at_bottom,_rgba(59,130,246,0.12),_transparent_70%)]',
|
||||
],
|
||||
containerClassName: 'relative px-6 sm:px-8',
|
||||
contentClassName: 'mx-auto max-w-7xl',
|
||||
gridClassName:
|
||||
'grid gap-10 lg:grid-cols-[minmax(0,1fr)_360px] lg:items-start lg:gap-12',
|
||||
sectionClassName: 'relative isolate py-20 sm:py-24',
|
||||
overlays: ['absolute inset-x-0 top-0 h-px bg-brand-border/70'],
|
||||
containerClassName: 'relative px-8',
|
||||
contentClassName: 'mx-auto w-full max-w-6xl',
|
||||
gridClassName: 'grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px] lg:items-start lg:gap-12',
|
||||
slots: [
|
||||
{ key: 'ArticleFeed' },
|
||||
{ key: 'Sidebar' },
|
||||
|
||||
@ -5,23 +5,58 @@ import { translations } from '../i18n/translations'
|
||||
export default function Footer() {
|
||||
const { language } = useLanguage()
|
||||
const t = translations[language].footerLinks
|
||||
const [privacy, terms, contact] = t
|
||||
|
||||
return (
|
||||
<footer className="bg-gray-100 text-gray-900 py-12 px-4">
|
||||
<div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<div className="flex gap-6 text-sm">
|
||||
<a href="#privacy" className="hover:text-purple-600">{t[0]}</a>
|
||||
<a href="#terms" className="hover:text-purple-600">{t[1]}</a>
|
||||
<a href="#contact" className="hover:text-purple-600">{t[2]}</a>
|
||||
<footer className="bg-brand-navy text-white">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-10 px-8 py-14">
|
||||
<div className="flex flex-col gap-8 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.32em] text-brand-light/90">CloudNative Suite</p>
|
||||
<p className="max-w-lg text-sm text-white/70">
|
||||
Unified observability, DevOps, and AI workflows for enterprise cloud native teams.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 text-sm text-white/80">
|
||||
<a href="#privacy" className="transition hover:text-brand-light">
|
||||
{privacy}
|
||||
</a>
|
||||
<a href="#terms" className="transition hover:text-brand-light">
|
||||
{terms}
|
||||
</a>
|
||||
<a href="#contact" className="transition hover:text-brand-light">
|
||||
{contact}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 text-sm">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold text-white">GitHub</p>
|
||||
<a
|
||||
href="https://github.com/CloudNativeSuite"
|
||||
className="inline-flex items-center gap-2 text-white/80 transition hover:text-brand-light"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span>github.com/CloudNativeSuite</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold text-white">公众号</p>
|
||||
<span className="text-white/80">CloudNative Suite 官方资讯</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold text-white">Contact</p>
|
||||
<a href="mailto:contact@cloudnativesuite.io" className="text-white/80 transition hover:text-brand-light">
|
||||
contact@cloudnativesuite.io
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 text-xl">
|
||||
<a href="#" title="Twitter">🐦</a>
|
||||
<a href="#" title="Email">📧</a>
|
||||
<div className="flex flex-col gap-3 border-t border-white/10 pt-6 text-sm text-white/60 sm:flex-row sm:items-center sm:justify-between">
|
||||
<span>© 2025 CloudNative Suite. All rights reserved.</span>
|
||||
<span>Build with confidence in the cloud native era.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center text-gray-500 text-sm mt-6">
|
||||
© 2025 CloudNative Suite. All rights reserved.
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -215,7 +215,7 @@ export default function Navbar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium uppercase text-purple-600">
|
||||
<span className="rounded-full bg-brand-surface px-2 py-0.5 text-xs font-medium uppercase text-brand">
|
||||
{channelLabels.badges[previewChannel]}
|
||||
</span>
|
||||
)
|
||||
@ -281,8 +281,8 @@ export default function Navbar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav ref={navRef} className="fixed top-0 z-50 w-full border-b border-gray-200 bg-white/80 backdrop-blur">
|
||||
<div className="mx-auto flex max-w-7xl flex-col px-4">
|
||||
<nav ref={navRef} className="fixed top-0 z-50 w-full border-b border-brand-border/60 bg-white/85 backdrop-blur">
|
||||
<div className="mx-auto flex max-w-7xl flex-col px-6 sm:px-8">
|
||||
<div className="flex items-center gap-6 py-4">
|
||||
<div className="flex flex-1 items-center gap-8">
|
||||
<Link href="/" className="flex items-center gap-2 text-xl font-semibold text-gray-900">
|
||||
@ -296,18 +296,18 @@ export default function Navbar() {
|
||||
/>
|
||||
CloudNative Suite
|
||||
</Link>
|
||||
<div className="hidden lg:flex items-center gap-6 text-sm font-medium text-gray-700">
|
||||
<div className="hidden lg:flex items-center gap-6 text-sm font-medium text-brand-heading">
|
||||
{mainLinks.map((link) => (
|
||||
<Link key={link.key} href={link.href} className="transition hover:text-purple-600">
|
||||
<Link key={link.key} href={link.href} className="transition hover:text-brand">
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
{serviceItems.length > 0 ? (
|
||||
<div className="group relative">
|
||||
<button className="flex items-center gap-1 transition hover:text-purple-600">
|
||||
<button className="flex items-center gap-1 transition hover:text-brand">
|
||||
<span>{labels.moreServices}</span>
|
||||
<svg
|
||||
className="h-4 w-4 text-gray-500 transition group-hover:text-purple-600"
|
||||
className="h-4 w-4 text-brand-heading/60 transition group-hover:text-brand"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
@ -317,7 +317,7 @@ export default function Navbar() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="pointer-events-none absolute left-0 top-full hidden min-w-[200px] translate-y-1 rounded-lg border border-gray-200 bg-white py-2 text-sm text-gray-700 opacity-0 shadow-lg transition-all duration-200 group-hover:pointer-events-auto group-hover:block group-hover:translate-y-2 group-hover:opacity-100">
|
||||
<div className="pointer-events-none absolute left-0 top-full hidden min-w-[200px] translate-y-1 rounded-lg border border-brand-border bg-white py-2 text-sm text-brand-heading opacity-0 shadow-[0_4px_20px_rgba(0,0,0,0.08)] transition-all duration-200 group-hover:pointer-events-auto group-hover:block group-hover:translate-y-2 group-hover:opacity-100">
|
||||
{serviceItems.map((child) => {
|
||||
const isExternal = child.href.startsWith('http')
|
||||
if (isExternal) {
|
||||
@ -325,7 +325,7 @@ export default function Navbar() {
|
||||
<a
|
||||
key={child.key}
|
||||
href={child.href}
|
||||
className="flex items-center justify-between gap-2 px-4 py-2 transition hover:bg-gray-100 hover:text-purple-600"
|
||||
className="flex items-center justify-between gap-2 px-4 py-2 transition hover:bg-brand-surface hover:text-brand"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@ -339,7 +339,7 @@ export default function Navbar() {
|
||||
<Link
|
||||
key={child.key}
|
||||
href={child.href}
|
||||
className="flex items-center justify-between gap-2 px-4 py-2 transition hover:bg-gray-100 hover:text-purple-600"
|
||||
className="flex items-center justify-between gap-2 px-4 py-2 transition hover:bg-brand-surface hover:text-brand"
|
||||
>
|
||||
<span>{child.label}</span>
|
||||
{getPreviewBadge(child.channels)}
|
||||
@ -359,11 +359,11 @@ export default function Navbar() {
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
placeholder={labels.searchPlaceholder}
|
||||
className="w-full rounded-full border border-gray-200 bg-gray-50 py-2 pl-4 pr-10 text-sm text-gray-900 transition focus:border-purple-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-purple-100"
|
||||
className="w-full rounded-full border border-brand-border bg-brand-surface/60 py-2 pl-4 pr-10 text-sm text-brand-heading transition focus:border-brand focus:bg-white focus:outline-none focus:ring-2 focus:ring-brand/20"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="absolute right-2 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full bg-purple-600 text-white transition hover:bg-purple-500"
|
||||
className="absolute right-2 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full bg-brand text-white transition hover:bg-brand-light"
|
||||
aria-label="Ask AI"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
@ -374,22 +374,22 @@ export default function Navbar() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAccountMenuOpen((prev) => !prev)}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full bg-purple-600 text-sm font-semibold text-white shadow-sm transition hover:bg-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-200 focus:ring-offset-2"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full bg-brand text-sm font-semibold text-white shadow-[0_4px_12px_rgba(51,102,255,0.3)] transition hover:bg-brand-light focus:outline-none focus:ring-2 focus:ring-brand/30 focus:ring-offset-2"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={accountMenuOpen}
|
||||
>
|
||||
{accountInitial}
|
||||
</button>
|
||||
{accountMenuOpen ? (
|
||||
<div className="absolute right-0 mt-2 w-56 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg">
|
||||
<div className="border-b border-gray-100 bg-purple-50/60 px-4 py-3">
|
||||
<p className="text-sm font-semibold text-gray-900">{user.username}</p>
|
||||
<p className="text-xs text-gray-500">{user.email}</p>
|
||||
<div className="absolute right-0 mt-2 w-56 overflow-hidden rounded-xl border border-brand-border bg-white shadow-[0_4px_20px_rgba(0,0,0,0.08)]">
|
||||
<div className="border-b border-brand-border/60 bg-brand-surface px-4 py-3">
|
||||
<p className="text-sm font-semibold text-brand-heading">{user.username}</p>
|
||||
<p className="text-xs text-brand-heading/70">{user.email}</p>
|
||||
</div>
|
||||
<div className="py-1 text-sm text-gray-700">
|
||||
<div className="py-1 text-sm text-brand-heading">
|
||||
<Link
|
||||
href="/panel"
|
||||
className="block px-4 py-2 hover:bg-gray-100"
|
||||
className="block px-4 py-2 transition hover:bg-brand-surface"
|
||||
onClick={() => setAccountMenuOpen(false)}
|
||||
>
|
||||
{accountCopy.userCenter}
|
||||
@ -406,14 +406,14 @@ export default function Navbar() {
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3 text-sm font-medium text-gray-700">
|
||||
<Link href="/login" className="transition hover:text-purple-600">
|
||||
<div className="flex items-center gap-3 text-sm font-medium text-brand-heading">
|
||||
<Link href="/login" className="transition hover:text-brand">
|
||||
{nav.account.login}
|
||||
</Link>
|
||||
<span className="h-3 w-px bg-gray-300" aria-hidden="true" />
|
||||
<Link
|
||||
href="/register"
|
||||
className="rounded-full border border-purple-100 px-4 py-1.5 text-purple-600 transition hover:border-purple-200 hover:bg-purple-50"
|
||||
className="rounded-full border border-brand-border px-4 py-1.5 text-brand transition hover:border-brand hover:bg-brand-surface"
|
||||
>
|
||||
{nav.account.register}
|
||||
</Link>
|
||||
@ -456,11 +456,11 @@ export default function Navbar() {
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
placeholder={labels.searchPlaceholder}
|
||||
className="w-full rounded-full border border-gray-200 bg-gray-50 py-2 pl-4 pr-10 text-sm text-gray-900 transition focus:border-purple-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-purple-100"
|
||||
className="w-full rounded-full border border-brand-border bg-brand-surface/60 py-2 pl-4 pr-10 text-sm text-brand-heading transition focus:border-brand focus:bg-white focus:outline-none focus:ring-2 focus:ring-brand/20"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="absolute right-2 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-purple-600 text-white transition hover:bg-purple-500"
|
||||
className="absolute right-2 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-brand text-white transition hover:bg-brand-light"
|
||||
aria-label="Ask AI"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
@ -532,26 +532,26 @@ export default function Navbar() {
|
||||
) : null}
|
||||
</div>
|
||||
{user ? (
|
||||
<div className="rounded-xl bg-purple-50 p-4 text-purple-700">
|
||||
<div className="rounded-xl border border-brand-border bg-white p-4 text-brand-heading shadow-[0_4px_20px_rgba(0,0,0,0.04)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-purple-600 text-sm font-semibold text-white">
|
||||
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-brand text-sm font-semibold text-white">
|
||||
{accountInitial}
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">{user.username}</p>
|
||||
<p className="text-xs text-purple-300">{user.email}</p>
|
||||
<p className="text-xs text-brand-heading/60">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href="/panel"
|
||||
className="mt-3 inline-flex items-center justify-center rounded-lg bg-white/80 px-3 py-1.5 text-xs font-semibold text-purple-600 transition hover:bg-white"
|
||||
className="mt-3 inline-flex items-center justify-center rounded-lg border border-brand-border bg-white px-3 py-1.5 text-xs font-semibold text-brand transition hover:border-brand hover:text-brand-light"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{accountCopy.userCenter}
|
||||
</Link>
|
||||
<Link
|
||||
href="/logout"
|
||||
className="mt-3 inline-flex items-center justify-center rounded-lg border border-purple-200 px-3 py-1.5 text-xs font-semibold text-purple-600 transition hover:border-purple-300 hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-purple-200 focus:ring-offset-2"
|
||||
className="mt-3 inline-flex items-center justify-center rounded-lg border border-brand-border px-3 py-1.5 text-xs font-semibold text-brand transition hover:border-brand hover:bg-brand-surface focus:outline-none focus:ring-2 focus:ring-brand/30 focus:ring-offset-2"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{accountCopy.logout}
|
||||
@ -565,7 +565,7 @@ export default function Navbar() {
|
||||
<span className="h-3 w-px bg-gray-300" aria-hidden="true" />
|
||||
<Link
|
||||
href="/register"
|
||||
className="rounded-full border border-purple-100 px-4 py-1.5 text-purple-600 transition hover:border-purple-200 hover:bg-purple-50"
|
||||
className="rounded-full border border-brand-border px-4 py-1.5 text-brand transition hover:border-brand hover:bg-brand-surface"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
{nav.account.register}
|
||||
|
||||
@ -48,52 +48,55 @@ export default function ArticleFeedClient({ posts }: ArticleFeedClientProps) {
|
||||
)
|
||||
|
||||
return (
|
||||
<section className="space-y-8 text-slate-900">
|
||||
<header className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-blue-600">{articleFeed.eyebrow}</p>
|
||||
<h2 className="text-2xl font-semibold text-slate-900 sm:text-3xl">{articleFeed.title}</h2>
|
||||
<section className="space-y-8 text-brand-heading">
|
||||
<header className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.32em] text-brand">{articleFeed.eyebrow}</p>
|
||||
<h2 className="text-2xl font-semibold text-brand-navy sm:text-[26px]">{articleFeed.title}</h2>
|
||||
</div>
|
||||
<Link href="/docs" className="text-sm font-medium text-blue-600 transition hover:text-blue-700">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="text-sm font-medium text-brand transition hover:text-brand-light"
|
||||
>
|
||||
{articleFeed.viewAll}
|
||||
</Link>
|
||||
</header>
|
||||
<div className="space-y-8">
|
||||
{!mappedPosts.length ? (
|
||||
<p className="rounded-3xl border border-dashed border-blue-200/70 bg-white/80 p-8 text-center text-sm text-slate-700">
|
||||
<p className="rounded-2xl border border-dashed border-brand-border bg-white p-8 text-center text-sm text-brand-heading/70">
|
||||
{articleFeed.empty}
|
||||
</p>
|
||||
) : null}
|
||||
{mappedPosts.map((post) => (
|
||||
<article
|
||||
key={post.slug}
|
||||
className="group rounded-3xl border border-blue-100 bg-white p-6 text-slate-800 shadow-lg shadow-blue-100/60 transition hover:-translate-y-1 hover:border-blue-200 hover:bg-blue-50/70 hover:shadow-blue-200 sm:p-8"
|
||||
className="group rounded-2xl border border-brand-border bg-white p-6 text-brand-heading shadow-[0_4px_20px_rgba(0,0,0,0.04)] transition hover:-translate-y-1 hover:border-brand hover:shadow-[0_6px_24px_rgba(51,102,255,0.2)] sm:p-8"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-slate-500 sm:text-sm">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-brand-heading/60 sm:text-sm">
|
||||
{post.formattedDate ? <span>{post.formattedDate}</span> : null}
|
||||
{post.author ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="hidden h-1 w-1 rounded-full bg-slate-300 sm:inline" aria-hidden />
|
||||
<span className="hidden h-1 w-1 rounded-full bg-brand-border sm:inline" aria-hidden />
|
||||
<span>{post.author}</span>
|
||||
</span>
|
||||
) : null}
|
||||
{post.readingTime ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="hidden h-1 w-1 rounded-full bg-slate-300 sm:inline" aria-hidden />
|
||||
<span className="hidden h-1 w-1 rounded-full bg-brand-border sm:inline" aria-hidden />
|
||||
<span>{post.readingTime}</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<h3 className="mt-4 text-xl font-semibold text-slate-900 transition group-hover:text-blue-700 sm:text-2xl">
|
||||
<h3 className="mt-4 text-lg font-medium text-brand-heading transition group-hover:text-brand sm:text-xl">
|
||||
{post.title}
|
||||
</h3>
|
||||
{post.excerpt ? <p className="mt-3 text-sm text-slate-700 sm:text-base">{post.excerpt}</p> : null}
|
||||
{post.excerpt ? <p className="mt-3 text-sm text-brand-heading/80 sm:text-base">{post.excerpt}</p> : null}
|
||||
{post.tags.length ? (
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{post.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-slate-700"
|
||||
className="inline-flex items-center rounded-full border border-brand-border bg-brand-surface px-3 py-1 text-xs font-medium text-brand-heading"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
|
||||
@ -103,7 +103,7 @@ function QrPreview({ item, qrAltSuffix }: QrPreviewProps) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="relative overflow-hidden rounded-2xl border border-blue-100 bg-white p-3 shadow-inner shadow-blue-100/60">
|
||||
<div className="relative overflow-hidden rounded-2xl border border-brand-border bg-white p-3 shadow-[0_4px_20px_rgba(0,0,0,0.04)]">
|
||||
<div className="relative aspect-square w-full">
|
||||
{item.qrImage ? (
|
||||
<Image
|
||||
@ -124,15 +124,15 @@ function QrPreview({ item, qrAltSuffix }: QrPreviewProps) {
|
||||
aria-hidden
|
||||
>
|
||||
{pattern!.flat().map((isFilled, index) => (
|
||||
<span key={`${item.slug}-${index}`} className={`block ${isFilled ? 'bg-slate-900' : 'bg-slate-50'}`} />
|
||||
<span key={`${item.slug}-${index}`} className={`block ${isFilled ? 'bg-brand-navy' : 'bg-brand-surface'}`} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-semibold text-slate-900">{item.title}</p>
|
||||
{item.description ? <p className="text-xs text-slate-600">{item.description}</p> : null}
|
||||
<p className="text-sm font-semibold text-brand-navy">{item.title}</p>
|
||||
{item.description ? <p className="text-xs text-brand-heading/70">{item.description}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -141,23 +141,23 @@ function QrPreview({ item, qrAltSuffix }: QrPreviewProps) {
|
||||
function InfoCard({ item }: { item: ContactItemContent }) {
|
||||
const Icon = getIcon(item.icon)
|
||||
return (
|
||||
<div className="flex items-start gap-3 rounded-2xl border border-blue-100 bg-blue-50/80 p-4">
|
||||
<div className="mt-1 rounded-full bg-blue-100 p-2 text-blue-600">
|
||||
<div className="flex items-start gap-3 rounded-2xl border border-brand-border bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.04)]">
|
||||
<div className="mt-1 rounded-full bg-brand-surface p-2 text-brand">
|
||||
<Icon className="h-5 w-5" aria-hidden />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-slate-900">{item.title}</p>
|
||||
{item.description ? <p className="mt-1 text-xs text-slate-600">{item.description}</p> : null}
|
||||
<p className="text-sm font-semibold text-brand-navy">{item.title}</p>
|
||||
{item.description ? <p className="mt-1 text-xs text-brand-heading/70">{item.description}</p> : null}
|
||||
{item.bodyHtml ? (
|
||||
<div
|
||||
className="prose prose-sm mt-2 max-w-none text-slate-700"
|
||||
className="prose prose-sm mt-2 max-w-none text-brand-heading/80 [&_a]:text-brand [&_a]:no-underline hover:[&_a]:underline"
|
||||
dangerouslySetInnerHTML={{ __html: item.bodyHtml }}
|
||||
/>
|
||||
) : null}
|
||||
{item.ctaLabel && item.ctaHref ? (
|
||||
<Link
|
||||
href={item.ctaHref}
|
||||
className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-blue-600 transition hover:text-blue-700"
|
||||
className="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-brand transition hover:text-brand-light"
|
||||
>
|
||||
{item.ctaLabel}
|
||||
<ChevronRight className="h-4 w-4" aria-hidden />
|
||||
@ -208,7 +208,7 @@ export default function ContactPanelClient({ panel, className }: ContactPanelCli
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCollapsed(false)}
|
||||
className="group inline-flex items-center gap-2 rounded-full border border-blue-300 bg-white px-4 py-2 text-sm font-semibold text-blue-700 shadow-sm shadow-blue-200/70"
|
||||
className="group inline-flex items-center gap-2 rounded-full border border-brand-border bg-white px-4 py-2 text-sm font-semibold text-brand shadow-[0_4px_20px_rgba(0,0,0,0.04)] transition hover:border-brand hover:text-brand-light"
|
||||
aria-label={contactMarketing.expandLabel}
|
||||
>
|
||||
<span>{contactMarketing.buttonLabel}</span>
|
||||
@ -216,19 +216,19 @@ export default function ContactPanelClient({ panel, className }: ContactPanelCli
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<section className="relative flex max-h-full min-h-0 flex-col overflow-hidden rounded-3xl border border-blue-100 bg-gradient-to-b from-white via-white to-blue-50 shadow-xl shadow-blue-200/60">
|
||||
<div className="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-blue-400 via-blue-500 to-indigo-400" aria-hidden />
|
||||
<div className="flex items-start justify-between gap-3 px-5 pt-5">
|
||||
<section className="relative flex max-h-full min-h-0 flex-col overflow-hidden rounded-2xl border border-brand-border bg-white shadow-[0_4px_20px_rgba(0,0,0,0.04)]">
|
||||
<div className="absolute inset-x-0 top-0 h-1 bg-brand" aria-hidden />
|
||||
<div className="flex items-start justify-between gap-3 px-6 pt-6">
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-blue-600">{localizedPanel.title}</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.32em] text-brand">{localizedPanel.title}</p>
|
||||
{localizedPanel.subtitle ? (
|
||||
<p className="mt-1 text-xs text-slate-600">{localizedPanel.subtitle}</p>
|
||||
<p className="mt-1 text-xs text-brand-heading/70">{localizedPanel.subtitle}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCollapsed(true)}
|
||||
className="rounded-full border border-blue-100 bg-white/80 p-1 text-slate-400 transition hover:text-slate-600"
|
||||
className="rounded-full border border-brand-border bg-white p-1 text-brand-heading/60 transition hover:border-brand hover:text-brand"
|
||||
aria-label={contactMarketing.collapseLabel}
|
||||
>
|
||||
<X className="h-4 w-4" aria-hidden />
|
||||
@ -237,11 +237,11 @@ export default function ContactPanelClient({ panel, className }: ContactPanelCli
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{localizedPanel.bodyHtml ? (
|
||||
<div
|
||||
className="prose prose-sm px-5 pt-3 text-slate-700"
|
||||
className="prose prose-sm px-6 pt-4 text-brand-heading/80"
|
||||
dangerouslySetInnerHTML={{ __html: localizedPanel.bodyHtml }}
|
||||
/>
|
||||
) : null}
|
||||
<div className="grid gap-4 px-5 pb-5 pt-4 sm:grid-cols-2">
|
||||
<div className="grid gap-4 px-6 pb-6 pt-4 sm:grid-cols-2">
|
||||
{localizedPanel.items.map((item) => {
|
||||
if (item.type === 'qr') {
|
||||
return (
|
||||
|
||||
@ -11,9 +11,9 @@ export default async function ProductMatrix() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_360px] lg:items-start lg:gap-12">
|
||||
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px] lg:items-start lg:gap-12">
|
||||
<ProductMatrixClient solutions={solutions} />
|
||||
<ContactPanel className="w-full lg:sticky lg:top-16 lg:h-fit lg:w-[360px] lg:self-start" />
|
||||
<ContactPanel className="w-full lg:sticky lg:top-0 lg:h-fit lg:w-[360px] lg:self-start" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -8,9 +8,6 @@ import type { HeroSolution } from '@cms/content'
|
||||
import { useLanguage } from '@i18n/LanguageProvider'
|
||||
import { translations } from '@i18n/translations'
|
||||
|
||||
const heroBackgroundOverlay =
|
||||
'absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.18),_transparent_70%)]'
|
||||
|
||||
type ProductMatrixClientProps = {
|
||||
solutions: HeroSolution[]
|
||||
}
|
||||
@ -79,39 +76,37 @@ export default function ProductMatrixClient({ solutions }: ProductMatrixClientPr
|
||||
].filter(Boolean) as Array<{ label: string; href: string; variant: 'primary' | 'secondary' | 'ghost' }>
|
||||
|
||||
return (
|
||||
<section className="space-y-10">
|
||||
<div className="relative overflow-hidden rounded-[2.5rem] border border-blue-100 bg-white p-8 text-slate-900 shadow-2xl shadow-blue-200/60 lg:p-12">
|
||||
<div className={heroBackgroundOverlay} aria-hidden />
|
||||
<div className="absolute inset-0 -z-10 bg-[linear-gradient(135deg,_rgba(234,243,255,0.95),_rgba(207,228,255,0.65))]" aria-hidden />
|
||||
<div className="relative grid gap-10 lg:grid-cols-[minmax(0,1.45fr)_minmax(0,1fr)]">
|
||||
<div className="space-y-6">
|
||||
<header className="space-y-3">
|
||||
<span className="inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-4 py-1 text-xs font-semibold uppercase tracking-[0.45em] text-blue-700">
|
||||
<section className="space-y-8">
|
||||
<div className="rounded-2xl border border-brand-border bg-white p-8 text-brand-heading shadow-[0_4px_20px_rgba(0,0,0,0.04)] sm:p-10 lg:p-12">
|
||||
<div className="grid gap-8 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)] lg:gap-10">
|
||||
<div className="space-y-8">
|
||||
<header className="space-y-4">
|
||||
<span className="inline-flex items-center rounded-full border border-brand-border bg-brand-surface px-4 py-1 text-xs font-semibold uppercase tracking-[0.4em] text-brand">
|
||||
{productMatrix.badge}
|
||||
</span>
|
||||
<h1 className="text-3xl font-extrabold leading-tight text-slate-900 sm:text-4xl lg:text-5xl">
|
||||
<h1 className="text-[30px] font-bold leading-tight text-brand sm:text-[34px] md:text-[36px]">
|
||||
{productMatrix.title}
|
||||
</h1>
|
||||
<p className="text-sm text-slate-700 sm:text-base lg:text-lg">
|
||||
<p className="text-sm text-brand-heading/80 sm:text-base lg:text-lg">
|
||||
{productMatrix.description}
|
||||
</p>
|
||||
</header>
|
||||
<ul className="grid gap-3 text-sm text-slate-700 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<ul className="grid gap-3 text-sm text-brand-heading/80 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{overviewHighlights.map((highlight) => (
|
||||
<li
|
||||
key={highlight}
|
||||
className="flex items-start gap-3 rounded-2xl border border-blue-100 bg-blue-50/60 p-3"
|
||||
className="flex items-start gap-3 rounded-2xl border border-brand-border bg-white p-3 shadow-[0_4px_20px_rgba(0,0,0,0.04)]"
|
||||
>
|
||||
<span className="mt-1 h-2 w-2 flex-shrink-0 rounded-full bg-blue-500" aria-hidden />
|
||||
<span className="mt-1 h-2 w-2 flex-shrink-0 rounded-full bg-brand" aria-hidden />
|
||||
<span>{highlight}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="rounded-3xl border border-blue-100 bg-blue-50/80 p-3 sm:p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-blue-700">
|
||||
<div className="rounded-2xl border border-brand-border bg-brand-surface p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-brand">
|
||||
{productMatrix.topicsLabel}
|
||||
</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{localizedSolutions.map((solution, index) => {
|
||||
const isActive = index === activeIndex
|
||||
return (
|
||||
@ -120,17 +115,17 @@ export default function ProductMatrixClient({ solutions }: ProductMatrixClientPr
|
||||
type="button"
|
||||
onClick={() => setActiveIndex(index)}
|
||||
className={clsx(
|
||||
'flex min-w-[9rem] flex-1 items-center justify-between rounded-2xl border px-4 py-3 text-left text-sm font-semibold transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-300',
|
||||
'flex min-w-[9rem] flex-1 items-center justify-between rounded-2xl border px-4 py-3 text-left text-sm font-semibold transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand/40',
|
||||
isActive
|
||||
? 'border-blue-400 bg-blue-100 text-slate-900 shadow-lg shadow-blue-300/40'
|
||||
: 'border-blue-100 bg-white text-slate-700 hover:border-blue-200 hover:bg-blue-50',
|
||||
? 'border-brand bg-brand/10 text-brand-heading shadow-[0_4px_20px_rgba(0,0,0,0.04)]'
|
||||
: 'border-brand-border bg-white text-brand-heading/80 hover:border-brand hover:bg-brand-surface',
|
||||
)}
|
||||
>
|
||||
<span className="flex-1">{solution.title}</span>
|
||||
<span className="flex-1 text-brand-heading">{solution.title}</span>
|
||||
<span
|
||||
className={clsx(
|
||||
'ml-2 text-xs font-medium transition',
|
||||
isActive ? 'text-slate-800/80' : 'text-slate-500',
|
||||
isActive ? 'text-brand-heading/70' : 'text-brand-heading/60',
|
||||
)}
|
||||
>
|
||||
{solution.tagline}
|
||||
@ -141,33 +136,36 @@ export default function ProductMatrixClient({ solutions }: ProductMatrixClientPr
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 rounded-3xl border border-blue-100 bg-white/90 p-6 shadow-inner shadow-blue-100/80 backdrop-blur-sm lg:p-8">
|
||||
<div className="flex flex-col gap-6 rounded-2xl border border-brand-border bg-white p-6 shadow-[0_4px_20px_rgba(0,0,0,0.04)] lg:p-8">
|
||||
<div className="space-y-4">
|
||||
{activeSolution.tagline ? (
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.35em] text-blue-700">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.35em] text-brand">
|
||||
{activeSolution.tagline}
|
||||
</p>
|
||||
) : null}
|
||||
<h2 className="text-2xl font-semibold text-slate-900 sm:text-3xl">{activeSolution.title}</h2>
|
||||
<h2 className="text-2xl font-semibold text-brand-navy sm:text-[26px]">{activeSolution.title}</h2>
|
||||
{activeSolution.description ? (
|
||||
<p className="text-sm text-slate-700 sm:text-base">{activeSolution.description}</p>
|
||||
<p className="text-sm text-brand-heading/80 sm:text-base">{activeSolution.description}</p>
|
||||
) : null}
|
||||
{activeSolution.bodyHtml ? (
|
||||
<div
|
||||
className="prose max-w-none text-sm text-slate-800 [&_strong]:text-slate-900"
|
||||
className="prose max-w-none text-sm text-brand-heading/90 [&_strong]:text-brand-navy"
|
||||
dangerouslySetInnerHTML={{ __html: activeSolution.bodyHtml }}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{activeSolution.features.length ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.35em] text-blue-700">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.35em] text-brand">
|
||||
{productMatrix.capabilitiesLabel}
|
||||
</p>
|
||||
<ul className="grid gap-2 text-sm text-slate-700 sm:grid-cols-2">
|
||||
<ul className="grid gap-2 text-sm text-brand-heading/80 sm:grid-cols-2">
|
||||
{activeSolution.features.map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-3 rounded-2xl border border-blue-100 bg-blue-50/60 p-3">
|
||||
<span className="mt-1 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-blue-500" aria-hidden />
|
||||
<li
|
||||
key={feature}
|
||||
className="flex items-start gap-3 rounded-2xl border border-brand-border bg-white p-3 shadow-[0_4px_20px_rgba(0,0,0,0.04)]"
|
||||
>
|
||||
<span className="mt-1 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand" aria-hidden />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
@ -182,12 +180,12 @@ export default function ProductMatrixClient({ solutions }: ProductMatrixClientPr
|
||||
prefetch={false}
|
||||
href={href}
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center rounded-full px-5 py-2 text-sm font-semibold transition',
|
||||
'inline-flex items-center justify-center rounded-full px-5 py-2 text-sm font-semibold transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand/30',
|
||||
variant === 'primary'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-300/50 hover:bg-blue-700'
|
||||
? 'bg-brand text-white shadow-[0_4px_20px_rgba(51,102,255,0.25)] hover:bg-brand-light'
|
||||
: variant === 'secondary'
|
||||
? 'border border-blue-200 text-blue-700 hover:border-blue-300 hover:bg-blue-50'
|
||||
: 'border border-blue-100 text-slate-700 hover:border-blue-200 hover:bg-blue-50/80',
|
||||
? 'border border-brand-border text-brand hover:border-brand hover:bg-brand-surface'
|
||||
: 'border border-brand-border text-brand-heading/80 hover:border-brand hover:bg-brand-surface',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
|
||||
@ -10,7 +10,7 @@ export default async function Sidebar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="w-full space-y-6 rounded-3xl border border-blue-100 bg-white/95 p-6 text-slate-900 shadow-xl shadow-blue-200/60 backdrop-blur lg:sticky lg:top-16 lg:h-fit lg:w-[360px]">
|
||||
<aside className="w-full space-y-6 rounded-2xl border border-brand-border bg-white p-6 text-brand-heading shadow-[0_4px_20px_rgba(0,0,0,0.04)] lg:sticky lg:top-0 lg:h-fit lg:w-[360px]">
|
||||
{sections.map((section) => (
|
||||
<SidebarCard key={section.slug} section={section} />
|
||||
))}
|
||||
|
||||
@ -36,13 +36,13 @@ export default function SidebarCard({ section }: SidebarCardProps) {
|
||||
const hasTagsLayout = localizedSection.layout === 'tags' && localizedSection.tags.length > 0
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl border border-blue-100 bg-white/95 p-5 shadow-md shadow-blue-200/50">
|
||||
<section className="rounded-2xl border border-brand-border bg-white p-5 text-brand-heading shadow-[0_4px_20px_rgba(0,0,0,0.04)]">
|
||||
<header className="mb-4 flex items-center justify-between gap-3">
|
||||
<h3 className="text-lg font-semibold text-slate-900">{localizedSection.title}</h3>
|
||||
<h3 className="text-lg font-medium text-brand-heading">{localizedSection.title}</h3>
|
||||
{isValidCta(localizedSection) ? (
|
||||
<Link
|
||||
href={localizedSection.ctaHref}
|
||||
className="text-sm font-medium text-blue-600 transition hover:text-blue-700"
|
||||
className="text-sm font-medium text-brand transition hover:text-brand-light"
|
||||
>
|
||||
{localizedSection.ctaLabel}
|
||||
</Link>
|
||||
@ -53,7 +53,7 @@ export default function SidebarCard({ section }: SidebarCardProps) {
|
||||
{localizedSection.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-slate-700"
|
||||
className="inline-flex items-center rounded-full border border-brand-border bg-brand-surface px-3 py-1 text-xs font-medium text-brand-heading"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
@ -61,7 +61,7 @@ export default function SidebarCard({ section }: SidebarCardProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="prose prose-sm max-w-none text-slate-700 [&_a]:text-blue-600 [&_a]:no-underline hover:[&_a]:underline"
|
||||
className="prose prose-sm max-w-none text-brand-heading/80 [&_a]:text-brand [&_a]:no-underline hover:[&_a]:underline"
|
||||
dangerouslySetInnerHTML={{ __html: localizedSection.bodyHtml }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Fragment, type ComponentType } from 'react'
|
||||
import Footer from '@components/Footer'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import type { HomePageSlotKey, HomePageTemplateSlots, TemplateComponent } from '../types'
|
||||
@ -103,6 +104,7 @@ export function createCommonHomeTemplate(
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Footer />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@ -8,6 +8,17 @@ module.exports = {
|
||||
sans: ['var(--font-geist-sans)', 'sans-serif'],
|
||||
mono: ['var(--font-geist-mono)', 'monospace'],
|
||||
},
|
||||
colors: {
|
||||
brand: {
|
||||
DEFAULT: '#3366FF',
|
||||
light: '#4D7AFF',
|
||||
dark: '#254EDB',
|
||||
surface: '#F5F8FF',
|
||||
border: '#D6E0FF',
|
||||
navy: '#1E2E55',
|
||||
heading: '#2E3A59',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import MarkdownSection from '../components/MarkdownSection'
|
||||
import Footer from '@components/Footer'
|
||||
import { useLanguage, type Language } from '../../i18n/LanguageProvider'
|
||||
|
||||
const SECTION_PATHS: Record<Language, {
|
||||
@ -36,29 +37,55 @@ export default function MarkdownHomepage() {
|
||||
const sections = SECTION_PATHS[language] ?? SECTION_PATHS[DEFAULT_LANGUAGE]
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-16 bg-white py-16">
|
||||
<header className="bg-slate-950 py-16 text-white">
|
||||
<div className="mx-auto flex max-w-4xl flex-col gap-6 px-6">
|
||||
<main className="flex flex-col bg-brand-surface text-brand-heading">
|
||||
<header className="bg-brand py-16 text-white">
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-8">
|
||||
<MarkdownSection
|
||||
src={sections.operations}
|
||||
headingLevel="h1"
|
||||
className="flex flex-col gap-4"
|
||||
headingClassName="text-3xl font-semibold text-white sm:text-4xl"
|
||||
contentClassName="prose-invert prose-headings:text-white prose-strong:text-white text-slate-100"
|
||||
headingClassName="text-[36px] font-bold text-white"
|
||||
contentClassName="prose-invert prose-headings:text-white prose-strong:text-white text-white/90"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<section className="mx-auto flex w-full max-w-6xl flex-col gap-12 px-6">
|
||||
<MarkdownSection src={sections.productSpotlight} />
|
||||
<div className="grid gap-12 lg:grid-cols-[2fr_1fr]">
|
||||
<MarkdownSection src={sections.news} />
|
||||
<section className="mx-auto flex w-full max-w-6xl flex-col gap-12 px-8 py-16">
|
||||
<MarkdownSection
|
||||
src={sections.productSpotlight}
|
||||
className="rounded-2xl border border-brand-border bg-white p-8 shadow-[0_4px_20px_rgba(0,0,0,0.04)]"
|
||||
headingClassName="text-2xl font-semibold text-brand-navy"
|
||||
contentClassName="prose prose-slate mt-6 max-w-none text-brand-heading/80"
|
||||
/>
|
||||
<div className="grid gap-12 lg:grid-cols-[minmax(0,2fr)_360px]">
|
||||
<MarkdownSection
|
||||
src={sections.news}
|
||||
className="rounded-2xl border border-brand-border bg-white p-8 shadow-[0_4px_20px_rgba(0,0,0,0.04)]"
|
||||
headingClassName="text-2xl font-semibold text-brand-navy"
|
||||
contentClassName="prose prose-slate mt-6 max-w-none text-brand-heading/80"
|
||||
/>
|
||||
<div className="flex flex-col gap-12">
|
||||
<MarkdownSection src={sections.support} />
|
||||
<MarkdownSection src={sections.resources} />
|
||||
<MarkdownSection
|
||||
src={sections.support}
|
||||
className="rounded-2xl border border-brand-border bg-white p-8 shadow-[0_4px_20px_rgba(0,0,0,0.04)]"
|
||||
headingClassName="text-2xl font-semibold text-brand-navy"
|
||||
contentClassName="prose prose-slate mt-6 max-w-none text-brand-heading/80"
|
||||
/>
|
||||
<MarkdownSection
|
||||
src={sections.resources}
|
||||
className="rounded-2xl border border-brand-border bg-white p-8 shadow-[0_4px_20px_rgba(0,0,0,0.04)]"
|
||||
headingClassName="text-2xl font-semibold text-brand-navy"
|
||||
contentClassName="prose prose-slate mt-6 max-w-none text-brand-heading/80"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<MarkdownSection src={sections.community} />
|
||||
<MarkdownSection
|
||||
src={sections.community}
|
||||
className="rounded-2xl border border-brand-border bg-white p-8 shadow-[0_4px_20px_rgba(0,0,0,0.04)]"
|
||||
headingClassName="text-2xl font-semibold text-brand-navy"
|
||||
contentClassName="prose prose-slate mt-6 max-w-none text-brand-heading/80"
|
||||
/>
|
||||
</section>
|
||||
<Footer />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user