@zorgo/next 1.1.7 → 2.0.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.
Files changed (74) hide show
  1. package/dist/cli.d.mts +1 -1
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +322 -1
  4. package/dist/cli.js.map +1 -0
  5. package/dist/cli.mjs +322 -1
  6. package/dist/cli.mjs.map +1 -0
  7. package/dist/components.d.mts +1047 -89
  8. package/dist/components.d.ts +1047 -89
  9. package/dist/components.js +4611 -1048
  10. package/dist/components.js.map +1 -1
  11. package/dist/components.mjs +4635 -1149
  12. package/dist/components.mjs.map +1 -1
  13. package/dist/images.d.mts +48 -0
  14. package/dist/images.d.ts +48 -0
  15. package/dist/images.js +72 -0
  16. package/dist/images.js.map +1 -0
  17. package/dist/images.mjs +39 -0
  18. package/dist/images.mjs.map +1 -0
  19. package/dist/index.d.mts +489 -14
  20. package/dist/index.d.ts +489 -14
  21. package/dist/index.js +168 -1
  22. package/dist/index.js.map +1 -0
  23. package/dist/index.mjs +168 -1
  24. package/dist/index.mjs.map +1 -0
  25. package/dist/item-page.d.mts +157 -0
  26. package/dist/item-page.d.ts +157 -0
  27. package/dist/item-page.js +168 -0
  28. package/dist/item-page.js.map +1 -0
  29. package/dist/item-page.mjs +131 -0
  30. package/dist/item-page.mjs.map +1 -0
  31. package/dist/legal.d.mts +31 -0
  32. package/dist/legal.d.ts +31 -0
  33. package/dist/legal.js +322 -0
  34. package/dist/legal.js.map +1 -0
  35. package/dist/legal.mjs +293 -0
  36. package/dist/legal.mjs.map +1 -0
  37. package/dist/seo.d.mts +87 -2
  38. package/dist/seo.d.ts +87 -2
  39. package/dist/seo.js +111 -2
  40. package/dist/seo.js.map +1 -1
  41. package/dist/seo.mjs +106 -1
  42. package/dist/seo.mjs.map +1 -1
  43. package/dist/server.d.mts +27 -1
  44. package/dist/server.d.ts +27 -1
  45. package/dist/server.js +761 -1
  46. package/dist/server.js.map +1 -0
  47. package/dist/server.mjs +722 -1
  48. package/dist/server.mjs.map +1 -0
  49. package/dist/slugs.d.mts +2 -1
  50. package/dist/slugs.d.ts +2 -1
  51. package/dist/slugs.js +2 -0
  52. package/dist/slugs.js.map +1 -1
  53. package/dist/slugs.mjs +1 -0
  54. package/dist/slugs.mjs.map +1 -1
  55. package/dist/tree/README.md +2 -2
  56. package/dist/tree/app/components/ClientItemForm.tsx +7 -0
  57. package/dist/tree/app/components/Header.tsx +3 -0
  58. package/dist/tree/app/components/ModalRegistry.tsx +3 -2
  59. package/dist/tree/app/components/index.ts +2 -1
  60. package/dist/tree/app/error.tsx +12 -3
  61. package/dist/tree/app/global-error.tsx +21 -4
  62. package/dist/tree/app/globals.css +41 -0
  63. package/dist/tree/app/menu/[slug]/page.tsx +16 -48
  64. package/dist/tree/app/not-found.tsx +17 -3
  65. package/dist/tree/app/privacy/page.tsx.gen.ts +28 -9
  66. package/dist/tree/app/terms/page.tsx.gen.ts +26 -10
  67. package/dist/tree/gitignore +7 -1
  68. package/dist/tree/image-loader.ts +4 -0
  69. package/dist/tree/next.config.ts +13 -0
  70. package/dist/tree/open-next.config.ts +7 -0
  71. package/dist/tree/package.json.gen.ts +8 -2
  72. package/package.json +17 -2
  73. package/dist/tree/app/globals.css.gen.ts +0 -123
  74. package/dist/tree/app/menu/[slug]/ItemCustomizationClient.tsx +0 -18
