@cogenta/theme-entreprise 1.1.2

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 (117) hide show
  1. package/LICENSE +373 -0
  2. package/dist/src/index.d.ts +16 -0
  3. package/dist/src/index.d.ts.map +1 -0
  4. package/dist/src/index.js +16 -0
  5. package/dist/src/index.js.map +1 -0
  6. package/dist/src/render/blocks/accordion.d.ts +4 -0
  7. package/dist/src/render/blocks/accordion.d.ts.map +1 -0
  8. package/dist/src/render/blocks/accordion.js +24 -0
  9. package/dist/src/render/blocks/accordion.js.map +1 -0
  10. package/dist/src/render/blocks/collection-list.d.ts +14 -0
  11. package/dist/src/render/blocks/collection-list.d.ts.map +1 -0
  12. package/dist/src/render/blocks/collection-list.js +55 -0
  13. package/dist/src/render/blocks/collection-list.js.map +1 -0
  14. package/dist/src/render/blocks/cta.d.ts +12 -0
  15. package/dist/src/render/blocks/cta.d.ts.map +1 -0
  16. package/dist/src/render/blocks/cta.js +19 -0
  17. package/dist/src/render/blocks/cta.js.map +1 -0
  18. package/dist/src/render/blocks/embed.d.ts +4 -0
  19. package/dist/src/render/blocks/embed.d.ts.map +1 -0
  20. package/dist/src/render/blocks/embed.js +92 -0
  21. package/dist/src/render/blocks/embed.js.map +1 -0
  22. package/dist/src/render/blocks/faq.d.ts +4 -0
  23. package/dist/src/render/blocks/faq.d.ts.map +1 -0
  24. package/dist/src/render/blocks/faq.js +25 -0
  25. package/dist/src/render/blocks/faq.js.map +1 -0
  26. package/dist/src/render/blocks/feature-grid.d.ts +4 -0
  27. package/dist/src/render/blocks/feature-grid.d.ts.map +1 -0
  28. package/dist/src/render/blocks/feature-grid.js +33 -0
  29. package/dist/src/render/blocks/feature-grid.js.map +1 -0
  30. package/dist/src/render/blocks/gallery.d.ts +15 -0
  31. package/dist/src/render/blocks/gallery.d.ts.map +1 -0
  32. package/dist/src/render/blocks/gallery.js +28 -0
  33. package/dist/src/render/blocks/gallery.js.map +1 -0
  34. package/dist/src/render/blocks/hero.d.ts +15 -0
  35. package/dist/src/render/blocks/hero.d.ts.map +1 -0
  36. package/dist/src/render/blocks/hero.js +31 -0
  37. package/dist/src/render/blocks/hero.js.map +1 -0
  38. package/dist/src/render/blocks/logo-strip.d.ts +4 -0
  39. package/dist/src/render/blocks/logo-strip.d.ts.map +1 -0
  40. package/dist/src/render/blocks/logo-strip.js +21 -0
  41. package/dist/src/render/blocks/logo-strip.js.map +1 -0
  42. package/dist/src/render/blocks/logos.d.ts +4 -0
  43. package/dist/src/render/blocks/logos.d.ts.map +1 -0
  44. package/dist/src/render/blocks/logos.js +30 -0
  45. package/dist/src/render/blocks/logos.js.map +1 -0
  46. package/dist/src/render/blocks/media-figure.d.ts +13 -0
  47. package/dist/src/render/blocks/media-figure.d.ts.map +1 -0
  48. package/dist/src/render/blocks/media-figure.js +28 -0
  49. package/dist/src/render/blocks/media-figure.js.map +1 -0
  50. package/dist/src/render/blocks/pricing-table.d.ts +4 -0
  51. package/dist/src/render/blocks/pricing-table.d.ts.map +1 -0
  52. package/dist/src/render/blocks/pricing-table.js +33 -0
  53. package/dist/src/render/blocks/pricing-table.js.map +1 -0
  54. package/dist/src/render/blocks/prose.d.ts +9 -0
  55. package/dist/src/render/blocks/prose.d.ts.map +1 -0
  56. package/dist/src/render/blocks/prose.js +10 -0
  57. package/dist/src/render/blocks/prose.js.map +1 -0
  58. package/dist/src/render/blocks/quote.d.ts +18 -0
  59. package/dist/src/render/blocks/quote.d.ts.map +1 -0
  60. package/dist/src/render/blocks/quote.js +31 -0
  61. package/dist/src/render/blocks/quote.js.map +1 -0
  62. package/dist/src/render/blocks/stat-counter.d.ts +4 -0
  63. package/dist/src/render/blocks/stat-counter.d.ts.map +1 -0
  64. package/dist/src/render/blocks/stat-counter.js +23 -0
  65. package/dist/src/render/blocks/stat-counter.js.map +1 -0
  66. package/dist/src/render/blocks/stats.d.ts +4 -0
  67. package/dist/src/render/blocks/stats.d.ts.map +1 -0
  68. package/dist/src/render/blocks/stats.js +23 -0
  69. package/dist/src/render/blocks/stats.js.map +1 -0
  70. package/dist/src/render/blocks/testimonial.d.ts +16 -0
  71. package/dist/src/render/blocks/testimonial.d.ts.map +1 -0
  72. package/dist/src/render/blocks/testimonial.js +25 -0
  73. package/dist/src/render/blocks/testimonial.js.map +1 -0
  74. package/dist/src/render/chrome.d.ts +3 -0
  75. package/dist/src/render/chrome.d.ts.map +1 -0
  76. package/dist/src/render/chrome.js +101 -0
  77. package/dist/src/render/chrome.js.map +1 -0
  78. package/dist/src/render/render-block.d.ts +24 -0
  79. package/dist/src/render/render-block.d.ts.map +1 -0
  80. package/dist/src/render/render-block.js +108 -0
  81. package/dist/src/render/render-block.js.map +1 -0
  82. package/dist/src/render/term-archive.d.ts +12 -0
  83. package/dist/src/render/term-archive.d.ts.map +1 -0
  84. package/dist/src/render/term-archive.js +34 -0
  85. package/dist/src/render/term-archive.js.map +1 -0
  86. package/dist/theme.config.d.ts +16 -0
  87. package/dist/theme.config.d.ts.map +1 -0
  88. package/dist/theme.config.js +46 -0
  89. package/dist/theme.config.js.map +1 -0
  90. package/package.json +57 -0
  91. package/src/index.ts +15 -0
  92. package/src/render/blocks/accordion.ts +60 -0
  93. package/src/render/blocks/collection-list.ts +122 -0
  94. package/src/render/blocks/cta.ts +48 -0
  95. package/src/render/blocks/embed.ts +115 -0
  96. package/src/render/blocks/faq.ts +61 -0
  97. package/src/render/blocks/feature-grid.ts +73 -0
  98. package/src/render/blocks/gallery.ts +48 -0
  99. package/src/render/blocks/hero.ts +60 -0
  100. package/src/render/blocks/logo-strip.ts +36 -0
  101. package/src/render/blocks/logos.ts +60 -0
  102. package/src/render/blocks/media-figure.ts +43 -0
  103. package/src/render/blocks/pricing-table.ts +77 -0
  104. package/src/render/blocks/prose.ts +11 -0
  105. package/src/render/blocks/quote.ts +48 -0
  106. package/src/render/blocks/stat-counter.ts +45 -0
  107. package/src/render/blocks/stats.ts +50 -0
  108. package/src/render/blocks/testimonial.ts +41 -0
  109. package/src/render/chrome.ts +128 -0
  110. package/src/render/render-block.ts +144 -0
  111. package/src/render/term-archive.ts +80 -0
  112. package/src/styles/archive.css +68 -0
  113. package/src/styles/base.css +895 -0
  114. package/src/styles/blocks.css +1433 -0
  115. package/src/styles/theme.css +36 -0
  116. package/src/styles/tokens.css +326 -0
  117. package/tokens.json +36 -0
