create-bearminion 0.0.0-stage → 1.0.1

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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -2
  3. package/dist/cli.mjs +1369 -0
  4. package/package.json +55 -4
  5. package/template/.storybook/main.ts +14 -0
  6. package/template/.storybook/preview.css +8 -0
  7. package/template/.storybook/preview.ts +17 -0
  8. package/template/README.md +251 -0
  9. package/template/_gitignore +21 -0
  10. package/template/astro.config.ts +46 -0
  11. package/template/content/pages/404.json +36 -0
  12. package/template/content/pages/about.json +101 -0
  13. package/template/content/pages/index.json +463 -0
  14. package/template/content/pages/preview.json +40 -0
  15. package/template/content/site.json +117 -0
  16. package/template/package.json +37 -0
  17. package/template/src/assets/uploads/favicon.png +0 -0
  18. package/template/src/assets/uploads/hero.png +0 -0
  19. package/template/src/assets/uploads/logo.png +0 -0
  20. package/template/src/assets/uploads/og.png +0 -0
  21. package/template/src/components/atoms/Button.astro +64 -0
  22. package/template/src/components/atoms/Button.stories.ts +42 -0
  23. package/template/src/components/atoms/Emphasis.astro +24 -0
  24. package/template/src/components/atoms/Emphasis.stories.ts +25 -0
  25. package/template/src/components/atoms/Image.astro +78 -0
  26. package/template/src/components/atoms/Image.stories.ts +35 -0
  27. package/template/src/components/atoms/LogoMark.astro +53 -0
  28. package/template/src/components/atoms/LogoMark.stories.ts +25 -0
  29. package/template/src/components/atoms/Typography.astro +50 -0
  30. package/template/src/components/atoms/Typography.stories.ts +20 -0
  31. package/template/src/components/atoms/button.ts +31 -0
  32. package/template/src/components/atoms/image.ts +34 -0
  33. package/template/src/components/atoms/options.ts +39 -0
  34. package/template/src/components/atoms/typography.ts +63 -0
  35. package/template/src/components/molecules/MediaFrame.astro +71 -0
  36. package/template/src/components/molecules/MediaFrame.stories.ts +14 -0
  37. package/template/src/components/molecules/Pill.astro +24 -0
  38. package/template/src/components/molecules/Pill.stories.ts +12 -0
  39. package/template/src/components/molecules/SectionHeader.astro +63 -0
  40. package/template/src/components/molecules/SectionHeader.stories.ts +15 -0
  41. package/template/src/components/molecules/SectionShell.astro +33 -0
  42. package/template/src/components/molecules/SectionShell.stories.ts +25 -0
  43. package/template/src/components/organisms/Footer.astro +133 -0
  44. package/template/src/components/organisms/Footer.stories.ts +16 -0
  45. package/template/src/components/organisms/FooterLinks.astro +32 -0
  46. package/template/src/components/organisms/Header.astro +94 -0
  47. package/template/src/components/organisms/Header.stories.ts +27 -0
  48. package/template/src/components/organisms/MobileBranch.astro +52 -0
  49. package/template/src/components/organisms/MobileMenu.astro +110 -0
  50. package/template/src/components/organisms/MobileMenu.stories.ts +30 -0
  51. package/template/src/components/organisms/NavBranch.astro +61 -0
  52. package/template/src/components/organisms/Navigation.astro +32 -0
  53. package/template/src/components/organisms/Navigation.stories.ts +25 -0
  54. package/template/src/components/organisms/site-definition.ts +28 -0
  55. package/template/src/components/sections/SectionList.astro +53 -0
  56. package/template/src/components/sections/contact/Contact.astro +227 -0
  57. package/template/src/components/sections/contact/Contact.stories.ts +16 -0
  58. package/template/src/components/sections/contact/definition.ts +50 -0
  59. package/template/src/components/sections/contact/wireframe.ts +15 -0
  60. package/template/src/components/sections/cta/Cta.astro +181 -0
  61. package/template/src/components/sections/cta/Cta.stories.ts +19 -0
  62. package/template/src/components/sections/cta/definition.ts +35 -0
  63. package/template/src/components/sections/cta/wireframe.ts +9 -0
  64. package/template/src/components/sections/definitions.ts +54 -0
  65. package/template/src/components/sections/faq/Faq.astro +131 -0
  66. package/template/src/components/sections/faq/Faq.stories.ts +16 -0
  67. package/template/src/components/sections/faq/definition.ts +51 -0
  68. package/template/src/components/sections/faq/wireframe.ts +19 -0
  69. package/template/src/components/sections/features/Features.astro +121 -0
  70. package/template/src/components/sections/features/Features.stories.ts +19 -0
  71. package/template/src/components/sections/features/definition.ts +64 -0
  72. package/template/src/components/sections/features/wireframe.ts +19 -0
  73. package/template/src/components/sections/hero/Hero.astro +155 -0
  74. package/template/src/components/sections/hero/Hero.stories.ts +28 -0
  75. package/template/src/components/sections/hero/definition.ts +36 -0
  76. package/template/src/components/sections/hero/wireframe.ts +12 -0
  77. package/template/src/components/sections/logo-strip/LogoStrip.astro +107 -0
  78. package/template/src/components/sections/logo-strip/LogoStrip.stories.ts +20 -0
  79. package/template/src/components/sections/logo-strip/definition.ts +30 -0
  80. package/template/src/components/sections/logo-strip/wireframe.ts +11 -0
  81. package/template/src/components/sections/pricing/Pricing.astro +210 -0
  82. package/template/src/components/sections/pricing/Pricing.stories.ts +16 -0
  83. package/template/src/components/sections/pricing/definition.ts +62 -0
  84. package/template/src/components/sections/pricing/wireframe.ts +18 -0
  85. package/template/src/components/sections/registry.ts +25 -0
  86. package/template/src/components/sections/shared.ts +13 -0
  87. package/template/src/components/sections/stats/Stats.astro +110 -0
  88. package/template/src/components/sections/stats/Stats.stories.ts +16 -0
  89. package/template/src/components/sections/stats/definition.ts +41 -0
  90. package/template/src/components/sections/stats/wireframe.ts +15 -0
  91. package/template/src/components/sections/testimonials/Testimonials.astro +172 -0
  92. package/template/src/components/sections/testimonials/Testimonials.stories.ts +19 -0
  93. package/template/src/components/sections/testimonials/definition.ts +42 -0
  94. package/template/src/components/sections/testimonials/wireframe.ts +21 -0
  95. package/template/src/components/sections/text-image/TextImage.astro +109 -0
  96. package/template/src/components/sections/text-image/TextImage.stories.ts +19 -0
  97. package/template/src/components/sections/text-image/definition.ts +38 -0
  98. package/template/src/components/sections/text-image/wireframe.ts +12 -0
  99. package/template/src/components/seo/Seo.astro +63 -0
  100. package/template/src/content.config.ts +19 -0
  101. package/template/src/dev/style-guide.astro +124 -0
  102. package/template/src/dev/variants.astro +117 -0
  103. package/template/src/env.d.ts +1 -0
  104. package/template/src/layouts/Base.astro +74 -0
  105. package/template/src/lib/assets.ts +13 -0
  106. package/template/src/lib/cn.ts +6 -0
  107. package/template/src/lib/content-dir.ts +8 -0
  108. package/template/src/lib/content-model.ts +25 -0
  109. package/template/src/lib/content.ts +26 -0
  110. package/template/src/lib/link-attrs.ts +74 -0
  111. package/template/src/lib/render-preview.ts +29 -0
  112. package/template/src/lib/sitemap.ts +36 -0
  113. package/template/src/pages/404.astro +15 -0
  114. package/template/src/pages/[...slug].astro +32 -0
  115. package/template/src/pages/robots.txt.ts +10 -0
  116. package/template/src/stories/args.ts +34 -0
  117. package/template/src/stories/options.ts +63 -0
  118. package/template/src/stories/sample.ts +24 -0
  119. package/template/src/styles/global.css +379 -0
  120. package/template/tsconfig.json +8 -0