@@ -1,57 +1,35 @@
1
1
  import type { Metadata } from "next";
2
- import { baseItemIdFromSlug, menuItemSlug } from "@zorgo/next/slugs";
3
- import { buildMenuItemJsonLd } from "@zorgo/next/seo";
2
+ import {
3
+ ItemCustomizationView as ServerItemForm,
4
+ menuItemMetadata,
5
+ menuItemStaticParams,
6
+ } from "@zorgo/next/item-page";
7
+ import { baseItemIdFromSlug } from "@zorgo/next/slugs";
4
8
  import { menu } from "../../../lib/generated/menu";
5
- import ItemCustomizationClient from "./ItemCustomizationClient";
9
+ import { ClientItemForm } from "../../components/ClientItemForm";
6
10
 
7
- // Single source of truth for resolving the item a slug points at — shared by
8
- // generateMetadata and the page so the lookup never drifts. Returns undefined
9
- // for an unknown slug; callers degrade gracefully (the page 404s downstream).
10
- function itemFromSlug(slug: string) {
11
- const baseItemId = baseItemIdFromSlug(slug);
12
- return menu.items.find((item) => item.base_item_id === baseItemId);
13
- }
11
+ // The server half (hero image, JSON-LD), its metadata, and its static params all
12
+ // live in @zorgo/next/item-page so a package bump updates every site at once.
13
+ // The interactive half is this site's app/components/ClientItemForm.tsx edit
14
+ // that file to customize the form, not this one.
14
15
 
15
16
  // menu items are known at build time from the prebuilt menu — pre-render
16
17
  // every item page instead of rendering on-demand.
17
18
  export function generateStaticParams() {
18
- return menu.items.map((item) => ({
19
- slug: menuItemSlug(item.name, item.base_item_id),
20
- }));
19
+ return menuItemStaticParams(menu);
21
20
  }
22
21
 
23
22
  // only slugs present in the prebuilt menu are valid routes; anything else 404s
24
23
  // instead of falling through to an on-demand render.
25
24
  export const dynamicParams = false;
26
25
 
27
- // per-item title/description/OG so each item ranks on its own long-tail terms
28
- // and shares with real context, instead of inheriting the generic root title.
29
- // title is just the item name; the root layout's title template wraps it as
30
- // `<name> | Brand`. metadataBase (set in the root layout) resolves OG URLs, so
31
- // nothing absolute is hardcoded here.
32
26
  export async function generateMetadata({
33
27
  params,
34
28
  }: {
35
29
  params: Promise<{ slug: string }>;
36
30
  }): Promise<Metadata> {
37
31
  const { slug } = await params;
38
- const item = itemFromSlug(slug);
39
- if (!item) return {};
40
-
41
- const description =
42
- item.description ?? `Order ${item.name} online for pickup or delivery.`;
43
-
44
- return {
45
- title: item.name,
46
- description,
47
- openGraph: {
48
- title: item.name,
49
- description,
50
- // Next's typed OpenGraph union has no "product"; "website" is the
51
- // closest valid value for a standalone item page.
52
- type: "website",
53
- },
54
- };
32
+ return menuItemMetadata(menu, slug);
55
33
  }
56
34
 