@@ -0,0 +1,43 @@
1
+ import type { MediaFigureBlock } from '@cogenta/blocks'
2
+ import { aspectRatio, type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+
4
+ /**
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.
8
+ *
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.
12
+ */
13
+ export function renderMediaFigure(block: MediaFigureBlock, ctx: RenderContext): HtmlElement {
14
+ const ratio = aspectRatio(block.ratio)
15
+ const hasCaption = block.caption !== undefined || block.credit !== undefined
16
+ return h(
17
+ 'figure',
18
+ {
19
+ class: 'cg-figure',
20
+ 'data-block': 'mediaFigure',
21
+ 'data-align': block.align ?? 'center',
22
+ style: ratio === undefined ? undefined : `--cg-ratio:${ratio}`,
23
+ },
24
+ h(
25
+ 'div',
26
+ { class: 'cg-figure__frame' },
27
+ image(ctx, block.media, {
28
+ className: 'cg-figure__media',
29
+ sizes: '(min-width: 48rem) 42rem, 100vw',
30
+ }),
31
+ ),
32
+ hasCaption
33
+ ? h(
34
+ 'figcaption',
35
+ { class: 'cg-figure__caption' },
36
+ block.caption,
37
+ block.credit === undefined
38
+ ? null
39
+ : h('span', { class: 'cg-figure__credit', 'data-field': 'credit' }, block.credit),
40
+ )
41
+ : null,
42
+ )
43
+ }
@@ -0,0 +1,77 @@
1
+ import type { PricingTableBlock, PricingTier } from '@cogenta/blocks'
2
+ import {
3
+ actionLink,
4
+ blockHeadingTag,
5
+ type HeadingTag,
6
+ type HtmlElement,
7
+ h,
8
+ heading,
9
+ nestedHeadingTag,
10
+ type RenderContext,
11
+ } from '@cogenta/theme-kit'
12
+
13
+ /**
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.
19
+ *
20
+ * `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.
23
+ */
24
+ function renderTier(tier: PricingTier, ctx: RenderContext, tag: HeadingTag): HtmlElement {
25
+ return h(
26
+ 'li',
27
+ {
28
+ class: 'cg-pricing__tier',
29
+ 'data-highlighted': tier.highlighted === true ? 'true' : undefined,
30
+ 'aria-current': tier.highlighted === true ? 'true' : undefined,
31
+ },
32
+ heading(tag, { class: 'cg-pricing__name' }, tier.name),
33
+ h(
34
+ 'p',
35
+ { class: 'cg-pricing__price' },
36
+ h('span', { class: 'cg-pricing__amount' }, tier.price),
37
+ tier.interval === undefined
38
+ ? null
39
+ : h('span', { class: 'cg-pricing__interval' }, tier.interval),
40
+ ),
41
+ tier.features.length === 0
42
+ ? null
43
+ : h(
44
+ 'ul',
45
+ { class: 'cg-pricing__features' },
46
+ tier.features.map((feature) => h('li', { class: 'cg-pricing__feature' }, feature)),
47
+ ),
48
+ tier.action === undefined
49
+ ? null
50
+ : h(
51
+ 'div',
52
+ { class: 'cg-pricing__action' },
53
+ actionLink(ctx, { ...tier.action, emphasis: tier.action.emphasis ?? 'primary' }),
54
+ ),
55
+ )
56
+ }
57
+
58
+ export function renderPricingTable(block: PricingTableBlock, ctx: RenderContext): HtmlElement {
59
+ const hasTitle = block.title !== undefined
60
+ const tierTag = nestedHeadingTag('pricingTable', hasTitle)
61
+ return h(
62
+ 'section',
63
+ { class: 'cg-pricing', 'data-block': 'pricingTable' },
64
+ hasTitle
65
+ ? heading(
66
+ blockHeadingTag('pricingTable') ?? 'h2',
67
+ { class: 'cg-pricing__title', 'data-field': 'title' },
68
+ block.title ?? '',
69
+ )
70
+ : null,
71
+ h(
72
+ 'ul',
73
+ { class: 'cg-pricing__tiers' },
74
+ block.tiers.map((tier) => renderTier(tier, ctx, tierTag)),
75
+ ),
76
+ )
77
+ }
@@ -0,0 +1,11 @@
1
+ import type { ProseBlock } from '@cogenta/blocks'
2
+ import { type HtmlElement, h, type RenderContext, renderRichText } from '@cogenta/theme-kit'
3
+
4
+ /**
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`.
8
+ */
9
+ export function renderProse(block: ProseBlock, ctx: RenderContext): HtmlElement {
10
+ return h('div', { class: 'cg-prose', 'data-block': 'prose' }, renderRichText(ctx, block.body))
11
+ }
@@ -0,0 +1,48 @@
1
+ import type { QuoteBlock } from '@cogenta/blocks'
2
+ import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+
4
+ /**
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.
9
+ *
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.
13
+ *
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).
17
+ */
18
+ export function renderQuote(block: QuoteBlock, ctx: RenderContext): HtmlElement {
19
+ const hasAttribution =
20
+ block.author !== undefined || block.role !== undefined || block.avatar !== undefined
21
+ return h(
22
+ 'figure',
23
+ { class: 'cg-quote', 'data-block': 'quote' },
24
+ h('blockquote', { class: 'cg-quote__text' }, h('p', { 'data-field': 'text' }, block.text)),
25
+ hasAttribution
26
+ ? h(
27
+ 'figcaption',
28
+ { class: 'cg-quote__attribution' },
29
+ block.avatar === undefined
30
+ ? null
31
+ : image(ctx, block.avatar, {
32
+ className: 'cg-quote__avatar',
33
+ variant: { width: 96, height: 96, fit: 'cover' },
34
+ }),
35
+ h(
36
+ 'span',
37
+ { class: 'cg-quote__who' },
38
+ block.author === undefined
39
+ ? null
40
+ : h('span', { class: 'cg-quote__author', 'data-field': 'author' }, block.author),
41
+ block.role === undefined
42
+ ? null
43
+ : h('span', { class: 'cg-quote__role', 'data-field': 'role' }, block.role),
44
+ ),
45
+ )
46
+ : null,
47
+ )
48
+ }
@@ -0,0 +1,45 @@
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'
9
+
10
+ /**
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.
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.
22
+ */
23
+ function renderItem(item: StatCounterItem): HtmlElement {
24
+ return h(
25
+ 'div',
26
+ { class: 'cg-kpi' },
27
+ h('dt', { class: 'cg-kpi__label' }, item.label),
28
+ h('dd', { class: 'cg-kpi__value' }, item.value),
29
+ )
30
+ }
31
+
32
+ export function renderStatCounter(block: StatCounterBlock, _ctx: RenderContext): HtmlElement {
33
+ return h(
34
+ 'section',
35
+ { class: 'cg-kpis', 'data-block': 'statCounter' },
36
+ block.title === undefined
37
+ ? null
38
+ : heading(
39
+ blockHeadingTag('statCounter') ?? 'h2',
40
+ { class: 'cg-kpis__title', 'data-field': 'title' },
41
+ block.title,
42
+ ),
43
+ h('dl', { class: 'cg-kpis__items' }, block.stats.map(renderItem)),
44
+ )
45
+ }
@@ -0,0 +1,50 @@
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'
9
+
10
+ /**
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.
18
+ *
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).
22
+ */
23
+ function renderItem(item: StatItem): HtmlElement {
24
+ return h(
25
+ 'div',
26
+ { class: 'cg-metric' },
27
+ h('dt', { class: 'cg-metric__label' }, item.label),
28
+ h(
29
+ 'dd',
30
+ { class: 'cg-metric__value' },
31
+ item.value,
32
+ item.unit === undefined ? null : h('span', { class: 'cg-metric__unit' }, item.unit),
33
+ ),
34
+ )
35
+ }
36
+
37
+ export function renderStats(block: StatsBlock, _ctx: RenderContext): HtmlElement {
38
+ return h(
39
+ 'section',
40
+ { class: 'cg-metrics', 'data-block': 'stats' },
41
+ block.title === undefined
42
+ ? null
43
+ : heading(
44
+ blockHeadingTag('stats') ?? 'h2',
45
+ { class: 'cg-metrics__title', 'data-field': 'title' },
46
+ block.title,
47
+ ),
48
+ h('dl', { class: 'cg-metrics__items' }, block.items.map(renderItem)),
49
+ )
50
+ }
@@ -0,0 +1,41 @@
1
+ import type { TestimonialBlock } from '@cogenta/blocks'
2
+ import { type HtmlElement, h, image, type RenderContext, renderRichText } from '@cogenta/theme-kit'
3
+
4
+ /**
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.
11
+ *
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).
15
+ */
16
+ export function renderTestimonial(block: TestimonialBlock, ctx: RenderContext): HtmlElement {
17
+ const { attribution } = block
18
+ return h(
19
+ 'figure',
20
+ { class: 'cg-testimonial', 'data-block': 'testimonial' },
21
+ h('blockquote', { class: 'cg-testimonial__quote' }, renderRichText(ctx, block.quote)),
22
+ h(
23
+ 'figcaption',
24
+ { class: 'cg-testimonial__attribution' },
25
+ attribution.avatar === undefined
26
+ ? null
27
+ : image(ctx, attribution.avatar, {
28
+ className: 'cg-testimonial__avatar',
29
+ variant: { width: 96, height: 96, fit: 'cover' },
30
+ }),
31
+ h(
32
+ 'span',
33
+ { class: 'cg-testimonial__who' },
34
+ h('span', { class: 'cg-testimonial__name' }, attribution.name),
35
+ attribution.role === undefined
36
+ ? null
37
+ : h('span', { class: 'cg-testimonial__role' }, attribution.role),
38
+ ),
39
+ ),
40
+ )
41
+ }
@@ -0,0 +1,128 @@
1
+ import {
2
+ type ChromeInput,
3
+ type ChromeNavLink,
4
+ type ChromeResult,
5
+ escapeAttribute,
6
+ escapeText,
7
+ renderBrandMark,
8
+ renderSocialLinks,
9
+ renderThemeToggle,
10
+ serialize,
11
+ } from '@cogenta/theme-kit'
12
+
13
+ /**
14
+ * This theme's own header and footer (`theme@1.4`) — a structurally
15
+ * different chrome from the canonical theme's, not a recolour of it.
16
+ *
17
+ * Header: a sticky bar with the corporate wordmark/logo on the left, a real
18
+ * desktop `<nav>`, `headerAction` (`theme@1.4`) rendered as a filled button
19
+ * — the one place this hairline-first chrome spends a solid accent surface
20
+ * — and, below the breakpoint the stylesheet sets, a CSS-only mobile menu:
21
+ * a checkbox (`#cg-nav-toggle`, visually hidden but still focusable and
22
+ * Space-operable) paired with a `<label>` styled as a three-bar button. No
23
+ * `<script>` anywhere: the sibling combinator in `base.css` is what shows
24
+ * the nav panel when the box is checked. This replaces the previous
25
+ * "the last header link doubles as the call to action" convention — with a
26
+ * real `headerAction` field, inventing a role for a plain nav link is no
27
+ * longer the honest reading of the data.
28
+ *
29
+ * Footer: a real four-column layout — a brand column (site name + `tagline`,
30
+ * `theme@1.4`), the site's own footer navigation, `social` (`theme@1.4`, via
31
+ * `renderSocialLinks`), and a fourth column carrying `footerNote`
32
+ * (`theme@1.4`) above Cogenta's own credit (or its white-label replacement).
33
+ * `brandingHtml` is placed exactly once, inside the footer, exactly as
34
+ * received: never altered, never dropped.
35
+ *
36
+ * The manual light/dark/system control (`renderThemeToggle`, `@cogenta/
37
+ * theme-kit`, L26) sits beside the mobile menu button, outside the `<nav>`
38
+ * it controls the collapse of — the `<nav>` panel is hidden under the
39
+ * breakpoint until the checkbox is checked, but a visitor must be able to
40
+ * flip appearance without opening that panel first, on desktop or mobile.
41
+ * This module ships no CSS of its own for it (`base.css` owns
42
+ * `.cg-theme-toggle`, in this theme's own visual register, not canonical's).
43
+ */
44
+
45
+ function navItems(links: readonly ChromeNavLink[]): string {
46
+ return links
47
+ .filter((link) => link.href !== null || link.kind === 'submenu-placeholder')
48
+ .map((link) => {
49
+ const label = escapeText(link.label)
50
+ const titleAttr = link.title === null ? '' : ` title="${escapeAttribute(link.title)}"`
51
+ if (link.href === null) return `<li><span${titleAttr}>${label}</span></li>`
52
+ const href = escapeAttribute(link.href)
53
+ const target = link.openInNewTab ? ' target="_blank" rel="noopener"' : ''
54
+ return `<li><a href="${href}"${target}${titleAttr}>${label}</a></li>`
55
+ })
56
+ .join('')
57
+ }
58
+
59
+ function renderNavList(links: readonly ChromeNavLink[]): string {
60
+ const items = navItems(links)
61
+ return items === '' ? '' : `<ul class="cg-nav__items">${items}</ul>`
62
+ }
63
+
64
+ /** The header's own call-to-action link (`theme@1.4`) — a filled button, distinct from the ordinary nav list. */
65
+ function renderHeaderAction(action: ChromeInput['headerAction']): string {
66
+ if (action === undefined) return ''
67
+ return (
68
+ `<a class="cg-action cg-site-header__action" data-emphasis="primary" ` +
69
+ `href="${escapeAttribute(action.href)}">${escapeText(action.label)}</a>`
70
+ )
71
+ }
72
+
73
+ export function renderChrome(input: ChromeInput): ChromeResult {
74
+ const siteNameText = escapeText(input.site.name)
75
+ const headerNavList = renderNavList(input.headerNav)
76
+ const headerAction = renderHeaderAction(input.headerAction)
77
+ const footerNavList = renderNavList(input.footerNav)
78
+ // A B2B header is a corporate identity slot before it is a wordmark slot.
79
+ const mark = renderBrandMark(input.brand, { className: 'cg-site-header__logo' }) ?? siteNameText
80
+ const themeToggle = serialize(renderThemeToggle(input.locale, { className: 'cg-theme-toggle' }))
81
+
82
+ const header =
83
+ `<header class="cg-site-header"><div class="cg-site-header__inner">` +
84
+ `<a class="cg-site-header__home" href="${escapeAttribute(input.homeHref)}">${mark}</a>` +
85
+ `<input type="checkbox" id="cg-nav-toggle" class="cg-nav-toggle-input" aria-label="Menu">` +
86
+ `<label for="cg-nav-toggle" class="cg-nav-toggle-label" aria-hidden="true">` +
87
+ `<span class="cg-nav-toggle-bar"></span><span class="cg-nav-toggle-bar"></span><span class="cg-nav-toggle-bar"></span>` +
88
+ `</label>` +
89
+ `${themeToggle}` +
90
+ `<nav class="cg-site-header__nav" id="cg-nav" aria-label="Primary">` +
91
+ `${headerNavList}${headerAction}` +
92
+ `</nav>` +
93
+ `</div></header>`
94
+
95
+ const tagline =
96
+ input.tagline === undefined
97
+ ? ''
98
+ : `<p class="cg-site-footer__tagline" data-field="tagline">${escapeText(input.tagline)}</p>`
99
+ const social =
100
+ input.social === undefined
101
+ ? ''
102
+ : serialize(
103
+ renderSocialLinks(input.social, {
104
+ className: 'cg-site-footer__social',
105
+ itemClassName: 'cg-site-footer__social-item',
106
+ }) ?? { kind: 'text', value: '' },
107
+ )
108
+ const footerNote =
109
+ input.footerNote === undefined
110
+ ? ''
111
+ : `<p class="cg-site-footer__note">${escapeText(input.footerNote)}</p>`
112
+
113
+ const footer =
114
+ `<footer class="cg-site-footer"><div class="cg-site-footer__grid">` +
115
+ `<div class="cg-site-footer__brand">` +
116
+ `<a href="${escapeAttribute(input.homeHref)}">${siteNameText}</a>${tagline}` +
117
+ `</div>` +
118
+ `${
119
+ footerNavList === ''
120
+ ? ''
121
+ : `<nav class="cg-site-footer__nav" aria-label="Footer">${footerNavList}</nav>`
122
+ }` +
123
+ `${social === '' ? '' : `<div class="cg-site-footer__social-col">${social}</div>`}` +
124
+ `<div class="cg-site-footer__meta">${footerNote}<div class="cg-site-footer__branding">${input.brandingHtml}</div></div>` +
125
+ `</div><div class="cg-site-footer__bottom"><span>${siteNameText}</span></div></footer>`
126
+
127
+ return { header, footer }
128
+ }
@@ -0,0 +1,144 @@
1
+ import { type BlockRegistry, VOCABULARY_NAMES, type VocabularyBlock } from '@cogenta/blocks'
2
+ import {
3
+ type FetchedEntries,
4
+ type HtmlElement,
5
+ h,
6
+ type PageContent,
7
+ pageHasOwnHeading,
8
+ type RenderContext,
9
+ renderEntryHeader,
10
+ resolveBlockForRender,
11
+ withBlockKey,
12
+ withBlockVariant,
13
+ } from '@cogenta/theme-kit'
14
+ import { renderAccordion } from './blocks/accordion.js'
15
+ import { renderCollectionList } from './blocks/collection-list.js'
16
+ import { renderCta } from './blocks/cta.js'
17
+ import { renderEmbed } from './blocks/embed.js'
18
+ import { renderFaq } from './blocks/faq.js'
19
+ import { renderFeatureGrid } from './blocks/feature-grid.js'
20
+ import { renderGallery } from './blocks/gallery.js'
21
+ import { renderHero } from './blocks/hero.js'
22
+ import { renderLogoStrip } from './blocks/logo-strip.js'
23
+ import { renderLogos } from './blocks/logos.js'
24
+ import { renderMediaFigure } from './blocks/media-figure.js'
25
+ import { renderPricingTable } from './blocks/pricing-table.js'
26
+ import { renderProse } from './blocks/prose.js'
27
+ import { renderQuote } from './blocks/quote.js'
28
+ import { renderStatCounter } from './blocks/stat-counter.js'
29
+ import { renderStats } from './blocks/stats.js'
30
+ import { renderTestimonial } from './blocks/testimonial.js'
31
+
32
+ export type { FetchedEntries, PageContent }
33
+ export { pageHasOwnHeading, renderEntryHeader, withBlockKey }
34
+
35
+ export function renderBlock(
36
+ block: VocabularyBlock,
37
+ ctx: RenderContext,
38
+ entries: FetchedEntries = {},
39
+ registry?: BlockRegistry,
40
+ ): HtmlElement | null {
41
+ // `block`'s type says `VocabularyBlock`, but the value crossing this
42
+ // boundary from stored content is not always literally one of the shared
43
+ // vocabulary — resolving here is what turns an unimplemented theme-private
44
+ // block into its declared fallback instead of a silently blank slot.
45
+ const resolved = resolveBlockForRender(block, VOCABULARY_NAMES, registry)
46
+ if (resolved === null) return null
47
+ const known = resolved as unknown as VocabularyBlock
48
+ // `withBlockVariant` (blocks@2.0, RFC 0002) is applied once here, after
49
+ // dispatch, rather than inside each of the seventeen block renderers —
50
+ // `variant` is envelope data every block carries identically, exactly the
51
+ // same reasoning `withBlockKey` in `renderPage` already follows for `_key`.
52
+ return withBlockVariant(renderKnownBlock(known, ctx, entries), known.variant)
53
+ }
54
+
55
+ function renderKnownBlock(
56
+ known: VocabularyBlock,
57
+ ctx: RenderContext,
58
+ entries: FetchedEntries,
59
+ ): HtmlElement | null {
60
+ switch (known._type) {
61
+ case 'hero':
62
+ return renderHero(known, ctx)
63
+ case 'prose':
64
+ return renderProse(known, ctx)
65
+ case 'mediaFigure':
66
+ return renderMediaFigure(known, ctx)
67
+ case 'featureGrid':
68
+ return renderFeatureGrid(known, ctx)
69
+ case 'cta':
70
+ return renderCta(known, ctx)
71
+ case 'gallery':
72
+ return renderGallery(known, ctx)
73
+ case 'quote':
74
+ return renderQuote(known, ctx)
75
+ case 'faq':
76
+ return renderFaq(known, ctx)
77
+ case 'stats':
78
+ return renderStats(known, ctx)
79
+ case 'logos':
80
+ return renderLogos(known, ctx)
81
+ case 'collectionList':
82
+ return renderCollectionList(known, ctx, entries[known._key] ?? [])
83
+ case 'embed':
84
+ return renderEmbed(known, ctx)
85
+ case 'testimonial':
86
+ return renderTestimonial(known, ctx)
87
+ case 'pricingTable':
88
+ return renderPricingTable(known, ctx)
89
+ case 'accordion':
90
+ return renderAccordion(known, ctx)
91
+ case 'statCounter':
92
+ return renderStatCounter(known, ctx)
93
+ case 'logoStrip':
94
+ return renderLogoStrip(known, ctx)
95
+ default: {
96
+ // Exhaustive over contract B's vocabulary: `known` is `never` here, so
97
+ // a block this package does not implement stops it compiling until it
98
+ // is. Returning null rather than throwing is deliberate — a theme has
99
+ // no access to `@cogenta/core`'s error types (contract D refuses the
100
+ // import), and choosing a fallback block is the render layer's job,
101
+ // not the theme's.
102
+ const unreachable: never = known
103
+ void unreachable
104
+ return null
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * `<main id="cg-main">` is mandatory: it is the skip-link's target, written
111
+ * once by `@cogenta/cli`'s `theme-render.ts` outside any theme's control.
112
+ *
113
+ * `renderEntryHeader` (`theme@1.4`) draws the eyebrow/title/excerpt/meta/
114
+ * cover furniture for a page backed by a real content entry — a service or
115
+ * a plain page — and already returns `null` for a page with no `entry`
116
+ * meta *and* for one whose blocks draw their own heading (a `hero`), so the
117
+ * bare `<h1 class="cg-page__title">` fallback below is the right markup in
118
+ * both of those cases and only those — never a double heading, and never a
119
+ * page with none at all.
120
+ *
121
+ * `withBlockKey` stamps every rendered block with its contract-B `_key` —
122
+ * required so the visual page builder (L16) can map a clicked element in
123
+ * the rendered iframe back to the block that produced it, for this theme
124
+ * exactly as for every other one.
125
+ */
126
+ export function renderPage(
127
+ page: PageContent,
128
+ ctx: RenderContext,
129
+ entries: FetchedEntries = {},
130
+ registry?: BlockRegistry,
131
+ ): HtmlElement {
132
+ const entryHeader = renderEntryHeader(page, ctx)
133
+ return h(
134
+ 'main',
135
+ { class: 'cg-main', id: 'cg-main' },
136
+ entryHeader,
137
+ entryHeader === null && !pageHasOwnHeading(page.blocks)
138
+ ? h('h1', { class: 'cg-page__title' }, page.title)
139
+ : null,
140
+ page.blocks.map((block) =>
141
+ withBlockKey(renderBlock(block, ctx, entries, registry), block._key),
142
+ ),
143
+ )
144
+ }
@@ -0,0 +1,80 @@
1
+ import { type HtmlElement, h, type TermArchiveInput } from '@cogenta/theme-kit'
2
+
3
+ /**
4
+ * The taxonomy-term archive (contract D `theme@1.3`) — a resource index.
5
+ *
6
+ * Built from the same `cg-list__row` rows `collectionList` already renders
7
+ * here: a date rail on the left, the title and its summary on the right,
8
+ * which is the shape a B2B insights or resources index takes. Reusing the
9
+ * block's own classes is what keeps an archive looking like the rest of the
10
+ * site without a second stylesheet to maintain.
11
+ */
12
+ export function renderTermArchive(input: TermArchiveInput): HtmlElement {
13
+ const rows = input.entries.map((entry) =>
14
+ h(
15
+ 'li',
16
+ { class: 'cg-list__row' },
17
+ entry.publishedAt === null
18
+ ? null
19
+ : h(
20
+ 'time',
21
+ { class: 'cg-list__date', datetime: entry.publishedAt },
22
+ entry.publishedAt.slice(0, 10),
23
+ ),
24
+ h(
25
+ 'div',
26
+ { class: 'cg-list__body' },
27
+ h(
28
+ 'h2',
29
+ { class: 'cg-list__title' },
30
+ entry.href === null
31
+ ? entry.title
32
+ : h('a', { class: 'cg-list__link', href: entry.href }, entry.title),
33
+ ),
34
+ entry.summary === null ? null : h('p', { class: 'cg-list__excerpt' }, entry.summary),
35
+ ),
36
+ ),
37
+ )
38
+
39
+ return h(
40
+ 'main',
41
+ { class: 'cg-main cg-archive', id: 'cg-main' },
42
+ input.ancestors.length === 0
43
+ ? null
44
+ : h(
45
+ 'nav',
46
+ { class: 'cg-archive__breadcrumb', 'aria-label': input.labels.breadcrumb },
47
+ h(
48
+ 'ol',
49
+ {},
50
+ ...input.ancestors.map((l) => h('li', {}, h('a', { href: l.href }, l.label))),
51
+ ),
52
+ ),
53
+ h('h1', { class: 'cg-list__title-heading' }, input.term.label),
54
+ input.children.length === 0
55
+ ? null
56
+ : h(
57
+ 'ul',
58
+ { class: 'cg-archive__children', 'aria-label': input.labels.subterms },
59
+ ...input.children.map((c) => h('li', {}, h('a', { href: c.href }, c.label))),
60
+ ),
61
+ rows.length === 0
62
+ ? h('p', { class: 'cg-list__empty' }, input.labels.empty)
63
+ : h('ul', { class: 'cg-list__items' }, ...rows),
64
+ pager(input),
65
+ )
66
+ }
67
+
68
+ function pager(input: TermArchiveInput): HtmlElement | null {
69
+ if (input.page.previousHref === null && input.page.nextHref === null) return null
70
+ return h(
71
+ 'nav',
72
+ { class: 'cg-archive__pager', 'aria-label': input.labels.pagination },
73
+ input.page.previousHref === null
74
+ ? null
75
+ : h('a', { rel: 'prev', href: input.page.previousHref }, input.labels.previous),
76
+ input.page.nextHref === null
77
+ ? null
78
+ : h('a', { rel: 'next', href: input.page.nextHref }, input.labels.next),
79
+ )
80
+ }