diff --git a/package.json b/package.json
index 57d9d23..b63aa8c 100644
--- a/package.json
+++ b/package.json
@@ -15,12 +15,16 @@
"license": "MIT",
"dependencies": {
"@tauri-apps/api": "^2",
- "@tauri-apps/plugin-opener": "^2"
+ "@tauri-apps/plugin-opener": "^2",
+ "carbon-components-svelte": "^0.99.0",
+ "carbon-icons-svelte": "^13.8.0",
+ "carbon-preprocess-svelte": "^0.11.23"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
+ "@tauri-apps/cli": "^2",
"@wdio/cli": "^9.19.0",
"@wdio/local-runner": "^9.19.0",
"@wdio/mocha-framework": "^9.19.0",
@@ -28,7 +32,6 @@
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
- "vite": "^6.0.3",
- "@tauri-apps/cli": "^2"
+ "vite": "^6.0.3"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3ab8a56..0ac0a1e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,15 @@ importers:
'@tauri-apps/plugin-opener':
specifier: ^2
version: 2.5.2
+ carbon-components-svelte:
+ specifier: ^0.99.0
+ version: 0.99.0
+ carbon-icons-svelte:
+ specifier: ^13.8.0
+ version: 13.8.0
+ carbon-preprocess-svelte:
+ specifier: ^0.11.23
+ version: 0.11.23
devDependencies:
'@sveltejs/adapter-static':
specifier: ^3.0.6
@@ -374,6 +383,10 @@ packages:
cpu: [x64]
os: [win32]
+ '@ibm/telemetry-js@1.11.0':
+ resolution: {integrity: sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA==}
+ hasBin: true
+
'@inquirer/ansi@1.0.2':
resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==}
engines: {node: '>=18'}
@@ -1099,6 +1112,15 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
+ carbon-components-svelte@0.99.0:
+ resolution: {integrity: sha512-h5S4/kaKa30q3nLLky4G8MTw25j6ZHtpBifdoxvC63Q2YTdaBv91OID+L0m70XRvNHCoxgi7ewfEZqzfsaZ03A==}
+
+ carbon-icons-svelte@13.8.0:
+ resolution: {integrity: sha512-1sTbSOhiKjs7BXj2Ctk6tekSNX4DLUy9miNeYaQK/jeveM4x/+et09JH8xnrwRE1QseLdRd3sOoVwqPZEYsdkA==}
+
+ carbon-preprocess-svelte@0.11.23:
+ resolution: {integrity: sha512-ftMbHZpsYRf1Vd4AepihhbN6iUGKW96F/BKr4Z8xkRt1VNujFYE+a0U6A4wzwGJiUvLfb1CyE+QFTCLgMGaueQ==}
+
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -1446,6 +1468,9 @@ packages:
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
hasBin: true
+ flatpickr@4.6.9:
+ resolution: {integrity: sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw==}
+
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
@@ -2599,6 +2624,8 @@ snapshots:
'@esbuild/win32-x64@0.27.2':
optional: true
+ '@ibm/telemetry-js@1.11.0': {}
+
'@inquirer/ansi@1.0.2': {}
'@inquirer/checkbox@4.3.2(@types/node@25.0.3)':
@@ -3365,6 +3392,15 @@ snapshots:
camelcase@6.3.0: {}
+ carbon-components-svelte@0.99.0:
+ dependencies:
+ '@ibm/telemetry-js': 1.11.0
+ flatpickr: 4.6.9
+
+ carbon-icons-svelte@13.8.0: {}
+
+ carbon-preprocess-svelte@0.11.23: {}
+
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@@ -3789,6 +3825,8 @@ snapshots:
flat@5.0.2: {}
+ flatpickr@4.6.9: {}
+
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
diff --git a/src/app.css b/src/app.css
index 8d7305b..7057c02 100644
--- a/src/app.css
+++ b/src/app.css
@@ -1,67 +1,10 @@
/* Global styles for Potasko */
+/* Note: Most styling is handled by Carbon Design System */
-:root {
- /* Responsive breakpoints */
- --mobile-breakpoint: 768px;
- --touch-target-min: 44px;
-
- /* Colors - Light mode */
- --bg-primary: #ffffff;
- --bg-secondary: #f9fafb;
- --bg-hover: #f3f4f6;
- --bg-selected: #e5e7eb;
- --text-primary: #111827;
- --text-secondary: #6b7280;
- --border-color: #e5e7eb;
- --accent-color: #3b82f6;
- --error-color: #dc2626;
-
- /* Priority colors - Light mode */
- --priority-high-bg: #fee2e2;
- --priority-high-text: #dc2626;
- --priority-medium-bg: #fef3c7;
- --priority-medium-text: #d97706;
- --priority-low-bg: #dbeafe;
- --priority-low-text: #2563eb;
-
- /* Typography */
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
- font-size: 16px;
- line-height: 1.5;
- color: var(--text-primary);
- background-color: var(--bg-primary);
-
- /* Rendering */
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-/* Dark mode */
-@media (prefers-color-scheme: dark) {
- :root {
- --bg-primary: #1f2937;
- --bg-secondary: #111827;
- --bg-hover: #374151;
- --bg-selected: #4b5563;
- --text-primary: #f9fafb;
- --text-secondary: #9ca3af;
- --border-color: #374151;
- --accent-color: #60a5fa;
- --error-color: #f87171;
-
- /* Priority colors - Dark mode */
- --priority-high-bg: #7f1d1d;
- --priority-high-text: #fca5a5;
- --priority-medium-bg: #78350f;
- --priority-medium-text: #fcd34d;
- --priority-low-bg: #1e3a5f;
- --priority-low-text: #93c5fd;
- }
-}
+/* ============================================
+ Base Reset & Setup
+ ============================================ */
-/* Reset */
*,
*::before,
*::after {
@@ -78,79 +21,56 @@ body {
overflow: hidden;
}
-/* Mobile drawer backdrop */
-.backdrop {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.5);
- z-index: 99;
- opacity: 0;
- pointer-events: none;
- transition: opacity 0.3s ease;
-}
-
-.backdrop.visible {
- opacity: 1;
- pointer-events: auto;
-}
-
-/* Mobile header */
-.mobile-header {
- display: none;
- align-items: center;
- gap: 0.75rem;
- padding: 0.75rem 1rem;
- padding-top: calc(0.75rem + env(safe-area-inset-top, 0));
- background: var(--bg-secondary);
- border-bottom: 1px solid var(--border-color);
-}
+/* ============================================
+ Legacy Color Variables (fallbacks)
+ Used by components that haven't fully migrated
+ ============================================ */
-.mobile-header h1 {
- font-size: 1.125rem;
- font-weight: 600;
- margin: 0;
-}
-
-.hamburger-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: var(--touch-target-min);
- height: var(--touch-target-min);
- background: none;
- border: none;
- cursor: pointer;
- color: var(--text-primary);
- border-radius: 8px;
-}
-
-.hamburger-btn:hover {
- background: var(--bg-hover);
-}
-
-.hamburger-btn svg {
- width: 24px;
- height: 24px;
-}
-
-@media (max-width: 768px) {
- .mobile-header {
- display: flex;
- }
-
- /* Safe area padding for mobile devices */
- .safe-area-bottom {
- padding-bottom: env(safe-area-inset-bottom, 0);
+:root {
+ /* Fallback colors - Carbon tokens preferred */
+ --bg-primary: #ffffff;
+ --bg-secondary: #f4f4f4;
+ --bg-hover: #e8e8e8;
+ --text-primary: #161616;
+ --text-secondary: #525252;
+ --border-color: #e0e0e0;
+ --accent-color: #0f62fe;
+ --error-color: #da1e28;
+
+ /* Priority colors for task tags */
+ --priority-high-bg: #fff1f1;
+ --priority-high-text: #da1e28;
+ --priority-medium-bg: #fff8e1;
+ --priority-medium-text: #f1c21b;
+ --priority-low-bg: #e5f6ff;
+ --priority-low-text: #0043ce;
+}
+
+/* Dark mode fallbacks */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg-primary: #262626;
+ --bg-secondary: #161616;
+ --bg-hover: #393939;
+ --text-primary: #f4f4f4;
+ --text-secondary: #c6c6c6;
+ --border-color: #525252;
+ --accent-color: #78a9ff;
+ --error-color: #ff8389;
+
+ --priority-high-bg: #520408;
+ --priority-high-text: #ffb3b8;
+ --priority-medium-bg: #4a3000;
+ --priority-medium-text: #f1c21b;
+ --priority-low-bg: #002d5c;
+ --priority-low-text: #a6c8ff;
}
}
-/* Focus styles */
-:focus-visible {
- outline: 2px solid var(--accent-color);
- outline-offset: 2px;
-}
+/* ============================================
+ Scrollbar Styling
+ ============================================ */
-/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
@@ -161,16 +81,19 @@ body {
}
::-webkit-scrollbar-thumb {
- background: var(--border-color);
+ background: var(--cds-border-subtle, var(--border-color));
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
- background: var(--text-secondary);
+ background: var(--cds-text-secondary, var(--text-secondary));
}
-/* Selection */
+/* ============================================
+ Selection
+ ============================================ */
+
::selection {
- background: var(--accent-color);
- color: white;
+ background: var(--cds-interactive, var(--accent-color));
+ color: var(--cds-text-on-color, white);
}
diff --git a/src/lib/carbon-overrides.css b/src/lib/carbon-overrides.css
new file mode 100644
index 0000000..0a95980
--- /dev/null
+++ b/src/lib/carbon-overrides.css
@@ -0,0 +1,160 @@
+/* Carbon Design System overrides for Potasko */
+
+/* ============================================
+ Safe Area Insets (notched devices)
+ ============================================ */
+
+/* Header - account for notch/status bar */
+.bx--header {
+ padding-top: env(safe-area-inset-top, 0);
+}
+
+/* Content - account for bottom safe area */
+.bx--content {
+ padding-bottom: env(safe-area-inset-bottom, 0);
+}
+
+/* SideNav - account for safe areas */
+.bx--side-nav {
+ padding-top: env(safe-area-inset-top, 0);
+ padding-bottom: env(safe-area-inset-bottom, 0);
+}
+
+/* ============================================
+ Mobile Bottom Sheet Modal
+ ============================================ */
+
+/* On mobile, position modals at bottom like a bottom sheet */
+@media (max-width: 1056px) {
+ .bx--modal-container {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: auto;
+ max-height: 90vh;
+ max-width: 100%;
+ width: 100%;
+ margin: 0;
+ border-radius: 1rem 1rem 0 0;
+ transform: translateY(100%);
+ animation: slideUp 0.3s ease forwards;
+ }
+
+ .bx--modal.is-visible .bx--modal-container {
+ transform: translateY(0);
+ }
+
+ /* Add drag handle appearance */
+ .bx--modal-header::before {
+ content: '';
+ display: block;
+ width: 36px;
+ height: 4px;
+ background: var(--cds-border-subtle, #e0e0e0);
+ border-radius: 2px;
+ margin: 0 auto 0.5rem;
+ }
+
+ /* Account for bottom safe area in modal footer */
+ .bx--modal-footer {
+ padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0));
+ }
+}
+
+@keyframes slideUp {
+ from {
+ transform: translateY(100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+/* ============================================
+ Touch Target Improvements
+ ============================================ */
+
+@media (max-width: 1056px) {
+ /* Larger checkboxes for touch */
+ .bx--checkbox-label::before,
+ .bx--checkbox-label::after {
+ width: 24px;
+ height: 24px;
+ }
+
+ /* Ensure buttons meet touch target guidelines */
+ .bx--btn--sm {
+ min-height: 44px;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+
+ /* Larger form inputs on mobile */
+ .bx--text-input,
+ .bx--text-area,
+ .bx--select-input,
+ .bx--date-picker__input {
+ min-height: 44px;
+ font-size: 16px; /* Prevents iOS zoom on focus */
+ }
+}
+
+/* ============================================
+ SideNav Tweaks
+ ============================================ */
+
+/* Ensure SideNav items have adequate touch targets on mobile */
+@media (max-width: 1056px) {
+ .bx--side-nav__link,
+ .bx--side-nav__menu-item {
+ min-height: 48px;
+ }
+}
+
+/* Fix SideNav menu text overflow */
+.bx--side-nav__link-text,
+.bx--side-nav__menu-item > .bx--side-nav__link-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* ============================================
+ DataTable Mobile Adjustments
+ ============================================ */
+
+@media (max-width: 1056px) {
+ /* Allow horizontal scroll on small screens */
+ .bx--data-table-container {
+ overflow-x: auto;
+ }
+
+ .bx--data-table {
+ min-width: 500px;
+ }
+}
+
+/* ============================================
+ Accordion Tweaks
+ ============================================ */
+
+/* Reduce padding in accordion content for task lists */
+.bx--accordion__content {
+ padding-left: 0;
+ padding-right: 0;
+}
+
+/* ============================================
+ Form Layout Improvements
+ ============================================ */
+
+/* Better spacing for form groups */
+.bx--form-item {
+ margin-bottom: 1rem;
+}
+
+/* DatePicker calendar positioning */
+.bx--date-picker--single .bx--date-picker__calendar {
+ z-index: 9100;
+}
diff --git a/src/lib/components/AccountForm.svelte b/src/lib/components/AccountForm.svelte
index 9826234..3b44eaa 100644
--- a/src/lib/components/AccountForm.svelte
+++ b/src/lib/components/AccountForm.svelte
@@ -3,6 +3,18 @@
import { accountStore } from '$lib/stores/accounts.svelte';
import { listStore } from '$lib/stores/lists.svelte';
import type { Account, CalendarInfo } from '$lib/types';
+ import {
+ Form,
+ TextInput,
+ PasswordInput,
+ Button,
+ ButtonSet,
+ InlineNotification,
+ UnorderedList,
+ ListItem,
+ InlineLoading,
+ } from 'carbon-components-svelte';
+ import { Checkmark, WarningFilled } from 'carbon-icons-svelte';
interface Props {
account?: Account | null;
@@ -122,111 +134,118 @@
}
});
+ const taskCalendars = $derived(discoveredCalendars.filter(c => c.supports_vtodo));
-
+
diff --git a/src/lib/components/AccountList.svelte b/src/lib/components/AccountList.svelte
index 18ad42c..7c9c6f2 100644
--- a/src/lib/components/AccountList.svelte
+++ b/src/lib/components/AccountList.svelte
@@ -5,6 +5,14 @@
import AccountForm from './AccountForm.svelte';
import SyncLogModal from './SyncLogModal.svelte';
import type { Account } from '$lib/types';
+ import {
+ Button,
+ Tile,
+ InlineLoading,
+ InlineNotification,
+ TooltipDefinition,
+ } from 'carbon-components-svelte';
+ import { Add, Renew, Edit, TrashCan, Checkmark } from 'carbon-icons-svelte';
// UI state
let showAddForm = $state(false);
@@ -88,88 +96,69 @@
{#if accountStore.loading}
-
Loading accounts...
+
{:else if accountStore.accounts.length === 0 && !showAddForm}
-
-
+
+
No CalDAV accounts configured
Add an account to sync tasks with your CalDAV server
-
+
{:else}
-
+
{#each accountStore.accounts as account (account.id)}
-
-
+
{account.name}
{account.server_url}
{account.username}
-
-
+
{/each}
-
+
{#if syncStore.lastResult}
-
- {#if syncStore.lastResult.success}
- ✓
- {formatSyncResult()}
- {:else}
- ✗
- {syncStore.lastResult.error || 'Sync failed'}
- {/if}
-
+
{/if}
- showSyncLog = true}>
+ showSyncLog = true} class="view-log-btn">
View Sync Log
-
+
{/if}
{#if showAddForm}
@@ -181,7 +170,12 @@
{/if}
{#if accountStore.error && !showAddForm && !editingAccount}
- {accountStore.error}
+
{/if}
{#if showSyncLog}
@@ -208,44 +202,13 @@
font-weight: 600;
}
- .add-btn {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 1rem;
- background: var(--accent-color);
- color: white;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 0.875rem;
- font-weight: 500;
- }
-
- .add-btn svg {
- width: 18px;
- height: 18px;
- }
-
- .add-btn:hover {
- opacity: 0.9;
- }
-
- .loading {
- color: var(--text-secondary);
- text-align: center;
- padding: 2rem;
- }
-
- .empty-state {
+ :global(.empty-state) {
text-align: center;
- padding: 3rem 2rem;
- color: var(--text-secondary);
+ padding: 3rem 2rem !important;
+ color: var(--cds-text-secondary, var(--text-secondary));
}
- .empty-icon {
- width: 48px;
- height: 48px;
+ :global(.empty-icon) {
opacity: 0.5;
margin-bottom: 1rem;
}
@@ -260,22 +223,15 @@
}
.accounts {
- list-style: none;
- margin: 0;
- padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
- .account-item {
- display: flex;
+ :global(.account-item) {
+ display: flex !important;
justify-content: space-between;
align-items: center;
- padding: 1rem;
- background: var(--bg-secondary);
- border-radius: 8px;
- border: 1px solid var(--border-color);
}
.account-info {
@@ -292,7 +248,7 @@
.account-server {
font-size: 0.875rem;
- color: var(--text-secondary);
+ color: var(--cds-text-secondary, var(--text-secondary));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -300,54 +256,17 @@
.account-user {
font-size: 0.8125rem;
- color: var(--text-secondary);
+ color: var(--cds-text-secondary, var(--text-secondary));
opacity: 0.8;
}
.account-actions {
display: flex;
- gap: 0.5rem;
+ gap: 0.25rem;
flex-shrink: 0;
}
- .icon-btn {
- width: 32px;
- height: 32px;
- border: none;
- background: transparent;
- cursor: pointer;
- border-radius: 6px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-secondary);
- }
-
- .icon-btn svg {
- width: 18px;
- height: 18px;
- }
-
- .icon-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- }
-
- .icon-btn.danger:hover {
- background: var(--priority-high-bg);
- color: var(--priority-high-text);
- }
-
- .icon-btn.sync-btn {
- color: var(--accent-color);
- }
-
- .icon-btn.sync-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- .spinning {
+ :global(.spinning) {
animation: spin 1s linear infinite;
}
@@ -356,54 +275,8 @@
to { transform: rotate(360deg); }
}
- .sync-result {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.75rem 1rem;
- border-radius: 6px;
- font-size: 0.875rem;
- margin-top: 0.5rem;
- }
-
- .sync-result.success {
- background: var(--priority-low-bg);
- color: var(--priority-low-text);
- }
-
- .sync-result.error {
- background: var(--priority-high-bg);
- color: var(--priority-high-text);
- }
-
- .sync-result-icon {
- font-weight: bold;
- }
-
- .error {
- color: var(--error-color);
- font-size: 0.875rem;
- padding: 0.75rem;
- background: var(--priority-high-bg);
- border-radius: 6px;
- margin: 0;
- }
-
- .view-log-btn {
- display: block;
+ :global(.view-log-btn) {
width: 100%;
- padding: 0.5rem;
- margin-top: 0.75rem;
- background: transparent;
- border: 1px solid var(--border-color);
- border-radius: 6px;
- cursor: pointer;
- font-size: 0.8125rem;
- color: var(--text-secondary);
- }
-
- .view-log-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
+ justify-content: center;
}
diff --git a/src/lib/components/DeleteListModal.svelte b/src/lib/components/DeleteListModal.svelte
index 946918e..8d06ba4 100644
--- a/src/lib/components/DeleteListModal.svelte
+++ b/src/lib/components/DeleteListModal.svelte
@@ -2,6 +2,16 @@
import { getTaskCount } from '$lib/api';
import { listStore } from '$lib/stores/lists.svelte';
import type { TaskList } from '$lib/types';
+ import {
+ ComposedModal,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ Button,
+ InlineNotification,
+ InlineLoading,
+ } from 'carbon-components-svelte';
+ import { Warning, WarningAlt } from 'carbon-icons-svelte';
interface Props {
list: TaskList;
@@ -52,263 +62,98 @@
}
}
- function handleBackdropClick(event: MouseEvent) {
- if (event.target === event.currentTarget) {
- onClose();
- }
- }
-
- function handleKeydown(event: KeyboardEvent) {
- if (event.key === 'Escape') {
- onClose();
- }
- }
-
const isSynced = $derived(!!list.caldav_url);
-
-
-
-
-
-
-
-
- {#if loading}
-
Loading...
- {:else if step === 'confirm'}
-
- Are you sure you want to delete {list.name}?
-
- {#if isSynced}
-
-
- This list is synced to a CalDAV server and will also be deleted from the server.
-
- {/if}
- {#if error}
-
{error}
- {/if}
-
-
- Cancel
-
-
- {deleting ? 'Deleting...' : 'Delete'}
-
-
- {:else}
-
-
+
+
+
+ {#if loading}
+
+ {:else if step === 'confirm'}
+
+ Are you sure you want to delete {list.name}?
+
+ {#if isSynced}
+
+ {/if}
+ {#if error}
+
+ {/if}
+ {:else}
+
+
+
This list contains {taskCount} {taskCount === 1 ? 'task' : 'tasks'} that will also be deleted.
-
This action cannot be undone.
- {#if error}
-
{error}
- {/if}
-
- step = 'confirm'} disabled={deleting}>
- Back
-
-
- {deleting ? 'Deleting...' : 'Delete Forever'}
-
-
+
+ This action cannot be undone.
+ {#if error}
+
{/if}
-
-
-
+ {/if}
+
+
+ {#if step === 'confirm'}
+
+ Cancel
+
+
+ {deleting ? 'Deleting...' : 'Delete'}
+
+ {:else}
+ step = 'confirm'} disabled={deleting}>
+ Back
+
+
+ {deleting ? 'Deleting...' : 'Delete Forever'}
+
+ {/if}
+
+
diff --git a/src/lib/components/ListFormModal.svelte b/src/lib/components/ListFormModal.svelte
new file mode 100644
index 0000000..52cc3e4
--- /dev/null
+++ b/src/lib/components/ListFormModal.svelte
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+ {#if !isEditing && accountStore.accounts.length > 0}
+
+ {/if}
+
+
+
Color
+
+ {#each COLORS as c}
+
color = c.value}
+ title={c.label}
+ disabled={saving}
+ >
+ {#if color === c.value}
+
+ {/if}
+
+ {/each}
+
+
+
+
+
+ Cancel
+
+
+ {saving ? 'Saving...' : isEditing ? 'Save' : 'Create'}
+
+
+
+
+
diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte
index 1a4a4a8..c8bb805 100644
--- a/src/lib/components/Sidebar.svelte
+++ b/src/lib/components/Sidebar.svelte
@@ -2,18 +2,29 @@
import type { TaskList } from '$lib/types';
import { listStore } from '$lib/stores/lists.svelte';
import { accountStore } from '$lib/stores/accounts.svelte';
- import { syncStore } from '$lib/stores/sync.svelte';
import DeleteListModal from './DeleteListModal.svelte';
+ import ListFormModal from './ListFormModal.svelte';
+ import {
+ SideNavLink,
+ SideNavMenu,
+ SideNavMenuItem,
+ SideNavDivider,
+ } from 'carbon-components-svelte';
+ import { Time, Warning, Add } from 'carbon-icons-svelte';
interface Props {
showSettings?: boolean;
onToggleSettings?: () => void;
isMobile?: boolean;
- isOpen?: boolean;
onClose?: () => void;
}
- let { showSettings = false, onToggleSettings, isMobile = false, isOpen = false, onClose }: Props = $props();
+ let { showSettings = false, onToggleSettings, isMobile = false, onClose }: Props = $props();
+
+ // Modal state
+ let showListForm = $state(false);
+ let editingList = $state(null);
+ let listToDelete = $state(null);
// Close sidebar when a navigation item is selected on mobile
function handleNavigation(action: () => void) {
@@ -23,32 +34,20 @@
}
}
- // Predefined color palette
- const COLORS = [
- '#6b7280', // gray
- '#ef4444', // red
- '#f97316', // orange
- '#eab308', // yellow
- '#22c55e', // green
- '#14b8a6', // teal
- '#3b82f6', // blue
- '#8b5cf6', // purple
- '#ec4899', // pink
- ];
-
- // Add list state
- let newListName = $state('');
- let newListColor = $state(COLORS[0]);
- let newListAccountId = $state(null);
- let isAdding = $state(false);
+ function handleAddList() {
+ editingList = null;
+ showListForm = true;
+ }
- // Edit list state
- let editingList = $state(null);
- let editName = $state('');
- let editColor = $state('');
+ function handleEditList(list: TaskList) {
+ editingList = list;
+ showListForm = true;
+ }
- // Delete modal state
- let listToDelete = $state(null);
+ function handleCloseListForm() {
+ showListForm = false;
+ editingList = null;
+ }
// Group lists by account
const groupedLists = $derived(() => {
@@ -73,689 +72,85 @@
const account = accountStore.accounts.find(a => a.id === accountId);
return account?.name ?? `Account #${accountId}`;
}
-
- async function handleAddList(e: Event) {
- e.preventDefault();
- if (!newListName.trim()) return;
-
- try {
- await listStore.create({
- name: newListName.trim(),
- color: newListColor,
- account_id: newListAccountId,
- });
- newListName = '';
- newListColor = COLORS[0];
- newListAccountId = null;
- isAdding = false;
- } catch {
- // Error is handled by store
- }
- }
-
- function startEdit(list: TaskList) {
- editingList = list;
- editName = list.name;
- editColor = list.color ?? COLORS[0];
- }
-
- async function handleEditList(e: Event) {
- e.preventDefault();
- if (!editingList || !editName.trim()) return;
-
- try {
- await listStore.update(editingList.id, { name: editName.trim(), color: editColor });
- editingList = null;
- } catch {
- // Error is handled by store
- }
- }
-
- function handleKeydown(e: KeyboardEvent) {
- if (e.key === 'Escape') {
- isAdding = false;
- newListName = '';
- newListColor = COLORS[0];
- newListAccountId = null;
- editingList = null;
- }
- }
-
- function handleDeleteClick(list: TaskList, e: Event) {
- e.stopPropagation();
- listToDelete = list;
- }
-
+
+{#if showListForm}
+
+{/if}
{#if listToDelete}
listToDelete = null} />
{/if}
diff --git a/src/lib/components/SyncLogModal.svelte b/src/lib/components/SyncLogModal.svelte
index fe3def8..bc4e4c8 100644
--- a/src/lib/components/SyncLogModal.svelte
+++ b/src/lib/components/SyncLogModal.svelte
@@ -1,6 +1,15 @@
-
-
-
-
-
-
-
-
- {#if loading && entries.length === 0}
-
Loading sync log...
- {:else if error}
-
{error}
- {:else if entries.length === 0}
-
No sync operations logged yet.
- {:else}
-
-
-
-
- | Time |
- Operation |
- Status |
- Details |
-
-
-
- {#each entries as entry (entry.id)}
-
- | {formatTimestamp(entry.timestamp)} |
- {getOperationLabel(entry.operation)} |
- {entry.status} |
-
- {#if entry.message}
- {entry.message}
- {/if}
- {#if entry.list_id || entry.task_id || entry.http_status}
-
- {#if entry.list_id}List: {entry.list_id}{/if}
- {#if entry.task_id}{entry.list_id ? ' · ' : ''}Task: {entry.task_id}{/if}
- {#if entry.http_status}{(entry.list_id || entry.task_id) ? ' · ' : ''}HTTP: {entry.http_status}{/if}
-
- {/if}
- |
-
- {/each}
-
-
-
+ function formatDetails(entry: SyncLogEntry): string {
+ const parts = [];
+ if (entry.message) parts.push(entry.message);
+ if (entry.list_id) parts.push(`List: ${entry.list_id}`);
+ if (entry.task_id) parts.push(`Task: ${entry.task_id}`);
+ if (entry.http_status) parts.push(`HTTP: ${entry.http_status}`);
+ return parts.join(' · ');
+ }
+
+ // Transform entries for DataTable
+ const tableRows = $derived(entries.map(entry => ({
+ id: String(entry.id),
+ time: formatTimestamp(entry.timestamp),
+ operation: getOperationLabel(entry.operation),
+ status: entry.status,
+ details: formatDetails(entry),
+ })));
+
+ const headers = [
+ { key: 'time', value: 'Time' },
+ { key: 'operation', value: 'Operation' },
+ { key: 'status', value: 'Status' },
+ { key: 'details', value: 'Details' },
+ ];
+
- {#if hasMore}
-
loadEntries(true)}
+
+
+
+ {#if loading && entries.length === 0}
+
+ {:else if error}
+ {error}
+ {:else if entries.length === 0}
+ No sync operations logged yet.
+ {:else}
+
+
+ {#if cell.key === 'status'}
+ {cell.value}
+ {:else}
+ {cell.value}
+ {/if}
+
+
+
+ {#if hasMore}
+
+ loadEntries(true)}
disabled={loading}
>
{loading ? 'Loading...' : 'Load More'}
-
- {/if}
+
+
{/if}
-
-
-
+ {/if}
+
+
diff --git a/src/lib/components/SyncStatus.svelte b/src/lib/components/SyncStatus.svelte
index fbc471d..3075221 100644
--- a/src/lib/components/SyncStatus.svelte
+++ b/src/lib/components/SyncStatus.svelte
@@ -1,6 +1,8 @@
-
+
+
+
diff --git a/src/lib/components/TaskItem.svelte b/src/lib/components/TaskItem.svelte
index 90bd058..8fe9ed9 100644
--- a/src/lib/components/TaskItem.svelte
+++ b/src/lib/components/TaskItem.svelte
@@ -1,6 +1,8 @@
-
- {#if task.completed}
-
- {/if}
-
+
onEdit?.(task)}>
{task.title}
-
-
-
+
diff --git a/src/lib/components/TaskListView.svelte b/src/lib/components/TaskListView.svelte
index d9ff09c..e4c1bee 100644
--- a/src/lib/components/TaskListView.svelte
+++ b/src/lib/components/TaskListView.svelte
@@ -4,6 +4,7 @@
import { listStore } from '$lib/stores/lists.svelte';
import TaskItem from './TaskItem.svelte';
import SyncStatus from './SyncStatus.svelte';
+ import { Accordion, AccordionItem, InlineLoading } from 'carbon-components-svelte';
interface Props {
onEditTask?: (task: Task) => void;
@@ -43,11 +44,13 @@
{/if}
- {incompleteTasks.length} tasks
+ {incompleteTasks.length} task{incompleteTasks.length !== 1 ? 's' : ''}
{#if taskStore.loading}
- Loading tasks...
+
+
+
{:else if taskStore.error}
{taskStore.error}
{:else}
@@ -65,14 +68,15 @@
{#if !hideCompletedSection && completedTasks.length > 0}
-
- Completed ({completedTasks.length})
-
- {#each completedTasks as task (task.id)}
-
- {/each}
-
-
+
+
+
+ {#each completedTasks as task (task.id)}
+
+ {/each}
+
+
+
{/if}
{/if}
@@ -90,8 +94,8 @@
}
.list-header {
- padding: 1.5rem 1.5rem 1rem;
- border-bottom: 1px solid var(--border-color);
+ padding: 1rem;
+ border-bottom: 1px solid var(--cds-border-subtle, var(--border-color));
}
.header-top {
@@ -103,19 +107,19 @@
.list-header h1 {
margin: 0;
- font-size: 1.5rem;
+ font-size: 1.25rem;
font-weight: 600;
}
.task-count {
font-size: 0.875rem;
- color: var(--text-secondary);
+ color: var(--cds-text-secondary, var(--text-secondary));
}
.tasks-container {
flex: 1;
overflow-y: auto;
- padding: 1rem 1.5rem;
+ padding: 0.5rem 1rem;
}
.task-section {
@@ -123,24 +127,6 @@
flex-direction: column;
}
- .completed-section {
- margin-top: 1.5rem;
- border-top: 1px solid var(--border-color);
- padding-top: 1rem;
- }
-
- .completed-section summary {
- cursor: pointer;
- font-size: 0.875rem;
- color: var(--text-secondary);
- padding: 0.5rem 0;
- user-select: none;
- }
-
- .completed-section summary:hover {
- color: var(--text-primary);
- }
-
.loading,
.error,
.empty-state {
@@ -149,11 +135,11 @@
}
.error {
- color: var(--error-color);
+ color: var(--cds-danger, var(--error-color));
}
.empty-state {
- color: var(--text-secondary);
+ color: var(--cds-text-secondary, var(--text-secondary));
}
.empty-state p {
@@ -164,15 +150,15 @@
font-size: 0.875rem;
}
- /* Mobile adjustments */
- @media (max-width: 768px) {
+ /* Hide header on mobile - shown in Carbon Header instead */
+ @media (max-width: 1056px) {
.list-header {
- display: none; /* Title shown in mobile header */
+ display: none;
}
.tasks-container {
- padding: 0.75rem 1rem;
- padding-bottom: env(safe-area-inset-bottom, 0.75rem);
+ padding: 0.5rem;
+ padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0));
}
}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 4039a26..89c9df7 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,5 +1,26 @@
-
+
+
+{@render children()}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 20df139..973197c 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -8,34 +8,30 @@
import TaskListView from '$lib/components/TaskListView.svelte';
import TaskForm from '$lib/components/TaskForm.svelte';
import AccountList from '$lib/components/AccountList.svelte';
+ import {
+ Header,
+ HeaderUtilities,
+ HeaderGlobalAction,
+ SideNav,
+ SideNavItems,
+ Content,
+ Breakpoint,
+ ComposedModal,
+ ModalHeader,
+ ModalBody,
+ } from 'carbon-components-svelte';
+ import { Settings, Add, Close } from 'carbon-icons-svelte';
let editingTask = $state(null);
let showSettings = $state(false);
- let showMobileForm = $state(false);
+ let showTaskForm = $state(false);
- // Mobile responsiveness state
- const MOBILE_BREAKPOINT = 768;
- let isMobile = $state(false);
+ // Mobile responsiveness using Carbon's Breakpoint
+ let breakpointSize: 'sm' | 'md' | 'lg' | 'xlg' | 'max' = $state('lg');
+ const isMobile = $derived(breakpointSize === 'sm' || breakpointSize === 'md');
let sidebarOpen = $state(false);
- function checkMobile() {
- const wasMobile = isMobile;
- isMobile = window.innerWidth < MOBILE_BREAKPOINT;
- // Close sidebar when switching to desktop mode
- if (wasMobile && !isMobile) {
- sidebarOpen = false;
- }
- }
-
- function toggleSidebar() {
- sidebarOpen = !sidebarOpen;
- }
-
- function closeSidebar() {
- sidebarOpen = false;
- }
-
- // Get current view title for mobile header
+ // Get current view title for header
const currentViewTitle = $derived(() => {
if (showSettings) return 'Settings';
const view = listStore.selectedView;
@@ -50,8 +46,6 @@
// Load lists on mount
onMount(() => {
listStore.load();
- checkMobile();
- window.addEventListener('resize', checkMobile);
// Setup sync event listeners to reload tasks when sync completes
syncStore.setupEventListeners((listId) => {
@@ -63,7 +57,6 @@
});
return () => {
- window.removeEventListener('resize', checkMobile);
syncStore.cleanup();
};
});
@@ -85,130 +78,85 @@
function handleEditTask(task: Task) {
editingTask = task;
- if (isMobile) {
- showMobileForm = true;
- }
+ showTaskForm = true;
}
function handleCloseEdit() {
editingTask = null;
- showMobileForm = false;
+ showTaskForm = false;
}
function handleToggleSettings() {
showSettings = !showSettings;
}
- function handleFabClick() {
+ function handleAddTask() {
editingTask = null;
- showMobileForm = true;
+ showTaskForm = true;
}
-
-
- {#if isMobile}
-
+
+
+
+
+
+
+ sidebarOpen = false}
+ />
+
+
+
+
+ {#if showSettings}
+
+ {:else if listStore.selectedView}
+
+ {:else if listStore.loading}
+ Loading...
+ {:else}
+
+
No lists yet
+
Create a list to get started
+
{/if}
-
-
-
- {#if isMobile && sidebarOpen}
-
- {/if}
-
-
- {#if showSettings}
-
- {:else if listStore.selectedView}
-
- {#if canAddTasks}
- {#if !isMobile}
-
- {#key editingTask?.id}
-
- {/key}
- {/if}
- {/if}
- {:else if listStore.loading}
- Loading...
- {:else}
-
-
No lists yet
-
Create a list to get started
-
- {/if}
-
-
-
- {#if isMobile && canAddTasks && !showSettings}
- {#if showMobileForm}
-
-
- {:else}
-
-
-
- {/if}
- {/if}
-
+
+
+
+{#if canAddTasks && !showSettings && !showTaskForm}
+
+
+
+{/if}
+
+
+{#if showTaskForm}
+
+
+
+ {#key editingTask?.id}
+
+ {/key}
+
+
+{/if}
diff --git a/svelte.config.js b/svelte.config.js
index a7830ea..15e42d4 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -4,10 +4,11 @@
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
import adapter from "@sveltejs/adapter-static";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
+import { optimizeImports } from "carbon-preprocess-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
- preprocess: vitePreprocess(),
+ preprocess: [vitePreprocess(), optimizeImports()],
kit: {
adapter: adapter({
fallback: "index.html",
diff --git a/vite.config.js b/vite.config.js
index bed9095..7890141 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,11 +1,12 @@
import { defineConfig } from "vite";
import { sveltekit } from "@sveltejs/kit/vite";
+import { optimizeCss } from "carbon-preprocess-svelte";
const host = process.env.TAURI_DEV_HOST;
// https://vite.dev/config/
export default defineConfig(async () => ({
- plugins: [sveltekit()],
+ plugins: [sveltekit(), optimizeCss()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//