@contentrain/astro-kit 0.30.0 → 0.32.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 (67) hide show
  1. package/README.md +25 -2
  2. package/catalog.json +3305 -83
  3. package/components/_shared/Section.astro +18 -3
  4. package/components/_shared/measured.ts +152 -0
  5. package/components/_shared/types.ts +40 -0
  6. package/components/band/Band.astro +132 -0
  7. package/components/band/fixtures.json +8 -0
  8. package/components/band/meta.json +323 -0
  9. package/components/card-grid/CardGrid.astro +7 -3
  10. package/components/card-grid/fixtures.json +2 -1
  11. package/components/card-grid/meta.json +111 -0
  12. package/components/contact-details/ContactDetails.astro +7 -2
  13. package/components/contact-details/meta.json +113 -1
  14. package/components/contact-form/ContactForm.astro +48 -12
  15. package/components/contact-form/fixtures.json +169 -9
  16. package/components/contact-form/meta.json +283 -15
  17. package/components/cta/Cta.astro +52 -12
  18. package/components/cta/fixtures.json +5 -1
  19. package/components/cta/meta.json +11 -2
  20. package/components/faq/Faq.astro +60 -17
  21. package/components/faq/fixtures.json +75 -0
  22. package/components/faq/meta.json +8 -2
  23. package/components/feature-list/FeatureList.astro +34 -6
  24. package/components/feature-list/fixtures.json +2 -1
  25. package/components/feature-list/meta.json +125 -1
  26. package/components/gallery/Gallery.astro +47 -13
  27. package/components/gallery/fixtures.json +275 -0
  28. package/components/gallery/meta.json +266 -14
  29. package/components/hero/Hero.astro +14 -8
  30. package/components/hero/fixtures.json +2 -1
  31. package/components/hero/meta.json +111 -0
  32. package/components/logo-cloud/LogoCloud.astro +85 -21
  33. package/components/logo-cloud/fixtures.json +239 -0
  34. package/components/logo-cloud/meta.json +145 -5
  35. package/components/newsletter/Newsletter.astro +5 -1
  36. package/components/newsletter/meta.json +113 -1
  37. package/components/post-card/PostCard.astro +4 -2
  38. package/components/post-card/meta.json +2 -1
  39. package/components/post-list/PostList.astro +114 -0
  40. package/components/post-list/fixtures.json +387 -0
  41. package/components/post-list/meta.json +308 -0
  42. package/components/pricing/Pricing.astro +128 -24
  43. package/components/pricing/fixtures.json +288 -4
  44. package/components/pricing/meta.json +188 -1
  45. package/components/split/Split.astro +76 -14
  46. package/components/split/fixtures.json +8 -1
  47. package/components/split/meta.json +226 -4
  48. package/components/stats/Stats.astro +42 -10
  49. package/components/stats/fixtures.json +173 -0
  50. package/components/stats/meta.json +153 -5
  51. package/components/steps/Steps.astro +36 -8
  52. package/components/steps/fixtures.json +217 -5
  53. package/components/steps/meta.json +157 -3
  54. package/components/tabs/Tabs.astro +5 -2
  55. package/components/tabs/meta.json +2 -1
  56. package/components/team/Team.astro +52 -8
  57. package/components/team/fixtures.json +217 -4
  58. package/components/team/meta.json +168 -3
  59. package/components/team/team.ts +9 -0
  60. package/components/testimonial/Testimonial.astro +71 -21
  61. package/components/testimonial/fixtures.json +73 -0
  62. package/components/testimonial/meta.json +167 -3
  63. package/mapping/classic.json +7 -1
  64. package/mapping/divi.json +21 -4
  65. package/mapping/elementor.json +33 -8
  66. package/mapping/gutenberg.json +20 -4
  67. package/package.json +2 -2
