@contentrain/astro-kit 0.6.1 → 0.7.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/catalog.json CHANGED
@@ -564,8 +564,7 @@
564
564
  "label": {
565
565
  "type": "string",
566
566
  "label": "Label",
567
- "required": true,
568
- "description": "What the detail is (Phone, Email, Address)."
567
+ "description": "What the detail is (Phone, Email, Address). Without labels the details are a plain list."
569
568
  },
570
569
  "value": {
571
570
  "type": "string",
@@ -605,7 +604,7 @@
605
604
  "classic/ninja-forms"
606
605
  ]
607
606
  },
608
- "a11y": "Contact details are a description list; links say what they open. The form's labels, errors and focus handling are Studio's form runtime (FormMount). The section is labelled by its heading.",
607
+ "a11y": "Contact details are a description list when every one has a label, a plain list otherwise; links say what they open. The form's labels, errors and focus handling are Studio's form runtime (FormMount). The section is labelled by its heading.",
609
608
  "files": [
610
609
  "ContactForm.astro"
611
610
  ],
@@ -634,10 +633,11 @@
634
633
  "layout": {
635
634
  "options": [
636
635
  "inline",
637
- "centered"
636
+ "centered",
637
+ "stacked"
638
638
  ],
639
639
  "default": "inline",
640
- "description": "inline: text left, buttons right on wide screens. centered: everything centred."
640
+ "description": "inline: text left, buttons right on wide screens. centered: everything centred. stacked: heading, text and buttons in one column at the start edge on every width."
641
641
  },
642
642
  "frame": {
643
643
  "options": [
@@ -652,10 +652,11 @@
652
652
  "default",
653
653
  "muted",
654
654
  "inverse",
655
- "accent"
655
+ "accent",
656
+ "none"
656
657
  ],
657
658
  "default": "accent",
658
- "description": "Background. On accent, primary buttons are drawn in the secondary style so they stay visible."
659
+ "description": "Background. On accent, primary buttons are drawn in the secondary style so they stay visible. none: no fill of its own; the page background shows through (a source colour the palette does not have)."
659
660
  }
660
661
  },
661
662
  "props": {
@@ -878,6 +879,14 @@
878
879
  ],
879
880
  "default": "default",
880
881
  "description": "Section background."
882
+ },
883
+ "questionStyle": {
884
+ "options": [
885
+ "heading",
886
+ "bold"
887
+ ],
888
+ "default": "heading",
889
+ "description": "How the questions read. heading: a small heading (lg, heading weight). bold: body size in bold, as accordion and toggle titles often are. The plain style sets its own and ignores this."
881
890
  }
882
891
  },
883
892
  "props": {
@@ -986,10 +995,11 @@
986
995
  "style": {
987
996
  "options": [
988
997
  "icon",
989
- "check"
998
+ "check",
999
+ "list"
990
1000
  ],
991
1001
  "default": "icon",
992
- "description": "icon: each feature's own icon in a tinted badge. check: the same check mark before every feature."
1002
+ "description": "icon: each feature's own icon in a tinted badge. check: the same check mark before every feature. list: an icon list — one column of body-size lines, each after its icon, titles as text rather than headings (columns is ignored)."
993
1003
  },
994
1004
  "tone": {
995
1005
  "options": [
@@ -1551,7 +1561,7 @@
1551
1561
  "cover"
1552
1562
  ],
1553
1563
  "default": "split",
1554
- "description": "split: text and image side by side (stacked on phones). centered: centred text with the image below. cover: the image fills the section behind the text, darkened by a scrim; without an image it falls back to split."
1564
+ "description": "split: text and image side by side (stacked on phones); without an image the text takes the whole row. centered: centred text with the image below. cover: the image fills the section behind the text, darkened by a scrim; without an image it falls back to split."
1555
1565
  },
1556
1566
  "tone": {
1557
1567
  "options": [
@@ -1570,6 +1580,14 @@
1570
1580
  ],
1571
1581
  "default": "center",
1572
1582
  "description": "Text position over a cover image (layout cover only): centred, as WordPress covers default to, or at the start edge."
1583
+ },
1584
+ "leadAlign": {
1585
+ "options": [
1586
+ "inherit",
1587
+ "start"
1588
+ ],
1589
+ "default": "inherit",
1590
+ "description": "The lead's alignment. inherit: as the layout sets it. start: at the start edge and across the column, under a centred heading (a centred heading widget over a left-aligned text editor in one container)."
1573
1591
  }
1574
1592
  },
1575
1593
  "props": {
@@ -2857,6 +2875,15 @@
2857
2875
  ],
2858
2876
  "default": "default",
2859
2877
  "description": "Section background."
2878
+ },
2879
+ "labelSize": {
2880
+ "options": [
2881
+ "sm",
2882
+ "base",
2883
+ "lg"
2884
+ ],
2885
+ "default": "sm",
2886
+ "description": "The label under each figure: sm (small), base (body size) or lg (a counter title set like a subheading)."
2860
2887
  }
2861
2888
  },
2862
2889
  "props": {
@@ -3184,6 +3211,14 @@
3184
3211
  ],
3185
3212
  "default": "card",
3186
3213
  "description": "Member chrome: a bordered card with the portrait on top, or none."
3214
+ },
3215
+ "roleStyle": {
3216
+ "options": [
3217
+ "accent",
3218
+ "plain"
3219
+ ],
3220
+ "default": "accent",
3221
+ "description": "The member's role. accent: a small accent-coloured line. plain: body-size text in the text colour, as a byline."
3187
3222
  }
3188
3223
  },
3189
3224
  "props": {
@@ -3315,6 +3350,16 @@
3315
3350
  ],
3316
3351
  "default": "default",
3317
3352
  "description": "Section background."
3353
+ },
3354
+ "quoteSize": {
3355
+ "options": [
3356
+ "auto",
3357
+ "base",
3358
+ "lg",
3359
+ "xl"
3360
+ ],
3361
+ "default": "auto",
3362
+ "description": "The quote's text size. auto: the layout's own (body size in a grid, 2xl–3xl for a single quote). base, lg, xl: that size on every width."
3318
3363
  }
3319
3364
  },
