@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.
- package/Backlinks.astro +56 -0
- package/CHANGELOG.md +60 -0
- package/Comments.astro +115 -0
- package/README.md +404 -0
- package/ReadingTime.astro +60 -0
- package/brand-manual/index.module.js +12 -0
- package/brand-manual/route.astro +341 -0
- package/confetti/Confetti.astro +42 -0
- package/confetti/index.module.js +22 -0
- package/content/index.module.js +19 -0
- package/content/route.astro +59 -0
- package/copy-buttons/index.module.js +9 -0
- package/deep-link/index.module.js +9 -0
- package/deep-link/init.js +14 -0
- package/eink/index.module.js +9 -0
- package/fonts/index.module.js +76 -0
- package/gestures/index.module.js +9 -0
- package/gestures/init.js +13 -0
- package/gsap/client.js +41 -0
- package/gsap/index.module.js +20 -0
- package/headers/index.module.js +16 -0
- package/headers/route.js +93 -0
- package/humans/index.module.js +17 -0
- package/humans/route.js +79 -0
- package/iconify/index.module.js +9 -0
- package/image-zoom/index.module.js +9 -0
- package/keyboard/index.module.js +9 -0
- package/lab/index.module.js +26 -0
- package/launcher/index.module.js +27 -0
- package/maintenance/index.module.js +18 -0
- package/maintenance/middleware.js +34 -0
- package/maintenance/routes/index.astro +45 -0
- package/manifest/index.module.js +16 -0
- package/manifest/route.js +57 -0
- package/math/client.js +98 -0
- package/math/index.module.js +19 -0
- package/math/styles.css +25 -0
- package/mermaid/client.js +125 -0
- package/mermaid/index.module.js +19 -0
- package/mermaid/styles.css +114 -0
- package/p5/client.js +25 -0
- package/p5/index.module.js +24 -0
- package/package.json +35 -0
- package/prism/README.md +13 -0
- package/prism/index.module.js +19 -0
- package/robots/index.module.js +16 -0
- package/robots/route.js +67 -0
- package/rss/index.module.js +18 -0
- package/rss/route.js +163 -0
- package/scroll-wrappers/index.module.js +9 -0
- package/security-txt/index.module.js +17 -0
- package/security-txt/route.js +90 -0
- package/sitemap/index.module.js +22 -0
- package/stripe/client.js +27 -0
- package/stripe/components/MockBuyButton.svelte +38 -0
- package/stripe/index.module.js +33 -0
- package/stripe/routes/mock-checkout.js +64 -0
- package/stripe/views/CheckoutView.svelte +181 -0
- package/styles/index.module.js +11 -0
- package/theme-utils/README.md +74 -0
- package/theme-utils/client.js +79 -0
- package/theme-utils/index.module.js +27 -0
- package/themes/index.module.js +50 -0
- package/toast/index.module.js +17 -0
- package/toast/toast.js +17 -0
package/Backlinks.astro
ADDED
|
@@ -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>
|