website-xp-phone 1.5.0
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.
- package/.astro/content-assets.mjs +1 -0
- package/.astro/content-modules.mjs +1 -0
- package/.astro/content.d.ts +199 -0
- package/.astro/data-store.json +1 -0
- package/.astro/settings.json +8 -0
- package/.astro/types.d.ts +1 -0
- package/.devcontainer/devcontainer.json +23 -0
- package/.env.firebase.example +8 -0
- package/.firebaserc +5 -0
- package/.gitattributes +2 -0
- package/.github/copilot-instructions.md +131 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +45 -0
- package/.github/workflows/deploy-admin.yml +48 -0
- package/.github/workflows/static.yml +43 -0
- package/.gitmodules +5 -0
- package/FIREBASE_SETUP.md +69 -0
- package/README.md +63 -0
- package/SECURITY.md +11 -0
- package/admin/Admin.csproj +7 -0
- package/admin/Dockerfile +14 -0
- package/admin/Program.cs +8 -0
- package/deploy-admin-cloud-run.md +229 -0
- package/eslint.config.js +28 -0
- package/firebase.json +5 -0
- package/firestore.rules +29 -0
- package/index.html +52 -0
- package/package.json +48 -0
- package/pagerts_output.json +1 -0
- package/public/5.html +967 -0
- package/public/BAHNSCHRIFT.TTF +0 -0
- package/public/Beep.ogg +0 -0
- package/public/Clippy.png +0 -0
- package/public/Layered Network Security Model for Home Networks (slides).pdf +0 -0
- package/public/Layered Network Security Model for Home Networks.pdf +0 -0
- package/public/TODO.pdf +0 -0
- package/public/WoW_Config.zip +3 -0
- package/public/addons/energy-swing.txt +1 -0
- package/public/addons/lego-yoda-death-readme.txt +11 -0
- package/public/addons/lego-yoda-death.mp3 +0 -0
- package/public/addons/mana-blast.txt +1 -0
- package/public/addons/rage-volley.txt +1 -0
- package/public/addons/rueg-cell.txt +1 -0
- package/public/addons/rueg-elvui-profile.txt +1 -0
- package/public/addons/rueg-grid2.txt +214 -0
- package/public/addons/rueg-plater-smol.txt +1 -0
- package/public/addons/rueg-plater.txt +1 -0
- package/public/addons/rueg-wa-druid.txt +1 -0
- package/public/addons/rueg-wa-priest.txt +1 -0
- package/public/addons/rueg-wa-rogue.txt +1 -0
- package/public/addons/rueg-wa-shaman.txt +1 -0
- package/public/addons/rueg-wa-warrior.txt +1 -0
- package/public/addons/spirit-smash.txt +1 -0
- package/public/avatar.jpg +0 -0
- package/public/avatar.png +0 -0
- package/public/crunchy_kick.ogg +0 -0
- package/public/documents/resume.html +312 -0
- package/public/favicon.ico +0 -0
- package/public/images/Ateric1.png +0 -0
- package/public/images/Ateric2.png +0 -0
- package/public/images/equal1.png +0 -0
- package/public/images/hyperawareofwhatacatis.png +0 -0
- package/public/images/kogg1.png +0 -0
- package/public/images/kogg2.png +0 -0
- package/public/images/rueg1.png +0 -0
- package/public/images/rueg2.png +0 -0
- package/public/incorrect_responses.txt +126 -0
- package/public/loading.css +51 -0
- package/public/resume.pdf +0 -0
- package/public/robots.txt +9 -0
- package/public/soundcloud.json +57 -0
- package/public/spinner.svg +12 -0
- package/public/tada.wav +0 -0
- package/public/yooh.mp3 +0 -0
- package/render.yaml +5 -0
- package/scripts/ensure-blog-worktree.mjs +24 -0
- package/scripts/generate-soundcloud-json.mjs +198 -0
- package/scripts/git-worktree-helper.mjs +122 -0
- package/scripts/hoist-dev-blog-local.mjs +149 -0
- package/scripts/music-schema.mjs +56 -0
- package/scripts/publish-soundcloud-json.mjs +32 -0
- package/scripts/sync-music-links-from-worktree.mjs +32 -0
- package/src/App.tsx +1500 -0
- package/src/addons.json +76 -0
- package/src/components/Addon.tsx +223 -0
- package/src/components/BlogContent.tsx +103 -0
- package/src/components/CopyToClipboardButton.tsx +21 -0
- package/src/components/MenuBar.tsx +151 -0
- package/src/components/MenuBarWithContext.tsx +6 -0
- package/src/components/Modal.tsx +17 -0
- package/src/components/MusicContent.tsx +309 -0
- package/src/components/NavBarController.tsx +55 -0
- package/src/components/NavBarControllerWrapper.tsx +13 -0
- package/src/components/Page.tsx +56 -0
- package/src/components/SitemapContent.tsx +125 -0
- package/src/contacts.json +32 -0
- package/src/env.d.ts +13 -0
- package/src/lib/assistantStateMachine.ts +80 -0
- package/src/lib/audioOverlap.ts +99 -0
- package/src/lib/keyboardInputUtils.ts +182 -0
- package/src/lib/musicSchema.ts +85 -0
- package/src/lib/naggingAssistantClient.ts +241 -0
- package/src/lib/resumeAnalytics.ts +163 -0
- package/src/main.tsx +35 -0
- package/src/pages.json +50 -0
- package/src/sections.json +243 -0
- package/src/src+addons.zip +3 -0
- package/src/styles/main.css +465 -0
- package/src/utils/blogSecurity.ts +87 -0
- package/src/utils/menuItems.ts +33 -0
- package/src/windowing/MinimizedSections.tsx +86 -0
- package/src/windowing/Section.tsx +586 -0
- package/src/windowing/context.tsx +13 -0
- package/src/windowing/hooks.ts +10 -0
- package/src/windowing/index.ts +7 -0
- package/src/windowing/provider.tsx +74 -0
- package/src/windowing/server.ts +3 -0
- package/src/windowing/types.ts +33 -0
- package/src/windowing/utils.ts +135 -0
- package/tests/generate-soundcloud-json.test.mjs +63 -0
- package/tests/music-schema.test.mjs +53 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +304 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
@import "../../public/loading.css";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
font-family: "Courier New", Courier, monospace;
|
|
5
|
+
font-synthesis: none;
|
|
6
|
+
text-rendering: optimizeLegibility;
|
|
7
|
+
-webkit-font-smoothing: antialiased;
|
|
8
|
+
-moz-osx-font-smoothing: grayscale;
|
|
9
|
+
--background-color: #000;
|
|
10
|
+
--border-size: 0.25em;
|
|
11
|
+
--gap-size: 0.5em;
|
|
12
|
+
--menu-bar-height: 24px;
|
|
13
|
+
font-size: 16px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@font-face {
|
|
17
|
+
font-family: "bahnscrift";
|
|
18
|
+
src: url("/BAHNSCHRIFT.TTF");
|
|
19
|
+
font-weight: normal;
|
|
20
|
+
font-style: normal;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Reset XP.css window styling overrides */
|
|
24
|
+
.window {
|
|
25
|
+
margin-bottom: var(--gap-size);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.window:last-child {
|
|
29
|
+
margin-bottom: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Add extra spacing for nested windows */
|
|
33
|
+
.window-body .window {
|
|
34
|
+
margin-bottom: 0.5em;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.window-body .window:last-child {
|
|
38
|
+
margin-bottom: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Spacing for windows inside lists (addon pages) */
|
|
42
|
+
.window-body ul li .window,
|
|
43
|
+
.window-body ul .window,
|
|
44
|
+
.window-body .addon-wrapper {
|
|
45
|
+
margin-bottom: 0.5em;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.window-body ul li:last-child .window,
|
|
49
|
+
.window-body ul .window:last-child,
|
|
50
|
+
.window-body .addon-wrapper:last-child {
|
|
51
|
+
margin-bottom: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Hide border on first-level windows to avoid double border */
|
|
55
|
+
.page>.window,
|
|
56
|
+
main>.page {
|
|
57
|
+
border: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Force title bar text to dark gray for readability */
|
|
61
|
+
.title-bar-text,
|
|
62
|
+
.title-bar-text a {
|
|
63
|
+
color: #eee !important;
|
|
64
|
+
font-size: 0.85rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Add more vertical padding to title bars */
|
|
68
|
+
.title-bar {
|
|
69
|
+
padding-top: 0.25em !important;
|
|
70
|
+
padding-bottom: 0.25em !important;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
body {
|
|
74
|
+
margin-bottom: 0;
|
|
75
|
+
margin-top: 0;
|
|
76
|
+
min-height: 100vh;
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
overflow-x: hidden;
|
|
80
|
+
overflow-x: clip;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#root {
|
|
84
|
+
min-height: 100%;
|
|
85
|
+
display: flex;
|
|
86
|
+
flex: 1;
|
|
87
|
+
flex-direction: column;
|
|
88
|
+
overflow-x: hidden;
|
|
89
|
+
overflow-x: clip;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main {
|
|
93
|
+
flex: 1;
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
justify-content: center;
|
|
97
|
+
min-height: 0;
|
|
98
|
+
overflow-x: hidden;
|
|
99
|
+
overflow-x: clip;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
body,
|
|
103
|
+
h1,
|
|
104
|
+
h2,
|
|
105
|
+
h3,
|
|
106
|
+
h4,
|
|
107
|
+
h5,
|
|
108
|
+
h6,
|
|
109
|
+
p,
|
|
110
|
+
span,
|
|
111
|
+
label,
|
|
112
|
+
input,
|
|
113
|
+
textarea,
|
|
114
|
+
button,
|
|
115
|
+
form,
|
|
116
|
+
div,
|
|
117
|
+
a,
|
|
118
|
+
li {
|
|
119
|
+
color: var(--text-color) !important;
|
|
120
|
+
opacity: 1 !important;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.page>ul,
|
|
124
|
+
li {
|
|
125
|
+
padding: 0;
|
|
126
|
+
margin: 0;
|
|
127
|
+
list-style: none;
|
|
128
|
+
margin-bottom: var(--gap-size);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.page {
|
|
132
|
+
margin: 0 auto;
|
|
133
|
+
display: grid;
|
|
134
|
+
grid-template-columns: 1fr;
|
|
135
|
+
gap: var(--gap-size);
|
|
136
|
+
border: 0;
|
|
137
|
+
position: relative;
|
|
138
|
+
z-index: 100;
|
|
139
|
+
width: calc(100% - (var(--gap-size) * 2));
|
|
140
|
+
max-width: calc(100% - (var(--gap-size) * 2));
|
|
141
|
+
box-sizing: border-box;
|
|
142
|
+
justify-items: center;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.page > .window {
|
|
146
|
+
width: fit-content;
|
|
147
|
+
max-width: 100%;
|
|
148
|
+
box-sizing: border-box;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.menu-item {
|
|
152
|
+
margin: 0;
|
|
153
|
+
padding: calc(var(--gap-size) / 2) var(--gap-size);
|
|
154
|
+
background-color: beige;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ul>section>h1,
|
|
158
|
+
ul>section>h4,
|
|
159
|
+
form#form>label {
|
|
160
|
+
margin: 0 calc(var(--gap-size) * 1.75);
|
|
161
|
+
font-family: bahnscrift, system-ui, Helvetica, Arial, sans-serif;
|
|
162
|
+
line-height: 1.5;
|
|
163
|
+
font-weight: 400;
|
|
164
|
+
font-size: 0.9rem;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.window-body ul {
|
|
168
|
+
margin: 0;
|
|
169
|
+
padding-left: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.window-body ul li {
|
|
173
|
+
padding-left: 1.5em;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.window-body {
|
|
177
|
+
min-width: 0;
|
|
178
|
+
max-width: 100%;
|
|
179
|
+
overflow-wrap: anywhere;
|
|
180
|
+
word-break: break-word;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.debug-printout-scroll {
|
|
184
|
+
width: 100%;
|
|
185
|
+
max-width: 100%;
|
|
186
|
+
overflow-x: auto;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.debug-printout-scroll pre,
|
|
190
|
+
.debug-printout-scroll code {
|
|
191
|
+
max-width: 100%;
|
|
192
|
+
box-sizing: border-box;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.debug-printout-scroll pre {
|
|
196
|
+
overflow-x: auto;
|
|
197
|
+
white-space: pre-wrap;
|
|
198
|
+
overflow-wrap: anywhere;
|
|
199
|
+
word-break: break-word;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.window-body pre {
|
|
203
|
+
white-space: pre-wrap;
|
|
204
|
+
overflow-wrap: anywhere;
|
|
205
|
+
word-break: break-word;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* xp.css sets very large heading sizes; keep markdown headings readable in content windows. */
|
|
209
|
+
.window-body h1 {
|
|
210
|
+
font-size: 1.2rem !important;
|
|
211
|
+
line-height: 1.25;
|
|
212
|
+
margin: 0.25em 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.window-body h2 {
|
|
216
|
+
text-align: right;
|
|
217
|
+
color: var(--button-shadow) !important;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.window-body>ul:first-child {
|
|
221
|
+
margin-top: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.music-track-title {
|
|
225
|
+
font-size: 1.08em;
|
|
226
|
+
font-weight: 600;
|
|
227
|
+
margin: 0 0 0.35em 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Menu Bar - Windows XP style */
|
|
231
|
+
.menu-bar {
|
|
232
|
+
position: fixed;
|
|
233
|
+
top: 0;
|
|
234
|
+
left: 0;
|
|
235
|
+
right: 0;
|
|
236
|
+
z-index: 1000;
|
|
237
|
+
background: linear-gradient(to bottom, #f0f0f0 0%, #e0e0e0 100%);
|
|
238
|
+
border-bottom: 1px solid #808080;
|
|
239
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
240
|
+
padding: 0;
|
|
241
|
+
margin: 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.menu-bar menu {
|
|
245
|
+
display: flex;
|
|
246
|
+
list-style: none;
|
|
247
|
+
margin: 0;
|
|
248
|
+
padding: 0;
|
|
249
|
+
gap: 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.menu-bar menu li {
|
|
253
|
+
margin: 0;
|
|
254
|
+
padding: 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.menu-bar menu a {
|
|
258
|
+
display: block;
|
|
259
|
+
padding: 4px 8px;
|
|
260
|
+
text-decoration: none;
|
|
261
|
+
color: #000 !important;
|
|
262
|
+
font-size: 13px;
|
|
263
|
+
font-family: "Tahoma", "MS Sans Serif", sans-serif;
|
|
264
|
+
white-space: nowrap;
|
|
265
|
+
transition: background-color 0.1s ease;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.menu-bar menu a:hover {
|
|
269
|
+
background-color: rgba(51, 153, 255, 0.2);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.menu-bar menu a:active {
|
|
273
|
+
background-color: rgba(51, 153, 255, 0.4);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.menu-bar .menu-underline {
|
|
277
|
+
text-decoration: underline;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* Minimized Sections Menu */
|
|
281
|
+
.minimized-sections-item {
|
|
282
|
+
margin-left: auto;
|
|
283
|
+
position: relative;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* Minimized sections button container */
|
|
287
|
+
.minimized-menu-container {
|
|
288
|
+
display: flex;
|
|
289
|
+
justify-content: center;
|
|
290
|
+
margin: 20px auto;
|
|
291
|
+
width: 100%;
|
|
292
|
+
max-width: 600px;
|
|
293
|
+
padding: 0 var(--gap-size);
|
|
294
|
+
position: relative;
|
|
295
|
+
box-sizing: border-box;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.minimized-menu-button {
|
|
299
|
+
min-width: 200px;
|
|
300
|
+
padding: 8px 24px !important;
|
|
301
|
+
font-size: 13px !important;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.minimized-menu {
|
|
305
|
+
position: relative;
|
|
306
|
+
display: inline-block;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.minimized-menu .menu-button {
|
|
310
|
+
display: block;
|
|
311
|
+
padding: 4px 8px;
|
|
312
|
+
text-decoration: none;
|
|
313
|
+
color: #000 !important;
|
|
314
|
+
font-size: 13px;
|
|
315
|
+
font-family: "Tahoma", "MS Sans Serif", sans-serif;
|
|
316
|
+
white-space: nowrap;
|
|
317
|
+
background: transparent;
|
|
318
|
+
border: none;
|
|
319
|
+
cursor: pointer;
|
|
320
|
+
transition: background-color 0.1s ease;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.minimized-menu .menu-button:hover {
|
|
324
|
+
background-color: rgba(51, 153, 255, 0.2);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.minimized-menu .menu-button:active {
|
|
328
|
+
background-color: rgba(51, 153, 255, 0.4);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.minimized-menu-container .dropdown-menu {
|
|
332
|
+
position: absolute;
|
|
333
|
+
top: 100%;
|
|
334
|
+
left: 50%;
|
|
335
|
+
transform: translateX(-50%);
|
|
336
|
+
background: #fff;
|
|
337
|
+
border: 2px outset #dfdfdf;
|
|
338
|
+
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
|
339
|
+
min-width: 250px;
|
|
340
|
+
max-height: 400px;
|
|
341
|
+
overflow-y: auto;
|
|
342
|
+
z-index: 1001;
|
|
343
|
+
padding: 2px;
|
|
344
|
+
display: flex;
|
|
345
|
+
flex-direction: column;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.minimized-menu-container .dropdown-item {
|
|
349
|
+
display: block;
|
|
350
|
+
width: 100%;
|
|
351
|
+
padding: 6px 12px;
|
|
352
|
+
text-align: left;
|
|
353
|
+
background: transparent;
|
|
354
|
+
border: none;
|
|
355
|
+
color: #000 !important;
|
|
356
|
+
font-size: 13px;
|
|
357
|
+
font-family: "Tahoma", "MS Sans Serif", sans-serif;
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
white-space: nowrap;
|
|
360
|
+
overflow: hidden;
|
|
361
|
+
text-overflow: ellipsis;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.minimized-menu-container .dropdown-item:hover {
|
|
365
|
+
background-color: #0a246a;
|
|
366
|
+
color: #fff !important;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* Account for fixed menu bar */
|
|
370
|
+
body {
|
|
371
|
+
padding-top: var(--menu-bar-height);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
@media (prefers-color-scheme: light) {
|
|
375
|
+
:root {
|
|
376
|
+
--background-color: #fefefe;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
@media screen and (min-width: 769px) {
|
|
381
|
+
.page {
|
|
382
|
+
min-width: min(600px, calc(100% - (var(--gap-size) * 2)));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
@media screen and (max-width: 768px) {
|
|
387
|
+
:root {
|
|
388
|
+
--gap-size: 0.5em;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.page {
|
|
392
|
+
margin: 0 auto;
|
|
393
|
+
width: 100%;
|
|
394
|
+
max-width: 100%;
|
|
395
|
+
padding: var(--gap-size);
|
|
396
|
+
justify-items: stretch;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.page > .window {
|
|
400
|
+
width: 100%;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* Constrain window width on mobile */
|
|
404
|
+
.window {
|
|
405
|
+
max-width: 100%;
|
|
406
|
+
box-sizing: border-box;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.window-body {
|
|
410
|
+
max-width: 100%;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* Mobile-friendly menu bar */
|
|
414
|
+
.menu-bar {
|
|
415
|
+
overflow-x: auto;
|
|
416
|
+
overflow-y: hidden;
|
|
417
|
+
-webkit-overflow-scrolling: touch;
|
|
418
|
+
scrollbar-width: thin;
|
|
419
|
+
scrollbar-color: #888 #f0f0f0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* Hide scrollbar for cleaner look but keep functionality */
|
|
423
|
+
.menu-bar::-webkit-scrollbar {
|
|
424
|
+
height: 3px;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.menu-bar::-webkit-scrollbar-track {
|
|
428
|
+
background: #f0f0f0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.menu-bar::-webkit-scrollbar-thumb {
|
|
432
|
+
background: #888;
|
|
433
|
+
border-radius: 2px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.menu-bar menu {
|
|
437
|
+
flex-wrap: nowrap;
|
|
438
|
+
justify-content: flex-start;
|
|
439
|
+
min-width: min-content;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.menu-bar menu a {
|
|
443
|
+
padding: 6px 12px;
|
|
444
|
+
font-size: 12px;
|
|
445
|
+
white-space: nowrap;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* Increase touch target size for better mobile usability */
|
|
449
|
+
.menu-bar menu li {
|
|
450
|
+
display: flex;
|
|
451
|
+
align-items: center;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
@media screen and (max-width: 480px) {
|
|
456
|
+
:root {
|
|
457
|
+
--menu-bar-height: 28px;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* Extra small screens - even more compact */
|
|
461
|
+
.menu-bar menu a {
|
|
462
|
+
padding: 5px 10px;
|
|
463
|
+
font-size: 11px;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const DEFAULT_BLOG_POSTS_URL = "https://raw.githubusercontent.com/akinevz2/frontend/refs/heads/blogging/blog/";
|
|
2
|
+
const DEV_BLOG_POSTS_PATH = "/blog-assets/";
|
|
3
|
+
const DEFAULT_ALLOWED_HOSTS = ["raw.githubusercontent.com"];
|
|
4
|
+
|
|
5
|
+
function parseAllowedHosts(allowedHostsEnv?: string): Set<string> {
|
|
6
|
+
const configuredHosts = (allowedHostsEnv || "")
|
|
7
|
+
.split(",")
|
|
8
|
+
.map((value) => value.trim().toLowerCase())
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
|
|
11
|
+
return new Set([...DEFAULT_ALLOWED_HOSTS, ...configuredHosts]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeHostPath(pathname: string): string {
|
|
15
|
+
const withoutFilename = pathname.endsWith(".json")
|
|
16
|
+
? pathname.replace(/[^/]+$/, "")
|
|
17
|
+
: pathname;
|
|
18
|
+
|
|
19
|
+
return withoutFilename.endsWith("/") ? withoutFilename : `${withoutFilename}/`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveTrustedBlogPostsHost(
|
|
23
|
+
configuredUrl?: string,
|
|
24
|
+
allowedHostsEnv?: string,
|
|
25
|
+
): string {
|
|
26
|
+
const source = configuredUrl || DEFAULT_BLOG_POSTS_URL;
|
|
27
|
+
const allowedHosts = parseAllowedHosts(allowedHostsEnv);
|
|
28
|
+
|
|
29
|
+
let parsed: URL;
|
|
30
|
+
try {
|
|
31
|
+
parsed = new URL(source);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(`Invalid blog host URL: '${source}'.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (parsed.protocol !== "https:") {
|
|
37
|
+
throw new Error(`Blog host must use https: '${source}'.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!allowedHosts.has(parsed.hostname.toLowerCase())) {
|
|
41
|
+
throw new Error(`Blog host '${parsed.hostname}' is not in the allowlist.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const normalizedPathname = normalizeHostPath(parsed.pathname);
|
|
45
|
+
return new URL(normalizedPathname, parsed.origin).toString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getSafeBlogPostsHost(
|
|
49
|
+
configuredUrl?: string,
|
|
50
|
+
allowedHostsEnv?: string,
|
|
51
|
+
): string {
|
|
52
|
+
try {
|
|
53
|
+
return resolveTrustedBlogPostsHost(configuredUrl, allowedHostsEnv);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
56
|
+
console.warn(`Using default blog host due to unsafe configuration: ${message}`);
|
|
57
|
+
return DEFAULT_BLOG_POSTS_URL;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveTrustedBlogAssetUrl(
|
|
62
|
+
assetPath: string,
|
|
63
|
+
blogPostsHost: string,
|
|
64
|
+
): string {
|
|
65
|
+
if (/^https?:\/\//i.test(assetPath)) {
|
|
66
|
+
throw new Error(`Absolute URLs are not allowed for blog assets: '${assetPath}'.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const normalizedAssetPath = assetPath.replace(/^\/+/, "");
|
|
70
|
+
return new URL(normalizedAssetPath, blogPostsHost).toString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getRuntimeBlogPostsHost(
|
|
74
|
+
isDev: boolean,
|
|
75
|
+
origin?: string,
|
|
76
|
+
): string {
|
|
77
|
+
if (isDev) {
|
|
78
|
+
if (!origin) {
|
|
79
|
+
return DEV_BLOG_POSTS_PATH;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return new URL(DEV_BLOG_POSTS_PATH, origin).toString();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Production is intentionally pinned to raw GitHub user content CDN.
|
|
86
|
+
return DEFAULT_BLOG_POSTS_URL;
|
|
87
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility to build and sort menu items from page metadata.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface MenuItem {
|
|
6
|
+
label: string;
|
|
7
|
+
href: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function generateMenuItems(
|
|
11
|
+
pages: Array<{ url?: string; label?: string }>,
|
|
12
|
+
additionalLinks: MenuItem[] = [],
|
|
13
|
+
): MenuItem[] {
|
|
14
|
+
const pageItems: MenuItem[] = pages
|
|
15
|
+
.map((page) => {
|
|
16
|
+
if (!page.url) return null;
|
|
17
|
+
|
|
18
|
+
const normalizedUrl = page.url === "/index" ? "/" : page.url;
|
|
19
|
+
const fallbackLabel =
|
|
20
|
+
normalizedUrl === "/"
|
|
21
|
+
? "home"
|
|
22
|
+
: normalizedUrl.replace(/^\//, "").replace(/\/$/, "").replace(/-/g, " ");
|
|
23
|
+
const label = page.label ?? fallbackLabel;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
label,
|
|
27
|
+
href: normalizedUrl,
|
|
28
|
+
};
|
|
29
|
+
})
|
|
30
|
+
.filter((item): item is NonNullable<typeof item> => item !== null);
|
|
31
|
+
|
|
32
|
+
return [...pageItems, ...additionalLinks];
|
|
33
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { useSectionContext } from './hooks';
|
|
3
|
+
|
|
4
|
+
export const MinimizedSections: React.FC = () => {
|
|
5
|
+
const { minimizedSections, restoreSection, pageMetadata } = useSectionContext();
|
|
6
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
7
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
8
|
+
|
|
9
|
+
// Close menu when clicking outside
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
12
|
+
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
13
|
+
setIsOpen(false);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
if (isOpen) {
|
|
18
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
19
|
+
}
|
|
20
|
+
return () => {
|
|
21
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
22
|
+
};
|
|
23
|
+
}, [isOpen]);
|
|
24
|
+
|
|
25
|
+
if (minimizedSections.size === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if any root-level sections (depth=0) are minimized
|
|
30
|
+
const hasRootLevelMinimized = Array.from(minimizedSections.keys()).some(uuid => {
|
|
31
|
+
const metadata = pageMetadata.sections.find(s => s.uuid === uuid);
|
|
32
|
+
return metadata?.depth === 0;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const buttonText = hasRootLevelMinimized ? 'unhide all' : 'unhide';
|
|
36
|
+
|
|
37
|
+
const handleButtonClick = () => {
|
|
38
|
+
if (hasRootLevelMinimized) {
|
|
39
|
+
// Restore all minimized sections
|
|
40
|
+
Array.from(minimizedSections.keys()).forEach(uuid => {
|
|
41
|
+
restoreSection(uuid);
|
|
42
|
+
});
|
|
43
|
+
setIsOpen(false);
|
|
44
|
+
} else {
|
|
45
|
+
// Toggle dropdown
|
|
46
|
+
setIsOpen(!isOpen);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="minimized-menu-container" ref={menuRef}>
|
|
52
|
+
<button
|
|
53
|
+
className="menu-button minimized-menu-button"
|
|
54
|
+
style={{
|
|
55
|
+
backgroundColor: minimizedSections.size > 0 ? 'rgba(51, 153, 255, 0.3)' : 'transparent'
|
|
56
|
+
}}
|
|
57
|
+
onClick={handleButtonClick}
|
|
58
|
+
aria-expanded={isOpen}
|
|
59
|
+
>
|
|
60
|
+
<span className="menu-underline">u</span>{buttonText.slice(1)} ({minimizedSections.size})
|
|
61
|
+
</button>
|
|
62
|
+
{isOpen && (
|
|
63
|
+
<div className="dropdown-menu">
|
|
64
|
+
{Array.from(minimizedSections.entries()).map(([uuid, heading]) => {
|
|
65
|
+
// Look up full metadata if available
|
|
66
|
+
const metadata = pageMetadata.sections.find(s => s.uuid === uuid);
|
|
67
|
+
const displayHeading = metadata?.heading || heading;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<button
|
|
71
|
+
key={uuid}
|
|
72
|
+
className="dropdown-item"
|
|
73
|
+
onClick={() => {
|
|
74
|
+
restoreSection(uuid);
|
|
75
|
+
setIsOpen(false);
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{displayHeading}
|
|
79
|
+
</button>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|