@stnd/layout 0.2.0 → 0.5.1

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