@contentrain/astro-kit 0.1.1 → 0.3.0
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/README.md +30 -3
- package/catalog.json +251 -4
- package/components/_shared/Button.astro +1 -1
- package/components/_shared/KitImage.astro +32 -5
- package/components/_shared/Section.astro +1 -1
- package/components/breadcrumb/Breadcrumb.astro +1 -1
- package/components/card-grid/CardGrid.astro +4 -4
- package/components/contact-form/ContactForm.astro +1 -1
- package/components/cta/Cta.astro +2 -2
- package/components/embed/Embed.astro +104 -0
- package/components/embed/embed.ts +106 -0
- package/components/embed/fixtures.json +34 -0
- package/components/embed/meta.json +32 -0
- package/components/faq/Faq.astro +5 -3
- package/components/faq/fixtures.json +4 -0
- package/components/faq/meta.json +1 -1
- package/components/footer/Footer.astro +4 -4
- package/components/footer/meta.json +107 -14
- package/components/gallery/Gallery.astro +3 -3
- package/components/header/Header.astro +1 -1
- package/components/hero/Hero.astro +14 -7
- package/components/hero/fixtures.json +21 -0
- package/components/hero/meta.json +8 -0
- package/components/nav/Nav.astro +4 -2
- package/components/nav/meta.json +86 -13
- package/components/newsletter/Newsletter.astro +110 -0
- package/components/newsletter/fixtures.json +6 -0
- package/components/newsletter/meta.json +105 -0
- package/components/newsletter/newsletter.ts +73 -0
- package/components/pagination/Pagination.astro +1 -1
- package/components/post-card/PostCard.astro +2 -2
- package/components/slider/Slider.astro +3 -3
- package/components/tabs/Tabs.astro +2 -2
- package/components/testimonial/Testimonial.astro +1 -1
- package/dist/index.d.mts +11 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +19 -7
- package/dist/index.mjs.map +1 -1
- package/mapping/classic.json +91 -1
- package/mapping/divi.json +22 -1
- package/mapping/elementor.json +46 -1
- package/mapping/gutenberg.json +70 -4
- package/package.json +2 -2
package/components/nav/meta.json
CHANGED
|
@@ -4,27 +4,100 @@
|
|
|
4
4
|
"description": "Site navigation: an inline list with hover/focus submenus on wide screens and a toggle on narrow ones that opens a native popover drawer or a <details> dropdown. No JavaScript.",
|
|
5
5
|
"category": "chrome",
|
|
6
6
|
"uses": [],
|
|
7
|
-
"shared": [
|
|
7
|
+
"shared": [
|
|
8
|
+
"types.ts"
|
|
9
|
+
],
|
|
8
10
|
"variants": {
|
|
9
|
-
"mobile": {
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
"mobile": {
|
|
12
|
+
"options": [
|
|
13
|
+
"popover",
|
|
14
|
+
"details"
|
|
15
|
+
],
|
|
16
|
+
"default": "popover",
|
|
17
|
+
"description": "Narrow-screen menu: a side drawer (Popover API) or a dropdown under the toggle."
|
|
18
|
+
},
|
|
19
|
+
"align": {
|
|
20
|
+
"options": [
|
|
21
|
+
"start",
|
|
22
|
+
"end",
|
|
23
|
+
"center"
|
|
24
|
+
],
|
|
25
|
+
"default": "end",
|
|
26
|
+
"description": "Horizontal alignment of the inline list."
|
|
27
|
+
},
|
|
28
|
+
"density": {
|
|
29
|
+
"options": [
|
|
30
|
+
"compact",
|
|
31
|
+
"comfortable"
|
|
32
|
+
],
|
|
33
|
+
"default": "compact",
|
|
34
|
+
"description": "Gap and text size of the inline list."
|
|
35
|
+
}
|
|
12
36
|
},
|
|
13
37
|
"props": {
|
|
14
|
-
"items": {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"required": true,
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"fields": {
|
|
44
|
+
"label": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"required": true
|
|
47
|
+
},
|
|
48
|
+
"href": {
|
|
49
|
+
"type": "url",
|
|
50
|
+
"required": true
|
|
51
|
+
},
|
|
52
|
+
"newTab": {
|
|
53
|
+
"type": "boolean"
|
|
54
|
+
},
|
|
55
|
+
"children": {
|
|
56
|
+
"type": "array"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"description": "Menu entries; one level of children is shown."
|
|
61
|
+
},
|
|
62
|
+
"label": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Accessible name of the navigation landmark (ui-strings nav.label)."
|
|
65
|
+
},
|
|
66
|
+
"menuLabel": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Text of the narrow-screen toggle (ui-strings nav.menu)."
|
|
69
|
+
},
|
|
70
|
+
"closeLabel": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Text of the drawer's close button (ui-strings nav.close)."
|
|
73
|
+
},
|
|
74
|
+
"id": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "Unique id when a page renders two navigations."
|
|
77
|
+
},
|
|
78
|
+
"part": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "What the navigation is, for checks that find a page's parts (data-cr-part), e.g. \"nav-header\"."
|
|
81
|
+
}
|
|
19
82
|
},
|
|
20
83
|
"slots": {},
|
|
21
84
|
"js": "none",
|
|
22
85
|
"dependencies": {},
|
|
23
86
|
"sources": {
|
|
24
|
-
"gutenberg": [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
87
|
+
"gutenberg": [
|
|
88
|
+
"core/navigation",
|
|
89
|
+
"core/page-list"
|
|
90
|
+
],
|
|
91
|
+
"elementor": [
|
|
92
|
+
"elementor/nav-menu"
|
|
93
|
+
],
|
|
94
|
+
"divi": [
|
|
95
|
+
"divi/et_pb_menu",
|
|
96
|
+
"divi/et_pb_fullwidth_menu"
|
|
97
|
+
],
|
|
98
|
+
"classic": [
|
|
99
|
+
"classic/nav-menu"
|
|
100
|
+
]
|
|
28
101
|
},
|
|
29
102
|
"a11y": "A <nav> landmark with a label; the current page carries aria-current. Submenus open on :focus-within as well as hover. The drawer is a native popover: Escape and light dismiss close it and focus returns to the toggle."
|
|
30
103
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A newsletter sign-up that posts straight to the list provider's hosted form
|
|
3
|
+
// endpoint (Mailchimp, Kit, MailerLite, Brevo, Buttondown): a plain HTML form,
|
|
4
|
+
// no script, no server of ours. The provider shows its own confirmation page,
|
|
5
|
+
// in a new tab by default so the reader keeps the site. The action must be
|
|
6
|
+
// the named provider's hosted endpoint (exact host and path) — a WordPress-side
|
|
7
|
+
// handler (MC4WP, Divi signup, Jetpack) is dead on a static site — or, for
|
|
8
|
+
// `other`, an https address off the site's own origin. Otherwise the section
|
|
9
|
+
// renders nothing; the migration plan lists such a sign-up for review instead
|
|
10
|
+
// of placing it.
|
|
11
|
+
import { tv } from 'tailwind-variants'
|
|
12
|
+
import Section from '../_shared/Section.astro'
|
|
13
|
+
import { newsletterForm } from './newsletter'
|
|
14
|
+
|
|
15
|
+
const newsletter = tv({
|
|
16
|
+
slots: {
|
|
17
|
+
box: 'flex flex-col gap-6',
|
|
18
|
+
copy: 'flex flex-col gap-3',
|
|
19
|
+
heading: 'text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-3xl',
|
|
20
|
+
intro: 'max-w-xl text-pretty opacity-85',
|
|
21
|
+
form: 'flex flex-col gap-3',
|
|
22
|
+
field: 'flex min-w-0 flex-col gap-1.5',
|
|
23
|
+
label: 'text-sm font-semibold',
|
|
24
|
+
input: 'min-h-11 w-full rounded-[var(--radius-control,var(--radius-md))] border border-line bg-surface px-3 py-2 text-base text-ink placeholder:text-ink-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
25
|
+
submit: 'inline-flex min-h-11 items-center justify-center rounded-[var(--radius-control,var(--radius-md))] px-5 py-2.5 font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 bg-accent text-accent-ink hover:bg-ink hover:text-surface',
|
|
26
|
+
note: 'text-sm opacity-75',
|
|
27
|
+
},
|
|
28
|
+
variants: {
|
|
29
|
+
layout: {
|
|
30
|
+
inline: { box: 'md:flex-row md:items-end md:justify-between md:gap-12', copy: 'md:max-w-md', form: 'md:flex-row md:items-end md:shrink-0' },
|
|
31
|
+
stacked: { box: 'mx-auto max-w-xl items-stretch text-center', intro: 'mx-auto', form: 'text-start' },
|
|
32
|
+
},
|
|
33
|
+
// accent/accent-ink is only ~5:1 — dimming text on it drops body copy under 4.5:1 (WCAG 1.4.3), so accent keeps it at full strength.
|
|
34
|
+
tone: {
|
|
35
|
+
default: {},
|
|
36
|
+
muted: {},
|
|
37
|
+
inverse: { submit: 'hover:bg-surface hover:text-ink' },
|
|
38
|
+
accent: { intro: 'opacity-100', note: 'opacity-100', submit: 'bg-ink text-surface hover:bg-surface hover:text-ink' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: { layout: 'inline', tone: 'muted' },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
interface Props {
|
|
45
|
+
/** The provider's form endpoint (Mailchimp `https://….list-manage.com/subscribe/post?u=…&id=…`, Kit `https://app.kit.com/forms/<id>/subscriptions`, …). */
|
|
46
|
+
action: string
|
|
47
|
+
provider?: 'mailchimp' | 'kit' | 'mailerlite' | 'brevo' | 'buttondown' | 'other' | undefined
|
|
48
|
+
/** Field name for the address when the provider is `other` or names it differently. */
|
|
49
|
+
emailName?: string | undefined
|
|
50
|
+
/** Ask for a first name too (when the provider takes one). */
|
|
51
|
+
askName?: boolean | undefined
|
|
52
|
+
heading?: string | undefined
|
|
53
|
+
intro?: string | undefined
|
|
54
|
+
/** Small print under the form (privacy, frequency). */
|
|
55
|
+
note?: string | undefined
|
|
56
|
+
/** ui-strings newsletter.email */
|
|
57
|
+
emailLabel?: string | undefined
|
|
58
|
+
/** ui-strings newsletter.name */
|
|
59
|
+
nameLabel?: string | undefined
|
|
60
|
+
/** ui-strings newsletter.submit */
|
|
61
|
+
submitLabel?: string | undefined
|
|
62
|
+
/** Open the provider's confirmation page in a new tab. */
|
|
63
|
+
newTab?: boolean | undefined
|
|
64
|
+
layout?: 'inline' | 'stacked' | undefined
|
|
65
|
+
tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
|
|
66
|
+
id?: string | undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const {
|
|
70
|
+
action, provider = 'mailchimp', emailName, askName = false, heading, intro, note,
|
|
71
|
+
emailLabel = 'Email address', nameLabel = 'First name', submitLabel = 'Subscribe', newTab = true,
|
|
72
|
+
layout, tone = 'muted', id = 'newsletter',
|
|
73
|
+
} = Astro.props
|
|
74
|
+
const form = newsletterForm(provider, action, { email: emailName }, Astro.site)
|
|
75
|
+
const s = newsletter({ layout, tone })
|
|
76
|
+
const headingId = heading ? `${id}-title` : undefined
|
|
77
|
+
// `rel` on a form is HTML (the provider's page gets no opener and no referrer) but not in Astro's form attribute types.
|
|
78
|
+
const formRel: Record<string, string> = newTab ? { rel: 'noopener noreferrer' } : {}
|
|
79
|
+
---
|
|
80
|
+
{form && (
|
|
81
|
+
<Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
|
|
82
|
+
<div class={s.box()}>
|
|
83
|
+
{(heading || intro) && (
|
|
84
|
+
<div class={s.copy()}>
|
|
85
|
+
{heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
|
|
86
|
+
{intro && <p class={s.intro()}>{intro}</p>}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
<div class="flex flex-col gap-2">
|
|
90
|
+
<form method="post" action={form.action} target={newTab ? '_blank' : undefined} {...formRel} class={s.form()} aria-labelledby={headingId} aria-label={headingId ? undefined : submitLabel}>
|
|
91
|
+
{askName && form.name && (
|
|
92
|
+
<div class={s.field()}>
|
|
93
|
+
<label for={`${id}-name`} class={s.label()}>{nameLabel}</label>
|
|
94
|
+
<input id={`${id}-name`} name={form.name} type="text" autocomplete="given-name" class={s.input()} />
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
<div class={s.field({ class: 'md:min-w-72' })}>
|
|
98
|
+
<label for={`${id}-email`} class={s.label()}>{emailLabel}</label>
|
|
99
|
+
<input id={`${id}-email`} name={form.email} type="email" required autocomplete="email" inputmode="email" class={s.input()} />
|
|
100
|
+
</div>
|
|
101
|
+
{form.hidden.map(field => (
|
|
102
|
+
<div aria-hidden="true" class="absolute -left-[5000px]"><input type="text" name={field.name} value={field.value} tabindex="-1" autocomplete="off" /></div>
|
|
103
|
+
))}
|
|
104
|
+
<button type="submit" class={s.submit()}>{submitLabel}</button>
|
|
105
|
+
</form>
|
|
106
|
+
{note && <p class={s.note()}>{note}</p>}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</Section>
|
|
110
|
+
)}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"name": "mailchimp-inline-muted", "props": {"action": "https://example.us21.list-manage.com/subscribe/post?u=0a1b2c3d&id=9f8e7d", "heading": "Letters from the field", "intro": "One long read a month, nothing else.", "note": "Unsubscribe any time. We never share your address.", "id": "newsletter-mailchimp"}},
|
|
3
|
+
{"name": "kit-stacked-name", "props": {"action": "https://app.kit.com/forms/1234567/subscriptions", "provider": "kit", "askName": true, "layout": "stacked", "tone": "default", "heading": "Join the journal", "submitLabel": "Sign me up", "id": "newsletter-kit"}},
|
|
4
|
+
{"name": "brevo-inverse", "props": {"action": "https://a1b2c3d4.sibforms.com/serve/MUIFAB", "provider": "brevo", "tone": "inverse", "heading": "News from the studio", "id": "newsletter-brevo"}},
|
|
5
|
+
{"name": "other-accent", "props": {"action": "https://buttondown.com/api/emails/embed-subscribe/journal", "provider": "buttondown", "tone": "accent", "id": "newsletter-accent"}}
|
|
6
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "newsletter",
|
|
3
|
+
"name": "Newsletter sign-up",
|
|
4
|
+
"description": "A newsletter sign-up that posts straight to the list provider's hosted form endpoint (Mailchimp, Kit, MailerLite, Brevo, Buttondown): a plain HTML form with no script and no server of ours. The provider shows its own confirmation page. The action must be the named provider's endpoint (exact host and path; `other`: https off the site's origin), otherwise nothing renders. WordPress-side sign-up plugins (MC4WP, Divi signup, Jetpack) post through the site and are not mapped here: the plan lists them for review.",
|
|
5
|
+
"category": "section",
|
|
6
|
+
"uses": [],
|
|
7
|
+
"shared": [
|
|
8
|
+
"Section.astro"
|
|
9
|
+
],
|
|
10
|
+
"variants": {
|
|
11
|
+
"layout": {
|
|
12
|
+
"options": [
|
|
13
|
+
"inline",
|
|
14
|
+
"stacked"
|
|
15
|
+
],
|
|
16
|
+
"default": "inline",
|
|
17
|
+
"description": "inline: copy beside the form on wide screens. stacked: one centred column."
|
|
18
|
+
},
|
|
19
|
+
"tone": {
|
|
20
|
+
"options": [
|
|
21
|
+
"default",
|
|
22
|
+
"muted",
|
|
23
|
+
"inverse",
|
|
24
|
+
"accent"
|
|
25
|
+
],
|
|
26
|
+
"default": "muted",
|
|
27
|
+
"description": "Section background."
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"props": {
|
|
31
|
+
"action": {
|
|
32
|
+
"type": "url",
|
|
33
|
+
"required": true,
|
|
34
|
+
"description": "The provider's hosted form endpoint (Mailchimp *.list-manage.com/subscribe/post, Kit app.kit.com/forms/<n>/subscriptions, MailerLite assets.mailerlite.com/jsonp/<n>/forms/<n>/subscribe, Brevo *.sibforms.com/serve/, Buttondown buttondown.com/api/emails/embed-subscribe/). At migration the engine reads it from the source form element (`dom:form@action`) into the section's content, where it stays editable in Studio; it never passes through the fact pack or a model."
|
|
35
|
+
},
|
|
36
|
+
"provider": {
|
|
37
|
+
"type": "select",
|
|
38
|
+
"options": [
|
|
39
|
+
"mailchimp",
|
|
40
|
+
"kit",
|
|
41
|
+
"mailerlite",
|
|
42
|
+
"brevo",
|
|
43
|
+
"buttondown",
|
|
44
|
+
"other"
|
|
45
|
+
],
|
|
46
|
+
"default": "mailchimp",
|
|
47
|
+
"description": "Names the fields the endpoint expects (Mailchimp EMAIL/FNAME + its bot-trap field, Kit email_address, MailerLite fields[email], Brevo EMAIL, Buttondown email)."
|
|
48
|
+
},
|
|
49
|
+
"emailName": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Field name for the address when the provider is other or names it differently."
|
|
52
|
+
},
|
|
53
|
+
"askName": {
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"default": false,
|
|
56
|
+
"description": "Ask for a first name too, when the provider takes one (the source form had a name field)."
|
|
57
|
+
},
|
|
58
|
+
"heading": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Section heading."
|
|
61
|
+
},
|
|
62
|
+
"intro": {
|
|
63
|
+
"type": "text",
|
|
64
|
+
"description": "A line or two about the newsletter."
|
|
65
|
+
},
|
|
66
|
+
"note": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Small print under the form (privacy, frequency)."
|
|
69
|
+
},
|
|
70
|
+
"emailLabel": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "ui-strings newsletter.email."
|
|
73
|
+
},
|
|
74
|
+
"nameLabel": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "ui-strings newsletter.name."
|
|
77
|
+
},
|
|
78
|
+
"submitLabel": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "ui-strings newsletter.submit."
|
|
81
|
+
},
|
|
82
|
+
"newTab": {
|
|
83
|
+
"type": "boolean",
|
|
84
|
+
"default": true,
|
|
85
|
+
"description": "Open the provider's confirmation page in a new tab (the reader keeps the site)."
|
|
86
|
+
},
|
|
87
|
+
"id": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "Section id, unique per page (two sign-ups on one page need two ids): the heading gets `<id>-title`, the inputs `<id>-email`/`<id>-name`."
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"slots": {},
|
|
93
|
+
"js": "none",
|
|
94
|
+
"dependencies": {},
|
|
95
|
+
"sources": {
|
|
96
|
+
"classic": [
|
|
97
|
+
"classic/mailchimp",
|
|
98
|
+
"classic/kit",
|
|
99
|
+
"classic/mailerlite",
|
|
100
|
+
"classic/brevo",
|
|
101
|
+
"classic/buttondown"
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
"a11y": "Every input has a visible label and the address field is type=email, required, with autocomplete=email, so the browser validates it and announces errors without script. The form is named by the section heading (or the submit label). Mailchimp's bot-trap field is aria-hidden, off screen and out of the tab order."
|
|
105
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// How a newsletter provider's hosted form endpoint wants its fields named, and
|
|
2
|
+
// whether an action really is that endpoint. The form posts straight to the
|
|
3
|
+
// provider — no script, no server of our own — so the names have to be the
|
|
4
|
+
// provider's, and the address has to be the provider's: a WordPress-side
|
|
5
|
+
// handler (MC4WP posting to its own page, Divi's admin-ajax, Jetpack) is dead
|
|
6
|
+
// on a static site, and a POST to it would lose every sign-up without a sound.
|
|
7
|
+
|
|
8
|
+
export type NewsletterProvider = 'mailchimp' | 'kit' | 'mailerlite' | 'brevo' | 'buttondown' | 'other'
|
|
9
|
+
|
|
10
|
+
export interface NewsletterForm {
|
|
11
|
+
action: string
|
|
12
|
+
email: string
|
|
13
|
+
/** First-name field, when the provider takes one. */
|
|
14
|
+
name: string | null
|
|
15
|
+
/** Hidden fields the provider expects (Mailchimp's bot trap). */
|
|
16
|
+
hidden: Array<{ name: string, value: string }>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Endpoint {
|
|
20
|
+
email: string
|
|
21
|
+
name: string | null
|
|
22
|
+
/** The provider's hosted form endpoint: exact host (or a subdomain of an exact parent) and path. */
|
|
23
|
+
accepts: (url: URL) => boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A subdomain of `parent`, one or two labels deep (Mailchimp: `<account>.us21.list-manage.com`). */
|
|
27
|
+
const sub = (host: string, parent: string): boolean => host.endsWith(`.${parent}`) && /^[a-z0-9-]+(?:\.[a-z0-9-]+)?$/.test(host.slice(0, -parent.length - 1))
|
|
28
|
+
|
|
29
|
+
export const NEWSLETTER_ENDPOINTS: Record<Exclude<NewsletterProvider, 'other'>, Endpoint> = {
|
|
30
|
+
mailchimp: { email: 'EMAIL', name: 'FNAME', accepts: u => sub(u.hostname, 'list-manage.com') && u.pathname.startsWith('/subscribe/post') },
|
|
31
|
+
kit: { email: 'email_address', name: 'fields[first_name]', accepts: u => ['app.kit.com', 'app.convertkit.com'].includes(u.hostname) && /^\/forms\/\d+\/subscriptions\/?$/.test(u.pathname) },
|
|
32
|
+
mailerlite: { email: 'fields[email]', name: 'fields[name]', accepts: u => u.hostname === 'assets.mailerlite.com' && /^\/jsonp\/\d+\/forms\/\d+\/subscribe\/?$/.test(u.pathname) },
|
|
33
|
+
brevo: { email: 'EMAIL', name: 'FIRSTNAME', accepts: u => sub(u.hostname, 'sibforms.com') && u.pathname.startsWith('/serve/') },
|
|
34
|
+
buttondown: { email: 'email', name: null, accepts: u => u.hostname === 'buttondown.com' && u.pathname.startsWith('/api/emails/embed-subscribe/') },
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const WP_HANDLER = /^\/(?:wp-admin|wp-json|wp-login\.php|xmlrpc\.php)(?:\/|$)|admin-ajax\.php|admin-post\.php/
|
|
38
|
+
|
|
39
|
+
/** The provider whose hosted endpoint an action is, or null. What the migration uses to choose `provider`. */
|
|
40
|
+
export function newsletterProviderOf(action: string): Exclude<NewsletterProvider, 'other'> | null {
|
|
41
|
+
let url: URL
|
|
42
|
+
try { url = new URL(action.trim()) }
|
|
43
|
+
catch { return null }
|
|
44
|
+
if (url.protocol !== 'https:') return null
|
|
45
|
+
return (Object.entries(NEWSLETTER_ENDPOINTS) as Array<[Exclude<NewsletterProvider, 'other'>, Endpoint]>).find(([, e]) => e.accepts(url))?.[0] ?? null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The form for a provider and its action; `null` when the action cannot deliver: not https, not the named
|
|
50
|
+
* provider's hosted endpoint, or (for `other`) the site's own origin — a static site cannot receive a POST.
|
|
51
|
+
*/
|
|
52
|
+
export function newsletterForm(provider: NewsletterProvider, action: string, names: { email?: string | undefined, name?: string | undefined } = {}, siteOrigin?: string | URL | undefined): NewsletterForm | null {
|
|
53
|
+
let url: URL
|
|
54
|
+
try { url = new URL(action.trim()) }
|
|
55
|
+
catch { return null }
|
|
56
|
+
if (url.protocol !== 'https:') return null
|
|
57
|
+
if (provider !== 'other' && !NEWSLETTER_ENDPOINTS[provider].accepts(url)) return null
|
|
58
|
+
if (provider === 'other') {
|
|
59
|
+
const own = siteOrigin ? new URL(String(siteOrigin)).hostname.replace(/^www\./, '') : null
|
|
60
|
+
if (own && url.hostname.replace(/^www\./, '') === own) return null
|
|
61
|
+
// A WordPress handler on any host (the old site under another name, a staging copy) is just as dead.
|
|
62
|
+
if (WP_HANDLER.test(url.pathname) || url.searchParams.has('wc-ajax')) return null
|
|
63
|
+
}
|
|
64
|
+
const preset = provider === 'other' ? { email: 'email', name: null } : NEWSLETTER_ENDPOINTS[provider]
|
|
65
|
+
const hidden: NewsletterForm['hidden'] = []
|
|
66
|
+
// Mailchimp drops a submission whose `b_<u>_<id>` field is filled: the field a bot fills and a person never sees.
|
|
67
|
+
if (provider === 'mailchimp') {
|
|
68
|
+
const u = url.searchParams.get('u')
|
|
69
|
+
const id = url.searchParams.get('id')
|
|
70
|
+
if (u && id && /^\w+$/.test(u) && /^\w+$/.test(id)) hidden.push({ name: `b_${u}_${id}`, value: '' })
|
|
71
|
+
}
|
|
72
|
+
return { action: url.href, email: names.email || preset.email, name: names.name || preset.name, hidden }
|
|
73
|
+
}
|
|
@@ -50,7 +50,7 @@ const shown = [...new Set([1, current - 2, current - 1, current, current + 1, cu
|
|
|
50
50
|
.toSorted((a, b) => a - b)
|
|
51
51
|
---
|
|
52
52
|
{total > 1 && (
|
|
53
|
-
<nav aria-label={label} class={slots.root()}>
|
|
53
|
+
<nav aria-label={label} data-cr-part="pagination" class={slots.root()}>
|
|
54
54
|
{current > 1
|
|
55
55
|
? <a href={href(current - 1)} rel="prev" class:list={[slots.item(), slots.link()]}><span aria-hidden="true">←</span> <span class="max-sm:sr-only">{prevLabel}</span></a>
|
|
56
56
|
: style === 'simple' && <span />}
|
|
@@ -8,10 +8,10 @@ import type { ImageInput, LinkInput } from '../_shared/types'
|
|
|
8
8
|
const card = tv({
|
|
9
9
|
slots: {
|
|
10
10
|
root: 'group relative flex gap-4',
|
|
11
|
-
media: 'shrink-0 overflow-hidden rounded-[var(--radius-card)]',
|
|
11
|
+
media: 'shrink-0 overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
|
|
12
12
|
body: 'flex min-w-0 flex-col gap-2',
|
|
13
13
|
meta: 'flex flex-wrap items-center gap-x-2 text-sm text-ink-muted',
|
|
14
|
-
title: 'leading-snug font-
|
|
14
|
+
title: 'leading-snug [font-weight:var(--font-weight-heading,600)] text-balance text-ink',
|
|
15
15
|
excerpt: 'text-ink-muted',
|
|
16
16
|
},
|
|
17
17
|
variants: {
|
|
@@ -27,9 +27,9 @@ const slider = tv({
|
|
|
27
27
|
square: { image: 'aspect-square' },
|
|
28
28
|
},
|
|
29
29
|
caption: {
|
|
30
|
-
below: { image: 'rounded-[var(--radius-card)]', caption: 'pt-4' },
|
|
30
|
+
below: { image: 'rounded-[var(--radius-image,var(--radius-card))]', caption: 'pt-4' },
|
|
31
31
|
// Over the image on wide screens; under it on phones, where the image is too short to hold text.
|
|
32
|
-
overlay: { image: 'rounded-[var(--radius-card)] md:rounded-none', slide: 'md:rounded-[var(--radius-card)]', caption: 'pt-4 md:absolute md:inset-x-0 md:bottom-0 md:bg-gradient-to-t md:from-ink/80 md:to-transparent md:p-6 md:pt-16 md:text-surface' },
|
|
32
|
+
overlay: { image: 'rounded-[var(--radius-image,var(--radius-card))] md:rounded-none', slide: 'md:rounded-[var(--radius-image,var(--radius-card))]', caption: 'pt-4 md:absolute md:inset-x-0 md:bottom-0 md:bg-gradient-to-t md:from-ink/80 md:to-transparent md:p-6 md:pt-16 md:text-surface' },
|
|
33
33
|
},
|
|
34
34
|
},
|
|
35
35
|
defaultVariants: { perView: '1', aspect: 'wide', caption: 'overlay' },
|
|
@@ -72,7 +72,7 @@ const arrow = 'absolute top-1/2 z-10 hidden size-11 -translate-y-1/2 items-cente
|
|
|
72
72
|
<KitImage image={slide.image} sizes={sizes} priority={index === 0} class={slots.image()} />
|
|
73
73
|
{(slide.title || slide.text || slide.action) && (
|
|
74
74
|
<div class={slots.caption()}>
|
|
75
|
-
{slide.title && <p class="text-xl font-
|
|
75
|
+
{slide.title && <p class="text-xl [font-weight:var(--font-weight-heading,600)] text-balance">{slide.title}</p>}
|
|
76
76
|
{slide.text && <p class="opacity-85">{slide.text}</p>}
|
|
77
77
|
{slide.action && <Button action={slide.action} size="sm" class="mt-2" />}
|
|
78
78
|
</div>
|
|
@@ -14,12 +14,12 @@ export interface TabItem {
|
|
|
14
14
|
|
|
15
15
|
const tabs = tv({
|
|
16
16
|
slots: {
|
|
17
|
-
heading: 'mb-8 text-2xl font-
|
|
17
|
+
heading: 'mb-8 text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-3xl',
|
|
18
18
|
root: 'group/tabs flex flex-col gap-6',
|
|
19
19
|
list: 'hidden gap-1 group-data-[ready]/tabs:flex',
|
|
20
20
|
trigger: 'cursor-pointer rounded-md px-4 py-2 text-sm font-medium whitespace-nowrap opacity-75 transition hover:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent data-[selected]:opacity-100',
|
|
21
21
|
panel: 'prose-site prose max-w-none text-current',
|
|
22
|
-
title: 'mb-3 text-lg font-
|
|
22
|
+
title: 'mb-3 text-lg [font-weight:var(--font-weight-heading,600)] group-data-[ready]/tabs:sr-only',
|
|
23
23
|
},
|
|
24
24
|
variants: {
|
|
25
25
|
style: {
|
|
@@ -16,7 +16,7 @@ export interface TestimonialItem {
|
|
|
16
16
|
|
|
17
17
|
const testimonial = tv({
|
|
18
18
|
slots: {
|
|
19
|
-
heading: 'mb-10 text-2xl font-
|
|
19
|
+
heading: 'mb-10 text-2xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-3xl',
|
|
20
20
|
list: 'grid gap-6',
|
|
21
21
|
card: 'flex h-full flex-col gap-6',
|
|
22
22
|
quote: 'text-pretty',
|
package/dist/index.d.mts
CHANGED
|
@@ -14,8 +14,8 @@ declare const BUILDER_PREFIX: Record<KitBuilder, string>;
|
|
|
14
14
|
*/
|
|
15
15
|
declare function isBuilderElement(builder: KitBuilder, name: string): boolean;
|
|
16
16
|
type KitCategory = 'chrome' | 'section' | 'content' | 'primitive';
|
|
17
|
-
/** Client JavaScript a component ships: none, Embla (carousel), Zag.js (accessible widget) or the Studio runtime. */
|
|
18
|
-
type KitJs = 'none' | 'embla' | 'zag' | 'studio';
|
|
17
|
+
/** Client JavaScript a component ships: none, a few lines of dependency-free script (`vanilla`), Embla (carousel), Zag.js (accessible widget) or the Studio runtime. */
|
|
18
|
+
type KitJs = 'none' | 'vanilla' | 'embla' | 'zag' | 'studio';
|
|
19
19
|
type KitProp = FieldDef & {
|
|
20
20
|
description: string;
|
|
21
21
|
};
|
|
@@ -97,6 +97,11 @@ interface MappingTable {
|
|
|
97
97
|
version: string;
|
|
98
98
|
/** What an element no rule covers becomes when no decision is made: rich text. */
|
|
99
99
|
fallback: 'prose';
|
|
100
|
+
/**
|
|
101
|
+
* Attribute values the builder leaves out of its data while they are at their default, per element:
|
|
102
|
+
* Elementor saves no `video_type` for a YouTube video. `when.attr` and `attr:` read through them.
|
|
103
|
+
*/
|
|
104
|
+
defaults?: Record<string, Record<string, string | number | boolean>>;
|
|
100
105
|
rules: MappingRule[];
|
|
101
106
|
}
|
|
102
107
|
/** Problems with a mapping table against the catalog, one line each; empty when it is sound. */
|
|
@@ -111,8 +116,10 @@ interface MappingNode {
|
|
|
111
116
|
attrs?: Record<string, unknown>;
|
|
112
117
|
children?: MappingNode[];
|
|
113
118
|
}
|
|
119
|
+
/** An element's attributes with the builder's unsaved defaults filled in. */
|
|
120
|
+
declare function attrsOf(node: MappingNode, defaults?: MappingTable['defaults']): Record<string, unknown>;
|
|
114
121
|
/** Whether a rule's `match` (with an optional `:qualifier`) and `when.attr` fit an element. */
|
|
115
|
-
declare function ruleMatches(rule: MappingRule, node: MappingNode): boolean;
|
|
122
|
+
declare function ruleMatches(rule: MappingRule, node: MappingNode, defaults?: MappingTable['defaults']): boolean;
|
|
116
123
|
/**
|
|
117
124
|
* Apply a table to a tree: the outermost matching element owns its whole subtree, and nothing inside
|
|
118
125
|
* it is matched again — a header template part holding the site title and logo is one header, not three.
|
|
@@ -146,5 +153,5 @@ declare function loadMapping(builder: KitBuilder): Promise<MappingTable>;
|
|
|
146
153
|
/** The published catalog (`catalog.json`). */
|
|
147
154
|
declare function loadCatalog(): Promise<KitCatalog>;
|
|
148
155
|
//#endregion
|
|
149
|
-
export { BUILDER_PREFIX, CATALOG_FORMAT, CopyPlan, KIT_BUILDERS, KIT_COMPONENTS_DIR, KIT_DIR, KitBuilder, KitCatalog, KitCategory, KitComponent, KitComponentMeta, KitJs, KitProp, KitVariant, MAPPING_FORMAT, MAPPING_VALUE_PATTERN, MappingNode, MappingRule, MappingTable, claimMatches, componentsForSource, copyComponents, isBuilderElement, loadCatalog, loadMapping, planCopy, ruleMatches, rulesFor, unmappedSources, validateCatalog, validateMapping };
|
|
156
|
+
export { BUILDER_PREFIX, CATALOG_FORMAT, CopyPlan, KIT_BUILDERS, KIT_COMPONENTS_DIR, KIT_DIR, KitBuilder, KitCatalog, KitCategory, KitComponent, KitComponentMeta, KitJs, KitProp, KitVariant, MAPPING_FORMAT, MAPPING_VALUE_PATTERN, MappingNode, MappingRule, MappingTable, attrsOf, claimMatches, componentsForSource, copyComponents, isBuilderElement, loadCatalog, loadMapping, planCopy, ruleMatches, rulesFor, unmappedSources, validateCatalog, validateMapping };
|
|
150
157
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/catalog.ts","../src/mapping.ts","../src/copy.ts","../src/index.ts"],"mappings":";;;cAWa,cAAA;;KAGD,UAAA;AAAA,cACC,YAAA,WAAuB,UAAA;;cAGvB,cAAA,EAAgB,MAAA,CAAO,UAAA;;AAJpC;;;;iBAgBgB,gBAAA,CAAiB,OAAA,EAAS,UAAA,EAAY,IAAA;AAAA,KAK1C,WAAA;;KAGA,KAAA;AAAA,KAEA,OAAA,GAAU,QAAA;EAAa,WAAA;AAAA;AAAA,UAElB,UAAA;EACf,OAAA;EACA,OAAA;EACA,WAAA;AAAA;;UAIe,gBAAA;EACf,EAAA;EACA,IAAA;EACA,WAAA;EACA,QAAA,EAAU,WAAA;EAvBsD;EAyBhE,IAAA;EApBU;EAsBV,MAAA;EACA,QAAA,EAAU,MAAA,SAAe,UAAA;EACzB,KAAA,EAAO,MAAA,SAAe,OAAA;EAxBD;EA0BrB,KAAA,EAAO,MAAA;EACP,EAAA,EAAI,KAAA;;EAEJ,YAAA,EAAc,MAAA;EA1BC;EA4Bf,OAAA,EAAS,OAAA,CAAQ,MAAA,CAAO,UAAA;EA1BP;EA4BjB,IAAA;AAAA;;UAIe,YAAA,SAAqB,gBAAA;EA9BX;EAgCzB,KAAA;AAAA;AAAA,UAGe,UAAA;EACf,MAAA,SAAe,cAAA;EAjCf;EAmCA,MAAA;EAnCW;EAqCX,YAAA,EAAc,MAAA;EACd,UAAA,EAAY,YAAA;AAAA;;iBAIE,eAAA,CAAgB,OAAA,EAAS,UAAA;;iBA6BzB,mBAAA,CAAoB,OAAA,EAAS,UAAA,EAAY,OAAA,WAAkB,YAAA;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/catalog.ts","../src/mapping.ts","../src/copy.ts","../src/index.ts"],"mappings":";;;cAWa,cAAA;;KAGD,UAAA;AAAA,cACC,YAAA,WAAuB,UAAA;;cAGvB,cAAA,EAAgB,MAAA,CAAO,UAAA;;AAJpC;;;;iBAgBgB,gBAAA,CAAiB,OAAA,EAAS,UAAA,EAAY,IAAA;AAAA,KAK1C,WAAA;;KAGA,KAAA;AAAA,KAEA,OAAA,GAAU,QAAA;EAAa,WAAA;AAAA;AAAA,UAElB,UAAA;EACf,OAAA;EACA,OAAA;EACA,WAAA;AAAA;;UAIe,gBAAA;EACf,EAAA;EACA,IAAA;EACA,WAAA;EACA,QAAA,EAAU,WAAA;EAvBsD;EAyBhE,IAAA;EApBU;EAsBV,MAAA;EACA,QAAA,EAAU,MAAA,SAAe,UAAA;EACzB,KAAA,EAAO,MAAA,SAAe,OAAA;EAxBD;EA0BrB,KAAA,EAAO,MAAA;EACP,EAAA,EAAI,KAAA;;EAEJ,YAAA,EAAc,MAAA;EA1BC;EA4Bf,OAAA,EAAS,OAAA,CAAQ,MAAA,CAAO,UAAA;EA1BP;EA4BjB,IAAA;AAAA;;UAIe,YAAA,SAAqB,gBAAA;EA9BX;EAgCzB,KAAA;AAAA;AAAA,UAGe,UAAA;EACf,MAAA,SAAe,cAAA;EAjCf;EAmCA,MAAA;EAnCW;EAqCX,YAAA,EAAc,MAAA;EACd,UAAA,EAAY,YAAA;AAAA;;iBAIE,eAAA,CAAgB,OAAA,EAAS,UAAA;;iBA6BzB,mBAAA,CAAoB,OAAA,EAAS,UAAA,EAAY,OAAA,WAAkB,YAAA;;;cCxF9D,cAAA;AAAA,cAEA,qBAAA,EAAqB,MAAA;AAAA,UAEjB,WAAA;EDrBU;ECuBzB,KAAA;EDvByB;ECyBzB,IAAA;IAAS,IAAA,GAAO,MAAA;IAA2C,KAAA;EAAA;EDtBvC;ECwBpB,SAAA;EACA,OAAA,GAAU,MAAA;EDxBoF;EC0B9F,IAAA;ED1BkC;EC4BlC,KAAA;EACA,KAAA,GAAQ,MAAA;EDrBT;ECuBC,IAAA;ED5B2B;EC8B3B,IAAA;EDlBc;ECoBd,IAAA,GAAO,MAAA;AAAA;AAAA,UAGQ,YAAA;EACf,MAAA,SAAe,cAAA;EACf,OAAA,EAAS,UAAA;EDzB2C;EC2BpD,OAAA;ED3BgE;EC6BhE,QAAA;EDxBqB;;;;EC6BrB,QAAA,GAAW,MAAA,SAAe,MAAA;EAC1B,KAAA,EAAO,WAAA;AAAA;;iBAIO,eAAA,CAAgB,KAAA,EAAO,YAAA,EAAc,OAAA,EAAS,UAAA;AD7B9D;AAAA,iBC0EgB,eAAA,CAAgB,KAAA,EAAO,YAAA,EAAc,OAAA,EAAS,UAAA;;iBAO9C,QAAA,CAAS,KAAA,EAAO,YAAA,EAAc,OAAA,WAAkB,WAAA;;UAO/C,WAAA;EACf,IAAA;EACA,KAAA,GAAQ,MAAA;EACR,QAAA,GAAW,WAAA;AAAA;;iBAIG,OAAA,CAAQ,IAAA,EAAM,WAAA,EAAa,QAAA,GAAU,YAAA,eAAgC,MAAA;;iBAKrE,WAAA,CAAY,IAAA,EAAM,WAAA,EAAa,IAAA,EAAM,WAAA,EAAa,QAAA,GAAW,YAAA;AD3F7E;;;;AAAA,iBC0GgB,YAAA,CAAa,KAAA,EAAO,WAAA,IAAe,KAAA,EAAO,YAAA,EAAc,IAAA;EAAe,IAAA;EAAc,IAAA,EAAM,WAAA;EAAa,IAAA,EAAM,WAAA;AAAA;;;;cChJjH,OAAA;AAAA,UAEI,QAAA;EFFU;EEIzB,UAAA;EFJyB;EEMzB,KAAA,EAAO,MAAA;EFHG;EEKV,YAAA,EAAc,MAAA;AAAA;AAAA,iBAGA,QAAA,CAAS,OAAA,EAAS,UAAA,EAAY,GAAA,sBAAyB,QAAA;;iBAwBjD,cAAA,CAAe,IAAA,EAAM,QAAA,EAAU,OAAA,UAAiB,QAAA,WAAmB,OAAA;;;;cCjC5E,kBAAA;;iBAGS,WAAA,CAAY,OAAA,EAAS,UAAA,GAAa,OAAA,CAAQ,YAAA;;iBAK1C,WAAA,CAAA,GAAe,OAAA,CAAQ,UAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -95,13 +95,14 @@ async function copyComponents(plan, kitRoot, siteRoot) {
|
|
|
95
95
|
//#endregion
|
|
96
96
|
//#region src/mapping.ts
|
|
97
97
|
const MAPPING_FORMAT = "astro-kit-mapping@1";
|
|
98
|
-
const MAPPING_VALUE_PATTERN = /^(?:field:[\w-]+|media:[\w-]+|ref:[\w-]+\.[\w-]+|href:(?:self|[\w-]+)|term:[\w-]+|ui:[\w.-]+|const:(?:true|false|-?\d+(?:\.\d+)?|[a-z][a-z0-9_-]{0,31})|site:[\w-]+|menu:(?:primary|footer)|page:(?:base|current|total|breadcrumb)|attr:[\w.-]+|(?:dom|img|link|html):[^@]*(?:@[\w-]+)?)$/;
|
|
98
|
+
const MAPPING_VALUE_PATTERN = /^(?:field:[\w-]+|media:[\w-]+|ref:[\w-]+\.[\w-]+|href:(?:self|[\w-]+)|term:[\w-]+|ui:[\w.-]+|const:(?:true|false|-?\d+(?:\.\d+)?|[a-z][a-z0-9_-]{0,31})|site:[\w-]+|menu:(?:primary|footer)|page:(?:base|current|total|breadcrumb)|attr:[\w.-]+|class:[\w*-]+=(?:[a-z][\w-]*)?\|(?:[a-z][\w-]*)?|(?:dom|img|link|html):[^@]*(?:@[\w-]+)?)$/;
|
|
99
99
|
/** Problems with a mapping table against the catalog, one line each; empty when it is sound. */
|
|
100
100
|
function validateMapping(table, catalog) {
|
|
101
101
|
const problems = [];
|
|
102
102
|
if (table.format !== "astro-kit-mapping@1") problems.push(`format is not ${MAPPING_FORMAT}`);
|
|
103
103
|
if (!KIT_BUILDERS.includes(table.builder)) problems.push(`unknown builder ${table.builder}`);
|
|
104
104
|
if (!/^\d+$/.test(table.version)) problems.push("version is not an integer string");
|
|
105
|
+
for (const element of Object.keys(table.defaults ?? {})) if (!isBuilderElement(table.builder, element)) problems.push(`defaults ${element}: not a ${table.builder} element`);
|
|
105
106
|
const components = new Map(catalog.components.map((c) => [c.id, c]));
|
|
106
107
|
const seen = /* @__PURE__ */ new Set();
|
|
107
108
|
for (const rule of table.rules) {
|
|
@@ -115,8 +116,12 @@ function validateMapping(table, catalog) {
|
|
|
115
116
|
problems.push(`${at}: component ${rule.component} is not in the catalog`);
|
|
116
117
|
continue;
|
|
117
118
|
}
|
|
118
|
-
for (const [axis, value] of Object.entries(rule.variant ?? {}))
|
|
119
|
-
|
|
119
|
+
for (const [axis, value] of Object.entries(rule.variant ?? {})) {
|
|
120
|
+
const choice = /^class:[\w*-]+=([a-z][\w-]*)?\|([a-z][\w-]*)?$/.exec(value);
|
|
121
|
+
const options = choice ? [choice[1], choice[2]].filter((option) => option !== void 0) : [value];
|
|
122
|
+
if (!c.variants[axis]) problems.push(`${at}: ${c.id} has no variant axis ${axis}`);
|
|
123
|
+
else for (const missing of options.filter((option) => !c.variants[axis].options.includes(option))) problems.push(`${at}: ${c.id}.${axis} has no option ${missing}`);
|
|
124
|
+
}
|
|
120
125
|
for (const [prop, value] of Object.entries({
|
|
121
126
|
...rule.props,
|
|
122
127
|
...rule.into ? {} : rule.item
|
|
@@ -150,11 +155,18 @@ function unmappedSources(table, catalog) {
|
|
|
150
155
|
function rulesFor(table, element) {
|
|
151
156
|
return table.rules.filter((r) => r.match === element || r.match.split(":")[0] === element).toSorted((a, b) => Number(!!b.when || b.match.includes(":")) - Number(!!a.when || a.match.includes(":")));
|
|
152
157
|
}
|
|
158
|
+
/** An element's attributes with the builder's unsaved defaults filled in. */
|
|
159
|
+
function attrsOf(node, defaults = {}) {
|
|
160
|
+
return {
|
|
161
|
+
...defaults[node.name],
|
|
162
|
+
...node.attrs
|
|
163
|
+
};
|
|
164
|
+
}
|
|
153
165
|
/** Whether a rule's `match` (with an optional `:qualifier`) and `when.attr` fit an element. */
|
|
154
|
-
function ruleMatches(rule, node) {
|
|
166
|
+
function ruleMatches(rule, node, defaults) {
|
|
155
167
|
const [name, qualifier] = rule.match.split(":");
|
|
156
168
|
if (name !== node.name) return false;
|
|
157
|
-
const attrs = node
|
|
169
|
+
const attrs = attrsOf(node, defaults);
|
|
158
170
|
if (qualifier && ![
|
|
159
171
|
attrs.area,
|
|
160
172
|
attrs.slug,
|
|
@@ -172,7 +184,7 @@ function ruleMatches(rule, node) {
|
|
|
172
184
|
function claimMatches(nodes, table, path = "0") {
|
|
173
185
|
return nodes.flatMap((node, i) => {
|
|
174
186
|
const at = `${path}.${i}`;
|
|
175
|
-
const rule = rulesFor(table, node.name).find((r) => ruleMatches(r, node));
|
|
187
|
+
const rule = rulesFor(table, node.name).find((r) => ruleMatches(r, node, table.defaults));
|
|
176
188
|
return rule ? [{
|
|
177
189
|
path: at,
|
|
178
190
|
node,
|
|
@@ -194,6 +206,6 @@ async function loadCatalog() {
|
|
|
194
206
|
return JSON.parse(await readFile(join(PACKAGE_ROOT, "catalog.json"), "utf8"));
|
|
195
207
|
}
|
|
196
208
|
//#endregion
|
|
197
|
-
export { BUILDER_PREFIX, CATALOG_FORMAT, KIT_BUILDERS, KIT_COMPONENTS_DIR, KIT_DIR, MAPPING_FORMAT, MAPPING_VALUE_PATTERN, claimMatches, componentsForSource, copyComponents, isBuilderElement, loadCatalog, loadMapping, planCopy, ruleMatches, rulesFor, unmappedSources, validateCatalog, validateMapping };
|
|
209
|
+
export { BUILDER_PREFIX, CATALOG_FORMAT, KIT_BUILDERS, KIT_COMPONENTS_DIR, KIT_DIR, MAPPING_FORMAT, MAPPING_VALUE_PATTERN, attrsOf, claimMatches, componentsForSource, copyComponents, isBuilderElement, loadCatalog, loadMapping, planCopy, ruleMatches, rulesFor, unmappedSources, validateCatalog, validateMapping };
|
|
198
210
|
|
|
199
211
|
//# sourceMappingURL=index.mjs.map
|