@cogenta/theme-entreprise 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 +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- 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 +12 -15
- package/dist/src/render/blocks/accordion.js.map +1 -1
- package/dist/src/render/blocks/collection-list.d.ts +5 -7
- package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
- package/dist/src/render/blocks/collection-list.js +48 -32
- package/dist/src/render/blocks/collection-list.js.map +1 -1
- package/dist/src/render/blocks/cta.d.ts +6 -6
- package/dist/src/render/blocks/cta.d.ts.map +1 -1
- package/dist/src/render/blocks/cta.js +12 -12
- 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 +22 -27
- 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 +13 -16
- 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 +22 -22
- package/dist/src/render/blocks/feature-grid.js.map +1 -1
- package/dist/src/render/blocks/gallery.d.ts +7 -8
- package/dist/src/render/blocks/gallery.d.ts.map +1 -1
- package/dist/src/render/blocks/gallery.js +16 -14
- package/dist/src/render/blocks/gallery.js.map +1 -1
- package/dist/src/render/blocks/hero.d.ts +13 -8
- package/dist/src/render/blocks/hero.d.ts.map +1 -1
- package/dist/src/render/blocks/hero.js +27 -17
- 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 +14 -11
- 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 +21 -12
- package/dist/src/render/blocks/logos.js.map +1 -1
- package/dist/src/render/blocks/media-figure.d.ts +10 -6
- package/dist/src/render/blocks/media-figure.d.ts.map +1 -1
- package/dist/src/render/blocks/media-figure.js +18 -13
- 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 +15 -11
- package/dist/src/render/blocks/pricing-table.js.map +1 -1
- package/dist/src/render/blocks/prose.d.ts +9 -3
- package/dist/src/render/blocks/prose.d.ts.map +1 -1
- package/dist/src/render/blocks/prose.js +11 -4
- package/dist/src/render/blocks/prose.js.map +1 -1
- package/dist/src/render/blocks/quote.d.ts +10 -10
- package/dist/src/render/blocks/quote.d.ts.map +1 -1
- package/dist/src/render/blocks/quote.js +14 -13
- 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 +11 -13
- 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 +12 -13
- package/dist/src/render/blocks/stats.js.map +1 -1
- package/dist/src/render/blocks/testimonial.d.ts +9 -9
- package/dist/src/render/blocks/testimonial.d.ts.map +1 -1
- package/dist/src/render/blocks/testimonial.js +15 -14
- package/dist/src/render/blocks/testimonial.js.map +1 -1
- package/dist/src/render/chrome.d.ts +3 -1
- package/dist/src/render/chrome.d.ts.map +1 -1
- package/dist/src/render/chrome.js +71 -46
- package/dist/src/render/chrome.js.map +1 -1
- package/dist/src/render/layout.d.ts +26 -0
- package/dist/src/render/layout.d.ts.map +1 -0
- package/dist/src/render/layout.js +51 -0
- package/dist/src/render/layout.js.map +1 -0
- package/dist/src/render/render-block.d.ts.map +1 -1
- package/dist/src/render/render-block.js +23 -2
- package/dist/src/render/render-block.js.map +1 -1
- package/dist/src/render/term-archive.d.ts +8 -6
- package/dist/src/render/term-archive.d.ts.map +1 -1
- package/dist/src/render/term-archive.js +22 -16
- package/dist/src/render/term-archive.js.map +1 -1
- package/dist/src/render/widget-areas.d.ts +13 -0
- package/dist/src/render/widget-areas.d.ts.map +1 -0
- package/dist/src/render/widget-areas.js +20 -0
- package/dist/src/render/widget-areas.js.map +1 -0
- package/dist/theme.config.js +2 -2
- package/dist/theme.config.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +1 -0
- package/src/render/blocks/accordion.ts +32 -27
- package/src/render/blocks/collection-list.ts +84 -53
- package/src/render/blocks/cta.ts +27 -28
- package/src/render/blocks/embed.ts +46 -43
- package/src/render/blocks/faq.ts +25 -21
- package/src/render/blocks/feature-grid.ts +42 -35
- package/src/render/blocks/gallery.ts +21 -16
- package/src/render/blocks/hero.ts +40 -28
- package/src/render/blocks/logo-strip.ts +26 -16
- package/src/render/blocks/logos.ts +33 -17
- package/src/render/blocks/media-figure.ts +22 -14
- package/src/render/blocks/pricing-table.ts +26 -15
- package/src/render/blocks/prose.ts +18 -4
- package/src/render/blocks/quote.ts +18 -14
- package/src/render/blocks/stat-counter.ts +25 -20
- package/src/render/blocks/stats.ts +31 -21
- package/src/render/blocks/testimonial.ts +22 -18
- package/src/render/chrome.ts +84 -49
- package/src/render/layout.ts +67 -0
- package/src/render/render-block.ts +24 -1
- package/src/render/term-archive.ts +54 -50
- package/src/render/widget-areas.ts +23 -0
- package/src/styles/archive.css +135 -38
- package/src/styles/base.css +557 -430
- package/src/styles/blocks.css +1192 -1000
- package/src/styles/theme.css +18 -24
- package/src/styles/tokens.css +162 -192
- package/src/styles/widgets.css +913 -0
- package/tokens.json +17 -17
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import type { EmbedBlock } from '@cogenta/blocks'
|
|
2
2
|
import { aspectRatio, type HtmlElement, h, type RenderContext } from '@cogenta/theme-kit'
|
|
3
|
+
import { section } from '../layout.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Nothing here contacts a third party before the visitor has consented.
|
|
6
7
|
*
|
|
7
|
-
* When `consentRequired` is true the block renders a
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* When `consentRequired` is true the block renders a notice and an outbound
|
|
9
|
+
* link: no `<iframe>`, no `<script>`, no preconnect, no poster fetched from
|
|
10
|
+
* the provider. An embed that "only" loads a thumbnail has already handed
|
|
11
|
+
* the visitor's IP address to the provider, which is the transfer consent
|
|
12
|
+
* was supposed to gate.
|
|
12
13
|
*
|
|
13
|
-
* The theme ships no consent
|
|
14
|
-
* granting consent is a site-wide decision
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* are (a thin bordered plate with a small provider tag), so the three read
|
|
19
|
-
* as one family of "media placeholder" treatments.
|
|
14
|
+
* The theme ships no consent button, because it ships no JavaScript:
|
|
15
|
+
* granting consent is a site-wide decision for the consent layer, not a
|
|
16
|
+
* block. The notice is the honest rendering until that layer exists. It
|
|
17
|
+
* holds the same eight central columns and the same ratio the frame would,
|
|
18
|
+
* so the page does not reflow when consent is later granted.
|
|
20
19
|
*/
|
|
21
20
|
|
|
22
21
|
/** `null` means "this provider has no embeddable frame we trust". */
|
|
@@ -28,7 +27,7 @@ function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string |
|
|
|
28
27
|
switch (provider) {
|
|
29
28
|
case 'youtube': {
|
|
30
29
|
// `youtube-nocookie.com` is the privacy-preserving host, and the only
|
|
31
|
-
// one this theme
|
|
30
|
+
// one this theme points an iframe at.
|
|
32
31
|
const id = url.hostname.endsWith('youtu.be')
|
|
33
32
|
? segments[0]
|
|
34
33
|
: (url.searchParams.get('v') ?? segments[1])
|
|
@@ -52,17 +51,17 @@ function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string |
|
|
|
52
51
|
return `https://w.soundcloud.com/player/?url=${encodeURIComponent(rawUrl)}`
|
|
53
52
|
default:
|
|
54
53
|
// bluesky, mastodon, other: a post embed is a script tag, which this
|
|
55
|
-
// theme does not load. The
|
|
54
|
+
// theme does not load. The notice is the rendering.
|
|
56
55
|
return null
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
function
|
|
59
|
+
function consentNotice(block: EmbedBlock, ctx: RenderContext, reason: string): HtmlElement {
|
|
61
60
|
return h(
|
|
62
61
|
'div',
|
|
63
|
-
{ class: 'cg-
|
|
62
|
+
{ class: 'cg-embed__notice' },
|
|
64
63
|
h('p', { class: 'cg-embed__provider', 'aria-hidden': 'true' }, block.provider),
|
|
65
|
-
h('p', { class: 'cg-
|
|
64
|
+
h('p', { class: 'cg-embed__reason' }, reason),
|
|
66
65
|
h(
|
|
67
66
|
'a',
|
|
68
67
|
{
|
|
@@ -77,39 +76,43 @@ function consentCard(block: EmbedBlock, ctx: RenderContext, reason: string): Htm
|
|
|
77
76
|
|
|
78
77
|
export function renderEmbed(block: EmbedBlock, ctx: RenderContext): HtmlElement {
|
|
79
78
|
const source = block.consentRequired ? null : frameSource(block.provider, block.url)
|
|
80
|
-
// Video providers are 16:9 unless the editor framed them otherwise;
|
|
81
|
-
//
|
|
82
|
-
// loads.
|
|
79
|
+
// Video providers are 16:9 unless the editor framed them otherwise; without
|
|
80
|
+
// a ratio the frame would collapse and shift the layout as it loads.
|
|
83
81
|
const ratio = aspectRatio(block.ratio) ?? '16 / 9'
|
|
84
82
|
|
|
85
|
-
return
|
|
83
|
+
return section(
|
|
86
84
|
'div',
|
|
85
|
+
'embed',
|
|
86
|
+
'cg-embed',
|
|
87
87
|
{
|
|
88
|
-
class: 'cg-embed',
|
|
89
|
-
'data-block': 'embed',
|
|
90
88
|
'data-provider': block.provider,
|
|
91
89
|
'data-consent': block.consentRequired ? 'required' : 'not-required',
|
|
92
90
|
style: `--cg-ratio:${ratio}`,
|
|
93
91
|
},
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
92
|
+
'div',
|
|
93
|
+
h(
|
|
94
|
+
'div',
|
|
95
|
+
{ class: 'cg-embed__frame-box' },
|
|
96
|
+
source === null
|
|
97
|
+
? consentNotice(
|
|
98
|
+
block,
|
|
99
|
+
ctx,
|
|
100
|
+
block.consentRequired
|
|
101
|
+
? ctx.t('embed.consentRequired', { provider: block.provider })
|
|
102
|
+
: ctx.t('embed.unsupported', { provider: block.provider }),
|
|
103
|
+
)
|
|
104
|
+
: h('iframe', {
|
|
105
|
+
class: 'cg-embed__frame',
|
|
106
|
+
src: source,
|
|
107
|
+
// An iframe with no accessible name is announced as "frame" and
|
|
108
|
+
// nothing else (WCAG 4.1.2).
|
|
109
|
+
title: ctx.t('embed.title', { provider: block.provider }),
|
|
110
|
+
loading: 'lazy',
|
|
111
|
+
referrerpolicy: 'strict-origin-when-cross-origin',
|
|
112
|
+
allow:
|
|
113
|
+
'accelerometer; clipboard-write; encrypted-media; picture-in-picture; fullscreen',
|
|
114
|
+
allowfullscreen: true,
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
114
117
|
)
|
|
115
118
|
}
|
package/src/render/blocks/faq.ts
CHANGED
|
@@ -7,23 +7,20 @@ import {
|
|
|
7
7
|
type RenderContext,
|
|
8
8
|
renderRichText,
|
|
9
9
|
} from '@cogenta/theme-kit'
|
|
10
|
+
import { section } from '../layout.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Each row carries a numeral (`01`, `02`, …) ahead of the question — the
|
|
17
|
-
* same indexing language `featureGrid` uses — and the disclosure affordance
|
|
18
|
-
* is a plus/minus mark drawn from two CSS borders, not an icon font.
|
|
13
|
+
* Two columns: the title holds the left four and stays in view while the
|
|
14
|
+
* questions, on the right eight, scroll past it. Each question is a row
|
|
15
|
+
* between hairlines with a plus that turns into a minus when open.
|
|
19
16
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* heading
|
|
17
|
+
* `<details>`/`<summary>` rather than a scripted accordion: expanding,
|
|
18
|
+
* keyboard operation and the expanded state announced to assistive
|
|
19
|
+
* technology all come from the browser, at zero bytes of JavaScript. The
|
|
20
|
+
* question is plain text inside `<summary>`, never a heading: a heading
|
|
21
|
+
* nested in a `<summary>` is announced inconsistently across screen readers.
|
|
24
22
|
*/
|
|
25
|
-
function renderItem(item: FaqItem, ctx: RenderContext
|
|
26
|
-
const number = String(index + 1).padStart(2, '0')
|
|
23
|
+
function renderItem(item: FaqItem, ctx: RenderContext): HtmlElement {
|
|
27
24
|
return h(
|
|
28
25
|
'li',
|
|
29
26
|
{ class: 'cg-faq__item' },
|
|
@@ -33,8 +30,8 @@ function renderItem(item: FaqItem, ctx: RenderContext, index: number): HtmlEleme
|
|
|
33
30
|
h(
|
|
34
31
|
'summary',
|
|
35
32
|
{ class: 'cg-faq__question' },
|
|
36
|
-
h('span', { class: 'cg-faq__index', 'aria-hidden': 'true' }, number),
|
|
37
33
|
h('span', { class: 'cg-faq__question-text' }, item.question),
|
|
34
|
+
h('span', { class: 'cg-toggle-mark', 'aria-hidden': 'true' }),
|
|
38
35
|
),
|
|
39
36
|
h('div', { class: 'cg-faq__answer' }, renderRichText(ctx, item.answer)),
|
|
40
37
|
),
|
|
@@ -42,20 +39,27 @@ function renderItem(item: FaqItem, ctx: RenderContext, index: number): HtmlEleme
|
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
export function renderFaq(block: FaqBlock, ctx: RenderContext): HtmlElement {
|
|
45
|
-
return
|
|
42
|
+
return section(
|
|
46
43
|
'section',
|
|
47
|
-
|
|
44
|
+
'faq',
|
|
45
|
+
'cg-faq',
|
|
46
|
+
{ 'data-titled': block.title === undefined ? 'false' : 'true' },
|
|
47
|
+
'div',
|
|
48
48
|
block.title === undefined
|
|
49
49
|
? null
|
|
50
|
-
:
|
|
51
|
-
|
|
52
|
-
{ class: 'cg-
|
|
53
|
-
|
|
50
|
+
: h(
|
|
51
|
+
'div',
|
|
52
|
+
{ class: 'cg-head cg-head--aside cg-head--sticky' },
|
|
53
|
+
heading(
|
|
54
|
+
blockHeadingTag('faq') ?? 'h2',
|
|
55
|
+
{ class: 'cg-head__title', 'data-field': 'title' },
|
|
56
|
+
block.title,
|
|
57
|
+
),
|
|
54
58
|
),
|
|
55
59
|
h(
|
|
56
60
|
'ul',
|
|
57
61
|
{ class: 'cg-faq__items' },
|
|
58
|
-
block.items.map((item
|
|
62
|
+
block.items.map((item) => renderItem(item, ctx)),
|
|
59
63
|
),
|
|
60
64
|
)
|
|
61
65
|
}
|
|
@@ -8,66 +8,73 @@ import {
|
|
|
8
8
|
href,
|
|
9
9
|
nestedHeadingTag,
|
|
10
10
|
type RenderContext,
|
|
11
|
-
renderIcon,
|
|
12
11
|
} from '@cogenta/theme-kit'
|
|
12
|
+
import { arrow, ordinal, section } from '../layout.js'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* B2B "capability card" register a consultancy or software vendor's
|
|
18
|
-
* services page uses, replacing the numbered ledger rows this block used to
|
|
19
|
-
* render (a plain list read as a table of contents, not as a services
|
|
20
|
-
* pitch a visitor scans in one pass).
|
|
15
|
+
* A numbered list of capabilities, ruled by hairlines: `01` to `06`, each
|
|
16
|
+
* row a name in the display serif and a short description beside it.
|
|
21
17
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
18
|
+
* This is how a consultancy presents its practices, and deliberately not a
|
|
19
|
+
* grid of identical cards with icon tiles. The block's own title holds the
|
|
20
|
+
* left four columns (and stays in view while the list scrolls past it on a
|
|
21
|
+
* wide screen); the list takes the right eight.
|
|
25
22
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* `icon` is stored by contract B as a symbol name, and this theme chooses not
|
|
24
|
+
* to draw it: in a numbered, typographic list an icon is decoration, and the
|
|
25
|
+
* ordinal already does the work of marking each row.
|
|
26
|
+
*
|
|
27
|
+
* A row with a link is clickable as a whole, but the link itself stays on the
|
|
28
|
+
* item's title, so its accessible name is the practice's own name
|
|
29
|
+
* (WCAG 2.4.4).
|
|
29
30
|
*/
|
|
30
|
-
function renderItem(
|
|
31
|
+
function renderItem(
|
|
32
|
+
item: FeatureItem,
|
|
33
|
+
index: number,
|
|
34
|
+
ctx: RenderContext,
|
|
35
|
+
tag: HeadingTag,
|
|
36
|
+
): HtmlElement {
|
|
31
37
|
const title =
|
|
32
38
|
item.link === undefined
|
|
33
|
-
? heading(tag, { class: 'cg-
|
|
39
|
+
? heading(tag, { class: 'cg-practice__title' }, item.title)
|
|
34
40
|
: heading(
|
|
35
41
|
tag,
|
|
36
|
-
{ class: 'cg-
|
|
37
|
-
h('a', { class: 'cg-
|
|
42
|
+
{ class: 'cg-practice__title' },
|
|
43
|
+
h('a', { class: 'cg-practice__link', href: href(ctx, item.link) }, item.title, arrow()),
|
|
38
44
|
)
|
|
39
45
|
return h(
|
|
40
46
|
'li',
|
|
41
|
-
{ class: 'cg-
|
|
42
|
-
|
|
43
|
-
? null
|
|
44
|
-
: h(
|
|
45
|
-
'span',
|
|
46
|
-
{ class: 'cg-service__icon', 'data-icon': item.icon, 'aria-hidden': 'true' },
|
|
47
|
-
renderIcon(item.icon),
|
|
48
|
-
),
|
|
47
|
+
{ class: 'cg-practice', 'data-linked': item.link === undefined ? undefined : 'true' },
|
|
48
|
+
h('span', { class: 'cg-practice__index', 'aria-hidden': 'true' }, ordinal(index)),
|
|
49
49
|
title,
|
|
50
|
-
item.text === undefined ? null : h('p', { class: 'cg-
|
|
50
|
+
item.text === undefined ? null : h('p', { class: 'cg-practice__text' }, item.text),
|
|
51
51
|
)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function renderFeatureGrid(block: FeatureGridBlock, ctx: RenderContext): HtmlElement {
|
|
55
55
|
const hasTitle = block.title !== undefined
|
|
56
56
|
const itemTag = nestedHeadingTag('featureGrid', hasTitle)
|
|
57
|
-
return
|
|
57
|
+
return section(
|
|
58
58
|
'section',
|
|
59
|
-
|
|
59
|
+
'featureGrid',
|
|
60
|
+
'cg-practices',
|
|
61
|
+
{ 'data-titled': hasTitle ? 'true' : 'false' },
|
|
62
|
+
'div',
|
|
60
63
|
hasTitle
|
|
61
|
-
?
|
|
62
|
-
|
|
63
|
-
{ class: 'cg-
|
|
64
|
-
|
|
64
|
+
? h(
|
|
65
|
+
'div',
|
|
66
|
+
{ class: 'cg-head cg-head--aside' },
|
|
67
|
+
heading(
|
|
68
|
+
blockHeadingTag('featureGrid') ?? 'h2',
|
|
69
|
+
{ class: 'cg-head__title', 'data-field': 'title' },
|
|
70
|
+
block.title ?? '',
|
|
71
|
+
),
|
|
65
72
|
)
|
|
66
73
|
: null,
|
|
67
74
|
h(
|
|
68
|
-
'
|
|
69
|
-
{ class: 'cg-
|
|
70
|
-
block.items.map((item) => renderItem(item, ctx, itemTag)),
|
|
75
|
+
'ol',
|
|
76
|
+
{ class: 'cg-practices__items' },
|
|
77
|
+
block.items.map((item, index) => renderItem(item, index, ctx, itemTag)),
|
|
71
78
|
),
|
|
72
79
|
)
|
|
73
80
|
}
|
|
@@ -1,37 +1,42 @@
|
|
|
1
1
|
import type { GalleryBlock } from '@cogenta/blocks'
|
|
2
2
|
import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
|
|
3
|
+
import { section } from '../layout.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
* trackpad scrolling and — because the list is focusable and labelled —
|
|
7
|
-
* arrow-key scrolling, with none of the failure modes of a scripted
|
|
8
|
-
* carousel: no auto-advance to fight (WCAG 2.2.2), no focus stolen from an
|
|
9
|
-
* off-screen slide, and it still works before any styling loads.
|
|
6
|
+
* Photographs on the grid, with no frames and no rounded plates.
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* `grid` crops every picture to the same 4:3 ratio in rows of three (two for
|
|
9
|
+
* a pair), so a set reads as a considered series. `masonry` keeps each
|
|
10
|
+
* picture's own proportions in three flowing columns. `carousel` is a
|
|
11
|
+
* scroll-snapping row: touch, trackpad and arrow keys all work because the
|
|
12
|
+
* list is focusable and labelled, with no JavaScript, no auto-advance
|
|
13
|
+
* (WCAG 2.2.2) and no focus stolen from an off-screen slide.
|
|
14
14
|
*/
|
|
15
15
|
export function renderGallery(block: GalleryBlock, ctx: RenderContext): HtmlElement {
|
|
16
16
|
const items = h(
|
|
17
17
|
'ul',
|
|
18
|
-
{
|
|
18
|
+
{
|
|
19
|
+
class: 'cg-gallery__items',
|
|
20
|
+
'data-count': String(Math.min(block.items.length, 6)),
|
|
21
|
+
},
|
|
19
22
|
block.items.map((item) =>
|
|
20
23
|
h(
|
|
21
24
|
'li',
|
|
22
25
|
{ class: 'cg-gallery__item' },
|
|
23
|
-
image(ctx, item.media, {
|
|
26
|
+
image(ctx, item.media, {
|
|
27
|
+
className: 'cg-gallery__image',
|
|
28
|
+
sizes: '(min-width: 64rem) 30vw, (min-width: 40rem) 45vw, 85vw',
|
|
29
|
+
}),
|
|
24
30
|
),
|
|
25
31
|
),
|
|
26
32
|
)
|
|
27
33
|
|
|
28
|
-
return
|
|
34
|
+
return section(
|
|
29
35
|
'section',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
36
|
+
'gallery',
|
|
37
|
+
'cg-gallery',
|
|
38
|
+
{ 'data-layout': block.layout },
|
|
39
|
+
'div',
|
|
35
40
|
block.layout === 'carousel'
|
|
36
41
|
? h(
|
|
37
42
|
'div',
|
|
@@ -8,52 +8,64 @@ import {
|
|
|
8
8
|
image,
|
|
9
9
|
type RenderContext,
|
|
10
10
|
} from '@cogenta/theme-kit'
|
|
11
|
+
import { section } from '../layout.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
-
* `renderPage` relies on it to avoid emitting a second one.
|
|
14
|
+
* A typographic hero on the twelve-column grid.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* The title leads, set large in the display serif across ten columns. Below
|
|
17
|
+
* it the page splits: the subtitle and the actions hold a narrow column on
|
|
18
|
+
* the left, and the photograph takes the right-hand seven columns, cropped
|
|
19
|
+
* frankly, with no frame, no shadow and no shape behind it. The eyebrow is
|
|
20
|
+
* a line of small capitals after a short rule, never a pill.
|
|
21
|
+
*
|
|
22
|
+
* With no media the subtitle moves to the right half of the grid instead, so
|
|
23
|
+
* a hero without a picture is still an asymmetric composition rather than a
|
|
24
|
+
* column of centred text.
|
|
25
|
+
*
|
|
26
|
+
* The hero carries the page's `h1` (contract B: `headingLevel: 'h1'`), which
|
|
27
|
+
* is why `renderPage` never adds a second one.
|
|
22
28
|
*/
|
|
23
29
|
export function renderHero(block: HeroBlock, ctx: RenderContext): HtmlElement {
|
|
24
30
|
const tag = blockHeadingTag('hero') ?? 'h1'
|
|
25
|
-
|
|
31
|
+
const actions = actionList(ctx, block.actions, ctx.t('hero.actions'))
|
|
32
|
+
const hasAside = block.subtitle !== undefined || actions !== null
|
|
33
|
+
return section(
|
|
26
34
|
'section',
|
|
27
|
-
|
|
35
|
+
'hero',
|
|
36
|
+
'cg-hero',
|
|
37
|
+
{ 'data-media': block.media === undefined ? 'none' : 'present' },
|
|
38
|
+
'div',
|
|
28
39
|
h(
|
|
29
40
|
'div',
|
|
30
41
|
{ class: 'cg-hero__intro' },
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'
|
|
34
|
-
|
|
35
|
-
block.eyebrow === undefined
|
|
36
|
-
? null
|
|
37
|
-
: h('p', { class: 'cg-hero__eyebrow', 'data-field': 'eyebrow' }, block.eyebrow),
|
|
38
|
-
heading(tag, { class: 'cg-hero__title', 'data-field': 'title' }, block.title),
|
|
39
|
-
block.subtitle === undefined
|
|
40
|
-
? null
|
|
41
|
-
: h('p', { class: 'cg-hero__subtitle', 'data-field': 'subtitle' }, block.subtitle),
|
|
42
|
-
actionList(ctx, block.actions, ctx.t('hero.actions')),
|
|
43
|
-
),
|
|
42
|
+
block.eyebrow === undefined
|
|
43
|
+
? null
|
|
44
|
+
: h('p', { class: 'cg-hero__eyebrow', 'data-field': 'eyebrow' }, block.eyebrow),
|
|
45
|
+
heading(tag, { class: 'cg-hero__title', 'data-field': 'title' }, block.title),
|
|
44
46
|
),
|
|
47
|
+
hasAside
|
|
48
|
+
? h(
|
|
49
|
+
'div',
|
|
50
|
+
{ class: 'cg-hero__aside' },
|
|
51
|
+
block.subtitle === undefined
|
|
52
|
+
? null
|
|
53
|
+
: h('p', { class: 'cg-hero__subtitle', 'data-field': 'subtitle' }, block.subtitle),
|
|
54
|
+
actions,
|
|
55
|
+
)
|
|
56
|
+
: null,
|
|
45
57
|
block.media === undefined
|
|
46
58
|
? null
|
|
47
59
|
: h(
|
|
48
60
|
'div',
|
|
49
|
-
{ class: 'cg-
|
|
61
|
+
{ class: 'cg-hero__media' },
|
|
50
62
|
// The only image above the fold by construction, so the only one
|
|
51
|
-
// that must not be lazy
|
|
52
|
-
// Lighthouse regression
|
|
63
|
+
// that must not be lazy: a lazy-loaded LCP element is a measured
|
|
64
|
+
// Lighthouse regression.
|
|
53
65
|
image(ctx, block.media, {
|
|
54
|
-
className: 'cg-
|
|
66
|
+
className: 'cg-hero__image',
|
|
55
67
|
loading: 'eager',
|
|
56
|
-
sizes: '(min-width: 64rem)
|
|
68
|
+
sizes: '(min-width: 64rem) 56vw, 100vw',
|
|
57
69
|
}),
|
|
58
70
|
),
|
|
59
71
|
)
|
|
@@ -1,36 +1,46 @@
|
|
|
1
1
|
import type { LogoStripBlock, LogoStripItem } from '@cogenta/blocks'
|
|
2
2
|
import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
|
|
3
|
+
import { section } from '../layout.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* `blocks@2.0` (RFC 0001). The lighter
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* names it, exactly as `logos` already falls back to when a link is absent.
|
|
6
|
+
* `blocks@2.0` (RFC 0001). The lighter "selected clients" line under a hero:
|
|
7
|
+
* the caption as a small-capital label in the left columns, then the marks
|
|
8
|
+
* in one even row across the rest, in greyscale, between two hairlines.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* feel `logos` is built for.
|
|
10
|
+
* The `<figcaption>` comes first in the figure so the label reads before the
|
|
11
|
+
* marks it introduces. No per-logo name or link: each image's own alt text
|
|
12
|
+
* (set once, in the media library) names it.
|
|
14
13
|
*/
|
|
15
14
|
function renderItem(item: LogoStripItem, ctx: RenderContext): HtmlElement {
|
|
16
15
|
return h(
|
|
17
16
|
'li',
|
|
18
|
-
{ class: 'cg-logo-
|
|
19
|
-
image(ctx, item.media, {
|
|
17
|
+
{ class: 'cg-logo-strip__item' },
|
|
18
|
+
image(ctx, item.media, {
|
|
19
|
+
className: 'cg-logo-strip__logo',
|
|
20
|
+
variant: { fit: 'contain' },
|
|
21
|
+
sizes: '12rem',
|
|
22
|
+
}),
|
|
20
23
|
)
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export function renderLogoStrip(block: LogoStripBlock, ctx: RenderContext): HtmlElement {
|
|
24
|
-
return
|
|
27
|
+
return section(
|
|
28
|
+
'div',
|
|
29
|
+
'logoStrip',
|
|
30
|
+
'cg-logo-strip',
|
|
31
|
+
{ 'data-count': String(Math.min(block.logos.length, 8)) },
|
|
25
32
|
'figure',
|
|
26
|
-
|
|
33
|
+
block.caption === undefined
|
|
34
|
+
? null
|
|
35
|
+
: h(
|
|
36
|
+
'figcaption',
|
|
37
|
+
{ class: 'cg-logo-strip__caption', 'data-field': 'caption' },
|
|
38
|
+
block.caption,
|
|
39
|
+
),
|
|
27
40
|
h(
|
|
28
41
|
'ul',
|
|
29
|
-
{ class: 'cg-logo-
|
|
42
|
+
{ class: 'cg-logo-strip__items' },
|
|
30
43
|
block.logos.map((item) => renderItem(item, ctx)),
|
|
31
44
|
),
|
|
32
|
-
block.caption === undefined
|
|
33
|
-
? null
|
|
34
|
-
: h('figcaption', { class: 'cg-logo-band__caption', 'data-field': 'caption' }, block.caption),
|
|
35
45
|
)
|
|
36
46
|
}
|
|
@@ -7,25 +7,24 @@ import {
|
|
|
7
7
|
image,
|
|
8
8
|
type RenderContext,
|
|
9
9
|
} from '@cogenta/theme-kit'
|
|
10
|
+
import { section } from '../layout.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* A
|
|
13
|
-
*
|
|
14
|
-
* text when the media entity has none, and no visually hidden duplicate is
|
|
15
|
-
* added: a link whose only content is an image with `alt` text is already
|
|
16
|
-
* named.
|
|
13
|
+
* A client register: wordmarks in greyscale, each in its own cell of a ruled
|
|
14
|
+
* table, so a dozen marks of different shapes and weights still line up.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* Contract B says the organisation's name is the accessible name of the
|
|
17
|
+
* link, so the image carries it as `alt` when the media entity has none, and
|
|
18
|
+
* no visually hidden duplicate is added. Greyscale is a `filter` in the
|
|
19
|
+
* stylesheet, never baked into the media: the same asset still renders in
|
|
20
|
+
* colour wherever a site owner uses it elsewhere.
|
|
23
21
|
*/
|
|
24
22
|
function renderItem(item: LogoItem, ctx: RenderContext): HtmlElement {
|
|
25
23
|
const logo = image(ctx, item.media, {
|
|
26
24
|
className: 'cg-clients__logo',
|
|
27
25
|
altFrom: item.name,
|
|
28
26
|
variant: { fit: 'contain' },
|
|
27
|
+
sizes: '12rem',
|
|
29
28
|
})
|
|
30
29
|
return h(
|
|
31
30
|
'li',
|
|
@@ -41,20 +40,37 @@ function renderItem(item: LogoItem, ctx: RenderContext): HtmlElement {
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
export function renderLogos(block: LogosBlock, ctx: RenderContext): HtmlElement {
|
|
44
|
-
return
|
|
43
|
+
return section(
|
|
45
44
|
'section',
|
|
46
|
-
|
|
45
|
+
'logos',
|
|
46
|
+
'cg-clients',
|
|
47
|
+
{},
|
|
48
|
+
'div',
|
|
47
49
|
block.title === undefined
|
|
48
50
|
? null
|
|
49
|
-
:
|
|
50
|
-
|
|
51
|
-
{ class: 'cg-
|
|
52
|
-
|
|
51
|
+
: h(
|
|
52
|
+
'div',
|
|
53
|
+
{ class: 'cg-head' },
|
|
54
|
+
heading(
|
|
55
|
+
blockHeadingTag('logos') ?? 'h2',
|
|
56
|
+
{ class: 'cg-head__title', 'data-field': 'title' },
|
|
57
|
+
block.title,
|
|
58
|
+
),
|
|
53
59
|
),
|
|
54
60
|
h(
|
|
55
61
|
'ul',
|
|
56
|
-
{ class: 'cg-clients__items' },
|
|
62
|
+
{ class: 'cg-clients__items', 'data-columns': String(columnsFor(block.items.length)) },
|
|
57
63
|
block.items.map((item) => renderItem(item, ctx)),
|
|
58
64
|
),
|
|
59
65
|
)
|
|
60
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* How many columns the register uses on a wide screen: four when the marks
|
|
70
|
+
* fill whole rows of four, otherwise three, so a register never ends on a
|
|
71
|
+
* row of empty ruled cells when a whole-row layout exists.
|
|
72
|
+
*/
|
|
73
|
+
function columnsFor(count: number): number {
|
|
74
|
+
if (count <= 3) return Math.max(count, 1)
|
|
75
|
+
return count % 4 === 0 ? 4 : 3
|
|
76
|
+
}
|