ferst-core 0.6.1 → 0.7.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/bin/ferst-update.mjs +135 -6
- package/components/BlockRenderer.astro +107 -103
- package/components/blocks/Prose.astro +56 -39
- package/components/recipes/Documents.astro +146 -0
- package/components/recipes/Hero.astro +383 -191
- package/components/recipes/Tiles.astro +23 -4
- package/components/recipes/Video.astro +157 -0
- package/content/blocks.ts +773 -719
- package/content/cookies.ts +112 -103
- package/examples/primitives.ts +28 -28
- package/examples/recipes.ts +4 -0
- package/lib/documents.ts +62 -0
- package/lib/inline-markdown.ts +59 -52
- package/lib/prose.ts +29 -0
- package/lib/video.ts +56 -0
- package/package.json +1 -1
package/bin/ferst-update.mjs
CHANGED
|
@@ -46,6 +46,8 @@ export function isDirectInvocation(argv1) {
|
|
|
46
46
|
export const DEFAULT_UPSTREAM = 'https://github.com/MyAI4WebDev/ferst-client-template.git';
|
|
47
47
|
/** The ownership manifest a seeded client repo carries at its root. */
|
|
48
48
|
export const MANIFEST = 'ferst-template.json';
|
|
49
|
+
/** The per-site provenance/config file a client repo carries at its root (gitBase, etc.). */
|
|
50
|
+
export const SITE_META = 'ferst-site.json';
|
|
49
51
|
|
|
50
52
|
// ── pure helpers (unit-tested; no shell, no fs writes) ───────────────────────────
|
|
51
53
|
|
|
@@ -97,11 +99,17 @@ export function resolveUpstream(manifest) {
|
|
|
97
99
|
return { url: DEFAULT_UPSTREAM, defaulted: true };
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* The caret range that pins an installed version to its major.minor, in the SAME
|
|
104
|
+
* canonical form the template ships (`^major.minor.0`, e.g. 0.6.3 → ^0.6.0). Emitting
|
|
105
|
+
* the `.0` matters: the template's manifest + package.json carry `^0.6.0`, so a matching
|
|
106
|
+
* canonical form makes the ownedPath resync + this rewrite idempotent — no per-update
|
|
107
|
+
* churn, and no `^0.6` vs `^0.6.0` drift between clients. (`^0.6.0` ≡ `^0.6` to npm.)
|
|
108
|
+
*/
|
|
101
109
|
export function coreRangeFor(version) {
|
|
102
110
|
const m = String(version).trim().replace(/^v/, '').match(/^(\d+)\.(\d+)/);
|
|
103
111
|
if (!m) throw new Error(`ferst-update: could not parse ferst-core version "${version}".`);
|
|
104
|
-
return `^${m[1]}.${m[2]}`;
|
|
112
|
+
return `^${m[1]}.${m[2]}.0`;
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
/** Read the `version` out of a package.json's text. Throws if absent. */
|
|
@@ -139,6 +147,42 @@ export function replaceCoreRange(raw, next) {
|
|
|
139
147
|
return raw.replace(RANGE, `$1${next}$2`);
|
|
140
148
|
}
|
|
141
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Set package.json's `ferst-core` dependency to the canonical range in place,
|
|
152
|
+
* formatting-preserving (same approach as replaceCoreRange). `npm install --save`
|
|
153
|
+
* would otherwise pin it to the exact installed patch (e.g. ^0.6.2), which churns
|
|
154
|
+
* against the template's `^0.6.0` on every resync; the exact version lives in the
|
|
155
|
+
* lockfile + the ferst-site.json drift stamp, so the dep range stays canonical.
|
|
156
|
+
* Returns the new text, or null when there is no `ferst-core` dependency to replace.
|
|
157
|
+
*/
|
|
158
|
+
export function replacePackageCoreDep(raw, next) {
|
|
159
|
+
const DEP = /("ferst-core"\s*:\s*")[^"]*(")/;
|
|
160
|
+
if (!DEP.test(raw)) return null;
|
|
161
|
+
return raw.replace(DEP, `$1${next}$2`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** package.json fields that describe the CLIENT's site, not the template's scaffold. */
|
|
165
|
+
export const CLIENT_PACKAGE_FIELDS = ['name', 'description', 'version', 'private', 'author', 'homepage', 'repository'];
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* After the scaffold sync overwrites package.json (a template-owned path), put the
|
|
169
|
+
* client's own identity fields back (name, description, …) so an update never renames
|
|
170
|
+
* the site. Keeps the synced file's indentation and key order. Returns the new text,
|
|
171
|
+
* or null when either side isn't valid JSON (then the synced file is left as is).
|
|
172
|
+
*/
|
|
173
|
+
export function keepClientPackageFields(syncedRaw, clientRaw) {
|
|
174
|
+
let synced, client;
|
|
175
|
+
try {
|
|
176
|
+
synced = JSON.parse(syncedRaw);
|
|
177
|
+
client = JSON.parse(clientRaw);
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
for (const k of CLIENT_PACKAGE_FIELDS) if (k in client) synced[k] = client[k];
|
|
182
|
+
const indent = (syncedRaw.match(/\n([ \t]+)"/) || [, ' '])[1];
|
|
183
|
+
return JSON.stringify(synced, null, indent) + (syncedRaw.endsWith('\n') ? '\n' : '');
|
|
184
|
+
}
|
|
185
|
+
|
|
142
186
|
/**
|
|
143
187
|
* The default branch name from `git ls-remote --symref <remote> HEAD` output
|
|
144
188
|
* (`ref: refs/heads/<branch>\tHEAD`). Null when it can't be read. Pure.
|
|
@@ -155,6 +199,33 @@ export function tail(text, n = 40) {
|
|
|
155
199
|
return lines.slice(-n).join('\n');
|
|
156
200
|
}
|
|
157
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Build the provenance "drift stamp" for a completed update: WHICH template commit and
|
|
204
|
+
* core version this repo landed on, and when. Pure — the write is the caller's job.
|
|
205
|
+
* @param {{ upstream?: string, commit?: string|null, coreVersion?: string|null, now?: Date|string }} p
|
|
206
|
+
* @returns {{ templateUpstream: string|null, templateCommit: string|null, coreVersion: string|null, at: string }}
|
|
207
|
+
*/
|
|
208
|
+
export function buildProvenance({ upstream, commit, coreVersion, now = new Date() } = {}) {
|
|
209
|
+
return {
|
|
210
|
+
templateUpstream: upstream ?? null,
|
|
211
|
+
templateCommit: commit ?? null,
|
|
212
|
+
coreVersion: coreVersion ?? null,
|
|
213
|
+
at: typeof now === 'string' ? now : now.toISOString(),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Merge a provenance stamp into an existing ferst-site.json object under `lastUpdate`,
|
|
219
|
+
* preserving every other field (gitBase, productionBranch, unknown keys) and overwriting
|
|
220
|
+
* any prior `lastUpdate`. Pass null (no ferst-site.json yet) → a fresh object with just
|
|
221
|
+
* the stamp. Pure — never mutates the input.
|
|
222
|
+
* @returns {object} the object to write back
|
|
223
|
+
*/
|
|
224
|
+
export function mergeSiteMeta(existing, provenance) {
|
|
225
|
+
const base = existing && typeof existing === 'object' && !Array.isArray(existing) ? existing : {};
|
|
226
|
+
return { ...base, lastUpdate: provenance };
|
|
227
|
+
}
|
|
228
|
+
|
|
158
229
|
// ── thin side-effecting shell (kept out of the pure helpers above) ────────────────
|
|
159
230
|
|
|
160
231
|
const exec = (cmd, cwd) => execSync(cmd, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -181,6 +252,16 @@ function readInstalledCoreVersion(cwd) {
|
|
|
181
252
|
}
|
|
182
253
|
}
|
|
183
254
|
|
|
255
|
+
/** The repo's ferst-site.json parsed to an object, or null when absent/unreadable/non-object. */
|
|
256
|
+
function readSiteMeta(cwd) {
|
|
257
|
+
try {
|
|
258
|
+
const parsed = JSON.parse(readFileSync(resolve(cwd, SITE_META), 'utf8'));
|
|
259
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
184
265
|
/** Ensure an `upstream` remote points at `url` (add, or set-url if it already exists). */
|
|
185
266
|
function ensureUpstream(url, cwd) {
|
|
186
267
|
const remotes = (tryGit('remote', cwd) || '').split(/\r?\n/).map((s) => s.trim());
|
|
@@ -240,6 +321,8 @@ function main() {
|
|
|
240
321
|
}
|
|
241
322
|
const branch = parseDefaultBranch(tryGit('ls-remote --symref upstream HEAD', cwd)) || 'main';
|
|
242
323
|
console.log(`ferst-update: upstream default branch = ${branch}`);
|
|
324
|
+
// The exact template commit this run syncs from (for the provenance stamp / #18 ledger).
|
|
325
|
+
const templateCommit = (tryGit(`rev-parse upstream/${branch}`, cwd) || '').trim() || null;
|
|
243
326
|
|
|
244
327
|
let scaffoldCount = 0; // files the scaffold sync touched (counted BEFORE the npm churn)
|
|
245
328
|
if (dryRun) {
|
|
@@ -248,6 +331,10 @@ function main() {
|
|
|
248
331
|
console.log('ferst-update: --dry-run — scaffold changes that WOULD apply:');
|
|
249
332
|
console.log(diff ? diff : ' (no scaffold changes)');
|
|
250
333
|
} else {
|
|
334
|
+
// package.json is template-owned, but its identity fields are the client's — keep them.
|
|
335
|
+
const pkgPath = resolve(cwd, 'package.json');
|
|
336
|
+
let clientPkg = null;
|
|
337
|
+
try { clientPkg = readFileSync(pkgPath, 'utf8'); } catch { /* none yet */ }
|
|
251
338
|
for (const p of ownedPaths) {
|
|
252
339
|
// Per-path so one path that upstream no longer ships (pathspec no-match) can't
|
|
253
340
|
// abort the whole sync — warn and continue.
|
|
@@ -255,6 +342,16 @@ function main() {
|
|
|
255
342
|
console.warn(`ferst-update: could not sync "${p}" (not present upstream?) — skipped.`);
|
|
256
343
|
}
|
|
257
344
|
}
|
|
345
|
+
if (clientPkg) {
|
|
346
|
+
try {
|
|
347
|
+
const synced = readFileSync(pkgPath, 'utf8');
|
|
348
|
+
const kept = keepClientPackageFields(synced, clientPkg);
|
|
349
|
+
if (kept && kept !== synced) {
|
|
350
|
+
writeFileSync(pkgPath, kept);
|
|
351
|
+
console.log("ferst-update: kept the site's own package.json name/description.");
|
|
352
|
+
}
|
|
353
|
+
} catch { /* package.json not synced — nothing to restore */ }
|
|
354
|
+
}
|
|
258
355
|
const status = (tryGit('status --short', cwd) || '').trim();
|
|
259
356
|
scaffoldCount = status ? status.split(/\r?\n/).filter(Boolean).length : 0;
|
|
260
357
|
console.log('ferst-update: scaffold synced. git status --short:');
|
|
@@ -263,12 +360,13 @@ function main() {
|
|
|
263
360
|
|
|
264
361
|
// 3. Engine (ferst-core) update.
|
|
265
362
|
let coreBump = null; // { before, after } once bumped
|
|
363
|
+
let dryLatest = null; // dry-run only: the version `@latest` WOULD install (for the stamp preview)
|
|
266
364
|
if (noCore) {
|
|
267
365
|
console.log('ferst-update: --no-core — skipping the ferst-core npm bump.');
|
|
268
366
|
} else if (dryRun) {
|
|
269
|
-
|
|
367
|
+
dryLatest = (tryExec('npm view ferst-core version', cwd) || '').trim() || null;
|
|
270
368
|
const current = readInstalledCoreVersion(cwd);
|
|
271
|
-
console.log(`ferst-update: ferst-core installed=${current ?? '(none)'} latest=${
|
|
369
|
+
console.log(`ferst-update: ferst-core installed=${current ?? '(none)'} latest=${dryLatest || '(unknown)'}`);
|
|
272
370
|
} else {
|
|
273
371
|
const before = readInstalledCoreVersion(cwd);
|
|
274
372
|
console.log('ferst-update: npm install ferst-core@latest --save ...');
|
|
@@ -297,11 +395,29 @@ function main() {
|
|
|
297
395
|
console.warn(`ferst-update: no "coreRange" field in ${MANIFEST} to update (installed ${after}).`);
|
|
298
396
|
}
|
|
299
397
|
}
|
|
398
|
+
|
|
399
|
+
// Normalise package.json's ferst-core dep to the same canonical range, undoing
|
|
400
|
+
// `--save`'s exact-patch pin (^0.6.2 → ^0.6.0) so it never churns against the
|
|
401
|
+
// template's ownedPath value. The exact version stays in the lock + drift stamp.
|
|
402
|
+
const pkgPath = resolve(cwd, 'package.json');
|
|
403
|
+
try {
|
|
404
|
+
const pkgRaw = readFileSync(pkgPath, 'utf8');
|
|
405
|
+
const pkgNext = replacePackageCoreDep(pkgRaw, next);
|
|
406
|
+
if (pkgNext && pkgNext !== pkgRaw) {
|
|
407
|
+
writeFileSync(pkgPath, pkgNext);
|
|
408
|
+
console.log(`ferst-update: package.json ferst-core → ${next}`);
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
/* no root package.json (shouldn't happen in a client) — skip silently */
|
|
412
|
+
}
|
|
300
413
|
}
|
|
301
414
|
|
|
302
415
|
// 4. Verify — a failed @latest bump is a real incompatibility to surface, so on
|
|
303
416
|
// failure we print the tail and exit non-zero WITHOUT reverting.
|
|
304
417
|
if (dryRun) {
|
|
418
|
+
const wouldCommit = templateCommit ? templateCommit.slice(0, 7) : '(unknown)';
|
|
419
|
+
const wouldCore = (noCore ? readInstalledCoreVersion(cwd) : dryLatest) ?? '(unknown)';
|
|
420
|
+
console.log(`ferst-update: --dry-run — WOULD stamp ${SITE_META}: template ${wouldCommit}, core ${wouldCore} (no write).`);
|
|
305
421
|
console.log('ferst-update: --dry-run complete (no writes, no verify).');
|
|
306
422
|
return;
|
|
307
423
|
}
|
|
@@ -315,12 +431,25 @@ function main() {
|
|
|
315
431
|
}
|
|
316
432
|
}
|
|
317
433
|
|
|
318
|
-
// 5.
|
|
434
|
+
// 5. Provenance drift stamp — record WHICH template commit + core version this repo
|
|
435
|
+
// landed on (only after a fully-successful update), so the repo and the future fleet
|
|
436
|
+
// drift ledger (#18) always know its state. MERGE: preserve gitBase/productionBranch
|
|
437
|
+
// and any other existing ferst-site.json fields.
|
|
438
|
+
const coreVersion = coreBump?.after ?? readInstalledCoreVersion(cwd);
|
|
439
|
+
const provenance = buildProvenance({ upstream: upstream.url, commit: templateCommit, coreVersion });
|
|
440
|
+
writeFileSync(
|
|
441
|
+
resolve(cwd, SITE_META),
|
|
442
|
+
`${JSON.stringify(mergeSiteMeta(readSiteMeta(cwd), provenance), null, 2)}\n`,
|
|
443
|
+
);
|
|
444
|
+
const stampSha = templateCommit ? templateCommit.slice(0, 7) : '(unknown)';
|
|
445
|
+
|
|
446
|
+
// 6. Summary.
|
|
319
447
|
const coreLine = coreBump
|
|
320
448
|
? `ferst-core ${coreBump.before ?? '(none)'} → ${coreBump.after}`
|
|
321
449
|
: 'ferst-core unchanged (--no-core)';
|
|
322
450
|
console.log(
|
|
323
|
-
`ferst-update: DONE — scaffold files updated: ${scaffoldCount}; ${coreLine};
|
|
451
|
+
`ferst-update: DONE — scaffold files updated: ${scaffoldCount}; ${coreLine}; ` +
|
|
452
|
+
`stamped ${SITE_META}: template ${stampSha}, core ${coreVersion ?? '(unknown)'}; build + check:thin green.`,
|
|
324
453
|
);
|
|
325
454
|
}
|
|
326
455
|
|
|
@@ -1,103 +1,107 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Renders an ordered list of blocks by mapping each block's `type` to its
|
|
4
|
-
* component via a registry. Disabled blocks are skipped. The registry is typed
|
|
5
|
-
* `Record<BlockType, …>`, so adding a block type to the union without a renderer
|
|
6
|
-
* here is a compile error — no silent `undefined` component at runtime.
|
|
7
|
-
*/
|
|
8
|
-
import type { Block, BlockType } from '../content/blocks';
|
|
9
|
-
import Heading from './blocks/Heading.astro';
|
|
10
|
-
import Prose from './blocks/Prose.astro';
|
|
11
|
-
import ImageBlock from './blocks/ImageBlock.astro';
|
|
12
|
-
import ButtonBlock from './blocks/ButtonBlock.astro';
|
|
13
|
-
import Quote from './blocks/Quote.astro';
|
|
14
|
-
import Divider from './blocks/Divider.astro';
|
|
15
|
-
import Spacer from './blocks/Spacer.astro';
|
|
16
|
-
import Badge from './blocks/Badge.astro';
|
|
17
|
-
import Stat from './blocks/Stat.astro';
|
|
18
|
-
import List from './blocks/List.astro';
|
|
19
|
-
import Section from './blocks/Section.astro';
|
|
20
|
-
import Grid from './blocks/Grid.astro';
|
|
21
|
-
import Stack from './blocks/Stack.astro';
|
|
22
|
-
import Hero from './recipes/Hero.astro';
|
|
23
|
-
import FeatureCards from './recipes/FeatureCards.astro';
|
|
24
|
-
import Cta from './recipes/Cta.astro';
|
|
25
|
-
import Faq from './recipes/Faq.astro';
|
|
26
|
-
import Gallery from './recipes/Gallery.astro';
|
|
27
|
-
import Testimonial from './recipes/Testimonial.astro';
|
|
28
|
-
import ContactForm from './recipes/ContactForm.astro';
|
|
29
|
-
import Accordion from './recipes/Accordion.astro';
|
|
30
|
-
import Tiles from './recipes/Tiles.astro';
|
|
31
|
-
import Banner from './recipes/Banner.astro';
|
|
32
|
-
import Stats from './recipes/Stats.astro';
|
|
33
|
-
import Steps from './recipes/Steps.astro';
|
|
34
|
-
import Pricing from './recipes/Pricing.astro';
|
|
35
|
-
import Logos from './recipes/Logos.astro';
|
|
36
|
-
import Tabs from './recipes/Tabs.astro';
|
|
37
|
-
import Announcement from './recipes/Announcement.astro';
|
|
38
|
-
import Bento from './recipes/Bento.astro';
|
|
39
|
-
import Marquee from './recipes/Marquee.astro';
|
|
40
|
-
import LatestPosts from './recipes/LatestPosts.astro';
|
|
41
|
-
import MapEmbed from './recipes/MapEmbed.astro';
|
|
42
|
-
import Table from './recipes/Table.astro';
|
|
43
|
-
import Notice from './recipes/Notice.astro';
|
|
44
|
-
import MediaCards from './recipes/MediaCards.astro';
|
|
45
|
-
import ContactCard from './recipes/ContactCard.astro';
|
|
46
|
-
import Locations from './recipes/Locations.astro';
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders an ordered list of blocks by mapping each block's `type` to its
|
|
4
|
+
* component via a registry. Disabled blocks are skipped. The registry is typed
|
|
5
|
+
* `Record<BlockType, …>`, so adding a block type to the union without a renderer
|
|
6
|
+
* here is a compile error — no silent `undefined` component at runtime.
|
|
7
|
+
*/
|
|
8
|
+
import type { Block, BlockType } from '../content/blocks';
|
|
9
|
+
import Heading from './blocks/Heading.astro';
|
|
10
|
+
import Prose from './blocks/Prose.astro';
|
|
11
|
+
import ImageBlock from './blocks/ImageBlock.astro';
|
|
12
|
+
import ButtonBlock from './blocks/ButtonBlock.astro';
|
|
13
|
+
import Quote from './blocks/Quote.astro';
|
|
14
|
+
import Divider from './blocks/Divider.astro';
|
|
15
|
+
import Spacer from './blocks/Spacer.astro';
|
|
16
|
+
import Badge from './blocks/Badge.astro';
|
|
17
|
+
import Stat from './blocks/Stat.astro';
|
|
18
|
+
import List from './blocks/List.astro';
|
|
19
|
+
import Section from './blocks/Section.astro';
|
|
20
|
+
import Grid from './blocks/Grid.astro';
|
|
21
|
+
import Stack from './blocks/Stack.astro';
|
|
22
|
+
import Hero from './recipes/Hero.astro';
|
|
23
|
+
import FeatureCards from './recipes/FeatureCards.astro';
|
|
24
|
+
import Cta from './recipes/Cta.astro';
|
|
25
|
+
import Faq from './recipes/Faq.astro';
|
|
26
|
+
import Gallery from './recipes/Gallery.astro';
|
|
27
|
+
import Testimonial from './recipes/Testimonial.astro';
|
|
28
|
+
import ContactForm from './recipes/ContactForm.astro';
|
|
29
|
+
import Accordion from './recipes/Accordion.astro';
|
|
30
|
+
import Tiles from './recipes/Tiles.astro';
|
|
31
|
+
import Banner from './recipes/Banner.astro';
|
|
32
|
+
import Stats from './recipes/Stats.astro';
|
|
33
|
+
import Steps from './recipes/Steps.astro';
|
|
34
|
+
import Pricing from './recipes/Pricing.astro';
|
|
35
|
+
import Logos from './recipes/Logos.astro';
|
|
36
|
+
import Tabs from './recipes/Tabs.astro';
|
|
37
|
+
import Announcement from './recipes/Announcement.astro';
|
|
38
|
+
import Bento from './recipes/Bento.astro';
|
|
39
|
+
import Marquee from './recipes/Marquee.astro';
|
|
40
|
+
import LatestPosts from './recipes/LatestPosts.astro';
|
|
41
|
+
import MapEmbed from './recipes/MapEmbed.astro';
|
|
42
|
+
import Table from './recipes/Table.astro';
|
|
43
|
+
import Notice from './recipes/Notice.astro';
|
|
44
|
+
import MediaCards from './recipes/MediaCards.astro';
|
|
45
|
+
import ContactCard from './recipes/ContactCard.astro';
|
|
46
|
+
import Locations from './recipes/Locations.astro';
|
|
47
|
+
import Video from './recipes/Video.astro';
|
|
48
|
+
import Documents from './recipes/Documents.astro';
|
|
49
|
+
|
|
50
|
+
interface Props {
|
|
51
|
+
blocks: Block[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { blocks } = Astro.props;
|
|
55
|
+
|
|
56
|
+
const registry: Record<BlockType, any> = {
|
|
57
|
+
heading: Heading,
|
|
58
|
+
prose: Prose,
|
|
59
|
+
image: ImageBlock,
|
|
60
|
+
button: ButtonBlock,
|
|
61
|
+
quote: Quote,
|
|
62
|
+
divider: Divider,
|
|
63
|
+
spacer: Spacer,
|
|
64
|
+
badge: Badge,
|
|
65
|
+
stat: Stat,
|
|
66
|
+
list: List,
|
|
67
|
+
section: Section,
|
|
68
|
+
grid: Grid,
|
|
69
|
+
stack: Stack,
|
|
70
|
+
hero: Hero,
|
|
71
|
+
featureCards: FeatureCards,
|
|
72
|
+
cta: Cta,
|
|
73
|
+
faq: Faq,
|
|
74
|
+
gallery: Gallery,
|
|
75
|
+
testimonial: Testimonial,
|
|
76
|
+
contactForm: ContactForm,
|
|
77
|
+
accordion: Accordion,
|
|
78
|
+
tiles: Tiles,
|
|
79
|
+
banner: Banner,
|
|
80
|
+
stats: Stats,
|
|
81
|
+
steps: Steps,
|
|
82
|
+
pricing: Pricing,
|
|
83
|
+
logos: Logos,
|
|
84
|
+
tabs: Tabs,
|
|
85
|
+
announcement: Announcement,
|
|
86
|
+
bento: Bento,
|
|
87
|
+
marquee: Marquee,
|
|
88
|
+
latestPosts: LatestPosts,
|
|
89
|
+
mapEmbed: MapEmbed,
|
|
90
|
+
table: Table,
|
|
91
|
+
notice: Notice,
|
|
92
|
+
mediaCards: MediaCards,
|
|
93
|
+
contactCard: ContactCard,
|
|
94
|
+
locations: Locations,
|
|
95
|
+
video: Video,
|
|
96
|
+
documents: Documents,
|
|
97
|
+
};
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
blocks
|
|
102
|
+
.filter((block) => block.enabled !== false)
|
|
103
|
+
.map((block) => {
|
|
104
|
+
const Component = registry[block.type];
|
|
105
|
+
return <Component {...block} />;
|
|
106
|
+
})
|
|
107
|
+
}
|
|
@@ -1,39 +1,56 @@
|
|
|
1
|
-
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
</
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.b-prose
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.b-prose
|
|
27
|
-
margin
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
---
|
|
2
|
+
import { proseParts } from '../../lib/prose';
|
|
3
|
+
interface Props {
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
const { text } = Astro.props;
|
|
7
|
+
// Blank lines separate paragraphs; `- ` / `1. ` lines make lists; links, bold and
|
|
8
|
+
// italic are rendered safely (lib/prose.ts → inlineMarkdown).
|
|
9
|
+
const parts = proseParts(text);
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="b-prose">
|
|
13
|
+
{parts.map((part) =>
|
|
14
|
+
part.kind === 'p' ? <p set:html={part.html} />
|
|
15
|
+
: part.kind === 'ul' ? <ul>{part.items.map((i) => <li set:html={i} />)}</ul>
|
|
16
|
+
: <ol>{part.items.map((i) => <li set:html={i} />)}</ol>
|
|
17
|
+
)}
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<style>
|
|
21
|
+
.b-prose {
|
|
22
|
+
color: var(--fg);
|
|
23
|
+
}
|
|
24
|
+
.b-prose p,
|
|
25
|
+
.b-prose ul,
|
|
26
|
+
.b-prose ol {
|
|
27
|
+
margin: 0 0 1rem;
|
|
28
|
+
line-height: 1.7;
|
|
29
|
+
max-width: var(--max);
|
|
30
|
+
}
|
|
31
|
+
.b-prose ul,
|
|
32
|
+
.b-prose ol {
|
|
33
|
+
padding-inline-start: 1.4em;
|
|
34
|
+
}
|
|
35
|
+
.b-prose li + li {
|
|
36
|
+
margin-top: 0.25rem;
|
|
37
|
+
}
|
|
38
|
+
.b-prose > :last-child {
|
|
39
|
+
margin-bottom: 0;
|
|
40
|
+
}
|
|
41
|
+
.b-prose :global(a) {
|
|
42
|
+
color: var(--accent);
|
|
43
|
+
text-decoration: underline;
|
|
44
|
+
text-underline-offset: 0.15em;
|
|
45
|
+
}
|
|
46
|
+
/* The max-width reading column is left-anchored by default; when the page/
|
|
47
|
+
section is centred (or end-aligned) re-anchor it so the text reads as
|
|
48
|
+
deliberately centred, not centred inside a left column. Text-align itself
|
|
49
|
+
is inherited from the content-align cascade. */
|
|
50
|
+
:global([data-content-align='center']) .b-prose > * {
|
|
51
|
+
margin-inline: auto;
|
|
52
|
+
}
|
|
53
|
+
:global([data-content-align='end']) .b-prose > * {
|
|
54
|
+
margin-inline-start: auto;
|
|
55
|
+
}
|
|
56
|
+
</style>
|