@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.
Files changed (138) hide show
  1. package/dist/src/index.d.ts +3 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +3 -0
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/render/blocks/accordion.d.ts +7 -0
  6. package/dist/src/render/blocks/accordion.d.ts.map +1 -1
  7. package/dist/src/render/blocks/accordion.js +10 -16
  8. package/dist/src/render/blocks/accordion.js.map +1 -1
  9. package/dist/src/render/blocks/collection-list.d.ts +4 -11
  10. package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
  11. package/dist/src/render/blocks/collection-list.js +96 -96
  12. package/dist/src/render/blocks/collection-list.js.map +1 -1
  13. package/dist/src/render/blocks/cta.d.ts +7 -4
  14. package/dist/src/render/blocks/cta.d.ts.map +1 -1
  15. package/dist/src/render/blocks/cta.js +9 -7
  16. package/dist/src/render/blocks/cta.js.map +1 -1
  17. package/dist/src/render/blocks/embed.d.ts.map +1 -1
  18. package/dist/src/render/blocks/embed.js +30 -28
  19. package/dist/src/render/blocks/embed.js.map +1 -1
  20. package/dist/src/render/blocks/faq.d.ts +13 -1
  21. package/dist/src/render/blocks/faq.d.ts.map +1 -1
  22. package/dist/src/render/blocks/faq.js +15 -11
  23. package/dist/src/render/blocks/faq.js.map +1 -1
  24. package/dist/src/render/blocks/feature-grid.d.ts.map +1 -1
  25. package/dist/src/render/blocks/feature-grid.js +43 -16
  26. package/dist/src/render/blocks/feature-grid.js.map +1 -1
  27. package/dist/src/render/blocks/gallery.d.ts +0 -7
  28. package/dist/src/render/blocks/gallery.d.ts.map +1 -1
  29. package/dist/src/render/blocks/gallery.js +28 -10
  30. package/dist/src/render/blocks/gallery.js.map +1 -1
  31. package/dist/src/render/blocks/hero.d.ts +16 -9
  32. package/dist/src/render/blocks/hero.d.ts.map +1 -1
  33. package/dist/src/render/blocks/hero.js +27 -20
  34. package/dist/src/render/blocks/hero.js.map +1 -1
  35. package/dist/src/render/blocks/logo-strip.d.ts +6 -0
  36. package/dist/src/render/blocks/logo-strip.d.ts.map +1 -1
  37. package/dist/src/render/blocks/logo-strip.js +7 -9
  38. package/dist/src/render/blocks/logo-strip.js.map +1 -1
  39. package/dist/src/render/blocks/logos.d.ts +8 -0
  40. package/dist/src/render/blocks/logos.d.ts.map +1 -1
  41. package/dist/src/render/blocks/logos.js +18 -18
  42. package/dist/src/render/blocks/logos.js.map +1 -1
  43. package/dist/src/render/blocks/media-figure.d.ts +14 -3
  44. package/dist/src/render/blocks/media-figure.d.ts.map +1 -1
  45. package/dist/src/render/blocks/media-figure.js +34 -16
  46. package/dist/src/render/blocks/media-figure.js.map +1 -1
  47. package/dist/src/render/blocks/pricing-table.d.ts.map +1 -1
  48. package/dist/src/render/blocks/pricing-table.js +17 -22
  49. package/dist/src/render/blocks/pricing-table.js.map +1 -1
  50. package/dist/src/render/blocks/prose.d.ts +11 -6
  51. package/dist/src/render/blocks/prose.d.ts.map +1 -1
  52. package/dist/src/render/blocks/prose.js +13 -7
  53. package/dist/src/render/blocks/prose.js.map +1 -1
  54. package/dist/src/render/blocks/quote.d.ts +6 -2
  55. package/dist/src/render/blocks/quote.d.ts.map +1 -1
  56. package/dist/src/render/blocks/quote.js +14 -12
  57. package/dist/src/render/blocks/quote.js.map +1 -1
  58. package/dist/src/render/blocks/stat-counter.d.ts +6 -0
  59. package/dist/src/render/blocks/stat-counter.d.ts.map +1 -1
  60. package/dist/src/render/blocks/stat-counter.js +7 -10
  61. package/dist/src/render/blocks/stat-counter.js.map +1 -1
  62. package/dist/src/render/blocks/stats.d.ts +7 -0
  63. package/dist/src/render/blocks/stats.d.ts.map +1 -1
  64. package/dist/src/render/blocks/stats.js +8 -16
  65. package/dist/src/render/blocks/stats.js.map +1 -1
  66. package/dist/src/render/blocks/testimonial.d.ts +5 -11
  67. package/dist/src/render/blocks/testimonial.d.ts.map +1 -1
  68. package/dist/src/render/blocks/testimonial.js +9 -17
  69. package/dist/src/render/blocks/testimonial.js.map +1 -1
  70. package/dist/src/render/chrome.d.ts.map +1 -1
  71. package/dist/src/render/chrome.js +101 -84
  72. package/dist/src/render/chrome.js.map +1 -1
  73. package/dist/src/render/dish.d.ts +13 -0
  74. package/dist/src/render/dish.d.ts.map +1 -0
  75. package/dist/src/render/dish.js +84 -0
  76. package/dist/src/render/dish.js.map +1 -0
  77. package/dist/src/render/layout.d.ts +23 -0
  78. package/dist/src/render/layout.d.ts.map +1 -0
  79. package/dist/src/render/layout.js +33 -0
  80. package/dist/src/render/layout.js.map +1 -0
  81. package/dist/src/render/menu.d.ts +53 -0
  82. package/dist/src/render/menu.d.ts.map +1 -0
  83. package/dist/src/render/menu.js +99 -0
  84. package/dist/src/render/menu.js.map +1 -0
  85. package/dist/src/render/render-block.d.ts +19 -0
  86. package/dist/src/render/render-block.d.ts.map +1 -1
  87. package/dist/src/render/render-block.js +50 -39
  88. package/dist/src/render/render-block.js.map +1 -1
  89. package/dist/src/render/strings.d.ts +15 -0
  90. package/dist/src/render/strings.d.ts.map +1 -0
  91. package/dist/src/render/strings.js +23 -0
  92. package/dist/src/render/strings.js.map +1 -0
  93. package/dist/src/render/term-archive.d.ts +10 -4
  94. package/dist/src/render/term-archive.d.ts.map +1 -1
  95. package/dist/src/render/term-archive.js +25 -16
  96. package/dist/src/render/term-archive.js.map +1 -1
  97. package/dist/src/render/widget-areas.d.ts +13 -0
  98. package/dist/src/render/widget-areas.d.ts.map +1 -0
  99. package/dist/src/render/widget-areas.js +20 -0
  100. package/dist/src/render/widget-areas.js.map +1 -0
  101. package/dist/theme.config.d.ts +1 -1
  102. package/dist/theme.config.js +2 -2
  103. package/dist/theme.config.js.map +1 -1
  104. package/package.json +5 -5
  105. package/src/index.ts +3 -0
  106. package/src/render/blocks/accordion.ts +18 -47
  107. package/src/render/blocks/collection-list.ts +245 -133
  108. package/src/render/blocks/cta.ts +20 -11
  109. package/src/render/blocks/embed.ts +45 -36
  110. package/src/render/blocks/faq.ts +32 -25
  111. package/src/render/blocks/feature-grid.ts +83 -33
  112. package/src/render/blocks/gallery.ts +39 -21
  113. package/src/render/blocks/hero.ts +41 -34
  114. package/src/render/blocks/logo-strip.ts +27 -25
  115. package/src/render/blocks/logos.ts +34 -43
  116. package/src/render/blocks/media-figure.ts +49 -24
  117. package/src/render/blocks/pricing-table.ts +30 -42
  118. package/src/render/blocks/prose.ts +18 -9
  119. package/src/render/blocks/quote.ts +29 -19
  120. package/src/render/blocks/stat-counter.ts +24 -29
  121. package/src/render/blocks/stats.ts +30 -40
  122. package/src/render/blocks/testimonial.ts +17 -22
  123. package/src/render/chrome.ts +113 -95
  124. package/src/render/dish.ts +195 -0
  125. package/src/render/layout.ts +64 -0
  126. package/src/render/menu.ts +117 -0
  127. package/src/render/render-block.ts +51 -40
  128. package/src/render/strings.ts +44 -0
  129. package/src/render/term-archive.ts +78 -44
  130. package/src/render/widget-areas.ts +23 -0
  131. package/src/styles/archive.css +46 -56
  132. package/src/styles/base.css +807 -554
  133. package/src/styles/blocks.css +723 -641
  134. package/src/styles/menu.css +482 -0
  135. package/src/styles/theme.css +24 -25
  136. package/src/styles/tokens.css +239 -225
  137. package/src/styles/widgets.css +853 -0
  138. package/tokens.json +16 -16
