ui: overhaul files, kanban, calendar, settings, chat modules
- FileBrowser: modernize breadcrumbs, toolbar, list/grid items, empty states - KanbanColumn: remove fixed height, border-based styling, compact header - KanbanCard: cleaner border styling, smaller tags, compact footer - Calendar: compact nav bar, border grid, today circle indicator, day view empty state - DocumentViewer: remove bg-night rounded-[32px], border-b header pattern - Settings tags: inline border/rounded-xl cards, icon action buttons - Chat: create +layout.svelte with PageHeader, overhaul sidebar and main area - Chat i18n: add nav_chat, chat_title, chat_subtitle keys (en + et) svelte-check: 0 errors, vitest: 112/112 passed
This commit is contained in:
@@ -254,6 +254,9 @@
|
|||||||
"entity_invite": "invite",
|
"entity_invite": "invite",
|
||||||
"entity_event": "event",
|
"entity_event": "event",
|
||||||
"nav_events": "Events",
|
"nav_events": "Events",
|
||||||
|
"nav_chat": "Chat",
|
||||||
|
"chat_title": "Chat",
|
||||||
|
"chat_subtitle": "Team messaging and communication",
|
||||||
"events_title": "Events",
|
"events_title": "Events",
|
||||||
"events_subtitle": "Organize and manage your events",
|
"events_subtitle": "Organize and manage your events",
|
||||||
"events_new": "New Event",
|
"events_new": "New Event",
|
||||||
|
|||||||
@@ -254,6 +254,9 @@
|
|||||||
"entity_invite": "kutse",
|
"entity_invite": "kutse",
|
||||||
"entity_event": "ürituse",
|
"entity_event": "ürituse",
|
||||||
"nav_events": "Üritused",
|
"nav_events": "Üritused",
|
||||||
|
"nav_chat": "Vestlus",
|
||||||
|
"chat_title": "Vestlus",
|
||||||
|
"chat_subtitle": "Meeskonna sõnumid ja suhtlus",
|
||||||
"events_title": "Üritused",
|
"events_title": "Üritused",
|
||||||
"events_subtitle": "Korralda ja halda oma üritusi",
|
"events_subtitle": "Korralda ja halda oma üritusi",
|
||||||
"events_new": "Uus üritus",
|
"events_new": "Uus üritus",
|
||||||
|
|||||||
@@ -123,63 +123,63 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full gap-2">
|
<div class="flex flex-col h-full">
|
||||||
<!-- Navigation bar -->
|
<!-- Navigation bar -->
|
||||||
<div class="flex items-center justify-between px-2">
|
<div class="flex items-center justify-between px-4 py-2 shrink-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
class="p-1 text-light/60 hover:text-light hover:bg-dark rounded-full transition-colors"
|
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
onclick={prev}
|
onclick={prev}
|
||||||
aria-label="Previous"
|
aria-label="Previous"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded"
|
class="material-symbols-rounded"
|
||||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
||||||
>chevron_left</span
|
>chevron_left</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
class="font-heading text-h4 text-white min-w-[200px] text-center"
|
class="font-heading text-body-sm text-white min-w-[180px] text-center"
|
||||||
>{headerTitle}</span
|
>{headerTitle}</span
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="p-1 text-light/60 hover:text-light hover:bg-dark rounded-full transition-colors"
|
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
onclick={next}
|
onclick={next}
|
||||||
aria-label="Next"
|
aria-label="Next"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded"
|
class="material-symbols-rounded"
|
||||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
||||||
>chevron_right</span
|
>chevron_right</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1 text-body-md font-body text-light/60 hover:text-white hover:bg-dark rounded-[32px] transition-colors ml-2"
|
class="px-2.5 py-1 text-body-sm font-body text-light/50 hover:text-white hover:bg-dark/50 rounded-lg transition-colors ml-1"
|
||||||
onclick={goToToday}
|
onclick={goToToday}
|
||||||
>
|
>
|
||||||
Today
|
Today
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex bg-dark rounded-[32px] p-0.5">
|
<div class="flex gap-0.5 bg-dark/30 rounded-lg p-0.5">
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1 text-body-md font-body rounded-[32px] transition-colors {currentView ===
|
class="px-2.5 py-1 text-[12px] font-body rounded-md transition-colors {currentView ===
|
||||||
'day'
|
'day'
|
||||||
? 'bg-primary text-night'
|
? 'bg-primary text-background'
|
||||||
: 'text-light/60 hover:text-light'}"
|
: 'text-light/50 hover:text-white'}"
|
||||||
onclick={() => (currentView = "day")}>Day</button
|
onclick={() => (currentView = "day")}>Day</button
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1 text-body-md font-body rounded-[32px] transition-colors {currentView ===
|
class="px-2.5 py-1 text-[12px] font-body rounded-md transition-colors {currentView ===
|
||||||
'week'
|
'week'
|
||||||
? 'bg-primary text-night'
|
? 'bg-primary text-background'
|
||||||
: 'text-light/60 hover:text-light'}"
|
: 'text-light/50 hover:text-white'}"
|
||||||
onclick={() => (currentView = "week")}>Week</button
|
onclick={() => (currentView = "week")}>Week</button
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1 text-body-md font-body rounded-[32px] transition-colors {currentView ===
|
class="px-2.5 py-1 text-[12px] font-body rounded-md transition-colors {currentView ===
|
||||||
'month'
|
'month'
|
||||||
? 'bg-primary text-night'
|
? 'bg-primary text-background'
|
||||||
: 'text-light/60 hover:text-light'}"
|
: 'text-light/50 hover:text-white'}"
|
||||||
onclick={() => (currentView = "month")}>Month</button
|
onclick={() => (currentView = "month")}>Month</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,48 +187,40 @@
|
|||||||
|
|
||||||
<!-- Month View -->
|
<!-- Month View -->
|
||||||
{#if currentView === "month"}
|
{#if currentView === "month"}
|
||||||
<div
|
<div class="flex flex-col flex-1 min-h-0">
|
||||||
class="flex flex-col flex-1 gap-2 min-h-0 bg-background rounded-xl p-2"
|
|
||||||
>
|
|
||||||
<!-- Day Headers -->
|
<!-- Day Headers -->
|
||||||
<div class="grid grid-cols-7 gap-2">
|
<div class="grid grid-cols-7 border-b border-light/5">
|
||||||
{#each weekDayHeaders as day}
|
{#each weekDayHeaders as day}
|
||||||
<div class="flex items-center justify-center py-2 px-2">
|
<div class="flex items-center justify-center py-2">
|
||||||
<span
|
<span class="font-body text-[11px] text-light/40 uppercase tracking-wider">{day}</span>
|
||||||
class="font-heading text-h4 text-white text-center"
|
|
||||||
>{day}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Calendar Grid -->
|
<!-- Calendar Grid -->
|
||||||
<div
|
<div class="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
class="flex-1 flex flex-col gap-2 min-h-0 rounded-lg overflow-hidden"
|
|
||||||
>
|
|
||||||
{#each weeks as week}
|
{#each weeks as week}
|
||||||
<div class="grid grid-cols-7 gap-2 flex-1">
|
<div class="grid grid-cols-7 flex-1 border-b border-light/5 last:border-b-0">
|
||||||
{#each week as day}
|
{#each week as day}
|
||||||
{@const dayEvents = getEventsForDay(day)}
|
{@const dayEvents = getEventsForDay(day)}
|
||||||
{@const isToday = isSameDay(day, today)}
|
{@const isToday = isSameDay(day, today)}
|
||||||
{@const inMonth = isCurrentMonth(day)}
|
{@const inMonth = isCurrentMonth(day)}
|
||||||
<div
|
<button
|
||||||
class="bg-night rounded-none flex flex-col items-start px-2 py-2.5 overflow-hidden transition-colors hover:bg-dark/50 min-h-0 cursor-pointer
|
type="button"
|
||||||
{!inMonth ? 'opacity-50' : ''}"
|
class="flex flex-col items-start px-1.5 py-1 overflow-hidden transition-colors hover:bg-dark/30 min-h-0 cursor-pointer border-r border-light/5 last:border-r-0
|
||||||
|
{!inMonth ? 'opacity-40' : ''}"
|
||||||
onclick={() => onDateClick?.(day)}
|
onclick={() => onDateClick?.(day)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="font-body text-body text-white {isToday
|
class="text-[12px] font-body w-6 h-6 flex items-center justify-center rounded-full shrink-0
|
||||||
? 'text-primary font-bold'
|
{isToday ? 'bg-primary text-background font-bold' : 'text-light/60'}"
|
||||||
: ''}"
|
|
||||||
>
|
>
|
||||||
{day.getDate()}
|
{day.getDate()}
|
||||||
</span>
|
</span>
|
||||||
{#each dayEvents.slice(0, 2) as event}
|
{#each dayEvents.slice(0, 2) as event}
|
||||||
<button
|
<button
|
||||||
class="w-full mt-1 px-2 py-0.5 rounded-[4px] text-body-sm font-bold font-body text-night truncate text-left"
|
class="w-full mt-0.5 px-1.5 py-0.5 rounded text-[11px] font-body text-night truncate text-left font-medium"
|
||||||
style="background-color: {event.color ??
|
style="background-color: {event.color ?? '#00A3E0'}"
|
||||||
'#00A3E0'}"
|
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEventClick?.(event);
|
onEventClick?.(event);
|
||||||
@@ -238,12 +230,9 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{#if dayEvents.length > 2}
|
{#if dayEvents.length > 2}
|
||||||
<span
|
<span class="text-[10px] text-light/30 mt-0.5 px-1">+{dayEvents.length - 2}</span>
|
||||||
class="text-body-sm text-light/40 mt-0.5"
|
|
||||||
>+{dayEvents.length - 2} more</span
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -253,40 +242,25 @@
|
|||||||
|
|
||||||
<!-- Week View -->
|
<!-- Week View -->
|
||||||
{#if currentView === "week"}
|
{#if currentView === "week"}
|
||||||
<div
|
<div class="flex flex-col flex-1 min-h-0">
|
||||||
class="flex flex-col flex-1 gap-2 min-h-0 bg-background rounded-xl p-2"
|
<div class="grid grid-cols-7 flex-1 overflow-hidden">
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-7 gap-2 flex-1 rounded-lg overflow-hidden"
|
|
||||||
>
|
|
||||||
{#each weekDates as day}
|
{#each weekDates as day}
|
||||||
{@const dayEvents = getEventsForDay(day)}
|
{@const dayEvents = getEventsForDay(day)}
|
||||||
{@const isToday = isSameDay(day, today)}
|
{@const isToday = isSameDay(day, today)}
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden border-r border-light/5 last:border-r-0">
|
||||||
<div class="px-2 py-2 text-center">
|
<div class="px-2 py-2 text-center border-b border-light/5">
|
||||||
<div
|
<div class="text-[11px] font-body uppercase tracking-wider {isToday ? 'text-primary' : 'text-light/40'}">
|
||||||
class="font-heading text-h4 {isToday
|
|
||||||
? 'text-primary'
|
|
||||||
: 'text-white'}"
|
|
||||||
>
|
|
||||||
{weekDayHeaders[(day.getDay() + 6) % 7]}
|
{weekDayHeaders[(day.getDay() + 6) % 7]}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="text-body-sm font-heading mt-0.5 {isToday ? 'text-primary' : 'text-white'}">
|
||||||
class="font-body text-body-md {isToday
|
|
||||||
? 'text-primary'
|
|
||||||
: 'text-light/60'}"
|
|
||||||
>
|
|
||||||
{day.getDate()}
|
{day.getDate()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex-1 px-1.5 py-1.5 space-y-1 overflow-y-auto">
|
||||||
class="bg-night flex-1 px-2 pb-2 space-y-1 overflow-y-auto"
|
|
||||||
>
|
|
||||||
{#each dayEvents as event}
|
{#each dayEvents as event}
|
||||||
<button
|
<button
|
||||||
class="w-full px-2 py-1.5 rounded-[4px] text-body-sm font-bold font-body text-night truncate text-left"
|
class="w-full px-2 py-1.5 rounded text-[11px] font-body text-night truncate text-left font-medium"
|
||||||
style="background-color: {event.color ??
|
style="background-color: {event.color ?? '#00A3E0'}"
|
||||||
'#00A3E0'}"
|
|
||||||
onclick={() => onEventClick?.(event)}
|
onclick={() => onEventClick?.(event)}
|
||||||
>
|
>
|
||||||
{event.title}
|
{event.title}
|
||||||
@@ -302,27 +276,24 @@
|
|||||||
<!-- Day View -->
|
<!-- Day View -->
|
||||||
{#if currentView === "day"}
|
{#if currentView === "day"}
|
||||||
{@const dayEvents = getEventsForDay(currentDate)}
|
{@const dayEvents = getEventsForDay(currentDate)}
|
||||||
<div class="flex-1 bg-night px-4 py-5 min-h-0 overflow-auto">
|
<div class="flex-1 px-4 py-4 min-h-0 overflow-auto">
|
||||||
{#if dayEvents.length === 0}
|
{#if dayEvents.length === 0}
|
||||||
<div class="text-center text-light/40 py-12">
|
<div class="flex flex-col items-center justify-center h-full text-light/40">
|
||||||
<p class="font-body text-body">No events for this day</p>
|
<span class="material-symbols-rounded mb-3" style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;">event_busy</span>
|
||||||
|
<p class="text-body-sm">No events for this day</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each dayEvents as event}
|
{#each dayEvents as event}
|
||||||
<button
|
<button
|
||||||
class="w-full text-left p-3 rounded-[8px] transition-colors hover:opacity-80"
|
class="w-full text-left p-3 rounded-xl border border-light/5 hover:border-light/10 transition-all"
|
||||||
style="background-color: {event.color ??
|
style="border-left: 3px solid {event.color ?? '#00A3E0'}"
|
||||||
'#00A3E0'}20; border-left: 3px solid {event.color ??
|
|
||||||
'#00A3E0'}"
|
|
||||||
onclick={() => onEventClick?.(event)}
|
onclick={() => onEventClick?.(event)}
|
||||||
>
|
>
|
||||||
<div class="font-heading text-h5 text-white">
|
<div class="font-heading text-body-sm text-white">
|
||||||
{event.title}
|
{event.title}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="text-[12px] text-light/40 mt-1">
|
||||||
class="font-body text-body-md text-light/60 mt-1"
|
|
||||||
>
|
|
||||||
{new Date(event.start_time).toLocaleTimeString(
|
{new Date(event.start_time).toLocaleTimeString(
|
||||||
"en-US",
|
"en-US",
|
||||||
{ hour: "numeric", minute: "2-digit" },
|
{ hour: "numeric", minute: "2-digit" },
|
||||||
@@ -333,9 +304,7 @@
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{#if event.description}
|
{#if event.description}
|
||||||
<div
|
<div class="text-[12px] text-light/30 mt-1.5 line-clamp-2">
|
||||||
class="font-body text-body-md text-light/50 mt-2"
|
|
||||||
>
|
|
||||||
{event.description}
|
{event.description}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -42,17 +42,15 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="flex flex-col min-w-0 h-full overflow-hidden">
|
||||||
class="bg-night rounded-[32px] overflow-hidden flex flex-col min-w-0 h-full"
|
|
||||||
>
|
|
||||||
<!-- Lock Banner -->
|
<!-- Lock Banner -->
|
||||||
{#if locked}
|
{#if locked}
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 px-4 py-2.5 bg-warning/10 border-b border-warning/20"
|
class="flex items-center gap-2 px-4 py-2 bg-warning/10 border-b border-warning/20 shrink-0"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-warning"
|
class="material-symbols-rounded text-warning"
|
||||||
style="font-size: 20px; font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
style="font-size: 18px; font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||||
>
|
>
|
||||||
lock
|
lock
|
||||||
</span>
|
</span>
|
||||||
@@ -64,42 +62,35 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="flex items-center gap-2 px-4 py-5">
|
<div class="flex items-center gap-2 px-5 py-3 border-b border-light/5 shrink-0">
|
||||||
<h2 class="flex-1 font-heading text-h1 text-white truncate">
|
<h2 class="flex-1 font-heading text-body-sm text-white truncate">
|
||||||
{document.name}
|
{document.name}
|
||||||
</h2>
|
</h2>
|
||||||
{#if locked}
|
{#if locked}
|
||||||
<Button size="md" disabled>
|
<Button size="sm" disabled>Locked</Button>
|
||||||
<span
|
|
||||||
class="material-symbols-rounded mr-1"
|
|
||||||
style="font-size: 16px; font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
|
||||||
>lock</span
|
|
||||||
>
|
|
||||||
Locked
|
|
||||||
</Button>
|
|
||||||
{:else if mode === "edit"}
|
{:else if mode === "edit"}
|
||||||
<Button size="md" onclick={handleEditClick}>
|
<Button size="sm" onclick={handleEditClick}>
|
||||||
{isEditing ? "Preview" : "Edit"}
|
{isEditing ? "Preview" : "Edit"}
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button size="md" onclick={handleEditClick}>Edit</Button>
|
<Button size="sm" onclick={handleEditClick}>Edit</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="p-1 hover:bg-dark rounded-full transition-colors"
|
class="p-1 hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
aria-label="More options"
|
aria-label="More options"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light"
|
class="material-symbols-rounded text-light/40 hover:text-white"
|
||||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||||
>
|
>
|
||||||
more_horiz
|
more_horiz
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<!-- Editor Area -->
|
<!-- Editor Area -->
|
||||||
<div class="flex-1 bg-background rounded-[32px] mx-4 mb-4 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
<Editor {document} {onSave} editable={isEditing} />
|
<Editor {document} {onSave} editable={isEditing} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,9 +5,6 @@
|
|||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
Input,
|
Input,
|
||||||
Avatar,
|
|
||||||
IconButton,
|
|
||||||
Icon,
|
|
||||||
} from "$lib/components/ui";
|
} from "$lib/components/ui";
|
||||||
import { DocumentViewer } from "$lib/components/documents";
|
import { DocumentViewer } from "$lib/components/documents";
|
||||||
import { createLogger } from "$lib/utils/logger";
|
import { createLogger } from "$lib/utils/logger";
|
||||||
@@ -490,97 +487,101 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full gap-4">
|
<div class="flex h-full gap-0">
|
||||||
<!-- Files Panel -->
|
<!-- Files Panel -->
|
||||||
<div
|
<div class="flex flex-col flex-1 min-w-0 h-full overflow-hidden">
|
||||||
class="bg-night rounded-[32px] flex flex-col gap-4 px-4 py-5 overflow-hidden flex-1 min-w-0 h-full"
|
<!-- Toolbar: Breadcrumbs + Actions -->
|
||||||
>
|
<div class="flex items-center gap-2 px-6 py-3 border-b border-light/5 shrink-0">
|
||||||
<!-- Header -->
|
<!-- Breadcrumb Path -->
|
||||||
<header class="flex items-center gap-2 p-1">
|
<nav class="flex items-center gap-1 flex-1 min-w-0 overflow-x-auto">
|
||||||
<Avatar name={title} size="md" />
|
{#each breadcrumbPath as crumb, i}
|
||||||
<h1 class="flex-1 font-heading text-h1 text-white">{title}</h1>
|
{#if i > 0}
|
||||||
<Button size="md" onclick={handleAdd}>{m.btn_new()}</Button>
|
<span
|
||||||
<IconButton title={m.files_toggle_view()} onclick={toggleViewMode}>
|
class="material-symbols-rounded text-light/20 shrink-0"
|
||||||
<Icon
|
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||||
name={viewMode === "list" ? "grid_view" : "view_list"}
|
>
|
||||||
size={24}
|
chevron_right
|
||||||
/>
|
</span>
|
||||||
</IconButton>
|
{/if}
|
||||||
</header>
|
<a
|
||||||
|
href={getFolderUrl(crumb.id)}
|
||||||
<!-- Breadcrumb Path -->
|
class="px-2 py-1 rounded-lg text-body-sm font-body whitespace-nowrap transition-colors
|
||||||
<nav class="flex items-center gap-2 text-h3 font-heading">
|
{crumb.id === currentFolderId
|
||||||
{#each breadcrumbPath as crumb, i}
|
? 'text-white bg-dark/30'
|
||||||
{#if i > 0}
|
: 'text-light/50 hover:text-white hover:bg-dark/30'}
|
||||||
<span
|
{dragOverBreadcrumb === (crumb.id ?? '__root__')
|
||||||
class="material-symbols-rounded text-light/30"
|
? 'ring-2 ring-primary bg-primary/10'
|
||||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
: ''}"
|
||||||
>
|
ondragover={(e) => {
|
||||||
chevron_right
|
e.preventDefault();
|
||||||
</span>
|
e.stopPropagation();
|
||||||
{/if}
|
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||||||
<a
|
dragOverBreadcrumb = crumb.id ?? "__root__";
|
||||||
href={getFolderUrl(crumb.id)}
|
}}
|
||||||
class="px-3 py-1 rounded-xl transition-colors
|
ondragleave={() => {
|
||||||
{crumb.id === currentFolderId
|
dragOverBreadcrumb = undefined;
|
||||||
? 'text-white'
|
}}
|
||||||
: 'text-light/60 hover:text-primary'}
|
ondrop={async (e) => {
|
||||||
{dragOverBreadcrumb === (crumb.id ?? '__root__')
|
e.preventDefault();
|
||||||
? 'ring-2 ring-primary bg-primary/10'
|
e.stopPropagation();
|
||||||
: ''}"
|
dragOverBreadcrumb = undefined;
|
||||||
ondragover={(e) => {
|
if (!draggedItem) return;
|
||||||
e.preventDefault();
|
if (draggedItem.parent_id === crumb.id) {
|
||||||
e.stopPropagation();
|
resetDragState();
|
||||||
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
return;
|
||||||
dragOverBreadcrumb = crumb.id ?? "__root__";
|
}
|
||||||
}}
|
const draggedName = draggedItem.name;
|
||||||
ondragleave={() => {
|
await handleMove(draggedItem.id, crumb.id);
|
||||||
dragOverBreadcrumb = undefined;
|
toasts.success(
|
||||||
}}
|
`Moved "${draggedName}" to "${crumb.name}"`,
|
||||||
ondrop={async (e) => {
|
);
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
dragOverBreadcrumb = undefined;
|
|
||||||
if (!draggedItem) return;
|
|
||||||
if (draggedItem.parent_id === crumb.id) {
|
|
||||||
resetDragState();
|
resetDragState();
|
||||||
return;
|
}}
|
||||||
}
|
>
|
||||||
const draggedName = draggedItem.name;
|
{#if i === 0}
|
||||||
await handleMove(draggedItem.id, crumb.id);
|
<span class="material-symbols-rounded" style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;">home</span>
|
||||||
toasts.success(
|
{:else}
|
||||||
`Moved "${draggedName}" to "${crumb.name}"`,
|
{crumb.name}
|
||||||
);
|
{/if}
|
||||||
resetDragState();
|
</a>
|
||||||
}}
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Button size="sm" icon="add" onclick={handleAdd}>{m.btn_new()}</Button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-1.5 rounded-lg text-light/40 hover:text-white hover:bg-dark/50 transition-colors"
|
||||||
|
title={m.files_toggle_view()}
|
||||||
|
onclick={toggleViewMode}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded"
|
||||||
|
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
||||||
|
>{viewMode === "list" ? "grid_view" : "view_list"}</span
|
||||||
>
|
>
|
||||||
{crumb.name}
|
</button>
|
||||||
</a>
|
</div>
|
||||||
{/each}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- File List/Grid -->
|
<!-- File List/Grid -->
|
||||||
<div class="flex-1 overflow-auto min-h-0">
|
<div class="flex-1 overflow-auto min-h-0 p-4">
|
||||||
{#if viewMode === "list"}
|
{#if viewMode === "list"}
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-1"
|
class="flex flex-col gap-0.5"
|
||||||
ondragover={handleContainerDragOver}
|
ondragover={handleContainerDragOver}
|
||||||
ondrop={handleDropOnEmpty}
|
ondrop={handleDropOnEmpty}
|
||||||
role="list"
|
role="list"
|
||||||
>
|
>
|
||||||
{#if currentFolderItems.length === 0}
|
{#if currentFolderItems.length === 0}
|
||||||
<div class="text-center text-light/40 py-8 text-sm">
|
<div class="flex flex-col items-center justify-center text-light/40 py-16">
|
||||||
<p>
|
<span class="material-symbols-rounded mb-3" style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;">folder_open</span>
|
||||||
No files yet. Drag files here or create a new
|
<p class="text-body-sm">{m.files_empty()}</p>
|
||||||
one.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each currentFolderItems as item}
|
{#each currentFolderItems as item}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-2 h-10 pl-1 pr-2 py-1 rounded-[32px] w-full text-left transition-colors hover:bg-dark
|
class="flex items-center gap-3 px-3 py-2 rounded-xl w-full text-left transition-colors hover:bg-dark/50
|
||||||
{selectedDoc?.id === item.id ? 'bg-dark' : ''}
|
{selectedDoc?.id === item.id ? 'bg-dark/50 ring-1 ring-primary/20' : ''}
|
||||||
{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"
|
||||||
@@ -595,24 +596,20 @@
|
|||||||
oncontextmenu={(e) =>
|
oncontextmenu={(e) =>
|
||||||
handleContextMenu(e, item)}
|
handleContextMenu(e, item)}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
class="w-8 h-8 flex items-center justify-center p-1"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="material-symbols-rounded text-light"
|
|
||||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
|
||||||
>
|
|
||||||
{getDocIcon(item)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span
|
<span
|
||||||
class="font-body text-body text-white truncate flex-1"
|
class="material-symbols-rounded shrink-0 {item.type === 'folder' ? 'text-amber-400' : item.type === 'kanban' ? 'text-purple-400' : 'text-light/50'}"
|
||||||
|
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
||||||
|
>
|
||||||
|
{getDocIcon(item)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="font-body text-body-sm text-white truncate flex-1"
|
||||||
>{item.name}</span
|
>{item.name}</span
|
||||||
>
|
>
|
||||||
{#if item.type === "folder"}
|
{#if item.type === "folder"}
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light/50"
|
class="material-symbols-rounded text-light/20"
|
||||||
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||||
>
|
>
|
||||||
chevron_right
|
chevron_right
|
||||||
</span>
|
</span>
|
||||||
@@ -624,26 +621,22 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<!-- Grid View -->
|
<!-- Grid View -->
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4"
|
class="grid grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-2"
|
||||||
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="col-span-full text-center text-light/40 py-8 text-sm"
|
<span class="material-symbols-rounded mb-3" style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;">folder_open</span>
|
||||||
>
|
<p class="text-body-sm">{m.files_empty()}</p>
|
||||||
<p>
|
|
||||||
No files yet. Drag files here or create a new
|
|
||||||
one.
|
|
||||||
</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-4 rounded-xl transition-colors hover:bg-dark
|
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
|
||||||
{selectedDoc?.id === item.id ? 'bg-dark' : ''}
|
{selectedDoc?.id === item.id ? 'bg-dark/50 border-primary/20' : ''}
|
||||||
{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"
|
||||||
@@ -659,13 +652,13 @@
|
|||||||
handleContextMenu(e, item)}
|
handleContextMenu(e, item)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light"
|
class="material-symbols-rounded {item.type === 'folder' ? 'text-amber-400' : item.type === 'kanban' ? 'text-purple-400' : 'text-light/40'}"
|
||||||
style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 48;"
|
style="font-size: 40px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 40;"
|
||||||
>
|
>
|
||||||
{getDocIcon(item)}
|
{getDocIcon(item)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="font-body text-body-md text-white text-center truncate w-full"
|
class="font-body text-[12px] text-white text-center truncate w-full"
|
||||||
>{item.name}</span
|
>{item.name}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
@@ -678,7 +671,7 @@
|
|||||||
|
|
||||||
<!-- Compact Editor Panel (shown when a doc is selected) -->
|
<!-- Compact Editor Panel (shown when a doc is selected) -->
|
||||||
{#if selectedDoc}
|
{#if selectedDoc}
|
||||||
<div class="flex-1 min-w-0 h-full">
|
<div class="flex-1 min-w-0 h-full border-l border-light/5">
|
||||||
<DocumentViewer
|
<DocumentViewer
|
||||||
document={selectedDoc}
|
document={selectedDoc}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bg-night rounded-[16px] p-2 cursor-pointer hover:ring-1 hover:ring-primary/30 transition-all group w-full text-left overflow-clip flex flex-col gap-2 relative"
|
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:opacity-50={isDragging}
|
class:opacity-50={isDragging}
|
||||||
{draggable}
|
{draggable}
|
||||||
{ondragstart}
|
{ondragstart}
|
||||||
@@ -67,25 +67,25 @@
|
|||||||
{#if ondelete}
|
{#if ondelete}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="absolute top-1 right-1 p-1 rounded-full opacity-0 group-hover:opacity-100 hover:bg-error/20 transition-all z-10"
|
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"
|
||||||
onclick={handleDelete}
|
onclick={handleDelete}
|
||||||
aria-label="Delete card"
|
aria-label="Delete card"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light/40 hover:text-error"
|
class="material-symbols-rounded text-light/30 hover:text-error"
|
||||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
||||||
>
|
>
|
||||||
delete
|
close
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Tags / Chips -->
|
<!-- Tags / Chips -->
|
||||||
{#if card.tags && card.tags.length > 0}
|
{#if card.tags && card.tags.length > 0}
|
||||||
<div class="flex gap-[10px] items-start flex-wrap">
|
<div class="flex gap-1 items-start flex-wrap">
|
||||||
{#each card.tags as tag}
|
{#each card.tags as tag}
|
||||||
<span
|
<span
|
||||||
class="rounded-[4px] px-1 py-[4px] font-body font-bold text-[14px] text-night leading-none overflow-clip"
|
class="rounded-[4px] px-1.5 py-0.5 font-body font-bold text-[11px] text-night leading-none"
|
||||||
style="background-color: {tag.color || '#00A3E0'}"
|
style="background-color: {tag.color || '#00A3E0'}"
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
@@ -95,55 +95,40 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<p class="font-body text-body text-white w-full leading-none p-1">
|
<p class="font-body text-body-sm 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">
|
<div class="flex items-center justify-between w-full mt-0.5">
|
||||||
<div class="flex gap-1 items-center">
|
<div class="flex gap-2 items-center text-[11px] text-light/40">
|
||||||
<!-- Due date -->
|
|
||||||
{#if card.due_date}
|
{#if card.due_date}
|
||||||
<div class="flex items-center">
|
<span class="flex items-center gap-0.5">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light p-1"
|
class="material-symbols-rounded"
|
||||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
|
||||||
>
|
>calendar_today</span>
|
||||||
calendar_today
|
{formatDueDate(card.due_date)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
|
||||||
class="font-body text-[12px] text-light leading-none"
|
|
||||||
>
|
|
||||||
{formatDueDate(card.due_date)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Checklist -->
|
|
||||||
{#if (card.checklist_total ?? 0) > 0}
|
{#if (card.checklist_total ?? 0) > 0}
|
||||||
<div class="flex items-center">
|
<span class="flex items-center gap-0.5">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light p-1"
|
class="material-symbols-rounded"
|
||||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
|
||||||
>
|
>check_box</span>
|
||||||
check_box
|
{card.checklist_done ?? 0}/{card.checklist_total}
|
||||||
</span>
|
</span>
|
||||||
<span
|
|
||||||
class="font-body text-[12px] text-light leading-none"
|
|
||||||
>
|
|
||||||
{card.checklist_done ?? 0}/{card.checklist_total}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Assignee avatar -->
|
|
||||||
{#if card.assignee_id}
|
{#if card.assignee_id}
|
||||||
<Avatar
|
<Avatar
|
||||||
name={card.assignee_name || "?"}
|
name={card.assignee_name || "?"}
|
||||||
src={card.assignee_avatar}
|
src={card.assignee_avatar}
|
||||||
size="sm"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,28 +14,25 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-background flex flex-col gap-4 items-start overflow-hidden px-4 py-5 rounded-[32px] w-64 h-[512px]"
|
class="bg-dark/20 border border-light/5 flex flex-col overflow-hidden rounded-xl w-[272px] shrink-0 h-full"
|
||||||
>
|
>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center gap-2 p-1 rounded-[32px] w-full">
|
<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-h4 text-white truncate">{title}</span
|
<span class="font-heading text-body-sm text-white truncate">{title}</span>
|
||||||
>
|
<span
|
||||||
<div
|
class="text-[11px] text-light/40 bg-light/5 px-1.5 py-0.5 rounded-md shrink-0"
|
||||||
class="bg-dark flex items-center justify-center p-1 rounded-lg shrink-0"
|
>{count}</span>
|
||||||
>
|
|
||||||
<span class="font-heading text-h6 text-white">{count}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{#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="p-0.5 flex items-center justify-center hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
onclick={onMore}
|
onclick={onMore}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light"
|
class="material-symbols-rounded text-light/40 hover:text-white"
|
||||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||||
>
|
>
|
||||||
more_horiz
|
more_horiz
|
||||||
</span>
|
</span>
|
||||||
@@ -45,7 +42,7 @@
|
|||||||
|
|
||||||
<!-- Cards container -->
|
<!-- Cards container -->
|
||||||
<div
|
<div
|
||||||
class="flex-1 flex flex-col gap-2 items-start overflow-y-auto w-full 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()}
|
||||||
@@ -54,8 +51,10 @@
|
|||||||
|
|
||||||
<!-- Add button -->
|
<!-- Add button -->
|
||||||
{#if onAddCard}
|
{#if onAddCard}
|
||||||
<Button variant="secondary" fullWidth icon="add" onclick={onAddCard}>
|
<div class="px-2 pb-2">
|
||||||
Add card
|
<Button variant="tertiary" fullWidth size="sm" icon="add" onclick={onAddCard}>
|
||||||
</Button>
|
Add card
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
36
src/routes/[orgSlug]/chat/+layout.svelte
Normal file
36
src/routes/[orgSlug]/chat/+layout.svelte
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import { navigating } from "$app/stores";
|
||||||
|
import { PageHeader, ContentSkeleton } from "$lib/components/ui";
|
||||||
|
import * as m from "$lib/paraglide/messages";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: {
|
||||||
|
org: { id: string; name: string; slug: string };
|
||||||
|
};
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data, children }: Props = $props();
|
||||||
|
|
||||||
|
const isNavigatingHere = $derived(
|
||||||
|
$navigating?.to?.url.pathname?.includes(`/${data.org.slug}/chat`),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-full overflow-hidden">
|
||||||
|
<PageHeader
|
||||||
|
title={m.chat_title()}
|
||||||
|
subtitle={m.chat_subtitle()}
|
||||||
|
icon="chat"
|
||||||
|
iconColor="text-primary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if isNavigatingHere && !$navigating?.from?.url.pathname?.includes(`/${data.org.slug}/chat`)}
|
||||||
|
<ContentSkeleton variant="default" />
|
||||||
|
{:else}
|
||||||
|
<div class="flex-1 overflow-hidden">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -392,9 +392,9 @@
|
|||||||
<!-- Matrix Login Modal -->
|
<!-- Matrix Login Modal -->
|
||||||
{#if showMatrixLogin}
|
{#if showMatrixLogin}
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<div class="bg-night rounded-[32px] p-8 w-full max-w-md">
|
<div class="bg-dark/30 border border-light/5 rounded-xl p-8 w-full max-w-md">
|
||||||
<h2 class="font-heading text-h3 text-white mb-2">Connect to Chat</h2>
|
<h2 class="font-heading text-body text-white mb-1">Connect to Chat</h2>
|
||||||
<p class="text-light/50 text-body mb-6">
|
<p class="text-body-sm text-light/50 mb-6">
|
||||||
Enter your Matrix credentials to enable messaging.
|
Enter your Matrix credentials to enable messaging.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -410,12 +410,12 @@
|
|||||||
placeholder="@user:matrix.org"
|
placeholder="@user:matrix.org"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-body-sm font-body text-light mb-1">Password</label>
|
<label class="block text-body-sm font-body text-light/60 mb-1">Password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
bind:value={matrixPassword}
|
bind:value={matrixPassword}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
class="w-full bg-dark border border-light/10 rounded-2xl px-4 py-3 text-white font-body text-body placeholder:text-light/30 focus:outline-none focus:border-primary"
|
class="w-full bg-dark border border-light/10 rounded-xl px-3 py-2 text-white font-body text-body-sm placeholder:text-light/30 focus:outline-none focus:border-primary"
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (e.key === "Enter") handleMatrixLogin();
|
if (e.key === "Enter") handleMatrixLogin();
|
||||||
}}
|
}}
|
||||||
@@ -438,9 +438,9 @@
|
|||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div
|
<div
|
||||||
class="animate-spin w-12 h-12 border-4 border-primary border-t-transparent rounded-full mx-auto mb-4"
|
class="animate-spin w-10 h-10 border-3 border-primary border-t-transparent rounded-full mx-auto mb-4"
|
||||||
></div>
|
></div>
|
||||||
<p class="text-light/50">
|
<p class="text-body-sm text-light/40">
|
||||||
{#if isInitializing}
|
{#if isInitializing}
|
||||||
Connecting to Matrix...
|
Connecting to Matrix...
|
||||||
{:else if $syncState === "CATCHUP"}
|
{:else if $syncState === "CATCHUP"}
|
||||||
@@ -460,56 +460,50 @@
|
|||||||
{:else if matrixClient}
|
{:else if matrixClient}
|
||||||
<MatrixProvider client={matrixClient}>
|
<MatrixProvider client={matrixClient}>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<div class="h-full flex gap-2 min-h-0">
|
<div class="h-full flex min-h-0">
|
||||||
<!-- Chat Sidebar -->
|
<!-- Chat Sidebar -->
|
||||||
<aside class="w-56 bg-night rounded-[32px] flex flex-col overflow-hidden shrink-0">
|
<aside class="w-56 border-r border-light/5 flex flex-col overflow-hidden shrink-0">
|
||||||
<header class="px-3 py-5">
|
<div class="flex items-center gap-2 px-3 py-2.5 border-b border-light/5">
|
||||||
<div class="flex items-center gap-2">
|
<span class="flex-1 font-heading text-body-sm text-white">Messages</span>
|
||||||
<span class="material-symbols-rounded text-light" style="font-size: 20px;">chat</span>
|
<button
|
||||||
<span class="flex-1 font-heading text-light text-base">Messages</span>
|
class="p-1 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
<button
|
onclick={() => (showStartDMModal = true)}
|
||||||
class="text-light hover:text-primary transition-colors"
|
title="New message"
|
||||||
onclick={() => (showStartDMModal = true)}
|
>
|
||||||
title="New message"
|
<span class="material-symbols-rounded" style="font-size: 18px;">add</span>
|
||||||
>
|
</button>
|
||||||
<span class="material-symbols-rounded" style="font-size: 20px;">add</span>
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Room search -->
|
<!-- Room search -->
|
||||||
<div class="px-3 pb-2">
|
<div class="px-2 py-2">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded absolute left-3 top-1/2 -translate-y-1/2 text-light/40"
|
class="material-symbols-rounded absolute left-2.5 top-1/2 -translate-y-1/2 text-light/30"
|
||||||
style="font-size: 16px;"
|
style="font-size: 16px;"
|
||||||
>search</span>
|
>search</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={roomSearchQuery}
|
bind:value={roomSearchQuery}
|
||||||
placeholder="Search rooms..."
|
placeholder="Search..."
|
||||||
class="w-full pl-9 pr-3 py-2 bg-dark text-light text-sm rounded-lg border border-light/10 placeholder:text-light/30 focus:outline-none focus:border-primary"
|
class="w-full pl-8 pr-3 py-1.5 bg-dark/50 text-white text-[12px] rounded-lg border border-light/5 placeholder:text-light/30 focus:outline-none focus:border-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Room list (sectioned) -->
|
<!-- Room list (sectioned) -->
|
||||||
<nav class="flex-1 overflow-y-auto px-2 pb-2">
|
<nav class="flex-1 overflow-y-auto px-1.5 pb-2">
|
||||||
{#if allRooms.length === 0}
|
{#if allRooms.length === 0}
|
||||||
<p class="text-light/40 text-sm text-center py-8">
|
<p class="text-light/30 text-[12px] text-center py-8">
|
||||||
{roomSearchQuery ? "No matching rooms" : "No rooms yet"}
|
{roomSearchQuery ? "No matching rooms" : "No rooms yet"}
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Org / Space Rooms -->
|
<!-- Org / Space Rooms -->
|
||||||
{#if filteredOrgRooms.length > 0}
|
{#if filteredOrgRooms.length > 0}
|
||||||
<div class="mb-2">
|
<div class="mb-1.5">
|
||||||
<div class="flex items-center justify-between px-2 py-1">
|
<div class="flex items-center justify-between px-2 py-1">
|
||||||
<span class="text-xs font-semibold text-light/40 uppercase tracking-wider">
|
<span class="text-[10px] font-body text-light/30 uppercase tracking-wider">Organization</span>
|
||||||
<span class="material-symbols-rounded align-middle" style="font-size: 14px;">workspaces</span>
|
|
||||||
Organization
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
class="w-5 h-5 flex items-center justify-center text-light/40 hover:text-light hover:bg-light/10 rounded transition-colors"
|
class="p-0.5 text-light/30 hover:text-white hover:bg-dark/50 rounded transition-colors"
|
||||||
onclick={() => (showCreateRoomModal = true)}
|
onclick={() => (showCreateRoomModal = true)}
|
||||||
title="Create room"
|
title="Create room"
|
||||||
>
|
>
|
||||||
@@ -520,16 +514,16 @@
|
|||||||
{#each filteredOrgRooms as room (room.roomId)}
|
{#each filteredOrgRooms as room (room.roomId)}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] transition-colors text-left
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg transition-colors text-left
|
||||||
{$selectedRoomId === room.roomId ? 'bg-primary/20' : 'hover:bg-light/5'}"
|
{$selectedRoomId === room.roomId ? 'bg-primary/10 text-white' : 'text-light/60 hover:bg-dark/50 hover:text-white'}"
|
||||||
onclick={() => handleRoomSelect(room.roomId)}
|
onclick={() => handleRoomSelect(room.roomId)}
|
||||||
>
|
>
|
||||||
<Avatar src={room.avatarUrl} name={room.name} size="xs" />
|
<Avatar src={room.avatarUrl} name={room.name} size="xs" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<span class="font-bold text-sm text-light truncate block">{room.name}</span>
|
<span class="text-[12px] font-body truncate block">{room.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if room.unreadCount > 0}
|
{#if room.unreadCount > 0}
|
||||||
<span class="bg-primary text-white text-xs px-1.5 py-0.5 rounded-full min-w-[18px] text-center">
|
<span class="bg-primary text-background text-[10px] px-1.5 py-0.5 rounded-full min-w-[16px] text-center font-bold">
|
||||||
{room.unreadCount > 99 ? "99+" : room.unreadCount}
|
{room.unreadCount > 99 ? "99+" : room.unreadCount}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -542,14 +536,11 @@
|
|||||||
|
|
||||||
<!-- Direct Messages -->
|
<!-- Direct Messages -->
|
||||||
{#if filteredDmRooms.length > 0}
|
{#if filteredDmRooms.length > 0}
|
||||||
<div class="mb-2">
|
<div class="mb-1.5">
|
||||||
<div class="flex items-center justify-between px-2 py-1">
|
<div class="flex items-center justify-between px-2 py-1">
|
||||||
<span class="text-xs font-semibold text-light/40 uppercase tracking-wider">
|
<span class="text-[10px] font-body text-light/30 uppercase tracking-wider">Direct Messages</span>
|
||||||
<span class="material-symbols-rounded align-middle" style="font-size: 14px;">chat_bubble</span>
|
|
||||||
Direct Messages
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
class="w-5 h-5 flex items-center justify-center text-light/40 hover:text-light hover:bg-light/10 rounded transition-colors"
|
class="p-0.5 text-light/30 hover:text-white hover:bg-dark/50 rounded transition-colors"
|
||||||
onclick={() => (showStartDMModal = true)}
|
onclick={() => (showStartDMModal = true)}
|
||||||
title="New DM"
|
title="New DM"
|
||||||
>
|
>
|
||||||
@@ -560,16 +551,16 @@
|
|||||||
{#each filteredDmRooms as room (room.roomId)}
|
{#each filteredDmRooms as room (room.roomId)}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] transition-colors text-left
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg transition-colors text-left
|
||||||
{$selectedRoomId === room.roomId ? 'bg-primary/20' : 'hover:bg-light/5'}"
|
{$selectedRoomId === room.roomId ? 'bg-primary/10 text-white' : 'text-light/60 hover:bg-dark/50 hover:text-white'}"
|
||||||
onclick={() => handleRoomSelect(room.roomId)}
|
onclick={() => handleRoomSelect(room.roomId)}
|
||||||
>
|
>
|
||||||
<Avatar src={room.avatarUrl} name={room.name} size="xs" />
|
<Avatar src={room.avatarUrl} name={room.name} size="xs" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<span class="font-bold text-sm text-light truncate block">{room.name}</span>
|
<span class="text-[12px] font-body truncate block">{room.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if room.unreadCount > 0}
|
{#if room.unreadCount > 0}
|
||||||
<span class="bg-primary text-white text-xs px-1.5 py-0.5 rounded-full min-w-[18px] text-center">
|
<span class="bg-primary text-background text-[10px] px-1.5 py-0.5 rounded-full min-w-[16px] text-center font-bold">
|
||||||
{room.unreadCount > 99 ? "99+" : room.unreadCount}
|
{room.unreadCount > 99 ? "99+" : room.unreadCount}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -582,14 +573,11 @@
|
|||||||
|
|
||||||
<!-- Other Rooms (not in a space, not DMs) -->
|
<!-- Other Rooms (not in a space, not DMs) -->
|
||||||
{#if filteredOtherRooms.length > 0}
|
{#if filteredOtherRooms.length > 0}
|
||||||
<div class="mb-2">
|
<div class="mb-1.5">
|
||||||
<div class="flex items-center justify-between px-2 py-1">
|
<div class="flex items-center justify-between px-2 py-1">
|
||||||
<span class="text-xs font-semibold text-light/40 uppercase tracking-wider">
|
<span class="text-[10px] font-body text-light/30 uppercase tracking-wider">Rooms</span>
|
||||||
<span class="material-symbols-rounded align-middle" style="font-size: 14px;">tag</span>
|
|
||||||
Rooms
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
class="w-5 h-5 flex items-center justify-center text-light/40 hover:text-light hover:bg-light/10 rounded transition-colors"
|
class="p-0.5 text-light/30 hover:text-white hover:bg-dark/50 rounded transition-colors"
|
||||||
onclick={() => (showCreateRoomModal = true)}
|
onclick={() => (showCreateRoomModal = true)}
|
||||||
title="Create room"
|
title="Create room"
|
||||||
>
|
>
|
||||||
@@ -600,16 +588,16 @@
|
|||||||
{#each filteredOtherRooms as room (room.roomId)}
|
{#each filteredOtherRooms as room (room.roomId)}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center gap-2 pl-1 pr-2 py-1 rounded-[50px] transition-colors text-left
|
class="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg transition-colors text-left
|
||||||
{$selectedRoomId === room.roomId ? 'bg-primary/20' : 'hover:bg-light/5'}"
|
{$selectedRoomId === room.roomId ? 'bg-primary/10 text-white' : 'text-light/60 hover:bg-dark/50 hover:text-white'}"
|
||||||
onclick={() => handleRoomSelect(room.roomId)}
|
onclick={() => handleRoomSelect(room.roomId)}
|
||||||
>
|
>
|
||||||
<Avatar src={room.avatarUrl} name={room.name} size="xs" />
|
<Avatar src={room.avatarUrl} name={room.name} size="xs" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<span class="font-bold text-sm text-light truncate block">{room.name}</span>
|
<span class="text-[12px] font-body truncate block">{room.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if room.unreadCount > 0}
|
{#if room.unreadCount > 0}
|
||||||
<span class="bg-primary text-white text-xs px-1.5 py-0.5 rounded-full min-w-[18px] text-center">
|
<span class="bg-primary text-background text-[10px] px-1.5 py-0.5 rounded-full min-w-[16px] text-center font-bold">
|
||||||
{room.unreadCount > 99 ? "99+" : room.unreadCount}
|
{room.unreadCount > 99 ? "99+" : room.unreadCount}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -623,76 +611,76 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- User footer -->
|
<!-- User footer -->
|
||||||
<footer class="p-3 border-t border-light/10">
|
<div class="px-2 py-2 border-t border-light/5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Avatar name={$auth.userId || "User"} size="xs" status="online" />
|
<Avatar name={$auth.userId || "User"} size="xs" status="online" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-xs font-medium text-light truncate">{$auth.userId}</p>
|
<p class="text-[11px] text-light/50 truncate">{$auth.userId}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="text-light/50 hover:text-light p-1 rounded-lg hover:bg-light/10 transition-colors"
|
class="p-1 text-light/30 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
onclick={handleLogout}
|
onclick={handleLogout}
|
||||||
title="Disconnect chat"
|
title="Disconnect chat"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-rounded" style="font-size: 18px;">logout</span>
|
<span class="material-symbols-rounded" style="font-size: 16px;">logout</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main Chat Area -->
|
<!-- Main Chat Area -->
|
||||||
<main class="flex-1 flex flex-col min-h-0 overflow-hidden bg-night rounded-[32px]">
|
<main class="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
{#if $selectedRoomId}
|
{#if $selectedRoomId}
|
||||||
<div class="flex-1 flex flex-col min-h-0 overflow-hidden">
|
<div class="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
<!-- Room Header -->
|
<!-- Room Header -->
|
||||||
<header class="h-14 px-5 flex items-center border-b border-light/10">
|
<div class="px-4 py-2.5 flex items-center border-b border-light/5 shrink-0">
|
||||||
{#each $roomSummaries.filter((r) => r.roomId === $selectedRoomId) as room}
|
{#each $roomSummaries.filter((r) => r.roomId === $selectedRoomId) as room}
|
||||||
<div class="flex items-center gap-3 w-full">
|
<div class="flex items-center gap-2.5 w-full">
|
||||||
<Avatar src={room.avatarUrl} name={room.name} size="sm" />
|
<Avatar src={room.avatarUrl} name={room.name} size="sm" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h2 class="font-heading text-h5 text-light truncate">{room.name}</h2>
|
<h2 class="font-heading text-body-sm text-white truncate">{room.name}</h2>
|
||||||
<p class="text-xs text-light/50">
|
<p class="text-[11px] text-light/40">
|
||||||
{room.memberCount} members{room.isEncrypted ? " · Encrypted" : ""}
|
{room.memberCount} members{room.isEncrypted ? " · Encrypted" : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="p-2 text-light/50 hover:text-light hover:bg-light/10 rounded-lg transition-colors"
|
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
onclick={() => (showMessageSearch = !showMessageSearch)}
|
onclick={() => (showMessageSearch = !showMessageSearch)}
|
||||||
title="Search messages"
|
title="Search messages"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-rounded" style="font-size: 20px;">search</span>
|
<span class="material-symbols-rounded" style="font-size: 18px;">search</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="p-2 text-light/50 hover:text-light hover:bg-light/10 rounded-lg transition-colors"
|
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
onclick={() => (showRoomInfo = !showRoomInfo)}
|
onclick={() => (showRoomInfo = !showRoomInfo)}
|
||||||
title="Room info"
|
title="Room info"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-rounded" style="font-size: 20px;">info</span>
|
<span class="material-symbols-rounded" style="font-size: 18px;">info</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="p-2 text-light/50 hover:text-light hover:bg-light/10 rounded-lg transition-colors"
|
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
onclick={() => (showMemberList = !showMemberList)}
|
onclick={() => (showMemberList = !showMemberList)}
|
||||||
title="Members"
|
title="Members"
|
||||||
>
|
>
|
||||||
<span class="material-symbols-rounded" style="font-size: 20px;">group</span>
|
<span class="material-symbols-rounded" style="font-size: 18px;">group</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<!-- Message search panel -->
|
<!-- Message search panel -->
|
||||||
{#if showMessageSearch}
|
{#if showMessageSearch}
|
||||||
<div class="border-b border-light/10 p-3 bg-dark/50">
|
<div class="border-b border-light/5 px-4 py-2.5">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<span class="material-symbols-rounded absolute left-3 top-1/2 -translate-y-1/2 text-light/40" style="font-size: 16px;">search</span>
|
<span class="material-symbols-rounded absolute left-2.5 top-1/2 -translate-y-1/2 text-light/30" style="font-size: 16px;">search</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={messageSearchQuery}
|
bind:value={messageSearchQuery}
|
||||||
placeholder="Search messages in this room..."
|
placeholder="Search messages..."
|
||||||
class="w-full pl-9 pr-8 py-2 bg-night text-light text-sm rounded-lg border border-light/10 placeholder:text-light/30 focus:outline-none focus:border-primary"
|
class="w-full pl-8 pr-8 py-1.5 bg-dark/50 text-white text-[12px] rounded-lg border border-light/5 placeholder:text-light/30 focus:outline-none focus:border-primary"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-light/40 hover:text-light"
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-light/30 hover:text-white"
|
||||||
onclick={() => { showMessageSearch = false; messageSearchQuery = ""; }}
|
onclick={() => { showMessageSearch = false; messageSearchQuery = ""; }}
|
||||||
>
|
>
|
||||||
<span class="material-symbols-rounded" style="font-size: 16px;">close</span>
|
<span class="material-symbols-rounded" style="font-size: 16px;">close</span>
|
||||||
@@ -700,22 +688,22 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if messageSearchQuery && messageSearchResults.length > 0}
|
{#if messageSearchQuery && messageSearchResults.length > 0}
|
||||||
<div class="mt-2 max-h-48 overflow-y-auto">
|
<div class="mt-2 max-h-48 overflow-y-auto">
|
||||||
<p class="text-xs text-light/40 mb-2">
|
<p class="text-[11px] text-light/30 mb-1.5">
|
||||||
{messageSearchResults.length} result{messageSearchResults.length !== 1 ? "s" : ""}
|
{messageSearchResults.length} result{messageSearchResults.length !== 1 ? "s" : ""}
|
||||||
</p>
|
</p>
|
||||||
{#each messageSearchResults.slice(0, 20) as result}
|
{#each messageSearchResults.slice(0, 20) as result}
|
||||||
<button
|
<button
|
||||||
class="w-full text-left px-3 py-2 hover:bg-light/5 rounded transition-colors"
|
class="w-full text-left px-3 py-1.5 hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
onclick={() => { showMessageSearch = false; messageSearchQuery = ""; }}
|
onclick={() => { showMessageSearch = false; messageSearchQuery = ""; }}
|
||||||
>
|
>
|
||||||
<p class="text-xs text-primary">{result.senderName}</p>
|
<p class="text-[11px] text-primary">{result.senderName}</p>
|
||||||
<p class="text-sm text-light truncate">{result.content}</p>
|
<p class="text-body-sm text-white truncate">{result.content}</p>
|
||||||
<p class="text-xs text-light/30">{new Date(result.timestamp).toLocaleString()}</p>
|
<p class="text-[10px] text-light/30">{new Date(result.timestamp).toLocaleString()}</p>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else if messageSearchQuery}
|
{:else if messageSearchQuery}
|
||||||
<p class="text-sm text-light/40 mt-2">No results found</p>
|
<p class="text-body-sm text-light/30 mt-2">No results found</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -729,11 +717,11 @@
|
|||||||
role="region"
|
role="region"
|
||||||
>
|
>
|
||||||
{#if isDraggingFile}
|
{#if isDraggingFile}
|
||||||
<div class="absolute inset-0 z-50 bg-primary/20 border-2 border-dashed border-primary rounded-lg flex items-center justify-center backdrop-blur-sm">
|
<div class="absolute inset-0 z-50 bg-primary/10 border-2 border-dashed border-primary rounded-xl flex items-center justify-center backdrop-blur-sm">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<span class="material-symbols-rounded text-primary mb-4 block" style="font-size: 64px;">upload_file</span>
|
<span class="material-symbols-rounded text-primary mb-3 block" style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;">upload_file</span>
|
||||||
<p class="text-xl font-semibold text-primary">Drop to upload</p>
|
<p class="text-body-sm font-heading text-primary">Drop to upload</p>
|
||||||
<p class="text-sm text-light/60 mt-1">Release to send file</p>
|
<p class="text-[12px] text-light/40 mt-0.5">Release to send file</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -764,7 +752,7 @@
|
|||||||
<!-- Side panels -->
|
<!-- Side panels -->
|
||||||
{#if showRoomInfo}
|
{#if showRoomInfo}
|
||||||
{#each $roomSummaries.filter((r) => r.roomId === $selectedRoomId) as currentRoom}
|
{#each $roomSummaries.filter((r) => r.roomId === $selectedRoomId) as currentRoom}
|
||||||
<aside class="w-72 border-l border-light/10 bg-dark/30">
|
<aside class="w-72 border-l border-light/5">
|
||||||
<RoomInfoPanel
|
<RoomInfoPanel
|
||||||
room={currentRoom}
|
room={currentRoom}
|
||||||
members={currentMembers}
|
members={currentMembers}
|
||||||
@@ -773,7 +761,7 @@
|
|||||||
</aside>
|
</aside>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if showMemberList}
|
{:else if showMemberList}
|
||||||
<aside class="w-64 border-l border-light/10 bg-dark/30">
|
<aside class="w-64 border-l border-light/5">
|
||||||
<MemberList members={currentMembers} />
|
<MemberList members={currentMembers} />
|
||||||
</aside>
|
</aside>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -782,10 +770,10 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<!-- No room selected -->
|
<!-- No room selected -->
|
||||||
<div class="flex-1 flex items-center justify-center">
|
<div class="flex-1 flex items-center justify-center">
|
||||||
<div class="text-center text-light/40">
|
<div class="text-center text-light/30">
|
||||||
<span class="material-symbols-rounded mb-4 block" style="font-size: 64px; font-variation-settings: 'FILL' 0, 'wght' 300;">chat</span>
|
<span class="material-symbols-rounded mb-3 block" style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;">chat</span>
|
||||||
<h2 class="font-heading text-h4 text-light/50 mb-2">Select a room</h2>
|
<p class="text-body-sm text-light/40 mb-1">Select a room</p>
|
||||||
<p class="text-body text-light/30">Choose a conversation to start chatting</p>
|
<p class="text-[12px] text-light/20">Choose a conversation to start chatting</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -322,74 +322,73 @@
|
|||||||
|
|
||||||
<!-- Tags Tab -->
|
<!-- Tags Tab -->
|
||||||
{#if activeTab === "tags"}
|
{#if activeTab === "tags"}
|
||||||
<div class="space-y-6 max-w-2xl">
|
<div class="space-y-4 max-w-2xl">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold text-light">
|
<h2 class="text-body font-heading text-white">
|
||||||
{m.settings_tags_title()}
|
{m.settings_tags_title()}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm text-light/50">
|
<p class="text-body-sm text-light/50 mt-0.5">
|
||||||
{m.settings_tags_desc()}
|
{m.settings_tags_desc()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onclick={() => openTagModal()} icon="add">
|
<Button size="sm" onclick={() => openTagModal()} icon="add">
|
||||||
{m.settings_tags_create()}
|
{m.settings_tags_create()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if orgTags.length === 0 && tagsLoaded}
|
{#if orgTags.length === 0 && tagsLoaded}
|
||||||
<Card>
|
<div class="bg-dark/30 border border-light/5 rounded-xl p-8 text-center">
|
||||||
<div class="p-8 text-center">
|
<span
|
||||||
<span
|
class="material-symbols-rounded text-light/20 mb-3 block"
|
||||||
class="material-symbols-rounded text-light/20 mb-4 block"
|
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' 400, 'GRAD' 0, 'opsz' 48;"
|
>label</span
|
||||||
>label</span
|
>
|
||||||
>
|
<p class="text-body-sm text-light/40">{m.settings_tags_empty()}</p>
|
||||||
<p class="text-light/50">{m.settings_tags_empty()}</p>
|
</div>
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid gap-3">
|
<div class="flex flex-col gap-2">
|
||||||
{#each orgTags as tag}
|
{#each orgTags as tag}
|
||||||
<Card>
|
<div class="flex items-center justify-between px-4 py-3 bg-dark/30 border border-light/5 rounded-xl hover:border-light/10 transition-colors">
|
||||||
<div class="flex items-center justify-between p-4">
|
<div class="flex items-center gap-3">
|
||||||
<div class="flex items-center gap-3">
|
<div
|
||||||
<div
|
class="w-7 h-7 rounded-lg flex items-center justify-center shrink-0"
|
||||||
class="w-8 h-8 rounded-lg flex items-center justify-center"
|
style="background-color: {tag.color || '#00A3E0'}"
|
||||||
style="background-color: {tag.color ||
|
>
|
||||||
'#00A3E0'}"
|
<span
|
||||||
|
class="material-symbols-rounded text-night"
|
||||||
|
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 600, 'GRAD' 0, 'opsz' 16;"
|
||||||
|
>label</span
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
class="material-symbols-rounded text-night"
|
|
||||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 600, 'GRAD' 0, 'opsz' 18;"
|
|
||||||
>label</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-light font-medium">
|
|
||||||
{tag.name}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-light/40">
|
|
||||||
{tag.color || "#00A3E0"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div>
|
||||||
<Button
|
<p class="text-body-sm text-white font-medium">
|
||||||
variant="tertiary"
|
{tag.name}
|
||||||
size="sm"
|
</p>
|
||||||
onclick={() => openTagModal(tag)}
|
<p class="text-[11px] text-light/30">
|
||||||
>Edit</Button
|
{tag.color || "#00A3E0"}
|
||||||
>
|
</p>
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => deleteOrgTag(tag)}
|
|
||||||
>Delete</Button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div class="flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
|
onclick={() => openTagModal(tag)}
|
||||||
|
title="Edit"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">edit</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-1.5 text-light/40 hover:text-error hover:bg-error/10 rounded-lg transition-colors"
|
||||||
|
onclick={() => deleteOrgTag(tag)}
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user