@contractspec/bundle.marketing 3.8.7 → 3.8.9

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 (76) hide show
  1. package/.turbo/turbo-build.log +64 -32
  2. package/CHANGELOG.md +63 -0
  3. package/dist/browser/components/templates/TemplateCard.js +83 -0
  4. package/dist/browser/components/templates/TemplateCommandDialog.js +110 -0
  5. package/dist/browser/components/templates/TemplatePreviewContent.js +96 -0
  6. package/dist/browser/components/templates/TemplatesBrowseControls.js +115 -0
  7. package/dist/browser/components/templates/TemplatesCatalogSection.js +284 -0
  8. package/dist/browser/components/templates/TemplatesClientPage.js +840 -917
  9. package/dist/browser/components/templates/TemplatesHeroSection.js +87 -0
  10. package/dist/browser/components/templates/TemplatesNextStepsSection.js +126 -0
  11. package/dist/browser/components/templates/TemplatesPreviewModal.js +136 -126
  12. package/dist/browser/components/templates/index.js +873 -950
  13. package/dist/browser/components/templates/template-catalog.js +81 -0
  14. package/dist/browser/components/templates/template-new.js +23 -0
  15. package/dist/browser/components/templates/template-preview.js +43 -0
  16. package/dist/browser/components/templates/template-source.js +19 -0
  17. package/dist/browser/index.js +873 -950
  18. package/dist/components/templates/TemplateCard.d.ts +12 -0
  19. package/dist/components/templates/TemplateCard.js +78 -0
  20. package/dist/components/templates/TemplateCommandDialog.d.ts +6 -0
  21. package/dist/components/templates/TemplateCommandDialog.js +105 -0
  22. package/dist/components/templates/TemplatePreviewContent.d.ts +5 -0
  23. package/dist/components/templates/TemplatePreviewContent.js +91 -0
  24. package/dist/components/templates/TemplatesBrowseControls.d.ts +13 -0
  25. package/dist/components/templates/TemplatesBrowseControls.js +110 -0
  26. package/dist/components/templates/TemplatesCatalogSection.d.ts +14 -0
  27. package/dist/components/templates/TemplatesCatalogSection.js +279 -0
  28. package/dist/components/templates/TemplatesClientPage.js +840 -917
  29. package/dist/components/templates/TemplatesHeroSection.d.ts +5 -0
  30. package/dist/components/templates/TemplatesHeroSection.js +82 -0
  31. package/dist/components/templates/TemplatesNextStepsSection.d.ts +1 -0
  32. package/dist/components/templates/TemplatesNextStepsSection.js +121 -0
  33. package/dist/components/templates/TemplatesPreviewModal.d.ts +3 -4
  34. package/dist/components/templates/TemplatesPreviewModal.js +136 -126
  35. package/dist/components/templates/index.js +873 -950
  36. package/dist/components/templates/template-catalog.d.ts +27 -0
  37. package/dist/components/templates/template-catalog.js +76 -0
  38. package/dist/components/templates/template-catalog.test.d.ts +1 -0
  39. package/dist/components/templates/template-new.d.ts +2 -0
  40. package/dist/components/templates/template-new.js +18 -0
  41. package/dist/components/templates/template-preview.d.ts +18 -0
  42. package/dist/components/templates/template-preview.js +38 -0
  43. package/dist/components/templates/template-source.d.ts +3 -0
  44. package/dist/components/templates/template-source.js +14 -0
  45. package/dist/index.js +873 -950
  46. package/dist/node/components/templates/TemplateCard.js +78 -0
  47. package/dist/node/components/templates/TemplateCommandDialog.js +105 -0
  48. package/dist/node/components/templates/TemplatePreviewContent.js +91 -0
  49. package/dist/node/components/templates/TemplatesBrowseControls.js +110 -0
  50. package/dist/node/components/templates/TemplatesCatalogSection.js +279 -0
  51. package/dist/node/components/templates/TemplatesClientPage.js +840 -917
  52. package/dist/node/components/templates/TemplatesHeroSection.js +82 -0
  53. package/dist/node/components/templates/TemplatesNextStepsSection.js +121 -0
  54. package/dist/node/components/templates/TemplatesPreviewModal.js +136 -126
  55. package/dist/node/components/templates/index.js +873 -950
  56. package/dist/node/components/templates/template-catalog.js +76 -0
  57. package/dist/node/components/templates/template-new.js +18 -0
  58. package/dist/node/components/templates/template-preview.js +38 -0
  59. package/dist/node/components/templates/template-source.js +14 -0
  60. package/dist/node/index.js +873 -950
  61. package/package.json +185 -30
  62. package/src/components/templates/TemplateCard.tsx +74 -0
  63. package/src/components/templates/TemplateCommandDialog.tsx +92 -0
  64. package/src/components/templates/TemplatePreviewContent.tsx +182 -0
  65. package/src/components/templates/TemplatesBrowseControls.tsx +120 -0
  66. package/src/components/templates/TemplatesCatalogSection.tsx +166 -0
  67. package/src/components/templates/TemplatesClientPage.tsx +109 -741
  68. package/src/components/templates/TemplatesHeroSection.tsx +41 -0
  69. package/src/components/templates/TemplatesNextStepsSection.tsx +80 -0
  70. package/src/components/templates/TemplatesPreviewModal.tsx +19 -294
  71. package/src/components/templates/template-catalog.test.ts +66 -0
  72. package/src/components/templates/template-catalog.ts +132 -0
  73. package/src/components/templates/template-new.ts +12 -0
  74. package/src/components/templates/template-preview.ts +57 -0
  75. package/src/components/templates/template-source.ts +13 -0
  76. package/.turbo/turbo-prebuild.log +0 -1
