@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.
- package/LICENSE +373 -0
- package/dist/src/index.d.ts +21 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +21 -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 +15 -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 +77 -0
- package/dist/src/render/blocks/collection-list.js.map +1 -0
- package/dist/src/render/blocks/cta.d.ts +4 -0
- package/dist/src/render/blocks/cta.d.ts.map +1 -0
- package/dist/src/render/blocks/cta.js +7 -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 +71 -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 +15 -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 +27 -0
- package/dist/src/render/blocks/feature-grid.js.map +1 -0
- package/dist/src/render/blocks/gallery.d.ts +10 -0
- package/dist/src/render/blocks/gallery.d.ts.map +1 -0
- package/dist/src/render/blocks/gallery.js +19 -0
- package/dist/src/render/blocks/gallery.js.map +1 -0
- package/dist/src/render/blocks/hero.d.ts +16 -0
- package/dist/src/render/blocks/hero.d.ts.map +1 -0
- package/dist/src/render/blocks/hero.js +28 -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 +16 -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 +22 -0
- package/dist/src/render/blocks/logos.js.map +1 -0
- package/dist/src/render/blocks/media-figure.d.ts +10 -0
- package/dist/src/render/blocks/media-figure.d.ts.map +1 -0
- package/dist/src/render/blocks/media-figure.js +25 -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 +28 -0
- package/dist/src/render/blocks/pricing-table.js.map +1 -0
- package/dist/src/render/blocks/prose.d.ts +4 -0
- package/dist/src/render/blocks/prose.d.ts.map +1 -0
- package/dist/src/render/blocks/prose.js +36 -0
- package/dist/src/render/blocks/prose.js.map +1 -0
- package/dist/src/render/blocks/quote.d.ts +9 -0
- package/dist/src/render/blocks/quote.d.ts.map +1 -0
- package/dist/src/render/blocks/quote.js +22 -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 +15 -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 +11 -0
- package/dist/src/render/blocks/stats.js.map +1 -0
- package/dist/src/render/blocks/testimonial.d.ts +11 -0
- package/dist/src/render/blocks/testimonial.d.ts.map +1 -0
- package/dist/src/render/blocks/testimonial.js +20 -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 +94 -0
- package/dist/src/render/chrome.js.map +1 -0
- package/dist/src/render/render-block.d.ts +15 -0
- package/dist/src/render/render-block.d.ts.map +1 -0
- package/dist/src/render/render-block.js +176 -0
- package/dist/src/render/render-block.js.map +1 -0
- package/dist/src/render/term-archive.d.ts +10 -0
- package/dist/src/render/term-archive.d.ts.map +1 -0
- package/dist/src/render/term-archive.js +32 -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 +20 -0
- package/src/render/blocks/accordion.ts +51 -0
- package/src/render/blocks/collection-list.ts +170 -0
- package/src/render/blocks/cta.ts +25 -0
- package/src/render/blocks/embed.ts +94 -0
- package/src/render/blocks/faq.ts +46 -0
- package/src/render/blocks/feature-grid.ts +67 -0
- package/src/render/blocks/gallery.ts +39 -0
- package/src/render/blocks/hero.ts +61 -0
- package/src/render/blocks/logo-strip.ts +31 -0
- package/src/render/blocks/logos.ts +52 -0
- package/src/render/blocks/media-figure.ts +36 -0
- package/src/render/blocks/pricing-table.ts +72 -0
- package/src/render/blocks/prose.ts +49 -0
- package/src/render/blocks/quote.ts +35 -0
- package/src/render/blocks/stat-counter.ts +37 -0
- package/src/render/blocks/stats.ts +38 -0
- package/src/render/blocks/testimonial.ts +36 -0
- package/src/render/chrome.ts +124 -0
- package/src/render/render-block.ts +305 -0
- package/src/render/term-archive.ts +78 -0
- package/src/styles/archive.css +39 -0
- package/src/styles/base.css +1106 -0
- package/src/styles/blocks.css +1238 -0
- package/src/styles/theme.css +34 -0
- package/src/styles/tokens.css +307 -0
- package/tokens.json +36 -0
|
@@ -0,0 +1,72 @@
|
|
|
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). Bordered plates, one lifted (border + a small
|
|
15
|
+
* "recommended" rule) when `highlighted` is set — an editorial signal,
|
|
16
|
+
* never a colour: it becomes `data-highlighted` for the stylesheet, and
|
|
17
|
+
* `aria-current` so the emphasis is announced, not only shown.
|
|
18
|
+
*/
|
|
19
|
+
function renderTier(tier: PricingTier, ctx: RenderContext, tag: HeadingTag): HtmlElement {
|
|
20
|
+
return h(
|
|
21
|
+
'li',
|
|
22
|
+
{
|
|
23
|
+
class: 'cg-pricing__tier',
|
|
24
|
+
'data-highlighted': tier.highlighted === true ? 'true' : undefined,
|
|
25
|
+
'aria-current': tier.highlighted === true ? 'true' : undefined,
|
|
26
|
+
},
|
|
27
|
+
heading(tag, { class: 'cg-pricing__name' }, tier.name),
|
|
28
|
+
h(
|
|
29
|
+
'p',
|
|
30
|
+
{ class: 'cg-pricing__price' },
|
|
31
|
+
h('span', { class: 'cg-pricing__amount' }, tier.price),
|
|
32
|
+
tier.interval === undefined
|
|
33
|
+
? null
|
|
34
|
+
: h('span', { class: 'cg-pricing__interval' }, tier.interval),
|
|
35
|
+
),
|
|
36
|
+
tier.features.length === 0
|
|
37
|
+
? null
|
|
38
|
+
: h(
|
|
39
|
+
'ul',
|
|
40
|
+
{ class: 'cg-pricing__features' },
|
|
41
|
+
tier.features.map((feature) => h('li', { class: 'cg-pricing__feature' }, feature)),
|
|
42
|
+
),
|
|
43
|
+
tier.action === undefined
|
|
44
|
+
? null
|
|
45
|
+
: h(
|
|
46
|
+
'div',
|
|
47
|
+
{ class: 'cg-pricing__action' },
|
|
48
|
+
actionLink(ctx, { ...tier.action, emphasis: tier.action.emphasis ?? 'primary' }),
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function renderPricingTable(block: PricingTableBlock, ctx: RenderContext): HtmlElement {
|
|
54
|
+
const hasTitle = block.title !== undefined
|
|
55
|
+
const tierTag = nestedHeadingTag('pricingTable', hasTitle)
|
|
56
|
+
return h(
|
|
57
|
+
'section',
|
|
58
|
+
{ class: 'cg-block cg-pricing', 'data-block': 'pricingTable' },
|
|
59
|
+
hasTitle
|
|
60
|
+
? heading(
|
|
61
|
+
blockHeadingTag('pricingTable') ?? 'h2',
|
|
62
|
+
{ class: 'cg-pricing__title', 'data-field': 'title' },
|
|
63
|
+
block.title ?? '',
|
|
64
|
+
)
|
|
65
|
+
: null,
|
|
66
|
+
h(
|
|
67
|
+
'ul',
|
|
68
|
+
{ class: 'cg-pricing__tiers' },
|
|
69
|
+
block.tiers.map((tier) => renderTier(tier, ctx, tierTag)),
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ProseBlock } from '@cogenta/blocks'
|
|
2
|
+
import {
|
|
3
|
+
type HtmlElement,
|
|
4
|
+
type HtmlNode,
|
|
5
|
+
h,
|
|
6
|
+
type RenderContext,
|
|
7
|
+
renderRichText,
|
|
8
|
+
} from '@cogenta/theme-kit'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `prose` declares `headingLevel: 'none'`: it contributes no heading of its
|
|
12
|
+
* own. Whatever headings appear come from the rich text document, whose
|
|
13
|
+
* vocabulary starts at `h2`.
|
|
14
|
+
*
|
|
15
|
+
* Contract A's rich text schema (`@cogenta/schema`, frozen) has no fenced
|
|
16
|
+
* "code block" node — the only code-shaped thing it can carry is a
|
|
17
|
+
* paragraph whose sole span carries the `code` mark, which
|
|
18
|
+
* `renderRichText` (`@cogenta/theme-kit`) already turns into
|
|
19
|
+
* `<p><code>…</code></p>`. A documentation theme without a real, multi-line
|
|
20
|
+
* code block would be dishonest about what it is for, so this is the one
|
|
21
|
+
* place in the theme that post-processes theme-kit's own output rather than
|
|
22
|
+
* writing a second rich-text renderer: a `<p>` whose only child is a bare
|
|
23
|
+
* `<code>` is promoted to `<pre><code>`, everything else is untouched. The
|
|
24
|
+
* transform only ever changes two tag names on an already-escaped tree — no
|
|
25
|
+
* new content is invented, and R3 (a block never stores HTML) still holds,
|
|
26
|
+
* since the source is still the same structured span the editor wrote.
|
|
27
|
+
*/
|
|
28
|
+
function promoteCodeBlocks(nodes: readonly HtmlNode[]): readonly HtmlNode[] {
|
|
29
|
+
return nodes.map((node) => {
|
|
30
|
+
if (node.kind !== 'element') return node
|
|
31
|
+
if (
|
|
32
|
+
node.tag === 'p' &&
|
|
33
|
+
node.children.length === 1 &&
|
|
34
|
+
node.children[0]?.kind === 'element' &&
|
|
35
|
+
node.children[0].tag === 'code'
|
|
36
|
+
) {
|
|
37
|
+
return h('pre', { class: 'cg-prose__code' }, node.children[0])
|
|
38
|
+
}
|
|
39
|
+
return node
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderProse(block: ProseBlock, ctx: RenderContext): HtmlElement {
|
|
44
|
+
return h(
|
|
45
|
+
'div',
|
|
46
|
+
{ class: 'cg-block cg-prose', 'data-block': 'prose' },
|
|
47
|
+
promoteCodeBlocks(renderRichText(ctx, block.body)),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { QuoteBlock } from '@cogenta/blocks'
|
|
2
|
+
import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `<figure><blockquote>…</blockquote><figcaption>` is the attribution
|
|
6
|
+
* pattern the HTML spec prescribes: putting the author inside the
|
|
7
|
+
* `<blockquote>` would claim the author's name is part of what was said.
|
|
8
|
+
*/
|
|
9
|
+
export function renderQuote(block: QuoteBlock, ctx: RenderContext): HtmlElement {
|
|
10
|
+
const hasAttribution =
|
|
11
|
+
block.author !== undefined || block.role !== undefined || block.avatar !== undefined
|
|
12
|
+
return h(
|
|
13
|
+
'figure',
|
|
14
|
+
{ class: 'cg-block cg-quote', 'data-block': 'quote' },
|
|
15
|
+
h('blockquote', { class: 'cg-quote__text' }, h('p', { 'data-field': 'text' }, block.text)),
|
|
16
|
+
hasAttribution
|
|
17
|
+
? h(
|
|
18
|
+
'figcaption',
|
|
19
|
+
{ class: 'cg-quote__attribution' },
|
|
20
|
+
block.avatar === undefined
|
|
21
|
+
? null
|
|
22
|
+
: image(ctx, block.avatar, {
|
|
23
|
+
className: 'cg-quote__avatar',
|
|
24
|
+
variant: { width: 96, height: 96, fit: 'cover' },
|
|
25
|
+
}),
|
|
26
|
+
block.author === undefined
|
|
27
|
+
? null
|
|
28
|
+
: h('span', { class: 'cg-quote__author', 'data-field': 'author' }, block.author),
|
|
29
|
+
block.role === undefined
|
|
30
|
+
? null
|
|
31
|
+
: h('span', { class: 'cg-quote__role', 'data-field': 'role' }, block.role),
|
|
32
|
+
)
|
|
33
|
+
: null,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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). A `<dl>`, exactly like `stats`: the label sits
|
|
12
|
+
* before the figure in markup (WCAG 1.3.2); the stylesheet paints the big
|
|
13
|
+
* figure first.
|
|
14
|
+
*/
|
|
15
|
+
function renderItem(item: StatCounterItem): HtmlElement {
|
|
16
|
+
return h(
|
|
17
|
+
'div',
|
|
18
|
+
{ class: 'cg-kpi' },
|
|
19
|
+
h('dt', { class: 'cg-kpi__label' }, item.label),
|
|
20
|
+
h('dd', { class: 'cg-kpi__value' }, item.value),
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function renderStatCounter(block: StatCounterBlock, _ctx: RenderContext): HtmlElement {
|
|
25
|
+
return h(
|
|
26
|
+
'section',
|
|
27
|
+
{ class: 'cg-block cg-kpis', 'data-block': 'statCounter' },
|
|
28
|
+
block.title === undefined
|
|
29
|
+
? null
|
|
30
|
+
: heading(
|
|
31
|
+
blockHeadingTag('statCounter') ?? 'h2',
|
|
32
|
+
{ class: 'cg-kpis__title', 'data-field': 'title' },
|
|
33
|
+
block.title,
|
|
34
|
+
),
|
|
35
|
+
h('dl', { class: 'cg-kpis__items' }, block.stats.map(renderItem)),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
/** A description list: each figure is the description of its label. */
|
|
11
|
+
function renderItem(item: StatItem): HtmlElement {
|
|
12
|
+
return h(
|
|
13
|
+
'div',
|
|
14
|
+
{ class: 'cg-stat' },
|
|
15
|
+
h('dt', { class: 'cg-stat__label' }, item.label),
|
|
16
|
+
h(
|
|
17
|
+
'dd',
|
|
18
|
+
{ class: 'cg-stat__value' },
|
|
19
|
+
item.value,
|
|
20
|
+
item.unit === undefined ? null : h('span', { class: 'cg-stat__unit' }, item.unit),
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function renderStats(block: StatsBlock, _ctx: RenderContext): HtmlElement {
|
|
26
|
+
return h(
|
|
27
|
+
'section',
|
|
28
|
+
{ class: 'cg-block cg-stats', 'data-block': 'stats' },
|
|
29
|
+
block.title === undefined
|
|
30
|
+
? null
|
|
31
|
+
: heading(
|
|
32
|
+
blockHeadingTag('stats') ?? 'h2',
|
|
33
|
+
{ class: 'cg-stats__title', 'data-field': 'title' },
|
|
34
|
+
block.title,
|
|
35
|
+
),
|
|
36
|
+
h('dl', { class: 'cg-stats__items' }, block.items.map(renderItem)),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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). The same `<figure><blockquote>…</blockquote>
|
|
6
|
+
* <figcaption>` pattern `quote` uses, kept consistent so the two read as
|
|
7
|
+
* siblings — the quote itself is rich text here, and the attribution is
|
|
8
|
+
* the block's single grouped `attribution` field rather than three loose
|
|
9
|
+
* ones.
|
|
10
|
+
*/
|
|
11
|
+
export function renderTestimonial(block: TestimonialBlock, ctx: RenderContext): HtmlElement {
|
|
12
|
+
const { attribution } = block
|
|
13
|
+
return h(
|
|
14
|
+
'figure',
|
|
15
|
+
{ class: 'cg-block cg-testimonial', 'data-block': 'testimonial' },
|
|
16
|
+
h('blockquote', { class: 'cg-testimonial__quote' }, renderRichText(ctx, block.quote)),
|
|
17
|
+
h(
|
|
18
|
+
'figcaption',
|
|
19
|
+
{ class: 'cg-testimonial__attribution' },
|
|
20
|
+
attribution.avatar === undefined
|
|
21
|
+
? null
|
|
22
|
+
: image(ctx, attribution.avatar, {
|
|
23
|
+
className: 'cg-testimonial__avatar',
|
|
24
|
+
variant: { width: 96, height: 96, fit: 'cover' },
|
|
25
|
+
}),
|
|
26
|
+
h(
|
|
27
|
+
'span',
|
|
28
|
+
{ class: 'cg-testimonial__who' },
|
|
29
|
+
h('span', { class: 'cg-testimonial__name' }, attribution.name),
|
|
30
|
+
attribution.role === undefined
|
|
31
|
+
? null
|
|
32
|
+
: h('span', { class: 'cg-testimonial__role' }, attribution.role),
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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 — structurally distinct from the
|
|
15
|
+
* canonical theme's, not a recolour of it: a docs site's header carries a
|
|
16
|
+
* genuine CSS-only mobile menu (a `<details>` disclosure, never a script),
|
|
17
|
+
* because "Docs / Guides / Reference / Blog" plus a header action does not
|
|
18
|
+
* fit a narrow viewport the way three links do.
|
|
19
|
+
*
|
|
20
|
+
* The mobile panel is a **second, hidden-by-default copy** of the same nav
|
|
21
|
+
* markup, not a repositioned single copy: two static layouts (`display:none`
|
|
22
|
+
* either way at a given viewport width) is the only zero-JS way to put the
|
|
23
|
+
* links inline on a wide screen and inside a disclosure on a narrow one.
|
|
24
|
+
* `display:none` removes the inactive copy from the accessibility tree, so a
|
|
25
|
+
* screen reader is never offered two "Primary" navigations at once — only
|
|
26
|
+
* the sighted, CSS-driven layout differs.
|
|
27
|
+
*
|
|
28
|
+
* Footer: brand column (name + tagline), a footer nav column, social links,
|
|
29
|
+
* a short "about" note, and Cogenta's own credit (or its white-label
|
|
30
|
+
* replacement) — `brandingHtml` placed exactly once, unaltered.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
function navItems(links: readonly ChromeNavLink[]): string {
|
|
34
|
+
return links
|
|
35
|
+
.filter((link) => link.href !== null || link.kind === 'submenu-placeholder')
|
|
36
|
+
.map((link) => {
|
|
37
|
+
const label = escapeText(link.label)
|
|
38
|
+
const titleAttr = link.title === null ? '' : ` title="${escapeAttribute(link.title)}"`
|
|
39
|
+
if (link.href === null) return `<li><span${titleAttr}>${label}</span></li>`
|
|
40
|
+
const href = escapeAttribute(link.href)
|
|
41
|
+
const target = link.openInNewTab ? ' target="_blank" rel="noopener"' : ''
|
|
42
|
+
return `<li><a href="${href}"${target}${titleAttr}>${label}</a></li>`
|
|
43
|
+
})
|
|
44
|
+
.join('')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderNav(links: readonly ChromeNavLink[], className: string, label: string): string {
|
|
48
|
+
const items = navItems(links)
|
|
49
|
+
return items === ''
|
|
50
|
+
? ''
|
|
51
|
+
: `<nav class="${className}" aria-label="${label}"><ul class="cg-menu">${items}</ul></nav>`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The header's own call-to-action link (`theme@1.4`), reusing `.cg-action`'s own look. */
|
|
55
|
+
function renderHeaderAction(action: ChromeInput['headerAction'], className: string): string {
|
|
56
|
+
if (action === undefined) return ''
|
|
57
|
+
return (
|
|
58
|
+
`<a class="cg-action ${className}" data-emphasis="primary" ` +
|
|
59
|
+
`href="${escapeAttribute(action.href)}">${escapeText(action.label)}</a>`
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Three bars, drawn once for this file — not part of `renderIcon`'s closed vocabulary, since a "menu" glyph names no contract B field. */
|
|
64
|
+
const HAMBURGER_SVG =
|
|
65
|
+
'<svg class="cg-nav-toggle__icon" viewBox="0 0 24 24" width="22" height="22" fill="none" ' +
|
|
66
|
+
'stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false">' +
|
|
67
|
+
'<path d="M4 6h16M4 12h16M4 18h16"/></svg>'
|
|
68
|
+
|
|
69
|
+
export function renderChrome(input: ChromeInput): ChromeResult {
|
|
70
|
+
const siteName = escapeAttribute(input.site.name)
|
|
71
|
+
const siteNameText = escapeText(input.site.name)
|
|
72
|
+
const mark = renderBrandMark(input.brand, { className: 'cg-site-header__logo' }) ?? siteNameText
|
|
73
|
+
|
|
74
|
+
const desktopNav = renderNav(input.headerNav, 'cg-site-header__nav', 'Primary')
|
|
75
|
+
const themeToggle = serialize(renderThemeToggle(input.locale, { className: 'cg-theme-toggle' }))
|
|
76
|
+
const desktopAction = renderHeaderAction(input.headerAction, 'cg-site-header__action')
|
|
77
|
+
|
|
78
|
+
const mobileNavItems = navItems(input.headerNav)
|
|
79
|
+
const mobileAction = renderHeaderAction(input.headerAction, 'cg-nav-toggle__action')
|
|
80
|
+
const mobilePanel =
|
|
81
|
+
mobileNavItems === '' && mobileAction === ''
|
|
82
|
+
? ''
|
|
83
|
+
: `<nav class="cg-nav-toggle__panel" aria-label="Primary">` +
|
|
84
|
+
`${mobileNavItems === '' ? '' : `<ul class="cg-menu">${mobileNavItems}</ul>`}` +
|
|
85
|
+
`${mobileAction}</nav>`
|
|
86
|
+
|
|
87
|
+
const toggle =
|
|
88
|
+
mobilePanel === ''
|
|
89
|
+
? ''
|
|
90
|
+
: `<details class="cg-nav-toggle"><summary class="cg-nav-toggle__button" aria-label="Menu">${HAMBURGER_SVG}</summary>${mobilePanel}</details>`
|
|
91
|
+
|
|
92
|
+
const header =
|
|
93
|
+
`<header class="cg-site-header"><div class="cg-site-header__inner">` +
|
|
94
|
+
`<a class="cg-site-header__home" href="${escapeAttribute(input.homeHref)}">${mark}</a>` +
|
|
95
|
+
`${desktopNav}${themeToggle}${desktopAction}${toggle}` +
|
|
96
|
+
`</div></header>`
|
|
97
|
+
|
|
98
|
+
const footerNav = renderNav(input.footerNav, 'cg-site-footer__nav', 'Footer')
|
|
99
|
+
const tagline =
|
|
100
|
+
input.tagline === undefined
|
|
101
|
+
? ''
|
|
102
|
+
: `<p class="cg-site-footer__tagline">${escapeText(input.tagline)}</p>`
|
|
103
|
+
const socialElement =
|
|
104
|
+
input.social === undefined
|
|
105
|
+
? null
|
|
106
|
+
: renderSocialLinks(input.social, {
|
|
107
|
+
className: 'cg-site-footer__social',
|
|
108
|
+
itemClassName: 'cg-site-footer__social-item',
|
|
109
|
+
})
|
|
110
|
+
const social = socialElement === null ? '' : serialize(socialElement)
|
|
111
|
+
const footerNote =
|
|
112
|
+
input.footerNote === undefined
|
|
113
|
+
? ''
|
|
114
|
+
: `<p class="cg-site-footer__note">${escapeText(input.footerNote)}</p>`
|
|
115
|
+
|
|
116
|
+
const footer =
|
|
117
|
+
`<footer class="cg-site-footer"><div class="cg-site-footer__grid">` +
|
|
118
|
+
`<div class="cg-site-footer__brand"><a href="${escapeAttribute(input.homeHref)}">${siteName}</a>${tagline}${social}</div>` +
|
|
119
|
+
`<div class="cg-site-footer__nav-col">${footerNav}</div>` +
|
|
120
|
+
`<div class="cg-site-footer__about-col">${footerNote}<div class="cg-site-footer__branding">${input.brandingHtml}</div></div>` +
|
|
121
|
+
`</div><div class="cg-site-footer__bottom"><span>${siteName}</span></div></footer>`
|
|
122
|
+
|
|
123
|
+
return { header, footer }
|
|
124
|
+
}
|