@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,42 +1,58 @@
1
1
  import type { MediaFigureBlock } from '@cogenta/blocks'
2
2
  import { aspectRatio, type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+ import { section } from '../layout.js'
3
4
 
4
5
  /**
5
- * A plate, in the print sense: the picture runs large, and the caption sits
6
- * under a hairline rule set in small caps with the credit line trailing it —
7
- * the way a photo essay is captioned, rather than a card with a paragraph
8
- * under it.
6
+ * A photograph placed on the grid by its alignment, captioned the way a
7
+ * newspaper captions one: the caption in the interface face under the
8
+ * picture and the credit after it in small capitals, both starting on the
9
+ * picture's own left edge.
9
10
  *
10
- * `<figure>`/`<figcaption>` keep the caption announced as belonging to the
11
- * image; `align` stays a data attribute (contract B's intent, not a class),
12
- * so the skin — here, this theme — decides what `start`/`end`/`wide`/`full`
13
- * mean.
11
+ * - `center` (the default): the reading column.
12
+ * - `start` / `end`: half the page, from the left or to the right.
13
+ * - `wide`: ten columns, the width an article's lead photograph takes.
14
+ * - `full`: edge to edge, the caption returning to the grid.
14
15
  */
15
16
  export function renderMediaFigure(block: MediaFigureBlock, ctx: RenderContext): HtmlElement {
16
17
  const ratio = aspectRatio(block.ratio)
18
+ const align = block.align ?? 'center'
17
19
  const hasCaption = block.caption !== undefined || block.credit !== undefined
18
- return h(
19
- 'figure',
20
+ return section(
21
+ 'div',
22
+ 'mediaFigure',
23
+ 'cg-figure',
20
24
  {
21
- class: 'cg-block cg-plate',
22
- 'data-block': 'mediaFigure',
23
- 'data-align': block.align ?? 'center',
25
+ 'data-align': align,
24
26
  style: ratio === undefined ? undefined : `--cg-ratio:${ratio}`,
25
27
  },
26
- image(ctx, block.media, {
27
- className: 'cg-plate__media',
28
- sizes: '(min-width: 60rem) 48rem, 100vw',
29
- }),
28
+ 'figure',
29
+ h(
30
+ 'div',
31
+ { class: 'cg-figure__frame' },
32
+ image(ctx, block.media, {
33
+ className: 'cg-figure__image',
34
+ sizes:
35
+ align === 'full'
36
+ ? '100vw'
37
+ : align === 'wide'
38
+ ? '(min-width: 64rem) 66rem, 100vw'
39
+ : '(min-width: 64rem) 40rem, 100vw',
40
+ }),
41
+ ),
30
42
  hasCaption
31
43
  ? h(
32
44
  'figcaption',
33
- { class: 'cg-plate__caption' },
45
+ { class: 'cg-figure__caption' },
34
46
  block.caption === undefined
35
47
  ? null
36
- : h('span', { class: 'cg-plate__text' }, block.caption),
48
+ : h(
49
+ 'span',
50
+ { class: 'cg-figure__caption-text', 'data-field': 'caption' },
51
+ block.caption,
52
+ ),
37
53
  block.credit === undefined
38
54
  ? null
39
- : h('span', { class: 'cg-plate__credit', 'data-field': 'credit' }, block.credit),
55
+ : h('span', { class: 'cg-figure__credit', 'data-field': 'credit' }, block.credit),
40
56
  )
41
57
  : null,
42
58
  )
@@ -1,7 +1,6 @@
1
1
  import type { PricingTableBlock, PricingTier } from '@cogenta/blocks'
2
2
  import {
3
3
  actionLink,
4
- blockHeadingTag,
5
4
  type HeadingTag,
6
5
  type HtmlElement,
7
6
  h,
@@ -9,67 +8,63 @@ 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
- * `blocks@2.0` (RFC 0001). A classified-ads rate card: tiers set side by
15
- * side and separated by the theme's one visible column rule (the same
16
- * device `hero` and `collectionList` use), rather than a set of raised
17
- * cards — a print page has no elevation to raise a card off of.
18
- *
19
- * `highlighted` is an editorial signal, not a colour: it becomes
20
- * `data-highlighted` for the stylesheet to pick up (an ink-reversed panel,
21
- * here) and `aria-current` so it is announced, not only shown.
14
+ * Subscription rates, set as the columns of a ruled table rather than as
15
+ * cards: each tier opens on a hairline, its name in small capitals, the price
16
+ * in the display face with tabular numerals and the interval after it, what
17
+ * it includes as a list divided by hairlines, and its action at the foot. The
18
+ * tier the editor highlighted opens on a heavy red rule instead, and its
19
+ * action is the filled one; every other tier's action is drawn as an outlined
20
+ * control in `blocks.css`, so the four actions line up as one row.
22
21
  */
23
22
  function renderTier(tier: PricingTier, ctx: RenderContext, tag: HeadingTag): HtmlElement {
23
+ const highlighted = tier.highlighted === true
24
24
  return h(
25
25
  'li',
26
- {
27
- class: 'cg-ratecard__tier',
28
- 'data-highlighted': tier.highlighted === true ? 'true' : undefined,
29
- 'aria-current': tier.highlighted === true ? 'true' : undefined,
30
- },
31
- heading(tag, { class: 'cg-ratecard__name' }, tier.name),
26
+ { class: 'cg-rates__tier', 'data-highlighted': highlighted ? 'true' : undefined },
27
+ heading(tag, { class: 'cg-rates__name' }, tier.name),
32
28
  h(
33
29
  'p',
34
- { class: 'cg-ratecard__price' },
35
- h('span', { class: 'cg-ratecard__amount' }, tier.price),
30
+ { class: 'cg-rates__price' },
31
+ h('span', { class: 'cg-rates__amount' }, tier.price),
36
32
  tier.interval === undefined
37
33
  ? null
38
- : h('span', { class: 'cg-ratecard__interval' }, tier.interval),
34
+ : h('span', { class: 'cg-rates__interval' }, tier.interval),
39
35
  ),
40
36
  tier.features.length === 0
41
37
  ? null
42
38
  : h(
43
39
  'ul',
44
- { class: 'cg-ratecard__features' },
45
- tier.features.map((feature) => h('li', { class: 'cg-ratecard__feature' }, feature)),
40
+ { class: 'cg-rates__features' },
41
+ tier.features.map((feature) => h('li', { class: 'cg-rates__feature' }, feature)),
46
42
  ),
47
43
  tier.action === undefined
48
44
  ? null
49
45
  : h(
50
46
  'div',
51
- { class: 'cg-ratecard__action' },
52
- actionLink(ctx, { ...tier.action, emphasis: tier.action.emphasis ?? 'primary' }),
47
+ { class: 'cg-rates__action' },
48
+ actionLink(ctx, {
49
+ ...tier.action,
50
+ emphasis: tier.action.emphasis ?? (highlighted ? 'primary' : 'secondary'),
51
+ }),
53
52
  ),
54
53
  )
55
54
  }
56
55
 
57
56
  export function renderPricingTable(block: PricingTableBlock, ctx: RenderContext): HtmlElement {
58
- const hasTitle = block.title !== undefined
59
- const tierTag = nestedHeadingTag('pricingTable', hasTitle)
60
- return h(
57
+ const tierTag = nestedHeadingTag('pricingTable', block.title !== undefined)
58
+ return section(
61
59
  'section',
62
- { class: 'cg-block cg-ratecard', 'data-block': 'pricingTable' },
63
- hasTitle
64
- ? heading(
65
- blockHeadingTag('pricingTable') ?? 'h2',
66
- { class: 'cg-ratecard__title', 'data-field': 'title' },
67
- block.title ?? '',
68
- )
69
- : null,
60
+ 'pricingTable',
61
+ 'cg-rates',
62
+ { 'data-count': String(Math.min(block.tiers.length, 4)) },
63
+ 'div',
64
+ sectionHead('pricingTable', block.title),
70
65
  h(
71
66
  'ul',
72
- { class: 'cg-ratecard__tiers' },
67
+ { class: 'cg-rates__tiers' },
73
68
  block.tiers.map((tier) => renderTier(tier, ctx, tierTag)),
74
69
  ),
75
70
  )
@@ -1,22 +1,22 @@
1
1
  import type { ProseBlock } from '@cogenta/blocks'
2
2
  import { type HtmlElement, h, type RenderContext, renderRichText } from '@cogenta/theme-kit'
3
+ import { section } from '../layout.js'
3
4
 
4
5
  /**
5
- * `prose` declares `headingLevel: 'none'` — it contributes no heading of its
6
- * own, and `renderRichText` (shared, `@cogenta/theme-kit`) is what actually
7
- * turns the document into markup, including its embedded figures
8
- * (`.cg-prose__figure`, a class name that module already fixes — this file's
9
- * own container class has to match it).
10
- *
11
- * The drop cap and the pull-quote treatment on a nested `<blockquote>` are
12
- * both pure CSS, keyed off `.cg-prose > :first-child` and `.cg-prose
13
- * blockquote` in `blocks.css` — nothing here has to know which paragraph
14
- * came first.
6
+ * Running text, in the reading column: Source Serif at the reading size and
7
+ * a measure of about 66 characters, subheads in the display face, a
8
+ * blockquote set as a pull quote, figures captioned. On an article the first
9
+ * paragraph of the first `prose` block may open on a drop cap (`article.css`,
10
+ * keyed on `data-opening`), and only where the browser can set a real
11
+ * initial letter.
15
12
  */
16
13
  export function renderProse(block: ProseBlock, ctx: RenderContext): HtmlElement {
17
- return h(
14
+ return section(
18
15
  'div',
19
- { class: 'cg-block cg-prose', 'data-block': 'prose' },
20
- renderRichText(ctx, block.body),
16
+ 'prose',
17
+ 'cg-prose',
18
+ {},
19
+ 'div',
20
+ h('div', { class: 'cg-prose__body' }, renderRichText(ctx, block.body)),
21
21
  )
22
22
  }
@@ -1,25 +1,29 @@
1
1
  import type { QuoteBlock } 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
- * The pull quote — the one typographic gesture a magazine spread always
6
- * makes. Set large in the display serif, indented from a heavy left rule
7
- * rather than centred under a giant glyph, with the attribution trailing it
8
- * on its own line, joined by a typographic em-rule instead of a comma the
9
- * markup does not carry.
6
+ * A pull quote: a short red rule, the words in the display face at a large
7
+ * italic size, and the speaker in small capitals with their role after them.
8
+ * In an article it hangs to the left of the reading column; on any other page
9
+ * it takes the same columns as the text around it.
10
10
  *
11
- * `<figure><blockquote>…</blockquote><figcaption>` is the attribution
12
- * pattern the HTML spec itself prescribes — the author's name sitting
13
- * outside the `<blockquote>` is what stops it from being claimed as part of
14
- * what was said.
11
+ * The avatar is decorative (the name is beside it in text), so it keeps the
12
+ * media library's own empty `alt`.
15
13
  */
16
14
  export function renderQuote(block: QuoteBlock, ctx: RenderContext): HtmlElement {
17
- const hasAttribution =
18
- block.author !== undefined || block.role !== undefined || block.avatar !== undefined
19
- return h(
15
+ const hasAttribution = block.author !== undefined || block.role !== undefined
16
+ return section(
17
+ 'div',
18
+ 'quote',
19
+ 'cg-pullquote',
20
+ {},
20
21
  'figure',
21
- { class: 'cg-block cg-pullquote', 'data-block': 'quote' },
22
- h('blockquote', { class: 'cg-pullquote__text' }, h('p', { 'data-field': 'text' }, block.text)),
22
+ h(
23
+ 'blockquote',
24
+ { class: 'cg-pullquote__quote' },
25
+ h('p', { class: 'cg-pullquote__text', 'data-field': 'text' }, block.text),
26
+ ),
23
27
  hasAttribution
24
28
  ? h(
25
29
  'figcaption',
@@ -30,16 +34,12 @@ export function renderQuote(block: QuoteBlock, ctx: RenderContext): HtmlElement
30
34
  className: 'cg-pullquote__avatar',
31
35
  variant: { width: 96, height: 96, fit: 'cover' },
32
36
  }),
33
- h(
34
- 'span',
35
- { class: 'cg-pullquote__names' },
36
- block.author === undefined
37
- ? null
38
- : h('span', { class: 'cg-pullquote__author', 'data-field': 'author' }, block.author),
39
- block.role === undefined
40
- ? null
41
- : h('span', { class: 'cg-pullquote__role', 'data-field': 'role' }, block.role),
42
- ),
37
+ block.author === undefined
38
+ ? null
39
+ : h('span', { class: 'cg-pullquote__author', 'data-field': 'author' }, block.author),
40
+ block.role === undefined
41
+ ? null
42
+ : h('span', { class: 'cg-pullquote__role', 'data-field': 'role' }, block.role),
43
43
  )
44
44
  : null,
45
45
  )
@@ -1,39 +1,31 @@
1
1
  import type { StatCounterBlock, StatCounterItem } from '@cogenta/blocks'
2
- import {
3
- blockHeadingTag,
4
- type HtmlElement,
5
- h,
6
- heading,
7
- type RenderContext,
8
- } from '@cogenta/theme-kit'
2
+ import { type HtmlElement, h, type RenderContext } from '@cogenta/theme-kit'
3
+ import { section, sectionHead } from '../layout.js'
9
4
 
10
5
  /**
11
- * `blocks@2.0` (RFC 0001). A circulation tally, set apart from `stats`'s own
12
- * `cg-figures` (which carries vertical column rules and an optional `unit`):
13
- * `statCounter` has no `unit`, so it is laid out as a single running line —
14
- * each figure and its label separated only by a shared baseline rule, the
15
- * way a masthead prints its circulation and edition counts along one strip.
6
+ * "By the numbers": a ruled table of figures down the page, each row a value
7
+ * in the display face on the left and what it counts on the right. Where
8
+ * `stats` sets figures side by side, this sets them as a list a reader runs
9
+ * down, which suits a longer set. It carries no unit, the narrower shape
10
+ * contract B gives it.
16
11
  */
17
12
  function renderItem(item: StatCounterItem): HtmlElement {
18
13
  return h(
19
14
  'div',
20
- { class: 'cg-tally__item' },
21
- h('dd', { class: 'cg-tally__value' }, item.value),
15
+ { class: 'cg-tally__row' },
22
16
  h('dt', { class: 'cg-tally__label' }, item.label),
17
+ h('dd', { class: 'cg-tally__value' }, item.value),
23
18
  )
24
19
  }
25
20
 
26
21
  export function renderStatCounter(block: StatCounterBlock, _ctx: RenderContext): HtmlElement {
27
- return h(
22
+ return section(
28
23
  'section',
29
- { class: 'cg-block cg-tally', 'data-block': 'statCounter' },
30
- block.title === undefined
31
- ? null
32
- : heading(
33
- blockHeadingTag('statCounter') ?? 'h2',
34
- { class: 'cg-tally__title', 'data-field': 'title' },
35
- block.title,
36
- ),
37
- h('dl', { class: 'cg-tally__items' }, block.stats.map(renderItem)),
24
+ 'statCounter',
25
+ 'cg-tally',
26
+ {},
27
+ 'div',
28
+ sectionHead('statCounter', block.title),
29
+ h('dl', { class: 'cg-tally__rows' }, block.stats.map(renderItem)),
38
30
  )
39
31
  }
@@ -1,20 +1,13 @@
1
1
  import type { StatItem, StatsBlock } from '@cogenta/blocks'
2
- import {
3
- blockHeadingTag,
4
- type HtmlElement,
5
- h,
6
- heading,
7
- type RenderContext,
8
- } from '@cogenta/theme-kit'
2
+ import { type HtmlElement, h, type RenderContext } from '@cogenta/theme-kit'
3
+ import { section, sectionHead } from '../layout.js'
9
4
 
10
5
  /**
11
- * "By the numbers" — an infographic sidebar with a real newspaper
12
- * `column-rule` between figures, each set in the display serif with tabular
13
- * numerals so a column of changing digits never shifts width.
14
- *
15
- * A description list, as in the reference theme: each figure *describes* its
16
- * label, which is exactly what `<dt>`/`<dd>` mean, and it survives being read
17
- * linearly by anything that ignores the stylesheet.
6
+ * Figures in a row between two rules, the way a newspaper sets a fact box
7
+ * without the box: each value in the display face with lining tabular
8
+ * numerals, its unit smaller beside it, the label under it in the interface
9
+ * face, and a vertical hairline between figures. A description list, so
10
+ * each value is announced with its label.
18
11
  */
19
12
  function renderItem(item: StatItem): HtmlElement {
20
13
  return h(
@@ -24,23 +17,20 @@ function renderItem(item: StatItem): HtmlElement {
24
17
  h(
25
18
  'dd',
26
19
  { class: 'cg-figures__value' },
27
- item.value,
20
+ h('span', { class: 'cg-figures__number' }, item.value),
28
21
  item.unit === undefined ? null : h('span', { class: 'cg-figures__unit' }, item.unit),
29
22
  ),
30
23
  )
31
24
  }
32
25
 
33
26
  export function renderStats(block: StatsBlock, _ctx: RenderContext): HtmlElement {
34
- return h(
27
+ return section(
35
28
  'section',
36
- { class: 'cg-block cg-figures', 'data-block': 'stats' },
37
- block.title === undefined
38
- ? null
39
- : heading(
40
- blockHeadingTag('stats') ?? 'h2',
41
- { class: 'cg-figures__title', 'data-field': 'title' },
42
- block.title,
43
- ),
29
+ 'stats',
30
+ 'cg-figures',
31
+ { 'data-count': String(Math.min(block.items.length, 4)) },
32
+ 'div',
33
+ sectionHead('stats', block.title),
44
34
  h('dl', { class: 'cg-figures__items' }, block.items.map(renderItem)),
45
35
  )
46
36
  }
@@ -1,30 +1,25 @@
1
1
  import type { TestimonialBlock } from '@cogenta/blocks'
2
2
  import { type HtmlElement, h, image, type RenderContext, renderRichText } from '@cogenta/theme-kit'
3
+ import { section } from '../layout.js'
3
4
 
4
5
  /**
5
- * `blocks@2.0` (RFC 0001). A reader's letter, not a pull quote: `quote`
6
- * already owns the theme's one "loud" typographic gesture (the giant quote
7
- * mark and heavy rule in `cg-pullquote`), so a testimonial reads instead as
8
- * a boxed letter-to-the-editor — a framed panel, the quote set in italic
9
- * serif at body size rather than blown up, and a byline row underneath a
10
- * hairline rule, the way a magazine runs its reader mail column.
11
- *
12
- * The quote is rich text (unlike `quote`'s plain-text `text`), so a
13
- * testimonial can carry a link or emphasis; `renderRichText` handles it the
14
- * same way `accordion`'s answers and `prose`'s body do.
15
- *
16
- * The avatar is decorative, exactly like `quote`'s own: the name sits right
17
- * beside it in text, so its media entity's alt text is expected to be empty.
6
+ * A letter from a reader, set as the letters page sets one: the words in the
7
+ * reading face one step above the text size, a hairline, and the writer's
8
+ * name in small capitals with where they write from. Quieter than `quote` on
9
+ * purpose: a pull quote is display, a letter is correspondence.
18
10
  */
19
11
  export function renderTestimonial(block: TestimonialBlock, ctx: RenderContext): HtmlElement {
20
12
  const { attribution } = block
21
- return h(
13
+ return section(
14
+ 'div',
15
+ 'testimonial',
16
+ 'cg-letter',
17
+ {},
22
18
  'figure',
23
- { class: 'cg-block cg-letter', 'data-block': 'testimonial' },
24
19
  h('blockquote', { class: 'cg-letter__quote' }, renderRichText(ctx, block.quote)),
25
20
  h(
26
21
  'figcaption',
27
- { class: 'cg-letter__byline' },
22
+ { class: 'cg-letter__attribution' },
28
23
  attribution.avatar === undefined
29
24
  ? null
30
25
  : image(ctx, attribution.avatar, {
@@ -33,7 +28,7 @@ export function renderTestimonial(block: TestimonialBlock, ctx: RenderContext):
33
28
  }),
34
29
  h(
35
30
  'span',
36
- { class: 'cg-letter__names' },
31
+ { class: 'cg-letter__who' },
37
32
  h('span', { class: 'cg-letter__name' }, attribution.name),
38
33
  attribution.role === undefined
39
34
  ? null