@@ -0,0 +1,41 @@
1
+ export interface TemplatesHeroSectionProps {
2
+ localTemplateCount: number;
3
+ sourceCount: number;
4
+ }
5
+
6
+ export function TemplatesHeroSection({
7
+ localTemplateCount,
8
+ sourceCount,
9
+ }: TemplatesHeroSectionProps) {
10
+ return (
11
+ <section className="section-padding hero-gradient border-border/70 border-b">
12
+ <div className="editorial-shell space-y-8">
13
+ <div className="max-w-4xl space-y-5">
14
+ <p className="editorial-kicker">Proof through real scenarios</p>
15
+ <h1 className="editorial-title">
16
+ Templates that show the open system in practice.
17
+ </h1>
18
+ <p className="editorial-subtitle">
19
+ These scenarios are the fastest way to understand ContractSpec:
20
+ explicit contracts, aligned surfaces, and an adoption path from OSS
21
+ exploration into Studio deployment.
22
+ </p>
23
+ </div>
24
+ <div className="editorial-proof-strip">
25
+ <div className="editorial-stat">
26
+ <span className="editorial-stat-value">{localTemplateCount}</span>
27
+ <span className="editorial-label">curated scenarios</span>
28
+ </div>
29
+ <div className="editorial-stat">
30
+ <span className="editorial-stat-value">{sourceCount}</span>
31
+ <span className="editorial-label">entry paths</span>
32
+ </div>
33
+ <div className="editorial-stat">
34
+ <span className="editorial-stat-value">OSS</span>
35
+ <span className="editorial-label">first, Studio second</span>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </section>
40
+ );
41
+ }
@@ -0,0 +1,80 @@
1
+ import Link from 'next/link';
2
+
3
+ export function TemplatesNextStepsSection() {
4
+ return (
5
+ <section className="editorial-section bg-striped">
6
+ <div className="editorial-shell space-y-8">
7
+ <div className="max-w-3xl space-y-4">
8
+ <p className="editorial-kicker">From template to real system</p>
9
+ <h2 className="font-serif text-4xl tracking-[-0.04em] md:text-5xl">
10
+ Templates become useful when the system can absorb more context.
11
+ </h2>
12
+ <p className="editorial-copy">
13
+ Use templates to prove the base flow, then layer integrations,
14
+ knowledge, and runtime behavior on top without losing the same
15
+ contract source.
16
+ </p>
17
+ </div>
18
+ <div className="grid gap-6 md:grid-cols-3">
19
+ <div className="editorial-panel space-y-4">
20
+ <div className="text-3xl">💳</div>
21
+ <h3 className="font-serif text-2xl tracking-[-0.03em]">
22
+ Add payments
23
+ </h3>
24
+ <p className="text-muted-foreground text-sm">
25
+ Connect Stripe to any template for payment processing,
26
+ subscriptions, and invoicing. Type-safe and policy-enforced.
27
+ </p>
28
+ <Link
29
+ href="/docs/integrations/stripe"
30
+ className="font-medium text-[color:var(--blue)] text-sm hover:opacity-80"
31
+ >
32
+ Learn more →
33
+ </Link>
34
+ </div>
35
+ <div className="editorial-panel space-y-4">
36
+ <div className="text-3xl">📧</div>
37
+ <h3 className="font-serif text-2xl tracking-[-0.03em]">
38
+ Add notifications
39
+ </h3>
40
+ <p className="text-muted-foreground text-sm">
41
+ Send transactional emails via Postmark or Resend. Process inbound
42
+ emails with Gmail API. SMS via Twilio.
43
+ </p>
44
+ <Link
45
+ href="/docs/integrations"
46
+ className="font-medium text-[color:var(--blue)] text-sm hover:opacity-80"
47
+ >
48
+ View integrations →
49
+ </Link>
50
+ </div>
51
+ <div className="editorial-panel space-y-4">
52
+ <div className="text-3xl">🧠</div>
53
+ <h3 className="font-serif text-2xl tracking-[-0.03em]">
54
+ Add AI and knowledge
55
+ </h3>
56
+ <p className="text-muted-foreground text-sm">
57
+ Power templates with OpenAI, vector search via Qdrant, and
58
+ structured knowledge spaces for context-aware workflows.
59
+ </p>
60
+ <Link
61
+ href="/docs/knowledge"
62
+ className="font-medium text-[color:var(--blue)] text-sm hover:opacity-80"
63
+ >
64
+ Learn about knowledge →
65
+ </Link>
66
+ </div>
67
+ </div>
68
+ <div className="pt-4 text-center">
69
+ <p className="mb-4 text-muted-foreground text-sm">
70
+ All integrations are configured per-tenant with automatic health
71
+ checks and credential rotation.
72
+ </p>
73
+ <Link href="/docs/architecture" className="btn-primary">
74
+ View Architecture
75
+ </Link>
76
+ </div>
77
+ </div>
78
+ </section>
79
+ );
80
+ }
@@ -1,313 +1,38 @@
1
1
  'use client';
