@cogenta/theme-association 0.3.2 → 0.5.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 (148) hide show
  1. package/dist/src/index.d.ts +6 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +9 -0
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/render/blocks/accordion.d.ts.map +1 -1
  6. package/dist/src/render/blocks/accordion.js +13 -10
  7. package/dist/src/render/blocks/accordion.js.map +1 -1
  8. package/dist/src/render/blocks/breakdown.d.ts +14 -0
  9. package/dist/src/render/blocks/breakdown.d.ts.map +1 -0
  10. package/dist/src/render/blocks/breakdown.js +17 -0
  11. package/dist/src/render/blocks/breakdown.js.map +1 -0
  12. package/dist/src/render/blocks/collection-list.d.ts +4 -10
  13. package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
  14. package/dist/src/render/blocks/collection-list.js +89 -58
  15. package/dist/src/render/blocks/collection-list.js.map +1 -1
  16. package/dist/src/render/blocks/cta.d.ts +0 -5
  17. package/dist/src/render/blocks/cta.d.ts.map +1 -1
  18. package/dist/src/render/blocks/cta.js +22 -10
  19. package/dist/src/render/blocks/cta.js.map +1 -1
  20. package/dist/src/render/blocks/embed.d.ts.map +1 -1
  21. package/dist/src/render/blocks/embed.js +30 -22
  22. package/dist/src/render/blocks/embed.js.map +1 -1
  23. package/dist/src/render/blocks/faq.d.ts.map +1 -1
  24. package/dist/src/render/blocks/faq.js +11 -15
  25. package/dist/src/render/blocks/faq.js.map +1 -1
  26. package/dist/src/render/blocks/feature-grid.d.ts +6 -0
  27. package/dist/src/render/blocks/feature-grid.d.ts.map +1 -1
  28. package/dist/src/render/blocks/feature-grid.js +42 -20
  29. package/dist/src/render/blocks/feature-grid.js.map +1 -1
  30. package/dist/src/render/blocks/gallery.d.ts +9 -7
  31. package/dist/src/render/blocks/gallery.d.ts.map +1 -1
  32. package/dist/src/render/blocks/gallery.js +22 -16
  33. package/dist/src/render/blocks/gallery.js.map +1 -1
  34. package/dist/src/render/blocks/hero.d.ts +23 -9
  35. package/dist/src/render/blocks/hero.d.ts.map +1 -1
  36. package/dist/src/render/blocks/hero.js +40 -23
  37. package/dist/src/render/blocks/hero.js.map +1 -1
  38. package/dist/src/render/blocks/logo-strip.d.ts +6 -0
  39. package/dist/src/render/blocks/logo-strip.d.ts.map +1 -1
  40. package/dist/src/render/blocks/logo-strip.js +7 -9
  41. package/dist/src/render/blocks/logo-strip.js.map +1 -1
  42. package/dist/src/render/blocks/logos.d.ts +14 -0
  43. package/dist/src/render/blocks/logos.d.ts.map +1 -1
  44. package/dist/src/render/blocks/logos.js +30 -16
  45. package/dist/src/render/blocks/logos.js.map +1 -1
  46. package/dist/src/render/blocks/media-figure.d.ts +12 -5
  47. package/dist/src/render/blocks/media-figure.d.ts.map +1 -1
  48. package/dist/src/render/blocks/media-figure.js +32 -18
  49. package/dist/src/render/blocks/media-figure.js.map +1 -1
  50. package/dist/src/render/blocks/pricing-table.d.ts.map +1 -1
  51. package/dist/src/render/blocks/pricing-table.js +18 -20
  52. package/dist/src/render/blocks/pricing-table.js.map +1 -1
  53. package/dist/src/render/blocks/prose.d.ts +9 -3
  54. package/dist/src/render/blocks/prose.d.ts.map +1 -1
  55. package/dist/src/render/blocks/prose.js +11 -4
  56. package/dist/src/render/blocks/prose.js.map +1 -1
  57. package/dist/src/render/blocks/quote.d.ts +4 -7
  58. package/dist/src/render/blocks/quote.d.ts.map +1 -1
  59. package/dist/src/render/blocks/quote.js +12 -17
  60. package/dist/src/render/blocks/quote.js.map +1 -1
  61. package/dist/src/render/blocks/stat-counter.d.ts +8 -1
  62. package/dist/src/render/blocks/stat-counter.d.ts.map +1 -1
  63. package/dist/src/render/blocks/stat-counter.js +18 -10
  64. package/dist/src/render/blocks/stat-counter.js.map +1 -1
  65. package/dist/src/render/blocks/stats.d.ts +12 -1
  66. package/dist/src/render/blocks/stats.d.ts.map +1 -1
  67. package/dist/src/render/blocks/stats.js +24 -14
  68. package/dist/src/render/blocks/stats.js.map +1 -1
  69. package/dist/src/render/blocks/testimonial.d.ts +6 -5
  70. package/dist/src/render/blocks/testimonial.d.ts.map +1 -1
  71. package/dist/src/render/blocks/testimonial.js +13 -11
  72. package/dist/src/render/blocks/testimonial.js.map +1 -1
  73. package/dist/src/render/chrome.d.ts +38 -1
  74. package/dist/src/render/chrome.d.ts.map +1 -1
  75. package/dist/src/render/chrome.js +117 -91
  76. package/dist/src/render/chrome.js.map +1 -1
  77. package/dist/src/render/details.d.ts +79 -0
  78. package/dist/src/render/details.d.ts.map +1 -0
  79. package/dist/src/render/details.js +221 -0
  80. package/dist/src/render/details.js.map +1 -0
  81. package/dist/src/render/figures.d.ts +29 -0
  82. package/dist/src/render/figures.d.ts.map +1 -0
  83. package/dist/src/render/figures.js +59 -0
  84. package/dist/src/render/figures.js.map +1 -0
  85. package/dist/src/render/layout.d.ts +31 -0
  86. package/dist/src/render/layout.d.ts.map +1 -0
  87. package/dist/src/render/layout.js +46 -0
  88. package/dist/src/render/layout.js.map +1 -0
  89. package/dist/src/render/page-head.d.ts +7 -0
  90. package/dist/src/render/page-head.d.ts.map +1 -0
  91. package/dist/src/render/page-head.js +95 -0
  92. package/dist/src/render/page-head.js.map +1 -0
  93. package/dist/src/render/render-block.d.ts +8 -12
  94. package/dist/src/render/render-block.d.ts.map +1 -1
  95. package/dist/src/render/render-block.js +40 -29
  96. package/dist/src/render/render-block.js.map +1 -1
  97. package/dist/src/render/strings.d.ts +15 -0
  98. package/dist/src/render/strings.d.ts.map +1 -0
  99. package/dist/src/render/strings.js +39 -0
  100. package/dist/src/render/strings.js.map +1 -0
  101. package/dist/src/render/term-archive.d.ts.map +1 -1
  102. package/dist/src/render/term-archive.js +33 -25
  103. package/dist/src/render/term-archive.js.map +1 -1
  104. package/dist/src/render/widget-areas.d.ts +14 -0
  105. package/dist/src/render/widget-areas.d.ts.map +1 -0
  106. package/dist/src/render/widget-areas.js +21 -0
  107. package/dist/src/render/widget-areas.js.map +1 -0
  108. package/dist/theme.config.d.ts +1 -1
  109. package/dist/theme.config.js +3 -3
  110. package/dist/theme.config.js.map +1 -1
  111. package/package.json +5 -5
  112. package/src/index.ts +9 -0
  113. package/src/render/blocks/accordion.ts +28 -22
  114. package/src/render/blocks/breakdown.ts +42 -0
  115. package/src/render/blocks/collection-list.ts +234 -103
  116. package/src/render/blocks/cta.ts +48 -19
  117. package/src/render/blocks/embed.ts +47 -29
  118. package/src/render/blocks/faq.ts +22 -31
  119. package/src/render/blocks/feature-grid.ts +73 -41
  120. package/src/render/blocks/gallery.ts +32 -26
  121. package/src/render/blocks/hero.ts +56 -43
  122. package/src/render/blocks/logo-strip.ts +23 -21
  123. package/src/render/blocks/logos.ts +45 -40
  124. package/src/render/blocks/media-figure.ts +49 -24
  125. package/src/render/blocks/pricing-table.ts +31 -40
  126. package/src/render/blocks/prose.ts +16 -6
  127. package/src/render/blocks/quote.ts +34 -27
  128. package/src/render/blocks/stat-counter.ts +35 -29
  129. package/src/render/blocks/stats.ts +46 -38
  130. package/src/render/blocks/testimonial.ts +29 -20
  131. package/src/render/chrome.ts +165 -91
  132. package/src/render/details.ts +276 -0
  133. package/src/render/figures.ts +68 -0
  134. package/src/render/layout.ts +78 -0
  135. package/src/render/page-head.ts +193 -0
  136. package/src/render/render-block.ts +45 -32
  137. package/src/render/strings.ts +74 -0
  138. package/src/render/term-archive.ts +94 -58
  139. package/src/render/widget-areas.ts +24 -0
  140. package/src/styles/archive.css +38 -102
  141. package/src/styles/base.css +503 -554
  142. package/src/styles/blocks.css +913 -666
  143. package/src/styles/chrome.css +506 -0
  144. package/src/styles/listings.css +591 -0
  145. package/src/styles/theme.css +25 -19
  146. package/src/styles/tokens.css +284 -234
  147. package/src/styles/widgets.css +1059 -0
  148. package/tokens.json +18 -18
