4sp-dv 1.0.24 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  <title>4SP - VERSION 5 CLIENT</title>
7
7
  <link rel="icon" type="image/png" href="https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo.png">
8
8
 
9
- <base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.0.22/logged-in/">
9
+ <base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.0.25/logged-in/">
10
10
  <script src="https://cdn.tailwindcss.com"></script>
11
11
  <link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
12
12
 
@@ -774,7 +774,7 @@
774
774
  const displayUsername = document.getElementById('display-username');
775
775
  const displayEmail = document.getElementById('display-email');
776
776
 
777
- const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.22/logged-in/';
777
+ const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.25/logged-in/';
778
778
  const STORAGE_KEY = 'local_access_code';
779
779
  const USER_DATA_KEY = 'local_user_data';
780
780
  const LAST_PAGE_KEY = 'local_last_page';
@@ -349,7 +349,13 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
349
349
  function updateUrlHash(word) {
350
350
  const newHash = word ? `#word=${encodeURIComponent(word.toLowerCase())}` : '';
351
351
  if (window.location.hash === newHash) return;
352
- history.pushState(null, '', newHash);
352
+ try {
353
+ history.pushState(null, '', newHash);
354
+ } catch (e) {
355
+ // Ignore SecurityError when running via file:// protocol
356
+ // or fallback to basic hash assignment if needed
357
+ window.location.hash = newHash;
358
+ }
353
359
  }
354
360
 
