| @@ -3,7 +3,7 @@ |
| 3 | 3 | <head> |
| 4 | 4 | <meta charset="utf-8"> |
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | | - <meta name="color-scheme" content="light"> |
| 6 | + <meta name="color-scheme" content="light dark"> |
| 7 | 7 | <meta name="description" content="@yield('description', 'Postline is a clean personal workspace for social publishing.')"> |
| 8 | 8 | <title>@yield('title', 'Postline')</title> |
| 9 | 9 | <link rel="icon" href="/favicon.svg" type="image/svg+xml"> |
| @@ -18,15 +18,20 @@ |
| 18 | 18 | })() |
| 19 | 19 | </script> |
| 20 | 20 | <script> |
| 21 | + // Theme bootstrap — runs before paint to avoid a flash. Stored |
| 22 | + // preference is 'light' | 'dark' | 'system' (default system). |
| 21 | 23 | ;(() => { |
| 22 | | - document.documentElement.setAttribute('data-theme', 'light') |
| 23 | | - document.documentElement.classList.remove('dark') |
| 24 | + const root = document.documentElement |
| 24 | 25 | try { |
| 25 | | - window.localStorage?.setItem('theme', 'light') |
| 26 | + const stored = window.localStorage?.getItem('theme') || 'system' |
| 27 | + const dark = stored === 'dark' || (stored === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) |
| 28 | + root.classList.toggle('dark', dark) |
| 29 | + root.setAttribute('data-theme', dark ? 'dark' : 'light') |
| 30 | + |
| 26 | 31 | const sidebarCollapsed = window.localStorage?.getItem('postline.sidebar.collapsed') === 'true' |
| 27 | | - document.documentElement.classList.toggle('postline-sidebar-collapsed', sidebarCollapsed) |
| 28 | | - document.documentElement.dataset.stxSidebarCollapsed = sidebarCollapsed ? 'true' : 'false' |
| 29 | | - document.documentElement.style.setProperty('--postline-sidebar-width', sidebarCollapsed ? '0px' : '286px') |
| 32 | + root.classList.toggle('postline-sidebar-collapsed', sidebarCollapsed) |
| 33 | + root.dataset.stxSidebarCollapsed = sidebarCollapsed ? 'true' : 'false' |
| 34 | + root.style.setProperty('--postline-sidebar-width', sidebarCollapsed ? '0px' : '286px') |
| 30 | 35 | } catch {} |
| 31 | 36 | })() |
| 32 | 37 | </script> |
| @@ -37,6 +42,97 @@ |
| 37 | 42 | background: #f3f3f1; |
| 38 | 43 | } |
| 39 | 44 | |
| 45 | + html.dark { |
| 46 | + color-scheme: dark; |
| 47 | + background: #0a0a0c; |
| 48 | + } |
| 49 | + |
| 50 | + /* |
| 51 | + * Dark theme — a scoped remap of the app's consistent utility palette. |
| 52 | + * The views use a small, fixed set of hardcoded utilities (bg-white |
| 53 | + * cards, zinc borders, neutral text), so one place remaps them instead |
| 54 | + * of adding dark: variants to ~380 call sites. Unlayered head styles |
| 55 | + * beat crosswind's layered utilities regardless of specificity. |
| 56 | + */ |
| 57 | + html.dark body { background-color: #0a0a0c; color: #e4e4e7; } |
| 58 | + |
| 59 | + /* Surfaces */ |
| 60 | + .dark .bg-white { background-color: #18181b; } |
| 61 | + .dark .bg-\[\#f3f3f1\] { background-color: #0a0a0c; } |
| 62 | + .dark .bg-zinc-50, .dark .bg-zinc-50\/80, .dark .bg-zinc-50\/48 { background-color: #1c1c1f; } |
| 63 | + .dark .bg-zinc-50\/40 { background-color: rgba(255, 255, 255, 0.02); } |
| 64 | + .dark .bg-zinc-100 { background-color: #26262a; } |
| 65 | + .dark .bg-blue-50 { background-color: #172554; } |
| 66 | + .dark .bg-emerald-50 { background-color: #052e2b; } |
| 67 | + .dark .bg-amber-50 { background-color: #3a2a08; } |
| 68 | + .dark .bg-red-50 { background-color: #3a1414; } |
| 69 | + |
| 70 | + /* Borders */ |
| 71 | + .dark .border-zinc-200, .dark .border-zinc-200\/80, .dark .border-zinc-100 { border-color: #2a2a2e; } |
| 72 | + .dark .border-blue-200 { border-color: #1e3a8a; } |
| 73 | + .dark .border-emerald-200 { border-color: #0f766e; } |
| 74 | + .dark .border-amber-200 { border-color: #78500a; } |
| 75 | + .dark .border-red-200 { border-color: #7f1d1d; } |
| 76 | + |
| 77 | + /* Text */ |
| 78 | + .dark .text-neutral-950 { color: #f4f4f5; } |
| 79 | + .dark .text-slate-700 { color: #d4d4d8; } |
| 80 | + .dark .text-zinc-600, .dark .text-zinc-500 { color: #a1a1aa; } |
| 81 | + .dark .text-zinc-700, .dark .text-zinc-800 { color: #d4d4d8; } |
| 82 | + .dark .text-blue-700, .dark .text-blue-800 { color: #93c5fd; } |
| 83 | + .dark .text-emerald-700 { color: #6ee7b7; } |
| 84 | + .dark .text-amber-800 { color: #fcd34d; } |
| 85 | + .dark .text-red-700 { color: #fca5a5; } |
| 86 | + |
| 87 | + /* Hover states (higher specificity than the base utilities above) */ |
| 88 | + .dark .hover\:bg-white:hover { background-color: #26262a; } |
| 89 | + .dark .hover\:bg-zinc-50:hover { background-color: #26262a; } |
| 90 | + .dark .hover\:bg-zinc-100:hover { background-color: #2f2f34; } |
| 91 | + |
| 92 | + /* Primary buttons invert to light-on-dark-text so they pop on dark surfaces */ |
| 93 | + .dark .bg-neutral-950 { background-color: #f4f4f5; } |
| 94 | + .dark .bg-neutral-950.text-white, |
| 95 | + .dark .bg-neutral-950 .text-white, |
| 96 | + .dark .text-white.bg-neutral-950 { color: #0a0a0c; } |
| 97 | + .dark .hover\:bg-neutral-800:hover { background-color: #d4d4d8; } |
| 98 | + /* Keep the small "P" brand mark dark-on-light */ |
| 99 | + .dark .bg-neutral-950.rounded-xl > span, |
| 100 | + .dark [aria-label="Postline composer"] .bg-neutral-950 { color: #0a0a0c; } |
| 101 | + |
| 102 | + /* Inputs + code */ |
| 103 | + .dark input, .dark textarea, .dark select { color: #f4f4f5; } |
| 104 | + .dark input::placeholder, .dark textarea::placeholder { color: #71717a; } |
| 105 | + .dark code { background-color: #26262a; color: #e4e4e7; } |
| 106 | + |
| 107 | + /* Shell surfaces (targeted by data-attribute; opacity/gradient variants) */ |
| 108 | + .dark [data-postline-content-shell], |
| 109 | + .dark [data-postline-workspace], |
| 110 | + .dark [data-postline-sidebar] { background-color: #0a0a0c; } |
| 111 | + .dark [data-postline-workspace] { |
| 112 | + border-color: #2a2a2e; |
| 113 | + box-shadow: -10px 0 36px rgba(0, 0, 0, 0.4); |
| 114 | + } |
| 115 | + .dark [data-postline-sidebar] { background-color: rgba(24, 24, 27, 0.5); } |
| 116 | + .dark [data-postline-sidebar] > .absolute { opacity: 0.35; } |
| 117 | + /* Sidebar nav: active item + white/xx controls and badges */ |
| 118 | + .dark .bg-\[\#e8e8e6\]\/90 { background-color: rgba(63, 63, 70, 0.6); } |
| 119 | + .dark .bg-white\/95 { background-color: rgba(39, 39, 42, 0.95); } |
| 120 | + .dark .bg-white\/72 { background-color: rgba(63, 63, 70, 0.72); } |
| 121 | + .dark .bg-white\/60 { background-color: rgba(63, 63, 70, 0.6); } |
| 122 | + .dark .hover\:bg-white\/38:hover, |
| 123 | + .dark .hover\:bg-white\/60:hover, |
| 124 | + .dark .hover\:bg-white\/35:hover { background-color: rgba(63, 63, 70, 0.55); } |
| 125 | + .dark .bg-white\/48 { background-color: rgba(39, 39, 42, 0.48); } |
| 126 | + |
| 127 | + /* Analytics chart — CSS overrides the hardcoded SVG palette (CSS |
| 128 | + fill/stroke beat presentation attributes) so the chart isn't a |
| 129 | + light island on a dark page. */ |
| 130 | + .dark [data-analytics-chart] rect[rx="16"] { fill: #101014; } |
| 131 | + .dark [data-analytics-chart] line[stroke="#e4e4e7"] { stroke: #2a2a2e; } |
| 132 | + .dark [data-analytics-chart] text[fill="#18181b"] { fill: #e4e4e7; } |
| 133 | + .dark [data-analytics-chart] rect[fill="#fff"] { fill: #26262a; stroke: #3f3f46; } |
| 134 | + .dark [data-analytics-chart] .bg-zinc-100 { background-color: #26262a; } |
| 135 | + |
| 40 | 136 | @property --postline-sidebar-width { |
| 41 | 137 | syntax: '<length>'; |
| 42 | 138 | inherits: true; |