@cogenta/theme-restaurant 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 +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/render/blocks/accordion.d.ts +7 -0
- package/dist/src/render/blocks/accordion.d.ts.map +1 -1
- package/dist/src/render/blocks/accordion.js +10 -16
- package/dist/src/render/blocks/accordion.js.map +1 -1
- package/dist/src/render/blocks/collection-list.d.ts +4 -11
- package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
- package/dist/src/render/blocks/collection-list.js +96 -96
- package/dist/src/render/blocks/collection-list.js.map +1 -1
- package/dist/src/render/blocks/cta.d.ts +7 -4
- package/dist/src/render/blocks/cta.d.ts.map +1 -1
- package/dist/src/render/blocks/cta.js +9 -7
- 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 -28
- package/dist/src/render/blocks/embed.js.map +1 -1
- package/dist/src/render/blocks/faq.d.ts +13 -1
- package/dist/src/render/blocks/faq.d.ts.map +1 -1
- package/dist/src/render/blocks/faq.js +15 -11
- 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 +43 -16
- package/dist/src/render/blocks/feature-grid.js.map +1 -1
- package/dist/src/render/blocks/gallery.d.ts +0 -7
- package/dist/src/render/blocks/gallery.d.ts.map +1 -1
- package/dist/src/render/blocks/gallery.js +28 -10
- package/dist/src/render/blocks/gallery.js.map +1 -1
- package/dist/src/render/blocks/hero.d.ts +16 -9
- package/dist/src/render/blocks/hero.d.ts.map +1 -1
- package/dist/src/render/blocks/hero.js +27 -20
- 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 +8 -0
- package/dist/src/render/blocks/logos.d.ts.map +1 -1
- package/dist/src/render/blocks/logos.js +18 -18
- package/dist/src/render/blocks/logos.js.map +1 -1
- package/dist/src/render/blocks/media-figure.d.ts +14 -3
- package/dist/src/render/blocks/media-figure.d.ts.map +1 -1
- package/dist/src/render/blocks/media-figure.js +34 -16
- 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 -22
- package/dist/src/render/blocks/pricing-table.js.map +1 -1
- package/dist/src/render/blocks/prose.d.ts +11 -6
- package/dist/src/render/blocks/prose.d.ts.map +1 -1
- package/dist/src/render/blocks/prose.js +13 -7
- package/dist/src/render/blocks/prose.js.map +1 -1
- package/dist/src/render/blocks/quote.d.ts +6 -2
- package/dist/src/render/blocks/quote.d.ts.map +1 -1
- package/dist/src/render/blocks/quote.js +14 -12
- package/dist/src/render/blocks/quote.js.map +1 -1
- package/dist/src/render/blocks/stat-counter.d.ts +6 -0
- package/dist/src/render/blocks/stat-counter.d.ts.map +1 -1
- package/dist/src/render/blocks/stat-counter.js +7 -10
- package/dist/src/render/blocks/stat-counter.js.map +1 -1
- package/dist/src/render/blocks/stats.d.ts +7 -0
- package/dist/src/render/blocks/stats.d.ts.map +1 -1
- package/dist/src/render/blocks/stats.js +8 -16
- package/dist/src/render/blocks/stats.js.map +1 -1
- package/dist/src/render/blocks/testimonial.d.ts +5 -11
- package/dist/src/render/blocks/testimonial.d.ts.map +1 -1
- package/dist/src/render/blocks/testimonial.js +9 -17
- 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 +101 -84
- package/dist/src/render/chrome.js.map +1 -1
- package/dist/src/render/dish.d.ts +13 -0
- package/dist/src/render/dish.d.ts.map +1 -0
- package/dist/src/render/dish.js +84 -0
- package/dist/src/render/dish.js.map +1 -0
- package/dist/src/render/layout.d.ts +23 -0
- package/dist/src/render/layout.d.ts.map +1 -0
- package/dist/src/render/layout.js +33 -0
- package/dist/src/render/layout.js.map +1 -0
- package/dist/src/render/menu.d.ts +53 -0
- package/dist/src/render/menu.d.ts.map +1 -0
- package/dist/src/render/menu.js +99 -0
- package/dist/src/render/menu.js.map +1 -0
- package/dist/src/render/render-block.d.ts +19 -0
- package/dist/src/render/render-block.d.ts.map +1 -1
- package/dist/src/render/render-block.js +50 -39
- 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 +23 -0
- package/dist/src/render/strings.js.map +1 -0
- package/dist/src/render/term-archive.d.ts +10 -4
- package/dist/src/render/term-archive.d.ts.map +1 -1
- package/dist/src/render/term-archive.js +25 -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.d.ts +1 -1
- package/dist/theme.config.js +2 -2
- package/dist/theme.config.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +3 -0
- package/src/render/blocks/accordion.ts +18 -47
- package/src/render/blocks/collection-list.ts +245 -133
- package/src/render/blocks/cta.ts +20 -11
- package/src/render/blocks/embed.ts +45 -36
- package/src/render/blocks/faq.ts +32 -25
- package/src/render/blocks/feature-grid.ts +83 -33
- package/src/render/blocks/gallery.ts +39 -21
- package/src/render/blocks/hero.ts +41 -34
- package/src/render/blocks/logo-strip.ts +27 -25
- package/src/render/blocks/logos.ts +34 -43
- package/src/render/blocks/media-figure.ts +49 -24
- package/src/render/blocks/pricing-table.ts +30 -42
- package/src/render/blocks/prose.ts +18 -9
- package/src/render/blocks/quote.ts +29 -19
- package/src/render/blocks/stat-counter.ts +24 -29
- package/src/render/blocks/stats.ts +30 -40
- package/src/render/blocks/testimonial.ts +17 -22
- package/src/render/chrome.ts +113 -95
- package/src/render/dish.ts +195 -0
- package/src/render/layout.ts +64 -0
- package/src/render/menu.ts +117 -0
- package/src/render/render-block.ts +51 -40
- package/src/render/strings.ts +44 -0
- package/src/render/term-archive.ts +78 -44
- package/src/render/widget-areas.ts +23 -0
- package/src/styles/archive.css +46 -56
- package/src/styles/base.css +807 -554
- package/src/styles/blocks.css +723 -641
- package/src/styles/menu.css +482 -0
- package/src/styles/theme.css +24 -25
- package/src/styles/tokens.css +239 -225
- package/src/styles/widgets.css +853 -0
- package/tokens.json +16 -16
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { CollectionListBlock } from '@cogenta/blocks'
|
|
2
2
|
import {
|
|
3
|
-
blockHeadingTag,
|
|
4
3
|
buildCollectionListQuery,
|
|
5
4
|
type ContentEntry,
|
|
6
5
|
entryExcerpt,
|
|
6
|
+
entryHref,
|
|
7
7
|
entryImage,
|
|
8
8
|
entryTitle,
|
|
9
9
|
type HeadingTag,
|
|
@@ -11,179 +11,291 @@ import {
|
|
|
11
11
|
h,
|
|
12
12
|
heading,
|
|
13
13
|
nestedHeadingTag,
|
|
14
|
+
type QueryRequest,
|
|
14
15
|
type RenderContext,
|
|
15
16
|
renderImageSource,
|
|
16
17
|
} from '@cogenta/theme-kit'
|
|
18
|
+
import { section, sectionHead } from '../layout.js'
|
|
19
|
+
import {
|
|
20
|
+
currencyOf,
|
|
21
|
+
formatPrice,
|
|
22
|
+
groupBySection,
|
|
23
|
+
isVegetarian,
|
|
24
|
+
type MenuSection,
|
|
25
|
+
priceOf,
|
|
26
|
+
sectionOf,
|
|
27
|
+
} from '../menu.js'
|
|
28
|
+
import { menuString } from '../strings.js'
|
|
17
29
|
|
|
18
30
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* (
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
+
* A list of entries, read by what the entries are rather than by a setting.
|
|
32
|
+
*
|
|
33
|
+
* - **A menu.** Every entry has a price, so this is a menu, and it is set like
|
|
34
|
+
* a printed one: grouped by the section each dish names (in the order the
|
|
35
|
+
* dishes arrive), each section under its name in small capitals, each dish
|
|
36
|
+
* on one line (its name in the display serif, a dotted leader, the price in
|
|
37
|
+
* tabular figures aligned right) with its description under it in muted
|
|
38
|
+
* text. `grid` sets the sections in two columns on a wide screen, `list` in
|
|
39
|
+
* one column at a reading measure. `carousel` turns the same dishes into a
|
|
40
|
+
* band of plates: the photograph at 4:5, the name and the price under it,
|
|
41
|
+
* in a row that scrolls sideways.
|
|
42
|
+
* - **Photographs.** Entries with a picture and no price (a note from the
|
|
43
|
+
* kitchen, a private room): 4:5 photographs with the name under them, as an
|
|
44
|
+
* arrow link, and the summary.
|
|
45
|
+
* - **An index.** Anything else: ruled rows of titles and their summaries.
|
|
31
46
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* "pièges connus" — a new block would need an RFC). Grouping reads the
|
|
35
|
-
* entry's raw `category` field directly: a theme sees a whole entry, not
|
|
36
|
-
* only the fields contract A's system columns declare, and `category` is
|
|
37
|
-
* exactly the field the `restaurant` blueprint's `menu_item` collection
|
|
38
|
-
* names for this. An entry with no `category` (or a collection that never
|
|
39
|
-
* declares one) still renders, in one unlabelled group — grouping is a
|
|
40
|
-
* presentation choice, never a requirement placed back on the schema.
|
|
47
|
+
* The page an entry is shown on is never listed on itself: a "more starters"
|
|
48
|
+
* list on a dish's own page skips the dish above it.
|
|
41
49
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* currency itself is not part of what a theme's `RenderContext` carries, so
|
|
46
|
-
* EUR is this theme's own documented demo default, exactly the way the
|
|
47
|
-
* `store` blueprint's own catalogue prices need no currency conversion
|
|
48
|
-
* either. A site that prices in another currency still gets a sensibly
|
|
49
|
-
* localised number; only the currency symbol would need a real per-site
|
|
50
|
-
* setting this contract does not yet offer.
|
|
50
|
+
* Each picture repeats the link of its name, so it is taken out of the tab
|
|
51
|
+
* order and hidden from assistive technology: one link per entry is
|
|
52
|
+
* announced, the name.
|
|
51
53
|
*/
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
const raw = entry.price
|
|
55
|
-
if (typeof raw !== 'number' || !Number.isFinite(raw)) return null
|
|
56
|
-
try {
|
|
57
|
-
return new Intl.NumberFormat(ctx.locale, { style: 'currency', currency: 'EUR' }).format(raw)
|
|
58
|
-
} catch {
|
|
59
|
-
// An `Intl` failure (an exotic `ctx.locale`) is still a real number worth
|
|
60
|
-
// showing, just without locale-aware grouping/decimal marks.
|
|
61
|
-
return raw.toFixed(2)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
55
|
+
type Shape = 'menu' | 'plates' | 'photos' | 'index'
|
|
64
56
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
57
|
+
function shapeOf(
|
|
58
|
+
entries: readonly ContentEntry[],
|
|
59
|
+
ctx: RenderContext,
|
|
60
|
+
layout: CollectionListBlock['layout'],
|
|
61
|
+
): Shape {
|
|
62
|
+
if (entries.every((entry) => priceOf(entry.price) !== undefined)) {
|
|
63
|
+
return layout === 'carousel' ? 'plates' : 'menu'
|
|
64
|
+
}
|
|
65
|
+
if (entries.every((entry) => entryImage(entry, ctx) !== undefined)) return 'photos'
|
|
66
|
+
return 'index'
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
function price(entry: ContentEntry, ctx: RenderContext, className: string): HtmlElement {
|
|
70
|
+
const amount = priceOf(entry.price) as number
|
|
71
|
+
return h(
|
|
72
|
+
'data',
|
|
73
|
+
{ class: className, value: String(amount) },
|
|
74
|
+
formatPrice(amount, currencyOf(entry.currency), ctx.locale),
|
|
75
|
+
)
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
function
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const photo = entryImage(entry, ctx, { width: 120, height: 120, fit: 'cover' })
|
|
78
|
+
function dish(entry: ContentEntry, ctx: RenderContext): HtmlElement {
|
|
79
|
+
const description = entryExcerpt(entry)
|
|
80
|
+
const vegetarian = isVegetarian(entry)
|
|
80
81
|
return h(
|
|
81
82
|
'li',
|
|
82
|
-
{ class: '
|
|
83
|
-
|
|
83
|
+
{ class: 'cr-menu__item', 'data-vegetarian': vegetarian ? 'true' : undefined },
|
|
84
|
+
h(
|
|
85
|
+
'p',
|
|
86
|
+
{ class: 'cr-menu__line' },
|
|
87
|
+
h('a', { class: 'cr-menu__name', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
|
|
88
|
+
h('span', { class: 'cr-menu__leader', 'aria-hidden': 'true' }),
|
|
89
|
+
price(entry, ctx, 'cr-menu__price'),
|
|
90
|
+
),
|
|
91
|
+
description === undefined && !vegetarian
|
|
84
92
|
? null
|
|
85
93
|
: h(
|
|
86
|
-
'
|
|
87
|
-
{ class: '
|
|
88
|
-
|
|
94
|
+
'p',
|
|
95
|
+
{ class: 'cr-menu__description' },
|
|
96
|
+
description === undefined ? null : h('span', { class: 'cr-menu__text' }, description),
|
|
97
|
+
vegetarian
|
|
98
|
+
? h('span', { class: 'cr-menu__diet' }, menuString(ctx.locale, 'vegetarian'))
|
|
99
|
+
: null,
|
|
89
100
|
),
|
|
90
|
-
h(
|
|
91
|
-
'div',
|
|
92
|
-
{ class: 'cg-menu__dish-body' },
|
|
93
|
-
h(
|
|
94
|
-
'div',
|
|
95
|
-
{ class: 'cg-menu__dish-row' },
|
|
96
|
-
heading(tag, { class: 'cg-menu__dish-name' }, entryTitle(entry, ctx)),
|
|
97
|
-
// Leader and price are one wrapping unit (`cg-menu__dish-tail`): on
|
|
98
|
-
// a narrow screen where the name alone fills the row, the dotted
|
|
99
|
-
// leader must move to the next line together with the price it
|
|
100
|
-
// points to — never left dangling at the end of the name's line
|
|
101
|
-
// with the price it introduces stranded on a line of its own.
|
|
102
|
-
h(
|
|
103
|
-
'span',
|
|
104
|
-
{ class: 'cg-menu__dish-tail' },
|
|
105
|
-
h('span', { class: 'cg-menu__dish-leader', 'aria-hidden': 'true' }),
|
|
106
|
-
price === null ? null : h('span', { class: 'cg-menu__dish-price' }, price),
|
|
107
|
-
),
|
|
108
|
-
),
|
|
109
|
-
excerpt === undefined ? null : h('p', { class: 'cg-menu__dish-description' }, excerpt),
|
|
110
|
-
),
|
|
111
101
|
)
|
|
112
102
|
}
|
|
113
103
|
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
entries: readonly ContentEntry[],
|
|
104
|
+
function menuSection(
|
|
105
|
+
group: MenuSection<ContentEntry>,
|
|
117
106
|
ctx: RenderContext,
|
|
118
|
-
|
|
107
|
+
tag: HeadingTag,
|
|
108
|
+
named: boolean,
|
|
119
109
|
): HtmlElement {
|
|
120
|
-
const dishTag = stepDown(groupTag)
|
|
121
110
|
return h(
|
|
122
111
|
'div',
|
|
123
|
-
{ class: '
|
|
124
|
-
|
|
112
|
+
{ class: 'cr-menu__section' },
|
|
113
|
+
group.title === undefined || !named
|
|
114
|
+
? null
|
|
115
|
+
: heading(tag, { class: 'cr-menu__section-title' }, group.title),
|
|
125
116
|
h(
|
|
126
117
|
'ul',
|
|
127
|
-
{ class: '
|
|
128
|
-
|
|
118
|
+
{ class: 'cr-menu__items' },
|
|
119
|
+
group.items.map((entry) => dish(entry, ctx)),
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function mediaLink(
|
|
125
|
+
entry: ContentEntry,
|
|
126
|
+
ctx: RenderContext,
|
|
127
|
+
className: string,
|
|
128
|
+
sizes: string,
|
|
129
|
+
): HtmlElement | null {
|
|
130
|
+
const source = entryImage(entry, ctx)
|
|
131
|
+
if (source === undefined) return null
|
|
132
|
+
return h(
|
|
133
|
+
'a',
|
|
134
|
+
{
|
|
135
|
+
class: `${className}__media`,
|
|
136
|
+
href: entryHref(entry, ctx),
|
|
137
|
+
tabindex: '-1',
|
|
138
|
+
'aria-hidden': 'true',
|
|
139
|
+
},
|
|
140
|
+
renderImageSource(source, { className: `${className}__image`, sizes }),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function plate(entry: ContentEntry, ctx: RenderContext): HtmlElement {
|
|
145
|
+
const group = sectionOf(entry)
|
|
146
|
+
return h(
|
|
147
|
+
'li',
|
|
148
|
+
{ class: 'cr-plates__item', 'data-media': String(entryImage(entry, ctx) !== undefined) },
|
|
149
|
+
mediaLink(entry, ctx, 'cr-plates', '(min-width: 64rem) 22vw, 70vw'),
|
|
150
|
+
h(
|
|
151
|
+
'p',
|
|
152
|
+
{ class: 'cr-plates__line' },
|
|
153
|
+
h('a', { class: 'cr-plates__name', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
|
|
154
|
+
price(entry, ctx, 'cr-plates__price'),
|
|
129
155
|
),
|
|
156
|
+
group === undefined ? null : h('p', { class: 'cr-plates__section' }, group),
|
|
130
157
|
)
|
|
131
158
|
}
|
|
132
159
|
|
|
133
|
-
function
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
function photo(entry: ContentEntry, ctx: RenderContext, tag: HeadingTag): HtmlElement {
|
|
161
|
+
const excerpt = entryExcerpt(entry)
|
|
162
|
+
return h(
|
|
163
|
+
'li',
|
|
164
|
+
{ class: 'cr-photos__item' },
|
|
165
|
+
mediaLink(entry, ctx, 'cr-photos', '(min-width: 64rem) 28vw, 100vw'),
|
|
166
|
+
heading(
|
|
167
|
+
tag,
|
|
168
|
+
{ class: 'cr-photos__name' },
|
|
169
|
+
h('a', { class: 'cr-arrow-link', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
|
|
170
|
+
),
|
|
171
|
+
excerpt === undefined ? null : h('p', { class: 'cr-photos__text' }, excerpt),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function indexItem(entry: ContentEntry, ctx: RenderContext, tag: HeadingTag): HtmlElement {
|
|
176
|
+
const excerpt = entryExcerpt(entry)
|
|
177
|
+
return h(
|
|
178
|
+
'li',
|
|
179
|
+
{ class: 'cr-index__item' },
|
|
180
|
+
heading(
|
|
181
|
+
tag,
|
|
182
|
+
{ class: 'cr-index__name' },
|
|
183
|
+
h('a', { class: 'cr-index__link', href: entryHref(entry, ctx) }, entryTitle(entry, ctx)),
|
|
184
|
+
),
|
|
185
|
+
excerpt === undefined ? null : h('p', { class: 'cr-index__text' }, excerpt),
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The entries that are not the page being rendered. */
|
|
190
|
+
function othersThanThisPage(
|
|
191
|
+
entries: readonly ContentEntry[],
|
|
192
|
+
ctx: RenderContext,
|
|
193
|
+
): readonly ContentEntry[] {
|
|
194
|
+
return entries.filter((entry) => entryHref(entry, ctx) !== ctx.url.pathname)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function body(
|
|
198
|
+
shape: Shape,
|
|
199
|
+
shown: readonly ContentEntry[],
|
|
200
|
+
block: CollectionListBlock,
|
|
201
|
+
ctx: RenderContext,
|
|
202
|
+
tag: HeadingTag,
|
|
203
|
+
): HtmlElement {
|
|
204
|
+
switch (shape) {
|
|
205
|
+
case 'menu': {
|
|
206
|
+
const groups = groupBySection(shown)
|
|
207
|
+
// One section under a title of its own ("The other starters") is named
|
|
208
|
+
// by that title already; printing "Starters" under it again says nothing.
|
|
209
|
+
const named = groups.length > 1 || block.title === undefined
|
|
210
|
+
return h(
|
|
211
|
+
'div',
|
|
212
|
+
{
|
|
213
|
+
class: 'cr-list__items cr-menu',
|
|
214
|
+
'data-sections': String(groups.length),
|
|
215
|
+
},
|
|
216
|
+
groups.map((group) => menuSection(group, ctx, tag, named)),
|
|
217
|
+
)
|
|
144
218
|
}
|
|
219
|
+
case 'plates':
|
|
220
|
+
return h(
|
|
221
|
+
'div',
|
|
222
|
+
{
|
|
223
|
+
class: 'cr-list__viewport',
|
|
224
|
+
role: 'region',
|
|
225
|
+
'aria-label': block.title ?? ctx.t('collection.carousel'),
|
|
226
|
+
tabindex: '0',
|
|
227
|
+
},
|
|
228
|
+
h(
|
|
229
|
+
'ul',
|
|
230
|
+
{ class: 'cr-list__items cr-plates' },
|
|
231
|
+
shown.map((entry) => plate(entry, ctx)),
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
case 'photos': {
|
|
235
|
+
const list = h(
|
|
236
|
+
'ul',
|
|
237
|
+
{
|
|
238
|
+
class: 'cr-list__items cr-photos',
|
|
239
|
+
'data-count': String(Math.min(shown.length, 3)),
|
|
240
|
+
},
|
|
241
|
+
shown.map((entry) => photo(entry, ctx, tag)),
|
|
242
|
+
)
|
|
243
|
+
return block.layout === 'carousel'
|
|
244
|
+
? h(
|
|
245
|
+
'div',
|
|
246
|
+
{
|
|
247
|
+
class: 'cr-list__viewport',
|
|
248
|
+
role: 'region',
|
|
249
|
+
'aria-label': block.title ?? ctx.t('collection.carousel'),
|
|
250
|
+
tabindex: '0',
|
|
251
|
+
},
|
|
252
|
+
list,
|
|
253
|
+
)
|
|
254
|
+
: list
|
|
255
|
+
}
|
|
256
|
+
case 'index':
|
|
257
|
+
return h(
|
|
258
|
+
'ul',
|
|
259
|
+
{ class: 'cr-list__items cr-index' },
|
|
260
|
+
shown.map((entry) => indexItem(entry, ctx, tag)),
|
|
261
|
+
)
|
|
145
262
|
}
|
|
146
|
-
return order.map((key) => [key, groups.get(key) as ContentEntry[]])
|
|
147
263
|
}
|
|
148
264
|
|
|
149
265
|
export function renderCollectionList(
|
|
150
266
|
block: CollectionListBlock,
|
|
151
267
|
ctx: RenderContext,
|
|
152
|
-
|
|
268
|
+
fetched: readonly ContentEntry[],
|
|
153
269
|
): HtmlElement {
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const body =
|
|
158
|
-
entries.length === 0
|
|
159
|
-
? h('p', { class: 'cg-menu__empty' }, ctx.t('collection.empty'))
|
|
160
|
-
: h(
|
|
161
|
-
'div',
|
|
162
|
-
{ class: 'cg-menu__groups' },
|
|
163
|
-
groups.map(([category, items]) => renderGroup(category, items, ctx, groupTag)),
|
|
164
|
-
)
|
|
270
|
+
const titled = block.title !== undefined
|
|
271
|
+
const shown = othersThanThisPage(fetched, ctx)
|
|
272
|
+
const tag = nestedHeadingTag('collectionList', titled)
|
|
165
273
|
|
|
166
|
-
|
|
274
|
+
if (shown.length === 0) {
|
|
275
|
+
return section(
|
|
276
|
+
'section',
|
|
277
|
+
'collectionList',
|
|
278
|
+
'cr-list',
|
|
279
|
+
{ 'data-layout': block.layout, 'data-shape': 'empty' },
|
|
280
|
+
'div',
|
|
281
|
+
sectionHead('collectionList', block.title),
|
|
282
|
+
h('p', { class: 'cr-empty' }, ctx.t('collection.empty')),
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const shape = shapeOf(shown, ctx, block.layout)
|
|
287
|
+
return section(
|
|
167
288
|
'section',
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
175
|
-
: null,
|
|
176
|
-
block.layout === 'carousel'
|
|
177
|
-
? h(
|
|
178
|
-
'div',
|
|
179
|
-
{
|
|
180
|
-
class: 'cg-menu__viewport',
|
|
181
|
-
role: 'region',
|
|
182
|
-
'aria-label': block.title ?? ctx.t('collection.carousel'),
|
|
183
|
-
tabindex: '0',
|
|
184
|
-
},
|
|
185
|
-
body,
|
|
186
|
-
)
|
|
187
|
-
: body,
|
|
289
|
+
'collectionList',
|
|
290
|
+
'cr-list',
|
|
291
|
+
{ 'data-layout': block.layout, 'data-shape': shape },
|
|
292
|
+
'div',
|
|
293
|
+
sectionHead('collectionList', block.title),
|
|
294
|
+
body(shape, shown, block, ctx, tag),
|
|
188
295
|
)
|
|
189
296
|
}
|
|
297
|
+
|
|
298
|
+
/** The query this block needs, fetched by the host before any markup is built. */
|
|
299
|
+
export function query(block: CollectionListBlock): QueryRequest {
|
|
300
|
+
return buildCollectionListQuery(block)
|
|
301
|
+
}
|
package/src/render/blocks/cta.ts
CHANGED
|
@@ -7,25 +7,34 @@ import {
|
|
|
7
7
|
heading,
|
|
8
8
|
type RenderContext,
|
|
9
9
|
} from '@cogenta/theme-kit'
|
|
10
|
+
import { optionalText, section } from '../layout.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* A call to action set as a line of the page, not a coloured box: a hairline
|
|
14
|
+
* in ink across the container, the title large and light on the first seven
|
|
15
|
+
* columns, the sentence and the actions on the last four. How to book a
|
|
16
|
+
* table, the private room, a gift for someone else.
|
|
17
|
+
*
|
|
18
|
+
* A site that wants a band asks for one with the `background` variant, which
|
|
19
|
+
* lays the same line on the paper's darker stock.
|
|
16
20
|
*/
|
|
17
21
|
export function renderCta(block: CtaBlock, ctx: RenderContext): HtmlElement {
|
|
18
|
-
return
|
|
22
|
+
return section(
|
|
19
23
|
'section',
|
|
20
|
-
|
|
24
|
+
'cta',
|
|
25
|
+
'cr-cta',
|
|
26
|
+
{},
|
|
27
|
+
'div',
|
|
21
28
|
heading(
|
|
22
29
|
blockHeadingTag('cta') ?? 'h2',
|
|
23
|
-
{ class: '
|
|
30
|
+
{ class: 'cr-cta__title', 'data-field': 'title' },
|
|
24
31
|
block.title,
|
|
25
32
|
),
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
h(
|
|
34
|
+
'div',
|
|
35
|
+
{ class: 'cr-cta__body' },
|
|
36
|
+
optionalText('p', 'cr-cta__text', block.text, { 'data-field': 'text' }),
|
|
37
|
+
actionList(ctx, block.actions, block.title),
|
|
38
|
+
),
|
|
30
39
|
)
|
|
31
40
|
}
|
|
@@ -1,23 +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 { section } from '../layout.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (`consentRequired: true`), which this renderer never has a trusted frame
|
|
8
|
-
* source for anyway, so it always renders the honest placeholder card below.
|
|
6
|
+
* A film of the kitchen, a map, a recording from a third party, eight
|
|
7
|
+
* columns wide from the third.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* the consent
|
|
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 ruled
|
|
12
|
+
* card, not an empty frame kept at the video's ratio: without a script there
|
|
13
|
+
* is nothing to load into that frame later, and a large blank rectangle on a
|
|
14
|
+
* page is a hole, not a placeholder. A small label in small capitals, one
|
|
15
|
+
* sentence, and one arrow link to the original. No preconnect, no poster
|
|
16
|
+
* fetched from the provider, no consent button (this theme ships no script).
|
|
18
17
|
*/
|
|
19
|
-
|
|
20
|
-
/** `null` means "this provider has no embeddable frame we trust". */
|
|
21
18
|
function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string | null {
|
|
22
19
|
const url = URL.parse(rawUrl)
|
|
23
20
|
if (url === null) return null
|
|
@@ -47,59 +44,71 @@ function frameSource(provider: EmbedBlock['provider'], rawUrl: string): string |
|
|
|
47
44
|
case 'soundcloud':
|
|
48
45
|
return `https://w.soundcloud.com/player/?url=${encodeURIComponent(rawUrl)}`
|
|
49
46
|
default:
|
|
50
|
-
// bluesky, mastodon, other
|
|
51
|
-
//
|
|
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.
|
|
52
50
|
return null
|
|
53
51
|
}
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
function
|
|
54
|
+
function notice(block: EmbedBlock, ctx: RenderContext, reason: string): HtmlElement {
|
|
57
55
|
return h(
|
|
58
56
|
'div',
|
|
59
|
-
{ class: '
|
|
60
|
-
h('p', { class: '
|
|
61
|
-
h('p', { class: '
|
|
57
|
+
{ class: 'cr-embed__notice' },
|
|
58
|
+
h('p', { class: 'cr-embed__label' }, ctx.t('embed.label')),
|
|
59
|
+
h('p', { class: 'cr-embed__reason' }, reason),
|
|
62
60
|
h(
|
|
63
61
|
'a',
|
|
64
62
|
{
|
|
65
|
-
class: '
|
|
63
|
+
class: 'cr-arrow-link cr-embed__link',
|
|
66
64
|
href: ctx.link(block.url),
|
|
67
65
|
rel: 'noopener noreferrer nofollow',
|
|
68
66
|
},
|
|
69
|
-
|
|
67
|
+
block.provider === 'other'
|
|
68
|
+
? ctx.t('embed.openOther')
|
|
69
|
+
: ctx.t('embed.open', { provider: block.provider }),
|
|
70
70
|
),
|
|
71
71
|
)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export function renderEmbed(block: EmbedBlock, ctx: RenderContext): HtmlElement {
|
|
75
75
|
const source = block.consentRequired ? null : frameSource(block.provider, block.url)
|
|
76
|
+
// Without a ratio the frame would collapse and shift the layout as it loads.
|
|
76
77
|
const ratio = aspectRatio(block.ratio) ?? '16 / 9'
|
|
77
78
|
|
|
78
|
-
return
|
|
79
|
+
return section(
|
|
79
80
|
'div',
|
|
81
|
+
'embed',
|
|
82
|
+
'cr-embed',
|
|
80
83
|
{
|
|
81
|
-
class: 'cg-embed',
|
|
82
|
-
'data-block': 'embed',
|
|
83
84
|
'data-provider': block.provider,
|
|
84
85
|
'data-consent': block.consentRequired ? 'required' : 'not-required',
|
|
85
|
-
|
|
86
|
+
'data-frame': source === null ? 'notice' : 'player',
|
|
86
87
|
},
|
|
88
|
+
'div',
|
|
87
89
|
source === null
|
|
88
|
-
?
|
|
90
|
+
? notice(
|
|
89
91
|
block,
|
|
90
92
|
ctx,
|
|
91
93
|
block.consentRequired
|
|
92
94
|
? ctx.t('embed.consentRequired', { provider: block.provider })
|
|
93
95
|
: ctx.t('embed.unsupported', { provider: block.provider }),
|
|
94
96
|
)
|
|
95
|
-
: h(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
: h(
|
|
98
|
+
'div',
|
|
99
|
+
{ class: 'cr-embed__frame', style: `aspect-ratio:${ratio}` },
|
|
100
|
+
h('iframe', {
|
|
101
|
+
class: 'cr-embed__player',
|
|
102
|
+
src: source,
|
|
103
|
+
// A frame with no accessible name is announced as "frame" and
|
|
104
|
+
// nothing else (WCAG 4.1.2).
|
|
105
|
+
title: ctx.t('embed.title', { provider: block.provider }),
|
|
106
|
+
loading: 'lazy',
|
|
107
|
+
referrerpolicy: 'strict-origin-when-cross-origin',
|
|
108
|
+
allow:
|
|
109
|
+
'accelerometer; clipboard-write; encrypted-media; picture-in-picture; fullscreen',
|
|
110
|
+
allowfullscreen: true,
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
104
113
|
)
|
|
105
114
|
}
|