@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.
Files changed (43) hide show
  1. package/README.md +30 -3
  2. package/catalog.json +251 -4
  3. package/components/_shared/Button.astro +1 -1
  4. package/components/_shared/KitImage.astro +32 -5
  5. package/components/_shared/Section.astro +1 -1
  6. package/components/breadcrumb/Breadcrumb.astro +1 -1
  7. package/components/card-grid/CardGrid.astro +4 -4
  8. package/components/contact-form/ContactForm.astro +1 -1
  9. package/components/cta/Cta.astro +2 -2
  10. package/components/embed/Embed.astro +104 -0
  11. package/components/embed/embed.ts +106 -0
  12. package/components/embed/fixtures.json +34 -0
  13. package/components/embed/meta.json +32 -0
  14. package/components/faq/Faq.astro +5 -3
  15. package/components/faq/fixtures.json +4 -0
  16. package/components/faq/meta.json +1 -1
  17. package/components/footer/Footer.astro +4 -4
  18. package/components/footer/meta.json +107 -14
  19. package/components/gallery/Gallery.astro +3 -3
  20. package/components/header/Header.astro +1 -1
  21. package/components/hero/Hero.astro +14 -7
  22. package/components/hero/fixtures.json +21 -0
  23. package/components/hero/meta.json +8 -0
  24. package/components/nav/Nav.astro +4 -2
  25. package/components/nav/meta.json +86 -13
  26. package/components/newsletter/Newsletter.astro +110 -0
  27. package/components/newsletter/fixtures.json +6 -0
  28. package/components/newsletter/meta.json +105 -0
  29. package/components/newsletter/newsletter.ts +73 -0
  30. package/components/pagination/Pagination.astro +1 -1
  31. package/components/post-card/PostCard.astro +2 -2
  32. package/components/slider/Slider.astro +3 -3
  33. package/components/tabs/Tabs.astro +2 -2
  34. package/components/testimonial/Testimonial.astro +1 -1
  35. package/dist/index.d.mts +11 -4
  36. package/dist/index.d.mts.map +1 -1
  37. package/dist/index.mjs +19 -7
  38. package/dist/index.mjs.map +1 -1
  39. package/mapping/classic.json +91 -1
  40. package/mapping/divi.json +22 -1
  41. package/mapping/elementor.json +46 -1
  42. package/mapping/gutenberg.json +70 -4
  43. package/package.json +2 -2
@@ -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": ["types.ts"],
7
+ "shared": [
8
+ "types.ts"
9
+ ],
8
10
  "variants": {
9
- "mobile": { "options": ["popover", "details"], "default": "popover", "description": "Narrow-screen menu: a side drawer (Popover API) or a dropdown under the toggle." },
10
- "align": { "options": ["start", "end", "center"], "default": "end", "description": "Horizontal alignment of the inline list." },
11
- "density": { "options": ["compact", "comfortable"], "default": "compact", "description": "Gap and text size of the inline list." }
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": { "type": "array", "required": true, "items": { "type": "object", "fields": { "label": { "type": "string", "required": true }, "href": { "type": "url", "required": true }, "newTab": { "type": "boolean" }, "children": { "type": "array" } } }, "description": "Menu entries; one level of children is shown." },
15
- "label": { "type": "string", "description": "Accessible name of the navigation landmark (ui-strings nav.label)." },
16
- "menuLabel": { "type": "string", "description": "Text of the narrow-screen toggle (ui-strings nav.menu)." },
17
- "closeLabel": { "type": "string", "description": "Text of the drawer's close button (ui-strings nav.close)." },
18
- "id": { "type": "string", "description": "Unique id when a page renders two navigations." }
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": ["core/navigation", "core/page-list"],
25
- "elementor": ["elementor/nav-menu"],
26
- "divi": ["divi/et_pb_menu", "divi/et_pb_fullwidth_menu"],
27
- "classic": ["classic/nav-menu"]
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-semibold text-balance text-ink',
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-semibold text-balance">{slide.title}</p>}
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-bold tracking-tight text-balance md:text-3xl',
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-semibold group-data-[ready]/tabs:sr-only',
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-bold tracking-tight text-balance md:text-3xl',
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
@@ -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;;;cC5F9D,cAAA;AAAA,cAEA,qBAAA,EAAqB,MAAA;AAAA,UAEjB,WAAA;EDjBU;ECmBzB,KAAA;EDnByB;ECqBzB,IAAA;IAAS,IAAA,GAAO,MAAA;IAA2C,KAAA;EAAA;EDlBvC;ECoBpB,SAAA;EACA,OAAA,GAAU,MAAA;EDpBoF;ECsB9F,IAAA;EDtBkC;ECwBlC,KAAA;EACA,KAAA,GAAQ,MAAA;EDjBT;ECmBC,IAAA;EDxB2B;EC0B3B,IAAA;EDdc;ECgBd,IAAA,GAAO,MAAA;AAAA;AAAA,UAGQ,YAAA;EACf,MAAA,SAAe,cAAA;EACf,OAAA,EAAS,UAAA;EDrB2C;ECuBpD,OAAA;EDvBgE;ECyBhE,QAAA;EACA,KAAA,EAAO,WAAA;AAAA;;iBAIO,eAAA,CAAgB,KAAA,EAAO,YAAA,EAAc,OAAA,EAAS,UAAA;ADtB9D;AAAA,iBC+DgB,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;AD5Eb;AAAA,iBCgFgB,WAAA,CAAY,IAAA,EAAM,WAAA,EAAa,IAAA,EAAM,WAAA;;;;;iBAerC,YAAA,CAAa,KAAA,EAAO,WAAA,IAAe,KAAA,EAAO,YAAA,EAAc,IAAA;EAAe,IAAA;EAAc,IAAA,EAAM,WAAA;EAAa,IAAA,EAAM,WAAA;AAAA;;;;cC9HjH,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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/catalog.ts","../src/mapping.ts","../src/copy.ts","../src/index.ts"],"mappings":"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"}
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 ?? {})) if (!c.variants[axis]) problems.push(`${at}: ${c.id} has no variant axis ${axis}`);
119
- else if (!c.variants[axis].options.includes(value)) problems.push(`${at}: ${c.id}.${axis} has no option ${value}`);
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.attrs ?? {};
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