|
5 | 5 | [reagent.dom :as rdom] |
6 | 6 | [clojure.string :as str])) |
7 | 7 |
|
| 8 | +;; ============================================================================ |
| 9 | +;; Theme Styles |
| 10 | +;; ============================================================================ |
| 11 | + |
| 12 | +(defn theme-styles |
| 13 | + "CSS for light and dark mode support using Quarto's data-bs-theme attribute" |
| 14 | + [] |
| 15 | + [:style " |
| 16 | + /* Light mode (default) */ |
| 17 | + [data-bs-theme='light'] { |
| 18 | + --card-bg: #ffffff; |
| 19 | + --card-secondary-bg: #f9fafb; |
| 20 | + --input-bg: #ffffff; |
| 21 | + --text-primary: #1f2937; |
| 22 | + --text-secondary: #6b7280; |
| 23 | + --text-tertiary: #9ca3af; |
| 24 | + --border-color: #e5e7eb; |
| 25 | + --border-color-dark: #d1d5db; |
| 26 | + --button-bg: #f3f4f6; |
| 27 | + --button-hover: #e5e7eb; |
| 28 | + --button-text: #374151; |
| 29 | + --shadow-color: rgba(0, 0, 0, 0.1); |
| 30 | + } |
| 31 | +
|
| 32 | + /* Dark mode */ |
| 33 | + [data-bs-theme='dark'] { |
| 34 | + --card-bg: #1f2937; |
| 35 | + --card-secondary-bg: #111827; |
| 36 | + --input-bg: #111827; |
| 37 | + --text-primary: #f3f4f6; |
| 38 | + --text-secondary: #d1d5db; |
| 39 | + --text-tertiary: #9ca3af; |
| 40 | + --border-color: #374151; |
| 41 | + --border-color-dark: #4b5563; |
| 42 | + --button-bg: #374151; |
| 43 | + --button-hover: #4b5563; |
| 44 | + --button-text: #f3f4f6; |
| 45 | + --shadow-color: rgba(0, 0, 0, 0.3); |
| 46 | + } |
| 47 | +
|
| 48 | + /* Hover states - applied universally */ |
| 49 | + button:not(.btn-primary):hover { |
| 50 | + background: var(--button-hover) !important; |
| 51 | + } |
| 52 | + "]) |
| 53 | + |
8 | 54 | ;; ============================================================================ |
9 | 55 | ;; API Functions (Consolidated) |
10 | 56 | ;; ============================================================================ |
|
82 | 128 | ;; Utilities (from previous demos) |
83 | 129 | ;; ============================================================================ |
84 | 130 |
|
85 | | -(defn celsius-to-fahrenheit [c] |
| 131 | +(defn celsius-to-fahrenheit |
| 132 | + [c] |
86 | 133 | (when c (+ (* c 1.8) 32))) |
87 | 134 |
|
88 | | -(defn fahrenheit-to-celsius [f] |
| 135 | +(defn fahrenheit-to-celsius |
| 136 | + [f] |
89 | 137 | (when f (* (- f 32) 0.5556))) |
90 | 138 |
|
91 | | -(defn fahrenheit-to-kelvin [f] |
| 139 | +(defn fahrenheit-to-kelvin |
| 140 | + [f] |
92 | 141 | (when f (+ (fahrenheit-to-celsius f) 273.15))) |
93 | 142 |
|
94 | | -(defn format-temp [fahrenheit unit] |
| 143 | +(defn format-temp |
| 144 | + [fahrenheit unit] |
95 | 145 | (when fahrenheit |
96 | 146 | (case unit |
97 | 147 | "F" (str fahrenheit "°F") |
98 | 148 | "C" (str (Math/round (fahrenheit-to-celsius fahrenheit)) "°C") |
99 | 149 | "K" (str (Math/round (fahrenheit-to-kelvin fahrenheit)) "K") |
100 | 150 | (str fahrenheit "°F")))) |
101 | 151 |
|
102 | | -(defn get-weather-icon [short-forecast] |
| 152 | +(defn get-weather-icon |
| 153 | + [short-forecast] |
103 | 154 | (let [forecast-lower (str/lower-case (or short-forecast ""))] |
104 | 155 | (cond |
105 | 156 | (re-find #"thunder|tstorm" forecast-lower) "⛈️" |
|
146 | 197 | :opacity "0.8"}) |
147 | 198 |
|
148 | 199 | (def card-style |
149 | | - {:background "#ffffff" |
150 | | - :border "1px solid #e0e0e0" |
| 200 | + {:background "var(--card-bg, #ffffff)" |
| 201 | + :color "var(--text-primary, #1f2937)" |
| 202 | + :border "1px solid var(--border-color, #e5e7eb)" |
151 | 203 | :border-radius "12px" |
152 | 204 | :padding "24px" |
153 | 205 | :margin-bottom "20px" |
154 | | - :box-shadow "0 2px 8px rgba(0,0,0,0.1)"}) |
| 206 | + :box-shadow "0 2px 8px var(--shadow-color, rgba(0,0,0,0.1))"}) |
155 | 207 |
|
156 | 208 | (def location-search-style |
157 | 209 | {:display "grid" |
|
161 | 213 |
|
162 | 214 | (def input-style |
163 | 215 | {:padding "12px 15px" |
164 | | - :border "1px solid #ddd" |
| 216 | + :background "var(--input-bg, #ffffff)" |
| 217 | + :color "var(--text-primary, #1f2937)" |
| 218 | + :border "1px solid var(--border-color-dark, #d1d5db)" |
165 | 219 | :border-radius "8px" |
166 | | - :font-size "14px"}) |
| 220 | + :font-size "14px" |
| 221 | + :box-shadow "inset 0 1px 2px var(--shadow-color, rgba(0,0,0,0.05))"}) |
167 | 222 |
|
168 | 223 | (def button-primary-style |
169 | 224 | {:padding "12px 24px" |
|
174 | 229 | :cursor "pointer" |
175 | 230 | :font-size "14px" |
176 | 231 | :font-weight "600" |
177 | | - :transition "all 0.2s"}) |
| 232 | + :transition "all 0.2s" |
| 233 | + :box-shadow "0 2px 4px rgba(0,0,0,0.3)"}) |
178 | 234 |
|
179 | 235 | (def quick-cities-grid-style |
180 | 236 | {:display "grid" |
|
184 | 240 |
|
185 | 241 | (def city-button-style |
186 | 242 | {:padding "10px" |
187 | | - :background "#f3f4f6" |
188 | | - :border "1px solid #e5e7eb" |
| 243 | + :background "var(--button-bg, #f3f4f6)" |
| 244 | + :color "var(--button-text, #374151)" |
| 245 | + :border "1px solid var(--border-color-dark, #d1d5db)" |
189 | 246 | :border-radius "8px" |
190 | 247 | :cursor "pointer" |
191 | 248 | :font-size "13px" |
| 249 | + :font-weight "500" |
192 | 250 | :text-align "center" |
193 | | - :transition "all 0.2s"}) |
| 251 | + :transition "all 0.2s" |
| 252 | + :box-shadow "0 1px 2px var(--shadow-color, rgba(0,0,0,0.1))"}) |
194 | 253 |
|
195 | 254 | (def tabs-container-style |
196 | 255 | {:display "flex" |
197 | 256 | :gap "5px" |
198 | | - :border-bottom "2px solid #e5e7eb" |
| 257 | + :border-bottom "2px solid #374151" |
199 | 258 | :margin-bottom "20px"}) |
200 | 259 |
|
201 | 260 | (defn tab-style [active?] |
|
215 | 274 | :justify-content "space-between" |
216 | 275 | :align-items "center" |
217 | 276 | :padding "15px" |
218 | | - :background "#f9fafb" |
| 277 | + :background "var(--card-secondary-bg, #f9fafb)" |
219 | 278 | :border-radius "8px" |
220 | 279 | :margin-bottom "20px" |
221 | 280 | :flex-wrap "wrap" |
|
229 | 288 | (def setting-label-style |
230 | 289 | {:font-size "14px" |
231 | 290 | :font-weight "500" |
232 | | - :color "#4b5563"}) |
| 291 | + :color "var(--text-secondary, #6b7280)"}) |
233 | 292 |
|
234 | 293 | (def toggle-buttons-style |
235 | 294 | {:display "flex" |
236 | 295 | :gap "5px"}) |
237 | 296 |
|
238 | 297 | (defn toggle-button-style [active?] |
239 | 298 | {:padding "6px 14px" |
240 | | - :background (if active? "#3b82f6" "white") |
241 | | - :color (if active? "white" "#6b7280") |
242 | | - :border "1px solid #d1d5db" |
| 299 | + :background (if active? "#3b82f6" "var(--button-bg, #f3f4f6)") |
| 300 | + :color (if active? "white" "var(--button-text, #374151)") |
| 301 | + :border (if active? "1px solid #3b82f6" "1px solid var(--border-color-dark, #d1d5db)") |
243 | 302 | :border-radius "6px" |
244 | 303 | :cursor "pointer" |
245 | 304 | :font-size "13px" |
|
257 | 316 | (def temp-display-style |
258 | 317 | {:font-size "72px" |
259 | 318 | :font-weight "300" |
260 | | - :color "#1a1a1a" |
| 319 | + :color "var(--text-primary, #1f2937)" |
261 | 320 | :line-height "1" |
262 | 321 | :margin "0"}) |
263 | 322 |
|
264 | 323 | (def condition-text-style |
265 | 324 | {:font-size "18px" |
266 | | - :color "#6b7280" |
| 325 | + :color "var(--text-tertiary, #9ca3af)" |
267 | 326 | :margin "10px 0"}) |
268 | 327 |
|
269 | 328 | (def metrics-quick-grid-style |
|
273 | 332 |
|
274 | 333 | (def metric-item-style |
275 | 334 | {:padding "12px" |
276 | | - :background "#f9fafb" |
| 335 | + :background "var(--card-secondary-bg, #f9fafb)" |
| 336 | + :border "1px solid var(--border-color, #e5e7eb)" |
277 | 337 | :border-radius "8px"}) |
278 | 338 |
|
279 | 339 | (def metric-label-style |
280 | 340 | {:font-size "12px" |
281 | | - :color "#6b7280" |
| 341 | + :color "#9ca3af" |
282 | 342 | :margin-bottom "5px" |
283 | 343 | :text-transform "uppercase" |
284 | 344 | :font-weight "600"}) |
285 | 345 |
|
286 | 346 | (def metric-value-style |
287 | 347 | {:font-size "20px" |
288 | | - :color "#1a1a1a" |
| 348 | + :color "var(--text-primary, #1f2937)" |
289 | 349 | :font-weight "500"}) |
290 | 350 |
|
291 | 351 | (def forecast-mini-grid-style |
|
295 | 355 |
|
296 | 356 | (def forecast-mini-card-style |
297 | 357 | {:padding "15px" |
298 | | - :background "#f9fafb" |
| 358 | + :background "var(--card-secondary-bg, #f9fafb)" |
| 359 | + :border "1px solid var(--border-color, #e5e7eb)" |
299 | 360 | :border-radius "8px" |
300 | 361 | :text-align "center"}) |
301 | 362 |
|
302 | 363 | (def loading-style |
303 | 364 | {:text-align "center" |
304 | 365 | :padding "60px" |
305 | | - :color "#6b7280"}) |
| 366 | + :color "#9ca3af"}) |
306 | 367 |
|
307 | 368 | ;; ============================================================================ |
308 | 369 | ;; Quick Cities |
|
354 | 415 | ^{:key (:name city)} |
355 | 416 | [:button |
356 | 417 | {:style city-button-style |
357 | | - :on-click #(on-city-select city) |
358 | | - :on-mouse-over #(set! (.. % -target -style -background) "#e5e7eb") |
359 | | - :on-mouse-out #(set! (.. % -target -style -background) "#f3f4f6")} |
| 418 | + :on-click #(on-city-select city)} |
360 | 419 | (:name city)])]]) |
361 | 420 |
|
362 | 421 | (defn settings-bar [{:keys [unit on-unit-change]}] |
|
371 | 430 | :on-click #(on-unit-change u)} |
372 | 431 | u])]] |
373 | 432 |
|
374 | | - [:div {:style {:font-size "13px" :color "#6b7280"}} |
| 433 | + [:div {:style {:font-size "13px" :color "#9ca3af"}} |
375 | 434 | "🔄 Auto-refresh: Off"]]) |
376 | 435 |
|
377 | 436 | (defn tabs-navigation [{:keys [active-tab on-tab-change]}] |
|
432 | 491 | (get-weather-icon (:shortForecast period))] |
433 | 492 | [:div {:style {:font-size "24px" :font-weight "600" :color "#3b82f6"}} |
434 | 493 | (format-temp (:temperature period) unit)] |
435 | | - [:div {:style {:font-size "13px" :color "#6b7280" :margin-top "8px"}} |
| 494 | + [:div {:style {:font-size "13px" :color "#9ca3af" :margin-top "8px"}} |
436 | 495 | (:shortForecast period)]])])) |
437 | 496 |
|
438 | 497 | (defn hourly-view [{:keys [hourly unit]}] |
|
459 | 518 | [:div {:style {:font-size "64px"}} "✅"] |
460 | 519 | [:div {:style {:font-size "18px" :font-weight "500" :margin-top "15px"}} |
461 | 520 | "No Active Weather Alerts"] |
462 | | - [:div {:style {:font-size "14px" :color "#6b7280" :margin-top "8px"}} |
| 521 | + [:div {:style {:font-size "14px" :color "#9ca3af" :margin-top "8px"}} |
463 | 522 | "All clear! No weather warnings or advisories."]] |
464 | 523 |
|
465 | 524 | [:div |
|
552 | 611 | (fetch-all-weather))] |
553 | 612 |
|
554 | 613 | (fn [] |
555 | | - [:div {:style app-container-style} |
556 | | - [:div {:style header-card-style} |
557 | | - [:h1 {:style app-title-style} |
558 | | - [:span "☀️"] [:span "Weather Dashboard"]] |
559 | | - (when @location-name |
560 | | - [:div {:style location-header-style} |
561 | | - "📍 " @location-name]) |
562 | | - (when @last-updated |
563 | | - [:div {:style last-updated-style} |
564 | | - "Last updated: " (.toLocaleTimeString @last-updated)])] |
565 | | - |
566 | | - [location-search-panel |
567 | | - {:lat @lat |
568 | | - :lon @lon |
569 | | - :on-lat-change #(reset! lat %) |
570 | | - :on-lon-change #(reset! lon %) |
571 | | - :on-fetch fetch-all-weather |
572 | | - :cities quick-cities |
573 | | - :on-city-select select-city}] |
574 | | - |
575 | | - (cond |
576 | | - @loading? [loading-spinner] |
577 | | - @weather-data [weather-dashboard |
578 | | - {:weather-data @weather-data |
579 | | - :location-name @location-name |
580 | | - :unit @unit |
581 | | - :on-unit-change #(reset! unit %)}])]))) |
| 614 | + [:div |
| 615 | + [theme-styles] |
| 616 | + [:div {:style app-container-style} |
| 617 | + [:div {:style header-card-style} |
| 618 | + [:h1 {:style app-title-style} |
| 619 | + [:span "☀️"] [:span "Weather Dashboard"]] |
| 620 | + (when @location-name |
| 621 | + [:div {:style location-header-style} |
| 622 | + "📍 " @location-name]) |
| 623 | + (when @last-updated |
| 624 | + [:div {:style last-updated-style} |
| 625 | + "Last updated: " (.toLocaleTimeString @last-updated)])] |
| 626 | + |
| 627 | + [location-search-panel |
| 628 | + {:lat @lat |
| 629 | + :lon @lon |
| 630 | + :on-lat-change #(reset! lat %) |
| 631 | + :on-lon-change #(reset! lon %) |
| 632 | + :on-fetch fetch-all-weather |
| 633 | + :cities quick-cities |
| 634 | + :on-city-select select-city}] |
| 635 | + |
| 636 | + (cond |
| 637 | + @loading? [loading-spinner] |
| 638 | + @weather-data [weather-dashboard |
| 639 | + {:weather-data @weather-data |
| 640 | + :location-name @location-name |
| 641 | + :unit @unit |
| 642 | + :on-unit-change #(reset! unit %)}])]]))) |
582 | 643 |
|
583 | 644 | ;; ============================================================================ |
584 | 645 | ;; Mount |
|
0 commit comments