@getvitops/emdash 2.0.0-alpha.0 → 2.0.0-alpha.1
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 +33 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +152 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/astro/Nothing.astro +3 -0
- package/src/astro/WidgetArea.astro +91 -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 +2 -7
- package/src/astro/blocks/Cards.astro +2 -1
- 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 +2 -1
- package/src/astro/blocks/Gallery.astro +2 -0
- package/src/astro/blocks/Hero.astro +2 -1
- package/src/astro/blocks/ImageCompare.astro +2 -1
- package/src/astro/blocks/Logos.astro +2 -2
- package/src/astro/blocks/Marquee.astro +2 -1
- package/src/astro/blocks/Nav.astro +2 -2
- package/src/astro/blocks/Pricing.astro +2 -2
- package/src/astro/blocks/Reviews.astro +2 -2
- package/src/astro/blocks/Section.astro +8 -10
- package/src/astro/blocks/Stats.astro +2 -2
- package/src/astro/blocks/Steps.astro +2 -2
- package/src/astro/blocks/WidgetArea.astro +21 -0
- package/src/astro/blocks/attrs.test.ts +56 -0
- package/src/astro/blocks/attrs.ts +104 -0
- package/src/astro/blocks/render.container.test.ts +40 -0
- package/src/astro/index.ts +7 -0
|
@@ -0,0 +1,91 @@
|
|
|
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 { PortableText, WidgetArea as StockWidgetArea } from 'emdash/ui';
|
|
19
|
+
import Nothing from './Nothing.astro';
|
|
20
|
+
import { headingTag, safeHref, text } from './blocks/value.ts';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
/** The area's `name` as set under Widgets. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Level of each widget's title; default `3`. */
|
|
26
|
+
level?: number | string;
|
|
27
|
+
class?: string;
|
|
28
|
+
id?: string;
|
|
29
|
+
style?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { name, level, class: className, id, style } = Astro.props;
|
|
33
|
+
const area = name ? await getWidgetArea(name) : null;
|
|
34
|
+
const widgets = area?.widgets ?? [];
|
|
35
|
+
const delegated = widgets.some((w) => w.type === 'component');
|
|
36
|
+
const Heading = headingTag(level, 3);
|
|
37
|
+
const menus = Object.fromEntries(
|
|
38
|
+
await Promise.all(
|
|
39
|
+
widgets
|
|
40
|
+
.filter((w) => w.type === 'menu' && w.menuName)
|
|
41
|
+
.map(async (w) => [w.id, await getMenu(w.menuName!)] as const),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
const inner = { type: { 'vitops.widgetArea': Nothing } };
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
widgets.length > 0 && (
|
|
49
|
+
<div id={id} class:list={['widget-area', className]} style={style} data-widget-area={area?.name}>
|
|
50
|
+
{delegated ? (
|
|
51
|
+
<StockWidgetArea name={name} />
|
|
52
|
+
) : (
|
|
53
|
+
<>
|
|
54
|
+
{widgets.map((widget) => (
|
|
55
|
+
<section class="widget" data-widget-type={widget.type}>
|
|
56
|
+
{text(widget.title) && widget.type !== 'menu' && (
|
|
57
|
+
<Heading class="widget__title font-heading">{widget.title}</Heading>
|
|
58
|
+
)}
|
|
59
|
+
{widget.type === 'content' && widget.content && (
|
|
60
|
+
<div class="widget__content rhythm">
|
|
61
|
+
<PortableText value={widget.content} components={inner} />
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
{widget.type === 'menu' && menus[widget.id] && (
|
|
65
|
+
<>
|
|
66
|
+
{text(widget.title) && <Heading class="widget__title font-heading">{widget.title}</Heading>}
|
|
67
|
+
<nav class="widget__menu" aria-label={text(widget.title) || area?.label}>
|
|
68
|
+
<ul role="list">
|
|
69
|
+
{menus[widget.id]!.items.map((item) => {
|
|
70
|
+
const href = safeHref(item.url);
|
|
71
|
+
return (
|
|
72
|
+
href && (
|
|
73
|
+
<li>
|
|
74
|
+
<a class="link" href={href} title={item.titleAttr || undefined}>
|
|
75
|
+
{item.label}
|
|
76
|
+
</a>
|
|
77
|
+
</li>
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</ul>
|
|
82
|
+
</nav>
|
|
83
|
+
</>
|
|
84
|
+
)}
|
|
85
|
+
</section>
|
|
86
|
+
))}
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -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
|
)}
|
|
@@ -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>}
|
|
@@ -50,11 +50,12 @@ const panel = value.variant === 'panel';
|
|
|
50
50
|
const Heading = headingTag(value.level, 3);
|
|
51
51
|
const tone = resolveTone(value.tone, value.color);
|
|
52
52
|
const Grid = panel ? Subgrid : Cards;
|
|
53
|
+
import { rootAttrs } from './attrs.ts';
|
|
53
54
|
---
|
|
54
55
|
|
|
55
56
|
{
|
|
56
57
|
cards.length > 0 && (
|
|
57
|
-
<Grid
|
|
58
|
+
<Grid {...rootAttrs(value, [], `--subgrid-cols: ${columns}`)}>
|
|
58
59
|
{cards.map((card) => (
|
|
59
60
|
<li
|
|
60
61
|
class:list={['card', 'subgrid-card', panel && tone.className]}
|
|
@@ -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
|
|
|
@@ -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
|
/>
|
|
@@ -57,11 +57,12 @@ const cards = rows<Record<string, unknown>>(value.cards)
|
|
|
57
57
|
linkLabel: text(row.linkLabel) || 'Learn more',
|
|
58
58
|
}))
|
|
59
59
|
.filter((card) => card.title || card.body);
|
|
60
|
+
import { rootAttrs } from './attrs.ts';
|
|
60
61
|
---
|
|
61
62
|
|
|
62
63
|
{
|
|
63
64
|
(heading || body) && (
|
|
64
|
-
<section
|
|
65
|
+
<section {...rootAttrs(value, [tone.style && tone.className], joinStyle(tone.style))}>
|
|
65
66
|
<div class={split ? 'split split-1-2' : 'centered'}>
|
|
66
67
|
<div class="rhythm">
|
|
67
68
|
{eyebrow && <p class="font-eyebrow">{eyebrow}</p>}
|
|
@@ -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>
|
|
@@ -22,15 +22,15 @@ const logos = rows<Record<string, unknown>>(value.logos)
|
|
|
22
22
|
.map((row) => ({ image: safeHref(row.image), alt: text(row.alt), href: safeHref(row.href) }))
|
|
23
23
|
.filter((logo) => logo.image);
|
|
24
24
|
const height = HEIGHTS[text(value.size)];
|
|
25
|
+
import { rootAttrs } from './attrs.ts';
|
|
25
26
|
---
|
|
26
27
|
|
|
27
28
|
{
|
|
28
29
|
logos.length > 0 && (
|
|
29
30
|
<ul
|
|
30
|
-
|
|
31
|
+
{...rootAttrs(value, ['logos', value.grayscale && 'logos--grayscale'], height ? `--logos-height: ${height}` : undefined)}
|
|
31
32
|
role="list"
|
|
32
33
|
aria-label={label}
|
|
33
|
-
style={height ? `--logos-height: ${height}` : undefined}
|
|
34
34
|
>
|
|
35
35
|
{logos.map((logo) => (
|
|
36
36
|
<li class="logos__item">
|
|
@@ -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' ? (
|
|
@@ -28,14 +28,14 @@ const plans = rows<Record<string, unknown>>(value.plans)
|
|
|
28
28
|
.filter((plan) => plan.name || plan.price);
|
|
29
29
|
const Heading = headingTag(value.level, 3);
|
|
30
30
|
const columns = Math.min(4, Math.max(1, plans.length));
|
|
31
|
+
import { rootAttrs } from './attrs.ts';
|
|
31
32
|
---
|
|
32
33
|
|
|
33
34
|
{
|
|
34
35
|
plans.length > 0 && (
|
|
35
36
|
<ul
|
|
36
|
-
|
|
37
|
+
{...rootAttrs(value, ['pricing-grid'], `--pricing-columns: ${columns}; --pricing-card-rows: 4`)}
|
|
37
38
|
role="list"
|
|
38
|
-
style={`--pricing-columns: ${columns}; --pricing-card-rows: 4`}
|
|
39
39
|
>
|
|
40
40
|
{plans.map((plan) => (
|
|
41
41
|
<li class:list={['pricing-card', plan.featured && 'pricing-card--featured']}>
|
|
@@ -75,13 +75,13 @@ const avatar = Boolean(value.avatars);
|
|
|
75
75
|
const tone = resolveTone(value.tone, value.color);
|
|
76
76
|
const toned = Boolean(value.tone || value.color);
|
|
77
77
|
const dev = import.meta.env.DEV;
|
|
78
|
+
import { rootAttrs } from './attrs.ts';
|
|
78
79
|
---
|
|
79
80
|
|
|
80
81
|
{
|
|
81
82
|
reviews.length > 0 ? (
|
|
82
83
|
<div
|
|
83
|
-
|
|
84
|
-
style={toned ? joinStyle(tone.style) : undefined}
|
|
84
|
+
{...rootAttrs(value, ['reviews', toned && tone.className], toned ? joinStyle(tone.style) : undefined)}
|
|
85
85
|
>
|
|
86
86
|
{layout === 'carousel' ? (
|
|
87
87
|
<Carousel label={label}>
|
|
@@ -30,6 +30,7 @@ interface Value {
|
|
|
30
30
|
tone?: string;
|
|
31
31
|
color?: string;
|
|
32
32
|
solid?: boolean;
|
|
33
|
+
theme?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const WIDTHS: Record<string, string> = {
|
|
@@ -44,24 +45,21 @@ const heading = text(value.heading);
|
|
|
44
45
|
const sub = text(value.sub);
|
|
45
46
|
const body = Array.isArray(value.body) ? value.body : [];
|
|
46
47
|
const Heading = headingTag(value.level, 2);
|
|
47
|
-
|
|
48
|
+
// `theme` is one pick for the common case: `<role>` tints, `<role>:solid` fills. It wins over the
|
|
49
|
+
// older `tone`/`color`/`solid` fields, which stay for content that already uses them.
|
|
50
|
+
const [themeRole, themeFill] = text(value.theme).replace(/^none$/, '').split(':');
|
|
51
|
+
const tone = themeRole ? resolveTone(themeRole) : resolveTone(value.tone, value.color);
|
|
52
|
+
const solid = themeRole ? themeFill === 'solid' : Boolean(value.solid);
|
|
48
53
|
const layout = text(value.layout);
|
|
49
54
|
const width = WIDTHS[text(value.width)];
|
|
50
55
|
const hasHead = Boolean(kicker || heading || sub);
|
|
56
|
+
import { rootAttrs } from './attrs.ts';
|
|
51
57
|
---
|
|
52
58
|
|
|
53
59
|
{
|
|
54
60
|
(hasHead || body.length > 0) && (
|
|
55
61
|
<section
|
|
56
|
-
|
|
57
|
-
'section',
|
|
58
|
-
'region',
|
|
59
|
-
tone.style && tone.className,
|
|
60
|
-
tone.style && value.solid && 'tone--solid',
|
|
61
|
-
layout === 'split' && 'section--split',
|
|
62
|
-
layout === 'center' && 'section--center',
|
|
63
|
-
]}
|
|
64
|
-
style={joinStyle(tone.style)}
|
|
62
|
+
{...rootAttrs(value, ['section', 'region', tone.style && tone.className, tone.style && solid && 'tone--solid', layout === 'split' && 'section--split', layout === 'center' && 'section--center'], joinStyle(tone.style))}
|
|
65
63
|
>
|
|
66
64
|
<div class="centered">
|
|
67
65
|
<div class:list={['section__inner', width]}>
|
|
@@ -35,14 +35,14 @@ const stats = rows<Record<string, unknown>>(value.stats)
|
|
|
35
35
|
const tiles = value.variant === 'tiles';
|
|
36
36
|
const animate = value.animate !== false;
|
|
37
37
|
const tone = resolveTone(value.tone, value.color);
|
|
38
|
+
import { rootAttrs } from './attrs.ts';
|
|
38
39
|
---
|
|
39
40
|
|
|
40
41
|
{
|
|
41
42
|
stats.length > 0 && (
|
|
42
43
|
<ul
|
|
43
|
-
|
|
44
|
+
{...rootAttrs(value, ['stats', tone.className, tiles && 'stats--tiles'], joinStyle(tone.style))}
|
|
44
45
|
role="list"
|
|
45
|
-
style={joinStyle(tone.style)}
|
|
46
46
|
>
|
|
47
47
|
{stats.map((stat) => (
|
|
48
48
|
<li class="stats__item" data-state={stat.state}>
|
|
@@ -23,13 +23,13 @@ const steps = rows<Record<string, unknown>>(value.steps)
|
|
|
23
23
|
.filter((step) => step.title || step.text);
|
|
24
24
|
const Heading = headingTag(value.level, 3);
|
|
25
25
|
const tone = resolveTone(value.tone, value.color);
|
|
26
|
+
import { rootAttrs } from './attrs.ts';
|
|
26
27
|
---
|
|
27
28
|
|
|
28
29
|
{
|
|
29
30
|
steps.length > 0 && (
|
|
30
31
|
<ol
|
|
31
|
-
|
|
32
|
-
style={joinStyle(tone.style)}
|
|
32
|
+
{...rootAttrs(value, ['steps', tone.className, value.layout === 'row' && 'steps--row'], joinStyle(tone.style))}
|
|
33
33
|
>
|
|
34
34
|
{steps.map((step) => (
|
|
35
35
|
<li class="steps__item">
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* `vitops.widgetArea` — a synced region inside page content: the area is defined once under
|
|
4
|
+
* Widgets in the admin and every page that places this block shows the same thing. (An EmDash
|
|
5
|
+
* Section, by contrast, is copied in.) See `../WidgetArea.astro` for what it renders and why a
|
|
6
|
+
* widget area cannot hold another. The markup (`.widget-area`) lives there.
|
|
7
|
+
*/
|
|
8
|
+
import WidgetArea from '../WidgetArea.astro';
|
|
9
|
+
import { resolveBlockValue, text } from './value.ts';
|
|
10
|
+
import { rootAttrs } from './attrs.ts';
|
|
11
|
+
|
|
12
|
+
interface Value {
|
|
13
|
+
area?: string;
|
|
14
|
+
level?: string | number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const value = resolveBlockValue<Value>(Astro.props);
|
|
18
|
+
const name = text(value.area);
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
{name && <WidgetArea name={name} level={value.level} {...rootAttrs(value, ['widget-area--in-page'])} />}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vite-plus/test';
|
|
2
|
+
import { anchorId, blockAttrs, classTokens, customProperties } from './attrs.ts';
|
|
3
|
+
|
|
4
|
+
describe('anchorId', () => {
|
|
5
|
+
it('slugifies and accepts a clean id', () => {
|
|
6
|
+
expect(anchorId('Contact Us')).toBe('contact-us');
|
|
7
|
+
expect(anchorId('contact')).toBe('contact');
|
|
8
|
+
});
|
|
9
|
+
it('rejects what cannot be an id', () => {
|
|
10
|
+
expect(anchorId('')).toBeUndefined();
|
|
11
|
+
expect(anchorId('9lives')).toBeUndefined();
|
|
12
|
+
expect(anchorId(42)).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('classTokens', () => {
|
|
17
|
+
it('keeps valid tokens, drops the rest', () => {
|
|
18
|
+
expect(classTokens('mt-4 md:flex "x" <b>')).toBe('mt-4 md:flex');
|
|
19
|
+
});
|
|
20
|
+
it('is undefined when nothing survives', () => {
|
|
21
|
+
expect(classTokens('"><script>')).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('customProperties', () => {
|
|
26
|
+
it('keeps custom properties', () => {
|
|
27
|
+
expect(customProperties('--p-card: 3rem; --gap: 1rem')).toBe('--p-card: 3rem; --gap: 1rem');
|
|
28
|
+
});
|
|
29
|
+
it('drops ordinary properties and fetching values', () => {
|
|
30
|
+
expect(
|
|
31
|
+
customProperties('background: url(x); --a: url(https://x/y); display: none'),
|
|
32
|
+
).toBeUndefined();
|
|
33
|
+
expect(customProperties('--a: 1px; position: fixed')).toBe('--a: 1px');
|
|
34
|
+
});
|
|
35
|
+
it('rejects values that try to leave the declaration', () => {
|
|
36
|
+
expect(customProperties('--a: red}</style><script>')).toBeUndefined();
|
|
37
|
+
expect(customProperties('--a: \\75rl(x)')).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('blockAttrs', () => {
|
|
42
|
+
it('is empty for a block that sets nothing', () => {
|
|
43
|
+
expect(blockAttrs({})).toEqual({});
|
|
44
|
+
expect(blockAttrs(undefined)).toEqual({});
|
|
45
|
+
});
|
|
46
|
+
it('reads the block-type spelling too', () => {
|
|
47
|
+
expect(blockAttrs({ class_name: 'mt-4' })).toEqual({ class: 'mt-4' });
|
|
48
|
+
});
|
|
49
|
+
it('collects the three fields', () => {
|
|
50
|
+
expect(blockAttrs({ anchor: 'services', className: 'mt-4', style: '--p-card: 2rem' })).toEqual({
|
|
51
|
+
id: 'services',
|
|
52
|
+
class: 'mt-4',
|
|
53
|
+
style: '--p-card: 2rem',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|