@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/Encrypted.astro CHANGED
@@ -1,196 +1,183 @@
1
1
  ---
2
- import "../../styles/standard.scss";
2
+ import "virtual:stnd/styles";
3
3
 
4
4
  const { site = {}, encryptedData } = Astro.props;
5
5
  ---
6
6
 
7
7
  <!doctype html>
8
8
  <html lang={site.language || "en"} class="rhythm" data-theme="system">
9
- <head>
10
- <meta charset="utf-8" />
11
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
12
- <title>Protected Content</title>
13
- </head>
14
- <body>
15
- <main class="prose md">
16
- <div class="encrypted-container">
17
- <h2>Cette page est protégée par un mot de passe</h2>
18
-
19
- <form id="decrypt-form">
20
- <div class="form-group">
21
- <input
22
- type="password"
23
- id="password"
24
- placeholder="Mot de passe"
25
- required
26
- autocomplete="current-password"
27
- />
28
- </div>
29
- <button type="submit" id="unlock-btn">
30
- <span class="btn-text">Déverrouiller</span>
31
- <span class="btn-loading" style="display: none;"
32
- >Déverrouillage...</span
33
- >
34
- </button>
35
- </form>
36
-
37
- <div id="error" class="error">
38
- Mot de passe incorrect. Veuillez réessayer.
39
- </div>
40
- <div id="loading" class="loading">
41
- Déchiffrement du contenu...
42
- </div>
43
- </div>
44
-
45
- <div id="encrypted-content" style="display: none;">
46
- {encryptedData}
47
- </div>
48
-
49
- <script is:inline>
50
- const encryptedData =
51
- document.getElementById("encrypted-content").textContent;
52
- const maxAttempts = 5;
53
- let attempts = 0;
54
-
55
- const form = document.getElementById("decrypt-form");
56
- const passwordInput = document.getElementById("password");
57
- const unlockBtn = document.getElementById("unlock-btn");
58
- const errorDiv = document.getElementById("error");
59
- const loadingDiv = document.getElementById("loading");
60
- const btnText = unlockBtn.querySelector(".btn-text");
61
- const btnLoading = unlockBtn.querySelector(".btn-loading");
62
-
63
- form.addEventListener("submit", async function (e) {
64
- e.preventDefault();
65
- const password = passwordInput.value;
66
- if (!password) return;
67
- await attemptDecryption(password);
68
- });
69
-
70
- function setLoading(loading) {
71
- unlockBtn.disabled = loading;
72
- btnText.style.display = loading ? "none" : "inline";
73
- btnLoading.style.display = loading ? "inline" : "none";
74
- loadingDiv.style.display = loading ? "block" : "none";
75
- }
76
-
77
- function showError(message) {
78
- errorDiv.textContent = message;
79
- errorDiv.style.display = "block";
80
- errorDiv.style.animation = "shake 0.5s ease-in-out";
81
- setTimeout(() => {
82
- errorDiv.style.animation = "";
83
- }, 500);
84
- }
85
-
86
- function hideError() {
87
- errorDiv.style.display = "none";
88
- }
89
-
90
- async function decrypt(encryptedData, password) {
91
- try {
92
- const encrypted = Uint8Array.from(
93
- atob(encryptedData),
94
- (c) => c.charCodeAt(0),
95
- );
96
- const encoder = new TextEncoder();
97
- const passwordBytes = encoder.encode(password);
98
- const hashBuffer = await crypto.subtle.digest(
99
- "SHA-256",
100
- passwordBytes,
101
- );
102
- const key = new Uint8Array(hashBuffer);
103
-
104
- const decrypted = new Uint8Array(encrypted.length);
105
- for (let i = 0; i < encrypted.length; i++) {
106
- decrypted[i] = encrypted[i] ^ key[i % key.length];
107
- }
108
-
109
- const result = new TextDecoder().decode(decrypted);
110
- return result;
111
- } catch (error) {
112
- throw new Error("Decryption failed");
113
- }
114
- }
115
-
116
- function isValidHTML(str) {
117
- const hasHTMLTags = /<[a-z][\s\S]*>/i.test(str);
118
- const hasDoctype = /<!DOCTYPE/i.test(str);
119
- const hasHtmlTag = /<html/i.test(str);
120
- const nonPrintable = str.match(
121
- /[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g,
122
- );
123
- const nonPrintableRatio = nonPrintable
124
- ? nonPrintable.length / str.length
125
- : 0;
126
- return (
127
- (hasHTMLTags || hasDoctype || hasHtmlTag) &&
128
- nonPrintableRatio < 0.1
129
- );
130
- }
131
-
132
- async function attemptDecryption(password) {
133
- if (attempts >= maxAttempts) {
134
- showError(
135
- "Trop de tentatives. Veuillez rafraîchir la page.",
136
- );
137
- return;
138
- }
139
-
140
- setLoading(true);
141
- hideError();
142
-
143
- try {
144
- const decrypted = await decrypt(
145
- encryptedData,
146
- password,
147
- );
148
-
149
- if (!isValidHTML(decrypted)) {
150
- throw new Error("Invalid decryption result");
151
- }
152
-
153
- const container = document.querySelector(
154
- ".encrypted-container",
155
- );
156
- container.outerHTML = decrypted;
157
- } catch (error) {
158
- attempts++;
159
- const remaining = maxAttempts - attempts;
160
-
161
- if (remaining > 0) {
162
- showError(
163
- `Mot de passe incorrect. Veuillez réessayer. (${remaining} tentatives restantes)`,
164
- );
165
- } else {
166
- showError(
167
- "Trop de tentatives. Veuillez rafraîchir la page.",
168
- );
169
- unlockBtn.disabled = true;
170
- }
171
-
172
- passwordInput.value = "";
173
- passwordInput.focus();
174
- } finally {
175
- setLoading(false);
176
- }
177
- }
178
-
179
- const hash = window.location.hash;
180
- if (hash && hash.length > 1) {
181
- const passwordFromHash = hash.substring(1);
182
- if (passwordFromHash) {
183
- history.replaceState(
184
- null,
185
- null,
186
- window.location.pathname + window.location.search,
187
- );
188
- attemptDecryption(passwordFromHash);
189
- }
190
- } else {
191
- passwordInput.focus();
192
- }
193
- </script>
194
- </main>
195
- </body>
9
+ <head>
10
+ <meta charset="utf-8" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
12
+ <title>Protected Content</title>
13
+ </head>
14
+ <body>
15
+ <main class="prose md">
16
+ <div class="encrypted-container">
17
+ <h2>Cette page est protégée par un mot de passe</h2>
18
+
19
+ <form id="decrypt-form">
20
+ <div class="form-group">
21
+ <input
22
+ type="password"
23
+ id="password"
24
+ placeholder="Mot de passe"
25
+ required
26
+ autocomplete="current-password"
27
+ />
28
+ </div>
29
+ <button type="submit" id="unlock-btn">
30
+ <span class="btn-text">Déverrouiller</span>
31
+ <span class="btn-loading" style="display: none;"
32
+ >Déverrouillage...</span
33
+ >
34
+ </button>
35
+ </form>
36
+
37
+ <div id="error" class="error">
38
+ Mot de passe incorrect. Veuillez réessayer.
39
+ </div>
40
+ <div id="loading" class="loading">Déchiffrement du contenu...</div>
41
+ </div>
42
+
43
+ <div id="encrypted-content" style="display: none;">
44
+ {encryptedData}
45
+ </div>
46
+
47
+ <script is:inline>
48
+ const encryptedData =
49
+ document.getElementById("encrypted-content").textContent;
50
+ const maxAttempts = 5;
51
+ let attempts = 0;
52
+
53
+ const form = document.getElementById("decrypt-form");
54
+ const passwordInput = document.getElementById("password");
55
+ const unlockBtn = document.getElementById("unlock-btn");
56
+ const errorDiv = document.getElementById("error");
57
+ const loadingDiv = document.getElementById("loading");
58
+ const btnText = unlockBtn.querySelector(".btn-text");
59
+ const btnLoading = unlockBtn.querySelector(".btn-loading");
60
+
61
+ form.addEventListener("submit", async function (e) {
62
+ e.preventDefault();
63
+ const password = passwordInput.value;
64
+ if (!password) return;
65
+ await attemptDecryption(password);
66
+ });
67
+
68
+ function setLoading(loading) {
69
+ unlockBtn.disabled = loading;
70
+ btnText.style.display = loading ? "none" : "inline";
71
+ btnLoading.style.display = loading ? "inline" : "none";
72
+ loadingDiv.style.display = loading ? "block" : "none";
73
+ }
74
+
75
+ function showError(message) {
76
+ errorDiv.textContent = message;
77
+ errorDiv.style.display = "block";
78
+ errorDiv.style.animation = "shake 0.5s ease-in-out";
79
+ setTimeout(() => {
80
+ errorDiv.style.animation = "";
81
+ }, 500);
82
+ }
83
+
84
+ function hideError() {
85
+ errorDiv.style.display = "none";
86
+ }
87
+
88
+ async function decrypt(encryptedData, password) {
89
+ try {
90
+ const encrypted = Uint8Array.from(atob(encryptedData), (c) =>
91
+ c.charCodeAt(0),
92
+ );
93
+ const encoder = new TextEncoder();
94
+ const passwordBytes = encoder.encode(password);
95
+ const hashBuffer = await crypto.subtle.digest(
96
+ "SHA-256",
97
+ passwordBytes,
98
+ );
99
+ const key = new Uint8Array(hashBuffer);
100
+
101
+ const decrypted = new Uint8Array(encrypted.length);
102
+ for (let i = 0; i < encrypted.length; i++) {
103
+ decrypted[i] = encrypted[i] ^ key[i % key.length];
104
+ }
105
+
106
+ const result = new TextDecoder().decode(decrypted);
107
+ return result;
108
+ } catch (error) {
109
+ throw new Error("Decryption failed");
110
+ }
111
+ }
112
+
113
+ function isValidHTML(str) {
114
+ const hasHTMLTags = /<[a-z][\s\S]*>/i.test(str);
115
+ const hasDoctype = /<!DOCTYPE/i.test(str);
116
+ const hasHtmlTag = /<html/i.test(str);
117
+ const nonPrintable = str.match(
118
+ /[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g,
119
+ );
120
+ const nonPrintableRatio = nonPrintable
121
+ ? nonPrintable.length / str.length
122
+ : 0;
123
+ return (
124
+ (hasHTMLTags || hasDoctype || hasHtmlTag) && nonPrintableRatio < 0.1
125
+ );
126
+ }
127
+
128
+ async function attemptDecryption(password) {
129
+ if (attempts >= maxAttempts) {
130
+ showError("Trop de tentatives. Veuillez rafraîchir la page.");
131
+ return;
132
+ }
133
+
134
+ setLoading(true);
135
+ hideError();
136
+
137
+ try {
138
+ const decrypted = await decrypt(encryptedData, password);
139
+
140
+ if (!isValidHTML(decrypted)) {
141
+ throw new Error("Invalid decryption result");
142
+ }
143
+
144
+ const container = document.querySelector(".encrypted-container");
145
+ container.outerHTML = decrypted;
146
+ } catch (error) {
147
+ attempts++;
148
+ const remaining = maxAttempts - attempts;
149
+
150
+ if (remaining > 0) {
151
+ showError(
152
+ `Mot de passe incorrect. Veuillez réessayer. (${remaining} tentatives restantes)`,
153
+ );
154
+ } else {
155
+ showError("Trop de tentatives. Veuillez rafraîchir la page.");
156
+ unlockBtn.disabled = true;
157
+ }
158
+
159
+ passwordInput.value = "";
160
+ passwordInput.focus();
161
+ } finally {
162
+ setLoading(false);
163
+ }
164
+ }
165
+
166
+ const hash = window.location.hash;
167
+ if (hash && hash.length > 1) {
168
+ const passwordFromHash = hash.substring(1);
169
+ if (passwordFromHash) {
170
+ history.replaceState(
171
+ null,
172
+ null,
173
+ window.location.pathname + window.location.search,
174
+ );
175
+ attemptDecryption(passwordFromHash);
176
+ }
177
+ } else {
178
+ passwordInput.focus();
179
+ }
180
+ </script>
181
+ </main>
182
+ </body>
196
183
  </html>
package/Footer.astro ADDED
@@ -0,0 +1,184 @@
1
+ ---
2
+ /**
3
+ * Footer.astro — Generic and highly flexible site footer.
4
+ *
5
+ * Key Features:
6
+ * - Reads site defaults from virtual:stnd/config (title, navigation).
7
+ * - Multi-column layout using grid-8 (3 columns for brand/info, 5 for custom links).
8
+ * - Responsive out of the box (stacks on mobile).
9
+ * - Supports slots for brand, description, copyright, and custom link columns.
10
+ *
11
+ * Usage:
12
+ * <Footer />
13
+ * <Footer title="My Garden">
14
+ * <div class="col-1">
15
+ * <h4>Section</h4>
16
+ * <ul><li><a href="/">Home</a></li></ul>
17
+ * </div>
18
+ * </Footer>
19
+ */
20
+ import config from "virtual:stnd/config";
21
+
22
+ interface Props {
23
+ title?: string;
24
+ class?: string;
25
+ }
26
+
27
+ const { title = config.title || "Home", class: className } = Astro.props;
28
+
29
+ const footerNav =
30
+ config.nav?.footer ??
31
+ config.nav?.header ??
32
+ (Array.isArray(config.nav) ? config.nav : []);
33
+ const year = new Date().getFullYear();
34
+ ---
35
+
36
+ <footer class:list={["stnd-footer", "compact", className]}>
37
+ <div class="grid-8 sm:grid-1">
38
+ <!-- Column 1: Brand & Info (Always Visible) -->
39
+ <div class="col-2 sm:col-1">
40
+ <h4 class="footer-title mb-2">
41
+ <a href={config.url} class="no-decoration">{title}</a>
42
+ </h4>
43
+
44
+
45
+
46
+ <!-- Column 2: Navigation -->
47
+ {
48
+ footerNav.length > 0 && (
49
+ <ul class="no-bullet">
50
+ {footerNav.map((item: any) => (
51
+ <li>
52
+ <a href={item.url}>{item.title}</a>
53
+ </li>
54
+ ))}
55
+ </ul>
56
+ )
57
+ }
58
+
59
+
60
+ {
61
+ config.description && (
62
+ <p class="text-2xs mt-half compact">{config.description}</p>
63
+ )
64
+ }
65
+
66
+ <p class="text-2xs mt-half">&copy; {year}.</p>
67
+ </div>
68
+
69
+ <!-- Column 2: Navigation -->
70
+ {/*
71
+ footerNav.length > 0 && (
72
+ <div class="col-1">
73
+ <h4>Navigation</h4>
74
+ <ul class="no-bullet">
75
+ {footerNav.map((item: any) => (
76
+ <li>
77
+ <a href={item.url}>{item.title}</a>
78
+ </li>
79
+ ))}
80
+ </ul>
81
+ </div>
82
+ )
83
+ */}
84
+
85
+ <!-- Column 3, 4, etc. -->
86
+ <slot />
87
+ </div>
88
+
89
+ {/* Bottom Bar (Optional) */}
90
+ {
91
+ (Astro.slots.has("bottom-left") || Astro.slots.has("bottom-right")) && (
92
+ <div class="footer-bottom flex-far justify-between items-center no-decoration sm:flex-col sm:items-start text-2xs">
93
+ <div class="footer-bottom-left">
94
+ <slot name="bottom-left" />
95
+ </div>
96
+ <div class="footer-bottom-right flex">
97
+ <slot name="bottom-right" />
98
+ </div>
99
+ </div>
100
+ )
101
+ }
102
+ </footer>
103
+
104
+ <style is:global>
105
+ .stnd-footer {
106
+ font-family: var(--font-interface);
107
+ color: var(--color-subtle);
108
+ border-top: var(--border);
109
+ margin-top: calc(var(--space) * 12);
110
+ padding-block-start: var(--gap-footer) !important;
111
+ h1,
112
+ h2,
113
+ h3,
114
+ h4,
115
+ h5,
116
+ h6 {
117
+ font-size: var(--size-base);
118
+ font-family: var(--font-interface);
119
+ margin-block-end: var(--space-half) !important;
120
+ font-weight: var(--font-header-weight);
121
+ margin-top: 0 !important;
122
+ }
123
+ h4 {
124
+ font-size: var(--size-sm);
125
+ }
126
+ li {
127
+ margin-block-end: var(--leading) !important;
128
+ }
129
+
130
+ /* Styling for link lists inside footer */
131
+ ul {
132
+ list-style: none;
133
+ padding: 0;
134
+ margin: 0 !important;
135
+ }
136
+
137
+ ul li {
138
+ font-size: var(--size-2xs);
139
+ margin-bottom: var(--space-2);
140
+ padding-inline-start: 0;
141
+ margin-top: 0 !important;
142
+ }
143
+
144
+ ul li a {
145
+ color: inherit;
146
+ text-decoration: none;
147
+ }
148
+
149
+ ul li::before {
150
+ display: none;
151
+ }
152
+ }
153
+
154
+ .footer-title {
155
+ font-family: var(--font-heading);
156
+ font-size: var(--size-base);
157
+ font-weight: var(--font-header-weight);
158
+ color: var(--color-foreground);
159
+ margin: 0;
160
+ }
161
+
162
+ .stnd-footer a {
163
+ color: inherit;
164
+ text-decoration: none;
165
+ }
166
+
167
+ .stnd-footer a:hover {
168
+ color: var(--color-accent, var(--color-foreground));
169
+ opacity: 1;
170
+ }
171
+
172
+ .footer-bottom {
173
+ margin-top: calc(var(--space) * 6);
174
+ margin-bottom: calc((var(--gap-footer) * -1));
175
+ padding-bottom: var(--space);
176
+
177
+ strong {
178
+ color: inherit;
179
+ }
180
+ em {
181
+ color: inherit;
182
+ }
183
+ }
184
+ </style>