2
2
 
3
3
  import type { TemplateId } from '@contractspec/lib.example-shared-ui';
4
- import { LoadingSpinner } from '@contractspec/lib.ui-kit-web/ui/atoms/LoadingSpinner';
5
4
  import { Dialog, DialogContent } from '@contractspec/lib.ui-kit-web/ui/dialog';
6
5
  import { ScrollArea } from '@contractspec/lib.ui-kit-web/ui/scroll-area';
7
- import dynamic from 'next/dynamic';
8
- import { useMemo } from 'react';
6
+ import { TemplateRuntimeProvider } from '@contractspec/module.examples';
7
+ import { TemplatePreviewContent } from './TemplatePreviewContent';
8
+ import { supportsInlineTemplatePreview } from './template-preview';
9
9
 
10
- // Dynamically import template components with ssr: false
11
- const TemplateShell = dynamic(
12
- () =>
13
- import('@contractspec/lib.example-shared-ui').then(
14
- (mod) => mod.TemplateShell
15
- ),
16
- { ssr: false, loading: () => <LoadingSpinner /> }
17
- );
18
-
19
- const TodosTaskList = dynamic(
20
- () =>
21
- import(
22
- '@contractspec/bundle.library/components/templates/todos/TaskList'
23
- ).then((mod) => mod.TaskList),
24
- { ssr: false, loading: () => <LoadingSpinner /> }
25
- );
26
-
27
- const MessagingWorkspace = dynamic(
28
- () =>
29
- import(
30
- '@contractspec/bundle.library/components/templates/messaging/MessagingWorkspace'
31
- ).then((mod) => mod.MessagingWorkspace),
32
- { ssr: false, loading: () => <LoadingSpinner /> }
33
- );
34
-
35
- const RecipesExperience = dynamic(
36
- () =>
37
- import(
38
- '@contractspec/bundle.library/components/templates/recipes/RecipeList'
39
- ).then((mod) => mod.RecipeList),
40
- { ssr: false, loading: () => <LoadingSpinner /> }
41
- );
42
-
43
- const SaasDashboard = dynamic(
44
- () =>
45
- import('@contractspec/example.saas-boilerplate').then(
46
- (mod) => mod.SaasDashboard
47
- ),
48
- { ssr: false, loading: () => <LoadingSpinner /> }
49
- );
50
-
51
- const CrmDashboard = dynamic(
52
- () =>
53
- import('@contractspec/example.crm-pipeline').then(
54
- (mod) => mod.CrmDashboard
55
- ),
56
- { ssr: false, loading: () => <LoadingSpinner /> }
57
- );
58
-
59
- const DataGridShowcase = dynamic(
60
- () =>
61
- import('@contractspec/example.data-grid-showcase/ui').then(
62
- (mod) => mod.DataGridShowcase
63
- ),
64
- { ssr: false, loading: () => <LoadingSpinner /> }
65
- );
66
-
67
- const VisualizationShowcase = dynamic(
68
- () =>
69
- import('@contractspec/example.visualization-showcase/ui').then(
70
- (mod) => mod.VisualizationShowcase
71
- ),
72
- { ssr: false, loading: () => <LoadingSpinner /> }
73
- );
74
-
75
- const AgentDashboard = dynamic(
76
- () =>
77
- import('@contractspec/example.agent-console/ui').then(
78
- (mod) => mod.AgentDashboard
79
- ),
80
- { ssr: false, loading: () => <LoadingSpinner /> }
81
- );
82
-
83
- const WorkflowDashboard = dynamic(
84
- () =>
85
- import('@contractspec/example.workflow-system/ui').then(
86
- (mod) => mod.WorkflowDashboard
87
- ),
88
- { ssr: false, loading: () => <LoadingSpinner /> }
89
- );
90
-
91
- const MarketplaceDashboard = dynamic(
92
- () =>
93
- import('@contractspec/example.marketplace/ui').then(
94
- (mod) => mod.MarketplaceDashboard
95
- ),
96
- { ssr: false, loading: () => <LoadingSpinner /> }
97
- );
98
-
99
- const IntegrationDashboard = dynamic(
100
- () =>
101
- import('@contractspec/example.integration-hub/ui').then(
102
- (mod) => mod.IntegrationDashboard
103
- ),
104
- { ssr: false, loading: () => <LoadingSpinner /> }
105
- );
106
-
107
- const AnalyticsDashboard = dynamic(
108
- () =>
109
- import('@contractspec/example.analytics-dashboard').then(
110
- (mod) => mod.AnalyticsDashboard
111
- ),
112
- { ssr: false, loading: () => <LoadingSpinner /> }
113
- );
114
-
115
- const AiChatAssistantDashboard = dynamic(
116
- () =>
117
- import('@contractspec/example.ai-chat-assistant').then(
118
- (mod) => mod.AiChatAssistantDashboard
119
- ),
120
- { ssr: false, loading: () => <LoadingSpinner /> }
121
- );
122
-
123
- interface TemplatePreviewModalProps {
124
- templateId: TemplateId | null;
10
+ export interface TemplatePreviewModalProps {
11
+ templateId: TemplateId;
125
12
  onClose: () => void;
126
13
  }
127
- //
128
- // return (
129
- // <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-4 py-10">
130
- // <div className="bg-background relative h-full max-h-[90vh] w-full max-w-5xl overflow-y-auto rounded-3xl p-6 shadow-2xl">
131
- // <button
132
- // type="button"
133
- // className="btn-ghost absolute top-4 right-4 text-sm"
134
- // onClick={onClose}
135
- // >
136
- // Close
137
- // </button>
138
- // {previewComponent}
139
- // </div>
140
- // </div>
141
- // );
142
- // };
143
14
 
144
- export const TemplatePreviewModal = ({
15
+ export function TemplatePreviewModal({
145
16
  templateId,
146
17
  onClose,
147
- }: TemplatePreviewModalProps) => {
148
- const previewComponent = useMemo(() => {
149
- switch (templateId) {
150
- case 'todos-app':
151
- return (
152
- <TemplateShell
153
- title="Starter tasks"
154
- description="Track work items with filters, priorities, and per-tenant data isolation."
155
- showSaveAction={false}
156
- >
157
- <TodosTaskList />
158
- </TemplateShell>
159
- );
160
- case 'messaging-app':
161
- return (
162
- <TemplateShell
163
- title="Messaging workspace"
164
- description="Realtime-ready messaging surface with optimistic delivery."
165
- showSaveAction={false}
166
- >
167
- <MessagingWorkspace />
168
- </TemplateShell>
169
- );
170
- case 'recipe-app-i18n':
171
- return (
172
- <TemplateShell
173
- title="Ceremony recipes"
174
- description="Switch locales and preview how rituals translate across teams."
175
- showSaveAction={false}
176
- >
177
- <RecipesExperience />
178
- </TemplateShell>
179
- );
180
- case 'saas-boilerplate':
181
- return (
182
- <TemplateShell
183
- title="SaaS Boilerplate"
184
- description="Multi-tenant organizations, projects, settings, and billing usage tracking."
185
- showSaveAction={false}
186
- >
187
- <SaasDashboard />
188
- </TemplateShell>
189
- );
190
- case 'crm-pipeline':
191
- return (
192
- <TemplateShell
193
- title="CRM Pipeline"
194
- description="Sales CRM with contacts, companies, deals, and pipeline stages."
195
- showSaveAction={false}
196
- >
197
- <CrmDashboard />
198
- </TemplateShell>
199
- );
200
- case 'data-grid-showcase':
201
- return (
202
- <TemplateShell
203
- title="Data Grid Showcase"
204
- description="Shared ContractSpec table primitives with client, server, and DataView-driven lanes."
205
- showSaveAction={false}
206
- >
207
- <DataGridShowcase />
208
- </TemplateShell>
209
- );
210
- case 'visualization-showcase':
211
- return (
212
- <TemplateShell
213
- title="Visualization Showcase"
214
- description="ContractSpec-owned chart primitives rendered through shared visualization contracts and design-system wrappers."
215
- showSaveAction={false}
216
- >
217
- <VisualizationShowcase />
218
- </TemplateShell>
219
- );
220
- case 'agent-console':
221
- return (
222
- <TemplateShell
223
- title="AI Agent Console"
224
- description="AI agent orchestration with tools, agents, runs, and execution logs."
225
- showSaveAction={false}
226
- >
227
- <AgentDashboard />
228
- </TemplateShell>
229
- );
230
- case 'ai-chat-assistant':
231
- return (
232
- <TemplateShell
233
- title="AI Chat Assistant"
234
- description="Focused assistant surface with reasoning, sources, suggestions, and MCP-aware tools."
235
- showSaveAction={false}
236
- >
237
- <AiChatAssistantDashboard />
238
- </TemplateShell>
239
- );
240
- case 'workflow-system':
241
- return (
242
- <TemplateShell
243
- title="Workflow System"
244
- description="Multi-step workflows with role-based approvals."
245
- showSaveAction={false}
246
- >
247
- <WorkflowDashboard />
248
- </TemplateShell>
249
- );
250
- case 'marketplace':
251
- return (
252
- <TemplateShell
253
- title="Marketplace"
254
- description="Two-sided marketplace with stores, products, and orders."
255
- showSaveAction={false}
256
- >
257
- <MarketplaceDashboard />
258
- </TemplateShell>
259
- );
260
- case 'integration-hub':
261
- return (
262
- <TemplateShell
263
- title="Integration Hub"
264
- description="Third-party integrations with sync and field mapping."
265
- showSaveAction={false}
266
- >
267
- <IntegrationDashboard />
268
- </TemplateShell>
269
- );
270
- case 'analytics-dashboard':
271
- return (
272
- <TemplateShell
273
- title="Analytics Dashboard"
274
- description="Custom dashboards with widgets and queries."
275
- showSaveAction={false}
276
- >
277
- <AnalyticsDashboard />
278
- </TemplateShell>
279
- );
280
- case null:
281
- return null;
282
- default:
283
- return null;
284
- }
285
- }, [templateId]);
18
+ }: TemplatePreviewModalProps) {
19
+ if (!supportsInlineTemplatePreview(templateId)) {
20
+ return null;
21
+ }
286
22
 
287
23
  return (
288
- <Dialog open={!!previewComponent} onOpenChange={onClose}>
289
- {/*<DialogTrigger asChild>*/}
290
- {/* <Button variant="outline">Fullscreen Dialog</Button>*/}
291
- {/*</DialogTrigger>*/}
24
+ <Dialog open onOpenChange={(open) => !open && onClose()}>
292
25
  <DialogContent className="mb-8 flex h-[calc(100vh-2rem)] min-w-[calc(100vw-2rem)] flex-col justify-between gap-0 p-0">
293
26
  <ScrollArea className="flex flex-col justify-between overflow-hidden">
294
- {/*<DialogHeader className="contents space-y-0 text-left">*/}
295
- {/* <DialogTitle className="px-6 pt-6">Product Information</DialogTitle>*/}
296
- {/* <DialogDescription asChild>*/}
297
- {/* </DialogDescription>*/}
298
- {/*</DialogHeader>*/}
299
- {previewComponent}
27
+ <TemplateRuntimeProvider
28
+ key={templateId}
29
+ templateId={templateId}
30
+ projectId={`marketing-preview-${templateId}`}
31
+ >
32
+ <TemplatePreviewContent templateId={templateId} />
33
+ </TemplateRuntimeProvider>
300
34
  </ScrollArea>
301
- {/*<DialogFooter className="px-6 pb-6 sm:justify-end">*/}
302
- {/* <DialogClose asChild>*/}
303
- {/* <Button variant="outline">*/}
304
- {/* <ChevronLeftIcon />*/}
305
- {/* Back*/}
306
- {/* </Button>*/}
307
- {/* </DialogClose>*/}
308
- {/* <Button type="button">Read More</Button>*/}
309
- {/*</DialogFooter>*/}
310
35
  </DialogContent>
311
36
  </Dialog>
312
37
  );
313
- };
38
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { listExamples, listTemplates } from '@contractspec/module.examples';
3
+ import {
4
+ buildLocalTemplateCatalog,
5
+ matchesTemplateFilters,
6
+ } from './template-catalog';
7
+ import { NEW_TEMPLATE_IDS } from './template-new';
8
+ import {
9
+ getAvailableTemplateSources,
10
+ isRegistryConfigured,
11
+ } from './template-source';
12
+
13
+ describe('template catalog', () => {
14
+ test('includes every public example exposed as a template', () => {
15
+ const catalog = buildLocalTemplateCatalog(listExamples(), listTemplates());
16
+ const actualIds = [...catalog].map((template) => template.id).sort();
17
+ const expectedIds = listExamples()
18
+ .filter(
19
+ (example) =>
20
+ example.meta.visibility === 'public' && example.surfaces.templates
21
+ )
22
+ .map((example) => example.meta.key)
23
+ .sort();
24
+
25
+ expect(actualIds).toEqual(expectedIds);
26
+ });
27
+
28
+ test('drops legacy conceptual cards and applies curated new badges', () => {
29
+ const catalog = buildLocalTemplateCatalog(listExamples(), listTemplates());
30
+ const ids = new Set(catalog.map((template) => template.id));
31
+ const newIds = catalog
32
+ .filter((template) => template.isNew)
33
+ .map((template) => template.id)
34
+ .sort();
35
+
36
+ expect(ids.has('plumber-ops')).toBe(false);
37
+ expect(ids.has('coliving-management')).toBe(false);
38
+ expect(ids.has('content-review')).toBe(false);
39
+ expect(newIds).toEqual([...NEW_TEMPLATE_IDS].sort());
40
+ });
41
+
42
+ test('derives searchable tags from real example metadata', () => {
43
+ const catalog = buildLocalTemplateCatalog(listExamples(), listTemplates());
44
+ const tags = new Set(catalog.flatMap((template) => template.tags));
45
+
46
+ expect(tags.has('quickstart')).toBe(true);
47
+ expect(tags.has('gradium')).toBe(true);
48
+ expect(
49
+ catalog.some((template) =>
50
+ matchesTemplateFilters(template, 'voice gradium', null)
51
+ )
52
+ ).toBe(true);
53
+ });
54
+ });
55
+
56
+ describe('template source configuration', () => {
57
+ test('only exposes community source when a registry url is configured', () => {
58
+ expect(isRegistryConfigured(undefined)).toBe(false);
59
+ expect(isRegistryConfigured(' ')).toBe(false);
60
+ expect(isRegistryConfigured('https://registry.contractspec.io')).toBe(true);
61
+ expect(getAvailableTemplateSources(undefined)).toEqual(['local']);
62
+ expect(
63
+ getAvailableTemplateSources('https://registry.contractspec.io')
64
+ ).toEqual(['local', 'registry']);
65
+ });
66
+ });
@@ -0,0 +1,132 @@
1
+ import type {
2
+ ExampleKind,
3
+ ExampleSandboxMode,
4
+ ExampleSpec,
5
+ } from '@contractspec/lib.contracts-spec/examples/types';
6
+ import type { Stability } from '@contractspec/lib.contracts-spec/ownership';
7
+ import type {
8
+ TemplateDefinition,
9
+ TemplateId,
10
+ } from '@contractspec/lib.example-shared-ui';
11
+ import { listExamples, listTemplates } from '@contractspec/module.examples';
12
+ import { isNewTemplateId, NEW_TEMPLATE_IDS } from './template-new';
13
+
14
+ export interface LocalTemplateCatalogItem {
15
+ id: TemplateId;
16
+ title: string;
17
+ description: string;
18
+ tags: string[];
19
+ kind: ExampleKind;
20
+ stability: Stability;
21
+ previewUrl: string;
22
+ featureList: string[];
23
+ sandboxModes: readonly ExampleSandboxMode[];
24
+ renderTargets: string[];
25
+ isNew: boolean;
26
+ packageName: string;
27
+ }
28
+
29
+ interface TemplateFilterCandidate {
30
+ title: string;
31
+ description: string;
32
+ tags: readonly string[];
33
+ }
34
+
35
+ const NEW_TEMPLATE_INDEX = new Map<string, number>(
36
+ NEW_TEMPLATE_IDS.map((templateId, index) => [templateId, index] as const)
37
+ );
38
+
39
+ export function buildLocalTemplateCatalog(
40
+ examples: readonly ExampleSpec[] = listExamples(),
41
+ templates: readonly TemplateDefinition[] = listTemplates()
42
+ ): LocalTemplateCatalogItem[] {
43
+ const templatesById = new Map(
44
+ templates.map((template) => [template.id, template])
45
+ );
46
+
47
+ return examples
48
+ .filter(
49
+ (example) =>
50
+ example.meta.visibility === 'public' && example.surfaces.templates
51
+ )
52
+ .map((example) => {
53
+ const template = templatesById.get(example.meta.key);
54
+ const tags = Array.from(
55
+ new Set(example.meta.tags.map((tag) => tag.trim()).filter(Boolean))
56
+ ).sort((left, right) => left.localeCompare(right));
57
+
58
+ return {
59
+ id: example.meta.key,
60
+ title: example.meta.title ?? template?.name ?? example.meta.key,
61
+ description: example.meta.summary ?? example.meta.description,
62
+ tags,
63
+ kind: example.meta.kind,
64
+ stability: example.meta.stability,
65
+ previewUrl:
66
+ template?.preview?.demoUrl ??
67
+ `/sandbox?template=${encodeURIComponent(example.meta.key)}`,
68
+ featureList: [...(template?.features ?? [])],
69
+ sandboxModes: example.surfaces.sandbox.modes,
70
+ renderTargets: [...(template?.renderTargets ?? [])],
71
+ isNew: isNewTemplateId(example.meta.key),
72
+ packageName: example.entrypoints.packageName,
73
+ };
74
+ })
75
+ .sort(compareLocalTemplateCatalogItems);
76
+ }
77
+
78
+ export function matchesTemplateFilters(
79
+ template: TemplateFilterCandidate,
80
+ search: string,
81
+ selectedTag: string | null
82
+ ): boolean {
83
+ const haystack = [
84
+ template.title,
85
+ template.description,
86
+ template.tags.join(' '),
87
+ ]
88
+ .join(' ')
89
+ .toLowerCase();
90
+ const searchTokens = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
91
+ const matchesSearch =
92
+ searchTokens.length === 0 ||
93
+ searchTokens.every((token) => haystack.includes(token));
94
+ const matchesTag =
95
+ selectedTag === null || template.tags.includes(selectedTag);
96
+
97
+ return matchesSearch && matchesTag;
98
+ }
99
+
100
+ export function formatExampleKindLabel(kind: ExampleKind): string {
101
+ return kind
102
+ .split('-')
103
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
104
+ .join(' ');
105
+ }
106
+
107
+ export function formatStabilityLabel(stability: Stability): string {
108
+ return stability
109
+ .split('_')
110
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
111
+ .join(' ');
112
+ }
113
+
114
+ function compareLocalTemplateCatalogItems(
115
+ left: LocalTemplateCatalogItem,
116
+ right: LocalTemplateCatalogItem
117
+ ): number {
118
+ const leftNewIndex = NEW_TEMPLATE_INDEX.get(left.id);
119
+ const rightNewIndex = NEW_TEMPLATE_INDEX.get(right.id);
120
+
121
+ if (leftNewIndex !== undefined || rightNewIndex !== undefined) {
122
+ if (leftNewIndex === undefined) {
123
+ return 1;
124
+ }
125
+ if (rightNewIndex === undefined) {
126
+ return -1;
127
+ }
128
+ return leftNewIndex - rightNewIndex;
129
+ }
130
+
131
+ return left.title.localeCompare(right.title);
132
+ }
@@ -0,0 +1,12 @@
1
+ export const NEW_TEMPLATE_IDS = [
2
+ 'minimal',
3
+ 'messaging-agent-actions',
4
+ 'policy-safe-knowledge-assistant',
5
+ 'visualization-showcase',
6
+ ] as const;
7
+
8
+ const NEW_TEMPLATE_ID_SET = new Set<string>(NEW_TEMPLATE_IDS);
9
+
10
+ export function isNewTemplateId(templateId: string): boolean {
11
+ return NEW_TEMPLATE_ID_SET.has(templateId);
12
+ }