bsmnt 0.6.3 → 0.7.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 (91) hide show
  1. package/dist/application/add-integration/index.d.ts.map +1 -1
  2. package/dist/application/add-integration/index.js +30 -4
  3. package/dist/application/add-integration/index.js.map +1 -1
  4. package/dist/application/shared/node-version.d.ts +8 -0
  5. package/dist/application/shared/node-version.d.ts.map +1 -0
  6. package/dist/application/shared/node-version.js +31 -0
  7. package/dist/application/shared/node-version.js.map +1 -0
  8. package/dist/application/shared/strip-ansi.d.ts +3 -0
  9. package/dist/application/shared/strip-ansi.d.ts.map +1 -0
  10. package/dist/application/shared/strip-ansi.js +7 -0
  11. package/dist/application/shared/strip-ansi.js.map +1 -0
  12. package/dist/core/create/execute-plan.d.ts +3 -0
  13. package/dist/core/create/execute-plan.d.ts.map +1 -1
  14. package/dist/core/create/execute-plan.js +3 -0
  15. package/dist/core/create/execute-plan.js.map +1 -1
  16. package/dist/domain/cms.d.ts +2 -0
  17. package/dist/domain/cms.d.ts.map +1 -1
  18. package/dist/domain/cms.js +1 -0
  19. package/dist/domain/cms.js.map +1 -1
  20. package/dist/domain/forms.d.ts +2 -2
  21. package/dist/domain/forms.js +1 -1
  22. package/dist/domain/forms.js.map +1 -1
  23. package/dist/domain/integration.d.ts +7 -3
  24. package/dist/domain/integration.d.ts.map +1 -1
  25. package/dist/domain/integration.js.map +1 -1
  26. package/dist/domain/skills.d.ts +26 -8
  27. package/dist/domain/skills.d.ts.map +1 -1
  28. package/dist/domain/skills.js +30 -9
  29. package/dist/domain/skills.js.map +1 -1
  30. package/dist/index.js +132 -16
  31. package/dist/index.js.map +1 -1
  32. package/dist/infrastructure/create/executor.d.ts.map +1 -1
  33. package/dist/infrastructure/create/executor.js +2 -1
  34. package/dist/infrastructure/create/executor.js.map +1 -1
  35. package/dist/infrastructure/create/init-git-repo.d.ts +11 -7
  36. package/dist/infrastructure/create/init-git-repo.d.ts.map +1 -1
  37. package/dist/infrastructure/create/init-git-repo.js +21 -18
  38. package/dist/infrastructure/create/init-git-repo.js.map +1 -1
  39. package/dist/infrastructure/create/sanity-auth.d.ts +11 -0
  40. package/dist/infrastructure/create/sanity-auth.d.ts.map +1 -0
  41. package/dist/infrastructure/create/sanity-auth.js +72 -0
  42. package/dist/infrastructure/create/sanity-auth.js.map +1 -0
  43. package/dist/infrastructure/create/setup-agent.d.ts +4 -3
  44. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  45. package/dist/infrastructure/create/setup-agent.js +51 -30
  46. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  47. package/dist/infrastructure/create/setup-dotenvx.js +1 -1
  48. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
  49. package/dist/infrastructure/create/setup-remote.d.ts +14 -0
  50. package/dist/infrastructure/create/setup-remote.d.ts.map +1 -0
  51. package/dist/infrastructure/create/setup-remote.js +139 -0
  52. package/dist/infrastructure/create/setup-remote.js.map +1 -0
  53. package/dist/infrastructure/create/setup-sanity.d.ts +3 -2
  54. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  55. package/dist/infrastructure/create/setup-sanity.js +34 -49
  56. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  57. package/dist/modules/features/env/dotenvx/config.d.ts.map +1 -1
  58. package/dist/modules/features/env/dotenvx/config.js +6 -0
  59. package/dist/modules/features/env/dotenvx/config.js.map +1 -1
  60. package/dist/paths.d.ts +0 -2
  61. package/dist/paths.d.ts.map +1 -1
  62. package/dist/paths.js +0 -2
  63. package/dist/paths.js.map +1 -1
  64. package/package.json +3 -3
  65. package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +160 -0
  66. package/src/agent-skills/rss-feed/SKILL.md +158 -0
  67. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.config.ts +3 -0
  68. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.config.ts +3 -0
  69. package/src/modules/features/cms/sanity-pagebuilder/files/lib/scripts/sanity-typegen.ts +16 -25
  70. package/src/modules/features/env/dotenvx/files/push-keys.mjs +7 -7
  71. package/src/modules/features/env/dotenvx/files/save-key.mjs +27 -12
  72. package/src/modules/features/env/dotenvx/files/setup-remote.ts +182 -0
  73. package/src/modules/features/forms/hubspot/files/_sanity-adapter/form-block.ts +60 -0
  74. package/src/modules/features/forms/hubspot/files/_sanity-adapter/form-field.ts +30 -0
  75. package/src/modules/features/forms/hubspot/files/_sanity-adapter/form-select-input.tsx +169 -0
  76. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/README.md +114 -0
  77. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/embed.tsx +165 -0
  78. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/form-selection.ts +40 -0
  79. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/form.tsx +29 -0
  80. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/forms.ts +142 -0
  81. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/hubspot-form.css +634 -0
  82. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/skeleton.tsx +24 -0
  83. package/src/templates/next-default/README.md +1 -0
  84. package/src/templates/next-default/knip.json +9 -1
  85. package/src/templates/next-experiments/README.md +1 -0
  86. package/src/templates/next-experiments/knip.json +9 -1
  87. package/src/templates/next-pagebuilder/README.md +1 -0
  88. package/src/templates/next-pagebuilder/knip.json +7 -2
  89. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +1197 -176
  90. package/src/templates/next-webgl/README.md +1 -0
  91. package/src/templates/next-webgl/knip.json +9 -1
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Create the GitHub repo, link a Vercel project to it, and push the dotenvx key
3
+ * to Vercel env — before the git connection, so the first push-to-deploy can
4
+ * decrypt the committed `.env`. No GitHub secret: CI reads the key from
5
+ * 1Password via OP_SERVICE_ACCOUNT_TOKEN.
6
+ *
7
+ * bun setup-remote # private repo under your account (default)
8
+ * bun setup-remote --public # public repo
9
+ * bun setup-remote --org my-org # create under an org
10
+ *
11
+ * The CLI runs the same sequence inline during `bsmnt create`
12
+ * (src/infrastructure/create/setup-remote.ts); keep the two in sync.
13
+ */
14
+ import { spawnSync } from "node:child_process";
15
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
16
+ import path from "node:path";
17
+
18
+ const GH_HINT =
19
+ "gh CLI not found — install it (https://cli.github.com) and run `gh auth login`.";
20
+ const VERCEL_HINT =
21
+ "vercel CLI not found — install it (`bun add -g vercel`) and try again.";
22
+
23
+ /** Run an interactive command, letting the user answer its prompts directly. */
24
+ function run(cmd: string, args: string[], notFoundHint: string): boolean {
25
+ const res = spawnSync(cmd, args, { stdio: "inherit" });
26
+ if ((res.error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
27
+ console.error(`\n${notFoundHint}`);
28
+ process.exit(1);
29
+ }
30
+ return res.status === 0;
31
+ }
32
+
33
+ /**
34
+ * Push the private keys to Vercel env via the tested push-keys path, rather
35
+ * than re-implementing the per-key (stdin-piped) upload here.
36
+ */
37
+ function pushVercelKeys(): boolean {
38
+ return (
39
+ spawnSync("bun", ["run", "scripts/push-keys.mjs", "vercel"], {
40
+ stdio: "inherit",
41
+ }).status === 0
42
+ );
43
+ }
44
+
45
+ function hasOriginRemote(): boolean {
46
+ return (
47
+ spawnSync("git", ["remote", "get-url", "origin"], { stdio: "ignore" })
48
+ .status === 0
49
+ );
50
+ }
51
+
52
+ function ghReady(): boolean {
53
+ const res = spawnSync("gh", ["auth", "status"], { stdio: "ignore" });
54
+ return (
55
+ (res.error as NodeJS.ErrnoException | undefined)?.code !== "ENOENT" &&
56
+ res.status === 0
57
+ );
58
+ }
59
+
60
+ /** True when `.env.keys` holds at least one non-empty DOTENV_PRIVATE_KEY*. */
61
+ function hasPrivateKey(): boolean {
62
+ if (!existsSync(".env.keys")) return false;
63
+ for (const line of readFileSync(".env.keys", "utf8").split("\n")) {
64
+ const value = line
65
+ .match(/^\s*DOTENV_PRIVATE_KEY[A-Z_]*\s*=\s*(.+?)\s*$/)?.[1]
66
+ ?.replace(/^["']|["']$/g, "");
67
+ if (value) return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ function main(): void {
73
+ // No key means a deploy can't decrypt `.env` — nothing to wire up yet.
74
+ if (!hasPrivateKey()) {
75
+ console.error(
76
+ "No DOTENV_PRIVATE_KEY* in .env.keys — run `bun run env:setup` first so there's a key to push.",
77
+ );
78
+ process.exit(1);
79
+ }
80
+
81
+ // GitHub: ensure the repo exists so Vercel can git-connect to it.
82
+ let connected = hasOriginRemote();
83
+ if (!connected) {
84
+ if (ghReady()) {
85
+ // `--source` forces non-interactive mode, which requires a visibility flag.
86
+ const visibility = process.argv.includes("--public")
87
+ ? "--public"
88
+ : "--private";
89
+ const orgIndex = process.argv.indexOf("--org");
90
+ const owner = orgIndex !== -1 ? process.argv[orgIndex + 1] : undefined;
91
+ const nameArg = owner ? [`${owner}/${path.basename(process.cwd())}`] : [];
92
+ console.info(`→ gh repo create (${visibility})`);
93
+ // No `--push` here: the push must come after `vercel git connect`, or
94
+ // the git integration never sees it and skips the first deployment.
95
+ if (
96
+ run(
97
+ "gh",
98
+ ["repo", "create", ...nameArg, "--source=.", "--remote=origin", visibility],
99
+ GH_HINT,
100
+ )
101
+ ) {
102
+ connected = hasOriginRemote();
103
+ }
104
+ } else {
105
+ console.info(
106
+ "\nNo `origin` remote and the gh CLI isn't ready — skipping GitHub setup.\n" +
107
+ " Install gh (https://cli.github.com), run `gh auth login`, then re-run\n" +
108
+ " (or create the repo manually and re-run).",
109
+ );
110
+ }
111
+ }
112
+
113
+ // Vercel: link the project and push the keys as env vars.
114
+ if (spawnSync("vercel", ["whoami"], { stdio: "ignore" }).status !== 0) {
115
+ console.info("→ vercel login");
116
+ if (!run("vercel", ["login"], VERCEL_HINT)) process.exit(1);
117
+ }
118
+ // Pick the scope via Vercel's team picker, then link with `--yes` so the
119
+ // validated lowercase dir name is used (the `Name?` prompt rejects uppercase).
120
+ console.info("→ vercel switch + link");
121
+ run("vercel", ["switch"], VERCEL_HINT);
122
+ if (!run("vercel", ["link", "--yes"], VERCEL_HINT)) {
123
+ console.error("\nLink cancelled — re-run `bun setup-remote` when ready.");
124
+ process.exit(1);
125
+ }
126
+ // Pin the Next.js framework so Vercel resolves the .next output (it sometimes
127
+ // creates the project as "Other"); commit it so the push below ships it.
128
+ if (!existsSync("vercel.json")) {
129
+ writeFileSync(
130
+ "vercel.json",
131
+ '{\n "$schema": "https://openapi.vercel.sh/vercel.json",\n "framework": "nextjs"\n}\n',
132
+ );
133
+ spawnSync("git", ["add", "vercel.json"], { stdio: "ignore" });
134
+ spawnSync(
135
+ "git",
136
+ [
137
+ "commit",
138
+ "-m",
139
+ "chore: pin Next.js framework for Vercel",
140
+ "--no-verify",
141
+ "--",
142
+ "vercel.json",
143
+ ],
144
+ { stdio: "ignore" },
145
+ );
146
+ }
147
+ console.info("→ uploading private keys to Vercel env");
148
+ if (!pushVercelKeys()) process.exit(1);
149
+
150
+ // Connect the two so pushes build from a clone (keys already in place).
151
+ if (!connected) {
152
+ console.info(
153
+ "\n✓ Vercel linked and keys uploaded. No GitHub repo connected yet — once\n" +
154
+ " one exists, run `vercel git connect` for push-to-deploy.",
155
+ );
156
+ return;
157
+ }
158
+ console.info("→ vercel git connect");
159
+ if (!run("vercel", ["git", "connect"], VERCEL_HINT)) {
160
+ console.error(
161
+ "\nKeys uploaded, but `vercel git connect` failed. If the Git integration\n" +
162
+ "isn't installed, add it once from the Vercel dashboard (Account/Team →\n" +
163
+ "Integrations → GitHub), then re-run `vercel git connect`.",
164
+ );
165
+ process.exit(1);
166
+ }
167
+
168
+ // Push after connecting so the git integration builds the first deployment.
169
+ console.info("→ git push");
170
+ if (!run("git", ["push", "-u", "origin", "HEAD"], "git not found.")) {
171
+ console.error(
172
+ "\nConnected, but the push failed — run `git push` to trigger the first deploy.",
173
+ );
174
+ process.exit(1);
175
+ }
176
+
177
+ console.info(
178
+ "\n✓ GitHub + Vercel ready. Pushed — Vercel is building the first deployment.",
179
+ );
180
+ }
181
+
182
+ main();
@@ -0,0 +1,60 @@
1
+ import { EnvelopeIcon } from "@sanity/icons"
2
+ import { defineField, defineType } from "sanity"
3
+ import {
4
+ decodeHubSpotFormSelection,
5
+ getHubSpotFormSelectionLabel,
6
+ } from "../form-selection"
7
+ import { hubspotFormField } from "./form-field"
8
+
9
+ /**
10
+ * Sample reusable HubSpot form block. Register it in your schema's document
11
+ * list (and page-builder config, if you use one), then render its `form` with
12
+ * <HubSpotForm /> from the integration core:
13
+ *
14
+ * @example
15
+ * import { HubSpotForm } from "@/lib/integrations/hubspot/form"
16
+ * import { decodeHubSpotFormSelection } from "@/lib/integrations/hubspot/form-selection"
17
+ *
18
+ * const selection = decodeHubSpotFormSelection(block.form)
19
+ * if (!selection) return null
20
+ * return <HubSpotForm {...selection} />
21
+ */
22
+ export const hubspotFormBlock = defineType({
23
+ name: "hubspotFormBlock",
24
+ title: "HubSpot Form Block",
25
+ type: "document",
26
+ icon: EnvelopeIcon,
27
+ fields: [
28
+ defineField({
29
+ name: "name",
30
+ title: "Name",
31
+ type: "string",
32
+ description: "Used to identify this block in the Studio.",
33
+ validation: (Rule) => Rule.required(),
34
+ }),
35
+ hubspotFormField,
36
+ ],
37
+ preview: {
38
+ select: {
39
+ name: "name",
40
+ form: "form",
41
+ },
42
+ prepare({ name, form }) {
43
+ const selectedForm = decodeHubSpotFormSelection(form)
44
+
45
+ return {
46
+ title: name || "Untitled",
47
+ ...(selectedForm
48
+ ? { subtitle: getHubSpotFormSelectionLabel(selectedForm) }
49
+ : {}),
50
+ }
51
+ },
52
+ },
53
+ })
54
+
55
+ export const hubspotFormBlockReference = defineType({
56
+ name: "hubspotFormBlockReference",
57
+ title: "HubSpot Form Block",
58
+ type: "reference",
59
+ to: [{ type: "hubspotFormBlock" }],
60
+ })
@@ -0,0 +1,30 @@
1
+ import { defineField } from "sanity"
2
+ import { decodeHubSpotFormSelection } from "../form-selection"
3
+ import { HubSpotFormSelectInput } from "./form-select-input"
4
+
5
+ /**
6
+ * Shared HubSpot form picker field. Stores the selected form as one encoded
7
+ * string and validates it decodes to a real selection. Reuse it in any block
8
+ * or document that embeds a HubSpot form.
9
+ *
10
+ * @example
11
+ * defineType({
12
+ * name: "contactBlock",
13
+ * type: "object",
14
+ * fields: [hubspotFormField],
15
+ * })
16
+ */
17
+ export const hubspotFormField = defineField({
18
+ name: "form",
19
+ title: "HubSpot Form",
20
+ type: "string",
21
+ components: {
22
+ input: HubSpotFormSelectInput,
23
+ },
24
+ description:
25
+ "Fetches HubSpot forms when the dropdown is opened. The selected form is stored as one string.",
26
+ validation: (Rule) =>
27
+ Rule.required().custom((value) =>
28
+ decodeHubSpotFormSelection(value) ? true : "Select a valid HubSpot form."
29
+ ),
30
+ })
@@ -0,0 +1,169 @@
1
+ "use client"
2
+
3
+ import {
4
+ Autocomplete,
5
+ Box,
6
+ Button,
7
+ Card,
8
+ Flex,
9
+ Spinner,
10
+ Stack,
11
+ Text,
12
+ } from "@sanity/ui"
13
+ import { useCallback, useMemo, useRef, useState } from "react"
14
+ import { PatchEvent, type StringInputProps, set, unset } from "sanity"
15
+ import {
16
+ decodeHubSpotFormSelection,
17
+ getHubSpotFormSelectionLabel,
18
+ } from "../form-selection"
19
+ import { getHubSpotFormOptions, type HubSpotFormOption } from "../forms"
20
+
21
+ type LoadStatus = "idle" | "loading" | "loaded" | "error"
22
+
23
+ function HubSpotFormOptionPreview({ option }: { option: HubSpotFormOption }) {
24
+ const form = decodeHubSpotFormSelection(option.value)
25
+
26
+ return (
27
+ <Card padding={3} style={{ cursor: "pointer" }}>
28
+ <Stack space={2}>
29
+ <Text size={1} weight="medium">
30
+ {form?.name ?? option.label}
31
+ </Text>
32
+ {form ? (
33
+ <Text muted size={1}>
34
+ {form.formId}
35
+ {option.renderRawHtml === false ? " · iframe embed" : ""}
36
+ </Text>
37
+ ) : null}
38
+ </Stack>
39
+ </Card>
40
+ )
41
+ }
42
+
43
+ /**
44
+ * Sanity Studio input that lazy-loads the connected portal's HubSpot forms and
45
+ * stores the chosen one as a single encoded string (see `hubspotFormField`).
46
+ */
47
+ export function HubSpotFormSelectInput(props: StringInputProps) {
48
+ const { value, onChange, readOnly, elementProps } = props
49
+ const { onChange: _ignoreElementOnChange, ...autocompleteElementProps } =
50
+ elementProps
51
+
52
+ const [forms, setForms] = useState<HubSpotFormOption[]>([])
53
+ const [status, setStatus] = useState<LoadStatus>("idle")
54
+ const [error, setError] = useState<string | null>(null)
55
+ const statusRef = useRef<LoadStatus>("idle")
56
+
57
+ const selectedForm = useMemo(() => decodeHubSpotFormSelection(value), [value])
58
+ const isLoading = status === "loading"
59
+
60
+ const loadForms = useCallback(async () => {
61
+ if (statusRef.current === "loading") return
62
+ statusRef.current = "loading"
63
+ setStatus("loading")
64
+ setError(null)
65
+
66
+ try {
67
+ const result = await getHubSpotFormOptions()
68
+ setForms(result.forms)
69
+ setError(result.error ?? null)
70
+ const next: LoadStatus = result.error ? "error" : "loaded"
71
+ statusRef.current = next
72
+ setStatus(next)
73
+ } catch (err) {
74
+ const message =
75
+ err instanceof Error ? err.message : "Unable to load forms."
76
+ setError(message)
77
+ statusRef.current = "error"
78
+ setStatus("error")
79
+ }
80
+ }, [])
81
+
82
+ const loadFormsOnce = useCallback(() => {
83
+ if (statusRef.current === "idle") void loadForms()
84
+ }, [loadForms])
85
+
86
+ // Keep the already-selected form visible while options are still loading.
87
+ const options =
88
+ selectedForm && !forms.some((form) => form.value === value)
89
+ ? [
90
+ {
91
+ label: getHubSpotFormSelectionLabel(selectedForm),
92
+ value: value as string,
93
+ renderRawHtml: null,
94
+ },
95
+ ...forms,
96
+ ]
97
+ : forms
98
+
99
+ return (
100
+ <Stack space={3}>
101
+ <Flex gap={2} align="center">
102
+ <Box flex={1}>
103
+ <Autocomplete
104
+ {...autocompleteElementProps}
105
+ disabled={readOnly || isLoading}
106
+ filterOption={(query, option) =>
107
+ option.label.toLowerCase().includes(query.toLowerCase())
108
+ }
109
+ loading={isLoading}
110
+ openButton
111
+ openOnFocus
112
+ options={options}
113
+ onFocus={loadFormsOnce}
114
+ onMouseDown={loadFormsOnce}
115
+ onQueryChange={(query) => {
116
+ if (query) loadFormsOnce()
117
+ }}
118
+ onSelect={(nextValue) =>
119
+ onChange(PatchEvent.from(nextValue ? set(nextValue) : unset()))
120
+ }
121
+ placeholder={
122
+ isLoading ? "Loading HubSpot forms..." : "Select a HubSpot form"
123
+ }
124
+ popover={{ portal: true, width: "auto" }}
125
+ radius={2}
126
+ renderOption={(option) => (
127
+ <HubSpotFormOptionPreview option={option} />
128
+ )}
129
+ renderValue={(_, option) => {
130
+ const form = option
131
+ ? decodeHubSpotFormSelection(option.value)
132
+ : selectedForm
133
+ return form ? getHubSpotFormSelectionLabel(form) : ""
134
+ }}
135
+ value={value ?? ""}
136
+ />
137
+ </Box>
138
+ <Button
139
+ disabled={readOnly || isLoading}
140
+ mode="ghost"
141
+ onClick={() => void loadForms()}
142
+ text="Refresh"
143
+ type="button"
144
+ />
145
+ </Flex>
146
+
147
+ {isLoading ? (
148
+ <Flex align="center" gap={2}>
149
+ <Spinner muted />
150
+ <Text muted size={1}>
151
+ Fetching forms from HubSpot
152
+ </Text>
153
+ </Flex>
154
+ ) : null}
155
+
156
+ {selectedForm ? (
157
+ <Text muted size={1}>
158
+ Selected form ID: {selectedForm.formId}
159
+ </Text>
160
+ ) : null}
161
+
162
+ {error ? (
163
+ <Text size={1} style={{ color: "var(--card-critical-fg-color)" }}>
164
+ {error}
165
+ </Text>
166
+ ) : null}
167
+ </Stack>
168
+ )
169
+ }
@@ -0,0 +1,114 @@
1
+ # HubSpot Forms integration
2
+
3
+ Agnostic, reusable HubSpot Forms embed. The core (`form.tsx`, `embed.tsx`,
4
+ `forms.ts`, `form-selection.ts`, `hubspot-form.css`) has **no CMS dependency**
5
+ and adds **no runtime packages** — it loads HubSpot's official forms script and
6
+ styles the injected DOM with self-contained, themeable CSS.
7
+
8
+ ## 1. Environment
9
+
10
+ Create a HubSpot **private app** with the `forms` and
11
+ `account-info.security.read` scopes, then add its token to `.env.local`:
12
+
13
+ ```bash
14
+ HUBSPOT_ACCESS_TOKEN=pat-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
15
+ ```
16
+
17
+ The token is only read server-side (in `forms.ts`); it is never exposed to the
18
+ client.
19
+
20
+ ## 2. Render a form
21
+
22
+ If you already know the form, pass `portalId` / `formId` / `region` directly:
23
+
24
+ ```tsx
25
+ import { HubSpotForm } from "@/lib/integrations/hubspot/form"
26
+
27
+ export function Contact() {
28
+ return (
29
+ <HubSpotForm
30
+ portalId="1234567"
31
+ formId="00000000-0000-0000-0000-000000000000"
32
+ region="na1"
33
+ />
34
+ )
35
+ }
36
+ ```
37
+
38
+ `region` is the portal's data-hosting location (`na1`, `eu1`, …).
39
+
40
+ ## 3. Theming
41
+
42
+ By default the form inherits the **basement starter** design tokens, so it
43
+ matches the surrounding site with no extra work:
44
+
45
+ | `--hs-*` token | Maps to (fallback) |
46
+ | --- | --- |
47
+ | `--hs-surface` | `--color-primary` (`#ffffff`) |
48
+ | `--hs-text` / `--hs-text-strong` | `--color-secondary` (`#18181b`) |
49
+ | `--hs-accent` | `--color-contrast` (`#ff4d00`) |
50
+ | `--hs-muted` | `--color-gray-400` (`#858585`) |
51
+ | `--hs-font-sans` | `--font-sans` → `--font-mono` → system |
52
+
53
+ Light/dark follows the host's `[data-theme]` automatically (via
54
+ `--color-primary` / `--color-secondary`). Outside the starter, the fallbacks
55
+ render a clean light theme.
56
+
57
+ Override any token from a parent scope to customize:
58
+
59
+ ```css
60
+ .my-section {
61
+ --hs-accent: #487cff;
62
+ --hs-surface: #0a0a0a;
63
+ --hs-text: #ededed;
64
+ --hs-radius: 8px;
65
+ }
66
+ ```
67
+
68
+ ## 4. Listing forms programmatically
69
+
70
+ `getHubSpotFormOptions()` (from `forms.ts`) returns every published form in the
71
+ portal as `{ label, value, renderRawHtml }`. Each `value` is a JSON-encoded
72
+ selection — decode it with `decodeHubSpotFormSelection` and spread it into
73
+ `<HubSpotForm />`.
74
+
75
+ ## 5. Optional: Sanity Studio form picker
76
+
77
+ When this integration is added to a project that already has Sanity, a
78
+ `sanity/` folder ships alongside the core with a Studio picker:
79
+
80
+ - `sanity/form-field.ts` — a ready-to-use `hubspotFormField` (`defineField`)
81
+ - `sanity/form-select-input.tsx` — the `<Autocomplete>` input it uses
82
+ - `sanity/form-block.ts` — a sample `hubspotFormBlock` document type
83
+
84
+ Wire the field into any schema:
85
+
86
+ ```ts
87
+ import { defineType } from "sanity"
88
+ import { hubspotFormField } from "@/lib/integrations/hubspot/sanity/form-field"
89
+
90
+ export const contactBlock = defineType({
91
+ name: "contactBlock",
92
+ type: "object",
93
+ fields: [hubspotFormField],
94
+ })
95
+ ```
96
+
97
+ Or register the sample block by adding `hubspotFormBlock` to your schema's
98
+ document list, then render it (see the example in `sanity/form-block.ts`).
99
+
100
+ ## 6. Use as a page-builder block
101
+
102
+ In a basement **Sanity page-builder** project, `hubspotFormBlock` can be a
103
+ selectable block. Adding this integration installs the
104
+ **`hubspot-pagebuilder-block`** agent skill — ask your coding agent to "wire the
105
+ HubSpot form into the page builder" and it makes the edits and verifies the
106
+ typecheck. To do it by hand:
107
+
108
+ 1. `schemas/index.ts` — import `hubspotFormBlock`, add it to `schema.types`
109
+ 2. `page-builder-config.ts` — add `"hubspotFormBlock"` to
110
+ `PageBuilderDocumentType` and a `hubspotFormBlockReference` member
111
+ 3. `structure.ts` — add `S.documentTypeListItem("hubspotFormBlock")`
112
+ 4. `components/page-builder/renderer.tsx` — add a `case "hubspotFormBlock"` that
113
+ decodes `block.form` and renders `<HubSpotForm />`
114
+ 5. `bun run sanity:typegen && bun typecheck`