@jimmy_harika/websitekit 0.4.0 → 0.5.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,77 @@
1
+ /**
2
+ * TeaserCardsBlock — 3-up internal-navigation cards (e.g. Weddings / Portraits
3
+ * / About) with outline buttons. Server-safe, edit-aware heading.
4
+ */
5
+ import { renderText, type EditPrimitives } from "../editable";
6
+ import {
7
+ BODY,
8
+ HAIRLINE,
9
+ HEADING,
10
+ HEADING_CASE,
11
+ MUTED,
12
+ RADIUS,
13
+ Section,
14
+ TEXT,
15
+ TokenButton,
16
+ } from "./primitives";
17
+
18
+ export interface TeaserCardItem {
19
+ title: string;
20
+ sub?: string;
21
+ href: string;
22
+ buttonLabel?: string;
23
+ }
24
+
25
+ export interface TeaserCardsBlockProps {
26
+ heading?: string;
27
+ items?: TeaserCardItem[];
28
+ edit?: EditPrimitives;
29
+ }
30
+
31
+ export function TeaserCardsBlock({ heading, items = [], edit }: TeaserCardsBlockProps) {
32
+ return (
33
+ <Section>
34
+ {(heading || edit) &&
35
+ renderText(edit, {
36
+ field: "heading",
37
+ value: heading ?? "",
38
+ as: "h2",
39
+ placeholder: "Where to next",
40
+ className: "mb-10 text-center text-[clamp(1.6rem,4vw,2.5rem)] font-light",
41
+ style: { fontFamily: HEADING, color: TEXT, textTransform: HEADING_CASE },
42
+ })}
43
+ {items.length === 0 ? (
44
+ <p className="py-8 text-center text-sm" style={{ color: MUTED, fontFamily: BODY }}>
45
+ Add teaser cards (title, line, link).
46
+ </p>
47
+ ) : (
48
+ <div className="grid gap-5 md:grid-cols-3">
49
+ {items.map((item, i) => (
50
+ <div
51
+ key={i}
52
+ className="flex flex-col items-center gap-4 px-8 py-12 text-center"
53
+ style={{ border: `1px solid ${HAIRLINE}`, borderRadius: RADIUS }}
54
+ >
55
+ <h3
56
+ className="text-[clamp(1.25rem,2.5vw,1.75rem)] font-light"
57
+ style={{ fontFamily: HEADING, color: TEXT, textTransform: HEADING_CASE }}
58
+ >
59
+ {item.title}
60
+ </h3>
61
+ {item.sub && (
62
+ <p className="max-w-xs text-[15px] leading-relaxed" style={{ fontFamily: BODY, color: MUTED }}>
63
+ {item.sub}
64
+ </p>
65
+ )}
66
+ <div className="mt-auto pt-4">
67
+ <TokenButton href={item.href} outline>
68
+ {item.buttonLabel || "Explore"}
69
+ </TokenButton>
70
+ </div>
71
+ </div>
72
+ ))}
73
+ </div>
74
+ )}
75
+ </Section>
76
+ );
77
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * TestimonialSliderBlock — kind words with three treatments:
3
+ * - "overlay": full-bleed photo behind each quote, crossfade slider
4
+ * - "split-panel": quote panel left / portrait right, both swap together
5
+ * - "band": static tinted band with up to three quotes (no slider)
6
+ * Server-safe; sliding variants reuse the shared <Slider> island.
7
+ */
8
+ import { renderText, type EditPrimitives } from "../editable";
9
+ import { ACCENT, BODY, HEADING, MUTED, RADIUS, Section, TEXT, TINT } from "./primitives";
10
+ import { Slider } from "./slider";
11
+
12
+ export interface TestimonialSlideItem {
13
+ quote: string;
14
+ name: string;
15
+ image?: string;
16
+ }
17
+
18
+ export interface TestimonialSliderBlockProps {
19
+ heading?: string;
20
+ items?: TestimonialSlideItem[];
21
+ variant?: "overlay" | "split-panel" | "band";
22
+ edit?: EditPrimitives;
23
+ }
24
+
25
+ export function TestimonialSliderBlock({
26
+ heading,
27
+ items = [],
28
+ variant = "split-panel",
29
+ edit,
30
+ }: TestimonialSliderBlockProps) {
31
+ if (items.length === 0) {
32
+ return (
33
+ <Section>
34
+ <p className="py-8 text-center text-sm" style={{ color: MUTED, fontFamily: BODY }}>
35
+ Add testimonials (quote, name, optional photo).
36
+ </p>
37
+ </Section>
38
+ );
39
+ }
40
+
41
+ const headingEl =
42
+ heading || edit
43
+ ? renderText(edit, {
44
+ field: "heading",
45
+ value: heading ?? "",
46
+ as: "h2",
47
+ placeholder: "Kind words",
48
+ className: "mb-10 text-center text-[11px] uppercase tracking-[0.3em]",
49
+ style: { fontFamily: BODY, color: ACCENT },
50
+ })
51
+ : null;
52
+
53
+ if (variant === "overlay") {
54
+ return (
55
+ <Slider
56
+ variant="fade"
57
+ arrows="overlay"
58
+ label={heading || "Testimonials"}
59
+ slides={items.map((item, i) => (
60
+ <div key={i} className="relative">
61
+ {item.image && (
62
+ <div className="absolute inset-0">
63
+ {/* eslint-disable-next-line @next/next/no-img-element */}
64
+ <img src={item.image} alt="" loading="lazy" className="h-full w-full object-cover" />
65
+ </div>
66
+ )}
67
+ <div
68
+ className="absolute inset-0"
69
+ style={{ background: item.image ? "rgba(0,0,0,0.55)" : TINT }}
70
+ />
71
+ <figure className="relative flex min-h-[70vh] flex-col items-center justify-center px-8 py-20 text-center sm:px-16">
72
+ <blockquote
73
+ className="max-w-3xl text-[clamp(1.3rem,3vw,2rem)] font-light italic leading-snug"
74
+ style={{ fontFamily: HEADING, color: item.image ? "#ffffff" : TEXT }}
75
+ >
76
+ &ldquo;{item.quote}&rdquo;
77
+ </blockquote>
78
+ <figcaption
79
+ className="mt-8 text-[11px] uppercase tracking-[0.3em]"
80
+ style={{ fontFamily: BODY, color: item.image ? "rgba(255,255,255,0.75)" : MUTED }}
81
+ >
82
+ {item.name}
83
+ </figcaption>
84
+ </figure>
85
+ </div>
86
+ ))}
87
+ />
88
+ );
89
+ }
90
+
91
+ if (variant === "split-panel") {
92
+ return (
93
+ <Section>
94
+ {headingEl}
95
+ <Slider
96
+ variant="fade"
97
+ arrows="below"
98
+ label={heading || "Testimonials"}
99
+ slides={items.map((item, i) => (
100
+ <div key={i} className="grid items-stretch gap-0 md:grid-cols-2">
101
+ <figure
102
+ className="flex flex-col justify-center px-8 py-14 sm:px-12"
103
+ style={{ background: TINT, borderRadius: RADIUS }}
104
+ >
105
+ <blockquote
106
+ className="text-[clamp(1.15rem,2.4vw,1.6rem)] font-light italic leading-snug"
107
+ style={{ fontFamily: HEADING, color: TEXT }}
108
+ >
109
+ &ldquo;{item.quote}&rdquo;
110
+ </blockquote>
111
+ <figcaption
112
+ className="mt-6 text-[11px] uppercase tracking-[0.3em]"
113
+ style={{ fontFamily: BODY, color: MUTED }}
114
+ >
115
+ {item.name}
116
+ </figcaption>
117
+ </figure>
118
+ <div
119
+ className="hidden aspect-[4/3] w-full overflow-hidden md:block md:aspect-auto"
120
+ style={{ borderRadius: RADIUS }}
121
+ >
122
+ {item.image && (
123
+ // eslint-disable-next-line @next/next/no-img-element
124
+ <img src={item.image} alt={item.name} loading="lazy" className="h-full w-full object-cover" />
125
+ )}
126
+ </div>
127
+ </div>
128
+ ))}
129
+ />
130
+ </Section>
131
+ );
132
+ }
133
+
134
+ // band — static, up to three quotes.
135
+ return (
136
+ <Section tint>
137
+ {headingEl}
138
+ <div className="grid gap-10 md:grid-cols-3">
139
+ {items.slice(0, 3).map((item, i) => (
140
+ <figure key={i} className="text-center">
141
+ {item.image && (
142
+ // eslint-disable-next-line @next/next/no-img-element
143
+ <img
144
+ src={item.image}
145
+ alt={item.name}
146
+ loading="lazy"
147
+ className="mx-auto mb-5 size-14 rounded-full object-cover"
148
+ />
149
+ )}
150
+ <blockquote
151
+ className="text-[15px] font-light italic leading-relaxed"
152
+ style={{ fontFamily: HEADING, color: TEXT }}
153
+ >
154
+ &ldquo;{item.quote}&rdquo;
155
+ </blockquote>
156
+ <figcaption
157
+ className="mt-4 text-[11px] uppercase tracking-[0.3em]"
158
+ style={{ fontFamily: BODY, color: MUTED }}
159
+ >
160
+ {item.name}
161
+ </figcaption>
162
+ </figure>
163
+ ))}
164
+ </div>
165
+ </Section>
166
+ );
167
+ }
package/src/model.ts CHANGED
@@ -10,6 +10,8 @@
10
10
 
11
11
  export type ThemeRadius = "none" | "sm" | "md" | "lg";
12
12
  export type ThemeSpacing = "tight" | "normal" | "roomy";
13
+ export type ThemeButtonStyle = "filled" | "outline";
14
+ export type ThemeHeadingCase = "none" | "uppercase";
13
15
 
14
16
  /** Responsive breakpoints a section can be hidden on. mobile <640, tablet
15
17
  * 640–1024, desktop ≥1024 (Tailwind sm / lg boundaries). */
@@ -31,6 +33,12 @@ export interface ThemeTokens {
31
33
  colors: ThemePalette;
32
34
  radius: ThemeRadius;
33
35
  spacing: ThemeSpacing;
36
+ /** Button treatment across all blocks (v0.5, optional — defaults to "filled"). */
37
+ buttonStyle?: ThemeButtonStyle;
38
+ /** Heading text-transform across all blocks (v0.5, optional — defaults to "none"). */
39
+ headingCase?: ThemeHeadingCase;
40
+ /** Scroll-reveal sections on the published site (v0.5, optional — off by default). */
41
+ reveal?: boolean;
34
42
  }
35
43
 
36
44
  export interface Section {
@@ -1,36 +1,44 @@
1
1
  /**
2
- * Production renderer — RSC-safe. No client imports, no hooks. The published
3
- * site uses this: server-renders a `PageDocument` to static HTML, theme tokens
4
- * on the container, each section via its catalog `Component` (no `edit` prop →
5
- * pure/static). The editor canvas renders the same components with `edit`
6
- * injected (see editor/Canvas).
2
+ * Production renderer — RSC-safe. The published site uses this: server-renders
3
+ * a `PageDocument` to static HTML, theme tokens on the container, each section
4
+ * via its catalog `Component` (no `edit` prop → pure/static). The editor canvas
5
+ * renders the same components with `edit` injected (see editor/Canvas).
6
+ *
7
+ * The only client import is the `<Reveal>` island, used solely when
8
+ * `doc.theme.reveal === true` (v0.5 scroll-reveal). The editor canvas does NOT
9
+ * go through `renderSection`, so reveals never run while editing.
7
10
  */
8
- import { createElement, type ReactNode } from "react";
11
+ import { createElement, Fragment, type ReactNode } from "react";
9
12
  import type { PageDocument, Section } from "../model";
10
13
  import { hideOnClasses } from "../model";
11
14
  import { type Catalog, getDefinition, resolveProps } from "../registry";
12
15
  import { googleFontsHref, themeVars } from "../theme";
16
+ import { Reveal } from "../blocks/reveal";
13
17
 
14
- /** One section, statically. Unknown types render an inert placeholder. */
15
- export function renderSection(catalog: Catalog, section: Section): ReactNode {
18
+ /**
19
+ * One section, statically. Unknown types render an inert placeholder.
20
+ * `reveal` (PageRenderer-only) wraps the section in the scroll-reveal island.
21
+ */
22
+ export function renderSection(catalog: Catalog, section: Section, reveal = false): ReactNode {
16
23
  const def = getDefinition(catalog, section.type);
17
24
  const visibility = hideOnClasses(section.hideOn);
25
+ let node: ReactNode;
18
26
  if (!def) {
19
- return createElement(
27
+ node = createElement(
20
28
  "div",
21
- {
22
- key: section.id,
23
- className: `px-6 py-8 text-center text-xs opacity-40 ${visibility}`,
24
- },
29
+ { className: `px-6 py-8 text-center text-xs opacity-40 ${visibility}` },
25
30
  `Unknown section: ${section.type}`,
26
31
  );
32
+ } else {
33
+ const props = resolveProps(def, section.props);
34
+ // Wrap only when the section is hidden on a breakpoint — keeps the DOM flat
35
+ // for normally-visible sections (no wrapper to disturb full-bleed layouts).
36
+ node = visibility
37
+ ? createElement("div", { className: visibility }, createElement(def.Component, props))
38
+ : createElement(def.Component, props);
27
39
  }
28
- const props = resolveProps(def, section.props);
29
- // Wrap only when the section is hidden on a breakpoint — keeps the DOM flat
30
- // for normally-visible sections (no wrapper to disturb full-bleed layouts).
31
- if (!visibility) return createElement(def.Component, { key: section.id, ...props });
32
- const el = createElement(def.Component, props);
33
- return createElement("div", { key: section.id, className: visibility }, el);
40
+ if (reveal) node = createElement(Reveal, null, node);
41
+ return createElement(Fragment, { key: section.id }, node);
34
42
  }
35
43
 
36
44
  export function PageRenderer({
@@ -42,9 +50,12 @@ export function PageRenderer({
42
50
  catalog: Catalog;
43
51
  className?: string;
44
52
  }) {
53
+ const reveal = doc.theme.reveal === true;
45
54
  return (
46
55
  <div style={themeVars(doc.theme)} className={className}>
47
- {doc.sections.map((section) => renderSection(catalog, section))}
56
+ {/* Section 0 is never reveal-wrapped: it's above the fold (static anyway)
57
+ and a wrapper div would defeat a sticky/transparent NavBlock there. */}
58
+ {doc.sections.map((section, i) => renderSection(catalog, section, reveal && i > 0))}
48
59
  </div>
49
60
  );
50
61
  }
package/src/theme.ts CHANGED
@@ -28,6 +28,8 @@ function spacingScale(s: ThemeTokens["spacing"]): string {
28
28
  export function themeVars(theme: ThemeTokens): CSSProperties {
29
29
  const heading = fontStack(theme.fontHeading, "serif");
30
30
  const body = fontStack(theme.fontBody, "sans-serif");
31
+ // v0.5 optional treatments — computed here so blocks only ever read vars.
32
+ const filled = theme.buttonStyle !== "outline";
31
33
  const vars: Record<string, string> = {
32
34
  // legacy storefront tokens (existing renderers depend on these)
33
35
  "--sf-font-heading": heading,
@@ -46,6 +48,13 @@ export function themeVars(theme: ThemeTokens): CSSProperties {
46
48
  "--pb-color-muted": theme.colors.muted,
47
49
  "--pb-radius": radiusPx(theme.radius),
48
50
  "--pb-spacing": spacingScale(theme.spacing),
51
+ // v0.5: heading case — blocks apply `textTransform: var(--pb-heading-case, none)`.
52
+ "--pb-heading-case": theme.headingCase === "uppercase" ? "uppercase" : "none",
53
+ // v0.5: button treatment — filled (bg=primary, fg=page bg, border=primary)
54
+ // or outline (transparent bg, fg=text, border=accent). Blocks read the vars.
55
+ "--pb-btn-bg": filled ? theme.colors.primary : "transparent",
56
+ "--pb-btn-fg": filled ? theme.colors.bg : theme.colors.text,
57
+ "--pb-btn-border": filled ? theme.colors.primary : theme.colors.accent,
49
58
  };
50
59
  return {
51
60
  ...(vars as CSSProperties),
@@ -73,6 +82,8 @@ export const FONT_PAIRS: { id: string; label: string; heading: string; body: str
73
82
  { id: "modern", label: "Modern", heading: "Fraunces", body: "Manrope" },
74
83
  { id: "grotesk", label: "Grotesk", heading: "Space Grotesk", body: "Inter" },
75
84
  { id: "warm", label: "Warm", heading: "DM Serif Display", body: "DM Sans" },
85
+ { id: "cinematic", label: "Cinematic", heading: "Archivo", body: "Roboto Mono" },
86
+ { id: "romantic", label: "Romantic", heading: "Cormorant Garamond", body: "EB Garamond" },
76
87
  ];
77
88
 
78
89
  /** Curated palettes offered in the theme panel. */
@@ -82,4 +93,6 @@ export const PALETTES: { id: string; label: string; colors: ThemeTokens["colors"
82
93
  { id: "blush", label: "Blush", colors: { bg: "#fbf6f3", text: "#2b2422", primary: "#2b2422", accent: "#c98b7e", muted: "#8a7d78" } },
83
94
  { id: "sage", label: "Sage", colors: { bg: "#f4f6f2", text: "#22281f", primary: "#22281f", accent: "#7c8a6a", muted: "#7a8073" } },
84
95
  { id: "cobalt", label: "Cobalt", colors: { bg: "#ffffff", text: "#0f172a", primary: "#0f172a", accent: "#2f5cff", muted: "#64748b" } },
96
+ { id: "porcelain", label: "Porcelain", colors: { bg: "#faf9f6", text: "#1c1a17", primary: "#1c1a17", accent: "#a8894e", muted: "#7c766b" } },
97
+ { id: "onyx-mono", label: "Onyx Mono", colors: { bg: "#0c0c0c", text: "#f2efe9", primary: "#f2efe9", accent: "#9c968b", muted: "#7d786f" } },
85
98
  ];