@@ -1,31 +1,33 @@
1
- import type { LogoStripBlock, LogoStripItem } from '@cogenta/blocks'
1
+ import type { LogoStripBlock } 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). "Our partners" — the lighter-weight social-proof
6
- * row next to `logos`: no per-logo name or link, so no accessible-name field
7
- * to require — each image's own alt text (set once, in the media library)
8
- * names it.
6
+ * The lighter row of marks: the caption at the caption size above a rule,
7
+ * and the marks spread along one line under it, small and evenly spaced, in
8
+ * one ink. No links, by contract B; each mark is named by its own alt text
9
+ * from the media library.
9
10
  */
10
- function renderItem(item: LogoStripItem, ctx: RenderContext): HtmlElement {
11
- return h(
12
- 'li',
13
- { class: 'cg-logo-band__item' },
14
- image(ctx, item.media, { className: 'cg-logo-band__image', variant: { fit: 'contain' } }),
15
- )
16
- }
17
-
18
11
  export function renderLogoStrip(block: LogoStripBlock, ctx: RenderContext): HtmlElement {
19
- return h(
20
- 'figure',
21
- { class: 'cg-block cg-logo-band', 'data-block': 'logoStrip' },
12
+ return section(
13
+ 'div',
14
+ 'logoStrip',
15
+ 'ca-strip',
16
+ { 'data-captioned': String(block.caption !== undefined) },
17
+ 'div',
18
+ block.caption === undefined
19
+ ? null
20
+ : h('p', { class: 'ca-strip__caption', 'data-field': 'caption' }, block.caption),
22
21
  h(
23
22
  'ul',
24
- { class: 'cg-logo-band__items' },
25
- block.logos.map((item) => renderItem(item, ctx)),
23
+ { class: 'ca-strip__items' },
24
+ block.logos.map((logo) =>
25
+ h(
26
+ 'li',
27
+ { class: 'ca-strip__item' },
28
+ image(ctx, logo.media, { className: 'ca-mark', sizes: '12rem' }),
29
+ ),
30
+ ),
26
31
  ),
27
- block.caption === undefined
28
- ? null
29
- : h('figcaption', { class: 'cg-logo-band__caption', 'data-field': 'caption' }, block.caption),
30
32
  )
31
33
  }
@@ -1,52 +1,57 @@
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'
1
+ import type { LogosBlock } from '@cogenta/blocks'
2
+ import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+ import { section, sectionHead } from '../layout.js'
10
4
 
11
5
  /**
12
- * Contract B says the organisation's name **is** the accessible name of the
13
- * link. So the logo image carries it as alt text when the media entity has
14
- * none, and no visually hidden duplicate is added.
6
+ * The organisations that work alongside this one: the title on the first
7
+ * four columns, and their marks on the last eight in a grid of cells divided
8
+ * by hairlines, each mark set small and in one ink so no partner shouts
9
+ * louder than another. A mark drawn in ink is inverted in the dark scheme.
10
+ *
11
+ * The organisation's name is the mark's accessible name when the media
12
+ * library has no alt text for it, and the link's own name when it links out.
15
13
  */
16
- function renderItem(item: LogoItem, ctx: RenderContext): HtmlElement {
17
- const logo = image(ctx, item.media, {
18
- className: 'cg-logo__image',
19
- altFrom: item.name,
20
- variant: { fit: 'contain' },
21
- })
22
- return h(
23
- 'li',
24
- { class: 'cg-logo' },
25
- item.url === undefined
26
- ? logo
27
- : h(
28
- 'a',
29
- { class: 'cg-logo__link', href: ctx.link(item.url), rel: 'noopener noreferrer' },
30
- logo,
31
- ),
32
- )
14
+ /**
15
+ * Columns on a wide screen that leave no empty cell in the grid: three when
16
+ * the count divides by three, two when it divides by two, otherwise three.
17
+ */
18
+ export function logoColumnsFor(count: number): number {
19
+ if (count % 3 === 0) return 3
20
+ return count % 2 === 0 ? 2 : 3
33
21
  }
34
22
 
35
23
  export function renderLogos(block: LogosBlock, ctx: RenderContext): HtmlElement {
36
- return h(
24
+ return section(
37
25
  'section',
38
- { class: 'cg-block cg-logos', 'data-block': 'logos' },
39
- block.title === undefined
40
- ? null
41
- : heading(
42
- blockHeadingTag('logos') ?? 'h2',
43
- { class: 'cg-logos__title', 'data-field': 'title' },
44
- block.title,
45
- ),
26
+ 'logos',
27
+ 'ca-partners',
28
+ {
29
+ 'data-titled': String(block.title !== undefined),
30
+ 'data-columns': String(logoColumnsFor(block.items.length)),
31
+ },
32
+ 'div',
33
+ sectionHead('logos', block.title),
46
34
  h(
47
35
  'ul',
48
- { class: 'cg-logos__items' },
49
- block.items.map((item) => renderItem(item, ctx)),
36
+ { class: 'ca-partners__items' },
37
+ block.items.map((item) => {
38
+ const mark = image(ctx, item.media, {
39
+ className: 'ca-mark',
40
+ altFrom: item.name,
41
+ sizes: '12rem',
42
+ })
43
+ return h(
44
+ 'li',
45
+ { class: 'ca-partners__item' },
46
+ item.url === undefined
47
+ ? h('span', { class: 'ca-partners__cell' }, mark)
48
+ : h(
49
+ 'a',
50
+ { class: 'ca-partners__cell', href: item.url, rel: 'noopener noreferrer' },
51
+ mark,
52
+ ),
53
+ )
54
+ }),
50
55
  ),
51
56
  )
52
57
  }
@@ -1,38 +1,63 @@
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
- * `<figure>`/`<figcaption>` rather than a div and a paragraph: the
6
- * association between the picture and its caption is then in the markup.
6
+ * A photograph and its caption, the caption always under the picture's own
7
+ * left edge, in the text face, with the credit after it.
7
8
  *
8
- * The media sits in a big rounded card (`cg-figure__media`, `--cg-radius-lg`)
9
- * with a soft shadow, matching the hero's own frame treatment rather than a
10
- * hard-edged photo.
9
+ * `align` is an editorial intent, read as a layout:
10
+ *
11
+ * - `start` / `end`: seven columns on that side, the caption beside it on the
12
+ * other four, set against the bottom of the picture.
13
+ * - `center`: eight columns, set in from the second.
14
+ * - `wide` (and no value): the whole container.
15
+ * - `full`: the whole window, edge to edge.
16
+ *
17
+ * The ratio crops from the middle, or from the media's own focal point; the
18
+ * corners stay square and the picture casts no shadow.
11
19
  */
12
20
  export function renderMediaFigure(block: MediaFigureBlock, ctx: RenderContext): HtmlElement {
21
+ const align = block.align ?? 'wide'
22
+ const split = align === 'start' || align === 'end'
13
23
  const ratio = aspectRatio(block.ratio)
14
- const hasCaption = block.caption !== undefined || block.credit !== undefined
15
- return h(
16
- 'figure',
17
- {
18
- class: 'cg-block cg-figure',
19
- 'data-block': 'mediaFigure',
20
- 'data-align': block.align ?? 'center',
21
- style: ratio === undefined ? undefined : `--cg-ratio:${ratio}`,
22
- },
23
- image(ctx, block.media, {
24
- className: 'cg-figure__media',
25
- sizes: '(min-width: 45rem) 40rem, 100vw',
26
- }),
27
- hasCaption
28
- ? h(
24
+
25
+ const caption =
26
+ block.caption === undefined && block.credit === undefined
27
+ ? null
28
+ : h(
29
29
  'figcaption',
30
- { class: 'cg-figure__caption' },
31
- block.caption,
30
+ { class: 'ca-figure__caption' },
31
+ block.caption === undefined
32
+ ? null
33
+ : h('span', { class: 'ca-figure__text', 'data-field': 'caption' }, block.caption),
32
34
  block.credit === undefined
33
35
  ? null
34
- : h('span', { class: 'cg-figure__credit', 'data-field': 'credit' }, block.credit),
36
+ : h('span', { class: 'ca-figure__credit', 'data-field': 'credit' }, block.credit),
35
37
  )
36
- : null,
38
+
39
+ return section(
40
+ 'div',
41
+ 'mediaFigure',
42
+ 'ca-figure',
43
+ { 'data-align': align, 'data-layout': split ? 'split' : 'single' },
44
+ 'figure',
45
+ h(
46
+ 'div',
47
+ {
48
+ class: 'ca-figure__frame',
49
+ style: ratio === undefined ? undefined : `aspect-ratio:${ratio}`,
50
+ },
51
+ image(ctx, block.media, {
52
+ className: 'ca-figure__image',
53
+ sizes:
54
+ align === 'full'
55
+ ? '100vw'
56
+ : split
57
+ ? '(min-width: 64rem) 55vw, 100vw'
58
+ : '(min-width: 82rem) 78rem, 100vw',
59
+ }),
60
+ ),
61
+ caption,
37
62
  )
38
63
  }
@@ -1,73 +1,64 @@
1
1
  import type { PricingTableBlock, PricingTier } from '@cogenta/blocks'
2
2
  import {
3
3
  actionLink,
4
- blockHeadingTag,
5
- type HeadingTag,
6
4
  type HtmlElement,
7
5
  h,
8
6
  heading,
9
7
  nestedHeadingTag,
10
8
  type RenderContext,
11
9
  } from '@cogenta/theme-kit'
10
+ import { section, sectionHead } from '../layout.js'
12
11
 
13
12
  /**
14
- * `blocks@2.0` (RFC 0001). Rarely a nonprofit's own price list, but not never
15
- * (a membership fee, a course fee) — kept in the shared, rounded-card
16
- * register the rest of this theme uses. `highlighted` becomes
17
- * `data-highlighted` for the skin to style, and `aria-current` so it is
18
- * announced, not only shown.
13
+ * Levels side by side: a membership, a regular gift, a sponsored place.
14
+ * Ruled columns rather than cards: the name of the level, the amount large in
15
+ * the display face with tabular figures and what it covers ("a month") beside
16
+ * it, then what it includes one line at a time between hairlines, and the
17
+ * action at the foot.
18
+ *
19
+ * `highlighted` is an editorial emphasis, drawn as the column's top rule in
20
+ * the organisation's green, thicker, and its name in the same green. No
21
+ * ribbon, no raised card, no fill.
19
22
  */
20
- function renderTier(tier: PricingTier, ctx: RenderContext, tag: HeadingTag): HtmlElement {
23
+ function tier(item: PricingTier, ctx: RenderContext, titled: boolean): HtmlElement {
21
24
  return h(
22
25
  'li',
23
- {
24
- class: 'cg-pricing__tier',
25
- 'data-highlighted': tier.highlighted === true ? 'true' : undefined,
26
- 'aria-current': tier.highlighted === true ? 'true' : undefined,
27
- },
28
- heading(tag, { class: 'cg-pricing__name' }, tier.name),
26
+ { class: 'ca-levels__tier', 'data-highlighted': item.highlighted === true ? 'true' : 'false' },
27
+ heading(nestedHeadingTag('pricingTable', titled), { class: 'ca-levels__name' }, item.name),
29
28
  h(
30
29
  'p',
31
- { class: 'cg-pricing__price' },
32
- h('span', { class: 'cg-pricing__amount' }, tier.price),
33
- tier.interval === undefined
30
+ { class: 'ca-levels__price' },
31
+ h('span', { class: 'ca-levels__amount' }, item.price),
32
+ item.interval === undefined
34
33
  ? null
35
- : h('span', { class: 'cg-pricing__interval' }, tier.interval),
34
+ : h('span', { class: 'ca-levels__interval' }, item.interval),
36
35
  ),
37
- tier.features.length === 0
36
+ item.features.length === 0
38
37
  ? null
39
38
  : h(
40
39
  'ul',
41
- { class: 'cg-pricing__features' },
42
- tier.features.map((feature) => h('li', {}, feature)),
40
+ { class: 'ca-levels__lines' },
41
+ item.features.map((feature) => h('li', { class: 'ca-levels__line' }, feature)),
43
42
  ),
44
- tier.action === undefined
43
+ item.action === undefined
45
44
  ? null
46
- : h(
47
- 'div',
48
- { class: 'cg-pricing__action' },
49
- actionLink(ctx, { ...tier.action, emphasis: tier.action.emphasis ?? 'primary' }),
50
- ),
45
+ : h('div', { class: 'ca-levels__action' }, actionLink(ctx, item.action)),
51
46
  )
52
47
  }
53
48
 
54
49
  export function renderPricingTable(block: PricingTableBlock, ctx: RenderContext): HtmlElement {
55
- const hasTitle = block.title !== undefined
56
- const tierTag = nestedHeadingTag('pricingTable', hasTitle)
57
- return h(
50
+ const titled = block.title !== undefined
51
+ return section(
58
52
  'section',
59
- { class: 'cg-block cg-pricing', 'data-block': 'pricingTable' },
60
- hasTitle
61
- ? heading(
62
- blockHeadingTag('pricingTable') ?? 'h2',
63
- { class: 'cg-pricing__title', 'data-field': 'title' },
64
- block.title ?? '',
65
- )
66
- : null,
53
+ 'pricingTable',
54
+ 'ca-levels',
55
+ { 'data-count': String(Math.min(block.tiers.length, 4)) },
56
+ 'div',
57
+ sectionHead('pricingTable', block.title),
67
58
  h(
68
59
  'ul',
69
- { class: 'cg-pricing__tiers' },
70
- block.tiers.map((tier) => renderTier(tier, ctx, tierTag)),
60
+ { class: 'ca-levels__tiers' },
61
+ block.tiers.map((item) => tier(item, ctx, titled)),
71
62
  ),
72
63
  )
73
64
  }
@@ -1,15 +1,25 @@
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. Whatever headings appear come from the rich text document, whose
7
- * vocabulary starts at `h2`.
6
+ * Running text: the organisation's history, how to give, how volunteering
7
+ * works, a privacy notice.
8
+ *
9
+ * `prose` declares `headingLevel: 'none'`, so it adds no heading of its own;
10
+ * the rich text's own headings start at `h2`. The column starts on the first
11
+ * of twelve columns, on the same edge as the page title above it, and holds a
12
+ * reading measure of about sixty-six characters. The first paragraph of the
13
+ * first text under a page title is set a step larger, the way a report opens
14
+ * with its summary.
8
15
  */
9
16
  export function renderProse(block: ProseBlock, ctx: RenderContext): HtmlElement {
10
- return h(
17
+ return section(
11
18
  'div',
12
- { class: 'cg-block cg-prose', 'data-block': 'prose' },
13
- renderRichText(ctx, block.body),
19
+ 'prose',
20
+ 'ca-prose',
21
+ {},
22
+ 'div',
23
+ h('div', { class: 'ca-prose__body' }, renderRichText(ctx, block.body)),
14
24
  )
15
25
  }
@@ -1,39 +1,46 @@
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
- * `<figure><blockquote>…</blockquote><figcaption>` is the attribution
6
- * pattern the HTML spec prescribes: putting the author inside the
7
- * `<blockquote>` would claim the author's name is part of what was said.
8
- *
9
- * The avatar is decorative here — the name is right beside it in text — so
10
- * its media entity's alt text is expected to be empty. `image` still writes
11
- * the attribute either way.
6
+ * A few words someone said, set large: the text in the display face across
7
+ * nine columns, with a real opening quotation mark hung in the margin by the
8
+ * stylesheet, and the speaker's name and role small beneath it. A portrait,
9
+ * when there is one, is a small square crop beside the name.
12
10
  */
13
11
  export function renderQuote(block: QuoteBlock, ctx: RenderContext): HtmlElement {
14
- const hasAttribution =
15
- block.author !== undefined || block.role !== undefined || block.avatar !== undefined
16
- return h(
17
- 'figure',
18
- { class: 'cg-block cg-quote', 'data-block': 'quote' },
19
- h('blockquote', { class: 'cg-quote__text' }, h('p', { 'data-field': 'text' }, block.text)),
20
- hasAttribution
21
- ? h(
12
+ const who =
13
+ block.author === undefined && block.role === undefined
14
+ ? null
15
+ : h(
22
16
  'figcaption',
23
- { class: 'cg-quote__attribution' },
17
+ { class: 'ca-quote__attribution' },
24
18
  block.avatar === undefined
25
19
  ? null
26
- : image(ctx, block.avatar, {
27
- className: 'cg-quote__avatar',
28
- variant: { width: 96, height: 96, fit: 'cover' },
29
- }),
30
- block.author === undefined
31
- ? null
32
- : h('span', { class: 'cg-quote__author', 'data-field': 'author' }, block.author),
33
- block.role === undefined
34
- ? null
35
- : h('span', { class: 'cg-quote__role', 'data-field': 'role' }, block.role),
20
+ : image(ctx, block.avatar, { className: 'ca-quote__avatar', sizes: '3.5rem' }),
21
+ h(
22
+ 'span',
23
+ { class: 'ca-quote__who' },
24
+ block.author === undefined
25
+ ? null
26
+ : h('span', { class: 'ca-quote__author', 'data-field': 'author' }, block.author),
27
+ block.role === undefined
28
+ ? null
29
+ : h('span', { class: 'ca-quote__role', 'data-field': 'role' }, block.role),
30
+ ),
36
31
  )
37
- : null,
32
+
33
+ return section(
34
+ 'div',
35
+ 'quote',
36
+ 'ca-quote',
37
+ {},
38
+ 'figure',
39
+ h(
40
+ 'blockquote',
41
+ { class: 'ca-quote__quote' },
42
+ h('p', { class: 'ca-quote__text', 'data-field': 'text' }, block.text),
43
+ ),
44
+ who,
38
45
  )
39
46
  }
@@ -1,36 +1,42 @@
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'
1
+ import type { StatCounterBlock } from '@cogenta/blocks'
2
+ import { type HtmlElement, h, type RenderContext } from '@cogenta/theme-kit'
3
+ import { isBreakdown } from '../figures.js'
4
+ import { section, sectionHead } from '../layout.js'
5
+ import { renderBreakdown } from './breakdown.js'
9
6
 
10
7
  /**
11
- * `blocks@2.0` (RFC 0001). A `<dl>`, exactly like `stats` — narrower on
12
- * purpose (no `unit`), for one big figure and its label.
8
+ * The narrower sibling of `stats` (a value and a short label, no unit): a
9
+ * single row of figures across the container, divided by vertical hairlines,
10
+ * each number in the display face over its label. Percentages of one whole
11
+ * are drawn as a breakdown, the same as `stats`. Nothing counts up; a number
12
+ * that animates into place is a number a visitor has to wait for.
13
13
  */
14
- function renderItem(item: StatCounterItem): HtmlElement {
15
- return h(
16
- 'div',
17
- { class: 'cg-stat-counter' },
18
- h('dt', { class: 'cg-stat-counter__label' }, item.label),
19
- h('dd', { class: 'cg-stat-counter__value' }, item.value),
20
- )
21
- }
22
-
23
- export function renderStatCounter(block: StatCounterBlock, _ctx: RenderContext): HtmlElement {
24
- return h(
14
+ export function renderStatCounter(block: StatCounterBlock, ctx: RenderContext): HtmlElement {
15
+ const breakdown = isBreakdown(block.stats)
16
+ return section(
25
17
  'section',
26
- { class: 'cg-block cg-stat-counters', 'data-block': 'statCounter' },
27
- block.title === undefined
28
- ? null
29
- : heading(
30
- blockHeadingTag('statCounter') ?? 'h2',
31
- { class: 'cg-stat-counters__title', 'data-field': 'title' },
32
- block.title,
18
+ 'statCounter',
19
+ 'ca-counter',
20
+ {
21
+ 'data-shape': breakdown ? 'breakdown' : 'row',
22
+ 'data-count': String(Math.min(block.stats.length, 4)),
23
+ 'data-titled': String(block.title !== undefined),
24
+ },
25
+ 'div',
26
+ sectionHead('statCounter', block.title),
27
+ breakdown
28
+ ? renderBreakdown(block.stats, ctx)
29
+ : h(
30
+ 'dl',
31
+ { class: 'ca-counter__items' },
32
+ block.stats.map((stat) =>
33
+ h(
34
+ 'div',
35
+ { class: 'ca-counter__item' },
36
+ h('dt', { class: 'ca-counter__label' }, stat.label),
37
+ h('dd', { class: 'ca-counter__value' }, stat.value),
38
+ ),
39
+ ),
33
40
  ),
34
- h('dl', { class: 'cg-stat-counters__items' }, block.stats.map(renderItem)),
35
41
  )
36
42
  }
@@ -1,45 +1,53 @@
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'
1
+ import type { StatsBlock } from '@cogenta/blocks'
2
+ import { type HtmlElement, h, type RenderContext } from '@cogenta/theme-kit'
3
+ import { isBreakdown } from '../figures.js'
4
+ import { section, sectionHead } from '../layout.js'
5
+ import { renderBreakdown } from './breakdown.js'
9
6
 
10
7
  /**
11
- * The impact band — "12,400 meals served", "380 volunteers" — rendered as a
12
- * description list so each big figure stays the description of its label
13
- * (WCAG 1.3.2). Reading order is label then figure, exactly as the markup
14
- * says; the stylesheet paints the figure first with `column-reverse` (see
15
- * `blocks.css`'s own comment on `.cg-impact-stat`) — the paint order changes,
16
- * the DOM order a screen reader follows does not.
8
+ * What a year of work added up to, with the sentence that makes each number
9
+ * mean something: the title on the first four columns, and the figures on the
10
+ * last eight in two columns, each under a rule. The number in the display
11
+ * face, narrow and large, in the organisation's green with lining tabular
12
+ * figures and its unit a step smaller; under it the sentence of context in
13
+ * the text face. No box, no tint, nothing counts up.
14
+ *
15
+ * Figures that are all percentages of one whole are drawn as a breakdown
16
+ * instead (`renderBreakdown`).
17
17
  */
18
- function renderItem(item: StatItem): HtmlElement {
19
- return h(
20
- 'div',
21
- { class: 'cg-impact-stat' },
22
- h('dt', { class: 'cg-impact-stat__label' }, item.label),
23
- h(
24
- 'dd',
25
- { class: 'cg-impact-stat__value' },
26
- item.value,
27
- item.unit === undefined ? null : h('span', { class: 'cg-impact-stat__unit' }, item.unit),
28
- ),
29
- )
30
- }
31
-
32
- export function renderStats(block: StatsBlock, _ctx: RenderContext): HtmlElement {
33
- return h(
18
+ export function renderStats(block: StatsBlock, ctx: RenderContext): HtmlElement {
19
+ const breakdown = isBreakdown(block.items)
20
+ return section(
34
21
  'section',
35
- { class: 'cg-block cg-impact', 'data-block': 'stats' },
36
- block.title === undefined
37
- ? null
38
- : heading(
39
- blockHeadingTag('stats') ?? 'h2',
40
- { class: 'cg-impact__title', 'data-field': 'title' },
41
- block.title,
22
+ 'stats',
23
+ 'ca-figures',
24
+ {
25
+ 'data-shape': breakdown ? 'breakdown' : 'figures',
26
+ 'data-count': String(Math.min(block.items.length, 4)),
27
+ 'data-titled': String(block.title !== undefined),
28
+ },
29
+ 'div',
30
+ sectionHead('stats', block.title),
31
+ breakdown
32
+ ? renderBreakdown(block.items, ctx)
33
+ : h(
34
+ 'dl',
35
+ { class: 'ca-figures__items' },
36
+ block.items.map((item) =>
37
+ h(
38
+ 'div',
39
+ { class: 'ca-figures__item' },
40
+ h('dt', { class: 'ca-figures__label' }, item.label),
41
+ h(
42
+ 'dd',
43
+ { class: 'ca-figures__value' },
44
+ item.value,
45
+ item.unit === undefined
46
+ ? null
47
+ : h('span', { class: 'ca-figures__unit' }, item.unit),
48
+ ),
49
+ ),
50
+ ),
42
51
  ),
43
- h('dl', { class: 'cg-impact__items' }, block.items.map(renderItem)),
44
52
  )
45
53
  }