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.
@@ -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.2",
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;