bsmnt 0.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 (98) hide show
  1. package/.changeset/2026-02-11-test-patch-bump.md +5 -0
  2. package/.changeset/README.md +10 -0
  3. package/.changeset/config.json +16 -0
  4. package/.cursor/rules/README.md +184 -0
  5. package/.cursor/rules/architecture.mdc +437 -0
  6. package/.cursor/rules/components.mdc +436 -0
  7. package/.cursor/rules/integrations.mdc +447 -0
  8. package/.cursor/rules/main.mdc +278 -0
  9. package/.cursor/rules/styling.mdc +433 -0
  10. package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  11. package/.github/workflows/.gitkeep +0 -0
  12. package/.github/workflows/ci.yml +37 -0
  13. package/.github/workflows/release.yml +54 -0
  14. package/.tldr/cache/call_graph.json +7 -0
  15. package/.tldr/languages.json +6 -0
  16. package/.tldr/status +1 -0
  17. package/.tldrignore +84 -0
  18. package/.vscode/extensions.json +20 -0
  19. package/.vscode/settings.json +98 -0
  20. package/CHANGELOG.md +13 -0
  21. package/CLAUDE.md +138 -0
  22. package/README.md +176 -0
  23. package/bin/index.js +262 -0
  24. package/biome.json +44 -0
  25. package/bun.lock +496 -0
  26. package/changelog/04-02-26.md +86 -0
  27. package/changelog/05-02-26.md +101 -0
  28. package/changelog/09-02-26.md +83 -0
  29. package/docs/fix-studio-hydration.md +46 -0
  30. package/docs/plans/2026-01-29-sanity-smart-merge-design.md +196 -0
  31. package/docs/plans/2026-01-29-sanity-smart-merge-implementation.md +695 -0
  32. package/docs/sanity-setup-steps.md +199 -0
  33. package/integrations/basehub/README.md +3 -0
  34. package/integrations/sanity/app/api/draft-mode/disable/route.ts +7 -0
  35. package/integrations/sanity/app/api/draft-mode/enable/route.ts +21 -0
  36. package/integrations/sanity/app/api/revalidate/route.ts +37 -0
  37. package/integrations/sanity/app/layout.tsx +111 -0
  38. package/integrations/sanity/app/sitemap.ts +80 -0
  39. package/integrations/sanity/app/studio/[[...tool]]/page.tsx +8 -0
  40. package/integrations/sanity/app/studio/layout.tsx +7 -0
  41. package/integrations/sanity/components/ui/sanity-image/index.tsx +37 -0
  42. package/integrations/sanity/lib/integrations/README.md +58 -0
  43. package/integrations/sanity/lib/integrations/check-integration.ts +62 -0
  44. package/integrations/sanity/lib/integrations/sanity/README.md +144 -0
  45. package/integrations/sanity/lib/integrations/sanity/client.ts +30 -0
  46. package/integrations/sanity/lib/integrations/sanity/components/disable-draft-mode.tsx +29 -0
  47. package/integrations/sanity/lib/integrations/sanity/components/rich-text.tsx +73 -0
  48. package/integrations/sanity/lib/integrations/sanity/env.ts +38 -0
  49. package/integrations/sanity/lib/integrations/sanity/live/index.tsx +34 -0
  50. package/integrations/sanity/lib/integrations/sanity/queries.ts +99 -0
  51. package/integrations/sanity/lib/integrations/sanity/sanity.cli.ts +20 -0
  52. package/integrations/sanity/lib/integrations/sanity/sanity.config.ts +94 -0
  53. package/integrations/sanity/lib/integrations/sanity/sanity.types.ts +337 -0
  54. package/integrations/sanity/lib/integrations/sanity/schema.json +1850 -0
  55. package/integrations/sanity/lib/integrations/sanity/schemas/article.ts +132 -0
  56. package/integrations/sanity/lib/integrations/sanity/schemas/example.ts +203 -0
  57. package/integrations/sanity/lib/integrations/sanity/schemas/index.ts +37 -0
  58. package/integrations/sanity/lib/integrations/sanity/schemas/link.ts +127 -0
  59. package/integrations/sanity/lib/integrations/sanity/schemas/metadata.ts +68 -0
  60. package/integrations/sanity/lib/integrations/sanity/schemas/navigation.ts +39 -0
  61. package/integrations/sanity/lib/integrations/sanity/schemas/page.ts +77 -0
  62. package/integrations/sanity/lib/integrations/sanity/schemas/richText.ts +59 -0
  63. package/integrations/sanity/lib/integrations/sanity/structure.ts +5 -0
  64. package/integrations/sanity/lib/integrations/sanity/utils/image.ts +11 -0
  65. package/integrations/sanity/lib/integrations/sanity/utils/link.ts +61 -0
  66. package/integrations/sanity/lib/scripts/copy-sanity-mcp.ts +23 -0
  67. package/integrations/sanity/lib/scripts/generate-page.ts +310 -0
  68. package/integrations/sanity/lib/utils/metadata.ts +190 -0
  69. package/layers/experiment/components/layout/header/index.tsx +58 -0
  70. package/layers/experiment/components/layout/navigation-menu.tsx +127 -0
  71. package/layers/experiment/lib/constants.ts +12 -0
  72. package/layers/webgl/app/page.tsx +10 -0
  73. package/layers/webgl/components/webgl/canvas/dynamic.tsx +34 -0
  74. package/layers/webgl/components/webgl/canvas/index.tsx +43 -0
  75. package/layers/webgl/components/webgl/components/scene/index.tsx +21 -0
  76. package/layers/webgpu/.gitkeep +0 -0
  77. package/package.json +44 -0
  78. package/plugins/README.md +21 -0
  79. package/plugins/no-anchor-element.grit +11 -0
  80. package/plugins/no-relative-parent-imports.grit +6 -0
  81. package/plugins/no-unnecessary-forwardref.grit +5 -0
  82. package/src/commands/add-integration.js +325 -0
  83. package/src/commands/create.js +415 -0
  84. package/src/commands/setup-sanity.js +426 -0
  85. package/src/commands/worktree.js +805 -0
  86. package/src/mergers/check-integration-merger.js +105 -0
  87. package/src/mergers/config.js +137 -0
  88. package/src/mergers/index.js +355 -0
  89. package/src/mergers/layout-merger.js +223 -0
  90. package/src/mergers/next-config-merger.js +63 -0
  91. package/src/mergers/sitemap-merger.js +121 -0
  92. package/tasks/prd-next-starter-dynamic-layers.md +184 -0
  93. package/tasks/prd.json +153 -0
  94. package/tasks/progress.txt +115 -0
  95. package/template-hooks/use-battery.ts +126 -0
  96. package/template-hooks/use-device-perf.ts +184 -0
  97. package/template-hooks/use-intersection-observer.ts +32 -0
  98. package/template-hooks/use-media.ts +33 -0
