astroidjs 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -42
- package/bin/astroid.mjs +25 -25
- package/dist/analytics/index.d.ts +3 -3
- package/dist/analytics/index.js +9 -9
- package/dist/astro/csp.d.ts +5 -5
- package/dist/astro/csp.js +6 -6
- package/dist/astro/index.js +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +5 -5
- package/dist/commerce/adapters.d.ts +6 -6
- package/dist/commerce/adapters.js +9 -9
- package/dist/commerce/checkout-scaffold.d.ts +5 -5
- package/dist/commerce/checkout-scaffold.js +9 -9
- package/dist/commerce/checkout.d.ts +12 -12
- package/dist/commerce/checkout.js +7 -7
- package/dist/commerce/loader.d.ts +2 -2
- package/dist/commerce/loader.js +3 -3
- package/dist/commerce/mirror.d.ts +4 -4
- package/dist/commerce/mirror.js +9 -9
- package/dist/commerce/roles.d.ts +10 -10
- package/dist/commerce/roles.js +13 -13
- package/dist/commerce/secrets.d.ts +9 -9
- package/dist/commerce/secrets.js +9 -9
- package/dist/commerce/sync.d.ts +7 -7
- package/dist/commerce/sync.js +5 -5
- package/dist/components/sections.d.ts +9 -9
- package/dist/components/sections.js +12 -12
- package/dist/config.d.ts +62 -62
- package/dist/config.js +18 -18
- package/dist/email/inquiry.d.ts +2 -2
- package/dist/email/inquiry.js +1 -1
- package/dist/email/send.d.ts +4 -4
- package/dist/email/send.js +7 -7
- package/dist/email/templates.js +3 -3
- package/dist/email/theme.d.ts +1 -1
- package/dist/email/theme.js +4 -4
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.js +1 -1
- package/dist/map/pmtiles.d.ts +5 -5
- package/dist/map/pmtiles.js +5 -5
- package/dist/map/scaffold.d.ts +2 -2
- package/dist/map/scaffold.js +4 -4
- package/dist/map/style.d.ts +4 -4
- package/dist/map/style.js +1 -1
- package/dist/portal/config.d.ts +2 -2
- package/dist/portal/config.js +3 -3
- package/dist/portal/guard.d.ts +4 -4
- package/dist/portal/guard.js +4 -4
- package/dist/portal/nav.js +2 -2
- package/dist/portal/scaffold.d.ts +4 -4
- package/dist/portal/scaffold.js +6 -6
- package/dist/portal/session.d.ts +2 -2
- package/dist/portal/session.js +5 -5
- package/dist/portfolio/scaffold.d.ts +1 -1
- package/dist/portfolio/scaffold.js +4 -4
- package/dist/project/actions.d.ts +1 -1
- package/dist/project/actions.js +6 -6
- package/dist/project/generate.d.ts +4 -4
- package/dist/project/generate.js +15 -15
- package/dist/project/index.js +1 -1
- package/dist/project/scaffold.d.ts +2 -2
- package/dist/project/scaffold.js +11 -11
- package/dist/pwa/generate.d.ts +11 -11
- package/dist/pwa/generate.js +12 -12
- package/dist/queues/consumer.d.ts +3 -3
- package/dist/queues/consumer.js +2 -2
- package/dist/queues/messages.d.ts +4 -4
- package/dist/queues/messages.js +2 -2
- package/dist/queues/scaffold.d.ts +4 -4
- package/dist/queues/scaffold.js +8 -8
- package/dist/queues/webhook.d.ts +5 -5
- package/dist/queues/webhook.js +3 -3
- package/dist/realtime/scaffold.d.ts +4 -4
- package/dist/realtime/scaffold.js +8 -8
- package/dist/schema/collections.d.ts +6 -6
- package/dist/schema/collections.js +23 -23
- package/dist/schema/framework.d.ts +1 -1
- package/dist/schema/framework.js +2 -2
- package/dist/schema/generate.js +2 -2
- package/dist/schema/index.js +1 -1
- package/dist/secrets.d.ts +6 -6
- package/dist/secrets.js +6 -6
- package/dist/security/csp-origins.d.ts +1 -1
- package/dist/security/csp-origins.js +3 -3
- package/dist/security/rate-rules.d.ts +2 -2
- package/dist/security/rate-rules.js +8 -8
- package/dist/seo/resolve.d.ts +5 -5
- package/dist/seo/resolve.js +2 -2
- package/dist/seo/routes.d.ts +5 -5
- package/dist/seo/routes.js +3 -3
- package/dist/seo/structured-data.d.ts +6 -6
- package/dist/seo/structured-data.js +7 -7
- package/dist/status.d.ts +5 -5
- package/dist/status.js +7 -7
- package/dist/tenancy/index.d.ts +3 -3
- package/dist/tenancy/index.js +6 -6
- package/dist/worker/generate.d.ts +2 -2
- package/dist/worker/generate.js +19 -19
- package/dist/worker/index.js +1 -1
- package/dist/worker/routes.js +1 -1
- package/dist/workflow/advance.d.ts +3 -3
- package/dist/workflow/advance.js +6 -6
- package/dist/workflow/config.d.ts +4 -4
- package/dist/workflow/config.js +4 -4
- package/dist/workflow/generate.d.ts +2 -2
- package/dist/workflow/generate.js +4 -4
- package/package.json +3 -4
- package/src/components/Collection.tsx +5 -5
- package/src/components/Editable.astro +9 -9
- package/src/components/JustifiedGallery.astro +8 -8
- package/src/components/MediaSlot.astro +12 -12
- package/src/components/PortalShell.astro +4 -4
- package/src/components/RegisterSW.astro +3 -3
- package/src/components/Section.astro +8 -8
- package/src/components/Sections.astro +6 -6
- package/src/components/Seo.astro +3 -3
- package/src/components/StageBar.astro +3 -3
- package/src/components/StructuredData.astro +2 -2
- package/src/components/justify.ts +9 -9
- package/src/components/media-meta.ts +10 -10
- package/src/components/sections/AboutIntro.astro +1 -1
- package/src/components/sections/Contact.astro +1 -1
- package/src/components/sections/Cta.astro +1 -1
- package/src/components/sections/Faq.astro +1 -1
- package/src/components/sections/FeatureGrid.astro +2 -2
- package/src/components/sections/Hero.astro +1 -1
- package/src/components/sections/PricingTiers.astro +1 -1
- package/src/components/sections/ProductGrid.astro +1 -1
- package/src/components/sections/SplitImage.astro +1 -1
- package/src/components/sections/Steps.astro +1 -1
- package/src/components/sections/Testimonial.astro +1 -1
- package/src/components/sections.ts +17 -17
package/dist/workflow/config.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
2
|
//
|
|
3
|
-
// `defineWorkflow
|
|
3
|
+
// `defineWorkflow`—staged, audited pipelines.
|
|
4
4
|
//
|
|
5
5
|
// The shape this generalizes is ghostfire.coffee's production floor, and the
|
|
6
6
|
// framing correction in #256 is the important part: despite the name "order
|
|
7
7
|
// tracker", it is NOT queue- or Durable-Object-driven. It is a synchronous SSR
|
|
8
|
-
// + D1 state machine
|
|
8
|
+
// + D1 state machine—an integer `stage` column advanced by sign-off rows,
|
|
9
9
|
// where "liveness" is an email plus a page reload. That maps to a workflow
|
|
10
10
|
// module, distinct from the queues module and from #71's realtime DO. If live
|
|
11
11
|
// push is wanted later it layers on top; it is not required for the pattern.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// the same four things:
|
|
16
16
|
//
|
|
17
17
|
// 1. an ordered list of stages, and one integer saying which is in progress;
|
|
18
|
-
// 2. exactly one audit row per completed stage
|
|
18
|
+
// 2. exactly one audit row per completed stage—who, when, and what they
|
|
19
19
|
// recorded;
|
|
20
20
|
// 3. an advance that is safe when two operators press the button at once;
|
|
21
21
|
// 4. a per-stage side-effect hook (issue the invoice on packaging).
|
|
@@ -58,7 +58,7 @@ export function defineWorkflow(config) {
|
|
|
58
58
|
throw new AstroidConfigError(`Workflow stage keys must be lowercase identifiers; got ${JSON.stringify(stage.key)}`);
|
|
59
59
|
}
|
|
60
60
|
// Duplicates would make `stationFields` ambiguous and an audit row's stage
|
|
61
|
-
// key non-unique
|
|
61
|
+
// key non-unique—both silent, both awful to debug later.
|
|
62
62
|
if (seen.has(stage.key)) {
|
|
63
63
|
throw new AstroidConfigError(`Workflow ${JSON.stringify(config.key)} has a duplicate stage ${JSON.stringify(stage.key)}`);
|
|
64
64
|
}
|
|
@@ -3,7 +3,7 @@ import { type WorkflowConfig } from "./config.js";
|
|
|
3
3
|
* The Drizzle table sources a workflow adds: the audit table, and the override
|
|
4
4
|
* log when enabled.
|
|
5
5
|
*
|
|
6
|
-
* The `stage` column itself is NOT emitted here
|
|
6
|
+
* The `stage` column itself is NOT emitted here—it belongs to the project's
|
|
7
7
|
* own entity table (orders, applications, tickets), which Astroid doesn't own.
|
|
8
8
|
* The returned `stageColumn` is the line to paste into it, which keeps the
|
|
9
9
|
* ownership boundary honest rather than generating a table the project already has.
|
|
@@ -13,7 +13,7 @@ export declare function generateWorkflowSchema(config: WorkflowConfig): {
|
|
|
13
13
|
stageColumn: string;
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
* `src/pages/api/<key>/advance.ts
|
|
16
|
+
* `src/pages/api/<key>/advance.ts`—the guarded advance route.
|
|
17
17
|
*
|
|
18
18
|
* Thin by design: `advanceWorkflowStage` owns the concurrency guard and the
|
|
19
19
|
* status contract, so what's left here is authentication and the per-stage side
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
// Workflow → Drizzle schema + the advance route.
|
|
4
4
|
//
|
|
5
5
|
// The schema half is a pure function of the config (regenerate freely); the
|
|
6
|
-
// route is SCAFFOLD-ONCE, because per-stage side effects
|
|
7
|
-
// when packaging is signed, email the customer on every move
|
|
6
|
+
// route is SCAFFOLD-ONCE, because per-stage side effects—issue the invoice
|
|
7
|
+
// when packaging is signed, email the customer on every move—are the whole
|
|
8
8
|
// reason a project reaches for this module, and regenerating would erase them.
|
|
9
9
|
import { workflowAuditTable, workflowOverrideTable, workflowStages, } from "./config.js";
|
|
10
10
|
/**
|
|
11
11
|
* The Drizzle table sources a workflow adds: the audit table, and the override
|
|
12
12
|
* log when enabled.
|
|
13
13
|
*
|
|
14
|
-
* The `stage` column itself is NOT emitted here
|
|
14
|
+
* The `stage` column itself is NOT emitted here—it belongs to the project's
|
|
15
15
|
* own entity table (orders, applications, tickets), which Astroid doesn't own.
|
|
16
16
|
* The returned `stageColumn` is the line to paste into it, which keeps the
|
|
17
17
|
* ownership boundary honest rather than generating a table the project already has.
|
|
@@ -68,7 +68,7 @@ export function generateWorkflowSchema(config) {
|
|
|
68
68
|
return { source: lines.join("\n"), stageColumn: 'stage: integer("stage").notNull().default(0),' };
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* `src/pages/api/<key>/advance.ts
|
|
71
|
+
* `src/pages/api/<key>/advance.ts`—the guarded advance route.
|
|
72
72
|
*
|
|
73
73
|
* Thin by design: `advanceWorkflowStage` owns the concurrency guard and the
|
|
74
74
|
* status contract, so what's left here is authentication and the per-stage side
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astroidjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Astroid — an opinionated meta-framework over Louise Toolkit and Astro for building editable, multi-editor sites on Cloudflare Workers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -59,20 +59,19 @@
|
|
|
59
59
|
"publishConfig": {
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
|
-
"dependencies": {
|
|
63
|
-
"louise-toolkit": "^0.31.0"
|
|
64
|
-
},
|
|
65
62
|
"devDependencies": {
|
|
66
63
|
"@types/node": "^24.13.3",
|
|
67
64
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
68
65
|
"@vitest/coverage-v8": "4.1.11",
|
|
69
66
|
"astro": "^7.2.9",
|
|
67
|
+
"louise-toolkit": "^0.31.0",
|
|
70
68
|
"solid-js": "^1.9.15",
|
|
71
69
|
"typescript": "^5.9.3",
|
|
72
70
|
"vitest": "^4.1.11"
|
|
73
71
|
},
|
|
74
72
|
"peerDependencies": {
|
|
75
73
|
"astro": "^7.0.9",
|
|
74
|
+
"louise-toolkit": "^0.31.0",
|
|
76
75
|
"solid-js": "^1.9.0"
|
|
77
76
|
},
|
|
78
77
|
"peerDependenciesMeta": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
2
|
//
|
|
3
|
-
// `<Collection
|
|
3
|
+
// `<Collection>`—the typed list primitive (ADR 0003, item 3). It renders a list
|
|
4
4
|
// and hands each item to a render function that is FULLY TYPED, with no
|
|
5
5
|
// hand-written interface: the item type is inferred from `items`, so `item` in the
|
|
6
6
|
// slot carries whatever shape you passed.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// </Collection>
|
|
11
11
|
//
|
|
12
12
|
// Why a Solid render-prop and not an `.astro` component: Astro slots can't type a
|
|
13
|
-
// per-item `{item}`, but a Solid children-as-function can
|
|
13
|
+
// per-item `{item}`, but a Solid children-as-function can—this is the only shape
|
|
14
14
|
// that delivers the ADR's "`{item}` in its slot is fully typed". Server-render it
|
|
15
15
|
// by using it inside a Solid island without a `client:*` directive: it emits
|
|
16
16
|
// static HTML and ships no JS. Add `client:load`/`client:visible` only if the list
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
// On the item type + ADR 0003 §6 ("infers from the collection's Zod schema"):
|
|
20
20
|
// Louise's `CollectionConfig` is type-erased (`fields: Record<string,
|
|
21
21
|
// FieldConfig>`), so the item shape can't be recovered from the collection VALUE.
|
|
22
|
-
// Instead you type `items` from your data
|
|
23
|
-
// result
|
|
22
|
+
// Instead you type `items` from your data—per ADR 0001, a Zod-inferred query
|
|
23
|
+
// result—and it flows through to the slot. Same guarantee (a typed `item`),
|
|
24
24
|
// sourced from the data rather than the erased config.
|
|
25
25
|
|
|
26
26
|
import { For, type JSX } from "solid-js";
|
|
@@ -28,7 +28,7 @@ import { For, type JSX } from "solid-js";
|
|
|
28
28
|
export interface CollectionProps<T> {
|
|
29
29
|
/** The items to render, in order. The item type `T` is inferred from here. */
|
|
30
30
|
items: readonly T[];
|
|
31
|
-
/** Render one item
|
|
31
|
+
/** Render one item—receives the fully-typed `item` and a reactive `index`. */
|
|
32
32
|
children: (item: T, index: () => number) => JSX.Element;
|
|
33
33
|
/** Rendered when `items` is empty. */
|
|
34
34
|
fallback?: JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
// `<Editable
|
|
2
|
+
// `<Editable>`—the Astroid inline-edit primitive (ADR 0003, checklist item 1).
|
|
3
3
|
//
|
|
4
4
|
// It owns the `data-louise-*` marker contract so sites stop stamping it by hand:
|
|
5
5
|
// in edit mode it renders your element with the markers the Louise client
|
|
@@ -8,25 +8,25 @@
|
|
|
8
8
|
// the public HTML.
|
|
9
9
|
//
|
|
10
10
|
// Two flavors, per which surface the field lives on:
|
|
11
|
-
// • PAGE field
|
|
11
|
+
// • PAGE field: pass `collection` + `key` + `field`; emits
|
|
12
12
|
// `data-louise-field="<collection>:<key>:<field>"` (saved as a
|
|
13
13
|
// versioned draft when the page is mounted with `versionedPageId`).
|
|
14
|
-
// • SECTION field
|
|
14
|
+
// • SECTION field: pass `base` (this item's path, for example, `"2"` or `"2.blocks.0"`)
|
|
15
15
|
// plus `field`; emits `data-louise-node="<base>.<field>"` for
|
|
16
16
|
// the on-canvas section editor. `sfield` stays as the escape
|
|
17
|
-
// hatch for a path you build yourself
|
|
17
|
+
// hatch for a path you build yourself—an array entry:
|
|
18
18
|
// sfield={`${base}.items.${i}.title`}.
|
|
19
19
|
//
|
|
20
20
|
// Since ADR 0010 A2 this is the SAME attribute the section
|
|
21
21
|
// boundary carries, one path deeper. The catalog says whether a
|
|
22
22
|
// field is edited in place and with which editor, so `type` and
|
|
23
|
-
// `multiline` no longer affect a section field's markup
|
|
23
|
+
// `multiline` no longer affect a section field's markup—they
|
|
24
24
|
// are accepted for compatibility and ignored.
|
|
25
25
|
//
|
|
26
26
|
// The `base` form is what ADR 0005 §2 asks for: "a site author writes `<Editable
|
|
27
27
|
// field="heading">` and never hand-stamps the deeper path". `<Section>` supplies
|
|
28
28
|
// `base`, so the SAME component renders correctly whether it sits at the top level
|
|
29
|
-
// (`"2.heading"`) or inside a block (`"2.blocks.0.heading"`)
|
|
29
|
+
// (`"2.heading"`) or inside a block (`"2.blocks.0.heading"`)—the depth never
|
|
30
30
|
// appears in the component's source, which is what makes a section component
|
|
31
31
|
// reusable as a block component.
|
|
32
32
|
//
|
|
@@ -38,11 +38,11 @@ import type { HTMLAttributes, HTMLTag } from "astro/types";
|
|
|
38
38
|
interface Props extends HTMLAttributes<"div"> {
|
|
39
39
|
/** The element to render. Default `"div"`. */
|
|
40
40
|
as?: HTMLTag;
|
|
41
|
-
/** PAGE-field mode: the Louise collection slug (
|
|
41
|
+
/** PAGE-field mode: the Louise collection slug (for example, `"pages"`). */
|
|
42
42
|
collection?: string;
|
|
43
43
|
/** PAGE-field mode: the row key/id the field is on. */
|
|
44
44
|
key?: string | number;
|
|
45
|
-
/** PAGE-field mode: the field name (
|
|
45
|
+
/** PAGE-field mode: the field name (for example, `"title"`, `"body"`). Also the field
|
|
46
46
|
* name in SECTION mode, where it's joined onto `base`. */
|
|
47
47
|
field?: string;
|
|
48
48
|
/** SECTION-field mode: this item's path prefix (`"2"`, `"2.blocks.0"`),
|
|
@@ -87,7 +87,7 @@ let markers: Record<string, string> = {};
|
|
|
87
87
|
if (editing) {
|
|
88
88
|
if (sectionPath) {
|
|
89
89
|
// ONE marker (ADR 0010 A2). This used to emit `data-louise-sfield` plus
|
|
90
|
-
// `data-louise-type="richtext"` plus `data-louise-multiline
|
|
90
|
+
// `data-louise-type="richtext"` plus `data-louise-multiline`—three
|
|
91
91
|
// attributes describing a field the catalog already fully describes. The
|
|
92
92
|
// editor now reads the field's type to know whether it's rich text and
|
|
93
93
|
// whether it holds more than one line, so `type` and `multiline` are inert
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
// `<JustifiedGallery
|
|
2
|
+
// `<JustifiedGallery>`—Flickr-style row-balanced photo grid (#257).
|
|
3
3
|
//
|
|
4
4
|
// Images keep their aspect ratios, rows fill the container exactly, and row
|
|
5
5
|
// heights land near a target. CSS alone can't do it: `grid` wants uniform
|
|
6
6
|
// tracks, and `columns` gives a masonry *column* flow where reading order runs
|
|
7
|
-
// down the page instead of across it
|
|
7
|
+
// down the page instead of across it—wrong for a portfolio, and wrong for
|
|
8
8
|
// keyboard/screen-reader order.
|
|
9
9
|
//
|
|
10
10
|
// Two layers, so it never depends on JavaScript to be usable:
|
|
11
11
|
//
|
|
12
12
|
// 1. SSR emits a flex-wrap floor. Each tile carries its declared aspect ratio
|
|
13
13
|
// (from the media library's recorded dimensions) as `flex-grow`/`flex-basis`,
|
|
14
|
-
// which already produces a justified, gap-correct grid with no layout shift
|
|
15
|
-
//
|
|
14
|
+
// which already produces a justified, gap-correct grid with no layout shift—that's
|
|
15
|
+
// what someone with JS blocked, or on the first paint before the
|
|
16
16
|
// bundle lands, sees.
|
|
17
17
|
// 2. On the client, once the images decode and their TRUE dimensions are known,
|
|
18
18
|
// `justifyRows` recomputes exact rows and pins each tile's box. This is the
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// right, so the correction isn't visible as a jump on a well-populated library.
|
|
24
24
|
//
|
|
25
25
|
// CSP: the layout script is a normal (bundled) `<script>`, so Astro hashes it
|
|
26
|
-
// into `script-src
|
|
26
|
+
// into `script-src`—no `is:inline`, no `unsafe-inline`. Per-tile sizing is
|
|
27
27
|
// written as inline `style`, which is what the `style-src` rewrite in #253
|
|
28
28
|
// exists for.
|
|
29
29
|
import { DEFAULT_ASPECT, type GalleryItem } from "./justify.js";
|
|
@@ -129,7 +129,7 @@ const aspectOf = (item: GalleryItem) =>
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/* Reveal-on-scroll. The visible state is the DEFAULT so that a browser with
|
|
132
|
-
no IntersectionObserver
|
|
132
|
+
no IntersectionObserver—or no JS at all—shows every tile; the script
|
|
133
133
|
opts tiles into the hidden state only once it can guarantee it will undo it. */
|
|
134
134
|
.astroid-gallery--reveal .astroid-gallery__tile[data-reveal="pending"] {
|
|
135
135
|
opacity: 0;
|
|
@@ -164,7 +164,7 @@ const aspectOf = (item: GalleryItem) =>
|
|
|
164
164
|
|
|
165
165
|
const targetHeight = Number(root.dataset.targetHeight) || 260;
|
|
166
166
|
const gap = Number(root.dataset.gap) || 8;
|
|
167
|
-
// clientWidth excludes the scrollbar, which offsetWidth does not
|
|
167
|
+
// clientWidth excludes the scrollbar, which offsetWidth does not—using the
|
|
168
168
|
// wrong one overflows the row by the scrollbar width on Windows.
|
|
169
169
|
const containerWidth = root.clientWidth;
|
|
170
170
|
|
|
@@ -242,7 +242,7 @@ const aspectOf = (item: GalleryItem) =>
|
|
|
242
242
|
|
|
243
243
|
if (typeof ResizeObserver !== "undefined") {
|
|
244
244
|
let frame = 0;
|
|
245
|
-
// Coalesce to one layout per animation frame
|
|
245
|
+
// Coalesce to one layout per animation frame—a drag-resize fires this
|
|
246
246
|
// continuously, and each run touches every tile's style.
|
|
247
247
|
const observer = new ResizeObserver(() => {
|
|
248
248
|
cancelAnimationFrame(frame);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
// `<MediaSlot
|
|
2
|
+
// `<MediaSlot>`—the responsive-image primitive (#257).
|
|
3
3
|
//
|
|
4
4
|
// One place that knows how to put a media-library image on a page correctly:
|
|
5
5
|
// a width-descriptor `srcset` over Cloudflare Image Resizing, a `sizes` hint so
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// box so the image can't shift the page as it loads, and lazy/async decoding by
|
|
8
8
|
// default.
|
|
9
9
|
//
|
|
10
|
-
// The math is not new
|
|
10
|
+
// The math is not new—`louise-toolkit/media` already ships `cfImageSrcset`
|
|
11
11
|
// (width descriptors, ratio-derived heights, cover-crop gravity, `format=auto`)
|
|
12
12
|
// and `circleImage`. What every consuming site rebuilt by hand was the *component*
|
|
13
13
|
// around it: the ratio box, the `sizes` string, the focal-point crop, and
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
// Contentful Paint regression. That's what this owns.
|
|
16
16
|
//
|
|
17
17
|
// CSP: the focal/zoom knobs emit a data-driven inline `style` attribute. That is
|
|
18
|
-
// exactly the case astroid's middleware rewrites `style-src` for (#253)
|
|
18
|
+
// exactly the case astroid's middleware rewrites `style-src` for (#253)—scripts
|
|
19
19
|
// stay hash-pinned with no `unsafe-inline`.
|
|
20
20
|
import { cfImageSrcset, circleImage } from "louise-toolkit/media";
|
|
21
21
|
import type { HTMLAttributes } from "astro/types";
|
|
22
22
|
|
|
23
23
|
interface Props extends Omit<HTMLAttributes<"img">, "src" | "srcset" | "sizes" | "width" | "height"> {
|
|
24
|
-
/** Image URL
|
|
24
|
+
/** Image URL—a media-library asset, or any URL on a resizing-enabled zone. */
|
|
25
25
|
src: string;
|
|
26
26
|
/**
|
|
27
27
|
* Alternative text. REQUIRED, and `""` is a legitimate value: an image that
|
|
@@ -33,18 +33,18 @@ interface Props extends Omit<HTMLAttributes<"img">, "src" | "srcset" | "sizes" |
|
|
|
33
33
|
alt: string;
|
|
34
34
|
/**
|
|
35
35
|
* The largest width, in CSS px, this image is displayed at on a 1× screen.
|
|
36
|
-
* Drives the srcset ladder
|
|
36
|
+
* Drives the srcset ladder—not a hard render width.
|
|
37
37
|
*/
|
|
38
38
|
width?: number;
|
|
39
39
|
/**
|
|
40
|
-
* The `sizes` attribute: how wide the image renders at each breakpoint,
|
|
40
|
+
* The `sizes` attribute: how wide the image renders at each breakpoint, for example,
|
|
41
41
|
* `"(max-width: 640px) 100vw, 33vw"`. Without it the browser assumes `100vw`
|
|
42
42
|
* and over-fetches on every multi-column layout, which is the single most
|
|
43
43
|
* common way a "responsive" image ends up slower than a fixed one.
|
|
44
44
|
*/
|
|
45
45
|
sizes?: string;
|
|
46
|
-
/** Aspect ratio as `"w/h"` (
|
|
47
|
-
* derivative's height so the crop matches what's shown
|
|
46
|
+
/** Aspect ratio as `"w/h"` (for example, `"16/9"`). Reserves the box and derives each
|
|
47
|
+
* derivative's height so the crop matches what's shown—no wasted pixels. */
|
|
48
48
|
ratio?: string;
|
|
49
49
|
/** `rect` (default) or `circle`. A circle uses a square focal crop + 2× retina. */
|
|
50
50
|
shape?: "rect" | "circle";
|
|
@@ -57,7 +57,7 @@ interface Props extends Omit<HTMLAttributes<"img">, "src" | "srcset" | "sizes" |
|
|
|
57
57
|
/** Encode quality, 1–100. Default 82. */
|
|
58
58
|
quality?: number;
|
|
59
59
|
/**
|
|
60
|
-
* Focal point as percentages, for when `gravity: auto` picks wrong
|
|
60
|
+
* Focal point as percentages, for when `gravity: auto` picks wrong—a portrait
|
|
61
61
|
* where the subject sits high, say. Applied as `object-position`, so it's a
|
|
62
62
|
* render-time framing choice and the same derivative is reused.
|
|
63
63
|
*/
|
|
@@ -65,7 +65,7 @@ interface Props extends Omit<HTMLAttributes<"img">, "src" | "srcset" | "sizes" |
|
|
|
65
65
|
/** Scale factor inside the box, for a punch-in crop. Default 1. */
|
|
66
66
|
zoom?: number;
|
|
67
67
|
/**
|
|
68
|
-
* `lazy` (default) or `eager`. Set `eager
|
|
68
|
+
* `lazy` (default) or `eager`. Set `eager`—and `priority`—for anything
|
|
69
69
|
* above the fold: lazy-loading the LCP image delays it by a full network
|
|
70
70
|
* round-trip after layout.
|
|
71
71
|
*/
|
|
@@ -111,7 +111,7 @@ const sizesAttr = sizes ?? (circle ? `${size}px` : `${width}px`);
|
|
|
111
111
|
|
|
112
112
|
// Framing lives on the <img>: `object-position` moves the visible window of a
|
|
113
113
|
// cover-crop, `scale` punches in. Both are per-placement, so they must NOT go
|
|
114
|
-
// through the CDN
|
|
114
|
+
// through the CDN—that would mint a second derivative of the same source.
|
|
115
115
|
const frame = [
|
|
116
116
|
focal ? `object-position:${focal.x}% ${focal.y}%` : "",
|
|
117
117
|
zoom !== 1 ? `transform:scale(${zoom})` : "",
|
|
@@ -132,7 +132,7 @@ const img = {
|
|
|
132
132
|
src: derived.src,
|
|
133
133
|
srcset: derived.srcset,
|
|
134
134
|
// A circle's srcset uses DPR descriptors (`1x`/`2x`), which `sizes` does not
|
|
135
|
-
// apply to
|
|
135
|
+
// apply to—supplying it there would be ignored at best and confusing at worst.
|
|
136
136
|
...(circle ? {} : { sizes: sizesAttr }),
|
|
137
137
|
alt,
|
|
138
138
|
loading,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
// `<PortalShell
|
|
2
|
+
// `<PortalShell>`—the account-area chrome: a sidebar of nav items and a main
|
|
3
3
|
// column, collapsing to a burger on small screens.
|
|
4
4
|
//
|
|
5
5
|
// Theme-tokened, never brand-styled. Every colour here is a daisyUI token
|
|
@@ -14,11 +14,11 @@ import type { PortalNav } from "astroidjs";
|
|
|
14
14
|
|
|
15
15
|
interface Props {
|
|
16
16
|
nav: PortalNav;
|
|
17
|
-
/** The signed-in user's role
|
|
17
|
+
/** The signed-in user's role—decides which nav items are visible. */
|
|
18
18
|
role?: string | null;
|
|
19
19
|
/** Current path, for the active highlight. Defaults to the rendered URL. */
|
|
20
20
|
path?: string;
|
|
21
|
-
/** Heading above the nav,
|
|
21
|
+
/** Heading above the nav, for example, the account holder's name. */
|
|
22
22
|
title?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ const { nav, role, path, title } = Astro.props;
|
|
|
26
26
|
const current = path ?? Astro.url.pathname;
|
|
27
27
|
|
|
28
28
|
// Filtered by role BEFORE render: an item the viewer can't reach is never
|
|
29
|
-
// drawn, rather than drawn and then 403'd
|
|
29
|
+
// drawn, rather than drawn and then 403'd—which reads as a broken link.
|
|
30
30
|
const items = nav.forRole(role);
|
|
31
31
|
const active = nav.activeFor(current);
|
|
32
32
|
---
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
// `<RegisterSW
|
|
2
|
+
// `<RegisterSW>`—registers the PWA service worker.
|
|
3
3
|
//
|
|
4
4
|
// A bundled `<script>`, never an inline one: Astro hashes it into `script-src`,
|
|
5
5
|
// so this works under the strict CSP without `unsafe-inline`. That's also why
|
|
6
6
|
// the scope rides on a `data-` attribute rather than being interpolated into
|
|
7
|
-
// the script
|
|
7
|
+
// the script—`define:vars` forces `is:inline`, whose content varies per
|
|
8
8
|
// render and therefore can't be hashed.
|
|
9
9
|
//
|
|
10
10
|
// Registration is best-effort by design. A service worker is an enhancement:
|
|
@@ -36,7 +36,7 @@ const { scope = "/", src = "/sw.js" } = Astro.props;
|
|
|
36
36
|
navigator.serviceWorker
|
|
37
37
|
.register(el.dataset.src ?? "/sw.js", { scope: el.dataset.scope ?? "/" })
|
|
38
38
|
.catch((error) => {
|
|
39
|
-
// Not fatal
|
|
39
|
+
// Not fatal—the app works without it. Logged rather than swallowed so
|
|
40
40
|
// a scope/MIME misconfiguration is findable instead of just "the PWA
|
|
41
41
|
// doesn't install and nobody knows why".
|
|
42
42
|
console.warn("[astroid] service worker registration failed", error);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
// `<Section
|
|
2
|
+
// `<Section>`—the section-library dispatcher (ADR 0003 item 2, on the ADR 0005
|
|
3
3
|
// model).
|
|
4
4
|
//
|
|
5
5
|
// It renders ONE stored `SectionItem`: dispatch on `_type`, hand the component
|
|
6
6
|
// its `base` path, and let the component own every pixel. What changed from the
|
|
7
|
-
// old `kind`-prop version is where the truth lives
|
|
7
|
+
// old `kind`-prop version is where the truth lives—the item is data loaded from
|
|
8
8
|
// the page's `sections` column, not props a template hand-wrote, so what renders
|
|
9
9
|
// is exactly what the on-canvas editor edits in place.
|
|
10
10
|
//
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// • the FIELDS are stamped by `<Editable base={base} field="…">` inside each
|
|
15
15
|
// component, because only the component knows which of its text nodes are
|
|
16
16
|
// editable. Since A2 those carry the SAME `data-louise-node` attribute, one
|
|
17
|
-
// path deeper
|
|
17
|
+
// path deeper—the catalog says whether a field is edited in place, so the
|
|
18
18
|
// marker no longer has to.
|
|
19
19
|
//
|
|
20
20
|
// Blocks recurse through this same component with a deeper `base`. That's the
|
|
@@ -51,8 +51,8 @@ interface Props {
|
|
|
51
51
|
/**
|
|
52
52
|
* `_type` → render component. A map rather than a chain of `{type === "x" && …}`
|
|
53
53
|
* for one reason that matters: it can be checked. A catalog entry whose type is
|
|
54
|
-
* missing here renders NOTHING
|
|
55
|
-
*
|
|
54
|
+
* missing here renders NOTHING—a silent hole in the page rather than an error—so
|
|
55
|
+
* the test suite asserts these keys and the catalog's are the same set.
|
|
56
56
|
*/
|
|
57
57
|
const COMPONENTS: Record<string, ((props: SectionRenderProps) => unknown) | undefined> = {
|
|
58
58
|
hero: Hero,
|
|
@@ -82,12 +82,12 @@ const type = String(item._type);
|
|
|
82
82
|
const blocks = Array.isArray(item.blocks) ? item.blocks : [];
|
|
83
83
|
|
|
84
84
|
// The boundary marker the on-canvas chrome draws its ring on. It is the item's
|
|
85
|
-
// path, verbatim
|
|
85
|
+
// path, verbatim—pre-0010 this line string-sniffed `base` for ".blocks." to pick
|
|
86
86
|
// between two attribute names, which was the three-attribute editing model leaking
|
|
87
87
|
// into an otherwise uniform recursive render (ADR 0010, Context). The editor
|
|
88
88
|
// resolves what a path CAN do from the catalog; the render just says where it is.
|
|
89
89
|
//
|
|
90
|
-
// `class="contents"` keeps this wrapper out of layout entirely
|
|
90
|
+
// `class="contents"` keeps this wrapper out of layout entirely—the ring is drawn
|
|
91
91
|
// as a box-shadow on the element itself, so an extra block box here would offset
|
|
92
92
|
// every section's spacing for the sake of an attribute.
|
|
93
93
|
const boundary = editing ? { "data-louise-node": base } : {};
|
|
@@ -95,7 +95,7 @@ const boundary = editing ? { "data-louise-node": base } : {};
|
|
|
95
95
|
const shared = { item, base, edit: editing, mediaMeta };
|
|
96
96
|
|
|
97
97
|
// Resolved in the frontmatter so the template is one dynamic tag rather than a
|
|
98
|
-
// ladder of comparisons. The map itself is the dispatch check
|
|
98
|
+
// ladder of comparisons. The map itself is the dispatch check—an unknown
|
|
99
99
|
// `_type` (legitimate mid-migration, and already rejected on write) simply
|
|
100
100
|
// finds nothing and renders nothing, which is why the lookup is typed as
|
|
101
101
|
// possibly-undefined rather than asserted.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
// `<Sections
|
|
2
|
+
// `<Sections>`—render a page's whole `sections` array.
|
|
3
3
|
//
|
|
4
4
|
// This exists for one reason `<Section>` can't serve: the media lookup has to
|
|
5
5
|
// span the page. A section stores an image as a URL, but the `alt`/`caption` an
|
|
6
6
|
// editor typed live on the media ASSET, so rendering images correctly means
|
|
7
|
-
// joining back to the registry
|
|
7
|
+
// joining back to the registry—and doing that per image is thirty D1
|
|
8
8
|
// round-trips for one gallery. Here it's ONE bounded `IN (...)` per page,
|
|
9
9
|
// threaded down as `mediaMeta`.
|
|
10
10
|
//
|
|
@@ -16,7 +16,7 @@ import { astroidSectionCatalog } from "./sections.js";
|
|
|
16
16
|
import Section from "./Section.astro";
|
|
17
17
|
|
|
18
18
|
interface Props {
|
|
19
|
-
/** The page's stored `sections` array. Untrusted JSON
|
|
19
|
+
/** The page's stored `sections` array. Untrusted JSON—a non-array, or an
|
|
20
20
|
* item with an unknown `_type`, renders nothing rather than throwing. */
|
|
21
21
|
items: unknown;
|
|
22
22
|
/** D1, for the media alt/caption lookup. Omit to skip it entirely. */
|
|
@@ -24,7 +24,7 @@ interface Props {
|
|
|
24
24
|
/** Public media base, for turning a stored URL back into a registry key.
|
|
25
25
|
* Matches the site's `MEDIA_URL`. */
|
|
26
26
|
mediaBase?: string;
|
|
27
|
-
/** Override the catalog
|
|
27
|
+
/** Override the catalog—a site that extends Astroid's set passes its own. */
|
|
28
28
|
catalog?: SectionCatalog;
|
|
29
29
|
/** Force edit markers on/off. Defaults to `Astro.locals.editMode`. */
|
|
30
30
|
edit?: boolean;
|
|
@@ -42,7 +42,7 @@ const {
|
|
|
42
42
|
} = Astro.props;
|
|
43
43
|
|
|
44
44
|
// A page with no sections column yet, or a malformed one, is a normal state
|
|
45
|
-
// (nothing seeded)
|
|
45
|
+
// (nothing seeded)—not an error worth failing the render over.
|
|
46
46
|
const list: SectionItem[] = Array.isArray(items) ? (items as SectionItem[]) : [];
|
|
47
47
|
|
|
48
48
|
// One lookup for the page. Skipped when the caller already did it (a layout that
|
|
@@ -54,7 +54,7 @@ const mediaMeta =
|
|
|
54
54
|
{/*
|
|
55
55
|
The host element. `mountSections(el, opts)` takes the element to scan for
|
|
56
56
|
`[data-louise-node]` markers and to hang on-canvas chrome off, so rendering
|
|
57
|
-
it here
|
|
57
|
+
it here—rather than asking every page to remember a wrapper—is what makes
|
|
58
58
|
`<Sections>` self-sufficient: drop it on a page and the editor can find it.
|
|
59
59
|
A plain block wrapper, not `display: contents`, because the chrome positions
|
|
60
60
|
toolbars against a real box.
|
package/src/components/Seo.astro
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
// `<Seo
|
|
2
|
+
// `<Seo>`—the settings-driven `<head>` block: title (templated), description,
|
|
3
3
|
// canonical, robots, Open Graph, and the Twitter/X card.
|
|
4
4
|
//
|
|
5
5
|
// Tags are emitted directly rather than through a wrapper library. There are
|
|
6
6
|
// about fifteen of them, the shapes are frozen by the OG and Twitter specs, and
|
|
7
7
|
// owning them means the resolution rules (see `resolvePageSeo`) are the ONLY
|
|
8
|
-
// place a value can come from
|
|
8
|
+
// place a value can come from—which is what makes the layer predictable.
|
|
9
9
|
//
|
|
10
10
|
// JSON-LD is not here: it's a separate concern with different escaping rules and
|
|
11
11
|
// its own component (`<StructuredData>`).
|
|
@@ -40,7 +40,7 @@ const seo = resolvePageSeo(settings, page, {
|
|
|
40
40
|
{seo.noindex && <meta name="robots" content="noindex, nofollow" />}
|
|
41
41
|
|
|
42
42
|
<meta property="og:type" content={seo.ogType} />
|
|
43
|
-
{/* The bare title, not the templated one
|
|
43
|
+
{/* The bare title, not the templated one—og:site_name already carries the brand. */}
|
|
44
44
|
<meta property="og:title" content={seo.bareTitle} />
|
|
45
45
|
<meta property="og:url" content={seo.canonical} />
|
|
46
46
|
{seo.description && <meta property="og:description" content={seo.description} />}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
// `<StageBar
|
|
2
|
+
// `<StageBar>`—a pipeline's progress as a segmented bar.
|
|
3
3
|
//
|
|
4
4
|
// Generalized from ghostfire's six-stage floor tracker, which hard-coded the
|
|
5
5
|
// segment count, the corner radii, the brand hex codes, and a mascot image.
|
|
@@ -17,7 +17,7 @@ interface Props {
|
|
|
17
17
|
stage: number;
|
|
18
18
|
/**
|
|
19
19
|
* An optional marker (an emoji, a mascot glyph) that rides above the active
|
|
20
|
-
* segment. Off by default
|
|
20
|
+
* segment. Off by default—the reference's bobbing ghost is brand, not
|
|
21
21
|
* mechanism.
|
|
22
22
|
*/
|
|
23
23
|
marker?: string;
|
|
@@ -81,7 +81,7 @@ const markerLeft = complete ? null : `${(((current + 0.5) / stages.length) * 100
|
|
|
81
81
|
transition: background-color 300ms ease;
|
|
82
82
|
}
|
|
83
83
|
/* Round only the outer ends, so the bar reads as one track rather than a row
|
|
84
|
-
of separate pills
|
|
84
|
+
of separate pills—at any segment count. */
|
|
85
85
|
.astroid-stagebar__step:first-child .astroid-stagebar__seg {
|
|
86
86
|
border-start-start-radius: 999px;
|
|
87
87
|
border-end-start-radius: 999px;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
// `<StructuredData
|
|
2
|
+
// `<StructuredData>`—the schema.org JSON-LD `@graph` for rich results and AI
|
|
3
3
|
// answer surfaces: the business (its `@type` from the archetype), the WebSite,
|
|
4
4
|
// and optionally the entity this page is about.
|
|
5
5
|
//
|
|
@@ -16,7 +16,7 @@ interface Props {
|
|
|
16
16
|
/** Absolute origin serving this page. Defaults to the current origin, which
|
|
17
17
|
* keeps a preview deploy describing itself rather than the live domain. */
|
|
18
18
|
siteUrl?: string;
|
|
19
|
-
/** The thing this page is about
|
|
19
|
+
/** The thing this page is about—a Product, VisualArtwork, Article. */
|
|
20
20
|
entity?: JsonLdNode;
|
|
21
21
|
}
|
|
22
22
|
|