@jimmy_harika/websitekit 0.3.1

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.
Files changed (45) hide show
  1. package/README.md +86 -0
  2. package/package.json +54 -0
  3. package/src/blocks/author-bio.tsx +52 -0
  4. package/src/blocks/author-hero.tsx +99 -0
  5. package/src/blocks/book-grid.tsx +97 -0
  6. package/src/blocks/book-spotlight.tsx +109 -0
  7. package/src/blocks/contact.tsx +64 -0
  8. package/src/blocks/faq.tsx +65 -0
  9. package/src/blocks/form.tsx +234 -0
  10. package/src/blocks/gallery.tsx +230 -0
  11. package/src/blocks/index.ts +32 -0
  12. package/src/blocks/newsletter.tsx +100 -0
  13. package/src/blocks/pricing.tsx +120 -0
  14. package/src/blocks/primitives.tsx +109 -0
  15. package/src/blocks/testimonials.tsx +89 -0
  16. package/src/catalogs/author/catalog.tsx +496 -0
  17. package/src/catalogs/author/index.ts +9 -0
  18. package/src/catalogs/author/templates.ts +133 -0
  19. package/src/catalogs/publisher/catalog.tsx +308 -0
  20. package/src/catalogs/publisher/index.ts +10 -0
  21. package/src/catalogs/publisher/templates.ts +158 -0
  22. package/src/editable.tsx +76 -0
  23. package/src/editor/BlockLibrary.tsx +110 -0
  24. package/src/editor/Canvas.tsx +128 -0
  25. package/src/editor/ErrorBoundary.tsx +54 -0
  26. package/src/editor/InsertPoint.tsx +31 -0
  27. package/src/editor/Inspector.tsx +341 -0
  28. package/src/editor/PageBuilder.tsx +217 -0
  29. package/src/editor/SectionFrame.tsx +127 -0
  30. package/src/editor/SectionsPanel.tsx +149 -0
  31. package/src/editor/TemplateGallery.tsx +98 -0
  32. package/src/editor/ThemePanel.tsx +131 -0
  33. package/src/editor/Toolbar.tsx +162 -0
  34. package/src/editor/dnd.tsx +61 -0
  35. package/src/editor/edit-primitives.tsx +164 -0
  36. package/src/editor/index.tsx +8 -0
  37. package/src/editor/shortcuts.ts +106 -0
  38. package/src/editor/ui-context.tsx +32 -0
  39. package/src/index.ts +11 -0
  40. package/src/lib/cn.ts +8 -0
  41. package/src/model.ts +116 -0
  42. package/src/registry.ts +138 -0
  43. package/src/renderer/index.tsx +55 -0
  44. package/src/store.ts +216 -0
  45. package/src/theme.ts +85 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Pricing — package cards (photographer packages, speaking fees, etc.). Items
