@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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import config from "virtual:stnd/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* /.well-known/security.txt — RFC 9116
|
|
5
|
+
*
|
|
6
|
+
* Generates a standards-compliant security.txt so security researchers
|
|
7
|
+
* know how to report vulnerabilities. Config-driven via standard() options.
|
|
8
|
+
*
|
|
9
|
+
* @see https://securitytxt.org/
|
|
10
|
+
* @see https://www.rfc-editor.org/rfc/rfc9116
|
|
11
|
+
*/
|
|
12
|
+
export async function GET(context) {
|
|
13
|
+
const securityTxt = config.securityTxt || {};
|
|
14
|
+
if (securityTxt.enabled === false) {
|
|
15
|
+
return new Response(null, { status: 404 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const site = context.site
|
|
19
|
+
? context.site.toString().replace(/\/$/, "")
|
|
20
|
+
: "https://example.com";
|
|
21
|
+
|
|
22
|
+
// Contact is REQUIRED by RFC 9116
|
|
23
|
+
const contacts = Array.isArray(securityTxt.contact)
|
|
24
|
+
? securityTxt.contact
|
|
25
|
+
: securityTxt.contact
|
|
26
|
+
? [securityTxt.contact]
|
|
27
|
+
: config.author?.email
|
|
28
|
+
? [`mailto:${config.author.email}`]
|
|
29
|
+
: [`${site}/security`];
|
|
30
|
+
|
|
31
|
+
// Expires is REQUIRED by RFC 9116 — default to 1 year from build time
|
|
32
|
+
const expires =
|
|
33
|
+
securityTxt.expires ||
|
|
34
|
+
new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
|
|
35
|
+
|
|
36
|
+
const preferredLanguages = securityTxt.preferredLanguages ||
|
|
37
|
+
config.language ||
|
|
38
|
+
"en";
|
|
39
|
+
|
|
40
|
+
let content = `# security.txt — RFC 9116
|
|
41
|
+
# ${site}
|
|
42
|
+
# Generated by Standard Framework
|
|
43
|
+
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
// Contact (REQUIRED, can have multiple)
|
|
47
|
+
contacts.forEach((c) => {
|
|
48
|
+
content += `Contact: ${c}\n`;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Expires (REQUIRED)
|
|
52
|
+
content += `Expires: ${expires}\n`;
|
|
53
|
+
|
|
54
|
+
// Preferred-Languages
|
|
55
|
+
content += `Preferred-Languages: ${preferredLanguages}\n`;
|
|
56
|
+
|
|
57
|
+
// Canonical (RECOMMENDED)
|
|
58
|
+
const canonical =
|
|
59
|
+
securityTxt.canonical || `${site}/.well-known/security.txt`;
|
|
60
|
+
content += `Canonical: ${canonical}\n`;
|
|
61
|
+
|
|
62
|
+
// Optional fields
|
|
63
|
+
if (securityTxt.encryption) {
|
|
64
|
+
const keys = Array.isArray(securityTxt.encryption)
|
|
65
|
+
? securityTxt.encryption
|
|
66
|
+
: [securityTxt.encryption];
|
|
67
|
+
keys.forEach((k) => {
|
|
68
|
+
content += `Encryption: ${k}\n`;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (securityTxt.acknowledgments) {
|
|
73
|
+
content += `Acknowledgments: ${securityTxt.acknowledgments}\n`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (securityTxt.policy) {
|
|
77
|
+
content += `Policy: ${securityTxt.policy}\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (securityTxt.hiring) {
|
|
81
|
+
content += `Hiring: ${securityTxt.hiring}\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return new Response(content, {
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
87
|
+
"Cache-Control": "public, max-age=86400",
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap core module
|
|
3
|
+
*
|
|
4
|
+
* Registers the @astrojs/sitemap integration via the Standard module system.
|
|
5
|
+
* Uses global Standard config (if present) to allow consumers to override options.
|
|
6
|
+
*/
|
|
7
|
+
import sitemap from "@astrojs/sitemap";
|
|
8
|
+
|
|
9
|
+
const standardConfig = globalThis.__STANDARD_CONFIG__ || {};
|
|
10
|
+
|
|
11
|
+
const sitemapOptions = {
|
|
12
|
+
// Default: exclude API and underscore-prefixed routes
|
|
13
|
+
filter: (page) => !page.includes("/_") && !page.includes("/api/"),
|
|
14
|
+
...(standardConfig.sitemap || {}),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
id: "stnd-sitemap",
|
|
19
|
+
name: "Stnd :: Sitemap",
|
|
20
|
+
description: "Adds sitemap generation via the Standard module system.",
|
|
21
|
+
integrations: [sitemap(sitemapOptions)],
|
|
22
|
+
};
|
package/stripe/client.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe Client
|
|
3
|
+
* Frontend utility to trigger standard checkout (or mock mode)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const stripeClient = {
|
|
7
|
+
/**
|
|
8
|
+
* Trigger a mock checkout to bypass the real Stripe flow during dev/testing.
|
|
9
|
+
* @param {string} formationSlug - the slug of the product/formation
|
|
10
|
+
*/
|
|
11
|
+
async mockCheckout(formationSlug) {
|
|
12
|
+
const res = await fetch("/api/stripe/mock-checkout", {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json"
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({ formationSlug })
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
throw new Error(data?.error || "Failed to process mock checkout");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return await res.json();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { useSession } from "@stnd/account/client";
|
|
3
|
+
import { launcherView } from "@stnd/launcher/launcher";
|
|
4
|
+
import Icon from "@stnd/icon/Icon.svelte";
|
|
5
|
+
|
|
6
|
+
let { formationSlug, title = "Mock Buy" } = $props();
|
|
7
|
+
let loading = $state(false);
|
|
8
|
+
|
|
9
|
+
// Watch session state to gracefully update UI or conditionally show logic
|
|
10
|
+
const session = useSession();
|
|
11
|
+
|
|
12
|
+
async function handleMockBuy() {
|
|
13
|
+
if (!$session?.data?.user) {
|
|
14
|
+
window.toast("You need to login first before buying.", "error");
|
|
15
|
+
window.dispatchEvent(
|
|
16
|
+
new CustomEvent("open-launcher", {
|
|
17
|
+
detail: { viewTrigger: "::login" },
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
launcherView("::checkout", {
|
|
24
|
+
slug: formationSlug,
|
|
25
|
+
title: title.replace("Acheter : ", ""),
|
|
26
|
+
price: 97, // Hardcoded demo price
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<button
|
|
32
|
+
onclick={handleMockBuy}
|
|
33
|
+
disabled={loading}
|
|
34
|
+
class="secondary flex-far mx-auto"
|
|
35
|
+
>
|
|
36
|
+
<Icon icon="cart" />
|
|
37
|
+
<span>{title}</span>
|
|
38
|
+
</button>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe Module Manifest
|
|
3
|
+
*
|
|
4
|
+
* Exposes a Stripe integration module for commerce and formations.
|
|
5
|
+
* Includes a mock purchase service to unlock specific formations during development.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
id: "stnd-stripe",
|
|
9
|
+
name: "Stnd :: Stripe",
|
|
10
|
+
description: "Commerce Integration and Mock Checkout Service",
|
|
11
|
+
routes: [
|
|
12
|
+
{
|
|
13
|
+
path: "/api/stripe/mock-checkout",
|
|
14
|
+
entrypoint: "./routes/mock-checkout.js",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
aliases: {
|
|
18
|
+
"@stripe/client": "./client.js",
|
|
19
|
+
"@stripe/components": "./components",
|
|
20
|
+
},
|
|
21
|
+
hooks: {
|
|
22
|
+
"launcher:view": [
|
|
23
|
+
{
|
|
24
|
+
trigger: "::checkout",
|
|
25
|
+
component: "./views/CheckoutView.svelte",
|
|
26
|
+
meta: {
|
|
27
|
+
title: "Checkout",
|
|
28
|
+
size: { width: "standard", height: "auto" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { initAuth } from "@stnd/account/auth";
|
|
2
|
+
import { env as cfEnv } from "cloudflare:workers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mock Checkout API Route Handler
|
|
6
|
+
*
|
|
7
|
+
* Simulates purchasing a product (like a formation) by modifying
|
|
8
|
+
* the user's `access_rights` JSON directly.
|
|
9
|
+
*/
|
|
10
|
+
export const POST = async (context) => {
|
|
11
|
+
const { request, locals } = context;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Safe environment discovery for Astro v6 / Cloudflare
|
|
15
|
+
const env = cfEnv || locals?.env || locals?.cfContext?.env || {};
|
|
16
|
+
const auth = initAuth(env);
|
|
17
|
+
|
|
18
|
+
// 1. Verify user session
|
|
19
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
20
|
+
if (!session?.user) {
|
|
21
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const body = await request.json();
|
|
25
|
+
const { formationSlug } = body;
|
|
26
|
+
|
|
27
|
+
if (!formationSlug) {
|
|
28
|
+
return new Response(JSON.stringify({ error: "Missing formationSlug" }), { status: 400 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Parse existing rights
|
|
32
|
+
const user = session.user;
|
|
33
|
+
let rights = {};
|
|
34
|
+
if (typeof user.access_rights === "string") {
|
|
35
|
+
try {
|
|
36
|
+
rights = JSON.parse(user.access_rights);
|
|
37
|
+
} catch (e) { }
|
|
38
|
+
} else if (typeof user.access_rights === "object") {
|
|
39
|
+
rights = user.access_rights || {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3. Grant access directly into rights structure
|
|
43
|
+
rights[formationSlug] = {
|
|
44
|
+
granted_at: new Date().toISOString(),
|
|
45
|
+
source: "mock_stripe",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// 4. Update User directly in DB
|
|
49
|
+
const db = env.ADE_DB || env.DB || env.Garden;
|
|
50
|
+
if (!db) {
|
|
51
|
+
throw new Error("Database not available for update");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await db.prepare("UPDATE user SET access_rights = ?, updatedAt = ? WHERE id = ?")
|
|
55
|
+
.bind(JSON.stringify(rights), Date.now(), user.id)
|
|
56
|
+
.run();
|
|
57
|
+
|
|
58
|
+
return new Response(JSON.stringify({ success: true, message: `Access granted for ${formationSlug}` }), { status: 200 });
|
|
59
|
+
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error("Mock checkout failed:", e);
|
|
62
|
+
return new Response(JSON.stringify({ error: e.message || "Failed" }), { status: 500 });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { stripeClient } from "@stripe/client";
|
|
3
|
+
import { launcherClose } from "@stnd/launcher/launcher";
|
|
4
|
+
import Icon from "@stnd/icon/Icon.svelte";
|
|
5
|
+
|
|
6
|
+
let { slug, title = "Formation", price = 97 } = $props();
|
|
7
|
+
let loading = $state(false);
|
|
8
|
+
let success = $state(false);
|
|
9
|
+
|
|
10
|
+
// Mock Form State
|
|
11
|
+
let cardName = $state("testtetstsetst");
|
|
12
|
+
let cardNumber = $state("4242 4242 4242 4242");
|
|
13
|
+
let cardExpiry = $state("12/28");
|
|
14
|
+
let cardCvc = $state("123");
|
|
15
|
+
|
|
16
|
+
async function handlePayment(e) {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
loading = true;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Artificial delay for "WOW" effect of payment processing
|
|
22
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
23
|
+
|
|
24
|
+
await stripeClient.mockCheckout(slug);
|
|
25
|
+
success = true;
|
|
26
|
+
|
|
27
|
+
window.toast(`Paiement réussi pour : ${title}`, "success");
|
|
28
|
+
|
|
29
|
+
// Brief success screen before transition
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
launcherClose();
|
|
32
|
+
window.location.reload();
|
|
33
|
+
}, 2000);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
window.toast(e.message, "error");
|
|
36
|
+
} finally {
|
|
37
|
+
loading = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div class="checkout-view rhythm card flex-col" style="align-items: center;">
|
|
43
|
+
{#if !success}
|
|
44
|
+
<header class="flex-between items-start">
|
|
45
|
+
<div>
|
|
46
|
+
<h2 class="mb-0">{title}</h2>
|
|
47
|
+
<p class="text-sm text-muted">Accès immédiat et à vie</p>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="badge accent mb-0">
|
|
50
|
+
{price}$
|
|
51
|
+
</div>
|
|
52
|
+
</header>
|
|
53
|
+
|
|
54
|
+
<form onsubmit={handlePayment} class="rhythm">
|
|
55
|
+
<div class="card">
|
|
56
|
+
<label for="card-details" class="small bold uppercase muted mb-2 block"
|
|
57
|
+
>Détails de paiement</label
|
|
58
|
+
>
|
|
59
|
+
|
|
60
|
+
<div class="card-input rhythm-xs">
|
|
61
|
+
<input
|
|
62
|
+
type="text"
|
|
63
|
+
placeholder="Nom sur la carte"
|
|
64
|
+
bind:value={cardName}
|
|
65
|
+
class="ghost-input w-full"
|
|
66
|
+
required
|
|
67
|
+
/>
|
|
68
|
+
<div class="divider"></div>
|
|
69
|
+
<div class="flex gap items-center">
|
|
70
|
+
<Icon icon="cart" class="muted h4" />
|
|
71
|
+
<input
|
|
72
|
+
type="text"
|
|
73
|
+
bind:value={cardNumber}
|
|
74
|
+
placeholder="Numéro de carte"
|
|
75
|
+
class="ghost-input flex-1"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="flex gap">
|
|
79
|
+
<input
|
|
80
|
+
type="text"
|
|
81
|
+
bind:value={cardExpiry}
|
|
82
|
+
placeholder="MM/AA"
|
|
83
|
+
class="ghost-input w-24"
|
|
84
|
+
/>
|
|
85
|
+
<input
|
|
86
|
+
type="text"
|
|
87
|
+
bind:value={cardCvc}
|
|
88
|
+
placeholder="CVC"
|
|
89
|
+
class="ghost-input w-24"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<button type="submit" disabled={loading} class="secondary">
|
|
96
|
+
{#if loading}
|
|
97
|
+
<Icon icon="loading" class="animate-spin" />
|
|
98
|
+
Traitement...
|
|
99
|
+
{:else}
|
|
100
|
+
<Icon icon="lock" />
|
|
101
|
+
Payer {price}$
|
|
102
|
+
{/if}
|
|
103
|
+
</button>
|
|
104
|
+
|
|
105
|
+
<em class="text-center text-sm text-muted">
|
|
106
|
+
<Icon icon="admin" class="inline mb-0" /> Paiement sécurisé par Stripe (Mode
|
|
107
|
+
Test)
|
|
108
|
+
</em>
|
|
109
|
+
</form>
|
|
110
|
+
{:else}
|
|
111
|
+
<div class="success-screen rhythm">
|
|
112
|
+
<Icon icon="check" class="h-2 w-2" />
|
|
113
|
+
|
|
114
|
+
<h2>Paiement Partout !</h2>
|
|
115
|
+
<p class="text-muted">
|
|
116
|
+
Votre accès à <strong>{title}</strong> a été activé.
|
|
117
|
+
</p>
|
|
118
|
+
<em class="text-sm">Redirection dans quelques secondes...</em>
|
|
119
|
+
</div>
|
|
120
|
+
{/if}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<style>
|
|
124
|
+
.price-badge {
|
|
125
|
+
font-variant-numeric: tabular-nums;
|
|
126
|
+
font-weight: 800;
|
|
127
|
+
color: var(--color-accent);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.ghost-input {
|
|
131
|
+
background: transparent;
|
|
132
|
+
border: none;
|
|
133
|
+
outline: none;
|
|
134
|
+
font-size: 1rem;
|
|
135
|
+
padding: 0.5rem 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.ghost-input::placeholder {
|
|
139
|
+
color: var(--color-subtle);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.divider {
|
|
143
|
+
height: 1px;
|
|
144
|
+
background: var(--color-border);
|
|
145
|
+
margin: 0.25rem 0;
|
|
146
|
+
opacity: 0.5;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.success-icon {
|
|
150
|
+
display: inline-flex;
|
|
151
|
+
animation: scale-in 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@keyframes scale-in {
|
|
155
|
+
from {
|
|
156
|
+
transform: scale(0);
|
|
157
|
+
opacity: 0;
|
|
158
|
+
}
|
|
159
|
+
to {
|
|
160
|
+
transform: scale(1);
|
|
161
|
+
opacity: 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
:global(.animate-spin) {
|
|
166
|
+
animation: spin 1s linear infinite;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@keyframes spin {
|
|
170
|
+
from {
|
|
171
|
+
transform: rotate(0deg);
|
|
172
|
+
}
|
|
173
|
+
to {
|
|
174
|
+
transform: rotate(360deg);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.bg-success-soft {
|
|
179
|
+
background: rgba(var(--color-success-rgb), 0.1);
|
|
180
|
+
}
|
|
181
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Styles module
|
|
3
|
+
*
|
|
4
|
+
* Injects the Standard design stylesheet as a root style.
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
id: "stnd-styles",
|
|
8
|
+
name: "Stnd :: Styles",
|
|
9
|
+
description: "Loads the Standard design stylesheet.",
|
|
10
|
+
styles: ["@stnd/styles/standard.scss"],
|
|
11
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Theme Utils Module
|
|
2
|
+
|
|
3
|
+
Global theme switcher for managing themes across the entire site.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Add to your base layout (e.g., `Base.astro`):
|
|
8
|
+
|
|
9
|
+
```astro
|
|
10
|
+
<script>
|
|
11
|
+
import { initThemeSwitcher } from "@stnd/modules/theme-utils/client.js";
|
|
12
|
+
initThemeSwitcher();
|
|
13
|
+
</script>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### Buttons
|
|
19
|
+
|
|
20
|
+
Add buttons anywhere with `data-theme` attribute:
|
|
21
|
+
|
|
22
|
+
```astro
|
|
23
|
+
<button data-theme="technical">Technical</button>
|
|
24
|
+
<button data-theme="academic">Academic</button>
|
|
25
|
+
<button data-theme="garden">Garden</button>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Programmatic Control
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { setTheme, getCurrentTheme } from "@stnd/modules/theme-utils/client.js";
|
|
32
|
+
|
|
33
|
+
// Set theme
|
|
34
|
+
setTheme("technical");
|
|
35
|
+
|
|
36
|
+
// Get current theme
|
|
37
|
+
const theme = getCurrentTheme(); // "technical"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Custom Configuration
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
initThemeSwitcher({
|
|
44
|
+
storageKey: "my-theme-key",
|
|
45
|
+
defaultTheme: "academic",
|
|
46
|
+
selector: "[data-switch-theme]"
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## How It Works
|
|
51
|
+
|
|
52
|
+
- Listens for clicks on elements with `data-theme` attribute
|
|
53
|
+
- Sets `data-theme` on `<html>` element
|
|
54
|
+
- Persists selection to localStorage
|
|
55
|
+
- Restores theme on page load
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
### `initThemeSwitcher(options)`
|
|
60
|
+
|
|
61
|
+
Initialize the switcher.
|
|
62
|
+
|
|
63
|
+
**Options:**
|
|
64
|
+
- `storageKey` - localStorage key (default: `"stnd-theme-global"`)
|
|
65
|
+
- `defaultTheme` - fallback theme (default: `"garden"`)
|
|
66
|
+
- `selector` - button selector (default: `"button[data-theme]"`)
|
|
67
|
+
|
|
68
|
+
### `setTheme(themeName, storageKey?)`
|
|
69
|
+
|
|
70
|
+
Programmatically set theme.
|
|
71
|
+
|
|
72
|
+
### `getCurrentTheme(storageKey?)`
|
|
73
|
+
|
|
74
|
+
Get current theme from localStorage.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Switcher Client
|
|
3
|
+
* Provides global theme switching functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Initialize global theme switcher
|
|
8
|
+
* Listens for clicks on elements with data-theme attribute
|
|
9
|
+
* Applies theme to document root and persists to localStorage
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {string} options.storageKey - localStorage key (default: "stnd-theme-global")
|
|
13
|
+
* @param {string} options.defaultTheme - fallback theme (default: "garden")
|
|
14
|
+
* @param {string} options.selector - button selector (default: "button[data-theme]")
|
|
15
|
+
*/
|
|
16
|
+
export function initThemeSwitcher(options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
storageKey = "stnd-theme-global",
|
|
19
|
+
defaultTheme = document.documentElement.getAttribute("data-theme") || "garden",
|
|
20
|
+
selector = "button[data-theme]",
|
|
21
|
+
} = options;
|
|
22
|
+
|
|
23
|
+
console.log("[theme-utils] Initializing theme switcher");
|
|
24
|
+
console.log("[theme-utils] Default theme from HTML:", defaultTheme);
|
|
25
|
+
|
|
26
|
+
// Restore persisted theme on page load, but only if it's different from the default
|
|
27
|
+
const savedTheme = localStorage.getItem(storageKey);
|
|
28
|
+
const themeToApply = savedTheme || defaultTheme;
|
|
29
|
+
console.log("[theme-utils] Saved theme in storage:", savedTheme);
|
|
30
|
+
console.log("[theme-utils] Applying theme:", themeToApply);
|
|
31
|
+
applyTheme(themeToApply);
|
|
32
|
+
|
|
33
|
+
// Listen for theme switcher button clicks
|
|
34
|
+
document.addEventListener("click", (e) => {
|
|
35
|
+
const target = e.target;
|
|
36
|
+
const btn = target.closest(selector);
|
|
37
|
+
|
|
38
|
+
if (!btn) return;
|
|
39
|
+
|
|
40
|
+
const themeName = btn.getAttribute("data-theme");
|
|
41
|
+
if (themeName) {
|
|
42
|
+
console.log("[theme-utils] Switching to theme:", themeName);
|
|
43
|
+
applyTheme(themeName);
|
|
44
|
+
// Only persist if different from default (don't clutter storage with defaults)
|
|
45
|
+
if (themeName !== defaultTheme) {
|
|
46
|
+
localStorage.setItem(storageKey, themeName);
|
|
47
|
+
} else {
|
|
48
|
+
localStorage.removeItem(storageKey);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Apply theme to document root
|
|
56
|
+
* @param {string} themeName
|
|
57
|
+
*/
|
|
58
|
+
function applyTheme(themeName) {
|
|
59
|
+
document.documentElement.setAttribute("data-theme", themeName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Programmatically set the current theme
|
|
64
|
+
* @param {string} themeName
|
|
65
|
+
* @param {string} storageKey
|
|
66
|
+
*/
|
|
67
|
+
export function setTheme(themeName, storageKey = "stnd-theme-global") {
|
|
68
|
+
applyTheme(themeName);
|
|
69
|
+
localStorage.setItem(storageKey, themeName);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the currently applied theme
|
|
74
|
+
* @param {string} storageKey
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function getCurrentTheme(storageKey = "stnd-theme-global") {
|
|
78
|
+
return localStorage.getItem(storageKey) || "garden";
|
|
79
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Utils Module
|
|
3
|
+
* Global theme switcher for managing data-theme attribute across the site.
|
|
4
|
+
*
|
|
5
|
+
* Auto-initializes on page load. No manual setup needed.
|
|
6
|
+
*
|
|
7
|
+
* Usage — just add buttons anywhere:
|
|
8
|
+
* <button data-theme="technical">Switch to Technical</button>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
id: "stnd-theme-utils",
|
|
13
|
+
name: "Stnd :: Theme Utils",
|
|
14
|
+
description: "Global theme switcher — apply themes via data-theme buttons",
|
|
15
|
+
|
|
16
|
+
scripts: [
|
|
17
|
+
{
|
|
18
|
+
src: "./client.js",
|
|
19
|
+
init: "initThemeSwitcher",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
metadata: {
|
|
24
|
+
version: "0.1.0",
|
|
25
|
+
tags: ["theme", "switcher", "css"],
|
|
26
|
+
},
|
|
27
|
+
};
|