ferst-core 0.9.2 → 0.9.4
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/components/Button.astro +8 -8
- package/components/CalendarEmbed.astro +1 -1
- package/components/CookieBanner.astro +1 -1
- package/components/PostsFilter.astro +1 -1
- package/components/SiteFooter.astro +1 -0
- package/components/SiteHeader.astro +1 -1
- package/components/StickyCallout.astro +1 -1
- package/components/recipes/Announcement.astro +1 -1
- package/components/recipes/Bento.astro +4 -2
- package/components/recipes/Hero.astro +533 -533
- package/components/recipes/LatestPosts.astro +1 -1
- package/components/recipes/LinkRow.astro +3 -1
- package/components/recipes/Locations.astro +1 -1
- package/components/recipes/MapEmbed.astro +1 -1
- package/components/recipes/Pricing.astro +1 -1
- package/components/recipes/Steps.astro +1 -1
- package/components/recipes/Tiles.astro +4 -2
- package/components/recipes/Video.astro +1 -1
- package/content/accessibility.ts +62 -0
- package/content/schemas.ts +2 -1
- package/integration.mjs +39 -3
- package/layouts/Base.astro +17 -2
- package/lib/contrast.ts +146 -0
- package/lib/pages.ts +18 -0
- package/lib/seo.mjs +16 -0
- package/lib/structured-data.mjs +37 -11
- package/lib/themeDefaults.ts +15 -0
- package/lib/themeTokens.ts +11 -9
- package/package.json +1 -1
- package/routes/accessibility.astro +44 -0
- package/styles/theme.css +2 -0
package/lib/themeTokens.ts
CHANGED
|
@@ -25,14 +25,11 @@
|
|
|
25
25
|
* Pure + framework-free so it is unit-testable without a render.
|
|
26
26
|
*/
|
|
27
27
|
import type { ThemeSettings } from '../content/schemas';
|
|
28
|
+
import { DARK_DEFAULTS, NEUTRAL_SOURCE } from './themeDefaults';
|
|
29
|
+
import { onBrandColour } from './contrast';
|
|
30
|
+
|
|
31
|
+
export { DARK_DEFAULTS, NEUTRAL_SOURCE };
|
|
28
32
|
|
|
29
|
-
/** The neutral source triple — used to fill any colour a client leaves blank so
|
|
30
|
-
* the derivation is always self-contained (mirrors `styles/theme.css`). */
|
|
31
|
-
export const NEUTRAL_SOURCE = {
|
|
32
|
-
brand: '#4F46E5',
|
|
33
|
-
ink: '#1F2937',
|
|
34
|
-
surface: '#F1F5F9',
|
|
35
|
-
} as const;
|
|
36
33
|
|
|
37
34
|
/** Corner-knob vocabulary → the `--radius` value every surface reads. */
|
|
38
35
|
export const CORNERS_PX: Record<ThemeSettings['corners'], string> = {
|
|
@@ -111,10 +108,11 @@ const DARK_DOWNSTREAM = [
|
|
|
111
108
|
'--gold-light:oklch(from var(--dark-accent) calc(l - 0.46) calc(c * 0.6) h)',
|
|
112
109
|
'--accent-2:oklch(from var(--dark-accent) calc(l - 0.08) c calc(h + 26))',
|
|
113
110
|
'--text-footer-link:var(--dark-accent)',
|
|
111
|
+
// Text on brand fills: the accent is light in dark mode, so its text is the dark ground
|
|
112
|
+
// (light text on it fell to ~1.9:1; lib/contrast.ts checks it).
|
|
113
|
+
'--on-brand:var(--dark-ground)',
|
|
114
114
|
].join(';');
|
|
115
115
|
|
|
116
|
-
/** Defaults for the dark-formula knobs (also the CMS/schema defaults). */
|
|
117
|
-
export const DARK_DEFAULTS = { depth: 0.21, chroma: 0.05 } as const;
|
|
118
116
|
|
|
119
117
|
/** Clamp a numeric knob to a range, falling back to `fallback` when absent/NaN. */
|
|
120
118
|
function clampNum(v: unknown, min: number, max: number, fallback: number): number {
|
|
@@ -188,6 +186,10 @@ export function buildThemeStyle(theme: ThemeSettings): ThemeStyle {
|
|
|
188
186
|
rootDecls.push(`--brand:${brand || NEUTRAL_SOURCE.brand}`);
|
|
189
187
|
rootDecls.push(`--ink:${ink || NEUTRAL_SOURCE.ink}`);
|
|
190
188
|
rootDecls.push(`--surface:${surface || NEUTRAL_SOURCE.surface}`);
|
|
189
|
+
// Text on brand fills: white or the ink, whichever reads better on the brand (dark mode
|
|
190
|
+
// overrides it with the dark ground). A colour it can't read leaves theme.css's white.
|
|
191
|
+
const onBrand = onBrandColour(brand || NEUTRAL_SOURCE.brand, ink || NEUTRAL_SOURCE.ink);
|
|
192
|
+
if (onBrand) rootDecls.push(`--on-brand:${onBrand}`);
|
|
191
193
|
}
|
|
192
194
|
const fh = clean(theme.fonts?.heading);
|
|
193
195
|
const fb = clean(theme.fonts?.body);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ferst-core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ferst Core — the shared, brand-agnostic client-site system: Astro components, layouts, content schemas, the layered config resolver, and a neutral styling architecture that every client overrides.",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The accessibility statement (`/accessibility`) — INJECTED by the ferst() integration, and
|
|
4
|
+
* linked from every page's footer. Generated from the site's name and contact details
|
|
5
|
+
* (content/accessibility.ts). A site with its own `src/pages/accessibility.astro` keeps it
|
|
6
|
+
* (the integration skips this route then).
|
|
7
|
+
*/
|
|
8
|
+
import Base from '../layouts/Base.astro';
|
|
9
|
+
import { loadSiteSettings } from '../lib/siteSettings';
|
|
10
|
+
import { accessibilityStatement } from '../content/accessibility';
|
|
11
|
+
|
|
12
|
+
const { identity, contact } = await loadSiteSettings();
|
|
13
|
+
const statement = accessibilityStatement({
|
|
14
|
+
siteName: identity.siteTitle || identity.shortName,
|
|
15
|
+
email: contact.email || undefined,
|
|
16
|
+
phone: contact.phoneDisplay || undefined,
|
|
17
|
+
});
|
|
18
|
+
const tel = contact.phoneTel || contact.phoneDisplay;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<Base title={statement.title} description={`How ${identity.shortName || identity.siteTitle} works for everyone, and how to report a problem.`}>
|
|
22
|
+
<section class="b-accessibility">
|
|
23
|
+
<h1>{statement.title}</h1>
|
|
24
|
+
<p>{statement.intro}</p>
|
|
25
|
+
{statement.sections.map((s) => (
|
|
26
|
+
<>
|
|
27
|
+
<h2>{s.heading}</h2>
|
|
28
|
+
{s.paragraphs.map((p) => <p>{p}</p>)}
|
|
29
|
+
{s.items && <ul>{s.items.map((i) => <li>{i}</li>)}</ul>}
|
|
30
|
+
{s.contact && (
|
|
31
|
+
<ul>
|
|
32
|
+
{s.contact.email && <li>Email: <a href={`mailto:${s.contact.email}`}>{s.contact.email}</a></li>}
|
|
33
|
+
{s.contact.phone && <li>Phone: <a href={`tel:${tel}`}>{s.contact.phone}</a></li>}
|
|
34
|
+
</ul>
|
|
35
|
+
)}
|
|
36
|
+
</>
|
|
37
|
+
))}
|
|
38
|
+
</section>
|
|
39
|
+
</Base>
|
|
40
|
+
|
|
41
|
+
<style>
|
|
42
|
+
.b-accessibility { max-width: 52rem; margin-inline: auto; padding: 2rem 1rem 3rem; }
|
|
43
|
+
.b-accessibility ul { padding-inline-start: 1.25rem; }
|
|
44
|
+
</style>
|
package/styles/theme.css
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
--gold-light: #E0E7FF;
|
|
42
42
|
--brown: #4B5563;
|
|
43
43
|
--text-light: #FFFFFF;
|
|
44
|
+
--on-brand: #FFFFFF; /* text on brand-coloured fills (buttons, badges) */
|
|
44
45
|
--text-footer: #4B5563;
|
|
45
46
|
--text-footer-link: #4F46E5;
|
|
46
47
|
--text-copyright: #94A3B8;
|
|
@@ -109,6 +110,7 @@
|
|
|
109
110
|
--gold-light: #312E81;
|
|
110
111
|
--brown: #94A3B8;
|
|
111
112
|
--text-light: #E2E8F0;
|
|
113
|
+
--on-brand: #0F172A; /* the light accent needs dark text */
|
|
112
114
|
--text-footer: #94A3B8;
|
|
113
115
|
--text-footer-link: #818CF8;
|
|
114
116
|
--text-copyright: #64748B;
|