create-bearminion 1.1.0 → 1.2.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 +21 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/template/README.md +5 -5
- package/template/content/pages/index.json +4 -0
- package/template/package.json +1 -1
- package/template/src/components/atoms/typography.ts +3 -2
- package/template/src/components/molecules/CommandLine.astro +109 -0
- package/template/src/components/molecules/SectionHeader.astro +1 -1
- package/template/src/components/sections/SectionList.astro +9 -0
- package/template/src/components/sections/cta/Cta.astro +36 -11
- package/template/src/components/sections/cta/definition.ts +16 -2
- package/template/src/components/sections/definitions.ts +6 -0
- package/template/src/components/sections/faq/Faq.astro +9 -4
- package/template/src/components/sections/faq/definition.ts +6 -3
- package/template/src/components/sections/features/Features.astro +40 -15
- package/template/src/components/sections/features/definition.ts +2 -12
- package/template/src/components/sections/hero/Hero.astro +60 -30
- package/template/src/components/sections/hero/HeroBadge.astro +26 -0
- package/template/src/components/sections/hero/definition.ts +41 -3
- package/template/src/components/sections/media/Media.astro +115 -0
- package/template/src/components/sections/media/Media.stories.ts +31 -0
- package/template/src/components/sections/media/MediaPlayer.astro +133 -0
- package/template/src/components/sections/media/definition.ts +72 -0
- package/template/src/components/sections/media/source.ts +33 -0
- package/template/src/components/sections/media/wireframe.ts +9 -0
- package/template/src/components/sections/registry.ts +6 -0
- package/template/src/components/sections/steps/Steps.astro +139 -0
- package/template/src/components/sections/steps/Steps.stories.ts +27 -0
- package/template/src/components/sections/steps/definition.ts +62 -0
- package/template/src/components/sections/steps/icons.ts +27 -0
- package/template/src/components/sections/steps/wireframe.ts +16 -0
- package/template/src/components/sections/text-image/TextImage.astro +2 -4
- package/template/src/components/sections/text-image/definition.ts +3 -2
- package/template/src/components/sections/video/Video.astro +78 -0
- package/template/src/components/sections/video/Video.stories.ts +17 -0
- package/template/src/components/sections/video/VideoFrame.astro +60 -0
- package/template/src/components/sections/video/definition.ts +58 -0
- package/template/src/components/sections/video/wireframe.ts +9 -0
- package/template/src/layouts/Base.astro +1 -1
- package/template/src/lib/image-url.ts +21 -0
- package/template/src/styles/global.css +18 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# create-bearminion
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4c3f823: New sections and section options in the default template, from the bearminion.com design:
|
|
8
|
+
|
|
9
|
+
- **Media**: one video or image under a heading, in centered, wide and split layouts, with a shape (16:9, 4:3, 21:9, 1:1) and a switch for the heading. A YouTube or Vimeo link plays through youtube-nocookie.com or Vimeo with Do Not Track, and nothing loads until the visitor presses play.
|
|
10
|
+
- **Steps**: two to six short points with an icon each, as cards, a rail or a list, numbered or not. The grid never leaves one card alone on a row.
|
|
11
|
+
- **Hero**: an install command with a Copy button in three styles (in the button row, a chip, or a terminal), a switch that hides the primary button, and a linked badge in place of the eyebrow, and an optional soft background glow in the accent color.
|
|
12
|
+
- **Features**: the bento layout takes three to six cards instead of exactly four.
|
|
13
|
+
- **Text + Image**: points are rich text, so they can hold bold, links and `code`.
|
|
14
|
+
- **Call to action**: the terminal layout is a dark terminal in every tone, and each printed line can start with a prompt such as → or ✓.
|
|
15
|
+
|
|
16
|
+
Fixes: headings render semibold and eyebrows in the mono font, as the type scale defines (a default weight and font were overriding them); a section with motion switched off is no longer hidden on a long page; an empty Video or Media frame shows only in the editor and is left out of the built site.
|
|
17
|
+
- 9261888: The default template has a new **Video** section (framed, wide and split layouts) for an `.mp4` or `.webm` file, with a poster image, a caption, and a `loop` playback option that plays muted while the video is on screen and stays paused with reduced motion. YouTube and other page links are refused with a message saying why. The hero gains an optional `command`, shown with a copy button, and the terminal call to action's printed lines are now editable (`output`) instead of fixed text.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- d584e76: The FAQ's split layout had a fixed "Contact us" link to `#contact` that could not be edited or removed, and pointed nowhere on a page without a contact section. It is now an optional `link`, editable in the editor like any other button.
|
|
22
|
+
- 19c1e5a: The default template uses the new field rules: Steps icons are picked from an icon grid, Media shows the Video, Poster and Label fields only for a video, the Image field only for an image, and the heading fields only while Show heading is on, and Hero hides the command while Install command is off, the primary button while it is switched off, the secondary button while the command takes its place, and the badge in the editorial layout.
|
|
23
|
+
|
|
3
24
|
## 1.1.0
|
|
4
25
|
|
|
5
26
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ The folder must be new or empty. Inside an existing git repository, the site is
|
|
|
54
54
|
|
|
55
55
|
## What you get
|
|
56
56
|
|
|
57
|
-
The [default BearMinion template](https://github.com/langehelp-software/bearminion/tree/main/templates/default): an Astro and Tailwind site with
|
|
57
|
+
The [default BearMinion template](https://github.com/langehelp-software/bearminion/tree/main/templates/default): an Astro and Tailwind site with thirteen section types, the [`bearminion`](https://www.npmjs.com/package/bearminion) editor at the same version as this package, and content as JSON files you own and commit. The template ships inside this package, so it works offline and always matches its editor.
|
|
58
58
|
|
|
59
59
|
## Learn more
|
|
60
60
|
|
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -29,18 +29,18 @@ This template is yours to change: it is the theme. `bearminion` is the only runt
|
|
|
29
29
|
|
|
30
30
|
Organisms are Header, Navigation, MobileMenu, and Footer. A modal, a dialog, and a cookie banner belong in this folder too. They are the chrome around every page (or a standalone widget), and they are fed by `content/site.json`.
|
|
31
31
|
|
|
32
|
-
Sections are `hero`, `logoStrip`, `features`, `textImage`, `stats`, `testimonials`, `pricing`, `faq`, `cta`, and `contact`. Each page entry is `{ id, anchor?, type, variant, tone, motion?, props }`. `anchor` is an optional lowercase slug, unique on the page, and it becomes the section element's `id`. `data-section-id` stays the section id. `[data-section-id]` sets `scroll-margin-top: 7rem` so a sticky or floating header does not cover the target. `html` uses `scroll-behavior: smooth`, and `prefers-reduced-motion: reduce` forces `scroll-behavior: auto`. Section components are built from molecules and atoms. A section may use an organism when it needs one, such as a modal. The section root carries `data-section-id`, `data-section-type`, `data-variant`, `data-tone`, and `data-motion`. Atoms accept a `class` and forward `data-*` attributes, including `data-field`, so the editor can find the text.
|
|
32
|
+
Sections are `hero`, `logoStrip`, `steps`, `features`, `textImage`, `stats`, `testimonials`, `pricing`, `video`, `media`, `faq`, `cta`, and `contact`. Each page entry is `{ id, anchor?, type, variant, tone, motion?, props }`. `anchor` is an optional lowercase slug, unique on the page, and it becomes the section element's `id`. `data-section-id` stays the section id. `[data-section-id]` sets `scroll-margin-top: 7rem` so a sticky or floating header does not cover the target. `html` uses `scroll-behavior: smooth`, and `prefers-reduced-motion: reduce` forces `scroll-behavior: auto`. Section components are built from molecules and atoms. A section may use an organism when it needs one, such as a modal. The section root carries `data-section-id`, `data-section-type`, `data-variant`, `data-tone`, and `data-motion`. Atoms accept a `class` and forward `data-*` attributes, including `data-field`, so the editor can find the text.
|
|
33
33
|
|
|
34
|
-
Paragraph fields (`sub`, `intro`, `body`, `description`, `quote`, `note`, `thanks`, and
|
|
34
|
+
Paragraph fields (`sub`, `intro`, `body`, `description`, `quote`, `note`, `thanks`, FAQ answers and Text + Image `points`) are markdown. The template renders bold, italic, inline `code` (a mono chip), links, and paragraphs through `bearminion/rich-text`, escapes every other character, and adds `rel="noopener noreferrer"` on `http` and `https` links.
|
|
35
35
|
|
|
36
36
|
```text
|
|
37
37
|
src/components/
|
|
38
38
|
atoms/ Typography, Button, Image, LogoMark, Emphasis
|
|
39
|
-
molecules/ SectionShell, SectionHeader, MediaFrame, Pill
|
|
39
|
+
molecules/ SectionShell, SectionHeader, MediaFrame, Pill, CommandLine
|
|
40
40
|
organisms/ Header, Navigation, MobileMenu, Footer, plus site-definition.ts (header and footer layouts, limits)
|
|
41
41
|
sections/ one directory per type, plus definitions.ts, registry.ts, shared.ts, SectionList.astro
|
|
42
42
|
hero/ definition.ts (zod schema, defaults, options), wireframe.ts (picker thumbnail), Hero.astro, Hero.stories.ts
|
|
43
|
-
logo-strip/ features/ text-image/ stats/ testimonials/ pricing/ faq/ cta/ contact/
|
|
43
|
+
logo-strip/ steps/ features/ text-image/ stats/ testimonials/ pricing/ video/ media/ faq/ cta/ contact/
|
|
44
44
|
seo/ Seo
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -196,7 +196,7 @@ The style guide at `/style-guide` renders every typography size and button varia
|
|
|
196
196
|
|
|
197
197
|
The page gets a shadow-root overlay. Click text with `data-field` to edit it, and hover a section (`data-section-id`) for add, duplicate, move, and delete. The corner **Editor** button or Ctrl+E / Cmd+E opens the sidebar. Ctrl+S / Cmd+S saves. Escape leaves a field, then closes the add picker or the sidebar. Unsaved edits turn the corner dot amber and trigger a beforeunload warning.
|
|
198
198
|
|
|
199
|
-
A variant change
|
|
199
|
+
A variant change, a new, duplicated, moved, or deleted section, and a sidebar value with no text of its own on the page (such as a video link) are re-rendered by `POST /__bearminion/api/preview`. The handler validates the draft, renders `SectionList` with Astro's container API inside the Vite SSR runner, and returns HTML. The overlay replaces `main#content` and runs scripts the page had not loaded, so a section type added from the picker works at once. A section script binds on load and again on the `bearminion:preview` event, and marks what it bound so it never binds twice. Nothing is written until save. In dev, an image path under `src/assets/` that is missing from the eager glob is served from `/__bearminion/assets` so a new upload can use a plain `<img>` immediately. When the file is gone, that route returns a Missing image placeholder instead, so the page shows the gap. Production still resolves that path with `import.meta.glob` and Astro `<Image>`, and still throws if the file is missing; the editor's save validation keeps content from pointing at a missing file.
|
|
200
200
|
|
|
201
201
|
Header links nest up to three levels (`MAX_NAV_DEPTH`). Desktop navigation uses hover and focus flyouts. The mobile menu uses an accordion. The link picker lists pages, `#anchor` on the current page, `/page#anchor` on other pages, and a custom URL.
|
|
202
202
|
|
package/template/package.json
CHANGED
|
@@ -18,6 +18,7 @@ export const typography = cva('text-balance', {
|
|
|
18
18
|
'mx-auto max-w-[16ch] font-sans text-[clamp(2.75rem,1.2rem+6.6cqi,7rem)] leading-[0.93] font-semibold tracking-[-0.05em]',
|
|
19
19
|
'display-editorial': 'font-sans text-[clamp(3.25rem,0.5rem+11cqi,10.5rem)] leading-[0.86] font-semibold tracking-[-0.06em]',
|
|
20
20
|
h2: 'font-sans text-[clamp(2rem,1.2rem+3.4cqi,3.75rem)] leading-none font-semibold tracking-[-0.04em]',
|
|
21
|
+
'h2-md': 'font-sans text-[clamp(2.25rem,1.3rem+3.6cqi,4rem)] leading-[0.98] font-semibold tracking-[-0.045em]',
|
|
21
22
|
'h2-lg': 'font-sans text-[clamp(2.25rem,1.2rem+4.4cqi,4.75rem)] leading-[0.95] font-semibold tracking-[-0.05em]',
|
|
22
23
|
h3: 'font-sans text-[1.25rem] leading-[1.25] font-semibold tracking-[-0.02em]',
|
|
23
24
|
lead: 'text-pretty font-sans text-[clamp(1.0625rem,1rem+0.35cqi,1.25rem)] leading-[1.55]',
|
|
@@ -52,11 +53,11 @@ export const typography = cva('text-balance', {
|
|
|
52
53
|
right: 'text-right',
|
|
53
54
|
},
|
|
54
55
|
},
|
|
56
|
+
// No default weight or font: the size already sets them (headings are semibold, eyebrows mono), and a default here
|
|
57
|
+
// would be merged in after the size and override it.
|
|
55
58
|
defaultVariants: {
|
|
56
59
|
size: 'body',
|
|
57
60
|
color: 'default',
|
|
58
|
-
weight: 'regular',
|
|
59
|
-
font: 'sans',
|
|
60
61
|
},
|
|
61
62
|
})
|
|
62
63
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cn } from '../../lib/cn'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** The shell command, such as `npm create bearminion@latest`. */
|
|
6
|
+
command: string
|
|
7
|
+
/**
|
|
8
|
+
* `chip`: a small pill. `action`: a pill as tall as a button, for a button row. `terminal`: a dark block with a
|
|
9
|
+
* "Terminal" bar, the same in every tone.
|
|
10
|
+
*/
|
|
11
|
+
variant?: 'chip' | 'action' | 'terminal'
|
|
12
|
+
class?: string
|
|
13
|
+
'data-field'?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { command, variant = 'chip', class: className, 'data-field': dataField } = Astro.props
|
|
17
|
+
const copyIcon = 'M8 8h11v11H8zM5 16V5h11'
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
variant === 'terminal' ? (
|
|
22
|
+
<div
|
|
23
|
+
class={cn(
|
|
24
|
+
'w-full max-w-[30rem] overflow-hidden rounded-2xl bg-[oklch(0.17_0.012_255)] text-left text-[oklch(0.93_0.004_250)] shadow-[0_0_0_1px_oklch(0.97_0.004_250/0.08),0_32px_64px_-40px_oklch(0.15_0.02_255/0.6)]',
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
data-command-line
|
|
28
|
+
>
|
|
29
|
+
<div class="flex items-center justify-between border-b border-[oklch(0.97_0.004_250/0.08)] py-2 ps-4 pe-2">
|
|
30
|
+
<span class="font-mono text-[0.6875rem] tracking-[0.08em] text-[oklch(0.93_0.004_250/0.55)] uppercase">Terminal</span>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
class="h-[1.875rem] rounded-full bg-[oklch(0.97_0.004_250/0.08)] px-3 text-[0.75rem] font-medium transition-colors hover:bg-[oklch(0.97_0.004_250/0.14)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
|
34
|
+
aria-label="Copy command"
|
|
35
|
+
data-copy-command
|
|
36
|
+
>
|
|
37
|
+
<span data-copy-label>Copy</span>
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="flex gap-3 overflow-x-auto p-4 font-mono text-[0.8125rem] leading-[1.6] whitespace-pre">
|
|
41
|
+
<span class="text-[oklch(0.87_0.2_145)] select-none" aria-hidden="true">$</span>
|
|
42
|
+
<code data-command data-field={dataField}>{command}</code>
|
|
43
|
+
</div>
|
|
44
|
+
<span class="sr-only" role="status" data-copy-status />
|
|
45
|
+
</div>
|
|
46
|
+
) : (
|
|
47
|
+
<div
|
|
48
|
+
class={cn(
|
|
49
|
+
'inline-flex max-w-full items-center rounded-full bg-surface font-mono text-fg ring-line',
|
|
50
|
+
variant === 'action' ? 'h-12 gap-3.5 ps-[1.125rem] pe-1.5 text-[0.875rem]' : 'h-11 gap-3 ps-4 pe-1.5 text-[0.8125rem]',
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
data-command-line
|
|
54
|
+
>
|
|
55
|
+
<span class="text-muted select-none" aria-hidden="true">$</span>
|
|
56
|
+
<code class="min-w-0 truncate" data-command data-field={dataField}>{command}</code>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
class={cn(
|
|
60
|
+
'inline-flex shrink-0 items-center gap-1.5 rounded-full bg-fg font-sans font-medium text-bg transition-opacity hover:opacity-85 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
|
|
61
|
+
variant === 'action' ? 'h-9 px-3.5 text-[0.8125rem]' : 'h-8 px-3 text-[0.75rem]',
|
|
62
|
+
)}
|
|
63
|
+
aria-label="Copy command"
|
|
64
|
+
data-copy-command
|
|
65
|
+
>
|
|
66
|
+
{variant === 'action' && (
|
|
67
|
+
<svg viewBox="0 0 24 24" class="size-[13px]" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true">
|
|
68
|
+
<path d={copyIcon} />
|
|
69
|
+
</svg>
|
|
70
|
+
)}
|
|
71
|
+
<span data-copy-label>Copy</span>
|
|
72
|
+
</button>
|
|
73
|
+
<span class="sr-only" role="status" data-copy-status />
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
<script>
|
|
79
|
+
/** Copies the command as it is on the page now, so an edit in the editor is what gets copied. */
|
|
80
|
+
function bindCommandLines() {
|
|
81
|
+
for (const button of document.querySelectorAll<HTMLButtonElement>('[data-copy-command]')) {
|
|
82
|
+
if (button.dataset.bound === 'true') continue
|
|
83
|
+
button.dataset.bound = 'true'
|
|
84
|
+
const line = button.closest('[data-command-line]')
|
|
85
|
+
const label = button.querySelector('[data-copy-label]')
|
|
86
|
+
const status = line?.querySelector('[data-copy-status]')
|
|
87
|
+
button.addEventListener('click', async () => {
|
|
88
|
+
const code = line?.querySelector('[data-command]')
|
|
89
|
+
const command = code?.textContent?.trim() ?? ''
|
|
90
|
+
if (!command) return
|
|
91
|
+
try {
|
|
92
|
+
await navigator.clipboard.writeText(command)
|
|
93
|
+
} catch {
|
|
94
|
+
// No clipboard access (an insecure origin, or the permission was refused): select the text instead.
|
|
95
|
+
if (code) window.getSelection()?.selectAllChildren(code)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
if (label) label.textContent = 'Copied'
|
|
99
|
+
if (status) status.textContent = 'Command copied'
|
|
100
|
+
window.setTimeout(() => {
|
|
101
|
+
if (label) label.textContent = 'Copy'
|
|
102
|
+
if (status) status.textContent = ''
|
|
103
|
+
}, 1600)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
bindCommandLines()
|
|
108
|
+
document.addEventListener('bearminion:preview', bindCommandLines)
|
|
109
|
+
</script>
|
|
@@ -11,7 +11,7 @@ interface Props {
|
|
|
11
11
|
intro?: string
|
|
12
12
|
align?: 'left' | 'center'
|
|
13
13
|
headingAs?: 'h1' | 'h2'
|
|
14
|
-
size?: 'h2' | 'h2-lg' | 'display' | 'display-center' | 'display-editorial'
|
|
14
|
+
size?: 'h2' | 'h2-md' | 'h2-lg' | 'display' | 'display-center' | 'display-editorial'
|
|
15
15
|
field?: (path: string) => string
|
|
16
16
|
/** Prop name used for the intro `data-field` path. */
|
|
17
17
|
introField?: string
|
|
@@ -11,11 +11,14 @@ interface Props {
|
|
|
11
11
|
const { sections, motion = true } = Astro.props
|
|
12
12
|
const Hero = sectionComponents.hero
|
|
13
13
|
const LogoStrip = sectionComponents.logoStrip
|
|
14
|
+
const Steps = sectionComponents.steps
|
|
14
15
|
const Features = sectionComponents.features
|
|
15
16
|
const TextImage = sectionComponents.textImage
|
|
16
17
|
const Stats = sectionComponents.stats
|
|
17
18
|
const Testimonials = sectionComponents.testimonials
|
|
18
19
|
const Pricing = sectionComponents.pricing
|
|
20
|
+
const Video = sectionComponents.video
|
|
21
|
+
const Media = sectionComponents.media
|
|
19
22
|
const Faq = sectionComponents.faq
|
|
20
23
|
const Cta = sectionComponents.cta
|
|
21
24
|
const Contact = sectionComponents.contact
|
|
@@ -28,6 +31,8 @@ const Contact = sectionComponents.contact
|
|
|
28
31
|
return <Hero section={section} index={index} motion={motion} />
|
|
29
32
|
case 'logoStrip':
|
|
30
33
|
return <LogoStrip section={section} index={index} motion={motion} />
|
|
34
|
+
case 'steps':
|
|
35
|
+
return <Steps section={section} index={index} motion={motion} />
|
|
31
36
|
case 'features':
|
|
32
37
|
return <Features section={section} index={index} motion={motion} />
|
|
33
38
|
case 'textImage':
|
|
@@ -38,6 +43,10 @@ const Contact = sectionComponents.contact
|
|
|
38
43
|
return <Testimonials section={section} index={index} motion={motion} />
|
|
39
44
|
case 'pricing':
|
|
40
45
|
return <Pricing section={section} index={index} motion={motion} />
|
|
46
|
+
case 'video':
|
|
47
|
+
return <Video section={section} index={index} motion={motion} />
|
|
48
|
+
case 'media':
|
|
49
|
+
return <Media section={section} index={index} motion={motion} />
|
|
41
50
|
case 'faq':
|
|
42
51
|
return <Faq section={section} index={index} motion={motion} />
|
|
43
52
|
case 'cta':
|
|
@@ -109,7 +109,7 @@ const bandSecondary = props.secondary ? buttonVariant(props.secondary, 'secondar
|
|
|
109
109
|
}
|
|
110
110
|
{
|
|
111
111
|
section.variant === 'terminal' && (
|
|
112
|
-
<div class="section-container grid
|
|
112
|
+
<div class="section-container grid grid-cols-[repeat(auto-fit,minmax(min(100%,22rem),1fr))] items-center gap-[clamp(2.5rem,6cqi,5rem)]" data-cta>
|
|
113
113
|
{
|
|
114
114
|
(props.heading || props.body || hasActions) && (
|
|
115
115
|
<div class="flex flex-col items-start gap-6">
|
|
@@ -136,20 +136,43 @@ const bandSecondary = props.secondary ? buttonVariant(props.secondary, 'secondar
|
|
|
136
136
|
</div>
|
|
137
137
|
)
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
{/* A dark terminal in every tone, like the one on your machine. Nothing to print, no terminal. */}
|
|
140
|
+
{(props.command || (props.output ?? []).length > 0) && (<div class="overflow-hidden rounded-[1.25rem] bg-[oklch(0.17_0.012_255)] text-[oklch(0.93_0.004_250)] shadow-[0_0_0_1px_oklch(0.97_0.004_250/0.08),0_40px_80px_-40px_oklch(0.15_0.02_255/0.6)]">
|
|
141
|
+
<div class="flex items-center justify-between border-b border-[oklch(0.97_0.004_250/0.08)] py-3 ps-4 pe-3">
|
|
142
|
+
<div class="flex gap-1.5" aria-hidden="true">
|
|
143
|
+
<span class="size-2.5 rounded-full bg-[oklch(0.97_0.004_250/0.18)]" />
|
|
144
|
+
<span class="size-2.5 rounded-full bg-[oklch(0.97_0.004_250/0.18)]" />
|
|
145
|
+
<span class="size-2.5 rounded-full bg-[oklch(0.97_0.004_250/0.18)]" />
|
|
146
|
+
</div>
|
|
142
147
|
{props.command && (
|
|
143
|
-
<button
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
data-copy
|
|
151
|
+
class="h-[1.875rem] rounded-full bg-[oklch(0.97_0.004_250/0.08)] px-3 text-[0.75rem] font-medium transition-colors hover:bg-[oklch(0.97_0.004_250/0.14)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[oklch(0.87_0.2_145)]"
|
|
152
|
+
aria-label="Copy command"
|
|
153
|
+
data-command={props.command}
|
|
154
|
+
>
|
|
144
155
|
<span data-copy-label>Copy</span>
|
|
145
156
|
</button>
|
|
146
157
|
)}
|
|
147
158
|
</div>
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
<ol class="flex flex-col gap-2.5 overflow-x-auto px-5 pt-5 pb-6 font-mono text-[clamp(0.75rem,0.7rem+0.2cqi,0.875rem)] leading-[1.6]">
|
|
160
|
+
{props.command && (
|
|
161
|
+
<li class="flex gap-3 whitespace-pre">
|
|
162
|
+
<span class="text-[oklch(0.87_0.2_145)] select-none" aria-hidden="true">$</span>
|
|
163
|
+
<span data-terminal-command data-field={field('command')}>{props.command}</span>
|
|
164
|
+
</li>
|
|
165
|
+
)}
|
|
166
|
+
{(props.output ?? []).map((line, lineIndex) => (
|
|
167
|
+
<li class="flex gap-3 whitespace-pre">
|
|
168
|
+
<span class="min-w-[1ch] text-[oklch(0.87_0.2_145)] select-none" aria-hidden={line.prompt ? undefined : 'true'} data-field={line.prompt ? field(`output.${lineIndex}.prompt`) : undefined}>
|
|
169
|
+
{line.prompt ?? ' '}
|
|
170
|
+
</span>
|
|
171
|
+
<span class="text-[oklch(0.93_0.004_250/0.45)]" data-field={field(`output.${lineIndex}.text`)}>{line.text}</span>
|
|
172
|
+
</li>
|
|
173
|
+
))}
|
|
174
|
+
</ol>
|
|
175
|
+
</div>)}
|
|
153
176
|
</div>
|
|
154
177
|
)
|
|
155
178
|
}
|
|
@@ -163,7 +186,9 @@ const bandSecondary = props.secondary ? buttonVariant(props.secondary, 'secondar
|
|
|
163
186
|
const label = button.querySelector('[data-copy-label]')
|
|
164
187
|
const original = label?.textContent ?? 'Copy'
|
|
165
188
|
button.addEventListener('click', async () => {
|
|
166
|
-
|
|
189
|
+
// The command as it is on the page now, so an edit made in the editor is what gets copied.
|
|
190
|
+
const shown = button.closest('[data-cta]')?.querySelector('[data-terminal-command]')?.textContent?.trim()
|
|
191
|
+
const command = shown || button.dataset.command || ''
|
|
167
192
|
try {
|
|
168
193
|
await navigator.clipboard.writeText(command)
|
|
169
194
|
} catch {
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod'
|
|
2
2
|
import { defineSection, linkSchema, richTextSchema, textSchema, withEmphasis } from 'bearminion'
|
|
3
3
|
import { accent, heading } from '../shared'
|
|
4
4
|
import { ctaWireframe } from './wireframe'
|
|
5
5
|
|
|
6
6
|
export const ctaVariants = ['band', 'centered', 'terminal'] as const
|
|
7
7
|
|
|
8
|
+
export const ctaLineSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
prompt: textSchema.optional(),
|
|
11
|
+
text: textSchema,
|
|
12
|
+
})
|
|
13
|
+
.strict()
|
|
14
|
+
|
|
8
15
|
export const ctaSchema = withEmphasis({
|
|
9
16
|
heading,
|
|
10
17
|
body: richTextSchema.optional(),
|
|
11
18
|
command: textSchema.optional(),
|
|
19
|
+
/** Lines the terminal variant shows under the command, such as what it prints. `prompt` is a mark before the line, such as → or ✓. */
|
|
20
|
+
output: z.array(ctaLineSchema).max(8).optional(),
|
|
12
21
|
primary: linkSchema.optional(),
|
|
13
22
|
secondary: linkSchema.optional(),
|
|
14
23
|
})
|
|
@@ -25,11 +34,16 @@ export const cta = defineSection({
|
|
|
25
34
|
marks: accent('Ready to get started?', 'get started?'),
|
|
26
35
|
body: 'One sentence that repeats the main benefit and tells the reader what happens next.',
|
|
27
36
|
command: 'npm create astro@latest',
|
|
37
|
+
output: [
|
|
38
|
+
{ text: '# pages live in content/pages' },
|
|
39
|
+
{ text: '# site chrome lives in content/site.json' },
|
|
40
|
+
{ text: '# production is the static build' },
|
|
41
|
+
],
|
|
28
42
|
primary: { label: 'Primary action', href: '#' },
|
|
29
43
|
secondary: { label: 'Secondary action', href: '#' },
|
|
30
44
|
},
|
|
31
45
|
variants: ctaVariants,
|
|
32
46
|
defaultVariant: 'band',
|
|
33
|
-
optional: { props: ['heading', 'body', 'command', 'primary', 'secondary'] },
|
|
47
|
+
optional: { props: ['heading', 'body', 'command', 'output', 'primary', 'secondary'], items: { output: ['prompt'] } },
|
|
34
48
|
wireframe: ctaWireframe,
|
|
35
49
|
})
|
|
@@ -12,10 +12,13 @@ import { faq } from './faq/definition'
|
|
|
12
12
|
import { features } from './features/definition'
|
|
13
13
|
import { hero } from './hero/definition'
|
|
14
14
|
import { logoStrip } from './logo-strip/definition'
|
|
15
|
+
import { media } from './media/definition'
|
|
15
16
|
import { pricing } from './pricing/definition'
|
|
16
17
|
import { stats } from './stats/definition'
|
|
18
|
+
import { steps } from './steps/definition'
|
|
17
19
|
import { testimonials } from './testimonials/definition'
|
|
18
20
|
import { textImage } from './text-image/definition'
|
|
21
|
+
import { video } from './video/definition'
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Every section type of this site. Each one is defined in its own directory:
|
|
@@ -27,11 +30,14 @@ import { textImage } from './text-image/definition'
|
|
|
27
30
|
export const sectionDefinitions = {
|
|
28
31
|
hero,
|
|
29
32
|
logoStrip,
|
|
33
|
+
steps,
|
|
30
34
|
features,
|
|
31
35
|
textImage,
|
|
32
36
|
stats,
|
|
33
37
|
testimonials,
|
|
34
38
|
pricing,
|
|
39
|
+
video,
|
|
40
|
+
media,
|
|
35
41
|
faq,
|
|
36
42
|
cta,
|
|
37
43
|
contact,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
2
|
+
import { anchorAttrs } from '../../../lib/link-attrs'
|
|
3
3
|
import type { Section } from '../definitions'
|
|
4
4
|
import Typography from '../../atoms/Typography.astro'
|
|
5
5
|
import SectionHeader from '../../molecules/SectionHeader.astro'
|
|
@@ -50,9 +50,14 @@ const reveal = section.motion ?? motion
|
|
|
50
50
|
{props.more}
|
|
51
51
|
</Typography>
|
|
52
52
|
)}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
{props.link && (
|
|
54
|
+
<a
|
|
55
|
+
{...anchorAttrs(props.link)}
|
|
56
|
+
class="text-[0.9375rem] font-medium text-link underline-offset-4 hover:underline"
|
|
57
|
+
>
|
|
58
|
+
<span data-field={field('link.label')}>{props.link.label}</span> <span aria-hidden="true">→</span>
|
|
59
|
+
</a>
|
|
60
|
+
)}
|
|
56
61
|
</div>
|
|
57
62
|
<div>
|
|
58
63
|
{props.items.map((item, itemIndex) => (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import { defineSection, richTextSchema, textSchema, withEmphasis, type CountRule } from 'bearminion'
|
|
3
|
-
import { accent, eyebrow, heading } from '../shared'
|
|
2
|
+
import { defineSection, linkSchema, richTextSchema, textSchema, withEmphasis, type CountRule } from 'bearminion'
|
|
3
|
+
import { DEFAULT_CONTACT_ANCHOR, accent, eyebrow, heading } from '../shared'
|
|
4
4
|
import { faqWireframe } from './wireframe'
|
|
5
5
|
|
|
6
6
|
export const faqVariants = ['accordion', 'split', 'grid'] as const
|
|
@@ -21,6 +21,8 @@ export const faqSchema = withEmphasis({
|
|
|
21
21
|
eyebrow,
|
|
22
22
|
heading,
|
|
23
23
|
more: textSchema.optional(),
|
|
24
|
+
/** The link under the heading in the split layout, such as one to the contact section. */
|
|
25
|
+
link: linkSchema.optional(),
|
|
24
26
|
items: z.array(faqItemSchema).min(faqCounts.items.min).max(faqCounts.items.max),
|
|
25
27
|
})
|
|
26
28
|
|
|
@@ -36,6 +38,7 @@ export const faq = defineSection({
|
|
|
36
38
|
eyebrow: 'FAQ',
|
|
37
39
|
heading: 'Questions, answered plainly.',
|
|
38
40
|
marks: accent('Questions, answered plainly.', 'plainly.'),
|
|
41
|
+
link: { label: 'Contact us', href: `#${DEFAULT_CONTACT_ANCHOR}` },
|
|
39
42
|
items: [
|
|
40
43
|
{ q: 'What is included?', a: 'Answer the question people ask most before they buy, in two or three sentences.' },
|
|
41
44
|
{ q: 'How long does it take to get started?', a: 'Give a realistic time frame and say what the reader needs to have ready.' },
|
|
@@ -45,7 +48,7 @@ export const faq = defineSection({
|
|
|
45
48
|
},
|
|
46
49
|
variants: faqVariants,
|
|
47
50
|
defaultVariant: 'accordion',
|
|
48
|
-
optional: { props: ['eyebrow', 'heading', 'more'] },
|
|
51
|
+
optional: { props: ['eyebrow', 'heading', 'more', 'link'] },
|
|
49
52
|
counts: faqCounts,
|
|
50
53
|
wireframe: faqWireframe,
|
|
51
54
|
})
|
|
@@ -6,6 +6,7 @@ import Typography from '../../atoms/Typography.astro'
|
|
|
6
6
|
import MediaFrame from '../../molecules/MediaFrame.astro'
|
|
7
7
|
import SectionHeader from '../../molecules/SectionHeader.astro'
|
|
8
8
|
import SectionShell from '../../molecules/SectionShell.astro'
|
|
9
|
+
import { cn } from '../../../lib/cn'
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
section: Extract<Section, { type: 'features' }>
|
|
@@ -17,6 +18,9 @@ const { section, index, motion = true } = Astro.props
|
|
|
17
18
|
const props = section.props
|
|
18
19
|
const field = (path: string) => `sections.${index}.props.${path}`
|
|
19
20
|
const reveal = section.motion ?? motion
|
|
21
|
+
const wideFourth = props.items.length === 4
|
|
22
|
+
/** At two columns, an even count leaves the last card alone on its row, so it spans the row with its text beside the picture. */
|
|
23
|
+
const spanLast = (itemIndex: number) => !wideFourth && itemIndex === props.items.length - 1 && props.items.length % 2 === 0
|
|
20
24
|
---
|
|
21
25
|
|
|
22
26
|
<SectionShell id={section.id} anchor={section.anchor} type={section.type} variant={section.variant} tone={section.tone} motion={reveal}>
|
|
@@ -88,29 +92,50 @@ const reveal = section.motion ?? motion
|
|
|
88
92
|
section.variant === 'bento' && (
|
|
89
93
|
<div class="section-container">
|
|
90
94
|
<SectionHeader eyebrow={props.eyebrow} heading={props.heading} marks={props.marks} intro={props.intro} align="center" field={field} class="mx-auto max-w-3xl" />
|
|
91
|
-
<ul class="mt-
|
|
95
|
+
<ul class="mt-[clamp(2.5rem,4cqi,4rem)] grid grid-cols-1 gap-4 @min-[620px]:grid-cols-2 @min-[960px]:grid-cols-3">
|
|
92
96
|
{props.items.map((item, itemIndex) => (
|
|
93
97
|
<li
|
|
94
98
|
class:list={[
|
|
95
|
-
'flex flex-col gap-4 rounded-card-lg bg-surface p-
|
|
96
|
-
|
|
97
|
-
itemIndex ===
|
|
99
|
+
'flex min-h-72 flex-col gap-4 rounded-card-lg bg-surface p-2 ring-line',
|
|
100
|
+
// The first card is the big one: two columns, and two rows at three columns.
|
|
101
|
+
itemIndex === 0 && '@min-[620px]:col-span-2 @min-[620px]:min-h-[26rem] @min-[960px]:row-span-2',
|
|
102
|
+
// Four cards: the fourth spans the row, with its text beside the picture at three columns.
|
|
103
|
+
wideFourth &&
|
|
104
|
+
itemIndex === 3 &&
|
|
105
|
+
'@min-[620px]:col-span-2 @min-[960px]:col-span-3 @min-[960px]:flex-row-reverse @min-[960px]:items-center',
|
|
106
|
+
// Any other count at two columns: the last card spans the row when it would sit alone, picture beside text.
|
|
107
|
+
spanLast(itemIndex) && '@min-[620px]:@max-[959px]:col-span-2 @min-[620px]:@max-[959px]:flex-row-reverse @min-[620px]:@max-[959px]:items-center',
|
|
98
108
|
]}
|
|
99
109
|
>
|
|
100
110
|
{!item.image?.hidden && (
|
|
101
|
-
<MediaFrame
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
<MediaFrame
|
|
112
|
+
src={item.image?.src}
|
|
113
|
+
alt={item.image?.alt ?? item.title}
|
|
114
|
+
aspectClass={cn(
|
|
115
|
+
'min-h-36 flex-1',
|
|
116
|
+
wideFourth && itemIndex === 3 && '@min-[960px]:self-stretch',
|
|
117
|
+
spanLast(itemIndex) && '@min-[620px]:@max-[959px]:self-stretch',
|
|
118
|
+
)}
|
|
119
|
+
roundedClass="rounded-2xl"
|
|
120
|
+
class="p-3"
|
|
121
|
+
fit={item.image?.fit}
|
|
122
|
+
position={item.image?.position}
|
|
123
|
+
data-field={field(`items.${itemIndex}.image.alt`)}
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
<div
|
|
127
|
+
class:list={[
|
|
128
|
+
'flex flex-col gap-1.5 px-2 pt-1 pb-2',
|
|
129
|
+
wideFourth && itemIndex === 3 && '@min-[960px]:flex-1 @min-[960px]:px-6',
|
|
130
|
+
spanLast(itemIndex) && '@min-[620px]:@max-[959px]:flex-1 @min-[620px]:@max-[959px]:px-6',
|
|
131
|
+
]}
|
|
132
|
+
>
|
|
133
|
+
<Typography as="h3" size="h3" data-field={field(`items.${itemIndex}.title`)}>
|
|
111
134
|
{item.title}
|
|
112
135
|
</Typography>
|
|
113
|
-
{item.description &&
|
|
136
|
+
{item.description && (
|
|
137
|
+
<Typography as="p" size="body" color="muted" class="max-w-[30rem]" data-field={field(`items.${itemIndex}.description`)} markdown={item.description} />
|
|
138
|
+
)}
|
|
114
139
|
</div>
|
|
115
140
|
</li>
|
|
116
141
|
))}
|
|
@@ -5,9 +5,9 @@ import { featuresWireframe } from './wireframe'
|
|
|
5
5
|
|
|
6
6
|
export const featuresVariants = ['cards', 'list', 'bento'] as const
|
|
7
7
|
|
|
8
|
-
/** Three to six cards
|
|
8
|
+
/** Three to six cards. In the bento grid, four keeps a wide fourth card; any other count fills rows of three. */
|
|
9
9
|
export const featuresCounts = {
|
|
10
|
-
items: { recommended: '4
|
|
10
|
+
items: { recommended: '3–6; 4 or 6 fill the bento grid best', min: 3, max: 6 },
|
|
11
11
|
} as const satisfies Record<string, CountRule>
|
|
12
12
|
|
|
13
13
|
export const featureItemSchema = z
|
|
@@ -51,14 +51,4 @@ export const features = defineSection({
|
|
|
51
51
|
optional: { props: ['eyebrow', 'heading', 'intro', 'link'], items: { items: ['description', 'image'] } },
|
|
52
52
|
counts: featuresCounts,
|
|
53
53
|
wireframe: featuresWireframe,
|
|
54
|
-
refine({ variant, props }, ctx) {
|
|
55
|
-
const { variant: exactVariant, count } = featuresCounts.items.exact
|
|
56
|
-
if (variant === exactVariant && props.items.length !== count) {
|
|
57
|
-
ctx.addIssue({
|
|
58
|
-
code: 'custom',
|
|
59
|
-
message: `The ${exactVariant} variant needs exactly ${count} feature items`,
|
|
60
|
-
path: ['props', 'items'],
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
54
|
})
|