57
35
  export default async function ItemPage({
@@ -60,19 +38,9 @@ export default async function ItemPage({
60
38
  params: Promise<{ slug: string }>;
61
39
  }) {
62
40
  const { slug } = await params;
63
- const item = itemFromSlug(slug);
64
-
65
41
  return (
66
- <div className="p-8 md:p-16 max-w-4xl mx-auto">
67
- {item && (
68
- <script
69
- type="application/ld+json"
70
- dangerouslySetInnerHTML={{
71
- __html: JSON.stringify(buildMenuItemJsonLd(item)),
72
- }}
73
- />
74
- )}
75
- <ItemCustomizationClient baseItemId={baseItemIdFromSlug(slug)} />
76
- </div>
42
+ <ServerItemForm menu={menu} slug={slug}>
43
+ <ClientItemForm baseItemId={baseItemIdFromSlug(slug)} />
44
+ </ServerItemForm>
77
45
  );
78
46
  }
@@ -1,8 +1,22 @@
1
+ import Link from "next/link";
2
+
3
+ // min-h-full, not a viewport height: this renders inside the layout’s
4
+ // <main>, between a real header and footer, so centring on the viewport would
5
+ // push it below the fold.
1
6
  export default function NotFound() {
2
7
  return (
3
- <div>
4
- <h2>Not Found</h2>
5
- <p>Could not find requested resource</p>
8
+ <div className="mx-auto flex min-h-full max-w-md flex-col items-center justify-center gap-4 px-4 py-24 text-center">
9
+ <p className="text-6xl font-black text-primary">404</p>
10
+ <h1 className="text-3xl font-bold">We can&apos;t find that page</h1>
11
+ <p className="opacity-70">
12
+ It may have moved, or the link that brought you here is out of date.
13
+ </p>
14
+ <Link
15
+ href="/"
16
+ className="mt-2 inline-flex min-h-11 items-center justify-center rounded-xl bg-primary px-6 py-3 font-semibold text-primary-foreground transition-colors duration-300 hover:bg-primary/80"
17
+ >
18
+ Back to the home page
19
+ </Link>
6
20
  </div>
7
21
  );
8
22
  }
@@ -1,15 +1,34 @@
1
1
  import type { ScaffoldOptions } from "../../../../types";
2
2
 
3
+ // The policy text itself lives in @zorgo/next/components (src/components/client/legal.ts),
4
+ // so updating the package updates every scaffolded site. Only the brand name and
5
+ // the "last updated" date are baked in here.
3
6
  export default function generatePrivacyPage(opts: ScaffoldOptions) {
7
+ if (!opts.includePrivacyPage) return;
4
8
 
5
- if (opts.includePrivacyPage) {
6
- return `export default function PrivacyPage() {
7
- return (
8
- <div>
9
- <h1>Privacy Policy</h1>
10
- </div>
11
- )
12
- }`;
13
- }
9
+ const today = new Date().toLocaleDateString("en-US", {
10
+ year: "numeric",
11
+ month: "long",
12
+ day: "numeric",
13
+ });
14
14
 
15
+ return `import { LegalPage, privacyPolicyHtml } from "@zorgo/next/legal";
16
+
17
+ const BRAND = ${JSON.stringify(opts.franchiseName)};
18
+
19
+ export const metadata = {
20
+ title: "Privacy Policy",
21
+ description: \`How \${BRAND} handles your information.\`,
22
+ };
23
+
24
+ export default function PrivacyPage() {
25
+ return (
26
+ <LegalPage
27
+ title="Privacy Policy"
28
+ lastUpdated="${today}"
29
+ html={privacyPolicyHtml({ brand: BRAND, lastUpdated: "${today}" })}
30
+ />
31
+ );
32
+ }
33
+ `;
15
34
  }
@@ -1,16 +1,32 @@
1
1
  import type { ScaffoldOptions } from "../../../../types";
2
2
 
3
- // add bullshit tos here
3
+ // See privacy/page.tsx.gen.ts the terms text is shipped by the package.
4
4
  export default function generateTermsPage(opts: ScaffoldOptions) {
5
+ if (!opts.includeTermsPage) return;
5
6
 
6
- if (opts.includeTermsPage) {
7
- return `export default function TermsPage() {
8
- return (
9
- <div>
10
- <h1>Terms of Service</h1>
11
- </div>
12
- )
13
- }`;
14
- }
7
+ const today = new Date().toLocaleDateString("en-US", {
8
+ year: "numeric",
9
+ month: "long",
10
+ day: "numeric",
11
+ });
15
12
 
13
+ return `import { LegalPage, termsOfUseHtml } from "@zorgo/next/legal";
14
+
15
+ const BRAND = ${JSON.stringify(opts.franchiseName)};
16
+
17
+ export const metadata = {
18
+ title: "Terms of Service",
19
+ description: \`Terms for ordering online from \${BRAND}.\`,
20
+ };
21
+
22
+ export default function TermsPage() {
23
+ return (
24
+ <LegalPage
25
+ title="Online Ordering Terms of Use"
26
+ lastUpdated="${today}"
27
+ html={termsOfUseHtml({ brand: BRAND, lastUpdated: "${today}" })}
28
+ />
29
+ );
30
+ }
31
+ `;
16
32
  }
@@ -39,6 +39,12 @@ next-env.d.ts
39
39
 
40
40
  /.wrangler/
41
41
 
42
- # generated from .env by scripts/prebuild.ts
42
+ # generated from .env / franchise_branding by scripts/prebuild.ts
43
43
  /wrangler.toml
44
44
  /lib/generated/
45
+ /app/generated/
46
+ /public/styles/
47
+ /public/images/items/
48
+ /public/images/logo.*
49
+ # AVIF ladders encoded from assets/images/ — the originals are what's committed
50
+ /public/images/**/*.avif
@@ -0,0 +1,4 @@
1
+ // next.config.ts points images.loaderFile here. The implementation lives in the
2
+ // package so a @zorgo/next bump can change the variant ladder without every site
3
+ // editing this file; Next only requires that it default-export a loader.
4
+ export { default } from "@zorgo/next/images";
@@ -1,5 +1,6 @@
1
1
  import type { NextConfig } from "next";
2
2
  import path from "path";
3
+ import { IMAGE_WIDTHS } from "@zorgo/next/images";
3
4
 
4
5
  const monorepoRoot = path.resolve(__dirname, "../../..");
5
6
 
@@ -9,6 +10,18 @@ const nextConfig: NextConfig = {
9
10
  root: monorepoRoot,
10
11
  },
11
12
  transpilePackages: ["@zorgo", "@zorgo-website-core"],
13
+ // Menu item images and the site's own photos in assets/images/ are both
14
+ // resized to a fixed AVIF ladder at prebuild, so nothing needs a runtime
15
+ // optimizer (and no Supabase host in remotePatterns — a custom loader
16
+ // bypasses that check entirely).
17
+ images: {
18
+ loader: "custom",
19
+ loaderFile: "./image-loader.ts",
20
+ // Must mirror the rungs prebuild emits, or Next advertises srcset widths no
21
+ // variant has and the browser picks a candidate narrower than it asked for.
22
+ deviceSizes: [...IMAGE_WIDTHS],
23
+ imageSizes: [],
24
+ },
12
25
  };
13
26
 
14
27
  export default nextConfig;
@@ -1,4 +1,11 @@
1
1
  import { defineCloudflareConfig } from "@opennextjs/cloudflare";
2
+ import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
2
3
 
3
4
  export default defineCloudflareConfig({
5
+ // Without this the cache defaults to "dummy": every prerendered page is a
6
+ // miss, and `dynamicParams = false` turns that miss into a 404. The menu is
7
+ // rebuilt from the prebuilt data on every deploy, so a read-only cache
8
+ // served from Workers assets is enough — swap to r2IncrementalCache if a
9
+ // site ever needs ISR.
10
+ incrementalCache: staticAssetsIncrementalCache,
4
11
  });
@@ -12,8 +12,14 @@ export default function generatePackageJson(opts: ScaffoldOptions): string {
12
12
  "dev:no-prebuild": "next dev",
13
13
  "prebuild": "tsx scripts/prebuild.ts",
14
14
  "build": "tsx scripts/prebuild.ts && next build",
15
- "cloudflare:deploy": "tsx scripts/prebuild.ts && wrangler deploy",
16
- "preview": "tsx scripts/prebuild.ts && wrangler dev",
15
+ "cloudflare:deploy":
16
+ "tsx scripts/prebuild.ts && opennextjs-cloudflare build && wrangler deploy",
17
+ "cloudflare:deploy:staging":
18
+ "tsx scripts/prebuild.ts && opennextjs-cloudflare build && wrangler deploy --env staging",
19
+ "preview":
20
+ "tsx scripts/prebuild.ts && opennextjs-cloudflare build && wrangler dev",
21
+ "preview:staging":
22
+ "tsx scripts/prebuild.ts && opennextjs-cloudflare build && wrangler dev --env staging",
17
23
  },
18
24
  dependencies: PINNED_VERSIONS,
19
25
  devDependencies: DEV_DEPENDENCIES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zorgo/next",
3
- "version": "1.1.7",
3
+ "version": "2.0.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "bin": {
@@ -54,6 +54,21 @@
54
54
  "require": "./dist/slugs.js",
55
55
  "types": "./dist/slugs.d.ts"
56
56
  },
