@cogenta/theme-association 0.3.2 → 0.5.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 +6 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -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 +13 -10
- package/dist/src/render/blocks/accordion.js.map +1 -1
- package/dist/src/render/blocks/breakdown.d.ts +14 -0
- package/dist/src/render/blocks/breakdown.d.ts.map +1 -0
- package/dist/src/render/blocks/breakdown.js +17 -0
- package/dist/src/render/blocks/breakdown.js.map +1 -0
- package/dist/src/render/blocks/collection-list.d.ts +4 -10
- package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
- package/dist/src/render/blocks/collection-list.js +89 -58
- package/dist/src/render/blocks/collection-list.js.map +1 -1
- package/dist/src/render/blocks/cta.d.ts +0 -5
- package/dist/src/render/blocks/cta.d.ts.map +1 -1
- package/dist/src/render/blocks/cta.js +22 -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 +30 -22
- 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 +11 -15
- package/dist/src/render/blocks/faq.js.map +1 -1
- package/dist/src/render/blocks/feature-grid.d.ts +6 -0
- package/dist/src/render/blocks/feature-grid.d.ts.map +1 -1
- package/dist/src/render/blocks/feature-grid.js +42 -20
- package/dist/src/render/blocks/feature-grid.js.map +1 -1
- package/dist/src/render/blocks/gallery.d.ts +9 -7
- package/dist/src/render/blocks/gallery.d.ts.map +1 -1
- package/dist/src/render/blocks/gallery.js +22 -16
- package/dist/src/render/blocks/gallery.js.map +1 -1
- package/dist/src/render/blocks/hero.d.ts +23 -9
- package/dist/src/render/blocks/hero.d.ts.map +1 -1
- package/dist/src/render/blocks/hero.js +40 -23
- package/dist/src/render/blocks/hero.js.map +1 -1
- package/dist/src/render/blocks/logo-strip.d.ts +6 -0
- package/dist/src/render/blocks/logo-strip.d.ts.map +1 -1
- package/dist/src/render/blocks/logo-strip.js +7 -9
- package/dist/src/render/blocks/logo-strip.js.map +1 -1
- package/dist/src/render/blocks/logos.d.ts +14 -0
- package/dist/src/render/blocks/logos.d.ts.map +1 -1
- package/dist/src/render/blocks/logos.js +30 -16
- package/dist/src/render/blocks/logos.js.map +1 -1
- package/dist/src/render/blocks/media-figure.d.ts +12 -5
- package/dist/src/render/blocks/media-figure.d.ts.map +1 -1
- package/dist/src/render/blocks/media-figure.js +32 -18
- 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 +18 -20
- 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 +4 -7
- package/dist/src/render/blocks/quote.d.ts.map +1 -1
- package/dist/src/render/blocks/quote.js +12 -17
- package/dist/src/render/blocks/quote.js.map +1 -1
- package/dist/src/render/blocks/stat-counter.d.ts +8 -1
- package/dist/src/render/blocks/stat-counter.d.ts.map +1 -1
- package/dist/src/render/blocks/stat-counter.js +18 -10
- package/dist/src/render/blocks/stat-counter.js.map +1 -1
- package/dist/src/render/blocks/stats.d.ts +12 -1
- package/dist/src/render/blocks/stats.d.ts.map +1 -1
- package/dist/src/render/blocks/stats.js +24 -14
- package/dist/src/render/blocks/stats.js.map +1 -1
- package/dist/src/render/blocks/testimonial.d.ts +6 -5
- package/dist/src/render/blocks/testimonial.d.ts.map +1 -1
- package/dist/src/render/blocks/testimonial.js +13 -11
- package/dist/src/render/blocks/testimonial.js.map +1 -1
- package/dist/src/render/chrome.d.ts +38 -1
- package/dist/src/render/chrome.d.ts.map +1 -1
- package/dist/src/render/chrome.js +117 -91
- package/dist/src/render/chrome.js.map +1 -1
- package/dist/src/render/details.d.ts +79 -0
- package/dist/src/render/details.d.ts.map +1 -0
- package/dist/src/render/details.js +221 -0
- package/dist/src/render/details.js.map +1 -0
- package/dist/src/render/figures.d.ts +29 -0
- package/dist/src/render/figures.d.ts.map +1 -0
- package/dist/src/render/figures.js +59 -0
- package/dist/src/render/figures.js.map +1 -0
- package/dist/src/render/layout.d.ts +31 -0
- package/dist/src/render/layout.d.ts.map +1 -0
- package/dist/src/render/layout.js +46 -0
- package/dist/src/render/layout.js.map +1 -0
- package/dist/src/render/page-head.d.ts +7 -0
- package/dist/src/render/page-head.d.ts.map +1 -0
- package/dist/src/render/page-head.js +95 -0
- package/dist/src/render/page-head.js.map +1 -0
- package/dist/src/render/render-block.d.ts +8 -12
- package/dist/src/render/render-block.d.ts.map +1 -1
- package/dist/src/render/render-block.js +40 -29
- package/dist/src/render/render-block.js.map +1 -1
- package/dist/src/render/strings.d.ts +15 -0
- package/dist/src/render/strings.d.ts.map +1 -0
- package/dist/src/render/strings.js +39 -0
- package/dist/src/render/strings.js.map +1 -0
- package/dist/src/render/term-archive.d.ts.map +1 -1
- package/dist/src/render/term-archive.js +33 -25
- 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/theme.config.d.ts +1 -1
- package/dist/theme.config.js +3 -3
- package/dist/theme.config.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +9 -0
- package/src/render/blocks/accordion.ts +28 -22
- package/src/render/blocks/breakdown.ts +42 -0
- package/src/render/blocks/collection-list.ts +234 -103
- package/src/render/blocks/cta.ts +48 -19
- package/src/render/blocks/embed.ts +47 -29
- package/src/render/blocks/faq.ts +22 -31
- package/src/render/blocks/feature-grid.ts +73 -41
- package/src/render/blocks/gallery.ts +32 -26
- package/src/render/blocks/hero.ts +56 -43
- package/src/render/blocks/logo-strip.ts +23 -21
- package/src/render/blocks/logos.ts +45 -40
- package/src/render/blocks/media-figure.ts +49 -24
- package/src/render/blocks/pricing-table.ts +31 -40
- package/src/render/blocks/prose.ts +16 -6
- package/src/render/blocks/quote.ts +34 -27
- package/src/render/blocks/stat-counter.ts +35 -29
- package/src/render/blocks/stats.ts +46 -38
- package/src/render/blocks/testimonial.ts +29 -20
- package/src/render/chrome.ts +165 -91
- package/src/render/details.ts +276 -0
- package/src/render/figures.ts +68 -0
- package/src/render/layout.ts +78 -0
- package/src/render/page-head.ts +193 -0
- package/src/render/render-block.ts +45 -32
- package/src/render/strings.ts +74 -0
- package/src/render/term-archive.ts +94 -58
- package/src/render/widget-areas.ts +24 -0
- package/src/styles/archive.css +38 -102
- package/src/styles/base.css +503 -554
- package/src/styles/blocks.css +913 -666
- package/src/styles/chrome.css +506 -0
- package/src/styles/listings.css +591 -0
- package/src/styles/theme.css +25 -19
- package/src/styles/tokens.css +284 -234
- package/src/styles/widgets.css +1059 -0
- package/tokens.json +18 -18
package/src/render/blocks/cta.ts
CHANGED
|
@@ -7,31 +7,60 @@ import {
|
|
|
7
7
|
heading,
|
|
8
8
|
type RenderContext,
|
|
9
9
|
} from '@cogenta/theme-kit'
|
|
10
|
+
import { giftsOf } from '../figures.js'
|
|
11
|
+
import { optionalText, section } from '../layout.js'
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* The ask. On a charity's site the band that asks is the donation ask, so
|
|
15
|
+
* this theme gives it the signal yellow, across the whole width of the
|
|
16
|
+
* window: the title large on the first six columns, and on the other six the
|
|
17
|
+
* sentence and the actions, the first one a filled ink button.
|
|
18
|
+
*
|
|
19
|
+
* When the text says what a gift pays for, sentence by sentence, each
|
|
20
|
+
* sentence starting with an amount, the amounts are set large beside what
|
|
21
|
+
* they buy (`giftsOf`); any other text stays one paragraph.
|
|
22
|
+
*
|
|
23
|
+
* An invitation that is not a donation ask (a volunteer evening, a
|
|
24
|
+
* newsletter) asks for the quieter band with the `background` variant, which
|
|
25
|
+
* lays the same layout on the paper's darker stock.
|
|
15
26
|
*/
|
|
16
|
-
|
|
27
|
+
function body(block: CtaBlock): HtmlElement | null {
|
|
28
|
+
const gifts = giftsOf(block.text)
|
|
29
|
+
if (gifts === undefined) {
|
|
30
|
+
return optionalText('p', 'ca-cta__text', block.text, { 'data-field': 'text' })
|
|
31
|
+
}
|
|
17
32
|
return h(
|
|
18
|
-
'
|
|
19
|
-
{ class: '
|
|
33
|
+
'div',
|
|
34
|
+
{ class: 'ca-cta__gifts' },
|
|
35
|
+
gifts.lead === undefined ? null : h('p', { class: 'ca-cta__text' }, gifts.lead),
|
|
20
36
|
h(
|
|
21
|
-
'
|
|
22
|
-
{ class: '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
'dl',
|
|
38
|
+
{ class: 'ca-cta__ladder' },
|
|
39
|
+
gifts.gifts.map((gift) =>
|
|
40
|
+
h(
|
|
41
|
+
'div',
|
|
42
|
+
{ class: 'ca-cta__gift' },
|
|
43
|
+
h('dt', { class: 'ca-cta__amount' }, gift.amount),
|
|
44
|
+
h('dd', { class: 'ca-cta__buys' }, gift.text),
|
|
45
|
+
),
|
|
27
46
|
),
|
|
28
|
-
block.text === undefined
|
|
29
|
-
? null
|
|
30
|
-
: h('p', { class: 'cg-cta__text', 'data-field': 'text' }, block.text),
|
|
31
|
-
// `actions` is required and non-empty for a `cta`, so the list is
|
|
32
|
-
// always rendered; the null branch stays reachable only through
|
|
33
|
-
// invalid data.
|
|
34
|
-
actionList(ctx, block.actions, block.title),
|
|
35
47
|
),
|
|
48
|
+
gifts.tail === undefined ? null : h('p', { class: 'ca-cta__text' }, gifts.tail),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function renderCta(block: CtaBlock, ctx: RenderContext): HtmlElement {
|
|
53
|
+
return section(
|
|
54
|
+
'section',
|
|
55
|
+
'cta',
|
|
56
|
+
'ca-cta',
|
|
57
|
+
{ 'data-band': 'signal' },
|
|
58
|
+
'div',
|
|
59
|
+
heading(
|
|
60
|
+
blockHeadingTag('cta') ?? 'h2',
|
|
61
|
+
{ class: 'ca-cta__title', 'data-field': 'title' },
|
|
62
|
+
block.title,
|
|
63
|
+
),
|
|
64
|
+
h('div', { class: 'ca-cta__body' }, body(block), actionList(ctx, block.actions, block.title)),
|
|
36
65
|
)
|
|
37
66
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import type { EmbedBlock } from '@cogenta/blocks'
|
|
2
2
|
import { aspectRatio, type HtmlElement, h, type RenderContext } from '@cogenta/theme-kit'
|
|
3
|
+
import { arrowWords, section } from '../layout.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
+
* A film about the food bank, a recording of the annual meeting, a post from
|
|
7
|
+
* a partner: eight columns of the reading width.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* a
|
|
9
|
+
* Nothing here contacts a third party before the visitor consents: when
|
|
10
|
+
* `consentRequired` is set, or the provider has no cookie-free frame (a map,
|
|
11
|
+
* a post), a notice takes the frame's place. The notice is a short card
|
|
12
|
+
* inside a hairline, not an empty frame kept at the video's ratio: without a
|
|
13
|
+
* script there is nothing to load into that frame later, and a large blank
|
|
14
|
+
* rectangle on a page is a hole. A label, one sentence, and one arrow link to
|
|
15
|
+
* the original. No preconnect, no poster fetched from the provider, no
|
|
16
|
+
* consent button (this theme ships no script).
|
|
12
17
|
*/
|
|
13
|
-
|
|
14
|
-
/** `null` means "this provider has no embeddable frame we trust". */
|
|
15
18
|
function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string | null {
|
|
16
19
|
const url = URL.parse(rawUrl)
|
|
17
20
|
if (url === null) return null
|
|
@@ -41,58 +44,73 @@ function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string |
|
|
|
41
44
|
case 'soundcloud':
|
|
42
45
|
return `https://w.soundcloud.com/player/?url=${encodeURIComponent(rawUrl)}`
|
|
43
46
|
default:
|
|
44
|
-
// bluesky, mastodon, other: a post embed is a script tag
|
|
45
|
-
//
|
|
47
|
+
// bluesky, mastodon, other: a post embed is a script tag and a map is
|
|
48
|
+
// a third-party page, neither of which this theme loads. The notice is
|
|
49
|
+
// the rendering.
|
|
46
50
|
return null
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
function
|
|
54
|
+
function notice(block: EmbedBlock, ctx: RenderContext, reason: string): HtmlElement {
|
|
51
55
|
return h(
|
|
52
56
|
'div',
|
|
53
|
-
{ class: '
|
|
54
|
-
h('p', { class: '
|
|
57
|
+
{ class: 'ca-embed__notice' },
|
|
58
|
+
h('p', { class: 'ca-embed__label' }, ctx.t('embed.label')),
|
|
59
|
+
h('p', { class: 'ca-embed__reason' }, reason),
|
|
55
60
|
h(
|
|
56
61
|
'a',
|
|
57
62
|
{
|
|
58
|
-
class: '
|
|
63
|
+
class: 'ca-arrow-link ca-embed__link',
|
|
59
64
|
href: ctx.link(block.url),
|
|
60
65
|
rel: 'noopener noreferrer nofollow',
|
|
61
66
|
},
|
|
62
|
-
|
|
67
|
+
arrowWords(
|
|
68
|
+
block.provider === 'other'
|
|
69
|
+
? ctx.t('embed.openOther')
|
|
70
|
+
: ctx.t('embed.open', { provider: block.provider }),
|
|
71
|
+
),
|
|
63
72
|
),
|
|
64
73
|
)
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
export function renderEmbed(block: EmbedBlock, ctx: RenderContext): HtmlElement {
|
|
68
77
|
const source = block.consentRequired ? null : frameSource(block.provider, block.url)
|
|
78
|
+
// Without a ratio the frame would collapse and shift the layout as it loads.
|
|
69
79
|
const ratio = aspectRatio(block.ratio) ?? '16 / 9'
|
|
70
80
|
|
|
71
|
-
return
|
|
81
|
+
return section(
|
|
72
82
|
'div',
|
|
83
|
+
'embed',
|
|
84
|
+
'ca-embed',
|
|
73
85
|
{
|
|
74
|
-
class: 'cg-block cg-embed',
|
|
75
|
-
'data-block': 'embed',
|
|
76
86
|
'data-provider': block.provider,
|
|
77
87
|
'data-consent': block.consentRequired ? 'required' : 'not-required',
|
|
78
|
-
|
|
88
|
+
'data-frame': source === null ? 'notice' : 'player',
|
|
79
89
|
},
|
|
90
|
+
'div',
|
|
80
91
|
source === null
|
|
81
|
-
?
|
|
92
|
+
? notice(
|
|
82
93
|
block,
|
|
83
94
|
ctx,
|
|
84
95
|
block.consentRequired
|
|
85
96
|
? ctx.t('embed.consentRequired', { provider: block.provider })
|
|
86
97
|
: ctx.t('embed.unsupported', { provider: block.provider }),
|
|
87
98
|
)
|
|
88
|
-
: h(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
: h(
|
|
100
|
+
'div',
|
|
101
|
+
{ class: 'ca-embed__frame', style: `aspect-ratio:${ratio}` },
|
|
102
|
+
h('iframe', {
|
|
103
|
+
class: 'ca-embed__player',
|
|
104
|
+
src: source,
|
|
105
|
+
// A frame with no accessible name is announced as "frame" and
|
|
106
|
+
// nothing else (WCAG 4.1.2).
|
|
107
|
+
title: ctx.t('embed.title', { provider: block.provider }),
|
|
108
|
+
loading: 'lazy',
|
|
109
|
+
referrerpolicy: 'strict-origin-when-cross-origin',
|
|
110
|
+
allow:
|
|
111
|
+
'accelerometer; clipboard-write; encrypted-media; picture-in-picture; fullscreen',
|
|
112
|
+
allowfullscreen: true,
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
97
115
|
)
|
|
98
116
|
}
|
package/src/render/blocks/faq.ts
CHANGED
|
@@ -1,52 +1,43 @@
|
|
|
1
1
|
import type { FaqBlock, FaqItem } from '@cogenta/blocks'
|
|
2
2
|
import {
|
|
3
|
-
blockHeadingTag,
|
|
4
3
|
type HtmlElement,
|
|
5
4
|
h,
|
|
6
5
|
heading,
|
|
6
|
+
nestedHeadingTag,
|
|
7
7
|
type RenderContext,
|
|
8
8
|
renderRichText,
|
|
9
9
|
} from '@cogenta/theme-kit'
|
|
10
|
+
import { section, sectionHead } from '../layout.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* The question is plain text inside `<summary>`, not a heading: a heading
|
|
18
|
-
* nested in a summary is rendered inconsistently across screen readers, and
|
|
19
|
-
* the questions are already reachable as a list under the block's own
|
|
20
|
-
* heading.
|
|
13
|
+
* The questions people ask before they give an hour or a pound, answered in
|
|
14
|
+
* the open: every answer is printed under its question, two columns of them
|
|
15
|
+
* on a wide screen, each pair under a hairline. A charity's answers are short
|
|
16
|
+
* and they are the point; hiding them behind a click makes a visitor work for
|
|
17
|
+
* reassurance. (The `accordion` block is the collapsible one.)
|
|
21
18
|
*/
|
|
22
|
-
function
|
|
19
|
+
function pair(item: FaqItem, ctx: RenderContext, titled: boolean): HtmlElement {
|
|
23
20
|
return h(
|
|
24
|
-
'
|
|
25
|
-
{ class: '
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ class: 'cg-faq__details' },
|
|
29
|
-
h('summary', { class: 'cg-faq__question' }, item.question),
|
|
30
|
-
h('div', { class: 'cg-faq__answer' }, renderRichText(ctx, item.answer)),
|
|
31
|
-
),
|
|
21
|
+
'div',
|
|
22
|
+
{ class: 'ca-faq__item' },
|
|
23
|
+
heading(nestedHeadingTag('faq', titled), { class: 'ca-faq__question' }, item.question),
|
|
24
|
+
h('div', { class: 'ca-faq__answer' }, renderRichText(ctx, item.answer)),
|
|
32
25
|
)
|
|
33
26
|
}
|
|
34
27
|
|
|
35
28
|
export function renderFaq(block: FaqBlock, ctx: RenderContext): HtmlElement {
|
|
36
|
-
|
|
29
|
+
const titled = block.title !== undefined
|
|
30
|
+
return section(
|
|
37
31
|
'section',
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{ class: 'cg-faq__title', 'data-field': 'title' },
|
|
44
|
-
block.title,
|
|
45
|
-
),
|
|
32
|
+
'faq',
|
|
33
|
+
'ca-faq',
|
|
34
|
+
{ 'data-titled': String(titled) },
|
|
35
|
+
'div',
|
|
36
|
+
sectionHead('faq', block.title),
|
|
46
37
|
h(
|
|
47
|
-
'
|
|
48
|
-
{ class: '
|
|
49
|
-
block.items.map((item) =>
|
|
38
|
+
'div',
|
|
39
|
+
{ class: 'ca-faq__items' },
|
|
40
|
+
block.items.map((item) => pair(item, ctx, titled)),
|
|
50
41
|
),
|
|
51
42
|
)
|
|
52
43
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { FeatureGridBlock, FeatureItem } from '@cogenta/blocks'
|
|
2
2
|
import {
|
|
3
|
-
blockHeadingTag,
|
|
4
|
-
type HeadingTag,
|
|
5
3
|
type HtmlElement,
|
|
6
4
|
h,
|
|
7
5
|
heading,
|
|
@@ -10,58 +8,92 @@ import {
|
|
|
10
8
|
type RenderContext,
|
|
11
9
|
renderIcon,
|
|
12
10
|
} from '@cogenta/theme-kit'
|
|
11
|
+
import { arrowWords, section, sectionHead } from '../layout.js'
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* link, so the link's accessible name is the programme's own name (WCAG
|
|
18
|
-
* 2.4.4) — the block has no separate "learn more" field to write.
|
|
14
|
+
* Two readings of the same block, chosen by its content rather than by a
|
|
15
|
+
* setting:
|
|
19
16
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* - **Ways in**, when an item names an icon this theme can draw: ruled
|
|
18
|
+
* columns, each with a small line icon in the organisation's green, the
|
|
19
|
+
* name, its sentence, and the name as an arrow link when it leads somewhere.
|
|
20
|
+
* Never a tinted tile behind an icon, never a card.
|
|
21
|
+
* - **Steps**, when no item names an icon: an ordered list, each step under a
|
|
22
|
+
* rule with its number set large in the display face, because a list of
|
|
23
|
+
* things without pictures on a charity's site is almost always "how it
|
|
24
|
+
* works": how to sign up, what happens after you give, the first evening.
|
|
23
25
|
*/
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
function hasIcons(block: FeatureGridBlock): boolean {
|
|
27
|
+
return block.items.some((item) => item.icon !== undefined)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How many columns a wide screen gets: as many as there are items up to
|
|
32
|
+
* four, then three when the count divides by three, so six items make two
|
|
33
|
+
* full rows of three rather than a row of four and a row of two.
|
|
34
|
+
*/
|
|
35
|
+
export function columnsFor(count: number): number {
|
|
36
|
+
if (count <= 4) return count
|
|
37
|
+
return count % 3 === 0 ? 3 : 4
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function name(item: FeatureItem, ctx: RenderContext): HtmlElement | string {
|
|
41
|
+
return item.link === undefined
|
|
42
|
+
? item.title
|
|
43
|
+
: h('a', { class: 'ca-arrow-link', href: href(ctx, item.link) }, arrowWords(item.title))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function column(item: FeatureItem, ctx: RenderContext, titled: boolean): HtmlElement {
|
|
47
|
+
const icon =
|
|
48
|
+
item.icon === undefined ? null : renderIcon(item.icon, { className: 'ca-ways__icon', size: 28 })
|
|
33
49
|
return h(
|
|
34
50
|
'li',
|
|
35
|
-
{ class: '
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'span',
|
|
40
|
-
{ class: 'cg-feature__icon', 'data-icon': item.icon, 'aria-hidden': 'true' },
|
|
41
|
-
renderIcon(item.icon),
|
|
42
|
-
),
|
|
43
|
-
title,
|
|
44
|
-
item.text === undefined ? null : h('p', { class: 'cg-feature__text' }, item.text),
|
|
51
|
+
{ class: 'ca-ways__item' },
|
|
52
|
+
icon,
|
|
53
|
+
heading(nestedHeadingTag('featureGrid', titled), { class: 'ca-ways__name' }, name(item, ctx)),
|
|
54
|
+
item.text === undefined ? null : h('p', { class: 'ca-ways__text' }, item.text),
|
|
45
55
|
)
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
const hasTitle = block.title !== undefined
|
|
50
|
-
const itemTag = nestedHeadingTag('featureGrid', hasTitle)
|
|
58
|
+
function step(item: FeatureItem, ctx: RenderContext, titled: boolean, index: number): HtmlElement {
|
|
51
59
|
return h(
|
|
60
|
+
'li',
|
|
61
|
+
{ class: 'ca-steps__item' },
|
|
62
|
+
h('span', { class: 'ca-steps__number', 'aria-hidden': 'true' }, String(index + 1)),
|
|
63
|
+
heading(nestedHeadingTag('featureGrid', titled), { class: 'ca-steps__name' }, name(item, ctx)),
|
|
64
|
+
item.text === undefined ? null : h('p', { class: 'ca-steps__text' }, item.text),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function renderFeatureGrid(block: FeatureGridBlock, ctx: RenderContext): HtmlElement {
|
|
69
|
+
const titled = block.title !== undefined
|
|
70
|
+
const count = String(columnsFor(block.items.length))
|
|
71
|
+
if (!hasIcons(block)) {
|
|
72
|
+
return section(
|
|
73
|
+
'section',
|
|
74
|
+
'featureGrid',
|
|
75
|
+
'ca-steps',
|
|
76
|
+
{ 'data-titled': String(titled), 'data-count': count },
|
|
77
|
+
'div',
|
|
78
|
+
sectionHead('featureGrid', block.title),
|
|
79
|
+
h(
|
|
80
|
+
'ol',
|
|
81
|
+
{ class: 'ca-steps__items' },
|
|
82
|
+
block.items.map((item, index) => step(item, ctx, titled, index)),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
return section(
|
|
52
87
|
'section',
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
block.title ?? '',
|
|
59
|
-
)
|
|
60
|
-
: null,
|
|
88
|
+
'featureGrid',
|
|
89
|
+
'ca-ways',
|
|
90
|
+
{ 'data-titled': String(titled), 'data-count': count },
|
|
91
|
+
'div',
|
|
92
|
+
sectionHead('featureGrid', block.title),
|
|
61
93
|
h(
|
|
62
94
|
'ul',
|
|
63
|
-
{ class: '
|
|
64
|
-
block.items.map((item) =>
|
|
95
|
+
{ class: 'ca-ways__items' },
|
|
96
|
+
block.items.map((item) => column(item, ctx, titled)),
|
|
65
97
|
),
|
|
66
98
|
)
|
|
67
99
|
}
|
|
@@ -1,47 +1,53 @@
|
|
|
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
|
-
* gives touch swiping, trackpad scrolling and — because the list is focusable
|
|
7
|
-
* and labelled — arrow-key scrolling, with none of the failure modes of a
|
|
8
|
-
* scripted carousel (WCAG 2.2.2).
|
|
6
|
+
* Photographs of a year at the hall, in the three layouts contract B names:
|
|
9
7
|
*
|
|
10
|
-
* `grid
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* - `grid`: one picture takes the row at 16:9; two share it at 3:2; three
|
|
9
|
+
* make one large picture on the first eight columns with the other two
|
|
10
|
+
* stacked beside it; four make two rows of two at 3:2; five and more make
|
|
11
|
+
* rows of three at 4:3. No row is ever left with a gap in it.
|
|
12
|
+
* - `masonry`: three columns of pictures at their own proportions.
|
|
13
|
+
* - `carousel`: a row of 4:3 crops that scrolls sideways, with native scroll
|
|
14
|
+
* snapping. It is a focusable, labelled region so the row can be scrolled
|
|
15
|
+
* from the keyboard; no script drives it.
|
|
13
16
|
*/
|
|
14
17
|
export function renderGallery(block: GalleryBlock, ctx: RenderContext): HtmlElement {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
const count = block.items.length
|
|
19
|
+
const items = block.items.map((item, index) =>
|
|
20
|
+
h(
|
|
21
|
+
'li',
|
|
22
|
+
{ class: 'ca-gallery__item' },
|
|
23
|
+
image(ctx, item.media, {
|
|
24
|
+
className: 'ca-gallery__image',
|
|
25
|
+
sizes:
|
|
26
|
+
block.layout === 'grid' && index === 0
|
|
27
|
+
? '(min-width: 64rem) 52vw, 100vw'
|
|
28
|
+
: '(min-width: 64rem) 30vw, 80vw',
|
|
29
|
+
}),
|
|
24
30
|
),
|
|
25
31
|
)
|
|
32
|
+
const list = h('ul', { class: 'ca-gallery__items' }, items)
|
|
26
33
|
|
|
27
|
-
return
|
|
28
|
-
'
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
},
|
|
34
|
+
return section(
|
|
35
|
+
'div',
|
|
36
|
+
'gallery',
|
|
37
|
+
'ca-gallery',
|
|
38
|
+
{ 'data-layout': block.layout, 'data-count': count >= 5 ? 'many' : String(count) },
|
|
39
|
+
'div',
|
|
34
40
|
block.layout === 'carousel'
|
|
35
41
|
? h(
|
|
36
42
|
'div',
|
|
37
43
|
{
|
|
38
|
-
class: '
|
|
44
|
+
class: 'ca-gallery__viewport',
|
|
39
45
|
role: 'region',
|
|
40
46
|
'aria-label': ctx.t('gallery.carousel'),
|
|
41
47
|
tabindex: '0',
|
|
42
48
|
},
|
|
43
|
-
|
|
49
|
+
list,
|
|
44
50
|
)
|
|
45
|
-
:
|
|
51
|
+
: list,
|
|
46
52
|
)
|
|
47
53
|
}
|
|
@@ -1,60 +1,73 @@
|
|
|
1
1
|
import type { HeroBlock } from '@cogenta/blocks'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
blockHeadingTag,
|
|
5
|
-
type HtmlElement,
|
|
6
|
-
h,
|
|
7
|
-
heading,
|
|
8
|
-
image,
|
|
9
|
-
type RenderContext,
|
|
10
|
-
} from '@cogenta/theme-kit'
|
|
2
|
+
import { actionList, type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
|
|
3
|
+
import { optionalText } from '../layout.js'
|
|
11
4
|
|
|
12
5
|
/**
|
|
13
|
-
* The
|
|
14
|
-
*
|
|
6
|
+
* The opening of a page: a photograph of the people the organisation works
|
|
7
|
+
* with, across the whole width of the window, and the statement of the cause
|
|
8
|
+
* set large on a sheet of the page's own paper that starts at the window's
|
|
9
|
+
* left edge and rises into the bottom of the photograph.
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
11
|
+
* On a wide screen the sheet holds the eyebrow and the statement on the first
|
|
12
|
+
* seven columns; the subtitle and the actions sit under the photograph on
|
|
13
|
+
* the last four, starting where the photograph ends, so the two halves of the
|
|
14
|
+
* opening read as one composition rather than a box with a hole beside it.
|
|
15
|
+
*
|
|
16
|
+
* The words are never laid over the picture. A headline floated in the middle
|
|
17
|
+
* of a dimmed photograph needs a veil to stay legible on every picture an
|
|
18
|
+
* editor may choose, and that veil is the look of every generated charity
|
|
19
|
+
* template; on the paper the statement is legible in both schemes whatever
|
|
20
|
+
* the photograph, and the faces keep their own light.
|
|
21
|
+
*
|
|
22
|
+
* The first action is the donation ask, drawn in the signal yellow; the
|
|
23
|
+
* others are underlined words. An eyebrow, when there is one, is a short line
|
|
24
|
+
* in the organisation's green, never a badge. Without media the block is the
|
|
25
|
+
* sheet alone.
|
|
26
|
+
*
|
|
27
|
+
* The image is the only one on a page loaded eagerly: it is above the fold
|
|
28
|
+
* by construction.
|
|
22
29
|
*/
|
|
23
30
|
export function renderHero(block: HeroBlock, ctx: RenderContext): HtmlElement {
|
|
24
|
-
const
|
|
31
|
+
const hasMedia = block.media !== undefined
|
|
32
|
+
const actions = actionList(ctx, block.actions, ctx.t('hero.actions'))
|
|
33
|
+
const hasAside = block.subtitle !== undefined || actions !== null
|
|
25
34
|
return h(
|
|
26
35
|
'section',
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
'
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
? null
|
|
33
|
-
: h(
|
|
34
|
-
'p',
|
|
35
|
-
{ class: 'cg-hero__eyebrow', 'data-field': 'eyebrow' },
|
|
36
|
-
h('span', { class: 'cg-pill' }, block.eyebrow),
|
|
37
|
-
),
|
|
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
|
-
),
|
|
36
|
+
{
|
|
37
|
+
class: 'ca-section ca-hero ca-ask',
|
|
38
|
+
'data-block': 'hero',
|
|
39
|
+
'data-media': String(hasMedia),
|
|
40
|
+
},
|
|
44
41
|
block.media === undefined
|
|
45
42
|
? null
|
|
46
43
|
: h(
|
|
47
|
-
'
|
|
48
|
-
{ class: '
|
|
49
|
-
h('span', { class: 'cg-hero__halo', 'aria-hidden': 'true' }),
|
|
50
|
-
// The only image above the fold by construction, so the only one
|
|
51
|
-
// that must not be lazy — a lazy-loaded LCP element is a measured
|
|
52
|
-
// Lighthouse regression, not a theoretical one.
|
|
44
|
+
'figure',
|
|
45
|
+
{ class: 'ca-hero__media' },
|
|
53
46
|
image(ctx, block.media, {
|
|
54
|
-
className: '
|
|
47
|
+
className: 'ca-hero__image',
|
|
55
48
|
loading: 'eager',
|
|
56
|
-
sizes: '
|
|
49
|
+
sizes: '100vw',
|
|
57
50
|
}),
|
|
58
51
|
),
|
|
52
|
+
h(
|
|
53
|
+
'div',
|
|
54
|
+
{ class: 'ca-container ca-hero__inner' },
|
|
55
|
+
h(
|
|
56
|
+
'div',
|
|
57
|
+
{ class: 'ca-hero__panel' },
|
|
58
|
+
optionalText('p', 'ca-kicker ca-hero__eyebrow', block.eyebrow, {
|
|
59
|
+
'data-field': 'eyebrow',
|
|
60
|
+
}),
|
|
61
|
+
h('h1', { class: 'ca-hero__title', 'data-field': 'title' }, block.title),
|
|
62
|
+
),
|
|
63
|
+
hasAside
|
|
64
|
+
? h(
|
|
65
|
+
'div',
|
|
66
|
+
{ class: 'ca-hero__aside' },
|
|
67
|
+
optionalText('p', 'ca-hero__subtitle', block.subtitle, { 'data-field': 'subtitle' }),
|
|
68
|
+
actions,
|
|
69
|
+
)
|
|
70
|
+
: null,
|
|
71
|
+
),
|
|
59
72
|
)
|
|
60
73
|
}
|