@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,49 +1,55 @@
|
|
|
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
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* scripted carousel: no auto-advance to fight (WCAG 2.2.2), no focus stolen
|
|
9
|
-
* from an off-screen slide, and it still works before hydration.
|
|
6
|
+
* The sequence of pictures a case study is made of. Every picture keeps its
|
|
7
|
+
* own shape: a poster stays a poster, a spread stays a spread. The layout the
|
|
8
|
+
* editor chose decides the rhythm:
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
* - `grid`: a sequence of three places on twelve columns, a large picture
|
|
11
|
+
* across seven columns beside a smaller one across five set to the same
|
|
12
|
+
* bottom line, then one across the whole container.
|
|
13
|
+
* - `masonry`: two columns (three on a wide screen) at each picture's own
|
|
14
|
+
* shape.
|
|
15
|
+
* - `carousel`: one row at a single height that scrolls and snaps, focusable
|
|
16
|
+
* and labelled; touch, trackpad and arrow keys are all native. No script.
|
|
17
17
|
*/
|
|
18
|
+
function sizesFor(layout: GalleryBlock['layout'], place: number): string {
|
|
19
|
+
if (layout === 'carousel') return '(min-width: 64rem) 40rem, 80vw'
|
|
20
|
+
if (layout === 'masonry') return '(min-width: 64rem) 30rem, 50vw'
|
|
21
|
+
if (place === 3) return '(min-width: 96rem) 92rem, 100vw'
|
|
22
|
+
return place === 1 ? '(min-width: 64rem) 54rem, 100vw' : '(min-width: 64rem) 38rem, 100vw'
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
export function renderGallery(block: GalleryBlock, ctx: RenderContext): HtmlElement {
|
|
19
26
|
const items = h(
|
|
20
27
|
'ul',
|
|
21
|
-
{ class: 'cg-
|
|
22
|
-
block.items.map((item) =>
|
|
23
|
-
|
|
28
|
+
{ class: 'cg-pictures__items', 'data-count': String(block.items.length) },
|
|
29
|
+
block.items.map((item, index) => {
|
|
30
|
+
const place = (index % 3) + 1
|
|
31
|
+
return h(
|
|
24
32
|
'li',
|
|
25
|
-
{ class: 'cg-
|
|
26
|
-
h('span', { class: 'cg-gallery__index', 'aria-hidden': 'true' }),
|
|
33
|
+
{ class: 'cg-pictures__item', 'data-place': String(place) },
|
|
27
34
|
image(ctx, item.media, {
|
|
28
|
-
className: 'cg-
|
|
29
|
-
sizes:
|
|
35
|
+
className: 'cg-pictures__image',
|
|
36
|
+
sizes: sizesFor(block.layout, place),
|
|
30
37
|
}),
|
|
31
|
-
)
|
|
32
|
-
),
|
|
38
|
+
)
|
|
39
|
+
}),
|
|
33
40
|
)
|
|
34
41
|
|
|
35
|
-
return
|
|
36
|
-
'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
},
|
|
42
|
+
return section(
|
|
43
|
+
'div',
|
|
44
|
+
'gallery',
|
|
45
|
+
'cg-pictures',
|
|
46
|
+
{ 'data-layout': block.layout },
|
|
47
|
+
'div',
|
|
42
48
|
block.layout === 'carousel'
|
|
43
49
|
? h(
|
|
44
50
|
'div',
|
|
45
51
|
{
|
|
46
|
-
class: 'cg-
|
|
52
|
+
class: 'cg-pictures__viewport',
|
|
47
53
|
role: 'region',
|
|
48
54
|
'aria-label': ctx.t('gallery.carousel'),
|
|
49
55
|
tabindex: '0',
|
|
@@ -8,47 +8,50 @@ import {
|
|
|
8
8
|
image,
|
|
9
9
|
type RenderContext,
|
|
10
10
|
} from '@cogenta/theme-kit'
|
|
11
|
+
import { section } from '../layout.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* The hero carries the page's `h1`
|
|
14
|
-
*
|
|
14
|
+
* The statement. `hero` carries the page's own `h1` (contract B), and on a
|
|
15
|
+
* studio's home page that heading is what the studio says it does, set very
|
|
16
|
+
* large in the display width across eleven columns. The eyebrow is a small
|
|
17
|
+
* line above it in the text face, never a badge. The subtitle drops to the
|
|
18
|
+
* right half of the grid, so the opening is asymmetric by construction; the
|
|
19
|
+
* actions follow it as underlined words.
|
|
15
20
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* headline that overflows its own column on wide screens (`blocks.css` gives
|
|
19
|
-
* it a fluid `clamp()`), and the media is offset into the title's own
|
|
20
|
-
* negative margin rather than sitting in a tidy second column — the overlap
|
|
21
|
-
* is what makes this a poster, not a slide.
|
|
21
|
+
* A photograph or a film, when there is one, runs under the words across the
|
|
22
|
+
* whole container at 16:9: the one image above the fold, loaded eagerly.
|
|
22
23
|
*/
|
|
23
24
|
export function renderHero(block: HeroBlock, ctx: RenderContext): HtmlElement {
|
|
24
25
|
const tag = blockHeadingTag('hero') ?? 'h1'
|
|
25
|
-
return
|
|
26
|
+
return section(
|
|
26
27
|
'section',
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
'hero',
|
|
29
|
+
'cg-statement',
|
|
30
|
+
{ 'data-media': block.media === undefined ? 'none' : 'image' },
|
|
31
|
+
'div',
|
|
32
|
+
block.eyebrow === undefined
|
|
33
|
+
? null
|
|
34
|
+
: h('p', { class: 'cg-statement__eyebrow', 'data-field': 'eyebrow' }, block.eyebrow),
|
|
35
|
+
heading(tag, { class: 'cg-statement__title', 'data-field': 'title' }, block.title),
|
|
36
|
+
block.subtitle === undefined && (block.actions === undefined || block.actions.length === 0)
|
|
37
|
+
? null
|
|
38
|
+
: h(
|
|
39
|
+
'div',
|
|
40
|
+
{ class: 'cg-statement__aside' },
|
|
41
|
+
block.subtitle === undefined
|
|
42
|
+
? null
|
|
43
|
+
: h('p', { class: 'cg-statement__subtitle', 'data-field': 'subtitle' }, block.subtitle),
|
|
44
|
+
actionList(ctx, block.actions, ctx.t('hero.actions')),
|
|
45
|
+
),
|
|
40
46
|
block.media === undefined
|
|
41
47
|
? null
|
|
42
48
|
: h(
|
|
43
49
|
'div',
|
|
44
|
-
{ class: 'cg-
|
|
45
|
-
// The only image on the page that is above the fold by construction,
|
|
46
|
-
// so the only one that must not be lazy: lazy-loading the LCP element
|
|
47
|
-
// is a measured Lighthouse regression, not a theoretical one.
|
|
50
|
+
{ class: 'cg-statement__media' },
|
|
48
51
|
image(ctx, block.media, {
|
|
49
|
-
className: 'cg-
|
|
52
|
+
className: 'cg-statement__image',
|
|
50
53
|
loading: 'eager',
|
|
51
|
-
sizes: '(min-width:
|
|
54
|
+
sizes: '(min-width: 96rem) 92rem, 100vw',
|
|
52
55
|
}),
|
|
53
56
|
),
|
|
54
57
|
)
|
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
import type { LogoStripBlock, LogoStripItem } from '@cogenta/blocks'
|
|
2
2
|
import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
|
|
3
|
+
import { section } from '../layout.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* The image's own alt text (set once, in the media library) is what names
|
|
11
|
-
* each mark, same as `logos.ts` falls back to when a link is absent — never
|
|
12
|
-
* invented here, which is why `image` is called with no `altFrom`.
|
|
6
|
+
* A line of marks between two hairlines: the caption in the first three
|
|
7
|
+
* columns, the marks along the rest of the row at one height. `logoStrip`
|
|
8
|
+
* carries no name per mark, so each image's own alt text (set once, in the
|
|
9
|
+
* media library) names it. In the dark scheme the row sits on a light plate
|
|
10
|
+
* for the same reason `logos` does.
|
|
13
11
|
*/
|
|
14
12
|
function renderItem(item: LogoStripItem, ctx: RenderContext): HtmlElement {
|
|
15
13
|
return h(
|
|
16
14
|
'li',
|
|
17
|
-
{ class: 'cg-
|
|
18
|
-
image(ctx, item.media, {
|
|
19
|
-
className: 'cg-logostrip__image',
|
|
20
|
-
variant: { fit: 'contain' },
|
|
21
|
-
}),
|
|
15
|
+
{ class: 'cg-credits__item' },
|
|
16
|
+
image(ctx, item.media, { className: 'cg-credits__image', variant: { fit: 'contain' } }),
|
|
22
17
|
)
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
export function renderLogoStrip(block: LogoStripBlock, ctx: RenderContext): HtmlElement {
|
|
26
|
-
return
|
|
27
|
-
'
|
|
28
|
-
|
|
21
|
+
return section(
|
|
22
|
+
'div',
|
|
23
|
+
'logoStrip',
|
|
24
|
+
'cg-credits',
|
|
25
|
+
{ 'data-captioned': block.caption === undefined ? 'false' : 'true' },
|
|
26
|
+
'div',
|
|
27
|
+
block.caption === undefined
|
|
28
|
+
? null
|
|
29
|
+
: h('p', { class: 'cg-credits__caption', 'data-field': 'caption' }, block.caption),
|
|
29
30
|
h(
|
|
30
31
|
'ul',
|
|
31
|
-
{ class: 'cg-
|
|
32
|
+
{ class: 'cg-credits__items' },
|
|
32
33
|
block.logos.map((item) => renderItem(item, ctx)),
|
|
33
34
|
),
|
|
34
|
-
block.caption === undefined
|
|
35
|
-
? null
|
|
36
|
-
: h('figcaption', { class: 'cg-logostrip__caption', 'data-field': 'caption' }, block.caption),
|
|
37
35
|
)
|
|
38
36
|
}
|
|
@@ -1,63 +1,42 @@
|
|
|
1
1
|
import type { LogoItem, LogosBlock } from '@cogenta/blocks'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
type HtmlElement,
|
|
5
|
-
h,
|
|
6
|
-
heading,
|
|
7
|
-
image,
|
|
8
|
-
type RenderContext,
|
|
9
|
-
} from '@cogenta/theme-kit'
|
|
2
|
+
import { type HtmlElement, h, image, type RenderContext } from '@cogenta/theme-kit'
|
|
3
|
+
import { section, sectionHead } from '../layout.js'
|
|
10
4
|
|
|
11
5
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* is
|
|
16
|
-
*
|
|
17
|
-
* Rendered as a single-row ledger rather than a grid of tiles — a running
|
|
18
|
-
* mono index beside each mark, in a desaturated register that resolves to
|
|
19
|
-
* full ink only on hover or focus (`blocks.css`), which is the theme's own
|
|
20
|
-
* take on the "selected clients" strip.
|
|
6
|
+
* Marks of clients or partners, each on its own plate in a ruled grid, the
|
|
7
|
+
* name under the plate in the caption size. The plate stays light in the dark
|
|
8
|
+
* scheme, so a dark wordmark drawn for white paper is never lost on black.
|
|
9
|
+
* The organisation's name is the image's accessible name when the media
|
|
10
|
+
* entity has none, and a mark with a URL links it.
|
|
21
11
|
*/
|
|
22
|
-
function renderItem(item: LogoItem, ctx: RenderContext
|
|
23
|
-
const
|
|
24
|
-
className: 'cg-
|
|
12
|
+
function renderItem(item: LogoItem, ctx: RenderContext): HtmlElement {
|
|
13
|
+
const mark = image(ctx, item.media, {
|
|
14
|
+
className: 'cg-marks__image',
|
|
25
15
|
altFrom: item.name,
|
|
26
16
|
variant: { fit: 'contain' },
|
|
27
17
|
})
|
|
28
18
|
return h(
|
|
29
19
|
'li',
|
|
30
|
-
{ class: 'cg-
|
|
31
|
-
h(
|
|
32
|
-
'span',
|
|
33
|
-
{ class: 'cg-logo__index', 'aria-hidden': 'true' },
|
|
34
|
-
String(index + 1).padStart(2, '0'),
|
|
35
|
-
),
|
|
20
|
+
{ class: 'cg-marks__item' },
|
|
36
21
|
item.url === undefined
|
|
37
|
-
?
|
|
38
|
-
: h(
|
|
39
|
-
|
|
40
|
-
{ class: 'cg-logo__link', href: ctx.link(item.url), rel: 'noopener noreferrer' },
|
|
41
|
-
logo,
|
|
42
|
-
),
|
|
22
|
+
? h('div', { class: 'cg-marks__plate' }, mark)
|
|
23
|
+
: h('a', { class: 'cg-marks__plate', href: item.url, rel: 'noopener noreferrer' }, mark),
|
|
24
|
+
h('p', { class: 'cg-marks__name' }, item.name),
|
|
43
25
|
)
|
|
44
26
|
}
|
|
45
27
|
|
|
46
28
|
export function renderLogos(block: LogosBlock, ctx: RenderContext): HtmlElement {
|
|
47
|
-
return
|
|
29
|
+
return section(
|
|
48
30
|
'section',
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{ class: 'cg-logos__title', 'data-field': 'title' },
|
|
55
|
-
block.title,
|
|
56
|
-
),
|
|
31
|
+
'logos',
|
|
32
|
+
'cg-marks',
|
|
33
|
+
{ 'data-count': String(block.items.length) },
|
|
34
|
+
'div',
|
|
35
|
+
sectionHead('logos', block.title),
|
|
57
36
|
h(
|
|
58
37
|
'ul',
|
|
59
|
-
{ class: 'cg-
|
|
60
|
-
block.items.map((item
|
|
38
|
+
{ class: 'cg-marks__items' },
|
|
39
|
+
block.items.map((item) => renderItem(item, ctx)),
|
|
61
40
|
),
|
|
62
41
|
)
|
|
63
42
|
}
|
|
@@ -1,38 +1,42 @@
|
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* An image placed on the grid by its alignment, square-cornered, with a
|
|
7
|
+
* small caption under it on the image's own left edge and the credit after
|
|
8
|
+
* the caption. Alignment is how a case study gets its rhythm of large and
|
|
9
|
+
* small pictures:
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `align` is written as a data attribute, never as a class: contract B's
|
|
16
|
-
* values are `start`/`end`, an intent that mirrors in right-to-left
|
|
17
|
-
* locales, and the skin decides what it means.
|
|
11
|
+
* - `center` (the default): columns 3 to 10.
|
|
12
|
+
* - `start`: the first seven columns. `end`: the last seven.
|
|
13
|
+
* - `wide`: the whole container.
|
|
14
|
+
* - `full`: edge to edge, the caption returning to the grid.
|
|
18
15
|
*/
|
|
19
16
|
export function renderMediaFigure(block: MediaFigureBlock, ctx: RenderContext): HtmlElement {
|
|
20
17
|
const ratio = aspectRatio(block.ratio)
|
|
18
|
+
const align = block.align ?? 'center'
|
|
21
19
|
const hasCaption = block.caption !== undefined || block.credit !== undefined
|
|
22
|
-
return
|
|
23
|
-
'
|
|
20
|
+
return section(
|
|
21
|
+
'div',
|
|
22
|
+
'mediaFigure',
|
|
23
|
+
'cg-figure',
|
|
24
24
|
{
|
|
25
|
-
|
|
26
|
-
'data-block': 'mediaFigure',
|
|
27
|
-
'data-align': block.align ?? 'center',
|
|
25
|
+
'data-align': align,
|
|
28
26
|
style: ratio === undefined ? undefined : `--cg-ratio:${ratio}`,
|
|
29
27
|
},
|
|
28
|
+
'figure',
|
|
30
29
|
h(
|
|
31
30
|
'div',
|
|
32
31
|
{ class: 'cg-figure__frame' },
|
|
33
32
|
image(ctx, block.media, {
|
|
34
|
-
className: 'cg-
|
|
35
|
-
sizes:
|
|
33
|
+
className: 'cg-figure__image',
|
|
34
|
+
sizes:
|
|
35
|
+
align === 'full'
|
|
36
|
+
? '100vw'
|
|
37
|
+
: align === 'wide'
|
|
38
|
+
? '(min-width: 96rem) 92rem, 100vw'
|
|
39
|
+
: '(min-width: 64rem) 56rem, 100vw',
|
|
36
40
|
}),
|
|
37
41
|
),
|
|
38
42
|
hasCaption
|
|
@@ -41,7 +45,11 @@ export function renderMediaFigure(block: MediaFigureBlock, ctx: RenderContext):
|
|
|
41
45
|
{ class: 'cg-figure__caption' },
|
|
42
46
|
block.caption === undefined
|
|
43
47
|
? null
|
|
44
|
-
: h(
|
|
48
|
+
: h(
|
|
49
|
+
'span',
|
|
50
|
+
{ class: 'cg-figure__caption-text', 'data-field': 'caption' },
|
|
51
|
+
block.caption,
|
|
52
|
+
),
|
|
45
53
|
block.credit === undefined
|
|
46
54
|
? null
|
|
47
55
|
: h('span', { class: 'cg-figure__credit', 'data-field': 'credit' }, block.credit),
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { PricingTableBlock, PricingTier } from '@cogenta/blocks'
|
|
2
2
|
import {
|
|
3
3
|
actionLink,
|
|
4
|
-
blockHeadingTag,
|
|
5
4
|
type HeadingTag,
|
|
6
5
|
type HtmlElement,
|
|
7
6
|
h,
|
|
@@ -9,60 +8,60 @@ import {
|
|
|
9
8
|
nestedHeadingTag,
|
|
10
9
|
type RenderContext,
|
|
11
10
|
} from '@cogenta/theme-kit'
|
|
11
|
+
import { section, sectionHead } from '../layout.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* Fees or engagements, set as the columns of a ruled table rather than as
|
|
15
|
+
* cards: each tier opens on a hairline, its name in the text face, the price
|
|
16
|
+
* in the display width with tabular numerals and the interval after it, what
|
|
17
|
+
* it includes as a list divided by hairlines, and its action at the foot. The
|
|
18
|
+
* tier the editor highlighted opens on a heavy rule in ink instead, and its
|
|
19
|
+
* action is the filled one; the others keep their words underlined.
|
|
19
20
|
*/
|
|
20
21
|
function renderTier(tier: PricingTier, ctx: RenderContext, tag: HeadingTag): HtmlElement {
|
|
22
|
+
const highlighted = tier.highlighted === true
|
|
21
23
|
return h(
|
|
22
24
|
'li',
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
'data-highlighted': tier.highlighted === true ? 'true' : undefined,
|
|
26
|
-
'aria-current': tier.highlighted === true ? 'true' : undefined,
|
|
27
|
-
},
|
|
28
|
-
heading(tag, { class: 'cg-pricing__name' }, tier.name),
|
|
25
|
+
{ class: 'cg-fees__tier', 'data-highlighted': highlighted ? 'true' : undefined },
|
|
26
|
+
heading(tag, { class: 'cg-fees__name' }, tier.name),
|
|
29
27
|
h(
|
|
30
28
|
'p',
|
|
31
|
-
{ class: 'cg-
|
|
32
|
-
tier.price,
|
|
33
|
-
tier.interval === undefined
|
|
34
|
-
? null
|
|
35
|
-
: h('span', { class: 'cg-pricing__interval' }, tier.interval),
|
|
29
|
+
{ class: 'cg-fees__price' },
|
|
30
|
+
h('span', { class: 'cg-fees__amount' }, tier.price),
|
|
31
|
+
tier.interval === undefined ? null : h('span', { class: 'cg-fees__interval' }, tier.interval),
|
|
36
32
|
),
|
|
37
33
|
tier.features.length === 0
|
|
38
34
|
? null
|
|
39
35
|
: h(
|
|
40
36
|
'ul',
|
|
41
|
-
{ class: 'cg-
|
|
42
|
-
tier.features.map((feature) => h('li', {}, feature)),
|
|
37
|
+
{ class: 'cg-fees__features' },
|
|
38
|
+
tier.features.map((feature) => h('li', { class: 'cg-fees__feature' }, feature)),
|
|
43
39
|
),
|
|
44
40
|
tier.action === undefined
|
|
45
41
|
? null
|
|
46
|
-
: h(
|
|
42
|
+
: h(
|
|
43
|
+
'div',
|
|
44
|
+
{ class: 'cg-fees__action' },
|
|
45
|
+
actionLink(ctx, {
|
|
46
|
+
...tier.action,
|
|
47
|
+
emphasis: tier.action.emphasis ?? (highlighted ? 'primary' : 'secondary'),
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
47
50
|
)
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export function renderPricingTable(block: PricingTableBlock, ctx: RenderContext): HtmlElement {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
return h(
|
|
54
|
+
const tierTag = nestedHeadingTag('pricingTable', block.title !== undefined)
|
|
55
|
+
return section(
|
|
54
56
|
'section',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
block.title ?? '',
|
|
61
|
-
)
|
|
62
|
-
: null,
|
|
57
|
+
'pricingTable',
|
|
58
|
+
'cg-fees',
|
|
59
|
+
{ 'data-count': String(Math.min(block.tiers.length, 4)) },
|
|
60
|
+
'div',
|
|
61
|
+
sectionHead('pricingTable', block.title),
|
|
63
62
|
h(
|
|
64
63
|
'ul',
|
|
65
|
-
{ class: 'cg-
|
|
64
|
+
{ class: 'cg-fees__tiers' },
|
|
66
65
|
block.tiers.map((tier) => renderTier(tier, ctx, tierTag)),
|
|
67
66
|
),
|
|
68
67
|
)
|
|
@@ -1,22 +1,47 @@
|
|
|
1
|
-
import type { ProseBlock } from '@cogenta/blocks'
|
|
1
|
+
import type { ProseBlock, RichTextDocument } from '@cogenta/blocks'
|
|
2
2
|
import { type HtmlElement, h, type RenderContext, renderRichText } from '@cogenta/theme-kit'
|
|
3
|
+
import { section } from '../layout.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Running text on the studio grid. When the text opens with a subhead, the
|
|
7
|
+
* subhead moves into the first three columns as the section's label and the
|
|
8
|
+
* text starts at the fourth, level with it: the asymmetric page of a case
|
|
9
|
+
* study. Without a subhead the text simply takes the reading column.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* heading) lives entirely in `blocks.css`, driven off the very structure
|
|
14
|
-
* `renderRichText` already produces.
|
|
11
|
+
* On a project page a long text sets in two columns on a wide screen
|
|
12
|
+
* (`work.css`). A short one stays in one: two columns of four lines each
|
|
13
|
+
* read as a mistake, not as a page. Everywhere else the text keeps one
|
|
14
|
+
* column at the reading measure.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
/** Below this many words, a text is kept in one column. */
|
|
18
|
+
export const TWO_COLUMN_MIN_WORDS = 110
|
|
19
|
+
|
|
20
|
+
function wordCount(document: RichTextDocument): number {
|
|
21
|
+
let words = 0
|
|
22
|
+
for (const node of document) {
|
|
23
|
+
if (node._type !== 'block') continue
|
|
24
|
+
for (const span of node.children) {
|
|
25
|
+
words += span.text.split(/\s+/u).filter((word) => word !== '').length
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return words
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
export function renderProse(block: ProseBlock, ctx: RenderContext): HtmlElement {
|
|
17
|
-
|
|
32
|
+
const [first, ...rest] = block.body
|
|
33
|
+
const opensOnSubhead = first !== undefined && first._type === 'block' && first.style === 'h2'
|
|
34
|
+
const body = opensOnSubhead ? rest : block.body
|
|
35
|
+
return section(
|
|
36
|
+
'div',
|
|
37
|
+
'prose',
|
|
38
|
+
'cg-prose',
|
|
39
|
+
{
|
|
40
|
+
'data-label': opensOnSubhead ? 'true' : 'false',
|
|
41
|
+
'data-length': wordCount(body) >= TWO_COLUMN_MIN_WORDS ? 'long' : 'short',
|
|
42
|
+
},
|
|
18
43
|
'div',
|
|
19
|
-
{ class: 'cg-
|
|
20
|
-
renderRichText(ctx,
|
|
44
|
+
opensOnSubhead ? h('div', { class: 'cg-prose__label' }, renderRichText(ctx, [first])) : null,
|
|
45
|
+
h('div', { class: 'cg-prose__body' }, renderRichText(ctx, body)),
|
|
21
46
|
)
|
|
22
47
|
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import type { QuoteBlock } 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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* fills the column edge to edge, the way a magazine breaks one out of a
|
|
11
|
-
* feature. The attribution is set in the mono register, the theme's
|
|
12
|
-
* register for credits and metadata.
|
|
13
|
-
*
|
|
14
|
-
* The avatar is decorative here — the name is right beside it in text — so
|
|
15
|
-
* its media entity's alt text is expected to be empty. `image` still writes
|
|
16
|
-
* the attribute either way.
|
|
6
|
+
* A client's words, set large in the text width from the fourth column, with
|
|
7
|
+
* typographic quotation marks from the stylesheet (`quotes`), and the speaker
|
|
8
|
+
* under them in the caption size: their name, then their role. The portrait,
|
|
9
|
+
* when there is one, is a small square: it is decorative (the name is beside
|
|
10
|
+
* it in text), so it keeps the media library's own empty `alt`.
|
|
17
11
|
*/
|
|
18
12
|
export function renderQuote(block: QuoteBlock, ctx: RenderContext): HtmlElement {
|
|
19
|
-
const hasAttribution =
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
const hasAttribution = block.author !== undefined || block.role !== undefined
|
|
14
|
+
return section(
|
|
15
|
+
'div',
|
|
16
|
+
'quote',
|
|
17
|
+
'cg-quote',
|
|
18
|
+
{},
|
|
22
19
|
'figure',
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
h(
|
|
21
|
+
'blockquote',
|
|
22
|
+
{ class: 'cg-quote__quote' },
|
|
23
|
+
h('p', { class: 'cg-quote__text', 'data-field': 'text' }, block.text),
|
|
24
|
+
),
|
|
25
25
|
hasAttribution
|
|
26
26
|
? h(
|
|
27
27
|
'figcaption',
|
|
@@ -34,7 +34,7 @@ export function renderQuote(block: QuoteBlock, ctx: RenderContext): HtmlElement
|
|
|
34
34
|
}),
|
|
35
35
|
h(
|
|
36
36
|
'span',
|
|
37
|
-
{ class: 'cg-
|
|
37
|
+
{ class: 'cg-quote__who' },
|
|
38
38
|
block.author === undefined
|
|
39
39
|
? null
|
|
40
40
|
: h('span', { class: 'cg-quote__author', 'data-field': 'author' }, block.author),
|