@cogenta/theme-entreprise 1.1.3 → 1.4.0

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 (115) hide show
  1. package/dist/src/index.d.ts +1 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +1 -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 +12 -15
  7. package/dist/src/render/blocks/accordion.js.map +1 -1
  8. package/dist/src/render/blocks/collection-list.d.ts +5 -7
  9. package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
  10. package/dist/src/render/blocks/collection-list.js +61 -32
  11. package/dist/src/render/blocks/collection-list.js.map +1 -1
  12. package/dist/src/render/blocks/cta.d.ts +6 -6
  13. package/dist/src/render/blocks/cta.d.ts.map +1 -1
  14. package/dist/src/render/blocks/cta.js +12 -12
  15. package/dist/src/render/blocks/cta.js.map +1 -1
  16. package/dist/src/render/blocks/embed.d.ts.map +1 -1
  17. package/dist/src/render/blocks/embed.js +22 -27
  18. package/dist/src/render/blocks/embed.js.map +1 -1
  19. package/dist/src/render/blocks/faq.d.ts.map +1 -1
  20. package/dist/src/render/blocks/faq.js +13 -16
  21. package/dist/src/render/blocks/faq.js.map +1 -1
  22. package/dist/src/render/blocks/feature-grid.d.ts.map +1 -1
  23. package/dist/src/render/blocks/feature-grid.js +22 -22
  24. package/dist/src/render/blocks/feature-grid.js.map +1 -1
  25. package/dist/src/render/blocks/gallery.d.ts +7 -8
  26. package/dist/src/render/blocks/gallery.d.ts.map +1 -1
  27. package/dist/src/render/blocks/gallery.js +16 -14
  28. package/dist/src/render/blocks/gallery.js.map +1 -1
  29. package/dist/src/render/blocks/hero.d.ts +13 -8
  30. package/dist/src/render/blocks/hero.d.ts.map +1 -1
  31. package/dist/src/render/blocks/hero.js +27 -17
  32. package/dist/src/render/blocks/hero.js.map +1 -1
  33. package/dist/src/render/blocks/logo-strip.d.ts.map +1 -1
  34. package/dist/src/render/blocks/logo-strip.js +14 -11
  35. package/dist/src/render/blocks/logo-strip.js.map +1 -1
  36. package/dist/src/render/blocks/logos.d.ts.map +1 -1
  37. package/dist/src/render/blocks/logos.js +21 -12
  38. package/dist/src/render/blocks/logos.js.map +1 -1
  39. package/dist/src/render/blocks/media-figure.d.ts +10 -6
  40. package/dist/src/render/blocks/media-figure.d.ts.map +1 -1
  41. package/dist/src/render/blocks/media-figure.js +18 -13
  42. package/dist/src/render/blocks/media-figure.js.map +1 -1
  43. package/dist/src/render/blocks/pricing-table.d.ts.map +1 -1
  44. package/dist/src/render/blocks/pricing-table.js +15 -11
  45. package/dist/src/render/blocks/pricing-table.js.map +1 -1
  46. package/dist/src/render/blocks/prose.d.ts +9 -3
  47. package/dist/src/render/blocks/prose.d.ts.map +1 -1
  48. package/dist/src/render/blocks/prose.js +11 -4
  49. package/dist/src/render/blocks/prose.js.map +1 -1
  50. package/dist/src/render/blocks/quote.d.ts +10 -10
  51. package/dist/src/render/blocks/quote.d.ts.map +1 -1
  52. package/dist/src/render/blocks/quote.js +14 -13
  53. package/dist/src/render/blocks/quote.js.map +1 -1
  54. package/dist/src/render/blocks/stat-counter.d.ts.map +1 -1
  55. package/dist/src/render/blocks/stat-counter.js +11 -13
  56. package/dist/src/render/blocks/stat-counter.js.map +1 -1
  57. package/dist/src/render/blocks/stats.d.ts.map +1 -1
  58. package/dist/src/render/blocks/stats.js +12 -13
  59. package/dist/src/render/blocks/stats.js.map +1 -1
  60. package/dist/src/render/blocks/testimonial.d.ts +9 -9
  61. package/dist/src/render/blocks/testimonial.d.ts.map +1 -1
  62. package/dist/src/render/blocks/testimonial.js +15 -14
  63. package/dist/src/render/blocks/testimonial.js.map +1 -1
  64. package/dist/src/render/chrome.d.ts +3 -1
  65. package/dist/src/render/chrome.d.ts.map +1 -1
  66. package/dist/src/render/chrome.js +71 -46
  67. package/dist/src/render/chrome.js.map +1 -1
  68. package/dist/src/render/layout.d.ts +26 -0
  69. package/dist/src/render/layout.d.ts.map +1 -0
  70. package/dist/src/render/layout.js +51 -0
  71. package/dist/src/render/layout.js.map +1 -0
  72. package/dist/src/render/render-block.d.ts.map +1 -1
  73. package/dist/src/render/render-block.js +23 -2
  74. package/dist/src/render/render-block.js.map +1 -1
  75. package/dist/src/render/term-archive.d.ts +8 -6
  76. package/dist/src/render/term-archive.d.ts.map +1 -1
  77. package/dist/src/render/term-archive.js +22 -16
  78. package/dist/src/render/term-archive.js.map +1 -1
  79. package/dist/src/render/widget-areas.d.ts +13 -0
  80. package/dist/src/render/widget-areas.d.ts.map +1 -0
  81. package/dist/src/render/widget-areas.js +20 -0
  82. package/dist/src/render/widget-areas.js.map +1 -0
  83. package/dist/theme.config.js +2 -2
  84. package/dist/theme.config.js.map +1 -1
  85. package/package.json +5 -5
  86. package/src/index.ts +1 -0
  87. package/src/render/blocks/accordion.ts +32 -27
  88. package/src/render/blocks/collection-list.ts +103 -53
  89. package/src/render/blocks/cta.ts +27 -28
  90. package/src/render/blocks/embed.ts +46 -43
  91. package/src/render/blocks/faq.ts +25 -21
  92. package/src/render/blocks/feature-grid.ts +42 -35
  93. package/src/render/blocks/gallery.ts +21 -16
  94. package/src/render/blocks/hero.ts +40 -28
  95. package/src/render/blocks/logo-strip.ts +26 -16
  96. package/src/render/blocks/logos.ts +33 -17
  97. package/src/render/blocks/media-figure.ts +22 -14
  98. package/src/render/blocks/pricing-table.ts +26 -15
  99. package/src/render/blocks/prose.ts +18 -4
  100. package/src/render/blocks/quote.ts +18 -14
  101. package/src/render/blocks/stat-counter.ts +25 -20
  102. package/src/render/blocks/stats.ts +31 -21
  103. package/src/render/blocks/testimonial.ts +22 -18
  104. package/src/render/chrome.ts +84 -49
  105. package/src/render/layout.ts +67 -0
  106. package/src/render/render-block.ts +24 -1
  107. package/src/render/term-archive.ts +54 -50
  108. package/src/render/widget-areas.ts +23 -0
  109. package/src/styles/archive.css +137 -38
  110. package/src/styles/base.css +553 -426
  111. package/src/styles/blocks.css +1343 -1010
  112. package/src/styles/theme.css +18 -24
  113. package/src/styles/tokens.css +166 -192
  114. package/src/styles/widgets.css +914 -0
  115. package/tokens.json +17 -17
