@contentrain/astro-kit 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/catalog.json +4600 -1584
  2. package/components/_shared/Button.astro +2 -1
  3. package/components/_shared/KitImage.astro +9 -4
  4. package/components/_shared/Section.astro +3 -1
  5. package/components/_shared/SectionHeader.astro +27 -0
  6. package/components/_shared/measured.ts +23 -0
  7. package/components/_shared/safe-url.ts +67 -0
  8. package/components/_shared/social.ts +73 -0
  9. package/components/announcement/Announcement.astro +2 -1
  10. package/components/announcement/meta.json +1 -1
  11. package/components/band/Band.astro +3 -2
  12. package/components/band/meta.json +1 -0
  13. package/components/breadcrumb/Breadcrumb.astro +2 -1
  14. package/components/breadcrumb/meta.json +1 -1
  15. package/components/card-grid/CardGrid.astro +2 -1
  16. package/components/card-grid/meta.json +1 -0
  17. package/components/consent/Consent.astro +2 -1
  18. package/components/consent/meta.json +1 -1
  19. package/components/contact-details/ContactDetails.astro +3 -2
  20. package/components/contact-details/meta.json +1 -0
  21. package/components/contact-form/ContactForm.astro +51 -14
  22. package/components/contact-form/fixtures.json +169 -9
  23. package/components/contact-form/meta.json +284 -15
  24. package/components/cta/Cta.astro +52 -12
  25. package/components/cta/fixtures.json +5 -1
  26. package/components/cta/meta.json +11 -2
  27. package/components/dialog/Dialog.astro +113 -0
  28. package/components/dialog/fixtures.json +84 -0
  29. package/components/dialog/meta.json +252 -0
  30. package/components/embed/meta.json +1 -1
  31. package/components/faq/Faq.astro +60 -17
  32. package/components/faq/fixtures.json +79 -0
  33. package/components/faq/meta.json +9 -3
  34. package/components/feature-list/FeatureList.astro +28 -4
  35. package/components/feature-list/meta.json +16 -3
  36. package/components/footer/Footer.astro +5 -4
  37. package/components/footer/meta.json +1 -0
  38. package/components/gallery/Gallery.astro +49 -14
  39. package/components/gallery/fixtures.json +275 -0
  40. package/components/gallery/meta.json +267 -14
  41. package/components/header/Header.astro +17 -5
  42. package/components/header/fixtures.json +90 -1
  43. package/components/header/meta.json +5 -3
  44. package/components/hero/meta.json +1 -0
  45. package/components/language-switcher/LanguageSwitcher.astro +68 -0
  46. package/components/language-switcher/fixtures.json +51 -0
  47. package/components/language-switcher/meta.json +73 -0
  48. package/components/logo-cloud/LogoCloud.astro +86 -21
  49. package/components/logo-cloud/fixtures.json +239 -0
  50. package/components/logo-cloud/meta.json +145 -5
  51. package/components/nav/Nav.astro +77 -24
  52. package/components/nav/NavTree.astro +40 -0
  53. package/components/nav/fixtures.json +165 -0
  54. package/components/nav/meta.json +11 -2
  55. package/components/newsletter/Newsletter.astro +2 -1
  56. package/components/newsletter/meta.json +1 -0
  57. package/components/pagination/Pagination.astro +4 -3
  58. package/components/pagination/meta.json +1 -1
  59. package/components/post-card/PostCard.astro +8 -5
  60. package/components/post-card/meta.json +3 -2
  61. package/components/post-list/PostList.astro +115 -0
  62. package/components/post-list/fixtures.json +387 -0
  63. package/components/post-list/meta.json +309 -0
  64. package/components/pricing/Pricing.astro +128 -24
  65. package/components/pricing/fixtures.json +288 -4
  66. package/components/pricing/meta.json +188 -1
  67. package/components/search-box/SearchBox.astro +51 -0
  68. package/components/search-box/fixtures.json +31 -0
  69. package/components/search-box/meta.json +75 -0
  70. package/components/slider/Slider.astro +19 -115
  71. package/components/slider/SliderTrack.astro +178 -0
  72. package/components/slider/fixtures.json +2 -1
  73. package/components/slider/meta.json +11 -5
  74. package/components/social-links/SocialLinks.astro +74 -0
  75. package/components/social-links/fixtures.json +67 -0
  76. package/components/social-links/meta.json +94 -0
  77. package/components/split/Split.astro +76 -14
  78. package/components/split/fixtures.json +8 -1
  79. package/components/split/meta.json +226 -4
  80. package/components/stats/Stats.astro +54 -11
  81. package/components/stats/fixtures.json +260 -0
  82. package/components/stats/meta.json +151 -4
  83. package/components/steps/Steps.astro +36 -8
  84. package/components/steps/fixtures.json +217 -5
  85. package/components/steps/meta.json +157 -3
  86. package/components/tabs/Tabs.astro +10 -3
  87. package/components/tabs/fixtures.json +4 -0
  88. package/components/tabs/meta.json +5 -2
  89. package/components/team/Team.astro +54 -9
  90. package/components/team/fixtures.json +217 -4
  91. package/components/team/meta.json +169 -3
  92. package/components/team/team.ts +4 -0
  93. package/components/testimonial/Testimonial.astro +71 -21
  94. package/components/testimonial/fixtures.json +73 -0
  95. package/components/testimonial/meta.json +168 -6
  96. package/dist/index.mjs +1 -1
  97. package/dist/index.mjs.map +1 -1
  98. package/mapping/classic.json +124 -3
  99. package/mapping/divi.json +26 -8
  100. package/mapping/elementor.json +133 -14
  101. package/mapping/gutenberg.json +38 -10
  102. package/package.json +2 -2
