@cogenta/theme-docs 0.3.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 +21 -0
  3. package/dist/src/index.d.ts.map +1 -0
  4. package/dist/src/index.js +21 -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 +15 -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 +77 -0
  13. package/dist/src/render/blocks/collection-list.js.map +1 -0
  14. package/dist/src/render/blocks/cta.d.ts +4 -0
  15. package/dist/src/render/blocks/cta.d.ts.map +1 -0
  16. package/dist/src/render/blocks/cta.js +7 -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 +71 -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 +15 -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 +27 -0
  29. package/dist/src/render/blocks/feature-grid.js.map +1 -0
  30. package/dist/src/render/blocks/gallery.d.ts +10 -0
  31. package/dist/src/render/blocks/gallery.d.ts.map +1 -0
  32. package/dist/src/render/blocks/gallery.js +19 -0
  33. package/dist/src/render/blocks/gallery.js.map +1 -0
  34. package/dist/src/render/blocks/hero.d.ts +16 -0
  35. package/dist/src/render/blocks/hero.d.ts.map +1 -0
  36. package/dist/src/render/blocks/hero.js +28 -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 +16 -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 +22 -0
  45. package/dist/src/render/blocks/logos.js.map +1 -0
  46. package/dist/src/render/blocks/media-figure.d.ts +10 -0
  47. package/dist/src/render/blocks/media-figure.d.ts.map +1 -0
  48. package/dist/src/render/blocks/media-figure.js +25 -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 +28 -0
  53. package/dist/src/render/blocks/pricing-table.js.map +1 -0
  54. package/dist/src/render/blocks/prose.d.ts +4 -0
  55. package/dist/src/render/blocks/prose.d.ts.map +1 -0
  56. package/dist/src/render/blocks/prose.js +36 -0
  57. package/dist/src/render/blocks/prose.js.map +1 -0
  58. package/dist/src/render/blocks/quote.d.ts +9 -0
  59. package/dist/src/render/blocks/quote.d.ts.map +1 -0
  60. package/dist/src/render/blocks/quote.js +22 -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 +15 -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 +11 -0
  69. package/dist/src/render/blocks/stats.js.map +1 -0
  70. package/dist/src/render/blocks/testimonial.d.ts +11 -0
  71. package/dist/src/render/blocks/testimonial.d.ts.map +1 -0
  72. package/dist/src/render/blocks/testimonial.js +20 -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 +94 -0
  77. package/dist/src/render/chrome.js.map +1 -0
  78. package/dist/src/render/render-block.d.ts +15 -0
  79. package/dist/src/render/render-block.d.ts.map +1 -0
  80. package/dist/src/render/render-block.js +176 -0
  81. package/dist/src/render/render-block.js.map +1 -0
  82. package/dist/src/render/term-archive.d.ts +10 -0
  83. package/dist/src/render/term-archive.d.ts.map +1 -0
  84. package/dist/src/render/term-archive.js +32 -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 +20 -0
  92. package/src/render/blocks/accordion.ts +51 -0
  93. package/src/render/blocks/collection-list.ts +170 -0
  94. package/src/render/blocks/cta.ts +25 -0
  95. package/src/render/blocks/embed.ts +94 -0
  96. package/src/render/blocks/faq.ts +46 -0
  97. package/src/render/blocks/feature-grid.ts +67 -0
  98. package/src/render/blocks/gallery.ts +39 -0
  99. package/src/render/blocks/hero.ts +61 -0
  100. package/src/render/blocks/logo-strip.ts +31 -0
  101. package/src/render/blocks/logos.ts +52 -0
  102. package/src/render/blocks/media-figure.ts +36 -0
  103. package/src/render/blocks/pricing-table.ts +72 -0
  104. package/src/render/blocks/prose.ts +49 -0
  105. package/src/render/blocks/quote.ts +35 -0
  106. package/src/render/blocks/stat-counter.ts +37 -0
  107. package/src/render/blocks/stats.ts +38 -0
  108. package/src/render/blocks/testimonial.ts +36 -0
  109. package/src/render/chrome.ts +124 -0
  110. package/src/render/render-block.ts +305 -0
  111. package/src/render/term-archive.ts +78 -0
  112. package/src/styles/archive.css +39 -0
  113. package/src/styles/base.css +1106 -0
  114. package/src/styles/blocks.css +1238 -0
  115. package/src/styles/theme.css +34 -0
  116. package/src/styles/tokens.css +307 -0
  117. package/tokens.json +36 -0
