@cogenta/theme-magazine 1.1.3 → 1.3.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.
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,50 +1,43 @@
1
1
  import type { AccordionBlock, AccordionItem } from '@cogenta/blocks'
2
- import {
3
- blockHeadingTag,
4
- type HtmlElement,
5
- h,
6
- heading,
7
- type RenderContext,
8
- renderRichText,
9
- } from '@cogenta/theme-kit'
2
+ import { type HtmlElement, h, type RenderContext, renderRichText } from '@cogenta/theme-kit'
3
+ import { section, sectionHead } from '../layout.js'
10
4
 
11
5
  /**
12
- * `blocks@2.0` (RFC 0001). A reference dossier, not the reader mailbag:
13
- * shares `faq`'s `<details>`/`<summary>` mechanics — expanding, keyboard
14
- * operation and the announced expanded state all come from the browser, at
15
- * zero bytes of JavaScript — but its own markup and class names (`cg-dossier`
16
- * rather than `cg-mailbag`), a tab-style entry marker in the margin instead
17
- * of a numeral, so the two blocks can diverge visually later without one
18
- * depending on the other. "Accordion" and "frequently asked question" are
19
- * different editorial intents even where the question/answer shape coincides.
6
+ * A set of collapsible notes on a ruled list: each question on its own row
7
+ * in the interface face, a plus drawn from two hairlines that turns into a
8
+ * minus when the row is open, and the answer in the reading face. Opening,
9
+ * keyboard behaviour and the announced state are all native `<details>`. No
10
+ * script.
20
11
  */
21
12
  function renderItem(item: AccordionItem, ctx: RenderContext): HtmlElement {
22
13
  return h(
23
14
  'li',
24
- { class: 'cg-dossier__entry' },
15
+ { class: 'cg-notes__item' },
25
16
  h(
26
17
  'details',
27
- { class: 'cg-dossier__details' },
28
- h('summary', { class: 'cg-dossier__term' }, item.question),
29
- h('div', { class: 'cg-dossier__definition' }, renderRichText(ctx, item.answer)),
18
+ { class: 'cg-notes__details' },
19
+ h(
20
+ 'summary',
21
+ { class: 'cg-notes__question' },
22
+ h('span', { class: 'cg-notes__question-text' }, item.question),
23
+ h('span', { class: 'cg-notes__mark', 'aria-hidden': 'true' }),
24
+ ),
25
+ h('div', { class: 'cg-notes__answer' }, renderRichText(ctx, item.answer)),
30
26
  ),
31
27
  )
32
28
  }
33
29
 
