UI redesign vol1

This commit is contained in:
AlacrisDevs
2026-02-28 17:06:03 +02:00
parent 7ab206fe96
commit 2a28d88849
37 changed files with 1722 additions and 954 deletions

View File

@@ -1,7 +1,9 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"app_name": "Root", "app_name": "Root",
"nav_home": "Home",
"nav_files": "Files", "nav_files": "Files",
"nav_storage": "Storage",
"nav_calendar": "Calendar", "nav_calendar": "Calendar",
"nav_settings": "Settings", "nav_settings": "Settings",
"nav_kanban": "Kanban", "nav_kanban": "Kanban",
@@ -43,6 +45,7 @@
"org_selector_slug_placeholder": "my-team", "org_selector_slug_placeholder": "my-team",
"org_overview": "Organization Overview", "org_overview": "Organization Overview",
"files_title": "Files", "files_title": "Files",
"files_subtitle": "Organization files are stored here.",
"files_breadcrumb_home": "Home", "files_breadcrumb_home": "Home",
"files_create_title": "Create New", "files_create_title": "Create New",
"files_type_document": "Document", "files_type_document": "Document",
@@ -82,6 +85,7 @@
"kanban_go_to_files": "Go to Files", "kanban_go_to_files": "Go to Files",
"kanban_select_board": "Select a board above", "kanban_select_board": "Select a board above",
"calendar_title": "Calendar", "calendar_title": "Calendar",
"calendar_subtitle": "Keep track of organization events here.",
"calendar_subscribe": "Subscribe to Calendar", "calendar_subscribe": "Subscribe to Calendar",
"calendar_refresh": "Refresh Events", "calendar_refresh": "Refresh Events",
"calendar_settings": "Calendar Settings", "calendar_settings": "Calendar Settings",
@@ -1035,5 +1039,21 @@
"settings_transfer_ownership": "Transfer ownership", "settings_transfer_ownership": "Transfer ownership",
"settings_transfer_confirm": "Transfer ownership to {name}? You will be demoted to admin. This action is immediate.", "settings_transfer_confirm": "Transfer ownership to {name}? You will be demoted to admin. This action is immediate.",
"toast_error_transfer_ownership": "Failed to transfer ownership", "toast_error_transfer_ownership": "Failed to transfer ownership",
"toast_success_transfer_ownership": "Ownership transferred to {name}" "toast_success_transfer_ownership": "Ownership transferred to {name}",
"home_nav_organizations": "Organizations",
"home_title": "Your Organizations",
"home_subtitle": "Select an Organization to get started.",
"home_empty_title": "No organizations yet",
"home_empty_desc": "Create your first organization to start collaborating",
"home_pending_invitations": "Pending Invitations",
"home_invite_click_accept": "Click to accept",
"home_invite_joining": "Joining...",
"home_create_org_title": "Create Organization",
"home_create_org_name_label": "Organization Name",
"home_create_org_name_placeholder": "e.g. Acme Inc",
"home_create_org_url_preview": "URL: /{slug}",
"account_settings_title": "Your Settings",
"account_settings_subtitle": "Manage your settings here.",
"user_settings_title": "User Settings",
"user_settings_subtitle": "Manage your personal account settings."
} }

View File

@@ -1,7 +1,9 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"app_name": "Root", "app_name": "Root",
"nav_home": "Avaleht",
"nav_files": "Failid", "nav_files": "Failid",
"nav_storage": "Hoidla",
"nav_calendar": "Kalender", "nav_calendar": "Kalender",
"nav_settings": "Seaded", "nav_settings": "Seaded",
"nav_kanban": "Kanban", "nav_kanban": "Kanban",
@@ -42,6 +44,7 @@
"org_selector_slug_placeholder": "minu-meeskond", "org_selector_slug_placeholder": "minu-meeskond",
"org_overview": "Organisatsiooni ülevaade", "org_overview": "Organisatsiooni ülevaade",
"files_title": "Failid", "files_title": "Failid",
"files_subtitle": "Organisatsiooni failid on siin.",
"files_breadcrumb_home": "Avaleht", "files_breadcrumb_home": "Avaleht",
"files_create_title": "Loo uus", "files_create_title": "Loo uus",
"files_type_document": "Dokument", "files_type_document": "Dokument",
@@ -81,6 +84,7 @@
"kanban_go_to_files": "Mine Failidesse", "kanban_go_to_files": "Mine Failidesse",
"kanban_select_board": "Vali tahvel ülalt", "kanban_select_board": "Vali tahvel ülalt",
"calendar_title": "Kalender", "calendar_title": "Kalender",
"calendar_subtitle": "Jälgi organisatsiooni sündmusi siin.",
"calendar_subscribe": "Telli kalender", "calendar_subscribe": "Telli kalender",
"calendar_refresh": "Värskenda sündmusi", "calendar_refresh": "Värskenda sündmusi",
"calendar_settings": "Kalendri seaded", "calendar_settings": "Kalendri seaded",
@@ -1035,5 +1039,21 @@
"settings_transfer_ownership": "Kanna omanikõigused üle", "settings_transfer_ownership": "Kanna omanikõigused üle",
"settings_transfer_confirm": "Kanna omanikõigused üle kasutajale {name}? Sind alandatakse adminiks. See toiming on kohene.", "settings_transfer_confirm": "Kanna omanikõigused üle kasutajale {name}? Sind alandatakse adminiks. See toiming on kohene.",
"toast_error_transfer_ownership": "Omanikõiguste ülekandmine ebaõnnestus", "toast_error_transfer_ownership": "Omanikõiguste ülekandmine ebaõnnestus",
"toast_success_transfer_ownership": "Omanikõigused kanti üle kasutajale {name}" "toast_success_transfer_ownership": "Omanikõigused kanti üle kasutajale {name}",
"home_nav_organizations": "Organisatsioonid",
"home_title": "Sinu organisatsioonid",
"home_subtitle": "Vali organisatsioon alustamiseks.",
"home_empty_title": "Organisatsioone pole veel",
"home_empty_desc": "Loo oma esimene organisatsioon koostöö alustamiseks",
"home_pending_invitations": "Ootel kutsed",
"home_invite_click_accept": "Kliki nõustumiseks",
"home_invite_joining": "Liitun...",
"home_create_org_title": "Loo organisatsioon",
"home_create_org_name_label": "Organisatsiooni nimi",
"home_create_org_name_placeholder": "nt. Acme OÜ",
"home_create_org_url_preview": "URL: /{slug}",
"account_settings_title": "Sinu seaded",
"account_settings_subtitle": "Halda oma seadeid siin.",
"user_settings_title": "Kasutaja seaded",
"user_settings_subtitle": "Halda oma isiklikke konto seadeid."
} }

View File

