@getvitops/emdash 2.0.0-alpha.2 → 2.0.0-alpha.4
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/CHANGELOG.md +44 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +99 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/astro/blocks/Accordion.astro +10 -1
- package/src/astro/blocks/Badges.astro +1 -1
- package/src/astro/blocks/Callout.astro +1 -1
- package/src/astro/blocks/Cards.astro +4 -3
- package/src/astro/blocks/Hero.astro +4 -3
- package/src/astro/blocks/Logos.astro +25 -0
- package/src/astro/blocks/Nav.astro +1 -1
- package/src/astro/blocks/Pricing.astro +4 -0
- package/src/astro/blocks/Reviews.astro +1 -1
- package/src/astro/blocks/Section.astro +41 -6
- package/src/astro/blocks/attrs.test.ts +4 -2
- package/src/astro/blocks/attrs.ts +1 -1
- package/src/astro/blocks/render.container.test.ts +83 -3
- package/src/astro/blocks/tone.test.ts +2 -2
- package/src/astro/blocks/tone.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getvitops/emdash",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.4",
|
|
4
4
|
"description": "EmDash CMS native plugin for the Vitops design system — Portable Text block types editors can insert from the admin, rendered with the design-system web components and CSS framework.",
|
|
5
5
|
"homepage": "https://docs.vitops.ca",
|
|
6
6
|
"bugs": "https://github.com/getvitops/design-system/issues",
|
|
@@ -31,15 +31,15 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@getvitops/generator": "8.3.0-alpha.
|
|
35
|
-
"@getvitops/utils": "8.3.0-alpha.
|
|
34
|
+
"@getvitops/generator": "8.3.0-alpha.4",
|
|
35
|
+
"@getvitops/utils": "8.3.0-alpha.4"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@astrojs/cloudflare": "^14.0.0",
|
|
39
39
|
"@astrojs/react": "^7.0.0",
|
|
40
40
|
"@emdash-cms/cloudflare": "^1.4.0",
|
|
41
|
-
"@getvitops/astro": "8.3.0-alpha.
|
|
42
|
-
"@getvitops/core": "8.3.0-alpha.
|
|
41
|
+
"@getvitops/astro": "8.3.0-alpha.4",
|
|
42
|
+
"@getvitops/core": "8.3.0-alpha.4",
|
|
43
43
|
"@types/react": "^19.3.0",
|
|
44
44
|
"@types/react-dom": "^19.3.0",
|
|
45
45
|
"astro": "^7.4.4",
|
|
@@ -20,6 +20,8 @@ const items = rows<Record<string, unknown>>(value.items)
|
|
|
20
20
|
open: Boolean(row.open),
|
|
21
21
|
}))
|
|
22
22
|
.filter((item) => item.summary);
|
|
23
|
+
const { spriteBase = '/vitops' } = Astro.props as { spriteBase?: string };
|
|
24
|
+
const sprite = `${spriteBase}/icons.svg`;
|
|
23
25
|
const group = value.exclusive ? `accordion-${value._key ?? 'group'}` : undefined;
|
|
24
26
|
import { rootAttrs } from './attrs.ts';
|
|
25
27
|
---
|
|
@@ -29,7 +31,14 @@ import { rootAttrs } from './attrs.ts';
|
|
|
29
31
|
<div {...rootAttrs(value, ['accordion'])}>
|
|
30
32
|
{items.map((item) => (
|
|
31
33
|
<details name={group} open={item.open}>
|
|
32
|
-
<summary>
|
|
34
|
+
<summary>
|
|
35
|
+
<span>{item.summary}</span>
|
|
36
|
+
<span class="icon details-icon" aria-hidden="true">
|
|
37
|
+
<svg>
|
|
38
|
+
<use href={`${sprite}#icon-chevron-down`} />
|
|
39
|
+
</svg>
|
|
40
|
+
</span>
|
|
41
|
+
</summary>
|
|
33
42
|
{item.body.map((p) => (
|
|
34
43
|
<p>{p}</p>
|
|
35
44
|
))}
|
|
@@ -46,7 +46,7 @@ const items = rows<Record<string, unknown>>(value.items)
|
|
|
46
46
|
return (
|
|
47
47
|
<li>
|
|
48
48
|
<Tag
|
|
49
|
-
class:list={[kind, item.tone.style && item.tone.
|
|
49
|
+
class:list={[kind, item.tone.style && item.tone.surface, item.href && 'link']}
|
|
50
50
|
href={item.href || undefined}
|
|
51
51
|
style={item.tone.style || undefined}
|
|
52
52
|
>
|
|
@@ -39,7 +39,7 @@ import { rootAttrs } from './attrs.ts';
|
|
|
39
39
|
{
|
|
40
40
|
(heading || body) && (
|
|
41
41
|
<div
|
|
42
|
-
{...rootAttrs(value, ['callout', tone.className, value.solid && 'tone--solid', value.align === 'center' && 'callout--center'], joinStyle(tone.style))}
|
|
42
|
+
{...rootAttrs(value, ['callout', value.solid ? tone.className : tone.surface, value.solid && 'tone--solid', value.align === 'center' && 'callout--center'], joinStyle(tone.style))}
|
|
43
43
|
>
|
|
44
44
|
<div class="callout__body">
|
|
45
45
|
{heading && <Heading class="callout__title font-title">{heading}</Heading>}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import Cards from '@getvitops/astro/components/Cards.astro';
|
|
20
20
|
import Subgrid from '@getvitops/astro/components/Subgrid.astro';
|
|
21
21
|
import RichText from './RichText.astro';
|
|
22
|
-
import { joinStyle, resolveTone, rowTone } from './tone.ts';
|
|
22
|
+
import { joinStyle, resolveTheme, resolveTone, rowTone } from './tone.ts';
|
|
23
23
|
import { headingTag, lines, resolveBlockValue, rows, safeHref, text } from './value.ts';
|
|
24
24
|
|
|
25
25
|
interface Value {
|
|
@@ -44,6 +44,7 @@ const cards = rows<Record<string, unknown>>(value.cards)
|
|
|
44
44
|
ctaLabel: text(row.ctaLabel),
|
|
45
45
|
ctaHref: safeHref(row.ctaHref),
|
|
46
46
|
style: rowTone(row),
|
|
47
|
+
theme: resolveTheme(row.theme),
|
|
47
48
|
}))
|
|
48
49
|
.filter((card) => card.title || card.body || card.image);
|
|
49
50
|
const columns = Math.min(4, Math.max(1, Math.round(Number(value.columns) || 3)));
|
|
@@ -59,8 +60,8 @@ import { rootAttrs } from './attrs.ts';
|
|
|
59
60
|
<Grid {...rootAttrs(value, [], `--subgrid-cols: ${columns}`)}>
|
|
60
61
|
{cards.map((card) => (
|
|
61
62
|
<li
|
|
62
|
-
class:list={['card', 'subgrid-card', panel && tone.className]}
|
|
63
|
-
style={panel ? joinStyle(tone.style, card.style) : card.style}
|
|
63
|
+
class:list={['card', 'subgrid-card', panel && tone.surface, card.theme?.className]}
|
|
64
|
+
style={joinStyle(panel ? joinStyle(tone.style, card.style) : card.style, card.theme?.style)}
|
|
64
65
|
>
|
|
65
66
|
{card.image && <img src={card.image} alt={card.imageAlt} loading="lazy" />}
|
|
66
67
|
{card.title && <Heading class="card__title font-heading">{card.title}</Heading>}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Flat fields, because a block cannot contain other blocks.
|
|
9
9
|
*/
|
|
10
10
|
import RichText from './RichText.astro';
|
|
11
|
-
import { joinStyle, resolveTone, rowTone } from './tone.ts';
|
|
11
|
+
import { joinStyle, resolveTheme, resolveTone, rowTone } from './tone.ts';
|
|
12
12
|
import {
|
|
13
13
|
accentRuns,
|
|
14
14
|
outlineTag,
|
|
@@ -61,6 +61,7 @@ const cards = rows<Record<string, unknown>>(value.cards)
|
|
|
61
61
|
ctaLabel: text(row.ctaLabel),
|
|
62
62
|
ctaHref: safeHref(row.ctaHref),
|
|
63
63
|
style: rowTone(row),
|
|
64
|
+
theme: resolveTheme(row.theme),
|
|
64
65
|
}))
|
|
65
66
|
.filter((card) => card.title || card.body);
|
|
66
67
|
import { rootAttrs } from './attrs.ts';
|
|
@@ -68,7 +69,7 @@ import { rootAttrs } from './attrs.ts';
|
|
|
68
69
|
|
|
69
70
|
{
|
|
70
71
|
(heading || body) && (
|
|
71
|
-
<section {...rootAttrs(value, [tone.style && tone.
|
|
72
|
+
<section {...rootAttrs(value, [tone.style && tone.surface], joinStyle(tone.style))}>
|
|
72
73
|
<div class={split ? 'split split-1-2' : 'centered'}>
|
|
73
74
|
<div class="rhythm">
|
|
74
75
|
{eyebrow && <p class="font-eyebrow">{eyebrow}</p>}
|
|
@@ -100,7 +101,7 @@ import { rootAttrs } from './attrs.ts';
|
|
|
100
101
|
{cards.length > 0 && (
|
|
101
102
|
<ul class="subgrid centered" role="list" style={`--subgrid-cols: ${Math.min(4, cards.length)}`}>
|
|
102
103
|
{cards.map((card) => (
|
|
103
|
-
<li class:list={['card', 'subgrid-card', card.style && 'tone']} style={card.style}>
|
|
104
|
+
<li class:list={['card', 'subgrid-card', card.style && 'tone tone--tint', card.theme?.className]} style={joinStyle(card.style, card.theme?.style)}>
|
|
104
105
|
{card.eyebrow && <p class="font-eyebrow">{card.eyebrow}</p>}
|
|
105
106
|
{card.title && <CardHeading class="font-heading">{card.title}</CardHeading>}
|
|
106
107
|
{card.body && (
|
|
@@ -13,6 +13,10 @@ interface Value {
|
|
|
13
13
|
logos?: unknown;
|
|
14
14
|
grayscale?: boolean;
|
|
15
15
|
size?: string;
|
|
16
|
+
kicker?: string;
|
|
17
|
+
note?: string;
|
|
18
|
+
linkLabel?: string;
|
|
19
|
+
linkHref?: string;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
const HEIGHTS: Record<string, string> = { sm: '1.75rem', md: '2.5rem', lg: '3.5rem' };
|
|
@@ -23,11 +27,23 @@ const logos = rows<Record<string, unknown>>(value.logos)
|
|
|
23
27
|
.map((row) => ({ image: safeHref(row.image), alt: text(row.alt), href: safeHref(row.href) }))
|
|
24
28
|
.filter((logo) => logo.image || logo.alt);
|
|
25
29
|
const height = HEIGHTS[text(value.size)];
|
|
30
|
+
const kicker = text(value.kicker);
|
|
31
|
+
const note = text(value.note);
|
|
32
|
+
const linkLabel = text(value.linkLabel);
|
|
33
|
+
const linkHref = safeHref(value.linkHref);
|
|
34
|
+
const hasLink = Boolean(linkLabel && linkHref);
|
|
35
|
+
// A wrapper exists only when there is a kicker, note or link to hold. The attributes stay on the
|
|
36
|
+
// list in both cases, as they do on `vitops_cards`, so a selector on the list works either way.
|
|
37
|
+
const framed = Boolean(kicker || note || hasLink);
|
|
38
|
+
const Wrap = framed ? 'div' : Fragment;
|
|
39
|
+
const wrapProps = framed ? { class: 'logos-block' } : {};
|
|
26
40
|
import { rootAttrs } from './attrs.ts';
|
|
27
41
|
---
|
|
28
42
|
|
|
29
43
|
{
|
|
30
44
|
logos.length > 0 && (
|
|
45
|
+
<Wrap {...wrapProps}>
|
|
46
|
+
{kicker && <p class="logos__kicker font-eyebrow">{kicker}</p>}
|
|
31
47
|
<ul
|
|
32
48
|
{...rootAttrs(value, ['logos', value.grayscale && 'logos--grayscale'], height ? `--logos-height: ${height}` : undefined)}
|
|
33
49
|
role="list"
|
|
@@ -47,5 +63,14 @@ import { rootAttrs } from './attrs.ts';
|
|
|
47
63
|
</li>
|
|
48
64
|
))}
|
|
49
65
|
</ul>
|
|
66
|
+
{note && <p class="logos__note font-caption">{note}</p>}
|
|
67
|
+
{hasLink && (
|
|
68
|
+
<p class="logos__link">
|
|
69
|
+
<a class="link" href={linkHref}>
|
|
70
|
+
{linkLabel}
|
|
71
|
+
</a>
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
</Wrap>
|
|
50
75
|
)
|
|
51
76
|
}
|
|
@@ -66,7 +66,7 @@ import { rootAttrs } from './attrs.ts';
|
|
|
66
66
|
{
|
|
67
67
|
items.length > 0 && (
|
|
68
68
|
<div
|
|
69
|
-
{...rootAttrs(value, ['navblock', toned && tone.
|
|
69
|
+
{...rootAttrs(value, ['navblock', toned && tone.surface], toned ? joinStyle(tone.style) : undefined)}
|
|
70
70
|
>
|
|
71
71
|
{heading && <Heading class="font-heading">{heading}</Heading>}
|
|
72
72
|
{variant === 'sitenav' ? (
|
|
@@ -20,6 +20,8 @@ const plans = rows<Record<string, unknown>>(value.plans)
|
|
|
20
20
|
name: text(row.name),
|
|
21
21
|
price: text(row.price),
|
|
22
22
|
period: text(row.period),
|
|
23
|
+
yearly: text(row.yearly),
|
|
24
|
+
note: text(row.note),
|
|
23
25
|
features: lines(row.features),
|
|
24
26
|
ctaLabel: text(row.ctaLabel),
|
|
25
27
|
ctaHref: safeHref(row.ctaHref),
|
|
@@ -41,10 +43,12 @@ import { rootAttrs } from './attrs.ts';
|
|
|
41
43
|
<li class:list={['pricing-card', plan.featured && 'pricing-card--featured']}>
|
|
42
44
|
<div class="pricing-card__header">
|
|
43
45
|
{plan.name && <Heading class="font-title">{plan.name}</Heading>}
|
|
46
|
+
{plan.note && <p class="pricing-card__note font-caption">{plan.note}</p>}
|
|
44
47
|
</div>
|
|
45
48
|
<p class="pricing-card__price">
|
|
46
49
|
{plan.price}
|
|
47
50
|
{plan.period && <small class="font-caption"> {plan.period}</small>}
|
|
51
|
+
{plan.yearly && <small class="pricing-card__yearly font-caption">{plan.yearly}</small>}
|
|
48
52
|
</p>
|
|
49
53
|
<ul class="pricing-card__features" role="list">
|
|
50
54
|
{plan.features.map((feature) => (
|
|
@@ -81,7 +81,7 @@ import { rootAttrs } from './attrs.ts';
|
|
|
81
81
|
{
|
|
82
82
|
reviews.length > 0 ? (
|
|
83
83
|
<div
|
|
84
|
-
{...rootAttrs(value, ['reviews', toned && tone.
|
|
84
|
+
{...rootAttrs(value, ['reviews', toned && tone.surface], toned ? joinStyle(tone.style) : undefined)}
|
|
85
85
|
>
|
|
86
86
|
{layout === 'carousel' ? (
|
|
87
87
|
<Carousel label={label}>
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* Composed from the framework's layout primitives: `.region` for vertical rhythm, `.centered`
|
|
9
9
|
* for the tracks (the `width` field picks `breakout`/`spotlight`/`fullbleed`), `.section` for
|
|
10
10
|
* the head and the heading-left `split` layout, `.tone` for the backdrop. The heading is a
|
|
11
|
-
* real heading at the editor's `level
|
|
11
|
+
* real heading at the editor's `level`, with `*accent*` runs. Level 1 plus `cta_label`/`secondary_label`
|
|
12
|
+
* make it the page opener, so a hero is not nested in a section. `stack: rhythm` puts `.rhythm` on the
|
|
13
|
+
* inner element so head and body are spaced by the vertical-rhythm scale.
|
|
12
14
|
*
|
|
13
15
|
* The body is rendered by EmDash's `<PortableText>`, which merges this plugin's
|
|
14
16
|
* `blockComponents`, so a `vitops_cards` or `vitops_actionLink` placed in it renders with its
|
|
@@ -22,7 +24,7 @@
|
|
|
22
24
|
const { PortableText } = await import('emdash/ui');
|
|
23
25
|
import { unrenderedTypes } from './unrendered.ts';
|
|
24
26
|
import { joinStyle, resolveTone } from './tone.ts';
|
|
25
|
-
import { outlineTag, resolveBlockValue, text } from './value.ts';
|
|
27
|
+
import { accentRuns, outlineTag, resolveBlockValue, safeHref, text } from './value.ts';
|
|
26
28
|
|
|
27
29
|
interface Value {
|
|
28
30
|
kicker?: string;
|
|
@@ -36,6 +38,12 @@ interface Value {
|
|
|
36
38
|
color?: string;
|
|
37
39
|
solid?: boolean;
|
|
38
40
|
theme?: string;
|
|
41
|
+
// A block TYPE's slugs must be lowercase (like `class_name`), so these are snake_case.
|
|
42
|
+
cta_label?: string;
|
|
43
|
+
cta_href?: string;
|
|
44
|
+
secondary_label?: string;
|
|
45
|
+
secondary_href?: string;
|
|
46
|
+
stack?: string;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
const WIDTHS: Record<string, string> = {
|
|
@@ -57,22 +65,49 @@ const tone = resolveTone(value.tone, value.color);
|
|
|
57
65
|
const solid = Boolean(value.solid);
|
|
58
66
|
const layout = text(value.layout);
|
|
59
67
|
const width = WIDTHS[text(value.width)];
|
|
60
|
-
const
|
|
68
|
+
const headingRuns = accentRuns(heading);
|
|
69
|
+
const ctaLabel = text(value.cta_label);
|
|
70
|
+
const ctaHref = safeHref(value.cta_href);
|
|
71
|
+
const secondaryLabel = text(value.secondary_label);
|
|
72
|
+
const secondaryHref = safeHref(value.secondary_href);
|
|
73
|
+
const hasActions = Boolean((ctaLabel && ctaHref) || (secondaryLabel && secondaryHref));
|
|
74
|
+
const stacked = text(value.stack) === 'rhythm';
|
|
75
|
+
const hasHead = Boolean(kicker || heading || sub || hasActions);
|
|
61
76
|
import { rootAttrs } from './attrs.ts';
|
|
62
77
|
---
|
|
63
78
|
|
|
64
79
|
{
|
|
65
80
|
(hasHead || body.length > 0) && (
|
|
66
81
|
<section
|
|
67
|
-
{...rootAttrs(value, ['section', 'region', tone.style && tone.className, tone.style && solid && 'tone--solid', layout === 'split' && 'section--split', layout === 'center' && 'section--center'], joinStyle(tone.style))}
|
|
82
|
+
{...rootAttrs(value, ['section', 'region', tone.style && (solid ? tone.className : tone.surface), tone.style && solid && 'tone--solid', layout === 'split' && 'section--split', layout === 'center' && 'section--center'], joinStyle(tone.style))}
|
|
68
83
|
>
|
|
69
84
|
<div class="centered">
|
|
70
|
-
<div class:list={['section__inner', width]}>
|
|
85
|
+
<div class:list={['section__inner', width, stacked && 'rhythm']}>
|
|
71
86
|
{hasHead && (
|
|
72
87
|
<header class="section__head">
|
|
73
88
|
{kicker && <p class="section__kicker font-eyebrow">{kicker}</p>}
|
|
74
|
-
{heading &&
|
|
89
|
+
{heading && (
|
|
90
|
+
<Heading class="section__title font-title">
|
|
91
|
+
{headingRuns.map((run) =>
|
|
92
|
+
run.accent ? <em class="accent">{run.text}</em> : run.text,
|
|
93
|
+
)}
|
|
94
|
+
</Heading>
|
|
95
|
+
)}
|
|
75
96
|
{sub && <p class="section__sub font-lead">{sub}</p>}
|
|
97
|
+
{hasActions && (
|
|
98
|
+
<p class="section__actions cluster">
|
|
99
|
+
{ctaLabel && ctaHref && (
|
|
100
|
+
<a class="cta" href={ctaHref}>
|
|
101
|
+
{ctaLabel}
|
|
102
|
+
</a>
|
|
103
|
+
)}
|
|
104
|
+
{secondaryLabel && secondaryHref && (
|
|
105
|
+
<a class="link" href={secondaryHref}>
|
|
106
|
+
{secondaryLabel}
|
|
107
|
+
</a>
|
|
108
|
+
)}
|
|
109
|
+
</p>
|
|
110
|
+
)}
|
|
76
111
|
</header>
|
|
77
112
|
)}
|
|
78
113
|
{body.length > 0 && (
|
|
@@ -58,7 +58,7 @@ describe('blockAttrs', () => {
|
|
|
58
58
|
describe('theme on every block', () => {
|
|
59
59
|
it('adds the tinted classes and --tone-* properties to the root', () => {
|
|
60
60
|
const a = rootAttrs({ theme: 'surface' }, ['callout']);
|
|
61
|
-
expect(a.class).toBe('callout tone');
|
|
61
|
+
expect(a.class).toBe('callout tone tone--tint');
|
|
62
62
|
expect(a.style).toContain('--tone-bg: var(--color-bg-surface-muted');
|
|
63
63
|
});
|
|
64
64
|
it('fills with the role solid and its computed text for `<role>:solid`', () => {
|
|
@@ -67,7 +67,9 @@ describe('theme on every block', () => {
|
|
|
67
67
|
expect(a.style).toContain('--tone-on-solid: var(--color-text-on-brand-primary');
|
|
68
68
|
});
|
|
69
69
|
it("replaces a block's own tone classes rather than doubling them, and is inert for none", () => {
|
|
70
|
-
expect(rootAttrs({ theme: 'info' }, ['x', 'tone', 'tone--solid']).class).toBe(
|
|
70
|
+
expect(rootAttrs({ theme: 'info' }, ['x', 'tone', 'tone--solid']).class).toBe(
|
|
71
|
+
'x tone tone--tint',
|
|
72
|
+
);
|
|
71
73
|
expect(rootAttrs({ theme: 'none' }, ['x', 'tone']).class).toBe('x tone');
|
|
72
74
|
expect(rootAttrs({ theme: 'bad role!' }, ['x']).class).toBe('x');
|
|
73
75
|
});
|
|
@@ -102,7 +102,7 @@ export function rootAttrs(
|
|
|
102
102
|
const out: BlockAttrs = {};
|
|
103
103
|
if (user.id) out.id = user.id;
|
|
104
104
|
const cls = [
|
|
105
|
-
...own.filter((c) => !theme || (c !== 'tone' && c !== 'tone--solid')),
|
|
105
|
+
...own.filter((c) => !theme || (c !== 'tone' && c !== 'tone--solid' && c !== 'tone--tint')),
|
|
106
106
|
theme?.className,
|
|
107
107
|
user.id && 'block-anchor',
|
|
108
108
|
user.class,
|
|
@@ -59,7 +59,7 @@ describe('vitops_cards', () => {
|
|
|
59
59
|
tone: 'brand-primary',
|
|
60
60
|
cards: [{ title: 'Pillar', checklist: 'one\ntwo', ctaLabel: 'Go', ctaHref: '/go' }],
|
|
61
61
|
});
|
|
62
|
-
expect(html).toContain('class="card subgrid-card tone"');
|
|
62
|
+
expect(html).toContain('class="card subgrid-card tone tone--tint"');
|
|
63
63
|
expect(html).toContain('--tone-bg: var(--color-bg-brand-primary-muted');
|
|
64
64
|
expect(html).toContain('<li>one</li>');
|
|
65
65
|
expect(html).toContain('class="cta" href="/go"');
|
|
@@ -210,6 +210,25 @@ describe('vitops_entries', () => {
|
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
describe('vitops_logos frame', () => {
|
|
214
|
+
it('wraps the row with a kicker, note and link only when one is set', async () => {
|
|
215
|
+
const bare = await render(Logos, { logos: [{ alt: 'Acme' }] });
|
|
216
|
+
expect(bare).not.toContain('logos-block');
|
|
217
|
+
const html = await render(Logos, {
|
|
218
|
+
kicker: 'Trusted by',
|
|
219
|
+
note: 'and 40 more',
|
|
220
|
+
linkLabel: 'All partners',
|
|
221
|
+
linkHref: '/partners',
|
|
222
|
+
logos: [{ alt: 'Acme' }],
|
|
223
|
+
});
|
|
224
|
+
expect(html).toContain('<div class="logos-block">');
|
|
225
|
+
expect(html).toContain('<p class="logos__kicker font-eyebrow">Trusted by</p>');
|
|
226
|
+
expect(html).toContain('<p class="logos__note font-caption">and 40 more</p>');
|
|
227
|
+
expect(html).toContain('<a class="link" href="/partners">All partners</a>');
|
|
228
|
+
expect(html).toContain('aria-label="Partners"');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
213
232
|
describe('vitops_pricing', () => {
|
|
214
233
|
it('aligns plans through the pricing-card subgrid rows', async () => {
|
|
215
234
|
const html = await render(Pricing, {
|
|
@@ -238,6 +257,24 @@ describe('vitops_pricing', () => {
|
|
|
238
257
|
expect(html).toContain('<h3 class="font-title">Pro</h3>');
|
|
239
258
|
expect(html).toContain('pricing-card__feature');
|
|
240
259
|
});
|
|
260
|
+
|
|
261
|
+
it('carries a yearly line and a note per plan', async () => {
|
|
262
|
+
const html = await render(Pricing, {
|
|
263
|
+
plans: [
|
|
264
|
+
{
|
|
265
|
+
name: 'Pro',
|
|
266
|
+
price: '$39',
|
|
267
|
+
period: '/month',
|
|
268
|
+
yearly: '$34 paid yearly',
|
|
269
|
+
note: 'Best for teams',
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
expect(html).toContain(
|
|
274
|
+
'<small class="pricing-card__yearly font-caption">$34 paid yearly</small>',
|
|
275
|
+
);
|
|
276
|
+
expect(html).toContain('<p class="pricing-card__note font-caption">Best for teams</p>');
|
|
277
|
+
});
|
|
241
278
|
});
|
|
242
279
|
|
|
243
280
|
describe('vitops_embed', () => {
|
|
@@ -385,7 +422,7 @@ describe('vitops_reviews', () => {
|
|
|
385
422
|
fixtures.cache = { version: 1, locations: { ottawa: { reviews: [review('a', 5)] } } };
|
|
386
423
|
const { default: Reviews } = await import('./Reviews.astro');
|
|
387
424
|
const html = await render(Reviews, { tone: 'brand-primary' });
|
|
388
|
-
expect(html).toContain('class="reviews tone"');
|
|
425
|
+
expect(html).toContain('class="reviews tone tone--tint"');
|
|
389
426
|
expect(html).toContain('<article class="review"');
|
|
390
427
|
});
|
|
391
428
|
});
|
|
@@ -491,13 +528,38 @@ describe('vitops_section', () => {
|
|
|
491
528
|
},
|
|
492
529
|
},
|
|
493
530
|
});
|
|
494
|
-
expect(html).toContain('section region tone section--split');
|
|
531
|
+
expect(html).toContain('section region tone tone--tint section--split');
|
|
495
532
|
expect(html).toContain('<h3 class="section__title font-title">Two services</h3>');
|
|
496
533
|
expect(html).toContain('Why us');
|
|
497
534
|
expect(html).toContain('Body copy');
|
|
498
535
|
expect(html).toContain('class="logos"');
|
|
499
536
|
expect(html).toContain('alt="Acme"');
|
|
500
537
|
}, 60_000);
|
|
538
|
+
|
|
539
|
+
it('is a page opener: accent heading, two actions and a rhythm stack', async () => {
|
|
540
|
+
const { default: Section } = await import('./Section.astro');
|
|
541
|
+
const container = await AstroContainer.create();
|
|
542
|
+
const html = await container.renderToString(Section as unknown as Component, {
|
|
543
|
+
props: {
|
|
544
|
+
value: {
|
|
545
|
+
heading: 'Two services. *One team.*',
|
|
546
|
+
level: '1',
|
|
547
|
+
sub: 'Lead',
|
|
548
|
+
cta_label: 'Book a call',
|
|
549
|
+
cta_href: '/contact',
|
|
550
|
+
secondary_label: 'See pricing',
|
|
551
|
+
secondary_href: '/pricing',
|
|
552
|
+
stack: 'rhythm',
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
expect(html).toContain(
|
|
557
|
+
'<h1 class="section__title font-title">Two services. <em class="accent">One team.</em></h1>',
|
|
558
|
+
);
|
|
559
|
+
expect(html).toContain('<a class="cta" href="/contact">Book a call</a>');
|
|
560
|
+
expect(html).toContain('<a class="link" href="/pricing">See pricing</a>');
|
|
561
|
+
expect(html).toContain('class="section__inner rhythm"');
|
|
562
|
+
}, 60_000);
|
|
501
563
|
});
|
|
502
564
|
|
|
503
565
|
describe('vitops_form', () => {
|
|
@@ -680,3 +742,21 @@ describe('per-item colour and hero card extras', () => {
|
|
|
680
742
|
expect(html).not.toContain('stretched-link');
|
|
681
743
|
});
|
|
682
744
|
});
|
|
745
|
+
|
|
746
|
+
describe('per-card theme', () => {
|
|
747
|
+
it('tints one card without touching its siblings', async () => {
|
|
748
|
+
const container = await AstroContainer.create();
|
|
749
|
+
const html = await container.renderToString(Cards as unknown as Component, {
|
|
750
|
+
props: {
|
|
751
|
+
value: {
|
|
752
|
+
variant: 'panel',
|
|
753
|
+
cards: [{ title: 'Without us', theme: 'danger' }, { title: 'With us' }],
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
const [first, second] = html.split('<li ').slice(1);
|
|
758
|
+
expect(first).toContain('tone tone--tint');
|
|
759
|
+
expect(first).toContain('--tone-bg');
|
|
760
|
+
expect(second).not.toContain('--tone-bg');
|
|
761
|
+
}, 60_000);
|
|
762
|
+
});
|
|
@@ -16,8 +16,8 @@ describe('toneChain', () => {
|
|
|
16
16
|
|
|
17
17
|
describe('resolveTone', () => {
|
|
18
18
|
it('is empty when nothing is set, so an unconfigured block stays transparent', () => {
|
|
19
|
-
expect(resolveTone()).toEqual({ className: 'tone', style: '' });
|
|
20
|
-
expect(resolveTone('', '')).toEqual({ className: 'tone', style: '' });
|
|
19
|
+
expect(resolveTone()).toEqual({ className: 'tone', surface: 'tone', style: '' });
|
|
20
|
+
expect(resolveTone('', '')).toEqual({ className: 'tone', surface: 'tone', style: '' });
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it('resolves a role to var() chains the framework tokens satisfy', () => {
|
package/src/astro/blocks/tone.ts
CHANGED
|
@@ -68,6 +68,8 @@ function chain(tokens: string[]): string {
|
|
|
68
68
|
export interface ResolvedTone {
|
|
69
69
|
/** Always `tone`; a block adds `tone--solid` itself when it fills rather than tints. */
|
|
70
70
|
className: string;
|
|
71
|
+
/** `tone tone--tint`: what a block that is a surface (callout, card, band) puts on its root. */
|
|
72
|
+
surface: string;
|
|
71
73
|
/** Inline `style` value of `--tone-*` properties (no trailing semicolon). Empty when unset. */
|
|
72
74
|
style: string;
|
|
73
75
|
}
|
|
@@ -94,6 +96,7 @@ export function resolveTone(tone?: unknown, color?: unknown): ResolvedTone {
|
|
|
94
96
|
const flip = (light: string, dark: string) => `light-dark(${at(light)}, ${at(dark)})`;
|
|
95
97
|
return {
|
|
96
98
|
className: 'tone',
|
|
99
|
+
surface: 'tone tone--tint',
|
|
97
100
|
style: [
|
|
98
101
|
`--tone-bg: ${flip('100', '900')}`,
|
|
99
102
|
`--tone-fg: ${flip('900', '100')}`,
|
|
@@ -104,13 +107,14 @@ export function resolveTone(tone?: unknown, color?: unknown): ResolvedTone {
|
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
if (!hasTone) return { className: 'tone', style: '' };
|
|
110
|
+
if (!hasTone) return { className: 'tone', surface: 'tone', style: '' };
|
|
108
111
|
|
|
109
112
|
const roles = toneChain(tone);
|
|
110
113
|
const of = (prefix: string, suffix = '') =>
|
|
111
114
|
chain(roles.map((r) => `--color-${prefix}-${r}${suffix}`));
|
|
112
115
|
return {
|
|
113
116
|
className: 'tone',
|
|
117
|
+
surface: 'tone tone--tint',
|
|
114
118
|
style: [
|
|
115
119
|
`--tone-bg: ${of('bg', '-muted')}`,
|
|
116
120
|
`--tone-fg: ${of('text')}`,
|
|
@@ -160,5 +164,8 @@ export function resolveTheme(theme: unknown): { className: string; style: string
|
|
|
160
164
|
if (!role || !ROLE.test(role)) return undefined;
|
|
161
165
|
const tone = resolveTone(role);
|
|
162
166
|
if (!tone.style) return undefined;
|
|
163
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
className: fill === 'solid' ? 'tone tone--solid' : 'tone tone--tint',
|
|
169
|
+
style: tone.style,
|
|
170
|
+
};
|
|
164
171
|
}
|