bsmnt 0.7.0 → 0.9.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 (106) hide show
  1. package/dist/application/add-hooks/index.d.ts.map +1 -1
  2. package/dist/application/add-hooks/index.js +1 -0
  3. package/dist/application/add-hooks/index.js.map +1 -1
  4. package/dist/application/add-integration/index.d.ts.map +1 -1
  5. package/dist/application/add-integration/index.js +1 -0
  6. package/dist/application/add-integration/index.js.map +1 -1
  7. package/dist/core/create/execute-plan.d.ts +2 -0
  8. package/dist/core/create/execute-plan.d.ts.map +1 -1
  9. package/dist/core/create/execute-plan.js +3 -0
  10. package/dist/core/create/execute-plan.js.map +1 -1
  11. package/dist/domain/version.d.ts +14 -0
  12. package/dist/domain/version.d.ts.map +1 -0
  13. package/dist/domain/version.js +81 -0
  14. package/dist/domain/version.js.map +1 -0
  15. package/dist/index.js +3 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/infrastructure/create/executor.d.ts.map +1 -1
  18. package/dist/infrastructure/create/executor.js +2 -0
  19. package/dist/infrastructure/create/executor.js.map +1 -1
  20. package/dist/infrastructure/create/render-generated-docs.d.ts +18 -0
  21. package/dist/infrastructure/create/render-generated-docs.d.ts.map +1 -0
  22. package/dist/infrastructure/create/render-generated-docs.js +72 -0
  23. package/dist/infrastructure/create/render-generated-docs.js.map +1 -0
  24. package/dist/infrastructure/create/setup-dotenvx.d.ts.map +1 -1
  25. package/dist/infrastructure/create/setup-dotenvx.js +10 -4
  26. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
  27. package/dist/infrastructure/create/setup-remote.d.ts +6 -1
  28. package/dist/infrastructure/create/setup-remote.d.ts.map +1 -1
  29. package/dist/infrastructure/create/setup-remote.js +7 -2
  30. package/dist/infrastructure/create/setup-remote.js.map +1 -1
  31. package/dist/infrastructure/update/cache.d.ts +14 -0
  32. package/dist/infrastructure/update/cache.d.ts.map +1 -0
  33. package/dist/infrastructure/update/cache.js +41 -0
  34. package/dist/infrastructure/update/cache.js.map +1 -0
  35. package/dist/infrastructure/update/check-version.d.ts +2 -0
  36. package/dist/infrastructure/update/check-version.d.ts.map +1 -0
  37. package/dist/infrastructure/update/check-version.js +33 -0
  38. package/dist/infrastructure/update/check-version.js.map +1 -0
  39. package/dist/infrastructure/update/notifier.d.ts +8 -0
  40. package/dist/infrastructure/update/notifier.d.ts.map +1 -0
  41. package/dist/infrastructure/update/notifier.js +141 -0
  42. package/dist/infrastructure/update/notifier.js.map +1 -0
  43. package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
  44. package/dist/modules/features/cms/sanity-pagebuilder/config.js +2 -0
  45. package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
  46. package/package.json +2 -1
  47. package/src/agent-skills/manage-env/SKILL.md +5 -3
  48. package/src/modules/features/cms/sanity/files/app/blog/[slug]/page.tsx +10 -9
  49. package/src/modules/features/cms/sanity/files/app/blog/page.tsx +5 -5
  50. package/src/modules/features/cms/sanity/files/lib/scripts/copy-sanity-mcp.ts +6 -6
  51. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/block-actions-item.tsx +81 -0
  52. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -0
  53. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +7 -0
  54. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +4 -6
  55. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +72 -21
  56. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  57. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +178 -0
  58. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +9 -1
  59. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +5 -4
  60. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +54 -1
  61. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -0
  62. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +11 -0
  63. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +4 -0
  64. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +20 -0
  65. package/src/modules/features/cms/sanity-pagebuilder/files/lib/scripts/sanity-typegen.ts +1 -1
  66. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/json-ld.tsx +1 -0
  67. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/metadata.ts +27 -5
  68. package/src/modules/features/env/dotenvx/files/decrypt-env.mjs +12 -12
  69. package/src/modules/features/env/dotenvx/files/push-keys.mjs +3 -3
  70. package/src/modules/features/env/dotenvx/files/save-key.mjs +3 -3
  71. package/src/modules/features/env/dotenvx/files/setup-remote.ts +3 -1
  72. package/src/modules/features/env/dotenvx/files/write-env.mjs +6 -6
  73. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/hubspot-form.css +2 -0
  74. package/src/templates/next-default/.env.example +8 -0
  75. package/src/templates/next-default/.github/workflows/ci.yml +7 -0
  76. package/src/templates/next-default/README.md +14 -0
  77. package/src/templates/next-default/biome.json +1 -1
  78. package/src/templates/next-default/components/basement.svg +1 -1
  79. package/src/templates/next-default/knip.json +8 -6
  80. package/src/templates/next-default/lefthook.yml +0 -5
  81. package/src/templates/next-experiments/.env.example +8 -0
  82. package/src/templates/next-experiments/.github/workflows/ci.yml +7 -0
  83. package/src/templates/next-experiments/README.md +14 -0
  84. package/src/templates/next-experiments/biome.json +1 -1
  85. package/src/templates/next-experiments/components/basement.svg +1 -1
  86. package/src/templates/next-experiments/knip.json +8 -6
  87. package/src/templates/next-experiments/lefthook.yml +0 -5
  88. package/src/templates/next-pagebuilder/.env.example +8 -0
  89. package/src/templates/next-pagebuilder/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  90. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +15 -0
  91. package/src/templates/next-pagebuilder/README.md +14 -0
  92. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +2 -2
  93. package/src/templates/next-pagebuilder/app/actions/unlock-gated-content.ts +39 -0
  94. package/src/templates/next-pagebuilder/biome.json +1 -1
  95. package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +169 -0
  96. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +3 -0
  97. package/src/templates/next-pagebuilder/knip.json +8 -6
  98. package/src/templates/next-pagebuilder/lefthook.yml +0 -5
  99. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +81 -0
  100. package/src/templates/next-webgl/.env.example +8 -0
  101. package/src/templates/next-webgl/.github/workflows/ci.yml +7 -0
  102. package/src/templates/next-webgl/README.md +14 -0
  103. package/src/templates/next-webgl/biome.json +1 -1
  104. package/src/templates/next-webgl/components/basement.svg +1 -1
  105. package/src/templates/next-webgl/knip.json +8 -6
  106. package/src/templates/next-webgl/lefthook.yml +0 -5
