@contentrain/astro-kit 0.26.1 → 0.27.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
@@ -539,6 +539,14 @@
539
539
  ],
540
540
  "default": "default",
541
541
  "description": "Section background; the form always sits on a surface card."
542
+ },
543
+ "headingPlace": {
544
+ "options": [
545
+ "top",
546
+ "after"
547
+ ],
548
+ "default": "top",
549
+ "description": "Where the heading sits. top: above the introduction and details. after: after them, as a source that writes its lead line and contact buttons first has it; beside a form it titles the form, without one it follows the details."
542
550
  }
543
551
  },
544
552
  "props": {
@@ -43,6 +43,10 @@ const contact = tv({
43
43
  // The stacked column: centred in a reading width, or the section's whole column from its start edge (a page
44
44
  // builder's container whose text has no alignment of its own sits left, full width).
45
45
  align: { center: {}, start: {} },
46
+ // Where the section's heading sits: above everything (the default), or after the introduction and the details,
47
+ // where a source that writes its lead line and contact buttons first puts it (G-text: output order = source order).
48
+ // Beside a form it then titles the form; without one it follows the details.
49
+ headingPlace: { top: {}, after: {} },
46
50
  // 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.
47
51
  tone: { default: { form: 'border border-line bg-surface p-6 md:p-8' }, muted: { form: 'bg-surface p-6 text-ink md:p-8' }, inverse: { form: 'bg-surface p-6 text-ink md:p-8' }, accent: { form: 'bg-surface p-6 text-ink md:p-8', intro: 'opacity-100', term: 'opacity-100' } },
48
52
  },
@@ -63,16 +67,22 @@ interface Props {
63
67
  layout?: 'split' | 'stacked' | undefined
64
68
  /** Stacked only (and the details alone, before a Studio form): a centred column, or the whole column from the start edge. */
65
69
  align?: 'center' | 'start' | undefined
70
+ /** `after`: the heading follows the introduction and the details (as the source wrote them), titling the form when there is one. */
71
+ headingPlace?: 'top' | 'after' | undefined
66
72
  tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
67
73
  id?: string | undefined
68
74
  }
69
75
 
70
- const { model, heading, intro, formHeading, details = [], layout, align, tone, id = 'contact' } = Astro.props
76
+ const { model, heading, intro, formHeading, details = [], layout, align, tone, headingPlace = 'top', id = 'contact' } = Astro.props
71
77
  // Nothing to render (Studio not bound yet): a split would leave an empty column — the details stand alone.
72
78
  const formShown = formHome(model) !== null
73
79
  const bare = contactSectionBare({ formShown, heading, intro, details })
74
80
  const s = contact({ layout: formShown ? layout : 'stacked', align, tone })
75
81
  const headingId = heading ? `${id}-title` : undefined
82
+ // `after`: the heading goes with the form when there is one, else it follows the details in the copy column.
83
+ const headingTop = headingPlace !== 'after'
84
+ const headingInCopy = Boolean(heading) && (headingTop || !formShown)
85
+ const headingInForm = Boolean(heading) && !headingTop && formShown
76
86
  // A description list needs a term for every detail; an icon list carried from the source has values only.
77
87
  const labelled = details.every(detail => detail.label)
78
88
  const LINK = 'underline decoration-line underline-offset-4 hover:decoration-current'
@@ -82,9 +92,9 @@ const FormHeading = heading ? 'h3' : 'h2'
82
92
  {!bare && (
83
93
  <Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
84
94
  <div class={s.grid()}>
85
- {(heading || intro || details.length > 0) && (
95
+ {(heading || intro || details.length > 0) && !(headingInForm && !intro && details.length === 0) && (
86
96
  <div class={s.copy()}>
87
- {heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
97
+ {headingTop && heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
88
98
  {intro && <p class={s.intro()}>{intro}</p>}
89
99
  {details.length > 0 && (labelled
90
100
  ? (
@@ -107,12 +117,14 @@ const FormHeading = heading ? 'h3' : 'h2'
107
117
  ))}
108
118
  </ul>
109
119
  ))}
120
+ {!headingTop && headingInCopy && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
110
121
  </div>
111
122
  )}
112
- {formShown && (formHeading
123
+ {formShown && (formHeading || headingInForm
113
124
  ? (
114
125
  <div class={s.formColumn()}>
115
- <FormHeading class={s.formHeading()}>{formHeading}</FormHeading>
126
+ {headingInForm && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
127
+ {formHeading && <FormHeading class={s.formHeading()}>{formHeading}</FormHeading>}
116
128
  <StudioForm model={model} class={s.form()} />
117
129
  </div>
118
130
  )
@@ -16,5 +16,6 @@
16
16
  { "name": "stacked-start", "props": { "model": "contact-submissions", "intro": "Tell us what you make and when you want to launch.", "details": [{ "value": "Studio 4, Harbour Lane" }], "layout": "stacked", "align": "start", "id": "start" } },
17
17
  { "name": "split-inverse", "props": { "model": "contact-submissions", "heading": "Press", "tone": "inverse", "id": "press", "details": [{ "label": "Press office", "value": "press@northwind.example", "href": "mailto:press@northwind.example" }] } },
18
18
  { "name": "stacked-accent", "props": { "model": "newsletter-requests", "heading": "Request a print copy", "intro": "Print copies ship in the first week of each quarter.", "layout": "stacked", "tone": "accent", "id": "print" } },
19
- { "name": "form-heading", "props": { "model": "contact-submissions", "heading": "Contact", "intro": "Tell us where you are and what you need.", "formHeading": "Tell us about your project", "details": [{ "label": "Address", "value": "12 Harbour Street, Leith" }] } }
19
+ { "name": "form-heading", "props": { "model": "contact-submissions", "heading": "Contact", "intro": "Tell us where you are and what you need.", "formHeading": "Tell us about your project", "details": [{ "label": "Address", "value": "12 Harbour Street, Leith" }] } },
20
+ { "name": "heading-after", "props": { "model": "contact-submissions", "heading": "Tell us about your project", "intro": "Write to us and we answer within two working days.", "details": [{"value": "Email the studio", "href": "mailto:hello@northwind.example"}], "headingPlace": "after"} }
20
21
  ]
@@ -8,7 +8,8 @@
8
8
  "variants": {
9
9
  "layout": { "options": ["split", "stacked"], "default": "split", "description": "split: details beside the form on wide screens. stacked: one column (see align)." },
10
10
  "align": { "options": ["center", "start"], "default": "center", "description": "Stacked only, and the details alone before a Studio form. center: one centred column at a reading width. start: the section's whole column from the start edge, as a page builder's container whose text sets no alignment." },
11
- "tone": { "options": ["default", "muted", "inverse", "accent"], "default": "default", "description": "Section background; the form always sits on a surface card." }
11
+ "tone": { "options": ["default", "muted", "inverse", "accent"], "default": "default", "description": "Section background; the form always sits on a surface card." },
12
+ "headingPlace": { "options": ["top", "after"], "default": "top", "description": "Where the heading sits. top: above the introduction and details. after: after them, as a source that writes its lead line and contact buttons first has it; beside a form it titles the form, without one it follows the details." }
12
13
  },
13
14
  "props": {
14
15
  "model": { "type": "string", "required": true, "content": false, "description": "The Studio model submissions are saved to — the form's fields are that model's form settings (from the plugin form's fields at migration)." },
@@ -1110,6 +1110,62 @@
1110
1110
  "name": "dom:cite"
1111
1111
  }
1112
1112
  },
1113
+ {
1114
+ "id": "contact-form.intro-first",
1115
+ "component": "contact-form",
1116
+ "variant": {
1117
+ "headingPlace": "after"
1118
+ },
1119
+ "when": {
1120
+ "order": [
1121
+ "intro",
1122
+ "title"
1123
+ ]
1124
+ },
1125
+ "slots": {
1126
+ "title": {
1127
+ "match": "core/heading",
1128
+ "max": 1,
1129
+ "min": 1
1130
+ },
1131
+ "formTitle": {
1132
+ "match": "core/heading",
1133
+ "max": 1
1134
+ },
1135
+ "intro": {
1136
+ "match": "core/paragraph",
1137
+ "max": 1,
1138
+ "min": 1
1139
+ },
1140
+ "details": {
1141
+ "match": "core/button",
1142
+ "max": 3
1143
+ },
1144
+ "form": {
1145
+ "match": [
1146
+ "contact-form-7/contact-form-selector",
1147
+ "jetpack/contact-form",
1148
+ "wpforms/form-selector",
1149
+ "gravityforms/form",
1150
+ "ninja-forms/form"
1151
+ ],
1152
+ "min": 1,
1153
+ "max": 1
1154
+ }
1155
+ },
1156
+ "props": {
1157
+ "model": "const:contact",
1158
+ "heading": "@title dom:",
1159
+ "intro": "@intro dom:",
1160
+ "formHeading": "@formTitle dom:"
1161
+ },
1162
+ "into": "details",
1163
+ "each": "@details",
1164
+ "item": {
1165
+ "value": "dom:a",
1166
+ "href": "dom:a@href"
1167
+ }
1168
+ },
1113
1169
  {
1114
1170
  "id": "contact-form.form",
1115
1171
  "component": "contact-form",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/astro-kit",
3
- "version": "0.26.1",
3
+ "version": "0.27.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",