Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions app/design-system/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use client';

import { ErrorState } from '@/components/states/ErrorState';
import { EmptyState } from '@/components/states/EmptyState';
import { LoadingState, Skeleton } from '@/components/states/LoadingState';
import { OfflineState } from '@/components/states/OfflineState';
import { PermissionDenied } from '@/components/states/PermissionDenied';

export default function DesignSystemStates() {
return (
<div className="shell py-12 space-y-16">
<div>
<h1 className="text-3xl font-extrabold mb-4">State Components</h1>
<p className="text-muted mb-8 max-w-2xl">
A showcase of all the shared state components used throughout the application.
These cover loading, errors, empty data, offline, and permission denied scenarios.
</p>
</div>

<section>
<h2 className="text-xl font-bold mb-6 flex items-center gap-2">
<span className="flex h-6 w-6 items-center justify-center rounded bg-primary text-xs font-black text-bg">1</span>
Loading States
</h2>
<div className="grid md:grid-cols-2 gap-8">
<div className="space-y-2">
<h3 className="font-semibold text-sm text-muted uppercase tracking-wider">Spinner</h3>
<div className="rounded-xl border border-muted/20 bg-card p-4">
<LoadingState text="Loading data..." />
</div>
</div>
<div className="space-y-2">
<h3 className="font-semibold text-sm text-muted uppercase tracking-wider">Skeleton</h3>
<div className="rounded-xl border border-muted/20 bg-card p-8">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
<Skeleton className="h-[200px] w-full rounded-xl" />
</div>
</div>
</div>
</div>
</section>

<section>
<h2 className="text-xl font-bold mb-6 flex items-center gap-2">
<span className="flex h-6 w-6 items-center justify-center rounded bg-primary text-xs font-black text-bg">2</span>
Error & Offline
</h2>
<div className="grid md:grid-cols-2 gap-8">
<div className="space-y-2">
<h3 className="font-semibold text-sm text-muted uppercase tracking-wider">General Error</h3>
<ErrorState
title="Failed to load dashboard"
message="The server responded with a 500 status code. Our team has been notified."
onRetry={() => console.log('retrying...')}
/>
</div>
<div className="space-y-2">
<h3 className="font-semibold text-sm text-muted uppercase tracking-wider">Offline</h3>
<OfflineState
onRetry={() => console.log('retrying...')}
/>
</div>
</div>
</section>

<section>
<h2 className="text-xl font-bold mb-6 flex items-center gap-2">
<span className="flex h-6 w-6 items-center justify-center rounded bg-primary text-xs font-black text-bg">3</span>
Empty & Permission
</h2>
<div className="grid md:grid-cols-2 gap-8">
<div className="space-y-2">
<h3 className="font-semibold text-sm text-muted uppercase tracking-wider">Empty Data</h3>
<EmptyState
title="No issues found"
description="There are currently no open issues in this workspace that match your filters."
action={<button type="button" className="button-primary mt-4">Create Issue</button>}
/>
</div>
<div className="space-y-2">
<h3 className="font-semibold text-sm text-muted uppercase tracking-wider">Permission Denied</h3>
<PermissionDenied
message="You need Admin privileges to view the settings for this workspace."
/>
</div>
</div>
</section>
</div>
);
}
36 changes: 27 additions & 9 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@
@tailwind utilities;

:root {
/* Legacy aliases remain while existing routes migrate to semantic tokens. */
--ink: #111111;
--ink-soft: #555555;
--ink-faint: #858585;
--line: #e8e8e8;
--accent: #e53935;
--accent-deep: #c62828;
--cream: #f9f5f0;
--bg: #FFF8EB;
--card: #FFF0D2;
--primary: #FF5D68;
--teal: #00B4A5;
--gold: #FFBE1E;
--orange: #FF8C1E;
--purple: #B450DC;
--green: #3CC864;
--text-bright: #262630;
--muted: #A07864;
}