@@ -0,0 +1,39 @@
1
+ "use server"
2
+
3
+ import { fetchSanity } from "@/lib/integrations/sanity/live"
4
+ import { GATED_CONTENT_QUERY } from "@/lib/integrations/sanity/queries"
5
+ import type { GATED_CONTENT_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
6
+
7
+ export type GatedContentPayload = NonNullable<GATED_CONTENT_QUERY_RESULT>
8
+
9
+ export type UnlockResult =
10
+ | { ok: true; payload: GatedContentPayload }
11
+ | { ok: false; error: string }
12
+
13
+ type UnlockInput = { documentId: string; email: string }
14
+
15
+ export async function unlockGatedContent({
16
+ documentId,
17
+ email,
18
+ }: UnlockInput): Promise<UnlockResult> {
19
+ if (!documentId) return { ok: false, error: "Missing content id." }
20
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
21
+ return { ok: false, error: "Enter a valid email address." }
22
+ }
23
+
24
+ // Lead-capture seam: forward `email` to your CRM / email provider here.
25
+ // This is not access control — the payload is withheld from the page until
26
+ // this action runs, but anyone who submits the form receives it.
27
+
28
+ const { data } = await fetchSanity<GATED_CONTENT_QUERY_RESULT>({
29
+ query: GATED_CONTENT_QUERY,
30
+ params: { id: documentId },
31
+ // stega must stay off: it injects invisible characters into strings, which
32
+ // would corrupt the asset URLs returned for download/playback.
33
+ stega: false,
34
+ })
35
+
36
+ if (!data) return { ok: false, error: "Content not found." }
37
+
38
+ return { ok: true, payload: data }
39
+ }
@@ -82,7 +82,7 @@
82
82
  "complexity": {
83
83
  "noForEach": "off",
84
84
  "useFlatMap": "warn",
85
- "useSimplifiedLogicExpression": "warn"
85
+ "useSimplifiedLogicExpression": "off"
86
86
  },
