@getvitops/emdash 2.0.0-alpha.0 → 2.0.0-alpha.2
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 +78 -0
- package/README.md +18 -16
- package/dist/index.d.mts +26 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +578 -129
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/astro/Nothing.astro +3 -0
- package/src/astro/WidgetArea.astro +113 -0
- package/src/astro/blocks/Accordion.astro +2 -1
- package/src/astro/blocks/ActionLink.astro +2 -1
- package/src/astro/blocks/Badges.astro +67 -0
- package/src/astro/blocks/Banner.astro +3 -2
- package/src/astro/blocks/Callout.astro +3 -8
- package/src/astro/blocks/Cards.astro +6 -4
- package/src/astro/blocks/Carousel.astro +2 -0
- package/src/astro/blocks/CopyButton.astro +3 -2
- package/src/astro/blocks/Details.astro +2 -1
- package/src/astro/blocks/Embed.astro +2 -1
- package/src/astro/blocks/Entries.astro +2 -1
- package/src/astro/blocks/Form.astro +20 -1
- package/src/astro/blocks/Gallery.astro +2 -0
- package/src/astro/blocks/Hero.astro +27 -11
- package/src/astro/blocks/ImageCompare.astro +2 -1
- package/src/astro/blocks/Logos.astro +10 -7
- package/src/astro/blocks/Marquee.astro +2 -1
- package/src/astro/blocks/Nav.astro +2 -2
- package/src/astro/blocks/NavTree.astro +1 -1
- package/src/astro/blocks/Pricing.astro +2 -2
- package/src/astro/blocks/Reviews.astro +3 -3
- package/src/astro/blocks/Section.astro +17 -13
- package/src/astro/blocks/Stats.astro +5 -4
- package/src/astro/blocks/Steps.astro +5 -5
- package/src/astro/blocks/WidgetArea.astro +21 -0
- package/src/astro/blocks/attrs.test.ts +74 -0
- package/src/astro/blocks/attrs.ts +116 -0
- package/src/astro/blocks/cycle.container.test.ts +73 -0
- package/src/astro/blocks/form.sample.ts +8 -2
- package/src/astro/blocks/form.ts +16 -5
- package/src/astro/blocks/nav.ts +1 -1
- package/src/astro/blocks/render.container.test.ts +131 -19
- package/src/astro/blocks/tone.test.ts +10 -0
- package/src/astro/blocks/tone.ts +48 -6
- package/src/astro/blocks/unrendered.ts +23 -0
- package/src/astro/blocks/value.test.ts +27 -1
- package/src/astro/blocks/value.ts +16 -0
- package/src/astro/index.ts +38 -21
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.2",
|
|
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.2",
|
|
35
|
+
"@getvitops/utils": "8.3.0-alpha.2"
|
|
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.2",
|
|
42
|
+
"@getvitops/core": "8.3.0-alpha.2",
|
|
43
43
|
"@types/react": "^19.3.0",
|
|
44
44
|
"@types/react-dom": "^19.3.0",
|
|
45
45
|
"astro": "^7.4.4",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A widget area rendered in the framework's vocabulary, in place of `emdash/ui`'s
|
|
4
|
+
* `<WidgetArea>`: titles at a chosen heading level (the stock one is always `h3`), a menu as a
|
|
5
|
+
* labelled `<nav>` of `.link`s, content through `<PortableText>` so every `vitops_*` block
|
|
6
|
+
* works inside it. Renders nothing when the area is missing or empty.
|
|
7
|
+
*
|
|
8
|
+
* Two limits, both deliberate:
|
|
9
|
+
* - **No widget area inside a widget area.** A content widget that held a `vitops_widgetArea`
|
|
10
|
+
* block could name its own area and recurse, and a request-scoped depth counter cannot be kept
|
|
11
|
+
* honest while Astro streams children, so the block is simply not rendered in here.
|
|
12
|
+
* - **`component` widgets** (`core:recent-posts`, …) are EmDash's closed map and its renderer is
|
|
13
|
+
* not exported, so an area that holds one is handed whole to the stock `<WidgetArea>`.
|
|
14
|
+
*
|
|
15
|
+
* Prerendered pages bake the area at build time: an edit in the admin shows after the next deploy.
|
|
16
|
+
*/
|
|
17
|
+
import { getMenu, getWidgetArea } from 'emdash';
|
|
18
|
+
import Nothing from './Nothing.astro';
|
|
19
|
+
import { unrenderedTypes } from './blocks/unrendered.ts';
|
|
20
|
+
import { joinStyle, resolveTheme } from './blocks/tone.ts';
|
|
21
|
+
import { headingTag, safeHref, text } from './blocks/value.ts';
|
|
22
|
+
// Lazy for the same reason as `blocks/Section.astro`: a static `emdash/ui` import makes a cycle
|
|
23
|
+
// through `virtual:emdash/block-components` and empties the plugin block map in a static build.
|
|
24
|
+
const { PortableText, WidgetArea: StockWidgetArea } = await import('emdash/ui');
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
/** The area's `name` as set under Widgets. */
|
|
28
|
+
name: string;
|
|
29
|
+
/** Level of each widget's title; default `3`. */
|
|
30
|
+
level?: number | string;
|
|
31
|
+
class?: string;
|
|
32
|
+
id?: string;
|
|
33
|
+
style?: string;
|
|
34
|
+
/** A backdrop for the area, from the site's colour roles: `surface`, `brand-primary:solid`, … */
|
|
35
|
+
theme?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { name, level, class: className, id, style, theme } = Astro.props;
|
|
39
|
+
const themed = resolveTheme(theme);
|
|
40
|
+
const area = name ? await getWidgetArea(name) : null;
|
|
41
|
+
const widgets = area?.widgets ?? [];
|
|
42
|
+
const delegated = widgets.some((w) => w.type === 'component');
|
|
43
|
+
const Heading = headingTag(level, 3);
|
|
44
|
+
const menus = Object.fromEntries(
|
|
45
|
+
await Promise.all(
|
|
46
|
+
widgets
|
|
47
|
+
.filter((w) => w.type === 'menu' && w.menuName)
|
|
48
|
+
.map(async (w) => [w.id, await getMenu(w.menuName!)] as const),
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
/** A content widget with no text and no blocks is what the admin creates by default; showing its title alone is noise. */
|
|
52
|
+
const hasBody = (content: unknown) =>
|
|
53
|
+
Array.isArray(content) &&
|
|
54
|
+
content.some(
|
|
55
|
+
(b: any) =>
|
|
56
|
+
b?._type !== 'block' ||
|
|
57
|
+
(Array.isArray(b.children) && b.children.some((c: any) => text(c?.text).trim())),
|
|
58
|
+
);
|
|
59
|
+
const shown = widgets.filter(
|
|
60
|
+
(w) => w.type !== 'content' || hasBody(w.content),
|
|
61
|
+
);
|
|
62
|
+
const missing = [
|
|
63
|
+
...new Set((await Promise.all(shown.map((w) => unrenderedTypes(w.content)))).flat()),
|
|
64
|
+
];
|
|
65
|
+
const inner = { type: { 'vitops_widgetArea': Nothing } };
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
shown.length > 0 && (
|
|
70
|
+
<div id={id} class:list={['widget-area', themed?.className, className]} style={joinStyle(themed?.style, style)} data-widget-area={area?.name}>
|
|
71
|
+
{delegated ? (
|
|
72
|
+
<StockWidgetArea name={name} />
|
|
73
|
+
) : (
|
|
74
|
+
<>
|
|
75
|
+
{shown.map((widget) => (
|
|
76
|
+
<section class="widget" data-widget-type={widget.type}>
|
|
77
|
+
{text(widget.title) && widget.type !== 'menu' && (
|
|
78
|
+
<Heading class="widget__title font-heading">{widget.title}</Heading>
|
|
79
|
+
)}
|
|
80
|
+
{widget.type === 'content' && (
|
|
81
|
+
<div class="widget__content rhythm">
|
|
82
|
+
<PortableText value={widget.content} components={inner} />
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
{widget.type === 'menu' && menus[widget.id] && (
|
|
86
|
+
<>
|
|
87
|
+
{text(widget.title) && <Heading class="widget__title font-heading">{widget.title}</Heading>}
|
|
88
|
+
<nav class="widget__menu" aria-label={text(widget.title) || area?.label}>
|
|
89
|
+
<ul role="list">
|
|
90
|
+
{menus[widget.id]!.items.map((item) => {
|
|
91
|
+
const href = safeHref(item.url);
|
|
92
|
+
return (
|
|
93
|
+
href && (
|
|
94
|
+
<li>
|
|
95
|
+
<a class="link" href={href} title={item.titleAttr || undefined}>
|
|
96
|
+
{item.label}
|
|
97
|
+
</a>
|
|
98
|
+
</li>
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</ul>
|
|
103
|
+
</nav>
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</section>
|
|
107
|
+
))}
|
|
108
|
+
{missing.map((type) => <template data-vitops-unrendered={type} />)}
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -21,11 +21,12 @@ const items = rows<Record<string, unknown>>(value.items)
|
|
|
21
21
|
}))
|
|
22
22
|
.filter((item) => item.summary);
|
|
23
23
|
const group = value.exclusive ? `accordion-${value._key ?? 'group'}` : undefined;
|
|
24
|
+
import { rootAttrs } from './attrs.ts';
|
|
24
25
|
---
|
|
25
26
|
|
|
26
27
|
{
|
|
27
28
|
items.length > 0 && (
|
|
28
|
-
<div
|
|
29
|
+
<div {...rootAttrs(value, ['accordion'])}>
|
|
29
30
|
{items.map((item) => (
|
|
30
31
|
<details name={group} open={item.open}>
|
|
31
32
|
<summary>{item.summary}</summary>
|
|
@@ -51,12 +51,13 @@ const classes = [variant, role ? `${variant}-${role}` : null, newTab ? 'link--ex
|
|
|
51
51
|
// Only render when there is somewhere to go and something to read — a bare icon
|
|
52
52
|
// with no label would be an unlabelled link.
|
|
53
53
|
const renderable = Boolean(label && href);
|
|
54
|
+
import { rootAttrs } from './attrs.ts';
|
|
54
55
|
---
|
|
55
56
|
|
|
56
57
|
{
|
|
57
58
|
renderable && (
|
|
58
59
|
<a
|
|
59
|
-
|
|
60
|
+
{...rootAttrs(value, [classes])}
|
|
60
61
|
href={href}
|
|
61
62
|
{...(newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {})}>
|
|
62
63
|
{startIcon && (
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* `vitops_badges` — a row of short labels on the framework's `.badge` (static) or `.tag`
|
|
4
|
+
* pattern. Each row may carry its own tone, falling back to the block's, which is what lets
|
|
5
|
+
* one block show a pine label beside an amber one. An icon is child order, not a modifier, and
|
|
6
|
+
* a row with a `href` becomes a link.
|
|
7
|
+
*/
|
|
8
|
+
import { resolveTone } from './tone.ts';
|
|
9
|
+
import { resolveBlockValue, rows, safeHref, text } from './value.ts';
|
|
10
|
+
import { rootAttrs } from './attrs.ts';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/** Base URL of the sprite. Matches VitopsEmdashOptions.wcBase. */
|
|
14
|
+
spriteBase?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Value {
|
|
18
|
+
items?: unknown;
|
|
19
|
+
variant?: string;
|
|
20
|
+
tone?: string;
|
|
21
|
+
color?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const value = resolveBlockValue<Value>(Astro.props);
|
|
25
|
+
const { spriteBase = '/vitops' } = Astro.props as Props;
|
|
26
|
+
const kind = text(value.variant) === 'tag' ? 'tag' : 'badge';
|
|
27
|
+
const items = rows<Record<string, unknown>>(value.items)
|
|
28
|
+
.map((row) => {
|
|
29
|
+
const own = text(row.tone) || text(row.color);
|
|
30
|
+
const tone = own ? resolveTone(row.tone, row.color) : resolveTone(value.tone, value.color);
|
|
31
|
+
return {
|
|
32
|
+
label: text(row.label),
|
|
33
|
+
icon: /^[a-z0-9-]+$/.test(text(row.icon)) ? text(row.icon) : '',
|
|
34
|
+
href: safeHref(row.href),
|
|
35
|
+
tone,
|
|
36
|
+
};
|
|
37
|
+
})
|
|
38
|
+
.filter((item) => item.label);
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
items.length > 0 && (
|
|
43
|
+
<ul {...rootAttrs(value, ['cluster'])} role="list">
|
|
44
|
+
{items.map((item) => {
|
|
45
|
+
const Tag = item.href ? 'a' : 'span';
|
|
46
|
+
return (
|
|
47
|
+
<li>
|
|
48
|
+
<Tag
|
|
49
|
+
class:list={[kind, item.tone.style && item.tone.className, item.href && 'link']}
|
|
50
|
+
href={item.href || undefined}
|
|
51
|
+
style={item.tone.style || undefined}
|
|
52
|
+
>
|
|
53
|
+
{item.icon && (
|
|
54
|
+
<span class="icon" aria-hidden="true">
|
|
55
|
+
<svg>
|
|
56
|
+
<use href={`${spriteBase}/icons.svg#icon-${item.icon}`} />
|
|
57
|
+
</svg>
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
{item.label}
|
|
61
|
+
</Tag>
|
|
62
|
+
</li>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</ul>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -10,10 +10,11 @@ interface Value {
|
|
|
10
10
|
const value = resolveBlockValue<Value>(Astro.props);
|
|
11
11
|
const { message, tone = 'info', dismissible = true } = value;
|
|
12
12
|
const bannerClass = `banner bg-${tone}${dismissible ? ' banner--dismissible' : ''}`;
|
|
13
|
+
import { rootAttrs } from './attrs.ts';
|
|
13
14
|
---
|
|
14
15
|
|
|
15
16
|
{message && dismissible && (
|
|
16
|
-
<wc-dismissable>
|
|
17
|
+
<wc-dismissable {...rootAttrs(value)}>
|
|
17
18
|
<div class={bannerClass}>
|
|
18
19
|
<div class="banner__content">{message}</div>
|
|
19
20
|
<button class="banner__close" data-dismiss aria-label="Dismiss">✕</button>
|
|
@@ -21,7 +22,7 @@ const bannerClass = `banner bg-${tone}${dismissible ? ' banner--dismissible' : '
|
|
|
21
22
|
</wc-dismissable>
|
|
22
23
|
)}
|
|
23
24
|
{message && !dismissible && (
|
|
24
|
-
<div
|
|
25
|
+
<div {...rootAttrs(value, [bannerClass])}>
|
|
25
26
|
<div class="banner__content">{message}</div>
|
|
26
27
|
</div>
|
|
27
28
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* A call-to-action band on the framework's `.callout` pattern. Not `
|
|
3
|
+
* A call-to-action band on the framework's `.callout` pattern. Not `vitops_banner`: that is
|
|
4
4
|
* a dismissible announcement, this is content that stays. The heading is a real heading at
|
|
5
5
|
* the editor's `level`; colour is the shared tone convention, tinted by default and filled
|
|
6
6
|
* with the role's solid when `solid` is on.
|
|
@@ -33,18 +33,13 @@ const secondaryHref = safeHref(value.secondaryHref);
|
|
|
33
33
|
const hasActions = Boolean((primaryLabel && primaryHref) || (secondaryLabel && secondaryHref));
|
|
34
34
|
const Heading = headingTag(value.level, 2);
|
|
35
35
|
const tone = resolveTone(value.tone, value.color);
|
|
36
|
+
import { rootAttrs } from './attrs.ts';
|
|
36
37
|
---
|
|
37
38
|
|
|
38
39
|
{
|
|
39
40
|
(heading || body) && (
|
|
40
41
|
<div
|
|
41
|
-
|
|
42
|
-
'callout',
|
|
43
|
-
tone.className,
|
|
44
|
-
value.solid && 'tone--solid',
|
|
45
|
-
value.align === 'center' && 'callout--center',
|
|
46
|
-
]}
|
|
47
|
-
style={joinStyle(tone.style)}
|
|
42
|
+
{...rootAttrs(value, ['callout', tone.className, value.solid && 'tone--solid', value.align === 'center' && 'callout--center'], joinStyle(tone.style))}
|
|
48
43
|
>
|
|
49
44
|
<div class="callout__body">
|
|
50
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 } from './tone.ts';
|
|
22
|
+
import { joinStyle, resolveTone, rowTone } from './tone.ts';
|
|
23
23
|
import { headingTag, lines, resolveBlockValue, rows, safeHref, text } from './value.ts';
|
|
24
24
|
|
|
25
25
|
interface Value {
|
|
@@ -43,6 +43,7 @@ const cards = rows<Record<string, unknown>>(value.cards)
|
|
|
43
43
|
checklist: lines(row.checklist),
|
|
44
44
|
ctaLabel: text(row.ctaLabel),
|
|
45
45
|
ctaHref: safeHref(row.ctaHref),
|
|
46
|
+
style: rowTone(row),
|
|
46
47
|
}))
|
|
47
48
|
.filter((card) => card.title || card.body || card.image);
|
|
48
49
|
const columns = Math.min(4, Math.max(1, Math.round(Number(value.columns) || 3)));
|
|
@@ -50,18 +51,19 @@ const panel = value.variant === 'panel';
|
|
|
50
51
|
const Heading = headingTag(value.level, 3);
|
|
51
52
|
const tone = resolveTone(value.tone, value.color);
|
|
52
53
|
const Grid = panel ? Subgrid : Cards;
|
|
54
|
+
import { rootAttrs } from './attrs.ts';
|
|
53
55
|
---
|
|
54
56
|
|
|
55
57
|
{
|
|
56
58
|
cards.length > 0 && (
|
|
57
|
-
<Grid
|
|
59
|
+
<Grid {...rootAttrs(value, [], `--subgrid-cols: ${columns}`)}>
|
|
58
60
|
{cards.map((card) => (
|
|
59
61
|
<li
|
|
60
62
|
class:list={['card', 'subgrid-card', panel && tone.className]}
|
|
61
|
-
style={panel ? joinStyle(tone.style) :
|
|
63
|
+
style={panel ? joinStyle(tone.style, card.style) : card.style}
|
|
62
64
|
>
|
|
63
65
|
{card.image && <img src={card.image} alt={card.imageAlt} loading="lazy" />}
|
|
64
|
-
{card.title && <Heading class="font-heading">{card.title}</Heading>}
|
|
66
|
+
{card.title && <Heading class="card__title font-heading">{card.title}</Heading>}
|
|
65
67
|
{card.body && (
|
|
66
68
|
<p>
|
|
67
69
|
<RichText text={card.body} />
|
|
@@ -39,6 +39,7 @@ const slides = fromRepeater.length > 0 ? fromRepeater : fromLegacy;
|
|
|
39
39
|
// Two blocks with the same label would derive the same id; `_key` is unique per block.
|
|
40
40
|
const id = value._key ? `carousel-${value._key.replace(/[^a-z0-9_-]/gi, '')}` : undefined;
|
|
41
41
|
const autoplay = Number(value.autoplay) || 0;
|
|
42
|
+
import { rootAttrs } from './attrs.ts';
|
|
42
43
|
---
|
|
43
44
|
|
|
44
45
|
{
|
|
@@ -50,6 +51,7 @@ const autoplay = Number(value.autoplay) || 0;
|
|
|
50
51
|
aspect={value.aspect || '16 / 9'}
|
|
51
52
|
slides={slides}
|
|
52
53
|
id={id}
|
|
54
|
+
{...rootAttrs(value)}
|
|
53
55
|
/>
|
|
54
56
|
)
|
|
55
57
|
}
|
|
@@ -9,14 +9,15 @@ interface Value {
|
|
|
9
9
|
|
|
10
10
|
const value = resolveBlockValue<Value>(Astro.props);
|
|
11
11
|
const { text, label = 'Copy', display = 'code-block' } = value;
|
|
12
|
+
import { rootAttrs } from './attrs.ts';
|
|
12
13
|
---
|
|
13
14
|
|
|
14
15
|
{text && display === 'code-block' && (
|
|
15
|
-
<div
|
|
16
|
+
<div {...rootAttrs(value, ['copy'])}>
|
|
16
17
|
<pre><code>{text}</code></pre>
|
|
17
18
|
<wc-copy value={text} label={label}>{label}</wc-copy>
|
|
18
19
|
</div>
|
|
19
20
|
)}
|
|
20
21
|
{text && display === 'inline' && (
|
|
21
|
-
<wc-copy value={text} label={label}><code>{text}</code></wc-copy>
|
|
22
|
+
<wc-copy {...rootAttrs(value)} value={text} label={label}><code>{text}</code></wc-copy>
|
|
22
23
|
)}
|
|
@@ -9,10 +9,11 @@ interface Value {
|
|
|
9
9
|
|
|
10
10
|
const value = resolveBlockValue<Value>(Astro.props);
|
|
11
11
|
const { summary, body, open = false } = value;
|
|
12
|
+
import { rootAttrs } from './attrs.ts';
|
|
12
13
|
---
|
|
13
14
|
|
|
14
15
|
{summary && (
|
|
15
|
-
<details open={open}>
|
|
16
|
+
<details open={open} {...rootAttrs(value)}>
|
|
16
17
|
<summary>{summary}</summary>
|
|
17
18
|
<p>{body}</p>
|
|
18
19
|
</details>
|
|
@@ -33,11 +33,12 @@ const embed = resolveEmbed(value.provider, value.url);
|
|
|
33
33
|
const category = CATEGORIES.has(text(value.category)) ? text(value.category) : 'marketing';
|
|
34
34
|
// A ratio lands in a style attribute: digits, slash and spaces only.
|
|
35
35
|
const ratio = /^\d+(\.\d+)?\s*\/\s*\d+(\.\d+)?$/.test(text(value.ratio)) ? text(value.ratio) : null;
|
|
36
|
+
import { rootAttrs } from './attrs.ts';
|
|
36
37
|
---
|
|
37
38
|
|
|
38
39
|
{
|
|
39
40
|
title && embed && (
|
|
40
|
-
<div
|
|
41
|
+
<div {...rootAttrs(value, ['embed'], ratio ? `--embed-ratio: ${ratio}` : undefined)}>
|
|
41
42
|
<iframe
|
|
42
43
|
class="embed__frame"
|
|
43
44
|
title={title}
|
|
@@ -25,6 +25,7 @@ const level = Number(headingTag(value.level, 3).slice(1)) as 2 | 3 | 4 | 5 | 6;
|
|
|
25
25
|
const breakpoint = /^\d+(\.\d+)?(rem|em|px|ch)$/.test(text(value.breakpoint))
|
|
26
26
|
? text(value.breakpoint)
|
|
27
27
|
: undefined;
|
|
28
|
+
import { rootAttrs } from './attrs.ts';
|
|
28
29
|
---
|
|
29
30
|
|
|
30
|
-
{entries.length > 0 && <Entries entries={entries} level={level} breakpoint={breakpoint} />}
|
|
31
|
+
{entries.length > 0 && <Entries entries={entries} level={level} breakpoint={breakpoint} {...rootAttrs(value)} />}
|
|
@@ -64,11 +64,12 @@ const tone = resolveTone(value.tone, value.color);
|
|
|
64
64
|
const successTone = resolveTone('success', '');
|
|
65
65
|
const errorTone = resolveTone('danger', '');
|
|
66
66
|
const hasForm = fields.length > 0;
|
|
67
|
+
import { rootAttrs } from './attrs.ts';
|
|
67
68
|
---
|
|
68
69
|
|
|
69
70
|
{
|
|
70
71
|
hasForm && (
|
|
71
|
-
<div
|
|
72
|
+
<div {...rootAttrs(value, ['form-block', 'rhythm', tone.className], joinStyle(tone.style))}>
|
|
72
73
|
{heading && <Heading class="font-heading">{heading}</Heading>}
|
|
73
74
|
{intro && <p>{intro}</p>}
|
|
74
75
|
|
|
@@ -122,6 +123,24 @@ const hasForm = fields.length > 0;
|
|
|
122
123
|
</p>
|
|
123
124
|
)}
|
|
124
125
|
</div>
|
|
126
|
+
) : f.type === 'checkboxes' ? (
|
|
127
|
+
<fieldset class="form-group" aria-describedby={describedby}>
|
|
128
|
+
<legend class="form-group__label">{f.label}</legend>
|
|
129
|
+
{f.options.map((o) => (
|
|
130
|
+
<label class="custom-check">
|
|
131
|
+
<input type="checkbox" name={f.name} value={o} />
|
|
132
|
+
<span class="custom-check__box">
|
|
133
|
+
<span class="custom-check__indicator custom-check__indicator--check" />
|
|
134
|
+
</span>
|
|
135
|
+
<span>{o}</span>
|
|
136
|
+
</label>
|
|
137
|
+
))}
|
|
138
|
+
{f.help && (
|
|
139
|
+
<p id={`${f.id}-help`} class="form-group__helper">
|
|
140
|
+
{f.help}
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
143
|
+
</fieldset>
|
|
125
144
|
) : (
|
|
126
145
|
<div class="form-group">
|
|
127
146
|
<label class="form-group__label" for={f.id}>
|
|
@@ -25,6 +25,7 @@ const images = rows<{ src?: unknown; alt?: unknown; caption?: unknown }>(value.i
|
|
|
25
25
|
return src ? [{ src, alt: text(row.alt), ...(caption ? { caption } : {}) }] : [];
|
|
26
26
|
},
|
|
27
27
|
);
|
|
28
|
+
import { rootAttrs } from './attrs.ts';
|
|
28
29
|
---
|
|
29
30
|
|
|
30
31
|
{
|
|
@@ -33,6 +34,7 @@ const images = rows<{ src?: unknown; alt?: unknown; caption?: unknown }>(value.i
|
|
|
33
34
|
label={text(value.label) || 'Gallery'}
|
|
34
35
|
images={images}
|
|
35
36
|
id={id}
|
|
37
|
+
{...rootAttrs(value)}
|
|
36
38
|
aspect={text(value.aspect) || undefined}
|
|
37
39
|
loop={Boolean(value.loop)}
|
|
38
40
|
/>
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* Flat fields, because a block cannot contain other blocks.
|
|
9
9
|
*/
|
|
10
10
|
import RichText from './RichText.astro';
|
|
11
|
-
import { joinStyle, resolveTone } from './tone.ts';
|
|
11
|
+
import { joinStyle, resolveTone, rowTone } from './tone.ts';
|
|
12
12
|
import {
|
|
13
13
|
accentRuns,
|
|
14
|
-
|
|
14
|
+
outlineTag,
|
|
15
15
|
resolveBlockValue,
|
|
16
16
|
rows,
|
|
17
17
|
safeHref,
|
|
@@ -39,7 +39,9 @@ const value = resolveBlockValue<Value>(Astro.props);
|
|
|
39
39
|
const eyebrow = text(value.eyebrow);
|
|
40
40
|
const heading = text(value.heading);
|
|
41
41
|
const headingRuns = accentRuns(heading);
|
|
42
|
-
const Heading =
|
|
42
|
+
const Heading = outlineTag(value.level, 1);
|
|
43
|
+
// A hero titled `h1` puts its option cards one level down, at `h2`.
|
|
44
|
+
const CardHeading = Heading === 'h1' ? 'h2' : 'h3';
|
|
43
45
|
const body = text(value.body);
|
|
44
46
|
const primaryLabel = text(value.primaryLabel);
|
|
45
47
|
const primaryHref = safeHref(value.primaryHref);
|
|
@@ -55,13 +57,18 @@ const cards = rows<Record<string, unknown>>(value.cards)
|
|
|
55
57
|
body: text(row.body),
|
|
56
58
|
href: safeHref(row.href),
|
|
57
59
|
linkLabel: text(row.linkLabel) || 'Learn more',
|
|
60
|
+
eyebrow: text(row.eyebrow),
|
|
61
|
+
ctaLabel: text(row.ctaLabel),
|
|
62
|
+
ctaHref: safeHref(row.ctaHref),
|
|
63
|
+
style: rowTone(row),
|
|
58
64
|
}))
|
|
59
65
|
.filter((card) => card.title || card.body);
|
|
66
|
+
import { rootAttrs } from './attrs.ts';
|
|
60
67
|
---
|
|
61
68
|
|
|
62
69
|
{
|
|
63
70
|
(heading || body) && (
|
|
64
|
-
<section
|
|
71
|
+
<section {...rootAttrs(value, [tone.style && tone.className], joinStyle(tone.style))}>
|
|
65
72
|
<div class={split ? 'split split-1-2' : 'centered'}>
|
|
66
73
|
<div class="rhythm">
|
|
67
74
|
{eyebrow && <p class="font-eyebrow">{eyebrow}</p>}
|
|
@@ -93,18 +100,27 @@ const cards = rows<Record<string, unknown>>(value.cards)
|
|
|
93
100
|
{cards.length > 0 && (
|
|
94
101
|
<ul class="subgrid centered" role="list" style={`--subgrid-cols: ${Math.min(4, cards.length)}`}>
|
|
95
102
|
{cards.map((card) => (
|
|
96
|
-
<li class=
|
|
97
|
-
{card.
|
|
103
|
+
<li class:list={['card', 'subgrid-card', card.style && 'tone']} style={card.style}>
|
|
104
|
+
{card.eyebrow && <p class="font-eyebrow">{card.eyebrow}</p>}
|
|
105
|
+
{card.title && <CardHeading class="font-heading">{card.title}</CardHeading>}
|
|
98
106
|
{card.body && (
|
|
99
107
|
<p>
|
|
100
108
|
<RichText text={card.body} />
|
|
101
109
|
</p>
|
|
102
110
|
)}
|
|
103
|
-
{card.
|
|
104
|
-
<
|
|
105
|
-
{card.
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
{card.ctaLabel && card.ctaHref ? (
|
|
112
|
+
<p class="subgrid-card__footer">
|
|
113
|
+
<a class="cta" href={card.ctaHref}>
|
|
114
|
+
{card.ctaLabel}
|
|
115
|
+
</a>
|
|
116
|
+
</p>
|
|
117
|
+
) : (
|
|
118
|
+
card.href && (
|
|
119
|
+
<a class="link stretched-link" href={card.href}>
|
|
120
|
+
{card.linkLabel}
|
|
121
|
+
{card.title && <span class="sr-only">: {card.title}</span>}
|
|
122
|
+
</a>
|
|
123
|
+
)
|
|
108
124
|
)}
|
|
109
125
|
</li>
|
|
110
126
|
))}
|
|
@@ -15,10 +15,11 @@ const { beforeAlt = '', beforeLabel = 'Before', afterAlt = '', afterLabel = 'Aft
|
|
|
15
15
|
// A `javascript:`/`data:` src is dropped, so the block renders nothing rather than that image.
|
|
16
16
|
const before = safeHref(value.before);
|
|
17
17
|
const after = safeHref(value.after);
|
|
18
|
+
import { rootAttrs } from './attrs.ts';
|
|
18
19
|
---
|
|
19
20
|
|
|
20
21
|
{before && after && (
|
|
21
|
-
<wc-image-compare before-label={beforeLabel} after-label={afterLabel}>
|
|
22
|
+
<wc-image-compare {...rootAttrs(value)} before-label={beforeLabel} after-label={afterLabel}>
|
|
22
23
|
<img slot="before" src={before} alt={beforeAlt} loading="lazy" />
|
|
23
24
|
<img slot="after" src={after} alt={afterAlt} loading="lazy" />
|
|
24
25
|
</wc-image-compare>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* A still row of partner/client marks on the framework's `.logos` pattern. `
|
|
3
|
+
* A still row of partner/client marks on the framework's `.logos` pattern. `vitops_marquee`
|
|
4
4
|
* is the scrolling version; this one holds still. A list, so the count is announced, and
|
|
5
5
|
* each mark is an image with the editor's name as its alt text — a logo with no alt is a
|
|
6
|
-
* link or a fact the page silently drops.
|
|
6
|
+
* link or a fact the page silently drops. An entry with a name and no image renders the name as
|
|
7
|
+
* text in the same list (`.logos__name`), for partners who have no wordmark.
|
|
7
8
|
*/
|
|
8
9
|
import { resolveBlockValue, rows, safeHref, text } from './value.ts';
|
|
9
10
|
|
|
@@ -20,26 +21,28 @@ const value = resolveBlockValue<Value>(Astro.props);
|
|
|
20
21
|
const label = text(value.label) || 'Partners';
|
|
21
22
|
const logos = rows<Record<string, unknown>>(value.logos)
|
|
22
23
|
.map((row) => ({ image: safeHref(row.image), alt: text(row.alt), href: safeHref(row.href) }))
|
|
23
|
-
.filter((logo) => logo.image);
|
|
24
|
+
.filter((logo) => logo.image || logo.alt);
|
|
24
25
|
const height = HEIGHTS[text(value.size)];
|
|
26
|
+
import { rootAttrs } from './attrs.ts';
|
|
25
27
|
---
|
|
26
28
|
|
|
27
29
|
{
|
|
28
30
|
logos.length > 0 && (
|
|
29
31
|
<ul
|
|
30
|
-
|
|
32
|
+
{...rootAttrs(value, ['logos', value.grayscale && 'logos--grayscale'], height ? `--logos-height: ${height}` : undefined)}
|
|
31
33
|
role="list"
|
|
32
34
|
aria-label={label}
|
|
33
|
-
style={height ? `--logos-height: ${height}` : undefined}
|
|
34
35
|
>
|
|
35
36
|
{logos.map((logo) => (
|
|
36
37
|
<li class="logos__item">
|
|
37
38
|
{logo.href ? (
|
|
38
39
|
<a href={logo.href}>
|
|
39
|
-
<img src={logo.image} alt={logo.alt} loading="lazy" />
|
|
40
|
+
{logo.image ? <img src={logo.image} alt={logo.alt} loading="lazy" /> : <span class="logos__name font-heading">{logo.alt}</span>}
|
|
40
41
|
</a>
|
|
41
|
-
) : (
|
|
42
|
+
) : logo.image ? (
|
|
42
43
|
<img src={logo.image} alt={logo.alt} loading="lazy" />
|
|
44
|
+
) : (
|
|
45
|
+
<span class="logos__name font-heading">{logo.alt}</span>
|
|
43
46
|
)}
|
|
44
47
|
</li>
|
|
45
48
|
))}
|
|
@@ -21,11 +21,12 @@ const items = rows<Record<string, unknown>>(value.items)
|
|
|
21
21
|
alt: text(row.alt),
|
|
22
22
|
}))
|
|
23
23
|
.filter((item) => item.text || item.image);
|
|
24
|
+
import { rootAttrs } from './attrs.ts';
|
|
24
25
|
---
|
|
25
26
|
|
|
26
27
|
{
|
|
27
28
|
items.length > 0 && (
|
|
28
|
-
<Marquee pauseOnHover={Boolean(value.pauseOnHover)} reverse={Boolean(value.reverse)}>
|
|
29
|
+
<Marquee pauseOnHover={Boolean(value.pauseOnHover)} reverse={Boolean(value.reverse)} {...rootAttrs(value)}>
|
|
29
30
|
{items.map((item) =>
|
|
30
31
|
item.image ? (
|
|
31
32
|
<img src={item.image} alt={item.alt} loading="lazy" />
|
|
@@ -60,13 +60,13 @@ const tone = resolveTone(value.tone, value.color);
|
|
|
60
60
|
const toned = Boolean(value.tone || value.color);
|
|
61
61
|
const trimmed = trimDepth(items, depth);
|
|
62
62
|
const current = Astro.url.pathname;
|
|
63
|
+
import { rootAttrs } from './attrs.ts';
|
|
63
64
|
---
|
|
64
65
|
|
|
65
66
|
{
|
|
66
67
|
items.length > 0 && (
|
|
67
68
|
<div
|
|
68
|
-
|
|
69
|
-
style={toned ? joinStyle(tone.style) : undefined}
|
|
69
|
+
{...rootAttrs(value, ['navblock', toned && tone.className], toned ? joinStyle(tone.style) : undefined)}
|
|
70
70
|
>
|
|
71
71
|
{heading && <Heading class="font-heading">{heading}</Heading>}
|
|
72
72
|
{variant === 'sitenav' ? (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* One `<ul class="navlist">` level of `
|
|
3
|
+
* One `<ul class="navlist">` level of `vitops_nav`, recursing for sub-links. Internal — import
|
|
4
4
|
* `Nav.astro`. Plain nested lists, no disclosure widgets: a page-body menu is read, not
|
|
5
5
|
* collapsed, so there is no state to manage and no runtime JS. The `.navlist` pattern is
|
|
6
6
|
* `patterns/sitenav.css`'s (all formats).
|