.dark {
Expand All @@ -28,23 +39,25 @@ body {
margin: 0;
min-height: 100vh;
overflow-x: hidden;
background: #fff;
color: var(--ink);
background: var(--bg);
color: var(--text-bright);
font-family: var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a { color: inherit; }
::selection { background: rgba(229, 57, 53, .18); }
::selection { background: rgba(255, 93, 104, .25); }

@layer components {
.shell { @apply mx-auto w-full max-w-[1240px] px-5 md:px-8; }
.eyebrow { @apply font-mono text-[11px] font-bold uppercase tracking-[0.12em] text-accent; }
.button-primary { @apply inline-flex items-center justify-center gap-2 rounded-full bg-ink px-5 py-3 text-sm font-bold text-white no-underline transition-all duration-200 hover:-translate-y-0.5 hover:bg-[#303030]; }
.button-secondary { @apply inline-flex items-center justify-center gap-2 rounded-full border border-[#d7d7d7] bg-white px-5 py-3 text-sm font-bold no-underline transition-all duration-200 hover:-translate-y-0.5 hover:border-[#aaa]; }
.surface { @apply rounded-2xl border border-[var(--line)] bg-white shadow-card; }
.eyebrow { @apply font-mono text-[11px] font-bold uppercase tracking-[0.12em] text-primary; }
.button-primary { @apply inline-flex items-center justify-center gap-2 rounded-full bg-text-bright px-5 py-3 text-sm font-bold text-bg no-underline transition-all duration-200 hover:-translate-y-0.5 hover:bg-[#1a1a24]; }
.button-secondary { @apply inline-flex items-center justify-center gap-2 rounded-full border border-muted/20 bg-card px-5 py-3 text-sm font-bold no-underline transition-all duration-200 hover:-translate-y-0.5 hover:border-muted/40 hover:bg-[#ffe3b0]; }
.surface { @apply rounded-2xl border border-muted/10 bg-card shadow-card; }
}

.hero-grid {
background-image: linear-gradient(rgba(17,17,17,.035) 1px, transparent 1px), linear-gradient(90deg, rgba(17,17,17,.035) 1px, transparent 1px);
background-image: linear-gradient(rgba(38,38,48,.035) 1px, transparent 1px), linear-gradient(90deg, rgba(38,38,48,.035) 1px, transparent 1px);
background-size: 32px 32px;
mask-image: linear-gradient(to bottom, black 35%, transparent 95%);
}
Expand All @@ -54,7 +67,12 @@ a { color: inherit; }

@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

.dark body {
Expand Down
26 changes: 26 additions & 0 deletions app/host/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';

import { useEffect } from 'react';
import { ErrorState } from '@/components/states/ErrorState';

export default function HostError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);

return (
<div className="flex-1 py-12">
<ErrorState
title="Something went wrong"
message={error.message || 'An unexpected error occurred in the host workspace.'}
onRetry={reset}
/>
</div>
);
}
9 changes: 9 additions & 0 deletions app/host/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LoadingState } from '@/components/states/LoadingState';

export default function HostLoading() {
return (
<div className="flex-1 py-12">
<LoadingState text="Loading workspace..." />
</div>
);
}
26 changes: 26 additions & 0 deletions app/host/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from 'next/link';
import { Icon } from '@/components/icons';
import { EmptyState } from '@/components/states/EmptyState';

export default function HostNotFound() {
return (
<div className="flex-1 py-12">
<EmptyState
icon={
<Icon size={24}>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</Icon>
}
title="Page not found"
description="The host workspace page you are looking for does not exist."
action={
<Link href="/host" className="button-primary">
Return to Host Dashboard
</Link>
}
/>
</div>
);
}
12 changes: 6 additions & 6 deletions components/host-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ const navigation = [

export function HostSidebar() {
return (
<aside className="hidden min-h-screen border-r border-[var(--line)] bg-white p-5 lg:block">
<aside className="hidden min-h-screen border-r border-muted/20 bg-bg p-5 lg:block">
<Logo />
<div className="mt-9 rounded-xl border border-[var(--line)] p-3">
<p className="text-[10px] font-bold uppercase tracking-wider text-[#999]">Host workspace</p>
<p className="mt-1 text-sm font-bold">Elixpo Community</p>
<div className="mt-9 rounded-xl border border-muted/20 p-3">
<p className="text-[10px] font-bold uppercase tracking-wider text-muted">Host workspace</p>
<p className="mt-1 text-sm font-bold text-text-bright">Elixpo Community</p>
</div>
<nav className="mt-5 space-y-1" aria-label="Host panel">
{navigation.map(([label, href], index) => <Link key={label} href={href} className={`block rounded-lg px-3 py-2.5 text-sm no-underline ${index === 0 ? 'bg-accent-soft font-bold text-accent' : 'text-[#666] hover:bg-[#f7f7f7] hover:text-ink'}`}>{label}</Link>)}
{navigation.map(([label, href], index) => <Link key={label} href={href} className={`block rounded-lg px-3 py-2.5 text-sm no-underline ${index === 0 ? 'bg-primary/10 font-bold text-primary' : 'text-muted hover:bg-card hover:text-text-bright'}`}>{label}</Link>)}
</nav>
<div className="mt-8 border-t border-[var(--line)] pt-5"><p className="px-3 text-[10px] font-bold uppercase tracking-wider text-[#aaa]">Platform</p><Link href="/" className="mt-2 block rounded-lg px-3 py-2.5 text-sm text-[#666] no-underline hover:bg-[#f7f7f7]">Public site</Link></div>
<div className="mt-8 border-t border-muted/20 pt-5"><p className="px-3 text-[10px] font-bold uppercase tracking-wider text-muted">Platform</p><Link href="/" className="mt-2 block rounded-lg px-3 py-2.5 text-sm text-muted no-underline hover:bg-card">Public site</Link></div>
</aside>
);
}
40 changes: 40 additions & 0 deletions components/shell/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client';

import * as React from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';


export function Breadcrumbs() {
const pathname = usePathname();
const segments = pathname.split('/').filter(Boolean);

if (segments.length === 0) return null;

return (
<nav aria-label="Breadcrumb" className="flex items-center text-sm text-muted">
<ol className="flex items-center gap-2">
<li>
<Link href="/" className="hover:text-text-bright transition-colors">Home</Link>
</li>
{segments.map((segment: string, index: number) => {
const isLast = index === segments.length - 1;
const href = `/${segments.slice(0, index + 1).join('/')}`;

return (
<React.Fragment key={href}>
<li className="text-muted/50 mx-1">/</li>
<li>
{isLast ? (
<span className="font-medium text-text-bright" aria-current="page">{segment}</span>
) : (
<Link href={href} className="hover:text-text-bright transition-colors">{segment}</Link>
)}
</li>
</React.Fragment>
);
})}
</ol>
</nav>
);
}
81 changes: 81 additions & 0 deletions components/shell/CommandSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client';

import * as React from 'react';

import { Icon } from '@/components/icons';

const SearchIcon = () => (
<Icon size={16}>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</Icon>
);

export function CommandSearch() {
const [open, setOpen] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);

React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};

document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, []);

React.useEffect(() => {
if (open) inputRef.current?.focus();
}, [open]);

return (
<>
<button
type="button"
onClick={() => setOpen(true)}
className="flex items-center gap-2 rounded-md border border-muted/20 bg-bg px-3 py-1.5 text-sm text-muted transition-colors hover:bg-card w-full max-w-[240px] sm:max-w-xs"
>
<SearchIcon />
<span className="flex-1 text-left">Search or type a command...</span>
<kbd className="hidden sm:inline-flex h-5 items-center gap-1 rounded border border-muted/20 bg-card px-1.5 font-mono text-[10px] font-medium text-muted opacity-100">
<span className="text-xs">⌘</span>K
</kbd>
</button>

{open && (
<div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh] sm:pt-[15vh]">
<button
type="button"
aria-label="Close command search"
className="fixed inset-0 bg-text-bright/60 backdrop-blur-sm transition-opacity"
onClick={() => setOpen(false)}
/>
<div className="relative z-50 w-full max-w-xl rounded-xl border border-muted/20 bg-card shadow-2xl overflow-hidden" role="dialog" aria-modal="true">
<div className="flex items-center border-b border-muted/20 px-3">
<SearchIcon />
<input
ref={inputRef}
className="flex h-12 w-full bg-transparent py-3 px-2 text-sm text-text-bright placeholder:text-muted focus:outline-none"
placeholder="Type a command or search..."
/>
</div>
<div className="max-h-[300px] overflow-y-auto p-2">
<div className="px-2 py-1.5 text-xs font-semibold text-muted uppercase tracking-wider">Suggestions (Mock)</div>
<div className="flex flex-col gap-1 mt-1">
<button type="button" className="w-full rounded-md px-3 py-2 text-left text-sm text-text-bright hover:bg-muted/10 hover:text-primary transition-colors flex items-center gap-2">
<span>Go to Dashboard</span>
</button>
<button type="button" className="w-full rounded-md px-3 py-2 text-left text-sm text-text-bright hover:bg-muted/10 hover:text-primary transition-colors flex items-center gap-2">
<span>Create new Issue</span>
</button>
</div>
</div>
</div>
</div>
)}
</>
);
}
Loading
Loading