87
87
  "correctness": {
88
88
  "noInvalidUseBeforeDeclaration": "error",
@@ -0,0 +1,169 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import {
5
+ type GatedContentPayload,
6
+ unlockGatedContent,
7
+ } from "@/app/actions/unlock-gated-content"
8
+ import type { PageBuilderBlock } from "@/components/page-builder/types"
9
+ import { RichText } from "@/components/sanity/rich-text"
10
+ import { Link } from "@/components/ui/link"
11
+
12
+ type GatedContentBlock = Extract<PageBuilderBlock, { _type: "gatedContent" }>
13
+
14
+ type GatedContentProps = {
15
+ block: GatedContentBlock
16
+ }
17
+
18
+ type GateState = "locked" | "loading" | "unlocked"
19
+
20
+ const STORAGE_PREFIX = "gated-content:"
21
+
22
+ export function GatedContent({ block }: GatedContentProps) {
23
+ const storageKey = `${STORAGE_PREFIX}${block._id}`
24
+ const [state, setState] = useState<GateState>("locked")
25
+ const [payload, setPayload] = useState<GatedContentPayload | null>(null)
26
+ const [error, setError] = useState<string | null>(null)
27
+ const [justUnlocked, setJustUnlocked] = useState(false)
28
+
29
+ // The payload is withheld from the page query, so it never reaches the client
30
+ // until this action runs. We remember the submitted email so a returning
31
+ // visitor re-fetches without seeing the form again.
32
+ const unlock = async (email: string, persist: boolean) => {
33
+ setState("loading")
34
+ setError(null)
35
+
36
+ const result = await unlockGatedContent({ documentId: block._id, email })
37
+
38
+ if (!result.ok) {
39
+ setError(result.error)
40
+ setState("locked")
41
+ if (!persist) window.localStorage.removeItem(storageKey)
42
+ return
43
+ }
44
+
45
+ setPayload(result.payload)
46
+ setState("unlocked")
47
+ if (persist) {
48
+ window.localStorage.setItem(storageKey, email)
49
+ setJustUnlocked(true)
50
+ }
51
+ }
52
+
53
+ // biome-ignore lint/correctness/useExhaustiveDependencies: re-fetch only when the block changes; unlock closes over stable values
54
+ useEffect(() => {
55
+ const savedEmail = window.localStorage.getItem(storageKey)
56
+ if (savedEmail) void unlock(savedEmail, false)
57
+ }, [storageKey])
58
+
59
+ const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
60
+ event.preventDefault()
61
+ const email =
62
+ new FormData(event.currentTarget).get("email")?.toString() ?? ""
63
+ void unlock(email, true)
64
+ }
65
+
66
+ return (
67
+ <section className="py-8 md:py-12">
68
+ {block.teaserHeading ? (
69
+ <h2 className="mb-4 font-semibold text-2xl tracking-tight md:text-4xl">
70
+ {block.teaserHeading}
71
+ </h2>
72
+ ) : null}
73
+
74
+ {block.teaserBody ? (
75
+ <div className="max-w-[62ch] space-y-4 text-base/7 text-current/80 md:text-lg/8">
76
+ <RichText content={block.teaserBody} />
77
+ </div>
78
+ ) : null}
79
+
80
+ {state === "unlocked" && payload ? (
81
+ <div className="mt-8">
82
+ {justUnlocked && block.successMessage ? (
83
+ <p className="mb-6 rounded-lg bg-black/[0.04] px-4 py-3 text-current/72 text-sm">
84
+ {block.successMessage}
85
+ </p>
86
+ ) : null}
87
+ <GatedReveal payload={payload} />
88
+ </div>
89
+ ) : (
90
+ <form
91
+ onSubmit={handleSubmit}
92
+ className="mt-8 flex max-w-md flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-6"
93
+ >
94
+ {block.gateHeadline ? (
95
+ <h3 className="font-semibold text-lg">{block.gateHeadline}</h3>
96
+ ) : null}
97
+ {block.gateDescription ? (
98
+ <p className="text-current/72 text-sm">{block.gateDescription}</p>
99
+ ) : null}
100
+ <input
101
+ type="email"
102
+ name="email"
103
+ required
104
+ placeholder="you@example.com"
105
+ className="rounded-lg border border-black/10 bg-white px-4 py-2.5 text-sm outline-none focus:border-black/40"
106
+ />
107
+ {error ? <p className="text-red-600 text-sm">{error}</p> : null}
108
+ <button
109
+ type="submit"
110
+ disabled={state === "loading"}
111
+ className="rounded-lg bg-black px-4 py-2.5 font-medium text-sm text-white transition-colors hover:bg-black/80 disabled:opacity-60"
112
+ >
113
+ {state === "loading" ? "Unlocking…" : block.submitLabel || "Unlock"}
114
+ </button>
115
+ </form>
116
+ )}
117
+ </section>
118
+ )
119
+ }
120
+
121
+ function GatedReveal({ payload }: { payload: GatedContentPayload }) {
122
+ if (payload.kind === "richText" && payload.content) {
123
+ return (
124
+ <div className="max-w-[62ch] space-y-4 text-base/7 text-current/80 md:text-lg/8">
125
+ <RichText content={payload.content} />
126
+ </div>
127
+ )
128
+ }
129
+
130
+ if (payload.kind === "file" && payload.file?.url) {
131
+ return (
132
+ <Link
133
+ href={payload.file.url}
134
+ download
135
+ className="inline-flex items-center gap-2 rounded-lg bg-black px-4 py-2.5 font-medium text-sm text-white transition-colors hover:bg-black/80"
136
+ >
137
+ Download {payload.file.originalFilename ?? "file"}
138
+ </Link>
139
+ )
140
+ }
141
+
142
+ if (payload.kind === "video") {
143
+ if (payload.video.url) {
144
+ return (
145
+ <div className="aspect-video w-full max-w-3xl overflow-hidden rounded-lg">
146
+ <iframe
147
+ src={payload.video.url}
148
+ title="Gated video"
149
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
150
+ allowFullScreen
151
+ className="h-full w-full border-0"
152
+ />
153
+ </div>
154
+ )
155
+ }
156
+ if (payload.video.file?.url) {
157
+ return (
158
+ // biome-ignore lint/a11y/useMediaCaption: editor-supplied video without a caption track
159
+ <video
160
+ controls
161
+ src={payload.video.file.url}
162
+ className="w-full max-w-3xl rounded-lg"
163
+ />
164
+ )
165
+ }
166
+ }
167
+
168
+ return null
169
+ }
@@ -2,6 +2,7 @@ import { ArticleContent } from "./components/article-content"
2
2
  import { ContentCollection } from "./components/content-collection"