57
+ "./images": {
58
+ "import": "./dist/images.mjs",
59
+ "require": "./dist/images.js",
60
+ "types": "./dist/images.d.ts"
61
+ },
62
+ "./legal": {
63
+ "import": "./dist/legal.mjs",
64
+ "require": "./dist/legal.js",
65
+ "types": "./dist/legal.d.ts"
66
+ },
67
+ "./item-page": {
68
+ "import": "./dist/item-page.mjs",
69
+ "require": "./dist/item-page.js",
70
+ "types": "./dist/item-page.d.ts"
71
+ },
57
72
  "./seo": {
58
73
  "import": "./dist/seo.mjs",
59
74
  "require": "./dist/seo.js",
@@ -77,7 +92,7 @@
77
92
  "zustand": "^4.x.x"
78
93
  },
79
94
  "scripts": {
80
- "build": "tsup && tsx scripts/obfuscate.ts",
95
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsup && tsx scripts/obfuscate.ts",
81
96
  "release": "node scripts/publish.mjs",
82
97
  "ship": "changeset && changeset version && node scripts/publish.mjs",
83
98
  "dev": "echo \"No build step. Consumers transpile TS sources.\"",
@@ -1,123 +0,0 @@
1
- import type { ScaffoldOptions } from "../../../types";
2
-
3
- /** Keep in sync with scripts/prebuild.ts HARDCODED_BRANDING until DB branding lands. */
4
- const HARDCODED_BRANDING = {
5
- backgroundBase: "#F5F5F5",
6
- primary: "#3264FF",
7
- accent: "#9664FF",
8
- fontFamily: "Inter",
9
- };
10
-
11
- const FONT_ON_LIGHT = "#0a0a0a";
12
- const FONT_ON_DARK = "#f5f5f5";
13
- /** Channel delta toward white (elevated) / black (recessed). */
14
- const BACKGROUND_SURFACE_DELTA = 15;
15
-
16
- function parseHexRgb(
17
- background: string,
18
- ): { r: number; g: number; b: number } | null {
19
- const hex = background.trim().replace(/^#/, "");
20
- const full =
21
- hex.length === 3
22
- ? hex
23
- .split("")
24
- .map((c) => c + c)
25
- .join("")
26
- : hex;
27
- if (full.length !== 6 || Number.isNaN(Number.parseInt(full, 16))) {
28
- return null;
29
- }
30
- return {
31
- r: Number.parseInt(full.slice(0, 2), 16),
32
- g: Number.parseInt(full.slice(2, 4), 16),
33
- b: Number.parseInt(full.slice(4, 6), 16),
34
- };
35
- }
36
-
37
- function toHex({ r, g, b }: { r: number; g: number; b: number }): string {
38
- return `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
39
- }
40
-
41
- function deriveFontColor(background: string): string {
42
- const rgb = parseHexRgb(background);
43
- if (!rgb) return FONT_ON_LIGHT;
44
- const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
45
- return luminance > 0.5 ? FONT_ON_LIGHT : FONT_ON_DARK;
46
- }
47
-
48
- function deriveElevatedBackground(background: string): string {
49
- const rgb = parseHexRgb(background);
50
- if (!rgb) return background;
51
- return toHex({
52
- r: Math.min(255, rgb.r + BACKGROUND_SURFACE_DELTA),
53
- g: Math.min(255, rgb.g + BACKGROUND_SURFACE_DELTA),
54
- b: Math.min(255, rgb.b + BACKGROUND_SURFACE_DELTA),
55
- });
56
- }
57
-
58
- function deriveRecessedBackground(background: string): string {
59
- const rgb = parseHexRgb(background);
60
- if (!rgb) return background;
61
- return toHex({
62
- r: Math.max(0, rgb.r - BACKGROUND_SURFACE_DELTA),
63
- g: Math.max(0, rgb.g - BACKGROUND_SURFACE_DELTA),
64
- b: Math.max(0, rgb.b - BACKGROUND_SURFACE_DELTA),
65
- });
66
- }
67
-
68
- export default function generateGlobalsCss(_opts: ScaffoldOptions): string {
69
- const branding = HARDCODED_BRANDING;
70
- const fontFamilyParam = branding.fontFamily.replace(/ /g, "+");
71
- const foreground = deriveFontColor(branding.backgroundBase);
72
- const primaryForeground = deriveFontColor(branding.primary);
73
- const accentForeground = deriveFontColor(branding.accent);
74
- const backgroundElevated = deriveElevatedBackground(branding.backgroundBase);
75
- const backgroundRecessed = deriveRecessedBackground(branding.backgroundBase);
76
-
77
- return `@import url("https://fonts.googleapis.com/css2?family=${fontFamilyParam}:wght@400;700&display=swap");
78
- @import "tailwindcss";
79
- /* Tailwind v4 skips node_modules; @zorgo/next ships un-obfuscated components so
80
- their class names are scannable here. */
81
- @source "../node_modules/@zorgo/next/dist/components.mjs";
82
-
83
- :root {
84
- --background: ${branding.backgroundBase};
85
- --background-elevated: ${backgroundElevated};
86
- --background-recessed: ${backgroundRecessed};
87
- --foreground: ${foreground};
88
- --primary: ${branding.primary};
89
- --primary-foreground: ${primaryForeground};
90
- --accent: ${branding.accent};
91
- --accent-foreground: ${accentForeground};
92
- --font-sans: "${branding.fontFamily}", sans-serif;
93
- }
94
-
95
- @theme inline {
96
- --color-background: var(--background);
97
- --color-background-elevated: var(--background-elevated);
98
- --color-background-recessed: var(--background-recessed);
99
- --color-foreground: var(--foreground);
100
- --color-primary: var(--primary);
101
- --color-primary-foreground: var(--primary-foreground);
102
- --color-accent: var(--accent);
103
- --color-accent-foreground: var(--accent-foreground);
104
- --font-sans: var(--font-sans);
105
- }
106
-
107
- @layer base {
108
- button,
109
- [type="button"],
110
- [type="submit"],
111
- [type="reset"] {
112
- cursor: pointer;
113
- }
114
-
115
- button:disabled,
116
- [type="button"]:disabled,
117
- [type="submit"]:disabled,
118
- [type="reset"]:disabled {
119
- cursor: not-allowed;
120
- }
121
- }
122
- `;
123
- }
@@ -1,18 +0,0 @@
1
- "use client";
2
- import { useRouter } from "next/navigation";
3
- import { ItemCustomizationForm } from "@zorgo/next/components";
4
-
5
- export default function ItemCustomizationClient({
6
- baseItemId,
7
- }: {
8
- baseItemId: number;
9
- }) {
10
- const router = useRouter();
11
-
12
- return (
13
- <ItemCustomizationForm
14
- baseItemId={baseItemId}
15
- onSubmitSuccess={() => router.push("/menu")}
16
- />
17
- );
18
- }