@stnd/layout 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Base.astro CHANGED
@@ -1,18 +1,17 @@
1
1
  ---
2
2
  /**
3
- * @stnd/layout/Base.astro
3
+ * @component Base
4
+ * @description The definitive root layout for all Standard Garden
5
+ * websites — `<html>`/`<head>`/`<body>`, theme/color-mode/language
6
+ * resolution, and the header/footer slots every page gets by default.
4
7
  *
5
- * The definitive root layout for all Standard Garden websites.
6
- *
7
- * System-layer components (Toast, Confetti, Lab) are no longer hardcoded here.
8
- * They are registered as Gold Standard modules and injected via hook zones:
9
- *
10
- * stnd:base — Static Astro components (Toast, Confetti). No hydration.
11
- * stnd:client — Hydrated Svelte components (Lab). Client-side interactivity.
12
- * backpack — App-level extensions (DropZone, Graft, etc.). Hydrated.
13
- *
14
- * To exclude any of these, use `moduleExclude` in your astro.config:
15
- * standard({ moduleExclude: ["@stnd/modules/confetti"] })
8
+ * System-layer components (Toast, Confetti, Lab) are not hardcoded here —
9
+ * they're registered as Gold Standard modules and injected via `StndInit`'s
10
+ * hook zones: `stnd:base` (static Astro components, no hydration) and
11
+ * `stnd:client` (hydrated Svelte components). To exclude one, use
12
+ * `moduleExclude` in `astro.config`:
13
+ * `standard({ moduleExclude: ["@stnd/modules/confetti"] })`. See
14
+ * [Hooks & Extension Points](/manual/modules/hooks) for the full list.
16
15
  */
17
16
 
18
17
  import "virtual:stnd/styles";
@@ -26,21 +25,26 @@ import LauncherHint from "@stnd/ui/LauncherHint.astro";
26
25
  import { ClientRouter } from "astro:transitions";
27
26
 
