@contractspec/example.saas-boilerplate 1.46.1 β†’ 1.47.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 (135) hide show
  1. package/.turbo/turbo-build$colon$bundle.log +183 -108
  2. package/.turbo/turbo-build.log +182 -107
  3. package/CHANGELOG.md +45 -0
  4. package/README.md +0 -1
  5. package/dist/billing/billing.event.d.ts +4 -4
  6. package/dist/billing/billing.event.js +1 -1
  7. package/dist/billing/billing.operations.d.ts +5 -5
  8. package/dist/billing/billing.presentation.d.ts +3 -4
  9. package/dist/billing/billing.presentation.d.ts.map +1 -1
  10. package/dist/billing/billing.presentation.js +5 -5
  11. package/dist/billing/billing.presentation.js.map +1 -1
  12. package/dist/dashboard/dashboard.presentation.d.ts +3 -4
  13. package/dist/dashboard/dashboard.presentation.d.ts.map +1 -1
  14. package/dist/dashboard/dashboard.presentation.js +5 -5
  15. package/dist/dashboard/dashboard.presentation.js.map +1 -1
  16. package/dist/example.d.ts +2 -2
  17. package/dist/example.d.ts.map +1 -1
  18. package/dist/example.js +4 -2
  19. package/dist/example.js.map +1 -1
  20. package/dist/handlers/index.d.ts +2 -1
  21. package/dist/handlers/index.js +2 -1
  22. package/dist/handlers/saas.handlers.d.ts +68 -0
  23. package/dist/handlers/saas.handlers.d.ts.map +1 -0
  24. package/dist/handlers/saas.handlers.js +148 -0
  25. package/dist/handlers/saas.handlers.js.map +1 -0
  26. package/dist/index.d.ts +13 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +13 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/project/project.enum.d.ts +3 -3
  31. package/dist/project/project.event.d.ts +22 -22
  32. package/dist/project/project.event.d.ts.map +1 -1
  33. package/dist/project/project.event.js +1 -1
  34. package/dist/project/project.operations.d.ts +103 -103
  35. package/dist/project/project.presentation.d.ts +3 -4
  36. package/dist/project/project.presentation.d.ts.map +1 -1
  37. package/dist/project/project.presentation.js +5 -5
  38. package/dist/project/project.presentation.js.map +1 -1
  39. package/dist/project/project.schema.d.ts +54 -54
  40. package/dist/saas-boilerplate.feature.d.ts +2 -2
  41. package/dist/saas-boilerplate.feature.d.ts.map +1 -1
  42. package/dist/saas-boilerplate.feature.js +9 -2
  43. package/dist/saas-boilerplate.feature.js.map +1 -1
  44. package/dist/seeders/index.d.ts +10 -0
  45. package/dist/seeders/index.d.ts.map +1 -0
  46. package/dist/seeders/index.js +19 -0
  47. package/dist/seeders/index.js.map +1 -0
  48. package/dist/settings/settings.entity.d.ts +24 -24
  49. package/dist/settings/settings.enum.d.ts +2 -2
  50. package/dist/shared/overlay-types.d.ts +34 -0
  51. package/dist/shared/overlay-types.d.ts.map +1 -0
  52. package/dist/shared/overlay-types.js +0 -0
  53. package/dist/tests/operations.test-spec.d.ts +10 -0
  54. package/dist/tests/operations.test-spec.d.ts.map +1 -0
  55. package/dist/tests/operations.test-spec.js +123 -0
  56. package/dist/tests/operations.test-spec.js.map +1 -0
  57. package/dist/ui/SaasDashboard.d.ts +7 -0
  58. package/dist/ui/SaasDashboard.d.ts.map +1 -0
  59. package/dist/ui/SaasDashboard.js +298 -0
  60. package/dist/ui/SaasDashboard.js.map +1 -0
  61. package/dist/ui/SaasProjectList.d.ts +14 -0
  62. package/dist/ui/SaasProjectList.d.ts.map +1 -0
  63. package/dist/ui/SaasProjectList.js +76 -0
  64. package/dist/ui/SaasProjectList.js.map +1 -0
  65. package/dist/ui/SaasSettingsPanel.d.ts +7 -0
  66. package/dist/ui/SaasSettingsPanel.d.ts.map +1 -0
  67. package/dist/ui/SaasSettingsPanel.js +138 -0
  68. package/dist/ui/SaasSettingsPanel.js.map +1 -0
  69. package/dist/ui/hooks/index.d.ts +3 -0
  70. package/dist/ui/hooks/index.js +6 -0
  71. package/dist/ui/hooks/useProjectList.d.ts +34 -0
  72. package/dist/ui/hooks/useProjectList.d.ts.map +1 -0
  73. package/dist/ui/hooks/useProjectList.js +75 -0
  74. package/dist/ui/hooks/useProjectList.js.map +1 -0
  75. package/dist/ui/hooks/useProjectMutations.d.ts +28 -0
  76. package/dist/ui/hooks/useProjectMutations.d.ts.map +1 -0
  77. package/dist/ui/hooks/useProjectMutations.js +146 -0
  78. package/dist/ui/hooks/useProjectMutations.js.map +1 -0
  79. package/dist/ui/index.d.ts +14 -0
  80. package/dist/ui/index.js +15 -0
  81. package/dist/ui/modals/CreateProjectModal.d.ts +23 -0
  82. package/dist/ui/modals/CreateProjectModal.d.ts.map +1 -0
  83. package/dist/ui/modals/CreateProjectModal.js +139 -0
  84. package/dist/ui/modals/CreateProjectModal.js.map +1 -0
  85. package/dist/ui/modals/ProjectActionsModal.d.ts +38 -0
  86. package/dist/ui/modals/ProjectActionsModal.d.ts.map +1 -0
  87. package/dist/ui/modals/ProjectActionsModal.js +292 -0
  88. package/dist/ui/modals/ProjectActionsModal.js.map +1 -0
  89. package/dist/ui/modals/index.d.ts +3 -0
  90. package/dist/ui/modals/index.js +4 -0
  91. package/dist/ui/overlays/demo-overlays.d.ts +19 -0
  92. package/dist/ui/overlays/demo-overlays.d.ts.map +1 -0
  93. package/dist/ui/overlays/demo-overlays.js +70 -0
  94. package/dist/ui/overlays/demo-overlays.js.map +1 -0
  95. package/dist/ui/overlays/index.d.ts +2 -0
  96. package/dist/ui/overlays/index.js +3 -0
  97. package/dist/ui/renderers/index.d.ts +3 -0
  98. package/dist/ui/renderers/index.js +4 -0
  99. package/dist/ui/renderers/project-list.markdown.d.ts +31 -0
  100. package/dist/ui/renderers/project-list.markdown.d.ts.map +1 -0
  101. package/dist/ui/renderers/project-list.markdown.js +148 -0
  102. package/dist/ui/renderers/project-list.markdown.js.map +1 -0
  103. package/dist/ui/renderers/project-list.renderer.d.ts +9 -0
  104. package/dist/ui/renderers/project-list.renderer.d.ts.map +1 -0
  105. package/dist/ui/renderers/project-list.renderer.js +17 -0
  106. package/dist/ui/renderers/project-list.renderer.js.map +1 -0
  107. package/package.json +38 -14
  108. package/src/billing/billing.presentation.ts +5 -6
  109. package/src/dashboard/dashboard.presentation.ts +5 -6
  110. package/src/example.ts +3 -3
  111. package/src/handlers/index.ts +3 -0
  112. package/src/handlers/saas.handlers.ts +300 -0
  113. package/src/index.ts +5 -0
  114. package/src/project/project.presentation.ts +5 -6
  115. package/src/saas-boilerplate.feature.ts +3 -3
  116. package/src/seeders/index.ts +28 -0
  117. package/src/shared/overlay-types.ts +39 -0
  118. package/src/tests/operations.test-spec.ts +109 -0
  119. package/src/ui/SaasDashboard.tsx +325 -0
  120. package/src/ui/SaasProjectList.tsx +113 -0
  121. package/src/ui/SaasSettingsPanel.tsx +96 -0
  122. package/src/ui/hooks/index.ts +10 -0
  123. package/src/ui/hooks/useProjectList.ts +95 -0
  124. package/src/ui/hooks/useProjectMutations.ts +166 -0
  125. package/src/ui/index.ts +18 -0
  126. package/src/ui/modals/CreateProjectModal.tsx +176 -0
  127. package/src/ui/modals/ProjectActionsModal.tsx +346 -0
  128. package/src/ui/modals/index.ts +2 -0
  129. package/src/ui/overlays/demo-overlays.ts +74 -0
  130. package/src/ui/overlays/index.ts +1 -0
  131. package/src/ui/renderers/index.ts +7 -0
  132. package/src/ui/renderers/project-list.markdown.ts +239 -0
  133. package/src/ui/renderers/project-list.renderer.tsx +22 -0
  134. package/tsconfig.json +1 -1
  135. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,346 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ProjectActionsModal - Actions for a specific project