@@ -7,25 +7,24 @@ import {
7
7
  image,
8
8
  type RenderContext,
9
9
  } from '@cogenta/theme-kit'
10
+ import { section } from '../layout.js'
10
11
 
11
12
  /**
12
- * A "trusted by" client strip. Contract B says the organisation's name **is**
13
- * the accessible name of the link, so the logo image carries it as `alt`
14
- * text when the media entity has none, and no visually hidden duplicate is
15
- * added: a link whose only content is an image with `alt` text is already
16
- * named.
13
+ * A client register: wordmarks in greyscale, each in its own cell of a ruled
14
+ * table, so a dozen marks of different shapes and weights still line up.
17
15
  *
18
- * The restrained, grayscale-leaning treatment (`filter: grayscale()` in the
19
- * stylesheet) is the "held back until hovered" client-logo language the
20
- * aesthetic direction calls for — a `filter`, never `grayscale` baked into
21
- * the media itself, since the underlying asset still arrives as a real
22
- * `<img>` from `image()`.
16
+ * Contract B says the organisation's name is the accessible name of the
17
+ * link, so the image carries it as `alt` when the media entity has none, and
18
+ * no visually hidden duplicate is added. Greyscale is a `filter` in the
19
+ * stylesheet, never baked into the media: the same asset still renders in
20
+ * colour wherever a site owner uses it elsewhere.
23
21
  */
