Phase 8: Polish & UX — dark mode, toasts, keyboard shortcuts, responsive, accessibility
- Add dark mode with theme toggle, localStorage/cookie persistence, anti-FOUC script - Add toast notification system with auto-dismiss and color-coded types - Add api() fetch wrapper with auto JSON, X-Socket-ID, and error toasts - Add keyboard shortcuts (/ search, b boards, ? help, n new card, Esc close) - Add error page with status code and navigation - Add focus trap utility and apply to CardModal with full ARIA dialog support - Add hamburger menu for mobile, responsive columns, fullscreen card modal on mobile - Add aria-labels to icon-only buttons and DnD live region - Migrate all ~30 fetch calls to api() wrapper with success toasts - Add dark: Tailwind variants to all 20+ pages and components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
import { generatePosition } from '$lib/utils/fractional-index.js';
|
||||
import { getDueUrgency, getUrgencyClasses, formatDueDate } from '$lib/utils/due-date.js';
|
||||
import { joinBoard, leaveBoard, onBoardEvent, offBoardEvent, getSocketId } from '$lib/stores/socket.js';
|
||||
import { api } from '$lib/utils/api.js';
|
||||
import { toast } from '$lib/stores/toasts.js';
|
||||
import CardModal from '$lib/components/CardModal.svelte';
|
||||
import ActivityFeed from '$lib/components/ActivityFeed.svelte';
|
||||
import BoardFilters from '$lib/components/BoardFilters.svelte';
|
||||
@@ -75,9 +77,8 @@
|
||||
async function toggleArchivedCards() {
|
||||
showArchivedCards = !showArchivedCards;
|
||||
if (showArchivedCards && archivedCards.length === 0) {
|
||||
const res = await fetch(`/api/boards/${data.board.id}/archived-cards`);
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
const { ok, data: result } = await api(`/api/boards/${data.board.id}/archived-cards`);
|
||||
if (ok) {
|
||||
archivedCards = result.cards;
|
||||
}
|
||||
}
|
||||
@@ -87,12 +88,6 @@
|
||||
return archivedCards.filter((c: any) => c.columnId === columnId);
|
||||
}
|
||||
|
||||
// ─── Socket.IO header for self-dedup ─────────────
|
||||
function socketHeaders(): Record<string, string> {
|
||||
const sid = getSocketId();
|
||||
return sid ? { 'X-Socket-ID': sid } : {};
|
||||
}
|
||||
|
||||
// ─── Real-Time Event Handlers ────────────────────
|
||||
function isSelf(d: any) {
|
||||
return d?.socketId && d.socketId === getSocketId();
|
||||
@@ -134,9 +129,9 @@
|
||||
const cardId = d.cardId || d.card?.id;
|
||||
if (!colId || !cardId) return;
|
||||
|
||||
const res = await fetch(`/api/boards/${data.board.id}/columns/${colId}/cards/${cardId}`);
|
||||
if (!res.ok) return;
|
||||
const { card: freshCard } = await res.json();
|
||||
const { ok, data: result } = await api(`/api/boards/${data.board.id}/columns/${colId}/cards/${cardId}`);
|
||||
if (!ok) return;
|
||||
const freshCard = result.card;
|
||||
|
||||
// The card might have moved columns — remove from old location first
|
||||
for (const col of columns) {
|
||||
@@ -247,9 +242,8 @@
|
||||
if (col.id === e.detail.info.id) {
|
||||
const newPos = generatePosition(before, after);
|
||||
col.position = newPos;
|
||||
fetch(`/api/boards/${data.board.id}/columns/${col.id}`, {
|
||||
api(`/api/boards/${data.board.id}/columns/${col.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', ...socketHeaders() },
|
||||
body: JSON.stringify({ position: newPos })
|
||||
});
|
||||
}
|
||||
@@ -273,9 +267,8 @@
|
||||
const newPos = generatePosition(before, after);
|
||||
col.cards[cardIdx].position = newPos;
|
||||
|
||||
fetch(`/api/boards/${data.board.id}/columns/${columnId}/cards/${cardId}`, {
|
||||
api(`/api/boards/${data.board.id}/columns/${columnId}/cards/${cardId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', ...socketHeaders() },
|
||||
body: JSON.stringify({ columnId, position: newPos })
|
||||
});
|
||||
}
|
||||
@@ -284,56 +277,53 @@
|
||||
// ─── Add Column ──────────────────────────────────
|
||||
async function addColumn() {
|
||||
if (!addingColumnTitle.trim()) return;
|
||||
const res = await fetch(`/api/boards/${data.board.id}/columns`, {
|
||||
const { ok, data: result } = await api(`/api/boards/${data.board.id}/columns`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...socketHeaders() },
|
||||
body: JSON.stringify({ title: addingColumnTitle })
|
||||
});
|
||||
if (res.ok) {
|
||||
const { column } = await res.json();
|
||||
columns = [...columns, { ...column, cards: [] }];
|
||||
if (ok) {
|
||||
columns = [...columns, { ...result.column, cards: [] }];
|
||||
addingColumnTitle = '';
|
||||
showAddColumn = false;
|
||||
toast.success('Column created');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Add Card ────────────────────────────────────
|
||||
async function addCard(columnId: string) {
|
||||
if (!newCardTitle.trim()) return;
|
||||
const res = await fetch(`/api/boards/${data.board.id}/columns/${columnId}/cards`, {
|
||||
const { ok, data: result } = await api(`/api/boards/${data.board.id}/columns/${columnId}/cards`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...socketHeaders() },
|
||||
body: JSON.stringify({ title: newCardTitle })
|
||||
});
|
||||
if (res.ok) {
|
||||
const { card } = await res.json();
|
||||
if (ok) {
|
||||
const col = columns.find((c: any) => c.id === columnId);
|
||||
if (col) col.cards = [...col.cards, card];
|
||||
if (col) col.cards = [...col.cards, result.card];
|
||||
newCardTitle = '';
|
||||
addingCardColumnId = null;
|
||||
toast.success('Card added');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Delete Column ───────────────────────────────
|
||||
async function deleteColumn(columnId: string) {
|
||||
const res = await fetch(`/api/boards/${data.board.id}/columns/${columnId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { ...socketHeaders() }
|
||||
const { ok } = await api(`/api/boards/${data.board.id}/columns/${columnId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (res.ok) {
|
||||
if (ok) {
|
||||
columns = columns.filter((c: any) => c.id !== columnId);
|
||||
toast.success('Column deleted');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Rename Column ───────────────────────────────
|
||||
async function renameColumn(columnId: string) {
|
||||
if (!editingColumnTitle.trim()) return;
|
||||
const res = await fetch(`/api/boards/${data.board.id}/columns/${columnId}`, {
|
||||
const { ok } = await api(`/api/boards/${data.board.id}/columns/${columnId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', ...socketHeaders() },
|
||||
body: JSON.stringify({ title: editingColumnTitle })
|
||||
});
|
||||
if (res.ok) {
|
||||
if (ok) {
|
||||
const col = columns.find((c: any) => c.id === columnId);
|
||||
if (col) col.title = editingColumnTitle;
|
||||
}
|
||||
@@ -342,14 +332,14 @@
|
||||
|
||||
// ─── Delete Card ─────────────────────────────────
|
||||
async function deleteCard(columnId: string, cardId: string) {
|
||||
const res = await fetch(`/api/boards/${data.board.id}/columns/${columnId}/cards/${cardId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { ...socketHeaders() }
|
||||
const { ok } = await api(`/api/boards/${data.board.id}/columns/${columnId}/cards/${cardId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (res.ok) {
|
||||
if (ok) {
|
||||
const col = columns.find((c: any) => c.id === columnId);
|
||||
if (col) col.cards = col.cards.filter((c: any) => c.id !== cardId);
|
||||
if (selectedCard?.id === cardId) selectedCard = null;
|
||||
toast.success('Card deleted');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,32 +376,34 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="sr-only" aria-live="polite" id="dnd-announcer"></div>
|
||||
|
||||
<div class="flex flex-col h-[calc(100vh-3.5rem)]">
|
||||
<!-- Board header -->
|
||||
<div class="flex items-center gap-3 px-4 py-3 bg-white/80 border-b border-gray-200">
|
||||
<a href="/boards" class="text-gray-400 hover:text-gray-600 transition" aria-label="Back to boards">
|
||||
<div class="flex items-center gap-3 px-4 py-3 bg-white/80 border-b border-gray-200 dark:bg-gray-900/80 dark:border-gray-700 flex-wrap">
|
||||
<a href="/boards" class="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition" aria-label="Back to boards">
|
||||
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="text-lg font-bold text-gray-900">{data.board.title}</h1>
|
||||
<span class="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-500">
|
||||
<h1 class="text-lg font-bold text-gray-900 dark:text-gray-100">{data.board.title}</h1>
|
||||
<span class="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-500 dark:bg-gray-800 dark:text-gray-400">
|
||||
{data.board.visibility === 'PUBLIC' ? 'Public' : 'Private'}
|
||||
</span>
|
||||
<a
|
||||
href="/boards/{data.board.id}/members"
|
||||
class="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-500 hover:bg-gray-200 hover:text-gray-700 transition"
|
||||
class="rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-500 hover:bg-gray-200 hover:text-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 transition"
|
||||
>
|
||||
{data.board.members.length} {data.board.members.length === 1 ? 'member' : 'members'}
|
||||
</a>
|
||||
|
||||
{#if viewingUsers.filter((u) => u.id !== data.user?.id).length > 0}
|
||||
<div class="ml-auto flex items-center gap-1.5">
|
||||
<span class="text-xs text-gray-400">Also here:</span>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">Also here:</span>
|
||||
<div class="flex -space-x-1.5">
|
||||
{#each viewingUsers.filter((u) => u.id !== data.user?.id) as viewer}
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-emerald-500 text-white text-[11px] flex items-center justify-center border-2 border-white"
|
||||
class="w-6 h-6 rounded-full bg-emerald-500 text-white text-[11px] flex items-center justify-center border-2 border-white dark:border-gray-900"
|
||||
title={viewer.name}
|
||||
>
|
||||
{viewer.name[0].toUpperCase()}
|
||||
@@ -423,9 +415,10 @@
|
||||
|
||||
<button
|
||||
onclick={() => toggleArchivedCards()}
|
||||
class="ml-auto rounded px-2 py-1 text-sm text-gray-500 hover:bg-gray-200 hover:text-gray-700 transition flex items-center gap-1"
|
||||
class="ml-auto rounded px-2 py-1 text-sm text-gray-500 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 transition flex items-center gap-1"
|
||||
class:bg-gray-200={showArchivedCards}
|
||||
title="Toggle archived cards"
|
||||
aria-label="Toggle archived cards"
|
||||
>
|
||||
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M2 3a1 1 0 00-1 1v1a1 1 0 001 1h16a1 1 0 001-1V4a1 1 0 00-1-1H2z" />
|
||||
@@ -436,9 +429,10 @@
|
||||
|
||||
<button
|
||||
onclick={() => (showActivity = !showActivity)}
|
||||
class="rounded px-2 py-1 text-sm text-gray-500 hover:bg-gray-200 hover:text-gray-700 transition flex items-center gap-1"
|
||||
class="rounded px-2 py-1 text-sm text-gray-500 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 transition flex items-center gap-1"
|
||||
class:bg-gray-200={showActivity}
|
||||
title="Toggle activity feed"
|
||||
aria-label="Toggle activity feed"
|
||||
>
|
||||
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
|
||||
@@ -448,7 +442,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Filters bar -->
|
||||
<div class="px-4 py-2 bg-white/60 border-b border-gray-100">
|
||||
<div class="px-4 py-2 bg-white/60 border-b border-gray-100 dark:bg-gray-900/60 dark:border-gray-800">
|
||||
<BoardFilters
|
||||
{columns}
|
||||
members={data.board.members}
|
||||
@@ -474,7 +468,7 @@
|
||||
{#each columns as column (column.id)}
|
||||
{@const visibleCards = filterCards(column.cards)}
|
||||
<div
|
||||
class="flex-shrink-0 w-72 bg-[var(--color-column-bg)] rounded-xl flex flex-col max-h-full"
|
||||
class="flex-shrink-0 w-64 sm:w-72 bg-[var(--color-column-bg)] rounded-xl flex flex-col max-h-full"
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
>
|
||||
<!-- Column header -->
|
||||
@@ -486,14 +480,14 @@
|
||||
>
|
||||
<input
|
||||
bind:value={editingColumnTitle}
|
||||
class="flex-1 rounded border border-gray-300 px-2 py-1 text-sm font-semibold"
|
||||
class="flex-1 rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 px-2 py-1 text-sm font-semibold"
|
||||
autofocus
|
||||
onblur={() => renameColumn(column.id)}
|
||||
/>
|
||||
</form>
|
||||
{:else}
|
||||
<button
|
||||
class="text-sm font-semibold text-gray-700 hover:text-gray-900 text-left flex-1"
|
||||
class="text-sm font-semibold text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100 text-left flex-1"
|
||||
ondblclick={() => {
|
||||
if (data.canEdit) {
|
||||
editingColumnId = column.id;
|
||||
@@ -502,14 +496,15 @@
|
||||
}}
|
||||
>
|
||||
{column.title}
|
||||
<span class="ml-1 text-xs text-gray-400 font-normal">{visibleCards.length}{#if hasActiveFilters}/{column.cards.length}{/if}</span>
|
||||
<span class="ml-1 text-xs text-gray-400 dark:text-gray-500 font-normal">{visibleCards.length}{#if hasActiveFilters}/{column.cards.length}{/if}</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if data.canEdit}
|
||||
<button
|
||||
onclick={() => deleteColumn(column.id)}
|
||||
class="text-gray-400 hover:text-[var(--color-danger)] transition p-1"
|
||||
class="text-gray-400 dark:text-gray-500 hover:text-[var(--color-danger)] transition p-1"
|
||||
title="Delete column"
|
||||
aria-label="Delete column"
|
||||
>
|
||||
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
@@ -534,7 +529,7 @@
|
||||
{@const progress = getChecklistProgress(card)}
|
||||
{@const dueUrgency = getDueUrgency(card.dueDate)}
|
||||
<button
|
||||
class="w-full text-left mb-2 rounded-lg bg-[var(--color-card-bg)] p-3 shadow-sm hover:shadow-md border border-gray-200 hover:border-gray-300 transition cursor-pointer"
|
||||
class="w-full text-left mb-2 rounded-lg bg-[var(--color-card-bg)] p-3 shadow-sm hover:shadow-md border border-gray-200 hover:border-gray-300 dark:border-gray-600 dark:hover:border-gray-500 transition cursor-pointer"
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
onclick={() => (selectedCard = card)}
|
||||
>
|
||||
@@ -550,9 +545,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<span class="text-sm text-gray-800">{card.title}</span>
|
||||
<span class="text-sm text-gray-800 dark:text-gray-200">{card.title}</span>
|
||||
{#if card.dueDate || card._count?.comments > 0 || card._count?.attachments > 0 || progress}
|
||||
<div class="flex items-center gap-2 mt-2 text-xs text-gray-400 flex-wrap">
|
||||
<div class="flex items-center gap-2 mt-2 text-xs text-gray-400 dark:text-gray-500 flex-wrap">
|
||||
{#if card.dueDate}
|
||||
<span class="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium {getUrgencyClasses(dueUrgency)}">
|
||||
{formatDueDate(card.dueDate)}
|
||||
@@ -596,7 +591,7 @@
|
||||
{/each}
|
||||
{#if card.assignees.length > 3}
|
||||
<div
|
||||
class="w-5 h-5 rounded-full bg-gray-300 text-gray-600 text-[10px] flex items-center justify-center border border-white"
|
||||
class="w-5 h-5 rounded-full bg-gray-300 text-gray-600 text-[10px] flex items-center justify-center border border-white dark:bg-gray-600 dark:text-gray-300 dark:border-gray-700"
|
||||
>
|
||||
+{card.assignees.length - 3}
|
||||
</div>
|
||||
@@ -612,10 +607,10 @@
|
||||
{@const archivedInCol = getArchivedCardsForColumn(column.id)}
|
||||
{#if archivedInCol.length > 0}
|
||||
<div class="px-2 pb-2">
|
||||
<div class="text-[10px] text-gray-400 uppercase tracking-wide mb-1 px-1">Archived</div>
|
||||
<div class="text-[10px] text-gray-400 dark:text-gray-500 uppercase tracking-wide mb-1 px-1">Archived</div>
|
||||
{#each archivedInCol as card}
|
||||
<div class="mb-1.5 rounded-lg bg-gray-100 p-2.5 border border-gray-200 opacity-60">
|
||||
<span class="text-sm text-gray-500">{card.title}</span>
|
||||
<div class="mb-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 p-2.5 border border-gray-200 dark:border-gray-700 opacity-60">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{card.title}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -632,7 +627,7 @@
|
||||
<textarea
|
||||
bind:value={newCardTitle}
|
||||
placeholder="Enter a title for this card..."
|
||||
class="w-full rounded-lg border border-gray-300 p-2 text-sm resize-none focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
|
||||
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 p-2 text-sm resize-none focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
|
||||
rows="2"
|
||||
autofocus
|
||||
onkeydown={(e) => {
|
||||
@@ -656,7 +651,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { addingCardColumnId = null; newCardTitle = ''; }}
|
||||
class="text-gray-500 hover:text-gray-700 transition text-sm"
|
||||
class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 transition text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -666,7 +661,7 @@
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => { addingCardColumnId = column.id; newCardTitle = ''; }}
|
||||
class="mx-2 mb-2 rounded-lg px-3 py-2 text-sm text-gray-500 hover:bg-gray-200/50 transition text-left"
|
||||
class="mx-2 mb-2 rounded-lg px-3 py-2 text-sm text-gray-500 hover:bg-gray-200/50 dark:text-gray-400 dark:hover:bg-gray-600/50 transition text-left"
|
||||
>
|
||||
+ Add a card
|
||||
</button>
|
||||
@@ -684,7 +679,7 @@
|
||||
<input
|
||||
bind:value={addingColumnTitle}
|
||||
placeholder="Enter list title..."
|
||||
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
|
||||
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 px-3 py-2 text-sm focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
|
||||
autofocus
|
||||
onkeydown={(e) => { if (e.key === 'Escape') { showAddColumn = false; addingColumnTitle = ''; } }}
|
||||
/>
|
||||
@@ -698,7 +693,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { showAddColumn = false; addingColumnTitle = ''; }}
|
||||
class="text-gray-500 hover:text-gray-700 transition text-sm"
|
||||
class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 transition text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -708,7 +703,7 @@
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => (showAddColumn = true)}
|
||||
class="w-full rounded-xl bg-white/60 hover:bg-white/80 border border-dashed border-gray-300 px-4 py-3 text-sm text-gray-500 transition text-left"
|
||||
class="w-full rounded-xl bg-white/60 hover:bg-white/80 dark:bg-gray-800/60 dark:hover:bg-gray-800/80 border border-dashed border-gray-300 dark:border-gray-600 px-4 py-3 text-sm text-gray-500 dark:text-gray-400 transition text-left"
|
||||
>
|
||||
+ Add another list
|
||||
</button>
|
||||
@@ -720,10 +715,10 @@
|
||||
|
||||
<!-- Activity sidebar -->
|
||||
{#if showActivity}
|
||||
<div class="w-80 flex-shrink-0 border-l border-gray-200 bg-white overflow-y-auto p-4">
|
||||
<div class="fixed inset-0 z-40 bg-white dark:bg-gray-900 overflow-y-auto p-4 sm:relative sm:inset-auto sm:z-auto sm:w-80 sm:flex-shrink-0 sm:border-l sm:border-gray-200 sm:dark:border-gray-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-sm font-semibold text-gray-700">Activity</h2>
|
||||
<button onclick={() => (showActivity = false)} class="text-gray-400 hover:text-gray-600 transition p-1">
|
||||
<h2 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Activity</h2>
|
||||
<button onclick={() => (showActivity = false)} class="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition p-1" aria-label="Close activity">
|
||||
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user