@@ -1,53 +1,60 @@
1
1
  import type { FaqBlock, FaqItem } from '@cogenta/blocks'
2
2
  import {
3
- blockHeadingTag,
4
3
  type HtmlElement,
5
4
  h,
6
- heading,
5
+ nestedHeadingTag,
7
6
  type RenderContext,
8
7
  renderRichText,
9
8
  } from '@cogenta/theme-kit'
9
+ import { section, sectionHead } from '../layout.js'
10
10
 
11
11
  /**
12
- * `<details>`/`<summary>` — zero bytes of JavaScript. Kept visually distinct
13
- * from `accordion` (this theme's own "Hours & location"): each question
14
- * carries a small serif numeral rather than the plus/minus mark, the
15
- * "questions before you book" register of a restaurant's own FAQ.
12
+ * Questions before a visit (a table for eight, a child's plate, a dog in the
13
+ * room), in two columns on a wide screen: the title on the first four, and
14
+ * the questions on the last eight, each under a hairline.
15
+ *
16
+ * Every answer is a `<details>`: it opens without a script and is found by
17
+ * the browser's own search. The question sits in a real heading inside the
18
+ * summary, so a screen reader's heading list still reaches every one of
19
+ * them. The plus that turns into a minus is two hairlines drawn by the
20
+ * stylesheet, not a glyph.
16
21
  */