@@ -0,0 +1,199 @@
1
+ # Sanity Project Setup — Automated Steps
2
+
3
+ > Reproducible steps for programmatically creating a Sanity project, dataset, API token, and `.env.local` configuration. No browser or interactive prompts required.
4
+
5
+ ---
6
+
7
+ ## Prerequisites
8
+
9
+ - `@sanity/cli` available via `npx` (comes with `sanity` package)
10
+ - User must be logged in to Sanity CLI (`npx sanity login`)
11
+ - Project already has `next-sanity`, `@sanity/client`, `sanity` installed
12
+
13
+ ---
14
+
15
+ ## Step 1: Verify Sanity CLI is available
16
+
17
+ ```bash
18
+ npx sanity --version
19
+ ```
20
+
21
+ If this fails, the sanity packages aren't installed. Run:
22
+
23
+ ```bash
24
+ bun add next-sanity @sanity/client @sanity/image-url sanity @sanity/vision
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Step 2: Verify user is authenticated
30
+
31
+ ```bash
32
+ npx sanity projects list
33
+ ```
34
+
35
+ - If this returns a list of projects, the user is logged in.
36
+ - If it fails with an auth error, the user needs to run `npx sanity login` first (this is interactive and opens a browser — cannot be automated).
37
+
38
+ ---
39
+
40
+ ## Step 3: Create the Sanity project
41
+
42
+ ```bash
43
+ npx sanity projects create --name "<project-name>" --dataset production
44
+ ```
45
+
46
+ **Output to parse:**
47
+
48
+ ```
49
+ Project created successfully!
50
+ ID: <project-id>
51
+ Name: <project-name>
52
+ Organization: <org>
53
+ Dataset: production (public)
54
+ ```
55
+
56
+ **Extract the project ID** from the output. It's the alphanumeric string after `ID: ` (e.g., `1poovu2i`).
57
+
58
+ ---
59
+
60
+ ## Step 4: Get the user's auth token
61
+
62
+ The Sanity CLI stores the user's session token locally. Retrieve it with:
63
+
64
+ ```bash
65
+ npx sanity debug --secrets 2>&1 | grep "Auth token"
66
+ ```
67
+
68
+ **Output:**
69
+
70
+ ```
71
+ Auth token: '<token-string>'
72
+ ```
73
+
74
+ **Parse the token** from between the single quotes. Note: the output includes ANSI color codes (`[32m` and `[39m`) that need to be stripped.
75
+
76
+ ---
77
+
78
+ ## Step 5: Create an API read token
79
+
80
+ Use the Sanity HTTP API to create a viewer token for the project:
81
+
82
+ ```bash
83
+ curl -s -X POST "https://api.sanity.io/v2021-06-07/projects/<project-id>/tokens" \
84
+ -H "Authorization: Bearer <auth-token-from-step-4>" \
85
+ -H "Content-Type: application/json" \
86
+ -d '{"label": "Next.js Read Token", "roleName": "viewer"}'
87
+ ```
88
+
89
+ **Response:**
90
+
91
+ ```json
92
+ {
93
+ "id": "si8WgRxQuts4CN",
94
+ "key": "<the-api-read-token>",
95
+ "roles": [{ "name": "viewer", "title": "Viewer" }],
96
+ "label": "Next.js Read Token"
97
+ }
98
+ ```
99
+
100
+ **Extract the `key` field** — this is the `SANITY_API_READ_TOKEN`.
101
+
102
+ ---
103
+
104
+ ## Step 6: Write `.env.local`
105
+
106
+ Create `.env.local` in the project root with:
107
+
108
+ ```env
109
+ # Sanity CMS
110
+ NEXT_PUBLIC_SANITY_PROJECT_ID=<project-id>
111
+ NEXT_PUBLIC_SANITY_DATASET=production
112
+ SANITY_API_READ_TOKEN=<api-read-token>
113
+ NEXT_PUBLIC_SANITY_API_READ_TOKEN=<api-read-token>
114
+ ```
115
+
116
+ **Why both `SANITY_API_READ_TOKEN` and `NEXT_PUBLIC_SANITY_API_READ_TOKEN`?**
117
+
118
+ The next-starter template's `env.ts` reads from `NEXT_PUBLIC_SANITY_API_READ_TOKEN` (exposed to client for draft mode / visual editing). `SANITY_API_READ_TOKEN` is the conventional server-only name. Setting both ensures compatibility regardless of which convention the project follows.
119
+
120
+ ---
121
+
122
+ ## Step 7: Verify the connection
123
+
124
+ Test that the token and project ID work by querying the API:
125
+
126
+ ```bash
127
+ curl -s "https://<project-id>.api.sanity.io/v2024-03-15/data/query/production?query=*%5B0%5D" \
128
+ -H "Authorization: Bearer <api-read-token>"
129
+ ```
130
+
131
+ **Success response:**
132
+
133
+ ```json
134
+ {
135
+ "query": "*[0]",
136
+ "result": { ... },
137
+ "ms": 2
138
+ }
139
+ ```
140
+
141
+ If the response contains `"result"` (even if it's `null` for an empty dataset), the setup is correct.
142
+
143
+ ---
144
+
145
+ ## Step 8: Ensure `.env.local` is gitignored
146
+
147
+ Check if `.gitignore` exists and includes `.env.local`. If not, add it:
148
+
149
+ ```bash
150
+ # Check
151
+ grep -q ".env.local" .gitignore 2>/dev/null
152
+
153
+ # If missing or .gitignore doesn't exist, ensure it's added
154
+ echo ".env.local" >> .gitignore
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Summary of values produced
160
+
161
+ | Variable | Source |
162
+ |----------|--------|
163
+ | `NEXT_PUBLIC_SANITY_PROJECT_ID` | Step 3 — `ID` field from project creation output |
164
+ | `NEXT_PUBLIC_SANITY_DATASET` | Hardcoded to `production` (passed in Step 3) |
165
+ | `SANITY_API_READ_TOKEN` | Step 5 — `key` field from token creation response |
166
+ | `NEXT_PUBLIC_SANITY_API_READ_TOKEN` | Same as above |
167
+
168
+ ---
169
+
170
+ ## Error handling notes
171
+
172
+ | Step | Possible failure | How to handle |
173
+ |------|-----------------|---------------|
174
+ | 2 | Not logged in | Prompt user to run `npx sanity login` — requires browser |
175
+ | 3 | Org selection needed | Add `--organization <org-id>` flag if user has multiple orgs |
176
+ | 4 | Token not found | Auth token location varies by OS. Fallback: read `~/.config/sanity/config.json` |
177
+ | 5 | 401/403 from API | Auth token expired or invalid — user needs to `npx sanity login` again |
178
+ | 5 | 400 bad request | Check project ID is correct, check auth header format |
179
+ | 7 | Connection refused | Project ID or dataset name is wrong |
180
+
181
+ ---
182
+
183
+ ## CLI integration notes
184
+
185
+ For a `basement init` command, the flow would be:
186
+
187
+ ```
188
+ 1. Check: `npx sanity projects list` → confirms auth
189
+ 2. Prompt: "Project name?" (default: directory name)
190
+ 3. Run: `npx sanity projects create --name <name> --dataset production`
191
+ 4. Parse: project ID from stdout
192
+ 5. Get: auth token from `npx sanity debug --secrets`
193
+ 6. Create: API token via HTTP POST
194
+ 7. Write: `.env.local` with all four variables
195
+ 8. Verify: curl test query
196
+ 9. Print: "Sanity Studio available at /studio"
197
+ ```
198
+
199
+ No interactive Sanity prompts needed. The only prerequisite is that `npx sanity login` has been run at least once.
@@ -0,0 +1,3 @@
1
+ # BaseHub Integration
2
+
3
+ Coming soon...
@@ -0,0 +1,7 @@
1
+ import { draftMode } from "next/headers";
2
+ import { type NextRequest, NextResponse } from "next/server";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ (await draftMode()).disable();
6
+ return NextResponse.redirect(new URL("/", request.url));
7
+ }
@@ -0,0 +1,21 @@
1
+ import { NextResponse } from "next/server";
2
+ import { defineEnableDraftMode } from "next-sanity/draft-mode";
3
+ import { isSanityConfigured } from "@/lib/integrations/check-integration";
4
+ import { client } from "@/lib/integrations/sanity/client";
5
+ import { privateToken } from "@/lib/integrations/sanity/env";
6
+
7
+ // Only enable draft mode if Sanity is configured
8
+ const draftModeHandler =
9
+ isSanityConfigured() && client
10
+ ? defineEnableDraftMode({
11
+ client: client.withConfig({ token: privateToken }),
12
+ })
13
+ : {
14
+ GET: () =>
15
+ NextResponse.json(
16
+ { error: "Sanity is not configured" },
17
+ { status: 503 },
18
+ ),
19
+ };
20
+
21
+ export const { GET } = draftModeHandler;
@@ -0,0 +1,37 @@
1
+ import { revalidateTag } from "next/cache";
2
+ import { type NextRequest, NextResponse } from "next/server";
3
+ import { parseBody } from "next-sanity/webhook";
4
+
5
+ export async function POST(request: NextRequest) {
6
+ try {
7
+ const { body, isValidSignature } = await parseBody<{
8
+ _type: string;
9
+ slug?: { current: string };
10
+ }>(request, process.env.SANITY_REVALIDATE_SECRET);
11
+
12
+ if (!isValidSignature) {
13
+ return new Response("Invalid signature", { status: 401 });
14
+ }
15
+
16
+ if (!body?._type) {
17
+ return new Response("Bad Request", { status: 400 });
18
+ }
19
+
20
+ // Revalidate the specific document type
21
+ revalidateTag(body._type);
22
+
23
+ // If there's a slug, revalidate the specific page
24
+ if (body.slug?.current) {
25
+ revalidateTag(`${body._type}:${body.slug.current}`);
26
+ }
27
+
28
+ return NextResponse.json({
29
+ status: 200,
30
+ revalidated: true,
31
+ now: Date.now(),
32
+ });
33
+ } catch (error) {
34
+ console.error("Revalidation error:", error);
35
+ return new Response("Internal Server Error", { status: 500 });
36
+ }
37
+ }
@@ -0,0 +1,111 @@
1
+ import type { Metadata, Viewport } from "next";
2
+ import { Geist } from "next/font/google";
3
+ import { type PropsWithChildren, Suspense } from "react";
4
+ import { SanityStudioGuard } from "@/components/sanity/studio-guard";
5
+ import { SanityVisualEditing } from "@/components/sanity/visual-editing";
6
+ import { Link } from "@/components/ui/link";
7
+ import { themes } from "@/lib/styles/colors";
8
+ import { fontsVariable } from "@/lib/styles/fonts";
9
+ import AppData from "@/package.json";
10
+ import "@/lib/styles/css/index.css";
11
+ import { cn } from "@/lib/styles/cn";
12
+
13
+ const APP_NAME = AppData.name;
14
+ const APP_DEFAULT_TITLE = "Basement Starter";
15
+ const APP_TITLE_TEMPLATE = "%s - Basement Starter";
16
+ const APP_DESCRIPTION = AppData.description;
17
+ const APP_BASE_URL =
18
+ process.env.NEXT_PUBLIC_BASE_URL ?? "https://localhost:3000";
19
+
20
+ const geist = Geist({
21
+ subsets: ["latin"],
22
+ });
23
+
24
+ export const metadata: Metadata = {
25
+ alternates: {
26
+ canonical: "/",
27
+ languages: {
28
+ "en-US": "/en-US",
29
+ },
30
+ },
31
+ appleWebApp: {
32
+ capable: true,
33
+ statusBarStyle: "default",
34
+ title: APP_DEFAULT_TITLE,
35
+ },
36
+ applicationName: APP_NAME,
37
+ authors: [{ name: "basement.studio", url: "https://basement.studio" }],
38
+ description: APP_DESCRIPTION,
39
+ formatDetection: { telephone: false },
40
+ metadataBase: new URL(APP_BASE_URL),
41
+ openGraph: {
42
+ description: APP_DESCRIPTION,
43
+ images: [
44
+ {
45
+ alt: APP_DEFAULT_TITLE,
46
+ height: 630,
47
+ url: "/opengraph-image.jpg",
48
+ width: 1200,
49
+ },
50
+ ],
51
+ locale: "en_US",
52
+ siteName: APP_NAME,
53
+ title: {
54
+ default: APP_DEFAULT_TITLE,
55
+ template: APP_TITLE_TEMPLATE,
56
+ },
57
+ type: "website",
58
+ url: APP_BASE_URL,
59
+ },
60
+ other: {
61
+ "fb:app_id": process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || "",
62
+ },
63
+ title: {
64
+ default: APP_DEFAULT_TITLE,
65
+ template: APP_TITLE_TEMPLATE,
66
+ },
67
+ twitter: {
68
+ card: "summary_large_image",
69
+ description: APP_DESCRIPTION,
70
+ title: {
71
+ default: APP_DEFAULT_TITLE,
72
+ template: APP_TITLE_TEMPLATE,
73
+ },
74
+ },
75
+ };
76
+
77
+ export const viewport: Viewport = {
78
+ colorScheme: "normal",
79
+ themeColor: themes.dark.primary,
80
+ };
81
+
82
+ export default function Layout({ children }: PropsWithChildren) {
83
+ return (
84
+ <html
85
+ lang="en"
86
+ dir="ltr"
87
+ className={cn(fontsVariable, geist.className)}
88
+ suppressHydrationWarning
89
+ >
90
+ <body>
91
+ <Suspense fallback={null}>
92
+ <SanityStudioGuard>
93
+ {/* Skip link for keyboard navigation accessibility */}
94
+ <Suspense fallback={null}>
95
+ <Link
96
+ href="#main-content"
97
+ className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-9999 focus:rounded focus:bg-black focus:px-4 focus:py-2 focus:text-white focus:outline-none focus:ring-2 focus:ring-white"
98
+ >
99
+ Skip to main content
100
+ </Link>
101
+ </Suspense>
102
+ </SanityStudioGuard>
103
+ </Suspense>
104
+
105
+ {children}
106
+
107
+ <SanityVisualEditing />
108
+ </body>
109
+ </html>
110
+ );
111
+ }
@@ -0,0 +1,80 @@
1
+ import type { MetadataRoute } from "next";
2
+ import { isSanityConfigured } from "@/lib/integrations/check-integration";
3
+
4
+ const APP_BASE_URL =
5
+ process.env.NEXT_PUBLIC_BASE_URL ?? "https://localhost:3000";
6
+
7
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
8
+ const baseRoutes: MetadataRoute.Sitemap = [
9
+ {
10
+ url: APP_BASE_URL,
11
+ lastModified: new Date(),
12
+ changeFrequency: "daily",
13
+ priority: 1,
14
+ },
15
+ ];
16
+
17
+ // Only fetch Sanity pages if Sanity is configured
18
+ if (isSanityConfigured()) {
19
+ try {
20
+ const sanityModule = await import("@/lib/integrations/sanity/client");
21
+ const sanityGroq = await import("next-sanity");
22
+
23
+ const client = sanityModule?.client;
24
+ const groq = sanityGroq?.groq;
25
+
26
+ // Skip if client is null (shouldn't happen since we check isSanityConfigured)
27
+ if (!(client && groq)) return baseRoutes;
28
+
29
+ type SanityDocument = {
30
+ slug: { current: string };
31
+ _updatedAt: string;
32
+ metadata?: { noIndex?: boolean };
33
+ };
34
+
35
+ // Fetch all published pages and articles
36
+ const pages = (await client.fetch(
37
+ groq`*[_type == "page" && defined(slug.current)] {
38
+ slug,
39
+ _updatedAt,
40
+ metadata
41
+ }`,
42
+ )) as SanityDocument[];
43
+
44
+ const articles = (await client.fetch(
45
+ groq`*[_type == "article" && defined(slug.current)] {
46
+ slug,
47
+ _updatedAt,
48
+ metadata
49
+ }`,
50
+ )) as SanityDocument[];
51
+
52
+ // Add pages to sitemap (exclude noIndex pages)
53
+ const pageEntries: MetadataRoute.Sitemap = pages
54
+ .filter((page: SanityDocument) => !page.metadata?.noIndex)
55
+ .map((page: SanityDocument) => ({
56
+ url: `${APP_BASE_URL}/${page.slug.current}`,
57
+ lastModified: new Date(page._updatedAt),
58
+ changeFrequency: "weekly" as const,
59
+ priority: 0.8,
60
+ }));
61
+
62
+ // Add articles to sitemap (exclude noIndex articles)
63
+ const articleEntries: MetadataRoute.Sitemap = articles
64
+ .filter((article: SanityDocument) => !article.metadata?.noIndex)
65
+ .map((article: SanityDocument) => ({
66
+ url: `${APP_BASE_URL}/blog/${article.slug.current}`,
67
+ lastModified: new Date(article._updatedAt),
68
+ changeFrequency: "weekly" as const,
69
+ priority: 0.7,
70
+ }));
71
+
72
+ return [...baseRoutes, ...pageEntries, ...articleEntries];
73
+ } catch (error) {
74
+ console.error("Error generating sitemap from Sanity:", error);
75
+ return baseRoutes;
76
+ }
77
+ }
78
+
79
+ return baseRoutes;
80
+ }
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ import { NextStudio } from "next-sanity/studio";
4
+ import config from "@/lib/integrations/sanity/sanity.config";
5
+
6
+ export default function StudioPage() {
7
+ return <NextStudio config={config} />;
8
+ }
@@ -0,0 +1,7 @@
1
+ export default function StudioLayout({
2
+ children,
3
+ }: {
4
+ children: React.ReactNode;
5
+ }) {
6
+ return children;
7
+ }
@@ -0,0 +1,37 @@
1
+ import { getImageDimensions } from "@sanity/asset-utils";
2
+ import { Image, type ImageProps } from "@/components/ui/image";
3
+ import { urlForImage } from "@/lib/integrations/sanity/utils/image";
4
+
5
+ interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
6
+ image: {
7
+ asset: {
8
+ _ref: string;
9
+ _type: "reference";
10
+ };
11
+ alt?: string;
12
+ hotspot?: object;
13
+ crop?: object;
14
+ };
15
+ maxWidth?: number;
16
+ }
17
+
18
+ export function SanityImage({
19
+ image,
20
+ maxWidth = 1920,
21
+ alt,
22
+ ...props
23
+ }: SanityImageProps) {
24
+ if (!image?.asset) return null;
25
+
26
+ const { width, height } = getImageDimensions(image.asset);
27
+ const aspectRatio = width / height;
28
+
29
+ return (
30
+ <Image
31
+ src={urlForImage(image).width(maxWidth).url()}
32
+ alt={alt || image.alt || ""}
33
+ aspectRatio={aspectRatio}
34
+ {...props}
35
+ />
36
+ );
37
+ }
@@ -0,0 +1,58 @@
1
+ # Integrations
2
+
3
+ Third-party service integrations. All are optional—remove unused ones with `bun run setup:project`.
4
+
5
+ ## Available Integrations
6
+
7
+ | Integration | Purpose | Documentation |
8
+ |-------------|---------|---------------|
9
+ | [Sanity](sanity/README.md) | Headless CMS | Visual editing, content management |
10
+ | [Shopify](shopify/README.md) | E-commerce | Cart, products, checkout |
11
+
12
+ ## Environment Variables
13
+
14
+ ```env
15
+ # Sanity CMS
16
+ NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
17
+ NEXT_PUBLIC_SANITY_DATASET="production"
18
+ NEXT_PUBLIC_SANITY_STUDIO_URL="http://localhost:3000/studio"
19
+ SANITY_API_WRITE_TOKEN="your-write-token"
20
+
21
+ # Shopify
22
+ SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"
23
+ SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-token"
24
+ SHOPIFY_REVALIDATION_SECRET="your-secret"
25
+
26
+ # Analytics
27
+ NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=GTM-XXXXXX
28
+ NEXT_PUBLIC_GOOGLE_ANALYTICS=G-XXXXXXXXXX
29
+ ```
30
+
31
+ ## Quick Usage
32
+
33
+ ```tsx
34
+ // Sanity
35
+ import { sanityFetch } from '@/integrations/sanity/live'
36
+ import { RichText } from '@/integrations/sanity/components/rich-text'
37
+ const { data } = await sanityFetch({ query: pageQuery })
38
+
39
+ // Shopify
40
+ import { Cart, AddToCart } from '@/lib/integrations/shopify/cart'
41
+ <Cart><AddToCart product={product} /></Cart>
42
+
43
+ ```
44
+
45
+ ## Removing Integrations
46
+
47
+ Run `bun run setup:project` for interactive removal, or manually:
48
+
49
+ ```bash
50
+ # Sanity (~150-200KB savings)
51
+ rm -rf lib/integrations/sanity app/studio app/(examples)/sanity
52
+ bun remove @sanity/asset-utils @sanity/image-url next-sanity sanity
53
+
54
+ # Shopify (~50-80KB)
55
+ rm -rf lib/integrations/shopify app/(examples)/shopify
56
+ ```
57
+
58
+ After removal: `bun lint:fix && bun build`
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Integration Configuration Checker
3
+ *
4
+ * Utilities to check if integrations are configured via environment variables.
5
+ * This helps with tree-shaking unused integrations from the bundle.
6
+ *
7
+ * Usage:
8
+ * ```ts
9
+ * import { isSanityConfigured } from '@/integrations/check-integration'
10
+ *
11
+ * if (isSanityConfigured()) {
12
+ * // Only import and use Sanity code if configured
13
+ * const { sanityFetch } = await import('next-sanity/live')
14
+ * }
15
+ * ```
16
+ */
17
+
18
+ /**
19
+ * Check if Sanity CMS is configured
20
+ * Requires: NEXT_PUBLIC_SANITY_PROJECT_ID and NEXT_PUBLIC_SANITY_DATASET
21
+ */
22
+ export function isSanityConfigured(): boolean {
23
+ return Boolean(
24
+ process.env.NEXT_PUBLIC_SANITY_PROJECT_ID &&
25
+ process.env.NEXT_PUBLIC_SANITY_DATASET,
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Check if Google Analytics is configured
31
+ * Requires: NEXT_PUBLIC_GOOGLE_ANALYTICS or NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID
32
+ */
33
+ export function isAnalyticsConfigured(): boolean {
34
+ return Boolean(
35
+ process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS ||
36
+ process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID,
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Get a list of all configured integrations
42
+ */
43
+ export function getConfiguredIntegrations(): string[] {
44
+ const integrations: string[] = [];
45
+
46
+ if (isSanityConfigured()) integrations.push("Sanity");
47
+ if (isAnalyticsConfigured()) integrations.push("Analytics");
48
+
49
+ return integrations;
50
+ }
51
+
52
+ /**
53
+ * Get a list of all unconfigured integrations
54
+ */
55
+ export function getUnconfiguredIntegrations(): string[] {
56
+ const integrations: string[] = [];
57
+
58
+ if (!isSanityConfigured()) integrations.push("Sanity");
59
+ if (!isAnalyticsConfigured()) integrations.push("Analytics");
60
+
61
+ return integrations;
62
+ }