@cogenta/theme-magazine 1.1.2 → 1.3.3

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.
Files changed (126) hide show
  1. package/dist/src/index.d.ts +3 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +3 -0
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/render/article.d.ts +32 -0
  6. package/dist/src/render/article.d.ts.map +1 -0
  7. package/dist/src/render/article.js +137 -0
  8. package/dist/src/render/article.js.map +1 -0
  9. package/dist/src/render/blocks/accordion.d.ts.map +1 -1
  10. package/dist/src/render/blocks/accordion.js +9 -13
  11. package/dist/src/render/blocks/accordion.js.map +1 -1
  12. package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
  13. package/dist/src/render/blocks/collection-list.js +91 -118
  14. package/dist/src/render/blocks/collection-list.js.map +1 -1
  15. package/dist/src/render/blocks/cta.d.ts +4 -4
  16. package/dist/src/render/blocks/cta.d.ts.map +1 -1
  17. package/dist/src/render/blocks/cta.js +7 -9
  18. package/dist/src/render/blocks/cta.js.map +1 -1
  19. package/dist/src/render/blocks/embed.d.ts.map +1 -1
  20. package/dist/src/render/blocks/embed.js +17 -20
  21. package/dist/src/render/blocks/embed.js.map +1 -1
  22. package/dist/src/render/blocks/faq.d.ts.map +1 -1
  23. package/dist/src/render/blocks/faq.js +12 -11
  24. package/dist/src/render/blocks/faq.js.map +1 -1
  25. package/dist/src/render/blocks/feature-grid.d.ts.map +1 -1
  26. package/dist/src/render/blocks/feature-grid.js +13 -20
  27. package/dist/src/render/blocks/feature-grid.js.map +1 -1
  28. package/dist/src/render/blocks/gallery.d.ts +6 -6
  29. package/dist/src/render/blocks/gallery.d.ts.map +1 -1
  30. package/dist/src/render/blocks/gallery.js +15 -13
  31. package/dist/src/render/blocks/gallery.js.map +1 -1
  32. package/dist/src/render/blocks/hero.d.ts +6 -5
  33. package/dist/src/render/blocks/hero.d.ts.map +1 -1
  34. package/dist/src/render/blocks/hero.js +15 -14
  35. package/dist/src/render/blocks/hero.js.map +1 -1
  36. package/dist/src/render/blocks/logo-strip.d.ts.map +1 -1
  37. package/dist/src/render/blocks/logo-strip.js +8 -11
  38. package/dist/src/render/blocks/logo-strip.js.map +1 -1
  39. package/dist/src/render/blocks/logos.d.ts.map +1 -1
  40. package/dist/src/render/blocks/logos.js +16 -16
  41. package/dist/src/render/blocks/logos.js.map +1 -1
  42. package/dist/src/render/blocks/media-figure.d.ts +8 -8
  43. package/dist/src/render/blocks/media-figure.d.ts.map +1 -1
  44. package/dist/src/render/blocks/media-figure.js +23 -19
  45. package/dist/src/render/blocks/media-figure.js.map +1 -1
  46. package/dist/src/render/blocks/pricing-table.d.ts.map +1 -1
  47. package/dist/src/render/blocks/pricing-table.js +19 -22
  48. package/dist/src/render/blocks/pricing-table.js.map +1 -1
  49. package/dist/src/render/blocks/prose.d.ts +6 -10
  50. package/dist/src/render/blocks/prose.d.ts.map +1 -1
  51. package/dist/src/render/blocks/prose.js +8 -11
  52. package/dist/src/render/blocks/prose.js.map +1 -1
  53. package/dist/src/render/blocks/quote.d.ts +6 -9
  54. package/dist/src/render/blocks/quote.d.ts.map +1 -1
  55. package/dist/src/render/blocks/quote.js +11 -13
  56. package/dist/src/render/blocks/quote.js.map +1 -1
  57. package/dist/src/render/blocks/stat-counter.d.ts.map +1 -1
  58. package/dist/src/render/blocks/stat-counter.js +9 -10
  59. package/dist/src/render/blocks/stat-counter.js.map +1 -1
  60. package/dist/src/render/blocks/stats.d.ts.map +1 -1
  61. package/dist/src/render/blocks/stats.js +9 -12
  62. package/dist/src/render/blocks/stats.js.map +1 -1
  63. package/dist/src/render/blocks/testimonial.d.ts +4 -13
  64. package/dist/src/render/blocks/testimonial.d.ts.map +1 -1
  65. package/dist/src/render/blocks/testimonial.js +7 -15
  66. package/dist/src/render/blocks/testimonial.js.map +1 -1
  67. package/dist/src/render/chrome.d.ts.map +1 -1
  68. package/dist/src/render/chrome.js +109 -97
  69. package/dist/src/render/chrome.js.map +1 -1
  70. package/dist/src/render/layout.d.ts +28 -0
  71. package/dist/src/render/layout.d.ts.map +1 -0
  72. package/dist/src/render/layout.js +72 -0
  73. package/dist/src/render/layout.js.map +1 -0
  74. package/dist/src/render/render-block.d.ts +15 -13
  75. package/dist/src/render/render-block.d.ts.map +1 -1
  76. package/dist/src/render/render-block.js +53 -29
  77. package/dist/src/render/render-block.js.map +1 -1
  78. package/dist/src/render/story.d.ts +41 -0
  79. package/dist/src/render/story.d.ts.map +1 -0
  80. package/dist/src/render/story.js +65 -0
  81. package/dist/src/render/story.js.map +1 -0
  82. package/dist/src/render/term-archive.d.ts +0 -10
  83. package/dist/src/render/term-archive.d.ts.map +1 -1
  84. package/dist/src/render/term-archive.js +42 -19
  85. package/dist/src/render/term-archive.js.map +1 -1
  86. package/dist/src/render/widget-areas.d.ts +12 -0
  87. package/dist/src/render/widget-areas.d.ts.map +1 -0
  88. package/dist/src/render/widget-areas.js +19 -0
  89. package/dist/src/render/widget-areas.js.map +1 -0
  90. package/dist/theme.config.js +2 -2
  91. package/dist/theme.config.js.map +1 -1
  92. package/package.json +5 -5
  93. package/src/index.ts +3 -0
  94. package/src/render/article.ts +239 -0
  95. package/src/render/blocks/accordion.ts +23 -30
  96. package/src/render/blocks/collection-list.ts +161 -225
  97. package/src/render/blocks/cta.ts +19 -13
  98. package/src/render/blocks/embed.ts +40 -36
  99. package/src/render/blocks/faq.ts +31 -36
  100. package/src/render/blocks/feature-grid.ts +26 -36
  101. package/src/render/blocks/gallery.ts +24 -18
  102. package/src/render/blocks/hero.ts +32 -32
  103. package/src/render/blocks/logo-strip.ts +17 -17
  104. package/src/render/blocks/logos.ts +23 -31
  105. package/src/render/blocks/media-figure.ts +36 -20
  106. package/src/render/blocks/pricing-table.ts +29 -34
  107. package/src/render/blocks/prose.ts +13 -13
  108. package/src/render/blocks/quote.ts +24 -24
  109. package/src/render/blocks/stat-counter.ts +16 -24
  110. package/src/render/blocks/stats.ts +14 -24
  111. package/src/render/blocks/testimonial.ts +12 -17
  112. package/src/render/chrome.ts +123 -104
  113. package/src/render/layout.ts +105 -0
  114. package/src/render/render-block.ts +61 -33
  115. package/src/render/story.ts +139 -0
  116. package/src/render/term-archive.ts +111 -58
  117. package/src/render/widget-areas.ts +22 -0
  118. package/src/styles/archive.css +114 -17
  119. package/src/styles/article.css +237 -0
  120. package/src/styles/base.css +848 -591
  121. package/src/styles/blocks.css +903 -921
  122. package/src/styles/stories.css +593 -0
  123. package/src/styles/theme.css +28 -9
  124. package/src/styles/tokens.css +200 -185
  125. package/src/styles/widgets.css +614 -0
  126. package/tokens.json +18 -18