@@ -0,0 +1,61 @@
1
+ ---
2
+ import type { Link } from 'bearminion'
3
+ import { anchorAttrs } from '../../lib/link-attrs'
4
+ import NavBranch from './NavBranch.astro'
5
+
6
+ interface Props {
7
+ link: Link
8
+ fieldPath: string
9
+ depth: number
10
+ pathname: string
11
+ pills?: boolean
12
+ }
13
+
14
+ const { link, fieldPath, depth, pathname, pills = false } = Astro.props
15
+ const children = link.children ?? []
16
+
17
+ function isCurrent(href: string): boolean {
18
+ if (href.startsWith('#')) return false
19
+ const path = href.split('#')[0] || '/'
20
+ const current = pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname
21
+ return path === current
22
+ }
23
+ ---
24
+
25
+ <div class:list={[depth === 1 ? 'group/nav' : 'group/sub', 'relative', depth > 1 && 'w-full']}>
26
+ <a
27
+ {...anchorAttrs(link)}
28
+ data-field={`${fieldPath}.label`}
29
+ aria-current={isCurrent(link.href) ? 'page' : undefined}
30
+ aria-haspopup={children.length > 0 ? 'true' : undefined}
31
+ class:list={[
32
+ 'text-muted transition-colors hover:text-fg',
33
+ depth === 1
34
+ ? ['text-[0.9375rem]', pills && 'inline-flex h-9 items-center rounded-full px-3.5 hover:bg-line']
35
+ : 'flex rounded-lg px-3 py-2 text-[0.875rem] hover:bg-line hover:text-fg',
36
+ ]}
37
+ >
38
+ {link.label}
39
+ </a>
40
+ {
41
+ children.length > 0 && (
42
+ <div
43
+ class:list={[
44
+ 'absolute z-50 hidden min-w-48',
45
+ depth === 1 ? 'top-full left-0 pt-2 group-hover/nav:block group-focus-within/nav:block' : 'top-0 left-full pl-2 group-hover/sub:block group-focus-within/sub:block',
46
+ ]}
47
+ >
48
+ <div class="flex flex-col rounded-xl bg-surface p-2 shadow-[0_16px_40px_-24px_oklch(0.2_0.02_255/0.45)] ring-line">
49
+ {children.map((child, index) => (
50
+ <NavBranch
51
+ link={child}
52
+ fieldPath={`${fieldPath}.children.${index}`}
53
+ depth={depth + 1}
54
+ pathname={pathname}
55
+ />
56
+ ))}
57
+ </div>
58
+ </div>
59
+ )
60
+ }
61
+ </div>
@@ -0,0 +1,32 @@
1
+ ---
2
+ import type { Site } from './site-definition'
3
+ import { cn } from '../../lib/cn'
4
+ import NavBranch from './NavBranch.astro'
5
+
6
+ interface Props {
7
+ site: Site
8
+ pathname: string
9
+ /** Slice of `site.header.links`. Omit `count` to take the rest. */
10
+ start?: number
11
+ count?: number
12
+ pills?: boolean
13
+ class?: string
14
+ }
15
+
16
+ const { site, pathname, start = 0, count, pills = false, class: className } = Astro.props
17
+ const links = site.header.links.slice(start, count === undefined ? undefined : start + count)
18
+ ---
19
+
20
+ <nav aria-label="Primary" class={cn('flex items-center', pills ? 'gap-1' : 'gap-8', className)}>
21
+ {
22
+ links.map((link, offset) => (
23
+ <NavBranch
24
+ link={link}
25
+ fieldPath={`header.links.${start + offset}`}
26
+ depth={1}
27
+ pathname={pathname}
28
+ pills={pills}
29
+ />
30
+ ))
31
+ }
32
+ </nav>
@@ -0,0 +1,25 @@
1
+ import { site } from '../../stories/sample'
2
+ import Navigation from './Navigation.astro'
3
+
4
+ export default {
5
+ title: 'Organisms/Navigation',
6
+ component: Navigation,
7
+ argTypes: {
8
+ pathname: { control: 'select', options: ['/', '/about', '/about/'] },
9
+ site: { control: 'object' },
10
+ },
11
+ }
12
+
13
+ export const Home = {
14
+ args: {
15
+ site,
16
+ pathname: '/',
17
+ },
18
+ }
19
+
20
+ export const About = {
21
+ args: {
22
+ site,
23
+ pathname: '/about',
24
+ },
25
+ }
@@ -0,0 +1,28 @@
1
+ import { defineSite, type SiteOf } from 'bearminion'
2
+
3
+ /**
4
+ * The site chrome of this theme: the header and footer layouts, how many buttons and columns they
5
+ * hold, and whether the footer has a badge. The editor builds its Header and Footer tabs from this,
6
+ * and `site.json` is validated against it. Header.astro and Footer.astro render each variant.
7
+ *
8
+ * It holds no `.astro` imports, so `astro.config.ts` and the editor can load it.
9
+ */
10
+ export const siteDefinition = defineSite({
11
+ header: {
12
+ variants: ['bar', 'centered', 'floating'],
13
+ defaultVariant: 'bar',
14
+ maxActions: 2,
15
+ recommendedLinks: 4,
16
+ },
17
+ footer: {
18
+ variants: ['simple', 'columns', 'wordmark'],
19
+ defaultVariant: 'columns',
20
+ maxColumns: 3,
21
+ badge: true,
22
+ },
23
+ })
24
+
25
+ export type Site = SiteOf<typeof siteDefinition>
26
+
27
+ export const headerVariants = siteDefinition.header.variants
28
+ export const footerVariants = siteDefinition.footer.variants
@@ -0,0 +1,53 @@
1
+ ---
2
+ import type { Section } from './definitions'
3
+ import { sectionComponents } from './registry'
4
+
5
+ interface Props {
6
+ sections: Section[]
7
+ /** Site-wide motion. A section's own `motion` overrides this. */
8
+ motion?: boolean
9
+ }
10
+
11
+ const { sections, motion = true } = Astro.props
12
+ const Hero = sectionComponents.hero
13
+ const LogoStrip = sectionComponents.logoStrip
14
+ const Features = sectionComponents.features
15
+ const TextImage = sectionComponents.textImage
16
+ const Stats = sectionComponents.stats
17
+ const Testimonials = sectionComponents.testimonials
18
+ const Pricing = sectionComponents.pricing
19
+ const Faq = sectionComponents.faq
20
+ const Cta = sectionComponents.cta
21
+ const Contact = sectionComponents.contact
22
+ ---
23
+
24
+ {
25
+ sections.map((section, index) => {
26
+ switch (section.type) {
27
+ case 'hero':
28
+ return <Hero section={section} index={index} motion={motion} />
29
+ case 'logoStrip':
30
+ return <LogoStrip section={section} index={index} motion={motion} />
31
+ case 'features':
32
+ return <Features section={section} index={index} motion={motion} />
33
+ case 'textImage':
34
+ return <TextImage section={section} index={index} motion={motion} />
35
+ case 'stats':
36
+ return <Stats section={section} index={index} motion={motion} />
37
+ case 'testimonials':
38
+ return <Testimonials section={section} index={index} motion={motion} />
39
+ case 'pricing':
40
+ return <Pricing section={section} index={index} motion={motion} />
41
+ case 'faq':
42
+ return <Faq section={section} index={index} motion={motion} />
43
+ case 'cta':
44
+ return <Cta section={section} index={index} motion={motion} />
45
+ case 'contact':
46
+ return <Contact section={section} index={index} motion={motion} />
47
+ default: {
48
+ const unreachable: never = section
49
+ return unreachable
50
+ }
51
+ }
52
+ })
53
+ }
@@ -0,0 +1,227 @@
1
+ ---
2
+ import type { Section } from '../definitions'
3
+ import Button from '../../atoms/Button.astro'
4
+ import Emphasis from '../../atoms/Emphasis.astro'
5
+ import Typography from '../../atoms/Typography.astro'
6
+ import SectionHeader from '../../molecules/SectionHeader.astro'
7
+ import SectionShell from '../../molecules/SectionShell.astro'
8
+
9
+ interface Props {
10
+ section: Extract<Section, { type: 'contact' }>
11
+ index: number
12
+ motion?: boolean
13
+ }
14
+
15
+ const { section, index, motion = true } = Astro.props
16
+ const props = section.props
17
+ const field = (path: string) => `sections.${index}.props.${path}`
18
+ const reveal = section.motion ?? motion
19
+ const formId = `contact-form-${section.id}`
20
+ ---
21
+
22
+ <SectionShell id={section.id} anchor={section.anchor} type={section.type} variant={section.variant} tone={section.tone} motion={reveal}>
23
+ {
24
+ section.variant === 'split' && (
25
+ <div class="section-container grid items-start gap-12 @min-[880px]:grid-cols-[minmax(22rem,0.85fr)_minmax(0,1.15fr)]" data-contact>
26
+ <div>
27
+ <SectionHeader eyebrow={props.eyebrow} heading={props.heading} marks={props.marks} field={field} />
28
+ {props.body && <Typography as="p" size="lead" color="muted" class="mt-5" data-field={field('body')} markdown={props.body} />}
29
+ {
30
+ props.details.length > 0 && (
31
+ <dl class="mt-8 flex flex-col">
32
+ {props.details.map((detail, detailIndex) => (
33
+ <div class="grid grid-cols-[7rem_1fr] gap-4 border-t border-[var(--line)] py-4">
34
+ <dt class="font-mono text-[0.75rem] tracking-[0.06em] text-muted uppercase" data-field={field(`details.${detailIndex}.label`)}>
35
+ {detail.label}
36
+ </dt>
37
+ <dd data-field={field(`details.${detailIndex}.value`)}>{detail.value}</dd>
38
+ </div>
39
+ ))}
40
+ </dl>
41
+ )
42
+ }
43
+ </div>
44
+ <div class="rounded-panel bg-surface p-6 ring-line @min-[700px]:p-8">
45
+ <form id={formId} class="grid gap-4 @min-[700px]:grid-cols-2" data-contact-form action={props.action} method="post" novalidate>
46
+ <label class="flex flex-col gap-2 text-[0.8125rem]">
47
+ Name
48
+ <input class="bm-field" name="name" required autocomplete="name" />
49
+ </label>
50
+ <label class="flex flex-col gap-2 text-[0.8125rem]">
51
+ Email
52
+ <input class="bm-field" type="email" name="email" required autocomplete="email" />
53
+ </label>
54
+ {
55
+ props.topics.length > 0 && (
56
+ <label class="flex flex-col gap-2 text-[0.8125rem] @min-[700px]:col-span-2">
57
+ Topic
58
+ <select class="bm-field" name="topic" required>
59
+ {props.topics.map((topic) => (
60
+ <option value={topic}>{topic}</option>
61
+ ))}
62
+ </select>
63
+ </label>
64
+ )
65
+ }
66
+ <label class="flex flex-col gap-2 text-[0.8125rem] @min-[700px]:col-span-2">
67
+ Message
68
+ <textarea class="bm-field h-32 py-3" name="message" required></textarea>
69
+ </label>
70
+ <div class="@min-[700px]:col-span-2">
71
+ <Button type="submit">Send message</Button>
72
+ {props.note && <Typography as="p" size="small" color="muted" class="mt-3" data-field={field('note')} markdown={props.note} />}
73
+ </div>
74
+ </form>
75
+ <div data-contact-success hidden>
76
+ <p class="text-[1.5rem] font-semibold tracking-[-0.03em]">Message sent.</p>
77
+ {props.thanks && <Typography as="p" size="body" color="muted" class="mt-3" data-field={field('thanks')} markdown={props.thanks} />}
78
+ <Button type="button" variant="secondary" class="mt-6" data-contact-reset>
79
+ Send another
80
+ </Button>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ )
85
+ }
86
+ {
87
+ section.variant === 'card' && (
88
+ <div class="section-container" data-contact>
89
+ <SectionHeader eyebrow={props.eyebrow} heading={props.heading} marks={props.marks} intro={props.body} introField="body" align="center" field={field} class="mx-auto max-w-2xl" />
90
+ <div class="mx-auto mt-10 max-w-[42rem] rounded-panel bg-surface p-6 ring-line @min-[700px]:p-8">
91
+ <form id={formId} class="grid gap-4 @min-[700px]:grid-cols-2" data-contact-form action={props.action} method="post" novalidate>
92
+ <label class="flex flex-col gap-2 text-[0.8125rem]">
93
+ Name
94
+ <input class="bm-field" name="name" required autocomplete="name" />
95
+ </label>
96
+ <label class="flex flex-col gap-2 text-[0.8125rem]">
97
+ Email
98
+ <input class="bm-field" type="email" name="email" required autocomplete="email" />
99
+ </label>
100
+ {
101
+ props.topics.length > 0 && (
102
+ <div class="@min-[700px]:col-span-2">
103
+ <p class="mb-2 text-[0.8125rem]">Topic</p>
104
+ <div class="flex flex-wrap gap-2" data-topics>
105
+ {props.topics.map((topic, topicIndex) => (
106
+ <button type="button" class="h-10 rounded-full px-4 text-[0.875rem] ring-line" data-topic aria-pressed={topicIndex === 0 ? 'true' : 'false'}>
107
+ {topic}
108
+ </button>
109
+ ))}
110
+ </div>
111
+ <input type="hidden" name="topic" value={props.topics[0]} data-topic-input required />
112
+ </div>
113
+ )
114
+ }
115
+ <label class="flex flex-col gap-2 text-[0.8125rem] @min-[700px]:col-span-2">
116
+ Message
117
+ <textarea class="bm-field h-32 py-3" name="message" required></textarea>
118
+ </label>
119
+ <div class="@min-[700px]:col-span-2">
120
+ <Button type="submit" class="w-full">Send message</Button>
121
+ {props.note && <Typography as="p" size="small" color="muted" class="mt-3 text-center" data-field={field('note')} markdown={props.note} />}
122
+ </div>
123
+ </form>
124
+ <div data-contact-success hidden>
125
+ <p class="text-[1.5rem] font-semibold tracking-[-0.03em]">Message sent.</p>
126
+ {props.thanks && <Typography as="p" size="body" color="muted" class="mt-3" data-field={field('thanks')} markdown={props.thanks} />}
127
+ <Button type="button" variant="secondary" class="mt-6" data-contact-reset>
128
+ Send another
129
+ </Button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ )
134
+ }
135
+ {
136
+ section.variant === 'minimal' && (
137
+ <div class="section-container" data-contact>
138
+ {
139
+ props.heading && (
140
+ <h2 class="max-w-[12ch] text-[clamp(2.75rem,1rem+6cqi,7rem)] leading-[0.92] font-semibold tracking-[-0.05em]">
141
+ <Emphasis heading={props.heading} marks={props.marks} data-field={field('heading')} />
142
+ </h2>
143
+ )
144
+ }
145
+ <form id={formId} class="mt-10 grid gap-8 @min-[760px]:grid-cols-2" data-contact-form action={props.action} method="post" novalidate>
146
+ <label class="flex flex-col gap-2">
147
+ <span class="font-mono text-[0.75rem] tracking-[0.08em] text-muted uppercase">Name</span>
148
+ <input class="bm-underline" name="name" required autocomplete="name" />
149
+ </label>
150
+ <label class="flex flex-col gap-2">
151
+ <span class="font-mono text-[0.75rem] tracking-[0.08em] text-muted uppercase">Email</span>
152
+ <input class="bm-underline" type="email" name="email" required autocomplete="email" />
153
+ </label>
154
+ <label class="flex flex-col gap-2 @min-[760px]:col-span-2">
155
+ <span class="font-mono text-[0.75rem] tracking-[0.08em] text-muted uppercase">Message</span>
156
+ <textarea class="bm-underline" name="message" required rows="3"></textarea>
157
+ </label>
158
+ <div class="flex flex-wrap items-end justify-between gap-6 @min-[760px]:col-span-2">
159
+ {
160
+ props.details.length > 0 && (
161
+ <dl class="flex flex-wrap gap-x-8 gap-y-3">
162
+ {props.details.map((detail, detailIndex) => (
163
+ <div>
164
+ <dt class="font-mono text-[0.6875rem] tracking-[0.08em] text-muted uppercase" data-field={field(`details.${detailIndex}.label`)}>{detail.label}</dt>
165
+ <dd class="mt-1" data-field={field(`details.${detailIndex}.value`)}>{detail.value}</dd>
166
+ </div>
167
+ ))}
168
+ </dl>
169
+ )
170
+ }
171
+ <Button type="submit">Send</Button>
172
+ </div>
173
+ </form>
174
+ <div data-contact-success hidden>
175
+ <p class="text-[clamp(2rem,1rem+3cqi,3.5rem)] leading-[0.95] font-semibold tracking-[-0.04em]">Message sent.</p>
176
+ {props.thanks && <Typography as="p" size="body" color="muted" class="mt-4" data-field={field('thanks')} markdown={props.thanks} />}
177
+ <Button type="button" variant="secondary" class="mt-6" data-contact-reset>
178
+ Send another
179
+ </Button>
180
+ </div>
181
+ </div>
182
+ )
183
+ }
184
+ </SectionShell>
185
+
186
+ <script>
187
+ function bindContact() {
188
+ document.querySelectorAll('[data-contact]').forEach((root) => {
189
+ if (!(root instanceof HTMLElement) || root.dataset.bound === 'true') return
190
+ root.dataset.bound = 'true'
191
+ const form = root.querySelector('[data-contact-form]')
192
+ const success = root.querySelector('[data-contact-success]')
193
+ const reset = root.querySelector('[data-contact-reset]')
194
+ const topicInput = root.querySelector('[data-topic-input]')
195
+ if (!(form instanceof HTMLFormElement) || !(success instanceof HTMLElement)) return
196
+
197
+ root.querySelectorAll('[data-topic]').forEach((chip) => {
198
+ chip.addEventListener('click', () => {
199
+ root.querySelectorAll('[data-topic]').forEach((item) => {
200
+ if (item instanceof HTMLButtonElement) item.setAttribute('aria-pressed', 'false')
201
+ })
202
+ if (chip instanceof HTMLButtonElement) chip.setAttribute('aria-pressed', 'true')
203
+ if (topicInput instanceof HTMLInputElement) topicInput.value = chip.textContent?.trim() ?? ''
204
+ })
205
+ })
206
+
207
+ form.addEventListener('submit', (event) => {
208
+ if (!form.reportValidity()) {
209
+ event.preventDefault()
210
+ return
211
+ }
212
+ if (form.getAttribute('action')) return
213
+ event.preventDefault()
214
+ form.hidden = true
215
+ success.hidden = false
216
+ })
217
+
218
+ reset?.addEventListener('click', () => {
219
+ form.reset()
220
+ form.hidden = false
221
+ success.hidden = true
222
+ })
223
+ })
224
+ }
225
+ bindContact()
226
+ document.addEventListener('bearminion:preview', bindContact)
227
+ </script>
@@ -0,0 +1,16 @@
1
+ import { sectionArgTypes } from '../../../stories/options'
2
+ import { sectionEntry } from '../../../stories/sample'
3
+ import Contact from './Contact.astro'
4
+
5
+ const entry = sectionEntry('contact')
6
+
7
+ export default {
8
+ title: 'Sections/Contact',
9
+ component: Contact,
10
+ parameters: { layout: 'fullscreen' },
11
+ argTypes: sectionArgTypes('contact'),
12
+ }
13
+
14
+ export const Home = {
15
+ args: { section: entry.section, index: entry.index, motion: false },
16
+ }
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod'
2
+ import { defineSection, richTextSchema, textSchema, withEmphasis } from 'bearminion'
3
+ import { accent, eyebrow, heading } from '../shared'
4
+ import { contactWireframe } from './wireframe'
5
+
6
+ export const contactVariants = ['split', 'card', 'minimal'] as const
7
+
8
+ export const contactDetailSchema = z
9
+ .object({
10
+ label: textSchema,
11
+ value: textSchema,
12
+ })
13
+ .strict()
14
+
15
+ export const contactSchema = withEmphasis({
16
+ eyebrow,
17
+ heading,
18
+ body: richTextSchema.optional(),
19
+ details: z.array(contactDetailSchema),
20
+ topics: z.array(textSchema),
21
+ note: richTextSchema.optional(),
22
+ thanks: richTextSchema.optional(),
23
+ /** Form endpoint. The starter still shows a success state and does not post anywhere by default. */
24
+ action: z.string().min(1).optional(),
25
+ })
26
+
27
+ export type ContactDetail = z.infer<typeof contactDetailSchema>
28
+ export type ContactProps = z.infer<typeof contactSchema>
29
+
30
+ export const contact = defineSection({
31
+ type: 'contact',
32
+ label: 'Contact',
33
+ description: 'Contact details beside a form.',
34
+ schema: contactSchema,
35
+ defaults: {
36
+ eyebrow: 'Contact',
37
+ heading: 'Get in touch.',
38
+ marks: accent('Get in touch.', 'in touch.'),
39
+ body: 'Tell people what they can contact you about and when they will hear back.',
40
+ details: [{ label: 'Email', value: 'hello@example.com' }],
41
+ topics: ['General'],
42
+ note: 'We only use your details to reply to you.',
43
+ thanks: 'Thanks, your message is on its way.',
44
+ },
45
+ variants: contactVariants,
46
+ defaultVariant: 'split',
47
+ optional: { props: ['eyebrow', 'heading', 'body', 'note', 'thanks'] },
48
+ counts: { details: { recommended: 3 }, topics: { recommended: 3 } },
49
+ wireframe: contactWireframe,
50
+ })
@@ -0,0 +1,15 @@
1
+ import { defineWireframe } from 'bearminion'
2
+
3
+ /** Add section picker thumbnail: [left %, top %, width %, height %, kind]. */
4
+ export const contactWireframe = defineWireframe([
5
+ [8, 16, 30, 8, 'title'],
6
+ [8, 30, 34, 4, 'text'],
7
+ [8, 38, 28, 4, 'text'],
8
+ [8, 56, 22, 4, 'text'],
9
+ [8, 64, 26, 4, 'text'],
10
+ [52, 12, 40, 76, 'card'],
11
+ [56, 20, 32, 9, 'block'],
12
+ [56, 34, 32, 9, 'block'],
13
+ [56, 48, 32, 18, 'block'],
14
+ [56, 72, 14, 9, 'button'],
15
+ ])