@@ -11,9 +11,12 @@
11
11
  // form on WordPress.
12
12
  import { tv } from 'tailwind-variants'
13
13
  import Section from '../_shared/Section.astro'
14
+ import type { MeasuredStyle } from '../_shared/types'
15
+ import Embed from '../embed/Embed.astro'
14
16
  import StudioForm from '../../studio/StudioForm.astro'
15
17
  import { formHome } from '../../../lib/features'
16
18
  import { contactSectionBare } from './contact-form'
19
+ import { safeHref } from '../_shared/safe-url'
17
20
 
18
21
  export interface ContactDetail {
19
22
  /** What the detail is (Phone, Email); a source's icon list often has none. */
@@ -35,12 +38,21 @@ const contact = tv({
35
38
  formColumn: 'flex flex-col gap-4',
36
39
  formHeading: 'text-xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-2xl',
37
40
  form: 'rounded-card',
41
+ card: 'flex flex-col gap-1 rounded-card border border-line p-4',
42
+ hours: 'mt-2 whitespace-pre-line text-sm opacity-85',
43
+ socials: 'mt-2 flex flex-wrap gap-4 text-sm',
38
44
  },
39
45
  variants: {
40
46
  layout: {
41
47
  split: { grid: 'md:grid-cols-[2fr_3fr] md:items-start' },
42
48
  stacked: {},
49
+ // Only the form (under its heading): the details, hours and map are left to other sections.
50
+ 'form-only': { grid: 'mx-auto max-w-2xl' },
43
51
  },
52
+ // How the ways to reach you are drawn: a plain list, or a card each (label over value).
53
+ detailsStyle: { list: {}, cards: { details: 'mt-2 grid gap-3 sm:grid-cols-2' } },
54
+ // The form on its own surface (the default), or flush on the section's background.
55
+ panel: { card: {}, none: {} },
44
56
  // The stacked column: centred in a reading width, or the section's whole column from its start edge (a page
45
57
  // builder's container whose text has no alignment of its own sits left, full width).
46
58
  align: { center: {}, start: {} },
@@ -54,7 +66,7 @@ const contact = tv({
54
66
  compoundVariants: [
55
67
  { layout: 'stacked', align: 'center', class: { grid: 'mx-auto max-w-2xl' } },
56
68
  ],
57
- defaultVariants: { layout: 'split', tone: 'default', align: 'center' },
69
+ defaultVariants: { layout: 'split', tone: 'default', align: 'center', detailsStyle: 'list', panel: 'card' },
58
70
  })
59
71
 
60
72
  interface Props {
@@ -65,25 +77,38 @@ interface Props {
65
77
  /** The form's own heading, above the form ("Tell us about your project"). */
66
78
  formHeading?: string | undefined
67
79
  details?: ContactDetail[] | undefined
68
- layout?: 'split' | 'stacked' | undefined
80
+ layout?: 'split' | 'stacked' | 'form-only' | undefined
81
+ detailsStyle?: 'list' | 'cards' | undefined
82
+ panel?: 'card' | 'none' | undefined
83
+ /** Opening hours, as written (line breaks kept). */
84
+ hours?: string | undefined
85
+ /** Profile links shown as a row under the details. */
86
+ socials?: Array<{ label: string, href: string }> | undefined
87
+ /** A map of the address, loaded only on a click (`Embed`); `address` is also printed so it is never the only way to find you. */
88
+ map?: { address?: string | undefined, url?: string | undefined, title: string } | undefined
69
89
  /** Stacked only (and the details alone, before a Studio form): a centred column, or the whole column from the start edge. */
70
90
  align?: 'center' | 'start' | undefined
71
91
  /** `after`: the heading follows the introduction and the details (as the source wrote them), titling the form when there is one. */
72
92
  headingPlace?: 'top' | 'after' | undefined
73
93
  tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
94
+ /** The source section's measured style (`MeasuredStyle`): frame, padding, fill, background image, columns per width, gap. Absent, the site's tokens; present, it wins over tone and columns. */
95
+ measured?: MeasuredStyle | undefined
74
96
  id?: string | undefined
75
97
  }
76
98
 
77
- const { model, heading, intro, formHeading, details = [], layout, align, tone, headingPlace = 'top', id = 'contact' } = Astro.props
99
+ const { model, heading, intro, formHeading, details = [], layout, detailsStyle, panel, hours, socials = [], map, align, tone, headingPlace = 'top', measured, id = 'contact' } = Astro.props
78
100
  // Nothing to render (Studio not bound and no fallback): a split would leave an empty column — the details stand alone.
79
101
  const formShown = formHome(model) !== null
80
102
  const bare = contactSectionBare({ formShown, heading, intro, details })
81
- const s = contact({ layout: formShown ? layout : 'stacked', align, tone })
103
+ // Without a form there is nothing for `form-only` to show: the section falls back to its stacked copy.
104
+ const formOnly = layout === 'form-only' && formShown
105
+ const s = contact({ layout: formShown ? layout : 'stacked', align, tone, detailsStyle, panel })
106
+ const formClass = s.form({ class: panel === 'none' ? 'border-0 bg-transparent p-0 text-current' : undefined })
82
107
  const headingId = heading ? `${id}-title` : undefined
83
108
  // `after`: the heading goes with the form when there is one, else it follows the details in the copy column.
84
109
  const headingTop = headingPlace !== 'after'
85
- const headingInCopy = Boolean(heading) && (headingTop || !formShown)
86
- const headingInForm = Boolean(heading) && !headingTop && formShown
110
+ const headingInCopy = Boolean(heading) && !formOnly && (headingTop || !formShown)
111
+ const headingInForm = Boolean(heading) && (formOnly || (!headingTop && formShown))
87
112
  // A description list needs a term for every detail; an icon list carried from the source has values only.
88
113
  const labelled = details.every(detail => detail.label)
89
114
  const LINK = 'underline decoration-line underline-offset-4 hover:decoration-current'
@@ -91,9 +116,9 @@ const LINK = 'underline decoration-line underline-offset-4 hover:decoration-curr
91
116
  const FormHeading = heading ? 'h3' : 'h2'
92
117
  ---
93
118
  {!bare && (
94
- <Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
119
+ <Section measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
95
120
  <div class={s.grid()}>
96
- {(heading || intro || details.length > 0) && !(headingInForm && !intro && details.length === 0) && (
121
+ {!formOnly && (heading || intro || details.length > 0 || hours || socials.length > 0 || map) && !(headingInForm && !hours && socials.length === 0 && !map && !intro && details.length === 0) && (
97
122
  <div class={s.copy()}>
98
123
  {headingTop && heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
99
124
  {intro && <p class={s.intro()}>{intro}</p>}
@@ -101,9 +126,9 @@ const FormHeading = heading ? 'h3' : 'h2'
101
126
  ? (
102
127
  <dl class={s.details()}>
103
128
  {details.map(detail => (
104
- <div>
129
+ <div class={detailsStyle === 'cards' ? s.card() : undefined}>
105
130
  <dt class={s.term()}>{detail.label}</dt>
106
- <dd class={s.value()}>{detail.href ? <a href={detail.href} class={LINK}>{detail.value}</a> : detail.value}</dd>
131
+ <dd class={s.value()}>{detail.href ? <a href={safeHref(detail.href)} class={LINK}>{detail.value}</a> : detail.value}</dd>
107
132
  </div>
108
133
  ))}
109
134
  </dl>
@@ -111,13 +136,25 @@ const FormHeading = heading ? 'h3' : 'h2'
111
136
  : (
112
137
  <ul class={s.details()}>
113
138
  {details.map(detail => (
114
- <li class={s.value()}>
139
+ <li class={s.value({ class: detailsStyle === 'cards' ? s.card() : undefined })}>
115
140
  {detail.label && <span class={s.term()}>{detail.label}: </span>}
116
- {detail.href ? <a href={detail.href} class={LINK}>{detail.value}</a> : detail.value}
141
+ {detail.href ? <a href={safeHref(detail.href)} class={LINK}>{detail.value}</a> : detail.value}
117
142
  </li>
118
143
  ))}
119
144
  </ul>
120
145
  ))}
146
+ {hours && <p class={s.hours()}>{hours}</p>}
147
+ {socials.length > 0 && (
148
+ <ul class={s.socials()}>
149
+ {socials.map(link => <li><a href={safeHref(link.href)} class={LINK}>{link.label}</a></li>)}
150
+ </ul>
151
+ )}
152
+ {map && (
153
+ <div>
154
+ <Embed address={map.address} url={map.url} title={map.title} aspect="map" width="page" />
155
+ {map.address && <p class="text-sm opacity-85">{map.address}</p>}
156
+ </div>
157
+ )}
121
158
  {!headingTop && headingInCopy && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
122
159
  </div>
123
160
  )}
@@ -126,10 +163,10 @@ const FormHeading = heading ? 'h3' : 'h2'
126
163
  <div class={s.formColumn()}>
127
164
  {headingInForm && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
128
165
  {formHeading && <FormHeading class={s.formHeading()}>{formHeading}</FormHeading>}
129
- <StudioForm model={model} class={s.form()} />
166
+ <StudioForm model={model} class={formClass} />
130
167
  </div>
131
168
  )
132
- : <StudioForm model={model} class={s.form()} />)}
169
+ : <StudioForm model={model} class={formClass} />)}
133
170
  </div>
134
171
  </Section>
135
172
  )}
@@ -6,16 +6,176 @@
6
6
  "heading": "Get in touch",
7
7
  "intro": "Questions, corrections and invitations are all welcome. We answer within two working days.",
8
8
  "details": [
9
- { "label": "Email", "value": "hello@northwind.example", "href": "mailto:hello@northwind.example" },
10
- { "label": "Phone", "value": "+351 21 000 0000", "href": "tel:+351210000000" },
11
- { "label": "Studio", "value": "Rua das Flores 12, Lisbon" }
9
+ {
10
+ "label": "Email",
11
+ "value": "hello@northwind.example",
12
+ "href": "mailto:hello@northwind.example"
13
+ },
14
+ {
15
+ "label": "Phone",
16
+ "value": "+351 21 000 0000",
17
+ "href": "tel:+351210000000"
18
+ },
19
+ {
20
+ "label": "Studio",
21
+ "value": "Rua das Flores 12, Lisbon"
22
+ }
12
23
  ]
13
24
  }
14
25
  },
15
- { "name": "stacked-muted", "props": { "model": "contact-submissions", "heading": "Write to us", "intro": "We read every message and answer within two working days.", "layout": "stacked", "tone": "muted", "id": "write" } },
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
- { "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
- { "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" }] } },
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"} }
26
+ {
27
+ "name": "stacked-muted",
28
+ "props": {
29
+ "model": "contact-submissions",
30
+ "heading": "Write to us",
31
+ "intro": "We read every message and answer within two working days.",
32
+ "layout": "stacked",
33
+ "tone": "muted",
34
+ "id": "write"
35
+ }
36
+ },
37
+ {
38
+ "name": "stacked-start",
39
+ "props": {
40
+ "model": "contact-submissions",
41
+ "intro": "Tell us what you make and when you want to launch.",
42
+ "details": [
43
+ {
44
+ "value": "Studio 4, Harbour Lane"
45
+ }
46
+ ],
47
+ "layout": "stacked",
48
+ "align": "start",
49
+ "id": "start"
50
+ }
51
+ },
52
+ {
53
+ "name": "split-inverse",
54
+ "props": {
55
+ "model": "contact-submissions",
56
+ "heading": "Press",
57
+ "tone": "inverse",
58
+ "id": "press",
59
+ "details": [
60
+ {
61
+ "label": "Press office",
62
+ "value": "press@northwind.example",
63
+ "href": "mailto:press@northwind.example"
64
+ }
65
+ ]
66
+ }
67
+ },
68
+ {
69
+ "name": "stacked-accent",
70
+ "props": {
71
+ "model": "newsletter-requests",
72
+ "heading": "Request a print copy",
73
+ "intro": "Print copies ship in the first week of each quarter.",
74
+ "layout": "stacked",
75
+ "tone": "accent",
76
+ "id": "print"
77
+ }
78
+ },
79
+ {
80
+ "name": "form-heading",
81
+ "props": {
82
+ "model": "contact-submissions",
83
+ "heading": "Contact",
84
+ "intro": "Tell us where you are and what you need.",
85
+ "formHeading": "Tell us about your project",
86
+ "details": [
87
+ {
88
+ "label": "Address",
89
+ "value": "12 Harbour Street, Leith"
90
+ }
91
+ ]
92
+ }
93
+ },
94
+ {
95
+ "name": "heading-after",
96
+ "props": {
97
+ "model": "contact-submissions",
98
+ "heading": "Tell us about your project",
99
+ "intro": "Write to us and we answer within two working days.",
100
+ "details": [
101
+ {
102
+ "value": "Email the studio",
103
+ "href": "mailto:hello@northwind.example"
104
+ }
105
+ ],
106
+ "headingPlace": "after"
107
+ }
108
+ },
109
+ {
110
+ "name": "form-only",
111
+ "props": {
112
+ "model": "contact-submissions",
113
+ "heading": "Write to us",
114
+ "layout": "form-only",
115
+ "panel": "none"
116
+ }
117
+ },
118
+ {
119
+ "name": "cards-hours-socials",
120
+ "props": {
121
+ "model": "contact-submissions",
122
+ "heading": "Find us",
123
+ "detailsStyle": "cards",
124
+ "details": [
125
+ {
126
+ "label": "Email",
127
+ "value": "hello@northwind.example",
128
+ "href": "mailto:hello@northwind.example"
129
+ },
130
+ {
131
+ "label": "Phone",
132
+ "value": "+351 21 000 0000",
133
+ "href": "tel:+351210000000"
134
+ },
135
+ {
136
+ "label": "Studio",
137
+ "value": "Rua das Flores 12, Lisbon"
138
+ }
139
+ ],
140
+ "hours": "Mon–Fri 9:00–18:00\nSat 10:00–14:00",
141
+ "socials": [
142
+ {
143
+ "label": "Instagram",
144
+ "href": "https://instagram.com/northwind"
145
+ },
146
+ {
147
+ "label": "LinkedIn",
148
+ "href": "https://linkedin.com/company/northwind"
149
+ }
150
+ ]
151
+ }
152
+ },
153
+ {
154
+ "name": "map-click-to-load",
155
+ "props": {
156
+ "model": "contact-submissions",
157
+ "heading": "Visit",
158
+ "layout": "stacked",
159
+ "details": [
160
+ {
161
+ "label": "Email",
162
+ "value": "hello@northwind.example",
163
+ "href": "mailto:hello@northwind.example"
164
+ },
165
+ {
166
+ "label": "Phone",
167
+ "value": "+351 21 000 0000",
168
+ "href": "tel:+351210000000"
169
+ },
170
+ {
171
+ "label": "Studio",
172
+ "value": "Rua das Flores 12, Lisbon"
173
+ }
174
+ ],
175
+ "map": {
176
+ "address": "Rua das Flores 12, Lisbon",
177
+ "title": "Our studio"
178
+ }
179
+ }
180
+ }
21
181
  ]