@@ -1,16 +1,17 @@
1
1
  import type { EmbedBlock } from '@cogenta/blocks'
2
2
  import { aspectRatio, type HtmlElement, h, type RenderContext } from '@cogenta/theme-kit'
3
+ import { section } from '../layout.js'
3
4
 
4
5
  /**
5
- * Nothing here contacts a third party before the visitor has consented — the
6
- * same rule the reference theme enforces, reused verbatim: an iframe (or a
7
- * thumbnail fetch) already hands the provider an IP address and a cookie,
8
- * which is exactly the transfer consent is supposed to gate. No consent
9
- * *button* ships either, because this theme ships no JavaScript to wire one
10
- * to: granting consent is a site-wide decision for a future consent layer,
11
- * not a block's.
6
+ * Nothing here contacts a third party before the visitor consents: when
7
+ * `consentRequired` is set, or the provider has no cookie-free frame source,
8
+ * a notice and an outbound link render instead of an `<iframe>`. No
9
+ * preconnect, no thumbnail fetched from the provider.
10
+ *
11
+ * The frame and the notice both take the reading column at the block's
12
+ * ratio. The notice holds the place the player will take: a hairline frame,
13
+ * the provider named in small capitals, one sentence, one arrow link.
12
14
  */
13
-
14
15
  function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string | null {
15
16
  const url = URL.parse(rawUrl)
16
17
  if (url === null) return null
@@ -40,23 +41,20 @@ function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string |
40
41
  case 'soundcloud':
41
42
  return `https://w.soundcloud.com/player/?url=${encodeURIComponent(rawUrl)}`
42
43
  default:
43
- // bluesky, mastodon, other: a post embed is a script tag this theme
44
- // does not load. The sidebar note card is the rendering.
45
44
  return null
46
45
  }
