ferst-core 0.2.0 → 0.2.2

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.
@@ -59,10 +59,18 @@ const display = Number.isNaN(d.valueOf())
59
59
  }
60
60
  .b-post-card__media {
61
61
  display: block;
62
+ position: relative;
62
63
  aspect-ratio: 16 / 9;
64
+ overflow: hidden;
63
65
  background: var(--bg-section);
64
66
  }
65
- .b-post-card__media img {
67
+ /* Absolutely-positioned + object-fit:cover so ANY source ratio is cropped to a
68
+ uniform 16:9 tile — never stretched, never overflowing into the text. The
69
+ compound selector (and inset/size) also out-specifies the global
70
+ `img { height:auto }` in Base, which otherwise lets tall images blow the box. */
71
+ .b-post-card .b-post-card__media img {
72
+ position: absolute;
73
+ inset: 0;
66
74
  width: 100%;
67
75
  height: 100%;
68
76
  object-fit: cover;
@@ -5,22 +5,26 @@ interface Props {
5
5
  align?: 'start' | 'center' | 'end';
6
6
  gradient?: boolean;
7
7
  }
8
- const { text, level = 2, align = 'start', gradient = false } = Astro.props;
8
+ const { text, level = 2, align, gradient = false } = Astro.props;
9
9
  const Tag = `h${level}` as unknown as keyof HTMLElementTagNameMap;
10
10
  ---
11
11
 
12
12
  <Tag class="b-heading" data-align={align} data-gradient={gradient ? 'true' : 'false'}>{text}</Tag>
13
13
 
14
14
  <style>
15
- /* A heading owns its own alignment via the `align` prop it never depends on
16
- an ancestor rule. (Base no longer force-centres headings, so a recipe that
17
- asks for `start` really is left-aligned, and `center` really is centred,
18
- wherever the block is placed including AI-authored block JSON.) */
15
+ /* By default a heading INHERITS the page/section content alignment (the
16
+ `--content-align` cascade set on `.site-main` / a `section`), so a whole
17
+ template stays coherent. An explicit `align` prop overrides it for this one
18
+ heading. (`text-align` inherits, and inheritance carries the resolved
19
+ alignment; the `[data-align]` rules win over it when set.) */
19
20
  .b-heading {
20
21
  color: var(--fg);
21
22
  margin: 0;
22
23
  text-wrap: balance;
23
24
  line-height: 1.2;
25
+ text-align: inherit;
26
+ }
27
+ .b-heading[data-align='start'] {
24
28
  text-align: start;
25
29
  }
26
30
  .b-heading[data-align='center'] {
@@ -29,6 +29,13 @@ const Tag = style === 'number' ? 'ol' : 'ul';
29
29
  gap: 0.6rem;
30
30
  color: var(--muted);
31
31
  line-height: 1.6;
32
+ text-align: start;
33
+ }
34
+ /* A bulleted/numbered list always reads left; on a centred page centre the
35
+ list as a block but keep the items left-aligned (centred markers read badly). */
36
+ :global([data-content-align='center']) .b-list {
37
+ width: fit-content;
38
+ margin-inline: auto;
32
39
  }
33
40
  .b-list__item {
34
41
  display: flex;
@@ -26,4 +26,14 @@ const paragraphs = text
26
26
  .b-prose p:last-child {
27
27
  margin-bottom: 0;
28
28
  }
29
+ /* The max-width reading column is left-anchored by default; when the page/
30
+ section is centred (or end-aligned) re-anchor it so the text reads as
31
+ deliberately centred, not centred inside a left column. Text-align itself
32
+ is inherited from the content-align cascade. */
33
+ :global([data-content-align='center']) .b-prose p {
34
+ margin-inline: auto;
35
+ }
36
+ :global([data-content-align='end']) .b-prose p {
37
+ margin-inline-start: auto;
38
+ }
29
39
  </style>
@@ -17,6 +17,12 @@ const { text, cite } = Astro.props;
17
17
  border-inline-start: 3px solid var(--gold);
18
18
  padding-inline-start: 1rem;
19
19
  }
20
+ /* On a centred page/section the left rule reads as off-axis — drop it so the
21
+ quote centres cleanly. (Inherits text-align from the content-align cascade.) */
22
+ :global([data-content-align='center']) .b-quote {
23
+ border-inline-start: none;
24
+ padding-inline-start: 0;
25
+ }
20
26
  .b-quote blockquote {
21
27
  margin: 0;
22
28
  color: var(--fg);
@@ -10,13 +10,15 @@ import BlockRenderer from '../BlockRenderer.astro';
10
10
  interface Props {
11
11
  background?: 'none' | 'muted' | 'accent';
12
12
  width?: 'normal' | 'wide' | 'full';
13
+ /** Override the page's content alignment for this section's subtree. */
14
+ align?: 'start' | 'center' | 'end';
13
15
  blocks?: any[];
14
16
  }
15
- const { background = 'none', width = 'normal', blocks = [] } = Astro.props;
17
+ const { background = 'none', width = 'normal', align, blocks = [] } = Astro.props;
16
18
  ---
17
19
 
18
20
  <section class="b-section" data-bg={background}>
19
- <div class="b-section__inner" data-width={width}>
21
+ <div class="b-section__inner" data-width={width} data-content-align={align}>
20
22
  <BlockRenderer blocks={blocks} />
21
23
  </div>
22
24
  </section>
package/content/blocks.ts CHANGED
@@ -19,7 +19,9 @@ export const headingBlock = z.object({
19
19
  enabled: z.boolean().default(true),
20
20
  text: z.string(),
21
21
  level: z.number().int().min(1).max(6).default(2),
22
- align: z.enum(['start', 'center', 'end']).default('start'),
22
+ /** Explicit per-heading alignment. OMITTED ⇒ inherit the page/section
23
+ * `--content-align` cascade; set it only to override for this one heading. */
24
+ align: z.enum(['start', 'center', 'end']).optional(),
23
25
  /** Paint the text with a brand → accent-2 gradient (falls back to solid ink
24
26
  * where background-clip:text is unsupported). A per-block opt-in effect. */
25
27
  gradient: z.boolean().default(false),
@@ -446,6 +448,8 @@ export const sectionBlock = z.object({
446
448
  enabled: z.boolean().default(true),
447
449
  background: z.enum(['none', 'muted', 'accent']).default('none'),
448
450
  width: z.enum(['normal', 'wide', 'full']).default('normal'),
451
+ /** Override the page's content alignment for this section's subtree. */
452
+ align: z.enum(['start', 'center', 'end']).optional(),
449
453
  blocks: z.lazy(() => z.array(blockSchema)).default([]),
450
454
  });
451
455
 
@@ -512,6 +516,15 @@ export const pageSchema = z.object({
512
516
  description: z.string().optional(),
513
517
  /** Preloaded as the LCP image (passed to the layout) when set. */
514
518
  heroImage: z.string().optional(),
519
+ /**
520
+ * Template-level content alignment — the default for text-bearing blocks on the
521
+ * page (headings, prose, quote). Cascades as `--content-align` so every recipe
522
+ * inherits one coherent alignment instead of each hard-coding its own. A
523
+ * `section` block can override it for its subtree; a `heading` can override per
524
+ * block. `start` (left) is the readable default for long-form; `center` suits
525
+ * short, hero-led pages.
526
+ */
527
+ align: z.enum(['start', 'center', 'end']).default('start'),
515
528
  /**
516
529
  * Provenance — the ferst-library template this page was instantiated from.
517
530
  * The triangular model (docs/BLOCK-ARCHITECTURE.md §5): lego box → a template
@@ -37,6 +37,9 @@ interface Props {
37
37
  footerClass?: string;
38
38
  /** Render the secondary logo centred at the bottom of the page content. */
39
39
  showSecondaryLogo?: boolean;
40
+ /** Template-level content alignment — cascades to text-bearing blocks via
41
+ * `data-content-align` on <main>. `start` (default) keeps long-form readable. */
42
+ align?: 'start' | 'center' | 'end';
40
43
  }
41
44
 
42
45
  const {
@@ -47,6 +50,7 @@ const {
47
50
  mainClass,
48
51
  footerClass,
49
52
  showSecondaryLogo = false,
53
+ align = 'start',
50
54
  } = Astro.props;
51
55
 
52
56
  const settings = await loadSiteSettings();
@@ -144,7 +148,7 @@ const metaDescription = description ?? identity.description;
144
148
  dropdown={navigation.dropdown}
145
149
  />
146
150
 
147
- <main class:list={['site-main', mainClass]}>
151
+ <main class:list={['site-main', mainClass]} data-content-align={align}>
148
152
  <slot name="hero" />
149
153
  <slot />
150
154
  {showSecondaryLogo && (secondaryLogoLight || secondaryLogoDark) && (
@@ -356,6 +360,23 @@ const metaDescription = description ?? identity.description;
356
360
  margin-block: 0;
357
361
  }
358
362
 
363
+ /* ── Content alignment cascade ──────────────────────────────────────
364
+ A page sets its default alignment via the `align` prop (→
365
+ data-content-align on <main>); a `section` block can override it for
366
+ its own subtree. Text-bearing blocks inherit `text-align` from here, so
367
+ a whole template stays on one alignment axis instead of each recipe
368
+ hard-coding its own. A few blocks adjust when centred so the result
369
+ reads as deliberately centred, not centred-inside-a-left-column.
370
+ `start` (left) is the readable default for long-form; a heading can
371
+ still override per-block via its own `align`. */
372
+ [data-content-align='start'] { text-align: start; }
373
+ [data-content-align='center'] { text-align: center; }
374
+ [data-content-align='end'] { text-align: end; }
375
+ /* Each content block owns how it ADJUSTS when centred/ended (re-centring
376
+ its reading column, dropping a left rule, etc.) in its own component,
377
+ so the rule out-specifies the block's own scoped styles. Here we only
378
+ set the inherited text-align axis. */
379
+
359
380
  /* ── Secondary logo block: centered ── */
360
381
  .secondary-logo {
361
382
  display: flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ferst-core",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",