ferst-core 0.7.5 → 0.7.7
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-modules.mjs +91 -0
- package/cms/config.template.yml +13 -0
- package/components/BlockRenderer.astro +2 -0
- package/components/ProseParts.astro +26 -0
- package/components/SiteFooter.astro +52 -1
- package/components/SiteHeader.astro +1246 -1243
- package/components/blocks/Prose.astro +12 -17
- package/components/recipes/Accordion.astro +3 -7
- package/components/recipes/Contacts.astro +3 -3
- package/components/recipes/Document.astro +7 -11
- package/components/recipes/Documents.astro +2 -2
- package/components/recipes/Form.astro +30 -0
- package/components/recipes/MediaText.astro +88 -92
- package/components/recipes/Steps.astro +121 -120
- package/components/recipes/Table.astro +3 -2
- package/components/recipes/Video.astro +1 -1
- package/content/blocks.ts +917 -887
- package/content/schemas.ts +5 -0
- package/fonts/OFL.txt +287 -0
- package/fonts/inter-latin-400-normal.woff2 +0 -0
- package/fonts/inter-latin-500-normal.woff2 +0 -0
- package/fonts/inter-latin-600-normal.woff2 +0 -0
- package/fonts/playfair-display-latin-700-normal.woff2 +0 -0
- package/fonts/poppins-latin-300-normal.woff2 +0 -0
- package/fonts/poppins-latin-400-normal.woff2 +0 -0
- package/fonts/poppins-latin-500-normal.woff2 +0 -0
- package/fonts/poppins-latin-600-normal.woff2 +0 -0
- package/fonts/poppins-latin-700-normal.woff2 +0 -0
- package/integration.mjs +41 -1
- package/layouts/Base.astro +1 -0
- package/lib/forms.ts +57 -0
- package/lib/icons.ts +10 -0
- package/lib/links.ts +24 -22
- package/lib/nav.ts +12 -0
- package/lib/siteSettings.ts +2 -1
- package/package.json +5 -3
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ferst-modules — what this site has from its organisation's modules in the Ferst portal:
|
|
3
|
+
// the forms it can place and the calendar it shows. Run it in a client repo root:
|
|
4
|
+
//
|
|
5
|
+
// npx --no-install ferst-modules # the site named in ferst-site.json
|
|
6
|
+
// npx --no-install ferst-modules --site <address> # or ask by the site's address
|
|
7
|
+
// npx --no-install ferst-modules --json # the raw answer
|
|
8
|
+
//
|
|
9
|
+
// Forms and calendars live in the portal, never in the repo: place a form with a
|
|
10
|
+
// `{ "type": "form", "form": "<id>" }` block, and show the calendar by setting
|
|
11
|
+
// calendarSettings.embedUrl to the address printed here.
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { resolve } from 'node:path';
|
|
14
|
+
import { isDirectInvocation, resolveGitBase } from './cms-config.mjs';
|
|
15
|
+
|
|
16
|
+
/** The site to ask about: `--site <ref>`, else ferst-site.json's siteId, else null. */
|
|
17
|
+
export function moduleRef(argv, siteMeta) {
|
|
18
|
+
const i = argv.indexOf('--site');
|
|
19
|
+
if (i !== -1 && argv[i + 1]) return argv[i + 1];
|
|
20
|
+
return siteMeta && typeof siteMeta.siteId === 'string' && siteMeta.siteId ? siteMeta.siteId : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A readable summary of GET /sites/:ref/modules. Pure — unit-tested. */
|
|
24
|
+
export function formatModules(data) {
|
|
25
|
+
const out = [];
|
|
26
|
+
out.push(`Site: ${data.site.address ?? data.site.id} (${data.site.id})`, '');
|
|
27
|
+
|
|
28
|
+
out.push('Forms');
|
|
29
|
+
if (!data.modules.forms) out.push(' The Forms module is off for this site.');
|
|
30
|
+
else if (data.forms.length === 0) out.push(' No forms yet. They are created in the portal (Forms).');
|
|
31
|
+
for (const f of data.forms) {
|
|
32
|
+
out.push(` ${f.name}`);
|
|
33
|
+
out.push(` place it with: { "type": "form", "form": "${f.id}" }`);
|
|
34
|
+
out.push(` fields: ${f.fields.map((x) => `${x.label}${x.required ? ' *' : ''}`).join(', ') || '(none)'}`);
|
|
35
|
+
out.push(` button: ${f.submitLabel}`);
|
|
36
|
+
}
|
|
37
|
+
out.push('');
|
|
38
|
+
|
|
39
|
+
out.push('Events');
|
|
40
|
+
if (!data.events) out.push(' The Events module is off for this site.');
|
|
41
|
+
else {
|
|
42
|
+
out.push(` calendarSettings.embedUrl = ${data.events.embedUrl}`);
|
|
43
|
+
out.push(
|
|
44
|
+
data.events.calendars.length
|
|
45
|
+
? ` shows: ${data.events.calendars.map((c) => c.name).join(', ')}`
|
|
46
|
+
: ' No calendars chosen yet. They are connected in the portal (Events).',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return out.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readSiteMeta(cwd) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(resolve(cwd, 'ferst-site.json'), 'utf8'));
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
const argv = process.argv.slice(2);
|
|
62
|
+
const ref = moduleRef(argv, readSiteMeta(process.cwd()));
|
|
63
|
+
if (!ref) {
|
|
64
|
+
console.error('ferst-modules: no site to ask about. Pass --site <your site address> (e.g. --site example.pages.dev), or add "siteId" to ferst-site.json.');
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
const base = resolveGitBase();
|
|
68
|
+
const url = `${base}/sites/${encodeURIComponent(ref)}/modules`;
|
|
69
|
+
let res;
|
|
70
|
+
try {
|
|
71
|
+
res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error(`ferst-modules: couldn't reach the Ferst platform at ${url}: ${e instanceof Error ? e.message : e}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
if (res.status === 404) {
|
|
77
|
+
console.error(`ferst-modules: the platform doesn't know a site "${ref}". Check the address, or ask the core team (a core-request issue).`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
console.error(`ferst-modules: the platform answered ${res.status} for ${url}.`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
console.log(argv.includes('--json') ? JSON.stringify(data, null, 2) : formatModules(data));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Run when invoked directly as a bin — including via npm's node_modules/.bin symlink.
|
|
89
|
+
if (isDirectInvocation(process.argv[1])) {
|
|
90
|
+
main();
|
|
91
|
+
}
|
package/cms/config.template.yml
CHANGED
|
@@ -126,6 +126,19 @@ collections:
|
|
|
126
126
|
fields:
|
|
127
127
|
- { name: label, label: Label, widget: string }
|
|
128
128
|
- { name: href, label: Link, widget: string }
|
|
129
|
+
- name: groups
|
|
130
|
+
label: Footer columns
|
|
131
|
+
widget: list
|
|
132
|
+
required: false
|
|
133
|
+
hint: Headed columns for policies, safeguarding, documents and forms — keep the main menu short.
|
|
134
|
+
fields:
|
|
135
|
+
- { name: heading, label: Heading, widget: string }
|
|
136
|
+
- name: links
|
|
137
|
+
label: Links
|
|
138
|
+
widget: list
|
|
139
|
+
fields:
|
|
140
|
+
- { name: label, label: Label, widget: string }
|
|
141
|
+
- { name: href, label: Link, widget: string }
|
|
129
142
|
|
|
130
143
|
- name: calendar-settings
|
|
131
144
|
label: Calendar
|
|
@@ -26,6 +26,7 @@ import Faq from './recipes/Faq.astro';
|
|
|
26
26
|
import Gallery from './recipes/Gallery.astro';
|
|
27
27
|
import Testimonial from './recipes/Testimonial.astro';
|
|
28
28
|
import ContactForm from './recipes/ContactForm.astro';
|
|
29
|
+
import Form from './recipes/Form.astro';
|
|
29
30
|
import Accordion from './recipes/Accordion.astro';
|
|
30
31
|
import Tiles from './recipes/Tiles.astro';
|
|
31
32
|
import Banner from './recipes/Banner.astro';
|
|
@@ -79,6 +80,7 @@ const registry: Record<BlockType, any> = {
|
|
|
79
80
|
gallery: Gallery,
|
|
80
81
|
testimonial: Testimonial,
|
|
81
82
|
contactForm: ContactForm,
|
|
83
|
+
form: Form,
|
|
82
84
|
accordion: Accordion,
|
|
83
85
|
tiles: Tiles,
|
|
84
86
|
banner: Banner,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ProseParts — renders prose text as paragraphs and lists (lib/prose: blank lines →
|
|
4
|
+
* paragraphs, `- ` / `1. ` lines → lists, inline links / bold / italic, all escaped).
|
|
5
|
+
* The one place this markup lives, shared by prose, document, accordion, mediaText and
|
|
6
|
+
* table intros, so a fix lands everywhere at once.
|
|
7
|
+
*
|
|
8
|
+
* Note for callers: these elements carry THIS component's style scope, so a parent styles
|
|
9
|
+
* them through its own wrapper with `:global(p)`, `:global(ul)` etc.
|
|
10
|
+
*/
|
|
11
|
+
import { proseParts } from '../lib/prose';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
text: string | undefined | null;
|
|
15
|
+
/** Optional class on each paragraph / list (e.g. a table intro's). */
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
const { text, class: cls } = Astro.props;
|
|
19
|
+
const parts = proseParts(text);
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
{parts.map((part) =>
|
|
23
|
+
part.kind === 'p' ? <p class={cls} set:html={part.html} />
|
|
24
|
+
: part.kind === 'ul' ? <ul class={cls}>{part.items.map((x) => <li set:html={x} />)}</ul>
|
|
25
|
+
: <ol class={cls}>{part.items.map((x) => <li set:html={x} />)}</ol>
|
|
26
|
+
)}
|
|
@@ -28,9 +28,11 @@ interface Props {
|
|
|
28
28
|
tagline?: string;
|
|
29
29
|
/** The site's call-to-action (navbarSettings.cta), shown beside Facebook; null hides it. */
|
|
30
30
|
cta?: { label: string; href: string } | null;
|
|
31
|
+
/** Headed columns of reference links (policies, documents, forms). */
|
|
32
|
+
groups?: Array<{ heading: string; links: NavLink[] }>;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
const { facebookUrl, footerLinks, footerClass, shortName, tagline, cta = { label: 'Contact', href: '/contact/' } } = Astro.props;
|
|
35
|
+
const { facebookUrl, footerLinks, footerClass, shortName, tagline, cta = { label: 'Contact', href: '/contact/' }, groups = [] } = Astro.props;
|
|
34
36
|
const year = new Date().getFullYear();
|
|
35
37
|
---
|
|
36
38
|
|
|
@@ -65,6 +67,19 @@ const year = new Date().getFullYear();
|
|
|
65
67
|
</div>
|
|
66
68
|
</div>
|
|
67
69
|
|
|
70
|
+
{groups.length > 0 && (
|
|
71
|
+
<nav class="footer-groups" aria-label="Site information">
|
|
72
|
+
{groups.map((g) => (
|
|
73
|
+
<div class="footer-group">
|
|
74
|
+
<h2 class="footer-group__title">{g.heading}</h2>
|
|
75
|
+
<ul>
|
|
76
|
+
{g.links.map((l) => <li><a href={l.href}>{l.label}</a></li>)}
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</nav>
|
|
81
|
+
)}
|
|
82
|
+
|
|
68
83
|
<div class="footer-bottom">
|
|
69
84
|
<nav class="footer-nav" aria-label="Footer navigation">
|
|
70
85
|
{footerLinks.map((link) => (
|
|
@@ -271,4 +286,40 @@ const year = new Date().getFullYear();
|
|
|
271
286
|
gap: 1rem;
|
|
272
287
|
}
|
|
273
288
|
}
|
|
289
|
+
|
|
290
|
+
/* Headed columns of reference links: policies, documents, forms. */
|
|
291
|
+
.site-footer .footer-groups {
|
|
292
|
+
display: grid;
|
|
293
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
|
|
294
|
+
gap: 1.5rem 2rem;
|
|
295
|
+
max-width: 72rem;
|
|
296
|
+
margin: 0 auto;
|
|
297
|
+
padding: 1.75rem clamp(1.25rem, 4vw, 2.5rem) 0.5rem;
|
|
298
|
+
}
|
|
299
|
+
.site-footer .footer-group__title {
|
|
300
|
+
margin: 0 0 0.6rem;
|
|
301
|
+
font-family: var(--font-heading);
|
|
302
|
+
font-size: 0.75rem;
|
|
303
|
+
font-weight: 600;
|
|
304
|
+
letter-spacing: 0.1em;
|
|
305
|
+
text-transform: uppercase;
|
|
306
|
+
color: var(--text-footer);
|
|
307
|
+
opacity: 0.8;
|
|
308
|
+
}
|
|
309
|
+
.site-footer .footer-group ul {
|
|
310
|
+
list-style: none;
|
|
311
|
+
margin: 0;
|
|
312
|
+
padding: 0;
|
|
313
|
+
display: grid;
|
|
314
|
+
gap: 0.45rem;
|
|
315
|
+
}
|
|
316
|
+
.site-footer .footer-group a {
|
|
317
|
+
color: var(--text-footer-link);
|
|
318
|
+
text-decoration: none;
|
|
319
|
+
font-size: 0.9375rem;
|
|
320
|
+
}
|
|
321
|
+
.site-footer .footer-group a:hover,
|
|
322
|
+
.site-footer .footer-group a:focus-visible {
|
|
323
|
+
text-decoration: underline;
|
|
324
|
+
}
|
|
274
325
|
</style>
|