3
3
  import type { ContentCollectionSearchParams } from "./components/content-collection/types"
4
4
  import { Description } from "./components/description"
5
+ import { GatedContent } from "./components/gated-content"
5
6
  import { Hero } from "./components/hero"
6
7
  import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
7
8
 
@@ -42,6 +43,8 @@ function PageBuilderNode({
42
43
  return <Hero block={block} />
43
44
  case "description":
44
45
  return <Description block={block} />
46
+ case "gatedContent":
47
+ return <GatedContent block={block} />
45
48
  default:
46
49
  return null
47
50
  }
@@ -22,11 +22,13 @@
22
22
  "rxjs",
23
23
  "gsap",
24
24
  "@gsap/react",
25
- "motion"
25
+ "motion",
26
+ "detect-gpu",
27
+ "lodash-es",
28
+ "@types/lodash-es",
29
+ "react-device-detect",
30
+ "zustand"
26
31
  ],
27
- "ignoreBinaries": [
28
- "gh",
29
- "op",
30
- "vercel"
31
- ]
32
+ "ignoreBinaries": ["gh", "op", "vercel"],
33
+ "ignoreFiles": [".agents/**", ".claude/**", "hooks/**"]
32
34
  }
@@ -14,11 +14,6 @@ pre-commit:
14
14
  run: bunx tsgo --noEmit --incremental false
15
15
  stage_fixed: false
16
16
 
17
- pre-push:
18
- commands:
19
- knip:
20
- run: bun run knip
21
-
22
17
  post-merge:
23
18
  commands:
24
19
  install-deps:
@@ -50,6 +50,13 @@ export type SanityImageAssetReference = {
50
50
  [internalGroqTypeReferenceTo]?: "sanity.imageAsset"
51
51
  }
52
52
 
