@okjt/site 0.0.0-stage → 0.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/README.md +122 -2
- package/dist/FaqSection.js +33 -0
- package/dist/FeatureExplorer.js +78 -0
- package/dist/FeatureGroup.js +47 -0
- package/dist/ImageCard.js +37 -0
- package/dist/Journey.js +51 -0
- package/dist/LegalPage.js +94 -0
- package/dist/MediaFrame.js +55 -0
- package/dist/Photo.js +56 -0
- package/dist/PhotoHero.js +37 -0
- package/dist/PricingSection.js +38 -0
- package/dist/RollingWord.js +19 -0
- package/dist/SectionNav.js +36 -0
- package/dist/Seo.js +55 -0
- package/dist/ShowcaseCarousel.js +60 -0
- package/dist/SiteFooter.js +74 -0
- package/dist/SiteLayout.js +36 -0
- package/dist/SiteNav.js +86 -0
- package/dist/SiteProvider.js +33 -0
- package/dist/index.js +58 -0
- package/dist/ui.js +55 -0
- package/dist/useRolling.js +18 -0
- package/dist/useScrollSpy.js +28 -0
- package/package.json +21 -4
package/README.md
CHANGED
|
@@ -1,3 +1,123 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @okjt/site
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The public pages of an OKJTech project: layout, navigation, footer, SEO, and
|
|
4
|
+
the sections pages are built from (photo hero, feature explorer, showcase,
|
|
5
|
+
pricing, FAQ, legal documents). Colours and fonts come from `@okjt/ui`'s
|
|
6
|
+
theme; words, links, pictures and screens come from the project's `site`
|
|
7
|
+
settings.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @okjt/site
|
|
13
|
+
npm i -D @okjt/ui
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Peer dependencies: `react`, `react-router-dom`, `framer-motion`, `lucide-react`.
|
|
17
|
+
|
|
18
|
+
Let Tailwind see the kit's class names, after the theme (see `@okjt/ui`):
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
@import "tailwindcss";
|
|
22
|
+
@import "./okjt-theme.css";
|
|
23
|
+
@source "../node_modules/@okjt/site/dist";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Set it up
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
import { SiteProvider } from '@okjt/site';
|
|
30
|
+
import { site } from './site.config';
|
|
31
|
+
|
|
32
|
+
<SiteProvider site={site}>
|
|
33
|
+
<App />
|
|
34
|
+
</SiteProvider>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
// src/site.config.jsx
|
|
39
|
+
import { ShieldCheck, FileText } from 'lucide-react';
|
|
40
|
+
import { useAuth } from './context/AuthContext';
|
|
41
|
+
|
|
42
|
+
export const site = {
|
|
43
|
+
brand: { name: 'Najenga', logo: { light: '/brand/logo-light.png', dark: '/brand/logo-dark.png' } },
|
|
44
|
+
|
|
45
|
+
// `mega: { Menu, MobileList, id, buttonLabel }` on one item opens a large menu under the bar.
|
|
46
|
+
nav: [
|
|
47
|
+
{ to: '/', label: 'Home' },
|
|
48
|
+
{ to: '/features', label: 'Features' },
|
|
49
|
+
{ to: '/#pricing', label: 'Pricing' },
|
|
50
|
+
],
|
|
51
|
+
|
|
52
|
+
useAccount: () => ({ signedIn: useAuth().isAuthenticated }),
|
|
53
|
+
accountLinks: {
|
|
54
|
+
signIn: { to: '/sign-in', label: 'Sign in' },
|
|
55
|
+
signUp: { to: '/sign-up', label: 'Get started' },
|
|
56
|
+
home: { to: '/projects', label: 'Your projects' },
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// Photos are keys; these two hooks turn a key into a URL and alt text, and a
|
|
60
|
+
// named place on the pages ("footer", "group-money") into its photo(s).
|
|
61
|
+
usePhotos: () => ({ url: (key, width) => `/photos/${key}${width <= 900 ? '-sm' : ''}.webp`, alt: (key) => ALT[key] }),
|
|
62
|
+
usePlaces: () => (place) => PLACES[place],
|
|
63
|
+
|
|
64
|
+
// Product screens and clips: { src, alt, url, video?, phone? }.
|
|
65
|
+
media: { dashboard: { src: '/screens/dashboard.webp', alt: 'The dashboard', url: 'najenga.co.ke/dashboard' } },
|
|
66
|
+
|
|
67
|
+
footer: {
|
|
68
|
+
about: 'Najenga keeps every shilling and every site photo of your build in one place.',
|
|
69
|
+
columns: [{ title: 'Product', links: [{ to: '/features', label: 'Features' }, { to: '/sign-in', label: 'Sign in', signedIn: false }] }],
|
|
70
|
+
promises: [{ icon: ShieldCheck, text: 'Your records stay yours' }],
|
|
71
|
+
cta: {
|
|
72
|
+
place: 'footer', eyebrow: 'Start today', title: 'Build with your eyes open.', text: 'Set up a project in a minute.',
|
|
73
|
+
primary: { to: '/sign-up', label: 'Start a project' }, secondary: { to: '/features', label: 'Explore features' },
|
|
74
|
+
},
|
|
75
|
+
watermark: 'najenga.',
|
|
76
|
+
copyright: 'Najenga',
|
|
77
|
+
madeBy: { label: 'Built to last by', name: 'OKJ Technologies', href: 'https://okjtech.co.ke', logo: '/okjt-logo.png' },
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
legal: {
|
|
81
|
+
documents: [{ to: '/terms', label: 'Terms of service', icon: FileText }],
|
|
82
|
+
contact: 'Contact OKJ Technologies through okjtech.co.ke.',
|
|
83
|
+
lastUpdated: '2026-10-29',
|
|
84
|
+
seeAlso: [{ to: '/terms', label: 'Terms of service' }],
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
seo: {
|
|
88
|
+
origin: 'https://najenga.co.ke',
|
|
89
|
+
siteName: 'Najenga',
|
|
90
|
+
locale: 'en_KE',
|
|
91
|
+
defaultTitle: 'Najenga — building, accounted for',
|
|
92
|
+
defaultDescription: 'Track the costs, receipts and progress of your construction projects.',
|
|
93
|
+
pages: { '/features': { title: 'Features | Najenga', description: '…' } },
|
|
94
|
+
resolve: (path) => undefined, // titles for pages made from data, e.g. /projects/:slug
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Any path not in `seo.pages` (and not resolved) gets the defaults and `noindex`.
|
|
100
|
+
|
|
101
|
+
## Building blocks
|
|
102
|
+
|
|
103
|
+
| Export | What it is |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `SiteLayout` | Nav, page, footer; `overlay` puts the nav over a full-bleed hero, `cta={false}` drops the footer's call to action |
|
|
106
|
+
| `SiteNav`, `SiteFooter`, `Seo` | The parts `SiteLayout` uses |
|
|
107
|
+
| `Container`, `Logo`, `PillLink`, `Eyebrow`, `Chip`, `SectionHeading` | Page width, the brand mark, call-to-action links, labels, headings |
|
|
108
|
+
| `PhotoHero`, `HeroOrnament` | A full-bleed photo hero with a large light headline |
|
|
109
|
+
| `ImageCard` | A large rounded card with a photo behind its words; a link when given `to` |
|
|
110
|
+
| `Photo` | A photo filling its parent; a list of keys cross-fades |
|
|
111
|
+
| `MediaFrame`, `MediaFill` | A product screen or clip in a browser or phone frame, or bare |
|
|
112
|
+
| `FeatureExplorer` | Feature groups as tabs beside a large photo card with a summary, numbers and a screen |
|
|
113
|
+
| `FeatureGroup` | One feature group in depth: heading, numbers, photo card and screen, every tool |
|
|
114
|
+
| `JourneyGlance`, `JourneyChapter` | A journey's numbers at a glance, and one chapter of it |
|
|
115
|
+
| `ShowcaseCarousel` | A stack of screens with arrows and a list |
|
|
116
|
+
| `SectionNav`, `SECTION_OFFSET`, `useScrollSpy` | The sticky bar of the page's sections, with "you are here" |
|
|
117
|
+
| `FaqSection` | Questions and answers |
|
|
118
|
+
| `PricingSection` | Plan cards, already worded by the project |
|
|
119
|
+
| `LegalPage` | A legal document with its contents beside it |
|
|
120
|
+
| `RollingWord`, `useRolling` | Words that roll through a list in a headline |
|
|
121
|
+
|
|
122
|
+
Headings inside the kit's components carry `!` on their font and colour, so a
|
|
123
|
+
host app's own global `h1`–`h3` styles don't change them.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Plus } from "lucide-react";
|
|
4
|
+
import { Container, SectionHeading } from "./ui.js";
|
|
5
|
+
import { SECTION_OFFSET } from "./SectionNav.js";
|
|
6
|
+
function FaqSection({ id = "faq", items, eyebrow = "FAQ", title = "Questions, answered", text }) {
|
|
7
|
+
const [openIndex, setOpenIndex] = useState(0);
|
|
8
|
+
return /* @__PURE__ */ jsx("section", { id, className: `${SECTION_OFFSET} py-24`, children: /* @__PURE__ */ jsxs(Container, { className: "grid gap-12 lg:grid-cols-12", children: [
|
|
9
|
+
/* @__PURE__ */ jsx("div", { className: "lg:col-span-4", children: /* @__PURE__ */ jsx(SectionHeading, { eyebrow, title, text, align: "left", className: "lg:sticky lg:top-28" }) }),
|
|
10
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-3 lg:col-span-8", children: items.map((item, i) => {
|
|
11
|
+
const open = openIndex === i;
|
|
12
|
+
return /* @__PURE__ */ jsxs("div", { className: `rounded-3xl border transition-colors ${open ? "border-transparent bg-white shadow-[0_20px_50px_-35px_color-mix(in_srgb,var(--color-ink)_50%,transparent)]" : "border-line bg-white/50"}`, children: [
|
|
13
|
+
/* @__PURE__ */ jsxs(
|
|
14
|
+
"button",
|
|
15
|
+
{
|
|
16
|
+
type: "button",
|
|
17
|
+
onClick: () => setOpenIndex(open ? -1 : i),
|
|
18
|
+
"aria-expanded": open,
|
|
19
|
+
className: "flex w-full items-center justify-between gap-4 px-6 py-5 text-left font-medium text-ink sm:px-8",
|
|
20
|
+
children: [
|
|
21
|
+
item.q,
|
|
22
|
+
/* @__PURE__ */ jsx("span", { className: `grid h-8 w-8 shrink-0 place-items-center rounded-full transition-all ${open ? "rotate-45 bg-ink text-white" : "bg-tint text-accent"}`, children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4", "aria-hidden": "true" }) })
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
),
|
|
26
|
+
open && /* @__PURE__ */ jsx("div", { className: "px-6 pb-6 text-sm leading-relaxed text-soft sm:px-8", children: item.a })
|
|
27
|
+
] }, item.q);
|
|
28
|
+
}) })
|
|
29
|
+
] }) });
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
FaqSection as default
|
|
33
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Link } from "react-router-dom";
|
|
4
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
5
|
+
import { ArrowUpRight } from "lucide-react";
|
|
6
|
+
import Photo from "./Photo.js";
|
|
7
|
+
import MediaFrame from "./MediaFrame.js";
|
|
8
|
+
import { usePlaces, useSite } from "./SiteProvider.js";
|
|
9
|
+
function FeatureExplorer({
|
|
10
|
+
groups,
|
|
11
|
+
href = (group) => `/features#${group.id}`,
|
|
12
|
+
linkLabel = "See every detail",
|
|
13
|
+
countLabel = "tools",
|
|
14
|
+
label = "Feature areas"
|
|
15
|
+
}) {
|
|
16
|
+
const place = usePlaces();
|
|
17
|
+
const { media: MEDIA = {} } = useSite();
|
|
18
|
+
const [activeId, setActiveId] = useState(groups[0].id);
|
|
19
|
+
const active = groups.find((g) => g.id === activeId) || groups[0];
|
|
20
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid gap-8 lg:grid-cols-12 lg:gap-12", children: [
|
|
21
|
+
/* @__PURE__ */ jsx("div", { role: "tablist", "aria-label": label, "aria-orientation": "vertical", className: "-mx-4 flex gap-2 overflow-x-auto px-4 pb-2 lg:mx-0 lg:block lg:space-y-1 lg:overflow-visible lg:border-l lg:border-line-strong lg:px-0 lg:pb-0 lg:col-span-3", children: groups.map((group) => {
|
|
22
|
+
const selected = group.id === active.id;
|
|
23
|
+
return /* @__PURE__ */ jsxs(
|
|
24
|
+
"button",
|
|
25
|
+
{
|
|
26
|
+
type: "button",
|
|
27
|
+
role: "tab",
|
|
28
|
+
"aria-selected": selected,
|
|
29
|
+
onClick: () => setActiveId(group.id),
|
|
30
|
+
className: `relative shrink-0 rounded-full px-4 py-2 text-left transition-colors lg:block lg:w-full lg:rounded-none lg:px-6 lg:py-3 ${selected ? "bg-ink text-white lg:bg-transparent lg:text-ink" : "bg-white/60 text-muted hover:text-ink lg:bg-transparent"}`,
|
|
31
|
+
children: [
|
|
32
|
+
selected && /* @__PURE__ */ jsx(motion.span, { layoutId: "explorer-bar", className: "absolute -left-px top-0 hidden h-full w-0.5 bg-ink lg:block" }),
|
|
33
|
+
/* @__PURE__ */ jsx("span", { className: "block whitespace-nowrap text-sm font-semibold lg:text-base lg:font-medium", children: group.eyebrow }),
|
|
34
|
+
/* @__PURE__ */ jsxs("span", { className: "hidden text-xs text-faint lg:block", children: [
|
|
35
|
+
group.features.length,
|
|
36
|
+
" ",
|
|
37
|
+
countLabel
|
|
38
|
+
] })
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
group.id
|
|
42
|
+
);
|
|
43
|
+
}) }),
|
|
44
|
+
/* @__PURE__ */ jsx("div", { className: "lg:col-span-9", children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", children: /* @__PURE__ */ jsxs(
|
|
45
|
+
motion.div,
|
|
46
|
+
{
|
|
47
|
+
role: "tabpanel",
|
|
48
|
+
initial: { opacity: 0, y: 12 },
|
|
49
|
+
animate: { opacity: 1, y: 0 },
|
|
50
|
+
exit: { opacity: 0, y: -12 },
|
|
51
|
+
transition: { duration: 0.35 },
|
|
52
|
+
className: "group relative isolate flex min-h-[34rem] flex-col justify-between overflow-hidden rounded-[2rem] p-4 sm:p-6 lg:min-h-[38rem]",
|
|
53
|
+
children: [
|
|
54
|
+
/* @__PURE__ */ jsx(Photo, { image: place(`group-${active.id}`), className: "-z-10" }),
|
|
55
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 -z-10 bg-gradient-to-t from-ink-deep/85 via-ink-deep/10 to-ink-deep/10" }),
|
|
56
|
+
/* @__PURE__ */ jsxs("div", { className: "max-w-sm rounded-3xl border border-white/50 bg-white/75 p-6 shadow-xl backdrop-blur-xl", children: [
|
|
57
|
+
/* @__PURE__ */ jsx("h3", { className: "!font-display text-2xl !font-normal tracking-tight !text-ink", children: active.title }),
|
|
58
|
+
/* @__PURE__ */ jsx("div", { className: "mt-3 text-sm leading-relaxed text-body", children: active.summary }),
|
|
59
|
+
/* @__PURE__ */ jsxs(Link, { to: href(active), className: "mt-5 inline-flex items-center gap-1 text-[11px] font-bold uppercase tracking-[0.15em] text-ink hover:text-accent", children: [
|
|
60
|
+
linkLabel,
|
|
61
|
+
" ",
|
|
62
|
+
/* @__PURE__ */ jsx(ArrowUpRight, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
|
|
63
|
+
] })
|
|
64
|
+
] }),
|
|
65
|
+
/* @__PURE__ */ jsx("div", { className: `pointer-events-none absolute hidden rotate-[-2deg] lg:block ${MEDIA[active.media]?.phone ? "bottom-8 right-10 w-52" : "bottom-24 right-6 w-[46%]"}`, children: /* @__PURE__ */ jsx(MediaFrame, { media: active.media }) }),
|
|
66
|
+
/* @__PURE__ */ jsx("dl", { className: "mt-10 grid grid-cols-3 gap-4 px-2 text-white sm:max-w-xl", children: active.stats.map(([value, label2]) => /* @__PURE__ */ jsxs("div", { children: [
|
|
67
|
+
/* @__PURE__ */ jsx("dt", { className: "text-xs text-white/70", children: label2 }),
|
|
68
|
+
/* @__PURE__ */ jsx("dd", { className: "mt-1 font-display text-xl font-light sm:text-3xl", children: value })
|
|
69
|
+
] }, label2)) })
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
active.id
|
|
73
|
+
) }) })
|
|
74
|
+
] });
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
FeatureExplorer as default
|
|
78
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Check } from "lucide-react";
|
|
3
|
+
import ImageCard from "./ImageCard.js";
|
|
4
|
+
import MediaFrame from "./MediaFrame.js";
|
|
5
|
+
import { Container, Eyebrow } from "./ui.js";
|
|
6
|
+
import { SECTION_OFFSET } from "./SectionNav.js";
|
|
7
|
+
import { usePlaces, useSite } from "./SiteProvider.js";
|
|
8
|
+
function FeatureGroup({ group, index }) {
|
|
9
|
+
const place = usePlaces();
|
|
10
|
+
const { media: MEDIA = {} } = useSite();
|
|
11
|
+
const media = MEDIA[group.clip] || MEDIA[group.media];
|
|
12
|
+
const flip = index % 2 === 1;
|
|
13
|
+
return /* @__PURE__ */ jsx("section", { id: group.id, className: `${SECTION_OFFSET} py-20 lg:py-28`, children: /* @__PURE__ */ jsxs(Container, { children: [
|
|
14
|
+
/* @__PURE__ */ jsxs("div", { className: "grid items-end gap-8 lg:grid-cols-12", children: [
|
|
15
|
+
/* @__PURE__ */ jsxs("div", { className: "lg:col-span-7", children: [
|
|
16
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
17
|
+
/* @__PURE__ */ jsx("span", { className: "font-display text-sm font-semibold text-accent-soft", children: String(index + 1).padStart(2, "0") }),
|
|
18
|
+
/* @__PURE__ */ jsx(Eyebrow, { children: group.eyebrow })
|
|
19
|
+
] }),
|
|
20
|
+
/* @__PURE__ */ jsx("h2", { className: "mt-4 !font-display text-4xl !font-light leading-[1.05] tracking-tight !text-ink sm:text-5xl", children: group.title }),
|
|
21
|
+
/* @__PURE__ */ jsx("div", { className: "mt-5 max-w-2xl text-lg leading-relaxed text-soft", children: group.summary })
|
|
22
|
+
] }),
|
|
23
|
+
/* @__PURE__ */ jsx("dl", { className: "grid grid-cols-3 gap-3 lg:col-span-5", children: group.stats.map(([value, label]) => /* @__PURE__ */ jsxs("div", { className: "rounded-2xl border border-line bg-white/60 p-4", children: [
|
|
24
|
+
/* @__PURE__ */ jsx("dd", { className: "font-display text-xl font-normal tracking-tight text-ink sm:text-2xl", children: value }),
|
|
25
|
+
/* @__PURE__ */ jsx("dt", { className: "mt-1 text-xs text-muted", children: label })
|
|
26
|
+
] }, label)) })
|
|
27
|
+
] }),
|
|
28
|
+
/* @__PURE__ */ jsxs("div", { className: `mt-12 grid gap-5 lg:grid-cols-12 ${flip ? "lg:[&>*:first-child]:order-2" : ""}`, children: [
|
|
29
|
+
/* @__PURE__ */ jsx(ImageCard, { image: place(`group-${group.id}-card`), tags: [group.eyebrow], title: group.features[0].title, text: group.features[0].text, className: "min-h-[24rem] lg:col-span-5" }),
|
|
30
|
+
/* @__PURE__ */ jsxs("div", { className: "relative overflow-hidden rounded-[1.75rem] bg-line p-4 sm:p-8 lg:col-span-7", children: [
|
|
31
|
+
/* @__PURE__ */ jsx("div", { className: "absolute -right-24 -top-24 h-64 w-64 rounded-full bg-white/50 blur-3xl", "aria-hidden": "true" }),
|
|
32
|
+
/* @__PURE__ */ jsx(MediaFrame, { media, className: "relative" })
|
|
33
|
+
] })
|
|
34
|
+
] }),
|
|
35
|
+
/* @__PURE__ */ jsx("div", { className: "mt-5 grid gap-5 sm:grid-cols-2 lg:grid-cols-3", children: group.features.map((feature) => /* @__PURE__ */ jsxs("article", { className: "flex flex-col rounded-[1.75rem] border border-line bg-white p-6 sm:p-7", children: [
|
|
36
|
+
/* @__PURE__ */ jsx("h3", { className: "!font-display text-xl !font-medium !text-ink", children: feature.title }),
|
|
37
|
+
/* @__PURE__ */ jsx("div", { className: "mt-3 text-sm leading-relaxed text-soft", children: feature.text }),
|
|
38
|
+
feature.details.length > 0 && /* @__PURE__ */ jsx("ul", { className: "mt-5 space-y-2 border-t border-tint pt-5 text-sm text-body", children: feature.details.map((detail) => /* @__PURE__ */ jsxs("li", { className: "flex gap-2.5", children: [
|
|
39
|
+
/* @__PURE__ */ jsx(Check, { className: "mt-0.5 h-4 w-4 shrink-0 text-accent", "aria-hidden": "true" }),
|
|
40
|
+
detail
|
|
41
|
+
] }, detail)) })
|
|
42
|
+
] }, feature.title)) })
|
|
43
|
+
] }) });
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
FeatureGroup as default
|
|
47
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { ArrowUpRight } from "lucide-react";
|
|
4
|
+
import Photo from "./Photo.js";
|
|
5
|
+
import { Chip } from "./ui.js";
|
|
6
|
+
function ImageCard({
|
|
7
|
+
image,
|
|
8
|
+
title,
|
|
9
|
+
text,
|
|
10
|
+
tags = [],
|
|
11
|
+
to,
|
|
12
|
+
position,
|
|
13
|
+
className = "min-h-[22rem]",
|
|
14
|
+
titleClass = "text-2xl sm:text-3xl",
|
|
15
|
+
children
|
|
16
|
+
}) {
|
|
17
|
+
const Wrapper = to ? Link : "div";
|
|
18
|
+
return /* @__PURE__ */ jsxs(
|
|
19
|
+
Wrapper,
|
|
20
|
+
{
|
|
21
|
+
to,
|
|
22
|
+
className: `group relative isolate flex flex-col justify-end overflow-hidden rounded-[1.75rem] p-6 text-white shadow-[0_30px_60px_-35px_color-mix(in_srgb,var(--color-ink)_70%,transparent)] sm:p-8 ${className}`,
|
|
23
|
+
children: [
|
|
24
|
+
/* @__PURE__ */ jsx(Photo, { image, position, className: "-z-10" }),
|
|
25
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 -z-10 bg-gradient-to-t from-ink-deep/85 via-ink-deep/25 to-transparent" }),
|
|
26
|
+
to && /* @__PURE__ */ jsx("span", { className: "absolute right-5 top-5 grid h-10 w-10 place-items-center rounded-full bg-white/15 backdrop-blur transition-colors group-hover:bg-white group-hover:text-ink", "aria-hidden": "true", children: /* @__PURE__ */ jsx(ArrowUpRight, { className: "h-4 w-4" }) }),
|
|
27
|
+
tags.length > 0 && /* @__PURE__ */ jsx("div", { className: "mb-3 flex flex-wrap gap-2", children: tags.map((tag) => /* @__PURE__ */ jsx(Chip, { children: tag }, tag)) }),
|
|
28
|
+
title && /* @__PURE__ */ jsx("h3", { className: `!font-display !font-normal leading-tight tracking-tight !text-white ${titleClass}`, children: title }),
|
|
29
|
+
text && /* @__PURE__ */ jsx("div", { className: "mt-2 max-w-md text-sm leading-relaxed text-white/80", children: text }),
|
|
30
|
+
children
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
ImageCard as default
|
|
37
|
+
};
|
package/dist/Journey.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CheckCircle2, Lightbulb } from "lucide-react";
|
|
3
|
+
import MediaFrame from "./MediaFrame.js";
|
|
4
|
+
import { Eyebrow } from "./ui.js";
|
|
5
|
+
import { SECTION_OFFSET } from "./SectionNav.js";
|
|
6
|
+
import { useMedia } from "./SiteProvider.js";
|
|
7
|
+
function JourneyGlance({ journey }) {
|
|
8
|
+
return /* @__PURE__ */ jsx("div", { className: "rounded-[2rem] bg-white p-6 shadow-[0_40px_80px_-40px_color-mix(in_srgb,var(--color-ink)_50%,transparent)] sm:p-10", children: /* @__PURE__ */ jsx("dl", { className: "grid grid-cols-2 gap-6 lg:grid-cols-4", children: journey.glance.map(([value, label]) => /* @__PURE__ */ jsxs("div", { className: "rounded-2xl bg-tint p-5", children: [
|
|
9
|
+
/* @__PURE__ */ jsx("dd", { className: "font-display text-3xl font-light tracking-tight text-ink sm:text-4xl", children: value }),
|
|
10
|
+
/* @__PURE__ */ jsx("dt", { className: "mt-1 text-sm text-soft", children: label })
|
|
11
|
+
] }, label)) }) });
|
|
12
|
+
}
|
|
13
|
+
function JourneyChapter({ chapter, index, detailsLabel = "The nitty-gritty", tipLabel = "Good to know" }) {
|
|
14
|
+
const phone = useMedia(chapter.media)?.phone;
|
|
15
|
+
return /* @__PURE__ */ jsxs("article", { id: chapter.id, className: `${SECTION_OFFSET} grid gap-8 border-t border-line py-14 first:border-t-0 first:pt-0 lg:grid-cols-12 lg:gap-12 lg:py-16`, children: [
|
|
16
|
+
/* @__PURE__ */ jsx("div", { className: `lg:sticky lg:top-40 lg:self-start ${phone ? "lg:col-span-5" : "lg:col-span-7"}`, children: /* @__PURE__ */ jsx(MediaFrame, { media: chapter.media, className: phone ? "max-w-[16rem]" : "" }) }),
|
|
17
|
+
/* @__PURE__ */ jsxs("div", { className: `${phone ? "lg:col-span-7" : "lg:col-span-5"}`, children: [
|
|
18
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
19
|
+
/* @__PURE__ */ jsx("span", { className: "font-display text-4xl font-extralight text-accent-soft", children: String(index + 1).padStart(2, "0") }),
|
|
20
|
+
/* @__PURE__ */ jsx("span", { className: "rounded-full bg-ink px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.15em] text-white", children: chapter.when })
|
|
21
|
+
] }),
|
|
22
|
+
/* @__PURE__ */ jsx("h2", { className: "mt-3 !font-display text-3xl !font-light tracking-tight !text-ink sm:text-4xl", children: chapter.title }),
|
|
23
|
+
/* @__PURE__ */ jsx("div", { className: "mt-3 leading-relaxed text-soft", children: chapter.intro }),
|
|
24
|
+
/* @__PURE__ */ jsx("ol", { className: "mt-6 divide-y divide-tint rounded-3xl border border-line bg-white", children: chapter.steps.map((step, i) => /* @__PURE__ */ jsxs("li", { className: "flex gap-4 p-5", children: [
|
|
25
|
+
/* @__PURE__ */ jsx("span", { className: "grid h-7 w-7 shrink-0 place-items-center rounded-full bg-tint text-xs font-semibold text-accent", children: i + 1 }),
|
|
26
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
27
|
+
/* @__PURE__ */ jsx("h3", { className: "!font-sans text-base !font-semibold !text-ink", children: step.title }),
|
|
28
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 text-sm leading-relaxed text-soft", children: step.text })
|
|
29
|
+
] })
|
|
30
|
+
] }, step.title)) }),
|
|
31
|
+
chapter.details.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-3xl bg-tint p-5", children: [
|
|
32
|
+
/* @__PURE__ */ jsx(Eyebrow, { children: detailsLabel }),
|
|
33
|
+
/* @__PURE__ */ jsx("ul", { className: "mt-3 space-y-2 text-sm text-body", children: chapter.details.map((detail) => /* @__PURE__ */ jsxs("li", { className: "flex gap-3", children: [
|
|
34
|
+
/* @__PURE__ */ jsx(CheckCircle2, { className: "mt-0.5 h-4 w-4 shrink-0 text-accent", "aria-hidden": "true" }),
|
|
35
|
+
detail
|
|
36
|
+
] }, detail)) })
|
|
37
|
+
] }),
|
|
38
|
+
chapter.tip && /* @__PURE__ */ jsxs("div", { className: "mt-4 flex gap-4 rounded-3xl bg-ink p-5 text-white", children: [
|
|
39
|
+
/* @__PURE__ */ jsx(Lightbulb, { className: "h-5 w-5 shrink-0 text-accent-soft", "aria-hidden": "true" }),
|
|
40
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
41
|
+
/* @__PURE__ */ jsx("div", { className: "text-[11px] font-semibold uppercase tracking-[0.2em] text-accent-soft", children: tipLabel }),
|
|
42
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 text-sm leading-relaxed text-white/85", children: chapter.tip })
|
|
43
|
+
] })
|
|
44
|
+
] })
|
|
45
|
+
] })
|
|
46
|
+
] });
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
JourneyChapter,
|
|
50
|
+
JourneyGlance
|
|
51
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from "react";
|
|
3
|
+
import { Link, NavLink } from "react-router-dom";
|
|
4
|
+
import { Mail } from "lucide-react";
|
|
5
|
+
import SiteLayout from "./SiteLayout.js";
|
|
6
|
+
import { Container, Eyebrow } from "./ui.js";
|
|
7
|
+
import useScrollSpy from "./useScrollSpy.js";
|
|
8
|
+
import SectionNav, { SECTION_OFFSET } from "./SectionNav.js";
|
|
9
|
+
import { useSite } from "./SiteProvider.js";
|
|
10
|
+
function Sidebar({ sections, active }) {
|
|
11
|
+
const { legal } = useSite();
|
|
12
|
+
return /* @__PURE__ */ jsxs("nav", { "aria-label": "Legal", className: "space-y-8 lg:sticky lg:top-28", children: [
|
|
13
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 lg:flex-col", children: legal.documents.map((doc) => {
|
|
14
|
+
const Icon = doc.icon;
|
|
15
|
+
return /* @__PURE__ */ jsxs(
|
|
16
|
+
NavLink,
|
|
17
|
+
{
|
|
18
|
+
to: doc.to,
|
|
19
|
+
className: ({ isActive }) => `flex min-w-[10rem] flex-1 items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium transition-colors ${isActive ? "bg-ink text-white" : "bg-white text-body hover:bg-tint"}`,
|
|
20
|
+
children: [
|
|
21
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
|
|
22
|
+
doc.label
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
doc.to
|
|
26
|
+
);
|
|
27
|
+
}) }),
|
|
28
|
+
/* @__PURE__ */ jsxs("div", { className: "hidden lg:block", children: [
|
|
29
|
+
/* @__PURE__ */ jsx(Eyebrow, { children: "Contents" }),
|
|
30
|
+
/* @__PURE__ */ jsx("ol", { className: "mt-4 space-y-0.5 border-l border-line-strong", children: sections.map((s, i) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
31
|
+
"a",
|
|
32
|
+
{
|
|
33
|
+
href: `#${s.id}`,
|
|
34
|
+
"aria-current": active === s.id ? "location" : void 0,
|
|
35
|
+
className: `-ml-px flex gap-3 border-l-2 py-1.5 pl-4 text-sm transition-colors ${active === s.id ? "border-ink font-medium text-ink" : "border-transparent text-muted hover:text-body"}`,
|
|
36
|
+
children: [
|
|
37
|
+
/* @__PURE__ */ jsxs("span", { className: "w-5 shrink-0 tabular-nums text-accent-soft", children: [
|
|
38
|
+
i + 1,
|
|
39
|
+
"."
|
|
40
|
+
] }),
|
|
41
|
+
s.title
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
) }, s.id)) })
|
|
45
|
+
] }),
|
|
46
|
+
/* @__PURE__ */ jsxs("div", { className: "hidden rounded-2xl bg-tint p-5 text-sm text-body lg:block", children: [
|
|
47
|
+
/* @__PURE__ */ jsx(Mail, { className: "h-4 w-4 text-accent", "aria-hidden": "true" }),
|
|
48
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 font-medium text-ink", children: "Questions?" }),
|
|
49
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 text-soft", children: legal.contact })
|
|
50
|
+
] })
|
|
51
|
+
] });
|
|
52
|
+
}
|
|
53
|
+
function LegalPage({ title, intro, sections }) {
|
|
54
|
+
const { legal } = useSite();
|
|
55
|
+
const updated = (/* @__PURE__ */ new Date(`${legal.lastUpdated}T00:00:00`)).toLocaleDateString(void 0, { dateStyle: "long" });
|
|
56
|
+
const active = useScrollSpy(sections.map((s) => s.id));
|
|
57
|
+
return /* @__PURE__ */ jsxs(SiteLayout, { cta: false, children: [
|
|
58
|
+
/* @__PURE__ */ jsx(Container, { className: "pt-10 lg:pt-16", children: /* @__PURE__ */ jsxs("header", { className: "max-w-3xl", children: [
|
|
59
|
+
/* @__PURE__ */ jsx(Eyebrow, { children: "Legal" }),
|
|
60
|
+
/* @__PURE__ */ jsx("h1", { className: "mt-3 !font-display text-4xl !font-light tracking-tight !text-ink sm:text-6xl", children: title }),
|
|
61
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-3 text-sm text-muted", children: [
|
|
62
|
+
"Last updated ",
|
|
63
|
+
updated
|
|
64
|
+
] })
|
|
65
|
+
] }) }),
|
|
66
|
+
/* @__PURE__ */ jsx(SectionNav, { label: "Contents", numbered: true, className: "lg:hidden", sections: sections.map((s) => ({ id: s.id, label: s.title })) }),
|
|
67
|
+
/* @__PURE__ */ jsx(Container, { className: "pb-16 pt-8 lg:pt-0", children: /* @__PURE__ */ jsxs("div", { className: "mt-4 grid gap-10 lg:mt-12 lg:grid-cols-12", children: [
|
|
68
|
+
/* @__PURE__ */ jsx("aside", { className: "lg:col-span-4 xl:col-span-3", children: /* @__PURE__ */ jsx(Sidebar, { sections, active }) }),
|
|
69
|
+
/* @__PURE__ */ jsxs("article", { className: "rounded-[2rem] bg-white p-6 sm:p-10 lg:col-span-8 xl:col-span-9 xl:p-14", children: [
|
|
70
|
+
/* @__PURE__ */ jsx("div", { className: "text-lg leading-relaxed text-body", children: intro }),
|
|
71
|
+
sections.map((s, i) => /* @__PURE__ */ jsxs("section", { id: s.id, className: `${SECTION_OFFSET} lg:scroll-mt-28 border-t border-tint pt-10 mt-10 first-of-type:mt-10`, children: [
|
|
72
|
+
/* @__PURE__ */ jsxs("h2", { className: "!font-display text-2xl !font-normal tracking-tight !text-ink", children: [
|
|
73
|
+
/* @__PURE__ */ jsxs("span", { className: "mr-2 text-accent-soft", children: [
|
|
74
|
+
i + 1,
|
|
75
|
+
"."
|
|
76
|
+
] }),
|
|
77
|
+
s.title
|
|
78
|
+
] }),
|
|
79
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 max-w-3xl space-y-3 leading-relaxed text-body [&_a]:text-accent-strong [&_li]:ml-5 [&_li]:list-disc [&_ul]:space-y-1.5", children: s.body })
|
|
80
|
+
] }, s.id)),
|
|
81
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-12 border-t border-tint pt-6 text-sm text-muted", children: [
|
|
82
|
+
"See also: ",
|
|
83
|
+
legal.seeAlso.map((link, i) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
84
|
+
i > 0 && " \xB7 ",
|
|
85
|
+
/* @__PURE__ */ jsx(Link, { to: link.to, className: "hover:text-body", children: link.label })
|
|
86
|
+
] }, link.to))
|
|
87
|
+
] })
|
|
88
|
+
] })
|
|
89
|
+
] }) })
|
|
90
|
+
] });
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
LegalPage as default
|
|
94
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useMedia } from "./SiteProvider.js";
|
|
4
|
+
function useInViewPlayback() {
|
|
5
|
+
const ref = useRef(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const video = ref.current;
|
|
8
|
+
if (!video || typeof IntersectionObserver === "undefined") return void 0;
|
|
9
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return void 0;
|
|
10
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
11
|
+
if (entry.isIntersecting) video.play?.()?.catch?.(() => {
|
|
12
|
+
});
|
|
13
|
+
else video.pause?.();
|
|
14
|
+
}, { threshold: 0.25 });
|
|
15
|
+
observer.observe(video);
|
|
16
|
+
return () => observer.disconnect();
|
|
17
|
+
}, []);
|
|
18
|
+
return ref;
|
|
19
|
+
}
|
|
20
|
+
function MediaFill({ media, className = "", position = "top" }) {
|
|
21
|
+
const item = useMedia(media);
|
|
22
|
+
const videoRef = useInViewPlayback();
|
|
23
|
+
const [failed, setFailed] = useState(false);
|
|
24
|
+
if (!item) return null;
|
|
25
|
+
const fit = `h-full w-full object-cover ${position === "top" ? "object-top" : "object-center"} ${className}`;
|
|
26
|
+
if (failed) return /* @__PURE__ */ jsx("div", { className: `grid place-items-center bg-tint p-6 text-center text-sm text-muted ${fit}`, children: item.alt });
|
|
27
|
+
if (item.video) {
|
|
28
|
+
return /* @__PURE__ */ jsx("video", { ref: videoRef, className: fit, poster: item.src, muted: true, loop: true, playsInline: true, preload: "none", "aria-label": item.alt, onError: () => setFailed(true), children: /* @__PURE__ */ jsx("source", { src: item.video, type: "video/mp4" }) });
|
|
29
|
+
}
|
|
30
|
+
return /* @__PURE__ */ jsx("img", { src: item.src, alt: item.alt, loading: "lazy", decoding: "async", className: fit, onError: () => setFailed(true) });
|
|
31
|
+
}
|
|
32
|
+
function MediaFrame({ media, className = "", variant }) {
|
|
33
|
+
const item = useMedia(media);
|
|
34
|
+
if (!item) return null;
|
|
35
|
+
const kind = variant || (item.phone ? "phone" : "browser");
|
|
36
|
+
if (kind === "phone") {
|
|
37
|
+
return /* @__PURE__ */ jsxs("div", { className: `relative mx-auto aspect-[9/19] w-full max-w-[17rem] overflow-hidden rounded-[2.4rem] border-[7px] border-ink bg-ink shadow-[0_40px_80px_-30px_color-mix(in_srgb,var(--color-ink)_70%,transparent)] ${className}`, children: [
|
|
38
|
+
/* @__PURE__ */ jsx("div", { className: "absolute left-1/2 top-2 z-10 h-5 w-20 -translate-x-1/2 rounded-full bg-ink", "aria-hidden": "true" }),
|
|
39
|
+
/* @__PURE__ */ jsx("div", { className: "h-full overflow-hidden rounded-[1.9rem] bg-white", children: /* @__PURE__ */ jsx(MediaFill, { media: item }) })
|
|
40
|
+
] });
|
|
41
|
+
}
|
|
42
|
+
return /* @__PURE__ */ jsxs("div", { className: `overflow-hidden rounded-2xl border border-white/60 bg-white shadow-[0_40px_80px_-35px_color-mix(in_srgb,var(--color-ink)_55%,transparent)] ${className}`, children: [
|
|
43
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-line bg-paper px-4 py-2.5", "aria-hidden": "true", children: [
|
|
44
|
+
/* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 rounded-full bg-[#E8C4B8]" }),
|
|
45
|
+
/* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 rounded-full bg-line" }),
|
|
46
|
+
/* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 rounded-full bg-[#C5D4B9]" }),
|
|
47
|
+
/* @__PURE__ */ jsx("span", { className: "ml-3 truncate rounded-full bg-white px-3 py-0.5 text-[11px] text-muted", children: item.url })
|
|
48
|
+
] }),
|
|
49
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-[16/10] bg-paper", children: /* @__PURE__ */ jsx(MediaFill, { media: item }) })
|
|
50
|
+
] });
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
MediaFill,
|
|
54
|
+
MediaFrame as default
|
|
55
|
+
};
|
package/dist/Photo.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
|
4
|
+
import { usePhotos } from "./SiteProvider.js";
|
|
5
|
+
const ROTATE_EVERY = 7e3;
|
|
6
|
+
const asList = (image) => Array.isArray(image) ? image : [image];
|
|
7
|
+
function Photo({ image, phoneImage, width = 1600, alt, position = "center", className = "", eager = false }) {
|
|
8
|
+
const images = asList(image);
|
|
9
|
+
const phones = phoneImage ? asList(phoneImage) : null;
|
|
10
|
+
const [index, setIndex] = useState(0);
|
|
11
|
+
const [failedSrc, setFailedSrc] = useState(null);
|
|
12
|
+
const reduceMotion = useReducedMotion();
|
|
13
|
+
const photos = usePhotos();
|
|
14
|
+
const url = (key, w) => photos.url(key, w);
|
|
15
|
+
const current = images[index % images.length];
|
|
16
|
+
const src = url(current, width);
|
|
17
|
+
const failed = failedSrc === src;
|
|
18
|
+
const label = alt ?? photos.alt(current) ?? "";
|
|
19
|
+
const rotating = images.length > 1 && !reduceMotion;
|
|
20
|
+
const nextSrc = rotating ? url(images[(index + 1) % images.length], width) : null;
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!rotating) return void 0;
|
|
23
|
+
new Image().src = nextSrc;
|
|
24
|
+
const id = setTimeout(() => setIndex((i) => i + 1), ROTATE_EVERY);
|
|
25
|
+
return () => clearTimeout(id);
|
|
26
|
+
}, [rotating, nextSrc, index]);
|
|
27
|
+
return /* @__PURE__ */ jsx("div", { className: `absolute inset-0 overflow-hidden bg-gradient-to-br from-accent-soft via-accent to-body ${className}`, children: /* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: !failed && /* @__PURE__ */ jsxs(
|
|
28
|
+
motion.picture,
|
|
29
|
+
{
|
|
30
|
+
initial: { opacity: 0 },
|
|
31
|
+
animate: { opacity: 1 },
|
|
32
|
+
exit: { opacity: 0 },
|
|
33
|
+
transition: { duration: 1.4, ease: "easeInOut" },
|
|
34
|
+
className: "absolute inset-0",
|
|
35
|
+
children: [
|
|
36
|
+
phones && /* @__PURE__ */ jsx("source", { media: "(max-width: 640px) and (orientation: portrait)", srcSet: url(phones[index % phones.length], 900) }),
|
|
37
|
+
/* @__PURE__ */ jsx(
|
|
38
|
+
"img",
|
|
39
|
+
{
|
|
40
|
+
src,
|
|
41
|
+
alt: label,
|
|
42
|
+
loading: eager ? "eager" : "lazy",
|
|
43
|
+
decoding: "async",
|
|
44
|
+
onError: () => setFailedSrc(src),
|
|
45
|
+
style: { objectPosition: position },
|
|
46
|
+
className: "h-full w-full object-cover transition-transform duration-700 ease-out group-hover:scale-[1.04]"
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
index
|
|
52
|
+
) }) });
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
Photo as default
|
|
56
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { motion } from "framer-motion";
|
|
3
|
+
import Photo from "./Photo.js";
|
|
4
|
+
import { Container, Eyebrow } from "./ui.js";
|
|
5
|
+
function HeroOrnament({ kind = "pill", className = "" }) {
|
|
6
|
+
if (kind === "hatch") {
|
|
7
|
+
return /* @__PURE__ */ jsx(
|
|
8
|
+
"span",
|
|
9
|
+
{
|
|
10
|
+
"aria-hidden": "true",
|
|
11
|
+
className: `inline-block h-[0.55em] w-[2.2em] rounded-sm align-middle opacity-70 ${className}`,
|
|
12
|
+
style: { backgroundImage: "repeating-linear-gradient(-55deg, currentColor 0 1.5px, transparent 1.5px 9px)" }
|
|
13
|
+
}
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: `relative inline-block h-[0.55em] w-[2.6em] align-middle ${className}`, children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsx("span", { className: "absolute rounded-full border border-current opacity-60", style: { inset: `${i * 0.06}em ${i * 0.12}em` } }, i)) });
|
|
17
|
+
}
|
|
18
|
+
function PhotoHero({ image, phoneImage, eyebrow, title, text, children, feature, after, position = "center", tall = false }) {
|
|
19
|
+
return /* @__PURE__ */ jsxs("section", { className: "relative", children: [
|
|
20
|
+
/* @__PURE__ */ jsxs("div", { className: `relative isolate overflow-hidden ${tall ? "min-h-[100svh] pb-56 sm:pb-64" : "min-h-[78svh] pb-24"} flex flex-col justify-center pt-32 text-white`, children: [
|
|
21
|
+
/* @__PURE__ */ jsx(Photo, { image, phoneImage, width: 2400, position, eager: true, className: "-z-20" }),
|
|
22
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 -z-10 bg-gradient-to-b from-ink-deep/70 via-ink-deep/35 to-ink-deep/60" }),
|
|
23
|
+
/* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsxs(motion.div, { initial: { opacity: 0, y: 24 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.7 }, className: "mx-auto max-w-5xl text-center", children: [
|
|
24
|
+
eyebrow && /* @__PURE__ */ jsx(Eyebrow, { light: true, children: eyebrow }),
|
|
25
|
+
/* @__PURE__ */ jsx("h1", { className: "mt-5 !font-display text-[2.75rem] !font-extralight leading-[1.02] tracking-tight !text-white sm:text-7xl lg:text-[6.25rem]", children: title }),
|
|
26
|
+
text && /* @__PURE__ */ jsx("div", { className: "mx-auto mt-6 max-w-2xl text-base leading-relaxed text-white/85 sm:text-lg", children: text }),
|
|
27
|
+
feature,
|
|
28
|
+
children && /* @__PURE__ */ jsx("div", { className: "mt-9 flex flex-col justify-center gap-3 sm:flex-row", children })
|
|
29
|
+
] }) })
|
|
30
|
+
] }),
|
|
31
|
+
after && /* @__PURE__ */ jsx(Container, { className: "relative z-10 -mt-44 sm:-mt-52", children: after })
|
|
32
|
+
] });
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
HeroOrnament,
|
|
36
|
+
PhotoHero as default
|
|
37
|
+
};
|