34
30
  export function renderAccordion(block: AccordionBlock, ctx: RenderContext): HtmlElement {
35
- return h(
31
+ return section(
36
32
  'section',
37
- { class: 'cg-block cg-dossier', 'data-block': 'accordion' },
38
- block.title === undefined
39
- ? null
40
- : heading(
41
- blockHeadingTag('accordion') ?? 'h2',
42
- { class: 'cg-dossier__title', 'data-field': 'title' },
43
- block.title,
44
- ),
33
+ 'accordion',
34
+ 'cg-notes',
35
+ {},
36
+ 'div',
37
+ sectionHead('accordion', block.title),
45
38
  h(
46
39
  'ul',
47
- { class: 'cg-dossier__entries' },
40
+ { class: 'cg-notes__items' },
48
41
  block.items.map((item) => renderItem(item, ctx)),
49
42
  ),
50
43
  )
@@ -1,239 +1,178 @@
1
1
  import type { CollectionListBlock } from '@cogenta/blocks'
2
2
  import {
3
- blockHeadingTag,
4
3
  buildCollectionListQuery,
5
4
  type ContentEntry,
6
- entryDate,
7
- entryExcerpt,
8
- entryHref,
9
- entryImage,
10
- entryTitle,
11
5
  type HeadingTag,
12
6
  type HtmlElement,
13
7
  h,
14
- heading,
15
8
  nestedHeadingTag,
16
9
  type RenderContext,
17
- renderImageSource,
18
10
  } from '@cogenta/theme-kit'
11
+ import { section, sectionHead } from '../layout.js'
12
+ import { renderStory, type Story, storyFromEntry } from '../story.js'
19
13
 
20
14
  export { buildCollectionListQuery as query }
21
15
 
22
16
  /**
23
- * A rubric label, read from whichever of the usual "section-like" field
24
- * names a collection actually declares — the same "never invented, only the
25
- * usual convention" rule `entryImage`/`entryExcerpt` (`@cogenta/theme-kit`)
26
- * already follow, extended locally: contract D's `PageEntryMeta` only
27
- * resolves *taxonomy* classifications into an eyebrow (`renderEntryHeader`),
28
- * never an arbitrary `select` field, so a magazine whose "section" is a
29
- * plain field (the `magazine` blueprint's own choice — see `blueprints/
30
- * magazine.ts`, kept deliberately non-taxonomic so this exact value is
31
- * readable raw here) can still show it on a card, where the full
32
- * `ContentEntry` — unlike a rendered page's `PageEntryMeta` — is available.
17
+ * A listing, set in one of four newspaper forms chosen from what the editor
18
+ * already decided in contract B (the layout, and whether the list has a
19
+ * title), never from a setting this theme invents:
20
+ *
21
+ * - `grid` without a title is **the front**: the first story across eight
22
+ * columns with its photograph, a column of three briefs beside it behind a
23
+ * vertical rule, and every further story in a row of four below, divided
24
+ * by hairlines. This is the opening of a front page.
25
+ * - `grid` with a title is **a rail**: the section label on a heavy rule,
26
+ * its newest story large with a photograph, and the rest as a column of
27
+ * headlines beside it.
28
+ * - `carousel` is **a strip**: stories side by side between column rules,
29
+ * scrolling on a narrow screen. It is how the opinion columns are set,
30
+ * with the columnist's name in the kicker.
31
+ * - `list` is **a ranked list**: numbered, the numerals in the display face,
32
+ * side by side on a wide screen.
33
+ *
34
+ * An empty list keeps its head and says so in one line.
33
35
  */
34
- const SECTION_FIELDS = ['section', 'category', 'topic', 'department'] as const
35
36
 
36
- function entrySection(entry: ContentEntry): string | undefined {
37
- for (const field of SECTION_FIELDS) {
38
- const value = entry[field]
39
- if (typeof value === 'string' && value.trim() !== '') return value
40
- }
41
- return undefined
42
- }
43
-
44
- function formatDate(iso: string, ctx: RenderContext, style: 'long' | 'medium'): string {
45
- try {
46
- return new Intl.DateTimeFormat(ctx.locale, { dateStyle: style }).format(new Date(iso))
47
- } catch {
48
- return iso
49
- }
50
- }
37
+ const FRONT_BRIEFS = 3
51
38
 
52
- /**
53
- * The "une": a large 16:9 cover, the section eyebrow (falling back to the
54
- * generic "Featured" kicker when the entry carries none), a big headline and
55
- * the full excerpt — the front-page treatment `layout: 'grid'` gives its
56
- * first entry, per the L25 pro-pass brief.
57
- */
58
- function renderLead(entry: ContentEntry, ctx: RenderContext, tag: HeadingTag): HtmlElement {
59
- const date = entryDate(entry)
60
- const excerpt = entryExcerpt(entry)
61
- const section = entrySection(entry)
62
- const cover = entryImage(entry, ctx, { width: 1200, height: 675, fit: 'cover' })
39
+ function frontStories(stories: readonly Story[], tag: HeadingTag): HtmlElement {
40
+ const [lead, ...others] = stories
41
+ const briefs = others.slice(0, FRONT_BRIEFS)
42
+ const more = others.slice(FRONT_BRIEFS)
63
43
  return h(
64
- 'article',
65
- { class: 'cg-issue__lead' },
66
- cover === undefined
44
+ 'div',
45
+ { class: 'cg-front', 'data-briefs': String(briefs.length), 'data-more': String(more.length) },
46
+ lead === undefined
67
47
  ? null
68
48
  : h(
69
- 'a',
70
- { class: 'cg-issue__lead-cover-link', href: entryHref(entry, ctx), tabindex: -1 },
71
- renderImageSource(cover, {
72
- className: 'cg-issue__lead-cover',
49
+ 'div',
50
+ { class: 'cg-front__lead' },
51
+ renderStory(lead, {
52
+ tag,
53
+ variant: 'lead',
54
+ image: true,
55
+ standfirst: true,
56
+ sizes: '(min-width: 64rem) 52rem, 100vw',
73
57
  loading: 'eager',
74
- sizes: '(min-width: 64rem) 76rem, 100vw',
75
58
  }),
76
59
  ),
77
- h('div', { class: 'cg-issue__lead-body' }, [
78
- h('p', { class: 'cg-issue__kicker' }, section ?? ctx.t('collection.featured')),
79
- heading(
80
- tag,
81
- { class: 'cg-issue__lead-title' },
82
- h('a', { class: 'cg-issue__link', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
83
- ),
84
- excerpt === undefined ? null : h('p', { class: 'cg-issue__lead-excerpt' }, excerpt),
85
- date === undefined
86
- ? null
87
- : h(
88
- 'time',
89
- { class: 'cg-issue__lead-date', datetime: date },
90
- formatDate(date, ctx, 'long'),
91
- ),
92
- ]),
93
- )
94
- }
95
-
96
- /** A card in the "rest" 3-column grid: cover, section eyebrow, title, date — no excerpt (the lead alone carries one). */
97
- function renderCard(entry: ContentEntry, ctx: RenderContext, tag: HeadingTag): HtmlElement {
98
- const date = entryDate(entry)
99
- const section = entrySection(entry)
100
- const cover = entryImage(entry, ctx, { width: 480, height: 320, fit: 'cover' })
101
- return h(
102
- 'li',
103
- { class: 'cg-issue__card-item' },
104
- h(
105
- 'article',
106
- { class: 'cg-issue__card' },
107
- cover === undefined
108
- ? null
109
- : h(
110
- 'a',
111
- { class: 'cg-issue__card-cover-link', href: entryHref(entry, ctx), tabindex: -1 },
112
- renderImageSource(cover, {
113
- className: 'cg-issue__card-cover',
114
- sizes: '(min-width: 64rem) 24rem, (min-width: 48rem) 45vw, 100vw',
115
- }),
116
- ),
117
- section === undefined ? null : h('p', { class: 'cg-issue__card-eyebrow' }, section),
118
- heading(
119
- tag,
120
- { class: 'cg-issue__card-title' },
121
- h('a', { class: 'cg-issue__link', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
122
- ),
123
- date === undefined
124
- ? null
125
- : h(
126
- 'time',
127
- { class: 'cg-issue__card-date', datetime: date },
128
- formatDate(date, ctx, 'medium'),
129
- ),
130
- ),
131
- )
132
- }
133
-
134
- /**
135
- * A rail row for `layout: 'list'` — a small square thumbnail (when the entry
136
- * has one) beside the title and date, the "rubric rail" treatment; a plain
137
- * numbered row (this theme's original front-page-index look) when the entry
138
- * carries no image, so a collection with no cover field still reads as a
139
- * considered contents strip rather than a broken thumbnail.
140
- */
141
- function renderRow(
142
- entry: ContentEntry,
143
- ctx: RenderContext,
144
- tag: HeadingTag,
145
- index: number,
146
- ): HtmlElement {
147
- const date = entryDate(entry)
148
- const cover = entryImage(entry, ctx, { width: 112, height: 112, fit: 'cover' })
149
- return h(
150
- 'li',
151
- { class: 'cg-issue__row' },
152
- cover === undefined
153
- ? h(
154
- 'span',
155
- { class: 'cg-issue__row-number', 'aria-hidden': 'true' },
156
- String(index).padStart(2, '0'),
157
- )
60
+ briefs.length === 0
61
+ ? null
158
62
  : h(
159
- 'a',
160
- { class: 'cg-issue__row-thumb-link', href: entryHref(entry, ctx), tabindex: -1 },
161
- renderImageSource(cover, { className: 'cg-issue__row-thumb' }),
63
+ 'ul',
64
+ { class: 'cg-front__briefs' },
65
+ briefs.map((story) =>
66
+ h(
67
+ 'li',
68
+ { class: 'cg-front__brief' },
69
+ renderStory(story, { tag, variant: 'brief', standfirst: true }),
70
+ ),
71
+ ),
162
72
  ),
163
- h(
164
- 'div',
165
- { class: 'cg-issue__row-body' },
166
- heading(
167
- tag,
168
- { class: 'cg-issue__row-title' },
169
- h('a', { class: 'cg-issue__link', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
170
- ),
171
- date === undefined
172
- ? null
173
- : h(
174
- 'time',
175
- { class: 'cg-issue__row-date', datetime: date },
176
- formatDate(date, ctx, 'medium'),
73
+ more.length === 0
74
+ ? null
75
+ : h(
76
+ 'ul',
77
+ { class: 'cg-front__more' },
78
+ more.map((story) =>
79
+ h(
80
+ 'li',
81
+ { class: 'cg-front__item' },
82
+ renderStory(story, {
83
+ tag,
84
+ variant: 'secondary',
85
+ image: true,
86
+ sizes: '(min-width: 64rem) 18rem, (min-width: 40rem) 45vw, 100vw',
87
+ }),
88
+ ),
177
89
  ),
178
- ),
90
+ ),
179
91
  )
180
92
  }
181
93
 
182
- /** A uniform frame for the horizontal-scroll `carousel` layout — a cover, the section eyebrow, title, date, excerpt. */
183
- function renderFrame(entry: ContentEntry, ctx: RenderContext, tag: HeadingTag): HtmlElement {
184
- const date = entryDate(entry)
185
- const excerpt = entryExcerpt(entry)
186
- const section = entrySection(entry)
187
- const cover = entryImage(entry, ctx, { width: 480, height: 320, fit: 'cover' })
94
+ function railStories(stories: readonly Story[], tag: HeadingTag): HtmlElement {
95
+ const [lead, ...others] = stories
188
96
  return h(
189
- 'li',
190
- { class: 'cg-issue__frame' },
191
- cover === undefined
97
+ 'div',
98
+ { class: 'cg-rail', 'data-count': String(stories.length) },
99
+ lead === undefined
192
100
  ? null
193
101
  : h(
194
- 'a',
195
- { class: 'cg-issue__frame-cover-link', href: entryHref(entry, ctx), tabindex: -1 },
196
- renderImageSource(cover, { className: 'cg-issue__frame-cover' }),
102
+ 'div',
103
+ { class: 'cg-rail__lead' },
104
+ renderStory(lead, {
105
+ tag,
106
+ variant: 'rail',
107
+ image: true,
108
+ standfirst: true,
109
+ date: true,
110
+ sizes: '(min-width: 64rem) 34rem, 100vw',
111
+ }),
197
112
  ),
198
- section === undefined ? null : h('p', { class: 'cg-issue__card-eyebrow' }, section),
199
- heading(
200
- tag,
201
- { class: 'cg-issue__frame-title' },
202
- h('a', { class: 'cg-issue__link', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
203
- ),
204
- date === undefined
113
+ others.length === 0
205
114
  ? null
206
115
  : h(
207
- 'time',
208
- { class: 'cg-issue__frame-date', datetime: date },
209
- formatDate(date, ctx, 'medium'),
116
+ 'ul',
117
+ { class: 'cg-rail__list' },
118
+ others.map((story) =>
119
+ h(
120
+ 'li',
121
+ { class: 'cg-rail__item' },
122
+ renderStory(story, {
123
+ tag,
124
+ variant: 'column',
125
+ image: true,
126
+ standfirst: true,
127
+ date: true,
128
+ sizes: '(min-width: 64rem) 9rem, 30vw',
129
+ }),
130
+ ),
131
+ ),
210
132
  ),
211
- excerpt === undefined ? null : h('p', { class: 'cg-issue__frame-excerpt' }, excerpt),
212
133
  )
213
134
  }
214
135
 
215
- function renderGrid(entries: readonly ContentEntry[], ctx: RenderContext, tag: HeadingTag) {
216
- const [first, ...rest] = entries
217
- if (first === undefined) return null
136
+ function stripStories(
137
+ stories: readonly Story[],
138
+ tag: HeadingTag,
139
+ label: string,
140
+ count: number,
141
+ ): HtmlElement {
218
142
  return h(
219
143
  'div',
220
- { class: 'cg-issue__spread' },
221
- renderLead(first, ctx, tag),
222
- rest.length === 0
223
- ? null
224
- : h(
225
- 'ul',
226
- { class: 'cg-issue__cards' },
227
- rest.map((entry) => renderCard(entry, ctx, tag)),
144
+ {
145
+ class: 'cg-strip',
146
+ role: 'region',
147
+ 'aria-label': label,
148
+ tabindex: '0',
149
+ 'data-count': String(Math.min(count, 4)),
150
+ },
151
+ h(
152
+ 'ul',
153
+ { class: 'cg-strip__items' },
154
+ stories.map((story) =>
155
+ h(
156
+ 'li',
157
+ { class: 'cg-strip__item' },
158
+ renderStory(story, { tag, variant: 'strip', standfirst: true }),
228
159
  ),
160
+ ),
161
+ ),
229
162
  )
230
163
  }
231
164
 
232
- function renderList(entries: readonly ContentEntry[], ctx: RenderContext, tag: HeadingTag) {
165
+ function rankedStories(stories: readonly Story[], tag: HeadingTag): HtmlElement {
233
166
  return h(
234
- 'ul',
235
- { class: 'cg-issue__rows' },
236
- entries.map((entry, index) => renderRow(entry, ctx, tag, index + 1)),
167
+ 'ol',
168
+ { class: 'cg-ranked', 'data-count': String(Math.min(stories.length, 5)) },
169
+ stories.map((story, index) =>
170
+ h(
171
+ 'li',
172
+ { class: 'cg-ranked__item' },
173
+ renderStory(story, { tag, variant: 'ranked', numeral: String(index + 1) }),
174
+ ),
175
+ ),
237
176
  )
238
177
  }
239
178
 
@@ -242,45 +181,42 @@ export function renderCollectionList(
242
181
  ctx: RenderContext,
243
182
  entries: readonly ContentEntry[],
244
183
  ): HtmlElement {
245
- const hasTitle = block.title !== undefined
246
- const entryTag = nestedHeadingTag('collectionList', hasTitle)
184
+ const titled = block.title !== undefined
185
+ const tag = nestedHeadingTag('collectionList', titled)
186
+ const form =
187
+ block.layout === 'grid'
188
+ ? titled
189
+ ? 'rail'
190
+ : 'front'
191
+ : block.layout === 'carousel'
192
+ ? 'strip'
193
+ : 'ranked'
194
+ const imageWidth = form === 'front' ? 1200 : form === 'rail' ? 800 : 480
195
+ const stories = entries.map((entry) => storyFromEntry(entry, ctx, imageWidth))
247
196
 
248
197
  const body =
249
- entries.length === 0
250
- ? h('p', { class: 'cg-issue__empty' }, ctx.t('collection.empty'))
251
- : block.layout === 'grid'
252
- ? renderGrid(entries, ctx, entryTag)
253
- : block.layout === 'carousel'
254
- ? h(
255
- 'div',
256
- {
257
- class: 'cg-issue__viewport',
258
- role: 'region',
259
- 'aria-label': block.title ?? ctx.t('collection.carousel'),
260
- tabindex: '0',
261
- },
262
- h(
263
- 'ul',
264
- { class: 'cg-issue__frames' },
265
- entries.map((entry) => renderFrame(entry, ctx, entryTag)),
266
- ),
267
- )
268
- : renderList(entries, ctx, entryTag)
198
+ stories.length === 0
199
+ ? h('p', { class: 'cg-empty' }, ctx.t('collection.empty'))
200
+ : form === 'front'
201
+ ? frontStories(stories, tag)
202
+ : form === 'rail'
203
+ ? railStories(stories, tag)
204
+ : form === 'strip'
205
+ ? stripStories(
206
+ stories,
207
+ tag,
208
+ block.title ?? ctx.t('collection.carousel'),
209
+ stories.length,
210
+ )
211
+ : rankedStories(stories, tag)
269
212
 
270
- return h(
213
+ return section(
271
214
  'section',
272
- {
273
- class: 'cg-block cg-issue',
274
- 'data-block': 'collectionList',
275
- 'data-layout': block.layout,
276
- },
277
- hasTitle
278
- ? heading(
279
- blockHeadingTag('collectionList') ?? 'h2',
280
- { class: 'cg-issue__title', 'data-field': 'title' },
281
- block.title ?? '',
282
- )
283
- : null,
215
+ 'collectionList',
216
+ 'cg-listing',
217
+ { 'data-layout': block.layout, 'data-form': form },
218
+ 'div',
219
+ sectionHead('collectionList', block.title),
284
220
  body,
285
221
  )
286
222
  }
@@ -7,27 +7,33 @@ import {
7
7
  heading,
8
8
  type RenderContext,
9
9
  } from '@cogenta/theme-kit'
10
+ import { section } from '../layout.js'
10
11
 
11
12
  /**
12
- * The subscription page a print magazine always carries: a panel framed by
13
- * a heavy top-and-bottom rule rather than a rounded, shadowed card — the
14
- * theme's one deliberately "loud" moment, reserved for the block whose whole
15
- * job is to ask for something.
13
+ * An appeal, set like the subscription panel a newspaper runs between two
14
+ * sections: a double rule above, the title large in the display face across
15
+ * the left seven columns, the text and the actions in the right five. No
16
+ * coloured box: the rules and the type carry it.
16
17
  */
17
18
  export function renderCta(block: CtaBlock, ctx: RenderContext): HtmlElement {
18
- return h(
19
+ return section(
19
20
  'section',
20
- { class: 'cg-block cg-subscribe', 'data-block': 'cta' },
21
+ 'cta',
22
+ 'cg-appeal',
23
+ {},
24
+ 'div',
21
25
  heading(
22
26
  blockHeadingTag('cta') ?? 'h2',
23
- { class: 'cg-subscribe__title', 'data-field': 'title' },
27
+ { class: 'cg-appeal__title', 'data-field': 'title' },
24
28
  block.title,
25
29
  ),
26
- block.text === undefined
27
- ? null
28
- : h('p', { class: 'cg-subscribe__text', 'data-field': 'text' }, block.text),
29
- // `actions` is required and non-empty for a `cta`, so this always renders;
30
- // the null branch stays reachable only through invalid stored data.
31
- actionList(ctx, block.actions, block.title),
30
+ h(
31
+ 'div',
32
+ { class: 'cg-appeal__body' },
33
+ block.text === undefined
34
+ ? null
35
+ : h('p', { class: 'cg-appeal__text', 'data-field': 'text' }, block.text),
36
+ actionList(ctx, block.actions, block.title),
37
+ ),
32
38
  )
33
39
  }