17
- function renderItem(item: FaqItem, ctx: RenderContext, index: number): HtmlElement {
18
- const number = String(index + 1).padStart(2, '0')
22
+ export function answerItem(
23
+ item: FaqItem,
24
+ ctx: RenderContext,
25
+ blockName: 'faq' | 'accordion',
26
+ titled: boolean,
27
+ ): HtmlElement {
19
28
  return h(
20
29
  'li',
21
- { class: 'cg-faq__item' },
30
+ { class: 'cr-answers__item' },
22
31
  h(
23
32
  'details',
24
- { class: 'cg-faq__details' },
33
+ { class: 'cr-answers__details' },
25
34
  h(
26
35
  'summary',
27
- { class: 'cg-faq__question' },
28
- h('span', { class: 'cg-faq__index', 'aria-hidden': 'true' }, number),
29
- h('span', { class: 'cg-faq__question-text' }, item.question),
36
+ { class: 'cr-answers__summary' },
37
+ h(nestedHeadingTag(blockName, titled), { class: 'cr-answers__question' }, item.question),
38
+ h('span', { class: 'cr-answers__mark', 'aria-hidden': 'true' }),
30
39
  ),
31
- h('div', { class: 'cg-faq__answer' }, renderRichText(ctx, item.answer)),
40
+ h('div', { class: 'cr-answers__answer' }, renderRichText(ctx, item.answer)),
32
41
  ),
33
42
  )
34
43
  }
35
44
 
36
45
  export function renderFaq(block: FaqBlock, ctx: RenderContext): HtmlElement {
37
- return h(
46
+ const titled = block.title !== undefined
47
+ return section(
38
48
  'section',
39
- { class: 'cg-faq', 'data-block': 'faq' },
40
- block.title === undefined
41
- ? null
42
- : heading(
43
- blockHeadingTag('faq') ?? 'h2',
44
- { class: 'cg-faq__title', 'data-field': 'title' },
45
- block.title,
46
- ),
49
+ 'faq',
50
+ 'cr-answers',
51
+ { 'data-titled': String(titled) },
52
+ 'div',
53
+ sectionHead('faq', block.title),
47
54
  h(
48
55
  'ul',
49
- { class: 'cg-faq__items' },
50
- block.items.map((item, index) => renderItem(item, ctx, index)),
56
+ { class: 'cr-answers__items' },
57
+ block.items.map((item) => answerItem(item, ctx, 'faq', titled)),
51
58
  ),
52
59
  )
53
60
  }
@@ -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,52 +8,104 @@ import {
10
8
  type RenderContext,
11
9
  renderIcon,
12
10
  } from '@cogenta/theme-kit'
11
+ import { section, sectionHead } from '../layout.js'
13
12
 
14
13
  /**
15
- * A row of three centred columns, each topped by a thin circular icon mark —
16
- * the "how we cook" / "what makes this kitchen" convention, never a boxed
17
- * feature card.
14
+ * Two readings of the same block, chosen by its content rather than by a
15
+ * setting:
16
+ *
17
+ * - **A ruled table**, when no item names an icon. The items are facts a
18
+ * guest looks up (lunch, dinner, the address, the metro stop; a supplier
19
+ * and what they bring), so they are set like the back of a menu card: the
20
+ * title of the table on the first four columns, then one row per item under
21
+ * a hairline, its name in the serif on the left and its sentence on the
22
+ * right. A name with a link is an arrow link.
23
+ * - **Ruled columns**, when an item names an icon this theme can draw: a
24
+ * small line icon, the name, its sentence, each column under a hairline.
25
+ * Never a tinted tile behind an icon, never a card.
18
26
  */
19
- function renderItem(item: FeatureItem, ctx: RenderContext, tag: HeadingTag): HtmlElement {
20
- const title =
21
- item.link === undefined
22
- ? heading(tag, { class: 'cg-craft__title' }, item.title)
23
- : heading(
24
- tag,
25
- { class: 'cg-craft__title' },
26
- h('a', { class: 'cg-craft__link', href: href(ctx, item.link) }, item.title),
27
- )
27
+ function hasIcons(block: FeatureGridBlock): boolean {
28
+ return block.items.some((item) => item.icon !== undefined)
29
+ }
30
+
31
+ function name(item: FeatureItem, ctx: RenderContext, className: string): HtmlElement | string {
32
+ return item.link === undefined
33
+ ? item.title
34
+ : h('a', { class: `cr-arrow-link ${className}`, href: href(ctx, item.link) }, item.title)
35
+ }
36
+
37
+ /**
38
+ * In a table the link belongs to the value a guest acts on (the telephone
39
+ * number, the address), not to its label: "Telephone" is a heading, the
40
+ * number is what they tap. A row with no sentence links its label instead.
41
+ */
42
+ function row(item: FeatureItem, ctx: RenderContext, titled: boolean): HtmlElement {
43
+ const label = heading(
44
+ nestedHeadingTag('featureGrid', titled),
45
+ { class: 'cr-table__label' },
46
+ item.text === undefined ? name(item, ctx, 'cr-table__link') : item.title,
47
+ )
48
+ if (item.text === undefined) return h('li', { class: 'cr-table__row' }, label)
49
+ return h(
50
+ 'li',
51
+ { class: 'cr-table__row' },
52
+ label,
53
+ h(
54
+ 'p',
55
+ { class: 'cr-table__text' },
56
+ item.link === undefined
57
+ ? item.text
58
+ : h('a', { class: 'cr-table__value-link', href: href(ctx, item.link) }, item.text),
59
+ ),
60
+ )
61
+ }
62
+
63
+ function column(item: FeatureItem, ctx: RenderContext, titled: boolean): HtmlElement {
28
64
  const icon =
29
65
  item.icon === undefined
30
66
  ? null
31
- : (renderIcon(item.icon, { className: 'cg-craft__glyph' }) ??
32
- h('span', { class: 'cg-craft__mark-fallback', 'data-icon': item.icon }))
67
+ : renderIcon(item.icon, { className: 'cr-columns__icon', size: 22 })
33
68
  return h(
34
69
  'li',
35
- { class: 'cg-craft__item' },
36
- icon === null ? null : h('span', { class: 'cg-craft__mark', 'aria-hidden': 'true' }, icon),
37
- title,
38
- item.text === undefined ? null : h('p', { class: 'cg-craft__text' }, item.text),
70
+ { class: 'cr-columns__item' },
71
+ icon,
72
+ heading(
73
+ nestedHeadingTag('featureGrid', titled),
74
+ { class: 'cr-columns__name' },
75
+ name(item, ctx, 'cr-columns__link'),
76
+ ),
77
+ item.text === undefined ? null : h('p', { class: 'cr-columns__text' }, item.text),
39
78
  )
40
79
  }
41
80
 
42
81
  export function renderFeatureGrid(block: FeatureGridBlock, ctx: RenderContext): HtmlElement {
43
- const hasTitle = block.title !== undefined
44
- const itemTag = nestedHeadingTag('featureGrid', hasTitle)
45
- return h(
82
+ const titled = block.title !== undefined
83
+ if (!hasIcons(block)) {
84
+ return section(
85
+ 'section',
86
+ 'featureGrid',
87
+ 'cr-table',
88
+ { 'data-titled': String(titled) },
89
+ 'div',
90
+ sectionHead('featureGrid', block.title),
91
+ h(
92
+ 'ul',
93
+ { class: 'cr-table__rows' },
94
+ block.items.map((item) => row(item, ctx, titled)),
95
+ ),
96
+ )
97
+ }
98
+ return section(
46
99
  'section',
47
- { class: 'cg-craft', 'data-block': 'featureGrid' },
48
- hasTitle
49
- ? heading(
50
- blockHeadingTag('featureGrid') ?? 'h2',
51
- { class: 'cg-craft__title-heading', 'data-field': 'title' },
52
- block.title ?? '',
53
- )
54
- : null,
100
+ 'featureGrid',
101
+ 'cr-columns',
102
+ { 'data-titled': String(titled), 'data-count': String(Math.min(block.items.length, 4)) },
103
+ 'div',
104
+ sectionHead('featureGrid', block.title),
55
105
  h(
56
106
  'ul',
57
- { class: 'cg-craft__items' },
58
- block.items.map((item) => renderItem(item, ctx, itemTag)),
107
+ { class: 'cr-columns__items' },
108
+ block.items.map((item) => column(item, ctx, titled)),
59
109
  ),
60
110
  )
61
111
  }
@@ -1,40 +1,58 @@
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
- * Ships **no JavaScript**. `layout: 'masonry'` (the dining-room gallery this
6
- * theme's own blueprint seeds) is a CSS multi-column flow — `column-count`
7
- * with `break-inside: avoid` on each tile — which needs no measurement pass
8
- * a script would otherwise have to do; `grid`/`carousel` keep the same
9
- * zero-JS scroll-snapping list every other layout in this theme uses.
6
+ * Photographs of plates and of the room, in the three layouts contract B
7
+ * names:
8
+ *
9
+ * - `grid`: a band of dish photographs, every one cropped to the same 4:5
10
+ * portrait, four across on a wide screen (three when the count divides by
11
+ * three, so a row is never left with one picture), two on a phone.
12
+ * - `masonry`: three columns of pictures at their own proportions.
13
+ * - `carousel`: a row of 4:5 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.
10
16
  */
17
+ function columnsFor(count: number): number {
18
+ if (count <= 2) return count
19
+ return count % 3 === 0 ? 3 : 4
20
+ }
21
+
11
22
  export function renderGallery(block: GalleryBlock, ctx: RenderContext): HtmlElement {
12
- const items = h(
13
- 'ul',
14
- { class: 'cg-gallery__items' },
15
- block.items.map((item) =>
16
- h(
17
- 'li',
18
- { class: 'cg-gallery__item' },
19
- image(ctx, item.media, { sizes: '(min-width: 48rem) 22rem, 60vw' }),
20
- ),
23
+ const carousel = block.layout === 'carousel'
24
+ const columns = columnsFor(block.items.length)
25
+ const items = block.items.map((item) =>
26
+ h(
27
+ 'li',
28
+ { class: 'cr-gallery__item' },
29
+ image(ctx, item.media, {
30
+ className: 'cr-gallery__image',
31
+ sizes:
32
+ block.layout === 'grid'
33
+ ? `(min-width: 64rem) ${Math.round(90 / Math.max(columns, 1))}vw, 50vw`
34
+ : '(min-width: 64rem) 30vw, 80vw',
35
+ }),
21
36
  ),
22
37
  )
23
38
 
24
- return h(
25
- 'section',
26
- { class: 'cg-gallery', 'data-block': 'gallery', 'data-layout': block.layout },
27
- block.layout === 'carousel'
39
+ return section(
40
+ 'div',
41
+ 'gallery',
42
+ 'cr-gallery',
43
+ { 'data-layout': block.layout, 'data-columns': String(columns) },
44
+ 'div',
45
+ carousel
28
46
  ? h(
29
47
  'div',
30
48
  {
31
- class: 'cg-gallery__viewport',
49
+ class: 'cr-gallery__viewport',
32
50
  role: 'region',
33
51
  'aria-label': ctx.t('gallery.carousel'),
34
52
  tabindex: '0',
35
53
  },
36
- items,
54
+ h('ul', { class: 'cr-gallery__items' }, items),
37
55
  )
38
- : items,
56
+ : h('ul', { class: 'cr-gallery__items' }, items),
39
57
  )
40
58
  }
@@ -1,58 +1,65 @@
1
1
  import type { HeroBlock } from '@cogenta/blocks'
2
- import {
3
- actionList,
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 full-bleed dining-room hero: the media (when present) fills the whole
14
- * band, a gradient scrim sits between it and the copy so the centred text
15
- * stays legible over a photo of any tone, and the eyebrow/title/subtitle are
16
- * centred — the "elegant restaurant homepage" convention (Divi/Astra
17
- * "Restaurant") rather than the left-aligned marketing hero the other
18
- * built-in themes use.
6
+ * The opening of a page: the photograph of the room across the whole width
7
+ * of the window, then the name of the place set large and light in the
8
+ * display serif, on the page's own ground.
19
9
  *
20
- * The scrim is a CSS gradient composed from `--cg-scrim` (itself derived
21
- * from the skin's own ink, see `tokens.css`), never a literal colour: a
22
- * skin with a lighter ink still gets a scrim tuned to it.
10
+ * The words sit under the picture, never on it. A headline laid over a
11
+ * photograph needs a veil to stay legible on every picture an editor may
12
+ * choose, and a veil over a dining room is exactly what makes it look like
13
+ * every other restaurant template; set on the paper, the name is legible in
14
+ * both schemes whatever the photograph, and the room stays as it was lit.
15
+ *
16
+ * Name on the first eight columns, its baseline shared with the subtitle and
17
+ * the action on the last four. An eyebrow, when there is one, is a line of
18
+ * small capitals above the name, never a badge. Without media the block is
19
+ * the name row alone.
20
+ *
21
+ * The image is the only one on a page loaded eagerly: it is above the fold
22
+ * by construction.
23
23
  */
24
24
  export function renderHero(block: HeroBlock, ctx: RenderContext): HtmlElement {
25
- const tag = blockHeadingTag('hero') ?? 'h1'
25
+ const actions = actionList(ctx, block.actions, ctx.t('hero.actions'))
26
+ const hasAside = block.subtitle !== undefined || actions !== null
27
+
26
28
  return h(
27
29
  'section',
28
30
  {
29
- class: 'cg-hero',
31
+ class: 'cr-section cr-hero',
30
32
  'data-block': 'hero',
31
- 'data-has-media': block.media === undefined ? 'false' : 'true',
33
+ 'data-media': block.media === undefined ? 'false' : 'true',
32
34
  },
33
35
  block.media === undefined
34
36
  ? null
35
37
  : h(
36
- 'div',
37
- { class: 'cg-hero__media' },
38
+ 'figure',
39
+ { class: 'cr-hero__media' },
38
40
  image(ctx, block.media, {
39
- className: 'cg-hero__image',
41
+ className: 'cr-hero__image',
40
42
  loading: 'eager',
41
43
  sizes: '100vw',
42
44
  }),
43
- h('div', { class: 'cg-hero__scrim', 'aria-hidden': 'true' }),
44
45
  ),
45
46
  h(
46
47
  'div',
47
- { class: 'cg-hero__body' },
48
- block.eyebrow === undefined
49
- ? null
50
- : h('p', { class: 'cg-hero__eyebrow', 'data-field': 'eyebrow' }, block.eyebrow),
51
- heading(tag, { class: 'cg-hero__title', 'data-field': 'title' }, block.title),
52
- block.subtitle === undefined
53
- ? null
54
- : h('p', { class: 'cg-hero__subtitle', 'data-field': 'subtitle' }, block.subtitle),
55
- actionList(ctx, block.actions, ctx.t('hero.actions')),
48
+ { class: 'cr-container cr-hero__inner' },
49
+ h(
50
+ 'div',
51
+ { class: 'cr-hero__heading' },
52
+ optionalText('p', 'cr-hero__eyebrow', block.eyebrow, { 'data-field': 'eyebrow' }),
53
+ h('h1', { class: 'cr-hero__title', 'data-field': 'title' }, block.title),
54
+ ),
55
+ hasAside
56
+ ? h(
57
+ 'div',
58
+ { class: 'cr-hero__aside' },
59
+ optionalText('p', 'cr-hero__subtitle', block.subtitle, { 'data-field': 'subtitle' }),
60
+ actions,
61
+ )
62
+ : null,
56
63
  ),
57
64
  )
58
65
  }
@@ -1,35 +1,37 @@
1
- import type { LogoStripBlock, LogoStripItem } from '@cogenta/blocks'
1
+ import type { LogoStripBlock } 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-weight "as seen in" row next to
6
- * `logos`: no per-logo name or link, so no accessible-name field to
7
- * require — each image's own alt text (set once, in the media library)
8
- * names it, exactly as `logos` falls back to when a link is absent.
6
+ * The lighter row of marks: the caption at the caption size on the first
7
+ * three columns, the marks spread along one line across the rest, small and
8
+ * evenly spaced. No links, by contract B; each mark is named by its own alt
9
+ * text from the media library.
9
10
  */
10
- function renderItem(item: LogoStripItem, ctx: RenderContext): HtmlElement {
11
- return h(
12
- 'li',
13
- { class: 'cg-press-strip__item' },
14
- image(ctx, item.media, { className: 'cg-press-strip__image', variant: { fit: 'contain' } }),
15
- )
16
- }
17
-
18
11
  export function renderLogoStrip(block: LogoStripBlock, ctx: RenderContext): HtmlElement {
19
- return h(
20
- 'figure',
21
- { class: 'cg-press-strip', 'data-block': 'logoStrip' },
22
- h(
23
- 'ul',
24
- { class: 'cg-press-strip__items' },
25
- block.logos.map((item) => renderItem(item, ctx)),
26
- ),
12
+ return section(
13
+ 'div',
14
+ 'logoStrip',
15
+ 'cr-strip',
16
+ { 'data-captioned': String(block.caption !== undefined) },
17
+ 'div',
27
18
  block.caption === undefined
28
19
  ? null
29
- : h(
30
- 'figcaption',
31
- { class: 'cg-press-strip__caption', 'data-field': 'caption' },
32
- block.caption,
20
+ : h('p', { class: 'cr-strip__caption', 'data-field': 'caption' }, block.caption),
21
+ h(
22
+ 'ul',
23
+ { class: 'cr-strip__items' },
24
+ block.logos.map((logo) =>
25
+ h(
26
+ 'li',
27
+ { class: 'cr-strip__item' },
28
+ h(
29
+ 'span',
30
+ { class: 'cr-marks__plate' },
31
+ image(ctx, logo.media, { className: 'cr-marks__image', sizes: '10rem' }),
32
+ ),
33
33
  ),
34
+ ),
35
+ ),
34
36
  )
35
37
  }
@@ -1,53 +1,44 @@
1
- import type { LogoItem, LogosBlock } from '@cogenta/blocks'
2
- import {
3
- blockHeadingTag,
4
- type HtmlElement,
5
- h,
6
- heading,
7
- image,
8
- type RenderContext,
9
- } from '@cogenta/theme-kit'
1
+ import type { LogosBlock } from '@cogenta/blocks'
2
+ import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+ import { section, sectionHead } from '../layout.js'
10
4
 
11
5
  /**
12
- * "Featured in" / press mentions, held back to a quiet grayscale until
13
- * hovered — contract B says the organisation's name **is** the accessible
14
- * name of the link, so the logo image carries it as `alt` text when the
15
- * media entity has none, and no visually hidden duplicate is added.
6
+ * Guides, publications, the growers' cooperative: their marks along one
7
+ * ruled row, each set small and in its own colours. A mark drawn for paper
8
+ * sits on a pale plate in the dark scheme so it never disappears.
9
+ *
10
+ * The organisation's name is the mark's accessible name when the media
11
+ * library has no alt text for it, and the link's own name when it links out.
16
12
  */
17
- function renderItem(item: LogoItem, ctx: RenderContext): HtmlElement {
18
- const logo = image(ctx, item.media, {
19
- className: 'cg-press__logo',
20
- altFrom: item.name,
21
- variant: { fit: 'contain' },
22
- })
23
- return h(
24
- 'li',
25
- { class: 'cg-press__item' },
26
- item.url === undefined
27
- ? logo
28
- : h(
29
- 'a',
30
- { class: 'cg-press__link', href: ctx.link(item.url), rel: 'noopener noreferrer' },
31
- logo,
32
- ),
33
- )
34
- }
35
-
36
13
  export function renderLogos(block: LogosBlock, ctx: RenderContext): HtmlElement {
37
- return h(
14
+ return section(
38
15
  'section',
39
- { class: 'cg-press', 'data-block': 'logos' },
40
- block.title === undefined
41
- ? null
42
- : heading(
43
- blockHeadingTag('logos') ?? 'h2',
44
- { class: 'cg-press__title', 'data-field': 'title' },
45
- block.title,
46
- ),
16
+ 'logos',
17
+ 'cr-marks',
18
+ {},
19
+ 'div',
20
+ sectionHead('logos', block.title),
47
21
  h(
48
22
  'ul',
49
- { class: 'cg-press__items' },
50
- block.items.map((item) => renderItem(item, ctx)),
23
+ { class: 'cr-marks__items' },
24
+ block.items.map((item) => {
25
+ const mark = image(ctx, item.media, {
26
+ className: 'cr-marks__image',
27
+ altFrom: item.name,
28
+ sizes: '10rem',
29
+ })
30
+ return h(
31
+ 'li',
32
+ { class: 'cr-marks__item' },
33
+ item.url === undefined
34
+ ? h('span', { class: 'cr-marks__plate' }, mark)
35
+ : h(
36
+ 'a',
37
+ { class: 'cr-marks__plate', href: item.url, rel: 'noopener noreferrer' },
38
+ mark,
39
+ ),
40
+ )
41
+ }),
51
42
  ),
52
43
  )
53
44
  }
@@ -1,39 +1,64 @@
1
1
  import type { MediaFigureBlock } from '@cogenta/blocks'
2
2
  import { aspectRatio, type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
3
+ import { section } from '../layout.js'
3
4
 
4
5
  /**
5
- * A thin hairline frame around the image, a small italic caption beneath —
6
- * the restrained, "printed menu insert" treatment consistent with this
7
- * theme's gallery and embed placeholder.
6
+ * A photograph and its caption.
7
+ *
8
+ * `align` is an editorial intent, read as a layout:
9
+ *
10
+ * - `start` / `end`: the room-and-story split. The photograph takes seven
11
+ * columns on its side and the caption, set in the serif italic at the lead
12
+ * size, takes the other four, so a caption reads as a line from the house
13
+ * beside the picture.
14
+ * - `center`: eight columns, set in from the left edge.
15
+ * - `wide` (and no value): the whole container.
16
+ * - `full`: the whole window, edge to edge.
17
+ *
18
+ * The ratio crops from the middle, or from the media's own focal point; the
19
+ * corners stay square and the picture casts no shadow.
8
20
  */
9
21
  export function renderMediaFigure(block: MediaFigureBlock, ctx: RenderContext): HtmlElement {
22
+ const align = block.align ?? 'wide'
23
+ const split = align === 'start' || align === 'end'
10
24
  const ratio = aspectRatio(block.ratio)
11
- const hasCaption = block.caption !== undefined || block.credit !== undefined
12
- return h(
25
+
26
+ const caption =
27
+ block.caption === undefined && block.credit === undefined
28
+ ? null
29
+ : h(
30
+ 'figcaption',
31
+ { class: 'cr-figure__caption' },
32
+ block.caption === undefined
33
+ ? null
34
+ : h('span', { class: 'cr-figure__text', 'data-field': 'caption' }, block.caption),
35
+ block.credit === undefined
36
+ ? null
37
+ : h('span', { class: 'cr-figure__credit', 'data-field': 'credit' }, block.credit),
38
+ )
39
+
40
+ return section(
41
+ 'div',
42
+ 'mediaFigure',
43
+ 'cr-figure',
44
+ { 'data-align': align, 'data-layout': split ? 'split' : 'single' },
13
45
  'figure',
14
- {
15
- class: 'cg-plate',
16
- 'data-block': 'mediaFigure',
17
- 'data-align': block.align ?? 'center',
18
- style: ratio === undefined ? undefined : `--cg-ratio:${ratio}`,
19
- },
20
46
  h(
21
47
  'div',
22
- { class: 'cg-plate__frame' },
48
+ {
49
+ class: 'cr-figure__frame',
50
+ style: ratio === undefined ? undefined : `aspect-ratio:${ratio}`,
51
+ },
23
52
  image(ctx, block.media, {
24
- className: 'cg-plate__media',
25
- sizes: '(min-width: 48rem) 42rem, 100vw',
53
+ className: 'cr-figure__image',
54
+ sizes:
55
+ align === 'full'
56
+ ? '100vw'
57
+ : split
58
+ ? '(min-width: 64rem) 55vw, 100vw'
59
+ : '(min-width: 90rem) 86rem, 100vw',
26
60
  }),
27
61
  ),
28
- hasCaption
29
- ? h(
30
- 'figcaption',
31
- { class: 'cg-plate__caption' },
32
- block.caption,
33
- block.credit === undefined
34
- ? null
35
- : h('span', { class: 'cg-plate__credit', 'data-field': 'credit' }, block.credit),
36
- )
37
- : null,
62
+ caption,
38
63
  )
39
64
  }