@cogenta/theme-portfolio 1.1.2 → 1.3.3
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/dist/src/index.d.ts +9 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +10 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/render/blocks/accordion.d.ts.map +1 -1
- package/dist/src/render/blocks/accordion.js +10 -18
- package/dist/src/render/blocks/accordion.js.map +1 -1
- package/dist/src/render/blocks/collection-list.d.ts +0 -20
- package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
- package/dist/src/render/blocks/collection-list.js +59 -66
- package/dist/src/render/blocks/collection-list.js.map +1 -1
- package/dist/src/render/blocks/cta.d.ts +4 -4
- package/dist/src/render/blocks/cta.d.ts.map +1 -1
- package/dist/src/render/blocks/cta.js +15 -10
- package/dist/src/render/blocks/cta.js.map +1 -1
- package/dist/src/render/blocks/embed.d.ts.map +1 -1
- package/dist/src/render/blocks/embed.js +16 -31
- package/dist/src/render/blocks/embed.js.map +1 -1
- package/dist/src/render/blocks/faq.d.ts.map +1 -1
- package/dist/src/render/blocks/faq.js +12 -17
- package/dist/src/render/blocks/faq.js.map +1 -1
- package/dist/src/render/blocks/feature-grid.d.ts.map +1 -1
- package/dist/src/render/blocks/feature-grid.js +26 -23
- package/dist/src/render/blocks/feature-grid.js.map +1 -1
- package/dist/src/render/blocks/gallery.d.ts +0 -14
- package/dist/src/render/blocks/gallery.d.ts.map +1 -1
- package/dist/src/render/blocks/gallery.js +29 -21
- package/dist/src/render/blocks/gallery.js.map +1 -1
- package/dist/src/render/blocks/hero.d.ts +8 -8
- package/dist/src/render/blocks/hero.d.ts.map +1 -1
- package/dist/src/render/blocks/hero.js +17 -18
- package/dist/src/render/blocks/hero.js.map +1 -1
- package/dist/src/render/blocks/logo-strip.d.ts.map +1 -1
- package/dist/src/render/blocks/logo-strip.js +9 -14
- package/dist/src/render/blocks/logo-strip.js.map +1 -1
- package/dist/src/render/blocks/logos.d.ts.map +1 -1
- package/dist/src/render/blocks/logos.js +14 -19
- package/dist/src/render/blocks/logos.js.map +1 -1
- package/dist/src/render/blocks/media-figure.d.ts +8 -12
- package/dist/src/render/blocks/media-figure.d.ts.map +1 -1
- package/dist/src/render/blocks/media-figure.js +20 -20
- package/dist/src/render/blocks/media-figure.js.map +1 -1
- package/dist/src/render/blocks/pricing-table.d.ts.map +1 -1
- package/dist/src/render/blocks/pricing-table.js +17 -20
- package/dist/src/render/blocks/pricing-table.js.map +1 -1
- package/dist/src/render/blocks/prose.d.ts +10 -9
- package/dist/src/render/blocks/prose.d.ts.map +1 -1
- package/dist/src/render/blocks/prose.js +29 -10
- package/dist/src/render/blocks/prose.js.map +1 -1
- package/dist/src/render/blocks/quote.d.ts +5 -12
- package/dist/src/render/blocks/quote.d.ts.map +1 -1
- package/dist/src/render/blocks/quote.js +9 -15
- package/dist/src/render/blocks/quote.js.map +1 -1
- package/dist/src/render/blocks/stat-counter.d.ts.map +1 -1
- package/dist/src/render/blocks/stat-counter.js +10 -12
- package/dist/src/render/blocks/stat-counter.js.map +1 -1
- package/dist/src/render/blocks/stats.d.ts.map +1 -1
- package/dist/src/render/blocks/stats.js +7 -15
- package/dist/src/render/blocks/stats.js.map +1 -1
- package/dist/src/render/blocks/testimonial.d.ts +4 -14
- package/dist/src/render/blocks/testimonial.d.ts.map +1 -1
- package/dist/src/render/blocks/testimonial.js +9 -18
- package/dist/src/render/blocks/testimonial.js.map +1 -1
- package/dist/src/render/chrome.d.ts.map +1 -1
- package/dist/src/render/chrome.js +93 -90
- package/dist/src/render/chrome.js.map +1 -1
- package/dist/src/render/layout.d.ts +61 -0
- package/dist/src/render/layout.d.ts.map +1 -0
- package/dist/src/render/layout.js +134 -0
- package/dist/src/render/layout.js.map +1 -0
- package/dist/src/render/project.d.ts +27 -0
- package/dist/src/render/project.d.ts.map +1 -0
- package/dist/src/render/project.js +82 -0
- package/dist/src/render/project.js.map +1 -0
- package/dist/src/render/render-block.d.ts +17 -7
- package/dist/src/render/render-block.d.ts.map +1 -1
- package/dist/src/render/render-block.js +47 -34
- package/dist/src/render/render-block.js.map +1 -1
- package/dist/src/render/term-archive.d.ts +0 -11
- package/dist/src/render/term-archive.d.ts.map +1 -1
- package/dist/src/render/term-archive.js +36 -24
- package/dist/src/render/term-archive.js.map +1 -1
- package/dist/src/render/widget-areas.d.ts +14 -0
- package/dist/src/render/widget-areas.d.ts.map +1 -0
- package/dist/src/render/widget-areas.js +21 -0
- package/dist/src/render/widget-areas.js.map +1 -0
- package/dist/src/render/work.d.ts +37 -0
- package/dist/src/render/work.d.ts.map +1 -0
- package/dist/src/render/work.js +49 -0
- package/dist/src/render/work.js.map +1 -0
- package/dist/theme.config.d.ts +4 -7
- package/dist/theme.config.d.ts.map +1 -1
- package/dist/theme.config.js +6 -9
- package/dist/theme.config.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +10 -1
- package/src/render/blocks/accordion.ts +22 -42
- package/src/render/blocks/collection-list.ts +83 -137
- package/src/render/blocks/cta.ts +24 -12
- package/src/render/blocks/embed.ts +39 -47
- package/src/render/blocks/faq.ts +24 -33
- package/src/render/blocks/feature-grid.ts +38 -41
- package/src/render/blocks/gallery.ts +34 -28
- package/src/render/blocks/hero.ts +31 -28
- package/src/render/blocks/logo-strip.ts +18 -20
- package/src/render/blocks/logos.ts +22 -43
- package/src/render/blocks/media-figure.ts +28 -20
- package/src/render/blocks/pricing-table.ts +31 -32
- package/src/render/blocks/prose.ts +38 -13
- package/src/render/blocks/quote.ts +18 -18
- package/src/render/blocks/stat-counter.ts +19 -37
- package/src/render/blocks/stats.ts +18 -33
- package/src/render/blocks/testimonial.ts +16 -22
- package/src/render/chrome.ts +99 -97
- package/src/render/layout.ts +177 -0
- package/src/render/project.ts +140 -0
- package/src/render/render-block.ts +49 -45
- package/src/render/term-archive.ts +92 -70
- package/src/render/widget-areas.ts +25 -0
- package/src/render/work.ts +124 -0
- package/src/styles/archive.css +61 -21
- package/src/styles/base.css +763 -648
- package/src/styles/blocks.css +697 -1093
- package/src/styles/theme.css +22 -10
- package/src/styles/tokens.css +174 -252
- package/src/styles/widgets.css +948 -0
- package/src/styles/work.css +405 -0
- package/tokens.json +19 -19
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type HtmlElement,
|
|
3
|
+
h,
|
|
4
|
+
type PageContent,
|
|
5
|
+
type PageEntryMeta,
|
|
6
|
+
type PageEntryTerm,
|
|
7
|
+
renderImageSource,
|
|
8
|
+
} from '@cogenta/theme-kit'
|
|
9
|
+
import { taxonomyLabel, yearLabel, yearOf } from './layout.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The page of one project, and the plain page title everything else gets.
|
|
13
|
+
*
|
|
14
|
+
* A project page opens the way a studio presents a case study:
|
|
15
|
+
*
|
|
16
|
+
* 1. the title, very large, across the grid;
|
|
17
|
+
* 2. the statement (the entry's excerpt) in the lead size, across eight
|
|
18
|
+
* columns;
|
|
19
|
+
* 3. the lead visual, the project's own cover, full width at its own shape;
|
|
20
|
+
* 4. the fact sheet: one column per fact, a hairline over the row. The year
|
|
21
|
+
* comes from the entry's publication date, labelled with the platform's
|
|
22
|
+
* own word for "year" in the page's language; every other column is one
|
|
23
|
+
* taxonomy the collection declares (a client, the disciplines, the team),
|
|
24
|
+
* labelled with the taxonomy's own name and listing its terms, each linked
|
|
25
|
+
* to its archive.
|
|
26
|
+
*
|
|
27
|
+
* The body that follows is the editor's own blocks. On a project page a
|
|
28
|
+
* `prose` block sets its text in two columns on a wide screen (`work.css`,
|
|
29
|
+
* keyed on the `cg-project` class of `<main>`).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** What makes a page a project: it has an entry with a cover image and is not a plain page. */
|
|
33
|
+
export function isProject(entry: PageEntryMeta | undefined): entry is PageEntryMeta {
|
|
34
|
+
return entry !== undefined && entry.image !== undefined && entry.collection !== 'page'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Fact {
|
|
38
|
+
readonly key: string
|
|
39
|
+
readonly label: string
|
|
40
|
+
readonly values: readonly HtmlElement[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function termValue(term: PageEntryTerm): HtmlElement {
|
|
44
|
+
return h(
|
|
45
|
+
'li',
|
|
46
|
+
{ class: 'cg-facts__value' },
|
|
47
|
+
term.href === null
|
|
48
|
+
? term.label
|
|
49
|
+
: h('a', { class: 'cg-facts__link', href: term.href }, term.label),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function facts(entry: PageEntryMeta, locale: string): readonly Fact[] {
|
|
54
|
+
const out: Fact[] = []
|
|
55
|
+
const iso = entry.publishedAt
|
|
56
|
+
const year = iso === undefined ? undefined : yearOf(iso)
|
|
57
|
+
if (iso !== undefined && year !== undefined) {
|
|
58
|
+
out.push({
|
|
59
|
+
key: 'year',
|
|
60
|
+
label: yearLabel(locale),
|
|
61
|
+
values: [h('li', { class: 'cg-facts__value' }, h('time', { datetime: iso }, year))],
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
const groups = new Map<string, PageEntryTerm[]>()
|
|
65
|
+
for (const term of entry.terms ?? []) {
|
|
66
|
+
const group = groups.get(term.taxonomy)
|
|
67
|
+
if (group === undefined) groups.set(term.taxonomy, [term])
|
|
68
|
+
else group.push(term)
|
|
69
|
+
}
|
|
70
|
+
for (const [taxonomy, terms] of groups) {
|
|
71
|
+
out.push({
|
|
72
|
+
key: taxonomy,
|
|
73
|
+
label: taxonomyLabel(taxonomy, locale),
|
|
74
|
+
values: terms.map(termValue),
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderFacts(entry: PageEntryMeta, locale: string): HtmlElement | null {
|
|
81
|
+
const list = facts(entry, locale)
|
|
82
|
+
if (list.length === 0) return null
|
|
83
|
+
return h(
|
|
84
|
+
'dl',
|
|
85
|
+
{ class: 'cg-facts', 'data-count': String(Math.min(list.length, 4)) },
|
|
86
|
+
list.map((fact) =>
|
|
87
|
+
h(
|
|
88
|
+
'div',
|
|
89
|
+
{ class: 'cg-facts__item', 'data-fact': fact.key },
|
|
90
|
+
h('dt', { class: 'cg-facts__label' }, fact.label),
|
|
91
|
+
h('dd', { class: 'cg-facts__values' }, h('ul', { class: 'cg-facts__list' }, fact.values)),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function renderProjectHeader(
|
|
98
|
+
page: PageContent,
|
|
99
|
+
entry: PageEntryMeta,
|
|
100
|
+
locale: string,
|
|
101
|
+
): HtmlElement {
|
|
102
|
+
const image = entry.image
|
|
103
|
+
return h(
|
|
104
|
+
'header',
|
|
105
|
+
{ class: 'cg-project-head' },
|
|
106
|
+
h(
|
|
107
|
+
'div',
|
|
108
|
+
{ class: 'cg-container cg-project-head__inner' },
|
|
109
|
+
h('h1', { class: 'cg-project-head__title' }, page.title),
|
|
110
|
+
entry.excerpt === undefined
|
|
111
|
+
? null
|
|
112
|
+
: h('p', { class: 'cg-project-head__statement' }, entry.excerpt),
|
|
113
|
+
image === undefined
|
|
114
|
+
? null
|
|
115
|
+
: h(
|
|
116
|
+
'figure',
|
|
117
|
+
{ class: 'cg-project-head__lead' },
|
|
118
|
+
renderImageSource(image, {
|
|
119
|
+
className: 'cg-project-head__image',
|
|
120
|
+
loading: 'eager',
|
|
121
|
+
sizes: '(min-width: 96rem) 92rem, 100vw',
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
renderFacts(entry, locale),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The title of any other page: set large on the grid, with nothing around it. */
|
|
130
|
+
export function renderPageHeader(page: PageContent): HtmlElement {
|
|
131
|
+
return h(
|
|
132
|
+
'header',
|
|
133
|
+
{ class: 'cg-page-head' },
|
|
134
|
+
h(
|
|
135
|
+
'div',
|
|
136
|
+
{ class: 'cg-container cg-page-head__inner' },
|
|
137
|
+
h('h1', { class: 'cg-page-head__title' }, page.title),
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
h,
|
|
6
6
|
type PageContent,
|
|
7
7
|
pageHasOwnHeading,
|
|
8
|
+
providedBlockNode,
|
|
8
9
|
type RenderContext,
|
|
9
|
-
renderEntryHeader,
|
|
10
10
|
resolveBlockForRender,
|
|
11
11
|
withBlockKey,
|
|
12
12
|
withBlockVariant,
|
|
@@ -28,14 +28,10 @@ import { renderQuote } from './blocks/quote.js'
|
|
|
28
28
|
import { renderStatCounter } from './blocks/stat-counter.js'
|
|
29
29
|
import { renderStats } from './blocks/stats.js'
|
|
30
30
|
import { renderTestimonial } from './blocks/testimonial.js'
|
|
31
|
+
import { isProject, renderPageHeader, renderProjectHeader } from './project.js'
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
-
* `PageContent`/`FetchedEntries`/`pageHasOwnHeading`/`withBlockKey` are
|
|
34
|
-
* `@cogenta/theme-kit`'s own — re-exported here so this package's public
|
|
35
|
-
* surface names them, the same shape every theme package shares them under.
|
|
36
|
-
*/
|
|
37
33
|
export type { FetchedEntries, PageContent }
|
|
38
|
-
export { pageHasOwnHeading,
|
|
34
|
+
export { pageHasOwnHeading, withBlockKey }
|
|
39
35
|
|
|
40
36
|
export function renderBlock(
|
|
41
37
|
block: VocabularyBlock,
|
|
@@ -43,17 +39,19 @@ export function renderBlock(
|
|
|
43
39
|
entries: FetchedEntries = {},
|
|
44
40
|
registry?: BlockRegistry,
|
|
45
41
|
): HtmlElement | null {
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
42
|
+
// A block a plugin provides arrives already rendered by the host, in a
|
|
43
|
+
// process of its own (contract D theme@1.7). Nothing else in this theme
|
|
44
|
+
// needs to know such blocks exist.
|
|
45
|
+
const provided = providedBlockNode(block, ctx)
|
|
46
|
+
if (provided !== null) return withBlockVariant(provided, block.variant)
|
|
47
|
+
// A stored block is not always literally one of the shared vocabulary:
|
|
48
|
+
// resolving here turns an unimplemented theme-private block into its
|
|
49
|
+
// declared fallback instead of a silently blank slot.
|
|
50
50
|
const resolved = resolveBlockForRender(block, VOCABULARY_NAMES, registry)
|
|
51
51
|
if (resolved === null) return null
|
|
52
52
|
const known = resolved as unknown as VocabularyBlock
|
|
53
|
-
// `
|
|
54
|
-
//
|
|
55
|
-
// `variant` is envelope data every block carries identically, exactly the
|
|
56
|
-
// same reasoning `withBlockKey` in `renderPage` already follows for `_key`.
|
|
53
|
+
// `variant` is envelope data every block carries identically, applied once
|
|
54
|
+
// here rather than by each of the seventeen renderers.
|
|
57
55
|
return withBlockVariant(renderKnownBlock(known, ctx, entries), known.variant)
|
|
58
56
|
}
|
|
59
57
|
|
|
@@ -98,8 +96,9 @@ function renderKnownBlock(
|
|
|
98
96
|
case 'logoStrip':
|
|
99
97
|
return renderLogoStrip(known, ctx)
|
|
100
98
|
default: {
|
|
101
|
-
// Exhaustive over contract B
|
|
102
|
-
//
|
|
99
|
+
// Exhaustive over contract B: `known` is `never` here, so a block this
|
|
100
|
+
// package does not implement stops it compiling until it is. `null`
|
|
101
|
+
// rather than a throw: choosing a fallback is the render layer's job.
|
|
103
102
|
const unreachable: never = known
|
|
104
103
|
void unreachable
|
|
105
104
|
return null
|
|
@@ -108,48 +107,53 @@ function renderKnownBlock(
|
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* whose blocks draw their own heading (a `hero`), so the bare
|
|
115
|
-
* `<h1 class="cg-page__title">` fallback below is the right markup in both
|
|
116
|
-
* of those cases and only those — never a double heading, and never a page
|
|
117
|
-
* with none at all. A page rendered before `theme@1.4` (`page.entry`
|
|
118
|
-
* absent) gets exactly the pre-1.4 markup, byte for byte.
|
|
119
|
-
*/
|
|
120
|
-
/**
|
|
121
|
-
* `withBlockKey` stamps `data-block-key` (the L16 builder's own hook); this
|
|
122
|
-
* theme additionally stamps a real `id`, the block's own `_key`, on the same
|
|
123
|
-
* element — `_key` is already unique within a page (contract B), so it
|
|
124
|
-
* doubles as a genuine in-page anchor. The header/footer's own single-page
|
|
125
|
-
* links (the `portfolio` blueprint's "Work"/"Services" nav) resolve to a
|
|
126
|
-
* real target this way, rather than a `#fragment` nothing on the page
|
|
127
|
-
* answers to.
|
|
110
|
+
* The block's own `_key` doubles as an in-page anchor: it is unique within a
|
|
111
|
+
* page (contract B), so a menu item such as `/#home-work` lands on the block
|
|
112
|
+
* it names rather than on a fragment nothing answers to.
|
|
128
113
|
*/
|
|
129
|
-
function
|
|
114
|
+
function withAnchor(element: HtmlElement | null, key: string): HtmlElement | null {
|
|
130
115
|
if (element === null) return null
|
|
131
116
|
return { ...element, attrs: { ...element.attrs, id: key } }
|
|
132
117
|
}
|
|
133
118
|
|
|
119
|
+
/**
|
|
120
|
+
* `<main id="cg-main">` is mandatory: it is the skip link's target, written
|
|
121
|
+
* by `@cogenta/cli`'s `theme-render.ts` outside any theme's control.
|
|
122
|
+
*
|
|
123
|
+
* Three openings, and exactly one `h1` in each:
|
|
124
|
+
*
|
|
125
|
+
* - a page whose blocks include a `hero` lets the hero carry the title (the
|
|
126
|
+
* studio's statement, on a home page);
|
|
127
|
+
* - a project (`isProject`: an entry with a cover that is not a plain page)
|
|
128
|
+
* gets the project header: title, statement, lead visual, fact sheet;
|
|
129
|
+
* - anything else gets its title set large on the grid.
|
|
130
|
+
*
|
|
131
|
+
* `withBlockKey` stamps every rendered block with its contract-B `_key`, so
|
|
132
|
+
* the visual page builder (L16) can map a clicked element back to its block.
|
|
133
|
+
*/
|
|
134
134
|
export function renderPage(
|
|
135
135
|
page: PageContent,
|
|
136
136
|
ctx: RenderContext,
|
|
137
137
|
entries: FetchedEntries = {},
|
|
138
138
|
registry?: BlockRegistry,
|
|
139
139
|
): HtmlElement {
|
|
140
|
-
const
|
|
140
|
+
const ownHeading = pageHasOwnHeading(page.blocks)
|
|
141
|
+
const project = !ownHeading && isProject(page.entry) ? page.entry : undefined
|
|
142
|
+
const opening = ownHeading
|
|
143
|
+
? null
|
|
144
|
+
: project !== undefined
|
|
145
|
+
? renderProjectHeader(page, project, ctx.locale)
|
|
146
|
+
: renderPageHeader(page)
|
|
147
|
+
|
|
141
148
|
return h(
|
|
142
149
|
'main',
|
|
143
|
-
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
{
|
|
151
|
+
class: project !== undefined ? 'cg-main cg-project' : 'cg-main',
|
|
152
|
+
id: 'cg-main',
|
|
153
|
+
},
|
|
154
|
+
opening,
|
|
148
155
|
page.blocks.map((block) =>
|
|
149
|
-
|
|
150
|
-
withBlockKey(renderBlock(block, ctx, entries, registry), block._key),
|
|
151
|
-
block._key,
|
|
152
|
-
),
|
|
156
|
+
withAnchor(withBlockKey(renderBlock(block, ctx, entries, registry), block._key), block._key),
|
|
153
157
|
),
|
|
154
158
|
)
|
|
155
159
|
}
|
|
@@ -1,88 +1,110 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type HtmlElement,
|
|
3
|
+
h,
|
|
4
|
+
type TermArchiveEntry,
|
|
5
|
+
type TermArchiveInput,
|
|
6
|
+
} from '@cogenta/theme-kit'
|
|
7
|
+
import { taxonomyLabel, yearOf } from './layout.js'
|
|
8
|
+
import { renderIndexRow, type Work } from './work.js'
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
|
-
* The taxonomy
|
|
5
|
-
*
|
|
6
|
-
* taxonomy name as a kicker above it, and the entries as the same numbered
|
|
7
|
-
* `cg-entry` rows `collectionList` already renders here — an index, not a
|
|
8
|
-
* card grid.
|
|
11
|
+
* The page of one taxonomy term (contract D `theme@1.3`): every project filed
|
|
12
|
+
* under a discipline, a client or a member of the team.
|
|
9
13
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* It is set as the studio's index, not as a grid: an archive entry carries no
|
|
15
|
+
* picture (`TermArchiveEntry` has none), and a grid of empty frames is worse
|
|
16
|
+
* than a well-set list. The taxonomy's own name sits above the term in the
|
|
17
|
+
* caption size; the term itself is the page's one `h1`, very large; each
|
|
18
|
+
* project is a ruled row with its title, its statement when it has one, and
|
|
19
|
+
* its year; sub-terms and the pager are
|
|
20
|
+
* underlined words, the pager's arrows drawn by the stylesheet.
|
|
13
21
|
*/
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'span',
|
|
24
|
-
{ class: 'cg-entry__index', 'aria-hidden': 'true' },
|
|
25
|
-
String(index + 1).padStart(2, '0'),
|
|
26
|
-
),
|
|
27
|
-
h(
|
|
28
|
-
'h2',
|
|
29
|
-
{ class: 'cg-entry__title' },
|
|
30
|
-
entry.href === null
|
|
31
|
-
? entry.title
|
|
32
|
-
: h('a', { class: 'cg-entry__link', href: entry.href }, entry.title),
|
|
33
|
-
),
|
|
34
|
-
entry.publishedAt === null
|
|
35
|
-
? null
|
|
36
|
-
: h(
|
|
37
|
-
'time',
|
|
38
|
-
{ class: 'cg-entry__date', datetime: entry.publishedAt },
|
|
39
|
-
entry.publishedAt.slice(0, 10),
|
|
40
|
-
),
|
|
41
|
-
entry.summary === null ? null : h('p', { class: 'cg-entry__excerpt' }, entry.summary),
|
|
42
|
-
),
|
|
43
|
-
),
|
|
44
|
-
)
|
|
22
|
+
function workFromArchive(entry: TermArchiveEntry): Work {
|
|
23
|
+
const year = entry.publishedAt === null ? undefined : yearOf(entry.publishedAt)
|
|
24
|
+
return {
|
|
25
|
+
href: entry.href,
|
|
26
|
+
title: entry.title,
|
|
27
|
+
caption: year === undefined ? {} : { year },
|
|
28
|
+
...(entry.summary === null ? {} : { summary: entry.summary }),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
45
31
|
|
|
32
|
+
function pager(input: TermArchiveInput): HtmlElement | null {
|
|
33
|
+
if (input.page.previousHref === null && input.page.nextHref === null) return null
|
|
46
34
|
return h(
|
|
47
|
-
'
|
|
48
|
-
{ class: 'cg-
|
|
49
|
-
input.
|
|
35
|
+
'nav',
|
|
36
|
+
{ class: 'cg-archive__pager', 'aria-label': input.labels.pagination },
|
|
37
|
+
input.page.previousHref === null
|
|
50
38
|
? null
|
|
51
39
|
: h(
|
|
52
|
-
'
|
|
53
|
-
{
|
|
54
|
-
|
|
55
|
-
'
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
40
|
+
'a',
|
|
41
|
+
{
|
|
42
|
+
class: 'cg-arrow-link',
|
|
43
|
+
'data-direction': 'back',
|
|
44
|
+
rel: 'prev',
|
|
45
|
+
href: input.page.previousHref,
|
|
46
|
+
},
|
|
47
|
+
input.labels.previous,
|
|
59
48
|
),
|
|
60
|
-
|
|
61
|
-
h('h1', { class: 'cg-page__title' }, input.term.label),
|
|
62
|
-
input.children.length === 0
|
|
49
|
+
input.page.nextHref === null
|
|
63
50
|
? null
|
|
64
51
|
: h(
|
|
65
|
-
'
|
|
66
|
-
{ class: 'cg-
|
|
67
|
-
|
|
52
|
+
'a',
|
|
53
|
+
{ class: 'cg-arrow-link', rel: 'next', href: input.page.nextHref },
|
|
54
|
+
input.labels.next,
|
|
68
55
|
),
|
|
69
|
-
items.length === 0
|
|
70
|
-
? h('p', { class: 'cg-collection__empty' }, input.labels.empty)
|
|
71
|
-
: h('ul', { class: 'cg-collection__items' }, ...items),
|
|
72
|
-
pager(input),
|
|
73
56
|
)
|
|
74
57
|
}
|
|
75
58
|
|
|
76
|
-
function
|
|
77
|
-
if (input.page.previousHref === null && input.page.nextHref === null) return null
|
|
59
|
+
export function renderTermArchive(input: TermArchiveInput): HtmlElement {
|
|
78
60
|
return h(
|
|
79
|
-
'
|
|
80
|
-
{ class: 'cg-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
61
|
+
'main',
|
|
62
|
+
{ class: 'cg-main cg-archive', id: 'cg-main' },
|
|
63
|
+
h(
|
|
64
|
+
'header',
|
|
65
|
+
{ class: 'cg-archive__head' },
|
|
66
|
+
h(
|
|
67
|
+
'div',
|
|
68
|
+
{ class: 'cg-container cg-archive__head-inner' },
|
|
69
|
+
input.ancestors.length === 0
|
|
70
|
+
? null
|
|
71
|
+
: h(
|
|
72
|
+
'nav',
|
|
73
|
+
{ class: 'cg-archive__breadcrumb', 'aria-label': input.labels.breadcrumb },
|
|
74
|
+
h(
|
|
75
|
+
'ol',
|
|
76
|
+
{ class: 'cg-archive__trail' },
|
|
77
|
+
input.ancestors.map((link) => h('li', {}, h('a', { href: link.href }, link.label))),
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
h('p', { class: 'cg-archive__kicker' }, taxonomyLabel(input.taxonomyName, input.locale)),
|
|
81
|
+
h('h1', { class: 'cg-archive__title' }, input.term.label),
|
|
82
|
+
input.children.length === 0
|
|
83
|
+
? null
|
|
84
|
+
: h(
|
|
85
|
+
'ul',
|
|
86
|
+
{ class: 'cg-archive__children', 'aria-label': input.labels.subterms },
|
|
87
|
+
input.children.map((child) =>
|
|
88
|
+
h('li', {}, h('a', { class: 'cg-arrow-link', href: child.href }, child.label)),
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
h(
|
|
94
|
+
'section',
|
|
95
|
+
{ class: 'cg-section cg-collection', 'data-form': 'index' },
|
|
96
|
+
h(
|
|
97
|
+
'div',
|
|
98
|
+
{ class: 'cg-container cg-collection__inner' },
|
|
99
|
+
input.entries.length === 0
|
|
100
|
+
? h('p', { class: 'cg-empty' }, input.labels.empty)
|
|
101
|
+
: h(
|
|
102
|
+
'ol',
|
|
103
|
+
{ class: 'cg-index', 'data-count': String(input.entries.length) },
|
|
104
|
+
input.entries.map((entry) => renderIndexRow(workFromArchive(entry), 'h2')),
|
|
105
|
+
),
|
|
106
|
+
pager(input),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
87
109
|
)
|
|
88
110
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The widget areas this theme names (contract D `theme@1.6`). Declaring them
|
|
3
|
+
* is what tells the host this theme draws the footer columns inside its own
|
|
4
|
+
* colophon, on the footer's twelve columns; the page areas are placed by the
|
|
5
|
+
* host in the shared `cg-sidebar-layout` markup, which `styles/widgets.css`
|
|
6
|
+
* sets on the page's own grid: the column beside the content takes the last
|
|
7
|
+
* columns of the twelve, the way the header's navigation does.
|
|
8
|
+
*/
|
|
9
|
+
export const widgetAreas: readonly {
|
|
10
|
+
readonly id: string
|
|
11
|
+
readonly label: string
|
|
12
|
+
readonly description?: string
|
|
13
|
+
}[] = [
|
|
14
|
+
{
|
|
15
|
+
id: 'sidebar',
|
|
16
|
+
label: 'Beside the page',
|
|
17
|
+
description:
|
|
18
|
+
'Beside an index of work (a discipline, a client, a member of the team) and search results.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'content-after',
|
|
22
|
+
label: 'After the work',
|
|
23
|
+
description: 'Under a project or a page, before the footer: more work, a way to get in touch.',
|
|
24
|
+
},
|
|
25
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ContentEntry,
|
|
3
|
+
entryHref,
|
|
4
|
+
entryImage,
|
|
5
|
+
entryTitle,
|
|
6
|
+
type HeadingTag,
|
|
7
|
+
type HtmlElement,
|
|
8
|
+
h,
|
|
9
|
+
heading,
|
|
10
|
+
type ImageSource,
|
|
11
|
+
type RenderContext,
|
|
12
|
+
renderImageSource,
|
|
13
|
+
} from '@cogenta/theme-kit'
|
|
14
|
+
import { captionUnder, entryCaption, renderCaption, type WorkCaption } from './layout.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One piece of work as a listing sets it: the image, square-cornered and
|
|
18
|
+
* edge to edge within its columns, then the title and the caption line
|
|
19
|
+
* (client, discipline, year). The grid, the strip and the index all build
|
|
20
|
+
* their rows from this one unit.
|
|
21
|
+
*
|
|
22
|
+
* The title is the one link. The image links to the same page but is taken
|
|
23
|
+
* out of the tab order and hidden from assistive technology, so a keyboard or
|
|
24
|
+
* screen-reader user meets each project once.
|
|
25
|
+
*/
|
|
26
|
+
export interface Work {
|
|
27
|
+
readonly href: string | null
|
|
28
|
+
readonly title: string
|
|
29
|
+
readonly caption: WorkCaption
|
|
30
|
+
readonly image?: ImageSource
|
|
31
|
+
/** A sentence about the work, when the source carries one and the slot shows it (the archive index). */
|
|
32
|
+
readonly summary?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Covers are drawn at 3:2; a listing never asks for another shape, so no card is cropped differently from its neighbour. */
|
|
36
|
+
const COVER_WIDTH = 2000
|
|
37
|
+
|
|
38
|
+
export function workFromEntry(entry: ContentEntry, ctx: RenderContext): Work {
|
|
39
|
+
const image = entryImage(entry, ctx, {
|
|
40
|
+
width: COVER_WIDTH,
|
|
41
|
+
height: Math.round((COVER_WIDTH * 2) / 3),
|
|
42
|
+
fit: 'cover',
|
|
43
|
+
})
|
|
44
|
+
return {
|
|
45
|
+
href: entryHref(entry, ctx),
|
|
46
|
+
title: entryTitle(entry, ctx),
|
|
47
|
+
caption: entryCaption(entry),
|
|
48
|
+
...(image === undefined || image.kind !== 'image' ? {} : { image }),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorkCardOptions {
|
|
53
|
+
readonly tag: HeadingTag
|
|
54
|
+
readonly sizes: string
|
|
55
|
+
readonly loading?: 'eager' | 'lazy'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function titleOf(work: Work, tag: HeadingTag, className: string): HtmlElement {
|
|
59
|
+
return heading(
|
|
60
|
+
tag,
|
|
61
|
+
{ class: className },
|
|
62
|
+
work.href === null
|
|
63
|
+
? work.title
|
|
64
|
+
: h('a', { class: 'cg-work__link', href: work.href }, work.title),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function renderWorkCard(work: Work, options: WorkCardOptions): HtmlElement {
|
|
69
|
+
const picture =
|
|
70
|
+
work.image === undefined
|
|
71
|
+
? h('div', { class: 'cg-work__media', 'data-empty': 'true' })
|
|
72
|
+
: h(
|
|
73
|
+
work.href === null ? 'div' : 'a',
|
|
74
|
+
work.href === null
|
|
75
|
+
? { class: 'cg-work__media' }
|
|
76
|
+
: { class: 'cg-work__media', href: work.href, tabindex: '-1', 'aria-hidden': 'true' },
|
|
77
|
+
renderImageSource(work.image, {
|
|
78
|
+
className: 'cg-work__image',
|
|
79
|
+
sizes: options.sizes,
|
|
80
|
+
loading: options.loading ?? 'lazy',
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
return h(
|
|
84
|
+
'article',
|
|
85
|
+
{ class: 'cg-work' },
|
|
86
|
+
picture,
|
|
87
|
+
h(
|
|
88
|
+
'div',
|
|
89
|
+
{ class: 'cg-work__text' },
|
|
90
|
+
titleOf(work, options.tag, 'cg-work__title'),
|
|
91
|
+
renderCaption(captionUnder(work.caption, work.title), 'cg-work__caption'),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A row of the index: the title, then the client, the discipline and the
|
|
98
|
+
* year in their own columns, a hairline under each. The columns carry no
|
|
99
|
+
* header row: a header is words, and a theme has no translation for them.
|
|
100
|
+
* The client and the discipline share one wrapper, so on a phone they read
|
|
101
|
+
* as one line under the title; on a wide screen the wrapper steps aside
|
|
102
|
+
* (`display: contents`) and each takes its column.
|
|
103
|
+
*/
|
|
104
|
+
export function renderIndexRow(work: Work, tag: HeadingTag): HtmlElement {
|
|
105
|
+
const { client, discipline, year } = work.caption
|
|
106
|
+
return h(
|
|
107
|
+
'li',
|
|
108
|
+
{ class: 'cg-index__row' },
|
|
109
|
+
titleOf(work, tag, 'cg-index__title'),
|
|
110
|
+
work.summary === undefined
|
|
111
|
+
? h(
|
|
112
|
+
'span',
|
|
113
|
+
{ class: 'cg-index__meta' },
|
|
114
|
+
h('span', { class: 'cg-index__cell', 'data-part': 'client' }, client ?? ''),
|
|
115
|
+
h('span', { class: 'cg-index__cell', 'data-part': 'discipline' }, discipline ?? ''),
|
|
116
|
+
)
|
|
117
|
+
: h(
|
|
118
|
+
'span',
|
|
119
|
+
{ class: 'cg-index__meta' },
|
|
120
|
+
h('span', { class: 'cg-index__summary' }, work.summary),
|
|
121
|
+
),
|
|
122
|
+
h('span', { class: 'cg-index__cell', 'data-part': 'year' }, year ?? ''),
|
|
123
|
+
)
|
|
124
|
+
}
|