4sp-dv 1.0.25 → 1.0.27

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.25/logged-in/">
9
+ <base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.0.27/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.25/logged-in/';
777
+ const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.27/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';
@@ -11,8 +11,8 @@
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>
@@ -20,125 +20,126 @@
20
20
  <style>
21
21
  /* --- 4SP BASE STYLING --- */
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 --- */
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);
52
- padding: 0.5rem 1rem;
54
+ color: #d1d5db;
55
+ padding: 0.6rem 1.2rem;
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;
58
- gap: 0.5rem;
59
- font-size: 0.9rem;
60
- }
61
- .btn-toolbar:hover {
62
- background-color: var(--menu-item-hover-bg);
63
- border-color: #555;
64
- color: #fff;
65
- }
66
- .btn-toolbar.active {
67
- background-color: var(--accent-bg);
68
- border-color: var(--accent-border);
69
- color: var(--accent-text);
61
+ justify-content: center;
62
+ gap: 0.75rem;
63
+ font-size: 0.95rem;
64
+ white-space: nowrap;
70
65
  }
71
-
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
- transition: all 0.2s;
83
- outline: none;
66
+
67
+ .btn-toolbar-style:hover {
68
+ background-color: #000000;
69
+ border-color: #fff;
70
+ color: #ffffff;
71
+ transform: translateY(-1px);
84
72
  }
85
- .search-input:focus {
86
- border-color: var(--accent-border);
87
- box-shadow: 0 0 0 1px var(--accent-border);
73
+
74
+ .provider-select-wrapper {
75
+ position: relative;
76
+ display: inline-block;
77
+ min-width: 180px; /* Minimum width */
88
78
  }
89
79
 
90
- .search-dropdown {
91
- position: absolute;
92
- top: 100%;
93
- left: 0;
94
- right: 0;
95
- margin-top: 0.5rem;
80
+ .provider-select {
81
+ appearance: none;
96
82
  background: #0d0d0d;
97
83
  border: 1px solid #333;
84
+ color: #fff;
85
+ padding: 0.6rem 2.5rem 0.6rem 1rem;
98
86
  border-radius: 0.75rem;
99
- box-shadow: 0 10px 30px rgba(0,0,0,0.5);
100
- z-index: 100;
101
- max-height: 300px;
102
- overflow-y: auto;
103
- display: none;
104
- }
105
- .search-dropdown.active {
106
- display: block;
87
+ font-family: 'Geist', sans-serif;
88
+ font-size: 0.95rem;
89
+ cursor: pointer;
90
+ width: auto; /* Allow width to change based on content */
91
+ min-width: 100%;
92
+ transition: border-color 0.2s;
107
93
  }
108
94
 
109
- .search-item {
110
- padding: 0.75rem 1rem;
111
- cursor: pointer;
112
- border-bottom: 1px solid #1a1a1a;
113
- display: flex;
114
- flex-direction: column;
115
- gap: 2px;
116
- transition: background 0.2s;
95
+ .provider-select:hover {
96
+ border-color: #6366f1;
117
97
  }
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; }
122
98
 
123
- .geo-btn {
99
+ .provider-select:focus {
100
+ outline: none;
101
+ border-color: #6366f1;
102
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
103
+ }
104
+
105
+ .select-icon {
124
106
  position: absolute;
125
- right: 0.75rem;
107
+ right: 1rem;
126
108
  top: 50%;
127
109
  transform: translateY(-50%);
128
- color: #707070;
129
- cursor: pointer;
130
- transition: color 0.2s;
131
- background: none;
132
- border: none;
110
+ pointer-events: none;
111
+ color: #6b7280;
112
+ font-size: 0.75rem;
113
+ }
114
+
115
+ /* --- LAYOUT UTILS --- */
116
+ #page-content {
117
+ flex-grow: 1;
118
+ display: flex;
119
+ flex-direction: column;
120
+ }
121
+
122
+ header {
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: 1.5rem;
126
+ padding: 1.5rem;
127
+ border-bottom: 1px solid #1a1a1a;
128
+ background-color: var(--bg-page);
129
+ }
130
+ @media(min-width: 768px) {
131
+ header {
132
+ flex-direction: row;
133
+ justify-content: space-between;
134
+ align-items: center;
135
+ }
133
136
  }
134
- .geo-btn:hover { color: var(--accent-text); }
135
- .geo-btn:disabled { opacity: 0.5; cursor: not-allowed; }
136
137
 
137
138
  /* --- WEATHER CARDS --- */
138
139
  .weather-card {
139
140
  background-color: #0d0d0d;
140
141
  border: 1px solid #1a1a1a;
141
- border-radius: 1rem;
142
+ border-radius: 1.5rem;
142
143
  padding: 1.5rem;
143
144
  transition: border-color 0.2s;
144
145
  }
@@ -149,42 +150,52 @@
149
150
  /* Hero Section */
150
151
  .current-temp {
151
152
  font-family: 'Roboto', sans-serif;
152
- font-size: 5rem;
153
+ font-size: 4rem;
153
154
  font-weight: 300;
154
155
  color: #fff;
155
156
  line-height: 1;
156
157
  }
157
158
  .current-icon {
158
- font-size: 4rem;
159
- color: var(--accent-text);
159
+ font-size: 3rem;
160
+ color: var(--color-indigo);
160
161
  }
161
162
 
162
163
  /* Hourly Scroll */
164
+ .scroll-mask-container {
165
+ position: relative;
166
+ width: 100%;
167
+ -webkit-mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%);
168
+ mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%);
169
+ }
170
+
163
171
  .hourly-container {
164
172
  display: flex;
165
173
  overflow-x: auto;
166
- gap: 1rem;
167
- padding-bottom: 1rem;
168
- margin-top: 1rem;
169
- scrollbar-width: thin;
170
- scrollbar-color: #333 #0d0d0d;
174
+ gap: 0.75rem;
175
+ padding: 1rem 1.5rem;
176
+ margin-top: 0.5rem;
177
+ scrollbar-width: none;
178
+ -ms-overflow-style: none;
171
179
  }
