@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/Base.astro +106 -130
- package/CHANGELOG.md +36 -0
- package/Encrypted.astro +175 -188
- package/Footer.astro +184 -0
- package/Header.astro +533 -0
- package/Meta.astro +226 -328
- package/README.md +543 -0
- package/StndInit.astro +49 -0
- package/package.json +11 -4
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.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"author": "Francis Fontaine",
|
|
6
|
+
"license": "MIT",
|
|
5
7
|
"exports": {
|
|
6
|
-
"./*":
|
|
8
|
+
"./*": {
|
|
9
|
+
"astro": "./*.astro",
|
|
10
|
+
"svelte": "./*.svelte",
|
|
11
|
+
"default": "./*.astro"
|
|
12
|
+
}
|
|
7
13
|
},
|
|
8
14
|
"dependencies": {
|
|
9
|
-
"
|
|
10
|
-
"@stnd/
|
|
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"
|