355
361
  function checkUrlHashForWord() {
@@ -11,134 +11,175 @@
11
11
  <link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
12
12
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
13
13
 
14
- <!-- FontAwesome 6.5.2 (Free CDN) -->
15
14
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
15
+
16
16
  <script src="https://cdn.tailwindcss.com"></script>
17
17
  <script src="../navigation.js"></script>
18
18
  <script src="../injector.js"></script>
19
19
 
20
20
  <style>
21
- /* --- 4SP BASE STYLING --- */
21
+ /* --- 4SP BASE STYLING (MATCHING notes.html) --- */
22
22
  :root {
23
+ --bg-page: #040404;
24
+ --bg-container: #000000;
25
+ --border-color: #333;
26
+ --text-primary: #c0c0c0;
27
+ --color-indigo: #4f46e5;
23
28
  --menu-bg: #000000;
24
29
  --menu-border: #333;
25
- --menu-text: #d1d5db;
26
- --menu-item-hover-bg: #2a2a2a;
27
- --accent-bg: rgba(79, 70, 229, 0.1);
28
- --accent-border: #4f46e5;
29
- --accent-text: #6366f1;
30
30
  }
31
31
 
32
32
  body {
33
33
  font-family: 'Geist', sans-serif;
34
- background-color: #040404;
35
- color: #c0c0c0;
34
+ background-color: var(--bg-page);
35
+ color: var(--text-primary);
36
36
  transition: all 0.3s ease;
37
37
  font-size: 16px;
38
38
  overflow-x: hidden;
39
39
  font-weight: 300;
40
+ min-height: 100vh;
41
+ display: flex;
42
+ flex-direction: column;
40
43
  }
41
44
 
42
45
  h1, h2, h3, .font-bold, .font-semibold, strong {
43
- font-weight: 400 !important; /* Maintain the thin Geist look */
46
+ font-weight: 400 !important;
44
47
  }
45
48
 
46
- /* --- BUTTONS & CONTROLS --- */
47
- .btn-toolbar {
48
- background: var(--menu-bg);
49
- border: 1px solid var(--menu-border);
49
+ /* --- UI COMPONENTS (MATCHING notes.html) --- */
50
+ .btn-toolbar-style {
51
+ background: var(--menu-bg, #000000);
52
+ border: 1px solid var(--menu-border, #333);
50
53
  border-radius: 0.75rem;
51
- color: var(--menu-text);
54
+ color: #d1d5db;
52
55
  padding: 0.5rem 1rem;
53
56
  font-weight: 500;
54
57
  cursor: pointer;
55
58
  transition: all 0.2s;
56
59
  display: inline-flex;
57
60
  align-items: center;
61
+ justify-content: center;
58
62
  gap: 0.5rem;
59
- font-size: 0.9rem;
60
63
  }
61
- .btn-toolbar:hover {
62
- background-color: var(--menu-item-hover-bg);
63
- border-color: #555;
64
- color: #fff;
64
+
65
+ .btn-toolbar-style:hover {
66
+ background-color: #000000;
67
+ border-color: #fff;
68
+ color: #ffffff;
69
+ }
70
+
71
+ .btn-toolbar-style.btn-primary-override {
72
+ background-color: rgba(79, 70, 229, 0.1);
73
+ border: 1px solid #4f46e5;
74
+ color: #4f46e5;
65
75
  }
66
- .btn-toolbar.active {
67
- background-color: var(--accent-bg);
68
- border-color: var(--accent-border);
69
- color: var(--accent-text);
76
+ .btn-toolbar-style.btn-primary-override:hover {
77
+ background-color: rgba(79, 70, 229, 0.15);
78
+ border-color: #6366f1;
79
+ color: #6366f1;
70
80
  }
71
81
 
72
- /* --- SEARCH & DROPDOWN --- */
73
- .search-input {
74
- background: var(--menu-bg);
75
- border: 1px solid var(--menu-border);
76
- border-radius: 0.75rem;
77
- color: #fff;
78
- padding: 0.5rem 1rem;
79
- padding-right: 2.5rem; /* Space for icon */
80
- font-weight: 400;
81
- width: 260px;
82
+ /* Input Styling */
83
+ .input-text-style {
84
+ width: 100%;
85
+ padding: 0.6rem 1rem;
86
+ border: 1px solid #333;
87
+ background-color: #000000;
88
+ border-radius: 0.75rem;
89
+ color: #c0c0c0;
82
90
  transition: all 0.2s;
91
+ font-size: 0.95rem;
92
+ font-weight: 300;
93
+ }
94
+ .input-text-style:focus {
95
+ border-color: #505050;
83
96
  outline: none;
97
+ box-shadow: none;
98
+ }
99
+
100
+ /* --- LAYOUT UTILS --- */
101
+ #page-content {
102
+ flex-grow: 1;
103
+ display: flex;
104
+ flex-direction: column;
105
+ }
106
+
107
+ header {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 1rem;
111
+ padding: 1.5rem;
112
+ border-bottom: 1px solid #1a1a1a;
113
+ background-color: var(--bg-page);
84
114
  }
85
- .search-input:focus {
86
- border-color: var(--accent-border);
87
- box-shadow: 0 0 0 1px var(--accent-border);
115
+ @media(min-width: 768px) {
116
+ header {
117
+ flex-direction: row;
118
+ justify-content: space-between;
119
+ align-items: center;
120
+ }
121
+ }
122
+
123
+ /* --- SEARCH DROPDOWN --- */
124
+ .search-container {
125
+ position: relative;
126
+ width: 100%;
127
+ max-width: 350px;
88
128
  }
89
129
 
90
130
  .search-dropdown {
91
131
  position: absolute;
92
- top: 100%;
132
+ top: calc(100% + 0.5rem);
93
133
  left: 0;
94
134
  right: 0;
95
- margin-top: 0.5rem;
96
- background: #0d0d0d;
135
+ background: #000000;
97
136
  border: 1px solid #333;
98
- border-radius: 0.75rem;
137
+ border-radius: 1rem;
99
138
  box-shadow: 0 10px 30px rgba(0,0,0,0.5);
100
139
  z-index: 100;
101
140
  max-height: 300px;
102
141
  overflow-y: auto;
103
142
  display: none;
143
+ padding: 0.5rem;
104
144
  }
105
145
  .search-dropdown.active {
106
146
  display: block;
147
+ animation: fadeIn 0.2s ease-out;
107
148
  }
108
-
149
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } }
150
+
109
151
  .search-item {
110
- padding: 0.75rem 1rem;
152
+ padding: 0.6rem 0.8rem;
111
153
  cursor: pointer;
112
- border-bottom: 1px solid #1a1a1a;
154
+ border-radius: 0.5rem;
113
155
  display: flex;
114
156
  flex-direction: column;
115
157
  gap: 2px;
116
158
  transition: background 0.2s;
159
+ border-bottom: 1px solid transparent;
117
160
  }
118
- .search-item:last-child { border-bottom: none; }
119
- .search-item:hover { background: #1a1a1a; }
120
- .search-item-main { color: #fff; font-size: 0.95rem; }
121
- .search-item-sub { color: #707070; font-size: 0.8rem; }
161
+ .search-item:hover { background: #1a1a1a; color: #fff; }
162
+ .search-item-main { font-size: 0.95rem; color: #e5e7eb; }
163
+ .search-item-sub { color: #6b7280; font-size: 0.8rem; }
122
164
 
123
- .geo-btn {
165
+ .geo-btn-absolute {
124
166
  position: absolute;
125
167
  right: 0.75rem;
126
168
  top: 50%;
127
169
  transform: translateY(-50%);
128
- color: #707070;
170
+ color: #6b7280;
129
171
  cursor: pointer;
130
172
  transition: color 0.2s;
131
173
  background: none;
132
174
  border: none;
133
175
  }
134
- .geo-btn:hover { color: var(--accent-text); }
135
- .geo-btn:disabled { opacity: 0.5; cursor: not-allowed; }
176
+ .geo-btn-absolute:hover { color: #4f46e5; }
136
177
 
137
178
  /* --- WEATHER CARDS --- */
138
179
  .weather-card {
139
180
  background-color: #0d0d0d;
140
181
  border: 1px solid #1a1a1a;
141
- border-radius: 1rem;
182
+ border-radius: 1.5rem;
142
183
  padding: 1.5rem;
143
184
  transition: border-color 0.2s;
144
185
  }
@@ -149,42 +190,53 @@
149
190
  /* Hero Section */
150
191
  .current-temp {
151
192
  font-family: 'Roboto', sans-serif;
152
- font-size: 5rem;
193
+ font-size: 4rem;
153
194
  font-weight: 300;
154
195
  color: #fff;
155
196
  line-height: 1;
156
197
  }
157
198
  .current-icon {
158
- font-size: 4rem;
159
- color: var(--accent-text);
199
+ font-size: 3rem;
200
+ color: var(--color-indigo);
160
201
  }
161
202
 
162
- /* Hourly Scroll */
203
+ /* Hourly Scroll with Fading Edges */
204
+ .scroll-mask-container {
205
+ position: relative;
206
+ width: 100%;
207
+ /* CSS Mask for fading edges */
208
+ -webkit-mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%);
209
+ mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%);
210
+ }
211
+
163
212
  .hourly-container {
164
213
  display: flex;
165
214
  overflow-x: auto;
166
- gap: 1rem;
167
- padding-bottom: 1rem;
168
- margin-top: 1rem;
169
- scrollbar-width: thin;
170
- scrollbar-color: #333 #0d0d0d;
215
+ gap: 0.75rem;
216
+ padding: 1rem 1.5rem; /* Extra padding for the mask to not cut off content immediately */
217
+ margin-top: 0.5rem;
218
+ scrollbar-width: none; /* Hide scrollbar Firefox */
219
+ -ms-overflow-style: none; /* Hide scrollbar IE/Edge */
171
220
  }
221
+ .hourly-container::-webkit-scrollbar { display: none; } /* Hide scrollbar Chrome */
222
+
172
223
  .hourly-item {
173
224
  min-width: 80px;
174
225
  background: #111;
175
226
  border: 1px solid #222;
176
- border-radius: 0.75rem;
227
+ border-radius: 1rem;
177
228
  padding: 1rem 0.5rem;
178
229
  display: flex;
179
230
  flex-direction: column;
180
231
  align-items: center;
181
232
  gap: 0.5rem;
182
- cursor: pointer; /* Added cursor */
183
- transition: background 0.2s;
233
+ cursor: pointer;
234
+ transition: all 0.2s;
184
235
  }
185
236
  .hourly-item:hover {
186
237
  background: #1a1a1a;
187
238
  border-color: #333;
239
+ transform: translateY(-2px);
188
240
  }
189
241
  .hourly-time { font-size: 0.8rem; color: #707070; }
190
242
  .hourly-temp { font-weight: 500; color: #fff; }
@@ -194,9 +246,12 @@
194
246
  display: flex;
195
247
  align-items: center;
196
248
  justify-content: space-between;
197
- padding: 1rem 0;
249
+ padding: 1rem 0.5rem;
198
250
  border-bottom: 1px solid #1a1a1a;
251
+ transition: background 0.2s;
252
+ border-radius: 1rem;
199
253
  }
254
+ .daily-row:hover { background-color: #111; }
200
255
  .daily-row:last-child { border-bottom: none; }
201
256
  .daily-day { width: 100px; font-weight: 500; color: #fff; }
202
257
  .daily-icon { width: 40px; text-align: center; color: #9ca3af; }
@@ -217,21 +272,12 @@
217
272
  gap: 1rem;
218
273
  transition: opacity 0.5s;
219
274
  }
220
- .spinner {
221
- width: 40px;
222
- height: 40px;
223
- border: 3px solid #333;
224
- border-top-color: var(--accent-border);
225
- border-radius: 50%;
226
- animation: spin 1s linear infinite;
227
- }
228
- @keyframes spin { to { transform: rotate(360deg); } }
229
275
 
230
276
  /* --- MODAL (HOURLY) --- */
231
277
  #hourly-modal-overlay {
232
278
  position: fixed;
233
279
  inset: 0;
234
- background: rgba(0,0,0,0.7);
280
+ background: rgba(0,0,0,0.8);
235
281
  backdrop-filter: blur(5px);
236
282
  z-index: 60;
237
283
  display: flex;
@@ -245,7 +291,7 @@
245
291
  .modal-content {
246
292
  background: #0d0d0d;
247
293
  border: 1px solid #333;
248
- border-radius: 1rem;
294
+ border-radius: 2rem;
249
295
  padding: 2rem;
250
296
  width: 90%;
251
297
  max-width: 400px;
@@ -253,28 +299,22 @@
253
299
  display: flex;
254
300
  flex-direction: column;
255
301
  gap: 1.5rem;
302
+ transform: scale(0.95);
303
+ transition: transform 0.2s;
256
304
  }
257
- .modal-header-text { font-size: 1.5rem; color: #fff; font-weight: 400; }
305
+ #hourly-modal-overlay.active .modal-content { transform: scale(1); }
306
+
307
+ .modal-header-text { font-size: 1.25rem; color: #fff; font-weight: 400; }
258
308
  .detail-row { display: flex; justify-content: space-between; border-bottom: 1px solid #1a1a1a; padding-bottom: 0.5rem; }
259
309
  .detail-label { color: #707070; font-size: 0.9rem; }
260
310
  .detail-value { color: #fff; font-weight: 500; }
261
- .close-modal-btn {
262
- background: var(--menu-bg);
263
- border: 1px solid var(--menu-border);
311
+
312
+ /* Provider Select styling fix */
313
+ select option {
314
+ background: #000;
264
315
  color: #fff;
265
- padding: 0.5rem;
266
- border-radius: 0.5rem;
267
- cursor: pointer;
268
- text-align: center;
269
- margin-top: 0.5rem;
316
+ padding: 10px;
270
317
  }
271
- .close-modal-btn:hover { background: #1a1a1a; }
272
-
273
- /* Scrollbar */
274
- ::-webkit-scrollbar { width: 6px; height: 6px; }
275
- ::-webkit-scrollbar-track { background: #040404; }
276
- ::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
277
- ::-webkit-scrollbar-thumb:hover { background: #555; }
278
318
 
279
319
  /* INJECTED STYLES */
280
320
  :root { --bg-page: #040404; --bg-container: #000000; --border-color: #333; --text-primary: #c0c0c0; --text-light-grey: #d1d5db; --accent-color: #6366f1; --navbar-bg: #000000; --navbar-border: #1f2937; --tab-text: #9ca3af; --tab-hover-text: #ffffff; --tab-active-text: #4f46e5; --tab-active-bg: rgba(79, 70, 229, 0.1); --tab-active-border: #4f46e5; }
@@ -320,15 +360,13 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
320
360
  </style>
321
361
 
322
362
  </head>
323
- <body class="min-h-screen flex flex-col">
363
+ <body class="min-h-screen">
324
364
 
325
- <!-- LOADING OVERLAY -->
326
365
  <div id="loading-overlay">
327
- <div class="spinner"></div>
366
+ <i class="fa-solid fa-spinner fa-spin fa-2x text-white mb-4"></i>
328
367
  <div id="loading-text" class="text-sm tracking-widest uppercase text-gray-500">Locating...</div>
329
368
  </div>
330
369
 
331
- <!-- HOURLY DETAIL MODAL -->
332
370
  <div id="hourly-modal-overlay">
333
371
  <div class="modal-content">
334
372
  <div class="flex items-center gap-4">
@@ -338,54 +376,46 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
338
376
  <div id="modal-desc" class="text-gray-400 text-sm"></div>
339
377
  </div>
340
378
  </div>
341
- <div id="modal-details-list" class="flex flex-col gap-3">
342
- <!-- Injected Details -->
343
- </div>
344
- <button class="close-modal-btn" onclick="closeHourlyModal()">Close</button>
379
+ <div id="modal-details-list" class="flex flex-col gap-3 pt-2">
380
+ </div>
381
+ <button class="btn-toolbar-style w-full mt-4" onclick="closeHourlyModal()">Close</button>
345
382
  </div>
346
383
  </div>
347
384
 
348
- <!-- HEADER -->
349
- <header class="flex flex-col md:flex-row justify-between items-center p-6 border-b border-[#1a1a1a] gap-4">
350
- <div class="w-full md:w-auto">
385
+ <header>
386
+ <div>
351
387
  <h1 class="text-2xl font-bold text-white tracking-wide">4SP WEATHER</h1>
352
388
  <div id="location-display" class="text-xs text-[#707070] flex items-center gap-2 mt-1">
353
- <i class="fa-regular fa-location-dot"></i> <span id="loc-name">Detecting...</span>
389
+ <span id="loc-name">Detecting...</span>
354
390
  </div>
355
391
  </div>
356
392
 
357
- <div class="flex items-center gap-4 w-full md:w-auto justify-end">
358
- <!-- Search / Location Control -->
359
- <div class="relative w-full md:w-auto">
360
- <input type="text" id="loc-search-input" placeholder="Change Location..." class="search-input w-full md:w-[260px]">
361
- <button id="btn-geo" class="geo-btn" title="Use Current Location">
393
+ <div class="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
394
+ <div class="search-container">
395
+ <input type="text" id="loc-search-input" placeholder="Change Location..." class="input-text-style pr-10">
396
+ <button id="btn-geo" class="geo-btn-absolute" title="Use Current Location">
362
397
  <i class="fa-solid fa-location-crosshairs"></i>
363
398
  </button>
364
399
 
365
- <!-- Results Dropdown -->
366
400
  <div id="search-results" class="search-dropdown">
367
- <!-- Injected via JS -->
368
- </div>
401
+ </div>
369
402
  </div>
370
403
 
371
- <!-- API Provider Select -->
372
- <div class="relative">
373
- <select id="provider-select" class="btn-toolbar appearance-none pr-8 outline-none focus:border-indigo-500">
404
+ <div class="relative w-full md:w-auto">
405
+ <select id="provider-select" class="btn-toolbar-style w-full appearance-none pr-8 outline-none">
374
406
  <option value="nws">🇺🇸 NWS (USA)</option>
375
407
  <option value="open-meteo">🌍 Open-Meteo</option>
376
408
  </select>
377
- <i class="fa-solid fa-chevron-down absolute right-3 top-1/2 -translate-y-1/2 text-xs pointer-events-none text-gray-500"></i>
409
+ <i class="fa-solid fa-chevron-down absolute right-3 top-1/2 -translate-y-1/2 text-xs pointer-events-none text-gray-400"></i>
378
410
  </div>
379
411
  </div>
380
412
  </header>
381
413
 
382
- <!-- MAIN CONTENT -->
383
414
  <main id="main-content" class="flex-1 p-4 md:p-8 max-w-5xl mx-auto w-full flex flex-col gap-6 opacity-0 transition-opacity duration-500">
384
415
 
385
- <!-- CURRENT CONDITIONS HERO -->
386
416
  <div class="weather-card flex flex-col md:flex-row items-center justify-between gap-8">
387
- <div class="flex flex-col items-center md:items-start">
388
- <div class="text-sm text-[#4f46e5] uppercase tracking-wider font-bold mb-2">Current Conditions</div>
417
+ <div class="flex flex-col items-center md:items-start text-center md:text-left">
418
+ <div class="text-xs text-[#4f46e5] uppercase tracking-widest font-bold mb-2">Current Conditions</div>
389
419
  <div class="flex items-center gap-6">
390
420
  <div id="curr-icon" class="current-icon"><i class="fa-solid fa-spinner fa-spin"></i></div>
391
421
  <div>
@@ -395,40 +425,38 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
395
425
  </div>
396
426
  </div>
397
427
 
398
- <div class="grid grid-cols-2 gap-x-12 gap-y-4 text-right">
399
- <div>
400
- <div class="text-xs text-[#707070] uppercase">High</div>
428
+ <div class="grid grid-cols-2 gap-x-12 gap-y-6 text-right w-full md:w-auto">
429
+ <div class="flex flex-col">
430
+ <div class="text-[10px] text-[#707070] uppercase tracking-wider mb-1">High</div>
401
431
  <div id="curr-high" class="text-2xl text-white font-medium">--°</div>
402
432
  </div>
403
- <div>
404
- <div class="text-xs text-[#707070] uppercase">Low</div>
433
+ <div class="flex flex-col">
434
+ <div class="text-[10px] text-[#707070] uppercase tracking-wider mb-1">Low</div>
405
435
  <div id="curr-low" class="text-2xl text-gray-400 font-medium">--°</div>
406
436
  </div>
407
- <div>
408
- <div class="text-xs text-[#707070] uppercase">Wind</div>
437
+ <div class="flex flex-col">
438
+ <div class="text-[10px] text-[#707070] uppercase tracking-wider mb-1">Wind</div>
409
439
  <div id="curr-wind" class="text-lg text-gray-300">--</div>
410
440
  </div>
411
- <div>
412
- <div class="text-xs text-[#707070] uppercase">Humidity</div>
441
+ <div class="flex flex-col">
442
+ <div class="text-[10px] text-[#707070] uppercase tracking-wider mb-1">Humidity</div>
413
443
  <div id="curr-hum" class="text-lg text-gray-300">--%</div>
414
444
  </div>
415
445
  </div>
416
446
  </div>
417
447
 
418
- <!-- HOURLY FORECAST -->
419
- <div class="weather-card">
420
- <div class="text-sm text-gray-500 uppercase tracking-wider mb-4">Hourly Forecast</div>
421
- <div id="hourly-container" class="hourly-container">
422
- <!-- Injected via JS -->
448
+ <div class="weather-card overflow-hidden">
449
+ <div class="text-xs text-gray-500 uppercase tracking-widest mb-2 px-2">Hourly Forecast</div>
450
+ <div class="scroll-mask-container">
451
+ <div id="hourly-container" class="hourly-container">
452
+ </div>
423
453
  </div>
424
454
  </div>
425
455
 
426
- <!-- DAILY FORECAST -->
427
456
  <div class="weather-card">
428
- <div class="text-sm text-gray-500 uppercase tracking-wider mb-2">7-Day Forecast</div>
457
+ <div class="text-xs text-gray-500 uppercase tracking-widest mb-4">7-Day Forecast</div>
429
458
  <div id="daily-container">
430
- <!-- Injected via JS -->
431
- </div>
459
+ </div>
432
460
  </div>
433
461
 
434
462
  </main>
@@ -442,7 +470,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
442
470
  region: null,
443
471
  provider: 'nws',
444
472
  data: null,
445
- locationAllowed: false // To track if we can use current location
473
+ locationAllowed: false
446
474
  };
447
475
 
448
476
  // --- DOM ELEMENTS ---
@@ -477,10 +505,9 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
477
505
  const modalIconCont = document.getElementById('modal-icon-container');
478
506
  const modalList = document.getElementById('modal-details-list');
479
507
 
480
- // --- ICONS MAPPING (UPDATED FOR FA 6 FREE) ---
508
+ // --- ICONS MAPPING ---
481
509
  function getIconFromWMO(code, isDay = 1) {
482
510
  const day = isDay === 1;
483
- // Mapped to FA 6 Free Icons
484
511
  const map = {
485
512
  0: day ? 'fa-sun' : 'fa-moon',
486
513
  1: day ? 'fa-cloud-sun' : 'fa-cloud-moon',
@@ -498,7 +525,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
498
525
 
499
526
  function getIconFromNWS(shortForecast, isDay) {
500
527
  const lower = shortForecast.toLowerCase();
501
- const prefix = 'fa-solid'; // FA 6 Free uses solid mostly
528
+ const prefix = 'fa-solid';
502
529
 
503
530
  if (lower.includes('thunder')) return `${prefix} fa-cloud-bolt`;
504
531
  if (lower.includes('snow') || lower.includes('flurries')) return `${prefix} fa-snowflake`;
@@ -571,7 +598,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
571
598
  updateLocationDisplay();
572
599
  } catch (e) {
573
600
  console.warn("IP location fetch failed:", e);
574
- // Fallback to defaults or handle error state
601
+ // Fallback to defaults (NY)
575
602
  STATE.lat = 40.7128;
576
603
  STATE.lon = -74.0060;
577
604
  STATE.city = "New York";
@@ -589,6 +616,10 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
589
616
  searchResults.classList.remove('active');
590
617
  return;
591
618
  }
619
+ // Show loading state in dropdown
620
+ searchResults.innerHTML = '<div class="search-item text-gray-500"><i class="fa-solid fa-spinner fa-spin mr-2"></i> Searching...</div>';
621
+ searchResults.classList.add('active');
622
+
592
623
  debounceTimer = setTimeout(() => fetchSearchResults(query), 500);
593
624
  });
594
625
 
@@ -611,12 +642,10 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
611
642
  }
612
643
 
613
644
  try {
614
- // Request more results to filter locally
615
- const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=20&addressdetails=1${viewbox}`;
645
+ const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=10&addressdetails=1${viewbox}`;
616
646
  const res = await fetch(url);
617
647
  const data = await res.json();
618
648
 
619
- // Filter for only Places (cities, towns, villages) or Administrative boundaries
620
649
  const filtered = data.filter(item => {
621
650
  const type = item.type;
622
651
  const category = item.class;
@@ -625,13 +654,13 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
625
654
 
626
655
  if (!validClasses.includes(category)) return false;
627
656
  if (category === 'boundary' && type !== 'administrative') return false;
628
-
629
657
  return true;
630
658
  });
631
659
 
632
- renderSearchResults(filtered.slice(0, 5)); // Show top 5 matches
660
+ renderSearchResults(filtered.slice(0, 5));
633
661
  } catch (e) {
634
662
  console.error("Search failed", e);
663
+ searchResults.innerHTML = '<div class="search-item text-red-500">Error searching.</div>';
635
664
  }
636
665
  }
637
666
 
@@ -739,7 +768,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
739
768
  }
740
769
  }
741
770
 
742
- // --- NWS (API.WEATHER.GOV) ---
771
+ // --- NWS API ---
743
772
  async function fetchNWS() {
744
773
  loadingText.innerText = "Contacting National Weather Service...";
745
774
  const pointsUrl = `https://api.weather.gov/points/${STATE.lat},${STATE.lon}`;
@@ -767,18 +796,15 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
767
796
 
768
797
  function processNWSData(dailyRaw, hourlyRaw) {
769
798
  const current = hourlyRaw.properties.periods[0];
770
-
771
799
  const periodGroups = {};
772
800
 
773
801
  dailyRaw.properties.periods.forEach(p => {
774
802
  const date = p.startTime.split('T')[0];
775
803
  if (!periodGroups[date]) {
776
- periodGroups[date] = { temps: [], icons: [], precips: [], winds: [], isDay: [] };
804
+ periodGroups[date] = { temps: [], icons: [], isDay: [] };
777
805
  }
778
806
  periodGroups[date].temps.push(p.temperature);
779
807
  periodGroups[date].icons.push(p.shortForecast);
780
- periodGroups[date].precips.push(p.probabilityOfPrecipitation.value || 0);
781
- periodGroups[date].winds.push(parseInt(p.windSpeed) || 0);
782
808
  periodGroups[date].isDay.push(p.isDaytime);
783
809
  });
784
810
 
@@ -786,7 +812,6 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
786
812
  const group = periodGroups[date];
787
813
  const high = Math.max(...group.temps);
788
814
  const low = Math.min(...group.temps);
789
-
790
815
  let iconDesc = group.icons[0];
791
816
  const dayIdx = group.isDay.indexOf(true);
792
817
  if (dayIdx !== -1) iconDesc = group.icons[dayIdx];
@@ -807,8 +832,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
807
832
  low: dailyList[0]?.low || current.temperature - 10,
808
833
  wind: `${current.windSpeed} ${current.windDirection}`,
809
834
  humidity: current.relativeHumidity.value,
810
- iconClass: getIconFromNWS(current.shortForecast, current.isDaytime),
811
- isNWS: true
835
+ iconClass: getIconFromNWS(current.shortForecast, current.isDaytime)
812
836
  },
813
837
  hourly: hourlyRaw.properties.periods.slice(0, 24).map(h => ({
814
838
  time: new Date(h.startTime).getHours(),
@@ -826,7 +850,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
826
850
  };
827
851
  }
828
852
 
829
- // --- OPEN-METEO ---
853
+ // --- OPEN-METEO API ---
830
854
  async function fetchOpenMeteo() {
831
855
  loadingText.innerText = "Contacting Open-Meteo...";
832
856
  if (!STATE.city) updateLocationDisplay();
@@ -842,7 +866,6 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
842
866
  const curr = data.current;
843
867
  const daily = data.daily;
844
868
  const hourly = data.hourly;
845
-
846
869
  const now = new Date();
847
870
  const currentHour = now.getHours();
848
871
 
@@ -850,13 +873,11 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
850
873
  for(let i = currentHour; i < currentHour + 24; i++) {
851
874
  if (hourly.time[i]) {
852
875
  const d = new Date(hourly.time[i]);
853
- const temp = Math.round(hourly.temperature_2m[i]);
854
876
  const code = hourly.weather_code[i];
855
-
856
877
  hourlyList.push({
857
878
  time: d.getHours(),
858
879
  fullTime: hourly.time[i],
859
- temp: temp,
880
+ temp: Math.round(hourly.temperature_2m[i]),
860
881
  desc: getWMODescription(code),
861
882
  iconClass: getIconFromWMO(code, hourly.is_day[i]),
862
883
  details: {
@@ -888,8 +909,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
888
909
  low: Math.round(daily.temperature_2m_min[0]),
889
910
  wind: `${Math.round(curr.wind_speed_10m)} mph`,
890
911
  humidity: curr.relative_humidity_2m,
891
- iconClass: getIconFromWMO(curr.weather_code, curr.is_day),
892
- isNWS: false
912
+ iconClass: getIconFromWMO(curr.weather_code, curr.is_day)
893
913
  },
894
914
  hourly: hourlyList,
895
915
  daily: dailyList
@@ -936,8 +956,8 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
936
956
  <div class="daily-day">${day.dateName}</div>
937
957
  <div class="daily-icon text-xl"><i class="${day.iconClass}"></i></div>
938
958
  <div class="daily-temps">
939
- <span class="temp-high">${day.high}°</span>
940
- <span class="temp-low">${day.low}°</span>
959
+ <span class="temp-high font-medium">${day.high}°</span>
960
+ <span class="temp-low text-sm">${day.low}°</span>
941
961
  </div>
942
962
  </div>
943
963
  `).join('');
@@ -1037,4 +1057,4 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
1037
1057
  </script>
1038
1058
 
1039
1059
  </body>
1040
- </html>
1060
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4sp-dv",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/v5-4simpleproblems/v5-4simpleproblems-dv#readme",