180
+ .hourly-container::-webkit-scrollbar { display: none; }
181
+
172
182
  .hourly-item {
173
183
  min-width: 80px;
174
184
  background: #111;
175
185
  border: 1px solid #222;
176
- border-radius: 0.75rem;
186
+ border-radius: 1rem;
177
187
  padding: 1rem 0.5rem;
178
188
  display: flex;
179
189
  flex-direction: column;
180
190
  align-items: center;
181
191
  gap: 0.5rem;
182
- cursor: pointer; /* Added cursor */
183
- transition: background 0.2s;
192
+ cursor: pointer;
193
+ transition: all 0.2s;
184
194
  }
185
195
  .hourly-item:hover {
186
196
  background: #1a1a1a;
187
197
  border-color: #333;
198
+ transform: translateY(-2px);
188
199
  }
189
200
  .hourly-time { font-size: 0.8rem; color: #707070; }
190
201
  .hourly-temp { font-weight: 500; color: #fff; }
@@ -194,9 +205,12 @@
194
205
  display: flex;
195
206
  align-items: center;
196
207
  justify-content: space-between;
197
- padding: 1rem 0;
208
+ padding: 1rem 0.5rem;
198
209
  border-bottom: 1px solid #1a1a1a;
210
+ transition: background 0.2s;
211
+ border-radius: 1rem;
199
212
  }
213
+ .daily-row:hover { background-color: #111; }
200
214
  .daily-row:last-child { border-bottom: none; }
201
215
  .daily-day { width: 100px; font-weight: 500; color: #fff; }
202
216
  .daily-icon { width: 40px; text-align: center; color: #9ca3af; }
@@ -217,21 +231,12 @@
217
231
  gap: 1rem;
218
232
  transition: opacity 0.5s;
219
233
  }
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
234
 
230
235
  /* --- MODAL (HOURLY) --- */
231
236
  #hourly-modal-overlay {
232
237
  position: fixed;
233
238
  inset: 0;
234
- background: rgba(0,0,0,0.7);
239
+ background: rgba(0,0,0,0.8);
235
240
  backdrop-filter: blur(5px);
236
241
  z-index: 60;
237
242
  display: flex;
@@ -245,7 +250,7 @@
245
250
  .modal-content {
246
251
  background: #0d0d0d;
247
252
  border: 1px solid #333;
248
- border-radius: 1rem;
253
+ border-radius: 2rem;
249
254
  padding: 2rem;
250
255
  width: 90%;
251
256
  max-width: 400px;
@@ -253,82 +258,35 @@
253
258
  display: flex;
254
259
  flex-direction: column;
255
260
  gap: 1.5rem;
261
+ transform: scale(0.95);
262
+ transition: transform 0.2s;
256
263
  }
257
- .modal-header-text { font-size: 1.5rem; color: #fff; font-weight: 400; }
264
+ #hourly-modal-overlay.active .modal-content { transform: scale(1); }
265
+
266
+ .modal-header-text { font-size: 1.25rem; color: #fff; font-weight: 400; }
258
267
  .detail-row { display: flex; justify-content: space-between; border-bottom: 1px solid #1a1a1a; padding-bottom: 0.5rem; }
259
268
  .detail-label { color: #707070; font-size: 0.9rem; }
260
269
  .detail-value { color: #fff; font-weight: 500; }
261
- .close-modal-btn {
262
- background: var(--menu-bg);
263
- border: 1px solid var(--menu-border);
264
- color: #fff;
265
- padding: 0.5rem;
266
- border-radius: 0.5rem;
267
- cursor: pointer;
268
- text-align: center;
269
- margin-top: 0.5rem;
270
- }
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
-
279
- /* INJECTED STYLES */
280
- :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; }
281
- @keyframes shake { 0% { transform: translateX(0); } 25% { transform: translateX(-5px) rotate(-5deg); } 50% { transform: translateX(5px) rotate(5deg); } 75% { transform: translateX(-5px) rotate(-5deg); } 100% { transform: translateX(0); } }
282
- .shake-anim { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
283
- body.no-animations * { transition: none !important; animation: none !important; transform: none !important; }
284
- body:not(.no-animations) .btn-toolbar-style, body:not(.no-animations) .btn-lock-screen, body:not(.no-animations) .icon-btn, body:not(.no-animations) .nav-tab, body:not(.no-animations) .btn-card-action, body:not(.no-animations) .input-text-style, body:not(.no-animations) .input-lock-screen, body:not(.no-animations) .input-key-style { transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; }
285
- body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .btn-lock-screen:active, body:not(.no-animations) .icon-btn:active, body:not(.no-animations) .nav-tab:active, body:not(.no-animations) .btn-card-action:active { transform: scale(0.96); transition: transform 0.05s ease-out !important; }
286
- .btn-toolbar-style { background: #0a0a0a; border: 1px solid #333; border-radius: 14px; color: #d1d5db; padding: 0.75rem 1.25rem; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6); }
287
- .btn-toolbar-style:hover { transform: scale(1.05) translateY(-2px); box-shadow: 0 6px 20px rgba(255, 255, 255, 0.1); border-color: #fff; color: #ffffff; }
288
- .btn-toolbar-style.btn-primary-override { background-color: rgba(79, 70, 229, 0.1); border: 1px solid #4f46e5; color: #4f46e5; }
289
- .btn-toolbar-style.btn-primary-override:hover { background-color: rgba(79, 70, 229, 0.2); border-color: #6366f1; color: #6366f1; box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4); }
290
- .btn-toolbar-style.btn-primary-override-danger { background-color: rgba(220, 38, 38, 0.1); border: 1px solid #dc2626; color: #dc2626; }
291
- .btn-toolbar-style.btn-primary-override-danger:hover { background-color: rgba(220, 38, 38, 0.2); border-color: #ef4444; color: #ef4444; box-shadow: 0 6px 20px rgba(220, 38, 38, 0.4); }
292
- .btn-lock-screen { padding: 0.75rem 1.25rem; font-size: 0.875rem; font-weight: 500; border-radius: 14px; color: rgb(99 102 241); background-color: rgba(99, 102, 241, 0.1); border: 1px solid rgb(99 102 241); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6); }
293
- .btn-lock-screen:hover { transform: scale(1.05) translateY(-2px); box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4); background-color: rgba(99, 102, 241, 0.2); }
294
- .icon-btn { width: 40px; height: 40px; border-radius: 14px; border: 1px solid #4b5563; display: flex; align-items: center; justify-content: center; color: #d1d5db; cursor: pointer; background: transparent; }
295
- .icon-btn:hover { background-color: #374151; color: white; transform: scale(1.1); }
296
- .btn-card-action { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; border-radius: 14px; background-color: transparent; border: 1px solid transparent; color: #9ca3af; cursor: pointer; }
297
- .btn-card-action:hover { background-color: rgba(79, 70, 229, 0.1); color: #4f46e5; border-color: #4f46e5; transform: scale(1.15); }
298
- .input-text-style, .input-select-style { width: 100%; padding: 0.75rem; border: 1px solid #252525; background-color: #111111; border-radius: 14px; color: #c0c0c0; }
299
- .input-text-style:focus, .input-select-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.02); }
300
- .input-lock-screen { background-color: #111; border: 1px solid #252525; color: white; border-radius: 14px; padding: 0.75rem; text-align: center; letter-spacing: 0.2em; outline: none; }
301
- .input-lock-screen:focus { border-color: rgb(99 102 241); transform: scale(1.02); }
302
- .input-key-style { width: 4rem; padding: 0.75rem; border: 1px solid #252525; background-color: #111111; border-radius: 14px; color: #c0c0c0; font-size: 1.1rem; text-align: center; font-weight: 500; text-transform: uppercase; }
303
- .input-key-style:focus { border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5); transform: scale(1.1); }
304
- .settings-box { border: 1px solid #333; border-radius: 24px; background-color: #000000; padding: 1.5rem; }
305
- .nav-tab { padding: 0.5rem 1rem; color: var(--tab-text); font-size: 0.875rem; font-weight: 400; border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; border: 1px solid transparent; cursor: pointer; background: transparent; }
306
- .nav-tab:hover { color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05); border-color: #4f46e5; transform: scale(1.05); }
307
- .nav-tab.active { color: var(--tab-active-text); border-color: var(--tab-active-border); background-color: var(--tab-active-bg); transform: scale(1.05); }
308
- .eagler-dropdown { position: relative; background-color: rgba(20, 20, 20, 0.98); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 0.5rem; min-width: 200px; display: inline-block; }
309
- .eagler-dropdown-link { display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none; border-radius: 14px; font-size: 0.9rem; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
310
- .eagler-dropdown-link:hover { background-color: rgba(255, 255, 255, 0.1); color: white; transform: translateX(4px); }
311
- #notification-container { position: fixed; bottom: 2rem; right: 2rem; display: flex; flex-direction: column; gap: 0.75rem; z-index: 1000; pointer-events: none; }
312
- .notification-toast { background-color: #0a0a0a; border: 1px solid #333; border-radius: 14px; padding: 0.75rem 1.25rem; color: #fff; box-shadow: 0 4px 15px rgba(0,0,0,0.5); display: flex; align-items: center; gap: 0.75rem; font-size: 0.9rem; min-width: 200px; transform: translateX(120%); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s ease, background-color 0.2s; opacity: 0; pointer-events: auto; cursor: default; }
313
- .notification-toast.show { transform: translateX(0); opacity: 1; }
314
- .notification-toast.show:hover { transform: scale(1.05) translateX(-5px); background-color: #151515; border-color: #555; box-shadow: 0 8px 25px rgba(0,0,0,0.7); }
315
- .notification-icon { font-size: 1.1rem; }
316
- .notification-icon.success { color: #4ade80; }
317
- .notification-icon.info { color: #60a5fa; }
318
- .notification-icon.warning { color: #fbbf24; }
319
-
320
- </style>
270
+
271
+ /* Option Styling (Limited support) */
272
+ option { background-color: #000; color: #fff; padding: 10px; }
273
+
274
+ /* INJECTED STYLES (Animations) */
275
+ :root { --bg-page: #040404; --bg-container: #000000; --border-color: #333; --text-primary: #c0c0c0; --text-light-grey: #d1d5db; --accent-color: #6366f1; }
276
+ @keyframes shake { 0% { transform: translateX(0); } 25% { transform: translateX(-5px) rotate(-5deg); } 50% { transform: translateX(5px) rotate(5deg); } 75% { transform: translateX(-5px) rotate(-5deg); } 100% { transform: translateX(0); } }
277
+ .shake-anim { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
278
+ body:not(.no-animations) .btn-toolbar-style, body:not(.no-animations) .hourly-item, body:not(.no-animations) .daily-row { transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
279
+ body:not(.no-animations) .btn-toolbar-style:active { transform: scale(0.96); }
280
+ </style>
321
281
 
322
282
  </head>
323
- <body class="min-h-screen flex flex-col">
283
+ <body class="min-h-screen">
324
284
 
325
- <!-- LOADING OVERLAY -->
326
285
  <div id="loading-overlay">
327
- <div class="spinner"></div>
286
+ <i class="fa-solid fa-spinner fa-spin fa-2x text-white mb-4"></i>
328
287
  <div id="loading-text" class="text-sm tracking-widest uppercase text-gray-500">Locating...</div>
329
288
  </div>
330
289
 
331
- <!-- HOURLY DETAIL MODAL -->
332
290
  <div id="hourly-modal-overlay">
333
291
  <div class="modal-content">
334
292
  <div class="flex items-center gap-4">
@@ -338,54 +296,41 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
338
296
  <div id="modal-desc" class="text-gray-400 text-sm"></div>
339
297
  </div>
340
298
  </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>
299
+ <div id="modal-details-list" class="flex flex-col gap-3 pt-2">
300
+ </div>
301
+ <button class="btn-toolbar-style w-full mt-4 justify-center" onclick="closeHourlyModal()">Close</button>
345
302
  </div>
346
303
  </div>
347
304
 
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">
305
+ <header>
306
+ <div>
351
307
  <h1 class="text-2xl font-bold text-white tracking-wide">4SP WEATHER</h1>
352
308
  <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>
309
+ <span id="loc-name">Detecting...</span>
354
310
  </div>
355
311
  </div>
356
312
 
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">
362
- <i class="fa-solid fa-location-crosshairs"></i>
363
- </button>
364
-
365
- <!-- Results Dropdown -->
366
- <div id="search-results" class="search-dropdown">
367
- <!-- Injected via JS -->
368
- </div>
369
- </div>
370
-
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">
374
- <option value="nws">🇺🇸 NWS (USA)</option>
375
- <option value="open-meteo">🌍 Open-Meteo</option>
313
+ <div class="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
314
+ <button id="btn-geo" class="btn-toolbar-style w-full md:w-auto">
315
+ <i class="fa-solid fa-location-crosshairs text-[#6366f1]"></i>
316
+ <span>Use Current Location</span>
317
+ </button>
318
+
319
+ <div class="provider-select-wrapper w-full md:w-auto">
320
+ <select id="provider-select" class="provider-select">
321
+ <option value="nws">🇺🇸 NWS (Official)</option>
322
+ <option value="open-meteo">🌍 Open-Meteo (Global)</option>
376
323
  </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>
324
+ <i class="fa-solid fa-chevron-down select-icon"></i>
378
325
  </div>
379
326
  </div>
380
327
  </header>
381
328
 
382
- <!-- MAIN CONTENT -->
383
329
  <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
330
 
385
- <!-- CURRENT CONDITIONS HERO -->
386
331
  <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>
332
+ <div class="flex flex-col items-center md:items-start text-center md:text-left">
333
+ <div class="text-xs text-[#4f46e5] uppercase tracking-widest font-bold mb-2">Current Conditions</div>
389
334
  <div class="flex items-center gap-6">
390
335
  <div id="curr-icon" class="current-icon"><i class="fa-solid fa-spinner fa-spin"></i></div>
391
336
  <div>
@@ -395,40 +340,38 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
395
340
  </div>
396
341
  </div>
397
342
 
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>
343
+ <div class="grid grid-cols-2 gap-x-12 gap-y-6 text-right w-full md:w-auto">
344
+ <div class="flex flex-col">
345
+ <div class="text-[10px] text-[#707070] uppercase tracking-wider mb-1">High</div>
401
346
  <div id="curr-high" class="text-2xl text-white font-medium">--°</div>
402
347
  </div>
403
- <div>
404
- <div class="text-xs text-[#707070] uppercase">Low</div>
348
+ <div class="flex flex-col">
349
+ <div class="text-[10px] text-[#707070] uppercase tracking-wider mb-1">Low</div>
405
350
  <div id="curr-low" class="text-2xl text-gray-400 font-medium">--°</div>
406
351
  </div>
407
- <div>
408
- <div class="text-xs text-[#707070] uppercase">Wind</div>
352
+ <div class="flex flex-col">
353
+ <div class="text-[10px] text-[#707070] uppercase tracking-wider mb-1">Wind</div>
409
354
  <div id="curr-wind" class="text-lg text-gray-300">--</div>
410
355
  </div>
411
- <div>
412
- <div class="text-xs text-[#707070] uppercase">Humidity</div>
356
+ <div class="flex flex-col">
357
+ <div class="text-[10px] text-[#707070] uppercase tracking-wider mb-1">Humidity</div>
413
358
  <div id="curr-hum" class="text-lg text-gray-300">--%</div>
414
359
  </div>
415
360
  </div>
416
361
  </div>
417
362
 
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 -->
363
+ <div class="weather-card overflow-hidden">
364
+ <div class="text-xs text-gray-500 uppercase tracking-widest mb-2 px-2">Hourly Forecast</div>
365
+ <div class="scroll-mask-container">
366
+ <div id="hourly-container" class="hourly-container">
367
+ </div>
423
368
  </div>
424
369
  </div>
425
370
 
426
- <!-- DAILY FORECAST -->
427
371
  <div class="weather-card">
428
- <div class="text-sm text-gray-500 uppercase tracking-wider mb-2">7-Day Forecast</div>
372
+ <div class="text-xs text-gray-500 uppercase tracking-widest mb-4">7-Day Forecast</div>
429
373
  <div id="daily-container">
430
- <!-- Injected via JS -->
431
- </div>
374
+ </div>
432
375
  </div>
433
376
 
434
377
  </main>
@@ -442,7 +385,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
442
385
  region: null,
443
386
  provider: 'nws',
444
387
  data: null,
445
- locationAllowed: false // To track if we can use current location
388
+ locationAllowed: false
446
389
  };
447
390
 
448
391
  // --- DOM ELEMENTS ---
@@ -451,10 +394,6 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
451
394
  const mainContent = document.getElementById('main-content');
452
395
  const locNameEl = document.getElementById('loc-name');
453
396
  const providerSelect = document.getElementById('provider-select');
454
-
455
- // Search Elements
456
- const searchInput = document.getElementById('loc-search-input');
457
- const searchResults = document.getElementById('search-results');
458
397
  const btnGeo = document.getElementById('btn-geo');
459
398
 
460
399
  // Current Els
@@ -477,10 +416,9 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
477
416
  const modalIconCont = document.getElementById('modal-icon-container');
478
417
  const modalList = document.getElementById('modal-details-list');
479
418
 
480
- // --- ICONS MAPPING (UPDATED FOR FA 6 FREE) ---
419
+ // --- ICONS MAPPING ---
481
420
  function getIconFromWMO(code, isDay = 1) {
482
421
  const day = isDay === 1;
483
- // Mapped to FA 6 Free Icons
484
422
  const map = {
485
423
  0: day ? 'fa-sun' : 'fa-moon',
486
424
  1: day ? 'fa-cloud-sun' : 'fa-cloud-moon',
@@ -498,7 +436,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
498
436
 
499
437
  function getIconFromNWS(shortForecast, isDay) {
500
438
  const lower = shortForecast.toLowerCase();
501
- const prefix = 'fa-solid'; // FA 6 Free uses solid mostly
439
+ const prefix = 'fa-solid';
502
440
 
503
441
  if (lower.includes('thunder')) return `${prefix} fa-cloud-bolt`;
504
442
  if (lower.includes('snow') || lower.includes('flurries')) return `${prefix} fa-snowflake`;
@@ -543,7 +481,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
543
481
  loadWeather();
544
482
  } catch (err) {
545
483
  loadingText.innerText = "Location Failed.";
546
- alert("Could not determine location. Please search manually.");
484
+ alert("Could not determine location. Check browser permissions.");
547
485
  loadingOverlay.style.opacity = '0';
548
486
  loadingOverlay.style.pointerEvents = 'none';
549
487
  }
@@ -571,7 +509,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
571
509
  updateLocationDisplay();
572
510
  } catch (e) {
573
511
  console.warn("IP location fetch failed:", e);
574
- // Fallback to defaults or handle error state
512
+ // Fallback to defaults (NY)
575
513
  STATE.lat = 40.7128;
576
514
  STATE.lon = -74.0060;
577
515
  STATE.city = "New York";
@@ -580,129 +518,10 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
580
518
  }
581
519
  }
582
520
 
583
- // --- SEARCH LOGIC ---
584
- let debounceTimer;
585
- searchInput.addEventListener('input', (e) => {
586
- clearTimeout(debounceTimer);
587
- const query = e.target.value.trim();
588
- if(query.length < 3) {
589
- searchResults.classList.remove('active');
590
- return;
591
- }
592
- debounceTimer = setTimeout(() => fetchSearchResults(query), 500);
593
- });
594
-
595
- document.addEventListener('click', (e) => {
596
- if(!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
597
- searchResults.classList.remove('active');
598
- }
599
- });
600
-
601
521
  btnGeo.addEventListener('click', () => {
602
- searchInput.value = '';
603
522
  triggerAutoLocation();
604
523
  });
605
524
 
606
- async function fetchSearchResults(query) {
607
- let viewbox = '';
608
- if (STATE.lat && STATE.lon) {
609
- const b = 1;
610
- viewbox = `&viewbox=${STATE.lon-b},${STATE.lat+b},${STATE.lon+b},${STATE.lat-b}`;
611
- }
612
-
613
- 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}`;
616
- const res = await fetch(url);
617
- const data = await res.json();
618
-
619
- // Filter for only Places (cities, towns, villages) or Administrative boundaries
620
- const filtered = data.filter(item => {
621
- const type = item.type;
622
- const category = item.class;
623
- const validClasses = ['place', 'boundary'];
624
- const validTypes = ['city', 'town', 'village', 'hamlet', 'administrative'];
625
-
626
- if (!validClasses.includes(category)) return false;
627
- if (category === 'boundary' && type !== 'administrative') return false;
628
-
629
- return true;
630
- });
631
-
632
- renderSearchResults(filtered.slice(0, 5)); // Show top 5 matches
633
- } catch (e) {
634
- console.error("Search failed", e);
635
- }
636
- }
637
-
638
- function renderSearchResults(results) {
639
- searchResults.innerHTML = '';
640
-
641
- if (results.length === 0) {
642
- const empty = document.createElement('div');
643
- empty.className = 'search-item text-gray-500 cursor-default';
644
- empty.innerText = 'No places found';
645
- searchResults.appendChild(empty);
646
- } else {
647
- results.forEach(loc => {
648
- const div = document.createElement('div');
649
- div.className = 'search-item';
650
-
651
- const address = loc.address || {};
652
- const city = address.city || address.town || address.village || address.hamlet || loc.name;
653
- const state = address.state || address.country;
654
-
655
- div.innerHTML = `
656
- <div class="search-item-main">${city}</div>
657
- <div class="search-item-sub">${state}</div>
658
- `;
659
-
660
- div.addEventListener('click', () => {
661
- selectLocation(parseFloat(loc.lat), parseFloat(loc.lon), city, state, address.country_code);
662
- });
663
-
664
- searchResults.appendChild(div);
665
- });
666
- }
667
-
668
- // Add "Use Current Location" at the bottom
669
- const currDiv = document.createElement('div');
670
- currDiv.className = 'search-item border-t border-[#333] mt-1 pt-2';
671
- if (STATE.locationAllowed) {
672
- currDiv.innerHTML = `<div class="search-item-main text-[#6366f1]"><i class="fa-solid fa-location-crosshairs mr-2"></i> Use Current Location</div>`;
673
- currDiv.addEventListener('click', () => {
674
- searchInput.value = '';
675
- searchResults.classList.remove('active');
676
- triggerAutoLocation();
677
- });
678
- } else {
679
- currDiv.innerHTML = `<div class="search-item-main text-gray-500 cursor-not-allowed"><i class="fa-solid fa-location-slash mr-2"></i> Current Location Unavailable</div>`;
680
- }
681
- searchResults.appendChild(currDiv);
682
-
683
- searchResults.classList.add('active');
684
- }
685
-
686
- function selectLocation(lat, lon, city, region, countryCode) {
687
- STATE.lat = lat;
688
- STATE.lon = lon;
689
- STATE.city = city;
690
- STATE.region = region;
691
-
692
- searchInput.value = '';
693
- searchResults.classList.remove('active');
694
-
695
- const isUS = (countryCode && countryCode === 'us');
696
-
697
- if (!isUS) {
698
- STATE.provider = 'open-meteo';
699
- providerSelect.value = 'open-meteo';
700
- }
701
-
702
- updateLocationDisplay();
703
- loadWeather();
704
- }
705
-
706
525
  // --- WEATHER FETCHING LOGIC ---
707
526
 
708
527
  async function loadWeather() {
@@ -739,7 +558,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
739
558
  }
740
559
  }
741
560
 
742
- // --- NWS (API.WEATHER.GOV) ---
561
+ // --- NWS API ---
743
562
  async function fetchNWS() {
744
563
  loadingText.innerText = "Contacting National Weather Service...";
745
564
  const pointsUrl = `https://api.weather.gov/points/${STATE.lat},${STATE.lon}`;
@@ -767,18 +586,15 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
767
586
 
768
587
  function processNWSData(dailyRaw, hourlyRaw) {
769
588
  const current = hourlyRaw.properties.periods[0];
770
-
771
589
  const periodGroups = {};
772
590
 
773
591
  dailyRaw.properties.periods.forEach(p => {
774
592
  const date = p.startTime.split('T')[0];
775
593
  if (!periodGroups[date]) {
776
- periodGroups[date] = { temps: [], icons: [], precips: [], winds: [], isDay: [] };
594
+ periodGroups[date] = { temps: [], icons: [], isDay: [] };
777
595
  }
778
596
  periodGroups[date].temps.push(p.temperature);
779
597
  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
598
  periodGroups[date].isDay.push(p.isDaytime);
783
599
  });
784
600
 
@@ -786,7 +602,6 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
786
602
  const group = periodGroups[date];
787
603
  const high = Math.max(...group.temps);
788
604
  const low = Math.min(...group.temps);
789
-
790
605
  let iconDesc = group.icons[0];
791
606
  const dayIdx = group.isDay.indexOf(true);
792
607
  if (dayIdx !== -1) iconDesc = group.icons[dayIdx];
@@ -807,8 +622,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
807
622
  low: dailyList[0]?.low || current.temperature - 10,
808
623
  wind: `${current.windSpeed} ${current.windDirection}`,
809
624
  humidity: current.relativeHumidity.value,
810
- iconClass: getIconFromNWS(current.shortForecast, current.isDaytime),
811
- isNWS: true
625
+ iconClass: getIconFromNWS(current.shortForecast, current.isDaytime)
812
626
  },
813
627
  hourly: hourlyRaw.properties.periods.slice(0, 24).map(h => ({
814
628
  time: new Date(h.startTime).getHours(),
@@ -826,7 +640,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
826
640
  };
827
641
  }
828
642
 
829
- // --- OPEN-METEO ---
643
+ // --- OPEN-METEO API ---
830
644
  async function fetchOpenMeteo() {
831
645
  loadingText.innerText = "Contacting Open-Meteo...";
832
646
  if (!STATE.city) updateLocationDisplay();
@@ -842,7 +656,6 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
842
656
  const curr = data.current;
843
657
  const daily = data.daily;
844
658
  const hourly = data.hourly;
845
-
846
659
  const now = new Date();
847
660
  const currentHour = now.getHours();
848
661
 
@@ -850,13 +663,11 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
850
663
  for(let i = currentHour; i < currentHour + 24; i++) {
851
664
  if (hourly.time[i]) {
852
665
  const d = new Date(hourly.time[i]);
853
- const temp = Math.round(hourly.temperature_2m[i]);
854
666
  const code = hourly.weather_code[i];
855
-
856
667
  hourlyList.push({
857
668
  time: d.getHours(),
858
669
  fullTime: hourly.time[i],
859
- temp: temp,
670
+ temp: Math.round(hourly.temperature_2m[i]),
860
671
  desc: getWMODescription(code),
861
672
  iconClass: getIconFromWMO(code, hourly.is_day[i]),
862
673
  details: {
@@ -888,8 +699,7 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
888
699
  low: Math.round(daily.temperature_2m_min[0]),
889
700
  wind: `${Math.round(curr.wind_speed_10m)} mph`,
890
701
  humidity: curr.relative_humidity_2m,
891
- iconClass: getIconFromWMO(curr.weather_code, curr.is_day),
892
- isNWS: false
702
+ iconClass: getIconFromWMO(curr.weather_code, curr.is_day)
893
703
  },
894
704
  hourly: hourlyList,
895
705
  daily: dailyList
@@ -936,8 +746,8 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
936
746
  <div class="daily-day">${day.dateName}</div>
937
747
  <div class="daily-icon text-xl"><i class="${day.iconClass}"></i></div>
938
748
  <div class="daily-temps">
939
- <span class="temp-high">${day.high}°</span>
940
- <span class="temp-low">${day.low}°</span>
749
+ <span class="temp-high font-medium">${day.high}°</span>
750
+ <span class="temp-low text-sm">${day.low}°</span>
941
751
  </div>
942
752
  </div>
943
753
  `).join('');
@@ -1037,4 +847,4 @@ body:not(.no-animations) .btn-toolbar-style:active, body:not(.no-animations) .bt
1037
847
  </script>
1038
848
 
1039
849
  </body>
1040
- </html>
850
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4sp-dv",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/v5-4simpleproblems/v5-4simpleproblems-dv#readme",