53
+ export type SanityFileAssetReference = {
54
+ _ref: string
55
+ _type: "reference"
56
+ _weak?: boolean
57
+ [internalGroqTypeReferenceTo]?: "sanity.fileAsset"
58
+ }
59
+
53
60
  export type StructuredData = {
54
61
  _id: string
55
62
  _type: "structuredData"
@@ -307,6 +314,33 @@ export type BlogCollection = {
307
314
  subtitle?: string
308
315
  }
309
316
 
317
+ export type GatedContent = {
318
+ _id: string
319
+ _type: "gatedContent"
320
+ _createdAt: string
321
+ _updatedAt: string
322
+ _rev: string
323
+ internalTitle?: string
324
+ teaserHeading?: string
325
+ teaserBody?: RichText
326
+ gateType?: "form"
327
+ gateHeadline?: string
328
+ gateDescription?: string
329
+ submitLabel?: string
330
+ successMessage?: string
331
+ kind?: "richText" | "file" | "video"
332
+ content?: RichText
333
+ file?: {
334
+ asset?: SanityFileAssetReference
335
+ _type: "file"
336
+ }
337
+ videoUrl?: string
338
+ videoFile?: {
339
+ asset?: SanityFileAssetReference
340
+ _type: "file"
341
+ }
342
+ }
343
+
310
344
  export type NavItem = {
311
345
  _type: "navItem"
312
346
  title?: string
@@ -475,6 +509,13 @@ export type BlogCollectionReference = {
475
509
  [internalGroqTypeReferenceTo]?: "blogCollection"
476
510
  }
477
511
 
512
+ export type GatedContentReference = {
513
+ _ref: string
514
+ _type: "reference"
515
+ _weak?: boolean
516
+ [internalGroqTypeReferenceTo]?: "gatedContent"
517
+ }
518
+
478
519
  export type PageBuilder = Array<
479
520
  | ({
480
521
  _key: string
@@ -488,6 +529,9 @@ export type PageBuilder = Array<
488
529
  | ({
489
530
  _key: string
490
531
  } & BlogCollectionReference)
532
+ | ({
533
+ _key: string
534
+ } & GatedContentReference)
491
535
  >
492
536
 
493
537
  export type Metadata = {
@@ -699,6 +743,7 @@ export type AllSanitySchemaTypes =
699
743
  | Slug
700
744
  | Redirect
701
745
  | SanityImageAssetReference
746
+ | SanityFileAssetReference
702
747
  | StructuredData
703
748
  | SanityImageCrop
704
749
  | SanityImageHotspot
@@ -715,6 +760,7 @@ export type AllSanitySchemaTypes =
715
760
  | RichText
716
761
  | Author
717
762
  | BlogCollection
763
+ | GatedContent
718
764
  | NavItem
719
765
  | NavMegaMenu
720
766
  | NavColumn
@@ -730,6 +776,7 @@ export type AllSanitySchemaTypes =
730
776
  | DescriptionReference
731
777
  | BlogContentReference
732
778
  | BlogCollectionReference
779
+ | GatedContentReference
733
780
  | PageBuilder
734
781
  | Metadata
735
782
  | ExternalLink
@@ -768,6 +815,20 @@ export type PAGE_QUERY_RESULT = {
768
815
  _rev: string
769
816
  subtitle?: string
770
817
  }
818
+ | {
819
+ _key: string
820
+ _id: string
821
+ _type: "gatedContent"
822
+ internalTitle: string | null
823
+ teaserHeading: string | null
824
+ teaserBody: RichText | null
825
+ gateType: string | null
826
+ gateHeadline: string | null
827
+ gateDescription: string | null
828
+ submitLabel: string | null
829
+ successMessage: string | null
830
+ kind: "richText" | "file" | "video" | null
831
+ }
771
832
  | {
772
833
  _key: string
773
834
  _id: string
@@ -897,6 +958,26 @@ export type PAGE_QUERY_RESULT = {
897
958
  _updatedAt: string
898
959
  } | null
899
960
 
961
+ // Source: queries.ts
962
+ // Variable: GATED_CONTENT_QUERY
963
+ export type GATED_CONTENT_QUERY_RESULT = {
964
+ kind: "richText" | "file" | "video" | null
965
+ content: RichText | null
966
+ file: {
967
+ url: string | null
968
+ originalFilename: string | null
969
+ mimeType: string | null
970
+ size: number | null
971
+ } | null
972
+ video: {
973
+ url: string | null
974
+ file: {
975
+ url: string | null
976
+ mimeType: string | null
977
+ } | null
978
+ }
979
+ } | null
980
+
900
981
  // Source: queries.ts
901
982
  // Variable: PAGE_METADATA_QUERY
902
983
  // Query: *[ _type == "page" && ( ($slug == null && !defined(pageFolder._ref) && (!defined(slug.current) || slug.current == "" || slug.current == "/")) || select( defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current, defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current, defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current, defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current, defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current, defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->slug.current, defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current + "/" + slug.current, defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current + "/" + slug.current, defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current + "/" + slug.current, defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current + "/" + slug.current, defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current + "/" + slug.current, defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->slug.current + "/" + slug.current, slug.current ) == $slug ) ][0] { _id, title, metadata, _updatedAt }
@@ -1,6 +1,7 @@
1
1
  # ============================================
2
2
  # Basement Next Starter - Environment Variables
3
3
  # ============================================
4
+ # bsmnt:if dotenvx
4
5
  # This project uses dotenvx (https://dotenvx.com): the committed `.env` holds
5
6
  # shared values, with secrets encrypted there. The private keys live in
6
7
  # `.env.keys` (gitignored) — restore them with `bun run env:setup`.
@@ -15,6 +16,13 @@
15
16
  #
16
17
  # This file just documents the available variables; real values live in `.env`.
17
18
  # ============================================
19
+ # bsmnt:endif
20
+ # bsmnt:if !dotenvx
21
+ # Copy this file to `.env.local` and fill in the values you need.
22
+ # `.env.local` is gitignored, so real values stay off version control.
23
+ # Only NEXT_PUBLIC_* variables are exposed to the browser.
24
+ # ============================================
25
+ # bsmnt:endif
18
26
 
19
27
 
20
28
  # ============================================
@@ -46,6 +46,7 @@ jobs:
46
46
  - name: Lint
47
47
  run: bun run lint
48
48
 
49
+ # bsmnt:if dotenvx
49
50
  # dotenvx needs the private key to decrypt secrets at build time. It's
50
51
  # pulled from 1Password via an org-wide service-account token + the op-ref
51
52
  # committed in .env. Optional: builds pass without it (only plaintext /
@@ -70,6 +71,12 @@ jobs:
70
71
  fi
71
72
  bun run build
72
73
 
74
+ # bsmnt:endif
75
+ # bsmnt:if !dotenvx
76
+ - name: Build
77
+ run: bun run build
78
+
79
+ # bsmnt:endif
73
80
  # Runs your tests if any exist; a fresh project with none passes cleanly.
74
81
  - name: Test
75
82
  run: |
@@ -22,6 +22,7 @@ bun dev
22
22
  | `bun format` | Format the codebase |
23
23
  | `bun typecheck` | Run TypeScript |
24
24
  | `bun analyze` | Analyze the Next.js bundle |
25
+ <!-- bsmnt:if dotenvx -->
25
26
  | `bun env:set KEY value` | Encrypt one value into `.env` (`--plain`, `--preview`/`--prod`) |
26
27
  | `bun env:encrypt-all` | Bulk-encrypt every plaintext value in a file (`--preview`/`--prod`) |
27
28
  | `bun env:setup` | Restore `.env.keys` from 1Password |
@@ -29,7 +30,9 @@ bun dev
29
30
  | `bun env:setup-remote` | Push private keys to GitHub / Vercel (`github`/`vercel`, or pick) |
30
31
  | `bun setup-remote` | Create the GitHub repo, link a Vercel project, and push the key to Vercel env |
31
32
  | `bun env:save` | Save the private key to 1Password (Development vault) |
33
+ <!-- bsmnt:endif -->
32
34
 
35
+ <!-- bsmnt:if dotenvx -->
33
36
  ## Environment variables
34
37
 
35
38
  Secrets are encrypted at rest with [dotenvx](https://dotenvx.com): the committed `.env` holds encrypted values, the private keys live in `.env.keys` (gitignored), and dev/build run through `dotenvx run` so values decrypt automatically.
@@ -53,6 +56,15 @@ Secrets are encrypted at rest with [dotenvx](https://dotenvx.com): the committed
53
56
 
54
57
  A gitleaks pre-commit hook + CI scan block committed plaintext secrets (install: `brew install gitleaks`).
55
58
 
59
+ <!-- bsmnt:endif -->
60
+ <!-- bsmnt:if !dotenvx -->
61
+ ## Environment variables
62
+
63
+ Copy `.env.example` to `.env.local` and fill in the values you need. `.env.local` is gitignored, so it stays off version control. Only `NEXT_PUBLIC_*` variables are exposed to the browser — everything else is server-only.
64
+
65
+ For deploys, set the same variables in your host's dashboard (e.g. Vercel → Settings → Environment Variables).
66
+
67
+ <!-- bsmnt:endif -->
56
68
  ## Project Structure
57
69
 
58
70
  ```txt
@@ -91,8 +103,10 @@ The workflow takes its runtime versions from your manifests, so there's a single
91
103
 
92
104
  It also caches Bun's install cache between runs, and the test step is a no-op until you add real test files.
93
105
 
106
+ <!-- bsmnt:if dotenvx -->
94
107
  The build decrypts the committed `.env` with the dotenvx private key, pulled from 1Password: set an **org-level** `OP_SERVICE_ACCOUNT_TOKEN` (a read-only [service account](https://developer.1password.com/docs/service-accounts/) scoped to the `Development` vault) — one secret for every repo — and CI reads the key via the op-ref committed in `.env`. A per-repo `DOTENV_PRIVATE_KEY` secret (`bun run env:setup-remote github`) works as a fallback; builds pass without either (only plaintext / `NEXT_PUBLIC_*` is read).
95
108
 
109
+ <!-- bsmnt:endif -->
96
110
  ### Branch protection
97
111
 
98
112
  To block merges when CI fails, enable branch protection on `main`:
@@ -83,7 +83,7 @@
83
83
  "complexity": {
84
84
  "noForEach": "off",
85
85
  "useFlatMap": "warn",
86
- "useSimplifiedLogicExpression": "warn"
86
+ "useSimplifiedLogicExpression": "off"
87
87
  },
88
88
  "correctness": {
89
89
  "noInvalidUseBeforeDeclaration": "error",
@@ -1 +1 @@
1
- <svg width="123" height="18" viewBox="0 0 123 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.43626 6.76458V.252441H.381348V17.1671H4.33011v-3.0447c.40337 2.1989 1.78331 3.2984 4.13983 3.2984 3.96996 0 4.86166-.9515 4.86166-5.6664V9.91493c0-4.7361-.8704-5.66641-4.64937-5.66641-2.54758 0-3.77892.54973-4.24597 2.51606Zm.02123 5.94122V9.42863c.10615-1.14174.84919-1.69146 2.29282-1.69146 1.95315 0 2.31406.42286 2.31406 2.30463v1.628c0 1.8395-.36091 2.2623-2.31406 2.2623-1.4224 0-2.16544-.4228-2.29282-1.2263Zm10.23281.8035c0 3.4252.7643 4.0384 4.9253 3.9115 1.9956-.0211 3.418-.8035 3.97-2.4949v2.2412h4.0337V8.62519c0-3.70008-.8917-4.37667-5.6259-4.37667h-.9766c-4.9678 0-5.9019.65544-5.9019 4.14409v.50744h4.0337c-.0425-1.45889.3184-1.71261 2.314-1.71261 1.7833 0 2.123.25372 2.123 1.71261v.71887l-4.0337.02114c-4.0973 0-4.8616.61314-4.8616 3.86924Zm4.246.0423c0-.7823.2123-.9303 1.3162-.9303l3.3331-.0212v.5497c0 .9938-.6793 1.3744-2.4627 1.4166h-.2122c-1.656 0-1.9744-.1691-1.9744-1.0148Zm10.0417-.3383c0 3.5309.8917 4.2075 5.5622 4.2075h.9766c5.3499 0 6.3265-.592 6.3265-3.6367 0-1.9451-.4883-3.108-2.5051-3.7-1.104-.31718-2.0805-.5709-4.1611-.90919-1.3799-.21143-1.762-.46515-1.762-.99373 0-.78231.4246-.99374 1.9956-.99374 1.6347 0 2.0805.29601 2.0805 1.33203v.38058h4.0337v-.38058c0-3.61551-.8917-4.27095-5.6259-4.27095h-.9766c-4.8192 0-5.7533.57087-5.7533 3.8058 0 1.69146.4458 2.64288 2.1867 3.31948 1.2738.4863 3.2057.7823 3.9487.9303 1.8682.3383 2.3141.6766 2.3141 1.184 0 .7612-.4883.9726-2.3141.9726-1.9744 0-2.5051-.2748-2.5051-1.2474v-.2749H28.978v.2749Zm14.0967-2.3681c0 5.5396.9765 6.5756 6.1779 6.5756h.9765c5.1589 0 6.1142-.6766 6.1142-4.271h-4.2459c0 1.0784-.3609 1.3109-2.3353 1.3109-2.0593 0-2.4415-.3594-2.4415-2.1989v-.0634h9.1289v-1.9875c0-5.03207-.9766-5.96238-6.2204-5.96238h-.9765c-5.2014 0-6.1779 1.01488-6.1779 6.59668Zm4.2459-1.628v-.06343c0-1.64918.3822-1.96633 2.4415-1.96633 1.9956 0 2.4202.31715 2.4414 2.02976h-4.8829Zm10.9122 7.9499h4.0337V9.83035c0-1.48003.7006-2.22004 2.1017-2.22004 1.6135 0 1.932.31715 1.932 1.96632v7.59047h4.0124l-.0212-7.46361c.0424-1.39546.743-2.09318 2.1017-2.09318 1.6135 0 1.8895.31715 1.8895 1.96632v7.59047h4.0337l-.0212-8.20362c0-3.97494-.7219-4.71496-4.6069-4.71496-2.1443 0-3.4393.90916-3.9063 2.91778-.1699-2.41034-.8705-2.91778-4.0974-2.91778-1.8895 0-2.9722.76116-3.418 2.66406V4.50224h-4.0337V17.1671Zm21.6545-6.3219c0 5.5396.9766 6.5756 6.1991 6.5756h.9766c5.1164 0 6.0717-.6766 6.0717-4.271H88.91c0 1.0784-.3609 1.3109-2.3141 1.3109-2.1017 0-2.4839-.3594-2.4839-2.1989v-.0634h9.1289v-1.9875c0-5.03207-.9766-5.96238-6.1779-5.96238h-.9766c-5.2225 0-6.1991 1.01488-6.1991 6.59668Zm4.2247-1.628v-.06343c0-1.64918.3822-1.96633 2.4839-1.96633 1.9744 0 2.399.31715 2.4202 2.02976H84.112Zm10.891 7.9499h4.0337V9.61892c.0212-1.35317.7642-2.00861 2.2293-2.00861 1.868 0 2.208.35943 2.208 2.30462v7.25217h4.033l-.021-8.20362c0-3.97494-.7-4.71496-4.394-4.71496-2.336 0-3.5671.76116-4.0553 2.81206V4.50224H95.003V17.1671Zm13.545-9.30307h1.252v5.05327c0 3.5732.828 4.2498 5.095 4.2498h2.76v-3.5944h-2.335c-1.253 0-1.486-.2114-1.486-1.2686V7.86403h3.63V4.50224h-3.63V1.77476h-3.821v2.72748h-1.465v3.36179Zm10.636 9.30307h3.63v-3.5944h-3.63v3.5944Z" fill="#EFEFEF"/></svg>
1
+ <svg width="123" height="18" viewBox="0 0 123 18" fill="none" xmlns="http://www.w3.org/2000/svg"><title>basement</title><path d="M4.43626 6.76458V.252441H.381348V17.1671H4.33011v-3.0447c.40337 2.1989 1.78331 3.2984 4.13983 3.2984 3.96996 0 4.86166-.9515 4.86166-5.6664V9.91493c0-4.7361-.8704-5.66641-4.64937-5.66641-2.54758 0-3.77892.54973-4.24597 2.51606Zm.02123 5.94122V9.42863c.10615-1.14174.84919-1.69146 2.29282-1.69146 1.95315 0 2.31406.42286 2.31406 2.30463v1.628c0 1.8395-.36091 2.2623-2.31406 2.2623-1.4224 0-2.16544-.4228-2.29282-1.2263Zm10.23281.8035c0 3.4252.7643 4.0384 4.9253 3.9115 1.9956-.0211 3.418-.8035 3.97-2.4949v2.2412h4.0337V8.62519c0-3.70008-.8917-4.37667-5.6259-4.37667h-.9766c-4.9678 0-5.9019.65544-5.9019 4.14409v.50744h4.0337c-.0425-1.45889.3184-1.71261 2.314-1.71261 1.7833 0 2.123.25372 2.123 1.71261v.71887l-4.0337.02114c-4.0973 0-4.8616.61314-4.8616 3.86924Zm4.246.0423c0-.7823.2123-.9303 1.3162-.9303l3.3331-.0212v.5497c0 .9938-.6793 1.3744-2.4627 1.4166h-.2122c-1.656 0-1.9744-.1691-1.9744-1.0148Zm10.0417-.3383c0 3.5309.8917 4.2075 5.5622 4.2075h.9766c5.3499 0 6.3265-.592 6.3265-3.6367 0-1.9451-.4883-3.108-2.5051-3.7-1.104-.31718-2.0805-.5709-4.1611-.90919-1.3799-.21143-1.762-.46515-1.762-.99373 0-.78231.4246-.99374 1.9956-.99374 1.6347 0 2.0805.29601 2.0805 1.33203v.38058h4.0337v-.38058c0-3.61551-.8917-4.27095-5.6259-4.27095h-.9766c-4.8192 0-5.7533.57087-5.7533 3.8058 0 1.69146.4458 2.64288 2.1867 3.31948 1.2738.4863 3.2057.7823 3.9487.9303 1.8682.3383 2.3141.6766 2.3141 1.184 0 .7612-.4883.9726-2.3141.9726-1.9744 0-2.5051-.2748-2.5051-1.2474v-.2749H28.978v.2749Zm14.0967-2.3681c0 5.5396.9765 6.5756 6.1779 6.5756h.9765c5.1589 0 6.1142-.6766 6.1142-4.271h-4.2459c0 1.0784-.3609 1.3109-2.3353 1.3109-2.0593 0-2.4415-.3594-2.4415-2.1989v-.0634h9.1289v-1.9875c0-5.03207-.9766-5.96238-6.2204-5.96238h-.9765c-5.2014 0-6.1779 1.01488-6.1779 6.59668Zm4.2459-1.628v-.06343c0-1.64918.3822-1.96633 2.4415-1.96633 1.9956 0 2.4202.31715 2.4414 2.02976h-4.8829Zm10.9122 7.9499h4.0337V9.83035c0-1.48003.7006-2.22004 2.1017-2.22004 1.6135 0 1.932.31715 1.932 1.96632v7.59047h4.0124l-.0212-7.46361c.0424-1.39546.743-2.09318 2.1017-2.09318 1.6135 0 1.8895.31715 1.8895 1.96632v7.59047h4.0337l-.0212-8.20362c0-3.97494-.7219-4.71496-4.6069-4.71496-2.1443 0-3.4393.90916-3.9063 2.91778-.1699-2.41034-.8705-2.91778-4.0974-2.91778-1.8895 0-2.9722.76116-3.418 2.66406V4.50224h-4.0337V17.1671Zm21.6545-6.3219c0 5.5396.9766 6.5756 6.1991 6.5756h.9766c5.1164 0 6.0717-.6766 6.0717-4.271H88.91c0 1.0784-.3609 1.3109-2.3141 1.3109-2.1017 0-2.4839-.3594-2.4839-2.1989v-.0634h9.1289v-1.9875c0-5.03207-.9766-5.96238-6.1779-5.96238h-.9766c-5.2225 0-6.1991 1.01488-6.1991 6.59668Zm4.2247-1.628v-.06343c0-1.64918.3822-1.96633 2.4839-1.96633 1.9744 0 2.399.31715 2.4202 2.02976H84.112Zm10.891 7.9499h4.0337V9.61892c.0212-1.35317.7642-2.00861 2.2293-2.00861 1.868 0 2.208.35943 2.208 2.30462v7.25217h4.033l-.021-8.20362c0-3.97494-.7-4.71496-4.394-4.71496-2.336 0-3.5671.76116-4.0553 2.81206V4.50224H95.003V17.1671Zm13.545-9.30307h1.252v5.05327c0 3.5732.828 4.2498 5.095 4.2498h2.76v-3.5944h-2.335c-1.253 0-1.486-.2114-1.486-1.2686V7.86403h3.63V4.50224h-3.63V1.77476h-3.821v2.72748h-1.465v3.36179Zm10.636 9.30307h3.63v-3.5944h-3.63v3.5944Z" fill="#EFEFEF"/></svg>
@@ -11,11 +11,13 @@
11
11
  "@svgr/webpack",
12
12
  "gsap",
13
13
  "@gsap/react",
14
- "motion"
14
+ "motion",
15
+ "detect-gpu",
16
+ "lodash-es",
17
+ "@types/lodash-es",
18
+ "react-device-detect",
19
+ "zustand"
15
20
  ],
16
- "ignoreBinaries": [
17
- "op",
18
- "gh",
19
- "vercel"
20
- ]
21
+ "ignoreBinaries": ["op", "gh", "vercel"],
22
+ "ignoreFiles": [".agents/**", ".claude/**", "hooks/**"]
21
23
  }
@@ -14,11 +14,6 @@ pre-commit:
14
14
  run: bunx tsgo --noEmit --incremental false
15
15
  stage_fixed: false
16
16
 
17
- pre-push:
18
- commands:
19
- knip:
20
- run: bun run knip
21
-
22
17
  post-merge:
23
18
  commands:
24
19
  install-deps: