@okjt/site 0.0.0-stage → 0.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.
@@ -0,0 +1,38 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Check } from "lucide-react";
3
+ import { Container, PillLink, SectionHeading } from "./ui.js";
4
+ import { SECTION_OFFSET } from "./SectionNav.js";
5
+ function PricingSection({ id = "pricing", eyebrow = "Pricing", title, text, plans, footnote }) {
6
+ return /* @__PURE__ */ jsx("section", { id, className: `${SECTION_OFFSET} py-24`, children: /* @__PURE__ */ jsxs(Container, { children: [
7
+ /* @__PURE__ */ jsx(
8
+ SectionHeading,
9
+ {
10
+ eyebrow,
11
+ title,
12
+ text
13
+ }
14
+ ),
15
+ /* @__PURE__ */ jsx("div", { className: "mx-auto mt-14 grid max-w-5xl gap-6 md:grid-cols-2", children: plans.map((plan) => /* @__PURE__ */ jsxs(
16
+ "div",
17
+ {
18
+ className: `relative flex flex-col overflow-hidden rounded-[2rem] p-8 sm:p-10 ${plan.highlight ? "bg-fill text-on-ink shadow-[0_40px_80px_-40px_color-mix(in_srgb,var(--okjt-shadow,var(--color-ink))_80%,transparent)]" : "border border-line bg-surface"}`,
19
+ children: [
20
+ plan.highlight && /* @__PURE__ */ jsx("div", { className: "absolute -right-16 -top-16 h-48 w-48 rounded-full bg-accent-soft/20 blur-2xl", "aria-hidden": "true" }),
21
+ /* @__PURE__ */ jsx("div", { className: `text-xs font-semibold uppercase tracking-[0.2em] ${plan.highlight ? "text-accent-soft" : "text-accent"}`, children: plan.name }),
22
+ /* @__PURE__ */ jsx("div", { className: `mt-3 font-display text-3xl font-light tracking-tight sm:text-4xl ${plan.highlight ? "text-on-ink" : "text-ink"}`, children: plan.price }),
23
+ /* @__PURE__ */ jsx("div", { className: `mt-2 text-sm ${plan.highlight ? "text-on-ink/70" : "text-soft"}`, children: plan.tagline }),
24
+ /* @__PURE__ */ jsx("ul", { className: "mt-8 flex-1 space-y-3 text-sm", children: plan.lines.map((feature) => /* @__PURE__ */ jsxs("li", { className: "flex gap-3", children: [
25
+ /* @__PURE__ */ jsx("span", { className: `mt-0.5 grid h-5 w-5 shrink-0 place-items-center rounded-full ${plan.highlight ? "bg-on-ink/10 text-accent-soft" : "bg-tint text-accent"}`, children: /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) }),
26
+ feature
27
+ ] }, feature)) }),
28
+ /* @__PURE__ */ jsx(PillLink, { to: plan.cta.to, variant: plan.highlight ? "light" : "dark", className: "mt-10", children: plan.cta.label })
29
+ ]
30
+ },
31
+ plan.id
32
+ )) }),
33
+ footnote && /* @__PURE__ */ jsx("div", { className: "mt-8 text-center text-sm text-soft", children: footnote })
34
+ ] }) });
35
+ }
36
+ export {
37
+ PricingSection as default
38
+ };
@@ -0,0 +1,19 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ function RollingWord({ item, className = "", ...rest }) {
4
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: `relative block h-[1.15em] overflow-hidden ${className}`, ...rest, children: /* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: /* @__PURE__ */ jsx(
5
+ motion.span,
6
+ {
7
+ lang: item.code,
8
+ initial: { y: "70%", opacity: 0 },
9
+ animate: { y: 0, opacity: 1, transition: { duration: 0.6, delay: 0.15, ease: [0.22, 1, 0.36, 1] } },
10
+ exit: { y: "-70%", opacity: 0, transition: { duration: 0.3, ease: "easeIn" } },
11
+ className: "absolute inset-x-0 block whitespace-nowrap",
12
+ children: item.phrase
13
+ },
14
+ item.phrase
15
+ ) }) });
16
+ }
17
+ export {
18
+ RollingWord
19
+ };
@@ -0,0 +1,36 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import useScrollSpy from "./useScrollSpy.js";
4
+ import { Container } from "./ui.js";
5
+ const SECTION_OFFSET = "scroll-mt-36";
6
+ function SectionNav({ sections, label, numbered = false, className = "" }) {
7
+ const active = useScrollSpy(sections.map((s) => s.id));
8
+ const bar = useRef(null);
9
+ useEffect(() => {
10
+ const nav = bar.current;
11
+ const chip = nav?.querySelector("[aria-current]");
12
+ if (!nav || !chip) return;
13
+ const left = chip.offsetLeft - nav.clientWidth / 2 + chip.clientWidth / 2;
14
+ nav.scrollTo?.({ left, behavior: "smooth" });
15
+ }, [active]);
16
+ return /* @__PURE__ */ jsx("div", { className: `okjt-section-nav sticky top-[var(--okjt-nav-h,4.25rem)] z-30 border-y border-line/70 bg-tint/90 backdrop-blur-xl ${className}`, children: /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx("nav", { ref: bar, "aria-label": label, className: "-mx-4 flex items-center gap-2 overflow-x-auto px-4 py-3 [scrollbar-width:none] [mask-image:linear-gradient(to_right,black_calc(100%-2.5rem),transparent)]", children: sections.map((section, i) => {
17
+ const current = active === section.id;
18
+ return /* @__PURE__ */ jsxs(
19
+ "a",
20
+ {
21
+ href: `#${section.id}`,
22
+ "aria-current": current ? "location" : void 0,
23
+ className: `shrink-0 rounded-full px-4 py-2 text-xs font-semibold transition-colors ${current ? "bg-fill text-on-ink" : "bg-surface text-soft hover:text-ink"}`,
24
+ children: [
25
+ numbered && /* @__PURE__ */ jsx("span", { className: `mr-1.5 tabular-nums ${current ? "text-accent-soft" : "text-faint"}`, children: String(i + 1).padStart(2, "0") }),
26
+ section.label
27
+ ]
28
+ },
29
+ section.id
30
+ );
31
+ }) }) }) });
32
+ }
33
+ export {
34
+ SECTION_OFFSET,
35
+ SectionNav as default
36
+ };
package/dist/Seo.js ADDED
@@ -0,0 +1,55 @@
1
+ import { useEffect } from "react";
2
+ import { useLocation } from "react-router-dom";
3
+ import { useSite } from "./SiteProvider.js";
4
+ function getMetadata(seo, pathname) {
5
+ const path = pathname.replace(/\/$/, "") || "/";
6
+ const fallback = { title: seo.defaultTitle, description: seo.defaultDescription, indexable: false };
7
+ const resolved = seo.resolve?.(path);
8
+ if (resolved) return { ...fallback, indexable: true, ...resolved };
9
+ const metadata = seo.pages?.[path];
10
+ if (metadata) return { ...metadata, indexable: true };
11
+ return fallback;
12
+ }
13
+ function upsertMeta(attribute, key, value) {
14
+ let node = document.head.querySelector(`meta[${attribute}="${key}"]`);
15
+ if (!node) {
16
+ node = document.createElement("meta");
17
+ node.setAttribute(attribute, key);
18
+ document.head.appendChild(node);
19
+ }
20
+ node.setAttribute("content", value);
21
+ }
22
+ function upsertLink(rel, href) {
23
+ let node = document.head.querySelector(`link[rel="${rel}"]`);
24
+ if (!node) {
25
+ node = document.createElement("link");
26
+ node.setAttribute("rel", rel);
27
+ document.head.appendChild(node);
28
+ }
29
+ node.setAttribute("href", href);
30
+ }
31
+ function Seo() {
32
+ const { seo } = useSite();
33
+ const { pathname } = useLocation();
34
+ useEffect(() => {
35
+ const metadata = getMetadata(seo, pathname);
36
+ const canonical = `${seo.origin}${pathname === "/" ? "/" : pathname.replace(/\/$/, "")}`;
37
+ document.title = metadata.title;
38
+ upsertMeta("name", "description", metadata.description);
39
+ upsertMeta("name", "robots", metadata.indexable ? "index, follow" : "noindex, nofollow");
40
+ upsertLink("canonical", canonical);
41
+ upsertMeta("property", "og:type", "website");
42
+ upsertMeta("property", "og:site_name", seo.siteName);
43
+ upsertMeta("property", "og:title", metadata.title);
44
+ upsertMeta("property", "og:description", metadata.description);
45
+ upsertMeta("property", "og:url", canonical);
46
+ upsertMeta("property", "og:locale", seo.locale);
47
+ upsertMeta("name", "twitter:card", "summary");
48
+ upsertMeta("name", "twitter:title", metadata.title);
49
+ upsertMeta("name", "twitter:description", metadata.description);
50
+ }, [seo, pathname]);
51
+ return null;
52
+ }
53
+ export {
54
+ Seo as default
55
+ };
@@ -0,0 +1,60 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { AnimatePresence, motion } from "framer-motion";
4
+ import { ChevronLeft, ChevronRight } from "lucide-react";
5
+ import { MediaFill } from "./MediaFrame.js";
6
+ import { useSite } from "./SiteProvider.js";
7
+ import { Chip, SectionHeading } from "./ui.js";
8
+ function ShowcaseCarousel({ eyebrow, title, text, items }) {
9
+ const { media: MEDIA = {} } = useSite();
10
+ const [index, setIndex] = useState(0);
11
+ const go = (step) => setIndex((i) => (i + step + items.length) % items.length);
12
+ const item = items[index];
13
+ const next = items[(index + 1) % items.length];
14
+ const shape = MEDIA[item.media]?.phone ? "mx-auto aspect-[9/16] max-w-xs" : "aspect-[4/3]";
15
+ return /* @__PURE__ */ jsxs("div", { className: "grid items-center gap-12 lg:grid-cols-2", children: [
16
+ /* @__PURE__ */ jsxs("div", { children: [
17
+ /* @__PURE__ */ jsx(SectionHeading, { eyebrow, title, text, align: "left" }),
18
+ /* @__PURE__ */ jsxs("div", { className: "mt-8 flex items-center gap-3", children: [
19
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => go(-1), "aria-label": "Previous", className: "grid h-12 w-12 place-items-center rounded-full bg-surface text-ink shadow-[0_10px_30px_-12px_color-mix(in_srgb,var(--okjt-shadow,var(--color-ink))_35%,transparent)] transition-transform hover:-translate-x-0.5", children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5" }) }),
20
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => go(1), "aria-label": "Next", className: "grid h-12 w-12 place-items-center rounded-full bg-surface text-ink shadow-[0_10px_30px_-12px_color-mix(in_srgb,var(--okjt-shadow,var(--color-ink))_35%,transparent)] transition-transform hover:translate-x-0.5", children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-5 w-5" }) }),
21
+ /* @__PURE__ */ jsxs("span", { className: "ml-3 text-sm tabular-nums text-muted", "aria-live": "polite", children: [
22
+ index + 1,
23
+ " / ",
24
+ items.length
25
+ ] })
26
+ ] }),
27
+ /* @__PURE__ */ jsx("ol", { className: "mt-8 hidden space-y-2 text-sm lg:block", children: items.map((it, i) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => setIndex(i), className: `text-left transition-colors ${i === index ? "font-semibold text-ink" : "text-muted hover:text-body"}`, children: [
28
+ String(i + 1).padStart(2, "0"),
29
+ " \xB7 ",
30
+ it.title
31
+ ] }) }, it.title)) })
32
+ ] }),
33
+ /* @__PURE__ */ jsxs("div", { className: "relative mx-auto w-full max-w-xl pr-4 sm:pr-8", children: [
34
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-y-6 right-0 w-full overflow-hidden rounded-[2rem] opacity-60", "aria-hidden": "true", children: /* @__PURE__ */ jsx(MediaFill, { media: next.media }) }),
35
+ /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", children: /* @__PURE__ */ jsxs(
36
+ motion.div,
37
+ {
38
+ initial: { opacity: 0, x: 40 },
39
+ animate: { opacity: 1, x: 0 },
40
+ exit: { opacity: 0, x: -40 },
41
+ transition: { duration: 0.4 },
42
+ className: `relative overflow-hidden rounded-[2rem] border-[6px] border-surface bg-surface shadow-[0_40px_80px_-30px_color-mix(in_srgb,var(--okjt-shadow,var(--color-ink))_60%,transparent)] ${shape}`,
43
+ children: [
44
+ /* @__PURE__ */ jsx(MediaFill, { media: item.media }),
45
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-x-0 bottom-0 bg-gradient-to-t from-ink-deep/90 via-ink-deep/60 to-transparent p-4 pt-12 text-white sm:p-6 sm:pt-24", children: [
46
+ /* @__PURE__ */ jsx("div", { className: "hidden flex-wrap gap-2 sm:flex", children: item.tags.map((tag) => /* @__PURE__ */ jsx(Chip, { children: tag }, tag)) }),
47
+ /* @__PURE__ */ jsx("h3", { className: "!font-display text-lg !font-normal tracking-tight !text-white sm:mt-3 sm:text-3xl", children: item.title }),
48
+ /* @__PURE__ */ jsx("div", { className: "mt-2 hidden text-sm leading-relaxed text-white/80 sm:block", children: item.text })
49
+ ] })
50
+ ]
51
+ },
52
+ item.title
53
+ ) }),
54
+ /* @__PURE__ */ jsx("div", { className: "mt-4 text-sm leading-relaxed text-soft sm:hidden", children: item.text })
55
+ ] })
56
+ ] });
57
+ }
58
+ export {
59
+ ShowcaseCarousel as default
60
+ };
@@ -0,0 +1,74 @@
1
+ import { Fragment, 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 { Container, Eyebrow, Logo, PillLink } from "./ui.js";
6
+ import { usePlaces, useSignedIn, useSite } from "./SiteProvider.js";
7
+ function FooterCta({ cta }) {
8
+ const place = usePlaces();
9
+ return /* @__PURE__ */ jsx(Container, { className: "pb-6", children: /* @__PURE__ */ jsxs("div", { className: "group relative isolate overflow-hidden rounded-[2rem] bg-tint px-6 py-16 text-center sm:px-12 lg:py-24", children: [
10
+ /* @__PURE__ */ jsx(Photo, { image: place(cta.place), width: 1800, className: "-z-20" }),
11
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 -z-10 bg-gradient-to-r from-tint via-tint/90 to-tint/30" }),
12
+ /* @__PURE__ */ jsx(Eyebrow, { children: cta.eyebrow }),
13
+ /* @__PURE__ */ jsx("h2", { className: "mx-auto mt-4 max-w-3xl !font-display text-4xl !font-light leading-tight tracking-tight !text-ink sm:text-5xl lg:text-6xl", children: cta.title }),
14
+ /* @__PURE__ */ jsx("div", { className: "mx-auto mt-4 max-w-xl text-soft", children: cta.text }),
15
+ /* @__PURE__ */ jsxs("div", { className: "mt-8 flex flex-col justify-center gap-3 sm:flex-row", children: [
16
+ /* @__PURE__ */ jsx(PillLink, { to: cta.primary.to, size: "lg", children: cta.primary.label }),
17
+ /* @__PURE__ */ jsx(PillLink, { to: cta.secondary.to, variant: "light", size: "lg", arrow: true, children: cta.secondary.label })
18
+ ] })
19
+ ] }) });
20
+ }
21
+ function SiteFooter({ cta = true }) {
22
+ const { footer, accountLinks: links } = useSite();
23
+ const isAuthenticated = useSignedIn();
24
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
25
+ const shown = (link) => link.signedIn === void 0 || link.signedIn === Boolean(isAuthenticated);
26
+ return /* @__PURE__ */ jsxs("footer", { className: "pt-10", children: [
27
+ cta && footer.cta && /* @__PURE__ */ jsx(FooterCta, { cta: footer.cta }),
28
+ /* @__PURE__ */ jsx("div", { className: "px-2 pb-2 sm:px-4 sm:pb-4", children: /* @__PURE__ */ jsxs("div", { className: "relative overflow-hidden rounded-[2rem] bg-panel text-white", children: [
29
+ /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute -bottom-10 right-0 select-none font-display text-[9rem] font-semibold leading-none tracking-tighter text-white/[0.04] sm:text-[16rem] lg:text-[22rem]", "aria-hidden": "true", children: footer.watermark }),
30
+ /* @__PURE__ */ jsxs(Container, { className: "relative py-14 lg:py-20", children: [
31
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-12 lg:grid-cols-12", children: [
32
+ /* @__PURE__ */ jsxs("div", { className: "lg:col-span-4", children: [
33
+ /* @__PURE__ */ jsx(Logo, { light: true }),
34
+ /* @__PURE__ */ jsx("div", { className: "mt-5 max-w-sm text-sm leading-relaxed text-white/65", children: footer.about }),
35
+ /* @__PURE__ */ jsx("div", { className: "mt-6 flex flex-wrap gap-3", children: isAuthenticated ? /* @__PURE__ */ jsx(PillLink, { to: links.home.to, variant: "light", size: "sm", children: links.home.label }) : /* @__PURE__ */ jsxs(Fragment, { children: [
36
+ /* @__PURE__ */ jsx(PillLink, { to: links.signUp.to, variant: "light", size: "sm", children: links.signUp.label }),
37
+ /* @__PURE__ */ jsx(PillLink, { to: links.signIn.to, variant: "glass", size: "sm", children: links.signIn.label })
38
+ ] }) })
39
+ ] }),
40
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-8 sm:grid-cols-3 lg:col-span-8", children: footer.columns.map((column) => /* @__PURE__ */ jsxs("div", { children: [
41
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-semibold uppercase tracking-[0.2em] text-accent-soft", children: column.title }),
42
+ /* @__PURE__ */ jsx("ul", { className: "mt-4 space-y-3 text-sm", children: column.links.filter(shown).map((link) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Link, { to: link.to, className: "text-white/70 transition-colors hover:text-white", children: link.label }) }, link.to)) })
43
+ ] }, column.title)) })
44
+ ] }),
45
+ /* @__PURE__ */ jsx("div", { className: "mt-14 grid gap-3 sm:grid-cols-3", children: footer.promises.map((promise) => {
46
+ const Icon = promise.icon;
47
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white/75", children: [
48
+ /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0 text-accent-soft", "aria-hidden": "true" }),
49
+ promise.text
50
+ ] }, promise.text);
51
+ }) }),
52
+ /* @__PURE__ */ jsx("div", { className: "mt-10 flex flex-col gap-4 border-t border-white/10 pt-6 text-xs text-white/50 lg:flex-row lg:items-center lg:justify-between", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [
53
+ /* @__PURE__ */ jsxs("span", { children: [
54
+ "\xA9 ",
55
+ year,
56
+ " ",
57
+ footer.copyright,
58
+ ". All rights reserved."
59
+ ] }),
60
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
61
+ /* @__PURE__ */ jsx("span", { children: footer.madeBy.label }),
62
+ /* @__PURE__ */ jsxs("a", { href: footer.madeBy.href, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1.5 text-white/70 hover:text-white", children: [
63
+ /* @__PURE__ */ jsx("img", { src: footer.madeBy.logo, alt: "", className: "h-6 w-6 brightness-0 invert" }),
64
+ footer.madeBy.name,
65
+ /* @__PURE__ */ jsx(ArrowUpRight, { className: "h-3 w-3", "aria-hidden": "true" })
66
+ ] })
67
+ ] }) })
68
+ ] })
69
+ ] }) })
70
+ ] });
71
+ }
72
+ export {
73
+ SiteFooter as default
74
+ };
@@ -0,0 +1,36 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect } from "react";
3
+ import { useLocation } from "react-router-dom";
4
+ import SiteNav from "./SiteNav.js";
5
+ import SiteFooter from "./SiteFooter.js";
6
+ import Seo from "./Seo.js";
7
+ function useScrollToHash() {
8
+ const { pathname, hash } = useLocation();
9
+ useEffect(() => {
10
+ if (!hash) {
11
+ window.scrollTo(0, 0);
12
+ return void 0;
13
+ }
14
+ let tries = 0;
15
+ const timer = setInterval(() => {
16
+ const el = document.getElementById(decodeURIComponent(hash.slice(1)));
17
+ if (el || ++tries > 20) {
18
+ clearInterval(timer);
19
+ el?.scrollIntoView({ behavior: "smooth", block: "start" });
20
+ }
21
+ }, 50);
22
+ return () => clearInterval(timer);
23
+ }, [pathname, hash]);
24
+ }
25
+ function SiteLayout({ overlay = false, cta = true, children }) {
26
+ useScrollToHash();
27
+ return /* @__PURE__ */ jsxs("div", { className: "okjt-site min-h-dvh bg-tint font-sans text-ink antialiased", children: [
28
+ /* @__PURE__ */ jsx(Seo, {}),
29
+ /* @__PURE__ */ jsx(SiteNav, { overlay }),
30
+ /* @__PURE__ */ jsx("main", { className: overlay ? "" : "pt-24", children }),
31
+ /* @__PURE__ */ jsx(SiteFooter, { cta })
32
+ ] });
33
+ }
34
+ export {
35
+ SiteLayout as default
36
+ };
@@ -0,0 +1,100 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Link, NavLink } from "react-router-dom";
4
+ import { ChevronDown, Menu, X } from "lucide-react";
5
+ import { useSignedIn, useSite } from "./SiteProvider.js";
6
+ import { Container, Logo, PillLink } from "./ui.js";
7
+ function SiteNav({ overlay = false }) {
8
+ const { nav, accountLinks: links } = useSite();
9
+ const isAuthenticated = useSignedIn();
10
+ const megaItem = nav.find((item) => item.mega);
11
+ const [open, setOpen] = useState(false);
12
+ const [scrolled, setScrolled] = useState(false);
13
+ const [mega, setMega] = useState(false);
14
+ const header = useRef(null);
15
+ useEffect(() => {
16
+ const el = header.current;
17
+ if (!el || typeof ResizeObserver === "undefined") return void 0;
18
+ const root = document.documentElement;
19
+ const set = () => root.style.setProperty("--okjt-nav-h", `${el.offsetHeight}px`);
20
+ set();
21
+ const observer = new ResizeObserver(set);
22
+ observer.observe(el, { box: "border-box" });
23
+ return () => {
24
+ observer.disconnect();
25
+ root.style.removeProperty("--okjt-nav-h");
26
+ };
27
+ }, []);
28
+ useEffect(() => {
29
+ const onScroll = () => setScrolled(window.scrollY > 24);
30
+ onScroll();
31
+ window.addEventListener("scroll", onScroll, { passive: true });
32
+ return () => window.removeEventListener("scroll", onScroll);
33
+ }, []);
34
+ useEffect(() => {
35
+ if (!mega) return void 0;
36
+ const onKey = (e) => {
37
+ if (e.key === "Escape") setMega(false);
38
+ };
39
+ window.addEventListener("keydown", onKey);
40
+ return () => window.removeEventListener("keydown", onKey);
41
+ }, [mega]);
42
+ const light = overlay && !scrolled && !open && !mega;
43
+ const closeAll = () => {
44
+ setMega(false);
45
+ setOpen(false);
46
+ };
47
+ const pillLink = ({ isActive }) => `rounded-full px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.12em] transition-colors ${isActive ? "bg-surface text-ink shadow-sm" : light ? "text-white/85 hover:text-white" : "text-soft hover:text-ink"}`;
48
+ const isPage = (to) => !to.includes("#");
49
+ return /* @__PURE__ */ jsxs("header", { ref: header, onMouseLeave: () => setMega(false), className: `okjt-site-nav ${light ? "okjt-site-nav-over" : "okjt-site-nav-solid"} fixed inset-x-0 top-0 z-40 transition-all duration-300 ${light ? "py-5" : "border-b border-line/70 bg-tint/85 py-3 backdrop-blur-xl"}`, children: [
50
+ /* @__PURE__ */ jsxs(Container, { className: "flex items-center justify-between gap-4", children: [
51
+ /* @__PURE__ */ jsx(Logo, { light }),
52
+ /* @__PURE__ */ jsx("nav", { "aria-label": "Main", className: `hidden items-center gap-1 rounded-full p-1 lg:flex ${light ? "border border-white/30 bg-white/10 backdrop-blur-md" : "border border-line bg-tint/70"}`, children: nav.map((item) => {
53
+ if (item.mega) {
54
+ return /* @__PURE__ */ jsxs("span", { className: "flex items-center", onMouseEnter: () => setMega(true), children: [
55
+ /* @__PURE__ */ jsx(NavLink, { to: item.to, onClick: closeAll, className: (state) => `${pillLink({ isActive: state.isActive || mega })} pr-2`, children: item.label }),
56
+ /* @__PURE__ */ jsx(
57
+ "button",
58
+ {
59
+ type: "button",
60
+ "aria-label": `${item.label}: ${item.mega.buttonLabel}`,
61
+ "aria-expanded": mega,
62
+ "aria-controls": item.mega.id,
63
+ onClick: (e) => setMega((v) => e.detail === 0 ? !v : true),
64
+ className: `-ml-1 rounded-full p-1.5 ${light ? "text-white/85 hover:text-white" : "text-soft hover:text-ink"}`,
65
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: `h-3.5 w-3.5 transition-transform ${mega ? "rotate-180" : ""}`, "aria-hidden": "true" })
66
+ }
67
+ )
68
+ ] }, item.to);
69
+ }
70
+ return isPage(item.to) ? /* @__PURE__ */ jsx(NavLink, { to: item.to, end: true, className: pillLink, onMouseEnter: () => setMega(false), children: item.label }, item.to) : /* @__PURE__ */ jsx(Link, { to: item.to, className: pillLink({ isActive: false }), onMouseEnter: () => setMega(false), children: item.label }, item.to);
71
+ }) }),
72
+ /* @__PURE__ */ jsx("div", { className: "hidden items-center gap-3 lg:flex", children: isAuthenticated ? /* @__PURE__ */ jsx(PillLink, { to: links.home.to, variant: light ? "light" : "dark", size: "sm", children: links.home.label }) : /* @__PURE__ */ jsxs(Fragment, { children: [
73
+ /* @__PURE__ */ jsx(Link, { to: links.signIn.to, className: `text-xs font-semibold uppercase tracking-[0.12em] ${light ? "text-white hover:text-white/80" : "text-body hover:text-ink"}`, children: links.signIn.label }),
74
+ /* @__PURE__ */ jsx(PillLink, { to: links.signUp.to, variant: light ? "light" : "dark", size: "sm", children: links.signUp.label })
75
+ ] }) }),
76
+ /* @__PURE__ */ jsx(
77
+ "button",
78
+ {
79
+ type: "button",
80
+ className: `rounded-full p-2 lg:hidden ${light ? "text-white" : "text-body"}`,
81
+ "aria-label": open ? "Close menu" : "Open menu",
82
+ "aria-expanded": open,
83
+ onClick: () => setOpen((v) => !v),
84
+ children: open ? /* @__PURE__ */ jsx(X, { className: "h-6 w-6" }) : /* @__PURE__ */ jsx(Menu, { className: "h-6 w-6" })
85
+ }
86
+ )
87
+ ] }),
88
+ mega && megaItem && /* @__PURE__ */ jsx(megaItem.mega.Menu, { id: megaItem.mega.id, onNavigate: closeAll }),
89
+ open && /* @__PURE__ */ jsx(Container, { className: "lg:hidden", children: /* @__PURE__ */ jsxs("nav", { "aria-label": "Main", className: "mt-3 max-h-[calc(100svh-6rem)] overflow-y-auto rounded-3xl border border-line bg-surface p-3 shadow-xl", children: [
90
+ nav.map((item) => item.mega ? /* @__PURE__ */ jsx(item.mega.MobileList, { onNavigate: closeAll }, item.to) : /* @__PURE__ */ jsx(Link, { to: item.to, onClick: () => setOpen(false), className: "block rounded-2xl px-4 py-3 text-body hover:bg-tint", children: item.label }, item.to)),
91
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex gap-3 border-t border-line pt-3", children: [
92
+ /* @__PURE__ */ jsx(PillLink, { to: isAuthenticated ? links.home.to : links.signIn.to, variant: "outline", size: "sm", className: "flex-1", children: isAuthenticated ? links.home.label : links.signIn.label }),
93
+ !isAuthenticated && /* @__PURE__ */ jsx(PillLink, { to: links.signUp.to, size: "sm", className: "flex-1", children: links.signUp.label })
94
+ ] })
95
+ ] }) })
96
+ ] });
97
+ }
98
+ export {
99
+ SiteNav as default
100
+ };
@@ -0,0 +1,33 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const SiteContext = createContext(null);
4
+ function SiteProvider({ site, children }) {
5
+ return /* @__PURE__ */ jsx(SiteContext.Provider, { value: site, children });
6
+ }
7
+ function useSite() {
8
+ const site = useContext(SiteContext);
9
+ if (!site) throw new Error("@okjt/site: wrap the app in <SiteProvider site={\u2026}>");
10
+ return site;
11
+ }
12
+ function useSignedIn() {
13
+ const { useAccount } = useSite();
14
+ return Boolean(useAccount?.().signedIn);
15
+ }
16
+ function usePhotos() {
17
+ return useSite().usePhotos();
18
+ }
19
+ function usePlaces() {
20
+ return useSite().usePlaces();
21
+ }
22
+ function useMedia(media) {
23
+ const { media: catalogue = {} } = useSite();
24
+ return typeof media === "string" ? catalogue[media] : media;
25
+ }
26
+ export {
27
+ SiteProvider,
28
+ useMedia,
29
+ usePhotos,
30
+ usePlaces,
31
+ useSignedIn,
32
+ useSite
33
+ };
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ import { SiteProvider, useSite, useSignedIn, usePhotos, usePlaces, useMedia } from "./SiteProvider.js";
2
+ import { Container, Logo, PillLink, Eyebrow, Chip, SectionHeading } from "./ui.js";
3
+ import { default as default2 } from "./SiteLayout.js";
4
+ import { default as default3 } from "./SiteNav.js";
5
+ import { default as default4 } from "./SiteFooter.js";
6
+ import { default as default5 } from "./Seo.js";
7
+ import { default as default6 } from "./Photo.js";
8
+ import { default as default7, HeroOrnament } from "./PhotoHero.js";
9
+ import { default as default8 } from "./ImageCard.js";
10
+ import { default as default9, MediaFill } from "./MediaFrame.js";
11
+ import { default as default10 } from "./FeatureExplorer.js";
12
+ import { default as default11 } from "./FeatureGroup.js";
13
+ import { JourneyGlance, JourneyChapter } from "./Journey.js";
14
+ import { default as default12 } from "./ShowcaseCarousel.js";
15
+ import { default as default13, SECTION_OFFSET } from "./SectionNav.js";
16
+ import { default as default14 } from "./FaqSection.js";
17
+ import { default as default15 } from "./PricingSection.js";
18
+ import { default as default16 } from "./LegalPage.js";
19
+ import { RollingWord } from "./RollingWord.js";
20
+ import { default as default17 } from "./useRolling.js";
21
+ import { default as default18 } from "./useScrollSpy.js";
22
+ export {
23
+ Chip,
24
+ Container,
25
+ Eyebrow,
26
+ default14 as FaqSection,
27
+ default10 as FeatureExplorer,
28
+ default11 as FeatureGroup,
29
+ HeroOrnament,
30
+ default8 as ImageCard,
31
+ JourneyChapter,
32
+ JourneyGlance,
33
+ default16 as LegalPage,
34
+ Logo,
35
+ MediaFill,
36
+ default9 as MediaFrame,
37
+ default6 as Photo,
38
+ default7 as PhotoHero,
39
+ PillLink,
40
+ default15 as PricingSection,
41
+ RollingWord,
42
+ SECTION_OFFSET,
43
+ SectionHeading,
44
+ default13 as SectionNav,
45
+ default5 as Seo,
46
+ default12 as ShowcaseCarousel,
47
+ default4 as SiteFooter,
48
+ default2 as SiteLayout,
49
+ default3 as SiteNav,
50
+ SiteProvider,
51
+ useMedia,
52
+ usePhotos,
53
+ usePlaces,
54
+ default17 as useRolling,
55
+ default18 as useScrollSpy,
56
+ useSignedIn,
57
+ useSite
58
+ };
package/dist/ui.js ADDED
@@ -0,0 +1,55 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Link } from "react-router-dom";
3
+ import { ArrowUpRight } from "lucide-react";
4
+ import { useSite } from "./SiteProvider.js";
5
+ function Container({ className = "", children }) {
6
+ return /* @__PURE__ */ jsx("div", { className: `mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 ${className}`, children });
7
+ }
8
+ function Logo({ light = false, className = "" }) {
9
+ const { brand } = useSite();
10
+ return /* @__PURE__ */ jsx(Link, { to: "/", className: `flex items-center gap-2.5 ${className}`, "aria-label": `${brand.name} home`, children: /* @__PURE__ */ jsx("img", { src: light ? brand.logo.light : brand.logo.dark, alt: "", className: "h-8 w-auto" }) });
11
+ }
12
+ const PILL_VARIANTS = {
13
+ dark: "bg-fill text-on-ink hover:bg-fill-hover shadow-[0_10px_30px_-12px_color-mix(in_srgb,var(--okjt-shadow,var(--color-ink))_60%,transparent)]",
14
+ light: "bg-surface text-ink hover:bg-tint shadow-[0_10px_30px_-12px_color-mix(in_srgb,var(--okjt-shadow,var(--color-ink))_35%,transparent)]",
15
+ outline: "border border-line-strong text-body hover:border-accent hover:text-ink",
16
+ glass: "border border-white/40 bg-white/10 text-white backdrop-blur hover:bg-white/20"
17
+ };
18
+ function PillLink({ to, variant = "dark", size = "md", arrow = false, className = "", children, ...rest }) {
19
+ const sizes = { sm: "px-4 py-2 text-xs", md: "px-6 py-3 text-sm", lg: "px-7 py-3.5 text-sm sm:text-base" };
20
+ return /* @__PURE__ */ jsxs(
21
+ Link,
22
+ {
23
+ to,
24
+ className: `okjt-pill okjt-pill-${variant} inline-flex items-center justify-center gap-2 rounded-full font-semibold uppercase tracking-[0.08em] transition-colors ${sizes[size]} ${PILL_VARIANTS[variant]} ${className}`,
25
+ ...rest,
26
+ children: [
27
+ children,
28
+ arrow && /* @__PURE__ */ jsx(ArrowUpRight, { className: "h-4 w-4", "aria-hidden": "true" })
29
+ ]
30
+ }
31
+ );
32
+ }
33
+ function Eyebrow({ light = false, className = "", children }) {
34
+ return /* @__PURE__ */ jsx("div", { className: `text-[11px] font-semibold uppercase tracking-[0.22em] ${light ? "text-white/75" : "text-accent"} ${className}`, children });
35
+ }
36
+ function Chip({ light = true, children }) {
37
+ return /* @__PURE__ */ jsx("span", { className: `inline-flex rounded-full px-3 py-1 text-[11px] font-semibold ${light ? "bg-surface text-ink" : "bg-ink/5 text-body"}`, children });
38
+ }
39
+ function SectionHeading({ eyebrow, title, text, align = "center", light = false, as = "h2", className = "" }) {
40
+ const Tag = as;
41
+ const centered = align === "center";
42
+ return /* @__PURE__ */ jsxs("div", { className: `${centered ? "mx-auto max-w-3xl text-center" : "max-w-2xl"} ${className}`, children: [
43
+ eyebrow && /* @__PURE__ */ jsx(Eyebrow, { light, children: eyebrow }),
44
+ /* @__PURE__ */ jsx(Tag, { className: `mt-3 !font-display text-4xl !font-light leading-[1.05] tracking-tight sm:text-5xl lg:text-6xl ${light ? "!text-white" : "!text-ink"}`, children: title }),
45
+ text && /* @__PURE__ */ jsx("div", { className: `mt-5 text-base leading-relaxed sm:text-lg ${centered ? "mx-auto max-w-2xl" : ""} ${light ? "text-white/80" : "text-soft"}`, children: text })
46
+ ] });
47
+ }
48
+ export {
49
+ Chip,
50
+ Container,
51
+ Eyebrow,
52
+ Logo,
53
+ PillLink,
54
+ SectionHeading
55
+ };
@@ -0,0 +1,18 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useReducedMotion } from "framer-motion";
3
+ function useRolling(phrases, { interval = 2800 } = {}) {
4
+ const [index, setIndex] = useState(0);
5
+ const [paused, setPaused] = useState(false);
6
+ const [hovered, setHovered] = useState(false);
7
+ const still = useReducedMotion();
8
+ useEffect(() => {
9
+ if (still || paused || hovered) return void 0;
10
+ const timer = setInterval(() => setIndex((i) => (i + 1) % phrases.length), interval);
11
+ return () => clearInterval(timer);
12
+ }, [still, paused, hovered, interval, phrases.length]);
13
+ const hold = { onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false) };
14
+ return { item: phrases[index], paused, still, toggle: () => setPaused((v) => !v), hold };
15
+ }
16
+ export {
17
+ useRolling as default
18
+ };
@@ -0,0 +1,28 @@
1
+ import { useEffect, useState } from "react";
2
+ function useScrollSpy(ids) {
3
+ const [active, setActive] = useState(ids[0]);
4
+ const key = ids.join("|");
5
+ useEffect(() => {
6
+ const list = key.split("|");
7
+ const onScroll = () => {
8
+ const line = window.innerHeight / 3;
9
+ let current = list[0];
10
+ for (const id of list) {
11
+ const el = document.getElementById(id);
12
+ if (el && el.getBoundingClientRect().top <= line) current = id;
13
+ }
14
+ setActive(current);
15
+ };
16
+ onScroll();
17
+ window.addEventListener("scroll", onScroll, { passive: true });
18
+ window.addEventListener("resize", onScroll);
19
+ return () => {
20
+ window.removeEventListener("scroll", onScroll);
21
+ window.removeEventListener("resize", onScroll);
22
+ };
23
+ }, [key]);
24
+ return active;
25
+ }
26
+ export {
27
+ useScrollSpy as default
28
+ };