5
+ *
6
+ * Wires to UpdateProjectContract, DeleteProjectContract
7
+ * via useProjectMutations hook.
8
+ */
9
+ import { useEffect, useState } from 'react';
10
+ import { Button, Input } from '@contractspec/lib.design-system';
11
+
12
+ // Local type definitions for modal props
13
+ export interface Project {
14
+ id: string;
15
+ name: string;
16
+ description?: string;
17
+ status: 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
18
+ tier: 'FREE' | 'PRO' | 'ENTERPRISE';
19
+ }
20
+
21
+ export interface UpdateProjectInput {
22
+ id: string;
23
+ name?: string;
24
+ description?: string;
25
+ }
26
+
27
+ type ActionMode = 'menu' | 'edit' | 'archive' | 'delete';
28
+
29
+ interface ProjectActionsModalProps {
30
+ isOpen: boolean;
31
+ project: Project | null;
32
+ onClose: () => void;
33
+ onUpdate: (input: UpdateProjectInput) => Promise<void>;
34
+ onArchive: (projectId: string) => Promise<void>;
35
+ onActivate: (projectId: string) => Promise<void>;
36
+ onDelete: (projectId: string) => Promise<void>;
37
+ isLoading?: boolean;
38
+ }
39
+
40
+ export function ProjectActionsModal({
41
+ isOpen,
42
+ project,
43
+ onClose,
44
+ onUpdate,
45
+ onArchive,
46
+ onActivate,
47
+ onDelete,
48
+ isLoading = false,
49
+ }: ProjectActionsModalProps) {
50
+ const [mode, setMode] = useState<ActionMode>('menu');
51
+ const [name, setName] = useState('');
52
+ const [description, setDescription] = useState('');
53
+ const [error, setError] = useState<string | null>(null);
54
+
55
+ const resetForm = () => {
56
+ setMode('menu');
57
+ setError(null);
58
+ if (project) {
59
+ setName(project.name);
60
+ setDescription(project.description ?? '');
61
+ }
62
+ };
63
+
64
+ const handleClose = () => {
65
+ resetForm();
66
+ onClose();
67
+ };
68
+
69
+ // Initialize form when project changes
70
+ useEffect(() => {
71
+ if (project) {
72
+ setName(project.name);
73
+ setDescription(project.description ?? '');
74
+ }
75
+ }, [project]);
76
+
77
+ const handleEdit = async () => {
78
+ if (!project) return;
79
+ setError(null);
80
+
81
+ if (!name.trim()) {
82
+ setError('Project name is required');
83
+ return;
84
+ }
85
+
86
+ try {
87
+ await onUpdate({
88
+ id: project.id,
89
+ name: name.trim(),
90
+ description: description.trim() || undefined,
91
+ });
92
+ handleClose();
93
+ } catch (err) {
94
+ setError(err instanceof Error ? err.message : 'Failed to update project');
95
+ }
96
+ };
97
+
98
+ const handleArchive = async () => {
99
+ if (!project) return;
100
+ setError(null);
101
+
102
+ try {
103
+ await onArchive(project.id);
104
+ handleClose();
105
+ } catch (err) {
106
+ setError(
107
+ err instanceof Error ? err.message : 'Failed to archive project'
108
+ );
109
+ }
110
+ };
111
+
112
+ const handleActivate = async () => {
113
+ if (!project) return;
114
+ setError(null);
115
+
116
+ try {
117
+ await onActivate(project.id);
118
+ handleClose();
119
+ } catch (err) {
120
+ setError(
121
+ err instanceof Error ? err.message : 'Failed to activate project'
122
+ );
123
+ }
124
+ };
125
+
126
+ const handleDelete = async () => {
127
+ if (!project) return;
128
+ setError(null);
129
+
130
+ try {
131
+ await onDelete(project.id);
132
+ handleClose();
133
+ } catch (err) {
134
+ setError(err instanceof Error ? err.message : 'Failed to delete project');
135
+ }
136
+ };
137
+
138
+ if (!isOpen || !project) return null;
139
+
140
+ return (
141
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
142
+ {/* Backdrop */}
143
+ <div
144
+ className="bg-background/80 absolute inset-0 backdrop-blur-sm"
145
+ onClick={handleClose}
146
+ role="button"
147
+ tabIndex={0}
148
+ onKeyDown={(e) => {
149
+ if (e.key === 'Enter' || e.key === ' ') handleClose();
150
+ }}
151
+ aria-label="Close modal"
152
+ />
153
+
154
+ {/* Modal */}
155
+ <div className="bg-card border-border relative z-10 w-full max-w-md rounded-xl border p-6 shadow-xl">
156
+ {/* Project Header */}
157
+ <div className="border-border mb-4 border-b pb-4">
158
+ <h2 className="text-xl font-semibold">{project.name}</h2>
159
+ <p className="text-muted-foreground text-sm">
160
+ {project.tier} Β· {project.status}
161
+ </p>
162
+ </div>
163
+
164
+ {/* Main Menu */}
165
+ {mode === 'menu' && (
166
+ <div className="space-y-3">
167
+ <Button
168
+ className="w-full justify-start"
169
+ variant="ghost"
170
+ onPress={() => setMode('edit')}
171
+ >
172
+ <span className="mr-2">✏️</span> Edit Project
173
+ </Button>
174
+
175
+ {project.status === 'ACTIVE' || project.status === 'DRAFT' ? (
176
+ <Button
177
+ className="w-full justify-start"
178
+ variant="ghost"
179
+ onPress={() => setMode('archive')}
180
+ >
181
+ <span className="mr-2">πŸ“¦</span> Archive Project
182
+ </Button>
183
+ ) : project.status === 'ARCHIVED' ? (
184
+ <Button
185
+ className="w-full justify-start"
186
+ variant="ghost"
187
+ onPress={handleActivate}
188
+ disabled={isLoading}
189
+ >
190
+ <span className="mr-2">πŸ”„</span> Restore Project
191
+ </Button>
192
+ ) : null}
193
+
194
+ <Button
195
+ className="w-full justify-start text-red-500 hover:text-red-600"
196
+ variant="ghost"
197
+ onPress={() => setMode('delete')}
198
+ >
199
+ <span className="mr-2">πŸ—‘οΈ</span> Delete Project
200
+ </Button>
201
+
202
+ <div className="border-border border-t pt-3">
203
+ <Button
204
+ className="w-full"
205
+ variant="outline"
206
+ onPress={handleClose}
207
+ >
208
+ Close
209
+ </Button>
210
+ </div>
211
+ </div>
212
+ )}
213
+
214
+ {/* Edit Form */}
215
+ {mode === 'edit' && (
216
+ <div className="space-y-4">
217
+ <div>
218
+ <label
219
+ htmlFor="edit-name"
220
+ className="text-muted-foreground mb-1 block text-sm font-medium"
221
+ >
222
+ Project Name *
223
+ </label>
224
+ <Input
225
+ id="edit-name"
226
+ value={name}
227
+ onChange={(e) => setName(e.target.value)}
228
+ disabled={isLoading}
229
+ />
230
+ </div>
231
+
232
+ <div>
233
+ <label
234
+ htmlFor="edit-description"
235
+ className="text-muted-foreground mb-1 block text-sm font-medium"
236
+ >
237
+ Description
238
+ </label>
239
+ <textarea
240
+ id="edit-description"
241
+ value={description}
242
+ onChange={(e) => setDescription(e.target.value)}
243
+ rows={3}
244
+ disabled={isLoading}
245
+ className="border-input bg-background focus:ring-ring w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none disabled:opacity-50"
246
+ />
247
+ </div>
248
+
249
+ {error && (
250
+ <div className="bg-destructive/10 text-destructive rounded-md p-3 text-sm">
251
+ {error}
252
+ </div>
253
+ )}
254
+
255
+ <div className="flex justify-end gap-3 pt-2">
256
+ <Button
257
+ variant="ghost"
258
+ onPress={() => setMode('menu')}
259
+ disabled={isLoading}
260
+ >
261
+ Back
262
+ </Button>
263
+ <Button onPress={handleEdit} disabled={isLoading}>
264
+ {isLoading ? 'Saving...' : 'Save Changes'}
265
+ </Button>
266
+ </div>
267
+ </div>
268
+ )}
269
+
270
+ {/* Archive Confirmation */}
271
+ {mode === 'archive' && (
272
+ <div className="space-y-4">
273
+ <p className="text-muted-foreground">
274
+ Are you sure you want to archive{' '}
275
+ <span className="text-foreground font-medium">
276
+ {project.name}
277
+ </span>
278
+ ?
279
+ </p>
280
+ <p className="text-muted-foreground text-sm">
281
+ Archived projects can be restored later.
282
+ </p>
283
+
284
+ {error && (
285
+ <div className="bg-destructive/10 text-destructive rounded-md p-3 text-sm">
286
+ {error}
287
+ </div>
288
+ )}
289
+
290
+ <div className="flex justify-end gap-3 pt-2">
291
+ <Button
292
+ variant="ghost"
293
+ onPress={() => setMode('menu')}
294
+ disabled={isLoading}
295
+ >
296
+ Cancel
297
+ </Button>
298
+ <Button onPress={handleArchive} disabled={isLoading}>
299
+ {isLoading ? 'Archiving...' : 'πŸ“¦ Archive'}
300
+ </Button>
301
+ </div>
302
+ </div>
303
+ )}
304
+
305
+ {/* Delete Confirmation */}
306
+ {mode === 'delete' && (
307
+ <div className="space-y-4">
308
+ <p className="text-muted-foreground">
309
+ Are you sure you want to delete{' '}
310
+ <span className="text-foreground font-medium">
311
+ {project.name}
312
+ </span>
313
+ ?
314
+ </p>
315
+ <p className="text-destructive text-sm">
316
+ This action cannot be undone.
317
+ </p>
318
+
319
+ {error && (
320
+ <div className="bg-destructive/10 text-destructive rounded-md p-3 text-sm">
321
+ {error}
322
+ </div>
323
+ )}
324
+
325
+ <div className="flex justify-end gap-3 pt-2">
326
+ <Button
327
+ variant="ghost"
328
+ onPress={() => setMode('menu')}
329
+ disabled={isLoading}
330
+ >
331
+ Cancel
332
+ </Button>
333
+ <Button
334
+ variant="destructive"
335
+ onPress={handleDelete}
336
+ disabled={isLoading}
337
+ >
338
+ {isLoading ? 'Deleting...' : 'πŸ—‘οΈ Delete'}
339
+ </Button>
340
+ </div>
341
+ </div>
342
+ )}
343
+ </div>
344
+ </div>
345
+ );
346
+ }
@@ -0,0 +1,2 @@
1
+ export { CreateProjectModal } from './CreateProjectModal';
2
+ export { ProjectActionsModal } from './ProjectActionsModal';
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Demo Overlay Definitions for SaaS Boilerplate
3
+ *
4
+ * These overlays customize the presentation for different contexts
5
+ * (e.g., demo users, different subscription tiers).
6
+ */
7
+ import type { OverlayDefinition } from '../../shared/overlay-types';
8
+
9
+ /**
10
+ * Free tier overlay - shows upgrade prompts and limits
11
+ */
12
+ export const saasFreeUserOverlay: OverlayDefinition = {
13
+ overlayId: 'saas-boilerplate.free-tier',
14
+ version: '1.0.0',
15
+ description: 'Shows limitations for free tier users',
16
+ appliesTo: {
17
+ feature: 'saas-boilerplate',
18
+ tier: 'free',
19
+ },
20
+ modifications: [
21
+ {
22
+ type: 'setLimit',
23
+ field: 'projects',
24
+ max: 3,
25
+ message: 'Upgrade to create more projects',
26
+ },
27
+ { type: 'hideField', field: 'advancedSettings', reason: 'Pro feature' },
28
+ {
29
+ type: 'addBadge',
30
+ position: 'header',
31
+ label: 'Free Plan',
32
+ variant: 'default',
33
+ },
34
+ ],
35
+ };
36
+
37
+ /**
38
+ * Demo user overlay
39
+ */
40
+ export const saasDemoOverlay: OverlayDefinition = {
41
+ overlayId: 'saas-boilerplate.demo-user',
42
+ version: '1.0.0',
43
+ description: 'Demo mode for SaaS boilerplate',
44
+ appliesTo: {
45
+ feature: 'saas-boilerplate',
46
+ role: 'demo',
47
+ },
48
+ modifications: [
49
+ {
50
+ type: 'hideField',
51
+ field: 'billingSection',
52
+ reason: 'Demo users cannot access billing',
53
+ },
54
+ {
55
+ type: 'hideField',
56
+ field: 'deleteAccount',
57
+ reason: 'Not available in demo',
58
+ },
59
+ {
60
+ type: 'addBadge',
61
+ position: 'header',
62
+ label: 'Demo Mode',
63
+ variant: 'warning',
64
+ },
65
+ ],
66
+ };
67
+
68
+ /**
69
+ * All overlays for saas-boilerplate
70
+ */
71
+ export const saasOverlays: OverlayDefinition[] = [
72
+ saasFreeUserOverlay,
73
+ saasDemoOverlay,
74
+ ];
@@ -0,0 +1 @@
1
+ export * from './demo-overlays';
@@ -0,0 +1,7 @@
1
+ // SaaS renderers
2
+ export { projectListReactRenderer } from './project-list.renderer';
3
+ export {
4
+ projectListMarkdownRenderer,
5
+ saasDashboardMarkdownRenderer,
6
+ saasBillingMarkdownRenderer,
7
+ } from './project-list.markdown';
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Markdown renderer for SaaS Project List presentation
3
+ *
4
+ * Uses dynamic import to ensure correct build order.
5
+ */
6
+ import type { PresentationRenderer } from '@contractspec/lib.contracts';
7
+ import {
8
+ mockListProjectsHandler,
9
+ mockGetSubscriptionHandler,
10
+ } from '@contractspec/example.saas-boilerplate/handlers';
11
+
12
+ interface ProjectItem {
13
+ id: string;
14
+ name: string;
15
+ status: string;
16
+ description?: string;
17
+ }
18
+
19
+ /**
20
+ * Markdown renderer for saas-boilerplate.project.list presentation
21
+ * Only handles ProjectListView component
22
+ */
23
+ export const projectListMarkdownRenderer: PresentationRenderer<{
24
+ mimeType: string;
25
+ body: string;
26
+ }> = {
27
+ target: 'markdown',
28
+ render: async (desc, _ctx) => {
29
+ // Only handle ProjectListView
30
+ if (
31
+ desc.source.type !== 'component' ||
32
+ desc.source.componentKey !== 'ProjectListView'
33
+ ) {
34
+ throw new Error('projectListMarkdownRenderer: not ProjectListView');
35
+ }
36
+
37
+ const data = await mockListProjectsHandler({
38
+ limit: 20,
39
+ offset: 0,
40
+ });
41
+
42
+ // The example handler returns 'projects', not 'items'
43
+ const items =
44
+ (data as { projects?: ProjectItem[]; items?: ProjectItem[] }).projects ??
45
+ (data as { items?: ProjectItem[] }).items ??
46
+ [];
47
+
48
+ const lines: string[] = [
49
+ '# Projects',
50
+ '',
51
+ `**Total**: ${data.total} projects`,
52
+ '',
53
+ ];
54
+
55
+ if (items.length === 0) {
56
+ lines.push('_No projects found._');
57
+ } else {
58
+ lines.push('| Status | Project | Description |');
59
+ lines.push('|--------|---------|-------------|');
60
+ for (const project of items) {
61
+ const status =
62
+ project.status === 'ACTIVE'
63
+ ? 'βœ…'
64
+ : project.status === 'ARCHIVED'
65
+ ? 'πŸ“¦'
66
+ : '⏸️';
67
+ lines.push(
68
+ `| ${status} | **${project.name}** | ${project.description ?? '-'} |`
69
+ );
70
+ }
71
+ }
72
+
73
+ return {
74
+ mimeType: 'text/markdown',
75
+ body: lines.join('\n'),
76
+ };
77
+ },
78
+ };
79
+
80
+ /**
81
+ * Markdown renderer for saas-boilerplate.dashboard presentation
82
+ * Only handles SaasDashboard component
83
+ */
84
+ export const saasDashboardMarkdownRenderer: PresentationRenderer<{
85
+ mimeType: string;
86
+ body: string;
87
+ }> = {
88
+ target: 'markdown',
89
+ render: async (desc, _ctx) => {
90
+ // Only handle SaasDashboard
91
+ if (
92
+ desc.source.type !== 'component' ||
93
+ desc.source.componentKey !== 'SaasDashboard'
94
+ ) {
95
+ throw new Error('saasDashboardMarkdownRenderer: not SaasDashboard');
96
+ }
97
+
98
+ const [projectsData, subscription] = await Promise.all([
99
+ mockListProjectsHandler({ limit: 50 }),
100
+ mockGetSubscriptionHandler(),
101
+ ]);
102
+
103
+ const projects =
104
+ (projectsData as { projects?: ProjectItem[] }).projects ?? [];
105
+ const activeProjects = projects.filter((p) => p.status === 'ACTIVE').length;
106
+ const archivedProjects = projects.filter(
107
+ (p) => p.status === 'ARCHIVED'
108
+ ).length;
109
+
110
+ const lines: string[] = [
111
+ '# SaaS Dashboard',
112
+ '',
113
+ '> Organization overview and usage summary',
114
+ '',
115
+ '## Summary',
116
+ '',
117
+ '| Metric | Value |',
118
+ '|--------|-------|',
119
+ `| Total Projects | ${projectsData.total} |`,
120
+ `| Active Projects | ${activeProjects} |`,
121
+ `| Archived Projects | ${archivedProjects} |`,
122
+ `| Subscription Plan | ${subscription.planName} |`,
123
+ `| Subscription Status | ${subscription.status} |`,
124
+ '',
125
+ '## Projects',
126
+ '',
127
+ ];
128
+
129
+ if (projects.length === 0) {
130
+ lines.push('_No projects yet._');
131
+ } else {
132
+ lines.push('| Status | Project | Description |');
133
+ lines.push('|--------|---------|-------------|');
134
+ for (const project of projects.slice(0, 10)) {
135
+ const status =
136
+ project.status === 'ACTIVE'
137
+ ? 'βœ…'
138
+ : project.status === 'ARCHIVED'
139
+ ? 'πŸ“¦'
140
+ : '⏸️';
141
+ lines.push(
142
+ `| ${status} | **${project.name}** | ${project.description ?? '-'} |`
143
+ );
144
+ }
145
+ if (projects.length > 10) {
146
+ lines.push(
147
+ `| ... | ... | _${projectsData.total - 10} more projects_ |`
148
+ );
149
+ }
150
+ }
151
+
152
+ lines.push('');
153
+ lines.push('## Subscription');
154
+ lines.push('');
155
+ lines.push(`- **Plan**: ${subscription.planName}`);
156
+ lines.push(`- **Status**: ${subscription.status}`);
157
+ if (subscription.currentPeriodEnd) {
158
+ lines.push(
159
+ `- **Period End**: ${new Date(subscription.currentPeriodEnd).toLocaleDateString()}`
160
+ );
161
+ }
162
+
163
+ return {
164
+ mimeType: 'text/markdown',
165
+ body: lines.join('\n'),
166
+ };
167
+ },
168
+ };
169
+
170
+ /**
171
+ * Markdown renderer for saas-boilerplate.billing.settings presentation
172
+ * Only handles SubscriptionView component
173
+ */
174
+ export const saasBillingMarkdownRenderer: PresentationRenderer<{
175
+ mimeType: string;
176
+ body: string;
177
+ }> = {
178
+ target: 'markdown',
179
+ render: async (desc, _ctx) => {
180
+ // Only handle SubscriptionView
181
+ if (
182
+ desc.source.type !== 'component' ||
183
+ desc.source.componentKey !== 'SubscriptionView'
184
+ ) {
185
+ throw new Error('saasBillingMarkdownRenderer: not SubscriptionView');
186
+ }
187
+
188
+ const subscription = await mockGetSubscriptionHandler();
189
+
190
+ const lines: string[] = [
191
+ '# Billing & Subscription',
192
+ '',
193
+ '> Current subscription details and billing information',
194
+ '',
195
+ '## Subscription Details',
196
+ '',
197
+ '| Property | Value |',
198
+ '|----------|-------|',
199
+ `| Plan | ${subscription.planName} |`,
200
+ `| Status | ${subscription.status} |`,
201
+ `| ID | ${subscription.id} |`,
202
+ `| Period Start | ${new Date(subscription.currentPeriodStart).toLocaleDateString()} |`,
203
+ `| Period End | ${new Date(subscription.currentPeriodEnd).toLocaleDateString()} |`,
204
+ ];
205
+
206
+ lines.push('');
207
+ lines.push('## Plan Limits');
208
+ lines.push('');
209
+ lines.push(`- **Projects**: ${subscription.limits.projects}`);
210
+ lines.push(`- **Users**: ${subscription.limits.users}`);
211
+
212
+ lines.push('');
213
+ lines.push('## Plan Features');
214
+ lines.push('');
215
+
216
+ if (subscription.planName.toLowerCase().includes('free')) {
217
+ lines.push('- βœ… Up to 3 projects');
218
+ lines.push('- βœ… Basic support');
219
+ lines.push('- ❌ Priority support');
220
+ lines.push('- ❌ Advanced analytics');
221
+ } else if (subscription.planName.toLowerCase().includes('pro')) {
222
+ lines.push('- βœ… Unlimited projects');
223
+ lines.push('- βœ… Priority support');
224
+ lines.push('- βœ… Advanced analytics');
225
+ lines.push('- ❌ Custom integrations');
226
+ } else {
227
+ lines.push('- βœ… Unlimited projects');
228
+ lines.push('- βœ… Priority support');
229
+ lines.push('- βœ… Advanced analytics');
230
+ lines.push('- βœ… Custom integrations');
231
+ lines.push('- βœ… Dedicated support');
232
+ }
233
+
234
+ return {
235
+ mimeType: 'text/markdown',
236
+ body: lines.join('\n'),
237
+ };
238
+ },
239
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * React renderer for SaaS Project List presentation
3
+ */
4
+ import * as React from 'react';
5
+ import type { PresentationRenderer } from '@contractspec/lib.contracts';
6
+ import { SaasProjectList } from '../SaasProjectList';
7
+
8
+ export const projectListReactRenderer: PresentationRenderer<React.ReactElement> =
9
+ {
10
+ target: 'react',
11
+ render: async (desc, _ctx) => {
12
+ if (desc.source.type !== 'component') {
13
+ throw new Error('Invalid source type');
14
+ }
15
+
16
+ if (desc.source.componentKey !== 'SaasProjectListView') {
17
+ throw new Error(`Unknown component: ${desc.source.componentKey}`);
18
+ }
19
+
20
+ return <SaasProjectList />;
21
+ },
22
+ };
package/tsconfig.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "extends": "@contractspec/tool.typescript/react-library.json",
3
3
  "include": ["src"],
4
- "exclude": ["node_modules"],
4
+ "exclude": ["node_modules", "dist"],
5
5
  "compilerOptions": {
6
6
  "rootDir": "src",
7
7
  "outDir": "dist"