@stnd/layout 0.2.0 → 0.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/README.md ADDED
@@ -0,0 +1,543 @@
1
+ ---
2
+ title: "@stnd/layout"
3
+ aliases: []
4
+ created: 2026-07-05 07:43
5
+ modified: 2026-07-05 19:21
6
+ last_audited: 2026-07-14
7
+ audit_interval_days: 90
8
+ next_audit: 2026-10-12
9
+ audit_priority: 3
10
+ maturity: tree
11
+ mode: read
12
+ publish: false
13
+ status: active
14
+ tags:
15
+ - package
16
+ - stnd
17
+ theme: kernel
18
+ type: package
19
+ visibility: private
20
+ ---
21
+
22
+ # @[stnd](../README)/layout
23
+
24
+ Definitive root layouts and base components for Standard websites.
25
+
26
+ ---
27
+
28
+ ## ELI5
29
+
30
+ The `<html>` wrapper every Standard page shares: the `<head>` metadata, the header/footer, the *“restore theme before paint”* script. Most pages just want `Base.astro`; `Encrypted.astro` is the special case for a password-gated page.
31
+
32
+ ```astro
33
+ ---
34
+ import Base from "@stnd/layout/Base";
35
+ ---
36
+ <Base title="My Page"><h1>Hello</h1></Base>
37
+ ```
38
+
39
+ ---
40
+
41
+ **A caveat on `Encrypted.astro`** — it’s a *casual visitor gate*, not real cryptographic access control: the password check is a SHA-256-derived XOR keystream, and both the ciphertext and the decryption code ship to every visitor’s browser. Predictable HTML structure (`<!DOCTYPE html>` at a known offset) makes a known-plaintext attack on the keystream straightforward. Fine for *“keep casual visitors out”*; not a substitute for real server-side auth if the content actually matters.
42
+
43
+ ## Overview
44
+
45
+ `@stnd/layout` provides the foundational structure for all websites in the Standard ecosystem. It handles essential infrastructure such as meta tags, SEO, favicons, headers, footers, and system-wide initializations.
46
+
47
+ ## Key Layouts
48
+
49
+ ### Base Layout (`Base.astro`)
50
+ The primary layout for most pages. It includes:
51
+ - HTML structure with theme support.
52
+ - Meta tags for SEO and Open Graph.
53
+ - Site-wide Header and Footer.
54
+ - Global styling injection.
55
+ - System initializers (`StndInit.astro`).
56
+
57
+ ### Encrypted Layout (`Encrypted.astro`)
58
+ A specialized layout for password-protected content. It uses client-side encryption to ensure privacy while maintaining a consistent design.
59
+
60
+ ## Components
61
+
62
+ - `Header.astro`: A highly configurable header with support for branding, navigation, and custom actions.
63
+ - `Footer.astro`: A standard footer with links, copyright information, and slot-based extensions.
64
+ - `Meta.astro`: Manages all `<head>` metadata, including social sharing images and Favicons.
65
+ - `StndInit.astro`: Handles client-side system setup, such as theme restoration and e-ink detection.
66
+
67
+ ## Usage
68
+
69
+ In your Astro page:
70
+
71
+ ```astro
72
+ ---
73
+ import Base from "@stnd/layout/Base";
74
+ ---
75
+ <Base title="My Page" description="A well-made page.">
76
+ <h1>Hello Standard!</h1>
77
+ </Base>
78
+ ```
79
+
80
+ ## Configuration
81
+ Layouts inherit site-wide settings from the `config.nav` and `config.title` defined in your module manifests or `astro.config.mjs`.
82
+
83
+ ## Future & Roadmap Layouts
84
+
85
+ These layouts are parked here for future use (Phase 4 of the Standard Roadmap).
86
+
87
+ ### 1. Column Layout (`ColumnLayout.astro` — Astro Port, WIP)
88
+ A multi-column masonry-like grid layout based on `BaseLayout.astro`.
89
+
90
+ <details> <summary>View ColumnLayout.astro Code</summary>
91
+
92
+ ```astro
93
+ ---
94
+ import BaseLayout from "./BaseLayout.astro";
95
+
96
+ export interface Props {
97
+ title: string;
98
+ showTitle?: boolean;
99
+ }
100
+
101
+ const { title, showTitle = true } = Astro.props;
102
+ ---
103
+
104
+ <BaseLayout title={title}>
105
+ <div class="column-layout">
106
+ <hr />
107
+
108
+ {
109
+ showTitle && (
110
+ <header class="title-row">
111
+ <h1>{title}</h1>
112
+ </header>
113
+ )
114
+ }
115
+
116
+ <div class="content-row">
117
+ <div class="content-grid">
118
+ <div class="content-section">
119
+ <slot />
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </BaseLayout>
125
+ ```
126
+
127
+ </details>
128
+
129
+ ### 2. Column Template (`column.njk` — 11ty, Archive)
130
+ The Eleventy-era column layout using a 4-column `r-grid` with text-replacement helpers for styling HTML content blocks.
131
+
132
+ <details> <summary>View column.njk Code</summary>
133
+
134
+ ```njk
135
+ ---
136
+ layout: default
137
+ ---
138
+ {% include "partial/header.njk" %}
139
+
140
+ <r-grid columns=4>
141
+ <hr>
142
+ <r-cell span=row><h1>{{ page.title }}</h1></r-cell>
143
+ <r-cell span=row></r-cell>
144
+ <r-cell span=row flow-cols-s=1 flow-cols=2 flow-cols-l=3>
145
+ <article style="grid-template-columns:none;">
146
+ {% set content = content | replace("<ul>", "<ul class='tight'>") %}
147
+ {% set content = content | replace("<h2", "</article></r-cell><r-cell span=row><h2") %}
148
+ {% set content = content | replace("</h2>", "</h2></r-cell><r-cell span=row flow-cols-s=1 flow-cols=2 flow-cols-l=3><article style='grid-template-columns:none;'>") %}
149
+ {{ content }}
150
+ {% include "partial/backlinks.njk" %}
151
+ </article>
152
+ </r-cell>
153
+ </r-grid>
154
+ {% include "partial/footer.njk" %}
155
+ ```
156
+
157
+ </details>
158
+
159
+ ### 3. Encrypted Template (`encrypted.njk` — 11ty, Archive)
160
+ The Eleventy-era password-protected layout utilizing client-side XOR decryption and a stylized prompt, now succeeded by `Encrypted.astro`.
161
+
162
+ <details> <summary>View encrypted.njk Code</summary>
163
+
164
+ ```html
165
+ <!DOCTYPE html>
166
+ <html lang="fr">
167
+ <head>
168
+ <meta charset="utf-8">
169
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
170
+ <title>Cette page est protégée</title>
171
+ <style>
172
+ :root {
173
+ --primary-color: #d78a5a;
174
+ --error-color: #dc3545;
175
+ --rhythm-block: #ffffff;
176
+ --text-color: #333333;
177
+ --color-border: #ddd;
178
+ --font-family: InterVariable, system-ui, -apple-system, sans-serif;
179
+ }
180
+
181
+ * {
182
+ box-sizing: border-box;
183
+ }
184
+
185
+ body {
186
+ font-family: var(--font-family);
187
+ margin: 0;
188
+ padding: 40px 20px;
189
+ background-color: var(--rhythm-block);
190
+ color: var(--text-color);
191
+ line-height: 1.6;
192
+ min-height: 100vh;
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ }
197
+
198
+ .container {
199
+ max-width: 400px;
200
+ width: 100%;
201
+ padding: 2rem;
202
+ background: white;
203
+ }
204
+
205
+ h2 {
206
+ margin: 0 0 1.5rem 0;
207
+ text-align: center;
208
+ color: var(--text-color);
209
+ font-size: 1.5rem;
210
+ font-weight: 900;
211
+ font-feature-settings:
212
+ "dlig", "case", "kern", "cv01", "cv02", "cv03", "cv04", "cv05", "cv06",
213
+ "cv09", "cv10", "cv11", "cv12", "cv13";
214
+ font-variation-settings: "";
215
+ letter-spacing: -0.065em;
216
+ line-height: 0.75em;
217
+ }
218
+
219
+ .form-group {
220
+ margin-bottom: 1rem;
221
+ }
222
+
223
+ label {
224
+ display: block;
225
+ margin-bottom: 0.5rem;
226
+ font-weight: 600;
227
+ letter-spacing: -0.065em;
228
+ color: var(--text-color);
229
+ }
230
+
231
+ input[type="password"] {
232
+ width: 100%;
233
+ padding: 12px 16px;
234
+ border: 2px solid var(--color-border);
235
+ border-radius: 4px;
236
+ font-size: 1rem;
237
+ font-family: inherit;
238
+ font-weight: 600;
239
+ letter-spacing: -0.065em;
240
+ transition: border-color 0.2s ease;
241
+ text-align:center;
242
+ }
243
+
244
+ input[type="password"]:focus {
245
+ outline: none;
246
+ border-color: var(--primary-color);
247
+ box-shadow: 0 0 0 3px rgba(0, 124, 186, 0.1);
248
+ }
249
+
250
+ button {
251
+ width: 100%;
252
+ background: var(--primary-color);
253
+ color: white;
254
+ padding: 12px 24px;
255
+ border: none;
256
+ border-radius: 4px;
257
+ font-size: 1rem;
258
+ font-weight: 900;
259
+ cursor: pointer;
260
+ transition: background-color 0.2s ease, transform 0.1s ease;
261
+ font-family: inherit;
262
+ }
263
+
264
+ button:hover {
265
+ background: color-mix(in srgb, var(--primary-color) 40%, var(--text-color));
266
+ transform: translateY(-1px);
267
+ }
268
+
269
+ button:active {
270
+ transform: translateY(0);
271
+ }
272
+
273
+ button:disabled {
274
+ opacity: 0.6;
275
+ cursor: not-allowed;
276
+ transform: none;
277
+ }
278
+
279
+ .error {
280
+ color: var(--error-color);
281
+ text-align: center;
282
+ margin-top: 1rem;
283
+ padding: 0.75rem;
284
+ background: rgba(220, 53, 69, 0.1);
285
+ border-radius: 4px;
286
+ display: none;
287
+ }
288
+
289
+ .loading {
290
+ display: none;
291
+ text-align: center;
292
+ margin-top: 1rem;
293
+ color: var(--primary-color);
294
+ }
295
+
296
+ @font-face {
297
+ font-family: InterVariable;
298
+ font-style: normal;
299
+ font-weight: 100 900;
300
+ font-display: swap;
301
+ src: url('https://rsms.me/inter/font-files/InterVariable.woff2?v=4.0') format('woff2');
302
+ }
303
+ @font-face {
304
+ font-family: InterVariable;
305
+ font-style: italic;
306
+ font-weight: 100 900;
307
+ font-display: swap;
308
+ src: url('https://rsms.me/inter/font-files/InterVariable-Italic.woff2?v=4.0') format('woff2');
309
+ }
310
+ /* legacy name "Inter var" (Oct 2023) */
311
+ @font-face { font-family:'Inter var'; font-style:normal; font-weight:100 900; font-display:swap; src: url('https://rsms.me/inter/font-files/InterVariable.woff2?v=4.0') format('woff2'); }
312
+ @font-face { font-family:'Inter var'; font-style:italic; font-weight:100 900; font-display:swap; src: url('https://rsms.me/inter/font-files/InterVariable-Italic.woff2?v=4.0') format('woff2'); }
313
+ /* static fonts */
314
+ @font-face { font-family:Inter; font-style:normal; font-weight:100; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-Thin.woff2?v=4.0") format("woff2"); }
315
+ @font-face { font-family:Inter; font-style:italic; font-weight:100; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-ThinItalic.woff2?v=4.0") format("woff2"); }
316
+ @font-face { font-family:Inter; font-style:normal; font-weight:200; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-ExtraLight.woff2?v=4.0") format("woff2"); }
317
+ @font-face { font-family:Inter; font-style:italic; font-weight:200; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-ExtraLightItalic.woff2?v=4.0") format("woff2"); }
318
+ @font-face { font-family:Inter; font-style:normal; font-weight:300; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-Light.woff2?v=4.0") format("woff2"); }
319
+ @font-face { font-family:Inter; font-style:italic; font-weight:300; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-LightItalic.woff2?v=4.0") format("woff2"); }
320
+ @font-face { font-family:Inter; font-style:normal; font-weight:400; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-Regular.woff2?v=4.0") format("woff2"); }
321
+ @font-face { font-family:Inter; font-style:italic; font-weight:400; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-Italic.woff2?v=4.0") format("woff2"); }
322
+ @font-face { font-family:Inter; font-style:normal; font-weight:500; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-Medium.woff2?v=4.0") format("woff2"); }
323
+ @font-face { font-family:Inter; font-style:italic; font-weight:500; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-MediumItalic.woff2?v=4.0") format("woff2"); }
324
+ @font-face { font-family:Inter; font-style:normal; font-weight:600; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-SemiBold.woff2?v=4.0") format("woff2"); }
325
+ @font-face { font-family:Inter; font-style:italic; font-weight:600; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-SemiBoldItalic.woff2?v=4.0") format("woff2"); }
326
+ @font-face { font-family:Inter; font-style:normal; font-weight:700; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-Bold.woff2?v=4.0") format("woff2"); }
327
+ @font-face { font-family:Inter; font-style:italic; font-weight:700; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-BoldItalic.woff2?v=4.0") format("woff2"); }
328
+ @font-face { font-family:Inter; font-style:normal; font-weight:800; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-ExtraBold.woff2?v=4.0") format("woff2"); }
329
+ @font-face { font-family:Inter; font-style:italic; font-weight:800; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-ExtraBoldItalic.woff2?v=4.0") format("woff2"); }
330
+ @font-face { font-family:Inter; font-style:normal; font-weight:900; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-Black.woff2?v=4.0") format("woff2"); }
331
+ @font-face { font-family:Inter; font-style:italic; font-weight:900; font-display:swap; src:url("https://rsms.me/inter/font-files/Inter-BlackItalic.woff2?v=4.0") format("woff2"); }
332
+ @font-face { font-family:InterDisplay; font-style:normal; font-weight:100; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-Thin.woff2?v=4.0") format("woff2"); }
333
+ @font-face { font-family:InterDisplay; font-style:italic; font-weight:100; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-ThinItalic.woff2?v=4.0") format("woff2"); }
334
+ @font-face { font-family:InterDisplay; font-style:normal; font-weight:200; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-ExtraLight.woff2?v=4.0") format("woff2"); }
335
+ @font-face { font-family:InterDisplay; font-style:italic; font-weight:200; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-ExtraLightItalic.woff2?v=4.0") format("woff2"); }
336
+ @font-face { font-family:InterDisplay; font-style:normal; font-weight:300; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-Light.woff2?v=4.0") format("woff2"); }
337
+ @font-face { font-family:InterDisplay; font-style:italic; font-weight:300; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-LightItalic.woff2?v=4.0") format("woff2"); }
338
+ @font-face { font-family:InterDisplay; font-style:normal; font-weight:400; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-Regular.woff2?v=4.0") format("woff2"); }
339
+ @font-face { font-family:InterDisplay; font-style:italic; font-weight:400; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-Italic.woff2?v=4.0") format("woff2"); }
340
+ @font-face { font-family:InterDisplay; font-style:normal; font-weight:500; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-Medium.woff2?v=4.0") format("woff2"); }
341
+ @font-face { font-family:InterDisplay; font-style:italic; font-weight:500; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-MediumItalic.woff2?v=4.0") format("woff2"); }
342
+ @font-face { font-family:InterDisplay; font-style:normal; font-weight:600; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-SemiBold.woff2?v=4.0") format("woff2"); }
343
+ @font-face { font-family:InterDisplay; font-style:italic; font-weight:600; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-SemiBoldItalic.woff2?v=4.0") format("woff2"); }
344
+ @font-face { font-family:InterDisplay; font-style:normal; font-weight:700; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-Bold.woff2?v=4.0") format("woff2"); }
345
+ @font-face { font-family:InterDisplay; font-style:italic; font-weight:700; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-BoldItalic.woff2?v=4.0") format("woff2"); }
346
+ @font-face { font-family:InterDisplay; font-style:normal; font-weight:800; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-ExtraBold.woff2?v=4.0") format("woff2"); }
347
+ @font-face { font-family:InterDisplay; font-style:italic; font-weight:800; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-ExtraBoldItalic.woff2?v=4.0") format("woff2"); }
348
+ @font-face { font-family:InterDisplay; font-style:normal; font-weight:900; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-Black.woff2?v=4.0") format("woff2"); }
349
+ @font-face { font-family:InterDisplay; font-style:italic; font-weight:900; font-display:swap; src:url("https://rsms.me/inter/font-files/InterDisplay-BlackItalic.woff2?v=4.0") format("woff2"); }
350
+ </style>
351
+ </head>
352
+ <body>
353
+ <div class="container">
354
+ <h2>Cette page est protégée par un mot de passe</h2>
355
+
356
+ <form id="decrypt-form">
357
+ <div class="form-group">
358
+ <input
359
+ type="password"
360
+ id="password"
361
+ placeholder="Mot de passe"
362
+ required
363
+ autocomplete="current-password"
364
+ >
365
+ </div>
366
+ <button type="submit" id="unlock-btn">
367
+ <span class="btn-text">Déverrouiller</span>
368
+ <span class="btn-loading" style="display: none;">Déverrouillage...</span>
369
+ </button>
370
+ </form>
371
+
372
+ <div id="error" class="error">Mot de passe incorrect. Veuillez réessayer.</div>
373
+ <div id="loading" class="loading">Déchiffrement du contenu...</div>
374
+ </div>
375
+
376
+ <script>
377
+ const encryptedData = "{{ encryptedData }}";
378
+ const maxAttempts = 5;
379
+ let attempts = 0;
380
+
381
+ const form = document.getElementById('decrypt-form');
382
+ const passwordInput = document.getElementById('password');
383
+ const unlockBtn = document.getElementById('unlock-btn');
384
+ const errorDiv = document.getElementById('error');
385
+ const loadingDiv = document.getElementById('loading');
386
+ const btnText = unlockBtn.querySelector('.btn-text');
387
+ const btnLoading = unlockBtn.querySelector('.btn-loading');
388
+
389
+ form.addEventListener('submit', async function(e) {
390
+ e.preventDefault();
391
+
392
+ const password = passwordInput.value;
393
+ if (!password) return;
394
+
395
+ await attemptDecryption(password);
396
+ });
397
+
398
+ function setLoading(loading) {
399
+ unlockBtn.disabled = loading;
400
+ btnText.style.display = loading ? 'none' : 'inline';
401
+ btnLoading.style.display = loading ? 'inline' : 'none';
402
+ loadingDiv.style.display = loading ? 'block' : 'none';
403
+ }
404
+
405
+ function showError(message) {
406
+ errorDiv.textContent = message;
407
+ errorDiv.style.display = 'block';
408
+
409
+ // Add shake animation
410
+ errorDiv.style.animation = 'shake 0.5s ease-in-out';
411
+ setTimeout(() => {
412
+ errorDiv.style.animation = '';
413
+ }, 500);
414
+ }
415
+
416
+ function hideError() {
417
+ errorDiv.style.display = 'none';
418
+ }
419
+
420
+ async function decrypt(encryptedData, password) {
421
+ try {
422
+ // Simple XOR decryption matching server-side implementation
423
+ const encrypted = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
424
+
425
+ // Generate key from password using SHA-256
426
+ const encoder = new TextEncoder();
427
+ const passwordBytes = encoder.encode(password);
428
+ const hashBuffer = await crypto.subtle.digest('SHA-256', passwordBytes);
429
+ const key = new Uint8Array(hashBuffer);
430
+
431
+ // XOR decrypt
432
+ const decrypted = new Uint8Array(encrypted.length);
433
+ for (let i = 0; i < encrypted.length; i++) {
434
+ decrypted[i] = encrypted[i] ^ key[i % key.length];
435
+ }
436
+
437
+ const result = new TextDecoder().decode(decrypted);
438
+
439
+ // Basic validation that decryption worked
440
+ if (!result.includes('<!DOCTYPE') && !result.includes('<html')) {
441
+ throw new Error('Invalid decryption result');
442
+ }
443
+
444
+ return result;
445
+ } catch (error) {
446
+ throw new Error('Decryption failed');
447
+ }
448
+ }
449
+
450
+ // Check for password in URL hash on page load
451
+ function checkHashPassword() {
452
+ const allowHashPassword = true;
453
+
454
+ if (!allowHashPassword) {
455
+ passwordInput.focus();
456
+ return;
457
+ }
458
+
459
+ const hash = window.location.hash;
460
+ if (hash && hash.length > 1) {
461
+ const passwordFromHash = hash.substring(1); // Remove the # character
462
+ if (passwordFromHash) {
463
+ // Clear the hash from URL for security (if enabled)
464
+ const clearHashOnLoad = true;
465
+ if (clearHashOnLoad) {
466
+ history.replaceState(null, null, window.location.pathname + window.location.search);
467
+ }
468
+
469
+ // Auto-decrypt with hash password
470
+ attemptDecryption(passwordFromHash);
471
+ return;
472
+ }
473
+ }
474
+
475
+ // Auto-focus password input if no hash password
476
+ passwordInput.focus();
477
+ }
478
+
479
+ async function attemptDecryption(password) {
480
+ if (attempts >= maxAttempts) {
481
+ showError('Too many failed attempts. Please refresh the page.');
482
+ return;
483
+ }
484
+
485
+ setLoading(true);
486
+ hideError();
487
+
488
+ try {
489
+ const decrypted = await decrypt(encryptedData, password);
490
+
491
+ // Replace the current page with decrypted content
492
+ document.open();
493
+ document.write(decrypted);
494
+ document.close();
495
+
496
+ } catch (error) {
497
+ attempts++;
498
+ const remaining = maxAttempts - attempts;
499
+
500
+ if (remaining > 0) {
501
+ showError(`Mot de passe incorrect. Veuillez réessayer. (${remaining} tentatives restantes)`);
502
+ } else {
503
+ showError('Too many failed attempts. Please refresh the page.');
504
+ unlockBtn.disabled = true;
505
+ }
506
+
507
+ passwordInput.value = '';
508
+ passwordInput.focus();
509
+ } finally {
510
+ setLoading(false);
511
+ }
512
+ }
513
+
514
+ // Check for hash password on page load
515
+ checkHashPassword();
516
+
517
+ // Add CSS animation for shake effect
518
+ const style = document.createElement('style');
519
+ style.textContent = `
520
+ @keyframes shake {
521
+ 0%, 100% { transform: translateX(0); }
522
+ 25% { transform: translateX(-5px); }
523
+ 75% { transform: translateX(5px); }
524
+ }
525
+ `;
526
+ document.head.appendChild(style);
527
+
528
+ </script>
529
+ </body>
530
+ </html>
531
+ ```
532
+
533
+ </details>
534
+
535
+ ## Notes / Observations
536
+
537
+ - `Encrypted.astro`‘s SHA-256+XOR scheme is identical to the archived
538
+ `encrypted.njk` above — it wasn’t rewritten during the Astro port, weakness and all.
539
+
540
+ ## Todo
541
+
542
+ - [ ] **Decide `Encrypted.astro`’s real threat model** — if it’s ever meant to [priority:: 3] [token_scale:: 3] [created:: 2026-07-14] [area:: framework]
543
+ gate something that actually matters (not just “keep casual visitors out”), the XOR keystream needs replacing with real crypto (e.g. a server-verified password, or AES-GCM with a per-page random IV) — known-plaintext against predictable HTML headers breaks the current scheme. If *“casual gate”* is genuinely the whole intent, just say so in the docs instead of *“ensure privacy”*.
package/StndInit.astro ADDED
@@ -0,0 +1,49 @@
1
+ ---
2
+ import config from "virtual:stnd/config";
3
+ import Hook from "@stnd/core/Hook";
4
+ ---
5
+
6
+ <head>
7
+ {
8
+ /* External CDN assets declared by modules (styles & scripts).
9
+ Rendered directly in <head> because Astro 6 beta's injectScript
10
+ pipeline is bypassed by the NonRunnablePipeline used with the
11
+ Cloudflare adapter in dev mode. This is the only reliable path. */
12
+ }
13
+ {
14
+ (config._externalStyles || []).map((href: string) => (
15
+ <link rel="stylesheet" href={href} />
16
+ ))
17
+ }
18
+ {
19
+ (config._externalScripts || []).map((src: string) => (
20
+ <script is:inline src={src} />
21
+ ))
22
+ }
23
+ </head>
24
+ <!-- SYSTEM LAYER — Components injected by Gold Standard modules -->
25
+ <div id="stnd-os" transition:persist="stnd-os">
26
+ <!-- Static system components: Toast, Confetti (Astro SSR, zero JS overhead) -->
27
+ <Hook zone="stnd:base" />
28
+
29
+ <!-- Hydrated system components: Lab, DropZone, Graft, etc. (Svelte, client-side) -->
30
+ <Hook zone="stnd:client" hydrated={true} />
31
+ </div>
32
+
33
+ <!-- MODULE SCRIPTS (bypasses broken injectScript pipeline) -->
34
+ <script>
35
+ import "virtual:stnd/store";
36
+ import "virtual:stnd/scripts";
37
+ </script>
38
+
39
+ <!-- Global inline client script: boot behavior -->
40
+ <script is:inline>
41
+ // Fire standard ready event on page load.
42
+ document.addEventListener("astro:page-load", () => {
43
+ try {
44
+ window.dispatchEvent(new CustomEvent("standard:ready"));
45
+ } catch (e) {
46
+ // ignore
47
+ }
48
+ });
49
+ </script>
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "@stnd/layout",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
+ "author": "Francis Fontaine",
6
+ "license": "MIT",
5
7
  "exports": {
6
- "./*": "./*.astro"
8
+ "./*": {
9
+ "astro": "./*.astro",
10
+ "svelte": "./*.svelte",
11
+ "default": "./*.astro"
12
+ }
7
13
  },
8
14
  "dependencies": {
9
- "@stnd/utils": "0.2.0",
10
- "@stnd/log": "0.2.0"
15
+ "wrangler": "^4.105.0",
16
+ "@stnd/lab": "1.0.0",
17
+ "@stnd/log": "0.5.0"
11
18
  },
12
19
  "peerDependencies": {
13
20
  "astro": ">=5.0.0"