@@ -3,29 +3,298 @@
3
3
  "name": "Contact form",
4
4
  "description": "The contact section: heading, introduction, contact details (email, phone, address) and a Contentrain Studio form — fields, spam protection and notifications set in Studio, submissions reviewed there. Replaces form plugins; renders the details alone until the site is bound to a Studio project (a section left with only its heading is not drawn: the heading would title a form that is not there). When the owner chose another home at migration (site.config.ts `features.forms`) the form posts to their form service, or the section shows an address to write to or a link to the form on WordPress.",
5
5
  "category": "section",
6
- "uses": [],
7
- "shared": ["Section.astro", "measured.ts", "types.ts"],
6
+ "uses": [
7
+ "embed"
8
+ ],
9
+ "shared": [
10
+ "Section.astro",
11
+ "measured.ts",
12
+ "safe-url.ts",
13
+ "types.ts"
14
+ ],
8
15
  "variants": {
9
- "layout": { "options": ["split", "stacked"], "default": "split", "description": "split: details beside the form on wide screens. stacked: one column (see align)." },
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." },
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." }
16
+ "layout": {
17
+ "options": [
18
+ "split",
19
+ "stacked",
20
+ "form-only"
21
+ ],
22
+ "default": "split",
23
+ "description": "split: details beside the form on wide screens. stacked: one column (see align). form-only: only the form under its heading; details, hours and map are left out."
24
+ },
25
+ "align": {
26
+ "options": [
27
+ "center",
28
+ "start"
29
+ ],
30
+ "default": "center",
31
+ "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."
32
+ },
33
+ "tone": {
34
+ "options": [
35
+ "default",
36
+ "muted",
37
+ "inverse",
38
+ "accent"
39
+ ],
40
+ "default": "default",
41
+ "description": "Section background; the form always sits on a surface card."
42
+ },
43
+ "headingPlace": {
44
+ "options": [
45
+ "top",
46
+ "after"
47
+ ],
48
+ "default": "top",
49
+ "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."
50
+ },
51
+ "detailsStyle": {
52
+ "options": [
53
+ "list",
54
+ "cards"
55
+ ],
56
+ "default": "list",
57
+ "description": "The ways to reach you: a plain list, or a card each (label over value)."
58
+ },
59
+ "panel": {
60
+ "options": [
61
+ "card",
62
+ "none"
63
+ ],
64
+ "default": "card",
65
+ "description": "card: the form on its own surface. none: flush on the section's background."
66
+ }
13
67
  },
