Mega push vol1

This commit is contained in:
AlacrisDevs
2026-02-05 01:09:55 +02:00
parent 2cfbd8531a
commit 1534e1f0af
24 changed files with 1953 additions and 617 deletions

View File

@@ -29,9 +29,27 @@ export const load: LayoutServerLoad = async ({ params, locals }) => {
error(403, 'You are not a member of this organization');
}
// Fetch team members for sidebar
const { data: members } = await locals.supabase
.from('org_members')
.select(`
id,
user_id,
role,
profiles:user_id (
id,
email,
full_name,
avatar_url
)
`)
.eq('org_id', org.id)
.limit(10);
return {
org,
role: membership.role,
userRole: membership.role
userRole: membership.role,
members: members ?? []
};
};

View File

@@ -2,17 +2,32 @@
import { page } from "$app/stores";
import type { Snippet } from "svelte";
interface Member {
id: string;
user_id: string;
role: string;
profiles: {
id: string;
email: string;
full_name: string | null;
avatar_url: string | null;
};
}
interface Props {
data: {
org: { id: string; name: string; slug: string };
role: string;
userRole: string;
members: Member[];
};
children: Snippet;
}
let { data, children }: Props = $props();
let sidebarCollapsed = $state(false);
const isAdmin = $derived(
data.userRole === "owner" || data.userRole === "admin",
);
@@ -47,115 +62,210 @@
}
</script>
<div class="flex h-screen bg-dark">
<aside class="w-64 bg-surface border-r border-light/10 flex flex-col">
<div class="p-4 border-b border-light/10">
<h1 class="text-lg font-semibold text-light truncate">
{data.org.name}
</h1>
<p class="text-xs text-light/50 capitalize">{data.role}</p>
<!-- Figma-matched layout: bg-background with gap-4 padding -->
<div class="flex h-screen bg-background p-4 gap-4">
<!-- Organization Module -->
<aside
class="{sidebarCollapsed
? 'w-20'
: 'w-56'} bg-night rounded-[32px] flex flex-col px-3 py-5 transition-all duration-200 overflow-hidden"
>
<!-- Org Header -->
<div class="flex items-start gap-2 px-1 mb-2">
<div
class="w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center text-primary text-xl font-heading shrink-0"
>
{data.org.name[0].toUpperCase()}
</div>
{#if !sidebarCollapsed}
<div class="min-w-0 flex-1">
<h1 class="font-heading text-xl text-light truncate">
{data.org.name}
</h1>
<p class="text-xs text-white capitalize">{data.role}</p>
</div>
{/if}
</div>
<nav class="flex-1 p-2 space-y-1">
<!-- Nav Items -->
<nav class="flex-1 space-y-0.5">
{#each navItems as item}
<a
href={item.href}
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors {isActive(
class="flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] transition-colors {isActive(
item.href,
)
? 'bg-primary text-white'
: 'text-light/70 hover:bg-light/5 hover:text-light'}"
? 'bg-primary/20'
: 'hover:bg-light/5'}"
title={sidebarCollapsed ? item.label : undefined}
>
{#if item.icon === "home"}
<svg
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
<!-- Icon circle -->
<div
class="w-8 h-8 rounded-full {isActive(item.href)
? 'bg-primary'
: 'bg-light'} flex items-center justify-center shrink-0"
>
{#if item.icon === "home"}
<svg
class="w-4 h-4 {isActive(item.href)
? 'text-white'
: 'text-night'}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
/>
<polyline points="9,22 9,12 15,12 15,22" />
</svg>
{:else if item.icon === "file"}
<svg
class="w-4 h-4 {isActive(item.href)
? 'text-white'
: 'text-night'}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
/>
<polyline points="14,2 14,8 20,8" />
</svg>
{:else if item.icon === "kanban"}
<svg
class="w-4 h-4 {isActive(item.href)
? 'text-white'
: 'text-night'}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="3"
width="18"
height="18"
rx="2"
/>
<line x1="9" y1="3" x2="9" y2="21" />
<line x1="15" y1="3" x2="15" y2="21" />
</svg>
{:else if item.icon === "calendar"}
<svg
class="w-4 h-4 {isActive(item.href)
? 'text-white'
: 'text-night'}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="4"
width="18"
height="18"
rx="2"
/>
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
{:else if item.icon === "settings"}
<svg
class="w-4 h-4 {isActive(item.href)
? 'text-white'
: 'text-night'}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="3" />
<path
d="M12 1v2m0 18v2M4.2 4.2l1.4 1.4m12.8 12.8l1.4 1.4M1 12h2m18 0h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"
/>
</svg>
{/if}
</div>
{#if !sidebarCollapsed}
<span class="font-bold text-light truncate"
>{item.label}</span
>
<path
d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
/>
<polyline points="9,22 9,12 15,12 15,22" />
</svg>
{:else if item.icon === "file"}
<svg
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
/>
<polyline points="14,2 14,8 20,8" />
</svg>
{:else if item.icon === "kanban"}
<svg
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<line x1="9" y1="3" x2="9" y2="21" />
<line x1="15" y1="3" x2="15" y2="21" />
</svg>
{:else if item.icon === "calendar"}
<svg
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="4" width="18" height="18" rx="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
{:else if item.icon === "settings"}
<svg
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="3" />
<path
d="M12 1v2m0 18v2M4.2 4.2l1.4 1.4m12.8 12.8l1.4 1.4M1 12h2m18 0h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"
/>
</svg>
{/if}
{item.label}
</a>
{/each}
</nav>
<div class="p-4 border-t border-light/10">
<!-- Team Members -->
{#if !sidebarCollapsed}
<div class="mt-4 pt-4 border-t border-light/10">
<p class="font-heading text-base text-light mb-2 px-1">Team</p>
{#if data.members && data.members.length > 0}
<div class="space-y-0.5">
{#each data.members.slice(0, 5) as member}
<div
class="flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] hover:bg-light/5 transition-colors"
>
<div
class="w-5 h-5 rounded-full bg-gradient-to-br from-primary to-primary/50 flex items-center justify-center text-white text-xs font-medium"
>
{(member.profiles?.full_name ||
member.profiles?.email ||
"?")[0].toUpperCase()}
</div>
<span
class="text-sm font-bold text-light truncate flex-1"
>
{member.profiles?.full_name ||
member.profiles?.email?.split("@")[0] ||
"User"}
</span>
</div>
{/each}
</div>
{:else}
<p class="text-xs text-light/40 px-1">
No team members found
</p>
{/if}
</div>
{/if}
<!-- Back link -->
<div class="mt-auto pt-4">
<a
href="/"
class="flex items-center gap-2 text-sm text-light/50 hover:text-light transition-colors"
class="flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] text-light/50 hover:text-light hover:bg-light/5 transition-colors"
title={sidebarCollapsed ? "All Organizations" : undefined}
>
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
<div
class="w-5 h-5 rounded-full bg-light/20 flex items-center justify-center"
>
<path d="m15 18-6-6 6-6" />
</svg>
All Organizations
<svg
class="w-3 h-3"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="m15 18-6-6 6-6" />
</svg>
</div>
{#if !sidebarCollapsed}
<span class="text-sm">All Organizations</span>
{/if}
</a>
</div>
</aside>
<main class="flex-1 overflow-auto">
<!-- Main Content Area -->
<main class="flex-1 bg-night rounded-[32px] overflow-auto">
{@render children()}
</main>
</div>

View File

@@ -1,55 +1,143 @@
<script lang="ts">
import { Card } from '$lib/components/ui';
import { Card } from "$lib/components/ui";
interface Props {
data: {
org: { id: string; name: string; slug: string };
role: string;
members?: Array<{
id: string;
user_id: string;
role: string;
profiles: { full_name: string | null; email: string };
}>;
};
}
let { data }: Props = $props();
const quickLinks = [
{ href: `/${data.org.slug}/documents`, label: 'Documents', description: 'Collaborative docs and files', icon: 'file' },
{ href: `/${data.org.slug}/kanban`, label: 'Kanban', description: 'Track tasks and projects', icon: 'kanban' },
{ href: `/${data.org.slug}/calendar`, label: 'Calendar', description: 'Schedule events and meetings', icon: 'calendar' }
{
href: `/${data.org.slug}/documents`,
label: "Documents",
description: "Collaborative docs and files",
icon: "file",
},
{
href: `/${data.org.slug}/kanban`,
label: "Kanban",
description: "Track tasks and projects",
icon: "kanban",
},
{
href: `/${data.org.slug}/calendar`,
label: "Calendar",
description: "Schedule events and meetings",
icon: "calendar",
},
];
// Mock recent activity - will be replaced with real data from activity_log table
const recentActivity = [
{
id: "1",
action: "Created document",
entity: "Project Brief",
time: "2 hours ago",
icon: "file",
},
{
id: "2",
action: "Updated kanban card",
entity: "Design Review",
time: "4 hours ago",
icon: "kanban",
},
{
id: "3",
action: "Added team member",
entity: "New Developer",
time: "1 day ago",
icon: "user",
},
];
</script>
<svelte:head>
<title>{data.org.name} - Overview | Root</title>
</svelte:head>
<div class="p-8">
<header class="mb-8">
<h1 class="text-3xl font-bold text-light">{data.org.name}</h1>
<h1 class="text-3xl font-heading text-light">{data.org.name}</h1>
<p class="text-light/50 mt-1">Organization Overview</p>
</header>
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
{#each quickLinks as link}
<a href={link.href} class="block group">
<Card class="h-full hover:ring-1 hover:ring-primary/50 transition-all">
<Card
class="h-full hover:ring-1 hover:ring-primary/50 transition-all"
>
<div class="p-6">
<div class="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
{#if link.icon === 'file'}
<svg class="w-6 h-6 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<div
class="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors"
>
{#if link.icon === "file"}
<svg
class="w-6 h-6 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
/>
<polyline points="14,2 14,8 20,8" />
</svg>
{:else if link.icon === 'kanban'}
<svg class="w-6 h-6 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" />
{:else if link.icon === "kanban"}
<svg
class="w-6 h-6 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="3"
width="18"
height="18"
rx="2"
/>
<line x1="9" y1="3" x2="9" y2="21" />
<line x1="15" y1="3" x2="15" y2="21" />
</svg>
{:else if link.icon === 'calendar'}
<svg class="w-6 h-6 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" />
{:else if link.icon === "calendar"}
<svg
class="w-6 h-6 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="4"
width="18"
height="18"
rx="2"
/>
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
{/if}
</div>
<h3 class="text-lg font-semibold text-light mb-1">{link.label}</h3>
<h3 class="text-lg font-semibold text-light mb-1">
{link.label}
</h3>
<p class="text-sm text-light/50">{link.description}</p>
</div>
</Card>
@@ -58,11 +146,113 @@
</section>
<section>
<h2 class="text-xl font-semibold text-light mb-4">Recent Activity</h2>
<h2 class="text-xl font-heading text-light mb-4">Recent Activity</h2>
<Card>
<div class="p-6 text-center text-light/50">
<p>No recent activity to show</p>
<div class="divide-y divide-light/10">
{#each recentActivity as activity}
<div
class="flex items-center gap-4 p-4 hover:bg-light/5 transition-colors"
>
<div
class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center shrink-0"
>
{#if activity.icon === "file"}
<svg
class="w-5 h-5 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
/>
<polyline points="14,2 14,8 20,8" />
</svg>
{:else if activity.icon === "kanban"}
<svg
class="w-5 h-5 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="3"
width="18"
height="18"
rx="2"
/>
<line x1="9" y1="3" x2="9" y2="21" />
<line x1="15" y1="3" x2="15" y2="21" />
</svg>
{:else if activity.icon === "user"}
<svg
class="w-5 h-5 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
/>
<circle cx="12" cy="7" r="4" />
</svg>
{/if}
</div>
<div class="flex-1 min-w-0">
<p class="text-light font-medium">
{activity.action}
</p>
<p class="text-sm text-light/50 truncate">
{activity.entity}
</p>
</div>
<span class="text-xs text-light/40 shrink-0"
>{activity.time}</span
>
</div>
{/each}
</div>
</Card>
</section>
<!-- Team Stats -->
{#if data.members && data.members.length > 0}
<section class="mt-8">
<h2 class="text-xl font-heading text-light mb-4">Team</h2>
<Card>
<div class="p-4">
<div class="flex items-center gap-2 flex-wrap">
{#each data.members.slice(0, 8) as member}
<div
class="w-10 h-10 rounded-full bg-gradient-to-br from-primary to-primary/50 flex items-center justify-center text-white font-medium"
title={member.profiles?.full_name ||
member.profiles?.email}
>
{(member.profiles?.full_name ||
member.profiles?.email ||
"?")[0].toUpperCase()}
</div>
{/each}
{#if data.members.length > 8}
<div
class="w-10 h-10 rounded-full bg-light/10 flex items-center justify-center text-light/50 text-sm"
>
+{data.members.length - 8}
</div>
{/if}
</div>
<p class="text-sm text-light/50 mt-3">
{data.members.length} team member{data.members
.length !== 1
? "s"
: ""}
</p>
</div>
</Card>
</section>
{/if}
</div>

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import { getContext, onMount } from "svelte";
import { Button, Modal, Input, Textarea } from "$lib/components/ui";
import { Button, Modal } from "$lib/components/ui";
import { Calendar } from "$lib/components/calendar";
import { createEvent } from "$lib/api/calendar";
import {
getCalendarSubscribeUrl,
type GoogleCalendarEvent,
@@ -36,34 +35,11 @@
);
const allEvents = $derived([...events, ...googleEvents]);
let showCreateModal = $state(false);
let showEventModal = $state(false);
let isDeleting = $state(false);
let selectedEvent = $state<CalendarEvent | null>(null);
let selectedDate = $state<Date | null>(null);
let newEvent = $state({
title: "",
description: "",
date: "",
startTime: "09:00",
endTime: "10:00",
allDay: false,
color: "#6366f1",
});
const colorOptions = [
{ value: "#6366f1", label: "Indigo" },
{ value: "#ec4899", label: "Pink" },
{ value: "#10b981", label: "Green" },
{ value: "#f59e0b", label: "Amber" },
{ value: "#ef4444", label: "Red" },
{ value: "#8b5cf6", label: "Purple" },
];
function handleDateClick(date: Date) {
selectedDate = date;
newEvent.date = date.toISOString().split("T")[0];
showCreateModal = true;
function handleDateClick(_date: Date) {
// Event creation disabled
}
function handleEventClick(event: CalendarEvent) {
@@ -71,46 +47,25 @@
showEventModal = true;
}
async function handleCreateEvent() {
if (!newEvent.title.trim() || !newEvent.date || !data.user) return;
async function handleDeleteEvent() {
if (!selectedEvent || selectedEvent.id.startsWith("google-")) return;
const startTime = newEvent.allDay
? `${newEvent.date}T00:00:00`
: `${newEvent.date}T${newEvent.startTime}:00`;
const endTime = newEvent.allDay
? `${newEvent.date}T23:59:59`
: `${newEvent.date}T${newEvent.endTime}:00`;
isDeleting = true;
try {
const { error } = await supabase
.from("calendar_events")
.delete()
.eq("id", selectedEvent.id);
const created = await createEvent(
supabase,
data.org.id,
{
title: newEvent.title,
description: newEvent.description || undefined,
start_time: startTime,
end_time: endTime,
all_day: newEvent.allDay,
color: newEvent.color,
},
data.user.id,
);
events = [...events, created];
resetForm();
}
function resetForm() {
showCreateModal = false;
newEvent = {
title: "",
description: "",
date: "",
startTime: "09:00",
endTime: "10:00",
allDay: false,
color: "#6366f1",
};
selectedDate = null;
if (!error) {
events = events.filter((e) => e.id !== selectedEvent?.id);
showEventModal = false;
selectedEvent = null;
}
} catch (e) {
console.error("Failed to delete event:", e);
}
isDeleting = false;
}
function formatEventTime(event: CalendarEvent): string {
@@ -213,21 +168,12 @@
</div>
{/if}
</div>
<Button onclick={() => (showCreateModal = true)}>
<svg
class="w-4 h-4 mr-2"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
New Event
</Button>
</header>
<p class="text-light/50 text-sm mb-4">
View events from connected Google Calendar. Event creation coming soon.
</p>
<Calendar
events={allEvents}
onDateClick={handleDateClick}
@@ -235,78 +181,6 @@
/>
</div>
<Modal isOpen={showCreateModal} onClose={resetForm} title="Create Event">
<div class="space-y-4">
<Input
label="Title"
bind:value={newEvent.title}
placeholder="Event title"
/>
<Textarea
label="Description"
bind:value={newEvent.description}
placeholder="Optional description"
rows={2}
/>
<Input label="Date" type="date" bind:value={newEvent.date} />
<label class="flex items-center gap-2 text-sm text-light">
<input
type="checkbox"
bind:checked={newEvent.allDay}
class="rounded"
/>
All day event
</label>
{#if !newEvent.allDay}
<div class="grid grid-cols-2 gap-4">
<Input
label="Start Time"
type="time"
bind:value={newEvent.startTime}
/>
<Input
label="End Time"
type="time"
bind:value={newEvent.endTime}
/>
</div>
{/if}
<div>
<label class="block text-sm font-medium text-light mb-2"
>Color</label
>
<div class="flex gap-2">
{#each colorOptions as color}
<button
type="button"
class="w-8 h-8 rounded-full transition-transform"
class:ring-2={newEvent.color === color.value}
class:ring-white={newEvent.color === color.value}
class:scale-110={newEvent.color === color.value}
style="background-color: {color.value}"
onclick={() => (newEvent.color = color.value)}
aria-label={color.label}
></button>
{/each}
</div>
</div>
<div class="flex justify-end gap-2 pt-2">
<Button variant="ghost" onclick={resetForm}>Cancel</Button>
<Button
onclick={handleCreateEvent}
disabled={!newEvent.title.trim() || !newEvent.date}
>Create</Button
>
</div>
</div>
</Modal>
<Modal
isOpen={showEventModal}
onClose={() => (showEventModal = false)}
@@ -437,6 +311,19 @@
</p>
</div>
{/if}
<!-- Delete local event -->
{#if !selectedEvent.id.startsWith("google-")}
<div class="pt-3 border-t border-light/10">
<Button
variant="danger"
onclick={handleDeleteEvent}
loading={isDeleting}
>
Delete Event
</Button>
</div>
{/if}
</div>
{/if}
</Modal>

View File

@@ -22,9 +22,12 @@
let documents = $state(data.documents);
let selectedDoc = $state<Document | null>(null);
let showCreateModal = $state(false);
let showEditModal = $state(false);
let editingDoc = $state<Document | null>(null);
let newDocName = $state("");
let newDocType = $state<"folder" | "document">("document");
let parentFolderId = $state<string | null>(null);
let isEditing = $state(false);
const documentTree = $derived(buildDocumentTree(documents));
@@ -99,8 +102,72 @@
d.id === selectedDoc!.id ? { ...d, content } : d,
);
}
function handleEdit(doc: Document) {
editingDoc = doc;
newDocName = doc.name;
showEditModal = true;
}
async function handleRename() {
if (!editingDoc || !newDocName.trim()) return;
const { error } = await supabase
.from("documents")
.update({ name: newDocName, updated_at: new Date().toISOString() })
.eq("id", editingDoc.id);
if (!error) {
documents = documents.map((d) =>
d.id === editingDoc!.id ? { ...d, name: newDocName } : d,
);
if (selectedDoc?.id === editingDoc.id) {
selectedDoc = { ...selectedDoc, name: newDocName };
}
}
showEditModal = false;
editingDoc = null;
newDocName = "";
}
async function handleDelete(doc: Document) {
const itemType =
doc.type === "folder" ? "folder and all its contents" : "document";
if (!confirm(`Delete this ${itemType}?`)) return;
// If deleting a folder, delete all children first
if (doc.type === "folder") {
const childIds = documents
.filter((d) => d.parent_id === doc.id)
.map((d) => d.id);
if (childIds.length > 0) {
await supabase.from("documents").delete().in("id", childIds);
}
}
const { error } = await supabase
.from("documents")
.delete()
.eq("id", doc.id);
if (!error) {
documents = documents.filter(
(d) => d.id !== doc.id && d.parent_id !== doc.id,
);
if (selectedDoc?.id === doc.id) {
selectedDoc = null;
}
}
}
</script>
<svelte:head>
<title
>{selectedDoc ? `${selectedDoc.name} - ` : ""}Documents - {data.org
.name} | Root</title
>
</svelte:head>
<div class="flex h-full">
<aside class="w-72 border-r border-light/10 flex flex-col">
<div
@@ -109,7 +176,7 @@
<h2 class="font-semibold text-light">Documents</h2>
<Button size="sm" onclick={() => (showCreateModal = true)}>
<svg
class="w-4 h-4 mr-1"
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -118,7 +185,6 @@
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
New
</Button>
</div>
@@ -135,14 +201,37 @@
onSelect={handleSelect}
onAdd={handleAdd}
onMove={handleMove}
onEdit={handleEdit}
onDelete={handleDelete}
/>
{/if}
</div>
</aside>
<main class="flex-1 overflow-hidden">
<main class="flex-1 overflow-hidden flex flex-col">
{#if selectedDoc}
<Editor document={selectedDoc} onSave={handleSave} />
<div
class="flex items-center justify-between p-4 border-b border-light/10"
>
<h2 class="text-lg font-semibold text-light">
{selectedDoc.name}
</h2>
<button
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {isEditing
? 'bg-primary text-white'
: 'bg-light/10 text-light hover:bg-light/20'}"
onclick={() => (isEditing = !isEditing)}
>
{isEditing ? "Preview" : "Edit"}
</button>
</div>
<div class="flex-1 overflow-auto">
<Editor
document={selectedDoc}
onSave={handleSave}
editable={isEditing}
/>
</div>
{:else}
<div class="h-full flex items-center justify-center text-light/40">
<div class="text-center">
@@ -210,3 +299,35 @@
</div>
</div>
</Modal>
<Modal
isOpen={showEditModal}
onClose={() => {
showEditModal = false;
editingDoc = null;
newDocName = "";
}}
title="Rename"
>
<div class="space-y-4">
<Input
label="Name"
bind:value={newDocName}
placeholder="Enter new name"
/>
<div class="flex justify-end gap-2 pt-2">
<Button
variant="ghost"
onclick={() => {
showEditModal = false;
editingDoc = null;
newDocName = "";
}}>Cancel</Button
>
<Button onclick={handleRename} disabled={!newDocName.trim()}
>Save</Button
>
</div>
</div>
</Modal>

View File

@@ -5,7 +5,6 @@
import {
fetchBoardWithColumns,
createBoard,
createCard,
moveCard,
} from "$lib/api/kanban";
import type {
@@ -31,12 +30,13 @@
let boards = $state(data.boards);
let selectedBoard = $state<BoardWithColumns | null>(null);
let showCreateBoardModal = $state(false);
let showCreateCardModal = $state(false);
let showEditBoardModal = $state(false);
let showCardDetailModal = $state(false);
let selectedCard = $state<KanbanCard | null>(null);
let newBoardName = $state("");
let newCardTitle = $state("");
let editBoardName = $state("");
let targetColumnId = $state<string | null>(null);
let cardModalMode = $state<"edit" | "create">("edit");
async function loadBoard(boardId: string) {
selectedBoard = await fetchBoardWithColumns(supabase, boardId);
@@ -53,36 +53,68 @@
newBoardName = "";
}
async function handleAddCard(columnId: string) {
targetColumnId = columnId;
showCreateCardModal = true;
let editingBoardId = $state<string | null>(null);
function openEditBoardModal(board: KanbanBoardType) {
editingBoardId = board.id;
editBoardName = board.name;
showEditBoardModal = true;
}
async function handleCreateCard() {
if (
!newCardTitle.trim() ||
!targetColumnId ||
!selectedBoard ||
!data.user
)
return;
async function handleEditBoard() {
if (!editingBoardId || !editBoardName.trim()) return;
const column = selectedBoard.columns.find(
(c) => c.id === targetColumnId,
);
const position = column?.cards.length ?? 0;
const { error } = await supabase
.from("kanban_boards")
.update({ name: editBoardName })
.eq("id", editingBoardId);
await createCard(
supabase,
targetColumnId,
newCardTitle,
position,
data.user.id,
);
await loadBoard(selectedBoard.id);
if (!error) {
if (selectedBoard?.id === editingBoardId) {
selectedBoard = { ...selectedBoard, name: editBoardName };
}
boards = boards.map((b) =>
b.id === editingBoardId ? { ...b, name: editBoardName } : b,
);
}
showEditBoardModal = false;
editingBoardId = null;
}
showCreateCardModal = false;
newCardTitle = "";
async function handleDeleteBoard(e: MouseEvent, board: KanbanBoardType) {
e.stopPropagation();
if (!confirm(`Delete "${board.name}" and all its cards?`)) return;
const { error } = await supabase
.from("kanban_boards")
.delete()
.eq("id", board.id);
if (!error) {
boards = boards.filter((b) => b.id !== board.id);
if (selectedBoard?.id === board.id) {
selectedBoard = null;
}
}
}
async function handleAddCard(columnId: string) {
targetColumnId = columnId;
selectedCard = null;
cardModalMode = "create";
showCardDetailModal = true;
}
function handleCardCreated(newCard: KanbanCard) {
if (!selectedBoard) return;
selectedBoard = {
...selectedBoard,
columns: selectedBoard.columns.map((col) =>
col.id === newCard.column_id
? { ...col, cards: [...col.cards, newCard] }
: col,
),
};
targetColumnId = null;
}
@@ -93,12 +125,35 @@
) {
if (!selectedBoard) return;
await moveCard(supabase, cardId, toColumnId, toPosition);
await loadBoard(selectedBoard.id);
// Optimistic UI update - move card immediately
const fromColumn = selectedBoard.columns.find((c) =>
c.cards.some((card) => card.id === cardId),
);
const toColumn = selectedBoard.columns.find((c) => c.id === toColumnId);
if (!fromColumn || !toColumn) return;
const cardIndex = fromColumn.cards.findIndex((c) => c.id === cardId);
if (cardIndex === -1) return;
const [movedCard] = fromColumn.cards.splice(cardIndex, 1);
movedCard.column_id = toColumnId;
toColumn.cards.splice(toPosition, 0, movedCard);
// Trigger reactivity
selectedBoard = { ...selectedBoard };
// Persist to database in background
moveCard(supabase, cardId, toColumnId, toPosition).catch((err) => {
console.error("Failed to persist card move:", err);
// Reload to sync state on error
loadBoard(selectedBoard!.id);
});
}
function handleCardClick(card: KanbanCard) {
selectedCard = card;
cardModalMode = "edit";
showCardDetailModal = true;
}
@@ -122,6 +177,13 @@
}
</script>
<svelte:head>
<title
>{selectedBoard ? `${selectedBoard.name} - ` : ""}Kanban - {data.org
.name} | Root</title
>
</svelte:head>
<div class="flex h-full">
<aside class="w-64 border-r border-light/10 flex flex-col">
<div
@@ -149,15 +211,62 @@
</div>
{:else}
{#each boards as board}
<button
class="w-full text-left px-3 py-2 rounded-lg text-sm transition-colors {selectedBoard?.id ===
<div
class="group flex items-center gap-1 px-3 py-2 rounded-lg text-sm transition-colors cursor-pointer {selectedBoard?.id ===
board.id
? 'bg-primary text-white'
: 'text-light/70 hover:bg-light/5'}"
onclick={() => loadBoard(board.id)}
role="button"
tabindex="0"
>
{board.name}
</button>
<span class="flex-1 truncate">{board.name}</span>
<div
class="opacity-0 group-hover:opacity-100 flex items-center gap-0.5 transition-opacity"
>
<button
class="p-1 rounded hover:bg-light/20"
onclick={(e) => {
e.stopPropagation();
openEditBoardModal(board);
}}
title="Rename"
>
<svg
class="w-3.5 h-3.5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
/>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
/>
</svg>
</button>
<button
class="p-1 rounded hover:bg-error/20 hover:text-error"
onclick={(e) => handleDeleteBoard(e, board)}
title="Delete"
>
<svg
class="w-3.5 h-3.5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="3,6 5,6 21,6" />
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
/>
</svg>
</button>
</div>
</div>
{/each}
{/if}
</div>
@@ -222,23 +331,22 @@
</Modal>
<Modal
isOpen={showCreateCardModal}
onClose={() => (showCreateCardModal = false)}
title="Add Card"
isOpen={showEditBoardModal}
onClose={() => (showEditBoardModal = false)}
title="Edit Board"
>
<div class="space-y-4">
<Input
label="Title"
bind:value={newCardTitle}
placeholder="Card title"
label="Board Name"
bind:value={editBoardName}
placeholder="Board name"
/>
<div class="flex justify-end gap-2">
<Button
variant="ghost"
onclick={() => (showCreateCardModal = false)}>Cancel</Button
<Button variant="ghost" onclick={() => (showEditBoardModal = false)}
>Cancel</Button
>
<Button onclick={handleCreateCard} disabled={!newCardTitle.trim()}
>Add</Button
<Button onclick={handleEditBoard} disabled={!editBoardName.trim()}
>Save</Button
>
</div>
</div>
@@ -250,7 +358,12 @@
onClose={() => {
showCardDetailModal = false;
selectedCard = null;
targetColumnId = null;
}}
onUpdate={handleCardUpdate}
onDelete={handleCardDelete}
mode={cardModalMode}
columnId={targetColumnId ?? undefined}
userId={data.user?.id}
onCreate={handleCardCreated}
/>

View File

@@ -609,6 +609,7 @@
<Card>
<div class="divide-y divide-light/10">
{#each members as member}
{@const profile = member.profiles}
<div
class="flex items-center justify-between p-4 hover:bg-light/5 transition-colors"
>
@@ -616,16 +617,18 @@
<div
class="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary font-medium"
>
{(member.profiles.full_name ||
member.profiles.email ||
{(profile?.full_name ||
profile?.email ||
"?")[0].toUpperCase()}
</div>
<div>
<p class="text-light font-medium">
{member.profiles.full_name || "No name"}
{profile?.full_name ||
profile?.email ||
"Unknown User"}
</p>
<p class="text-sm text-light/50">
{member.profiles.email}
{profile?.email || "No email"}
</p>
</div>
</div>

View File

@@ -1,32 +1,55 @@
@import url('https://fonts.googleapis.com/css2?family=Tilt+Warp&family=Work+Sans:wght@400;500;600;700&display=swap');
@import 'tailwindcss';
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
@theme {
/* Colors - Dark theme */
--color-dark: #0a0a0f;
--color-surface: #14141f;
--color-light: #f0f0f5;
/* Colors - Figma Design System */
--color-background: #05090f;
--color-night: #0A121F;
--color-dark: #14243E;
--color-surface: #0A121F;
--color-light: #E5E6F0;
--color-text: #FFFFFF;
--color-text-muted: rgba(229, 230, 240, 0.5);
/* Brand */
--color-primary: #6366f1;
--color-primary-hover: #4f46e5;
/* Brand - Primary */
--color-primary: #00A3E0;
--color-primary-hover: #33b5e6;
/* Status */
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #3b82f6;
/* Status Colors */
--color-success: #33E000;
--color-warning: #FFAB00;
--color-error: #E03D00;
--color-info: #00A3E0;
/* Font */
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
/* Typography - Figma Fonts */
--font-heading: 'Tilt Warp', sans-serif;
--font-body: 'Work Sans', sans-serif;
--font-sans: 'Work Sans', system-ui, -apple-system, sans-serif;
/* Border Radius - Figma Design */
--radius-sm: 8px;
--radius-md: 16px;
--radius-lg: 24px;
--radius-xl: 32px;
--radius-pill: 32px;
--radius-circle: 128px;
}
/* Base styles */
body {
background-color: var(--color-dark);
html, body {
background-color: var(--color-background);
color: var(--color-light);
font-family: var(--font-sans);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Headings */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
font-weight: 400;
}
/* Scrollbar styling */
@@ -36,14 +59,105 @@ body {
}
::-webkit-scrollbar-track {
background: var(--color-dark);
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-light) / 0.2;
border-radius: 4px;
background: var(--color-night);
border-radius: var(--radius-pill);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-light) / 0.3;
background: var(--color-dark);
}
/* Focus styles */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Selection */
::selection {
background-color: rgba(0, 163, 224, 0.3);
color: var(--color-light);
}
/* Prose/Markdown styles */
.prose {
line-height: 1.6;
}
.prose p {
margin: 0.5em 0;
}
.prose strong {
font-weight: 700;
color: var(--color-light);
}
.prose code {
background: var(--color-night);
padding: 0.15em 0.4em;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.9em;
color: var(--color-primary);
}
.prose pre {
background: var(--color-night);
padding: 1em;
border-radius: var(--radius-sm);
overflow-x: auto;
margin: 0.5em 0;
}
.prose pre code {
background: none;
padding: 0;
color: var(--color-light);
}
.prose blockquote {
border-left: 3px solid var(--color-primary);
padding-left: 1em;
margin: 0.5em 0;
color: var(--color-text-muted);
font-style: italic;
}
.prose ul, .prose ol {
padding-left: 1.5em;
margin: 0.5em 0;
}
.prose ul {
list-style-type: disc;
}
.prose ol {
list-style-type: decimal;
}
.prose li {
margin: 0.25em 0;
}
.prose h1, .prose h2, .prose h3, .prose h4 {
color: var(--color-light);
margin: 0.75em 0 0.5em;
font-family: var(--font-heading);
}
.prose a {
color: var(--color-primary);
text-decoration: underline;
}
.prose hr {
border: none;
border-top: 1px solid var(--color-dark);
margin: 1em 0;
}

View File

@@ -9,19 +9,20 @@
Card,
Modal,
Spinner,
Toggle
} from '$lib/components/ui';
Toggle,
Toast,
} from "$lib/components/ui";
let inputValue = $state('');
let textareaValue = $state('');
let selectValue = $state('');
let inputValue = $state("");
let textareaValue = $state("");
let selectValue = $state("");
let toggleChecked = $state(false);
let modalOpen = $state(false);
const selectOptions = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' }
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
];
</script>
@@ -31,66 +32,95 @@
<div class="min-h-screen bg-dark p-8">
<div class="max-w-6xl mx-auto space-y-12">
<!-- Back Button -->
<a
href="/"
class="inline-flex items-center gap-2 text-light/60 hover:text-light transition-colors"
>
<svg
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
Back to Home
</a>
<!-- Header -->
<header class="text-center space-y-4">
<h1 class="text-4xl font-bold text-light">Root Style Guide</h1>
<p class="text-light/60">All UI components and their variants</p>
</header>
<!-- Colors -->
<!-- Colors - Figma Design System -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Colors</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Colors
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
<div class="space-y-2">
<div class="w-full h-20 rounded-xl bg-dark border border-light/20"></div>
<div
class="w-full h-20 rounded-[32px] bg-background border border-light/20"
></div>
<p class="text-sm text-light/60">Background</p>
<code class="text-xs text-light/40">#05090F</code>
</div>
<div class="space-y-2">
<div class="w-full h-20 rounded-[32px] bg-night"></div>
<p class="text-sm text-light/60">Night</p>
<code class="text-xs text-light/40">#0A121F</code>
</div>
<div class="space-y-2">
<div class="w-full h-20 rounded-[32px] bg-dark"></div>
<p class="text-sm text-light/60">Dark</p>
<code class="text-xs text-light/40">#0a0a0f</code>
<code class="text-xs text-light/40">#14243E</code>
</div>
<div class="space-y-2">
<div class="w-full h-20 rounded-xl bg-surface"></div>
<p class="text-sm text-light/60">Surface</p>
<code class="text-xs text-light/40">#14141f</code>
</div>
<div class="space-y-2">
<div class="w-full h-20 rounded-xl bg-light"></div>
<div class="w-full h-20 rounded-[32px] bg-light"></div>
<p class="text-sm text-light/60">Light</p>
<code class="text-xs text-light/40">#f0f0f5</code>
<code class="text-xs text-light/40">#E5E6F0</code>
</div>
<div class="space-y-2">
<div class="w-full h-20 rounded-xl bg-primary"></div>
<div class="w-full h-20 rounded-[32px] bg-primary"></div>
<p class="text-sm text-light/60">Primary</p>
<code class="text-xs text-light/40">#6366f1</code>
<code class="text-xs text-light/40">#00A3E0</code>
</div>
<div class="space-y-2">
<div class="w-full h-20 rounded-xl bg-success"></div>
<div class="w-full h-20 rounded-[32px] bg-success"></div>
<p class="text-sm text-light/60">Success</p>
<code class="text-xs text-light/40">#22c55e</code>
<code class="text-xs text-light/40">#33E000</code>
</div>
<div class="space-y-2">
<div class="w-full h-20 rounded-xl bg-warning"></div>
<div class="w-full h-20 rounded-[32px] bg-warning"></div>
<p class="text-sm text-light/60">Warning</p>
<code class="text-xs text-light/40">#f59e0b</code>
<code class="text-xs text-light/40">#FFAB00</code>
</div>
<div class="space-y-2">
<div class="w-full h-20 rounded-xl bg-error"></div>
<div class="w-full h-20 rounded-[32px] bg-error"></div>
<p class="text-sm text-light/60">Error</p>
<code class="text-xs text-light/40">#ef4444</code>
</div>
<div class="space-y-2">
<div class="w-full h-20 rounded-xl bg-info"></div>
<p class="text-sm text-light/60">Info</p>
<code class="text-xs text-light/40">#3b82f6</code>
<code class="text-xs text-light/40">#E03D00</code>
</div>
</div>
</section>
<!-- Buttons -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Buttons</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Buttons
</h2>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">Variants</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
Variants
</h3>
<div class="flex flex-wrap gap-3">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
@@ -101,9 +131,10 @@
</div>
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">Sizes</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
Sizes
</h3>
<div class="flex flex-wrap items-center gap-3">
<Button size="xs">Extra Small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
@@ -111,7 +142,9 @@
</div>
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">States</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
States
</h3>
<div class="flex flex-wrap gap-3">
<Button>Normal</Button>
<Button disabled>Disabled</Button>
@@ -120,7 +153,9 @@
</div>
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">Full Width</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
Full Width
</h3>
<div class="max-w-sm">
<Button fullWidth>Full Width Button</Button>
</div>
@@ -130,45 +165,103 @@
<!-- Inputs -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Inputs</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Inputs
</h2>
<div class="grid md:grid-cols-2 gap-6">
<Input label="Default Input" placeholder="Enter text..." bind:value={inputValue} />
<Input label="Required Field" placeholder="Required..." required />
<Input label="With Hint" placeholder="Email..." hint="We'll never share your email" />
<Input label="With Error" placeholder="Password..." error="Password is too short" />
<Input label="Disabled" placeholder="Can't edit this" disabled />
<Input type="password" label="Password" placeholder="••••••••" />
<Input
label="Default Input"
placeholder="Enter text..."
bind:value={inputValue}
/>
<Input
label="Required Field"
placeholder="Required..."
required
/>
<Input
label="With Hint"
placeholder="Email..."
hint="We'll never share your email"
/>
<Input
label="With Error"
placeholder="Password..."
error="Password is too short"
/>
<Input
label="Disabled"
placeholder="Can't edit this"
disabled
/>
<Input
type="password"
label="Password"
placeholder="••••••••"
/>
</div>
</section>
<!-- Textarea -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Textarea</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Textarea
</h2>
<div class="grid md:grid-cols-2 gap-6">
<Textarea label="Default Textarea" placeholder="Enter description..." bind:value={textareaValue} />
<Textarea label="With Error" placeholder="Description..." error="Description is required" />
<Textarea
label="Default Textarea"
placeholder="Enter description..."
bind:value={textareaValue}
/>
<Textarea
label="With Error"
placeholder="Description..."
error="Description is required"
/>
</div>
</section>
<!-- Select -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Select</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Select
</h2>
<div class="grid md:grid-cols-2 gap-6">
<Select label="Default Select" options={selectOptions} bind:value={selectValue} />
<Select label="With Error" options={selectOptions} error="Please select an option" />
<Select
label="Default Select"
options={selectOptions}
bind:value={selectValue}
/>
<Select
label="With Error"
options={selectOptions}
error="Please select an option"
/>
</div>
</section>
<!-- Avatars -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Avatars</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Avatars
</h2>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">Sizes</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
Sizes
</h3>
<div class="flex items-end gap-4">
<Avatar name="John Doe" size="xs" />
<Avatar name="John Doe" size="sm" />
@@ -180,17 +273,25 @@
</div>
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">With Status</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
With Status
</h3>
<div class="flex items-center gap-4">
<Avatar name="Online User" size="lg" status="online" />
<Avatar name="Away User" size="lg" status="away" />
<Avatar name="Busy User" size="lg" status="busy" />
<Avatar name="Offline User" size="lg" status="offline" />
<Avatar
name="Offline User"
size="lg"
status="offline"
/>
</div>
</div>
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">Different Names (Color Generation)</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
Different Names (Color Generation)
</h3>
<div class="flex items-center gap-4">
<Avatar name="Alice" size="lg" />
<Avatar name="Bob" size="lg" />
@@ -204,11 +305,17 @@
<!-- Badges -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Badges</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Badges
</h2>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">Variants</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
Variants
</h3>
<div class="flex flex-wrap gap-3">
<Badge variant="default">Default</Badge>
<Badge variant="primary">Primary</Badge>
@@ -220,7 +327,9 @@
</div>
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">Sizes</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
Sizes
</h3>
<div class="flex flex-wrap items-center gap-3">
<Badge size="sm">Small</Badge>
<Badge size="md">Medium</Badge>
@@ -232,31 +341,47 @@
<!-- Cards -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Cards</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Cards
</h2>
<div class="grid md:grid-cols-3 gap-6">
<Card variant="default">
<h3 class="font-semibold text-light mb-2">Default Card</h3>
<p class="text-light/60 text-sm">This is a default card with medium padding.</p>
<p class="text-light/60 text-sm">
This is a default card with medium padding.
</p>
</Card>
<Card variant="elevated">
<h3 class="font-semibold text-light mb-2">Elevated Card</h3>
<p class="text-light/60 text-sm">This card has a shadow for elevation.</p>
<p class="text-light/60 text-sm">
This card has a shadow for elevation.
</p>
</Card>
<Card variant="outlined">
<h3 class="font-semibold text-light mb-2">Outlined Card</h3>
<p class="text-light/60 text-sm">This card has a subtle border.</p>
<p class="text-light/60 text-sm">
This card has a subtle border.
</p>
</Card>
</div>
</section>
<!-- Toggle -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Toggle</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Toggle
</h2>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">Sizes</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
Sizes
</h3>
<div class="flex items-center gap-6">
<div class="flex items-center gap-2">
<Toggle size="sm" />
@@ -274,7 +399,9 @@
</div>
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">States</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
States
</h3>
<div class="flex items-center gap-6">
<div class="flex items-center gap-2">
<Toggle />
@@ -295,11 +422,17 @@
<!-- Spinners -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Spinners</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Spinners
</h2>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">Sizes</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
Sizes
</h3>
<div class="flex items-center gap-6">
<Spinner size="sm" />
<Spinner size="md" />
@@ -308,7 +441,9 @@
</div>
<div>
<h3 class="text-lg font-medium text-light/80 mb-3">Colors</h3>
<h3 class="text-lg font-medium text-light/80 mb-3">
Colors
</h3>
<div class="flex items-center gap-6">
<Spinner color="primary" />
<Spinner color="light" />
@@ -322,50 +457,112 @@
<!-- Modal -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Modal</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Modal
</h2>
<div>
<Button onclick={() => (modalOpen = true)}>Open Modal</Button>
</div>
</section>
<Modal isOpen={modalOpen} onClose={() => (modalOpen = false)} title="Example Modal">
<Modal
isOpen={modalOpen}
onClose={() => (modalOpen = false)}
title="Example Modal"
>
<p class="text-light/70 mb-4">
This is an example modal dialog. You can put any content here.
</p>
<div class="flex gap-3 justify-end">
<Button variant="secondary" onclick={() => (modalOpen = false)}>Cancel</Button>
<Button variant="secondary" onclick={() => (modalOpen = false)}
>Cancel</Button
>
<Button onclick={() => (modalOpen = false)}>Confirm</Button>
</div>
</Modal>
<!-- Typography -->
<section class="space-y-4">
<h2 class="text-2xl font-semibold text-light border-b border-light/10 pb-2">Typography</h2>
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Typography
</h2>
<div class="space-y-4">
<h1 class="text-4xl font-bold text-light">Heading 1 (4xl bold)</h1>
<h2 class="text-3xl font-bold text-light">Heading 2 (3xl bold)</h2>
<h3 class="text-2xl font-semibold text-light">Heading 3 (2xl semibold)</h3>
<h4 class="text-xl font-semibold text-light">Heading 4 (xl semibold)</h4>
<h5 class="text-lg font-medium text-light">Heading 5 (lg medium)</h5>
<h6 class="text-base font-medium text-light">Heading 6 (base medium)</h6>
<h1 class="text-4xl font-bold text-light">
Heading 1 (4xl bold)
</h1>
<h2 class="text-3xl font-bold text-light">
Heading 2 (3xl bold)
</h2>
<h3 class="text-2xl font-semibold text-light">
Heading 3 (2xl semibold)
</h3>
<h4 class="text-xl font-semibold text-light">
Heading 4 (xl semibold)
</h4>
<h5 class="text-lg font-medium text-light">
Heading 5 (lg medium)
</h5>
<h6 class="text-base font-medium text-light">
Heading 6 (base medium)
</h6>
<p class="text-base text-light/80">
Body text (base, 80% opacity) - Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Body text (base, 80% opacity) - Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
</p>
<p class="text-sm text-light/60">
Small text (sm, 60% opacity) - Used for secondary information and hints.
Small text (sm, 60% opacity) - Used for secondary
information and hints.
</p>
<p class="text-xs text-light/40">
Extra small text (xs, 40% opacity) - Used for metadata and timestamps.
Extra small text (xs, 40% opacity) - Used for metadata and
timestamps.
</p>
</div>
</section>
<!-- Toasts -->
<section class="space-y-4">
<h2
class="text-2xl font-semibold text-light border-b border-light/10 pb-2"
>
Toasts
</h2>
<div class="space-y-4">
<Toast
variant="success"
title="Success"
message="This is a success toast and will be dismissed after 5 seconds."
/>
<Toast
variant="error"
title="Error"
message="This is an error toast and must be dismissed by the user."
/>
<Toast
variant="warning"
title="Warning"
message="This is a warning toast and must be dismissed by the user."
/>
<Toast
variant="info"
title="Info"
message="This is an info toast and must be dismissed by the user."
/>
</div>
</section>
<!-- Footer -->
<footer class="text-center py-8 border-t border-light/10">
<p class="text-light/40 text-sm">Root Organization Platform - Style Guide</p>
<p class="text-light/40 text-sm">
Root Organization Platform - Style Guide
</p>
</footer>
</div>
</div>