create-bearminion 0.0.0-stage → 1.1.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/cli.mjs +1369 -0
- package/package.json +53 -3
- package/template/.storybook/main.ts +14 -0
- package/template/.storybook/preview.css +8 -0
- package/template/.storybook/preview.ts +17 -0
- package/template/README.md +251 -0
- package/template/_gitignore +21 -0
- package/template/astro.config.ts +46 -0
- package/template/content/pages/404.json +36 -0
- package/template/content/pages/about.json +101 -0
- package/template/content/pages/index.json +463 -0
- package/template/content/pages/preview.json +40 -0
- package/template/content/site.json +117 -0
- package/template/package.json +37 -0
- package/template/src/assets/uploads/favicon.png +0 -0
- package/template/src/assets/uploads/hero.png +0 -0
- package/template/src/assets/uploads/logo.png +0 -0
- package/template/src/assets/uploads/og.png +0 -0
- package/template/src/components/atoms/Button.astro +64 -0
- package/template/src/components/atoms/Button.stories.ts +42 -0
- package/template/src/components/atoms/Emphasis.astro +24 -0
- package/template/src/components/atoms/Emphasis.stories.ts +25 -0
- package/template/src/components/atoms/Image.astro +78 -0
- package/template/src/components/atoms/Image.stories.ts +35 -0
- package/template/src/components/atoms/LogoMark.astro +53 -0
- package/template/src/components/atoms/LogoMark.stories.ts +25 -0
- package/template/src/components/atoms/Typography.astro +50 -0
- package/template/src/components/atoms/Typography.stories.ts +20 -0
- package/template/src/components/atoms/button.ts +31 -0
- package/template/src/components/atoms/image.ts +34 -0
- package/template/src/components/atoms/options.ts +39 -0
- package/template/src/components/atoms/typography.ts +63 -0
- package/template/src/components/molecules/MediaFrame.astro +71 -0
- package/template/src/components/molecules/MediaFrame.stories.ts +14 -0
- package/template/src/components/molecules/Pill.astro +24 -0
- package/template/src/components/molecules/Pill.stories.ts +12 -0
- package/template/src/components/molecules/SectionHeader.astro +63 -0
- package/template/src/components/molecules/SectionHeader.stories.ts +15 -0
- package/template/src/components/molecules/SectionShell.astro +33 -0
- package/template/src/components/molecules/SectionShell.stories.ts +25 -0
- package/template/src/components/organisms/Footer.astro +133 -0
- package/template/src/components/organisms/Footer.stories.ts +16 -0
- package/template/src/components/organisms/FooterLinks.astro +32 -0
- package/template/src/components/organisms/Header.astro +94 -0
- package/template/src/components/organisms/Header.stories.ts +27 -0
- package/template/src/components/organisms/MobileBranch.astro +52 -0
- package/template/src/components/organisms/MobileMenu.astro +110 -0
- package/template/src/components/organisms/MobileMenu.stories.ts +30 -0
- package/template/src/components/organisms/NavBranch.astro +61 -0
- package/template/src/components/organisms/Navigation.astro +32 -0
- package/template/src/components/organisms/Navigation.stories.ts +25 -0
- package/template/src/components/organisms/site-definition.ts +28 -0
- package/template/src/components/sections/SectionList.astro +53 -0
- package/template/src/components/sections/contact/Contact.astro +227 -0
- package/template/src/components/sections/contact/Contact.stories.ts +16 -0
- package/template/src/components/sections/contact/definition.ts +50 -0
- package/template/src/components/sections/contact/wireframe.ts +15 -0
- package/template/src/components/sections/cta/Cta.astro +181 -0
- package/template/src/components/sections/cta/Cta.stories.ts +19 -0
- package/template/src/components/sections/cta/definition.ts +35 -0
- package/template/src/components/sections/cta/wireframe.ts +9 -0
- package/template/src/components/sections/definitions.ts +54 -0
- package/template/src/components/sections/faq/Faq.astro +131 -0
- package/template/src/components/sections/faq/Faq.stories.ts +16 -0
- package/template/src/components/sections/faq/definition.ts +51 -0
- package/template/src/components/sections/faq/wireframe.ts +19 -0
- package/template/src/components/sections/features/Features.astro +121 -0
- package/template/src/components/sections/features/Features.stories.ts +19 -0
- package/template/src/components/sections/features/definition.ts +64 -0
- package/template/src/components/sections/features/wireframe.ts +19 -0
- package/template/src/components/sections/hero/Hero.astro +155 -0
- package/template/src/components/sections/hero/Hero.stories.ts +28 -0
- package/template/src/components/sections/hero/definition.ts +36 -0
- package/template/src/components/sections/hero/wireframe.ts +12 -0
- package/template/src/components/sections/logo-strip/LogoStrip.astro +107 -0
- package/template/src/components/sections/logo-strip/LogoStrip.stories.ts +20 -0
- package/template/src/components/sections/logo-strip/definition.ts +30 -0
- package/template/src/components/sections/logo-strip/wireframe.ts +11 -0
- package/template/src/components/sections/pricing/Pricing.astro +210 -0
- package/template/src/components/sections/pricing/Pricing.stories.ts +16 -0
- package/template/src/components/sections/pricing/definition.ts +62 -0
- package/template/src/components/sections/pricing/wireframe.ts +18 -0
- package/template/src/components/sections/registry.ts +25 -0
- package/template/src/components/sections/shared.ts +13 -0
- package/template/src/components/sections/stats/Stats.astro +110 -0
- package/template/src/components/sections/stats/Stats.stories.ts +16 -0
- package/template/src/components/sections/stats/definition.ts +41 -0
- package/template/src/components/sections/stats/wireframe.ts +15 -0
- package/template/src/components/sections/testimonials/Testimonials.astro +172 -0
- package/template/src/components/sections/testimonials/Testimonials.stories.ts +19 -0
- package/template/src/components/sections/testimonials/definition.ts +42 -0
- package/template/src/components/sections/testimonials/wireframe.ts +21 -0
- package/template/src/components/sections/text-image/TextImage.astro +109 -0
- package/template/src/components/sections/text-image/TextImage.stories.ts +19 -0
- package/template/src/components/sections/text-image/definition.ts +38 -0
- package/template/src/components/sections/text-image/wireframe.ts +12 -0
- package/template/src/components/seo/Seo.astro +63 -0
- package/template/src/content.config.ts +19 -0
- package/template/src/dev/style-guide.astro +124 -0
- package/template/src/dev/variants.astro +117 -0
- package/template/src/env.d.ts +1 -0
- package/template/src/layouts/Base.astro +74 -0
- package/template/src/lib/assets.ts +13 -0
- package/template/src/lib/cn.ts +6 -0
- package/template/src/lib/content-dir.ts +8 -0
- package/template/src/lib/content-model.ts +25 -0
- package/template/src/lib/content.ts +26 -0
- package/template/src/lib/link-attrs.ts +74 -0
- package/template/src/lib/render-preview.ts +29 -0
- package/template/src/lib/sitemap.ts +36 -0
- package/template/src/pages/404.astro +15 -0
- package/template/src/pages/[...slug].astro +32 -0
- package/template/src/pages/robots.txt.ts +10 -0
- package/template/src/stories/args.ts +34 -0
- package/template/src/stories/options.ts +63 -0
- package/template/src/stories/sample.ts +24 -0
- package/template/src/styles/global.css +379 -0
- 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
|
+
])
|