@stnd/modules 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.
Files changed (65) hide show
  1. package/Backlinks.astro +56 -0
  2. package/CHANGELOG.md +60 -0
  3. package/Comments.astro +115 -0
  4. package/README.md +404 -0
  5. package/ReadingTime.astro +60 -0
  6. package/brand-manual/index.module.js +12 -0
  7. package/brand-manual/route.astro +341 -0
  8. package/confetti/Confetti.astro +42 -0
  9. package/confetti/index.module.js +22 -0
  10. package/content/index.module.js +19 -0
  11. package/content/route.astro +59 -0
  12. package/copy-buttons/index.module.js +9 -0
  13. package/deep-link/index.module.js +9 -0
  14. package/deep-link/init.js +14 -0
  15. package/eink/index.module.js +9 -0
  16. package/fonts/index.module.js +76 -0
  17. package/gestures/index.module.js +9 -0
  18. package/gestures/init.js +13 -0
  19. package/gsap/client.js +41 -0
  20. package/gsap/index.module.js +20 -0
  21. package/headers/index.module.js +16 -0
  22. package/headers/route.js +93 -0
  23. package/humans/index.module.js +17 -0
  24. package/humans/route.js +79 -0
  25. package/iconify/index.module.js +9 -0
  26. package/image-zoom/index.module.js +9 -0
  27. package/keyboard/index.module.js +9 -0
  28. package/lab/index.module.js +26 -0
  29. package/launcher/index.module.js +27 -0
  30. package/maintenance/index.module.js +18 -0
  31. package/maintenance/middleware.js +34 -0
  32. package/maintenance/routes/index.astro +45 -0
  33. package/manifest/index.module.js +16 -0
  34. package/manifest/route.js +57 -0
  35. package/math/client.js +98 -0
  36. package/math/index.module.js +19 -0
  37. package/math/styles.css +25 -0
  38. package/mermaid/client.js +125 -0
  39. package/mermaid/index.module.js +19 -0
  40. package/mermaid/styles.css +114 -0
  41. package/p5/client.js +25 -0
  42. package/p5/index.module.js +24 -0
  43. package/package.json +35 -0
  44. package/prism/README.md +13 -0
  45. package/prism/index.module.js +19 -0
  46. package/robots/index.module.js +16 -0
  47. package/robots/route.js +67 -0
  48. package/rss/index.module.js +18 -0
  49. package/rss/route.js +163 -0
  50. package/scroll-wrappers/index.module.js +9 -0
  51. package/security-txt/index.module.js +17 -0
  52. package/security-txt/route.js +90 -0
  53. package/sitemap/index.module.js +22 -0
  54. package/stripe/client.js +27 -0
  55. package/stripe/components/MockBuyButton.svelte +38 -0
  56. package/stripe/index.module.js +33 -0
  57. package/stripe/routes/mock-checkout.js +64 -0
  58. package/stripe/views/CheckoutView.svelte +181 -0
  59. package/styles/index.module.js +11 -0
  60. package/theme-utils/README.md +74 -0
  61. package/theme-utils/client.js +79 -0
  62. package/theme-utils/index.module.js +27 -0
  63. package/themes/index.module.js +50 -0
  64. package/toast/index.module.js +17 -0
  65. package/toast/toast.js +17 -0