3320
3365
  "props": {
@@ -12,6 +12,8 @@ const section = tv({
12
12
  muted: 'bg-surface-muted text-ink',
13
13
  inverse: 'bg-ink text-surface',
14
14
  accent: 'bg-accent text-accent-ink',
15
+ // No fill of its own: the page's background and text colour show through.
16
+ none: '',
15
17
  },
16
18
  spacing: {
17
19
  none: '',
@@ -26,7 +28,7 @@ const section = tv({
26
28
  export type Tone = 'default' | 'muted' | 'inverse' | 'accent'
27
29
 
28
30
  interface Props {
29
- tone?: Tone | undefined
31
+ tone?: Tone | 'none' | undefined
30
32
  spacing?: 'none' | 'sm' | 'md' | 'lg' | undefined
31
33
  /** `page` is the full frame, `prose` the reading column, `none` for full-bleed content. */
32
34
  width?: 'page' | 'prose' | 'none' | undefined
@@ -14,7 +14,8 @@ import StudioForm from '../../studio/StudioForm.astro'
14
14
  import { formHome } from '../../../lib/features'
15
15
 
16
16
  export interface ContactDetail {
17
- label: string
17
+ /** What the detail is (Phone, Email); a source's icon list often has none. */
18
+ label?: string | undefined
18
19
  value: string
19
20
  /** `mailto:`, `tel:` or a map link. */
20
21
  href?: string | undefined
@@ -62,6 +63,9 @@ const { model, heading, intro, formHeading, details = [], layout, tone, id = 'co
62
63
  const formShown = formHome(model) !== null
63
64
  const s = contact({ layout: formShown ? layout : 'stacked', tone })
64
65
  const headingId = heading ? `${id}-title` : undefined
66
+ // A description list needs a term for every detail; an icon list carried from the source has values only.
67
+ const labelled = details.every(detail => detail.label)
68
+ const LINK = 'underline decoration-line underline-offset-4 hover:decoration-current'
65
69
  // One level under the section heading, or the section's heading when there is none.
66
70
  const FormHeading = heading ? 'h3' : 'h2'
67
71
  ---
@@ -71,16 +75,27 @@ const FormHeading = heading ? 'h3' : 'h2'
71
75
  <div class={s.copy()}>
72
76
  {heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
73
77
  {intro && <p class={s.intro()}>{intro}</p>}
74
- {details.length > 0 && (
75
- <dl class={s.details()}>
76
- {details.map(detail => (
77
- <div>
78
- <dt class={s.term()}>{detail.label}</dt>
79
- <dd class={s.value()}>{detail.href ? <a href={detail.href} class="underline decoration-line underline-offset-4 hover:decoration-current">{detail.value}</a> : detail.value}</dd>
80
- </div>
78
+ {details.length > 0 && (labelled
79
+ ? (
80
+ <dl class={s.details()}>
81
+ {details.map(detail => (
82
+ <div>
83
+ <dt class={s.term()}>{detail.label}</dt>
84
+ <dd class={s.value()}>{detail.href ? <a href={detail.href} class={LINK}>{detail.value}</a> : detail.value}</dd>
85
+ </div>
86
+ ))}
87
+ </dl>
88
+ )
89
+ : (
90
+ <ul class={s.details()}>
91
+ {details.map(detail => (
92
+ <li class={s.value()}>
93
+ {detail.label && <span class={s.term()}>{detail.label}: </span>}
94
+ {detail.href ? <a href={detail.href} class={LINK}>{detail.value}</a> : detail.value}
95
+ </li>
96
+ ))}
97
+ </ul>
81
98
  ))}
82
- </dl>
83
- )}
84
99
  </div>
85
100
  )}
86
101
  {formShown && (formHeading
@@ -14,7 +14,7 @@
14
14
  "heading": { "type": "string", "label": "Heading", "description": "Section heading." },
15
15
  "intro": { "type": "text", "label": "Introduction", "description": "A line or two above the details." },
16
16
  "formHeading": { "type": "string", "label": "Form heading", "description": "The form's own heading, shown above the form." },
17
- "details": { "type": "array", "label": "Contact details", "items": { "type": "object", "fields": { "label": { "type": "string", "label": "Label", "required": true, "description": "What the detail is (Phone, Email, Address)." }, "value": { "type": "string", "label": "Value", "required": true, "description": "The detail as shown." }, "href": { "type": "url", "label": "Link", "description": "A tel:, mailto: or map link for it." } } }, "description": "Ways to reach you, in order (Email → mailto:, Phone → tel:, Address → map link)." },
17
+ "details": { "type": "array", "label": "Contact details", "items": { "type": "object", "fields": { "label": { "type": "string", "label": "Label", "description": "What the detail is (Phone, Email, Address). Without labels the details are a plain list." }, "value": { "type": "string", "label": "Value", "required": true, "description": "The detail as shown." }, "href": { "type": "url", "label": "Link", "description": "A tel:, mailto: or map link for it." } } }, "description": "Ways to reach you, in order (Email → mailto:, Phone → tel:, Address → map link)." },
18
18
  "id": { "type": "string", "content": false, "description": "Section id; the heading gets `<id>-title`." }
19
19
  },
20
20
  "slots": {},
@@ -25,5 +25,5 @@
25
25
  "divi": ["divi/et_pb_contact_form"],
26
26
  "classic": ["classic/contact-form-7", "classic/wpforms", "classic/gravityforms", "classic/ninja-forms"]
27
27
  },
28
- "a11y": "Contact details are a description list; links say what they open. The form's labels, errors and focus handling are Studio's form runtime (FormMount). The section is labelled by its heading."
28
+ "a11y": "Contact details are a description list when every one has a label, a plain list otherwise; links say what they open. The form's labels, errors and focus handling are Studio's form runtime (FormMount). The section is labelled by its heading."
29
29
  }
@@ -18,13 +18,16 @@ const cta = tv({
18
18
  layout: {
19
19
  inline: { box: 'md:flex-row md:items-center md:justify-between', actions: 'md:shrink-0' },
20
20
  centered: { box: 'items-center text-center', body: 'mx-auto', actions: 'justify-center' },
21
+ // Heading, text and buttons in one column at the start edge, on every width.
22
+ stacked: { box: 'items-start text-start' },
21
23
  },
22
24
  frame: {
23
25
  full: {},
24
26
  card: { box: 'rounded-card border border-line p-8 md:p-12' },
25
27
  },
26
28
  // 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.
27
- tone: { default: {}, muted: {}, inverse: {}, accent: { body: 'opacity-100' } },
29
+ // none: no fill of its own — the page background and text colour show through.
30
+ tone: { default: {}, muted: {}, inverse: {}, accent: { body: 'opacity-100' }, none: {} },
28
31
  },
29
32
  compoundVariants: [
30
33
  // A card carries the tone itself, on a plain section; a plain card still needs a surface to read as one.
@@ -40,9 +43,9 @@ interface Props {
40
43
  heading: string
41
44
  body?: string | undefined
42
45
  actions: ActionInput[]
43
- layout?: 'inline' | 'centered' | undefined
46
+ layout?: 'inline' | 'centered' | 'stacked' | undefined
44
47
  frame?: 'full' | 'card' | undefined
45
- tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
48
+ tone?: 'default' | 'muted' | 'inverse' | 'accent' | 'none' | undefined
46
49
  id?: string | undefined
47
50
  }
48
51
 
@@ -2,5 +2,6 @@
2
2
  { "name": "inline-accent", "props": { "heading": "Get the next issue in your inbox", "body": "One email a month. No tracking, unsubscribe any time.", "actions": [{ "label": "Subscribe", "href": "/subscribe/" }] } },
3
3
  { "name": "centered-inverse", "props": { "heading": "Planning a trip?", "body": "Start with the guides readers use most.", "layout": "centered", "tone": "inverse", "actions": [{ "label": "Popular guides", "href": "/guides/" }, { "label": "Ask a question", "href": "/contact/", "style": "ghost" }] } },
4
4
  { "name": "card-default", "props": { "heading": "Write for the journal", "body": "We publish reader essays every season.", "frame": "card", "tone": "default", "actions": [{ "label": "Pitch an essay", "href": "/pitch/" }] } },
5
- { "name": "card-muted", "props": { "heading": "Support independent travel writing", "frame": "card", "tone": "muted", "layout": "centered", "actions": [{ "label": "Become a member", "href": "/members/" }] } }
5
+ { "name": "card-muted", "props": { "heading": "Support independent travel writing", "frame": "card", "tone": "muted", "layout": "centered", "actions": [{ "label": "Become a member", "href": "/members/" }] } },
6
+ { "name": "stacked-none", "props": { "heading": "Ready when you are", "body": "Tell us where you want to go and we will send a first plan within a week.", "layout": "stacked", "tone": "none", "actions": [{ "label": "Start planning", "href": "/contact/" }] } }
6
7
  ]
@@ -6,9 +6,9 @@
6
6
  "uses": [],
7
7
  "shared": ["Button.astro", "Section.astro", "types.ts"],
8
8
  "variants": {
9
- "layout": { "options": ["inline", "centered"], "default": "inline", "description": "inline: text left, buttons right on wide screens. centered: everything centred." },
9
+ "layout": { "options": ["inline", "centered", "stacked"], "default": "inline", "description": "inline: text left, buttons right on wide screens. centered: everything centred. stacked: heading, text and buttons in one column at the start edge on every width." },
10
10
  "frame": { "options": ["full", "card"], "default": "full", "description": "full: the tone fills the section. card: a rounded box in a plain section carries the tone." },
11
- "tone": { "options": ["default", "muted", "inverse", "accent"], "default": "accent", "description": "Background. On accent, primary buttons are drawn in the secondary style so they stay visible." }
11
+ "tone": { "options": ["default", "muted", "inverse", "accent", "none"], "default": "accent", "description": "Background. On accent, primary buttons are drawn in the secondary style so they stay visible. none: no fill of its own; the page background shows through (a source colour the palette does not have)." }
12
12
  },
13
13
  "props": {
14
14
  "heading": { "type": "string", "label": "Heading", "required": true, "description": "The call's heading (Divi CTA title, Elementor call-to-action title)." },
@@ -29,17 +29,21 @@ const faq = tv({
29
29
  plain: { list: 'gap-2', summary: 'list-item list-[revert] py-1 text-[length:var(--text-body,1rem)] [font-weight:inherit] [&::-webkit-details-marker]:inline', marker: 'hidden', answer: 'pt-2 pl-5' },
30
30
  },
31
31
  tone: { default: {}, muted: { item: 'bg-surface' }, inverse: { answer: 'prose-invert' }, accent: { answer: 'prose-invert' } },
32
+ // heading: the question as a small heading. bold: body size in bold, as accordion and toggle titles often are.
33
+ questionStyle: { heading: {}, bold: { summary: 'text-[length:var(--text-body,1rem)] font-bold' } },
32
34
  },
33
35
  compoundVariants: [
34
36
  { style: 'cards', tone: 'muted', class: { item: 'border-transparent' } },
35
37
  ],
36
- defaultVariants: { style: 'lines', tone: 'default' },
38
+ defaultVariants: { style: 'lines', tone: 'default', questionStyle: 'heading' },
37
39
  })
38
40
 
39
41
  interface Props {
40
42
  items: FaqItem[]
41
43
  heading?: string | undefined
42
44
  style?: 'lines' | 'cards' | 'plain' | undefined
45
+ /** How the questions read; `plain` sets its own and ignores this. */
46
+ questionStyle?: 'heading' | 'bold' | undefined
43
47
  /** One answer open at a time (`<details name>`). */
44
48
  exclusive?: boolean | undefined
45
49
  /** Open the first answer. */
@@ -50,8 +54,8 @@ interface Props {
50
54
  id?: string | undefined
51
55
  }
52
56
 
53
- const { items, heading, style, exclusive = false, openFirst = false, schema = false, tone, id = 'faq' } = Astro.props
54
- const s = faq({ style, tone })
57
+ const { items, heading, style, questionStyle, exclusive = false, openFirst = false, schema = false, tone, id = 'faq' } = Astro.props
58
+ const s = faq({ style, questionStyle: style === 'plain' ? 'heading' : questionStyle, tone })
55
59
  const headingId = heading ? `${id}-title` : undefined
56
60
  const plain = (html: string) => html.replace(/<[^>]*>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()
57
61
  const structured = schema && items.length > 0
@@ -36,5 +36,9 @@
36
36
  {
37
37
  "name": "plain",
38
38
  "props": { "style": "plain", "id": "faq-plain", "items": [{ "question": "Do you accept guest essays?", "answer": "<p>Yes, send a short pitch first.</p>" }, { "question": "How often do you publish?", "answer": "<p>Two or three pieces a month.</p>" }] }
39
+ },
40
+ {
41
+ "name": "lines-bold",
42
+ "props": { "heading": "Questions", "questionStyle": "bold", "id": "faq-bold", "items": [{ "question": "Do you ship printed guides abroad?", "answer": "<p>Yes, to every country in the EU and the UK.</p>" }, { "question": "Can I change my route later?", "answer": "<p>Any time before the trip starts.</p>" }] }
39
43
  }
40
44
  ]
@@ -7,7 +7,8 @@
7
7
  "shared": ["Section.astro"],
8
8
  "variants": {
9
9
  "style": { "options": ["lines", "cards", "plain"], "default": "lines", "description": "lines: rows divided by rules. cards: each question in a rounded box. plain: the browser's own disclosure triangle without rules or boxes, questions at the body size (WordPress's details block)." },
10
- "tone": { "options": ["default", "muted", "inverse", "accent"], "default": "default", "description": "Section background." }
10
+ "tone": { "options": ["default", "muted", "inverse", "accent"], "default": "default", "description": "Section background." },
11
+ "questionStyle": { "options": ["heading", "bold"], "default": "heading", "description": "How the questions read. heading: a small heading (lg, heading weight). bold: body size in bold, as accordion and toggle titles often are. The plain style sets its own and ignores this." }
11
12
  },
12
13
  "props": {
13
14
  "items": { "type": "array", "label": "Questions", "required": true, "items": { "type": "object", "fields": { "question": { "type": "string", "label": "Question", "required": true, "description": "The question, as the visitor would ask it." }, "answer": { "type": "richtext", "label": "Answer", "required": true, "description": "The answer; links and lists allowed." } } }, "description": "The questions in order (core/details summary + content, Elementor accordion/toggle tabs, Divi accordion/toggle items)." },
@@ -21,6 +21,8 @@ const list = tv({
21
21
  style: {
22
22
  icon: { mark: 'size-10 rounded-[var(--radius-card)] bg-accent/10' },
23
23
  check: {},
24
+ // An icon list: one column of body-size lines, each after its icon. Titles are text here, not headings.
25
+ list: { grid: 'flex flex-col gap-3', item: 'items-center gap-3', mark: 'size-5', title: 'text-[length:var(--text-body,1rem)] leading-normal [font-weight:inherit] text-pretty', text: 'mt-0' },
24
26
  },
25
27
  // On accent the mark takes the ink colour, and text is never dimmed.
26
28
  tone: { default: {}, muted: {}, inverse: {}, accent: { mark: 'text-current', text: 'opacity-100' } },
@@ -51,8 +53,9 @@ interface Props {
51
53
  heading?: string | undefined
52
54
  intro?: string | undefined
53
55
  items: Item[]
56
+ /** Ignored by `list`, which is one column. */
54
57
  columns?: '2' | '3' | '4' | undefined
55
- style?: 'icon' | 'check' | undefined
58
+ style?: 'icon' | 'check' | 'list' | undefined
56
59
  tone?: Tone | undefined
57
60
  id?: string | undefined
58
61
  }
@@ -60,7 +63,8 @@ interface Props {
60
63
  const { heading, intro, items, columns, style = 'icon', tone, id } = Astro.props
61
64
  const s = list({ columns, style, tone })
62
65
  const headingId = heading && id ? `${id}-title` : undefined
63
- const pathOf = (item: Item) => (style === 'icon' && item.icon && icons[item.icon]) || icons.check
66
+ const pathOf = (item: Item) => (style !== 'check' && item.icon && icons[item.icon]) || icons.check
67
+ const Title = style === 'list' ? 'span' : 'h3'
64
68
  ---
65
69
  <Section tone={tone} id={id} aria-labelledby={headingId}>
66
70
  {(heading || intro) && (
@@ -76,7 +80,7 @@ const pathOf = (item: Item) => (style === 'icon' && item.icon && icons[item.icon
76
80
  <svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={pathOf(item)} /></svg>
77
81
  </span>
78
82
  <div>
79
- <h3 class={s.title()}>{item.title}</h3>
83
+ <Title class={s.title()}>{item.title}</Title>
80
84
  {item.text && <p class={s.text()} data-kit-text>{item.text}</p>}
81
85
  </div>
82
86
  </li>
@@ -125,5 +125,30 @@
125
125
  "style": "check",
126
126
  "tone": "accent"
127
127
  }
128
+ },
129
+ {
130
+ "name": "list",
131
+ "props": {
132
+ "heading": "In the package",
133
+ "items": [
134
+ {
135
+ "title": "Twelve printed guides",
136
+ "icon": "check"
137
+ },
138
+ {
139
+ "title": "Offline maps for every route",
140
+ "icon": "globe"
141
+ },
142
+ {
143
+ "title": "A year of member support",
144
+ "icon": "chat"
145
+ },
146
+ {
147
+ "title": "Free updates when routes change",
148
+ "icon": "clock"
149
+ }
150
+ ],
151
+ "style": "list"
152
+ }
128
153
  }
129
154
  ]
@@ -20,10 +20,11 @@
20
20
  "style": {
21
21
  "options": [
22
22
  "icon",
23
- "check"
23
+ "check",
24
+ "list"
24
25
  ],
25
26
  "default": "icon",
26
- "description": "icon: each feature's own icon in a tinted badge. check: the same check mark before every feature."
27
+ "description": "icon: each feature's own icon in a tinted badge. check: the same check mark before every feature. list: an icon list — one column of body-size lines, each after its icon, titles as text rather than headings (columns is ignored)."
27
28
  },
28
29
  "tone": {
29
30
  "options": [
@@ -27,11 +27,16 @@ const hero = tv({
27
27
  tone: { default: {}, muted: {}, inverse: {}, accent: { eyebrow: 'opacity-100', lead: 'opacity-100' } },
28
28
  // Where a cover sets its text over the image. WordPress covers centre it unless told otherwise.
29
29
  align: { start: {}, center: {} },
30
+ // The lead's own alignment: `start` keeps it at the start edge, across the column, under a centred heading
31
+ // (Hello centres the heading widget and leaves the text editor left in the same container).
32
+ leadAlign: { inherit: {}, start: {} },
30
33
  },
31
34
  compoundVariants: [
32
35
  { layout: 'cover', align: 'center', class: { grid: 'justify-items-center', copy: 'mx-auto items-center text-center', lead: 'mx-auto', actions: 'justify-center' } },
36
+ // After the centring rules, so the lead leaves them.
37
+ { leadAlign: 'start', class: { lead: 'mx-0 max-w-none self-stretch text-start' } },
33
38
  ],
34
- defaultVariants: { layout: 'split', tone: 'default', align: 'center' },
39
+ defaultVariants: { layout: 'split', tone: 'default', align: 'center', leadAlign: 'inherit' },
35
40
  })
36
41
 
37
42
  interface Props {
@@ -45,12 +50,14 @@ interface Props {
45
50
  layout?: 'split' | 'centered' | 'cover' | undefined
46
51
  /** Text position over a cover image; other layouts ignore it. */
47
52
  align?: 'start' | 'center' | undefined
53
+ /** The lead's alignment: as the layout sets it, or at the start edge under a centred heading. */
54
+ leadAlign?: 'inherit' | 'start' | undefined
48
55
  tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
49
56
  id?: string | undefined
50
57
  }
51
58
 
52
- const { heading, level = 1, eyebrow, lead, actions = [], image, layout = 'split', align, tone, id } = Astro.props
53
- const s = hero({ layout, align, tone })
59
+ const { heading, level = 1, eyebrow, lead, actions = [], image, layout = 'split', align, leadAlign, tone, id } = Astro.props
60
+ const s = hero({ layout, align, leadAlign, tone })
54
61
  const Heading = level === 1 ? 'h1' : 'h2'
55
62
  const headingId = id ? `${id}-title` : undefined
56
63
  const cover = layout === 'cover' && image
@@ -74,7 +81,8 @@ const cover = layout === 'cover' && image
74
81
  )
75
82
  : (
76
83
  <Section tone={tone} spacing="lg" id={id} aria-labelledby={headingId}>
77
- <div class={s.grid()}>
84
+ {/* Without an image a split has nothing beside the text: the text takes the whole row, not half of it. */}
85
+ <div class={s.grid({ class: image ? undefined : 'md:grid-cols-1' })}>
78
86
  <div class={s.copy()}>
79
87
  {eyebrow && <p class={s.eyebrow()}>{eyebrow}</p>}
80
88
  <Heading id={headingId} class={s.heading()}>{heading}</Heading>
@@ -112,5 +112,15 @@
112
112
  "lead": "Twelve new guides, and the long read on night trains.",
113
113
  "eyebrow": "New"
114
114
  }
115
+ },
116
+ {
117
+ "name": "centered-lead-start",
118
+ "props": {
119
+ "heading": "About the studio",
120
+ "lead": "We are a small team of editors and photographers. This page tells you who we are, how we work and what we will never do with your data.",
121
+ "layout": "centered",
122
+ "leadAlign": "start",
123
+ "level": 2
124
+ }
115
125
  }
116
126
  ]
@@ -18,7 +18,7 @@
18
18
  "cover"
19
19
  ],
20
20
  "default": "split",
21
- "description": "split: text and image side by side (stacked on phones). centered: centred text with the image below. cover: the image fills the section behind the text, darkened by a scrim; without an image it falls back to split."
21
+ "description": "split: text and image side by side (stacked on phones); without an image the text takes the whole row. centered: centred text with the image below. cover: the image fills the section behind the text, darkened by a scrim; without an image it falls back to split."
22
22
  },
23
23
  "tone": {
24
24
  "options": [
@@ -37,6 +37,14 @@
37
37
  ],
38
38
  "default": "center",
39
39
  "description": "Text position over a cover image (layout cover only): centred, as WordPress covers default to, or at the start edge."
40
+ },
41
+ "leadAlign": {
42
+ "options": [
43
+ "inherit",
44
+ "start"
45
+ ],
46
+ "default": "inherit",
47
+ "description": "The lead's alignment. inherit: as the layout sets it. start: at the start edge and across the column, under a centred heading (a centred heading widget over a left-aligned text editor in one container)."
40
48
  }
41
49
  },
42
50
  "props": {
@@ -23,8 +23,10 @@ const stats = tv({
23
23
  },
24
24
  // accent/accent-ink is only ~5:1 — dimming text on it drops it under 4.5:1 (WCAG 1.4.3).
25
25
  tone: { default: {}, muted: {}, inverse: {}, accent: { label: 'opacity-100' } },
26
+ // The label under each figure: small, body size, or large (a counter title set like a subheading).
27
+ labelSize: { sm: {}, base: { label: 'text-base' }, lg: { label: 'text-lg' } },
26
28
  },
27
- defaultVariants: { columns: '3', style: 'plain', tone: 'default' },
29
+ defaultVariants: { columns: '3', style: 'plain', tone: 'default', labelSize: 'sm' },
28
30
  })
29
31
 
30
32
  interface Item {
@@ -40,12 +42,13 @@ interface Props {
40
42
  items: Item[]
41
43
  columns?: '2' | '3' | '4' | undefined
42
44
  style?: 'plain' | 'card' | undefined
45
+ labelSize?: 'sm' | 'base' | 'lg' | undefined
43
46
  tone?: Tone | undefined
44
47
  id?: string | undefined
45
48
  }
46
49
 
47
- const { heading, intro, items, columns, style, tone, id } = Astro.props
48
- const s = stats({ columns, style, tone })
50
+ const { heading, intro, items, columns, style, labelSize, tone, id } = Astro.props
51
+ const s = stats({ columns, style, labelSize, tone })
49
52
  const headingId = heading && id ? `${id}-title` : undefined
50
53
  ---
51
54
  <Section tone={tone} id={id} aria-labelledby={headingId}>
@@ -94,5 +94,44 @@
94
94
  "style": "card",
95
95
  "tone": "accent"
96
96
  }
97
+ },
98
+ {
99
+ "name": "three-label-lg",
100
+ "props": {
101
+ "labelSize": "lg",
102
+ "items": [
103
+ {
104
+ "value": "12",
105
+ "label": "Years on the road"
106
+ },
107
+ {
108
+ "value": "340",
109
+ "label": "Guides written"
110
+ },
111
+ {
112
+ "value": "48",
113
+ "suffix": "k",
114
+ "label": "Readers a month"
115
+ }
116
+ ]
117
+ }
118
+ },
119
+ {
120
+ "name": "two-label-base",
121
+ "props": {
122
+ "columns": "2",
123
+ "labelSize": "base",
124
+ "items": [
125
+ {
126
+ "value": "98",
127
+ "suffix": "%",
128
+ "label": "Trips we would repeat"
129
+ },
130
+ {
131
+ "value": "4.9",
132
+ "label": "Average reader rating"
133
+ }
134
+ ]
135
+ }
97
136
  }
98
137
  ]
@@ -34,6 +34,15 @@
34
34
  ],
35
35
  "default": "default",
36
36
  "description": "Section background."
37
+ },
38
+ "labelSize": {
39
+ "options": [
40
+ "sm",
41
+ "base",
42
+ "lg"
43
+ ],
44
+ "default": "sm",
45
+ "description": "The label under each figure: sm (small), base (body size) or lg (a counter title set like a subheading)."
37
46
  }
38
47
  },
39
48
  "props": {
@@ -28,8 +28,10 @@ const team = tv({
28
28
  card: { member: 'rounded-card border border-line p-6', portrait: '-mx-6 -mt-6 mb-2 rounded-b-none' },
29
29
  plain: {},
30
30
  },
31
+ // accent: a small accent line. plain: body-size text in the text colour, as a byline.
32
+ roleStyle: { accent: {}, plain: { role: 'text-[length:var(--text-body,1rem)] font-normal text-current' } },
31
33
  },
32
- defaultVariants: { columns: '3', style: 'card' },
34
+ defaultVariants: { columns: '3', style: 'card', roleStyle: 'accent' },
33
35
  })
34
36
 
35
37
  interface Member {
@@ -48,11 +50,12 @@ interface Props {
48
50
  items: Member[]
49
51
  columns?: '2' | '3' | '4' | undefined
50
52
  style?: 'card' | 'plain' | undefined
53
+ roleStyle?: 'accent' | 'plain' | undefined
51
54
  id?: string | undefined
52
55
  }
53
56
 
54
- const { heading, intro, items, columns, style, id } = Astro.props
55
- const s = team({ columns, style })
57
+ const { heading, intro, items, columns, style, roleStyle, id } = Astro.props
58
+ const s = team({ columns, style, roleStyle })
56
59
  const headingId = heading && id ? `${id}-title` : undefined
57
60
  ---
58
61
  {items.length > 0 && (
@@ -1,5 +1,6 @@
1
1
  [
2
2
  {"name": "three-card", "props": {"id": "team", "heading": "The team", "intro": "Small, senior, and in the same room.", "items": [{"name": "Mira Kovač", "role": "Editor", "bio": "Keeps every page honest and every sentence short.", "image": "/kit-fixtures/avatar-1.svg", "href": "/team/mira/"}, {"name": "Jon Ade", "role": "Engineer", "bio": "Builds the sites and keeps them fast.", "image": "/kit-fixtures/avatar-2.svg", "href": "/team/jon/"}, {"name": "Lea Park", "role": "Designer", "bio": "Draws the systems the sites are made of.", "image": "/kit-fixtures/avatar-3.svg"}]}},
3
3
  {"name": "two-plain", "props": {"columns": "2", "style": "plain", "heading": "Founders", "items": [{"name": "Mira Kovač", "role": "Co-founder", "image": "/kit-fixtures/avatar-1.svg", "imageAlt": "Mira Kovač, smiling"}, {"name": "Jon Ade", "role": "Co-founder", "image": "/kit-fixtures/avatar-2.svg"}]}},
4
- {"name": "four-card-no-photos", "props": {"columns": "4", "items": [{"name": "Mira Kovač", "role": "Editor"}, {"name": "Jon Ade", "role": "Engineer"}, {"name": "Lea Park", "role": "Designer"}, {"name": "Sam Ortiz", "role": "Producer", "bio": "Keeps the schedule."}]}}
4
+ {"name": "four-card-no-photos", "props": {"columns": "4", "items": [{"name": "Mira Kovač", "role": "Editor"}, {"name": "Jon Ade", "role": "Engineer"}, {"name": "Lea Park", "role": "Designer"}, {"name": "Sam Ortiz", "role": "Producer", "bio": "Keeps the schedule."}]}},
5
+ {"name": "three-plain-role", "props": {"heading": "Our team", "style": "plain", "roleStyle": "plain", "items": [{"name": "Mira Kovač", "role": "Editor"}, {"name": "Jon Ade", "role": "Photographer"}, {"name": "Lea Park", "role": "Route planner"}]}}
5
6
  ]
@@ -27,6 +27,14 @@
27
27
  ],
28
28
  "default": "card",
29
29
  "description": "Member chrome: a bordered card with the portrait on top, or none."
30
+ },
31
+ "roleStyle": {
32
+ "options": [
33
+ "accent",
34
+ "plain"
35
+ ],
36
+ "default": "accent",
37
+ "description": "The member's role. accent: a small accent-coloured line. plain: body-size text in the text colour, as a byline."
30
38
  }
31
39
  },
32
40
  "props": {
@@ -32,23 +32,31 @@ const testimonial = tv({
32
32
  grid: { list: 'md:grid-cols-2 lg:grid-cols-3', card: 'rounded-card border border-line p-6', quote: 'text-base' },
33
33
  },
34
34
  tone: { default: {}, muted: { card: 'bg-surface' }, inverse: { card: 'border-surface/20' }, accent: { card: 'border-accent-ink/30', role: 'opacity-100' } },
35
+ // The quote's size; `auto` is the layout's own (body size in a grid, large for a single quote).
36
+ quoteSize: {
37
+ auto: {},
38
+ base: { quote: 'text-base leading-relaxed md:text-base' },
39
+ lg: { quote: 'text-lg leading-relaxed md:text-lg' },
40
+ xl: { quote: 'text-xl leading-snug md:text-xl' },
41
+ },
35
42
  },
36
43
  compoundVariants: [
37
44
  { layout: 'grid', tone: 'muted', class: { card: 'border-transparent text-ink' } },
38
45
  ],
39
- defaultVariants: { layout: 'grid', tone: 'default' },
46
+ defaultVariants: { layout: 'grid', tone: 'default', quoteSize: 'auto' },
40
47
  })
41
48
 
42
49
  interface Props {
43
50
  items: TestimonialItem[]
44
51
  heading?: string | undefined
45
52
  layout?: 'single' | 'grid' | undefined
53
+ quoteSize?: 'auto' | 'base' | 'lg' | 'xl' | undefined
46
54
  tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
47
55
  id?: string | undefined
48
56
  }
49
57
 
50
- const { items, heading, layout, tone, id } = Astro.props
51
- const s = testimonial({ layout, tone })
58
+ const { items, heading, layout, quoteSize, tone, id } = Astro.props
59
+ const s = testimonial({ layout, quoteSize, tone })
52
60
  const headingId = heading && id ? `${id}-title` : undefined
53
61
  ---
54
62
  {items.length > 0 && (
@@ -22,5 +22,17 @@
22
22
  {
23
23
  "name": "single-accent",
24
24
  "props": { "layout": "single", "tone": "accent", "items": [{ "quote": "Worth every minute.", "name": "Jonas Weber" }] }
25
+ },
26
+ {
27
+ "name": "grid-quote-xl",
28
+ "props": { "quoteSize": "xl", "items": [{ "quote": "The guide took us off the main road and into the best week of the year.", "name": "Ada Lindqvist", "role": "Reader since 2019" }, { "quote": "Fewer places, more time. It changed how we travel.", "name": "Tomas Reyes", "role": "Photographer" }] }
29
+ },
30
+ {
31
+ "name": "grid-quote-base",
32
+ "props": { "quoteSize": "base", "tone": "muted", "items": [{ "quote": "Short, clear and honest.", "name": "Ada Lindqvist" }, { "quote": "The maps alone are worth it.", "name": "Tomas Reyes", "role": "Cyclist" }] }
33
+ },
34
+ {
35
+ "name": "single-quote-lg",
36
+ "props": { "layout": "single", "quoteSize": "lg", "items": [{ "quote": "We planned less and saw more.", "name": "Jonas Weber", "role": "Reader" }] }
25
37
  }
26
38
  ]
@@ -28,6 +28,16 @@
28
28
  ],
29
29
  "default": "default",
30
30
  "description": "Section background."
31
+ },
32
+ "quoteSize": {
33
+ "options": [
34
+ "auto",
35
+ "base",
36
+ "lg",
37
+ "xl"
38
+ ],
39
+ "default": "auto",
40
+ "description": "The quote's text size. auto: the layout's own (body size in a grid, 2xl–3xl for a single quote). base, lg, xl: that size on every width."
31
41
  }
32
42
  },
33
43
  "props": {
@@ -1 +1 @@
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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"}
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
@@ -277,7 +277,7 @@ function validateSections(table, components) {
277
277
  if (!Object.keys(rule.slots).length) problems.push(`${at}: no slots`);
278
278
  for (const [name, slot] of Object.entries(rule.slots)) {
279
279
  if (!/^[a-z][\w-]*$/.test(name)) problems.push(`${at}: slot ${name} is not an identifier`);
280
- for (const element of [slot.match].flat()) if (!isBuilderElement(table.builder, element)) problems.push(`${at}: slot ${name} matches ${element}, not a ${table.builder} element`);
280
+ for (const element of [slot.match].flat()) if (!isBuilderElement(table.builder, element) && !isBuilderElement("classic", element)) problems.push(`${at}: slot ${name} matches ${element}, not a ${table.builder} or classic plugin element`);
281
281
  if (slot.min !== void 0 && slot.max !== void 0 && slot.min > slot.max) problems.push(`${at}: slot ${name} min > max`);
282
282
  }
283
283
  for (const root of [rule.when?.root ?? []].flat()) if (root !== "run" && !isBuilderElement(table.builder, root)) problems.push(`${at}: root ${root} is not a ${table.builder} element or run`);
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/catalog.ts","../src/copy.ts","../src/mapping.ts","../src/index.ts"],"sourcesContent":["// The kit catalog: what each component is, what it takes, how it varies and\n// which WordPress builder elements it stands in for. Machine-readable, so the\n// migration writer chooses components by querying it instead of reading\n// source, and the builder mapping tables (mapping/*.json) are checked against\n// it.\n//\n// Props are described with Contentrain's own `FieldDef`, so a model field and\n// the prop it feeds are compared type to type.\n\nimport type { FieldDef } from '@contentrain/types'\nimport { fieldDepth, sectionFieldName } from '@contentrain/types'\n\nexport const CATALOG_FORMAT = 'astro-kit-catalog@1'\n\n/** The WordPress builders whose elements a component is fed from. */\nexport type KitBuilder = 'gutenberg' | 'elementor' | 'divi' | 'classic'\nexport const KIT_BUILDERS: readonly KitBuilder[] = ['gutenberg', 'elementor', 'divi', 'classic']\n\n/** Element-name prefix per builder, as `facts.json` names elements (`core/cover`, `elementor/heading`, `divi/et_pb_blurb`). */\nexport const BUILDER_PREFIX: Record<KitBuilder, string> = {\n gutenberg: 'core/',\n elementor: 'elementor/',\n divi: 'divi/',\n classic: 'classic/',\n}\n\n/**\n * Whether an element name belongs to a builder. Gutenberg blocks are namespaced by their plugin\n * (`core/cover`, `yoast-seo/breadcrumbs`, `uagb/container`), so any `<ns>/<name>` that is not another\n * builder's is a Gutenberg block.\n */\nexport function isBuilderElement(builder: KitBuilder, name: string): boolean {\n if (builder !== 'gutenberg') return name.startsWith(BUILDER_PREFIX[builder])\n return /^[a-z0-9-]+\\/[a-z0-9-]+(:[\\w-]+)?$/.test(name) && !KIT_BUILDERS.some(b => b !== 'gutenberg' && name.startsWith(BUILDER_PREFIX[b]))\n}\n\nexport type KitCategory = 'chrome' | 'section' | 'content' | 'primitive'\n\n/** Client JavaScript a component ships: none, a few lines of dependency-free script (`vanilla`), Embla (carousel), Zag.js (accessible widget) or the Studio runtime. */\nexport type KitJs = 'none' | 'vanilla' | 'embla' | 'zag' | 'studio'\n\nexport type KitProp = FieldDef & {\n description: string\n /**\n * `false`: a setting the plan or writer passes (heading level, a label from `ui:`, an anchor id), not page\n * content. A section's page-singleton field holds only the other props, its content props.\n */\n content?: false\n}\n\n/**\n * A content prop's name: camelCase, a capital only to start a word (`imageAlt`, not `imageURL`), so it\n * turns into exactly one Contentrain field name.\n */\nexport const CONTENT_PROP_NAME = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)*$/\n\n/**\n * The page-singleton field a content prop is stored in (DECISIONS §7b): the prop name in snake_case, as\n * Contentrain field names are (`ctaLabel` → `cta_label`). The view passes each prop from its field.\n */\nexport const contentFieldName: (prop: string) => string = sectionFieldName\n\n/** The props a section's page-singleton field holds: all but the settings. */\nexport function contentProps(component: KitComponentMeta): Record<string, KitProp> {\n return Object.fromEntries(Object.entries(component.props).filter(([, prop]) => prop.content !== false))\n}\n\n/** Fields under a field, by dotted path (`items[].imageAlt`). */\nfunction fieldsByPath(fields: Record<string, FieldDef>, prefix = ''): Array<[string, FieldDef]> {\n const out: Array<[string, FieldDef]> = []\n for (const [name, field] of Object.entries(fields)) {\n out.push([prefix + name, field], ...fieldsByPath(field.fields ?? {}, `${prefix}${name}.`))\n if (typeof field.items === 'object') out.push(...fieldsByPath(field.items.fields ?? {}, `${prefix}${name}[].`))\n }\n return out\n}\n\n/** A content prop path as its page-singleton field path: every name in it through `contentFieldName`. */\nexport function contentFieldPath(path: string): string {\n return path.replace(/[A-Za-z0-9]+/g, contentFieldName)\n}\n\nexport interface KitVariant {\n options: string[]\n default: string\n description: string\n}\n\n/** One component as written in `components/<id>/meta.json`. */\nexport interface KitComponentMeta {\n id: string\n name: string\n description: string\n category: KitCategory\n /** Kit components this one renders. */\n uses: string[]\n /** Files it imports from `components/_shared/`. */\n shared: string[]\n variants: Record<string, KitVariant>\n props: Record<string, KitProp>\n /** Named slots → what goes in them. `default` is the unnamed slot. */\n slots: Record<string, string>\n js: KitJs\n /** npm packages the site needs for this component, name → range. */\n dependencies: Record<string, string>\n /** Builder elements this component replaces, by builder — what the mapping tables map from. */\n sources: Partial<Record<KitBuilder, string[]>>\n /** How it meets its accessibility pattern. */\n a11y: string\n /**\n * Sections only: the section's shape in a few words and the signals that show it in a source page, a\n * closed-set label for the classifier and the migration architect (`image beside heading + text`).\n */\n pattern?: { summary: string, signals: string[] }\n /**\n * Sections only, written by the catalog build: each content prop path (`slides[].ctaLabel`) → the\n * page-singleton field path that holds it (`slides[].cta_label`).\n */\n contentFields?: Record<string, string>\n}\n\n/** A catalog entry: the meta plus the files that make the component. */\nexport interface KitComponent extends KitComponentMeta {\n /** Paths relative to `components/<id>/`. */\n files: string[]\n}\n\nexport interface KitCatalog {\n format: typeof CATALOG_FORMAT\n /** Design tokens (Tailwind @theme names) the components read; the site must define them. */\n tokens: string[]\n /** npm packages every kit component needs. */\n dependencies: Record<string, string>\n components: KitComponent[]\n}\n\n/** Problems with a catalog, one line each; empty when it is sound. */\nexport function validateCatalog(catalog: KitCatalog): string[] {\n const problems: string[] = []\n const ids = new Set(catalog.components.map(c => c.id))\n for (const c of catalog.components) {\n const at = `component ${c.id}`\n if (!/^[a-z][a-z0-9-]*$/.test(c.id)) problems.push(`${at}: id must be kebab-case`)\n if (!c.files.length) problems.push(`${at}: no files`)\n for (const used of c.uses) if (!ids.has(used)) problems.push(`${at}: uses unknown component \"${used}\"`)\n for (const [axis, variant] of Object.entries(c.variants)) {\n if (!variant.options.includes(variant.default)) problems.push(`${at}: variant ${axis} default \"${variant.default}\" is not an option`)\n }\n for (const [name, prop] of Object.entries(c.props)) {\n if (!prop.description) problems.push(`${at}: prop ${name} has no description`)\n if (prop.type === 'select' && !prop.options?.length) problems.push(`${at}: select prop ${name} has no options`)\n }\n if (c.category === 'section') {\n // A section is stored as one object field of its page singleton: each content prop needs one field\n // name there, and that field must stay within two object levels.\n const content = contentProps(c)\n const paths = fieldsByPath(content)\n for (const [path, field] of paths) {\n if (!CONTENT_PROP_NAME.test(path.split(/\\.|\\[\\]\\./).at(-1)!)) problems.push(`${at}: content prop ${path} must be camelCase with one capital per word (or content: false)`)\n // Studio shows both to editors, and the plan reads them as the field's label and help text.\n if (!field.label) problems.push(`${at}: content prop ${path} has no label`)\n if (!field.description) problems.push(`${at}: content prop ${path} has no description`)\n }\n if (c.contentFields) {\n const expected = Object.fromEntries(paths.map(([path]) => [path, contentFieldPath(path)]))\n if (JSON.stringify(c.contentFields) !== JSON.stringify(expected)) problems.push(`${at}: contentFields is stale — rebuild the catalog`)\n }\n for (const [name, prop] of Object.entries(content)) {\n if (1 + fieldDepth(prop) > 2) problems.push(`${at}: content prop ${name} nests objects deeper than a page singleton allows`)\n }\n }\n for (const [builder, names] of Object.entries(c.sources) as Array<[KitBuilder, string[]]>) {\n const prefix = BUILDER_PREFIX[builder]\n for (const name of names) if (!isBuilderElement(builder, name)) problems.push(`${at}: source \"${name}\" is not a ${builder} element (${prefix}…)`)\n }\n }\n const seen = new Set<string>()\n for (const c of catalog.components) {\n if (seen.has(c.id)) problems.push(`component ${c.id}: listed twice`)\n seen.add(c.id)\n }\n return problems\n}\n\n/** Components that replace a builder element, e.g. `core/cover` → hero. */\nexport function componentsForSource(catalog: KitCatalog, element: string): KitComponent[] {\n return catalog.components.filter(c => Object.values(c.sources).some(names => names?.includes(element)))\n}\n","// Copying components into a site — the shadcn model: the site owns the\n// source, there is no runtime dependency on this package. A component is\n// copied with every kit component it renders and every shared file it\n// imports, under `src/components/kit/`, keeping the relative layout its\n// imports expect.\n\nimport { copyFile, mkdir } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport type { KitCatalog } from './catalog.js'\n\n/** Where kit files land in a site, relative to the site root. */\nexport const KIT_DIR = 'src/components/kit'\n\nexport interface CopyPlan {\n /** Components copied, the requested ones and everything they render, sorted. */\n components: string[]\n /** Site-relative destination → path relative to the kit's `components/` directory. */\n files: Record<string, string>\n /** npm packages the site needs, name → range. */\n dependencies: Record<string, string>\n}\n\nexport function planCopy(catalog: KitCatalog, ids: readonly string[]): CopyPlan {\n const byId = new Map(catalog.components.map(c => [c.id, c]))\n const wanted = new Set<string>()\n const visit = (id: string) => {\n if (wanted.has(id)) return\n const component = byId.get(id)\n if (!component) throw new Error(`astro-kit has no component \"${id}\". Known: ${[...byId.keys()].join(', ')}.`)\n wanted.add(id)\n for (const used of component.uses) visit(used)\n }\n for (const id of ids) visit(id)\n\n const files: Record<string, string> = {}\n const dependencies: Record<string, string> = { ...catalog.dependencies }\n for (const id of [...wanted].toSorted()) {\n const component = byId.get(id)!\n for (const file of component.files) files[`${KIT_DIR}/${id}/${file}`] = `${id}/${file}`\n for (const file of component.shared) files[`${KIT_DIR}/_shared/${file}`] = `_shared/${file}`\n Object.assign(dependencies, component.dependencies)\n }\n return { components: [...wanted].toSorted(), files, dependencies }\n}\n\n/** Carry out a plan: `kitRoot` is the kit's `components/` directory, `siteRoot` the site. */\nexport async function copyComponents(plan: CopyPlan, kitRoot: string, siteRoot: string): Promise<void> {\n await Promise.all(Object.entries(plan.files).map(async ([dest, source]) => {\n const target = join(siteRoot, dest)\n await mkdir(dirname(target), { recursive: true })\n await copyFile(join(kitRoot, source), target)\n }))\n}\n","// Builder mapping tables: which kit component a WordPress builder element\n// becomes, and where each of its props comes from. One versioned table per\n// builder (`mapping/<builder>.json`), written once and applied\n// deterministically to every site — the migration model is asked only about\n// elements no rule covers (Migrate's `unmapped_element` decision).\n//\n// A prop's source is one expression from a small closed set. The first six are\n// the project plan's values (`@contentrain/types` `PlanValue`) and bind\n// content; the rest read the builder element itself or the page it is on:\n//\n// field:<name> · media:<field> · ref:<field>.<field> · href:self|<field> · term:<field> · ui:<key> · const:<value>\n// site:<field> a field of the `site` singleton (`site:title`, `site:logo`)\n// menu:primary|footer the menu the starter shows in that area\n// page:<value> what the route knows: `page:base`, `page:current`, `page:total`, `page:breadcrumb`\n// attr:<name> a builder attribute (block attribute, Elementor setting, Divi shortcode attribute)\n// dom:<selector> text of the first match inside the element (empty selector: the element)\n// dom:<selector>@<attr> an attribute of it (`dom:a@href`)\n// img:<selector> an image as the kit's `ImageInput` (src, alt, width, height)\n// img:<selector>@src|@alt one part of that image, for a list item's flat `image` + `imageAlt` fields\n// link:<selector> a link as `{ label, href }`\n// html:<selector> inner markup, kept as Markdown (rich text)\n// class:<cls>=<a>|<b> <a> when the element, or an ancestor inside the matched element, has class\n// <cls> (`*` matches any run of characters); <b> otherwise. An empty branch\n// reads nothing and the prop keeps its default. `class:is-style-outline=ghost|primary`\n// is WordPress's outline button as the kit's ghost action.\n//\n// Section rules (`sections`) classify a whole page section by the leaves it holds and fill the component's\n// props from them: the same expressions, read from a named slot's leaf (`@title attr:title`).\n\nimport type { KitBuilder, KitCatalog, KitComponent } from './catalog.js'\nimport { isBuilderElement, KIT_BUILDERS } from './catalog.js'\n\nexport const MAPPING_FORMAT = 'astro-kit-mapping@1'\n\nexport 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-]+)?)$/\n\nexport interface MappingRule {\n /** Builder element, as the fact pack names it; a `:qualifier` narrows it (`core/template-part:header`). */\n match: string\n /** Extra conditions on the element. */\n when?: { attr?: Record<string, string | number | boolean>, class?: string }\n /** Kit component id. */\n component: string\n variant?: Record<string, string>\n /** Content the element lists rather than holds: one component per entry of the collection (`collection:posts`). */\n bind?: `collection:${string}`\n /** Consecutive sibling matches form one component (a row of Elementor icon boxes is one card grid). */\n group?: 'siblings'\n props?: Record<string, string>\n /** Array prop filled from repeated parts of the element (`each`) or the grouped siblings / bound entries. */\n into?: string\n /** Selector of the repeated parts inside the element, for `into`. */\n each?: string\n /** Props of one repeated part / sibling / entry, with sources relative to it. */\n item?: Record<string, string>\n}\n\nexport interface MappingTable {\n format: typeof MAPPING_FORMAT\n builder: KitBuilder\n /** Bumped on any rule change: plans record the version they applied. */\n version: string\n /** What an element no rule covers becomes when no decision is made: rich text. */\n fallback: 'prose'\n /**\n * Attribute values the builder leaves out of its data while they are at their default, per element:\n * Elementor saves no `video_type` for a YouTube video. `when.attr` and `attr:` read through them.\n */\n defaults?: Record<string, Record<string, string | number | boolean>>\n rules: MappingRule[]\n /**\n * Section rules: what a whole section of a page (an Elementor container, a Gutenberg group or columns,\n * a Divi section — facts `sections[]`) becomes, tried top to bottom before any element rule. The first\n * that matches wins; a section none matches falls through to the element `rules` inside it.\n */\n sections?: SectionRule[]\n}\n\n/**\n * A named group of a section's leaves: every leaf named `match` (and carrying `attr`) up to `max` of them.\n * `attr` values may list alternatives with `|` (`{ header_size: 'h1|h2' }`).\n */\nexport interface SectionSlot {\n match: string | string[]\n attr?: Record<string, string | number | boolean>\n /** At least this many leaves (default 0). */\n min?: number\n /** At most this many (default unbounded). */\n max?: number\n}\n\n/**\n * One section pattern. Values are the table's expressions, prefixed `@<slot> ` to read a slot's leaf\n * (`@title attr:title`); `a || b` takes the first non-empty (builder data first, rendered markup as fallback).\n * A slot of several leaves joins their text into a markdown prop; any other prop reads its first leaf.\n * A value without a slot reads the section's own root (a cover's background image, a container's setting).\n */\nexport interface SectionRule {\n /** Stable id (`hero.split`, `card-grid.icon-box`): plans record `<builder>:section:<id>`; JEW chooses among them. */\n id: string\n component: string\n variant?: Record<string, string>\n when?: {\n /** Where on the page: `first` is the page's first section, `last` its last. */\n position?: 'first' | 'last' | 'any'\n /** Column count(s) the section must have. */\n columns?: number | number[]\n /** Every leaf must fall into a slot (default true); false lets unclaimed leaves be. */\n only?: boolean\n /** `columns`: each column must match the slots on its own (a row of team members, price plans). */\n repeat?: 'columns'\n /** The section's root element(s): `core/cover`, `core/media-text` (a loose run of blocks is `run`). */\n root?: string | string[]\n }\n /**\n * Named leaf groups. Each leaf, in document order, goes to the first declared slot that matches it and\n * still has room, so declare a specific slot (an `attr` filter) before a general one on the same element.\n */\n slots: Record<string, SectionSlot>\n props?: Record<string, string>\n /**\n * Array prop filled once per `each`: a slot's leaves (`@card`), the parts a selector finds inside them\n * (`@list figure.wp-block-image`, `@faq .elementor-accordion-item`: a gallery's images, an accordion's\n * items), or the columns (`column`, with `repeat: 'columns'`).\n */\n into?: string\n each?: string\n /** One array item: relative to the leaf (`each: '@slot'`) or to the column, where `@<slot>` is that column's slot. */\n item?: Record<string, string>\n}\n\n/** A section leaf as the fact pack gives it: a builder element with its `blocks:` path. */\nexport interface SectionLeaf extends MappingNode {\n path: string\n}\n\n/** Where a section sits (facts `index`/`count`) and what it is (`root`: facts `root`). */\nexport interface SectionMeta {\n index: number\n count: number\n root?: string | undefined\n}\n\n/** Leaf paths per slot; with `repeat: 'columns'`, also per column. */\nexport interface SectionMatch {\n slots: Record<string, string[]>\n columns?: Record<string, string[]>[]\n}\n\n/** The id a plan records for a section rule. */\nexport function sectionRuleId(builder: KitBuilder, rule: Pick<SectionRule, 'id'>): string {\n return `${builder}:section:${rule.id}`\n}\n\nconst SECTION_LAYOUT_LEAVES = new Set(['elementor/spacer', 'elementor/divider', 'core/spacer', 'core/separator', 'divi/et_pb_divider'])\n\nfunction slotFits(slot: SectionSlot, leaf: SectionLeaf, defaults?: MappingTable['defaults']): boolean {\n if (![slot.match].flat().includes(leaf.name)) return false\n const attrs = attrsOf(leaf, defaults)\n return Object.entries(slot.attr ?? {}).every(([key, want]) => String(want).split('|').includes(String(attrs[key])))\n}\n\n/** Assign leaves to slots in document order, each to the first declared slot with room; null when the rule does not fit. */\nfunction claimLeaves(rule: SectionRule, leaves: SectionLeaf[], defaults?: MappingTable['defaults']): Record<string, string[]> | null {\n const claimed: Record<string, string[]> = Object.fromEntries(Object.keys(rule.slots).map(name => [name, []]))\n for (const leaf of leaves) {\n if (SECTION_LAYOUT_LEAVES.has(leaf.name)) continue\n const slot = Object.entries(rule.slots).find(([name, def]) => claimed[name]!.length < (def.max ?? Infinity) && slotFits(def, leaf, defaults))\n if (slot) claimed[slot[0]]!.push(leaf.path)\n else if (rule.when?.only !== false) return null\n }\n for (const [name, def] of Object.entries(rule.slots)) if (claimed[name]!.length < (def.min ?? 0)) return null\n return claimed\n}\n\n/**\n * Whether a section rule fits a section, and which leaves fill which slot. `columns` are the section's\n * columns left to right, each its leaves in document order (layout containers already opened).\n */\nexport function matchSection(rule: SectionRule, columns: SectionLeaf[][], meta: SectionMeta, defaults?: MappingTable['defaults']): SectionMatch | null {\n const when = rule.when ?? {}\n if (when.root !== undefined && !(meta.root !== undefined && [when.root].flat().includes(meta.root))) return null\n if (when.position === 'first' && meta.index !== 0) return null\n if (when.position === 'last' && meta.index !== meta.count - 1) return null\n if (when.columns !== undefined && ![when.columns].flat().includes(columns.length)) return null\n if (when.repeat === 'columns') {\n if (columns.length < 2) return null\n const per = columns.map(leaves => claimLeaves(rule, leaves, defaults))\n if (per.some(c => !c)) return null\n const slots: Record<string, string[]> = Object.fromEntries(Object.keys(rule.slots).map(name => [name, per.flatMap(c => c![name]!)]))\n return { slots, columns: per as Record<string, string[]>[] }\n }\n const slots = claimLeaves(rule, columns.flat(), defaults)\n return slots && { slots }\n}\n\n/** The first section rule of a table that fits, with its match. */\nexport function classifySection(table: MappingTable, columns: SectionLeaf[][], meta: SectionMeta): { rule: SectionRule, match: SectionMatch } | null {\n for (const rule of table.sections ?? []) {\n const match = matchSection(rule, columns, meta, table.defaults)\n if (match) return { rule, match }\n }\n return null\n}\n\n/** A section value: alternatives joined by ` || `, each an optional `@<slot> ` then a mapping value. Returns the slots it reads, or null when malformed. */\nexport function sectionValueSlots(value: string): string[] | null {\n const slots: string[] = []\n for (const alternative of value.split(' || ')) {\n const m = /^(?:@([a-z][\\w-]*) )?(.+)$/.exec(alternative)\n if (!m || !MAPPING_VALUE_PATTERN.test(m[2]!)) return null\n if (m[1]) slots.push(m[1])\n }\n return slots\n}\n\n/** Problems with a mapping table against the catalog, one line each; empty when it is sound. */\nexport function validateMapping(table: MappingTable, catalog: KitCatalog): string[] {\n const problems: string[] = []\n if (table.format !== MAPPING_FORMAT) problems.push(`format is not ${MAPPING_FORMAT}`)\n if (!KIT_BUILDERS.includes(table.builder)) problems.push(`unknown builder ${table.builder}`)\n if (!/^\\d+$/.test(table.version)) problems.push('version is not an integer string')\n for (const element of Object.keys(table.defaults ?? {})) if (!isBuilderElement(table.builder, element)) problems.push(`defaults ${element}: not a ${table.builder} element`)\n const components = new Map(catalog.components.map(c => [c.id, c]))\n const seen = new Set<string>()\n for (const rule of table.rules) {\n const at = `${table.builder} ${rule.match}${rule.when ? ' (when)' : ''}`\n const key = `${rule.match}|${JSON.stringify(rule.when ?? {})}`\n if (seen.has(key)) problems.push(`${at}: matched twice`)\n seen.add(key)\n if (!isBuilderElement(table.builder, rule.match)) problems.push(`${at}: not a ${table.builder} element`)\n const c = components.get(rule.component)\n if (!c) { problems.push(`${at}: component ${rule.component} is not in the catalog`); continue }\n for (const [axis, value] of Object.entries(rule.variant ?? {})) {\n // A variant is one option, or a `class:` choice between options read from the element.\n const choice = /^class:[\\w*-]+=([a-z][\\w-]*)?\\|([a-z][\\w-]*)?$/.exec(value)\n const options = choice ? [choice[1], choice[2]].filter((option): option is string => option !== undefined) : [value]\n if (!c.variants[axis]) problems.push(`${at}: ${c.id} has no variant axis ${axis}`)\n else for (const missing of options.filter(option => !c.variants[axis]!.options.includes(option))) problems.push(`${at}: ${c.id}.${axis} has no option ${missing}`)\n }\n for (const [prop, value] of Object.entries({ ...rule.props, ...(rule.into ? {} : rule.item) })) {\n if (!c.props[prop]) problems.push(`${at}: ${c.id} has no prop ${prop}`)\n if (!MAPPING_VALUE_PATTERN.test(value)) problems.push(`${at}: ${prop} = ${value} is not a mapping value`)\n }\n if (rule.into) {\n const target = c.props[rule.into]\n if (!target || target.type !== 'array') problems.push(`${at}: ${c.id}.${rule.into} is not an array prop`)\n const fields = target && typeof target.items === 'object' ? target.items.fields ?? {} : {}\n for (const [field, value] of Object.entries(rule.item ?? {})) {\n if (!fields[field]) problems.push(`${at}: ${c.id}.${rule.into}[] has no field ${field}`)\n if (!MAPPING_VALUE_PATTERN.test(value)) problems.push(`${at}: ${rule.into}[].${field} = ${value} is not a mapping value`)\n }\n if (!rule.each && !rule.group && !rule.bind) problems.push(`${at}: into ${rule.into} needs each, group or bind to say what repeats`)\n }\n if (rule.item && !rule.into && !rule.bind) problems.push(`${at}: item without into or bind`)\n const bound = new Set([...Object.keys(rule.props ?? {}), ...(rule.into ? [rule.into] : Object.keys(rule.item ?? {}))])\n for (const [prop, def] of Object.entries(c.props)) if (def.required && !bound.has(prop)) problems.push(`${at}: required prop ${c.id}.${prop} has no source`)\n }\n problems.push(...validateSections(table, components))\n return problems\n}\n\nfunction validateSections(table: MappingTable, components: Map<string, KitComponent>): string[] {\n const problems: string[] = []\n const seen = new Set<string>()\n for (const rule of table.sections ?? []) {\n const at = `${table.builder} section ${rule.id}`\n if (!/^[a-z][a-z0-9-]*(?:\\.[a-z0-9-]+)*$/.test(rule.id)) problems.push(`${at}: id is not dotted kebab-case`)\n if (seen.has(rule.id)) problems.push(`${at}: listed twice`)\n seen.add(rule.id)\n const c = components.get(rule.component)\n if (!c) { problems.push(`${at}: component ${rule.component} is not in the catalog`); continue }\n for (const [axis, value] of Object.entries(rule.variant ?? {})) {\n if (!c.variants[axis]) problems.push(`${at}: ${c.id} has no variant axis ${axis}`)\n else if (!c.variants[axis]!.options.includes(value)) problems.push(`${at}: ${c.id}.${axis} has no option ${value}`)\n }\n if (!Object.keys(rule.slots).length) problems.push(`${at}: no slots`)\n for (const [name, slot] of Object.entries(rule.slots)) {\n if (!/^[a-z][\\w-]*$/.test(name)) problems.push(`${at}: slot ${name} is not an identifier`)\n for (const element of [slot.match].flat()) if (!isBuilderElement(table.builder, element)) problems.push(`${at}: slot ${name} matches ${element}, not a ${table.builder} element`)\n if (slot.min !== undefined && slot.max !== undefined && slot.min > slot.max) problems.push(`${at}: slot ${name} min > max`)\n }\n for (const root of [rule.when?.root ?? []].flat()) if (root !== 'run' && !isBuilderElement(table.builder, root)) problems.push(`${at}: root ${root} is not a ${table.builder} element or run`)\n if (rule.when?.repeat === 'columns' && rule.each !== 'column') problems.push(`${at}: repeat columns needs each: column`)\n if (rule.each === 'column' && rule.when?.repeat !== 'columns') problems.push(`${at}: each column needs when.repeat columns`)\n const readsSlots = (value: string, where: string, type: string | undefined, slotted: boolean) => {\n const slots = sectionValueSlots(value)\n if (!slots) { problems.push(`${at}: ${where} = ${value} is not a section value`); return }\n if (!slotted && slots.length) problems.push(`${at}: ${where} reads a slot, but items of each ${rule.each} read their own leaf`)\n for (const slot of slots) {\n const def = rule.slots[slot]\n if (!def) problems.push(`${at}: ${where} reads undeclared slot ${slot}`)\n else if ((type === 'string' || type === 'text') && def.max !== 1) problems.push(`${at}: ${where} is ${type} but slot ${slot} may hold several leaves (max must be 1)`)\n }\n }\n for (const [prop, value] of Object.entries(rule.props ?? {})) {\n const def = c.props[prop]\n if (!def) problems.push(`${at}: ${c.id} has no prop ${prop}`)\n readsSlots(value, prop, def?.type, true)\n }\n if (rule.into) {\n const target = c.props[rule.into]\n if (!target || target.type !== 'array') problems.push(`${at}: ${c.id}.${rule.into} is not an array prop`)\n if (!rule.each) problems.push(`${at}: into ${rule.into} needs each`)\n else if (rule.each !== 'column' && !rule.slots[/^@([a-z][\\w-]*)(?: \\S.*)?$/.exec(rule.each)?.[1] ?? '']) problems.push(`${at}: each ${rule.each} is not @<declared slot> [selector] or column`)\n const fields = target && typeof target.items === 'object' ? target.items.fields ?? {} : {}\n for (const [field, value] of Object.entries(rule.item ?? {})) {\n if (!fields[field]) problems.push(`${at}: ${c.id}.${rule.into}[] has no field ${field}`)\n readsSlots(value, `${rule.into}[].${field}`, fields[field]?.type, rule.each === 'column')\n }\n } else if (rule.item || rule.each) problems.push(`${at}: each/item without into`)\n const bound = new Set([...Object.keys(rule.props ?? {}), ...(rule.into ? [rule.into] : [])])\n for (const [prop, def] of Object.entries(c.props)) if (def.required && !bound.has(prop)) problems.push(`${at}: required prop ${c.id}.${prop} has no source`)\n }\n return problems\n}\n\n/** Catalog sources of a builder that no rule of its table matches, as an element rule or a section slot. */\nexport function unmappedSources(table: MappingTable, catalog: KitCatalog): string[] {\n const matched = new Set([...table.rules.map(r => r.match), ...(table.sections ?? []).flatMap(r => Object.values(r.slots).flatMap(s => [s.match].flat()))])\n const sources = catalog.components.flatMap((c: KitComponent) => c.sources[table.builder] ?? [])\n return [...new Set(sources)].filter(s => !matched.has(s)).toSorted()\n}\n\n/** Rules for an element, most specific (with `when` / qualifier) first. */\nexport function rulesFor(table: MappingTable, element: string): MappingRule[] {\n return table.rules\n .filter(r => r.match === element || r.match.split(':')[0] === element)\n .toSorted((a, b) => Number(!!b.when || b.match.includes(':')) - Number(!!a.when || a.match.includes(':')))\n}\n\n/** A builder element as the fact pack gives it (`FactBlock`): name, attributes, children. */\nexport interface MappingNode {\n name: string\n attrs?: Record<string, unknown>\n children?: MappingNode[]\n}\n\n/** An element's attributes with the builder's unsaved defaults filled in. */\nexport function attrsOf(node: MappingNode, defaults: MappingTable['defaults'] = {}): Record<string, unknown> {\n return { ...defaults[node.name], ...node.attrs }\n}\n\n/** Whether a rule's `match` (with an optional `:qualifier`) and `when.attr` fit an element. */\nexport function ruleMatches(rule: MappingRule, node: MappingNode, defaults?: MappingTable['defaults']): boolean {\n const [name, qualifier] = rule.match.split(':')\n if (name !== node.name) return false\n const attrs = attrsOf(node, defaults)\n // A qualifier names the element's role: a template part's area or slug, or a sub-part the fact pack marks.\n if (qualifier && ![attrs.area, attrs.slug, attrs.tagName, attrs.qualifier].includes(qualifier)) return false\n for (const [key, value] of Object.entries(rule.when?.attr ?? {})) if (attrs[key] !== value) return false\n if (rule.when?.class && !String(attrs.className ?? attrs.classes ?? '').split(/\\s+/).includes(rule.when.class)) return false\n return true\n}\n\n/**\n * Apply a table to a tree: the outermost matching element owns its whole subtree, and nothing inside\n * it is matched again — a header template part holding the site title and logo is one header, not three.\n */\nexport function claimMatches(nodes: MappingNode[], table: MappingTable, path = '0'): { path: string, node: MappingNode, rule: MappingRule }[] {\n return nodes.flatMap((node, i) => {\n const at = `${path}.${i}`\n const rule = rulesFor(table, node.name).find(r => ruleMatches(r, node, table.defaults))\n return rule ? [{ path: at, node, rule }] : claimMatches(node.children ?? [], table, at)\n })\n}\n","import { readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { KitBuilder, KitCatalog } from './catalog.js'\nimport type { MappingTable } from './mapping.js'\n\nexport * from './catalog.js'\nexport * from './copy.js'\nexport * from './mapping.js'\n\nconst PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')\n\n/** The kit's `components/` directory — the source `copyComponents` copies from. */\nexport const KIT_COMPONENTS_DIR = join(PACKAGE_ROOT, 'components')\n\n/** A builder's mapping table (`mapping/<builder>.json`). */\nexport async function loadMapping(builder: KitBuilder): Promise<MappingTable> {\n return JSON.parse(await readFile(join(PACKAGE_ROOT, 'mapping', `${builder}.json`), 'utf8')) as MappingTable\n}\n\n/** The published catalog (`catalog.json`). */\nexport async function loadCatalog(): Promise<KitCatalog> {\n return JSON.parse(await readFile(join(PACKAGE_ROOT, 'catalog.json'), 'utf8')) as KitCatalog\n}\n"],"mappings":";;;;;AAYA,MAAa,iBAAiB;AAI9B,MAAa,eAAsC;CAAC;CAAa;CAAa;CAAQ;CAAU;;AAGhG,MAAa,iBAA6C;CACxD,WAAW;CACX,WAAW;CACX,MAAM;CACN,SAAS;CACV;;;;;;AAOD,SAAgB,iBAAiB,SAAqB,MAAuB;AAC3E,KAAI,YAAY,YAAa,QAAO,KAAK,WAAW,eAAe,SAAS;AAC5E,QAAO,qCAAqC,KAAK,KAAK,IAAI,CAAC,aAAa,MAAK,MAAK,MAAM,eAAe,KAAK,WAAW,eAAe,GAAG,CAAC;;;;;;AAqB5I,MAAa,oBAAoB;;;;;AAMjC,MAAa,mBAA6C;;AAG1D,SAAgB,aAAa,WAAsD;AACjF,QAAO,OAAO,YAAY,OAAO,QAAQ,UAAU,MAAM,CAAC,QAAQ,GAAG,UAAU,KAAK,YAAY,MAAM,CAAC;;;AAIzG,SAAS,aAAa,QAAkC,SAAS,IAA+B;CAC9F,MAAM,MAAiC,EAAE;AACzC,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,EAAE;AAClD,MAAI,KAAK,CAAC,SAAS,MAAM,MAAM,EAAE,GAAG,aAAa,MAAM,UAAU,EAAE,EAAE,GAAG,SAAS,KAAK,GAAG,CAAC;AAC1F,MAAI,OAAO,MAAM,UAAU,SAAU,KAAI,KAAK,GAAG,aAAa,MAAM,MAAM,UAAU,EAAE,EAAE,GAAG,SAAS,KAAK,KAAK,CAAC;;AAEjH,QAAO;;;AAIT,SAAgB,iBAAiB,MAAsB;AACrD,QAAO,KAAK,QAAQ,iBAAiB,iBAAiB;;;AA0DxD,SAAgB,gBAAgB,SAA+B;CAC7D,MAAM,WAAqB,EAAE;CAC7B,MAAM,MAAM,IAAI,IAAI,QAAQ,WAAW,KAAI,MAAK,EAAE,GAAG,CAAC;AACtD,MAAK,MAAM,KAAK,QAAQ,YAAY;EAClC,MAAM,KAAK,aAAa,EAAE;AAC1B,MAAI,CAAC,oBAAoB,KAAK,EAAE,GAAG,CAAE,UAAS,KAAK,GAAG,GAAG,yBAAyB;AAClF,MAAI,CAAC,EAAE,MAAM,OAAQ,UAAS,KAAK,GAAG,GAAG,YAAY;AACrD,OAAK,MAAM,QAAQ,EAAE,KAAM,KAAI,CAAC,IAAI,IAAI,KAAK,CAAE,UAAS,KAAK,GAAG,GAAG,4BAA4B,KAAK,GAAG;AACvG,OAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,EAAE,SAAS,CACtD,KAAI,CAAC,QAAQ,QAAQ,SAAS,QAAQ,QAAQ,CAAE,UAAS,KAAK,GAAG,GAAG,YAAY,KAAK,YAAY,QAAQ,QAAQ,oBAAoB;AAEvI,OAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,EAAE,MAAM,EAAE;AAClD,OAAI,CAAC,KAAK,YAAa,UAAS,KAAK,GAAG,GAAG,SAAS,KAAK,qBAAqB;AAC9E,OAAI,KAAK,SAAS,YAAY,CAAC,KAAK,SAAS,OAAQ,UAAS,KAAK,GAAG,GAAG,gBAAgB,KAAK,iBAAiB;;AAEjH,MAAI,EAAE,aAAa,WAAW;GAG5B,MAAM,UAAU,aAAa,EAAE;GAC/B,MAAM,QAAQ,aAAa,QAAQ;AACnC,QAAK,MAAM,CAAC,MAAM,UAAU,OAAO;AACjC,QAAI,CAAC,kBAAkB,KAAK,KAAK,MAAM,YAAY,CAAC,GAAG,GAAG,CAAE,CAAE,UAAS,KAAK,GAAG,GAAG,iBAAiB,KAAK,kEAAkE;AAE1K,QAAI,CAAC,MAAM,MAAO,UAAS,KAAK,GAAG,GAAG,iBAAiB,KAAK,eAAe;AAC3E,QAAI,CAAC,MAAM,YAAa,UAAS,KAAK,GAAG,GAAG,iBAAiB,KAAK,qBAAqB;;AAEzF,OAAI,EAAE,eAAe;IACnB,MAAM,WAAW,OAAO,YAAY,MAAM,KAAK,CAAC,UAAU,CAAC,MAAM,iBAAiB,KAAK,CAAC,CAAC,CAAC;AAC1F,QAAI,KAAK,UAAU,EAAE,cAAc,KAAK,KAAK,UAAU,SAAS,CAAE,UAAS,KAAK,GAAG,GAAG,gDAAgD;;AAExI,QAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,QAAQ,CAChD,KAAI,IAAI,WAAW,KAAK,GAAG,EAAG,UAAS,KAAK,GAAG,GAAG,iBAAiB,KAAK,oDAAoD;;AAGhI,OAAK,MAAM,CAAC,SAAS,UAAU,OAAO,QAAQ,EAAE,QAAQ,EAAmC;GACzF,MAAM,SAAS,eAAe;AAC9B,QAAK,MAAM,QAAQ,MAAO,KAAI,CAAC,iBAAiB,SAAS,KAAK,CAAE,UAAS,KAAK,GAAG,GAAG,YAAY,KAAK,aAAa,QAAQ,YAAY,OAAO,IAAI;;;CAGrJ,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,KAAK,QAAQ,YAAY;AAClC,MAAI,KAAK,IAAI,EAAE,GAAG,CAAE,UAAS,KAAK,aAAa,EAAE,GAAG,gBAAgB;AACpE,OAAK,IAAI,EAAE,GAAG;;AAEhB,QAAO;;;AAIT,SAAgB,oBAAoB,SAAqB,SAAiC;AACxF,QAAO,QAAQ,WAAW,QAAO,MAAK,OAAO,OAAO,EAAE,QAAQ,CAAC,MAAK,UAAS,OAAO,SAAS,QAAQ,CAAC,CAAC;;;;;AC/KzG,MAAa,UAAU;AAWvB,SAAgB,SAAS,SAAqB,KAAkC;CAC9E,MAAM,OAAO,IAAI,IAAI,QAAQ,WAAW,KAAI,MAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;CAC5D,MAAM,yBAAS,IAAI,KAAa;CAChC,MAAM,SAAS,OAAe;AAC5B,MAAI,OAAO,IAAI,GAAG,CAAE;EACpB,MAAM,YAAY,KAAK,IAAI,GAAG;AAC9B,MAAI,CAAC,UAAW,OAAM,IAAI,MAAM,+BAA+B,GAAG,YAAY,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG;AAC7G,SAAO,IAAI,GAAG;AACd,OAAK,MAAM,QAAQ,UAAU,KAAM,OAAM,KAAK;;AAEhD,MAAK,MAAM,MAAM,IAAK,OAAM,GAAG;CAE/B,MAAM,QAAgC,EAAE;CACxC,MAAM,eAAuC,EAAE,GAAG,QAAQ,cAAc;AACxE,MAAK,MAAM,MAAM,CAAC,GAAG,OAAO,CAAC,UAAU,EAAE;EACvC,MAAM,YAAY,KAAK,IAAI,GAAG;AAC9B,OAAK,MAAM,QAAQ,UAAU,MAAO,OAAM,GAAG,QAAQ,GAAG,GAAG,GAAG,UAAU,GAAG,GAAG,GAAG;AACjF,OAAK,MAAM,QAAQ,UAAU,OAAQ,OAAM,GAAG,QAAQ,WAAW,UAAU,WAAW;AACtF,SAAO,OAAO,cAAc,UAAU,aAAa;;AAErD,QAAO;EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,UAAU;EAAE;EAAO;EAAc;;;AAIpE,eAAsB,eAAe,MAAgB,SAAiB,UAAiC;AACrG,OAAM,QAAQ,IAAI,OAAO,QAAQ,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,YAAY;EACzE,MAAM,SAAS,KAAK,UAAU,KAAK;AACnC,QAAM,MAAM,QAAQ,OAAO,EAAE,EAAE,WAAW,MAAM,CAAC;AACjD,QAAM,SAAS,KAAK,SAAS,OAAO,EAAE,OAAO;GAC7C,CAAC;;;;ACnBL,MAAa,iBAAiB;AAE9B,MAAa,wBAAwB;;AAoHrC,SAAgB,cAAc,SAAqB,MAAuC;AACxF,QAAO,GAAG,QAAQ,WAAW,KAAK;;AAGpC,MAAM,wBAAwB,IAAI,IAAI;CAAC;CAAoB;CAAqB;CAAe;CAAkB;CAAqB,CAAC;AAEvI,SAAS,SAAS,MAAmB,MAAmB,UAA8C;AACpG,KAAI,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,KAAK,KAAK,CAAE,QAAO;CACrD,MAAM,QAAQ,QAAQ,MAAM,SAAS;AACrC,QAAO,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,UAAU,OAAO,KAAK,CAAC,MAAM,IAAI,CAAC,SAAS,OAAO,MAAM,KAAK,CAAC,CAAC;;;AAIrH,SAAS,YAAY,MAAmB,QAAuB,UAAsE;CACnI,MAAM,UAAoC,OAAO,YAAY,OAAO,KAAK,KAAK,MAAM,CAAC,KAAI,SAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AAC7G,MAAK,MAAM,QAAQ,QAAQ;AACzB,MAAI,sBAAsB,IAAI,KAAK,KAAK,CAAE;EAC1C,MAAM,OAAO,OAAO,QAAQ,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,SAAS,QAAQ,MAAO,UAAU,IAAI,OAAO,aAAa,SAAS,KAAK,MAAM,SAAS,CAAC;AAC7I,MAAI,KAAM,SAAQ,KAAK,IAAK,KAAK,KAAK,KAAK;WAClC,KAAK,MAAM,SAAS,MAAO,QAAO;;AAE7C,MAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,KAAK,MAAM,CAAE,KAAI,QAAQ,MAAO,UAAU,IAAI,OAAO,GAAI,QAAO;AACzG,QAAO;;;;;;AAOT,SAAgB,aAAa,MAAmB,SAA0B,MAAmB,UAA0D;CACrJ,MAAM,OAAO,KAAK,QAAQ,EAAE;AAC5B,KAAI,KAAK,SAAS,KAAA,KAAa,EAAE,KAAK,SAAS,KAAA,KAAa,CAAC,KAAK,KAAK,CAAC,MAAM,CAAC,SAAS,KAAK,KAAK,EAAG,QAAO;AAC5G,KAAI,KAAK,aAAa,WAAW,KAAK,UAAU,EAAG,QAAO;AAC1D,KAAI,KAAK,aAAa,UAAU,KAAK,UAAU,KAAK,QAAQ,EAAG,QAAO;AACtE,KAAI,KAAK,YAAY,KAAA,KAAa,CAAC,CAAC,KAAK,QAAQ,CAAC,MAAM,CAAC,SAAS,QAAQ,OAAO,CAAE,QAAO;AAC1F,KAAI,KAAK,WAAW,WAAW;AAC7B,MAAI,QAAQ,SAAS,EAAG,QAAO;EAC/B,MAAM,MAAM,QAAQ,KAAI,WAAU,YAAY,MAAM,QAAQ,SAAS,CAAC;AACtE,MAAI,IAAI,MAAK,MAAK,CAAC,EAAE,CAAE,QAAO;AAE9B,SAAO;GAAE,OAD+B,OAAO,YAAY,OAAO,KAAK,KAAK,MAAM,CAAC,KAAI,SAAQ,CAAC,MAAM,IAAI,SAAQ,MAAK,EAAG,MAAO,CAAC,CAAC,CAAC;GACpH,SAAS;GAAmC;;CAE9D,MAAM,QAAQ,YAAY,MAAM,QAAQ,MAAM,EAAE,SAAS;AACzD,QAAO,SAAS,EAAE,OAAO;;;AAI3B,SAAgB,gBAAgB,OAAqB,SAA0B,MAAsE;AACnJ,MAAK,MAAM,QAAQ,MAAM,YAAY,EAAE,EAAE;EACvC,MAAM,QAAQ,aAAa,MAAM,SAAS,MAAM,MAAM,SAAS;AAC/D,MAAI,MAAO,QAAO;GAAE;GAAM;GAAO;;AAEnC,QAAO;;;AAIT,SAAgB,kBAAkB,OAAgC;CAChE,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,eAAe,MAAM,MAAM,OAAO,EAAE;EAC7C,MAAM,IAAI,6BAA6B,KAAK,YAAY;AACxD,MAAI,CAAC,KAAK,CAAC,sBAAsB,KAAK,EAAE,GAAI,CAAE,QAAO;AACrD,MAAI,EAAE,GAAI,OAAM,KAAK,EAAE,GAAG;;AAE5B,QAAO;;;AAIT,SAAgB,gBAAgB,OAAqB,SAA+B;CAClF,MAAM,WAAqB,EAAE;AAC7B,KAAI,MAAM,WAAA,sBAA2B,UAAS,KAAK,iBAAiB,iBAAiB;AACrF,KAAI,CAAC,aAAa,SAAS,MAAM,QAAQ,CAAE,UAAS,KAAK,mBAAmB,MAAM,UAAU;AAC5F,KAAI,CAAC,QAAQ,KAAK,MAAM,QAAQ,CAAE,UAAS,KAAK,mCAAmC;AACnF,MAAK,MAAM,WAAW,OAAO,KAAK,MAAM,YAAY,EAAE,CAAC,CAAE,KAAI,CAAC,iBAAiB,MAAM,SAAS,QAAQ,CAAE,UAAS,KAAK,YAAY,QAAQ,UAAU,MAAM,QAAQ,UAAU;CAC5K,MAAM,aAAa,IAAI,IAAI,QAAQ,WAAW,KAAI,MAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;CAClE,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,QAAQ,MAAM,OAAO;EAC9B,MAAM,KAAK,GAAG,MAAM,QAAQ,GAAG,KAAK,QAAQ,KAAK,OAAO,YAAY;EACpE,MAAM,MAAM,GAAG,KAAK,MAAM,GAAG,KAAK,UAAU,KAAK,QAAQ,EAAE,CAAC;AAC5D,MAAI,KAAK,IAAI,IAAI,CAAE,UAAS,KAAK,GAAG,GAAG,iBAAiB;AACxD,OAAK,IAAI,IAAI;AACb,MAAI,CAAC,iBAAiB,MAAM,SAAS,KAAK,MAAM,CAAE,UAAS,KAAK,GAAG,GAAG,UAAU,MAAM,QAAQ,UAAU;EACxG,MAAM,IAAI,WAAW,IAAI,KAAK,UAAU;AACxC,MAAI,CAAC,GAAG;AAAE,YAAS,KAAK,GAAG,GAAG,cAAc,KAAK,UAAU,wBAAwB;AAAE;;AACrF,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC,EAAE;GAE9D,MAAM,SAAS,iDAAiD,KAAK,MAAM;GAC3E,MAAM,UAAU,SAAS,CAAC,OAAO,IAAI,OAAO,GAAG,CAAC,QAAQ,WAA6B,WAAW,KAAA,EAAU,GAAG,CAAC,MAAM;AACpH,OAAI,CAAC,EAAE,SAAS,MAAO,UAAS,KAAK,GAAG,GAAG,IAAI,EAAE,GAAG,uBAAuB,OAAO;OAC7E,MAAK,MAAM,WAAW,QAAQ,QAAO,WAAU,CAAC,EAAE,SAAS,MAAO,QAAQ,SAAS,OAAO,CAAC,CAAE,UAAS,KAAK,GAAG,GAAG,IAAI,EAAE,GAAG,GAAG,KAAK,iBAAiB,UAAU;;AAEpK,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ;GAAE,GAAG,KAAK;GAAO,GAAI,KAAK,OAAO,EAAE,GAAG,KAAK;GAAO,CAAC,EAAE;AAC9F,OAAI,CAAC,EAAE,MAAM,MAAO,UAAS,KAAK,GAAG,GAAG,IAAI,EAAE,GAAG,eAAe,OAAO;AACvE,OAAI,CAAC,sBAAsB,KAAK,MAAM,CAAE,UAAS,KAAK,GAAG,GAAG,IAAI,KAAK,KAAK,MAAM,yBAAyB;;AAE3G,MAAI,KAAK,MAAM;GACb,MAAM,SAAS,EAAE,MAAM,KAAK;AAC5B,OAAI,CAAC,UAAU,OAAO,SAAS,QAAS,UAAS,KAAK,GAAG,GAAG,IAAI,EAAE,GAAG,GAAG,KAAK,KAAK,uBAAuB;GACzG,MAAM,SAAS,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,MAAM,UAAU,EAAE,GAAG,EAAE;AAC1F,QAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC,EAAE;AAC5D,QAAI,CAAC,OAAO,OAAQ,UAAS,KAAK,GAAG,GAAG,IAAI,EAAE,GAAG,GAAG,KAAK,KAAK,kBAAkB,QAAQ;AACxF,QAAI,CAAC,sBAAsB,KAAK,MAAM,CAAE,UAAS,KAAK,GAAG,GAAG,IAAI,KAAK,KAAK,KAAK,MAAM,KAAK,MAAM,yBAAyB;;AAE3H,OAAI,CAAC,KAAK,QAAQ,CAAC,KAAK,SAAS,CAAC,KAAK,KAAM,UAAS,KAAK,GAAG,GAAG,SAAS,KAAK,KAAK,gDAAgD;;AAEtI,MAAI,KAAK,QAAQ,CAAC,KAAK,QAAQ,CAAC,KAAK,KAAM,UAAS,KAAK,GAAG,GAAG,6BAA6B;EAC5F,MAAM,QAAQ,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC,EAAE,GAAI,KAAK,OAAO,CAAC,KAAK,KAAK,GAAG,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC,CAAE,CAAC;AACtH,OAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,EAAE,MAAM,CAAE,KAAI,IAAI,YAAY,CAAC,MAAM,IAAI,KAAK,CAAE,UAAS,KAAK,GAAG,GAAG,kBAAkB,EAAE,GAAG,GAAG,KAAK,gBAAgB;;AAE9J,UAAS,KAAK,GAAG,iBAAiB,OAAO,WAAW,CAAC;AACrD,QAAO;;AAGT,SAAS,iBAAiB,OAAqB,YAAiD;CAC9F,MAAM,WAAqB,EAAE;CAC7B,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,QAAQ,MAAM,YAAY,EAAE,EAAE;EACvC,MAAM,KAAK,GAAG,MAAM,QAAQ,WAAW,KAAK;AAC5C,MAAI,CAAC,qCAAqC,KAAK,KAAK,GAAG,CAAE,UAAS,KAAK,GAAG,GAAG,+BAA+B;AAC5G,MAAI,KAAK,IAAI,KAAK,GAAG,CAAE,UAAS,KAAK,GAAG,GAAG,gBAAgB;AAC3D,OAAK,IAAI,KAAK,GAAG;EACjB,MAAM,IAAI,WAAW,IAAI,KAAK,UAAU;AACxC,MAAI,CAAC,GAAG;AAAE,YAAS,KAAK,GAAG,GAAG,cAAc,KAAK,UAAU,wBAAwB;AAAE;;AACrF,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC,CAC5D,KAAI,CAAC,EAAE,SAAS,MAAO,UAAS,KAAK,GAAG,GAAG,IAAI,EAAE,GAAG,uBAAuB,OAAO;WACzE,CAAC,EAAE,SAAS,MAAO,QAAQ,SAAS,MAAM,CAAE,UAAS,KAAK,GAAG,GAAG,IAAI,EAAE,GAAG,GAAG,KAAK,iBAAiB,QAAQ;AAErH,MAAI,CAAC,OAAO,KAAK,KAAK,MAAM,CAAC,OAAQ,UAAS,KAAK,GAAG,GAAG,YAAY;AACrE,OAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,KAAK,MAAM,EAAE;AACrD,OAAI,CAAC,gBAAgB,KAAK,KAAK,CAAE,UAAS,KAAK,GAAG,GAAG,SAAS,KAAK,uBAAuB;AAC1F,QAAK,MAAM,WAAW,CAAC,KAAK,MAAM,CAAC,MAAM,CAAE,KAAI,CAAC,iBAAiB,MAAM,SAAS,QAAQ,CAAE,UAAS,KAAK,GAAG,GAAG,SAAS,KAAK,WAAW,QAAQ,UAAU,MAAM,QAAQ,UAAU;AACjL,OAAI,KAAK,QAAQ,KAAA,KAAa,KAAK,QAAQ,KAAA,KAAa,KAAK,MAAM,KAAK,IAAK,UAAS,KAAK,GAAG,GAAG,SAAS,KAAK,YAAY;;AAE7H,OAAK,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAE,KAAI,SAAS,SAAS,CAAC,iBAAiB,MAAM,SAAS,KAAK,CAAE,UAAS,KAAK,GAAG,GAAG,SAAS,KAAK,YAAY,MAAM,QAAQ,iBAAiB;AAC9L,MAAI,KAAK,MAAM,WAAW,aAAa,KAAK,SAAS,SAAU,UAAS,KAAK,GAAG,GAAG,qCAAqC;AACxH,MAAI,KAAK,SAAS,YAAY,KAAK,MAAM,WAAW,UAAW,UAAS,KAAK,GAAG,GAAG,yCAAyC;EAC5H,MAAM,cAAc,OAAe,OAAe,MAA0B,YAAqB;GAC/F,MAAM,QAAQ,kBAAkB,MAAM;AACtC,OAAI,CAAC,OAAO;AAAE,aAAS,KAAK,GAAG,GAAG,IAAI,MAAM,KAAK,MAAM,yBAAyB;AAAE;;AAClF,OAAI,CAAC,WAAW,MAAM,OAAQ,UAAS,KAAK,GAAG,GAAG,IAAI,MAAM,mCAAmC,KAAK,KAAK,sBAAsB;AAC/H,QAAK,MAAM,QAAQ,OAAO;IACxB,MAAM,MAAM,KAAK,MAAM;AACvB,QAAI,CAAC,IAAK,UAAS,KAAK,GAAG,GAAG,IAAI,MAAM,yBAAyB,OAAO;cAC9D,SAAS,YAAY,SAAS,WAAW,IAAI,QAAQ,EAAG,UAAS,KAAK,GAAG,GAAG,IAAI,MAAM,MAAM,KAAK,YAAY,KAAK,0CAA0C;;;AAG1K,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC,EAAE;GAC5D,MAAM,MAAM,EAAE,MAAM;AACpB,OAAI,CAAC,IAAK,UAAS,KAAK,GAAG,GAAG,IAAI,EAAE,GAAG,eAAe,OAAO;AAC7D,cAAW,OAAO,MAAM,KAAK,MAAM,KAAK;;AAE1C,MAAI,KAAK,MAAM;GACb,MAAM,SAAS,EAAE,MAAM,KAAK;AAC5B,OAAI,CAAC,UAAU,OAAO,SAAS,QAAS,UAAS,KAAK,GAAG,GAAG,IAAI,EAAE,GAAG,GAAG,KAAK,KAAK,uBAAuB;AACzG,OAAI,CAAC,KAAK,KAAM,UAAS,KAAK,GAAG,GAAG,SAAS,KAAK,KAAK,aAAa;YAC3D,KAAK,SAAS,YAAY,CAAC,KAAK,MAAM,6BAA6B,KAAK,KAAK,KAAK,GAAG,MAAM,IAAK,UAAS,KAAK,GAAG,GAAG,SAAS,KAAK,KAAK,+CAA+C;GAC/L,MAAM,SAAS,UAAU,OAAO,OAAO,UAAU,WAAW,OAAO,MAAM,UAAU,EAAE,GAAG,EAAE;AAC1F,QAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC,EAAE;AAC5D,QAAI,CAAC,OAAO,OAAQ,UAAS,KAAK,GAAG,GAAG,IAAI,EAAE,GAAG,GAAG,KAAK,KAAK,kBAAkB,QAAQ;AACxF,eAAW,OAAO,GAAG,KAAK,KAAK,KAAK,SAAS,OAAO,QAAQ,MAAM,KAAK,SAAS,SAAS;;aAElF,KAAK,QAAQ,KAAK,KAAM,UAAS,KAAK,GAAG,GAAG,0BAA0B;EACjF,MAAM,QAAQ,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC,EAAE,GAAI,KAAK,OAAO,CAAC,KAAK,KAAK,GAAG,EAAE,CAAE,CAAC;AAC5F,OAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,EAAE,MAAM,CAAE,KAAI,IAAI,YAAY,CAAC,MAAM,IAAI,KAAK,CAAE,UAAS,KAAK,GAAG,GAAG,kBAAkB,EAAE,GAAG,GAAG,KAAK,gBAAgB;;AAE9J,QAAO;;;AAIT,SAAgB,gBAAgB,OAAqB,SAA+B;CAClF,MAAM,UAAU,IAAI,IAAI,CAAC,GAAG,MAAM,MAAM,KAAI,MAAK,EAAE,MAAM,EAAE,IAAI,MAAM,YAAY,EAAE,EAAE,SAAQ,MAAK,OAAO,OAAO,EAAE,MAAM,CAAC,SAAQ,MAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;CAC1J,MAAM,UAAU,QAAQ,WAAW,SAAS,MAAoB,EAAE,QAAQ,MAAM,YAAY,EAAE,CAAC;AAC/F,QAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC,CAAC,QAAO,MAAK,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,UAAU;;;AAItE,SAAgB,SAAS,OAAqB,SAAgC;AAC5E,QAAO,MAAM,MACV,QAAO,MAAK,EAAE,UAAU,WAAW,EAAE,MAAM,MAAM,IAAI,CAAC,OAAO,QAAQ,CACrE,UAAU,GAAG,MAAM,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,SAAS,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,SAAS,IAAI,CAAC,CAAC;;;AAW9G,SAAgB,QAAQ,MAAmB,WAAqC,EAAE,EAA2B;AAC3G,QAAO;EAAE,GAAG,SAAS,KAAK;EAAO,GAAG,KAAK;EAAO;;;AAIlD,SAAgB,YAAY,MAAmB,MAAmB,UAA8C;CAC9G,MAAM,CAAC,MAAM,aAAa,KAAK,MAAM,MAAM,IAAI;AAC/C,KAAI,SAAS,KAAK,KAAM,QAAO;CAC/B,MAAM,QAAQ,QAAQ,MAAM,SAAS;AAErC,KAAI,aAAa,CAAC;EAAC,MAAM;EAAM,MAAM;EAAM,MAAM;EAAS,MAAM;EAAU,CAAC,SAAS,UAAU,CAAE,QAAO;AACvG,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,MAAM,QAAQ,EAAE,CAAC,CAAE,KAAI,MAAM,SAAS,MAAO,QAAO;AACnG,KAAI,KAAK,MAAM,SAAS,CAAC,OAAO,MAAM,aAAa,MAAM,WAAW,GAAG,CAAC,MAAM,MAAM,CAAC,SAAS,KAAK,KAAK,MAAM,CAAE,QAAO;AACvH,QAAO;;;;;;AAOT,SAAgB,aAAa,OAAsB,OAAqB,OAAO,KAA+D;AAC5I,QAAO,MAAM,SAAS,MAAM,MAAM;EAChC,MAAM,KAAK,GAAG,KAAK,GAAG;EACtB,MAAM,OAAO,SAAS,OAAO,KAAK,KAAK,CAAC,MAAK,MAAK,YAAY,GAAG,MAAM,MAAM,SAAS,CAAC;AACvF,SAAO,OAAO,CAAC;GAAE,MAAM;GAAI;GAAM;GAAM,CAAC,GAAG,aAAa,KAAK,YAAY,EAAE,EAAE,OAAO,GAAG;GACvF;;;;AClWJ,MAAM,eAAe,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,KAAK;;AAGxE,MAAa,qBAAqB,KAAK,cAAc,aAAa;;AAGlE,eAAsB,YAAY,SAA4C;AAC5E,QAAO,KAAK,MAAM,MAAM,SAAS,KAAK,cAAc,WAAW,GAAG,QAAQ,OAAO,EAAE,OAAO,CAAC;;;AAI7F,eAAsB,cAAmC;AACvD,QAAO,KAAK,MAAM,MAAM,SAAS,KAAK,cAAc,eAAe,EAAE,OAAO,CAAC"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/catalog.ts","../src/copy.ts","../src/mapping.ts","../src/index.ts"],"sourcesContent":["// The kit catalog: what each component is, what it takes, how it varies and\n// which WordPress builder elements it stands in for. Machine-readable, so the\n// migration writer chooses components by querying it instead of reading\n// source, and the builder mapping tables (mapping/*.json) are checked against\n// it.\n//\n// Props are described with Contentrain's own `FieldDef`, so a model field and\n// the prop it feeds are compared type to type.\n\nimport type { FieldDef } from '@contentrain/types'\nimport { fieldDepth, sectionFieldName } from '@contentrain/types'\n\nexport const CATALOG_FORMAT = 'astro-kit-catalog@1'\n\n/** The WordPress builders whose elements a component is fed from. */\nexport type KitBuilder = 'gutenberg' | 'elementor' | 'divi' | 'classic'\nexport const KIT_BUILDERS: readonly KitBuilder[] = ['gutenberg', 'elementor', 'divi', 'classic']\n\n/** Element-name prefix per builder, as `facts.json` names elements (`core/cover`, `elementor/heading`, `divi/et_pb_blurb`). */\nexport const BUILDER_PREFIX: Record<KitBuilder, string> = {\n gutenberg: 'core/',\n elementor: 'elementor/',\n divi: 'divi/',\n classic: 'classic/',\n}\n\n/**\n * Whether an element name belongs to a builder. Gutenberg blocks are namespaced by their plugin\n * (`core/cover`, `yoast-seo/breadcrumbs`, `uagb/container`), so any `<ns>/<name>` that is not another\n * builder's is a Gutenberg block.\n */\nexport function isBuilderElement(builder: KitBuilder, name: string): boolean {\n if (builder !== 'gutenberg') return name.startsWith(BUILDER_PREFIX[builder])\n return /^[a-z0-9-]+\\/[a-z0-9-]+(:[\\w-]+)?$/.test(name) && !KIT_BUILDERS.some(b => b !== 'gutenberg' && name.startsWith(BUILDER_PREFIX[b]))\n}\n\nexport type KitCategory = 'chrome' | 'section' | 'content' | 'primitive'\n\n/** Client JavaScript a component ships: none, a few lines of dependency-free script (`vanilla`), Embla (carousel), Zag.js (accessible widget) or the Studio runtime. */\nexport type KitJs = 'none' | 'vanilla' | 'embla' | 'zag' | 'studio'\n\nexport type KitProp = FieldDef & {\n description: string\n /**\n * `false`: a setting the plan or writer passes (heading level, a label from `ui:`, an anchor id), not page\n * content. A section's page-singleton field holds only the other props, its content props.\n */\n content?: false\n}\n\n/**\n * A content prop's name: camelCase, a capital only to start a word (`imageAlt`, not `imageURL`), so it\n * turns into exactly one Contentrain field name.\n */\nexport const CONTENT_PROP_NAME = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)*$/\n\n/**\n * The page-singleton field a content prop is stored in (DECISIONS §7b): the prop name in snake_case, as\n * Contentrain field names are (`ctaLabel` → `cta_label`). The view passes each prop from its field.\n */\nexport const contentFieldName: (prop: string) => string = sectionFieldName\n\n/** The props a section's page-singleton field holds: all but the settings. */\nexport function contentProps(component: KitComponentMeta): Record<string, KitProp> {\n return Object.fromEntries(Object.entries(component.props).filter(([, prop]) => prop.content !== false))\n}\n\n/** Fields under a field, by dotted path (`items[].imageAlt`). */\nfunction fieldsByPath(fields: Record<string, FieldDef>, prefix = ''): Array<[string, FieldDef]> {\n const out: Array<[string, FieldDef]> = []\n for (const [name, field] of Object.entries(fields)) {\n out.push([prefix + name, field], ...fieldsByPath(field.fields ?? {}, `${prefix}${name}.`))\n if (typeof field.items === 'object') out.push(...fieldsByPath(field.items.fields ?? {}, `${prefix}${name}[].`))\n }\n return out\n}\n\n/** A content prop path as its page-singleton field path: every name in it through `contentFieldName`. */\nexport function contentFieldPath(path: string): string {\n return path.replace(/[A-Za-z0-9]+/g, contentFieldName)\n}\n\nexport interface KitVariant {\n options: string[]\n default: string\n description: string\n}\n\n/** One component as written in `components/<id>/meta.json`. */\nexport interface KitComponentMeta {\n id: string\n name: string\n description: string\n category: KitCategory\n /** Kit components this one renders. */\n uses: string[]\n /** Files it imports from `components/_shared/`. */\n shared: string[]\n variants: Record<string, KitVariant>\n props: Record<string, KitProp>\n /** Named slots → what goes in them. `default` is the unnamed slot. */\n slots: Record<string, string>\n js: KitJs\n /** npm packages the site needs for this component, name → range. */\n dependencies: Record<string, string>\n /** Builder elements this component replaces, by builder — what the mapping tables map from. */\n sources: Partial<Record<KitBuilder, string[]>>\n /** How it meets its accessibility pattern. */\n a11y: string\n /**\n * Sections only: the section's shape in a few words and the signals that show it in a source page, a\n * closed-set label for the classifier and the migration architect (`image beside heading + text`).\n */\n pattern?: { summary: string, signals: string[] }\n /**\n * Sections only, written by the catalog build: each content prop path (`slides[].ctaLabel`) → the\n * page-singleton field path that holds it (`slides[].cta_label`).\n */\n contentFields?: Record<string, string>\n}\n\n/** A catalog entry: the meta plus the files that make the component. */\nexport interface KitComponent extends KitComponentMeta {\n /** Paths relative to `components/<id>/`. */\n files: string[]\n}\n\nexport interface KitCatalog {\n format: typeof CATALOG_FORMAT\n /** Design tokens (Tailwind @theme names) the components read; the site must define them. */\n tokens: string[]\n /** npm packages every kit component needs. */\n dependencies: Record<string, string>\n components: KitComponent[]\n}\n\n/** Problems with a catalog, one line each; empty when it is sound. */\nexport function validateCatalog(catalog: KitCatalog): string[] {\n const problems: string[] = []\n const ids = new Set(catalog.components.map(c => c.id))\n for (const c of catalog.components) {\n const at = `component ${c.id}`\n if (!/^[a-z][a-z0-9-]*$/.test(c.id)) problems.push(`${at}: id must be kebab-case`)\n if (!c.files.length) problems.push(`${at}: no files`)\n for (const used of c.uses) if (!ids.has(used)) problems.push(`${at}: uses unknown component \"${used}\"`)\n for (const [axis, variant] of Object.entries(c.variants)) {\n if (!variant.options.includes(variant.default)) problems.push(`${at}: variant ${axis} default \"${variant.default}\" is not an option`)\n }\n for (const [name, prop] of Object.entries(c.props)) {\n if (!prop.description) problems.push(`${at}: prop ${name} has no description`)\n if (prop.type === 'select' && !prop.options?.length) problems.push(`${at}: select prop ${name} has no options`)\n }\n if (c.category === 'section') {\n // A section is stored as one object field of its page singleton: each content prop needs one field\n // name there, and that field must stay within two object levels.\n const content = contentProps(c)\n const paths = fieldsByPath(content)\n for (const [path, field] of paths) {\n if (!CONTENT_PROP_NAME.test(path.split(/\\.|\\[\\]\\./).at(-1)!)) problems.push(`${at}: content prop ${path} must be camelCase with one capital per word (or content: false)`)\n // Studio shows both to editors, and the plan reads them as the field's label and help text.\n if (!field.label) problems.push(`${at}: content prop ${path} has no label`)\n if (!field.description) problems.push(`${at}: content prop ${path} has no description`)\n }\n if (c.contentFields) {\n const expected = Object.fromEntries(paths.map(([path]) => [path, contentFieldPath(path)]))\n if (JSON.stringify(c.contentFields) !== JSON.stringify(expected)) problems.push(`${at}: contentFields is stale — rebuild the catalog`)\n }\n for (const [name, prop] of Object.entries(content)) {\n if (1 + fieldDepth(prop) > 2) problems.push(`${at}: content prop ${name} nests objects deeper than a page singleton allows`)\n }\n }\n for (const [builder, names] of Object.entries(c.sources) as Array<[KitBuilder, string[]]>) {\n const prefix = BUILDER_PREFIX[builder]\n for (const name of names) if (!isBuilderElement(builder, name)) problems.push(`${at}: source \"${name}\" is not a ${builder} element (${prefix}…)`)\n }\n }\n const seen = new Set<string>()\n for (const c of catalog.components) {\n if (seen.has(c.id)) problems.push(`component ${c.id}: listed twice`)\n seen.add(c.id)\n }\n return problems\n}\n\n/** Components that replace a builder element, e.g. `core/cover` → hero. */\nexport function componentsForSource(catalog: KitCatalog, element: string): KitComponent[] {\n return catalog.components.filter(c => Object.values(c.sources).some(names => names?.includes(element)))\n}\n","// Copying components into a site — the shadcn model: the site owns the\n// source, there is no runtime dependency on this package. A component is\n// copied with every kit component it renders and every shared file it\n// imports, under `src/components/kit/`, keeping the relative layout its\n// imports expect.\n\nimport { copyFile, mkdir } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport type { KitCatalog } from './catalog.js'\n\n/** Where kit files land in a site, relative to the site root. */\nexport const KIT_DIR = 'src/components/kit'\n\nexport interface CopyPlan {\n /** Components copied, the requested ones and everything they render, sorted. */\n components: string[]\n /** Site-relative destination → path relative to the kit's `components/` directory. */\n files: Record<string, string>\n /** npm packages the site needs, name → range. */\n dependencies: Record<string, string>\n}\n\nexport function planCopy(catalog: KitCatalog, ids: readonly string[]): CopyPlan {\n const byId = new Map(catalog.components.map(c => [c.id, c]))\n const wanted = new Set<string>()\n const visit = (id: string) => {\n if (wanted.has(id)) return\n const component = byId.get(id)\n if (!component) throw new Error(`astro-kit has no component \"${id}\". Known: ${[...byId.keys()].join(', ')}.`)\n wanted.add(id)\n for (const used of component.uses) visit(used)\n }\n for (const id of ids) visit(id)\n\n const files: Record<string, string> = {}\n const dependencies: Record<string, string> = { ...catalog.dependencies }\n for (const id of [...wanted].toSorted()) {\n const component = byId.get(id)!\n for (const file of component.files) files[`${KIT_DIR}/${id}/${file}`] = `${id}/${file}`\n for (const file of component.shared) files[`${KIT_DIR}/_shared/${file}`] = `_shared/${file}`\n Object.assign(dependencies, component.dependencies)\n }\n return { components: [...wanted].toSorted(), files, dependencies }\n}\n\n/** Carry out a plan: `kitRoot` is the kit's `components/` directory, `siteRoot` the site. */\nexport async function copyComponents(plan: CopyPlan, kitRoot: string, siteRoot: string): Promise<void> {\n await Promise.all(Object.entries(plan.files).map(async ([dest, source]) => {\n const target = join(siteRoot, dest)\n await mkdir(dirname(target), { recursive: true })\n await copyFile(join(kitRoot, source), target)\n }))\n}\n","// Builder mapping tables: which kit component a WordPress builder element\n// becomes, and where each of its props comes from. One versioned table per\n// builder (`mapping/<builder>.json`), written once and applied\n// deterministically to every site — the migration model is asked only about\n// elements no rule covers (Migrate's `unmapped_element` decision).\n//\n// A prop's source is one expression from a small closed set. The first six are\n// the project plan's values (`@contentrain/types` `PlanValue`) and bind\n// content; the rest read the builder element itself or the page it is on:\n//\n// field:<name> · media:<field> · ref:<field>.<field> · href:self|<field> · term:<field> · ui:<key> · const:<value>\n// site:<field> a field of the `site` singleton (`site:title`, `site:logo`)\n// menu:primary|footer the menu the starter shows in that area\n// page:<value> what the route knows: `page:base`, `page:current`, `page:total`, `page:breadcrumb`\n// attr:<name> a builder attribute (block attribute, Elementor setting, Divi shortcode attribute)\n// dom:<selector> text of the first match inside the element (empty selector: the element)\n// dom:<selector>@<attr> an attribute of it (`dom:a@href`)\n// img:<selector> an image as the kit's `ImageInput` (src, alt, width, height)\n// img:<selector>@src|@alt one part of that image, for a list item's flat `image` + `imageAlt` fields\n// link:<selector> a link as `{ label, href }`\n// html:<selector> inner markup, kept as Markdown (rich text)\n// class:<cls>=<a>|<b> <a> when the element, or an ancestor inside the matched element, has class\n// <cls> (`*` matches any run of characters); <b> otherwise. An empty branch\n// reads nothing and the prop keeps its default. `class:is-style-outline=ghost|primary`\n// is WordPress's outline button as the kit's ghost action.\n//\n// Section rules (`sections`) classify a whole page section by the leaves it holds and fill the component's\n// props from them: the same expressions, read from a named slot's leaf (`@title attr:title`).\n\nimport type { KitBuilder, KitCatalog, KitComponent } from './catalog.js'\nimport { isBuilderElement, KIT_BUILDERS } from './catalog.js'\n\nexport const MAPPING_FORMAT = 'astro-kit-mapping@1'\n\nexport 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-]+)?)$/\n\nexport interface MappingRule {\n /** Builder element, as the fact pack names it; a `:qualifier` narrows it (`core/template-part:header`). */\n match: string\n /** Extra conditions on the element. */\n when?: { attr?: Record<string, string | number | boolean>, class?: string }\n /** Kit component id. */\n component: string\n variant?: Record<string, string>\n /** Content the element lists rather than holds: one component per entry of the collection (`collection:posts`). */\n bind?: `collection:${string}`\n /** Consecutive sibling matches form one component (a row of Elementor icon boxes is one card grid). */\n group?: 'siblings'\n props?: Record<string, string>\n /** Array prop filled from repeated parts of the element (`each`) or the grouped siblings / bound entries. */\n into?: string\n /** Selector of the repeated parts inside the element, for `into`. */\n each?: string\n /** Props of one repeated part / sibling / entry, with sources relative to it. */\n item?: Record<string, string>\n}\n\nexport interface MappingTable {\n format: typeof MAPPING_FORMAT\n builder: KitBuilder\n /** Bumped on any rule change: plans record the version they applied. */\n version: string\n /** What an element no rule covers becomes when no decision is made: rich text. */\n fallback: 'prose'\n /**\n * Attribute values the builder leaves out of its data while they are at their default, per element:\n * Elementor saves no `video_type` for a YouTube video. `when.attr` and `attr:` read through them.\n */\n defaults?: Record<string, Record<string, string | number | boolean>>\n rules: MappingRule[]\n /**\n * Section rules: what a whole section of a page (an Elementor container, a Gutenberg group or columns,\n * a Divi section — facts `sections[]`) becomes, tried top to bottom before any element rule. The first\n * that matches wins; a section none matches falls through to the element `rules` inside it.\n */\n sections?: SectionRule[]\n}\n\n/**\n * A named group of a section's leaves: every leaf named `match` (and carrying `attr`) up to `max` of them.\n * `attr` values may list alternatives with `|` (`{ header_size: 'h1|h2' }`).\n */\nexport interface SectionSlot {\n match: string | string[]\n attr?: Record<string, string | number | boolean>\n /** At least this many leaves (default 0). */\n min?: number\n /** At most this many (default unbounded). */\n max?: number\n}\n\n/**\n * One section pattern. Values are the table's expressions, prefixed `@<slot> ` to read a slot's leaf\n * (`@title attr:title`); `a || b` takes the first non-empty (builder data first, rendered markup as fallback).\n * A slot of several leaves joins their text into a markdown prop; any other prop reads its first leaf.\n * A value without a slot reads the section's own root (a cover's background image, a container's setting).\n */\nexport interface SectionRule {\n /** Stable id (`hero.split`, `card-grid.icon-box`): plans record `<builder>:section:<id>`; JEW chooses among them. */\n id: string\n component: string\n variant?: Record<string, string>\n when?: {\n /** Where on the page: `first` is the page's first section, `last` its last. */\n position?: 'first' | 'last' | 'any'\n /** Column count(s) the section must have. */\n columns?: number | number[]\n /** Every leaf must fall into a slot (default true); false lets unclaimed leaves be. */\n only?: boolean\n /** `columns`: each column must match the slots on its own (a row of team members, price plans). */\n repeat?: 'columns'\n /** The section's root element(s): `core/cover`, `core/media-text` (a loose run of blocks is `run`). */\n root?: string | string[]\n }\n /**\n * Named leaf groups. Each leaf, in document order, goes to the first declared slot that matches it and\n * still has room, so declare a specific slot (an `attr` filter) before a general one on the same element.\n */\n slots: Record<string, SectionSlot>\n props?: Record<string, string>\n /**\n * Array prop filled once per `each`: a slot's leaves (`@card`), the parts a selector finds inside them\n * (`@list figure.wp-block-image`, `@faq .elementor-accordion-item`: a gallery's images, an accordion's\n * items), or the columns (`column`, with `repeat: 'columns'`).\n */\n into?: string\n each?: string\n /** One array item: relative to the leaf (`each: '@slot'`) or to the column, where `@<slot>` is that column's slot. */\n item?: Record<string, string>\n}\n\n/** A section leaf as the fact pack gives it: a builder element with its `blocks:` path. */\nexport interface SectionLeaf extends MappingNode {\n path: string\n}\n\n/** Where a section sits (facts `index`/`count`) and what it is (`root`: facts `root`). */\nexport interface SectionMeta {\n index: number\n count: number\n root?: string | undefined\n}\n\n/** Leaf paths per slot; with `repeat: 'columns'`, also per column. */\nexport interface SectionMatch {\n slots: Record<string, string[]>\n columns?: Record<string, string[]>[]\n}\n\n/** The id a plan records for a section rule. */\nexport function sectionRuleId(builder: KitBuilder, rule: Pick<SectionRule, 'id'>): string {\n return `${builder}:section:${rule.id}`\n}\n\nconst SECTION_LAYOUT_LEAVES = new Set(['elementor/spacer', 'elementor/divider', 'core/spacer', 'core/separator', 'divi/et_pb_divider'])\n\nfunction slotFits(slot: SectionSlot, leaf: SectionLeaf, defaults?: MappingTable['defaults']): boolean {\n if (![slot.match].flat().includes(leaf.name)) return false\n const attrs = attrsOf(leaf, defaults)\n return Object.entries(slot.attr ?? {}).every(([key, want]) => String(want).split('|').includes(String(attrs[key])))\n}\n\n/** Assign leaves to slots in document order, each to the first declared slot with room; null when the rule does not fit. */\nfunction claimLeaves(rule: SectionRule, leaves: SectionLeaf[], defaults?: MappingTable['defaults']): Record<string, string[]> | null {\n const claimed: Record<string, string[]> = Object.fromEntries(Object.keys(rule.slots).map(name => [name, []]))\n for (const leaf of leaves) {\n if (SECTION_LAYOUT_LEAVES.has(leaf.name)) continue\n const slot = Object.entries(rule.slots).find(([name, def]) => claimed[name]!.length < (def.max ?? Infinity) && slotFits(def, leaf, defaults))\n if (slot) claimed[slot[0]]!.push(leaf.path)\n else if (rule.when?.only !== false) return null\n }\n for (const [name, def] of Object.entries(rule.slots)) if (claimed[name]!.length < (def.min ?? 0)) return null\n return claimed\n}\n\n/**\n * Whether a section rule fits a section, and which leaves fill which slot. `columns` are the section's\n * columns left to right, each its leaves in document order (layout containers already opened).\n */\nexport function matchSection(rule: SectionRule, columns: SectionLeaf[][], meta: SectionMeta, defaults?: MappingTable['defaults']): SectionMatch | null {\n const when = rule.when ?? {}\n if (when.root !== undefined && !(meta.root !== undefined && [when.root].flat().includes(meta.root))) return null\n if (when.position === 'first' && meta.index !== 0) return null\n if (when.position === 'last' && meta.index !== meta.count - 1) return null\n if (when.columns !== undefined && ![when.columns].flat().includes(columns.length)) return null\n if (when.repeat === 'columns') {\n if (columns.length < 2) return null\n const per = columns.map(leaves => claimLeaves(rule, leaves, defaults))\n if (per.some(c => !c)) return null\n const slots: Record<string, string[]> = Object.fromEntries(Object.keys(rule.slots).map(name => [name, per.flatMap(c => c![name]!)]))\n return { slots, columns: per as Record<string, string[]>[] }\n }\n const slots = claimLeaves(rule, columns.flat(), defaults)\n return slots && { slots }\n}\n\n/** The first section rule of a table that fits, with its match. */\nexport function classifySection(table: MappingTable, columns: SectionLeaf[][], meta: SectionMeta): { rule: SectionRule, match: SectionMatch } | null {\n for (const rule of table.sections ?? []) {\n const match = matchSection(rule, columns, meta, table.defaults)\n if (match) return { rule, match }\n }\n return null\n}\n\n/** A section value: alternatives joined by ` || `, each an optional `@<slot> ` then a mapping value. Returns the slots it reads, or null when malformed. */\nexport function sectionValueSlots(value: string): string[] | null {\n const slots: string[] = []\n for (const alternative of value.split(' || ')) {\n const m = /^(?:@([a-z][\\w-]*) )?(.+)$/.exec(alternative)\n if (!m || !MAPPING_VALUE_PATTERN.test(m[2]!)) return null\n if (m[1]) slots.push(m[1])\n }\n return slots\n}\n\n/** Problems with a mapping table against the catalog, one line each; empty when it is sound. */\nexport function validateMapping(table: MappingTable, catalog: KitCatalog): string[] {\n const problems: string[] = []\n if (table.format !== MAPPING_FORMAT) problems.push(`format is not ${MAPPING_FORMAT}`)\n if (!KIT_BUILDERS.includes(table.builder)) problems.push(`unknown builder ${table.builder}`)\n if (!/^\\d+$/.test(table.version)) problems.push('version is not an integer string')\n for (const element of Object.keys(table.defaults ?? {})) if (!isBuilderElement(table.builder, element)) problems.push(`defaults ${element}: not a ${table.builder} element`)\n const components = new Map(catalog.components.map(c => [c.id, c]))\n const seen = new Set<string>()\n for (const rule of table.rules) {\n const at = `${table.builder} ${rule.match}${rule.when ? ' (when)' : ''}`\n const key = `${rule.match}|${JSON.stringify(rule.when ?? {})}`\n if (seen.has(key)) problems.push(`${at}: matched twice`)\n seen.add(key)\n if (!isBuilderElement(table.builder, rule.match)) problems.push(`${at}: not a ${table.builder} element`)\n const c = components.get(rule.component)\n if (!c) { problems.push(`${at}: component ${rule.component} is not in the catalog`); continue }\n for (const [axis, value] of Object.entries(rule.variant ?? {})) {\n // A variant is one option, or a `class:` choice between options read from the element.\n const choice = /^class:[\\w*-]+=([a-z][\\w-]*)?\\|([a-z][\\w-]*)?$/.exec(value)\n const options = choice ? [choice[1], choice[2]].filter((option): option is string => option !== undefined) : [value]\n if (!c.variants[axis]) problems.push(`${at}: ${c.id} has no variant axis ${axis}`)\n else for (const missing of options.filter(option => !c.variants[axis]!.options.includes(option))) problems.push(`${at}: ${c.id}.${axis} has no option ${missing}`)\n }\n for (const [prop, value] of Object.entries({ ...rule.props, ...(rule.into ? {} : rule.item) })) {\n if (!c.props[prop]) problems.push(`${at}: ${c.id} has no prop ${prop}`)\n if (!MAPPING_VALUE_PATTERN.test(value)) problems.push(`${at}: ${prop} = ${value} is not a mapping value`)\n }\n if (rule.into) {\n const target = c.props[rule.into]\n if (!target || target.type !== 'array') problems.push(`${at}: ${c.id}.${rule.into} is not an array prop`)\n const fields = target && typeof target.items === 'object' ? target.items.fields ?? {} : {}\n for (const [field, value] of Object.entries(rule.item ?? {})) {\n if (!fields[field]) problems.push(`${at}: ${c.id}.${rule.into}[] has no field ${field}`)\n if (!MAPPING_VALUE_PATTERN.test(value)) problems.push(`${at}: ${rule.into}[].${field} = ${value} is not a mapping value`)\n }\n if (!rule.each && !rule.group && !rule.bind) problems.push(`${at}: into ${rule.into} needs each, group or bind to say what repeats`)\n }\n if (rule.item && !rule.into && !rule.bind) problems.push(`${at}: item without into or bind`)\n const bound = new Set([...Object.keys(rule.props ?? {}), ...(rule.into ? [rule.into] : Object.keys(rule.item ?? {}))])\n for (const [prop, def] of Object.entries(c.props)) if (def.required && !bound.has(prop)) problems.push(`${at}: required prop ${c.id}.${prop} has no source`)\n }\n problems.push(...validateSections(table, components))\n return problems\n}\n\nfunction validateSections(table: MappingTable, components: Map<string, KitComponent>): string[] {\n const problems: string[] = []\n const seen = new Set<string>()\n for (const rule of table.sections ?? []) {\n const at = `${table.builder} section ${rule.id}`\n if (!/^[a-z][a-z0-9-]*(?:\\.[a-z0-9-]+)*$/.test(rule.id)) problems.push(`${at}: id is not dotted kebab-case`)\n if (seen.has(rule.id)) problems.push(`${at}: listed twice`)\n seen.add(rule.id)\n const c = components.get(rule.component)\n if (!c) { problems.push(`${at}: component ${rule.component} is not in the catalog`); continue }\n for (const [axis, value] of Object.entries(rule.variant ?? {})) {\n if (!c.variants[axis]) problems.push(`${at}: ${c.id} has no variant axis ${axis}`)\n else if (!c.variants[axis]!.options.includes(value)) problems.push(`${at}: ${c.id}.${axis} has no option ${value}`)\n }\n if (!Object.keys(rule.slots).length) problems.push(`${at}: no slots`)\n for (const [name, slot] of Object.entries(rule.slots)) {\n if (!/^[a-z][\\w-]*$/.test(name)) problems.push(`${at}: slot ${name} is not an identifier`)\n // A form or shortcode plugin (`classic/contact-form-7`) sits inside any builder's section, as the plan reads it from a shortcode.\n for (const element of [slot.match].flat()) if (!isBuilderElement(table.builder, element) && !isBuilderElement('classic', element)) problems.push(`${at}: slot ${name} matches ${element}, not a ${table.builder} or classic plugin element`)\n if (slot.min !== undefined && slot.max !== undefined && slot.min > slot.max) problems.push(`${at}: slot ${name} min > max`)\n }\n for (const root of [rule.when?.root ?? []].flat()) if (root !== 'run' && !isBuilderElement(table.builder, root)) problems.push(`${at}: root ${root} is not a ${table.builder} element or run`)\n if (rule.when?.repeat === 'columns' && rule.each !== 'column') problems.push(`${at}: repeat columns needs each: column`)\n if (rule.each === 'column' && rule.when?.repeat !== 'columns') problems.push(`${at}: each column needs when.repeat columns`)\n const readsSlots = (value: string, where: string, type: string | undefined, slotted: boolean) => {\n const slots = sectionValueSlots(value)\n if (!slots) { problems.push(`${at}: ${where} = ${value} is not a section value`); return }\n if (!slotted && slots.length) problems.push(`${at}: ${where} reads a slot, but items of each ${rule.each} read their own leaf`)\n for (const slot of slots) {\n const def = rule.slots[slot]\n if (!def) problems.push(`${at}: ${where} reads undeclared slot ${slot}`)\n else if ((type === 'string' || type === 'text') && def.max !== 1) problems.push(`${at}: ${where} is ${type} but slot ${slot} may hold several leaves (max must be 1)`)\n }\n }\n for (const [prop, value] of Object.entries(rule.props ?? {})) {\n const def = c.props[prop]\n if (!def) problems.push(`${at}: ${c.id} has no prop ${prop}`)\n readsSlots(value, prop, def?.type, true)\n }\n if (rule.into) {\n const target = c.props[rule.into]\n if (!target || target.type !== 'array') problems.push(`${at}: ${c.id}.${rule.into} is not an array prop`)\n if (!rule.each) problems.push(`${at}: into ${rule.into} needs each`)\n else if (rule.each !== 'column' && !rule.slots[/^@([a-z][\\w-]*)(?: \\S.*)?$/.exec(rule.each)?.[1] ?? '']) problems.push(`${at}: each ${rule.each} is not @<declared slot> [selector] or column`)\n const fields = target && typeof target.items === 'object' ? target.items.fields ?? {} : {}\n for (const [field, value] of Object.entries(rule.item ?? {})) {\n if (!fields[field]) problems.push(`${at}: ${c.id}.${rule.into}[] has no field ${field}`)\n readsSlots(value, `${rule.into}[].${field}`, fields[field]?.type, rule.each === 'column')\n }\n } else if (rule.item || rule.each) problems.push(`${at}: each/item without into`)\n const bound = new Set([...Object.keys(rule.props ?? {}), ...(rule.into ? [rule.into] : [])])\n for (const [prop, def] of Object.entries(c.props)) if (def.required && !bound.has(prop)) problems.push(`${at}: required prop ${c.id}.${prop} has no source`)\n }\n return problems\n}\n\n/** Catalog sources of a builder that no rule of its table matches, as an element rule or a section slot. */\nexport function unmappedSources(table: MappingTable, catalog: KitCatalog): string[] {\n const matched = new Set([...table.rules.map(r => r.match), ...(table.sections ?? []).flatMap(r => Object.values(r.slots).flatMap(s => [s.match].flat()))])\n const sources = catalog.components.flatMap((c: KitComponent) => c.sources[table.builder] ?? [])\n return [...new Set(sources)].filter(s => !matched.has(s)).toSorted()\n}\n\n/** Rules for an element, most specific (with `when` / qualifier) first. */\nexport function rulesFor(table: MappingTable, element: string): MappingRule[] {\n return table.rules\n .filter(r => r.match === element || r.match.split(':')[0] === element)\n .toSorted((a, b) => Number(!!b.when || b.match.includes(':')) - Number(!!a.when || a.match.includes(':')))\n}\n\n/** A builder element as the fact pack gives it (`FactBlock`): name, attributes, children. */\nexport interface MappingNode {\n name: string\n attrs?: Record<string, unknown>\n children?: MappingNode[]\n}\n\n/** An element's attributes with the builder's unsaved defaults filled in. */\nexport function attrsOf(node: MappingNode, defaults: MappingTable['defaults'] = {}): Record<string, unknown> {\n return { ...defaults[node.name], ...node.attrs }\n}\n\n/** Whether a rule's `match` (with an optional `:qualifier`) and `when.attr` fit an element. */\nexport function ruleMatches(rule: MappingRule, node: MappingNode, defaults?: MappingTable['defaults']): boolean {\n const [name, qualifier] = rule.match.split(':')\n if (name !== node.name) return false\n const attrs = attrsOf(node, defaults)\n // A qualifier names the element's role: a template part's area or slug, or a sub-part the fact pack marks.\n if (qualifier && ![attrs.area, attrs.slug, attrs.tagName, attrs.qualifier].includes(qualifier)) return false\n for (const [key, value] of Object.entries(rule.when?.attr ?? {})) if (attrs[key] !== value) return false\n if (rule.when?.class && !String(attrs.className ?? attrs.classes ?? '').split(/\\s+/).includes(rule.when.class)) return false\n return true\n}\n\n/**\n * Apply a table to a tree: the outermost matching element owns its whole subtree, and nothing inside\n * it is matched again — a header template part holding the site title and logo is one header, not three.\n */\nexport function claimMatches(nodes: MappingNode[], table: MappingTable, path = '0'): { path: string, node: MappingNode, rule: MappingRule }[] {\n return nodes.flatMap((node, i) => {\n const at = `${path}.${i}`\n const rule = rulesFor(table, node.name).find(r => ruleMatches(r, node, table.defaults))\n return rule ? [{ path: at, node, rule }] : claimMatches(node.children ?? [], table, at)\n })\n}\n","import { readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { KitBuilder, KitCatalog } from './catalog.js'\nimport type { MappingTable } from './mapping.js'\n\nexport * from './catalog.js'\nexport * from './copy.js'\nexport * from './mapping.js'\n\nconst PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')\n\n/** The kit's `components/` directory — the source `copyComponents` copies from. */\nexport const KIT_COMPONENTS_DIR = join(PACKAGE_ROOT, 'components')\n\n/** A builder's mapping table (`mapping/<builder>.json`). */\nexport async function loadMapping(builder: KitBuilder): Promise<MappingTable> {\n return JSON.parse(await readFile(join(PACKAGE_ROOT, 'mapping', `${builder}.json`), 'utf8')) as MappingTable\n}\n\n/** The published catalog (`catalog.json`). */\nexport async function loadCatalog(): Promise<KitCatalog> {\n return JSON.parse(await readFile(join(PACKAGE_ROOT, 'catalog.json'), 'utf8')) as KitCatalog\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "astro-kit-mapping@1",
3
3
  "builder": "elementor",
4
- "version": "6",
4
+ "version": "7",
5
5
  "fallback": "prose",
6
6
  "defaults": {
7
7
  "elementor/video": {
@@ -854,16 +854,26 @@
854
854
  "match": "elementor/heading",
855
855
  "max": 1
856
856
  },
857
- "formTitle": {
858
- "match": "elementor/heading",
859
- "max": 1
860
- },
861
857
  "intro": {
862
858
  "match": "elementor/text-editor",
863
859
  "max": 1
864
860
  },
861
+ "details": {
862
+ "match": "elementor/icon-list",
863
+ "max": 1
864
+ },
865
+ "formTitle": {
866
+ "match": "elementor/heading",
867
+ "max": 1
868
+ },
865
869
  "form": {
866
- "match": "elementor/form",
870
+ "match": [
871
+ "elementor/form",
872
+ "classic/contact-form-7",
873
+ "classic/wpforms",
874
+ "classic/gravityforms",
875
+ "classic/ninja-forms"
876
+ ],
867
877
  "min": 1,
868
878
  "max": 1
869
879
  }
@@ -873,6 +883,12 @@
873
883
  "heading": "@title attr:title || @title dom: || @form attr:form_name",
874
884
  "intro": "@intro dom:",
875
885
  "formHeading": "@formTitle attr:title || @formTitle dom:"
886
+ },
887
+ "into": "details",
888
+ "each": "@details .elementor-icon-list-item",
889
+ "item": {
890
+ "value": "dom:.elementor-icon-list-text",
891
+ "href": "dom:a@href"
876
892
  }
877
893
  },
878
894
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/astro-kit",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "description": "Copy-in Astro components for Contentrain sites (shadcn model): typed props, tailwind-variants, zero JavaScript by default — with a machine-readable catalog",
6
6
  "type": "module",