@contractspec/bundle.marketing 1.12.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 (216) hide show
  1. package/.turbo/turbo-build$colon$types.log +1 -0
  2. package/.turbo/turbo-build.log +175 -0
  3. package/.turbo/turbo-lint.log +3 -0
  4. package/AGENTS.md +36 -0
  5. package/CHANGELOG.md +416 -0
  6. package/README.md +57 -0
  7. package/dist/components/marketing/ChangelogPage.d.ts +21 -0
  8. package/dist/components/marketing/ChangelogPage.d.ts.map +1 -0
  9. package/dist/components/marketing/ChangelogPage.js +65 -0
  10. package/dist/components/marketing/ChangelogPage.js.map +1 -0
  11. package/dist/components/marketing/CofounderPage.d.ts +7 -0
  12. package/dist/components/marketing/CofounderPage.d.ts.map +1 -0
  13. package/dist/components/marketing/CofounderPage.js +468 -0
  14. package/dist/components/marketing/CofounderPage.js.map +1 -0
  15. package/dist/components/marketing/ContactClient.d.ts +7 -0
  16. package/dist/components/marketing/ContactClient.d.ts.map +1 -0
  17. package/dist/components/marketing/ContactClient.js +158 -0
  18. package/dist/components/marketing/ContactClient.js.map +1 -0
  19. package/dist/components/marketing/ContributePage.d.ts +9 -0
  20. package/dist/components/marketing/ContributePage.d.ts.map +1 -0
  21. package/dist/components/marketing/ContributePage.js +362 -0
  22. package/dist/components/marketing/ContributePage.js.map +1 -0
  23. package/dist/components/marketing/DesignPartnerPage.d.ts +9 -0
  24. package/dist/components/marketing/DesignPartnerPage.d.ts.map +1 -0
  25. package/dist/components/marketing/DesignPartnerPage.js +215 -0
  26. package/dist/components/marketing/DesignPartnerPage.js.map +1 -0
  27. package/dist/components/marketing/LandingPage.d.ts +7 -0
  28. package/dist/components/marketing/LandingPage.d.ts.map +1 -0
  29. package/dist/components/marketing/LandingPage.js +38 -0
  30. package/dist/components/marketing/LandingPage.js.map +1 -0
  31. package/dist/components/marketing/PricingClient.d.ts +7 -0
  32. package/dist/components/marketing/PricingClient.d.ts.map +1 -0
  33. package/dist/components/marketing/PricingClient.js +521 -0
  34. package/dist/components/marketing/PricingClient.js.map +1 -0
  35. package/dist/components/marketing/ProductClientPage.d.ts +7 -0
  36. package/dist/components/marketing/ProductClientPage.d.ts.map +1 -0
  37. package/dist/components/marketing/ProductClientPage.js +460 -0
  38. package/dist/components/marketing/ProductClientPage.js.map +1 -0
  39. package/dist/components/marketing/index.d.ts +11 -0
  40. package/dist/components/marketing/index.js +12 -0
  41. package/dist/components/marketing/pricing-thinking-modal.d.ts +16 -0
  42. package/dist/components/marketing/pricing-thinking-modal.d.ts.map +1 -0
  43. package/dist/components/marketing/pricing-thinking-modal.js +202 -0
  44. package/dist/components/marketing/pricing-thinking-modal.js.map +1 -0
  45. package/dist/components/marketing/sections/AudienceSection.d.ts +7 -0
  46. package/dist/components/marketing/sections/AudienceSection.d.ts.map +1 -0
  47. package/dist/components/marketing/sections/AudienceSection.js +68 -0
  48. package/dist/components/marketing/sections/AudienceSection.js.map +1 -0
  49. package/dist/components/marketing/sections/CorePositioningSection.d.ts +7 -0
  50. package/dist/components/marketing/sections/CorePositioningSection.d.ts.map +1 -0
  51. package/dist/components/marketing/sections/CorePositioningSection.js +59 -0
  52. package/dist/components/marketing/sections/CorePositioningSection.js.map +1 -0
  53. package/dist/components/marketing/sections/CtaSection.d.ts +7 -0
  54. package/dist/components/marketing/sections/CtaSection.d.ts.map +1 -0
  55. package/dist/components/marketing/sections/CtaSection.js +54 -0
  56. package/dist/components/marketing/sections/CtaSection.js.map +1 -0
  57. package/dist/components/marketing/sections/DevelopersSection.d.ts +7 -0
  58. package/dist/components/marketing/sections/DevelopersSection.d.ts.map +1 -0
  59. package/dist/components/marketing/sections/DevelopersSection.js +45 -0
  60. package/dist/components/marketing/sections/DevelopersSection.js.map +1 -0
  61. package/dist/components/marketing/sections/FearsSection.d.ts +7 -0
  62. package/dist/components/marketing/sections/FearsSection.d.ts.map +1 -0
  63. package/dist/components/marketing/sections/FearsSection.js +48 -0
  64. package/dist/components/marketing/sections/FearsSection.js.map +1 -0
  65. package/dist/components/marketing/sections/HeroMarketingSection.d.ts +7 -0
  66. package/dist/components/marketing/sections/HeroMarketingSection.d.ts.map +1 -0
  67. package/dist/components/marketing/sections/HeroMarketingSection.js +77 -0
  68. package/dist/components/marketing/sections/HeroMarketingSection.js.map +1 -0
  69. package/dist/components/marketing/sections/IconGridSection.d.ts +45 -0
  70. package/dist/components/marketing/sections/IconGridSection.d.ts.map +1 -0
  71. package/dist/components/marketing/sections/IconGridSection.js +44 -0
  72. package/dist/components/marketing/sections/IconGridSection.js.map +1 -0
  73. package/dist/components/marketing/sections/OutputsSection.d.ts +7 -0
  74. package/dist/components/marketing/sections/OutputsSection.d.ts.map +1 -0
  75. package/dist/components/marketing/sections/OutputsSection.js +59 -0
  76. package/dist/components/marketing/sections/OutputsSection.js.map +1 -0
  77. package/dist/components/marketing/sections/ProblemSection.d.ts +7 -0
  78. package/dist/components/marketing/sections/ProblemSection.d.ts.map +1 -0
  79. package/dist/components/marketing/sections/ProblemSection.js +46 -0
  80. package/dist/components/marketing/sections/ProblemSection.js.map +1 -0
  81. package/dist/components/marketing/sections/SolutionSection.d.ts +7 -0
  82. package/dist/components/marketing/sections/SolutionSection.d.ts.map +1 -0
  83. package/dist/components/marketing/sections/SolutionSection.js +46 -0
  84. package/dist/components/marketing/sections/SolutionSection.js.map +1 -0
  85. package/dist/components/marketing/sections/StepsSection.d.ts +7 -0
  86. package/dist/components/marketing/sections/StepsSection.d.ts.map +1 -0
  87. package/dist/components/marketing/sections/StepsSection.js +52 -0
  88. package/dist/components/marketing/sections/StepsSection.js.map +1 -0
  89. package/dist/components/marketing/waitlist-section.d.ts +15 -0
  90. package/dist/components/marketing/waitlist-section.d.ts.map +1 -0
  91. package/dist/components/marketing/waitlist-section.js +578 -0
  92. package/dist/components/marketing/waitlist-section.js.map +1 -0
  93. package/dist/components/templates/TemplatesClientPage.d.ts +7 -0
  94. package/dist/components/templates/TemplatesClientPage.d.ts.map +1 -0
  95. package/dist/components/templates/TemplatesClientPage.js +625 -0
  96. package/dist/components/templates/TemplatesClientPage.js.map +1 -0
  97. package/dist/components/templates/TemplatesPage.d.ts +7 -0
  98. package/dist/components/templates/TemplatesPage.d.ts.map +1 -0
  99. package/dist/components/templates/TemplatesPage.js +125 -0
  100. package/dist/components/templates/TemplatesPage.js.map +1 -0
  101. package/dist/components/templates/TemplatesPreviewModal.d.ts +15 -0
  102. package/dist/components/templates/TemplatesPreviewModal.d.ts.map +1 -0
  103. package/dist/components/templates/TemplatesPreviewModal.js +137 -0
  104. package/dist/components/templates/TemplatesPreviewModal.js.map +1 -0
  105. package/dist/components/templates/index.d.ts +4 -0
  106. package/dist/components/templates/index.js +5 -0
  107. package/dist/index.d.ts +29 -0
  108. package/dist/index.js +28 -0
  109. package/dist/libs/email/client.d.ts +15 -0
  110. package/dist/libs/email/client.d.ts.map +1 -0
  111. package/dist/libs/email/client.js +113 -0
  112. package/dist/libs/email/client.js.map +1 -0
  113. package/dist/libs/email/contact.d.ts +7 -0
  114. package/dist/libs/email/contact.d.ts.map +1 -0
  115. package/dist/libs/email/contact.js +71 -0
  116. package/dist/libs/email/contact.js.map +1 -0
  117. package/dist/libs/email/newsletter.d.ts +7 -0
  118. package/dist/libs/email/newsletter.d.ts.map +1 -0
  119. package/dist/libs/email/newsletter.js +95 -0
  120. package/dist/libs/email/newsletter.js.map +1 -0
  121. package/dist/libs/email/types.d.ts +53 -0
  122. package/dist/libs/email/types.d.ts.map +1 -0
  123. package/dist/libs/email/types.js +1 -0
  124. package/dist/libs/email/utils.d.ts +6 -0
  125. package/dist/libs/email/utils.d.ts.map +1 -0
  126. package/dist/libs/email/utils.js +7 -0
  127. package/dist/libs/email/utils.js.map +1 -0
  128. package/dist/libs/email/waitlist-application.d.ts +7 -0
  129. package/dist/libs/email/waitlist-application.d.ts.map +1 -0
  130. package/dist/libs/email/waitlist-application.js +170 -0
  131. package/dist/libs/email/waitlist-application.js.map +1 -0
  132. package/dist/libs/email/waitlist.d.ts +7 -0
  133. package/dist/libs/email/waitlist.d.ts.map +1 -0
  134. package/dist/libs/email/waitlist.js +105 -0
  135. package/dist/libs/email/waitlist.js.map +1 -0
  136. package/dist/libs/pricing-examples.d.ts +22 -0
  137. package/dist/libs/pricing-examples.d.ts.map +1 -0
  138. package/dist/libs/pricing-examples.js +21 -0
  139. package/dist/libs/pricing-examples.js.map +1 -0
  140. package/dist/registry/engine.d.ts +17 -0
  141. package/dist/registry/engine.d.ts.map +1 -0
  142. package/dist/registry/engine.js +24 -0
  143. package/dist/registry/engine.js.map +1 -0
  144. package/dist/registry/factory.d.ts +64 -0
  145. package/dist/registry/factory.d.ts.map +1 -0
  146. package/dist/registry/factory.js +61 -0
  147. package/dist/registry/factory.js.map +1 -0
  148. package/dist/registry/index.d.ts +8 -0
  149. package/dist/registry/index.js +8 -0
  150. package/dist/registry/registry-docs.d.ts +15 -0
  151. package/dist/registry/registry-docs.d.ts.map +1 -0
  152. package/dist/registry/registry-docs.js +305 -0
  153. package/dist/registry/registry-docs.js.map +1 -0
  154. package/dist/registry/registry-landing.d.ts +19 -0
  155. package/dist/registry/registry-landing.d.ts.map +1 -0
  156. package/dist/registry/registry-landing.js +95 -0
  157. package/dist/registry/registry-landing.js.map +1 -0
  158. package/dist/registry/registry.d.ts +30 -0
  159. package/dist/registry/registry.d.ts.map +1 -0
  160. package/dist/registry/registry.js +61 -0
  161. package/dist/registry/registry.js.map +1 -0
  162. package/dist/registry/types.d.ts +19 -0
  163. package/dist/registry/types.d.ts.map +1 -0
  164. package/dist/registry/types.js +0 -0
  165. package/dist/registry/utils.d.ts +31 -0
  166. package/dist/registry/utils.d.ts.map +1 -0
  167. package/dist/registry/utils.js +54 -0
  168. package/dist/registry/utils.js.map +1 -0
  169. package/package.json +151 -0
  170. package/src/components/marketing/ChangelogPage.tsx +110 -0
  171. package/src/components/marketing/CofounderPage.tsx +409 -0
  172. package/src/components/marketing/ContactClient.tsx +174 -0
  173. package/src/components/marketing/ContributePage.tsx +319 -0
  174. package/src/components/marketing/DesignPartnerPage.tsx +181 -0
  175. package/src/components/marketing/LandingPage.tsx +30 -0
  176. package/src/components/marketing/PricingClient.tsx +446 -0
  177. package/src/components/marketing/ProductClientPage.tsx +391 -0
  178. package/src/components/marketing/index.ts +10 -0
  179. package/src/components/marketing/pricing-thinking-modal.tsx +224 -0
  180. package/src/components/marketing/sections/AudienceSection.tsx +66 -0
  181. package/src/components/marketing/sections/CorePositioningSection.tsx +44 -0
  182. package/src/components/marketing/sections/CtaSection.tsx +57 -0
  183. package/src/components/marketing/sections/DevelopersSection.tsx +38 -0
  184. package/src/components/marketing/sections/FearsSection.tsx +45 -0
  185. package/src/components/marketing/sections/HeroMarketingSection.tsx +73 -0
  186. package/src/components/marketing/sections/IconGridSection.tsx +91 -0
  187. package/src/components/marketing/sections/OutputsSection.tsx +59 -0
  188. package/src/components/marketing/sections/ProblemSection.tsx +47 -0
  189. package/src/components/marketing/sections/SolutionSection.tsx +47 -0
  190. package/src/components/marketing/sections/StepsSection.tsx +55 -0
  191. package/src/components/marketing/waitlist-section.tsx +606 -0
  192. package/src/components/templates/TemplatesClientPage.tsx +711 -0
  193. package/src/components/templates/TemplatesPage.tsx +129 -0
  194. package/src/components/templates/TemplatesPreviewModal.tsx +260 -0
  195. package/src/components/templates/index.ts +3 -0
  196. package/src/index.ts +15 -0
  197. package/src/libs/email/client.test.ts +107 -0
  198. package/src/libs/email/client.ts +146 -0
  199. package/src/libs/email/contact.ts +80 -0
  200. package/src/libs/email/newsletter.ts +108 -0
  201. package/src/libs/email/types.ts +59 -0
  202. package/src/libs/email/utils.ts +8 -0
  203. package/src/libs/email/waitlist-application.ts +192 -0
  204. package/src/libs/email/waitlist.ts +118 -0
  205. package/src/libs/pricing-examples.ts +19 -0
  206. package/src/registry/engine.ts +38 -0
  207. package/src/registry/factory.ts +110 -0
  208. package/src/registry/index.ts +7 -0
  209. package/src/registry/registry-docs.ts +843 -0
  210. package/src/registry/registry-landing.ts +118 -0
  211. package/src/registry/registry.ts +85 -0
  212. package/src/registry/types.ts +17 -0
  213. package/src/registry/utils.ts +99 -0
  214. package/tsconfig.json +13 -0
  215. package/tsconfig.tsbuildinfo +1 -0
  216. package/tsdown.config.js +10 -0
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+
3
+ import { useMemo, useState } from 'react';
4
+ import {
5
+ Button,
6
+ ButtonLink,
7
+ Input,
8
+ MarketingCard,
9
+ MarketingCardContent,
10
+ MarketingCardDescription,
11
+ MarketingCardHeader,
12
+ MarketingCardTitle,
13
+ MarketingSection,
14
+ } from '@contractspec/lib.design-system';
15
+ import { HStack, VStack } from '@contractspec/lib.ui-kit-web/ui/stack';
16
+ import { listTemplates } from '@contractspec/module.examples';
17
+ import type { TemplateDefinition } from '@contractspec/lib.example-shared-ui';
18
+
19
+ function matchesQuery(t: TemplateDefinition, query: string): boolean {
20
+ const q = query.trim().toLowerCase();
21
+ if (!q) return true;
22
+ const hay =
23
+ `${t.id} ${t.name} ${t.description} ${t.tags.join(' ')}`.toLowerCase();
24
+ return hay.includes(q);
25
+ }
26
+
27
+ export function TemplatesMarketingPage() {
28
+ const [query, setQuery] = useState('');
29
+
30
+ const templates = useMemo(() => listTemplates(), []);
31
+ const filtered = useMemo(
32
+ () => templates.filter((t) => matchesQuery(t, query)),
33
+ [templates, query]
34
+ );
35
+
36
+ return (
37
+ <>
38
+ <MarketingSection tone="default">
39
+ <VStack as="header" gap="lg" align="center">
40
+ <VStack gap="sm" align="center">
41
+ <ButtonLink href="/docs" variant="ghost">
42
+ Docs
43
+ </ButtonLink>
44
+ <ButtonLink href="/sandbox" variant="ghost">
45
+ Open Sandbox
46
+ </ButtonLink>
47
+ </VStack>
48
+ <VStack gap="sm" align="center">
49
+ <ButtonLink href="/templates" variant="default">
50
+ Templates
51
+ </ButtonLink>
52
+ </VStack>
53
+ </VStack>
54
+ </MarketingSection>
55
+
56
+ <MarketingSection tone="muted">
57
+ <VStack gap="lg">
58
+ <VStack gap="sm">
59
+ <ButtonLink href="/templates" variant="ghost">
60
+ Browse all examples
61
+ </ButtonLink>
62
+ </VStack>
63
+ <HStack gap="md" align="center" justify="between" wrap="wrap">
64
+ <Input
65
+ aria-label="Search templates and examples"
66
+ placeholder="Search templates and examples…"
67
+ value={query}
68
+ onChange={(e) => setQuery(e.target.value)}
69
+ />
70
+ </HStack>
71
+ </VStack>
72
+ </MarketingSection>
73
+
74
+ <MarketingSection tone="default">
75
+ <VStack gap="lg">
76
+ <HStack gap="md" wrap="wrap">
77
+ {filtered.map((t) => (
78
+ <MarketingCard
79
+ key={t.id}
80
+ className="w-full md:w-[calc(50%-0.75rem)] lg:w-[calc(33.333%-1rem)]"
81
+ >
82
+ <MarketingCardHeader>
83
+ <MarketingCardTitle>
84
+ {t.icon} {t.name}
85
+ </MarketingCardTitle>
86
+ <MarketingCardDescription>
87
+ {t.description}
88
+ </MarketingCardDescription>
89
+ </MarketingCardHeader>
90
+ <MarketingCardContent>
91
+ <VStack gap="md">
92
+ <HStack gap="sm" wrap="wrap">
93
+ {t.tags.slice(0, 6).map((tag: string) => (
94
+ <ButtonLink
95
+ key={`${t.id}-${tag}`}
96
+ href={`/templates?tag=${encodeURIComponent(tag)}`}
97
+ variant="ghost"
98
+ >
99
+ {tag}
100
+ </ButtonLink>
101
+ ))}
102
+ </HStack>
103
+ <HStack gap="sm" justify="between" wrap="wrap">
104
+ <ButtonLinkToSandbox templateId={t.id} />
105
+ <Button variant="outline" onClick={() => void 0} disabled>
106
+ Install to Studio (soon)
107
+ </Button>
108
+ </HStack>
109
+ </VStack>
110
+ </MarketingCardContent>
111
+ </MarketingCard>
112
+ ))}
113
+ </HStack>
114
+ </VStack>
115
+ </MarketingSection>
116
+ </>
117
+ );
118
+ }
119
+
120
+ function ButtonLinkToSandbox({ templateId }: { templateId: string }) {
121
+ return (
122
+ <ButtonLink
123
+ href={`/sandbox?template=${encodeURIComponent(templateId)}`}
124
+ variant="default"
125
+ >
126
+ Preview
127
+ </ButtonLink>
128
+ );
129
+ }
@@ -0,0 +1,260 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+
5
+ import dynamic from 'next/dynamic';
6
+ import { Dialog, DialogContent } from '@contractspec/lib.ui-kit-web/ui/dialog';
7
+ import { ScrollArea } from '@contractspec/lib.ui-kit-web/ui/scroll-area';
8
+ import { LoadingSpinner } from '@contractspec/lib.ui-kit-web/ui/atoms/LoadingSpinner';
9
+ import type { TemplateId } from '@contractspec/lib.example-shared-ui';
10
+
11
+ // Dynamically import template components with ssr: false
12
+ const TemplateShell = dynamic(
13
+ () =>
14
+ import('@contractspec/lib.example-shared-ui').then(
15
+ (mod) => mod.TemplateShell
16
+ ),
17
+ { ssr: false, loading: () => <LoadingSpinner /> }
18
+ );
19
+
20
+ const TodosTaskList = dynamic(
21
+ () =>
22
+ import('@contractspec/bundle.library/components/templates/todos/TaskList').then(
23
+ (mod) => mod.TaskList
24
+ ),
25
+ { ssr: false, loading: () => <LoadingSpinner /> }
26
+ );
27
+
28
+ const MessagingWorkspace = dynamic(
29
+ () =>
30
+ import('@contractspec/bundle.library/components/templates/messaging/MessagingWorkspace').then(
31
+ (mod) => mod.MessagingWorkspace
32
+ ),
33
+ { ssr: false, loading: () => <LoadingSpinner /> }
34
+ );
35
+
36
+ const RecipesExperience = dynamic(
37
+ () =>
38
+ import('@contractspec/bundle.library/components/templates/recipes/RecipeList').then(
39
+ (mod) => mod.RecipeList
40
+ ),
41
+ { ssr: false, loading: () => <LoadingSpinner /> }
42
+ );
43
+
44
+ const SaasDashboard = dynamic(
45
+ () =>
46
+ import('@contractspec/example.saas-boilerplate').then(
47
+ (mod) => mod.SaasDashboard
48
+ ),
49
+ { ssr: false, loading: () => <LoadingSpinner /> }
50
+ );
51
+
52
+ const CrmDashboard = dynamic(
53
+ () =>
54
+ import('@contractspec/example.crm-pipeline').then(
55
+ (mod) => mod.CrmDashboard
56
+ ),
57
+ { ssr: false, loading: () => <LoadingSpinner /> }
58
+ );
59
+
60
+ const AgentDashboard = dynamic(
61
+ () =>
62
+ import('@contractspec/example.agent-console/ui').then(
63
+ (mod) => mod.AgentDashboard
64
+ ),
65
+ { ssr: false, loading: () => <LoadingSpinner /> }
66
+ );
67
+
68
+ const WorkflowDashboard = dynamic(
69
+ () =>
70
+ import('@contractspec/example.workflow-system/ui').then(
71
+ (mod) => mod.WorkflowDashboard
72
+ ),
73
+ { ssr: false, loading: () => <LoadingSpinner /> }
74
+ );
75
+
76
+ const MarketplaceDashboard = dynamic(
77
+ () =>
78
+ import('@contractspec/example.marketplace/ui').then(
79
+ (mod) => mod.MarketplaceDashboard
80
+ ),
81
+ { ssr: false, loading: () => <LoadingSpinner /> }
82
+ );
83
+
84
+ const IntegrationDashboard = dynamic(
85
+ () =>
86
+ import('@contractspec/example.integration-hub/ui').then(
87
+ (mod) => mod.IntegrationDashboard
88
+ ),
89
+ { ssr: false, loading: () => <LoadingSpinner /> }
90
+ );
91
+
92
+ const AnalyticsDashboard = dynamic(
93
+ () =>
94
+ import('@contractspec/example.analytics-dashboard').then(
95
+ (mod) => mod.AnalyticsDashboard
96
+ ),
97
+ { ssr: false, loading: () => <LoadingSpinner /> }
98
+ );
99
+
100
+ interface TemplatePreviewModalProps {
101
+ templateId: TemplateId | null;
102
+ onClose: () => void;
103
+ }
104
+ //
105
+ // return (
106
+ // <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-4 py-10">
107
+ // <div className="bg-background relative h-full max-h-[90vh] w-full max-w-5xl overflow-y-auto rounded-3xl p-6 shadow-2xl">
108
+ // <button
109
+ // type="button"
110
+ // className="btn-ghost absolute top-4 right-4 text-sm"
111
+ // onClick={onClose}
112
+ // >
113
+ // Close
114
+ // </button>
115
+ // {previewComponent}
116
+ // </div>
117
+ // </div>
118
+ // );
119
+ // };
120
+
121
+ export const TemplatePreviewModal = ({
122
+ templateId,
123
+ onClose,
124
+ }: TemplatePreviewModalProps) => {
125
+ const previewComponent = useMemo(() => {
126
+ switch (templateId) {
127
+ case 'todos-app':
128
+ return (
129
+ <TemplateShell
130
+ title="Starter tasks"
131
+ description="Track work items with filters, priorities, and per-tenant data isolation."
132
+ showSaveAction={false}
133
+ >
134
+ <TodosTaskList />
135
+ </TemplateShell>
136
+ );
137
+ case 'messaging-app':
138
+ return (
139
+ <TemplateShell
140
+ title="Messaging workspace"
141
+ description="Realtime-ready messaging surface with optimistic delivery."
142
+ showSaveAction={false}
143
+ >
144
+ <MessagingWorkspace />
145
+ </TemplateShell>
146
+ );
147
+ case 'recipe-app-i18n':
148
+ return (
149
+ <TemplateShell
150
+ title="Ceremony recipes"
151
+ description="Switch locales and preview how rituals translate across teams."
152
+ showSaveAction={false}
153
+ >
154
+ <RecipesExperience />
155
+ </TemplateShell>
156
+ );
157
+ case 'saas-boilerplate':
158
+ return (
159
+ <TemplateShell
160
+ title="SaaS Boilerplate"
161
+ description="Multi-tenant organizations, projects, settings, and billing usage tracking."
162
+ showSaveAction={false}
163
+ >
164
+ <SaasDashboard />
165
+ </TemplateShell>
166
+ );
167
+ case 'crm-pipeline':
168
+ return (
169
+ <TemplateShell
170
+ title="CRM Pipeline"
171
+ description="Sales CRM with contacts, companies, deals, and pipeline stages."
172
+ showSaveAction={false}
173
+ >
174
+ <CrmDashboard />
175
+ </TemplateShell>
176
+ );
177
+ case 'agent-console':
178
+ return (
179
+ <TemplateShell
180
+ title="AI Agent Console"
181
+ description="AI agent orchestration with tools, agents, runs, and execution logs."
182
+ showSaveAction={false}
183
+ >
184
+ <AgentDashboard />
185
+ </TemplateShell>
186
+ );
187
+ case 'workflow-system':
188
+ return (
189
+ <TemplateShell
190
+ title="Workflow System"
191
+ description="Multi-step workflows with role-based approvals."
192
+ showSaveAction={false}
193
+ >
194
+ <WorkflowDashboard />
195
+ </TemplateShell>
196
+ );
197
+ case 'marketplace':
198
+ return (
199
+ <TemplateShell
200
+ title="Marketplace"
201
+ description="Two-sided marketplace with stores, products, and orders."
202
+ showSaveAction={false}
203
+ >
204
+ <MarketplaceDashboard />
205
+ </TemplateShell>
206
+ );
207
+ case 'integration-hub':
208
+ return (
209
+ <TemplateShell
210
+ title="Integration Hub"
211
+ description="Third-party integrations with sync and field mapping."
212
+ showSaveAction={false}
213
+ >
214
+ <IntegrationDashboard />
215
+ </TemplateShell>
216
+ );
217
+ case 'analytics-dashboard':
218
+ return (
219
+ <TemplateShell
220
+ title="Analytics Dashboard"
221
+ description="Custom dashboards with widgets and queries."
222
+ showSaveAction={false}
223
+ >
224
+ <AnalyticsDashboard />
225
+ </TemplateShell>
226
+ );
227
+ case null:
228
+ return null;
229
+ default:
230
+ return null;
231
+ }
232
+ }, [templateId]);
233
+
234
+ return (
235
+ <Dialog open={!!previewComponent} onOpenChange={onClose}>
236
+ {/*<DialogTrigger asChild>*/}
237
+ {/* <Button variant="outline">Fullscreen Dialog</Button>*/}
238
+ {/*</DialogTrigger>*/}
239
+ <DialogContent className="mb-8 flex h-[calc(100vh-2rem)] min-w-[calc(100vw-2rem)] flex-col justify-between gap-0 p-0">
240
+ <ScrollArea className="flex flex-col justify-between overflow-hidden">
241
+ {/*<DialogHeader className="contents space-y-0 text-left">*/}
242
+ {/* <DialogTitle className="px-6 pt-6">Product Information</DialogTitle>*/}
243
+ {/* <DialogDescription asChild>*/}
244
+ {/* </DialogDescription>*/}
245
+ {/*</DialogHeader>*/}
246
+ {previewComponent}
247
+ </ScrollArea>
248
+ {/*<DialogFooter className="px-6 pb-6 sm:justify-end">*/}
249
+ {/* <DialogClose asChild>*/}
250
+ {/* <Button variant="outline">*/}
251
+ {/* <ChevronLeftIcon />*/}
252
+ {/* Back*/}
253
+ {/* </Button>*/}
254
+ {/* </DialogClose>*/}
255
+ {/* <Button type="button">Read More</Button>*/}
256
+ {/*</DialogFooter>*/}
257
+ </DialogContent>
258
+ </Dialog>
259
+ );
260
+ };
@@ -0,0 +1,3 @@
1
+ export * from './TemplatesClientPage';
2
+ export * from './TemplatesPage';
3
+ export * from './TemplatesPreviewModal';
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * from './components/marketing';
2
+ export * from './components/templates';
3
+ // Email utilities
4
+ export { submitContactForm } from './libs/email/contact';
5
+ export { subscribeToNewsletter } from './libs/email/newsletter';
6
+ export type {
7
+ SubmitContactFormResult,
8
+ SubmitNewsletterResult,
9
+ SubmitWaitlistApplicationResult,
10
+ SubmitWaitlistResult,
11
+ } from './libs/email/types';
12
+ export { joinWaitlist } from './libs/email/waitlist';
13
+ export { submitWaitlistApplication } from './libs/email/waitlist-application';
14
+
15
+ export * from './registry';
@@ -0,0 +1,107 @@
1
+ 'use server';
2
+
3
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- Pragmatic use of non-null assertion for tests */
4
+ import { beforeEach, describe, expect, it } from 'bun:test';
5
+ import { __internal, getEmailConfig, sendEmail } from './client';
6
+
7
+ const ENV_KEYS = [
8
+ 'SCALEWAY_ACCESS_KEY',
9
+ 'SCALEWAY_SECRET_KEY',
10
+ 'SCALEWAY_PROJECT_ID',
11
+ 'SCALEWAY_ACCESS_KEY_QUEUE',
12
+ 'SCALEWAY_SECRET_KEY_QUEUE',
13
+ 'SCALEWAY_REGION',
14
+ 'SCALEWAY_EMAIL_FROM_EMAIL',
15
+ 'SCALEWAY_EMAIL_FROM_NAME',
16
+ 'SCALEWAY_EMAIL_TEAM_EMAIL',
17
+ 'SCALEWAY_EMAIL_TEAM_NAME',
18
+ ];
19
+
20
+ const clearEnv = () => {
21
+ ENV_KEYS.forEach((key) => {
22
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Pragmatic use for environment cleanup in tests
23
+ delete process.env[key];
24
+ });
25
+ };
26
+
27
+ const setEnv = (values: Partial<Record<string, string>>) => {
28
+ Object.entries(values).forEach(([key, value]) => {
29
+ process.env[key] = value;
30
+ });
31
+ };
32
+
33
+ describe('email client config', () => {
34
+ beforeEach(() => {
35
+ __internal.resetCaches();
36
+ clearEnv();
37
+ });
38
+
39
+ it('returns a failure result when credentials are missing', () => {
40
+ const result = getEmailConfig();
41
+ expect(result.ok).toBeFalse();
42
+ expect(result.config).toBeUndefined();
43
+ });
44
+
45
+ it('prefers Scaleway env variables for config and region mapping', () => {
46
+ setEnv({
47
+ SCALEWAY_ACCESS_KEY: 'access',
48
+ SCALEWAY_SECRET_KEY: 'secret',
49
+ SCALEWAY_PROJECT_ID: 'project-123',
50
+ SCALEWAY_EMAIL_FROM_EMAIL: 'from@example.com',
51
+ SCALEWAY_EMAIL_FROM_NAME: 'From Name',
52
+ SCALEWAY_EMAIL_TEAM_EMAIL: 'team@example.com',
53
+ SCALEWAY_EMAIL_TEAM_NAME: 'Team Name',
54
+ });
55
+
56
+ const result = getEmailConfig();
57
+ expect(result.ok).toBeTrue();
58
+ expect(result.config?.accessKey).toBe('access');
59
+ expect(result.config?.secretKey).toBe('secret');
60
+ expect(result.config?.projectId).toBe('project-123');
61
+ expect(result.config?.region).toBe('fr-par');
62
+ expect(result.config?.from.email).toBe('from@example.com');
63
+ expect(result.config?.from.name).toBe('From Name');
64
+ expect(result.config?.teamInbox.email).toBe('team@example.com');
65
+ expect(result.config?.teamInbox.name).toBe('Team Name');
66
+ });
67
+
68
+ it('uses provided API factory to send email with reply-to header', async () => {
69
+ setEnv({
70
+ SCALEWAY_ACCESS_KEY: 'access',
71
+ SCALEWAY_SECRET_KEY: 'secret',
72
+ SCALEWAY_PROJECT_ID: 'project-123',
73
+ });
74
+
75
+ let captured: unknown = null;
76
+
77
+ __internal.setClient({
78
+ async createEmail(request: unknown) {
79
+ captured = request;
80
+ return { emails: [] };
81
+ },
82
+ });
83
+
84
+ const config = getEmailConfig().config!;
85
+
86
+ const response = await sendEmail(config, {
87
+ to: [{ email: 'user@example.com' }],
88
+ subject: 'Subject',
89
+ text: 'Plain text',
90
+ html: '<p>html</p>',
91
+ replyTo: 'reply@example.com',
92
+ context: 'test',
93
+ });
94
+
95
+ expect(response.success).toBeTrue();
96
+ const createEmailRequest = captured as Record<string, unknown>;
97
+ expect(createEmailRequest.region).toBe(config.region);
98
+ expect(createEmailRequest.projectId).toBe(config.projectId);
99
+ expect(createEmailRequest.from).toStrictEqual(config.from);
100
+ expect(createEmailRequest.to).toStrictEqual([
101
+ { email: 'user@example.com' },
102
+ ]);
103
+ expect(createEmailRequest.additionalHeaders).toStrictEqual([
104
+ { key: 'Reply-To', value: 'reply@example.com' },
105
+ ]);
106
+ });
107
+ });
@@ -0,0 +1,146 @@
1
+ import { createClient, Temv1alpha1 } from '@scaleway/sdk';
2
+ import type { Region } from '@scaleway/sdk-client';
3
+ import { Logger } from '@contractspec/lib.logger';
4
+ import type {
5
+ EmailAddress,
6
+ EmailConfigResult,
7
+ EmailSendOutcome,
8
+ EmailServiceConfig,
9
+ SendEmailRequest,
10
+ } from './types';
11
+
12
+ const DEFAULT_FROM: EmailAddress = {
13
+ email: 'noreply@transactional.contractspec.io',
14
+ name: 'ContractSpec',
15
+ };
16
+
17
+ const DEFAULT_TEAM_INBOX: EmailAddress = {
18
+ email: 'contact@contractspec.io',
19
+ name: 'ContractSpec Team',
20
+ };
21
+
22
+ const DEFAULT_REGION: Region = 'fr-par';
23
+
24
+ type EmailApi = Pick<Temv1alpha1.API, 'createEmail'>;
25
+
26
+ let cachedConfig: EmailServiceConfig | null = null;
27
+ let cachedClient: EmailApi | null = null;
28
+ let apiFactory: (client: ReturnType<typeof createClient>) => EmailApi = (
29
+ client
30
+ ) => new Temv1alpha1.API(client);
31
+
32
+ const mapRegion = (value?: string | null): Region => {
33
+ const normalized = value?.trim().toLowerCase();
34
+ if (normalized === 'par' || normalized === 'fr-par') return 'fr-par';
35
+ if (normalized === 'ams' || normalized === 'nl-ams') return 'nl-ams';
36
+ if (normalized === 'waw' || normalized === 'pl-waw') return 'pl-waw';
37
+ return DEFAULT_REGION;
38
+ };
39
+
40
+ export const getEmailConfig = (): EmailConfigResult => {
41
+ if (cachedConfig) {
42
+ return { ok: true, config: cachedConfig };
43
+ }
44
+
45
+ const accessKey =
46
+ process.env.SCALEWAY_ACCESS_KEY || process.env.SCALEWAY_ACCESS_KEY_QUEUE;
47
+ const secretKey =
48
+ process.env.SCALEWAY_SECRET_KEY || process.env.SCALEWAY_SECRET_KEY_QUEUE;
49
+ const projectId = process.env.SCALEWAY_PROJECT_ID;
50
+
51
+ if (!accessKey || !secretKey || !projectId) {
52
+ return {
53
+ ok: false,
54
+ errorMessage:
55
+ 'Email service is not configured. Please contact us directly at contact@contractspec.io.',
56
+ };
57
+ }
58
+
59
+ const region = mapRegion(process.env.SCALEWAY_REGION);
60
+
61
+ cachedConfig = {
62
+ accessKey,
63
+ secretKey,
64
+ projectId,
65
+ region,
66
+ defaultZone: `${region}-1`,
67
+ from: {
68
+ email: process.env.SCALEWAY_EMAIL_FROM_EMAIL ?? DEFAULT_FROM.email,
69
+ name: process.env.SCALEWAY_EMAIL_FROM_NAME ?? DEFAULT_FROM.name,
70
+ },
71
+ teamInbox: {
72
+ email: process.env.SCALEWAY_EMAIL_TEAM_EMAIL ?? DEFAULT_TEAM_INBOX.email,
73
+ name: process.env.SCALEWAY_EMAIL_TEAM_NAME ?? DEFAULT_TEAM_INBOX.name,
74
+ },
75
+ };
76
+
77
+ return { ok: true, config: cachedConfig };
78
+ };
79
+
80
+ const getTemClient = (config: EmailServiceConfig): EmailApi => {
81
+ if (cachedClient) {
82
+ return cachedClient;
83
+ }
84
+
85
+ const client = createClient({
86
+ accessKey: config.accessKey,
87
+ secretKey: config.secretKey,
88
+ defaultProjectId: config.projectId,
89
+ defaultRegion: config.region,
90
+ defaultZone: config.defaultZone,
91
+ });
92
+
93
+ cachedClient = apiFactory(client);
94
+ return cachedClient;
95
+ };
96
+
97
+ export const sendEmail = async (
98
+ config: EmailServiceConfig,
99
+ request: SendEmailRequest
100
+ ): Promise<EmailSendOutcome> => {
101
+ try {
102
+ const client = getTemClient(config);
103
+
104
+ await client.createEmail({
105
+ region: config.region,
106
+ projectId: config.projectId,
107
+ from: config.from,
108
+ to: request.to,
109
+ subject: request.subject,
110
+ text: request.text,
111
+ html: request.html || request.text,
112
+ additionalHeaders: request.replyTo
113
+ ? [{ key: 'Reply-To', value: request.replyTo }]
114
+ : undefined,
115
+ });
116
+
117
+ return { success: true };
118
+ } catch (error) {
119
+ new Logger().error('scaleway_tem_email_send_failed', {
120
+ context: request.context ?? 'email',
121
+ error: error instanceof Error ? error.message : error,
122
+ });
123
+ return {
124
+ success: false,
125
+ error,
126
+ errorMessage: 'Failed to send email via Scaleway.',
127
+ };
128
+ }
129
+ };
130
+
131
+ export const __internal = {
132
+ resetCaches() {
133
+ cachedClient = null;
134
+ cachedConfig = null;
135
+ apiFactory = (client: ReturnType<typeof createClient>) =>
136
+ new Temv1alpha1.API(client);
137
+ },
138
+ setApiFactory(
139
+ factory: (client: ReturnType<typeof createClient>) => EmailApi
140
+ ) {
141
+ apiFactory = factory;
142
+ },
143
+ setClient(client: EmailApi) {
144
+ cachedClient = client;
145
+ },
146
+ };