@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.
- package/LICENSE +373 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +16 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/render/blocks/accordion.d.ts +4 -0
- package/dist/src/render/blocks/accordion.d.ts.map +1 -0
- package/dist/src/render/blocks/accordion.js +24 -0
- package/dist/src/render/blocks/accordion.js.map +1 -0
- package/dist/src/render/blocks/collection-list.d.ts +14 -0
- package/dist/src/render/blocks/collection-list.d.ts.map +1 -0
- package/dist/src/render/blocks/collection-list.js +55 -0
- package/dist/src/render/blocks/collection-list.js.map +1 -0
- package/dist/src/render/blocks/cta.d.ts +12 -0
- package/dist/src/render/blocks/cta.d.ts.map +1 -0
- package/dist/src/render/blocks/cta.js +19 -0
- package/dist/src/render/blocks/cta.js.map +1 -0
- package/dist/src/render/blocks/embed.d.ts +4 -0
- package/dist/src/render/blocks/embed.d.ts.map +1 -0
- package/dist/src/render/blocks/embed.js +92 -0
- package/dist/src/render/blocks/embed.js.map +1 -0
- package/dist/src/render/blocks/faq.d.ts +4 -0
- package/dist/src/render/blocks/faq.d.ts.map +1 -0
- package/dist/src/render/blocks/faq.js +25 -0
- package/dist/src/render/blocks/faq.js.map +1 -0
- package/dist/src/render/blocks/feature-grid.d.ts +4 -0
- package/dist/src/render/blocks/feature-grid.d.ts.map +1 -0
- package/dist/src/render/blocks/feature-grid.js +33 -0
- package/dist/src/render/blocks/feature-grid.js.map +1 -0
- package/dist/src/render/blocks/gallery.d.ts +15 -0
- package/dist/src/render/blocks/gallery.d.ts.map +1 -0
- package/dist/src/render/blocks/gallery.js +28 -0
- package/dist/src/render/blocks/gallery.js.map +1 -0
- package/dist/src/render/blocks/hero.d.ts +15 -0
- package/dist/src/render/blocks/hero.d.ts.map +1 -0
- package/dist/src/render/blocks/hero.js +31 -0
- package/dist/src/render/blocks/hero.js.map +1 -0
- package/dist/src/render/blocks/logo-strip.d.ts +4 -0
- package/dist/src/render/blocks/logo-strip.d.ts.map +1 -0
- package/dist/src/render/blocks/logo-strip.js +21 -0
- package/dist/src/render/blocks/logo-strip.js.map +1 -0
- package/dist/src/render/blocks/logos.d.ts +4 -0
- package/dist/src/render/blocks/logos.d.ts.map +1 -0
- package/dist/src/render/blocks/logos.js +30 -0
- package/dist/src/render/blocks/logos.js.map +1 -0
- package/dist/src/render/blocks/media-figure.d.ts +13 -0
- package/dist/src/render/blocks/media-figure.d.ts.map +1 -0
- package/dist/src/render/blocks/media-figure.js +28 -0
- package/dist/src/render/blocks/media-figure.js.map +1 -0
- package/dist/src/render/blocks/pricing-table.d.ts +4 -0
- package/dist/src/render/blocks/pricing-table.d.ts.map +1 -0
- package/dist/src/render/blocks/pricing-table.js +33 -0
- package/dist/src/render/blocks/pricing-table.js.map +1 -0
- package/dist/src/render/blocks/prose.d.ts +9 -0
- package/dist/src/render/blocks/prose.d.ts.map +1 -0
- package/dist/src/render/blocks/prose.js +10 -0
- package/dist/src/render/blocks/prose.js.map +1 -0
- package/dist/src/render/blocks/quote.d.ts +18 -0
- package/dist/src/render/blocks/quote.d.ts.map +1 -0
- package/dist/src/render/blocks/quote.js +31 -0
- package/dist/src/render/blocks/quote.js.map +1 -0
- package/dist/src/render/blocks/stat-counter.d.ts +4 -0
- package/dist/src/render/blocks/stat-counter.d.ts.map +1 -0
- package/dist/src/render/blocks/stat-counter.js +23 -0
- package/dist/src/render/blocks/stat-counter.js.map +1 -0
- package/dist/src/render/blocks/stats.d.ts +4 -0
- package/dist/src/render/blocks/stats.d.ts.map +1 -0
- package/dist/src/render/blocks/stats.js +23 -0
- package/dist/src/render/blocks/stats.js.map +1 -0
- package/dist/src/render/blocks/testimonial.d.ts +16 -0
- package/dist/src/render/blocks/testimonial.d.ts.map +1 -0
- package/dist/src/render/blocks/testimonial.js +25 -0
- package/dist/src/render/blocks/testimonial.js.map +1 -0
- package/dist/src/render/chrome.d.ts +3 -0
- package/dist/src/render/chrome.d.ts.map +1 -0
- package/dist/src/render/chrome.js +101 -0
- package/dist/src/render/chrome.js.map +1 -0
- package/dist/src/render/render-block.d.ts +24 -0
- package/dist/src/render/render-block.d.ts.map +1 -0
- package/dist/src/render/render-block.js +108 -0
- package/dist/src/render/render-block.js.map +1 -0
- package/dist/src/render/term-archive.d.ts +12 -0
- package/dist/src/render/term-archive.d.ts.map +1 -0
- package/dist/src/render/term-archive.js +34 -0
- package/dist/src/render/term-archive.js.map +1 -0
- package/dist/theme.config.d.ts +16 -0
- package/dist/theme.config.d.ts.map +1 -0
- package/dist/theme.config.js +46 -0
- package/dist/theme.config.js.map +1 -0
- package/package.json +57 -0
- package/src/index.ts +15 -0
- package/src/render/blocks/accordion.ts +60 -0
- package/src/render/blocks/collection-list.ts +122 -0
- package/src/render/blocks/cta.ts +48 -0
- package/src/render/blocks/embed.ts +115 -0
- package/src/render/blocks/faq.ts +61 -0
- package/src/render/blocks/feature-grid.ts +73 -0
- package/src/render/blocks/gallery.ts +48 -0
- package/src/render/blocks/hero.ts +60 -0
- package/src/render/blocks/logo-strip.ts +36 -0
- package/src/render/blocks/logos.ts +60 -0
- package/src/render/blocks/media-figure.ts +43 -0
- package/src/render/blocks/pricing-table.ts +77 -0
- package/src/render/blocks/prose.ts +11 -0
- package/src/render/blocks/quote.ts +48 -0
- package/src/render/blocks/stat-counter.ts +45 -0
- package/src/render/blocks/stats.ts +50 -0
- package/src/render/blocks/testimonial.ts +41 -0
- package/src/render/chrome.ts +128 -0
- package/src/render/render-block.ts +144 -0
- package/src/render/term-archive.ts +80 -0
- package/src/styles/archive.css +68 -0
- package/src/styles/base.css +895 -0
- package/src/styles/blocks.css +1433 -0
- package/src/styles/theme.css +36 -0
- package/src/styles/tokens.css +326 -0
- 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
|
+
}
|