@atelic-action/ui 0.17.2 → 1.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 (184) hide show
  1. package/README.md +428 -44
  2. package/bin/atelic-check-base-path.ts +5 -0
  3. package/bin/atelic-clean-sitemap.ts +5 -0
  4. package/bin/atelic-contact-canary.ts +5 -0
  5. package/bin/atelic-print-sheets.sh +1 -1
  6. package/bin/atelic-seal-document.mjs +1 -1
  7. package/bin/atelic-serve-static.ts +1 -1
  8. package/dist/gate/index.d.ts +1 -1
  9. package/dist/gate/index.js +1 -1
  10. package/dist/gate/middleware.d.ts +4 -1
  11. package/dist/gate/middleware.js +14 -5
  12. package/dist/gate/wall.d.ts +25 -0
  13. package/dist/gate/wall.js +41 -2
  14. package/dist/routing/basePath.d.ts +24 -0
  15. package/dist/routing/basePath.js +54 -0
  16. package/dist/vite/index.d.ts +3 -2
  17. package/dist/vite/index.js +3 -2
  18. package/dist/vite/marketing-config.d.ts +50 -0
  19. package/dist/vite/marketing-config.js +83 -0
  20. package/package.json +164 -5
  21. package/src/app/AppProvider.tsx +41 -0
  22. package/src/app/AuthGuard.tsx +27 -0
  23. package/src/app/CardErrorBoundary.tsx +39 -0
  24. package/src/app/ErrorFallback.tsx +39 -0
  25. package/src/app/GoogleGlyph.tsx +22 -0
  26. package/src/app/config.ts +54 -0
  27. package/src/app/configure.ts +18 -0
  28. package/src/app/context/AppContext.tsx +52 -0
  29. package/src/app/context/SidebarContext.tsx +124 -0
  30. package/src/app/context/UserContext.tsx +139 -0
  31. package/src/app/hooks/useDebounce.ts +12 -0
  32. package/src/app/hooks/useIsDesktop.ts +31 -0
  33. package/src/app/hooks/useWakeLock.ts +49 -0
  34. package/src/app/index.ts +64 -0
  35. package/src/app/layout/ActionButton.tsx +67 -0
  36. package/src/app/layout/AppLayout.tsx +110 -0
  37. package/src/app/layout/AppPage.tsx +268 -0
  38. package/src/app/layout/Breadcrumbs.tsx +57 -0
  39. package/src/app/layout/DesktopRail.tsx +168 -0
  40. package/src/app/layout/FormActionButtons.tsx +69 -0
  41. package/src/app/layout/MobileMenu.tsx +156 -0
  42. package/src/app/layout/MobileTabBar.tsx +43 -0
  43. package/src/app/layout/PageError.tsx +30 -0
  44. package/src/app/layout/PageSkeleton.tsx +65 -0
  45. package/src/app/layout/SectionCards.tsx +33 -0
  46. package/src/app/layout/UserMenu.tsx +104 -0
  47. package/src/app/layout/identity.ts +22 -0
  48. package/src/app/layout/index.ts +20 -0
  49. package/src/app/layout/nav.ts +52 -0
  50. package/src/app/lib/dates.ts +57 -0
  51. package/src/app/lib/env.ts +37 -0
  52. package/src/app/lib/format.ts +61 -0
  53. package/src/app/lib/http.ts +205 -0
  54. package/src/app/lib/lastAuth.ts +45 -0
  55. package/src/app/lib/oauth.ts +32 -0
  56. package/src/app/lib/publicRoutes.ts +33 -0
  57. package/src/app/lib/queryClient.ts +38 -0
  58. package/src/app/lib/search.ts +17 -0
  59. package/src/app/lib/session.ts +36 -0
  60. package/src/app/lib/tokenRefresh.ts +137 -0
  61. package/src/app/lib/transformKeys.ts +60 -0
  62. package/src/app/lib/units.ts +228 -0
  63. package/src/app/lib/unwrap.ts +48 -0
  64. package/src/app/lib/utils.ts +10 -0
  65. package/src/app/stores/authStore.ts +80 -0
  66. package/src/app/stores/storage.ts +40 -0
  67. package/src/app/stores/unitsStore.ts +54 -0
  68. package/src/app/types/index.ts +72 -0
  69. package/src/app/types/query.ts +11 -0
  70. package/src/app/ui/accordion.tsx +54 -0
  71. package/src/app/ui/alert-dialog.tsx +114 -0
  72. package/src/app/ui/alert.tsx +49 -0
  73. package/src/app/ui/avatar.tsx +45 -0
  74. package/src/app/ui/badge-list.tsx +51 -0
  75. package/src/app/ui/badge.tsx +33 -0
  76. package/src/app/ui/button.tsx +49 -0
  77. package/src/app/ui/callout.tsx +22 -0
  78. package/src/app/ui/card.tsx +55 -0
  79. package/src/app/ui/checkbox.tsx +26 -0
  80. package/src/app/ui/clear-button.tsx +42 -0
  81. package/src/app/ui/command.tsx +140 -0
  82. package/src/app/ui/country-dropdown.tsx +184 -0
  83. package/src/app/ui/dialog.tsx +104 -0
  84. package/src/app/ui/dropdown-menu.tsx +185 -0
  85. package/src/app/ui/form.tsx +172 -0
  86. package/src/app/ui/index.ts +36 -0
  87. package/src/app/ui/input.tsx +22 -0
  88. package/src/app/ui/label.tsx +19 -0
  89. package/src/app/ui/option-select.tsx +56 -0
  90. package/src/app/ui/pagination.tsx +90 -0
  91. package/src/app/ui/popover.tsx +29 -0
  92. package/src/app/ui/progress.tsx +25 -0
  93. package/src/app/ui/search-input.tsx +63 -0
  94. package/src/app/ui/select.tsx +151 -0
  95. package/src/app/ui/separator.tsx +24 -0
  96. package/src/app/ui/sheet.tsx +119 -0
  97. package/src/app/ui/slider.tsx +23 -0
  98. package/src/app/ui/switch.tsx +27 -0
  99. package/src/app/ui/textarea.tsx +21 -0
  100. package/src/app/ui/tooltip.tsx +32 -0
  101. package/src/artifact/ArtifactShell.tsx +2 -2
  102. package/src/artifact/Gate.tsx +2 -2
  103. package/src/artifact/Writeup.tsx +7 -7
  104. package/src/chrome/SiteShell.tsx +141 -0
  105. package/src/chrome/index.ts +2 -0
  106. package/src/components/Disclosure.tsx +80 -14
  107. package/src/components/GateLogin.tsx +1 -1
  108. package/src/components/Watermark.tsx +1 -1
  109. package/src/components/index.ts +6 -2
  110. package/src/gate/index.ts +1 -1
  111. package/src/gate/middleware.ts +17 -6
  112. package/src/gate/wall.ts +60 -2
  113. package/src/lib/basePathAudit.ts +96 -0
  114. package/src/lib/displayDate.ts +25 -0
  115. package/src/lib/honeypot.ts +11 -0
  116. package/src/lib/hours.ts +28 -0
  117. package/src/lib/sitemap.ts +34 -0
  118. package/src/lib/trackEvent.ts +26 -0
  119. package/src/lib/useOpenHeight.ts +28 -0
  120. package/src/primitives/Button.tsx +3 -1
  121. package/src/primitives/Chip.tsx +3 -1
  122. package/src/primitives/ImageFrame.tsx +48 -0
  123. package/src/primitives/ProseBlocks.tsx +67 -0
  124. package/src/primitives/QuoteCard.tsx +20 -0
  125. package/src/primitives/StarRow.tsx +37 -0
  126. package/src/primitives/TickList.tsx +24 -0
  127. package/src/primitives/index.ts +5 -0
  128. package/src/routing/BasePathProvider.tsx +47 -0
  129. package/src/routing/basePath.ts +57 -0
  130. package/src/routing/index.ts +8 -0
  131. package/src/scripts/check-base-path.ts +130 -0
  132. package/src/scripts/clean-sitemap.ts +27 -0
  133. package/src/scripts/contact-canary.ts +98 -0
  134. package/src/scripts/serve-static.ts +29 -3
  135. package/src/sections/BlogIndex.tsx +57 -0
  136. package/src/sections/BlogPost.tsx +65 -0
  137. package/src/sections/CTABanner.tsx +93 -0
  138. package/src/sections/CTASection.tsx +60 -0
  139. package/src/sections/ComparisonTable.tsx +114 -0
  140. package/src/sections/ContactForm.tsx +163 -0
  141. package/src/sections/ContactInfoCard.tsx +92 -0
  142. package/src/sections/CredentialList.tsx +28 -0
  143. package/src/sections/DeviceShowcase.tsx +39 -0
  144. package/src/sections/ExpandList.tsx +58 -0
  145. package/src/sections/FAQBrowser.tsx +283 -0
  146. package/src/sections/FeatureDetail.tsx +60 -0
  147. package/src/sections/FeatureGrid.tsx +83 -0
  148. package/src/sections/Gallery.tsx +84 -0
  149. package/src/sections/Hero.tsx +110 -0
  150. package/src/sections/HoursTable.tsx +23 -0
  151. package/src/sections/JumpRail.tsx +35 -0
  152. package/src/sections/LegalPage.tsx +43 -0
  153. package/src/sections/LocationSection.tsx +66 -0
  154. package/src/sections/Locations.tsx +71 -0
  155. package/src/sections/LogoMarquee.tsx +27 -0
  156. package/src/sections/MapEmbed.tsx +75 -0
  157. package/src/sections/MediaBand.tsx +20 -0
  158. package/src/sections/MenuList.tsx +44 -0
  159. package/src/sections/PageHero.tsx +25 -0
  160. package/src/sections/PressQuotes.tsx +51 -0
  161. package/src/sections/PricingBand.tsx +43 -0
  162. package/src/sections/PricingCard.tsx +36 -0
  163. package/src/sections/ProcessSteps.tsx +45 -0
  164. package/src/sections/ProfileCards.tsx +73 -0
  165. package/src/sections/RevealSlider.tsx +156 -0
  166. package/src/sections/ReviewCarousel.tsx +223 -0
  167. package/src/sections/ReviewWall.tsx +220 -0
  168. package/src/sections/SocialStrip.tsx +83 -0
  169. package/src/sections/SplitSection.tsx +56 -0
  170. package/src/sections/index.ts +76 -0
  171. package/src/sections/siteConfig.ts +201 -0
  172. package/src/styles/app.css +673 -0
  173. package/src/styles/artifact.css +472 -399
  174. package/src/styles/base.css +49 -47
  175. package/src/styles/chrome.css +71 -71
  176. package/src/styles/components.css +150 -140
  177. package/src/styles/gate.css +29 -29
  178. package/src/styles/primitives.css +190 -25
  179. package/src/styles/sections.css +2652 -0
  180. package/src/styles/star-rating.css +2 -2
  181. package/src/vite/index.ts +9 -2
  182. package/src/vite/marketing-config.ts +128 -0
  183. package/src/components/Collapsible.tsx +0 -43
  184. /package/src/scripts/{build-gate-payload.mjs → seal-document.mjs} +0 -0
