@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,305 @@
|
|
|
1
|
+
import { type BlockRegistry, VOCABULARY_NAMES, type VocabularyBlock } from '@cogenta/blocks'
|
|
2
|
+
import {
|
|
3
|
+
type ContentEntry,
|
|
4
|
+
entryHref,
|
|
5
|
+
entryTitle,
|
|
6
|
+
type FetchedEntries,
|
|
7
|
+
type HtmlElement,
|
|
8
|
+
h,
|
|
9
|
+
type PageContent,
|
|
10
|
+
pageHasOwnHeading,
|
|
11
|
+
type RenderContext,
|
|
12
|
+
renderEntryHeader,
|
|
13
|
+
resolveBlockForRender,
|
|
14
|
+
withBlockKey,
|
|
15
|
+
withBlockVariant,
|
|
16
|
+
} from '@cogenta/theme-kit'
|
|
17
|
+
import { renderAccordion } from './blocks/accordion.js'
|
|
18
|
+
import { renderCollectionList } from './blocks/collection-list.js'
|
|
19
|
+
import { renderCta } from './blocks/cta.js'
|
|
20
|
+
import { renderEmbed } from './blocks/embed.js'
|
|
21
|
+
import { renderFaq } from './blocks/faq.js'
|
|
22
|
+
import { renderFeatureGrid } from './blocks/feature-grid.js'
|
|
23
|
+
import { renderGallery } from './blocks/gallery.js'
|
|
24
|
+
import { renderHero } from './blocks/hero.js'
|
|
25
|
+
import { renderLogoStrip } from './blocks/logo-strip.js'
|
|
26
|
+
import { renderLogos } from './blocks/logos.js'
|
|
27
|
+
import { renderMediaFigure } from './blocks/media-figure.js'
|
|
28
|
+
import { renderPricingTable } from './blocks/pricing-table.js'
|
|
29
|
+
import { renderProse } from './blocks/prose.js'
|
|
30
|
+
import { renderQuote } from './blocks/quote.js'
|
|
31
|
+
import { renderStatCounter } from './blocks/stat-counter.js'
|
|
32
|
+
import { renderStats } from './blocks/stats.js'
|
|
33
|
+
import { renderTestimonial } from './blocks/testimonial.js'
|
|
34
|
+
|
|
35
|
+
export type { FetchedEntries, PageContent }
|
|
36
|
+
|
|
37
|
+
export function renderBlock(
|
|
38
|
+
block: VocabularyBlock,
|
|
39
|
+
ctx: RenderContext,
|
|
40
|
+
entries: FetchedEntries = {},
|
|
41
|
+
registry?: BlockRegistry,
|
|
42
|
+
): HtmlElement | null {
|
|
43
|
+
const resolved = resolveBlockForRender(block, VOCABULARY_NAMES, registry)
|
|
44
|
+
if (resolved === null) return null
|
|
45
|
+
const known = resolved as unknown as VocabularyBlock
|
|
46
|
+
return withBlockVariant(renderKnownBlock(known, ctx, entries), known.variant)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderKnownBlock(
|
|
50
|
+
known: VocabularyBlock,
|
|
51
|
+
ctx: RenderContext,
|
|
52
|
+
entries: FetchedEntries,
|
|
53
|
+
): HtmlElement | null {
|
|
54
|
+
switch (known._type) {
|
|
55
|
+
case 'hero':
|
|
56
|
+
return renderHero(known, ctx)
|
|
57
|
+
case 'prose':
|
|
58
|
+
return renderProse(known, ctx)
|
|
59
|
+
case 'mediaFigure':
|
|
60
|
+
return renderMediaFigure(known, ctx)
|
|
61
|
+
case 'featureGrid':
|
|
62
|
+
return renderFeatureGrid(known, ctx)
|
|
63
|
+
case 'cta':
|
|
64
|
+
return renderCta(known, ctx)
|
|
65
|
+
case 'gallery':
|
|
66
|
+
return renderGallery(known, ctx)
|
|
67
|
+
case 'quote':
|
|
68
|
+
return renderQuote(known, ctx)
|
|
69
|
+
case 'faq':
|
|
70
|
+
return renderFaq(known, ctx)
|
|
71
|
+
case 'stats':
|
|
72
|
+
return renderStats(known, ctx)
|
|
73
|
+
case 'logos':
|
|
74
|
+
return renderLogos(known, ctx)
|
|
75
|
+
case 'collectionList':
|
|
76
|
+
return renderCollectionList(known, ctx, entries[known._key] ?? [])
|
|
77
|
+
case 'embed':
|
|
78
|
+
return renderEmbed(known, ctx)
|
|
79
|
+
case 'testimonial':
|
|
80
|
+
return renderTestimonial(known, ctx)
|
|
81
|
+
case 'pricingTable':
|
|
82
|
+
return renderPricingTable(known, ctx)
|
|
83
|
+
case 'accordion':
|
|
84
|
+
return renderAccordion(known, ctx)
|
|
85
|
+
case 'statCounter':
|
|
86
|
+
return renderStatCounter(known, ctx)
|
|
87
|
+
case 'logoStrip':
|
|
88
|
+
return renderLogoStrip(known, ctx)
|
|
89
|
+
default: {
|
|
90
|
+
const unreachable: never = known
|
|
91
|
+
void unreachable
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The name every doc page's own blueprint-seeded sidebar `collectionList`
|
|
99
|
+
* targets — checked structurally (first block, this collection) rather than
|
|
100
|
+
* by any flag on the block, because contract B carries no "this is chrome,
|
|
101
|
+
* not content" bit and inventing one would be a contract change for a
|
|
102
|
+
* single theme's layout choice.
|
|
103
|
+
*/
|
|
104
|
+
const DOC_PAGE_COLLECTION = 'doc_page'
|
|
105
|
+
|
|
106
|
+
function isDocSidebarBlock(block: VocabularyBlock): boolean {
|
|
107
|
+
return block._type === 'collectionList' && block.collection === DOC_PAGE_COLLECTION
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The sidebar entry whose own link resolves to the page being rendered — the "current page" the brief asks the sidebar to highlight and the breadcrumb to name. */
|
|
111
|
+
function findCurrentEntry(
|
|
112
|
+
entries: readonly ContentEntry[],
|
|
113
|
+
ctx: RenderContext,
|
|
114
|
+
): ContentEntry | undefined {
|
|
115
|
+
return entries.find((entry) => entryHref(entry, ctx) === ctx.url.pathname)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The doc-page sidebar — grouped by each entry's own `section` field, in the
|
|
120
|
+
* order the entries were fetched (the blueprint seeds them in `order`
|
|
121
|
+
* within a section already; `collectionList.sort` cannot name `section`/
|
|
122
|
+
* `order`, so an editor who reorders sections relies on the fetch order,
|
|
123
|
+
* exactly like the "All guides" index on the home page does).
|
|
124
|
+
*/
|
|
125
|
+
function renderSidebarPanel(
|
|
126
|
+
entries: readonly ContentEntry[],
|
|
127
|
+
ctx: RenderContext,
|
|
128
|
+
currentHref: string,
|
|
129
|
+
): HtmlElement {
|
|
130
|
+
const groups = new Map<string, ContentEntry[]>()
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
const section =
|
|
133
|
+
typeof entry.section === 'string' && entry.section !== ''
|
|
134
|
+
? entry.section
|
|
135
|
+
: ctx.t('entry.untitled')
|
|
136
|
+
const list = groups.get(section) ?? []
|
|
137
|
+
list.push(entry)
|
|
138
|
+
groups.set(section, list)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return h(
|
|
142
|
+
'nav',
|
|
143
|
+
{ class: 'cg-docs__nav-panel', 'aria-label': 'Documentation' },
|
|
144
|
+
[...groups.entries()].map(([section, sectionEntries]) =>
|
|
145
|
+
h(
|
|
146
|
+
'div',
|
|
147
|
+
{ class: 'cg-docs__nav-group' },
|
|
148
|
+
h('p', { class: 'cg-docs__nav-heading' }, section),
|
|
149
|
+
h(
|
|
150
|
+
'ul',
|
|
151
|
+
{ class: 'cg-docs__nav-items' },
|
|
152
|
+
sectionEntries.map((entry) => {
|
|
153
|
+
const href = entryHref(entry, ctx)
|
|
154
|
+
const isCurrent = href === currentHref
|
|
155
|
+
return h(
|
|
156
|
+
'li',
|
|
157
|
+
{},
|
|
158
|
+
h(
|
|
159
|
+
'a',
|
|
160
|
+
{
|
|
161
|
+
class: 'cg-docs__nav-link',
|
|
162
|
+
href,
|
|
163
|
+
'aria-current': isCurrent ? 'page' : undefined,
|
|
164
|
+
},
|
|
165
|
+
entryTitle(entry, ctx),
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
}),
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The sidebar, in two copies — the same zero-JS technique `chrome.ts` already
|
|
177
|
+
* uses for the header's own mobile menu, and for the same reason: Chrome (and
|
|
178
|
+
* every other engine implementing the current HTML rendering rules) hides a
|
|
179
|
+
* closed `<details>`'s non-summary content through its own internal
|
|
180
|
+
* `::details-content` box, not through the plain CSS `display: none` the spec
|
|
181
|
+
* text describes — so a stylesheet rule that sets `display: block` on that
|
|
182
|
+
* content, however specific, does not reliably bring it back while the
|
|
183
|
+
* element itself stays closed. Verified live: a build with a single
|
|
184
|
+
* `<details>` forced open only above the two-column breakpoint rendered an
|
|
185
|
+
* empty sidebar column at 1280px in a real Chrome tab, despite every
|
|
186
|
+
* computed style reporting `display: block`.
|
|
187
|
+
*
|
|
188
|
+
* The fix is the same one already proven for the header: a plain, always-
|
|
189
|
+
* live `<nav>` for the desktop column (nothing to collapse, so nothing for
|
|
190
|
+
* the browser to hide), and a **separate** `<details>` disclosure — a full
|
|
191
|
+
* second copy of the same panel — for narrow viewports. Exactly one of the
|
|
192
|
+
* two is ever `display: block` at a given viewport width (`base.css`), so a
|
|
193
|
+
* screen reader is never offered two "Documentation" navigations at once,
|
|
194
|
+
* matching the header's own accessibility guarantee.
|
|
195
|
+
*/
|
|
196
|
+
function renderSidebar(
|
|
197
|
+
entries: readonly ContentEntry[],
|
|
198
|
+
ctx: RenderContext,
|
|
199
|
+
currentHref: string,
|
|
200
|
+
): HtmlElement {
|
|
201
|
+
return h('div', { class: 'cg-docs__nav-desktop' }, renderSidebarPanel(entries, ctx, currentHref))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderSidebarMobile(
|
|
205
|
+
entries: readonly ContentEntry[],
|
|
206
|
+
ctx: RenderContext,
|
|
207
|
+
currentHref: string,
|
|
208
|
+
): HtmlElement {
|
|
209
|
+
return h(
|
|
210
|
+
'details',
|
|
211
|
+
{ class: 'cg-docs__nav-mobile' },
|
|
212
|
+
h('summary', { class: 'cg-docs__nav-toggle' }, 'On this site'),
|
|
213
|
+
renderSidebarPanel(entries, ctx, currentHref),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderBreadcrumb(section: string | undefined, title: string): HtmlElement {
|
|
218
|
+
return h(
|
|
219
|
+
'nav',
|
|
220
|
+
{ class: 'cg-docs__breadcrumb', 'aria-label': 'Breadcrumb' },
|
|
221
|
+
h(
|
|
222
|
+
'ol',
|
|
223
|
+
{},
|
|
224
|
+
section === undefined ? null : h('li', {}, section),
|
|
225
|
+
h('li', { 'aria-current': 'page' }, title),
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A doc page: two columns, CSS-only. The sidebar comes from the page's own
|
|
232
|
+
* *first* block — a `collectionList` on `doc_page` the blueprint seeds on
|
|
233
|
+
* every doc page for exactly this purpose — rendered as navigation rather
|
|
234
|
+
* than as a card list, and dropped from the ordinary block stream so it
|
|
235
|
+
* never also appears as content. The remaining blocks render in the content
|
|
236
|
+
* column, unchanged.
|
|
237
|
+
*/
|
|
238
|
+
function renderDocPage(
|
|
239
|
+
page: PageContent,
|
|
240
|
+
ctx: RenderContext,
|
|
241
|
+
sidebarBlock: VocabularyBlock,
|
|
242
|
+
entries: FetchedEntries,
|
|
243
|
+
registry: BlockRegistry | undefined,
|
|
244
|
+
): HtmlElement {
|
|
245
|
+
const sidebarEntries = entries[sidebarBlock._key] ?? []
|
|
246
|
+
const currentHref = ctx.url.pathname
|
|
247
|
+
const current = findCurrentEntry(sidebarEntries, ctx)
|
|
248
|
+
const section = typeof current?.section === 'string' ? current.section : undefined
|
|
249
|
+
const contentBlocks = page.blocks.slice(1)
|
|
250
|
+
const hasOwnHeading = pageHasOwnHeading(contentBlocks)
|
|
251
|
+
const entryHeader = renderEntryHeader(page, ctx)
|
|
252
|
+
|
|
253
|
+
return h(
|
|
254
|
+
'main',
|
|
255
|
+
{ class: 'cg-main cg-docs', id: 'cg-main' },
|
|
256
|
+
withBlockKey(
|
|
257
|
+
h(
|
|
258
|
+
'div',
|
|
259
|
+
{ class: 'cg-docs__nav' },
|
|
260
|
+
renderSidebar(sidebarEntries, ctx, currentHref),
|
|
261
|
+
renderSidebarMobile(sidebarEntries, ctx, currentHref),
|
|
262
|
+
),
|
|
263
|
+
sidebarBlock._key,
|
|
264
|
+
),
|
|
265
|
+
h(
|
|
266
|
+
'div',
|
|
267
|
+
{ class: 'cg-docs__content' },
|
|
268
|
+
renderBreadcrumb(section, page.title),
|
|
269
|
+
entryHeader ?? (hasOwnHeading ? null : h('h1', { class: 'cg-docs__title' }, page.title)),
|
|
270
|
+
contentBlocks.map((block) =>
|
|
271
|
+
withBlockKey(renderBlock(block, ctx, entries, registry), block._key),
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* `<main id="cg-main">` is mandatory: it is the skip-link's target, written
|
|
279
|
+
* once by `@cogenta/cli`'s own render path.
|
|
280
|
+
*
|
|
281
|
+
* `withBlockKey` stamps every rendered block with its contract-B `_key`, on
|
|
282
|
+
* every page including a doc page's content column — the visual page
|
|
283
|
+
* builder maps a clicked element back to the block that produced it, for
|
|
284
|
+
* this theme exactly as for every other one.
|
|
285
|
+
*/
|
|
286
|
+
export function renderPage(
|
|
287
|
+
page: PageContent,
|
|
288
|
+
ctx: RenderContext,
|
|
289
|
+
entries: FetchedEntries = {},
|
|
290
|
+
registry?: BlockRegistry,
|
|
291
|
+
): HtmlElement {
|
|
292
|
+
const first = page.blocks[0]
|
|
293
|
+
if (first !== undefined && isDocSidebarBlock(first)) {
|
|
294
|
+
return renderDocPage(page, ctx, first, entries, registry)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return h(
|
|
298
|
+
'main',
|
|
299
|
+
{ class: 'cg-main', id: 'cg-main' },
|
|
300
|
+
pageHasOwnHeading(page.blocks) ? null : h('h1', { class: 'cg-page__title' }, page.title),
|
|
301
|
+
page.blocks.map((block) =>
|
|
302
|
+
withBlockKey(renderBlock(block, ctx, entries, registry), block._key),
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type HtmlElement, h, type TermArchiveInput } from '@cogenta/theme-kit'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The taxonomy-term archive (contract D `theme@1.3`) — a simple titled list,
|
|
5
|
+
* deliberately: a documentation site's own information architecture is the
|
|
6
|
+
* sidebar (`section`/`order`, plain fields), not a taxonomy, so this page is
|
|
7
|
+
* the plain fallback for a term an editor still chooses to classify content
|
|
8
|
+
* under, not a second navigation surface competing with the sidebar.
|
|
9
|
+
*/
|
|
10
|
+
export function renderTermArchive(input: TermArchiveInput): HtmlElement {
|
|
11
|
+
const rows = input.entries.map((entry) =>
|
|
12
|
+
h(
|
|
13
|
+
'li',
|
|
14
|
+
{ class: 'cg-list__row' },
|
|
15
|
+
h(
|
|
16
|
+
'div',
|
|
17
|
+
{ class: 'cg-list__body' },
|
|
18
|
+
h(
|
|
19
|
+
'h2',
|
|
20
|
+
{ class: 'cg-list__title' },
|
|
21
|
+
entry.href === null
|
|
22
|
+
? entry.title
|
|
23
|
+
: h('a', { class: 'cg-list__link', href: entry.href }, entry.title),
|
|
24
|
+
),
|
|
25
|
+
entry.summary === null ? null : h('p', { class: 'cg-list__excerpt' }, entry.summary),
|
|
26
|
+
),
|
|
27
|
+
entry.publishedAt === null
|
|
28
|
+
? null
|
|
29
|
+
: h(
|
|
30
|
+
'time',
|
|
31
|
+
{ class: 'cg-list__date', datetime: entry.publishedAt },
|
|
32
|
+
entry.publishedAt.slice(0, 10),
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return h(
|
|
38
|
+
'main',
|
|
39
|
+
{ class: 'cg-main cg-archive', id: 'cg-main' },
|
|
40
|
+
input.ancestors.length === 0
|
|
41
|
+
? null
|
|
42
|
+
: h(
|
|
43
|
+
'nav',
|
|
44
|
+
{ class: 'cg-archive__breadcrumb', 'aria-label': input.labels.breadcrumb },
|
|
45
|
+
h(
|
|
46
|
+
'ol',
|
|
47
|
+
{},
|
|
48
|
+
...input.ancestors.map((l) => h('li', {}, h('a', { href: l.href }, l.label))),
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
h('h1', { class: 'cg-page__title' }, input.term.label),
|
|
52
|
+
input.children.length === 0
|
|
53
|
+
? null
|
|
54
|
+
: h(
|
|
55
|
+
'ul',
|
|
56
|
+
{ class: 'cg-archive__children', 'aria-label': input.labels.subterms },
|
|
57
|
+
...input.children.map((c) => h('li', {}, h('a', { href: c.href }, c.label))),
|
|
58
|
+
),
|
|
59
|
+
rows.length === 0
|
|
60
|
+
? h('p', { class: 'cg-list__empty' }, input.labels.empty)
|
|
61
|
+
: h('ul', { class: 'cg-list__items' }, ...rows),
|
|
62
|
+
pager(input),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pager(input: TermArchiveInput): HtmlElement | null {
|
|
67
|
+
if (input.page.previousHref === null && input.page.nextHref === null) return null
|
|
68
|
+
return h(
|
|
69
|
+
'nav',
|
|
70
|
+
{ class: 'cg-archive__pager', 'aria-label': input.labels.pagination },
|
|
71
|
+
input.page.previousHref === null
|
|
72
|
+
? null
|
|
73
|
+
: h('a', { rel: 'prev', href: input.page.previousHref }, input.labels.previous),
|
|
74
|
+
input.page.nextHref === null
|
|
75
|
+
? null
|
|
76
|
+
: h('a', { rel: 'next', href: input.page.nextHref }, input.labels.next),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The taxonomy-term archive (contract D `theme@1.3`) — a simple titled list.
|
|
3
|
+
*
|
|
4
|
+
* Only the three things an archive adds beyond what `collectionList` already
|
|
5
|
+
* styles — a breadcrumb, a sub-term row, a prev/next pager. The entry list
|
|
6
|
+
* itself reuses `cg-list__items`/`cg-list__row`, on purpose: an archive of
|
|
7
|
+
* entries must look like this theme's own list of entries.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.cg-archive {
|
|
11
|
+
max-inline-size: var(--cg-page);
|
|
12
|
+
margin-inline: auto;
|
|
13
|
+
padding: var(--cg-s10) var(--cg-gutter);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.cg-archive__breadcrumb ol,
|
|
17
|
+
.cg-archive__children {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
gap: var(--cg-s3);
|
|
21
|
+
margin: 0 0 var(--cg-s4);
|
|
22
|
+
padding: 0;
|
|
23
|
+
list-style: none;
|
|
24
|
+
font-size: var(--cg-text-sm);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.cg-archive__children {
|
|
28
|
+
margin-block: var(--cg-s4) var(--cg-s8);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.cg-archive__pager {
|
|
32
|
+
display: flex;
|
|
33
|
+
justify-content: space-between;
|
|
34
|
+
gap: var(--cg-s4);
|
|
35
|
+
margin-block-start: var(--cg-s10);
|
|
36
|
+
padding-block-start: var(--cg-s5);
|
|
37
|
+
border-block-start: 1px solid var(--cg-line-soft);
|
|
38
|
+
font-weight: var(--cg-weight-semibold);
|
|
39
|
+
}
|