@@ -141,10 +141,7 @@
} }
function getDocColor(doc: Document): string { function getDocColor(doc: Document): string {
if (doc.type === "folder") return "text-amber-400"; return "text-white";
if (doc.type === "kanban") return "text-purple-400";
if (doc.type === "file") return "text-pink-400";
return "text-light/50";
} }
function handleItemClick(doc: Document) { function handleItemClick(doc: Document) {
@@ -638,29 +635,27 @@
</div> </div>
{/if} {/if}
<!-- Toolbar: Breadcrumbs + Actions --> <!-- Breadcrumb Bar -->
<div <div
class="flex items-center gap-2 px-6 py-3 border-b border-light/5 shrink-0" class="flex flex-wrap items-center justify-between gap-y-2 w-full shrink-0"
> >
<!-- Breadcrumb Path --> <!-- Breadcrumb Path -->
<nav class="flex items-center gap-1 flex-1 min-w-0 overflow-x-auto"> <nav class="flex items-center flex-1 min-w-0 overflow-x-auto">
{#each breadcrumbPath as crumb, i} {#each breadcrumbPath as crumb, i}
{#if i > 0} {#if i > 0}
<span class="flex items-center p-1 shrink-0">
<span <span
class="material-symbols-rounded text-light/20 shrink-0" class="material-symbols-rounded text-white"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;" style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>chevron_forward</span
> >
chevron_right
</span> </span>
{/if} {/if}
<a <a
href={getFolderUrl(crumb.id)} href={getFolderUrl(crumb.id)}
class="px-2 py-1 rounded-lg text-body-sm font-body whitespace-nowrap transition-colors class="flex items-center gap-2 shrink-0 hover:opacity-80 transition-opacity
{crumb.id === currentFolderId
? 'text-white bg-dark/30'
: 'text-light/50 hover:text-white hover:bg-dark/30'}
{dragOverBreadcrumb === (crumb.id ?? '__root__') {dragOverBreadcrumb === (crumb.id ?? '__root__')
? 'ring-2 ring-primary bg-primary/10' ? 'ring-2 ring-primary bg-primary/10 rounded-lg'
: ''}" : ''}"
ondragover={(e) => { ondragover={(e) => {
e.preventDefault(); e.preventDefault();
@@ -689,54 +684,50 @@
resetDragState(); resetDragState();
}} }}
> >
{#if i === 0} <span class="flex items-center justify-center p-1">
<span <span
class="material-symbols-rounded" class="material-symbols-rounded text-white"
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;" style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>home</span >{i === 0 ? "cloud" : "folder"}</span
>
</span>
<span
class="font-heading text-h3 text-white whitespace-nowrap"
>{crumb.name}</span
> >
{:else}
{crumb.name}
{/if}
</a> </a>
{/each} {/each}
</nav> </nav>
<div class="flex items-center gap-2 shrink-0">
{#if fileUploading} {#if fileUploading}
<div <div
class="flex items-center gap-2 px-3 py-1.5 bg-primary/5 rounded-lg" class="flex items-center gap-2 px-3 py-1.5 bg-primary/10 rounded-[32px]"
> >
<span <span
class="material-symbols-rounded text-primary animate-spin" class="material-symbols-rounded text-primary animate-spin"
style="font-size: 14px;">progress_activity</span style="font-size: 14px;">progress_activity</span
> >
<span <span
class="text-[11px] text-primary truncate max-w-[200px]" class="text-body-sm text-primary truncate max-w-[200px]"
>{fileUploadProgress}</span >{fileUploadProgress}</span
> >
</div> </div>
{/if} {/if}
<Button
size="sm"
icon="upload"
onclick={() => fileUploadInput?.click()}>Upload</Button
>
<Button size="sm" icon="add" onclick={handleAdd}
>{m.btn_new()}</Button
>
<button <button
type="button" type="button"
class="p-1.5 rounded-lg text-light/40 hover:text-white hover:bg-dark/50 transition-colors" class="flex items-center justify-center p-1 shrink-0 hover:bg-white/5 rounded-lg transition-colors"
title={m.files_toggle_view()} title={m.files_toggle_view()}
onclick={toggleViewMode} onclick={toggleViewMode}
> >
<span <span
class="material-symbols-rounded" class="material-symbols-rounded text-white"
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;" style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>{viewMode === "list" ? "grid_view" : "view_list"}</span >{viewMode === "list" ? "grid_view" : "list"}</span
> >
</button> </button>
</div> </div>
</div>
<!-- File List/Grid --> <!-- File List/Grid -->
<div class="flex-1 overflow-auto min-h-0 p-4"> <div class="flex-1 overflow-auto min-h-0 p-4">
@@ -819,28 +810,32 @@
{:else} {:else}
<!-- Grid View --> <!-- Grid View -->
<div <div
class="grid grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-2" class="flex flex-wrap gap-8 items-start content-start"
ondragover={handleContainerDragOver} ondragover={handleContainerDragOver}
ondrop={handleDropOnEmpty} ondrop={handleDropOnEmpty}
role="list" role="list"
> >
{#if currentFolderItems.length === 0} {#if currentFolderItems.length === 0}
<div <div
class="col-span-full flex flex-col items-center justify-center text-light/40 py-16" class="w-full flex flex-col items-center justify-center text-white/30 py-16"
> >
<span <span
class="material-symbols-rounded mb-3" class="material-symbols-rounded mb-3"
style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;" style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
>folder_open</span >folder_open</span
> >
<p class="text-body-sm">{m.files_empty()}</p> <p class="font-body text-body-sm">
{m.files_empty()}
</p>
</div> </div>
{:else} {:else}
{#each currentFolderItems as item} {#each currentFolderItems as item}
<button <button
type="button" type="button"
class="flex flex-col items-center gap-2 p-3 rounded-xl border border-transparent transition-all hover:bg-dark/50 hover:border-light/5 class="flex flex-col gap-2 items-center justify-center w-[240px] min-w-[240px] h-[155px] min-h-[155px] p-4 rounded-[32px] overflow-clip transition-all cursor-pointer
{selectedDoc?.id === item.id ? 'bg-dark/50 border-primary/20' : ''} {selectedDoc?.id === item.id
? 'bg-surface ring-2 ring-primary/50'
: 'hover:bg-surface/50'}
{draggedItem?.id === item.id ? 'opacity-50' : ''} {draggedItem?.id === item.id ? 'opacity-50' : ''}
{dragOverFolder === item.id ? 'ring-2 ring-primary bg-primary/10' : ''}" {dragOverFolder === item.id ? 'ring-2 ring-primary bg-primary/10' : ''}"
draggable="true" draggable="true"
@@ -856,27 +851,20 @@
handleContextMenu(e, item)} handleContextMenu(e, item)}
> >
<span <span
class="material-symbols-rounded {getDocColor( class="flex items-center justify-center p-3"
item, >
)}" <span
style="font-size: 40px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 40;" class="material-symbols-rounded text-white"
style="font-size: 72px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
> >
{getDocIcon(item)} {getDocIcon(item)}
</span> </span>
<span </span>
class="font-body text-[12px] text-white text-center truncate w-full" <p
>{item.name}</span class="font-body text-body text-white text-center truncate w-full min-w-full"
> >
{#if item.type === "file"} {item.name}
{@const fileMeta = getFileMetadata(item)} </p>
{#if fileMeta}
<span class="text-[10px] text-light/25"
>{formatFileSize(
fileMeta.file_size,
)}</span
>
{/if}
{/if}
</button> </button>
{/each} {/each}
{/if} {/if}
@@ -961,7 +949,7 @@
: m.files_doc_placeholder()} : m.files_doc_placeholder()}
/> />
<div class="flex justify-end gap-2 pt-2"> <div class="flex justify-end gap-2 pt-2">
<Button variant="tertiary" onclick={() => (showCreateModal = false)} <Button variant="ghost" onclick={() => (showCreateModal = false)}
>{m.btn_cancel()}</Button >{m.btn_cancel()}</Button
> >
<Button onclick={handleCreate} disabled={!newDocName.trim()} <Button onclick={handleCreate} disabled={!newDocName.trim()}
@@ -1092,7 +1080,7 @@
/> />
<div class="flex justify-end gap-2 pt-2"> <div class="flex justify-end gap-2 pt-2">
<Button <Button
variant="tertiary" variant="ghost"
onclick={() => { onclick={() => {
showEditModal = false; showEditModal = false;
editingDoc = null; editingDoc = null;

View File

@@ -924,7 +924,7 @@
<div></div> <div></div>
{/if} {/if}
<div class="flex gap-2"> <div class="flex gap-2">
<Button variant="tertiary" onclick={onClose} <Button variant="ghost" onclick={onClose}
>{m.btn_cancel()}</Button >{m.btn_cancel()}</Button
> >
<Button <Button

View File

@@ -167,7 +167,7 @@
</script> </script>
<div <div
class="flex gap-2 overflow-x-auto pb-4 h-full kanban-scroll" class="flex gap-4 overflow-x-auto pb-4 h-full kanban-scroll"
role="presentation" role="presentation"
> >
{#each columns as column, colIndex (column.id)} {#each columns as column, colIndex (column.id)}
@@ -301,7 +301,7 @@
<!-- Add Card Button --> <!-- Add Card Button -->
{#if canEdit} {#if canEdit}
<Button <Button
variant="secondary" variant="primary"
fullWidth fullWidth
icon="add" icon="add"
onclick={() => onAddCard?.(column.id)} onclick={() => onAddCard?.(column.id)}
@@ -315,7 +315,7 @@
{#if canEdit} {#if canEdit}
<div class="flex-shrink-0 w-[256px]"> <div class="flex-shrink-0 w-[256px]">
<Button <Button
variant="secondary" variant="outline"
fullWidth fullWidth
icon="add" icon="add"
onclick={() => onAddColumn?.()} onclick={() => onAddColumn?.()}

View File

@@ -58,7 +58,7 @@
<button <button
type="button" type="button"
class="bg-night/80 border border-light/5 hover:border-light/10 rounded-xl px-3 py-2.5 cursor-pointer transition-all group w-full text-left flex flex-col gap-1.5 relative" class="bg-surface rounded-[16px] p-4 cursor-pointer transition-all group w-full text-left flex flex-col gap-2 relative hover:ring-1 hover:ring-white/10"
class:opacity-50={isDragging} class:opacity-50={isDragging}
data-card-id={card.id} data-card-id={card.id}
{draggable} {draggable}
@@ -71,13 +71,13 @@
<!-- svelte-ignore node_invalid_placement_ssr --> <!-- svelte-ignore node_invalid_placement_ssr -->
<button <button
type="button" type="button"
class="absolute top-1.5 right-1.5 p-0.5 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-error/10 transition-all z-10" class="absolute top-2 right-2 p-0.5 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-error/10 transition-all z-10"
onclick={handleDelete} onclick={handleDelete}
aria-label="Delete card" aria-label="Delete card"
> >
<span <span
class="material-symbols-rounded text-light/30 hover:text-error" class="material-symbols-rounded text-white/30 hover:text-error"
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
> >
close close
</span> </span>
@@ -86,10 +86,10 @@
<!-- Tags / Chips --> <!-- Tags / Chips -->
{#if card.tags && card.tags.length > 0} {#if card.tags && card.tags.length > 0}
<div class="flex gap-1 items-start flex-wrap"> <div class="flex gap-1.5 items-start flex-wrap">
{#each card.tags as tag} {#each card.tags as tag}
<span <span
class="rounded-[4px] px-1.5 py-0.5 font-body font-bold text-[11px] text-night leading-none" class="rounded-[32px] px-2.5 py-1 font-body font-bold text-body-sm text-night leading-none"
style="background-color: {tag.color || '#00A3E0'}" style="background-color: {tag.color || '#00A3E0'}"
> >
{tag.name} {tag.name}
@@ -99,19 +99,19 @@
{/if} {/if}
<!-- Title --> <!-- Title -->
<p class="font-body text-body-sm text-white w-full leading-snug"> <p class="font-body text-body text-white w-full leading-snug">
{card.title} {card.title}
</p> </p>
<!-- Bottom row: details + avatar --> <!-- Bottom row: details + avatar -->
{#if hasFooter} {#if hasFooter}
<div class="flex items-center justify-between w-full mt-0.5"> <div class="flex items-center justify-between w-full mt-1">
<div class="flex gap-2 items-center text-[11px] text-light/40"> <div class="flex gap-3 items-center text-body-sm text-white/40">
{#if card.due_date} {#if card.due_date}
<span class="flex items-center gap-0.5"> <span class="flex items-center gap-1">
<span <span
class="material-symbols-rounded" class="material-symbols-rounded"
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;" style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
>calendar_today</span >calendar_today</span
> >
{formatDueDate(card.due_date)} {formatDueDate(card.due_date)}
@@ -119,10 +119,10 @@
{/if} {/if}
{#if (card.checklist_total ?? 0) > 0} {#if (card.checklist_total ?? 0) > 0}
<span class="flex items-center gap-0.5"> <span class="flex items-center gap-1">
<span <span
class="material-symbols-rounded" class="material-symbols-rounded"
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;" style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
>check_box</span >check_box</span
> >
{card.checklist_done ?? 0}/{card.checklist_total} {card.checklist_done ?? 0}/{card.checklist_total}
@@ -134,7 +134,7 @@
<Avatar <Avatar
name={card.assignee_name || "?"} name={card.assignee_name || "?"}
src={card.assignee_avatar} src={card.assignee_avatar}
size="xs" size="sm"
/> />
{/if} {/if}
</div> </div>

View File

@@ -429,7 +429,7 @@
<Spinner /> <Spinner />
{:else if hasMore} {:else if hasMore}
<Button <Button
variant="tertiary" variant="ghost"
size="sm" size="sm"
icon="expand_more" icon="expand_more"
onclick={() => loadEntries(false)} onclick={() => loadEntries(false)}

View File

@@ -395,7 +395,7 @@
</Button> </Button>
{#if avatarUrl} {#if avatarUrl}
<Button <Button
variant="tertiary" variant="ghost"
size="sm" size="sm"
onclick={removeAvatar} onclick={removeAvatar}
> >

View File

@@ -298,7 +298,7 @@
</code> </code>
<Button <Button
size="sm" size="sm"
variant="tertiary" variant="ghost"
onclick={copyServiceEmail} onclick={copyServiceEmail}
> >
{emailCopied ? "Copied!" : "Copy"} {emailCopied ? "Copied!" : "Copy"}
@@ -330,9 +330,8 @@
{/if} {/if}
<div class="flex justify-end gap-2 pt-2"> <div class="flex justify-end gap-2 pt-2">
<Button <Button variant="ghost" onclick={() => (showConnectModal = false)}
variant="tertiary" >Cancel</Button
onclick={() => (showConnectModal = false)}>Cancel</Button
> >
<Button <Button
onclick={handleSaveOrgCalendar} onclick={handleSaveOrgCalendar}

View File

@@ -0,0 +1,63 @@
<script lang="ts">
interface BreadcrumbItem {
label: string;
href?: string;
icon: string;
}
interface Props {
items: BreadcrumbItem[];
onToggleView?: () => void;
viewMode?: 'grid' | 'list';
}
let { items, onToggleView, viewMode = 'grid' }: Props = $props();
</script>
<div class="flex flex-wrap items-center justify-between gap-y-2 w-full">
<div class="flex items-center">
{#each items as item, i}
{#if i > 0}
<span class="flex items-center p-1 shrink-0">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>chevron_forward</span>
</span>
{/if}
{#if item.href}
<a href={item.href} class="flex items-center gap-2 shrink-0 hover:opacity-80 transition-opacity">
<span class="flex items-center justify-center p-1">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>{item.icon}</span>
</span>
<span class="font-heading text-h3 text-white whitespace-nowrap">{item.label}</span>
</a>
{:else}
<div class="flex items-center gap-2 shrink-0">
<span class="flex items-center justify-center p-1">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>{item.icon}</span>
</span>
<span class="font-heading text-h3 text-white whitespace-nowrap">{item.label}</span>
</div>
{/if}
{/each}
</div>
{#if onToggleView}
<button
type="button"
class="flex items-center justify-center p-1 shrink-0 hover:bg-white/5 rounded-lg transition-colors"
onclick={onToggleView}
>
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>{viewMode === 'grid' ? 'list' : 'grid_view'}</span>
</button>
{/if}
</div>

View File

@@ -2,7 +2,13 @@
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
interface Props { interface Props {
variant?: "primary" | "secondary" | "tertiary" | "danger" | "success"; variant?:
| "primary"
| "secondary"
| "outline"
| "danger"
| "success"
| "ghost";
size?: "sm" | "md" | "lg"; size?: "sm" | "md" | "lg";
disabled?: boolean; disabled?: boolean;
loading?: boolean; loading?: boolean;
@@ -28,36 +34,26 @@
}: Props = $props(); }: Props = $props();
const baseClasses = const baseClasses =
"inline-flex items-center justify-center gap-2 font-heading rounded-[32px] overflow-clip transition-all cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"; "inline-flex items-center justify-center gap-2 font-bold font-body rounded-[32px] overflow-clip transition-all cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed";
const variantClasses = { const variantClasses = {
primary: primary:
"btn-primary bg-primary text-night hover:btn-primary-hover active:btn-primary-active", "bg-primary text-night hover:brightness-110 active:brightness-90",
secondary: secondary: "bg-night text-white hover:bg-surface active:bg-dark",
"bg-transparent text-primary border-solid border-primary hover:bg-primary/10 active:bg-primary/20", outline:
tertiary: "bg-transparent text-primary border-2 border-primary hover:bg-primary/10 active:bg-primary/20",
"bg-primary/10 text-primary hover:bg-primary/20 active:bg-primary/30", danger: "bg-error text-white hover:brightness-110 active:brightness-90",
danger: "btn-primary bg-error text-white hover:btn-primary-hover active:btn-primary-active",
success: success:
"btn-primary bg-success text-night hover:btn-primary-hover active:btn-primary-active", "bg-success text-night hover:brightness-110 active:brightness-90",
ghost: "bg-transparent text-white hover:bg-white/5 active:bg-white/10",
}; };
const sizeClasses = { const sizeClasses = {
sm: "min-w-[36px] p-[10px] text-btn-sm", sm: "min-w-[36px] px-[12px] py-[8px] text-btn-sm",
md: "min-w-[48px] p-[12px] text-btn-md", md: "min-w-[48px] px-[14px] py-[10px] text-btn-md",
lg: "min-w-[56px] p-[16px] text-btn-lg", lg: "min-w-[56px] px-[16px] py-[12px] text-btn-lg",
}; };
const borderClasses = {
sm: "border-2",
md: "border-3",
lg: "border-4",
};
const secondaryBorder = $derived(
variant === "secondary" ? borderClasses[size] : "",
);
const iconSize = $derived(size === "sm" ? 16 : size === "lg" ? 20 : 18); const iconSize = $derived(size === "sm" ? 16 : size === "lg" ? 20 : 18);
</script> </script>
@@ -65,7 +61,7 @@
{type} {type}
class="{baseClasses} {variantClasses[variant]} {sizeClasses[ class="{baseClasses} {variantClasses[variant]} {sizeClasses[
size size
]} {secondaryBorder} {className ?? ''}" ]} {className ?? ''}"
class:w-full={fullWidth} class:w-full={fullWidth}
disabled={disabled || loading} disabled={disabled || loading}
{onclick} {onclick}
@@ -89,30 +85,3 @@
{@render children()} {@render children()}
{/if} {/if}
</button> </button>
<style>
.btn-primary:hover:not(:disabled) {
background-image: linear-gradient(
rgba(255, 255, 255, 0.2),
rgba(255, 255, 255, 0.2)
);
}
.btn-primary-hover:not(:disabled) {
background-image: linear-gradient(
rgba(255, 255, 255, 0.2),
rgba(255, 255, 255, 0.2)
);
}
.btn-primary:active:not(:disabled) {
background-image: linear-gradient(
rgba(14, 15, 25, 0.2),
rgba(14, 15, 25, 0.2)
);
}
.btn-primary-active:not(:disabled) {
background-image: linear-gradient(
rgba(14, 15, 25, 0.2),
rgba(14, 15, 25, 0.2)
);
}
</style>

View File

@@ -4,35 +4,49 @@
interface Props { interface Props {
title: string; title: string;
subtitle?: string;
actionLabel?: string; actionLabel?: string;
actionIcon?: string;
onAction?: () => void; onAction?: () => void;
onMore?: () => void; onMore?: () => void;
children?: Snippet; children?: Snippet;
} }
let { title, actionLabel, onAction, onMore, children }: Props = $props(); let {
title,
subtitle,
actionLabel,
actionIcon,
onAction,
onMore,
children,
}: Props = $props();
</script> </script>
<div class="flex flex-wrap items-center gap-2 p-1 rounded-[32px] w-full"> <div class="flex items-center justify-between w-full shrink-0">
<div class="flex-1 min-w-0"> <div class="flex flex-1 flex-col gap-2 items-start min-w-0">
<h1 class="font-heading text-h1 text-white truncate">{title}</h1> <h2 class="font-heading text-h2 text-white">{title}</h2>
{#if subtitle}
<p class="font-body text-body text-white/50">{subtitle}</p>
{/if}
</div> </div>
<div class="flex items-center gap-2 shrink-0">
{#if children} {#if children}
{@render children()} {@render children()}
{/if} {/if}
{#if actionLabel && onAction} {#if actionLabel && onAction}
<Button variant="primary" onclick={onAction}> <Button variant="primary" icon={actionIcon} onclick={onAction}>
{actionLabel} {actionLabel}
</Button> </Button>
{/if} {/if}
{#if onMore} {#if onMore}
<button <button
type="button" type="button"
class="p-1 flex items-center justify-center hover:bg-dark/50 rounded-full transition-colors" class="flex items-center justify-center p-1 rounded-full hover:bg-white/5 transition-colors"
onclick={onMore} onclick={onMore}
> >
<span <span
class="material-symbols-rounded text-light" class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;" style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
> >
more_horiz more_horiz
@@ -40,3 +54,4 @@
</button> </button>
{/if} {/if}
</div> </div>
</div>

View File

@@ -14,6 +14,7 @@
value?: string; value?: string;
placeholder?: string; placeholder?: string;
label?: string; label?: string;
description?: string;
error?: string; error?: string;
hint?: string; hint?: string;
disabled?: boolean; disabled?: boolean;
@@ -33,6 +34,7 @@
value = $bindable(""), value = $bindable(""),
placeholder = "", placeholder = "",
label, label,
description,
error, error,
hint, hint,
disabled = false, disabled = false,
@@ -55,16 +57,22 @@
const inputType = $derived(isPassword && showPassword ? "text" : type); const inputType = $derived(isPassword && showPassword ? "text" : type);
</script> </script>
<div class="flex flex-col {isCompact ? 'gap-1.5' : 'gap-3'} w-full"> <div class="flex flex-col gap-3 w-full">
{#if label} {#if label}
<div class="flex flex-col gap-2 {isCompact ? '' : 'px-3'}">
<label <label
for={inputId} for={inputId}
class={isCompact class={isCompact
? "font-body text-body-sm text-light/60" ? "font-body text-body-sm text-light/60"
: "px-3 font-bold font-body text-body text-white"} : "font-bold font-body text-body text-white"}
> >
{#if required}<span class="text-error">* </span>{/if}{label} {#if required}<span class="text-error">*&nbsp;</span
>{/if}{label}
</label> </label>
{#if description}
<p class="text-body-sm text-white/50">{description}</p>
{/if}
</div>
{/if} {/if}
<div class="flex items-center gap-3 w-full"> <div class="flex items-center gap-3 w-full">
@@ -97,23 +105,20 @@
class=" class="
w-full {isCompact w-full {isCompact
? 'px-3 py-2 bg-dark border border-light/10 rounded-xl text-body-sm' ? 'px-3 py-2 bg-dark border border-light/10 rounded-xl text-body-sm'
: 'p-3 bg-background rounded-[32px] font-medium font-input text-body'} : 'px-3 py-2 bg-night rounded-[20px] font-medium font-input text-body'}
text-white placeholder:text-light/30 text-white placeholder:text-white/50
focus:outline-none {isCompact focus:outline-none focus:ring-2 focus:ring-primary/50
? 'focus:border-primary'
: 'focus:ring-2 focus:ring-primary'}
disabled:opacity-30 disabled:cursor-not-allowed disabled:opacity-30 disabled:cursor-not-allowed
transition-colors {className ?? ''} transition-colors {className ?? ''}
" "
class:ring-1={error && !isCompact} class:ring-1={error}
class:ring-error={error && !isCompact} class:ring-error={error}
class:border-error={error && isCompact}
class:pr-12={isPassword} class:pr-12={isPassword}
/> />
{#if isPassword} {#if isPassword}
<button <button
type="button" type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-white/60 hover:text-white transition-colors" class="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-full text-white/60 hover:text-white transition-colors"
onclick={() => (showPassword = !showPassword)} onclick={() => (showPassword = !showPassword)}
aria-label={showPassword aria-label={showPassword
? "Hide password" ? "Hide password"
@@ -121,7 +126,7 @@
> >
<span <span
class="material-symbols-rounded" class="material-symbols-rounded"
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;" style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
> >
{showPassword ? "visibility_off" : "visibility"} {showPassword ? "visibility_off" : "visibility"}
</span> </span>

View File

@@ -19,10 +19,13 @@
<!-- Header --> <!-- Header -->
<div class="flex items-center gap-2 px-3 py-2.5 border-b border-light/5"> <div class="flex items-center gap-2 px-3 py-2.5 border-b border-light/5">
<div class="flex-1 flex items-center gap-2 min-w-0"> <div class="flex-1 flex items-center gap-2 min-w-0">
<span class="font-heading text-body-sm text-white truncate">{title}</span> <span class="font-heading text-body-sm text-white truncate"
>{title}</span
>
<span <span
class="text-[11px] text-light/40 bg-light/5 px-1.5 py-0.5 rounded-md shrink-0" class="text-[11px] text-light/40 bg-light/5 px-1.5 py-0.5 rounded-md shrink-0"
>{count}</span> >{count}</span
>
</div> </div>
{#if onMore} {#if onMore}
<button <button
@@ -41,9 +44,7 @@
</div> </div>
<!-- Cards container --> <!-- Cards container -->
<div <div class="flex-1 flex flex-col gap-1.5 p-2 overflow-y-auto min-h-0">
class="flex-1 flex flex-col gap-1.5 p-2 overflow-y-auto min-h-0"
>
{#if children} {#if children}
{@render children()} {@render children()}
{/if} {/if}
@@ -52,7 +53,13 @@
<!-- Add button --> <!-- Add button -->
{#if onAddCard} {#if onAddCard}
<div class="px-2 pb-2"> <div class="px-2 pb-2">
<Button variant="tertiary" fullWidth size="sm" icon="add" onclick={onAddCard}> <Button
variant="ghost"
fullWidth
size="sm"
icon="add"
onclick={onAddCard}
>
Add card Add card
</Button> </Button>
</div> </div>

View File

@@ -0,0 +1,55 @@
<script lang="ts">
interface Props {
label: string;
type: 'folder' | 'document' | 'kanban';
href?: string;
onclick?: () => void;
ondblclick?: () => void;
onauxclick?: (e: MouseEvent) => void;
selected?: boolean;
}
let { label, type, href, onclick, ondblclick, onauxclick, selected = false }: Props = $props();
const iconMap = {
folder: 'folder',
document: 'description',
kanban: 'view_kanban',
};
</script>
{#if href}
<a
{href}
class="flex flex-col gap-2 items-center justify-center w-[240px] min-w-[240px] h-[155px] min-h-[155px] p-4 rounded-[32px] overflow-clip transition-all cursor-pointer
{selected ? 'bg-surface ring-2 ring-primary/50' : 'hover:bg-surface/50'}"
{onclick}
{ondblclick}
{onauxclick}
>
<span class="flex items-center justify-center p-3">
<span
class="material-symbols-rounded text-white"
style="font-size: 72px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
>{iconMap[type]}</span>
</span>
<p class="font-body text-body text-white text-center truncate w-full min-w-full">{label}</p>
</a>
{:else}
<button
type="button"
class="flex flex-col gap-2 items-center justify-center w-[240px] min-w-[240px] h-[155px] min-h-[155px] p-4 rounded-[32px] overflow-clip transition-all cursor-pointer
{selected ? 'bg-surface ring-2 ring-primary/50' : 'hover:bg-surface/50'}"
{onclick}
{ondblclick}
{onauxclick}
>
<span class="flex items-center justify-center p-3">
<span
class="material-symbols-rounded text-white"
style="font-size: 72px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
>{iconMap[type]}</span>
</span>
<p class="font-body text-body text-white text-center truncate w-full min-w-full">{label}</p>
</button>
{/if}

View File

@@ -20,9 +20,7 @@
}: Props = $props(); }: Props = $props();
</script> </script>
<header <header class="flex items-center justify-between shrink-0 {className}">
class="flex items-center justify-between px-6 py-5 border-b border-light/5 shrink-0 {className}"
>
<div class="flex items-center gap-3 min-w-0"> <div class="flex items-center gap-3 min-w-0">
{#if icon} {#if icon}
<span <span
@@ -31,10 +29,10 @@
>{icon}</span >{icon}</span
> >
{/if} {/if}
<div class="min-w-0"> <div class="min-w-0 flex flex-col gap-2">
<h1 class="text-h1 font-heading text-white truncate">{title}</h1> <h2 class="text-h2 font-heading text-white truncate">{title}</h2>
{#if subtitle} {#if subtitle}
<p class="text-body-sm text-light/50 mt-0.5">{subtitle}</p> <p class="text-body text-white/50 font-body">{subtitle}</p>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -24,13 +24,11 @@
}; };
</script> </script>
<div <div class="bg-surface rounded-[32px] {paddingClasses[padding]} {className}">
class="bg-dark/30 border border-light/5 rounded-xl {paddingClasses[padding]} {className}"
>
{#if title || titleRight} {#if title || titleRight}
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
{#if title} {#if title}
<h2 class="text-body font-heading text-white">{title}</h2> <h3 class="text-h4 font-heading text-white">{title}</h3>
{/if} {/if}
{#if titleRight} {#if titleRight}
{@render titleRight()} {@render titleRight()}

View File

@@ -66,11 +66,9 @@
{onchange} {onchange}
class="w-full {isCompact class="w-full {isCompact
? 'px-3 py-2 bg-dark border border-light/10 rounded-xl text-body-sm' ? 'px-3 py-2 bg-dark border border-light/10 rounded-xl text-body-sm'
: 'p-3 bg-background rounded-[32px] font-medium font-input text-body'} : 'px-3 py-2 bg-night rounded-[20px] font-medium font-input text-body'}
text-white text-white
focus:outline-none {isCompact focus:outline-none focus:ring-2 focus:ring-primary/50
? 'focus:border-primary'
: 'focus:ring-2 focus:ring-primary'}
disabled:opacity-30 disabled:cursor-not-allowed disabled:opacity-30 disabled:cursor-not-allowed
transition-colors appearance-none cursor-pointer {className ?? ''}" transition-colors appearance-none cursor-pointer {className ?? ''}"
class:ring-1={error && !isCompact} class:ring-1={error && !isCompact}

View File

@@ -21,38 +21,36 @@
{#if href} {#if href}
<a <a
{href} {href}
class="bg-dark/30 border border-light/5 hover:border-light/10 rounded-xl p-4 flex items-center gap-3 transition-all group" class="bg-surface rounded-[32px] p-5 flex items-center gap-4 transition-all hover:ring-1 hover:ring-white/10 group"
> >
<div <div
class="w-10 h-10 rounded-xl {bg} flex items-center justify-center shrink-0" class="w-12 h-12 rounded-full {bg} flex items-center justify-center shrink-0"
> >
<span <span
class="material-symbols-rounded {color}" class="material-symbols-rounded {color}"
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;" style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>{icon}</span >{icon}</span
> >
</div> </div>
<div> <div class="flex flex-col gap-1">
<p class="text-xl font-bold text-white leading-none">{value}</p> <p class="text-h2 font-heading text-white leading-none">{value}</p>
<p class="text-[12px] text-light/40 mt-0.5">{label}</p> <p class="text-body font-body text-white/50">{label}</p>
</div> </div>
</a> </a>
{:else} {:else}
<div class="bg-surface rounded-[32px] p-5 flex items-center gap-4">
<div <div
class="bg-dark/30 border border-light/5 rounded-xl p-4 flex items-center gap-3" class="w-12 h-12 rounded-full {bg} flex items-center justify-center shrink-0"
>
<div
class="w-10 h-10 rounded-xl {bg} flex items-center justify-center shrink-0"
> >
<span <span
class="material-symbols-rounded {color}" class="material-symbols-rounded {color}"
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;" style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>{icon}</span >{icon}</span
> >
</div> </div>
<div> <div class="flex flex-col gap-1">
<p class="text-xl font-bold text-white leading-none">{value}</p> <p class="text-h2 font-heading text-white leading-none">{value}</p>
<p class="text-[12px] text-light/40 mt-0.5">{label}</p> <p class="text-body font-body text-white/50">{label}</p>
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -43,3 +43,5 @@ export { default as Twemoji } from './Twemoji.svelte';
export { default as EmojiPicker } from './EmojiPicker.svelte'; export { default as EmojiPicker } from './EmojiPicker.svelte';
export { default as VirtualList } from './VirtualList.svelte'; export { default as VirtualList } from './VirtualList.svelte';
export { default as PersonContactModal } from './PersonContactModal.svelte'; export { default as PersonContactModal } from './PersonContactModal.svelte';
export { default as Breadcrumb } from './Breadcrumb.svelte';
export { default as ListCard } from './ListCard.svelte';

View File

@@ -36,7 +36,9 @@ ${dump}
<div class="space-y-2"> <div class="space-y-2">
<p class="text-[80px] font-heading text-primary">{$page.status}</p> <p class="text-[80px] font-heading text-primary">{$page.status}</p>
<h1 class="text-2xl font-heading text-white"> <h1 class="text-2xl font-heading text-white">
{$page.status === 404 ? "Page not found" : "Something went wrong"} {$page.status === 404
? "Page not found"
: "Something went wrong"}
</h1> </h1>
<p class="text-light/60 text-base"> <p class="text-light/60 text-base">
{$page.error?.message || "An unexpected error occurred."} {$page.error?.message || "An unexpected error occurred."}
@@ -54,10 +56,10 @@ ${dump}
</div> </div>
<div class="flex gap-3 justify-center flex-wrap"> <div class="flex gap-3 justify-center flex-wrap">
<Button onclick={() => window.location.href = "/"}> <Button onclick={() => (window.location.href = "/")}>
Go Home Go Home
</Button> </Button>
<Button variant="tertiary" onclick={() => window.location.reload()}> <Button variant="ghost" onclick={() => window.location.reload()}>
Retry Retry
</Button> </Button>
<Button variant="secondary" onclick={handleCopyLogs}> <Button variant="secondary" onclick={handleCopyLogs}>
@@ -75,7 +77,9 @@ ${dump}
</button> </button>
{#if showLogs} {#if showLogs}
<pre class="mt-4 p-4 bg-dark rounded-[16px] text-left text-xs text-light/70 overflow-auto max-h-[300px] font-mono whitespace-pre-wrap">{logDump || "No recent logs."}</pre> <pre
class="mt-4 p-4 bg-dark rounded-[16px] text-left text-xs text-light/70 overflow-auto max-h-[300px] font-mono whitespace-pre-wrap">{logDump ||
"No recent logs."}</pre>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -41,8 +41,21 @@ export const load: PageServerLoad = async ({ locals }) => {
org: (inv as Record<string, unknown>).organizations as { id: string; name: string; slug: string } | null, org: (inv as Record<string, unknown>).organizations as { id: string; name: string; slug: string } | null,
})).filter((inv) => inv.org !== null); })).filter((inv) => inv.org !== null);
// Fetch user profile for sidebar
const { data: profile } = await locals.supabase
.from('profiles')
.select('full_name, avatar_url')
.eq('id', user.id)
.single();
return { return {
organizations, organizations,
pendingInvites, pendingInvites,
user: {
id: user.id,
email: user.email,
fullName: profile?.full_name ?? user.user_metadata?.full_name ?? null,
avatarUrl: profile?.avatar_url ?? null,
},
}; };
}; };

View File

@@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { getContext } from "svelte"; import { getContext } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { Modal, Logo, Badge } from "$lib/components/ui"; import { page } from "$app/stores";
import { Modal, Logo, Avatar, Button, Input } from "$lib/components/ui";
import * as m from "$lib/paraglide/messages";
import { createOrganization, generateSlug } from "$lib/api/organizations"; import { createOrganization, generateSlug } from "$lib/api/organizations";
import { toasts } from "$lib/stores/toast.svelte"; import { toasts } from "$lib/stores/toast.svelte";
import type { SupabaseClient } from "@supabase/supabase-js"; import type { SupabaseClient } from "@supabase/supabase-js";
@@ -27,7 +29,12 @@
data: { data: {
organizations: OrgWithRole[]; organizations: OrgWithRole[];
pendingInvites: PendingInvite[]; pendingInvites: PendingInvite[];
user: any; user: {
id: string;
email: string | undefined;
fullName: string | null;
avatarUrl: string | null;
};
}; };
} }
@@ -122,250 +129,292 @@
<title>Organizations | root</title> <title>Organizations | root</title>
</svelte:head> </svelte:head>
<div class="min-h-screen bg-background"> <div class="h-screen bg-background flex gap-4 items-start p-4">
<!-- Header --> <!-- Sidebar -->
<header class="border-b border-light/5"> <aside
<div class="bg-night flex flex-col gap-4 h-full items-start min-w-[256px] overflow-clip px-4 py-5 rounded-[32px] shrink-0 w-[256px]"
class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between"
> >
<div class="flex items-center gap-2.5"> <!-- Logo -->
<Logo size="sm" /> <Logo size="sm" />
</div>
<div class="flex items-center gap-2"> <!-- Nav list -->
{#if pendingInvites.length > 0} <nav class="flex flex-1 flex-col gap-1 items-start w-full min-h-0">
{#each [{ href: "/", icon: "apartment", label: m.home_nav_organizations() }, { href: "/settings", icon: "settings", label: m.nav_settings() }] as item}
{@const active =
item.href === "/"
? $page.url.pathname === "/"
: $page.url.pathname.startsWith(item.href)}
<a <a
href="#invites" href={item.href}
class="relative p-2 text-light/40 hover:text-white hover:bg-dark/50 rounded-xl transition-colors" class="flex gap-2 items-center w-full overflow-clip pl-1 pr-2 py-1 rounded-[32px] transition-colors {active
title="{pendingInvites.length} pending invite{pendingInvites.length > ? 'bg-primary'
1 : 'hover:bg-white/5'}"
? 's'
: ''}"
> >
<span class="flex items-center p-1 shrink-0">
<span <span
class="material-symbols-rounded" class="material-symbols-rounded {active
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;" ? 'text-background'
>notifications</span : 'text-white'}"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>{item.icon}</span
> >
<span
class="absolute -top-0.5 -right-0.5 w-5 h-5 bg-primary text-background text-[10px] font-bold rounded-full flex items-center justify-center"
>
{pendingInvites.length}
</span> </span>
<span
class="flex-1 font-body text-body truncate {active
? 'text-background'
: 'text-white'}">{item.label}</span
>
</a> </a>
{/if} {/each}
</nav>
<!-- Profile footer -->
<div class="flex gap-4 items-center w-full">
<div class="flex flex-1 gap-4 items-center min-w-0">
<Avatar
name={data.user.fullName || data.user.email || "User"}
src={data.user.avatarUrl}
size="sm"
/>
<div class="flex flex-1 flex-col items-start min-w-0">
<p class="font-heading text-h5 text-white w-full truncate">
{data.user.fullName || data.user.email || "User"}
</p>
</div>
</div>
<form method="POST" action="/auth/logout"> <form method="POST" action="/auth/logout">
<button <button
type="submit" type="submit"
class="px-3 py-1.5 text-body-sm text-light/40 hover:text-white hover:bg-dark/50 rounded-xl transition-colors" class="flex items-center justify-center p-1 rounded-[32px] text-white/60 hover:text-white transition-colors"
>Sign Out</button title="Sign out"
> >
<span
class="material-symbols-rounded"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>exit_to_app</span
>
</button>
</form> </form>
</div> </div>
</div> </aside>
</header>
<!-- Content -->
<main
class="bg-night flex flex-1 flex-col gap-8 h-full items-start min-h-0 min-w-0 overflow-auto p-8 rounded-[32px]"
>
<!-- Header -->
<div class="flex items-center justify-between w-full shrink-0">
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
<h2 class="font-heading text-h2 text-white">
{m.home_title()}
</h2>
<p class="font-body text-body text-white/50">
{m.home_subtitle()}
</p>
</div>
<Button icon="add" onclick={() => (showCreateModal = true)}>
{m.btn_new()}
</Button>
</div>
<main class="max-w-5xl mx-auto px-6 py-8">
<!-- Pending Invites --> <!-- Pending Invites -->
{#if pendingInvites.length > 0} {#if pendingInvites.length > 0}
<section id="invites" class="mb-8"> <section id="invites" class="w-full shrink-0">
<div class="flex items-center gap-2 mb-4"> <div class="flex items-center gap-2 mb-4">
<span <span
class="material-symbols-rounded text-primary" class="material-symbols-rounded text-primary"
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;" style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;"
>mail</span >mail</span
> >
<h2 class="font-heading text-body text-white"> <h3 class="font-heading text-h3 text-white">
Pending Invitations {m.home_pending_invitations()}
</h2> </h3>
<span <span
class="text-[11px] px-2 py-0.5 bg-primary/10 text-primary rounded-lg font-body" class="text-body-sm px-2 py-0.5 bg-surface text-primary rounded-[8px] font-body"
> >
{pendingInvites.length} {pendingInvites.length}
</span> </span>
</div> </div>
<div <div class="flex flex-wrap gap-8 items-start">
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"
>
{#each pendingInvites as invite} {#each pendingInvites as invite}
<div <a
class="bg-primary/5 border border-primary/20 rounded-2xl p-5 transition-all" href="/{invite.org.slug}"
class="bg-background flex flex-col items-start overflow-clip rounded-[16px] w-[256px] cursor-pointer hover:ring-2 hover:ring-primary/30 transition-all"
onclick={(e) => {
e.preventDefault();
acceptInvite(invite);
}}
> >
<div class="flex items-start justify-between mb-3">
<div <div
class="w-10 h-10 bg-primary/10 rounded-xl flex items-center justify-center text-primary font-heading text-body" class="h-[80px] w-full bg-primary/10 flex items-center justify-center"
> >
{invite.org.name.charAt(0).toUpperCase()} <span
class="material-symbols-rounded text-primary"
style="font-size: 32px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 32;"
>mail</span
>
</div>
<div
class="flex flex-col gap-4 items-start justify-center px-4 py-5 w-full"
>
<div
class="flex items-start justify-between w-full"
>
<div
class="w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center text-primary font-heading text-h5"
>
{invite.org.name
.charAt(0)
.toUpperCase()}
</div> </div>
<span <span
class="text-[10px] px-2 py-0.5 bg-primary/10 rounded-lg text-primary capitalize font-body" class="bg-surface flex items-center justify-center p-1 rounded-[8px] font-body text-body-md text-white capitalize"
>{invite.role}</span >{invite.role}</span
> >
</div> </div>
<h3 <div class="flex flex-col gap-1 w-full">
class="font-heading text-body-sm text-white mb-1" <p class="font-heading text-h5 text-white">
>
{invite.org.name} {invite.org.name}
</h3>
<p class="text-[11px] text-light/30 mb-4 font-body">
You've been invited to join this organization
</p> </p>
<div class="flex items-center gap-2"> <p
<button class="font-body text-body-md text-white/50"
type="button"
disabled={acceptingInviteId === invite.id}
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-primary text-background rounded-xl text-body-sm font-body hover:bg-primary-hover transition-colors disabled:opacity-50"
onclick={() => acceptInvite(invite)}
>
<span
class="material-symbols-rounded"
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
>{acceptingInviteId === invite.id
? "hourglass_empty"
: "check"}</span
> >
{acceptingInviteId === invite.id {acceptingInviteId === invite.id
? "Joining..." ? m.home_invite_joining()
: "Accept"} : m.home_invite_click_accept()}
</button> </p>
</div> </div>
</div> </div>
</a>
{/each} {/each}
</div> </div>
</section> </section>
{/if} {/if}
<div class="flex items-center justify-between mb-6"> <!-- Org Card Grid -->
<div> <div
<h2 class="font-heading text-h3 text-white"> class="flex flex-1 flex-wrap gap-y-8 gap-x-8 items-start w-full content-start"
Your Organizations
</h2>
<p class="text-body-sm text-light/40 mt-1">
Select an organization to get started
</p>
</div>
<button
class="flex items-center gap-1.5 px-3 py-2 bg-primary text-background rounded-xl text-body-sm font-body hover:bg-primary-hover transition-colors"
onclick={() => (showCreateModal = true)}
> >
<span
class="material-symbols-rounded"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>add</span
>
New Organization
</button>
</div>
{#if organizations.length === 0} {#if organizations.length === 0}
<div <div
class="bg-dark/30 border border-light/5 rounded-2xl p-12 text-center" class="bg-background rounded-[16px] p-12 text-center w-full"
> >
<div <div
class="w-14 h-14 mx-auto mb-4 rounded-2xl bg-light/5 flex items-center justify-center" class="w-14 h-14 mx-auto mb-4 rounded-2xl bg-white/5 flex items-center justify-center"
> >
<span <span
class="material-symbols-rounded text-light/20" class="material-symbols-rounded text-white/20"
style="font-size: 28px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 28;" style="font-size: 28px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 28;"
>groups</span >groups</span
> >
</div> </div>
<h3 class="font-heading text-body text-white mb-1"> <h3 class="font-heading text-h5 text-white mb-1">
No organizations yet {m.home_empty_title()}
</h3> </h3>
<p class="text-body-sm text-light/40 mb-6"> <p class="font-body text-body-md text-white/50 mb-6">
Create your first organization to start collaborating {m.home_empty_desc()}
</p> </p>
<button <Button onclick={() => (showCreateModal = true)}>
class="px-4 py-2 bg-primary text-background rounded-xl text-body-sm font-body hover:bg-primary-hover transition-colors" {m.org_selector_create()}
onclick={() => (showCreateModal = true)} </Button>
>Create Organization</button
>
</div> </div>
{:else} {:else}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{#each organizations as org} {#each organizations as org}
<a href="/{org.slug}" class="block group"> <a
<div href="/{org.slug}"
class="bg-dark/30 border border-light/5 hover:border-primary/30 rounded-2xl p-5 transition-all h-full" class="bg-background flex flex-col items-start overflow-clip rounded-[16px] w-[256px] cursor-pointer hover:ring-2 hover:ring-primary/30 transition-all"
> >
<div class="flex items-start justify-between mb-3"> <!-- Card image area -->
<div class="h-[144px] w-full relative bg-surface">
{#if org.avatar_url} {#if org.avatar_url}
<img <img
src={org.avatar_url} src={org.avatar_url}
alt={org.name} alt={org.name}
class="w-10 h-10 rounded-xl object-cover" class="absolute inset-0 w-full h-full object-cover"
/> />
{:else} {:else}
<div <div
class="w-10 h-10 bg-primary/10 rounded-xl flex items-center justify-center text-primary font-heading text-body" class="w-full h-full flex items-center justify-center"
>
<span
class="font-heading text-h1 text-white/10"
>
{org.name.charAt(0).toUpperCase()}
</span>
</div>
{/if}
</div>
<!-- Card info -->
<div
class="flex flex-col gap-4 items-start justify-center px-4 py-5 w-full"
>
<div
class="flex items-start justify-between w-full"
>
{#if org.avatar_url}
<img
src={org.avatar_url}
alt={org.name}
class="w-12 h-12 rounded-full object-cover"
/>
{:else}
<div
class="w-12 h-12 rounded-full bg-primary flex items-center justify-center text-night font-heading text-h5"
> >
{org.name.charAt(0).toUpperCase()} {org.name.charAt(0).toUpperCase()}
</div> </div>
{/if} {/if}
<span <span
class="text-[10px] px-2 py-0.5 bg-light/5 rounded-lg text-light/40 capitalize font-body" class="bg-surface flex items-center justify-center p-1 rounded-[8px] font-body text-body-md text-white capitalize"
>{org.role}</span >{org.role}</span
> >
</div> </div>
<h3 <div class="flex flex-col gap-1 w-full">
class="font-heading text-body-sm text-white group-hover:text-primary transition-colors" <p class="font-heading text-h5 text-white">
>
{org.name} {org.name}
</h3> </p>
<p <p class="font-body text-body-md text-white/50">
class="text-[11px] text-light/30 mt-0.5 font-body"
>
/{org.slug} /{org.slug}
</p> </p>
</div> </div>
</div>
</a> </a>
{/each} {/each}
</div>
{/if} {/if}
</div>
</main> </main>
</div> </div>
<Modal <Modal
isOpen={showCreateModal} isOpen={showCreateModal}
onClose={() => (showCreateModal = false)} onClose={() => (showCreateModal = false)}
title="Create Organization" title={m.home_create_org_title()}
> >
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-6">
<div class="flex flex-col gap-1.5"> <Input
<label for="org-name" class="text-body-sm text-light/60 font-body" label={m.home_create_org_name_label()}
>Organization Name</label
>
<input
id="org-name"
type="text"
bind:value={newOrgName} bind:value={newOrgName}
placeholder="e.g. Acme Inc" placeholder={m.home_create_org_name_placeholder()}
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white placeholder:text-light/30 focus:outline-none focus:border-primary" required
/> />
</div>
{#if newOrgName} {#if newOrgName}
<p class="text-body-sm text-light/40"> <p class="font-body text-body-md text-white/50 px-3">
URL: <span class="text-white font-body" URL: <span class="text-white font-body"
>/{generateSlug(newOrgName)}</span >/{generateSlug(newOrgName)}</span
> >
</p> </p>
{/if} {/if}
<div <div class="flex items-center justify-end gap-3 pt-2">
class="flex items-center justify-end gap-3 pt-2 border-t border-light/5" <Button variant="ghost" onclick={() => (showCreateModal = false)}
>{m.btn_cancel()}</Button
> >
<button <Button
type="button"
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
onclick={() => (showCreateModal = false)}>Cancel</button
>
<button
type="button"
disabled={!newOrgName.trim() || creating} disabled={!newOrgName.trim() || creating}
class="px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
onclick={handleCreateOrg} onclick={handleCreateOrg}
loading={creating}
> >
{creating ? "Creating..." : "Create"} {m.btn_create()}
</button> </Button>
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@@ -3,7 +3,6 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { on } from "svelte/events";
import { Avatar, Logo } from "$lib/components/ui"; import { Avatar, Logo } from "$lib/components/ui";
import type { SupabaseClient } from "@supabase/supabase-js"; import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/supabase/types"; import type { Database } from "$lib/supabase/types";
@@ -75,55 +74,25 @@
hasPermission(data.userRole, data.userPermissions, permission); hasPermission(data.userRole, data.userPermissions, permission);
setContext("canAccess", canAccess); setContext("canAccess", canAccess);
// Sidebar is always expanded (collapse removed)
const sidebarCollapsed = false;
// User dropdown
let showUserMenu = $state(false);
let menuContainerEl = $state<HTMLElement | null>(null);
// Attach click-outside and Escape listeners only while menu is open.
// Uses svelte/events 'on' to respect Svelte 5 event delegation order.
$effect(() => {
if (!showUserMenu) return;
// Defer so the opening click doesn't immediately close the menu
const timer = setTimeout(() => {
cleanupClick = on(document, "click", (e: MouseEvent) => {
if (
menuContainerEl &&
!menuContainerEl.contains(e.target as Node)
) {
showUserMenu = false;
}
});
}, 0);
const cleanupKey = on(document, "keydown", (e: Event) => {
if ((e as KeyboardEvent).key === "Escape") showUserMenu = false;
});
let cleanupClick: (() => void) | undefined;
return () => {
clearTimeout(timer);
cleanupClick?.();
cleanupKey();
};
});
async function handleLogout() { async function handleLogout() {
await supabase.auth.signOut(); await supabase.auth.signOut();
goto("/"); goto("/");
} }
const navItems = $derived([ const navItems = $derived([
{
href: `/${data.org.slug}`,
label: m.nav_home(),
icon: "home",
exact: true,
},
...(canAccess("documents.view") ...(canAccess("documents.view")
? [ ? [
{ {
href: `/${data.org.slug}/documents`, href: `/${data.org.slug}/documents`,
label: m.nav_files(), label: m.nav_storage(),
icon: "cloud", icon: "cloud",
exact: false,
}, },
] ]
: []), : []),
@@ -133,6 +102,7 @@
href: `/${data.org.slug}/calendar`, href: `/${data.org.slug}/calendar`,
label: m.nav_calendar(), label: m.nav_calendar(),
icon: "calendar_today", icon: "calendar_today",
exact: false,
}, },
] ]
: []), : []),
@@ -140,6 +110,7 @@
href: `/${data.org.slug}/events`, href: `/${data.org.slug}/events`,
label: m.nav_events(), label: m.nav_events(),
icon: "celebration", icon: "celebration",
exact: false,
}, },
// Chat disabled until fully developed // Chat disabled until fully developed
// ...(data.org.feature_chat // ...(data.org.feature_chat
@@ -157,22 +128,23 @@
// }, // },
// ] // ]
// : []), // : []),
// Settings requires settings.view or admin role
...(canAccess("settings.view") ...(canAccess("settings.view")
? [ ? [
{ {
href: `/${data.org.slug}/settings`, href: `/${data.org.slug}/settings`,
label: m.nav_settings(), label: m.nav_settings(),
icon: "settings", icon: "settings",
exact: false,
}, },
] ]
: []), : []),
]); ]);
function isActive(href: string): boolean { function isActive(href: string, exact = false): boolean {
const navTo = $navigating?.to?.url.pathname; const navTo = $navigating?.to?.url.pathname;
if (navTo) return navTo.startsWith(href); const current = navTo || $page.url.pathname;
return $page.url.pathname.startsWith(href); if (exact) return current === href || current === href + "/";
return current.startsWith(href);
} }
function isNavigatingTo(href: string): boolean { function isNavigatingTo(href: string): boolean {
@@ -189,60 +161,62 @@
<div class="flex h-screen bg-background p-4 gap-4"> <div class="flex h-screen bg-background p-4 gap-4">
<!-- Organization Module --> <!-- Organization Module -->
<aside <aside
class="w-64 bg-night rounded-[32px] flex flex-col px-4 py-5 gap-4 overflow-hidden shrink-0" class="w-[256px] min-w-[256px] bg-night rounded-[32px] flex flex-col px-4 py-5 gap-4 overflow-hidden shrink-0 h-full"
> >
<!-- Logo -->
<Logo size="sm" />
<!-- Org Header --> <!-- Org Header -->
<a <a
href="/{data.org.slug}" href="/{data.org.slug}"
class="flex items-center gap-2 p-1 rounded-[32px] hover:bg-dark transition-colors" class="flex items-center gap-4 w-full hover:opacity-80 transition-opacity"
> >
<div class="shrink-0 w-12 h-12"> <div class="shrink-0">
<Avatar <Avatar
name={data.org.name} name={data.org.name}
src={data.org.avatar_url} src={data.org.avatar_url}
size="md" size="md"
/> />
</div> </div>
<div class="min-w-0 flex-1 overflow-hidden"> <div class="min-w-0 flex-1 flex flex-col gap-1">
<h1 <p class="font-heading text-h4 text-white truncate w-full">
class="font-heading text-h3 text-white truncate whitespace-nowrap"
>
{data.org.name} {data.org.name}
</h1> </p>
<p <p class="font-body text-body text-white capitalize w-full">
class="text-body-sm text-white font-body capitalize whitespace-nowrap"
>
{data.userRole} {data.userRole}
</p> </p>
</div> </div>
</a> </a>
<!-- Nav Items --> <!-- Nav Items -->
<nav class="flex-1 flex flex-col gap-1"> <nav class="flex-1 flex flex-col gap-1 min-h-0">
{#each navItems as item} {#each navItems as item}
<a <a
href={item.href} href={item.href}
class="flex items-center gap-2 h-10 pl-1 pr-2 py-1 rounded-[32px] transition-colors {isActive( class="flex items-center gap-2 pl-1 pr-2 py-1 rounded-[32px] transition-colors overflow-clip {isActive(
item.href, item.href,
item.exact,
) )
? 'bg-primary' ? 'bg-primary'
: 'hover:bg-dark'}" : 'hover:bg-white/5'}"
>
<div
class="w-8 h-8 flex items-center justify-center p-1 shrink-0"
> >
<span class="flex items-center p-1 shrink-0">
<span <span
class="material-symbols-rounded {isActive(item.href) class="material-symbols-rounded {isActive(
item.href,
item.exact,
)
? 'text-background' ? 'text-background'
: 'text-light'}" : 'text-white'}"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;" style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
> >
{item.icon} {item.icon}
</span> </span>
</div> </span>
<span <span
class="font-body text-body truncate whitespace-nowrap {isActive( class="flex-1 font-body text-body truncate {isActive(
item.href, item.href,
item.exact,
) )
? 'text-background' ? 'text-background'
: 'text-white'}">{item.label}</span : 'text-white'}">{item.label}</span
@@ -258,98 +232,38 @@
{/each} {/each}
</nav> </nav>
<!-- User Section + Logo at bottom --> <!-- Profile footer -->
<div class="mt-auto flex flex-col gap-3"> <div class="flex gap-4 items-center w-full">
<!-- User Avatar + Quick Menu --> <a
<div href="/{data.org.slug}/account"
class="relative user-menu-container" class="flex flex-1 gap-4 items-center min-w-0 hover:opacity-80 transition-opacity"
bind:this={menuContainerEl}
> >
<button <div class="shrink-0">
type="button"
class="flex items-center gap-2 p-1 rounded-[32px] hover:bg-dark transition-colors w-full"
onclick={() => (showUserMenu = !showUserMenu)}
aria-expanded={showUserMenu}
aria-haspopup="true"
>
<div class="shrink-0 w-10 h-10">
<Avatar <Avatar
name={data.profile.full_name || data.profile.email} name={data.profile.full_name || data.profile.email}
src={data.profile.avatar_url} src={data.profile.avatar_url}
size="sm" size="sm"
/> />
</div> </div>
<div class="min-w-0 flex-1 overflow-hidden text-left"> <div class="flex flex-1 flex-col items-start min-w-0">
<p <p class="font-heading text-h5 text-white w-full truncate">
class="font-body text-body-sm text-white truncate whitespace-nowrap leading-tight"
>
{data.profile.full_name || "User"} {data.profile.full_name || "User"}
</p> </p>
<p
class="font-body text-[11px] text-light/50 truncate whitespace-nowrap leading-tight"
>
{data.profile.email}
</p>
</div> </div>
</button>
{#if showUserMenu}
<div
class="absolute bottom-full left-0 mb-2 py-1 bg-dark border border-light/10 rounded-xl shadow-xl min-w-[200px] z-50"
>
<a
href="/{data.org.slug}/account"
class="flex items-center gap-3 px-3 py-2 text-sm text-light hover:bg-light/5 transition-colors"
onclick={() => (showUserMenu = false)}
>
<span
class="material-symbols-rounded text-light/50"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>
person
</span>
<span>{m.user_menu_account_settings()}</span>
</a> </a>
<a
href="/"
class="flex items-center gap-3 px-3 py-2 text-sm text-light hover:bg-light/5 transition-colors"
onclick={() => (showUserMenu = false)}
>
<span
class="material-symbols-rounded text-light/50"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>
swap_horiz
</span>
<span>{m.user_menu_switch_org()}</span>
</a>
<div class="border-t border-light/10 my-1"></div>
<button <button
type="button" type="button"
class="w-full flex items-center gap-3 px-3 py-2 text-sm text-error hover:bg-error/10 transition-colors" class="flex items-center justify-center p-1 rounded-[32px] shrink-0 text-white/60 hover:text-white transition-colors"
onclick={handleLogout} onclick={handleLogout}
title="Sign out"
> >
<span <span
class="material-symbols-rounded text-error/60" class="material-symbols-rounded"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;" style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>exit_to_app</span
> >
logout
</span>
<span>{m.user_menu_logout()}</span>
</button> </button>
</div> </div>
{/if}
</div>
<!-- Logo -->
<a
href="/"
title="Back to organizations"
class="flex items-center justify-center"
>
<Logo size="md" />
</a>
</div>
</aside> </aside>
<!-- Main Content Area --> <!-- Main Content Area -->

View File

@@ -79,13 +79,13 @@
<title>{data.org.name} | root</title> <title>{data.org.name} | root</title>
</svelte:head> </svelte:head>
<div class="flex flex-col h-full overflow-auto"> <div class="flex flex-col gap-8 h-full p-8 overflow-auto">
<PageHeader title={data.org.name} subtitle={m.overview_subtitle()}> <PageHeader title={data.org.name} subtitle={m.overview_subtitle()}>
{#snippet actions()} {#snippet actions()}
{#if isEditor} {#if isEditor}
<a <a
href="/{data.org.slug}/events" href="/{data.org.slug}/events"
class="flex items-center gap-2 px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors" class="flex items-center gap-2 px-5 py-2.5 bg-primary text-background rounded-[32px] font-body text-body hover:bg-primary-hover transition-colors"
> >
<span <span
class="material-symbols-rounded" class="material-symbols-rounded"
@@ -98,9 +98,9 @@
{/snippet} {/snippet}
</PageHeader> </PageHeader>
<div class="flex-1 p-6 overflow-auto"> <div class="flex-1 overflow-auto">
<!-- Stats Grid --> <!-- Stats Grid -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6"> <div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{#await data.stats} {#await data.stats}
{#each Array(4) as _} {#each Array(4) as _}
<Skeleton <Skeleton
@@ -145,7 +145,7 @@
{/await} {/await}
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left Column: Upcoming Events + Activity --> <!-- Left Column: Upcoming Events + Activity -->
<div class="lg:col-span-2 flex flex-col gap-6"> <div class="lg:col-span-2 flex flex-col gap-6">
<!-- Upcoming Events --> <!-- Upcoming Events -->
@@ -260,24 +260,24 @@
{#if isAdmin} {#if isAdmin}
<a <a
href="/{data.org.slug}/settings" href="/{data.org.slug}/settings"
class="flex items-center gap-3 bg-dark/30 border border-light/5 hover:border-light/10 rounded-xl p-4 transition-all group" class="flex items-center gap-4 bg-surface rounded-[32px] p-5 transition-all hover:ring-1 hover:ring-white/10 group"
> >
<div <div
class="w-10 h-10 rounded-xl bg-light/5 flex items-center justify-center" class="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center shrink-0"
> >
<span <span
class="material-symbols-rounded text-light/40 group-hover:text-white transition-colors" class="material-symbols-rounded text-white/50 group-hover:text-white transition-colors"
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;" style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>settings</span >settings</span
> >
</div> </div>
<div> <div class="flex flex-col gap-1">
<p <p
class="text-body-sm text-white group-hover:text-primary transition-colors" class="text-body font-heading text-white group-hover:text-primary transition-colors"
> >
{m.nav_settings()} {m.nav_settings()}
</p> </p>
<p class="text-[11px] text-light/30"> <p class="text-body-sm font-body text-white/40">
{m.settings_general_title()} {m.settings_general_title()}
</p> </p>
</div> </div>

View File

@@ -277,28 +277,50 @@
<title>Account Settings | root</title> <title>Account Settings | root</title>
</svelte:head> </svelte:head>
<div class="flex-1 p-6 overflow-auto"> <div class="flex-1 flex flex-col gap-8 h-full overflow-auto p-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4"> <!-- Header -->
<!-- Profile Section --> <div class="flex items-center justify-between w-full shrink-0">
<div <div class="flex flex-1 flex-col gap-2 items-start min-w-0">
class="bg-dark/30 border border-light/5 rounded-2xl p-5 flex flex-col gap-5" <h2 class="font-heading text-h2 text-white">
> {m.account_settings_title?.() ?? "Your Settings"}
<h2 class="font-heading text-body text-white">
{m.account_profile()}
</h2> </h2>
<p class="font-body text-body text-white/50">
{m.account_settings_subtitle?.() ??
"Manage your settings here."}
</p>
</div>
</div>
<!-- Avatar --> <!-- Settings panels -->
<div class="flex flex-col gap-3"> <div class="flex flex-1 gap-8 items-start min-h-0 overflow-clip w-full">
<span class="font-body text-body-sm text-light/60" <!-- Details panel -->
>{m.account_photo()}</span <div
class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
> >
<div class="flex items-center gap-4"> <!-- Panel header -->
<div
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 rounded-[32px] shrink-0 w-full"
>
<div class="flex flex-1 items-center min-w-0">
<h3
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
>
{m.account_profile()}
</h3>
</div>
</div>
<!-- Panel content -->
<div
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
>
<!-- Avatar -->
<div class="flex gap-4 items-center">
<Avatar <Avatar
name={fullName || data.profile.email} name={fullName || data.profile.email}
src={avatarUrl} src={avatarUrl}
size="xl" size="lg"
/> />
<div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
type="file" type="file"
@@ -315,17 +337,9 @@
> >
{m.btn_upload()} {m.btn_upload()}
</Button> </Button>
<Button
variant="tertiary"
size="sm"
onclick={syncGoogleAvatar}
>
{m.account_sync_google()}
</Button>
</div>
{#if avatarUrl} {#if avatarUrl}
<Button <Button
variant="tertiary" variant="danger"
size="sm" size="sm"
onclick={removeAvatar} onclick={removeAvatar}
> >
@@ -334,13 +348,13 @@
{/if} {/if}
</div> </div>
</div> </div>
</div>
<!-- Name --> <!-- Name -->
<Input <Input
label={m.account_display_name()} label={m.account_display_name()}
bind:value={fullName} bind:value={fullName}
placeholder={m.account_display_name_placeholder()} placeholder={m.account_display_name_placeholder()}
required
/> />
<!-- Email (read-only) --> <!-- Email (read-only) -->
@@ -348,22 +362,72 @@
label={m.account_email()} label={m.account_email()}
value={data.profile.email} value={data.profile.email}
disabled disabled
required
/> />
<div> <!-- Password -->
<Button onclick={saveProfile} loading={isSaving}> <Input
type="password"
label={m.account_password()}
value="••••••••••"
disabled
/>
<Button
variant="outline"
size="sm"
onclick={async () => {
const { error } =
await supabase.auth.resetPasswordForEmail(
data.profile.email,
{
redirectTo: `${window.location.origin}/${data.org.slug}/account`,
},
);
if (error) toasts.error(m.toast_error_reset_email());
else toasts.success(m.toast_success_reset_email());
}}
>
{m.account_send_reset()}
</Button>
</div>
<!-- Panel CTA -->
<div
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
>
<Button fullWidth onclick={saveProfile} loading={isSaving}>
{m.account_save_profile()} {m.account_save_profile()}
</Button> </Button>
</div> </div>
</div> </div>
<!-- Contact & Sizing Section --> <!-- Member Information panel -->
<div <div
class="bg-dark/30 border border-light/5 rounded-2xl p-5 flex flex-col gap-5" class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
>
<!-- Panel header -->
<div
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 rounded-[32px] shrink-0 w-full"
>
<div class="flex flex-1 items-center min-w-0">
<h3
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
> >
<h2 class="font-heading text-body text-white">
{m.account_contact_info()} {m.account_contact_info()}
</h2> </h3>
</div>
</div>
<!-- Panel content -->
<div
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
>
<Input
label={m.account_discord()}
bind:value={discordHandle}
placeholder={m.account_discord_placeholder()}
/>
<Input <Input
label={m.account_phone()} label={m.account_phone()}
@@ -371,22 +435,14 @@
placeholder={m.account_phone_placeholder()} placeholder={m.account_phone_placeholder()}
/> />
<Input
label={m.account_discord()}
bind:value={discordHandle}
placeholder={m.account_discord_placeholder()}
/>
<div class="grid grid-cols-2 gap-3">
<Select <Select
variant="compact"
label={m.account_shirt_size()} label={m.account_shirt_size()}
bind:value={shirtSize} bind:value={shirtSize}
placeholder={m.account_size_placeholder()} placeholder={m.account_size_placeholder()}
options={clothingSizes.map((s) => ({ value: s, label: s }))} options={clothingSizes.map((s) => ({ value: s, label: s }))}
/> />
<Select <Select
variant="compact"
label={m.account_hoodie_size()} label={m.account_hoodie_size()}
bind:value={hoodieSize} bind:value={hoodieSize}
placeholder={m.account_size_placeholder()} placeholder={m.account_size_placeholder()}
@@ -394,21 +450,37 @@
/> />
</div> </div>
<div> <!-- Panel CTA -->
<Button onclick={saveProfile} loading={isSaving}> <div
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
>
<Button fullWidth onclick={saveProfile} loading={isSaving}>
{m.account_save_profile()} {m.account_save_profile()}
</Button> </Button>
</div> </div>
</div> </div>
<!-- Appearance Section --> <!-- Appearance panel -->
<div <div
class="bg-dark/30 border border-light/5 rounded-2xl p-5 flex flex-col gap-5" class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
>
<!-- Panel header -->
<div
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 rounded-[32px] shrink-0 w-full"
>
<div class="flex flex-1 items-center min-w-0">
<h3
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
> >
<h2 class="font-heading text-body text-white">
{m.account_appearance()} {m.account_appearance()}
</h2> </h3>
</div>
</div>
<!-- Panel content -->
<div
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
>
<!-- Theme --> <!-- Theme -->
<Select <Select
label={m.account_theme()} label={m.account_theme()}
@@ -422,93 +494,32 @@
/> />
<!-- Language --> <!-- Language -->
<div class="flex flex-col gap-2"> <Select
<span class="font-body text-body-sm text-light/60" label={m.account_language()}
>{m.account_language()}</span bind:value={currentLocale}
> placeholder=""
<p class="font-body text-[11px] text-light/40"> options={locales.map((l) => ({
{m.account_language_desc()} value: l,
</p> label: localeLabels[l] ?? l,
<div class="flex gap-2 mt-1"> }))}
{#each locales as locale} onchange={(e) => {
<button const val = (e.target as HTMLSelectElement)?.value;
type="button" if (val)
class="px-3 py-1.5 rounded-lg text-[12px] font-medium transition-colors {currentLocale === handleLanguageChange(
locale val as (typeof locales)[number],
? 'bg-primary text-background' );
: 'bg-light/5 text-light/50 hover:bg-light/10'}" }}
onclick={() => handleLanguageChange(locale)} />
>
{localeLabels[locale] ?? locale}
</button>
{/each}
</div>
</div> </div>
<div> <!-- Panel CTA -->
<Button onclick={savePreferences} loading={isSaving}> <div
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
>
<Button fullWidth onclick={savePreferences} loading={isSaving}>
{m.account_save_preferences()} {m.account_save_preferences()}
</Button> </Button>
</div> </div>
</div> </div>
<!-- Security & Sessions Section -->
<div
class="bg-dark/30 border border-light/5 rounded-2xl p-5 flex flex-col gap-5"
>
<h2 class="font-heading text-body text-white">
{m.account_security()}
</h2>
<div class="flex flex-col gap-2">
<p class="font-body text-body-sm text-white">
{m.account_password()}
</p>
<p class="font-body text-[11px] text-light/40">
{m.account_password_desc()}
</p>
<div class="mt-2">
<Button
variant="secondary"
size="sm"
onclick={async () => {
const { error } =
await supabase.auth.resetPasswordForEmail(
data.profile.email,
{
redirectTo: `${window.location.origin}/${data.org.slug}/account`,
},
);
if (error)
toasts.error(m.toast_error_reset_email());
else toasts.success(m.toast_success_reset_email());
}}
>
{m.account_send_reset()}
</Button>
</div>
</div>
<div class="border-t border-light/5 pt-4 flex flex-col gap-2">
<p class="font-body text-body-sm text-white">
{m.account_active_sessions()}
</p>
<p class="font-body text-[11px] text-light/40">
{m.account_sessions_desc()}
</p>
<div class="mt-2">
<Button
variant="danger"
size="sm"
onclick={async () => {
await supabase.auth.signOut({ scope: "others" });
toasts.success(m.toast_success_signout_others());
}}
>
{m.account_signout_others()}
</Button>
</div>
</div>
</div>
</div> </div>
</div> </div>

View File

@@ -462,13 +462,19 @@
<title>{m.calendar_title()} - {data.org.name} | root</title> <title>{m.calendar_title()} - {data.org.name} | root</title>
</svelte:head> </svelte:head>
<div class="flex flex-col h-full"> <div class="flex flex-col gap-8 h-full p-8 overflow-hidden">
<!-- Toolbar --> <!-- Header -->
<div <div class="flex items-center justify-between w-full shrink-0">
class="flex items-center gap-2 px-6 py-3 border-b border-light/5 shrink-0" <div class="flex flex-1 flex-col gap-2 items-start min-w-0">
> <h2 class="font-heading text-h2 text-white">
<div class="flex-1"></div> {m.calendar_title()}
<Button size="sm" onclick={() => handleDateClick(new Date())} </h2>
<p class="font-body text-body text-white/50">
{m.calendar_subtitle()}
</p>
</div>
<div class="flex items-center gap-2 shrink-0">
<Button icon="add" onclick={() => handleDateClick(new Date())}
>{m.btn_new()}</Button >{m.btn_new()}</Button
> >
<ContextMenu <ContextMenu
@@ -509,9 +515,10 @@
]} ]}
/> />
</div> </div>
</div>
<!-- Calendar Grid --> <!-- Calendar Grid -->
<div class="flex-1 overflow-auto p-4"> <div class="flex-1 overflow-auto min-h-0">
<Calendar <Calendar
events={allEvents} events={allEvents}
onDateClick={handleDateClick} onDateClick={handleDateClick}
@@ -791,9 +798,7 @@
{/if} {/if}
<div class="flex justify-end gap-2 pt-2"> <div class="flex justify-end gap-2 pt-2">
<Button <Button variant="ghost" onclick={() => (showEventFormModal = false)}
variant="tertiary"
onclick={() => (showEventFormModal = false)}
>{m.btn_cancel()}</Button >{m.btn_cancel()}</Button
> >
<Button <Button

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { FileBrowser } from "$lib/components/documents"; import { FileBrowser } from "$lib/components/documents";
import { ContentHeader } from "$lib/components/ui";
import type { Document } from "$lib/supabase/types"; import type { Document } from "$lib/supabase/types";
import * as m from "$lib/paraglide/messages";
interface Props { interface Props {
data: { data: {
@@ -17,13 +19,24 @@
$effect(() => { $effect(() => {
documents = data.documents; documents = data.documents;
}); });
let fileBrowserRef:
| { handleAdd: () => void; handleUpload: () => void }
| undefined;
</script> </script>
<svelte:head> <svelte:head>
<title>Files - {data.org.name} | root</title> <title>{m.nav_storage()} - {data.org.name} | root</title>
</svelte:head> </svelte:head>
<div class="h-full p-6"> <div class="flex flex-col gap-8 h-full p-8 overflow-auto">
<ContentHeader
title={m.nav_storage()}
subtitle={m.files_subtitle()}
actionLabel={m.btn_new()}
actionIcon="add"
onAction={() => fileBrowserRef?.handleAdd()}
/>
<FileBrowser <FileBrowser
org={data.org} org={data.org}
bind:documents bind:documents

View File

@@ -664,9 +664,9 @@
<title>{data.document.name} - {data.org.name} | root</title> <title>{data.document.name} - {data.org.name} | root</title>
</svelte:head> </svelte:head>
<div class="flex flex-col h-full p-4 lg:p-5 gap-4"> <div class="flex flex-col h-full p-8 gap-8">
{#if data.isKanban} {#if data.isKanban}
<!-- Kanban: needs its own header since DocumentViewer is for documents --> <!-- Kanban header -->
<input <input
type="file" type="file"
accept=".json" accept=".json"
@@ -674,10 +674,15 @@
bind:this={fileInput} bind:this={fileInput}
onchange={handleJsonImport} onchange={handleJsonImport}
/> />
<header class="flex items-center gap-2 p-1"> <div class="flex items-center justify-between w-full shrink-0">
<h1 class="flex-1 font-heading text-h1 text-white truncate"> <div class="flex flex-1 flex-col gap-2 items-start min-w-0">
<h2 class="font-heading text-h2 text-white truncate w-full">
{data.document.name} {data.document.name}
</h1> </h2>
<p class="font-body text-body text-white/50">
{data.document.type}
</p>
</div>
<ContextMenu <ContextMenu
items={[ items={[
{ {
@@ -698,7 +703,61 @@
}, },
]} ]}
/> />
</header> </div>
<!-- Breadcrumb -->
<div
class="flex flex-wrap items-center justify-between gap-y-2 w-full shrink-0"
>
<nav class="flex items-center">
<a
href="/{data.org.slug}/documents"
class="flex items-center gap-2 shrink-0 hover:opacity-80 transition-opacity"
>
<span class="flex items-center justify-center p-1">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>cloud</span
>
</span>
<span
class="font-heading text-h3 text-white whitespace-nowrap"
>Home</span
>
</a>
<span class="flex items-center p-1 shrink-0">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>chevron_forward</span
>
</span>
<div class="flex items-center gap-2 shrink-0">
<span class="flex items-center justify-center p-1">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>view_kanban</span
>
</span>
<span
class="font-heading text-h3 text-white whitespace-nowrap"
>{data.document.name}</span
>
</div>
</nav>
<button
type="button"
class="flex items-center justify-center p-1 shrink-0 hover:bg-white/5 rounded-lg transition-colors"
>
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>list</span
>
</button>
</div>
<div class="flex-1 overflow-auto min-h-0"> <div class="flex-1 overflow-auto min-h-0">
<div class="h-full"> <div class="h-full">
@@ -718,19 +777,124 @@
<div class="flex items-center justify-center h-full"> <div class="flex items-center justify-center h-full">
<div class="text-center"> <div class="text-center">
<span <span
class="material-symbols-rounded text-light/30 animate-spin mb-4" class="material-symbols-rounded text-white/30 animate-spin mb-4"
style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 48;" style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 48;"
> >
progress_activity progress_activity
</span> </span>
<p class="text-light/50">Loading board...</p> <p class="font-body text-body text-white/50">
Loading board...
</p>
</div> </div>
</div> </div>
{/if} {/if}
</div> </div>
</div> </div>
{:else} {:else}
<!-- Document Editor: use shared DocumentViewer component --> <!-- Document header -->
<div class="flex items-center justify-between w-full shrink-0">
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
<h2 class="font-heading text-h2 text-white truncate w-full">
{data.document.name}
</h2>
<p class="font-body text-body text-white/50">
{data.document.type}
</p>
</div>
<button
type="button"
class="flex items-center justify-center p-1 rounded-full hover:bg-white/5 transition-colors"
>
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>more_horiz</span
>
</button>
</div>
<!-- Breadcrumb -->
<div
class="flex flex-wrap items-center justify-between gap-y-2 w-full shrink-0"
>
<nav class="flex items-center">
<a
href="/{data.org.slug}/documents"
class="flex items-center gap-2 shrink-0 hover:opacity-80 transition-opacity"
>
<span class="flex items-center justify-center p-1">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>cloud</span
>
</span>
<span
class="font-heading text-h3 text-white whitespace-nowrap"
>Home</span
>
</a>
{#if data.document.parent_id}
<span class="flex items-center p-1 shrink-0">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>chevron_forward</span
>
</span>
<a
href="/{data.org.slug}/documents/folder/{data.document
.parent_id}"
class="flex items-center gap-2 shrink-0 hover:opacity-80 transition-opacity"
>
<span class="flex items-center justify-center p-1">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>folder</span
>
</span>
<span
class="font-heading text-h3 text-white whitespace-nowrap"
>...</span
>
</a>
{/if}
<span class="flex items-center p-1 shrink-0">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>chevron_forward</span
>
</span>
<div class="flex items-center gap-2 shrink-0">
<span class="flex items-center justify-center p-1">
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>description</span
>
</span>
<span
class="font-heading text-h3 text-white whitespace-nowrap"
>{data.document.name}</span
>
</div>
</nav>
<button
type="button"
class="flex items-center justify-center p-1 shrink-0 hover:bg-white/5 rounded-lg transition-colors"
>
<span
class="material-symbols-rounded text-white"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>list</span
>
</button>
</div>
<!-- Document Editor -->
<div class="flex-1 min-h-0 overflow-auto">
<DocumentViewer <DocumentViewer
document={data.document} document={data.document}
onSave={handleSave} onSave={handleSave}
@@ -738,11 +902,12 @@
locked={lockInfo.isLocked && !lockInfo.isOwnLock} locked={lockInfo.isLocked && !lockInfo.isOwnLock}
lockedByName={lockInfo.lockedByName} lockedByName={lockInfo.lockedByName}
/> />
</div>
{/if} {/if}
<!-- Status Bar --> <!-- Status Bar -->
{#if isSaving} {#if isSaving}
<div class="text-body-sm text-light/50">Saving...</div> <div class="font-body text-body-sm text-white/50">Saving...</div>
{/if} {/if}
</div> </div>
@@ -803,7 +968,7 @@
/> />
<div class="flex justify-end gap-2 pt-2"> <div class="flex justify-end gap-2 pt-2">
<Button <Button
variant="tertiary" variant="ghost"
onclick={() => (showAddColumnModal = false)} onclick={() => (showAddColumnModal = false)}
> >
Cancel Cancel

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { FileBrowser } from "$lib/components/documents"; import { FileBrowser } from "$lib/components/documents";
import { ContentHeader } from "$lib/components/ui";
import type { Document } from "$lib/supabase/types"; import type { Document } from "$lib/supabase/types";
import * as m from "$lib/paraglide/messages";
interface Props { interface Props {
data: { data: {
@@ -25,7 +27,13 @@
<title>{data.folder.name} - {data.org.name} | root</title> <title>{data.folder.name} - {data.org.name} | root</title>
</svelte:head> </svelte:head>
<div class="h-full p-4 lg:p-5"> <div class="flex flex-col gap-8 h-full p-8 overflow-auto">
<ContentHeader
title={m.nav_storage()}
subtitle={m.files_subtitle()}
actionLabel={m.btn_new()}
actionIcon="add"
/>
<FileBrowser <FileBrowser
org={data.org} org={data.org}
bind:documents bind:documents

View File

@@ -176,43 +176,39 @@
<title>{m.events_title()} | {data.org.name}</title> <title>{m.events_title()} | {data.org.name}</title>
</svelte:head> </svelte:head>
<div class="flex flex-col h-full"> <div class="flex flex-col gap-8 h-full p-8 overflow-hidden">
<!-- Toolbar: Status Tabs + Create Button --> <!-- Header -->
<div <div class="flex items-center justify-between w-full shrink-0">
class="flex items-center justify-between px-6 py-3 border-b border-light/5 shrink-0" <div class="flex flex-1 flex-col gap-2 items-start min-w-0">
> <h2 class="font-heading text-h2 text-white">{m.events_title()}</h2>
<div class="flex items-center gap-1"> </div>
<div class="flex items-center gap-2 shrink-0">
{#if isEditor}
<Button icon="add" onclick={() => (showCreateModal = true)}>
{m.events_new()}
</Button>
{/if}
</div>
</div>
<!-- Status Tabs -->
<div class="flex items-center gap-1 shrink-0">
{#each statusTabs as tab} {#each statusTabs as tab}
<button <button
type="button" type="button"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-body-sm font-body transition-colors {data.statusFilter === class="flex items-center gap-1.5 px-4 py-2 rounded-[32px] text-body font-body transition-colors {data.statusFilter ===
tab.value tab.value
? 'bg-primary text-background' ? 'bg-surface text-white'
: 'text-light/50 hover:text-white hover:bg-dark/50'}" : 'text-white/50 hover:text-white hover:bg-white/5'}"
onclick={() => switchStatus(tab.value)} onclick={() => switchStatus(tab.value)}
>
<span
class="material-symbols-rounded"
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
>{tab.icon}</span
> >
{tab.label} {tab.label}
</button> </button>
{/each} {/each}
</div> </div>
{#if isEditor}
<Button
size="sm"
icon="add"
onclick={() => (showCreateModal = true)}
>
{m.events_new()}
</Button>
{/if}
</div>
<!-- Events Grid --> <!-- Events Grid -->
<div class="flex-1 overflow-auto p-6"> <div class="flex-1 overflow-auto min-h-0">
{#if data.events.length === 0} {#if data.events.length === 0}
<div <div
class="flex flex-col items-center justify-center h-full text-light/40" class="flex flex-col items-center justify-center h-full text-light/40"

View File

@@ -679,7 +679,7 @@
/> />
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<Button <Button
variant="tertiary" variant="ghost"
onclick={() => (showCreateBoardModal = false)} onclick={() => (showCreateBoardModal = false)}
>{m.btn_cancel()}</Button >{m.btn_cancel()}</Button
> >
@@ -712,7 +712,7 @@
> >
<div class="flex gap-2"> <div class="flex gap-2">
<Button <Button
variant="tertiary" variant="ghost"
onclick={() => (showEditBoardModal = false)} onclick={() => (showEditBoardModal = false)}
>{m.btn_cancel()}</Button >{m.btn_cancel()}</Button
> >
@@ -737,9 +737,7 @@
placeholder={m.kanban_column_name_placeholder()} placeholder={m.kanban_column_name_placeholder()}
/> />
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<Button <Button variant="ghost" onclick={() => (showAddColumnModal = false)}
variant="tertiary"
onclick={() => (showAddColumnModal = false)}
>{m.btn_cancel()}</Button >{m.btn_cancel()}</Button
> >
<Button <Button

View File

@@ -204,11 +204,7 @@
> >
{m.onboarding_save()} {m.onboarding_save()}
</Button> </Button>
<Button <Button fullWidth variant="ghost" onclick={skipOnboarding}>
fullWidth
variant="tertiary"
onclick={skipOnboarding}
>
{m.onboarding_skip()} {m.onboarding_skip()}
</Button> </Button>
</div> </div>

View File

@@ -6,44 +6,43 @@
@plugin '@tailwindcss/typography'; @plugin '@tailwindcss/typography';
@theme { @theme {
/* Colors - Figma Design System */ /* Colors - Figma Design System (exact exports) */
--color-background: #05090f; --color-background: rgba(5, 9, 15, 1);
--color-night: #0A121F; --color-night: rgba(10, 18, 31, 1);
--color-dark: #14243E; --color-dark: #14243E;
--color-surface: #0A121F; --color-surface: rgba(15, 28, 46, 1);
--color-light: #E5E6F0; --color-light: rgba(255, 255, 255, 1);
--color-text: #FFFFFF; --color-text: rgba(255, 255, 255, 1);
--color-text-muted: rgba(229, 230, 240, 0.5); --color-text-muted: rgba(255, 255, 255, 0.5);
/* Brand - Primary */ /* Brand - Primary */
--color-primary: #00A3E0; --color-primary: rgba(0, 163, 224, 1);
--color-primary-hover: #33b5e6; --color-primary-hover: #33b5e6;
/* Status Colors */ /* Status Colors */
--color-success: #33E000; --color-success: rgba(51, 224, 0, 1);
--color-warning: #FFAB00; --color-warning: rgba(255, 171, 0, 1);
--color-error: #E03D00; --color-error: rgba(224, 61, 0, 1);
--color-info: #00A3E0; --color-info: rgba(0, 163, 224, 1);
/* Typography - Figma Fonts */ /* Typography - Figma Fonts */
--font-heading: 'Tilt Warp', sans-serif; --font-heading: 'Tilt Warp', sans-serif;
--font-body: 'Work Sans', sans-serif; --font-body: 'Work Sans', sans-serif;
--font-input: 'Inter', sans-serif; --font-input: 'Work Sans', sans-serif;
--font-sans: 'Work Sans', system-ui, -apple-system, sans-serif; --font-sans: 'Work Sans', system-ui, -apple-system, sans-serif;
/* Font Sizes - Figma Text Styles (--text-* → text-* utilities) */ /* Font Sizes - Figma Text Styles: headings-buttons */
/* Headings (heading font) */ --text-h1: 28px;
--text-h1: 32px; --text-h2: 24px;
--text-h2: 28px; --text-h3: 20px;
--text-h3: 24px; --text-h4: 18px;
--text-h4: 20px;
--text-h5: 16px; --text-h5: 16px;
--text-h6: 14px; --text-h6: 14px;
/* Button text (heading font) */ /* Font Sizes - Figma Text Styles: body btn */
--text-btn-lg: 20px; --text-btn-lg: 18px;
--text-btn-md: 16px; --text-btn-md: 16px;
--text-btn-sm: 14px; --text-btn-sm: 14px;
/* Body text (body font) */ /* Font Sizes - Figma Text Styles: body */
--text-body: 16px; --text-body: 16px;
--text-body-md: 14px; --text-body-md: 14px;
--text-body-sm: 12px; --text-body-sm: 12px;

View File

@@ -97,19 +97,24 @@
<title>{mode === "login" ? "Log In" : "Sign Up"} | root</title> <title>{mode === "login" ? "Log In" : "Sign Up"} | root</title>
</svelte:head> </svelte:head>
<div class="min-h-screen bg-background flex items-center justify-center p-4"> <div class="h-screen bg-background flex items-start p-4">
<div class="w-full max-w-sm"> <div
<div class="text-center mb-8"> class="bg-night flex-1 flex flex-col gap-4 min-h-0 h-full items-center justify-center overflow-auto p-8 rounded-[32px]"
<div class="flex justify-center mb-4"> >
<!-- Logo -->
<div class="flex justify-center">
<Logo size="lg" /> <Logo size="lg" />
</div> </div>
<h1 class="text-heading-sm font-heading text-white mb-1">
{m.app_name()}
</h1>
<p class="text-body-sm text-light/40">{m.login_subtitle()}</p>
</div>
<div class="bg-surface rounded-2xl border border-light/5 p-6"> <!-- Tagline -->
<p class="font-bold font-body text-h4 text-white text-center">
{m.login_subtitle()}
</p>
<!-- Form card -->
<div
class="bg-background flex flex-col gap-8 items-center overflow-clip p-4 rounded-[32px] w-full max-w-[384px]"
>
{#if signupSuccess} {#if signupSuccess}
<div class="text-center py-4"> <div class="text-center py-4">
<div <div
@@ -129,7 +134,7 @@
{m.login_signup_success_text({ email })} {m.login_signup_success_text({ email })}
</p> </p>
<Button <Button
variant="tertiary" variant="secondary"
onclick={() => { onclick={() => {
signupSuccess = false; signupSuccess = false;
mode = "login"; mode = "login";
@@ -140,23 +145,21 @@
</div> </div>
{:else} {:else}
<!-- Tab switcher --> <!-- Tab switcher -->
<div <div class="flex gap-4 items-start w-full">
class="flex items-center gap-1 bg-dark/50 rounded-xl p-1 mb-6"
>
<button <button
class="flex-1 py-2 rounded-lg text-body-sm font-body transition-colors {mode === class="flex-1 flex items-center justify-center gap-2 min-w-[36px] overflow-clip px-3 py-2 rounded-[32px] font-bold font-body text-btn-sm transition-colors {mode ===
'login' 'login'
? 'bg-primary text-background' ? 'bg-primary text-night'
: 'text-light/40 hover:text-white'}" : 'bg-night text-white hover:bg-surface'}"
onclick={() => (mode = "login")} onclick={() => (mode = "login")}
> >
{m.login_tab_login()} {m.login_tab_login()}
</button> </button>
<button <button
class="flex-1 py-2 rounded-lg text-body-sm font-body transition-colors {mode === class="flex-1 flex items-center justify-center gap-2 min-w-[36px] overflow-clip px-3 py-2 rounded-[32px] font-bold font-body text-btn-sm transition-colors {mode ===
'signup' 'signup'
? 'bg-primary text-background' ? 'bg-primary text-night'
: 'text-light/40 hover:text-white'}" : 'bg-night text-white hover:bg-surface'}"
onclick={() => (mode = "signup")} onclick={() => (mode = "signup")}
> >
{m.login_tab_signup()} {m.login_tab_signup()}
@@ -165,7 +168,7 @@
{#if error} {#if error}
<div <div
class="mb-4 p-3 bg-error/10 border border-error/20 rounded-xl text-error text-body-sm flex items-center gap-2" class="w-full p-3 bg-error/10 border border-error/20 rounded-[20px] text-error text-body-sm flex items-center gap-2"
> >
<span <span
class="material-symbols-rounded" class="material-symbols-rounded"
@@ -181,16 +184,8 @@
e.preventDefault(); e.preventDefault();
handleSubmit(); handleSubmit();
}} }}
class="flex flex-col gap-4" class="flex flex-col gap-8 w-full"
> >
<Input
type="email"
label={m.login_email_label()}
placeholder={m.login_email_placeholder()}
bind:value={email}
required
/>
{#if mode === "signup"} {#if mode === "signup"}
<Input <Input
type="text" type="text"
@@ -201,6 +196,14 @@
/> />
{/if} {/if}
<Input
type="email"
label={m.login_email_label()}
placeholder={m.login_email_placeholder()}
bind:value={email}
required
/>
<Input <Input
type="password" type="password"
label={m.login_password_label()} label={m.login_password_label()}
@@ -209,46 +212,39 @@
required required
/> />
<Button type="submit" fullWidth loading={isLoading}> <Button
type="submit"
size="lg"
fullWidth
loading={isLoading}
>
{mode === "login" {mode === "login"
? m.login_btn_login() ? m.login_btn_login()
: m.login_btn_signup()} : m.login_btn_signup()}
</Button> </Button>
</form> </form>
<div class="my-5 flex items-center gap-3"> <!-- Separator -->
<div class="flex-1 h-px bg-light/10"></div> <div
class="flex gap-1 items-center justify-center opacity-50 overflow-clip w-full"
>
<div class="flex-1 h-px bg-white/30"></div>
<span <span
class="text-light/30 text-[11px] uppercase tracking-wider" class="font-body text-body-md text-white text-center px-2"
>{m.login_or_continue()}</span >{m.login_or_continue()}</span
> >
<div class="flex-1 h-px bg-light/10"></div> <div class="flex-1 h-px bg-white/30"></div>
</div> </div>
<button <!-- OAuth -->
class="w-full flex items-center justify-center gap-2.5 px-4 py-2.5 rounded-xl border border-light/10 hover:border-light/20 hover:bg-light/5 transition-all text-body-sm text-white" <Button
variant="secondary"
size="lg"
fullWidth
onclick={() => handleOAuth("google")} onclick={() => handleOAuth("google")}
> >
<svg class="w-4 h-4" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
{m.login_google()} {m.login_google()}
</button> </Button>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,29 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
const { session, user } = await locals.safeGetSession();
if (!session || !user) {
redirect(303, '/login');
}
const { data: profile } = await locals.supabase
.from('profiles')
.select('full_name, avatar_url, phone, discord_handle, shirt_size, hoodie_size')
.eq('id', user.id)
.single();
return {
user: {
id: user.id,
email: user.email,
fullName: profile?.full_name ?? user.user_metadata?.full_name ?? null,
avatarUrl: profile?.avatar_url ?? null,
phone: profile?.phone ?? null,
discordHandle: profile?.discord_handle ?? null,
shirtSize: profile?.shirt_size ?? null,
hoodieSize: profile?.hoodie_size ?? null,
},
};
};

View File

@@ -0,0 +1,437 @@
<script lang="ts">
import { getContext } from "svelte";
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import { Logo, Avatar, Button, Input, Select } from "$lib/components/ui";
import { toasts } from "$lib/stores/toast.svelte";
import * as m from "$lib/paraglide/messages";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/supabase/types";
import { getLocale, setLocale, locales } from "$lib/paraglide/runtime.js";
interface Props {
data: {
user: {
id: string;
email: string | undefined;
fullName: string | null;
avatarUrl: string | null;
phone: string | null;
discordHandle: string | null;
shirtSize: string | null;
hoodieSize: string | null;
};
};
}
let { data }: Props = $props();
const supabase = getContext<SupabaseClient<Database>>("supabase");
// svelte-ignore state_referenced_locally
let fullName = $state(data.user.fullName ?? "");
// svelte-ignore state_referenced_locally
let avatarUrl = $state(data.user.avatarUrl);
// svelte-ignore state_referenced_locally
let phone = $state(data.user.phone ?? "");
// svelte-ignore state_referenced_locally
let discordHandle = $state(data.user.discordHandle ?? "");
// svelte-ignore state_referenced_locally
let shirtSize = $state(data.user.shirtSize ?? "");
// svelte-ignore state_referenced_locally
let hoodieSize = $state(data.user.hoodieSize ?? "");
let isSaving = $state(false);
let isUploading = $state(false);
let avatarInput = $state<HTMLInputElement | null>(null);
$effect(() => {
fullName = data.user.fullName ?? "";
avatarUrl = data.user.avatarUrl;
phone = data.user.phone ?? "";
discordHandle = data.user.discordHandle ?? "";
shirtSize = data.user.shirtSize ?? "";
hoodieSize = data.user.hoodieSize ?? "";
});
const clothingSizes = ["XS", "S", "M", "L", "XL", "XXL", "3XL"];
let currentLocale = $state<(typeof locales)[number]>(getLocale());
const localeLabels: Record<string, string> = {
en: "English",
et: "Eesti",
};
async function saveProfile() {
isSaving = true;
try {
const { error } = await supabase
.from("profiles")
.update({
full_name: fullName || null,
phone: phone || null,
discord_handle: discordHandle || null,
shirt_size: shirtSize || null,
hoodie_size: hoodieSize || null,
})
.eq("id", data.user.id);
if (error) throw error;
toasts.success(m.account_save_profile());
} catch (e) {
toasts.error("Failed to save profile.");
} finally {
isSaving = false;
}
}
async function handleAvatarUpload(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
isUploading = true;
try {
const ext = file.name.split(".").pop();
const path = `avatars/${data.user.id}.${ext}`;
const { error: uploadError } = await supabase.storage
.from("avatars")
.upload(path, file, { upsert: true });
if (uploadError) throw uploadError;
const {
data: { publicUrl },
} = supabase.storage.from("avatars").getPublicUrl(path);
const url = `${publicUrl}?t=${Date.now()}`;
await supabase
.from("profiles")
.update({ avatar_url: url })
.eq("id", data.user.id);
avatarUrl = url;
toasts.success("Avatar updated.");
} catch (e) {
toasts.error("Failed to upload avatar.");
} finally {
isUploading = false;
}
}
async function removeAvatar() {
await supabase
.from("profiles")
.update({ avatar_url: null })
.eq("id", data.user.id);
avatarUrl = null;
toasts.success(m.account_remove_photo());
}
function handleLanguageChange(newLocale: (typeof locales)[number]) {
currentLocale = newLocale;
setLocale(newLocale);
}
async function handleLogout() {
await supabase.auth.signOut();
goto("/login");
}
</script>
<svelte:head>
<title>{m.user_settings_title()} | root</title>
</svelte:head>
<div class="h-screen bg-background flex gap-4 items-start p-4">
<!-- Sidebar -->
<aside
class="bg-night flex flex-col gap-4 h-full items-start min-w-[256px] overflow-clip px-4 py-5 rounded-[32px] shrink-0 w-[256px]"
>
<Logo size="sm" />
<nav class="flex flex-1 flex-col gap-1 items-start w-full min-h-0">
{#each [{ href: "/", icon: "apartment", label: m.home_nav_organizations() }, { href: "/settings", icon: "settings", label: m.nav_settings() }] as item}
{@const active =
item.href === "/"
? $page.url.pathname === "/"
: $page.url.pathname.startsWith(item.href)}
<a
href={item.href}
class="flex gap-2 items-center w-full overflow-clip pl-1 pr-2 py-1 rounded-[32px] transition-colors {active
? 'bg-primary'
: 'hover:bg-white/5'}"
>
<span class="flex items-center p-1 shrink-0">
<span
class="material-symbols-rounded {active
? 'text-background'
: 'text-white'}"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>{item.icon}</span
>
</span>
<span
class="flex-1 font-body text-body truncate {active
? 'text-background'
: 'text-white'}">{item.label}</span
>
</a>
{/each}
</nav>
<!-- Profile footer -->
<div class="flex gap-4 items-center w-full">
<div class="flex flex-1 gap-4 items-center min-w-0">
<Avatar
name={data.user.fullName || data.user.email || "User"}
src={data.user.avatarUrl}
size="sm"
/>
<div class="flex flex-1 flex-col items-start min-w-0">
<p class="font-heading text-h5 text-white w-full truncate">
{data.user.fullName || data.user.email || "User"}
</p>
</div>
</div>
<button
type="button"
class="flex items-center justify-center p-1 rounded-[32px] text-white/60 hover:text-white transition-colors"
title="Sign out"
onclick={handleLogout}
>
<span
class="material-symbols-rounded"
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
>exit_to_app</span
>
</button>
</div>
</aside>
<!-- Content -->
<main
class="bg-night flex flex-1 flex-col gap-8 h-full items-start min-h-0 min-w-0 overflow-auto p-8 rounded-[32px]"
>
<!-- Header -->
<div class="flex items-center justify-between w-full shrink-0">
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
<h2 class="font-heading text-h2 text-white">
{m.user_settings_title()}
</h2>
<p class="font-body text-body text-white/50">
{m.user_settings_subtitle()}
</p>
</div>
</div>
<!-- Settings panels -->
<div class="flex flex-1 gap-8 items-start min-h-0 overflow-clip w-full">
<!-- Details panel -->
<div
class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
>
<div
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 shrink-0 w-full"
>
<h3
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
>
{m.account_profile()}
</h3>
</div>
<div
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
>
<!-- Avatar -->
<div class="flex gap-4 items-center">
<Avatar
name={fullName || data.user.email || "User"}
src={avatarUrl}
size="lg"
/>
<div class="flex gap-2">
<input
type="file"
accept="image/*"
class="hidden"
bind:this={avatarInput}
onchange={handleAvatarUpload}
/>
<Button
variant="secondary"
size="sm"
onclick={() => avatarInput?.click()}
loading={isUploading}
>
{m.btn_upload()}
</Button>
{#if avatarUrl}
<Button
variant="danger"
size="sm"
onclick={removeAvatar}
>
{m.account_remove_photo()}
</Button>
{/if}
</div>
</div>
<Input
label={m.account_display_name()}
bind:value={fullName}
placeholder={m.account_display_name_placeholder()}
required
/>
<Input
label={m.account_email()}
value={data.user.email ?? ""}
disabled
required
/>
<Input
type="password"
label={m.account_password()}
value="••••••••••"
disabled
/>
<Button
variant="outline"
size="sm"
onclick={async () => {
const { error } =
await supabase.auth.resetPasswordForEmail(
data.user.email ?? "",
{
redirectTo: `${window.location.origin}/settings`,
},
);
if (error)
toasts.error(m.toast_error_reset_email());
else toasts.success(m.toast_success_reset_email());
}}
>
{m.account_send_reset()}
</Button>
</div>
<div
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
>
<Button fullWidth onclick={saveProfile} loading={isSaving}>
{m.account_save_profile()}
</Button>
</div>
</div>
<!-- Member Information panel -->
<div
class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
>
<div
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 shrink-0 w-full"
>
<h3
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
>
{m.account_contact_info()}
</h3>
</div>
<div
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
>
<Input
label={m.account_discord()}
bind:value={discordHandle}
placeholder={m.account_discord_placeholder()}
/>
<Input
label={m.account_phone()}
bind:value={phone}
placeholder={m.account_phone_placeholder()}
/>
<Select
label={m.account_shirt_size()}
bind:value={shirtSize}
placeholder={m.account_size_placeholder()}
options={clothingSizes.map((s) => ({
value: s,
label: s,
}))}
/>
<Select
label={m.account_hoodie_size()}
bind:value={hoodieSize}
placeholder={m.account_size_placeholder()}
options={clothingSizes.map((s) => ({
value: s,
label: s,
}))}
/>
</div>
<div
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
>
<Button fullWidth onclick={saveProfile} loading={isSaving}>
{m.account_save_profile()}
</Button>
</div>
</div>
<!-- Appearance panel -->
<div
class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
>
<div
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 shrink-0 w-full"
>
<h3
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
>
{m.account_appearance()}
</h3>
</div>
<div
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
>
<Select
label={m.account_language()}
bind:value={currentLocale}
placeholder=""
options={locales.map((l) => ({
value: l,
label: localeLabels[l] ?? l,
}))}
onchange={(e) => {
const val = (e.target as HTMLSelectElement)?.value;
if (val)
handleLanguageChange(
val as (typeof locales)[number],
);
}}
/>
</div>
<div
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
>
<Button fullWidth onclick={saveProfile} loading={isSaving}>
{m.account_save_preferences()}
</Button>
</div>
</div>
</div>
</main>
</div>