@@ -0,0 +1,170 @@
1
+ import type { CollectionListBlock } from '@cogenta/blocks'
2
+ import {
3
+ blockHeadingTag,
4
+ buildCollectionListQuery,
5
+ type ContentEntry,
6
+ entryDate,
7
+ entryExcerpt,
8
+ entryHref,
9
+ entryImage,
10
+ entryTitle,
11
+ type HeadingTag,
12
+ type HtmlElement,
13
+ h,
14
+ heading,
15
+ nestedHeadingTag,
16
+ type RenderContext,
17
+ renderImageSource,
18
+ } from '@cogenta/theme-kit'
19
+
20
+ /**
21
+ * The only block of the seventeen that reads data at render time — contract
22
+ * B marks it `runtime: 'server'` for that reason. The read is *not* done
23
+ * here: `query` builds the request from the block's own fields
24
+ * (`@cogenta/theme-kit`'s `buildCollectionListQuery`, shared across every
25
+ * theme), the caller awaits `ctx.content.list(...)` before rendering
26
+ * starts, and this function stays a pure function of the entries handed to
27
+ * it.
28
+ */
29
+ export { buildCollectionListQuery as query }
30
+
31
+ /**
32
+ * The general-purpose row: a cover image (`entryImage`, contract D
33
+ * `theme@1.4`) when the collection carries one, title, excerpt and date —
34
+ * this is what a non-docs collection placed in this block gets.
35
+ */
36
+ function renderRow(entry: ContentEntry, ctx: RenderContext, tag: HeadingTag): HtmlElement {
37
+ const date = entryDate(entry)
38
+ const excerpt = entryExcerpt(entry)
39
+ const image = entryImage(entry, ctx)
40
+ return h(
41
+ 'li',
42
+ { class: 'cg-list__row' },
43
+ image === undefined
44
+ ? null
45
+ : h(
46
+ 'div',
47
+ { class: 'cg-list__thumb' },
48
+ renderImageSource(image, { loading: 'lazy', className: 'cg-list__thumb-image' }),
49
+ ),
50
+ h(
51
+ 'div',
52
+ { class: 'cg-list__body' },
53
+ heading(
54
+ tag,
55
+ { class: 'cg-list__title' },
56
+ h('a', { class: 'cg-list__link', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
57
+ ),
58
+ date === undefined
59
+ ? null
60
+ : h(
61
+ 'time',
62
+ { class: 'cg-list__date', datetime: date },
63
+ new Intl.DateTimeFormat(ctx.locale, { dateStyle: 'long' }).format(new Date(date)),
64
+ ),
65
+ excerpt === undefined ? null : h('p', { class: 'cg-list__excerpt' }, excerpt),
66
+ ),
67
+ )
68
+ }
69
+
70
+ /**
71
+ * `doc_page`'s own layout: a compact, multi-column index grouped by the
72
+ * entry's own `section` field and ordered by its own `order` field within
73
+ * each group — neither is a valid `collectionList.sort.field` (contract B
74
+ * fixes that union to `id`/`createdAt`/`updatedAt`), so the theme re-groups
75
+ * and re-sorts the already-fetched slice itself rather than asking for a
76
+ * sort the contract does not offer. Sections are ordered alphabetically for
77
+ * a deterministic page; entries within a section fall back to the order
78
+ * they were fetched in when `order` is absent or not a number.
79
+ */
80
+ function renderGuidesIndex(entries: readonly ContentEntry[], ctx: RenderContext): HtmlElement {
81
+ const groups = new Map<string, ContentEntry[]>()
82
+ entries.forEach((entry) => {
83
+ const section =
84
+ typeof entry.section === 'string' && entry.section !== ''
85
+ ? entry.section
86
+ : ctx.t('entry.untitled')
87
+ const list = groups.get(section) ?? []
88
+ list.push(entry)
89
+ groups.set(section, list)
90
+ })
91
+
92
+ const sortedSections = [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))
93
+
94
+ return h(
95
+ 'div',
96
+ { class: 'cg-guides' },
97
+ sortedSections.map(([section, sectionEntries]) => {
98
+ const ordered = [...sectionEntries].sort((a, b) => {
99
+ const orderA = typeof a.order === 'number' ? a.order : Number.POSITIVE_INFINITY
100
+ const orderB = typeof b.order === 'number' ? b.order : Number.POSITIVE_INFINITY
101
+ return orderA - orderB
102
+ })
103
+ return h(
104
+ 'div',
105
+ { class: 'cg-guides__section' },
106
+ h('h3', { class: 'cg-guides__heading' }, section),
107
+ h(
108
+ 'ul',
109
+ { class: 'cg-guides__items' },
110
+ ordered.map((entry) =>
111
+ h(
112
+ 'li',
113
+ { class: 'cg-guides__item' },
114
+ h(
115
+ 'a',
116
+ { class: 'cg-guides__link', href: entryHref(entry, ctx) },
117
+ entryTitle(entry, ctx),
118
+ ),
119
+ ),
120
+ ),
121
+ ),
122
+ )
123
+ }),
124
+ )
125
+ }
126
+
127
+ export function renderCollectionList(
128
+ block: CollectionListBlock,
129
+ ctx: RenderContext,
130
+ entries: readonly ContentEntry[],
131
+ ): HtmlElement {
132
+ const hasTitle = block.title !== undefined
133
+ const entryTag = nestedHeadingTag('collectionList', hasTitle)
134
+ const isGuidesIndex = block.collection === 'doc_page'
135
+
136
+ const body =
137
+ entries.length === 0
138
+ ? h('p', { class: 'cg-list__empty' }, ctx.t('collection.empty'))
139
+ : isGuidesIndex
140
+ ? renderGuidesIndex(entries, ctx)
141
+ : h(
142
+ 'ul',
143
+ { class: 'cg-list__items' },
144
+ entries.map((entry) => renderRow(entry, ctx, entryTag)),
145
+ )
146
+
147
+ return h(
148
+ 'section',
149
+ { class: 'cg-block cg-list', 'data-block': 'collectionList', 'data-layout': block.layout },
150
+ hasTitle
151
+ ? heading(
152
+ blockHeadingTag('collectionList') ?? 'h2',
153
+ { class: 'cg-list__title-heading', 'data-field': 'title' },
154
+ block.title ?? '',
155
+ )
156
+ : null,
157
+ block.layout === 'carousel' && !isGuidesIndex
158
+ ? h(
159
+ 'div',
160
+ {
161
+ class: 'cg-list__viewport',
162
+ role: 'region',
163
+ 'aria-label': block.title ?? ctx.t('collection.carousel'),
164
+ tabindex: '0',
165
+ },
166
+ body,
167
+ )
168
+ : body,
169
+ )
170
+ }
@@ -0,0 +1,25 @@
1
+ import type { CtaBlock } from '@cogenta/blocks'
2
+ import {
3
+ actionList,
4
+ blockHeadingTag,
5
+ type HtmlElement,
6
+ h,
7
+ heading,
8
+ type RenderContext,
9
+ } from '@cogenta/theme-kit'
10
+
11
+ export function renderCta(block: CtaBlock, ctx: RenderContext): HtmlElement {
12
+ return h(
13
+ 'section',
14
+ { class: 'cg-block cg-cta', 'data-block': 'cta' },
15
+ heading(
16
+ blockHeadingTag('cta') ?? 'h2',
17
+ { class: 'cg-cta__title', 'data-field': 'title' },
18
+ block.title,
19
+ ),
20
+ block.text === undefined
21
+ ? null
22
+ : h('p', { class: 'cg-cta__text', 'data-field': 'text' }, block.text),
23
+ actionList(ctx, block.actions, block.title),
24
+ )
25
+ }
@@ -0,0 +1,94 @@
1
+ import type { EmbedBlock } from '@cogenta/blocks'
2
+ import { aspectRatio, type HtmlElement, h, type RenderContext } from '@cogenta/theme-kit'
3
+
4
+ /**
5
+ * Nothing here contacts a third party before the visitor has consented.
6
+ *
7
+ * When `consentRequired` is true the block renders a self-contained card and
8
+ * an outbound link — no `<iframe>`, no `<script>`, no preconnect, no poster
9
+ * image fetched from the provider.
10
+ */
11
+
12
+ function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string | null {
13
+ const url = URL.parse(rawUrl)
14
+ if (url === null) return null
15
+ const segments = url.pathname.split('/').filter((segment) => segment !== '')
16
+
17
+ switch (provider) {
18
+ case 'youtube': {
19
+ const id = url.hostname.endsWith('youtu.be')
20
+ ? segments[0]
21
+ : (url.searchParams.get('v') ?? segments[1])
22
+ return id === undefined || id === null ? null : `https://www.youtube-nocookie.com/embed/${id}`
23
+ }
24
+ case 'vimeo': {
25
+ const id = segments.find((segment) => /^\d+$/.test(segment))
26
+ return id === undefined ? null : `https://player.vimeo.com/video/${id}`
27
+ }
28
+ case 'dailymotion': {
29
+ const id = url.hostname.endsWith('dai.ly') ? segments[0] : segments[segments.length - 1]
30
+ return id === undefined ? null : `https://geo.dailymotion.com/player.html?video=${id}`
31
+ }
32
+ case 'spotify': {
33
+ const [kind, id] = segments
34
+ return kind === undefined || id === undefined
35
+ ? null
36
+ : `https://open.spotify.com/embed/${kind}/${id}`
37
+ }
38
+ case 'soundcloud':
39
+ return `https://w.soundcloud.com/player/?url=${encodeURIComponent(rawUrl)}`
40
+ default:
41
+ return null
42
+ }
43
+ }
44
+
45
+ function consentCard(block: EmbedBlock, ctx: RenderContext, reason: string): HtmlElement {
46
+ return h(
47
+ 'div',
48
+ { class: 'cg-embed__placeholder' },
49
+ h('p', { class: 'cg-embed__provider', 'aria-hidden': 'true' }, block.provider),
50
+ h('p', { class: 'cg-embed__notice' }, reason),
51
+ h(
52
+ 'a',
53
+ {
54
+ class: 'cg-embed__link',
55
+ href: ctx.link(block.url),
56
+ rel: 'noopener noreferrer nofollow',
57
+ },
58
+ ctx.t('embed.open', { provider: block.provider }),
59
+ ),
60
+ )
61
+ }
62
+
63
+ export function renderEmbed(block: EmbedBlock, ctx: RenderContext): HtmlElement {
64
+ const source = block.consentRequired ? null : frameSource(block.provider, block.url)
65
+ const ratio = aspectRatio(block.ratio) ?? '16 / 9'
66
+
67
+ return h(
68
+ 'div',
69
+ {
70
+ class: 'cg-block cg-embed',
71
+ 'data-block': 'embed',
72
+ 'data-provider': block.provider,
73
+ 'data-consent': block.consentRequired ? 'required' : 'not-required',
74
+ style: `--cg-ratio:${ratio}`,
75
+ },
76
+ source === null
77
+ ? consentCard(
78
+ block,
79
+ ctx,
80
+ block.consentRequired
81
+ ? ctx.t('embed.consentRequired', { provider: block.provider })
82
+ : ctx.t('embed.unsupported', { provider: block.provider }),
83
+ )
84
+ : h('iframe', {
85
+ class: 'cg-embed__frame',
86
+ src: source,
87
+ title: ctx.t('embed.title', { provider: block.provider }),
88
+ loading: 'lazy',
89
+ referrerpolicy: 'strict-origin-when-cross-origin',
90
+ allow: 'accelerometer; clipboard-write; encrypted-media; picture-in-picture; fullscreen',
91
+ allowfullscreen: true,
92
+ }),
93
+ )
94
+ }
@@ -0,0 +1,46 @@
1
+ import type { FaqBlock, FaqItem } from '@cogenta/blocks'
2
+ import {
3
+ blockHeadingTag,
4
+ type HtmlElement,
5
+ h,
6
+ heading,
7
+ type RenderContext,
8
+ renderRichText,
9
+ } from '@cogenta/theme-kit'
10
+
11
+ /**
12
+ * `<details>`/`<summary>` rather than a scripted accordion: expanding,
13
+ * keyboard operation, and the expanded state announced to assistive
14
+ * technology all come from the browser, at zero bytes of JavaScript.
15
+ */
16
+ function renderItem(item: FaqItem, ctx: RenderContext): HtmlElement {
17
+ return h(
18
+ 'li',
19
+ { class: 'cg-faq__item' },
20
+ h(
21
+ 'details',
22
+ { class: 'cg-faq__details' },
23
+ h('summary', { class: 'cg-faq__question' }, item.question),
24
+ h('div', { class: 'cg-faq__answer' }, renderRichText(ctx, item.answer)),
25
+ ),
26
+ )
27
+ }
28
+
29
+ export function renderFaq(block: FaqBlock, ctx: RenderContext): HtmlElement {
30
+ return h(
31
+ 'section',
32
+ { class: 'cg-block cg-faq', 'data-block': 'faq' },
33
+ block.title === undefined
34
+ ? null
35
+ : heading(
36
+ blockHeadingTag('faq') ?? 'h2',
37
+ { class: 'cg-faq__title', 'data-field': 'title' },
38
+ block.title,
39
+ ),
40
+ h(
41
+ 'ul',
42
+ { class: 'cg-faq__items' },
43
+ block.items.map((item) => renderItem(item, ctx)),
44
+ ),
45
+ )
46
+ }
@@ -0,0 +1,67 @@
1
+ import type { FeatureGridBlock, FeatureItem } from '@cogenta/blocks'
2
+ import {
3
+ blockHeadingTag,
4
+ type HeadingTag,
5
+ type HtmlElement,
6
+ h,
7
+ heading,
8
+ href,
9
+ nestedHeadingTag,
10
+ type RenderContext,
11
+ renderIcon,
12
+ } from '@cogenta/theme-kit'
13
+
14
+ /**
15
+ * The item's title is the link, so the link's accessible name is the
16
+ * feature's own name (WCAG 2.4.4) — the block carries no separate label
17
+ * field to write a "learn more" with anyway.
18
+ *
19
+ * `icon` names a symbol, never markup: when the name is one `renderIcon`
20
+ * recognises it becomes a real inline glyph, `aria-hidden` since the title
21
+ * already names the card. An unrecognised name keeps the bare, empty
22
+ * `data-icon` span for a skin/plugin to key off.
23
+ */
24
+ function renderItem(item: FeatureItem, ctx: RenderContext, tag: HeadingTag): HtmlElement {
25
+ const title =
26
+ item.link === undefined
27
+ ? heading(tag, { class: 'cg-feature__title' }, item.title)
28
+ : heading(
29
+ tag,
30
+ { class: 'cg-feature__title' },
31
+ h('a', { class: 'cg-feature__link', href: href(ctx, item.link) }, item.title),
32
+ )
33
+ return h(
34
+ 'li',
35
+ { class: 'cg-feature' },
36
+ item.icon === undefined
37
+ ? null
38
+ : h(
39
+ 'span',
40
+ { class: 'cg-feature__icon', 'data-icon': item.icon, 'aria-hidden': 'true' },
41
+ renderIcon(item.icon),
42
+ ),
43
+ title,
44
+ item.text === undefined ? null : h('p', { class: 'cg-feature__text' }, item.text),
45
+ )
46
+ }
47
+
48
+ export function renderFeatureGrid(block: FeatureGridBlock, ctx: RenderContext): HtmlElement {
49
+ const hasTitle = block.title !== undefined
50
+ const itemTag = nestedHeadingTag('featureGrid', hasTitle)
51
+ return h(
52
+ 'section',
53
+ { class: 'cg-block cg-features', 'data-block': 'featureGrid' },
54
+ hasTitle
55
+ ? heading(
56
+ blockHeadingTag('featureGrid') ?? 'h2',
57
+ { class: 'cg-features__title', 'data-field': 'title' },
58
+ block.title ?? '',
59
+ )
60
+ : null,
61
+ h(
62
+ 'ul',
63
+ { class: 'cg-features__items' },
64
+ block.items.map((item) => renderItem(item, ctx, itemTag)),
65
+ ),
66
+ )
67
+ }
@@ -0,0 +1,39 @@
1
+ import type { GalleryBlock } from '@cogenta/blocks'
2
+ import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+
4
+ /**
5
+ * The carousel ships **no JavaScript**. It is a scroll-snapping list, which
6
+ * gives touch swiping, trackpad scrolling and — because the list is
7
+ * focusable and labelled — arrow-key scrolling, with none of the failure
8
+ * modes of a scripted carousel.
9
+ */
10
+ export function renderGallery(block: GalleryBlock, ctx: RenderContext): HtmlElement {
11
+ const items = h(
12
+ 'ul',
13
+ { class: 'cg-gallery__items' },
14
+ block.items.map((item) =>
15
+ h(
16
+ 'li',
17
+ { class: 'cg-gallery__item' },
18
+ image(ctx, item.media, { sizes: '(min-width: 45rem) 20rem, 60vw' }),
19
+ ),
20
+ ),
21
+ )
22
+
23
+ return h(
24
+ 'section',
25
+ { class: 'cg-block cg-gallery', 'data-block': 'gallery', 'data-layout': block.layout },
26
+ block.layout === 'carousel'
27
+ ? h(
28
+ 'div',
29
+ {
30
+ class: 'cg-gallery__viewport',
31
+ role: 'region',
32
+ 'aria-label': ctx.t('gallery.carousel'),
33
+ tabindex: '0',
34
+ },
35
+ items,
36
+ )
37
+ : items,
38
+ )
39
+ }
@@ -0,0 +1,61 @@
1
+ import type { HeroBlock } from '@cogenta/blocks'
2
+ import {
3
+ actionList,
4
+ blockHeadingTag,
5
+ type HtmlElement,
6
+ h,
7
+ heading,
8
+ image,
9
+ type RenderContext,
10
+ renderIcon,
11
+ } from '@cogenta/theme-kit'
12
+
13
+ /**
14
+ * The hero carries the page's `h1` (contract B: `headingLevel: 'h1'`), so
15
+ * `renderPage` relies on it to avoid emitting a second one.
16
+ *
17
+ * A reference-docs hero: copy on the left, a decorative search-looking
18
+ * prompt right under the actions (purely presentational — `aria-hidden`,
19
+ * no `<input>`, no `<form>` — this theme ships zero client JavaScript and a
20
+ * fake control that looked operable would be a worse failure than no
21
+ * control at all), and an optional small illustration on the right rather
22
+ * than a full-bleed image wash, matching the information-dense register the
23
+ * aesthetic direction asks for.
24
+ */
25
+ export function renderHero(block: HeroBlock, ctx: RenderContext): HtmlElement {
26
+ const tag = blockHeadingTag('hero') ?? 'h1'
27
+ return h(
28
+ 'section',
29
+ { class: 'cg-block cg-hero', 'data-block': 'hero' },
30
+ h(
31
+ 'div',
32
+ { class: 'cg-hero__copy' },
33
+ block.eyebrow === undefined
34
+ ? null
35
+ : h('p', { class: 'cg-hero__eyebrow', 'data-field': 'eyebrow' }, block.eyebrow),
36
+ heading(tag, { class: 'cg-hero__title', 'data-field': 'title' }, block.title),
37
+ block.subtitle === undefined
38
+ ? null
39
+ : h('p', { class: 'cg-hero__subtitle', 'data-field': 'subtitle' }, block.subtitle),
40
+ actionList(ctx, block.actions, ctx.t('hero.actions')),
41
+ h(
42
+ 'div',
43
+ { class: 'cg-hero__search', 'aria-hidden': 'true' },
44
+ renderIcon('search', { className: 'cg-hero__search-icon' }),
45
+ h('span', { class: 'cg-hero__search-text' }, 'Search the docs…'),
46
+ h('kbd', { class: 'cg-hero__search-key' }, '/'),
47
+ ),
48
+ ),
49
+ block.media === undefined
50
+ ? null
51
+ : h(
52
+ 'div',
53
+ { class: 'cg-hero__panel' },
54
+ image(ctx, block.media, {
55
+ className: 'cg-hero__media',
56
+ loading: 'eager',
57
+ sizes: '(min-width: 64rem) 32vw, 100vw',
58
+ }),
59
+ ),
60
+ )
61
+ }
@@ -0,0 +1,31 @@
1
+ import type { LogoStripBlock, LogoStripItem } from '@cogenta/blocks'
2
+ import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+
4
+ /**
5
+ * `blocks@2.0` (RFC 0001). The lighter-weight "as seen in" row next to
6
+ * `logos`: no per-logo name or link, so no accessible-name field to
7
+ * require — each image's own alt text (set once, in the media library)
8
+ * names it.
9
+ */
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
+ export function renderLogoStrip(block: LogoStripBlock, ctx: RenderContext): HtmlElement {
19
+ return h(
20
+ 'figure',
21
+ { class: 'cg-block cg-logo-band', 'data-block': 'logoStrip' },
22
+ h(
23
+ 'ul',
24
+ { class: 'cg-logo-band__items' },
25
+ block.logos.map((item) => renderItem(item, ctx)),
26
+ ),
27
+ block.caption === undefined
28
+ ? null
29
+ : h('figcaption', { class: 'cg-logo-band__caption', 'data-field': 'caption' }, block.caption),
30
+ )
31
+ }
@@ -0,0 +1,52 @@
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'
10
+
11
+ /**
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.
15
+ */
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
+ )
33
+ }
34
+
35
+ export function renderLogos(block: LogosBlock, ctx: RenderContext): HtmlElement {
36
+ return h(
37
+ '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
+ ),
46
+ h(
47
+ 'ul',
48
+ { class: 'cg-logos__items' },
49
+ block.items.map((item) => renderItem(item, ctx)),
50
+ ),
51
+ )
52
+ }
@@ -0,0 +1,36 @@
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>` rather than a div and a paragraph: the
6
+ * association between the picture and its caption is then in the markup,
7
+ * and a screen reader announces the caption as belonging to the image
8
+ * instead of as loose text after it.
9
+ */
10
+ export function renderMediaFigure(block: MediaFigureBlock, ctx: RenderContext): HtmlElement {
11
+ const ratio = aspectRatio(block.ratio)
12
+ const hasCaption = block.caption !== undefined || block.credit !== undefined
13
+ return h(
14
+ 'figure',
15
+ {
16
+ class: 'cg-block cg-figure',
17
+ 'data-block': 'mediaFigure',
18
+ 'data-align': block.align ?? 'center',
19
+ style: ratio === undefined ? undefined : `--cg-ratio:${ratio}`,
20
+ },
21
+ image(ctx, block.media, {
22
+ className: 'cg-figure__media',
23
+ sizes: '(min-width: 45rem) 40rem, 100vw',
24
+ }),
25
+ hasCaption
26
+ ? h(
27
+ 'figcaption',
28
+ { class: 'cg-figure__caption' },
29
+ block.caption,
30
+ block.credit === undefined
31
+ ? null
32
+ : h('span', { class: 'cg-figure__credit', 'data-field': 'credit' }, block.credit),
33
+ )
34
+ : null,
35
+ )
36
+ }