@@ -0,0 +1,56 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export interface SplitSectionProps {
4
+ left: ReactNode;
5
+ right: ReactNode;
6
+ /**
7
+ * Column proportions: "even" (1/1), or "narrow-first" (0.9/1.1,
8
+ * the media-plus-copy pattern).
9
+ */
10
+ ratio?: "even" | "narrow-first";
11
+ align?: "start" | "center";
12
+ /** Section background band. */
13
+ surface?: "default" | "alt";
14
+ /** Compact top padding for sections directly under a page hero. */
15
+ tightTop?: boolean;
16
+ /**
17
+ * Compact bottom padding for when the next section continues this
18
+ * one's thought (e.g. a list that belongs to this section's copy).
19
+ * Pair with `tight-top` on the follower to close the gap.
20
+ */
21
+ tightBottom?: boolean;
22
+ id?: string;
23
+ className?: string;
24
+ }
25
+
26
+ /** The recurring two-column section: copy plus copy, or media plus copy. */
27
+ export function SplitSection({
28
+ left,
29
+ right,
30
+ ratio = "even",
31
+ align = "start",
32
+ surface = "default",
33
+ tightTop = false,
34
+ tightBottom = false,
35
+ id,
36
+ className,
37
+ }: SplitSectionProps) {
38
+ const sectionClasses = [
39
+ "section",
40
+ surface === "alt" && "surface-alt",
41
+ tightTop && "tight-top",
42
+ tightBottom && "tight-bottom",
43
+ className,
44
+ ]
45
+ .filter(Boolean)
46
+ .join(" ");
47
+ const gridClasses = ["split", `ratio-${ratio}`, `align-${align}`].join(" ");
48
+ return (
49
+ <section className={sectionClasses} id={id}>
50
+ <div className={`wrap ${gridClasses}`}>
51
+ <div>{left}</div>
52
+ <div>{right}</div>
53
+ </div>
54
+ </section>
55
+ );
56
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `@atelic-action/ui/sections`: the page scale sections a marketing site
3
+ * composes, moved here from template-marketing in 0.19.0 (ATE-643), with the
4
+ * `SiteConfig` contract they and `SiteShell` read. Their styles are
5
+ * `@atelic-action/ui/styles/sections.css`; a section imports no stylesheet of
6
+ * its own.
7
+ */
8
+ export { isHoneypotFilled } from "../lib/honeypot";
9
+ export { formatClock, formatHoursRange, isClosed } from "../lib/hours";
10
+ export { type EventParams, trackEvent } from "../lib/trackEvent";
11
+ export { BlogIndex, type BlogIndexItem, type BlogIndexProps } from "./BlogIndex";
12
+ export { BlogPost, type BlogPostDoc, type BlogPostProps } from "./BlogPost";
13
+ export {
14
+ type ComparisonRow,
15
+ type ComparisonState,
16
+ ComparisonTable,
17
+ type ComparisonTableProps,
18
+ } from "./ComparisonTable";
19
+ export { ContactForm, type ContactFormProps } from "./ContactForm";
20
+ export {
21
+ ContactInfoCard,
22
+ type ContactInfoCardProps,
23
+ type ContactInfoCardSite,
24
+ type ContactPhone,
25
+ } from "./ContactInfoCard";
26
+ export { type Credential, CredentialList, type CredentialListProps } from "./CredentialList";
27
+ export { CTABanner, type CTABannerCTA, type CTABannerProps } from "./CTABanner";
28
+ export { CTASection, type CTASectionProps } from "./CTASection";
29
+ export { type DeviceImage, DeviceShowcase, type DeviceShowcaseProps } from "./DeviceShowcase";
30
+ export { ExpandList, type ExpandListItem, type ExpandListProps } from "./ExpandList";
31
+ export {
32
+ FAQBrowser,
33
+ type FAQBrowserProps,
34
+ type FAQCategory,
35
+ type FAQCta,
36
+ type FAQEntry,
37
+ } from "./FAQBrowser";
38
+ export { FeatureDetail, type FeatureDetailProps } from "./FeatureDetail";
39
+ export { type FeatureCard, FeatureGrid, type FeatureGridProps } from "./FeatureGrid";
40
+ export { Gallery, type GalleryPhoto, type GalleryProps } from "./Gallery";
41
+ export { Hero, type HeroCTA, type HeroProps, type HeroTrustItem } from "./Hero";
42
+ export { HoursTable, type HoursTableProps } from "./HoursTable";
43
+ export { JumpRail, type JumpRailItem, type JumpRailProps } from "./JumpRail";
44
+ export { type LegalBlock, type LegalDoc, LegalPage, type LegalPageProps } from "./LegalPage";
45
+ export {
46
+ LocationSection,
47
+ type LocationSectionProps,
48
+ type LocationSectionSite,
49
+ } from "./LocationSection";
50
+ export { type LocationEntry, Locations, type LocationsProps } from "./Locations";
51
+ export { LogoMarquee, type LogoMarqueeProps } from "./LogoMarquee";
52
+ export { MapEmbed, type MapEmbedProps, type MapEmbedSite } from "./MapEmbed";
53
+ export { MediaBand, type MediaBandProps } from "./MediaBand";
54
+ export { MenuList, type MenuListItem, type MenuListProps } from "./MenuList";
55
+ export { PageHero, type PageHeroProps } from "./PageHero";
56
+ export { type PressItem, PressQuotes, type PressQuotesProps, sortPress } from "./PressQuotes";
57
+ export { PricingBand, type PricingBandProps } from "./PricingBand";
58
+ export { PricingCard, type PricingCardProps } from "./PricingCard";
59
+ export { type ProcessStep, ProcessSteps, type ProcessStepsProps } from "./ProcessSteps";
60
+ export { ProfileCards, type ProfileCardsProps, type ProfileItem } from "./ProfileCards";
61
+ export { type RevealSlide, RevealSlider, type RevealSliderProps } from "./RevealSlider";
62
+ export { ReviewCarousel, type ReviewCarouselProps, type ReviewStat } from "./ReviewCarousel";
63
+ export { type ReviewItem, ReviewWall, type ReviewWallProps } from "./ReviewWall";
64
+ export { type SocialPhoto, SocialStrip, type SocialStripProps } from "./SocialStrip";
65
+ export { SplitSection, type SplitSectionProps } from "./SplitSection";
66
+ export type {
67
+ Address,
68
+ CTA,
69
+ Founder,
70
+ GateConfig,
71
+ NavItem,
72
+ OpeningHours,
73
+ SiteConfig,
74
+ SiteLink,
75
+ SocialLink,
76
+ } from "./siteConfig";
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The contract between a marketing site and the package's sections, moved
3
+ * here from template-marketing in 0.19.0 (ATE-643).
4
+ *
5
+ * SiteShell maps this config onto the chrome (header, menu, footer, sticky
6
+ * CTA bar), the sections that need the business's facts take it (or only the
7
+ * keys they read) as their `site` prop, and a site's structured data helpers
8
+ * render from it, so standing up a new site means writing one of these plus
9
+ * a theme.css, never touching the components themselves.
10
+ */
11
+ import type { SiteLink, SocialLink } from "../types";
12
+
13
+ export type { SiteLink, SocialLink };
14
+
15
+ export interface NavItem {
16
+ label: string;
17
+ to: string;
18
+ }
19
+
20
+ export interface CTA {
21
+ label: string;
22
+ href: string;
23
+ /** Opens in a new tab with rel="noopener" when true (e.g. a booking portal). */
24
+ external?: boolean;
25
+ }
26
+
27
+ export interface OpeningHours {
28
+ /** Row label as displayed, e.g. "Monday" or "Friday to Sunday". */
29
+ label: string;
30
+ /** Compact label for the footer, e.g. "Mon" or "Fri to Sun". */
31
+ labelShort: string;
32
+ /** schema.org day names covered by this row (empty for closed spans). */
33
+ days: string[];
34
+ /** 24 hour "HH:MM"; omit both for a closed row. */
35
+ opens?: string;
36
+ closes?: string;
37
+ /**
38
+ * Display text for a row without posted times, e.g. "By appointment".
39
+ * Renders wherever hours appear; defaults to "Closed". Rows without
40
+ * opens/closes never emit an openingHoursSpecification, so a
41
+ * by-appointment business states no invented hours in structured data.
42
+ */
43
+ note?: string;
44
+ }
45
+
46
+ export interface Address {
47
+ street: string;
48
+ locality: string;
49
+ region: string;
50
+ postalCode: string;
51
+ country: string;
52
+ }
53
+
54
+ export interface Founder {
55
+ name: string;
56
+ jobTitle: string;
57
+ honorificSuffix?: string;
58
+ alumniOf?: string;
59
+ }
60
+
61
+ /**
62
+ * Copy for the access-gate login screen. Present in the config so the login
63
+ * page is themed and worded per client with no code edits. The gate itself is
64
+ * turned on by setting GATE_SESSION_SECRET on the Vercel project, not by this
65
+ * block (README.md, The Access Gate).
66
+ */
67
+ export interface GateConfig {
68
+ /** Mono eyebrow above the headline, e.g. "Private preview". */
69
+ eyebrow?: string;
70
+ /** Login headline. */
71
+ headline: string;
72
+ /** Sub-lead under the headline. */
73
+ lead: string;
74
+ /** Fine-print attribution, e.g. "Built by Jane Maker". Defaults to `attribution`. */
75
+ builtBy?: string;
76
+ /** Contact email shown in the fine print. Defaults to `email`. */
77
+ contactEmail?: string;
78
+ }
79
+
80
+ export interface SiteConfig {
81
+ /** Brand name as rendered in the wordmark and structured data. */
82
+ name: string;
83
+ /**
84
+ * Canonical URL, no trailing slash, e.g. "https://example.com". For a cut
85
+ * with a `basePath` it is the full URL including the prefix
86
+ * ("https://atwood.atelic.me/proto"), and the build fails if it does not
87
+ * end with the base.
88
+ */
89
+ url: string;
90
+ /**
91
+ * The path prefix the site is served under, for a cut that does not own the
92
+ * root of its host: a site build previewed at "/proto" on the business's
93
+ * artifact host, which proxies those requests here. "" (the default) is the
94
+ * root. `marketingConfig` reads it for Vite's `base` and the router's
95
+ * `basepath`, and `SiteShell` hands it to `BasePathProvider`, which every
96
+ * link and image a section renders reads through `useBasePath`.
97
+ */
98
+ basePath?: string;
99
+ /** Default meta description and LocalBusiness description. */
100
+ description: string;
101
+ /** schema.org business types, most general first, e.g. ["MedicalBusiness", "PhysicalTherapy"]. */
102
+ schemaType: string[];
103
+ /**
104
+ * The chrome identity. `src` is the mark image; omit it for a pure text
105
+ * wordmark. `tile` renders a CSS glyph tile instead of an image (a dark
106
+ * rounded square bearing the given character in the brand's own webfont,
107
+ * with the accent bar beneath — an SVG-as-img mark cannot load webfonts,
108
+ * so a lettered mark must be real DOM text to render true). `wordmark`
109
+ * overrides the rendered wordmark text when it should differ from `name`
110
+ * (e.g. a lowercase brand treatment); `run` appends the trailing fade bar
111
+ * after the wordmark (the Eyebrow's signature, worn by the brand itself).
112
+ */
113
+ logo: { src?: string; alt: string; tile?: string; wordmark?: string; run?: boolean };
114
+ /** Google Fonts stylesheet URL for the theme's font stack. */
115
+ fontsHref?: string;
116
+ nav: NavItem[];
117
+ /** The site's primary conversion action; rendered in nav, sticky bar, and menus. */
118
+ cta: CTA;
119
+ /** Omit for businesses with no public phone; call surfaces render nothing. */
120
+ phone?: { e164: string; display: string };
121
+ email: string;
122
+ address: Address;
123
+ geo?: { latitude: number; longitude: number };
124
+ mapEmbedUrl?: string;
125
+ mapLinkUrl?: string;
126
+ /**
127
+ * Path to a static map screenshot (e.g. "/images/map.jpg"). When set, the
128
+ * map renders as a lightweight image linking to directions instead of the
129
+ * click-to-load live embed — one request, no third-party chain, and it
130
+ * shows a real map on first paint.
131
+ */
132
+ mapImage?: string;
133
+ hours: OpeningHours[];
134
+ /** Neighborhoods or cities served; feeds local SEO copy and structured data. */
135
+ serviceAreas: string[];
136
+ priceRange?: string;
137
+ /** Cuisine types for a Restaurant/FoodEstablishment (schema.org servesCuisine). */
138
+ servesCuisine?: string[];
139
+ founder?: Founder;
140
+ /** Names of offered services for structured data. */
141
+ offers?: string[];
142
+ /** Short brand paragraph for the footer. */
143
+ footerBlurb: string;
144
+ /** Extra footer nav links beyond the main nav (e.g. a patient portal). */
145
+ footerLinks?: SiteLink[];
146
+ /** Icon links (socials, the founder's own site) rendered in the footer brand column. */
147
+ socialLinks?: SocialLink[];
148
+ legalLinks?: SiteLink[];
149
+ /** Copyright attribution line suffix, e.g. "Dr. Alex Rivers, PT, DPT". */
150
+ attribution?: string;
151
+ /**
152
+ * Footer wording overrides: the heading over the address column (defaults
153
+ * to "Visit", which reads wrong for a practice with no storefront) and a
154
+ * short closing note beside the copyright line ("Made with love in ...").
155
+ */
156
+ footer?: {
157
+ contactHeading?: string;
158
+ note?: string;
159
+ /**
160
+ * Set false to drop the name and address lines from the contact column
161
+ * and leave only the phone and email under the heading (a practice with
162
+ * no storefront has nothing to visit). Defaults to true.
163
+ */
164
+ showAddress?: boolean;
165
+ };
166
+ ogImage?: string;
167
+ /** Profile URLs for the schema.org sameAs entity graph (socials, listings). */
168
+ sameAs?: string[];
169
+ /**
170
+ * Robots meta override for every page (e.g. "noindex, nofollow" while the
171
+ * site lives on a preview subdomain). Defaults to indexable.
172
+ */
173
+ robots?: string;
174
+ /**
175
+ * Analytics wiring. When `hubspotPortalId` is set, the HubSpot tracking code
176
+ * loads on every page and records visits; because each prospect gets a
177
+ * unique preview URL, a visit is attributable to that prospect. `hubspotRegion`
178
+ * picks the loader host and defaults to "na1"; na2 portals (loggable via a
179
+ * bcc.na2 address) load from js-na2.hs-scripts.com.
180
+ *
181
+ * `identify` ties a visit to a specific prospect: when the visit arrives with
182
+ * `?k=<token>` (the token embedded in that prospect's outreach link), the
183
+ * visitor is identified to HubSpot as `email`, so the visit attaches to that
184
+ * prospect's contact timeline. Visits without the token stay anonymous.
185
+ */
186
+ analytics?: {
187
+ /**
188
+ * GA4 measurement id (e.g. "G-XXXXXXXXXX"). When set, the Google tag
189
+ * loads on every page and the conversion events in
190
+ * the site's own analytics module (the standard set in DEPLOY.md)
191
+ * reach GA. Independent of the HubSpot wiring; a site can carry either
192
+ * or both.
193
+ */
194
+ googleTagId?: string;
195
+ hubspotPortalId?: string;
196
+ hubspotRegion?: "na1" | "na2" | "eu1";
197
+ identify?: { token: string; email: string };
198
+ };
199
+ /** Copy for the access-gate login screen (see GateConfig). */
200
+ gate?: GateConfig;
201
+ }