24
22
  function renderItem(item: LogoItem, ctx: RenderContext): HtmlElement {
25
23
  const logo = image(ctx, item.media, {
26
24
  className: 'cg-clients__logo',
27
25
  altFrom: item.name,
28
26
  variant: { fit: 'contain' },
27
+ sizes: '12rem',
29
28
  })
30
29
  return h(
31
30
  'li',
@@ -41,20 +40,37 @@ function renderItem(item: LogoItem, ctx: RenderContext): HtmlElement {
41
40
  }
42
41
 
43
42
  export function renderLogos(block: LogosBlock, ctx: RenderContext): HtmlElement {
44
- return h(
43
+ return section(
45
44
  'section',
46
- { class: 'cg-clients', 'data-block': 'logos' },
45
+ 'logos',
46
+ 'cg-clients',
47
+ {},
48
+ 'div',
47
49
  block.title === undefined
48
50
  ? null
49
- : heading(
50
- blockHeadingTag('logos') ?? 'h2',
51
- { class: 'cg-clients__title', 'data-field': 'title' },
52
- block.title,
51
+ : h(
52
+ 'div',
53
+ { class: 'cg-head' },
54
+ heading(
55
+ blockHeadingTag('logos') ?? 'h2',
56
+ { class: 'cg-head__title', 'data-field': 'title' },
57
+ block.title,
58
+ ),
53
59
  ),
54
60
  h(
55
61
  'ul',
56
- { class: 'cg-clients__items' },
62
+ { class: 'cg-clients__items', 'data-columns': String(columnsFor(block.items.length)) },
57
63
  block.items.map((item) => renderItem(item, ctx)),
58
64
  ),
59
65
  )
60
66
  }
67
+
68
+ /**
69
+ * How many columns the register uses on a wide screen: four when the marks
70
+ * fill whole rows of four, otherwise three, so a register never ends on a
71
+ * row of empty ruled cells when a whole-row layout exists.
72
+ */
73
+ function columnsFor(count: number): number {
74
+ if (count <= 3) return Math.max(count, 1)
75
+ return count % 4 === 0 ? 4 : 3
76
+ }
@@ -1,39 +1,47 @@
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>`, matching the pattern the HTML spec prescribes for
6
- * an image and its own caption — a `<figcaption>` outside the `<figure>` would
7
- * lose that association for assistive technology.
6
+ * An image and its caption, placed on the grid by `align`.
8
7
  *
9
- * The frame reads as a bordered plate with a small caption rule underneath,
10
- * distinct from a floated/rounded treatment: the "structured, edged"
11
- * language this theme uses everywhere a picture appears.
8
+ * `wide` gives the image nine columns and sets the caption in the three to
9
+ * its right, under a hairline, aligned to the image's foot, the way an
10
+ * annual report places an exhibit and its note. `start` and `end` do the
11
+ * same on a narrower image; `center` keeps the caption under the image;
12
+ * `full` lets the image run the full width of the page. No frame, no shadow,
13
+ * no rounded corner: the picture is the object.
14
+ *
15
+ * `<figure>`/`<figcaption>` keep the caption associated with the image for
16
+ * assistive technology, which is why the figure is the grid container itself.
12
17
  */
13
18
  export function renderMediaFigure(block: MediaFigureBlock, ctx: RenderContext): HtmlElement {
14
19
  const ratio = aspectRatio(block.ratio)
15
20
  const hasCaption = block.caption !== undefined || block.credit !== undefined
16
- return h(
17
- 'figure',
21
+ return section(
22
+ 'div',
23
+ 'mediaFigure',
24
+ 'cg-figure',
18
25
  {
19
- class: 'cg-figure',
20
- 'data-block': 'mediaFigure',
21
26
  'data-align': block.align ?? 'center',
22
27
  style: ratio === undefined ? undefined : `--cg-ratio:${ratio}`,
23
28
  },
29
+ 'figure',
24
30
  h(
25
31
  'div',
26
- { class: 'cg-figure__frame' },
32
+ { class: 'cg-figure__media' },
27
33
  image(ctx, block.media, {
28
- className: 'cg-figure__media',
29
- sizes: '(min-width: 48rem) 42rem, 100vw',
34
+ className: 'cg-figure__image',
35
+ sizes: block.align === 'full' ? '100vw' : '(min-width: 64rem) 75vw, 100vw',
30
36
  }),
31
37
  ),
32
38
  hasCaption
33
39
  ? h(
34
40
  'figcaption',
35
41
  { class: 'cg-figure__caption' },
36
- block.caption,
42
+ block.caption === undefined
43
+ ? null
44
+ : h('span', { class: 'cg-figure__text', 'data-field': 'caption' }, block.caption),
37
45
  block.credit === undefined
38
46
  ? null
39
47
  : h('span', { class: 'cg-figure__credit', 'data-field': 'credit' }, block.credit),
@@ -9,17 +9,18 @@ import {
9
9
  nestedHeadingTag,
10
10
  type RenderContext,
11
11
  } from '@cogenta/theme-kit'
12
+ import { section } from '../layout.js'
12
13
 
13
14
  /**
14
- * `blocks@2.0` (RFC 0001). A side-by-side plan comparison — the ledger-row
15
- * language this theme uses for `featureGrid` would flatten the very
16
- * distinction a pricing table exists to show, so tiers instead sit as a row
17
- * of bordered plates, one plate lifted (border + a small "recommended" rule)
18
- * when `highlighted` is set.
15
+ * `blocks@2.0` (RFC 0001). A comparison set as a ruled table rather than a
16
+ * row of cards: tiers stand side by side between vertical hairlines, each
17
+ * with its name in small capitals, the price in the display serif with
18
+ * tabular numerals, the inclusions as a ruled list and the action at the
19
+ * foot, aligned across tiers.
19
20
  *
20
21
  * `highlighted` is an editorial signal, never a colour: it becomes
21
- * `data-highlighted` for the stylesheet to key off, and `aria-current` so the
22
- * emphasis is announced, not only shown.
22
+ * `data-highlighted` (a heavier rule above that tier, in ink) and
23
+ * `aria-current`, so the emphasis is announced as well as shown.
23
24
  */
24
25
  function renderTier(tier: PricingTier, ctx: RenderContext, tag: HeadingTag): HtmlElement {
25
26
  return h(
@@ -50,7 +51,10 @@ function renderTier(tier: PricingTier, ctx: RenderContext, tag: HeadingTag): Htm
50
51
  : h(
51
52
  'div',
52
53
  { class: 'cg-pricing__action' },
53
- actionLink(ctx, { ...tier.action, emphasis: tier.action.emphasis ?? 'primary' }),
54
+ actionLink(ctx, {
55
+ ...tier.action,
56
+ emphasis: tier.action.emphasis ?? (tier.highlighted === true ? 'primary' : 'secondary'),
57
+ }),
54
58
  ),
55
59
  )
56
60
  }
@@ -58,19 +62,26 @@ function renderTier(tier: PricingTier, ctx: RenderContext, tag: HeadingTag): Htm
58
62
  export function renderPricingTable(block: PricingTableBlock, ctx: RenderContext): HtmlElement {
59
63
  const hasTitle = block.title !== undefined
60
64
  const tierTag = nestedHeadingTag('pricingTable', hasTitle)
61
- return h(
65
+ return section(
62
66
  'section',
63
- { class: 'cg-pricing', 'data-block': 'pricingTable' },
67
+ 'pricingTable',
68
+ 'cg-pricing',
69
+ {},
70
+ 'div',
64
71
  hasTitle
65
- ? heading(
66
- blockHeadingTag('pricingTable') ?? 'h2',
67
- { class: 'cg-pricing__title', 'data-field': 'title' },
68
- block.title ?? '',
72
+ ? h(
73
+ 'div',
74
+ { class: 'cg-head' },
75
+ heading(
76
+ blockHeadingTag('pricingTable') ?? 'h2',
77
+ { class: 'cg-head__title', 'data-field': 'title' },
78
+ block.title ?? '',
79
+ ),
69
80
  )
70
81
  : null,
71
82
  h(
72
83
  'ul',
73
- { class: 'cg-pricing__tiers' },
84
+ { class: 'cg-pricing__tiers', 'data-count': String(Math.min(block.tiers.length, 4)) },
74
85
  block.tiers.map((tier) => renderTier(tier, ctx, tierTag)),
75
86
  ),
76
87
  )
@@ -1,11 +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
+ * A reading column set on the right-hand two thirds of the grid.
7
+ *
8
+ * On a wide screen the text starts at the fifth column and holds a 66ch
9
+ * measure; a second-level heading inside the document hangs out into the
10
+ * four columns to its left (`blocks.css`), so a long page reads as a report,
11
+ * with its section titles in the margin, rather than as one centred column.
12
+ *
13
+ * `prose` declares `headingLevel: 'none'`: whatever headings appear come
14
+ * from the rich text document, whose vocabulary starts at `h2`.
8
15
  */
9
16
  export function renderProse(block: ProseBlock, ctx: RenderContext): HtmlElement {
10
- return h('div', { class: 'cg-prose', 'data-block': 'prose' }, renderRichText(ctx, block.body))
17
+ return section(
18
+ 'div',
19
+ 'prose',
20
+ 'cg-text',
21
+ {},
22
+ 'div',
23
+ h('div', { class: 'cg-prose' }, renderRichText(ctx, block.body)),
24
+ )
11
25
  }
@@ -1,35 +1,39 @@
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
- * A testimonial card, not a centred pull-quote: a bordered panel with a
6
- * vertical accent rule beside the quotation and the attribution set below a
7
- * thin divider — the shape a case-study or client-testimonial section takes
8
- * on a serious B2B site.
6
+ * An editorial quotation inside the reading column: the display serif in
7
+ * italic, under a hairline, with the attribution set small beneath it.
9
8
  *
10
- * `<figure><blockquote>…</blockquote><figcaption>` is the attribution
11
- * pattern the HTML spec prescribes: putting the author inside the
12
- * `<blockquote>` would claim the author's name was part of what was said.
9
+ * Quieter than `testimonial`, which is this theme's large pull quote: a
10
+ * `quote` belongs to the flow of a page, aligned with the prose column
11
+ * rather than spread across the grid, and never in a shaded box.
13
12
  *
14
- * The avatar is decorative here — the name sits right beside it in text —
15
- * so its media entity's alt text is expected to be empty; `image` still
16
- * writes the attribute either way (WCAG 1.1.1).
13
+ * `<figure><blockquote>…</blockquote><figcaption>` is the attribution pattern
14
+ * the HTML spec prescribes: putting the name inside the `<blockquote>` would
15
+ * claim it was part of what was said. The portrait is decorative (the name
16
+ * is right beside it), so its media entity's alt text is expected to be
17
+ * empty; `image` writes the attribute either way (WCAG 1.1.1).
17
18
  */
18
19
  export function renderQuote(block: QuoteBlock, ctx: RenderContext): HtmlElement {
19
20
  const hasAttribution =
20
21
  block.author !== undefined || block.role !== undefined || block.avatar !== undefined
21
- return h(
22
+ return section(
23
+ 'div',
24
+ 'quote',
25
+ 'cg-quote',
26
+ {},
22
27
  'figure',
23
- { class: 'cg-quote', 'data-block': 'quote' },
24
28
  h('blockquote', { class: 'cg-quote__text' }, h('p', { 'data-field': 'text' }, block.text)),
25
29
  hasAttribution
26
30
  ? h(
27
31
  'figcaption',
28
- { class: 'cg-quote__attribution' },
32
+ { class: 'cg-quote__cite' },
29
33
  block.avatar === undefined
30
34
  ? null
31
35
  : image(ctx, block.avatar, {
32
- className: 'cg-quote__avatar',
36
+ className: 'cg-quote__portrait',
33
37
  variant: { width: 96, height: 96, fit: 'cover' },
34
38
  }),
35
39
  h(
@@ -6,40 +6,45 @@ import {
6
6
  heading,
7
7
  type RenderContext,
8
8
  } from '@cogenta/theme-kit'
9
+ import { section } from '../layout.js'
9
10
 
10
11
  /**
11
- * `blocks@2.0` (RFC 0001). Narrower than `stats` (no `unit`), and treated
12
- * that way here: `stats` reads as a quiet KPI strip on the page's own
13
- * surface, while `statCounter` is this theme's confident "impact numbers"
14
- * band — an accent-tinted panel of big, tabular figures, the instinct the
15
- * aesthetic direction already asks for on `stats` pushed one step further
16
- * for the narrower, single-figure shape this block offers.
12
+ * `blocks@2.0` (RFC 0001). Narrower than `stats` (no `unit`), and laid out as
13
+ * its counterpart: where `stats` is a single row between vertical rules,
14
+ * `statCounter` keeps its title in the left four columns and sets its
15
+ * figures in a two-by-two table on the right eight, each under a horizontal
16
+ * hairline, the way an annual report lists its headline numbers.
17
17
  *
18
- * A `<dl>`, exactly like `stats`: each figure is the description of its
19
- * label, which is what `<dt>`/`<dd>` mean. The label sits before the figure
20
- * in markup, the same reading-order guarantee `stats` gives (WCAG 1.3.2);
21
- * the stylesheet is free to paint the big figure first.
18
+ * A `<dl>`, with the label first in the markup for the same reading-order
19
+ * reason as `stats`.
22
20
  */
23
21
  function renderItem(item: StatCounterItem): HtmlElement {
24
22
  return h(
25
23
  'div',
26
- { class: 'cg-kpi' },
27
- h('dt', { class: 'cg-kpi__label' }, item.label),
28
- h('dd', { class: 'cg-kpi__value' }, item.value),
24
+ { class: 'cg-counter' },
25
+ h('dt', { class: 'cg-counter__label' }, item.label),
26
+ h('dd', { class: 'cg-counter__value' }, item.value),
29
27
  )
30
28
  }
31
29
 
32
30
  export function renderStatCounter(block: StatCounterBlock, _ctx: RenderContext): HtmlElement {
33
- return h(
31
+ return section(
34
32
  'section',
35
- { class: 'cg-kpis', 'data-block': 'statCounter' },
33
+ 'statCounter',
34
+ 'cg-counters',
35
+ { 'data-titled': block.title === undefined ? 'false' : 'true' },
36
+ 'div',
36
37
  block.title === undefined
37
38
  ? null
38
- : heading(
39
- blockHeadingTag('statCounter') ?? 'h2',
40
- { class: 'cg-kpis__title', 'data-field': 'title' },
41
- block.title,
39
+ : h(
40
+ 'div',
41
+ { class: 'cg-head cg-head--aside' },
42
+ heading(
43
+ blockHeadingTag('statCounter') ?? 'h2',
44
+ { class: 'cg-head__title', 'data-field': 'title' },
45
+ block.title,
46
+ ),
42
47
  ),
43
- h('dl', { class: 'cg-kpis__items' }, block.stats.map(renderItem)),
48
+ h('dl', { class: 'cg-counters__items' }, block.stats.map(renderItem)),
44
49
  )
45
50
  }
@@ -6,45 +6,55 @@ import {
6
6
  heading,
7
7
  type RenderContext,
8
8
  } from '@cogenta/theme-kit'
9
+ import { section } from '../layout.js'
9
10
 
10
11
  /**
11
- * The "by the numbers" strip. A description list, exactly as the canonical
12
- * theme uses — each figure is the description of its label, which is what
13
- * `<dt>`/`<dd>` mean, and it survives being read linearly — but laid out as
14
- * one continuous row of big, confident figures separated by vertical rules
15
- * (`border-inline-start` on every item but the first) rather than as
16
- * separate shadowed tiles: the "real KPI strip" read the aesthetic
17
- * direction asks for.
12
+ * Key figures as one typographic row between vertical rules: the figure in
13
+ * the display serif with lining, tabular numerals, its unit set smaller in
14
+ * the same face, and the label in small text beneath. No coloured band and
15
+ * no tiles.
18
16
  *
19
- * The label sits above the figure in the markup and the skin is free to
20
- * repaint the order visually; reading order and visual order stay
21
- * independent either way (WCAG 1.3.2).
17
+ * A description list, because each figure is the description of its label.
18
+ * The label comes first in the markup, so a screen reader announces what a
19
+ * number means before the number (WCAG 1.3.2); the stylesheet paints the
20
+ * figure above it.
22
21
  */
23
22
  function renderItem(item: StatItem): HtmlElement {
24
23
  return h(
25
24
  'div',
26
- { class: 'cg-metric' },
27
- h('dt', { class: 'cg-metric__label' }, item.label),
25
+ { class: 'cg-stat' },
26
+ h('dt', { class: 'cg-stat__label' }, item.label),
28
27
  h(
29
28
  'dd',
30
- { class: 'cg-metric__value' },
29
+ { class: 'cg-stat__value' },
31
30
  item.value,
32
- item.unit === undefined ? null : h('span', { class: 'cg-metric__unit' }, item.unit),
31
+ item.unit === undefined ? null : h('span', { class: 'cg-stat__unit' }, item.unit),
33
32
  ),
34
33
  )
35
34
  }
36
35
 
37
36
  export function renderStats(block: StatsBlock, _ctx: RenderContext): HtmlElement {
38
- return h(
37
+ return section(
39
38
  'section',
40
- { class: 'cg-metrics', 'data-block': 'stats' },
39
+ 'stats',
40
+ 'cg-stats',
41
+ {},
42
+ 'div',
41
43
  block.title === undefined
42
44
  ? null
43
- : heading(
44
- blockHeadingTag('stats') ?? 'h2',
45
- { class: 'cg-metrics__title', 'data-field': 'title' },
46
- block.title,
45
+ : h(
46
+ 'div',
47
+ { class: 'cg-head' },
48
+ heading(
49
+ blockHeadingTag('stats') ?? 'h2',
50
+ { class: 'cg-head__title', 'data-field': 'title' },
51
+ block.title,
52
+ ),
47
53
  ),
48
- h('dl', { class: 'cg-metrics__items' }, block.items.map(renderItem)),
54
+ h(
55
+ 'dl',
56
+ { class: 'cg-stats__items', 'data-count': String(Math.min(block.items.length, 5)) },
57
+ block.items.map(renderItem),
58
+ ),
49
59
  )
50
60
  }
@@ -1,40 +1,44 @@
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). This theme's "client success" panel — the same
6
- * `<figure><blockquote>…</blockquote><figcaption>` pattern `quote` uses, kept
7
- * consistent so the two read as siblings, but the quote itself is rich text
8
- * (a testimonial may carry a link or emphasis the shorter, plain-text `quote`
9
- * block cannot) and the attribution is the block's single grouped
10
- * `attribution` field rather than three loose ones.
6
+ * `blocks@2.0` (RFC 0001). The large pull quote: one client's words set in
7
+ * the display serif across ten columns, with typographic quotation marks
8
+ * hung in the margin, and a small square portrait beside the name.
11
9
  *
12
- * The avatar is decorative, exactly like `quote`'s own: the name sits right
13
- * beside it in text, so its media entity's alt text is expected to be empty;
14
- * `image` still writes the attribute either way (WCAG 1.1.1).
10
+ * No tinted panel, no border on one side, no italic box: the size of the
11
+ * type and the space around it are the emphasis. The quote is rich text, so
12
+ * a testimonial may carry a link or emphasis the plain-text `quote` cannot.
13
+ *
14
+ * The portrait is decorative (the name is beside it in text), so its alt
15
+ * text is expected to be empty; `image` writes the attribute either way.
15
16
  */
16
17
  export function renderTestimonial(block: TestimonialBlock, ctx: RenderContext): HtmlElement {
17
18
  const { attribution } = block
18
- return h(
19
+ return section(
20
+ 'div',
21
+ 'testimonial',
22
+ 'cg-pullquote',
23
+ {},
19
24
  'figure',
20
- { class: 'cg-testimonial', 'data-block': 'testimonial' },
21
- h('blockquote', { class: 'cg-testimonial__quote' }, renderRichText(ctx, block.quote)),
25
+ h('blockquote', { class: 'cg-pullquote__quote' }, renderRichText(ctx, block.quote)),
22
26
  h(
23
27
  'figcaption',
24
- { class: 'cg-testimonial__attribution' },
28
+ { class: 'cg-pullquote__cite' },
25
29
  attribution.avatar === undefined
26
30
  ? null
27
31
  : image(ctx, attribution.avatar, {
28
- className: 'cg-testimonial__avatar',
29
- variant: { width: 96, height: 96, fit: 'cover' },
32
+ className: 'cg-pullquote__portrait',
33
+ variant: { width: 144, height: 144, fit: 'cover' },
30
34
  }),
31
35
  h(
32
36
  'span',
33
- { class: 'cg-testimonial__who' },
34
- h('span', { class: 'cg-testimonial__name' }, attribution.name),
37
+ { class: 'cg-pullquote__who' },
38
+ h('span', { class: 'cg-pullquote__name' }, attribution.name),
35
39
  attribution.role === undefined
36
40
  ? null
37
- : h('span', { class: 'cg-testimonial__role' }, attribution.role),
41
+ : h('span', { class: 'cg-pullquote__role' }, attribution.role),
38
42
  ),
39
43
  ),
40
44
  )