28
27
  interface Props {
28
+ /** Page title — passed straight through to `<Meta>`. Defaults to the site's own config title. */
29
29
  title?: string;
30
30
  description?: string;
31
+ /** Social-share image URL. */
31
32
  image?: string;
32
33
  created?: string;
33
34
  modified?: string;
34
35
  tags?: string[];
36
+ /** Theme name — defaults to the visitor's saved preference, then the site's system default. */
35
37
  theme?: string;
36
38
  lang?: string;
39
+ /** Skip the default header/footer entirely (still renders `<html>`/`<head>`/`<body>`). */
37
40
  hideLayout?: boolean;
38
41
  class?: string;
39
42
  inlineStyle?: string;
43
+ /** Extra raw markup injected into `<head>`. */
40
44
  headExtras?: string;
41
45
  visibility?: "public" | "private" | "unlisted";
42
46
 
43
- // Component Configs
47
+ /** Forwarded to `<Header>` — brand text/link and nav items. */
44
48
  header?: {
45
49
  brand?: string;
46
50
  brandHref?: string;
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @stnd/layout
2
2
 
3
+ ## 0.5.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Auto-bumped @stnd/layout due to modified code.
8
+ - Updated dependencies
9
+ - Updated dependencies
10
+ - Updated dependencies
11
+ - @stnd/lab@1.0.2
12
+ - @stnd/log@0.5.1
13
+ - @stnd/ui@0.5.2
14
+
3
15
  ## 0.5.0
4
16
 
5
17
  ### Minor Changes
package/Encrypted.astro CHANGED
@@ -1,4 +1,16 @@
1
1
  ---
2
+ /**
3
+ * @component Encrypted
4
+ * @description A standalone password-gate page layout — not composed with
5
+ * `<Base>`, renders its own full `<html>`. Shows a password form; on
6
+ * submit, XORs the decoded `encryptedData` against a SHA-256 hash of the
7
+ * typed password entirely client-side (no server round-trip) and, if the
8
+ * result looks like real HTML, replaces the form with it in place. Wrong
9
+ * guesses are rate-limited to 5 attempts per page load.
10
+ *
11
+ * @property {Object} [site] - Site info; only `site.language` is read (sets `<html lang>`, default "en").
12
+ * @property {string} encryptedData - Base64-encoded, XOR-"encrypted" HTML payload to reveal on success.
13
+ */
2
14
  import "virtual:stnd/styles";
3
15
 
4
16
  const { site = {}, encryptedData } = Astro.props;
package/Footer.astro CHANGED
@@ -1,30 +1,38 @@
1
1
  ---
2
2
  /**
3
- * Footer.astro — Generic and highly flexible site footer.
3
+ * @component Footer
4
+ * @description Generic, flexible site footer. Reads defaults from
5
+ * `virtual:stnd/config` (title, navigation) so it works with zero props;
6
+ * a `grid-8` layout (3 columns brand/info, 5 for custom link columns),
7
+ * responsive out of the box. Slots: `brand`, `description`, `copyright`,
8
+ * `bottom-left`, `bottom-right`, and default (extra link columns).
4
9
  *
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>
10
+ * @example astro
11
+ * <Footer />
12
+ * <Footer title="My Garden">
13
+ * <div class="col-1">
14
+ * <h4>Section</h4>
15
+ * <ul><li><a href="/">Home</a></li></ul>
16
+ * </div>
17
+ * </Footer>
19
18
  */
20
19
  import config from "virtual:stnd/config";
21
20
 
22
21
  interface Props {
22
+ /** Brand name — defaults to the site config's own title, then "Home". */
23
23
  title?: string;
24
24
  class?: string;
25
+ hideBrand?: boolean;
26
+ /** Hide the nav column entirely (built from `config.nav.footer` / `config.nav.header`). */
27
+ hideNav?: boolean;
25
28
  }
26
29
 
27
- const { title = config.title || "Home", class: className } = Astro.props;
30
+ const {
31
+ title = config.title || "Home",
32
+ class: className,
33
+ hideBrand = false,
34
+ hideNav = false,
35
+ } = Astro.props;
28
36
 
29
37
  const footerNav =
30
38
  config.nav?.footer ??
@@ -35,36 +43,36 @@ const year = new Date().getFullYear();
35
43
 
36
44
  <footer class:list={["stnd-footer", "compact", className]}>
37
45
  <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>
46
+ <!-- Column 1: Brand & Info -->
47
+ {
48
+ !hideBrand && (
49
+ <div class="col-2 sm:col-1">
50
+ <h4 class="footer-title mb-2">
51
+ <a href={config.url} class="no-decoration">{title}</a>
52
+ </h4>
53
+
54
+ {
55
+ footerNav.length > 0 && !hideNav && (
56
+ <ul class="no-bullet">
57
+ {footerNav.map((item: any) => (
58
+ <li>
59
+ <a href={item.url}>{item.title}</a>
60
+ </li>
61
+ ))}
62
+ </ul>
63
+ )
64
+ }
65
+
66
+ {
67
+ config.description && (
68
+ <p class="text-2xs mt-half compact">{config.description}</p>
69
+ )
70
+ }
71
+
72
+ <p class="text-2xs mt-half">&copy; {year}.</p>
73
+ </div>
74
+ )
75
+ }
68
76
 
69
77
  <!-- Column 2: Navigation -->
70
78
  {/*
package/Header.astro CHANGED
@@ -1,19 +1,27 @@
1
1
  ---
2
2
  /**
3
- * Header.astro — Minimalist Responsive Header
3
+ * @component Header
4
+ * @description Minimalist responsive header — brand on the left, nav menu on
5
+ * the right, hamburger on mobile (below 768px). Fixed to the viewport top;
6
+ * hides on scroll-down and reappears on scroll-up when `startHidden` is set.
7
+ * Reads nav defaults from `virtual:stnd/config`. Slots: `brand` (replaces the
8
+ * default brand link), `actions` (page-level buttons — Share/Edit/etc. —
9
+ * rendered before the burger, visible regardless of `menu`/`hideMenu`),
10
+ * `right` (extra content after the nav menu), and two viewport-fixed slots
11
+ * rendered as siblings of the header itself — `bottom-left` / `bottom-right`.
4
12
  *
5
- * Brand on the left, menu on the right, hamburger on mobile.
6
- * Reads site defaults from virtual:stnd/config.
7
- *
8
- * Usage:
9
- * <Header />
10
- * <Header brand="@stnd" items={[{ title: "Docs", url: "/docs" }]} />
13
+ * @example astro
14
+ * <Header />
15
+ * <Header brand="@stnd" items={[{ title: "Docs", url: "/docs" }]} />
11
16
  */
12
17
  import config from "virtual:stnd/config";
13
18
 
14
19
  interface Props {
20
+ /** Brand text shown in the default `brand` slot. Defaults to the site config's own title, then "Home". */
15
21
  brand?: string;
22
+ /** Link target for the default brand. Default: "https://standard.garden/" */
16
23
  brandHref?: string;
24
+ /** Nav items — defaults to `config.nav.header` (or `config.nav` if it's an array). */
17
25
  items?: Array<{ title: string; url: string; external?: boolean }>;
18
26
  /** When false, the burger button and nav menu are not rendered. Default: true */
19
27
  menu?: boolean;
@@ -84,6 +92,7 @@ const renderedItems = safeItems.map((item) => {
84
92
  <a href={brandHref} class="stnd-header-brand">{brand}</a>
85
93
  </slot>
86
94
  <div class="flex items-center gap-half">
95
+ <slot name="actions" />
87
96
  {
88
97
  menuEnabled && (
89
98
  <button
package/Meta.astro CHANGED
@@ -1,14 +1,16 @@
1
1
  ---
2
2
  /**
3
- * Meta.astro — Universal <head> Meta Tags
3
+ * @component Meta
4
+ * @description Universal `<head>` meta tags — canonical link, description,
5
+ * Open Graph, Twitter Cards, Schema.org JSON-LD, favicons, theme-color, and
6
+ * mobile/Windows tile tags. Self-contained and zero-config: reads site
7
+ * defaults from `virtual:stnd/config`, with every prop overriding its
8
+ * matching config value for the current page only.
4
9
  *
5
- * Self-contained, zero-config. Reads site defaults from virtual:stnd/config
6
- * (same pattern as manifest/robots/headers folios). Per-page props override.
7
- *
8
- * Usage:
9
- * <Meta /> — site defaults from config
10
- * <Meta title="My Page" /> — override title
11
- * <Meta title="Article" type="article" created="2024-01-01" />
10
+ * @example astro
11
+ * <Meta />
12
+ * <Meta title="My Page" />
13
+ * <Meta title="Article" type="article" created="2024-01-01" />
12
14
  */
13
15
  import config from "virtual:stnd/config";
14
16
 
@@ -23,25 +25,33 @@ interface Props {
23
25
  modified?: string;
24
26
  tags?: string[];
25
27
 
26
- // Page type (controls og:type and Schema.org @type)
28
+ /** Controls `og:type` and the Schema.org `@type` (article → BlogPosting, profile → ProfilePage, else WebSite). Default: "website" */
27
29
  type?: "website" | "article" | "profile";
28
30
 
29
31
  // Appearance
32
+ /** `theme-color` meta tag. A single string is used for both color schemes; pass `{light, dark}` to vary by `prefers-color-scheme`. */
30
33
  themeColor?: string | { light: string; dark: string };
34
+ /** `msapplication-TileColor` for Windows tiles. Default: "#ffffff" */
31
35
  tileColor?: string;
32
36
 
33
- // Visibility
37
+ /** "private"/"unlisted" emit `noindex, nofollow`; "public" (default) allows indexing. */
34
38
  visibility?: "public" | "private" | "unlisted";
35
39
 
36
40
  // Optional features
37
41
  rssFeedUrl?: string | null;
38
42
  rssFeedTitle?: string;
43
+ /** Path prefix for the generated favicon package (.ico, PNG sizes, browserconfig.xml). Default: "/assets/favicon" */
39
44
  faviconPath?: string;
45
+ /**
46
+ * A single image URL used as *the* site icon, replacing the whole
47
+ * `faviconPath` set. Takes precedence over `faviconPath` when both are given.
48
+ * Modern browsers are happy with one SVG or PNG; use `faviconPath` when you
49
+ * have a full generated package (.ico, apple-touch-icon, browserconfig…).
50
+ */
51
+ favicon?: string | null;
40
52
  headExtensions?: string | string[];
41
53
 
42
- // Absolute origin overriding Astro.site (Astro.site is a build-time
43
- // constant — pass this when the actual per-request origin, e.g. a
44
- // user's verified custom domain, differs from the build's default site).
54
+ /** Absolute origin overriding `Astro.site` (a build-time constant) — pass this when the actual per-request origin, e.g. a user's verified custom domain, differs from the build's default site. */
45
55
  siteUrl?: string;
46
56
  }
47
57
 
@@ -61,6 +71,7 @@ const {
61
71
  rssFeedUrl,
62
72
  rssFeedTitle,
63
73
  faviconPath,
74
+ favicon,
64
75
  headExtensions,
65
76
  siteUrl: propsSiteUrl,
66
77
  } = Astro.props as Props;
@@ -111,7 +122,30 @@ const dateModified = modified
111
122
  : null;
112
123
 
113
124
  // ── Favicon ──
125
+ // A single `favicon` image replaces the generated package entirely. Anything
126
+ // else falls back to the `faviconPath` set (.ico + PNG sizes + browserconfig).
114
127
  const faviconBase = faviconPath || "/assets/favicon";
128
+ const singleFavicon =
129
+ typeof favicon === "string" && favicon.trim() !== "" ? favicon.trim() : null;
130
+
131
+ const FAVICON_MIME = {
132
+ svg: "image/svg+xml",
133
+ png: "image/png",
134
+ ico: "image/x-icon",
135
+ gif: "image/gif",
136
+ jpg: "image/jpeg",
137
+ jpeg: "image/jpeg",
138
+ webp: "image/webp",
139
+ avif: "image/avif",
140
+ };
141
+
142
+ // The extension can carry a query string (`/cdn/logo.svg?v=2`); strip it before
143
+ // matching, and leave `type` off entirely when the URL gives us nothing to go on.
144
+ const singleFaviconType = singleFavicon
145
+ ? (FAVICON_MIME[
146
+ (singleFavicon.split(/[?#]/)[0].split(".").pop() || "").toLowerCase()
147
+ ] ?? undefined)
148
+ : undefined;
115
149
 
116
150
  // ── Theme colors ──
117
151
  const resolvedThemeColor =
@@ -259,19 +293,34 @@ const headExtraHtml = Array.isArray(headExtensions)
259
293
  />
260
294
 
261
295
  <!-- Favicons -->
262
- <link
263
- rel="icon"
264
- type="image/png"
265
- href={`${faviconBase}/favicon-96x96.png`}
266
- sizes="96x96"
267
- />
268
- <link rel="icon" type="image/svg+xml" href={`${faviconBase}/favicon.svg`} />
269
- <link rel="shortcut icon" href={`${faviconBase}/favicon.ico`} />
270
- <link
271
- rel="apple-touch-icon"
272
- sizes="180x180"
273
- href={`${faviconBase}/apple-touch-icon.png`}
274
- />
296
+ {
297
+ singleFavicon ? (
298
+ <Fragment>
299
+ <link rel="icon" type={singleFaviconType} href={singleFavicon} />
300
+ <link rel="apple-touch-icon" href={singleFavicon} />
301
+ </Fragment>
302
+ ) : (
303
+ <Fragment>
304
+ <link
305
+ rel="icon"
306
+ type="image/png"
307
+ href={`${faviconBase}/favicon-96x96.png`}
308
+ sizes="96x96"
309
+ />
310
+ <link
311
+ rel="icon"
312
+ type="image/svg+xml"
313
+ href={`${faviconBase}/favicon.svg`}
314
+ />
315
+ <link rel="shortcut icon" href={`${faviconBase}/favicon.ico`} />
316
+ <link
317
+ rel="apple-touch-icon"
318
+ sizes="180x180"
319
+ href={`${faviconBase}/apple-touch-icon.png`}
320
+ />
321
+ </Fragment>
322
+ )
323
+ }
275
324
  <meta name="apple-mobile-web-app-title" content={siteTitle || title || ""} />
276
325
  <link rel="manifest" href="/site.webmanifest" />
277
326
 
@@ -294,7 +343,13 @@ const headExtraHtml = Array.isArray(headExtensions)
294
343
 
295
344
  <!-- Windows Tiles -->
296
345
  <meta name="msapplication-TileColor" content={resolvedTileColor} />
297
- <meta
298
- name="msapplication-config"
299
- content={`${faviconBase}/browserconfig.xml`}
300
- />
346
+ {
347
+ /* Only meaningful alongside the generated package — a single `favicon`
348
+ has no browserconfig.xml to point at. */
349
+ !singleFavicon && (
350
+ <meta
351
+ name="msapplication-config"
352
+ content={`${faviconBase}/browserconfig.xml`}
353
+ />
354
+ )
355
+ }
package/README.md CHANGED
@@ -2,21 +2,24 @@
2
2
  title: "@stnd/layout"
3
3
  aliases: []
4
4
  created: 2026-07-05 07:43
5
- modified: 2026-07-05 19:21
5
+ modified: 2026-09-16T17:28:37.447Z
6
6
  last_audited: 2026-07-14
7
7
  audit_interval_days: 90
8
8
  next_audit: 2026-10-12
9
9
  audit_priority: 3
10
10
  maturity: tree
11
11
  mode: read
12
- publish: false
12
+ publish: true
13
13
  status: active
14
14
  tags:
15
15
  - package
16
16
  - stnd
17
17
  theme: kernel
18
18
  type: package
19
- visibility: private
19
+ visibility: public
20
+ garden-url: https://standard.garden/@francis/readme
21
+ garden-short: https://stnd.gd/wAgx4e
22
+ permalink: readme
20
23
  ---
21
24
 
22
25
  # @[stnd](../README)/layout
package/StndInit.astro CHANGED
@@ -1,6 +1,31 @@
1
1
  ---
2
+ /**
3
+ * @component StndInit
4
+ * @description System-layer mount point rendered once per page, right after
5
+ * `</body>` in `<Base>`. Renders external module CDN assets, the Gold
6
+ * Standard module Hook zones (`stnd:base` for static Astro components like
7
+ * Toast/Confetti, `stnd:client` for hydrated Svelte components like Lab), the
8
+ * module store/scripts bundle, and a global `standard:ready` boot event. Not
9
+ * meant to be used outside `<Base>`.
10
+ */
2
11
  import config from "virtual:stnd/config";
3
12
  import Hook from "@stnd/core/Hook";
13
+
14
+ interface Props {
15
+ /**
16
+ * Optional allow-list handed to every Hook in this shell. A module opts a
17
+ * component out of the default "always mounted" behavior by giving it
18
+ * `meta: { scope: "something" }`; that entry only renders when `something`
19
+ * appears in `activeScopes`. Omitted (the default) means unrestricted —
20
+ * every app that doesn't pass this prop keeps today's behavior untouched.
21
+ * The framework never interprets what a scope name MEANS — that's the
22
+ * app's vocabulary, computed and passed in by whichever layout renders
23
+ * this component.
24
+ */
25
+ activeScopes?: string[];
26
+ }
27
+
28
+ const { activeScopes } = Astro.props as Props;
4
29
  ---
5
30
 
6
31
  <head>
@@ -24,10 +49,10 @@ import Hook from "@stnd/core/Hook";
24
49
  <!-- SYSTEM LAYER — Components injected by Gold Standard modules -->
25
50
  <div id="stnd-os" transition:persist="stnd-os">
26
51
  <!-- Static system components: Toast, Confetti (Astro SSR, zero JS overhead) -->
27
- <Hook zone="stnd:base" />
52
+ <Hook zone="stnd:base" activeScopes={activeScopes} />
28
53
 
29
54
  <!-- Hydrated system components: Lab, DropZone, Graft, etc. (Svelte, client-side) -->
30
- <Hook zone="stnd:client" hydrated={true} />
55
+ <Hook zone="stnd:client" hydrated={true} activeScopes={activeScopes} />
31
56
  </div>
32
57
 
33
58
  <!-- MODULE SCRIPTS (bypasses broken injectScript pipeline) -->
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@stnd/layout",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "author": "Francis Fontaine",
6
6
  "license": "MIT",
7
7
  "exports": {
8
+ "./*.astro": "./*.astro",
9
+ "./*.svelte": "./*.svelte",
8
10
  "./*": {
9
11
  "astro": "./*.astro",
10
12
  "svelte": "./*.svelte",
@@ -12,9 +14,10 @@
12
14
  }
13
15
  },
14
16
  "dependencies": {
15
- "wrangler": "^4.105.0",
16
- "@stnd/lab": "1.0.0",
17
- "@stnd/log": "0.5.0"
17
+ "wrangler": "^4.129.0",
18
+ "@stnd/lab": "1.0.2",
19
+ "@stnd/log": "0.5.1",
20
+ "@stnd/ui": "0.5.2"
18
21
  },
19
22
  "peerDependencies": {
20
23
  "astro": ">=5.0.0"