@@ -0,0 +1,56 @@
1
+ ---
2
+ interface BacklinkItem {
3
+ url: string;
4
+ title?: string;
5
+ data?: {
6
+ visibility?: string;
7
+ };
8
+ }
9
+
10
+ interface Props {
11
+ backlinks?: BacklinkItem[];
12
+ heading?: string;
13
+ ariaLabel?: string;
14
+ }
15
+
16
+ const props = Astro.props as Props;
17
+ const {
18
+ backlinks = [],
19
+ heading = "Mentionné dans",
20
+ ariaLabel = "Backlinks small",
21
+ } = props;
22
+
23
+ // ✨ NEW: Deduplicate backlinks by URL
24
+ const seenUrls = new Set();
25
+ const deduplicatedBacklinks = backlinks.filter((entry: BacklinkItem) => {
26
+ if (seenUrls.has(entry.url)) {
27
+ return false; // Skip duplicates
28
+ }
29
+ seenUrls.add(entry.url);
30
+ return true;
31
+ });
32
+
33
+ const publicBacklinks = deduplicatedBacklinks.filter(
34
+ (entry: BacklinkItem) => entry?.data?.visibility !== "private",
35
+ );
36
+ ---
37
+
38
+ {
39
+ publicBacklinks.length > 0 && (
40
+ <>
41
+ <hr />
42
+ <article class="backlinks prose small tight">
43
+ <p class="muted mb-d4 flow-row">{heading}</p>
44
+ <nav aria-label={ariaLabel}>
45
+ <ul>
46
+ {publicBacklinks.map((item: BacklinkItem) => (
47
+ <li>
48
+ <a href={item.url}>{item.title ?? item.url}</a>
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ </nav>
53
+ </article>
54
+ </>
55
+ )
56
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,60 @@
1
+ # @stnd/folios
2
+
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @stnd/styles@0.5.2
9
+
10
+ ## 0.5.0
11
+
12
+ ### Minor Changes
13
+
14
+ - Folio Load order
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies
19
+ - @stnd/styles@0.5.0
20
+ - @stnd/log@0.5.0
21
+ - @stnd/utils@0.5.0
22
+
23
+ ## 0.4.0
24
+
25
+ ### Minor Changes
26
+
27
+ - Folio order
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies
32
+ - @stnd/styles@0.4.0
33
+ - @stnd/log@0.4.0
34
+ - @stnd/utils@0.4.0
35
+
36
+ ## 0.3.0
37
+
38
+ ### Minor Changes
39
+
40
+ - 2d0ae02: NPM Test
41
+
42
+ ### Patch Changes
43
+
44
+ - Updated dependencies [2d0ae02]
45
+ - @stnd/styles@0.3.0
46
+ - @stnd/log@0.3.0
47
+ - @stnd/utils@0.3.0
48
+
49
+ ## 0.2.0
50
+
51
+ ### Minor Changes
52
+
53
+ - 6d8888a: First release
54
+
55
+ ### Patch Changes
56
+
57
+ - Updated dependencies [6d8888a]
58
+ - @stnd/styles@0.2.0
59
+ - @stnd/log@0.2.0
60
+ - @stnd/utils@0.2.0
package/Comments.astro ADDED
@@ -0,0 +1,115 @@
1
+ ---
2
+ /**
3
+ * Comments Component
4
+ * Renders the GitHub comments form.
5
+ */
6
+ const {
7
+ showSubmit = true,
8
+ showReset = false,
9
+ showCancel = false,
10
+ apiUrl = "/api/comments",
11
+ pollInterval = null
12
+ } = Astro.props;
13
+
14
+ // Get page ID from URL
15
+ const url = Astro.url.pathname;
16
+ const pageId = url
17
+ .replace(/^\//, "")
18
+ .replace(/\/$/, "")
19
+ .replace(/\//g, "_") || "index";
20
+
21
+ const initOptions = {
22
+ apiUrl,
23
+ pageId,
24
+ container: "#comments",
25
+ form: "#comment-form",
26
+ pollInterval
27
+ };
28
+ ---
29
+
30
+ <article class="comments-system">
31
+ <div id="comments"></div>
32
+
33
+ <button
34
+ id="show-comment-form-btn"
35
+ type="button"
36
+ class="button"
37
+ onclick="document.getElementById('comment-form-wrapper').style.display='block'; this.style.display='none';"
38
+ >
39
+ Write a Comment
40
+ </button>
41
+
42
+ <div id="comment-form-wrapper" style="display: none;">
43
+ <form id="comment-form" method="post" action={apiUrl} novalidate>
44
+ <fieldset>
45
+ <legend>Leave a Comment</legend>
46
+
47
+ <label for="author">Your Name <span aria-label="required">*</span></label>
48
+ <input
49
+ type="text"
50
+ id="author"
51
+ name="author"
52
+ placeholder="Your name will be displayed with your comment."
53
+ required
54
+ maxlength="100"
55
+ />
56
+
57
+ <label for="email">Email Address <span aria-label="required">*</span></label>
58
+ <input
59
+ type="email"
60
+ id="email"
61
+ name="email"
62
+ placeholder="Your email will not be displayed publicly."
63
+ required
64
+ />
65
+
66
+ <label for="content">Comment <span aria-label="required">*</span></label>
67
+ <textarea
68
+ id="content"
69
+ name="content"
70
+ placeholder="Share your thoughts... Supports **bold**, *italic*, `code`, and [links](url)"
71
+ required
72
+ minlength="3"
73
+ maxlength="10000"
74
+ rows="6"
75
+ ></textarea>
76
+
77
+ <!-- Hidden fields -->
78
+ <input type="hidden" id="pageId" name="pageId" value={pageId} />
79
+ <input type="hidden" id="parentId" name="parentId" value="" />
80
+
81
+ <!-- Status indicator -->
82
+ <div id="form-status" role="status" aria-live="polite"></div>
83
+
84
+ <!-- Submit buttons -->
85
+ <div class="form-actions">
86
+ {showSubmit && <button type="submit" class="button">Post Comment</button>}
87
+ {showReset && <button type="reset" class="button">Clear</button>}
88
+ {showCancel && <button type="button" class="button">Cancel</button>}
89
+ </div>
90
+ </fieldset>
91
+ </form>
92
+ </div>
93
+ </article>
94
+
95
+ <script define:vars={{ initOptions }} is:inline defer>
96
+ document.addEventListener("DOMContentLoaded", async () => {
97
+ // Assuming GitHubComments is loaded globally or imported.
98
+ // Since the original used a global script, we might need to import it here or assume it's available.
99
+ // For now, we'll assume the user includes the standard.comment.js script separately or we should import it.
100
+ // But standard.comment.js is in dist/.
101
+
102
+ if (typeof GitHubComments !== 'undefined') {
103
+ const comments = new GitHubComments(initOptions);
104
+ try {
105
+ await comments.load();
106
+ comments.render();
107
+ } catch (error) {
108
+ console.error("Error loading comments:", error);
109
+ const container = document.querySelector("#comments");
110
+ if (container) container.innerHTML = '<p class="error">Failed to load comments. Please try again later.</p>';
111
+ }
112
+ comments.attachFormHandler();
113
+ }
114
+ });
115
+ </script>
package/README.md ADDED
@@ -0,0 +1,404 @@
1
+ ---
2
+ title: "@stnd/modules"
3
+ aliases: []
4
+ created: 2026-07-04 23:28
5
+ modified: 2026-07-05 19:23
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)/modules
23
+
24
+ > *The spine of the Standard application.*
25
+
26
+ `@stnd/modules` is the discovery engine and runtime loader for the Standard vertical slice architecture. Each `*.module.js` manifest declares a self-contained section of your application — routes, styles, components, middleware, and actions in one folder.
27
+
28
+ The name reflects the modular, self-contained units of the application. Remove one, and that feature disappears cleanly.
29
+
30
+ ## ELI5
31
+
32
+ A *“module”* is one feature, in one folder: its routes, its styles, its components, all together. Want to add an RSS feed? `moduleLoad: ["@stnd/modules/rss"]` and it’s there — routes and all. Want to remove a feature entirely? Delete its folder. Nothing else in the app needs to know or change. This is the pattern behind everything: Standard itself ships as a pile of small modules (`@stnd/modules/robots`, `/sitemap`, `/rss`, `/toast`…) rather than one big framework blob.
33
+
34
+ **Use a built-in module (in `astro.config.mjs`):**
35
+
36
+ ```javascript
37
+ standard({ moduleLoad: ["@stnd/modules/rss", "@stnd/modules/humans"] });
38
+ ```
39
+
40
+ **Make your own:** `stnd module <name>` (see `packages/cli`) scaffolds `modules/<name>/index.module.js` for you.
41
+
42
+ ## Manifest Schema
43
+
44
+ A module manifest must be named `*.module.{js,ts}` (convention: `index.module.js`) and `export default { … }` with this shape:
45
+
46
+ ```javascript
47
+ // REQUIRED
48
+ id: string // unique module id (e.g., "my-feature")
49
+ name: string // human-friendly label
50
+ description?: string // human-friendly description
51
+
52
+ // Conditional Loading
53
+ status?: "disabled" // skip this module entirely
54
+ environment?: string | string[]
55
+ // Restrict to specific Astro commands: "dev", "build", "preview"
56
+ // Accepts a single string or an array (e.g., ["dev", "preview"])
57
+ // Omit to load in all environments (default)
58
+
59
+ // Unified Hooks (Logic & Interface)
60
+ // ---------------------------------------------------------------------------
61
+ // Standard automatically routes hooks based on their file extension:
62
+ // - .js, .ts -> LOGIC (Listeners / Handlers)
63
+ // - .astro, .svelte, .md -> UI (Components / Plugs)
64
+ //
65
+ // These can be a single string or an array of entries.
66
+ hooks?: {
67
+ [hookName: string]: string | Array<string | HookEntry>
68
+ }
69
+
70
+ // Routes (Astro)
71
+ routes?: Array<{
72
+ path: string // URL pattern (e.g., "/robots.txt")
73
+ entrypoint: string // relative to folio dir (e.g., "./route.js" or "./route.astro")
74
+ }>
75
+
76
+ // Styles
77
+ // - starts with "@" → imported as-is (package import)
78
+ // - else resolved relative to module dir and injected via injectScript("page-ssr")
79
+ styles?: string[]
80
+
81
+ // Scripts
82
+ // - starts with "@" → imported as-is
83
+ // - else resolved relative to module dir and injected on the client page
84
+ scripts?: string[]
85
+
86
+ // Head entries
87
+ // - string → imported like styles (SSR import)
88
+ // - { inline: string } → injected as inline head script
89
+ head?: Array<string | { inline: string }>
90
+
91
+ // Middleware
92
+ // - string → entrypoint, order defaults to 0
93
+ // - { entrypoint: string; order?: number }
94
+ middleware?: Array<string | { entrypoint: string; order?: number }>
95
+
96
+ // Astro integrations (passed through)
97
+ integrations?: Array<any>
98
+
99
+ // Actions (Astro Actions)
100
+ // - string → path to file exporting actions object(s)
101
+ actions?: string
102
+
103
+ // Content Collections
104
+ // - string → path to file exporting collections (e.g., "./content.ts")
105
+ content?: string
106
+
107
+ // Dependencies (other modules this one requires)
108
+ dependencies?: string[]
109
+ ```
110
+
111
+ ## Unified Hooks Architecture
112
+
113
+ The `hooks` object is the brain of your module. It handles both system events and UI injection.
114
+
115
+ ### 1. Integration Hooks (Logic)
116
+
117
+ If the hook name starts with `astro:` or the entry ends in `.js`/`.ts`, it’s treated as logic.
118
+
119
+ ```javascript
120
+ // index.module.js
121
+ export default {
122
+ id: "my-feature",
123
+ hooks: {
124
+ "astro:config:setup": "./hooks/setup.js", // Astro native hook
125
+ "app:init": "./hooks/init.ts", // Custom app hook
126
+ },
127
+ };
128
+ ```
129
+
130
+ ### 2. Interface Hooks (UI Plugs)
131
+
132
+ If the entry ends in `.astro`, `.svelte`, `.md`, or any other format, it’s treated as a UI component.
133
+
134
+ ```javascript
135
+ // index.module.js
136
+ export default {
137
+ id: "my-feature",
138
+ hooks: {
139
+ "header:top": ["./components/Banner.astro"],
140
+ "footer:bottom": "./components/Copyright.astro",
141
+ },
142
+ };
143
+ ```
144
+
145
+ ### Consuming Hooks
146
+
147
+ **UI Rendering (Zones):**
148
+
149
+ In your Layout or components, use the `<Hook />` component to render all registered components for a hook ID.
150
+
151
+ ```astro
152
+ ---
153
+ import Hook from "@stnd/core/Hook";
154
+ ---
155
+
156
+ <header>
157
+ <Hook id="header:top" props={{ theme: "dark" }} />
158
+ </header>
159
+ ```
160
+
161
+ **Logic Execution:**
162
+
163
+ Trigger logic hooks via the virtual module.
164
+
165
+ ```javascript
166
+ import { runHook } from "virtual:stnd/hooks";
167
+
168
+ await runHook("app:init", { some: "data" });
169
+ ```
170
+
171
+ ### Middleware
172
+
173
+ Module middlewares are native Astro Middlewares. They must follow the `(context, next)` signature and call `next()` to continue the chain.
174
+
175
+ ```javascript
176
+ // index.module.js
177
+ export default {
178
+ id: "auth",
179
+ middleware: [{ entrypoint: "./middleware.js", order: -100 }],
180
+ };
181
+ ```
182
+
183
+ ```javascript
184
+ // middleware.js
185
+ import { defineMiddleware } from "astro:middleware";
186
+
187
+ export const onRequest = defineMiddleware(async (context, next) => {
188
+ // Root initialization, auth checks, etc.
189
+ return next();
190
+ });
191
+ ```
192
+
193
+ ### Content Extensions
194
+
195
+ Modules can define Astro Content Collections.
196
+
197
+ ```javascript
198
+ // index.module.js
199
+ export default {
200
+ id: "my-feature",
201
+ content: "./content.ts",
202
+ };
203
+ ```
204
+
205
+ ```typescript
206
+ // content.ts
207
+ import { defineCollection, z } from "astro:content";
208
+ import { glob } from "astro/loaders";
209
+
210
+ export const myCollection = defineCollection({
211
+ loader: glob({ pattern: "*.md", base: "./content/my-collection" }),
212
+ schema: z.object({
213
+ /* ... */
214
+ }),
215
+ });
216
+ ```
217
+
218
+ The application’s `src/content.config.ts` imports and merges these collections:
219
+
220
+ ```typescript
221
+ import { collections as moduleCollections } from "virtual:stnd/content";
222
+
223
+ export const collections = {
224
+ ...moduleCollections,
225
+ };
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Authoring Guide
231
+
232
+ 1. Place modules under `modules/<name>/index.module.js` at the project root.
233
+ 2. Keep logic inside the module; `.astro` files should only consume model instances.
234
+ 3. Import from sibling modules via `@modules/<name>` — this alias is auto-registered by `@stnd/core`.
235
+ 4. Prefer OKLCH and Standard tokens for styles; avoid one-off CSS.
236
+ 5. No backward compatibility — ship only the current shape.
237
+
238
+ ## The `@modules` Import Alias
239
+
240
+ `@stnd/core` automatically registers `@modules` as a Vite alias pointing to the app’s `modules/` directory. Every app gets this for free — no manual tsconfig or Vite config needed.
241
+
242
+ ```javascript
243
+ import { Note } from "@modules/spine/models/Note";
244
+ import Author from "@modules/spine/models/Author";
245
+ import Base from "@modules/base/layouts/Base.astro";
246
+ ```
247
+
248
+ The corresponding `tsconfig.json` path (for editor intellisense):
249
+
250
+ ```json
251
+ {
252
+ "compilerOptions": {
253
+ "paths": {
254
+ "@modules/*": ["modules/*"]
255
+ }
256
+ }
257
+ }
258
+ ```
259
+
260
+ ## Boundary Rules
261
+
262
+ Modules follow strict vertical slice isolation enforced by `dependency-cruiser`:
263
+
264
+ - **Foundation modules** (`models`, `core`) — importable by any module
265
+ - **Feature modules** (everything else) — must NOT import from sibling feature modules
266
+ - **One-way dependencies** — features → foundation → `@stnd/*` packages, never reversed
267
+ - **No circular deps** — within or across modules
268
+
269
+ Run the boundary check:
270
+
271
+ ```bash
272
+ pnpm boundaries:gd # Check Standard Garden
273
+ pnpm boundaries:ade # Check L'art d'enseigner
274
+ ```
275
+
276
+ ## Loader Behavior
277
+
278
+ - Discovers `**/*.module.{js,ts}` in the configured `moduleFolder` (default: `modules`).
279
+ - `moduleLoad` in `astro.config` accepts:
280
+ - Bare names (auto-prefixed): `"launcher"`, `"design"`, etc.
281
+ - Explicit specifiers: `"@stnd/modules/design"` or `"./local/feature"`.
282
+ - Routes, styles, scripts, head, middleware are injected per manifest.
283
+ - Integrations are forwarded to Astro via `updateConfig`.
284
+ - UI/Component extensions are exposed via `virtual:stnd/components`.
285
+ - Client payload strips infrastructure keys; keeps `__importPath` for server use.
286
+
287
+ ## Disabling a Module
288
+
289
+ Prefix the folder name with `_` to temporarily disable without deleting:
290
+
291
+ ```bash
292
+ mv modules/export/ modules/_export/ # Disabled
293
+ mv modules/_export/ modules/export/ # Re-enabled
294
+ ```
295
+
296
+ The loader skips any folders starting with `_`.
297
+
298
+ ## Environment-Gated Modules
299
+
300
+ Restrict a module to specific Astro commands (`dev`, `build`, or `preview`) using the `environment` field. The module is skipped entirely when the current command doesn’t match.
301
+
302
+ ```javascript
303
+ // Only loaded during `astro dev`
304
+ export default {
305
+ id: "dev-tools",
306
+ name: "Dev Tools",
307
+ environment: "dev",
308
+ };
309
+
310
+ // Loaded during `astro dev` and `astro preview`, but not `astro build`
311
+ export default {
312
+ id: "staging-tools",
313
+ name: "Staging Tools",
314
+ environment: ["dev", "preview"],
315
+ };
316
+ ```
317
+
318
+ Omit the field to load in all environments (the default). When a module is skipped, its routes, styles, scripts, middleware, and hooks are completely absent from the build — as if the module didn’t exist.
319
+
320
+ ## Shipped Modules
321
+
322
+ These built-in modules come with `@stnd/modules` and can be loaded via `moduleLoad`:
323
+
324
+ ### Gold Standard (loaded by default)
325
+
326
+ Every `@stnd` site ships with these. Opt out via `moduleExclude`.
327
+
328
+ | Module | ID | Route | What it does |
329
+ | :----------------------- | :-------------- | :------------------ | :----------------------------------------------------------------- |
330
+ | `@stnd/modules/styles` | `stnd-styles` | — | Injects the Standard design stylesheet |
331
+ | `@stnd/modules/robots` | `stnd-robots` | `/robots.txt` | Generates `robots.txt` from site config |
332
+ | `@stnd/modules/headers` | `stnd-headers` | `/_headers` | Emits security headers (HSTS, X-Frame-Options, Permissions-Policy) |
333
+ | `@stnd/modules/manifest` | `stnd-manifest` | `/site.webmanifest` | Serves the web app manifest |
334
+ | `@stnd/modules/sitemap` | `stnd-sitemap` | — | Sitemap generation via `@astrojs/sitemap` |
335
+
336
+ ### Opt-In Modules
337
+
338
+ Load these explicitly via `moduleLoad` when your site needs them.
339
+
340
+ | Module | ID | Route | What it does |
341
+ | :--------------------------- | :------------------ | :-------------------------- | :----------------------------------------------------- |
342
+ | `@stnd/modules/rss` | `stnd-rss` | `/rss.xml` | Generates an RSS 2.0 feed from site content and config |
343
+ | `@stnd/modules/security-txt` | `stnd-security-txt` | `/.well-known/security.txt` | RFC 9116 security contact disclosure |
344
+ | `@stnd/modules/humans` | `stnd-humans` | `/humans.txt` | The people and tools behind the site |
345
+ | `@stnd/modules/themes` | `stnd-themes` | — | Theme/temperament stylesheet injection |
346
+ | `@stnd/modules/lab` | `stnd-lab` | — | StandardLab CSS inspector (dev tool) |
347
+ | `@stnd/modules/content` | `stnd-content` | `/[…slug]` | Content collection catch-all route |
348
+ | `@stnd/modules/maintenance` | `stnd-maintenance` | `/maintenance` | Maintenance mode with redirect middleware |
349
+
350
+ ## Usage in an App
351
+
352
+ Gold standard modules load automatically — just add your fonts, themes, and features:
353
+
354
+ ```javascript
355
+ // astro.config.mjs
356
+ import standard from "@stnd/core";
357
+
358
+ export default defineConfig({
359
+ integrations: [
360
+ standard({
361
+ // Gold standard modules load automatically:
362
+ // styles, robots, headers, manifest, sitemap, @stnd/fonts/inter
363
+
364
+ // Add your own modules on top of the defaults
365
+ moduleLoad: [
366
+ "@stnd/modules/rss",
367
+ "@stnd/modules/humans",
368
+ "@stnd/modules/security-txt",
369
+ "@stnd/fonts/kalice",
370
+ "@stnd/themes/editorial",
371
+ ],
372
+ }),
373
+ ],
374
+ });
375
+ ```
376
+
377
+ To opt out of a specific default, use `moduleExclude`:
378
+
379
+ ```javascript
380
+ standard({
381
+ // Everything except the sitemap
382
+ moduleExclude: ["@stnd/modules/sitemap"],
383
+ moduleLoad: ["@stnd/modules/rss"],
384
+ });
385
+ ```
386
+
387
+ ## Philosophy
388
+
389
+ - **Vertical slice**: each module is self-contained — a section of the application.
390
+ - **Strict boundaries**: features don’t cross-import. Dependencies flow one way.
391
+ - **Zero shims**: no legacy flags, no backward compatibility layers.
392
+ - **Performance and clarity**: small, explicit manifests; no hidden magic.
393
+
394
+ ## Notes / Observations
395
+
396
+ *(jot down anything noticed here — quirks, gotchas, ideas)*
397
+
398
+ ## Todo
399
+
400
+ - [ ] Nothing tracked yet. [priority:: 3] [token_scale:: 3] [created:: 2026-07-14] [area:: framework]
401
+
402
+ ---
403
+
404
+ *“A well-bound app holds together not because of glue, but because every module knows its place.”*
@@ -0,0 +1,60 @@
1
+ ---
2
+ /**
3
+ * ReadingTime Component
4
+ * Calculates and displays reading time estimate for given text
5
+ */
6
+ interface Props {
7
+ text?: string;
8
+ wordsPerMinute?: number;
9
+ showIcon?: boolean;
10
+ prefix?: string;
11
+ suffix?: string;
12
+ class?: string;
13
+ }
14
+
15
+ const {
16
+ text = "",
17
+ wordsPerMinute = 225,
18
+ showIcon = false,
19
+ prefix = " - ",
20
+ suffix = "",
21
+ class: className = "",
22
+ } = Astro.props;
23
+
24
+ // Return empty string if no text provided
25
+ if (!text || typeof text !== "string") {
26
+ return "";
27
+ }
28
+
29
+ // Count words by splitting on any whitespace
30
+ const wordCount = text.split(/\s+/g).filter((word) => word.length > 0).length;
31
+ const minutes = Math.floor(wordCount / wordsPerMinute);
32
+
33
+ // Format the reading time
34
+ let readingTime = "";
35
+ if (minutes === 1) {
36
+ readingTime = `${prefix}1 minute${suffix}`;
37
+ } else if (minutes > 1) {
38
+ readingTime = `${prefix}${minutes} minutes${suffix}`;
39
+ } else {
40
+ readingTime = `${prefix}less than 1 minute${suffix}`;
41
+ }
42
+
43
+ // Icon SVG (optional)
44
+ const icon = showIcon
45
+ ? `
46
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px; vertical-align: middle;">
47
+ <circle cx="12" cy="12" r="10"></circle>
48
+ <polyline points="12,6 12,12 16,14"></polyline>
49
+ </svg>
50
+ `
51
+ : "";
52
+ ---
53
+
54
+ <span
55
+ class:list={["reading-time", className]}
56
+ aria-label={`Estimated reading time: ${minutes} ${minutes === 1 ? "minute" : "minutes"}`}
57
+ >
58
+ <Fragment set:html={icon} />
59
+ <Fragment set:html={readingTime} />
60
+ </span>