web based infinite canvas

feat: improve spacing and add dark mode toggle

+156 -69
+1
apps/web/src/lib/assets/moon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-moon"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
+1
apps/web/src/lib/assets/sun.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sun"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
+5 -1
apps/web/src/lib/components/Icon.svelte
··· 2 2 import CloseIcon from '$lib/assets/close.svg?raw'; 3 3 import FolderIcon from '$lib/assets/folder.svg?raw'; 4 4 import InfoCircleIcon from '$lib/assets/info-circle.svg?raw'; 5 + import MoonIcon from '$lib/assets/moon.svg?raw'; 5 6 import PencilIcon from '$lib/assets/pencil.svg?raw'; 7 + import SunIcon from '$lib/assets/sun.svg?raw'; 6 8 import TrashIcon from '$lib/assets/trash.svg?raw'; 7 9 8 - export type IconName = 'close' | 'folder' | 'info-circle' | 'pencil' | 'trash'; 10 + export type IconName = 'close' | 'folder' | 'info-circle' | 'moon' | 'pencil' | 'sun' | 'trash'; 9 11 10 12 type Props = { name: IconName; size?: number; color?: string }; 11 13 ··· 15 17 close: CloseIcon, 16 18 folder: FolderIcon, 17 19 'info-circle': InfoCircleIcon, 20 + moon: MoonIcon, 18 21 pencil: PencilIcon, 22 + sun: SunIcon, 19 23 trash: TrashIcon 20 24 }; 21 25
+15 -8
apps/web/src/lib/components/StatusBar.svelte
··· 176 176 .status-bar { 177 177 display: grid; 178 178 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 179 - gap: 1rem; 180 - padding: 0.5rem 1rem; 179 + gap: 1.5rem; 180 + padding: 0.75rem 1.5rem; 181 181 background: var(--surface-elevated); 182 182 border-top: 1px solid var(--border); 183 183 font-size: 0.75rem; 184 184 align-items: center; 185 - min-height: 40px; 185 + min-height: 48px; 186 186 } 187 187 188 188 .status-bar__section { 189 189 display: flex; 190 190 flex-direction: row; 191 191 align-items: center; 192 - gap: 0.5rem; 192 + gap: 0.75rem; 193 193 position: relative; 194 194 } 195 195 196 196 .status-bar__toggle-row { 197 197 display: flex; 198 - gap: 1rem; 198 + gap: 1.25rem; 199 199 } 200 200 201 201 .status-bar__toggle { 202 202 display: flex; 203 203 align-items: center; 204 - gap: 0.25rem; 204 + gap: 0.375rem; 205 205 font-size: 0.75rem; 206 206 color: var(--text); 207 207 } ··· 209 209 .status-bar__toggle input { 210 210 margin: 0; 211 211 cursor: pointer; 212 + opacity: 0.8; 212 213 } 214 + 215 + .status-bar__toggle:hover input { 216 + opacity: 1; 217 + } 213 218 214 219 .status-bar__toggle input:focus { 215 220 outline: 2px solid var(--accent); ··· 217 222 } 218 223 219 224 .status-bar__label { 220 - font-size: 0.75rem; 225 + font-size: 0.6875rem; 221 226 color: var(--text-muted); 222 227 text-transform: uppercase; 223 - letter-spacing: 0.05em; 228 + letter-spacing: 0.075em; 229 + font-weight: 600; 224 230 } 225 231 226 232 .status-bar__value { 227 233 font-weight: 500; 228 234 color: var(--text); 235 + font-variant-numeric: tabular-nums; 229 236 } 230 237 231 238 .status-bar__value--error {
+87 -60
apps/web/src/lib/components/Toolbar.svelte
··· 10 10 } from '$lib/constants'; 11 11 import type { Platform } from '$lib/platform'; 12 12 import type { BrushSettings, BrushStore } from '$lib/status'; 13 + import { themeStore } from '$lib/theme.svelte'; 13 14 import type { 14 15 ArrowShape, 15 16 BoardMeta, ··· 608 609 </div> 609 610 610 611 <div class="toolbar__info-actions"> 612 + <button 613 + class="toolbar__info" 614 + onclick={() => themeStore.toggle()} 615 + aria-label="Toggle Dark Mode" 616 + title="Toggle Dark Mode"> 617 + <Icon name={themeStore.current === 'dark' ? 'sun' : 'moon'} size={16} /> 618 + <span class="toolbar__info-label">{themeStore.current === 'dark' ? 'Light' : 'Dark'}</span> 619 + </button> 611 620 {#if platform === 'web' && onOpenBrowser} 612 621 <button class="toolbar__info" onclick={onOpenBrowser} aria-label="Browse boards"> 613 622 <Icon name="folder" size={16} /> ··· 670 679 <style> 671 680 .toolbar { 672 681 display: flex; 673 - gap: 0.5rem; 674 - padding: 0.75rem; 682 + gap: 0.75rem; 683 + padding: 0.75rem 1rem; 675 684 background: var(--surface-elevated); 676 685 border-bottom: 1px solid var(--border); 677 686 align-items: center; 687 + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 688 + z-index: 10; 689 + position: relative; 678 690 } 679 691 692 + .toolbar__brand { 693 + display: flex; 694 + align-items: center; 695 + gap: 0.75rem; 696 + margin-right: 1.5rem; 697 + } 698 + 699 + .toolbar__logo img { 700 + width: 32px; 701 + height: 32px; 702 + } 703 + 704 + .toolbar__name { 705 + font-weight: 600; 706 + font-size: 1.125rem; 707 + letter-spacing: -0.025em; 708 + color: var(--text); 709 + } 710 + 711 + .toolbar__tagline { 712 + font-size: 0.75rem; 713 + color: var(--text-muted); 714 + font-weight: 500; 715 + } 716 + 680 717 .toolbar__tool-button { 681 718 display: flex; 682 719 flex-direction: column; 683 720 align-items: center; 684 - gap: 0.25rem; 685 - padding: 0.5rem 0.75rem; 686 - border: 1px solid var(--border); 687 - border-radius: 0.25rem; 688 - background: var(--surface); 689 - color: var(--text); 721 + gap: 0.375rem; 722 + padding: 0.625rem 0.875rem; 723 + border: 1px solid transparent; 724 + border-radius: 0.5rem; 725 + background: transparent; 726 + color: var(--text-muted); 690 727 cursor: pointer; 691 - transition: all 0.2s; 692 - min-width: 60px; 728 + transition: all 0.2s ease; 729 + min-width: 68px; 693 730 } 694 731 695 732 .toolbar__tool-button:hover { 696 - background: var(--surface-elevated); 697 - border-color: var(--text-muted); 733 + background: var(--bg-tertiary); 734 + color: var(--text); 698 735 } 699 736 700 737 .toolbar__tool-button:focus { ··· 706 743 .tool-button.active { 707 744 background: var(--accent); 708 745 color: var(--surface); 709 - border-color: var(--accent-hover); 746 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 710 747 } 711 748 712 749 .toolbar__tool-icon { 713 - font-size: 1.5rem; 750 + font-size: 1.25rem; 714 751 line-height: 1; 715 752 } 716 753 717 754 .toolbar__tool-label { 718 755 font-size: 0.75rem; 756 + font-weight: 500; 719 757 line-height: 1; 720 758 white-space: nowrap; 721 759 } ··· 723 761 .toolbar__divider { 724 762 width: 1px; 725 763 background-color: var(--border); 726 - margin: 0 8px; 727 - height: 40px; 728 - } 729 - 730 - .toolbar__colors { 731 - display: flex; 732 - gap: 0.75rem; 733 - align-items: center; 764 + margin: 0 1rem; 765 + height: 48px; 734 766 } 735 767 736 - .toolbar__color-control { 737 - display: flex; 738 - flex-direction: column; 739 - gap: 0.25rem; 740 - font-size: 0.75rem; 741 - color: var(--text-muted); 742 - } 743 - 744 - .toolbar__color-control input[type='color'] { 745 - width: 40px; 746 - height: 30px; 747 - border: 1px solid var(--border); 748 - border-radius: 6px; 749 - padding: 0; 750 - background: transparent; 751 - cursor: pointer; 752 - } 753 - 754 - .toolbar__color-control input[type='color']:disabled { 755 - opacity: 0.4; 756 - cursor: not-allowed; 757 - } 768 + .toolbar__info { 769 + display: flex; 770 + align-items: center; 771 + gap: 0.5rem; 772 + padding: 0.5rem 0.75rem; 773 + border-radius: 0.375rem; 774 + background: transparent; 775 + border: none; 776 + color: var(--text-muted); 777 + cursor: pointer; 778 + transition: color 0.2s; 779 + font-size: 0.875rem; 780 + } 781 + 782 + .toolbar__info:hover { 783 + background: var(--bg-tertiary); 784 + color: var(--text); 785 + } 758 786 759 787 .toolbar__zoom, 760 788 .toolbar__export { ··· 766 794 border: 1px solid var(--border); 767 795 background: var(--surface); 768 796 color: var(--text); 769 - padding: 0.5rem 0.75rem; 770 - border-radius: 0.25rem; 797 + padding: 0.5rem 1rem; 798 + border-radius: 0.375rem; 771 799 cursor: pointer; 772 - font-size: 13px; 773 - min-width: 60px; 800 + font-size: 0.875rem; 801 + font-weight: 500; 802 + min-width: 72px; 803 + transition: all 0.2s; 774 804 } 775 805 776 806 .toolbar__zoom-button:hover, 777 807 .toolbar__export-button:hover { 778 - background: var(--surface-elevated); 779 - } 780 - 781 - .toolbar__zoom-button:focus, 782 - .toolbar__export-button:focus { 783 - outline: 2px solid var(--accent); 784 - outline-offset: 2px; 808 + background: var(--bg-tertiary); 809 + border-color: var(--text-muted); 785 810 } 786 811 787 812 .toolbar__zoom-menu, 788 813 .toolbar__export-menu { 789 814 position: absolute; 790 - top: calc(100% + 4px); 815 + top: calc(100% + 8px); 791 816 left: 0; 792 - background: var(--surface); 817 + background: var(--surface-elevated); 793 818 color: var(--text); 794 819 border: 1px solid var(--border); 795 - border-radius: 6px; 796 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 797 - padding: 8px; 820 + border-radius: 0.5rem; 821 + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 822 + padding: 0.5rem; 798 823 display: flex; 799 824 flex-direction: column; 800 825 gap: 0.25rem; 826 + min-width: 160px; 827 + z-index: 20; 801 828 z-index: 10; 802 829 min-width: 150px; 803 830 }
+44
apps/web/src/lib/theme.svelte.ts
··· 1 + import { browser } from "$app/environment"; 2 + 3 + export type Theme = "light" | "dark"; 4 + 5 + export function createThemeStore() { 6 + let theme = $state<Theme>("dark"); 7 + 8 + if (browser) { 9 + const stored = localStorage.getItem("theme") as Theme | null; 10 + if (stored === "light" || stored === "dark") { 11 + theme = stored; 12 + } else { 13 + theme = "dark"; 14 + localStorage.setItem("theme", "dark"); 15 + } 16 + document.documentElement.setAttribute("data-theme", theme); 17 + } 18 + 19 + function toggle() { 20 + theme = theme === "dark" ? "light" : "dark"; 21 + if (browser) { 22 + localStorage.setItem("theme", theme); 23 + document.documentElement.setAttribute("data-theme", theme); 24 + } 25 + } 26 + 27 + function set(newTheme: Theme) { 28 + theme = newTheme; 29 + if (browser) { 30 + localStorage.setItem("theme", theme); 31 + document.documentElement.setAttribute("data-theme", theme); 32 + } 33 + } 34 + 35 + return { 36 + get current() { 37 + return theme; 38 + }, 39 + toggle, 40 + set, 41 + }; 42 + } 43 + 44 + export const themeStore = createThemeStore();
+3
apps/web/src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import favicon from '$lib/assets/favicon.svg'; 3 + import { themeStore } from '$lib/theme.svelte'; 3 4 import '../app.css'; 4 5 5 6 let { children } = $props(); 7 + 8 + const _ = themeStore; 6 9 </script> 7 10 8 11 <svelte:head>