@@ -11,6 +11,8 @@
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'
@@ -35,12 +37,21 @@ const contact = tv({
35
37
  formColumn: 'flex flex-col gap-4',
36
38
  formHeading: 'text-xl [font-weight:var(--font-weight-heading,700)] tracking-tight text-balance md:text-2xl',
37
39
  form: 'rounded-card',
40
+ card: 'flex flex-col gap-1 rounded-card border border-line p-4',
41
+ hours: 'mt-2 whitespace-pre-line text-sm opacity-85',
42
+ socials: 'mt-2 flex flex-wrap gap-4 text-sm',
38
43
  },
39
44
  variants: {
40
45
  layout: {
41
46
  split: { grid: 'md:grid-cols-[2fr_3fr] md:items-start' },
42
47
  stacked: {},
48
+ // Only the form (under its heading): the details, hours and map are left to other sections.
49
+ 'form-only': { grid: 'mx-auto max-w-2xl' },
43
50
  },
51
+ // How the ways to reach you are drawn: a plain list, or a card each (label over value).
52
+ detailsStyle: { list: {}, cards: { details: 'mt-2 grid gap-3 sm:grid-cols-2' } },
53
+ // The form on its own surface (the default), or flush on the section's background.
54
+ panel: { card: {}, none: {} },
44
55
  // The stacked column: centred in a reading width, or the section's whole column from its start edge (a page
45
56
  // builder's container whose text has no alignment of its own sits left, full width).
46
57
  align: { center: {}, start: {} },
@@ -54,7 +65,7 @@ const contact = tv({
54
65
  compoundVariants: [
55
66
  { layout: 'stacked', align: 'center', class: { grid: 'mx-auto max-w-2xl' } },
56
67
  ],
57
- defaultVariants: { layout: 'split', tone: 'default', align: 'center' },
68
+ defaultVariants: { layout: 'split', tone: 'default', align: 'center', detailsStyle: 'list', panel: 'card' },
58
69
  })
59
70
 
60
71
  interface Props {
@@ -65,25 +76,38 @@ interface Props {
65
76
  /** The form's own heading, above the form ("Tell us about your project"). */
66
77
  formHeading?: string | undefined
67
78
  details?: ContactDetail[] | undefined
68
- layout?: 'split' | 'stacked' | undefined
79
+ layout?: 'split' | 'stacked' | 'form-only' | undefined
80
+ detailsStyle?: 'list' | 'cards' | undefined
81
+ panel?: 'card' | 'none' | undefined
82
+ /** Opening hours, as written (line breaks kept). */
83
+ hours?: string | undefined
84
+ /** Profile links shown as a row under the details. */
85
+ socials?: Array<{ label: string, href: string }> | undefined
86
+ /** 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. */
87
+ map?: { address?: string | undefined, url?: string | undefined, title: string } | undefined
69
88
  /** Stacked only (and the details alone, before a Studio form): a centred column, or the whole column from the start edge. */
70
89
  align?: 'center' | 'start' | undefined
71
90
  /** `after`: the heading follows the introduction and the details (as the source wrote them), titling the form when there is one. */
72
91
  headingPlace?: 'top' | 'after' | undefined
73
92
  tone?: 'default' | 'muted' | 'inverse' | 'accent' | undefined
93
+ /** 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. */
94
+ measured?: MeasuredStyle | undefined
74
95
  id?: string | undefined
75
96
  }
76
97
 
77
- const { model, heading, intro, formHeading, details = [], layout, align, tone, headingPlace = 'top', id = 'contact' } = Astro.props
98
+ const { model, heading, intro, formHeading, details = [], layout, detailsStyle, panel, hours, socials = [], map, align, tone, headingPlace = 'top', measured, id = 'contact' } = Astro.props
78
99
  // Nothing to render (Studio not bound and no fallback): a split would leave an empty column — the details stand alone.
79
100
  const formShown = formHome(model) !== null
80
101
  const bare = contactSectionBare({ formShown, heading, intro, details })
81
- const s = contact({ layout: formShown ? layout : 'stacked', align, tone })
102
+ // Without a form there is nothing for `form-only` to show: the section falls back to its stacked copy.
103
+ const formOnly = layout === 'form-only' && formShown
104
+ const s = contact({ layout: formShown ? layout : 'stacked', align, tone, detailsStyle, panel })
105
+ const formClass = s.form({ class: panel === 'none' ? 'border-0 bg-transparent p-0 text-current' : undefined })
82
106
  const headingId = heading ? `${id}-title` : undefined
83
107
  // `after`: the heading goes with the form when there is one, else it follows the details in the copy column.
84
108
  const headingTop = headingPlace !== 'after'
85
- const headingInCopy = Boolean(heading) && (headingTop || !formShown)
86
- const headingInForm = Boolean(heading) && !headingTop && formShown
109
+ const headingInCopy = Boolean(heading) && !formOnly && (headingTop || !formShown)
110
+ const headingInForm = Boolean(heading) && (formOnly || (!headingTop && formShown))
87
111
  // A description list needs a term for every detail; an icon list carried from the source has values only.
88
112
  const labelled = details.every(detail => detail.label)
89
113
  const LINK = 'underline decoration-line underline-offset-4 hover:decoration-current'
@@ -91,9 +115,9 @@ const LINK = 'underline decoration-line underline-offset-4 hover:decoration-curr
91
115
  const FormHeading = heading ? 'h3' : 'h2'
92
116
  ---
93
117
  {!bare && (
94
- <Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
118
+ <Section measured={measured} tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
95
119
  <div class={s.grid()}>
96
- {(heading || intro || details.length > 0) && !(headingInForm && !intro && details.length === 0) && (
120
+ {!formOnly && (heading || intro || details.length > 0 || hours || socials.length > 0 || map) && !(headingInForm && !hours && socials.length === 0 && !map && !intro && details.length === 0) && (
97
121
  <div class={s.copy()}>
98
122
  {headingTop && heading && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
99
123
  {intro && <p class={s.intro()}>{intro}</p>}
@@ -101,7 +125,7 @@ const FormHeading = heading ? 'h3' : 'h2'
101
125
  ? (
102
126
  <dl class={s.details()}>
103
127
  {details.map(detail => (
104
- <div>
128
+ <div class={detailsStyle === 'cards' ? s.card() : undefined}>
105
129
  <dt class={s.term()}>{detail.label}</dt>
106
130
  <dd class={s.value()}>{detail.href ? <a href={detail.href} class={LINK}>{detail.value}</a> : detail.value}</dd>
107
131
  </div>
@@ -111,13 +135,25 @@ const FormHeading = heading ? 'h3' : 'h2'
111
135
  : (
112
136
  <ul class={s.details()}>
113
137
  {details.map(detail => (
114
- <li class={s.value()}>
138
+ <li class={s.value({ class: detailsStyle === 'cards' ? s.card() : undefined })}>
115
139
  {detail.label && <span class={s.term()}>{detail.label}: </span>}
116
140
  {detail.href ? <a href={detail.href} class={LINK}>{detail.value}</a> : detail.value}
117
141
  </li>
118
142
  ))}
119
143
  </ul>
120
144
  ))}
145
+ {hours && <p class={s.hours()}>{hours}</p>}
146
+ {socials.length > 0 && (
147
+ <ul class={s.socials()}>
148
+ {socials.map(link => <li><a href={link.href} class={LINK}>{link.label}</a></li>)}
149
+ </ul>
150
+ )}
151
+ {map && (
152
+ <div>
153
+ <Embed address={map.address} url={map.url} title={map.title} aspect="map" width="page" />
154
+ {map.address && <p class="text-sm opacity-85">{map.address}</p>}
155
+ </div>
156
+ )}
121
157
  {!headingTop && headingInCopy && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
122
158
  </div>
123
159
  )}
@@ -126,10 +162,10 @@ const FormHeading = heading ? 'h3' : 'h2'
126
162
  <div class={s.formColumn()}>
127
163
  {headingInForm && <h2 id={headingId} class={s.heading()}>{heading}</h2>}
128
164
  {formHeading && <FormHeading class={s.formHeading()}>{formHeading}</FormHeading>}
129
- <StudioForm model={model} class={s.form()} />
165
+ <StudioForm model={model} class={formClass} />
130
166
  </div>
131
167
  )
132
- : <StudioForm model={model} class={s.form()} />)}
168
+ : <StudioForm model={model} class={formClass} />)}
133
169
  </div>
134
170
  </Section>
135
171
  )}
@@ -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,297 @@
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"],
6
+ "uses": [
7
+ "embed"
8
+ ],
9
+ "shared": [
10
+ "Section.astro",
11
+ "measured.ts",
12
+ "types.ts"
13
+ ],
8
14
  "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." }
15
+ "layout": {
16
+ "options": [
17
+ "split",
18
+ "stacked",
19
+ "form-only"
20
+ ],
21
+ "default": "split",
22
+ "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."
23
+ },
24
+ "align": {
25
+ "options": [
26
+ "center",
27
+ "start"
28
+ ],
29
+ "default": "center",
30
+ "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."
31
+ },
32
+ "tone": {
33
+ "options": [
34
+ "default",
35
+ "muted",
36
+ "inverse",
37
+ "accent"
38
+ ],
39
+ "default": "default",
40
+ "description": "Section background; the form always sits on a surface card."
41
+ },
42
+ "headingPlace": {
43
+ "options": [
44
+ "top",
45
+ "after"
46
+ ],
47
+ "default": "top",
48
+ "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."
49
+ },
50
+ "detailsStyle": {
51
+ "options": [
52
+ "list",
53
+ "cards"
54
+ ],
55
+ "default": "list",
56
+ "description": "The ways to reach you: a plain list, or a card each (label over value)."
57
+ },
58
+ "panel": {
59
+ "options": [
60
+ "card",
61
+ "none"
62
+ ],
63
+ "default": "card",
64
+ "description": "card: the form on its own surface. none: flush on the section's background."
65
+ }
13
66
  },
14
67
  "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`." }
68
+ "model": {
69
+ "type": "string",
70
+ "required": true,
71
+ "content": false,
72
+ "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)."
73
+ },
74
+ "heading": {
75
+ "type": "string",
76
+ "label": "Heading",
77
+ "description": "Section heading."
78
+ },
79
+ "intro": {
80
+ "type": "text",
81
+ "label": "Introduction",
82
+ "description": "A line or two above the details."
83
+ },
84
+ "formHeading": {
85
+ "type": "string",
86
+ "label": "Form heading",
87
+ "description": "The form's own heading, shown above the form."
88
+ },
89
+ "details": {
90
+ "type": "array",
91
+ "label": "Contact details",
92
+ "items": {
93
+ "type": "object",
94
+ "fields": {
95
+ "label": {
96
+ "type": "string",
97
+ "label": "Label",
98
+ "description": "What the detail is (Phone, Email, Address). Without labels the details are a plain list."
99
+ },
100
+ "value": {
101
+ "type": "string",
102
+ "label": "Value",
103
+ "required": true,
104
+ "description": "The detail as shown."
105
+ },
106
+ "href": {
107
+ "type": "url",
108
+ "label": "Link",
109
+ "description": "A tel:, mailto: or map link for it."
110
+ }
111
+ }
112
+ },
113
+ "description": "Ways to reach you, in order (Email → mailto:, Phone → tel:, Address → map link)."
114
+ },
115
+ "id": {
116
+ "type": "string",
117
+ "content": false,
118
+ "description": "Section id; the heading gets `<id>-title`."
119
+ },
120
+ "hours": {
121
+ "type": "text",
122
+ "label": "Opening hours",
123
+ "description": "Opening hours as written; line breaks are kept."
124
+ },
125
+ "socials": {
126
+ "type": "array",
127
+ "label": "Profile links",
128
+ "description": "Social or profile links, shown as a row under the details.",
129
+ "items": {
130
+ "type": "object",
131
+ "fields": {
132
+ "label": {
133
+ "type": "string",
134
+ "label": "Label",
135
+ "required": true,
136
+ "description": "Visible text of the link."
137
+ },
138
+ "href": {
139
+ "type": "url",
140
+ "label": "Link",
141
+ "required": true,
142
+ "description": "The profile address."
143
+ }
144
+ }
145
+ }
146
+ },
147
+ "map": {
148
+ "type": "object",
149
+ "label": "Map",
150
+ "description": "A map of the address, loaded only when the reader asks (no iframe on load); the address is printed under it.",
151
+ "fields": {
152
+ "address": {
153
+ "type": "string",
154
+ "label": "Address",
155
+ "description": "The place shown on the map."
156
+ },
157
+ "url": {
158
+ "type": "url",
159
+ "label": "Map address",
160
+ "description": "A Google Maps embed address, when the source has one."
161
+ },
162
+ "title": {
163
+ "type": "string",
164
+ "label": "Map title",
165
+ "required": true,
166
+ "description": "What the map is (\"Our studio\"): its accessible name."
167
+ }
168
+ }
169
+ },
170
+ "measured": {
171
+ "type": "object",
172
+ "content": false,
173
+ "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.",
174
+ "fields": {
175
+ "containerPx": {
176
+ "type": "integer",
177
+ "min": 0,
178
+ "max": 2560,
179
+ "description": "The frame's content width."
180
+ },
181
+ "padY": {
182
+ "type": "integer",
183
+ "min": 0,
184
+ "max": 400,
185
+ "description": "Top and bottom padding."
186
+ },
187
+ "padX": {
188
+ "type": "integer",
189
+ "min": 0,
190
+ "max": 200,
191
+ "description": "The frame's side padding (gutter)."
192
+ },
193
+ "bg": {
194
+ "type": "object",
195
+ "description": "The section's background.",
196
+ "fields": {
197
+ "color": {
198
+ "type": "string",
199
+ "description": "Fill colour: hex, rgb(a) or hsl(a)."
200
+ },
201
+ "image": {
202
+ "type": "string",
203
+ "description": "Background image address (http(s) or root-relative), drawn behind the content."
204
+ },
205
+ "overlay": {
206
+ "type": "string",
207
+ "description": "Colour or linear/radial gradient laid over the image or the fill."
208
+ },
209
+ "size": {
210
+ "type": "string",
211
+ "description": "cover, contain or auto; anything else draws as cover."
212
+ },
213
+ "position": {
214
+ "type": "string",
215
+ "description": "One or two of center/top/bottom/left/right or percentages."
216
+ }
217
+ }
218
+ },
219
+ "columns": {
220
+ "type": "object",
221
+ "description": "Item columns at 390 (base), 768 (md) and 1280 (lg) px; a missing width follows the narrower one.",
222
+ "fields": {
223
+ "390": {
224
+ "type": "integer",
225
+ "min": 1,
226
+ "max": 6,
227
+ "description": "Phone."
228
+ },
229
+ "768": {
230
+ "type": "integer",
231
+ "min": 1,
232
+ "max": 6,
233
+ "description": "Tablet."
234
+ },
235
+ "1280": {
236
+ "type": "integer",
237
+ "min": 1,
238
+ "max": 6,
239
+ "description": "Desktop."
240
+ }
241
+ }
242
+ },
243
+ "gap": {
244
+ "type": "integer",
245
+ "min": 0,
246
+ "max": 200,
247
+ "description": "Gap between items."
248
+ },
249
+ "tone": {
250
+ "type": "select",
251
+ "options": [
252
+ "light",
253
+ "dark"
254
+ ],
255
+ "description": "What the text is set for: dark = light text on a dark band. Without it, a fill's own lightness decides."
256
+ },
257
+ "align": {
258
+ "type": "select",
259
+ "options": [
260
+ "start",
261
+ "center",
262
+ "end"
263
+ ],
264
+ "description": "Where the heading and lead sit; read by a component whose own align is not set."
265
+ },
266
+ "radius": {
267
+ "type": "integer",
268
+ "min": 0,
269
+ "max": 200,
270
+ "description": "Card and item corner radius."
271
+ },
272
+ "minHeight": {
273
+ "type": "integer",
274
+ "min": 0,
275
+ "max": 2000,
276
+ "description": "The section's minimum height."
277
+ }
278
+ }
279
+ }
21
280
  },
22
281
  "slots": {},
23
282
  "js": "studio",
24
283
  "dependencies": {},
25
284
  "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"]
285
+ "elementor": [
286
+ "elementor/form"
287
+ ],
288
+ "divi": [
289
+ "divi/et_pb_contact_form"
290
+ ],
291
+ "classic": [
292
+ "classic/contact-form-7",
293
+ "classic/wpforms",
294
+ "classic/gravityforms",
295
+ "classic/ninja-forms"
296
+ ]
29
297
  },
30
298
  "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
299
  }