47
46
  }
48
47
 
49
- /** A boxed sidebar note — dashed rule, small caps label — the editorial way of saying "not loaded here". */
50
- function noteCard(block: EmbedBlock, ctx: RenderContext, reason: string): HtmlElement {
48
+ function notice(block: EmbedBlock, ctx: RenderContext, reason: string): HtmlElement {
51
49
  return h(
52
50
  'div',
53
- { class: 'cg-sidenote__box' },
54
- h('p', { class: 'cg-sidenote__label' }, ctx.t('embed.label')),
55
- h('p', { class: 'cg-sidenote__notice' }, reason),
51
+ { class: 'cg-embed__notice' },
52
+ h('p', { class: 'cg-embed__label' }, ctx.t('embed.label')),
53
+ h('p', { class: 'cg-embed__reason' }, reason),
56
54
  h(
57
55
  'a',
58
56
  {
59
- class: 'cg-sidenote__link',
57
+ class: 'cg-arrow-link cg-embed__link',
60
58
  href: ctx.link(block.url),
61
59
  rel: 'noopener noreferrer nofollow',
62
60
  },
@@ -69,31 +67,37 @@ export function renderEmbed(block: EmbedBlock, ctx: RenderContext): HtmlElement
69
67
  const source = block.consentRequired ? null : frameSource(block.provider, block.url)
70
68
  const ratio = aspectRatio(block.ratio) ?? '16 / 9'
71
69
 
72
- return h(
70
+ return section(
73
71
  'div',
72
+ 'embed',
73
+ 'cg-embed',
74
74
  {
75
- class: 'cg-block cg-sidenote',
76
- 'data-block': 'embed',
77
75
  'data-provider': block.provider,
78
76
  'data-consent': block.consentRequired ? 'required' : 'not-required',
79
77
  style: `--cg-ratio:${ratio}`,
80
78
  },
81
- source === null
82
- ? noteCard(
83
- block,
84
- ctx,
85
- block.consentRequired
86
- ? ctx.t('embed.consentRequired', { provider: block.provider })
87
- : ctx.t('embed.unsupported', { provider: block.provider }),
88
- )
89
- : h('iframe', {
90
- class: 'cg-sidenote__frame',
91
- src: source,
92
- title: ctx.t('embed.title', { provider: block.provider }),
93
- loading: 'lazy',
94
- referrerpolicy: 'strict-origin-when-cross-origin',
95
- allow: 'accelerometer; clipboard-write; encrypted-media; picture-in-picture; fullscreen',
96
- allowfullscreen: true,
97
- }),
79
+ 'div',
80
+ h(
81
+ 'div',
82
+ { class: 'cg-embed__frame' },
83
+ source === null
84
+ ? notice(
85
+ block,
86
+ ctx,
87
+ block.consentRequired
88
+ ? ctx.t('embed.consentRequired', { provider: block.provider })
89
+ : ctx.t('embed.unsupported', { provider: block.provider }),
90
+ )
91
+ : h('iframe', {
92
+ class: 'cg-embed__player',
93
+ src: source,
94
+ title: ctx.t('embed.title', { provider: block.provider }),
95
+ loading: 'lazy',
96
+ referrerpolicy: 'strict-origin-when-cross-origin',
97
+ allow:
98
+ 'accelerometer; clipboard-write; encrypted-media; picture-in-picture; fullscreen',
99
+ allowfullscreen: true,
100
+ }),
101
+ ),
98
102
  )
99
103
  }
@@ -7,52 +7,47 @@ import {
7
7
  type RenderContext,
8
8
  renderRichText,
9
9
  } from '@cogenta/theme-kit'
10
+ import { section } from '../layout.js'
10
11
 
11
12
  /**
12
- * A reader-mailbag column: numbered questions set in the display serif, the
13
- * answer running underneath once opened. `<details>`/`<summary>` still does
14
- * all the work — expand/collapse, keyboard operation and the announced
15
- * expanded state all come from the browser at zero bytes of JavaScript,
16
- * exactly as in the reference theme; only the typography and the numbering
17
- * are this theme's own.
13
+ * Questions and their answers, set open, the way a newspaper prints a
14
+ * reader's guide: the title in the left three columns, and down the right
15
+ * the questions in the display face with each answer under it, divided by
16
+ * hairlines. Nothing is hidden behind a disclosure: a short guide is read
17
+ * whole. (`accordion` is the collapsible form of the same content.)
18
18
  */
19
- function renderItem(item: FaqItem, ctx: RenderContext, index: number): HtmlElement {
19
+ function renderItem(item: FaqItem, ctx: RenderContext, tag: 'h3' | 'h2'): HtmlElement {
20
20
  return h(
21
- 'li',
22
- { class: 'cg-mailbag__item' },
23
- h(
24
- 'details',
25
- { class: 'cg-mailbag__details' },
26
- h(
27
- 'summary',
28
- { class: 'cg-mailbag__question' },
29
- h(
30
- 'span',
31
- { class: 'cg-mailbag__number', 'aria-hidden': 'true' },
32
- String(index + 1).padStart(2, '0'),
33
- ),
34
- h('span', { class: 'cg-mailbag__question-text' }, item.question),
35
- ),
36
- h('div', { class: 'cg-mailbag__answer' }, renderRichText(ctx, item.answer)),
37
- ),
21
+ 'div',
22
+ { class: 'cg-guide__item' },
23
+ heading(tag, { class: 'cg-guide__question' }, item.question),
24
+ h('div', { class: 'cg-guide__answer' }, renderRichText(ctx, item.answer)),
38
25
  )
39
26
  }
40
27
 
41
28
  export function renderFaq(block: FaqBlock, ctx: RenderContext): HtmlElement {
42
- return h(
29
+ const titled = block.title !== undefined
30
+ return section(
43
31
  'section',
44
- { class: 'cg-block cg-mailbag', 'data-block': 'faq' },
45
- block.title === undefined
46
- ? null
47
- : heading(
48
- blockHeadingTag('faq') ?? 'h2',
49
- { class: 'cg-mailbag__title', 'data-field': 'title' },
50
- block.title,
51
- ),
32
+ 'faq',
33
+ 'cg-guide',
34
+ { 'data-titled': titled ? 'true' : 'false' },
35
+ 'div',
36
+ titled
37
+ ? h(
38
+ 'div',
39
+ { class: 'cg-guide__head' },
40
+ heading(
41
+ blockHeadingTag('faq') ?? 'h2',
42
+ { class: 'cg-guide__title', 'data-field': 'title' },
43
+ block.title ?? '',
44
+ ),
45
+ )
46
+ : null,
52
47
  h(
53
- 'ul',
54
- { class: 'cg-mailbag__items' },
55
- block.items.map((item, index) => renderItem(item, ctx, index)),
48
+ 'div',
49
+ { class: 'cg-guide__items' },
50
+ block.items.map((item) => renderItem(item, ctx, titled ? 'h3' : 'h2')),
56
51
  ),
57
52
  )
58
53
  }
@@ -1,6 +1,5 @@
1
1
  import type { FeatureGridBlock, FeatureItem } from '@cogenta/blocks'
2
2
  import {
3
- blockHeadingTag,
4
3
  type HeadingTag,
5
4
  type HtmlElement,
6
5
  h,
@@ -9,53 +8,44 @@ import {
9
8
  nestedHeadingTag,
10
9
  type RenderContext,
11
10
  } from '@cogenta/theme-kit'
11
+ import { section, sectionHead } from '../layout.js'
12
12
 
13
13
  /**
14
- * A contents index, not a card grid: a bordered panel, a real newspaper
15
- * `column-rule` between the columns it falls into at width, and each entry
16
- * numbered by a CSS counter rather than an icon chip — this vocabulary's
17
- * `icon` names a symbol no icon set this theme ships can resolve into
18
- * markup, so a numeral the browser generates for free reads as considered
19
- * rather than as a placeholder.
20
- *
21
- * The item's own title carries the link, exactly as in the reference theme:
22
- * a bare "read more" repeated across a grid fails WCAG 2.4.4, and the block
23
- * has no separate label field to fix that with.
14
+ * A set of short items, set as a contents panel: up to four columns divided
15
+ * by vertical hairlines, each item a title in the display face and a line of
16
+ * text. An item with a link makes its title the link, with an arrow set
17
+ * inline after the last word so the underline runs under the words and never
18
+ * under a lone arrow. No icon tiles: `icon` names are ignored on purpose, a
19
+ * newspaper sets a contents panel in type.
24
20
  */
25
21
  function renderItem(item: FeatureItem, ctx: RenderContext, tag: HeadingTag): HtmlElement {
26
- const title =
27
- item.link === undefined
28
- ? heading(tag, { class: 'cg-index__title' }, item.title)
29
- : heading(
30
- tag,
31
- { class: 'cg-index__title' },
32
- h('a', { class: 'cg-index__link', href: href(ctx, item.link) }, item.title),
33
- )
34
22
  return h(
35
23
  'li',
36
- { class: 'cg-index__item', 'data-icon': item.icon },
37
- title,
38
- item.text === undefined ? null : h('p', { class: 'cg-index__text' }, item.text),
24
+ { class: 'cg-contents__item' },
25
+ heading(
26
+ tag,
27
+ { class: 'cg-contents__title' },
28
+ item.link === undefined
29
+ ? item.title
30
+ : h('a', { class: 'cg-arrow-link', href: href(ctx, item.link) }, item.title),
31
+ ),
32
+ item.text === undefined ? null : h('p', { class: 'cg-contents__text' }, item.text),
39
33
  )
40
34
  }
41
35
 
42
36
  export function renderFeatureGrid(block: FeatureGridBlock, ctx: RenderContext): HtmlElement {
43
- const hasTitle = block.title !== undefined
44
- const itemTag = nestedHeadingTag('featureGrid', hasTitle)
45
- return h(
37
+ const tag = nestedHeadingTag('featureGrid', block.title !== undefined)
38
+ return section(
46
39
  'section',
47
- { class: 'cg-block cg-index', 'data-block': 'featureGrid' },
48
- hasTitle
49
- ? heading(
50
- blockHeadingTag('featureGrid') ?? 'h2',
51
- { class: 'cg-index__heading', 'data-field': 'title' },
52
- block.title ?? '',
53
- )
54
- : null,
40
+ 'featureGrid',
41
+ 'cg-contents',
42
+ { 'data-count': String(Math.min(block.items.length, 4)) },
43
+ 'div',
44
+ sectionHead('featureGrid', block.title),
55
45
  h(
56
- 'ol',
57
- { class: 'cg-index__items' },
58
- block.items.map((item) => renderItem(item, ctx, itemTag)),
46
+ 'ul',
47
+ { class: 'cg-contents__items' },
48
+ block.items.map((item) => renderItem(item, ctx, tag)),
59
49
  ),
60
50
  )
61
51
  }
@@ -1,40 +1,46 @@
1
1
  import type { GalleryBlock } from '@cogenta/blocks'
2
2
  import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+ import { section } from '../layout.js'
3
4
 
4
5
  /**
5
- * A contact sheet: numbered frames (a CSS counter, drawn in the negative
6
- * space of the print corner) in a tight grid with hairline gutters, rather
7
- * than the reference theme's rounded, shadowed tiles.
6
+ * A picture page, square-cornered, with a constant gutter:
8
7
  *
9
- * The carousel layout ships **no JavaScript** — a scroll-snapping, focusable,
10
- * labelled region gives keyboard, touch and trackpad scrolling for free
11
- * (WCAG 2.4.3/4.1.2), with none of a scripted carousel's failure modes.
8
+ * - `grid`: the first photograph across eight columns, the next two stacked
9
+ * beside it, then rows of three at 3:2, the rhythm of a picture spread.
10
+ * - `masonry`: three columns at each picture's own ratio.
11
+ * - `carousel`: one row that scrolls and snaps, focusable and labelled; touch,
12
+ * trackpad and arrow keys are all native. No script.
12
13
  */
13
14
  export function renderGallery(block: GalleryBlock, ctx: RenderContext): HtmlElement {
14
15
  const items = h(
15
16
  'ul',
16
- { class: 'cg-contactsheet__items' },
17
- block.items.map((item) =>
17
+ { class: 'cg-pictures__items', 'data-count': String(block.items.length) },
18
+ block.items.map((item, index) =>
18
19
  h(
19
20
  'li',
20
- { class: 'cg-contactsheet__item' },
21
- image(ctx, item.media, { sizes: '(min-width: 45rem) 18rem, 50vw' }),
21
+ { class: 'cg-pictures__item' },
22
+ image(ctx, item.media, {
23
+ className: 'cg-pictures__image',
24
+ sizes:
25
+ block.layout === 'grid' && index === 0
26
+ ? '(min-width: 64rem) 52rem, 100vw'
27
+ : '(min-width: 64rem) 26rem, (min-width: 40rem) 45vw, 90vw',
28
+ }),
22
29
  ),
23
30
  ),
24
31
  )
25
32
 
26
- return h(
27
- 'section',
28
- {
29
- class: 'cg-block cg-contactsheet',
30
- 'data-block': 'gallery',
31
- 'data-layout': block.layout,
32
- },
33
+ return section(
34
+ 'div',
35
+ 'gallery',
36
+ 'cg-pictures',
37
+ { 'data-layout': block.layout },
38
+ 'div',
33
39
  block.layout === 'carousel'
34
40
  ? h(
35
41
  'div',
36
42
  {
37
- class: 'cg-contactsheet__viewport',
43
+ class: 'cg-pictures__viewport',
38
44
  role: 'region',
39
45
  'aria-label': ctx.t('gallery.carousel'),
40
46
  tabindex: '0',
@@ -8,47 +8,47 @@ import {
8
8
  image,
9
9
  type RenderContext,
10
10
  } from '@cogenta/theme-kit'
11
+ import { section } from '../layout.js'
11
12
 
12
13
  /**
13
- * The cover story. `hero` carries the page's own `h1` (contract B's
14
- * `headingLevel: 'h1'`), so this is the one place the masthead's front page
15
- * lives: an eyebrow set as a kicker, a big serif headline, a dek, and the
16
- * lead image on the other side of a hairline column rule — the two-column
17
- * "story well" a print front page opens on, rather than a centred banner.
14
+ * A cover story. `hero` carries the page's own `h1` (contract B), so it is
15
+ * set as the opening of a special issue: the eyebrow as a red kicker, the
16
+ * headline in the display face at its largest, the subtitle as an italic
17
+ * standfirst, and the photograph beside the words across seven columns,
18
+ * its bottom edge on the same line as the last action. Without a photograph
19
+ * the headline takes ten columns over a double rule.
18
20
  */
19
21
  export function renderHero(block: HeroBlock, ctx: RenderContext): HtmlElement {
20
22
  const tag = blockHeadingTag('hero') ?? 'h1'
21
- return h(
23
+ return section(
22
24
  'section',
23
- { class: 'cg-block cg-hero', 'data-block': 'hero' },
25
+ 'hero',
26
+ 'cg-cover',
27
+ { 'data-media': block.media === undefined ? 'none' : 'image' },
28
+ 'div',
24
29
  h(
25
30
  'div',
26
- { class: 'cg-hero__grid' },
27
- h(
28
- 'div',
29
- { class: 'cg-hero__copy' },
30
- block.eyebrow === undefined
31
- ? null
32
- : h('p', { class: 'cg-hero__eyebrow', 'data-field': 'eyebrow' }, block.eyebrow),
33
- heading(tag, { class: 'cg-hero__title', 'data-field': 'title' }, block.title),
34
- block.subtitle === undefined
35
- ? null
36
- : h('p', { class: 'cg-hero__subtitle', 'data-field': 'subtitle' }, block.subtitle),
37
- actionList(ctx, block.actions, ctx.t('hero.actions')),
38
- ),
39
- block.media === undefined
31
+ { class: 'cg-cover__copy' },
32
+ block.eyebrow === undefined
40
33
  ? null
41
- : h(
42
- 'div',
43
- { class: 'cg-hero__media' },
44
- // The only image above the fold, so the only one loaded eagerly —
45
- // lazy-loading it is a measured LCP regression, not a theory.
46
- image(ctx, block.media, {
47
- className: 'cg-hero__image',
48
- loading: 'eager',
49
- sizes: '(min-width: 64rem) 56vw, 100vw',
50
- }),
51
- ),
34
+ : h('p', { class: 'cg-cover__kicker', 'data-field': 'eyebrow' }, block.eyebrow),
35
+ heading(tag, { class: 'cg-cover__title', 'data-field': 'title' }, block.title),
36
+ block.subtitle === undefined
37
+ ? null
38
+ : h('p', { class: 'cg-cover__standfirst', 'data-field': 'subtitle' }, block.subtitle),
39
+ actionList(ctx, block.actions, ctx.t('hero.actions')),
52
40
  ),
41
+ block.media === undefined
42
+ ? null
43
+ : h(
44
+ 'div',
45
+ { class: 'cg-cover__media' },
46
+ // The one image above the fold, so the one loaded eagerly.
47
+ image(ctx, block.media, {
48
+ className: 'cg-cover__image',
49
+ loading: 'eager',
50
+ sizes: '(min-width: 64rem) 46rem, 100vw',
51
+ }),
52
+ ),
53
53
  )
54
54
  }
@@ -1,35 +1,35 @@
1
1
  import type { LogoStripBlock, LogoStripItem } from '@cogenta/blocks'
2
2
  import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+ import { section } from '../layout.js'
3
4
 
4
5
  /**
5
- * `blocks@2.0` (RFC 0001). The lighter-weight "as used by" strip next to
6
- * `logos`'s own press row (`cg-press`, which titles itself and links each
7
- * mark to its organisation): `logoStrip` has no per-logo name or url, so it
8
- * carries no title and no links — a plain band of marks with an optional
9
- * caption set underneath, the way a colophon credits its supporters along
10
- * the bottom of a masthead page. Each image's own alt text (set once, in
11
- * the media library) is what names it, exactly as `logos` already falls
12
- * back to when a link is absent.
6
+ * A line of credits: the caption in small capitals on the left three
7
+ * columns, the marks in one tone along the rest of the row, between two
8
+ * hairlines. `logoStrip` has no per-mark name or link, so each image's own
9
+ * alt text (set once, in the media library) is what names it.
13
10
  */
14
11
  function renderItem(item: LogoStripItem, ctx: RenderContext): HtmlElement {
15
12
  return h(
16
13
  'li',
17
- { class: 'cg-imprint__item' },
18
- image(ctx, item.media, { className: 'cg-imprint__image', variant: { fit: 'contain' } }),
14
+ { class: 'cg-credits__item' },
15
+ image(ctx, item.media, { className: 'cg-credits__image', variant: { fit: 'contain' } }),
19
16
  )
20
17
  }
21
18
 
22
19
  export function renderLogoStrip(block: LogoStripBlock, ctx: RenderContext): HtmlElement {
23
- return h(
24
- 'figure',
25
- { class: 'cg-block cg-imprint', 'data-block': 'logoStrip' },
20
+ return section(
21
+ 'div',
22
+ 'logoStrip',
23
+ 'cg-credits',
24
+ {},
25
+ 'div',
26
+ block.caption === undefined
27
+ ? null
28
+ : h('p', { class: 'cg-credits__caption', 'data-field': 'caption' }, block.caption),
26
29
  h(
27
30
  'ul',
28
- { class: 'cg-imprint__items' },
31
+ { class: 'cg-credits__items' },
29
32
  block.logos.map((item) => renderItem(item, ctx)),
30
33
  ),
31
- block.caption === undefined
32
- ? null
33
- : h('figcaption', { class: 'cg-imprint__caption', 'data-field': 'caption' }, block.caption),
34
34
  )
35
35
  }
@@ -1,56 +1,48 @@
1
1
  import type { LogoItem, LogosBlock } from '@cogenta/blocks'
2
- import {
3
- blockHeadingTag,
4
- type HtmlElement,
5
- h,
6
- heading,
7
- image,
8
- type RenderContext,
9
- } from '@cogenta/theme-kit'
2
+ import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+ import { section, sectionHead } from '../layout.js'
10
4
 
11
5
  /**
12
- * A press strip — "As seen in" — a single row divided by hairline rules
13
- * rather than a card grid, the way a masthead credits its syndication
14
- * partners along one line.
6
+ * Organisations credited by name, set as a ruled grid of marks in ink: up to
7
+ * six to a row on a wide screen (`data-count`, never fewer than three columns,
8
+ * so two marks do not stretch across the page), a hairline between each, the
9
+ * marks reduced to
10
+ * one tone so no brand colour competes with the page's single red.
15
11
  *
16
- * The organisation's name is contract B's own accessible name for the link:
17
- * the logo image carries it as `alt` when the media entity has none, so no
18
- * visually hidden duplicate text is needed — a link whose only content is an
19
- * image with `alt` text is already named.
12
+ * The organisation's name is contract B's own accessible name: the image
13
+ * carries it as `alt` when the media entity has none, so a linked mark is
14
+ * named without a hidden duplicate.
20
15
  */
21
16
  function renderItem(item: LogoItem, ctx: RenderContext): HtmlElement {
22
- const logo = image(ctx, item.media, {
23
- className: 'cg-press__image',
17
+ const mark = image(ctx, item.media, {
18
+ className: 'cg-marks__image',
24
19
  altFrom: item.name,
25
20
  variant: { fit: 'contain' },
26
21
  })
27
22
  return h(
28
23
  'li',
29
- { class: 'cg-press__item' },
24
+ { class: 'cg-marks__item' },
30
25
  item.url === undefined
31
- ? logo
26
+ ? mark
32
27
  : h(
33
28
  'a',
34
- { class: 'cg-press__link', href: ctx.link(item.url), rel: 'noopener noreferrer' },
35
- logo,
29
+ { class: 'cg-marks__link', href: ctx.link(item.url), rel: 'noopener noreferrer' },
30
+ mark,
36
31
  ),
37
32
  )
38
33
  }
39
34
 
40
35
  export function renderLogos(block: LogosBlock, ctx: RenderContext): HtmlElement {
41
- return h(
36
+ return section(
42
37
  'section',
43
- { class: 'cg-block cg-press', 'data-block': 'logos' },
44
- block.title === undefined
45
- ? null
46
- : heading(
47
- blockHeadingTag('logos') ?? 'h2',
48
- { class: 'cg-press__title', 'data-field': 'title' },
49
- block.title,
50
- ),
38
+ 'logos',
39
+ 'cg-marks',
40
+ { 'data-count': String(Math.min(Math.max(block.items.length, 3), 6)) },
41
+ 'div',
42
+ sectionHead('logos', block.title),
51
43
  h(
52
44
  'ul',
53
- { class: 'cg-press__items' },
45
+ { class: 'cg-marks__items' },
54
46
  block.items.map((item) => renderItem(item, ctx)),
55
47
  ),
56
48
  )