ReviewOS

also looking at this

stacks/postline

Dark mode

#27
Merged glennmichael123 wants to merge feat/dark-mode into main
5 files +198 -7
resources/assets/styles/postline-timeline.cssmodified+19-0
Changes to resources/assets/styles/postline-timeline.css
@@ -265,3 +265,22 @@
265265 background-position: -100% 0;
266266 }
267267}
268
269/* Dark theme — the feed uses its own classes (not utilities), so it needs
270 its own overrides to match the app's .dark remap. */
271.dark .postline-feed__header,
272.dark .postline-feed__list { background: #18181b; }
273.dark .postline-feed__header { border-bottom-color: #2a2a2e; }
274.dark .postline-feed-post { border-bottom-color: #232327; }
275.dark .postline-feed-post:hover { background: #202024; }
276.dark .postline-feed-post__name { color: #f4f4f5; }
277.dark .postline-feed-post__body { color: #d4d4d8; }
278.dark .postline-feed__empty,
279.dark .postline-feed__loading { color: #a1a1aa; }
280.dark .postline-feed-post__avatar { border-color: rgb(255 255 255 / 0.12); }
281.dark .postline-feed__skeleton { border-bottom-color: #232327; }
282.dark .postline-feed__skeleton-avatar,
283.dark .postline-feed__skeleton-line {
284 background: linear-gradient(90deg, #232327 25%, #2c2c31 50%, #232327 75%);
285 background-size: 200% 100%;
286}
resources/assets/styles/postline-toast.cssmodified+7-0
Changes to resources/assets/styles/postline-toast.css
@@ -202,3 +202,10 @@
202202 gap: 0.75rem;
203203 align-items: flex-start;
204204}
205
206/* Dark theme */
207.dark .postline-toast {
208 background: rgb(24 24 27 / 0.97);
209 border-color: rgb(63 63 70 / 0.9);
210 color: #e4e4e7;
211}
resources/views/layouts/postline.stxmodified+103-7
Changes to resources/views/layouts/postline.stx
@@ -3,7 +3,7 @@
33 <head>
44 <meta charset="utf-8">
55 <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">
77 <meta name="description" content="@yield('description', 'Postline is a clean personal workspace for social publishing.')">
88 <title>@yield('title', 'Postline')</title>
99 <link rel="icon" href="/favicon.svg" type="image/svg+xml">
@@ -18,15 +18,20 @@
1818 })()
1919 </script>
2020 <script>
21 // Theme bootstrap — runs before paint to avoid a flash. Stored
22 // preference is 'light' | 'dark' | 'system' (default system).
2123 ;(() => {
22 document.documentElement.setAttribute('data-theme', 'light')
23 document.documentElement.classList.remove('dark')
24 const root = document.documentElement
2425 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
2631 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')
3035 } catch {}
3136 })()
3237 </script>
@@ -37,6 +42,97 @@
3742 background: #f3f3f1;
3843 }
3944
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
40136 @property --postline-sidebar-width {
41137 syntax: '<length>';
42138 inherits: true;
resources/views/login.stxmodified+26-0
Changes to resources/views/login.stx
@@ -4,6 +4,32 @@
44@section('description', 'Sign in to your Postline workspace.')
55
66@section('content')
7 <script>
8 // Login uses a different layout, so it carries its own theme bootstrap.
9 ;(() => {
10 try {
11 const s = localStorage.getItem('theme') || 'system'
12 const d = s === 'dark' || (s === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
13 document.documentElement.classList.toggle('dark', d)
14 document.documentElement.setAttribute('data-theme', d ? 'dark' : 'light')
15 } catch {}
16 })()
17 </script>
18 <style>
19 html.dark { color-scheme: dark; background: #0a0a0c; }
20 html.dark body { background-color: #0a0a0c; }
21 .dark .bg-\[\#f3f3f1\] { background-color: #0a0a0c; }
22 .dark .bg-white { background-color: #18181b; }
23 .dark .border-zinc-200 { border-color: #2a2a2e; }
24 .dark .text-neutral-950 { color: #f4f4f5; }
25 .dark .text-zinc-600, .dark .text-zinc-500 { color: #a1a1aa; }
26 .dark .text-red-700 { color: #fca5a5; }
27 .dark .bg-neutral-950 { background-color: #f4f4f5; color: #0a0a0c; }
28 .dark .hover\:bg-neutral-800:hover { background-color: #d4d4d8; }
29 .dark .hover\:text-zinc-700:hover { color: #d4d4d8; }
30 .dark input { color: #f4f4f5; background-color: #18181b; }
31 .dark input::placeholder { color: #71717a; }
32 </style>
733 <main class="grid place-items-center px-4 min-h-screen bg-[#f3f3f1]" data-testid="login-page">
834 <section class="grid gap-5 p-6 max-w-sm w-full bg-white border border-zinc-200 rounded-2xl shadow-sm">
935 <header class="grid gap-2 justify-items-center text-center">
resources/views/settings.stxmodified+43-0
Changes to resources/views/settings.stx
@@ -15,6 +15,20 @@
1515 </header>
1616
1717 <section class="grid gap-3.5 lg:grid-cols-2">
18 <article class="grid gap-3 lg:col-span-2 p-4 bg-white border border-zinc-200 rounded-2xl shadow-sm">
19 <header class="flex gap-3 items-center justify-between pb-3 border-b border-zinc-200">
20 <div>
21 <strong class="text-[13px]">Appearance</strong>
22 <p class="mb-0 mt-1 text-xs text-zinc-500">Light, dark, or match your system.</p>
23 </div>
24 <div class="grid grid-cols-3 p-0.5 bg-zinc-100 border border-zinc-200 rounded-xl" role="group" aria-label="Theme">
25 <button class="px-3 py-1.5 min-w-16 font-bold text-xs text-zinc-500 rounded-lg transition-colors" type="button" data-theme-option="light">Light</button>
26 <button class="px-3 py-1.5 min-w-16 font-bold text-xs text-zinc-500 rounded-lg transition-colors" type="button" data-theme-option="dark">Dark</button>
27 <button class="px-3 py-1.5 min-w-16 font-bold text-xs text-zinc-500 rounded-lg transition-colors" type="button" data-theme-option="system">System</button>
28 </div>
29 </header>
30 </article>
31
1832 <article class="grid gap-3 p-4 bg-white border border-zinc-200 rounded-2xl shadow-sm">
1933 <header class="flex gap-3 items-center justify-between pb-3 border-b border-zinc-200">
2034 <div>
@@ -58,6 +72,35 @@
5872 window.location.assign('/login')
5973 })
6074
75 // --- Theme (light / dark / system) ---
76 const themeOptions = Array.from(root.querySelectorAll('[data-theme-option]'))
77 const applyTheme = (pref) => {
78 const dark = pref === 'dark' || (pref === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
79 document.documentElement.classList.toggle('dark', dark)
80 document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light')
81 }
82 const renderTheme = (pref) => {
83 themeOptions.forEach((button) => {
84 const active = button.dataset.themeOption === pref
85 button.classList.toggle('bg-white', active)
86 button.classList.toggle('text-neutral-950', active)
87 button.classList.toggle('shadow-sm', active)
88 button.classList.toggle('text-zinc-500', !active)
89 })
90 }
91 const currentPref = (() => { try { return localStorage.getItem('theme') || 'system' } catch { return 'system' } })()
92 renderTheme(currentPref)
93 themeOptions.forEach(button => button.addEventListener('click', () => {
94 const pref = button.dataset.themeOption
95 try { localStorage.setItem('theme', pref) } catch {}
96 applyTheme(pref)
97 renderTheme(pref)
98 }))
99 // Keep 'system' live if the OS theme flips while the page is open.
100 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
101 try { if ((localStorage.getItem('theme') || 'system') === 'system') applyTheme('system') } catch {}
102 })
103
61104 const summary = root.querySelector('[data-settings-bluesky-summary]')
62105 const pill = root.querySelector('[data-settings-bluesky-pill]')
63106 const charLimit = root.querySelector('[data-settings-char-limit]')