@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
|
@@ -1,134 +1,100 @@
|
|
|
1
1
|
import type { CollectionListBlock } from '@cogenta/blocks'
|
|
2
2
|
import {
|
|
3
|
-
blockHeadingTag,
|
|
4
3
|
buildCollectionListQuery,
|
|
5
4
|
type ContentEntry,
|
|
6
|
-
entryDate,
|
|
7
|
-
entryExcerpt,
|
|
8
|
-
entryHref,
|
|
9
|
-
entryImage,
|
|
10
|
-
entryTitle,
|
|
11
|
-
type HeadingTag,
|
|
12
5
|
type HtmlElement,
|
|
13
6
|
h,
|
|
14
|
-
heading,
|
|
15
7
|
nestedHeadingTag,
|
|
16
8
|
type RenderContext,
|
|
17
|
-
renderImageSource,
|
|
18
9
|
} from '@cogenta/theme-kit'
|
|
10
|
+
import { section, sectionHead } from '../layout.js'
|
|
11
|
+
import { renderIndexRow, renderWorkCard, workFromEntry } from '../work.js'
|
|
12
|
+
|
|
13
|
+
export { buildCollectionListQuery as query }
|
|
19
14
|
|
|
20
15
|
/**
|
|
21
|
-
*
|
|
22
|
-
* B
|
|
16
|
+
* A listing of work, in one of three forms taken from the layout the editor
|
|
17
|
+
* chose in contract B, never from a setting this theme invents:
|
|
23
18
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
19
|
+
* - `grid` is **the work grid**: an asymmetric sequence on twelve columns,
|
|
20
|
+
* six places long. A large image across seven columns with a smaller one
|
|
21
|
+
* across four, dropped lower, beside it; a full-width image; the pair again
|
|
22
|
+
* mirrored; a full-width image. Every cover keeps the same 3:2 shape, so
|
|
23
|
+
* the asymmetry comes from the columns and the drop, never from cropping
|
|
24
|
+
* one project differently from the next. The pattern lives in `work.css`,
|
|
25
|
+
* keyed on each item's `data-place`.
|
|
26
|
+
* - `list` is **the index**: one ruled row per project, title, client,
|
|
27
|
+
* discipline and year in columns.
|
|
28
|
+
* - `carousel` is **a strip**: cards side by side at one width, scrolling on
|
|
29
|
+
* any screen, a focusable and labelled region. No script.
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
* title set at feature-headline scale, the date pushed to the far edge —
|
|
32
|
-
* unchanged since L23. `grid`/`carousel` (`theme@1.4`) retile the same
|
|
33
|
-
* entries into full-bleed project cards instead: a cover (`entryImage`,
|
|
34
|
-
* 4:3), the title in display type, and — read straight off the raw
|
|
35
|
-
* `ContentEntry` this block's entries already are, never invented — the
|
|
36
|
-
* entry's own `role`/`year` fields as a meta line when a collection happens
|
|
37
|
-
* to declare them (the `portfolio` blueprint's `project` does; any other
|
|
38
|
-
* collection simply shows no meta line).
|
|
31
|
+
* An empty list keeps its label and says so in one line.
|
|
39
32
|
*/
|
|
40
|
-
export { buildCollectionListQuery as query }
|
|
41
33
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
const PLACES = 6
|
|
35
|
+
|
|
36
|
+
function sizesFor(place: number): string {
|
|
37
|
+
if (place === 3 || place === 0) return '(min-width: 64rem) 92rem, 100vw'
|
|
38
|
+
if (place === 1 || place === 5) return '(min-width: 64rem) 54rem, 100vw'
|
|
39
|
+
return '(min-width: 64rem) 30rem, 100vw'
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const role = rawText(entry, 'role')
|
|
50
|
-
const year = rawText(entry, 'year')
|
|
51
|
-
if (role === undefined && year === undefined) return null
|
|
42
|
+
function grid(entries: readonly ContentEntry[], ctx: RenderContext, titled: boolean): HtmlElement {
|
|
43
|
+
const tag = nestedHeadingTag('collectionList', titled)
|
|
52
44
|
return h(
|
|
53
|
-
'
|
|
54
|
-
{ class: 'cg-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
'ul',
|
|
46
|
+
{ class: 'cg-grid', 'data-count': String(entries.length) },
|
|
47
|
+
entries.map((entry, index) => {
|
|
48
|
+
const place = (index + 1) % PLACES
|
|
49
|
+
return h(
|
|
50
|
+
'li',
|
|
51
|
+
{ class: 'cg-grid__item', 'data-place': String(place === 0 ? PLACES : place) },
|
|
52
|
+
renderWorkCard(workFromEntry(entry, ctx), {
|
|
53
|
+
tag,
|
|
54
|
+
sizes: sizesFor(place),
|
|
55
|
+
loading: index < 2 ? 'eager' : 'lazy',
|
|
56
|
+
}),
|
|
57
|
+
)
|
|
58
|
+
}),
|
|
57
59
|
)
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
ctx: RenderContext,
|
|
63
|
-
tag: HeadingTag,
|
|
64
|
-
index: number,
|
|
65
|
-
): HtmlElement {
|
|
66
|
-
const cover = entryImage(entry, ctx, { width: 900, height: 675, fit: 'cover' })
|
|
62
|
+
function index(entries: readonly ContentEntry[], ctx: RenderContext, titled: boolean): HtmlElement {
|
|
63
|
+
const tag = nestedHeadingTag('collectionList', titled)
|
|
67
64
|
return h(
|
|
68
|
-
'
|
|
69
|
-
{ class: 'cg-
|
|
70
|
-
|
|
71
|
-
'article',
|
|
72
|
-
{ class: 'cg-entry__body' },
|
|
73
|
-
h(
|
|
74
|
-
'a',
|
|
75
|
-
{ class: 'cg-entry__cover-link', href: entryHref(entry, ctx), tabindex: -1 },
|
|
76
|
-
cover === undefined
|
|
77
|
-
? h('span', { class: 'cg-entry__cover-empty', 'aria-hidden': 'true' })
|
|
78
|
-
: renderImageSource(cover, {
|
|
79
|
-
className: 'cg-entry__cover',
|
|
80
|
-
sizes: '(min-width: 80rem) 50vw, 100vw',
|
|
81
|
-
}),
|
|
82
|
-
),
|
|
83
|
-
h(
|
|
84
|
-
'span',
|
|
85
|
-
{ class: 'cg-entry__index', 'aria-hidden': 'true' },
|
|
86
|
-
String(index + 1).padStart(2, '0'),
|
|
87
|
-
),
|
|
88
|
-
heading(
|
|
89
|
-
tag,
|
|
90
|
-
{ class: 'cg-entry__title' },
|
|
91
|
-
h('a', { class: 'cg-entry__link', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
|
|
92
|
-
),
|
|
93
|
-
renderCardMeta(entry),
|
|
94
|
-
),
|
|
65
|
+
'ol',
|
|
66
|
+
{ class: 'cg-index', 'data-count': String(entries.length) },
|
|
67
|
+
entries.map((entry) => renderIndexRow(workFromEntry(entry, ctx), tag)),
|
|
95
68
|
)
|
|
96
69
|
}
|
|
97
70
|
|
|
98
|
-
function
|
|
99
|
-
|
|
71
|
+
function strip(
|
|
72
|
+
entries: readonly ContentEntry[],
|
|
100
73
|
ctx: RenderContext,
|
|
101
|
-
|
|
102
|
-
index: number,
|
|
74
|
+
block: CollectionListBlock,
|
|
103
75
|
): HtmlElement {
|
|
104
|
-
const
|
|
105
|
-
const excerpt = entryExcerpt(entry)
|
|
76
|
+
const tag = nestedHeadingTag('collectionList', block.title !== undefined)
|
|
106
77
|
return h(
|
|
107
|
-
'
|
|
108
|
-
{
|
|
78
|
+
'div',
|
|
79
|
+
{
|
|
80
|
+
class: 'cg-strip',
|
|
81
|
+
role: 'region',
|
|
82
|
+
'aria-label': block.title ?? ctx.t('collection.carousel'),
|
|
83
|
+
tabindex: '0',
|
|
84
|
+
},
|
|
109
85
|
h(
|
|
110
|
-
'
|
|
111
|
-
{ class: 'cg-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
86
|
+
'ul',
|
|
87
|
+
{ class: 'cg-strip__items', 'data-count': String(entries.length) },
|
|
88
|
+
entries.map((entry) =>
|
|
89
|
+
h(
|
|
90
|
+
'li',
|
|
91
|
+
{ class: 'cg-strip__item' },
|
|
92
|
+
renderWorkCard(workFromEntry(entry, ctx), {
|
|
93
|
+
tag,
|
|
94
|
+
sizes: '(min-width: 64rem) 34rem, 80vw',
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
116
97
|
),
|
|
117
|
-
heading(
|
|
118
|
-
tag,
|
|
119
|
-
{ class: 'cg-entry__title' },
|
|
120
|
-
h('a', { class: 'cg-entry__link', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
|
|
121
|
-
),
|
|
122
|
-
date === undefined
|
|
123
|
-
? null
|
|
124
|
-
: h(
|
|
125
|
-
'time',
|
|
126
|
-
{ class: 'cg-entry__date', datetime: date },
|
|
127
|
-
// Formatted in the rendered locale rather than left as an ISO
|
|
128
|
-
// string: the machine-readable form is already in `datetime`.
|
|
129
|
-
new Intl.DateTimeFormat(ctx.locale, { dateStyle: 'long' }).format(new Date(date)),
|
|
130
|
-
),
|
|
131
|
-
excerpt === undefined ? null : h('p', { class: 'cg-entry__excerpt' }, excerpt),
|
|
132
98
|
),
|
|
133
99
|
)
|
|
134
100
|
}
|
|
@@ -138,44 +104,24 @@ export function renderCollectionList(
|
|
|
138
104
|
ctx: RenderContext,
|
|
139
105
|
entries: readonly ContentEntry[],
|
|
140
106
|
): HtmlElement {
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const renderItem = isTiled ? renderCard : renderRow
|
|
145
|
-
const items =
|
|
107
|
+
const titled = block.title !== undefined
|
|
108
|
+
const form = block.layout === 'list' ? 'index' : block.layout === 'carousel' ? 'strip' : 'grid'
|
|
109
|
+
const body =
|
|
146
110
|
entries.length === 0
|
|
147
|
-
? h('p', { class: 'cg-
|
|
148
|
-
:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
111
|
+
? h('p', { class: 'cg-empty' }, ctx.t('collection.empty'))
|
|
112
|
+
: form === 'index'
|
|
113
|
+
? index(entries, ctx, titled)
|
|
114
|
+
: form === 'strip'
|
|
115
|
+
? strip(entries, ctx, block)
|
|
116
|
+
: grid(entries, ctx, titled)
|
|
153
117
|
|
|
154
|
-
return
|
|
118
|
+
return section(
|
|
155
119
|
'section',
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
? heading(
|
|
163
|
-
blockHeadingTag('collectionList') ?? 'h2',
|
|
164
|
-
{ class: 'cg-collection__title', 'data-field': 'title' },
|
|
165
|
-
block.title ?? '',
|
|
166
|
-
)
|
|
167
|
-
: null,
|
|
168
|
-
block.layout === 'carousel'
|
|
169
|
-
? h(
|
|
170
|
-
'div',
|
|
171
|
-
{
|
|
172
|
-
class: 'cg-collection__viewport',
|
|
173
|
-
role: 'region',
|
|
174
|
-
'aria-label': block.title ?? ctx.t('collection.carousel'),
|
|
175
|
-
tabindex: '0',
|
|
176
|
-
},
|
|
177
|
-
items,
|
|
178
|
-
)
|
|
179
|
-
: items,
|
|
120
|
+
'collectionList',
|
|
121
|
+
'cg-collection',
|
|
122
|
+
{ 'data-form': form, 'data-collection': block.collection },
|
|
123
|
+
'div',
|
|
124
|
+
sectionHead('collectionList', block.title),
|
|
125
|
+
body,
|
|
180
126
|
)
|
|
181
127
|
}
|
package/src/render/blocks/cta.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CtaBlock } from '@cogenta/blocks'
|
|
2
2
|
import {
|
|
3
|
+
actionLink,
|
|
3
4
|
actionList,
|
|
4
5
|
blockHeadingTag,
|
|
5
6
|
type HtmlElement,
|
|
@@ -7,31 +8,42 @@ import {
|
|
|
7
8
|
heading,
|
|
8
9
|
type RenderContext,
|
|
9
10
|
} from '@cogenta/theme-kit'
|
|
11
|
+
import { section } from '../layout.js'
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* The invitation to get in touch, set the way a studio sets its contact
|
|
15
|
+
* line: a short title and a sentence, then the first action as a very large
|
|
16
|
+
* link in the display width, underlined in the one signal colour. Any further
|
|
17
|
+
* action follows as ordinary underlined words. No box, no band, no button.
|
|
16
18
|
*/
|
|
17
19
|
export function renderCta(block: CtaBlock, ctx: RenderContext): HtmlElement {
|
|
18
|
-
|
|
20
|
+
const [first, ...rest] = block.actions
|
|
21
|
+
const lead =
|
|
22
|
+
first === undefined
|
|
23
|
+
? null
|
|
24
|
+
: (() => {
|
|
25
|
+
const link = actionLink(ctx, first)
|
|
26
|
+
return { ...link, attrs: { ...link.attrs, class: 'cg-action cg-contact__link' } }
|
|
27
|
+
})()
|
|
28
|
+
return section(
|
|
19
29
|
'section',
|
|
20
|
-
|
|
30
|
+
'cta',
|
|
31
|
+
'cg-contact',
|
|
32
|
+
{},
|
|
33
|
+
'div',
|
|
21
34
|
h(
|
|
22
35
|
'div',
|
|
23
|
-
{ class: 'cg-
|
|
36
|
+
{ class: 'cg-contact__copy' },
|
|
24
37
|
heading(
|
|
25
38
|
blockHeadingTag('cta') ?? 'h2',
|
|
26
|
-
{ class: 'cg-
|
|
39
|
+
{ class: 'cg-contact__title', 'data-field': 'title' },
|
|
27
40
|
block.title,
|
|
28
41
|
),
|
|
29
42
|
block.text === undefined
|
|
30
43
|
? null
|
|
31
|
-
: h('p', { class: 'cg-
|
|
32
|
-
// `actions` is required and non-empty for a `cta`, so the list is always
|
|
33
|
-
// rendered; the null branch stays reachable only through invalid data.
|
|
34
|
-
actionList(ctx, block.actions, block.title),
|
|
44
|
+
: h('p', { class: 'cg-contact__text', 'data-field': 'text' }, block.text),
|
|
35
45
|
),
|
|
46
|
+
lead === null ? null : h('p', { class: 'cg-contact__lead' }, lead),
|
|
47
|
+
actionList(ctx, rest, block.title),
|
|
36
48
|
)
|
|
37
49
|
}
|
|
@@ -1,23 +1,17 @@
|
|
|
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
|
+
* A film or a recording from a third party, the size a project image would
|
|
7
|
+
* be: columns 3 to 10 at the block's ratio.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* The theme ships no consent *button*, because it ships no JavaScript:
|
|
14
|
-
* granting consent is a site-wide decision that belongs to the consent
|
|
15
|
-
* layer, not to a block. The card carries a bracketed provider tag in the
|
|
16
|
-
* mono register — `[ youtube ]` — the theme's own visual shorthand for
|
|
17
|
-
* "something external belongs here".
|
|
9
|
+
* Nothing here contacts a third party before the visitor consents: when
|
|
10
|
+
* `consentRequired` is set, or the provider has no cookie-free frame source,
|
|
11
|
+
* a notice renders in the frame's place instead of an `<iframe>`, holding the
|
|
12
|
+
* same shape: a hairline frame, a small label, one sentence and one arrow
|
|
13
|
+
* link. No preconnect, no thumbnail fetched from the provider.
|
|
18
14
|
*/
|
|
19
|
-
|
|
20
|
-
/** `null` means "this provider has no embeddable frame we trust". */
|
|
21
15
|
function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string | null {
|
|
22
16
|
const url = URL.parse(rawUrl)
|
|
23
17
|
if (url === null) return null
|
|
@@ -25,8 +19,6 @@ function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string |
|
|
|
25
19
|
|
|
26
20
|
switch (provider) {
|
|
27
21
|
case 'youtube': {
|
|
28
|
-
// `youtube-nocookie.com` is the privacy-preserving host, and the only one
|
|
29
|
-
// this theme will point an iframe at.
|
|
30
22
|
const id = url.hostname.endsWith('youtu.be')
|
|
31
23
|
? segments[0]
|
|
32
24
|
: (url.searchParams.get('v') ?? segments[1])
|
|
@@ -49,22 +41,20 @@ function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string |
|
|
|
49
41
|
case 'soundcloud':
|
|
50
42
|
return `https://w.soundcloud.com/player/?url=${encodeURIComponent(rawUrl)}`
|
|
51
43
|
default:
|
|
52
|
-
// bluesky, mastodon, other: a post embed is a script tag, which this
|
|
53
|
-
// theme does not load. The link card is the rendering.
|
|
54
44
|
return null
|
|
55
45
|
}
|
|
56
46
|
}
|
|
57
47
|
|
|
58
|
-
function
|
|
48
|
+
function notice(block: EmbedBlock, ctx: RenderContext, reason: string): HtmlElement {
|
|
59
49
|
return h(
|
|
60
50
|
'div',
|
|
61
|
-
{ class: 'cg-
|
|
62
|
-
h('
|
|
63
|
-
h('p', { class: 'cg-
|
|
51
|
+
{ class: 'cg-embed__notice' },
|
|
52
|
+
h('p', { class: 'cg-embed__label' }, ctx.t('embed.label')),
|
|
53
|
+
h('p', { class: 'cg-embed__reason' }, reason),
|
|
64
54
|
h(
|
|
65
55
|
'a',
|
|
66
56
|
{
|
|
67
|
-
class: 'cg-embed__link',
|
|
57
|
+
class: 'cg-arrow-link cg-embed__link',
|
|
68
58
|
href: ctx.link(block.url),
|
|
69
59
|
rel: 'noopener noreferrer nofollow',
|
|
70
60
|
},
|
|
@@ -75,37 +65,39 @@ function consentCard(block: EmbedBlock, ctx: RenderContext, reason: string): Htm
|
|
|
75
65
|
|
|
76
66
|
export function renderEmbed(block: EmbedBlock, ctx: RenderContext): HtmlElement {
|
|
77
67
|
const source = block.consentRequired ? null : frameSource(block.provider, block.url)
|
|
78
|
-
// Video providers are 16:9 unless the editor framed them otherwise; without a
|
|
79
|
-
// ratio the frame would collapse and shift the layout as it loads.
|
|
80
68
|
const ratio = aspectRatio(block.ratio) ?? '16 / 9'
|
|
81
69
|
|
|
82
|
-
return
|
|
70
|
+
return section(
|
|
83
71
|
'div',
|
|
72
|
+
'embed',
|
|
73
|
+
'cg-embed',
|
|
84
74
|
{
|
|
85
|
-
class: 'cg-block cg-embed',
|
|
86
|
-
'data-block': 'embed',
|
|
87
75
|
'data-provider': block.provider,
|
|
88
76
|
'data-consent': block.consentRequired ? 'required' : 'not-required',
|
|
89
77
|
style: `--cg-ratio:${ratio}`,
|
|
90
78
|
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
79
|
+
'div',
|
|
80
|
+
h(
|
|
81
|
+
'div',
|
|
82
|
+
{ class: 'cg-embed__frame' },
|
|
83
|
+
source === null
|
|
84
|
+
? notice(
|
|
85
|
+
block,
|
|
86
|
+
ctx,
|
|
87
|
+
block.consentRequired
|
|
88
|
+
? ctx.t('embed.consentRequired', { provider: block.provider })
|
|
89
|
+
: ctx.t('embed.unsupported', { provider: block.provider }),
|
|
90
|
+
)
|
|
91
|
+
: h('iframe', {
|
|
92
|
+
class: 'cg-embed__player',
|
|
93
|
+
src: source,
|
|
94
|
+
title: ctx.t('embed.title', { provider: block.provider }),
|
|
95
|
+
loading: 'lazy',
|
|
96
|
+
referrerpolicy: 'strict-origin-when-cross-origin',
|
|
97
|
+
allow:
|
|
98
|
+
'accelerometer; clipboard-write; encrypted-media; picture-in-picture; fullscreen',
|
|
99
|
+
allowfullscreen: true,
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
110
102
|
)
|
|
111
103
|
}
|
package/src/render/blocks/faq.ts
CHANGED
|
@@ -1,54 +1,45 @@
|
|
|
1
1
|
import type { FaqBlock, FaqItem } from '@cogenta/blocks'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
type HeadingTag,
|
|
4
4
|
type HtmlElement,
|
|
5
5
|
h,
|
|
6
6
|
heading,
|
|
7
|
+
nestedHeadingTag,
|
|
7
8
|
type RenderContext,
|
|
8
9
|
renderRichText,
|
|
9
10
|
} from '@cogenta/theme-kit'
|
|
11
|
+
import { section, sectionHead } from '../layout.js'
|
|
10
12
|
|
|
11
13
|
/**
|
|
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. A CSS-drawn plus mark (rotated to a cross on `[open]`) replaces
|
|
21
|
-
* the native marker rather than a chevron — the theme's own vocabulary of
|
|
22
|
-
* shapes, kept purely visual.
|
|
14
|
+
* Questions a client asks before a project starts, set open: the label in
|
|
15
|
+
* the first three columns, and from the fourth each question in the display
|
|
16
|
+
* width with its answer beside it on a wide screen, divided by hairlines.
|
|
17
|
+
* Nothing is hidden behind a disclosure: a short set of answers is read
|
|
18
|
+
* whole. (`accordion` is the collapsible form of the same content.)
|
|
23
19
|
*/
|
|
24
|
-
function renderItem(item: FaqItem, ctx: RenderContext): HtmlElement {
|
|
20
|
+
function renderItem(item: FaqItem, ctx: RenderContext, tag: HeadingTag): HtmlElement {
|
|
25
21
|
return h(
|
|
26
|
-
'
|
|
27
|
-
{ class: 'cg-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{ class: 'cg-faq__details' },
|
|
31
|
-
h('summary', { class: 'cg-faq__question' }, h('span', {}, item.question)),
|
|
32
|
-
h('div', { class: 'cg-faq__answer' }, renderRichText(ctx, item.answer)),
|
|
33
|
-
),
|
|
22
|
+
'div',
|
|
23
|
+
{ class: 'cg-answers__item' },
|
|
24
|
+
heading(tag, { class: 'cg-answers__question' }, item.question),
|
|
25
|
+
h('div', { class: 'cg-answers__answer' }, renderRichText(ctx, item.answer)),
|
|
34
26
|
)
|
|
35
27
|
}
|
|
36
28
|
|
|
37
29
|
export function renderFaq(block: FaqBlock, ctx: RenderContext): HtmlElement {
|
|
38
|
-
|
|
30
|
+
const titled = block.title !== undefined
|
|
31
|
+
const tag = nestedHeadingTag('faq', titled)
|
|
32
|
+
return section(
|
|
39
33
|
'section',
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{ class: 'cg-faq__title', 'data-field': 'title' },
|
|
46
|
-
block.title,
|
|
47
|
-
),
|
|
34
|
+
'faq',
|
|
35
|
+
'cg-answers cg-split',
|
|
36
|
+
{ 'data-titled': titled ? 'true' : 'false' },
|
|
37
|
+
'div',
|
|
38
|
+
sectionHead('faq', block.title),
|
|
48
39
|
h(
|
|
49
|
-
'
|
|
50
|
-
{ class: 'cg-
|
|
51
|
-
block.items.map((item) => renderItem(item, ctx)),
|
|
40
|
+
'div',
|
|
41
|
+
{ class: 'cg-answers__items' },
|
|
42
|
+
block.items.map((item) => renderItem(item, ctx, tag)),
|
|
52
43
|
),
|
|
53
44
|
)
|
|
54
45
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { FeatureGridBlock, FeatureItem } from '@cogenta/blocks'
|
|
2
2
|
import {
|
|
3
|
-
blockHeadingTag,
|
|
4
3
|
type HeadingTag,
|
|
5
4
|
type HtmlElement,
|
|
6
5
|
h,
|
|
@@ -8,61 +7,59 @@ import {
|
|
|
8
7
|
href,
|
|
9
8
|
nestedHeadingTag,
|
|
10
9
|
type RenderContext,
|
|
11
|
-
renderIcon,
|
|
12
10
|
} from '@cogenta/theme-kit'
|
|
11
|
+
import { section, sectionHead } from '../layout.js'
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* classic failure of WCAG 2.4.4, and the block carries no label field to
|
|
18
|
-
* write one anyway.
|
|
14
|
+
* Services, disciplines, principles or clients, set as a typographic list
|
|
15
|
+
* rather than as cards with icons. Two forms, chosen from the data itself:
|
|
19
16
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
17
|
+
* - **rows**, when any item carries a sentence: each item a ruled row, its
|
|
18
|
+
* name in the display width on the left and the sentence beside it. A
|
|
19
|
+
* linked item links its name, drawn as an arrow link.
|
|
20
|
+
* - **names**, when no item does: a list of names in columns, the way a
|
|
21
|
+
* studio lists its clients.
|
|
22
|
+
*
|
|
23
|
+
* An item's `icon` is not drawn: a studio's list of services is words, and a
|
|
24
|
+
* pictogram beside each would be decoration. The value stays in the content
|
|
25
|
+
* for a theme that wants it.
|
|
27
26
|
*/
|
|
28
|
-
function renderItem(
|
|
29
|
-
|
|
27
|
+
function renderItem(
|
|
28
|
+
item: FeatureItem,
|
|
29
|
+
ctx: RenderContext,
|
|
30
|
+
tag: HeadingTag,
|
|
31
|
+
form: 'rows' | 'names',
|
|
32
|
+
): HtmlElement {
|
|
33
|
+
const name =
|
|
30
34
|
item.link === undefined
|
|
31
|
-
?
|
|
32
|
-
:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
const icon =
|
|
38
|
-
item.icon === undefined ? null : renderIcon(item.icon, { className: 'cg-feature__icon' })
|
|
35
|
+
? item.title
|
|
36
|
+
: h('a', { class: 'cg-arrow-link cg-list__link', href: href(ctx, item.link) }, item.title)
|
|
37
|
+
// A name in a list of names is an item, not a section of the page: only a
|
|
38
|
+
// row with its own sentence earns a heading in the outline.
|
|
39
|
+
if (form === 'names') return h('li', { class: 'cg-list__item cg-list__name' }, name)
|
|
39
40
|
return h(
|
|
40
41
|
'li',
|
|
41
|
-
{ class: 'cg-
|
|
42
|
-
|
|
43
|
-
h('
|
|
44
|
-
title,
|
|
45
|
-
item.text === undefined ? null : h('p', { class: 'cg-feature__text' }, item.text),
|
|
42
|
+
{ class: 'cg-list__item' },
|
|
43
|
+
heading(tag, { class: 'cg-list__name' }, name),
|
|
44
|
+
item.text === undefined ? null : h('p', { class: 'cg-list__text' }, item.text),
|
|
46
45
|
)
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export function renderFeatureGrid(block: FeatureGridBlock, ctx: RenderContext): HtmlElement {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
49
|
+
const titled = block.title !== undefined
|
|
50
|
+
const tag = nestedHeadingTag('featureGrid', titled)
|
|
51
|
+
const form = block.items.some((item) => item.text !== undefined) ? 'rows' : 'names'
|
|
52
|
+
return section(
|
|
53
53
|
'section',
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
block.title ?? '',
|
|
60
|
-
)
|
|
61
|
-
: null,
|
|
54
|
+
'featureGrid',
|
|
55
|
+
'cg-list cg-split',
|
|
56
|
+
{ 'data-form': form, 'data-titled': titled ? 'true' : 'false' },
|
|
57
|
+
'div',
|
|
58
|
+
sectionHead('featureGrid', block.title),
|
|
62
59
|
h(
|
|
63
60
|
'ul',
|
|
64
|
-
{ class: 'cg-
|
|
65
|
-
block.items.map((item) => renderItem(item, ctx,
|
|
61
|
+
{ class: 'cg-list__items', 'data-count': String(block.items.length) },
|
|
62
|
+
block.items.map((item) => renderItem(item, ctx, tag, form)),
|
|
66
63
|
),
|
|
67
64
|
)
|
|
68
65
|
}
|