14
68
  "props": {
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)." },
16
- "heading": { "type": "string", "label": "Heading", "description": "Section heading." },
17
- "intro": { "type": "text", "label": "Introduction", "description": "A line or two above the details." },
18
- "formHeading": { "type": "string", "label": "Form heading", "description": "The form's own heading, shown above the form." },
19
- "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)." },
20
- "id": { "type": "string", "content": false, "description": "Section id; the heading gets `<id>-title`." }
69
+ "model": {
70
+ "type": "string",
71
+ "required": true,
72
+ "content": false,
73
+ "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)."
74
+ },
75
+ "heading": {
76
+ "type": "string",
77
+ "label": "Heading",
78
+ "description": "Section heading."
79
+ },
80
+ "intro": {
81
+ "type": "text",
82
+ "label": "Introduction",
83
+ "description": "A line or two above the details."
84
+ },
85
+ "formHeading": {
86
+ "type": "string",
87
+ "label": "Form heading",
88
+ "description": "The form's own heading, shown above the form."
89
+ },
90
+ "details": {
91
+ "type": "array",
92
+ "label": "Contact details",
93
+ "items": {
94
+ "type": "object",
95
+ "fields": {
96
+ "label": {
97
+ "type": "string",
98
+ "label": "Label",
99
+ "description": "What the detail is (Phone, Email, Address). Without labels the details are a plain list."
100
+ },
101
+ "value": {
102
+ "type": "string",
103
+ "label": "Value",
104
+ "required": true,
105
+ "description": "The detail as shown."
106
+ },
107
+ "href": {
108
+ "type": "url",
109
+ "label": "Link",
110
+ "description": "A tel:, mailto: or map link for it."
111
+ }
112
+ }
113
+ },
114
+ "description": "Ways to reach you, in order (Email → mailto:, Phone → tel:, Address → map link)."
115
+ },
116
+ "id": {
117
+ "type": "string",
118
+ "content": false,
119
+ "description": "Section id; the heading gets `<id>-title`."
120
+ },
121
+ "hours": {
122
+ "type": "text",
123
+ "label": "Opening hours",
124
+ "description": "Opening hours as written; line breaks are kept."
125
+ },
126
+ "socials": {
127
+ "type": "array",
128
+ "label": "Profile links",
129
+ "description": "Social or profile links, shown as a row under the details.",
130
+ "items": {
131
+ "type": "object",
132
+ "fields": {
133
+ "label": {
134
+ "type": "string",
135
+ "label": "Label",
136
+ "required": true,
137
+ "description": "Visible text of the link."
138
+ },
139
+ "href": {
140
+ "type": "url",
141
+ "label": "Link",
142
+ "required": true,
143
+ "description": "The profile address."
144
+ }
145
+ }
146
+ }
147
+ },
148
+ "map": {
149
+ "type": "object",
150
+ "label": "Map",
151
+ "description": "A map of the address, loaded only when the reader asks (no iframe on load); the address is printed under it.",
152
+ "fields": {
153
+ "address": {
154
+ "type": "string",
155
+ "label": "Address",
156
+ "description": "The place shown on the map."
157
+ },
158
+ "url": {
159
+ "type": "url",
160
+ "label": "Map address",
161
+ "description": "A Google Maps embed address, when the source has one."
162
+ },
163
+ "title": {
164
+ "type": "string",
165
+ "label": "Map title",
166
+ "required": true,
167
+ "description": "What the map is (\"Our studio\"): its accessible name."
168
+ }
169
+ }
170
+ },
171
+ "measured": {
172
+ "type": "object",
173
+ "content": false,
174
+ "description": "The source section's measured style (MeasuredStyle): frame width, padding, fill, image, columns per width, gap, text tone, radius, minimum height — all in pixels. Absent, the site's tokens; present, it wins over tone and columns. Values that are not a number, a colour or a known keyword are dropped.",
175
+ "fields": {
176
+ "containerPx": {
177
+ "type": "integer",
178
+ "min": 0,
179
+ "max": 2560,
180
+ "description": "The frame's content width."
181
+ },
182
+ "padY": {
183
+ "type": "integer",
184
+ "min": 0,
185
+ "max": 400,
186
+ "description": "Top and bottom padding."
187
+ },
188
+ "padX": {
189
+ "type": "integer",
190
+ "min": 0,
191
+ "max": 200,
192
+ "description": "The frame's side padding (gutter)."
193
+ },
194
+ "bg": {
195
+ "type": "object",
196
+ "description": "The section's background.",
197
+ "fields": {
198
+ "color": {
199
+ "type": "string",
200
+ "description": "Fill colour: hex, rgb(a) or hsl(a)."
201
+ },
202
+ "image": {
203
+ "type": "string",
204
+ "description": "Background image address (http(s) or root-relative), drawn behind the content."
205
+ },
206
+ "overlay": {
207
+ "type": "string",
208
+ "description": "Colour or linear/radial gradient laid over the image or the fill."
209
+ },
210
+ "size": {
211
+ "type": "string",
212
+ "description": "cover, contain or auto; anything else draws as cover."
213
+ },
214
+ "position": {
215
+ "type": "string",
216
+ "description": "One or two of center/top/bottom/left/right or percentages."
217
+ }
218
+ }
219
+ },
220
+ "columns": {
221
+ "type": "object",
222
+ "description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
223
+ "fields": {
224
+ "390": {
225
+ "type": "integer",
226
+ "min": 1,
227
+ "max": 6,
228
+ "description": "Phone."
229
+ },
230
+ "768": {
231
+ "type": "integer",
232
+ "min": 1,
233
+ "max": 6,
234
+ "description": "Tablet."
235
+ },
236
+ "1280": {
237
+ "type": "integer",
238
+ "min": 1,
239
+ "max": 6,
240
+ "description": "Desktop."
241
+ }
242
+ }
243
+ },
244
+ "gap": {
245
+ "type": "integer",
246
+ "min": 0,
247
+ "max": 200,
248
+ "description": "Gap between items."
249
+ },
250
+ "tone": {
251
+ "type": "select",
252
+ "options": [
253
+ "light",
254
+ "dark"
255
+ ],
256
+ "description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
257
+ },
258
+ "align": {
259
+ "type": "select",
260
+ "options": [
261
+ "start",
262
+ "center",
263
+ "end"
264
+ ],
265
+ "description": "Where the heading and lead sit; read by a component whose own align is not set."
266
+ },
267
+ "radius": {
268
+ "type": "integer",
269
+ "min": 0,
270
+ "max": 200,
271
+ "description": "Card and item corner radius."
272
+ },
273
+ "minHeight": {
274
+ "type": "integer",
275
+ "min": 0,
276
+ "max": 2000,
277
+ "description": "The section's minimum height."
278
+ }
279
+ }
280
+ }
21
281
  },
22
282
  "slots": {},
23
283
  "js": "studio",
24
284
  "dependencies": {},
25
285
  "sources": {
26
- "elementor": ["elementor/form"],
27
- "divi": ["divi/et_pb_contact_form"],
28
- "classic": ["classic/contact-form-7", "classic/wpforms", "classic/gravityforms", "classic/ninja-forms"]
286
+ "elementor": [
287
+ "elementor/form"
288
+ ],
289
+ "divi": [
290
+ "divi/et_pb_contact_form"
291
+ ],
292
+ "classic": [
293
+ "classic/contact-form-7",
294
+ "classic/wpforms",
295
+ "classic/gravityforms",
296
+ "classic/ninja-forms"
297
+ ]
29
298
  },
30
299
  "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. Without a form to show and with no introduction or details, the section is not drawn at all."
31
300
  }