3
+ * come from a `list` field: `name | price | cadence | features ; ctaLabel |
4
+ * ctaHref` per line (features ;-separated; cadence optional e.g. "/mo").
5
+ * Portable, RSC-safe, edit-aware. The highlighted card reads ACCENT.
6
+ */
7
+ import { renderText, type EditPrimitives } from "../editable";
8
+ import { ACCENT, BODY, HAIRLINE, HEADING, MUTED, RADIUS, Section, TEXT, TINT } from "./primitives";
9
+
10
+ export interface PricingProps {
11
+ heading?: string;
12
+ /** `name | price | cadence | f1; f2; f3 | ctaLabel | ctaHref` per line. */
13
+ plans?: string;
14
+ /** 1-based index of the plan to highlight (ACCENT), or blank for none. */
15
+ highlight?: string;
16
+ edit?: EditPrimitives;
17
+ }
18
+
19
+ interface Plan {
20
+ name: string;
21
+ price: string;
22
+ cadence?: string;
23
+ features: string[];
24
+ ctaLabel?: string;
25
+ ctaHref?: string;
26
+ }
27
+
28
+ function parsePlans(raw?: string): Plan[] {
29
+ if (!raw) return [];
30
+ return raw
31
+ .split("\n")
32
+ .map((l) => l.trim())
33
+ .filter(Boolean)
34
+ .map((l) => {
35
+ const p = l.split("|").map((s) => (s ?? "").trim());
36
+ return {
37
+ name: p[0] || "Plan",
38
+ price: p[1] || "",
39
+ cadence: p[2] || undefined,
40
+ features: (p[3] ?? "").split(";").map((f) => f.trim()).filter(Boolean),
41
+ ctaLabel: p[4] || undefined,
42
+ ctaHref: p[5] || undefined,
43
+ };
44
+ });
45
+ }
46
+
47
+ export function Pricing(props: PricingProps) {
48
+ const { heading, plans, highlight, edit } = props;
49
+ const items = parsePlans(plans);
50
+ const hi = highlight ? Number(highlight) - 1 : -1;
51
+
52
+ return (
53
+ <Section>
54
+ {heading &&
55
+ renderText(edit, {
56
+ field: "heading",
57
+ value: heading,
58
+ as: "h2",
59
+ placeholder: "Plans & pricing",
60
+ className: "mb-10 text-center text-[clamp(1.6rem,4vw,2.5rem)] font-light",
61
+ style: { fontFamily: HEADING, color: TEXT },
62
+ })}
63
+ <div className="grid gap-5 md:grid-cols-3">
64
+ {items.map((p, i) => {
65
+ const hot = i === hi;
66
+ return (
67
+ <div
68
+ key={i}
69
+ className="flex flex-col p-7"
70
+ style={{
71
+ borderRadius: RADIUS,
72
+ border: `1px solid ${hot ? ACCENT : HAIRLINE}`,
73
+ background: hot ? TINT : "transparent",
74
+ }}
75
+ >
76
+ <h3 className="text-sm font-semibold uppercase tracking-wide" style={{ fontFamily: BODY, color: MUTED }}>
77
+ {p.name}
78
+ </h3>
79
+ <div className="mt-3 flex items-baseline gap-1">
80
+ <span className="text-[clamp(1.8rem,4vw,2.6rem)] font-light" style={{ fontFamily: HEADING, color: TEXT }}>
81
+ {p.price}
82
+ </span>
83
+ {p.cadence && (
84
+ <span className="text-sm" style={{ fontFamily: BODY, color: MUTED }}>
85
+ {p.cadence}
86
+ </span>
87
+ )}
88
+ </div>
89
+ <ul className="mt-5 flex-1 space-y-2 text-[14px]" style={{ fontFamily: BODY, color: TEXT }}>
90
+ {p.features.map((f, j) => (
91
+ <li key={j} className="flex gap-2">
92
+ <span style={{ color: ACCENT }}>·</span>
93
+ <span>{f}</span>
94
+ </li>
95
+ ))}
96
+ </ul>
97
+ {p.ctaLabel && p.ctaHref && (
98
+ <a
99
+ href={p.ctaHref}
100
+ target={p.ctaHref.startsWith("http") ? "_blank" : undefined}
101
+ rel={p.ctaHref.startsWith("http") ? "noopener noreferrer" : undefined}
102
+ className="mt-6 inline-flex items-center justify-center px-5 py-2.5 text-[13px] font-medium transition-opacity hover:opacity-80"
103
+ style={{
104
+ background: hot ? ACCENT : "transparent",
105
+ color: hot ? "var(--pb-color-bg, #fff)" : ACCENT,
106
+ border: hot ? "none" : `1px solid ${ACCENT}`,
107
+ borderRadius: RADIUS,
108
+ fontFamily: BODY,
109
+ }}
110
+ >
111
+ {p.ctaLabel}
112
+ </a>
113
+ )}
114
+ </div>
115
+ );
116
+ })}
117
+ </div>
118
+ </Section>
119
+ );
120
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Shared block primitives — design-system-agnostic. These read ONLY the
3
+ * engine's `--pb-*` theme tokens (emitted by `themeVars`) and Tailwind utility
4
+ * classes, with NO app-UI imports. That is what makes a block built here render
5
+ * correctly on EVERY consuming app's site (the load-bearing decision of the
6
+ * shared-package effort). Blocks are pure/SSR-safe and edit-aware via the
7
+ * engine's `renderText`/`renderImage` bridge.
8
+ */
9
+ import { type CSSProperties, type ReactNode } from "react";
10
+
11
+ /* ── Theme token shorthands (with sane fallbacks for un-themed previews) ─────── */
12
+ export const HEADING = "var(--pb-font-heading, ui-serif, Georgia, 'Times New Roman', serif)";
13
+ export const BODY = "var(--pb-font-body, ui-sans-serif, system-ui, sans-serif)";
14
+ export const ACCENT = "var(--pb-color-accent, currentColor)";
15
+ export const TEXT = "var(--pb-color-text, #1a1a1a)";
16
+ export const MUTED = "var(--pb-color-muted, #6b7280)";
17
+ export const BG = "var(--pb-color-bg, #ffffff)";
18
+ export const RADIUS = "var(--pb-radius, 4px)";
19
+
20
+ /** A faint tint of the text color — for alternating section backgrounds. */
21
+ export const TINT = "color-mix(in srgb, var(--pb-color-text, #1a1a1a) 4%, transparent)";
22
+ /** A hairline border that adapts to the theme. */
23
+ export const HAIRLINE = "color-mix(in srgb, var(--pb-color-text, #1a1a1a) 12%, transparent)";
24
+
25
+ /** One book, as a block sees it — purely presentational. The app resolves live
26
+ * data (e.g. an author's real books) into this shape before render. */
27
+ export interface BookItem {
28
+ id?: string;
29
+ title: string;
30
+ subtitle?: string;
31
+ coverUrl?: string;
32
+ description?: string;
33
+ links?: { label: string; url: string }[];
34
+ }
35
+
36
+ /** Section shell: theme-scaled vertical rhythm + centered max-width container. */
37
+ export function Section({
38
+ children,
39
+ style,
40
+ className = "",
41
+ narrow = false,
42
+ tint = false,
43
+ }: {
44
+ children: ReactNode;
45
+ style?: CSSProperties;
46
+ className?: string;
47
+ narrow?: boolean;
48
+ tint?: boolean;
49
+ }) {
50
+ return (
51
+ <section
52
+ style={{
53
+ paddingBlock: "calc(var(--pb-spacing, 1) * 4.5rem)",
54
+ ...(tint ? { backgroundColor: TINT } : {}),
55
+ ...style,
56
+ }}
57
+ className={`w-full px-6 sm:px-10 ${className}`}
58
+ >
59
+ <div className={`mx-auto ${narrow ? "max-w-3xl" : "max-w-6xl"}`}>{children}</div>
60
+ </section>
61
+ );
62
+ }
63
+
64
+ /** Small-caps eyebrow above a heading. */
65
+ export function Eyebrow({ children }: { children?: ReactNode }) {
66
+ if (!children) return null;
67
+ return (
68
+ <p
69
+ className="mb-4 text-[11px] font-medium uppercase tracking-[0.3em]"
70
+ style={{ color: ACCENT, fontFamily: BODY }}
71
+ >
72
+ {children}
73
+ </p>
74
+ );
75
+ }
76
+
77
+ /** Pill / link button in the theme accent — used for book buy links + CTAs. */
78
+ export function AccentLink({
79
+ href,
80
+ children,
81
+ solid = true,
82
+ }: {
83
+ href: string;
84
+ children: ReactNode;
85
+ solid?: boolean;
86
+ }) {
87
+ const base =
88
+ "inline-flex items-center justify-center px-5 py-2.5 text-[13px] font-medium tracking-wide transition-opacity hover:opacity-80";
89
+ return (
90
+ <a
91
+ href={href || "#"}
92
+ target={href?.startsWith("http") ? "_blank" : undefined}
93
+ rel={href?.startsWith("http") ? "noopener noreferrer" : undefined}
94
+ className={base}
95
+ style={
96
+ solid
97
+ ? { background: ACCENT, color: BG, borderRadius: RADIUS, fontFamily: BODY }
98
+ : {
99
+ border: `1px solid ${ACCENT}`,
100
+ color: ACCENT,
101
+ borderRadius: RADIUS,
102
+ fontFamily: BODY,
103
+ }
104
+ }
105
+ >
106
+ {children}
107
+ </a>
108
+ );
109
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Testimonials — social proof. Items from a `list` field:
3
+ * `quote | author | role` per line. Variants: single (the first, large) or grid.
4
+ * Portable, RSC-safe, edit-aware. Portrait images optional (append ` | url`).
5
+ */
6
+ import { renderText, type EditPrimitives } from "../editable";
7
+ import { BODY, HEADING, MUTED, RADIUS, Section, TEXT, TINT } from "./primitives";
8
+
9
+ export interface TestimonialsProps {
10
+ heading?: string;
11
+ /** `quote | author | role` per line (role + portrait url optional). */
12
+ items?: string;
13
+ layout?: "single" | "grid";
14
+ edit?: EditPrimitives;
15
+ }
16
+
17
+ interface Quote {
18
+ quote: string;
19
+ author: string;
20
+ role?: string;
21
+ portrait?: string;
22
+ }
23
+
24
+ function parseItems(raw?: string): Quote[] {
25
+ if (!raw) return [];
26
+ return raw
27
+ .split("\n")
28
+ .map((l) => l.trim())
29
+ .filter(Boolean)
30
+ .map((l) => {
31
+ const p = l.split("|").map((s) => (s ?? "").trim());
32
+ return { quote: p[0] || "", author: p[1] || "—", role: p[2] || undefined, portrait: p[3] || undefined };
33
+ })
34
+ .filter((q) => q.quote);
35
+ }
36
+
37
+ export function Testimonials(props: TestimonialsProps) {
38
+ const { heading, items, layout = "grid", edit } = props;
39
+ const quotes = parseItems(items);
40
+ const featured = layout === "single" ? quotes.slice(0, 1) : quotes;
41
+
42
+ return (
43
+ <Section tint>
44
+ {heading &&
45
+ renderText(edit, {
46
+ field: "heading",
47
+ value: heading,
48
+ as: "h2",
49
+ placeholder: "Kind words",
50
+ className: "mb-10 text-center text-[clamp(1.6rem,4vw,2.5rem)] font-light",
51
+ style: { fontFamily: HEADING, color: TEXT },
52
+ })}
53
+ {layout === "single" && featured[0] ? (
54
+ <figure className="mx-auto max-w-3xl text-center">
55
+ <blockquote
56
+ className="text-[clamp(1.3rem,3vw,2rem)] font-light leading-snug"
57
+ style={{ fontFamily: HEADING, color: TEXT }}
58
+ >
59
+ “{featured[0].quote}”
60
+ </blockquote>
61
+ <figcaption className="mt-5 text-sm" style={{ fontFamily: BODY, color: MUTED }}>
62
+ {featured[0].author}
63
+ {featured[0].role ? ` · ${featured[0].role}` : ""}
64
+ </figcaption>
65
+ </figure>
66
+ ) : (
67
+ <div className="grid gap-5 md:grid-cols-3">
68
+ {featured.map((q, i) => (
69
+ <figure key={i} className="p-6" style={{ borderRadius: RADIUS, background: TINT }}>
70
+ <blockquote className="text-[15px] leading-relaxed" style={{ fontFamily: BODY, color: TEXT }}>
71
+ “{q.quote}”
72
+ </blockquote>
73
+ <figcaption className="mt-4 flex items-center gap-3 text-sm" style={{ fontFamily: BODY, color: MUTED }}>
74
+ {q.portrait && (
75
+ // eslint-disable-next-line @next/next/no-img-element
76
+ <img src={q.portrait} alt={q.author} className="size-9 rounded-full object-cover" loading="lazy" />
77
+ )}
78
+ <span>
79
+ <span style={{ color: TEXT }}>{q.author}</span>
80
+ {q.role ? ` · ${q.role}` : ""}
81
+ </span>
82
+ </figcaption>
83
+ </figure>
84
+ ))}
85
+ </div>
86
+ )}
87
+ </Section>
88
+ );
89
+ }