@contractspec/example.saas-boilerplate 1.46.1 → 1.48.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 +64 -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,109 @@
1
+ import { defineTestSpec } from '@contractspec/lib.contracts';
2
+
3
+ export const ProjectListTest = defineTestSpec({
4
+ meta: {
5
+ key: 'saas.project.list.test',
6
+ version: '1.0.0',
7
+ stability: 'experimental',
8
+ owners: ['@example.saas-boilerplate'],
9
+ description: 'Test for listing projects',
10
+ tags: ['test'],
11
+ },
12
+ target: {
13
+ type: 'operation',
14
+ operation: { key: 'saas.project.list', version: '1.0.0' },
15
+ },
16
+ scenarios: [
17
+ {
18
+ key: 'success',
19
+ when: { operation: { key: 'saas.project.list' } },
20
+ then: [{ type: 'expectOutput', match: {} }],
21
+ },
22
+ {
23
+ key: 'error',
24
+ when: { operation: { key: 'saas.project.list' } },
25
+ then: [{ type: 'expectError' }],
26
+ },
27
+ ],
28
+ });
29
+
30
+ export const ProjectGetTest = defineTestSpec({
31
+ meta: {
32
+ key: 'saas.project.get.test',
33
+ version: '1.0.0',
34
+ stability: 'experimental',
35
+ owners: ['@example.saas-boilerplate'],
36
+ description: 'Test for getting project',
37
+ tags: ['test'],
38
+ },
39
+ target: {
40
+ type: 'operation',
41
+ operation: { key: 'saas.project.get', version: '1.0.0' },
42
+ },
43
+ scenarios: [
44
+ {
45
+ key: 'success',
46
+ when: { operation: { key: 'saas.project.get' } },
47
+ then: [{ type: 'expectOutput', match: {} }],
48
+ },
49
+ {
50
+ key: 'error',
51
+ when: { operation: { key: 'saas.project.get' } },
52
+ then: [{ type: 'expectError' }],
53
+ },
54
+ ],
55
+ });
56
+
57
+ export const BillingSubscriptionGetTest = defineTestSpec({
58
+ meta: {
59
+ key: 'saas.billing.subscription.get.test',
60
+ version: '1.0.0',
61
+ stability: 'experimental',
62
+ owners: ['@example.saas-boilerplate'],
63
+ description: 'Test for getting subscription',
64
+ tags: ['test'],
65
+ },
66
+ target: {
67
+ type: 'operation',
68
+ operation: { key: 'saas.billing.subscription.get', version: '1.0.0' },
69
+ },
70
+ scenarios: [
71
+ {
72
+ key: 'success',
73
+ when: { operation: { key: 'saas.billing.subscription.get' } },
74
+ then: [{ type: 'expectOutput', match: {} }],
75
+ },
76
+ {
77
+ key: 'error',
78
+ when: { operation: { key: 'saas.billing.subscription.get' } },
79
+ then: [{ type: 'expectError' }],
80
+ },
81
+ ],
82
+ });
83
+
84
+ export const BillingUsageSummaryTest = defineTestSpec({
85
+ meta: {
86
+ key: 'saas.billing.usage.summary.test',
87
+ version: '1.0.0',
88
+ stability: 'experimental',
89
+ owners: ['@example.saas-boilerplate'],
90
+ description: 'Test for getting usage summary',
91
+ tags: ['test'],
92
+ },
93
+ target: {
94
+ type: 'operation',
95
+ operation: { key: 'saas.billing.usage.summary', version: '1.0.0' },
96
+ },
97
+ scenarios: [
98
+ {
99
+ key: 'success',
100
+ when: { operation: { key: 'saas.billing.usage.summary' } },
101
+ then: [{ type: 'expectOutput', match: {} }],
102
+ },
103
+ {
104
+ key: 'error',
105
+ when: { operation: { key: 'saas.billing.usage.summary' } },
106
+ then: [{ type: 'expectError' }],
107
+ },
108
+ ],
109
+ });
@@ -0,0 +1,325 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * SaaS Dashboard
5
+ *
6
+ * Fully integrated with ContractSpec example handlers
7
+ * and design-system components.
8
+ *
9
+ * Commands wired:
10
+ * - CreateProjectContract -> Create Project button + modal
11
+ * - UpdateProjectContract -> Edit project via modal
12
+ * - DeleteProjectContract -> Delete project via modal
13
+ */
14
+ import { useState, useCallback } from 'react';
15
+ import {
16
+ StatCard,
17
+ StatCardGroup,
18
+ StatusChip,
19
+ EntityCard,
20
+ EmptyState,
21
+ LoaderBlock,
22
+ ErrorState,
23
+ Button,
24
+ } from '@contractspec/lib.design-system';
25
+ import {
26
+ useProjectList,
27
+ type Project,
28
+ type Subscription,
29
+ } from './hooks/useProjectList';
30
+ import { useProjectMutations } from './hooks/useProjectMutations';
31
+ import { CreateProjectModal } from './modals/CreateProjectModal';
32
+ import { ProjectActionsModal } from './modals/ProjectActionsModal';
33
+
34
+ type Tab = 'projects' | 'billing' | 'settings';
35
+
36
+ function getStatusTone(
37
+ status: Project['status']
38
+ ): 'success' | 'warning' | 'neutral' | 'danger' {
39
+ switch (status) {
40
+ case 'ACTIVE':
41
+ return 'success';
42
+ case 'DRAFT':
43
+ return 'neutral';
44
+ case 'ARCHIVED':
45
+ return 'warning';
46
+ default:
47
+ return 'neutral';
48
+ }
49
+ }
50
+
51
+ export function SaasDashboard() {
52
+ const [activeTab, setActiveTab] = useState<Tab>('projects');
53
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
54
+ const [selectedProject, setSelectedProject] = useState<Project | null>(null);
55
+ const [isProjectActionsOpen, setIsProjectActionsOpen] = useState(false);
56
+
57
+ const { data, subscription, loading, error, stats, refetch } =
58
+ useProjectList();
59
+
60
+ const mutations = useProjectMutations({
61
+ onSuccess: () => {
62
+ refetch();
63
+ },
64
+ });
65
+
66
+ const handleProjectClick = useCallback((project: Project) => {
67
+ setSelectedProject(project);
68
+ setIsProjectActionsOpen(true);
69
+ }, []);
70
+
71
+ const tabs: { id: Tab; label: string; icon: string }[] = [
72
+ { id: 'projects', label: 'Projects', icon: '📁' },
73
+ { id: 'billing', label: 'Billing', icon: '💳' },
74
+ { id: 'settings', label: 'Settings', icon: '⚙️' },
75
+ ];
76
+
77
+ if (loading && !data) {
78
+ return <LoaderBlock label="Loading dashboard..." />;
79
+ }
80
+
81
+ if (error) {
82
+ return (
83
+ <ErrorState
84
+ title="Failed to load dashboard"
85
+ description={error.message}
86
+ onRetry={refetch}
87
+ retryLabel="Retry"
88
+ />
89
+ );
90
+ }
91
+
92
+ return (
93
+ <div className="space-y-6">
94
+ {/* Header */}
95
+ <div className="flex items-center justify-between">
96
+ <h2 className="text-2xl font-bold">SaaS Dashboard</h2>
97
+ {activeTab === 'projects' && (
98
+ <Button onPress={() => setIsCreateModalOpen(true)}>
99
+ <span className="mr-2">+</span> New Project
100
+ </Button>
101
+ )}
102
+ </div>
103
+
104
+ {/* Stats Row */}
105
+ {stats && subscription && (
106
+ <StatCardGroup>
107
+ <StatCard label="Projects" value={stats.total.toString()} />
108
+ <StatCard label="Active" value={stats.activeCount.toString()} />
109
+ <StatCard label="Draft" value={stats.draftCount.toString()} />
110
+ <StatCard
111
+ label="Plan"
112
+ value={subscription.plan}
113
+ hint={subscription.status}
114
+ />
115
+ </StatCardGroup>
116
+ )}
117
+
118
+ {/* Navigation Tabs */}
119
+ <nav className="bg-muted flex gap-1 rounded-lg p-1" role="tablist">
120
+ {tabs.map((tab) => (
121
+ <button
122
+ key={tab.id}
123
+ type="button"
124
+ role="tab"
125
+ aria-selected={activeTab === tab.id}
126
+ onClick={() => setActiveTab(tab.id)}
127
+ className={`flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors ${
128
+ activeTab === tab.id
129
+ ? 'bg-background text-foreground shadow-sm'
130
+ : 'text-muted-foreground hover:text-foreground'
131
+ }`}
132
+ >
133
+ <span>{tab.icon}</span>
134
+ {tab.label}
135
+ </button>
136
+ ))}
137
+ </nav>
138
+
139
+ {/* Tab Content */}
140
+ <div className="min-h-[400px]" role="tabpanel">
141
+ {activeTab === 'projects' && (
142
+ <ProjectsTab data={data} onProjectClick={handleProjectClick} />
143
+ )}
144
+ {activeTab === 'billing' && <BillingTab subscription={subscription} />}
145
+ {activeTab === 'settings' && <SettingsTab />}
146
+ </div>
147
+
148
+ {/* Create Project Modal */}
149
+ <CreateProjectModal
150
+ isOpen={isCreateModalOpen}
151
+ onClose={() => setIsCreateModalOpen(false)}
152
+ onSubmit={async (input) => {
153
+ await mutations.createProject(input);
154
+ }}
155
+ isLoading={mutations.createState.loading}
156
+ />
157
+
158
+ {/* Project Actions Modal */}
159
+ <ProjectActionsModal
160
+ isOpen={isProjectActionsOpen}
161
+ project={selectedProject}
162
+ onClose={() => {
163
+ setIsProjectActionsOpen(false);
164
+ setSelectedProject(null);
165
+ }}
166
+ onUpdate={async (input) => {
167
+ await mutations.updateProject(input);
168
+ }}
169
+ onArchive={async (projectId) => {
170
+ await mutations.archiveProject(projectId);
171
+ }}
172
+ onActivate={async (projectId) => {
173
+ await mutations.activateProject(projectId);
174
+ }}
175
+ onDelete={async (projectId) => {
176
+ await mutations.deleteProject(projectId);
177
+ }}
178
+ isLoading={mutations.isLoading}
179
+ />
180
+ </div>
181
+ );
182
+ }
183
+
184
+ interface ProjectsTabProps {
185
+ data: ReturnType<typeof useProjectList>['data'];
186
+ onProjectClick?: (project: Project) => void;
187
+ }
188
+
189
+ function ProjectsTab({ data, onProjectClick }: ProjectsTabProps) {
190
+ if (!data?.items.length) {
191
+ return (
192
+ <EmptyState
193
+ title="No projects yet"
194
+ description="Create your first project to get started."
195
+ />
196
+ );
197
+ }
198
+
199
+ return (
200
+ <div className="space-y-4">
201
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
202
+ {data.items.map((project: Project) => (
203
+ <EntityCard
204
+ key={project.id}
205
+ cardTitle={project.name}
206
+ cardSubtitle={project.tier}
207
+ meta={
208
+ <p className="text-muted-foreground text-sm">
209
+ {project.description}
210
+ </p>
211
+ }
212
+ chips={
213
+ <StatusChip
214
+ tone={getStatusTone(project.status)}
215
+ label={project.status}
216
+ />
217
+ }
218
+ footer={
219
+ <div className="flex w-full items-center justify-between">
220
+ <span className="text-muted-foreground text-xs">
221
+ {project.updatedAt.toLocaleDateString()}
222
+ </span>
223
+ <Button
224
+ variant="ghost"
225
+ size="sm"
226
+ onPress={() => onProjectClick?.(project)}
227
+ >
228
+ Actions
229
+ </Button>
230
+ </div>
231
+ }
232
+ />
233
+ ))}
234
+ </div>
235
+ </div>
236
+ );
237
+ }
238
+
239
+ function BillingTab({ subscription }: { subscription: Subscription | null }) {
240
+ if (!subscription) return null;
241
+
242
+ return (
243
+ <div className="space-y-6">
244
+ <div className="border-border bg-card rounded-xl border p-6">
245
+ <div className="flex items-start justify-between">
246
+ <div>
247
+ <h3 className="text-lg font-semibold">{subscription.plan} Plan</h3>
248
+ <p className="text-muted-foreground text-sm">
249
+ Current period:{' '}
250
+ {subscription.currentPeriodStart.toLocaleDateString()} -{' '}
251
+ {subscription.currentPeriodEnd.toLocaleDateString()}
252
+ </p>
253
+ <p className="text-muted-foreground text-sm">
254
+ Billing cycle: {subscription.billingCycle}
255
+ </p>
256
+ </div>
257
+ <StatusChip tone="success" label={subscription.status} />
258
+ </div>
259
+
260
+ <div className="mt-4 flex gap-3">
261
+ <Button variant="outline" onPress={() => alert('Upgrade clicked!')}>
262
+ Upgrade Plan
263
+ </Button>
264
+ <Button
265
+ variant="ghost"
266
+ onPress={() => alert('Manage Billing clicked!')}
267
+ >
268
+ Manage Billing
269
+ </Button>
270
+ </div>
271
+ </div>
272
+
273
+ {subscription.cancelAtPeriodEnd && (
274
+ <div className="border-border bg-destructive/10 text-destructive rounded-xl border p-4">
275
+ <p className="text-sm font-medium">
276
+ ⚠️ Your subscription will be cancelled at the end of the current
277
+ period.
278
+ </p>
279
+ </div>
280
+ )}
281
+ </div>
282
+ );
283
+ }
284
+
285
+ function SettingsTab() {
286
+ return (
287
+ <div className="space-y-6">
288
+ <div className="border-border bg-card rounded-xl border p-6">
289
+ <h3 className="mb-4 text-lg font-semibold">Organization Settings</h3>
290
+ <div className="space-y-4">
291
+ <div>
292
+ <label htmlFor="org-name" className="text-sm font-medium">
293
+ Organization Name
294
+ </label>
295
+ <input
296
+ id="org-name"
297
+ type="text"
298
+ defaultValue="Demo Organization"
299
+ className="border-input bg-background mt-1 block w-full rounded-md border px-3 py-2"
300
+ />
301
+ </div>
302
+ <div>
303
+ <label htmlFor="timezone" className="text-sm font-medium">
304
+ Default Timezone
305
+ </label>
306
+ <select
307
+ id="timezone"
308
+ className="border-input bg-background mt-1 block w-full rounded-md border px-3 py-2"
309
+ >
310
+ <option>UTC</option>
311
+ <option>America/New_York</option>
312
+ <option>Europe/London</option>
313
+ <option>Asia/Tokyo</option>
314
+ </select>
315
+ </div>
316
+ <div className="pt-2">
317
+ <Button onPress={() => alert('Settings saved!')}>
318
+ Save Settings
319
+ </Button>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ );
325
+ }
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * SaaS Project List - Standalone project list component
5
+ */
6
+ import {
7
+ StatCard,
8
+ StatCardGroup,
9
+ StatusChip,
10
+ EntityCard,
11
+ EmptyState,
12
+ LoaderBlock,
13
+ ErrorState,
14
+ Button,
15
+ } from '@contractspec/lib.design-system';
16
+ import { useProjectList, type Project } from './hooks/useProjectList';
17
+
18
+ interface SaasProjectListProps {
19
+ onProjectClick?: (projectId: string) => void;
20
+ onCreateProject?: () => void;
21
+ }
22
+
23
+ function getStatusTone(
24
+ status: Project['status']
25
+ ): 'success' | 'warning' | 'neutral' | 'danger' {
26
+ switch (status) {
27
+ case 'ACTIVE':
28
+ return 'success';
29
+ case 'DRAFT':
30
+ return 'neutral';
31
+ case 'ARCHIVED':
32
+ return 'danger';
33
+ default:
34
+ return 'neutral';
35
+ }
36
+ }
37
+
38
+ export function SaasProjectList({
39
+ onProjectClick,
40
+ onCreateProject,
41
+ }: SaasProjectListProps) {
42
+ const { data, loading, error, stats, refetch } = useProjectList();
43
+
44
+ if (loading && !data) {
45
+ return <LoaderBlock label="Loading projects..." />;
46
+ }
47
+
48
+ if (error) {
49
+ return (
50
+ <ErrorState
51
+ title="Failed to load projects"
52
+ description={error.message}
53
+ onRetry={refetch}
54
+ retryLabel="Retry"
55
+ />
56
+ );
57
+ }
58
+
59
+ if (!data?.items.length) {
60
+ return (
61
+ <EmptyState
62
+ title="No projects found"
63
+ description="Create your first project to get started."
64
+ primaryAction={
65
+ onCreateProject ? (
66
+ <Button onPress={onCreateProject}>Create Project</Button>
67
+ ) : undefined
68
+ }
69
+ />
70
+ );
71
+ }
72
+
73
+ return (
74
+ <div className="space-y-6">
75
+ {stats && (
76
+ <StatCardGroup>
77
+ <StatCard label="Total Projects" value={stats.total.toString()} />
78
+ <StatCard label="Active" value={stats.activeCount.toString()} />
79
+ <StatCard label="Draft" value={stats.draftCount.toString()} />
80
+ </StatCardGroup>
81
+ )}
82
+
83
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
84
+ {data.items.map((project: Project) => (
85
+ <EntityCard
86
+ key={project.id}
87
+ cardTitle={project.name}
88
+ cardSubtitle={project.tier}
89
+ meta={
90
+ <p className="text-muted-foreground text-sm">
91
+ {project.description}
92
+ </p>
93
+ }
94
+ chips={
95
+ <StatusChip
96
+ tone={getStatusTone(project.status)}
97
+ label={project.status}
98
+ />
99
+ }
100
+ footer={
101
+ <span className="text-muted-foreground text-xs">
102
+ {project.updatedAt.toLocaleDateString()}
103
+ </span>
104
+ }
105
+ onClick={
106
+ onProjectClick ? () => onProjectClick(project.id) : undefined
107
+ }
108
+ />
109
+ ))}
110
+ </div>
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * SaaS Settings Panel - Organization and user settings
5
+ */
6
+ import { useState } from 'react';
7
+ import { Button } from '@contractspec/lib.design-system';
8
+
9
+ export function SaasSettingsPanel() {
10
+ const [orgName, setOrgName] = useState('Demo Organization');
11
+ const [timezone, setTimezone] = useState('UTC');
12
+
13
+ return (
14
+ <div className="space-y-6">
15
+ <div className="border-border bg-card rounded-xl border p-6">
16
+ <h3 className="mb-4 text-lg font-semibold">Organization Settings</h3>
17
+ <div className="space-y-4">
18
+ <div>
19
+ <label
20
+ htmlFor="setting-org-name"
21
+ className="block text-sm font-medium"
22
+ >
23
+ Organization Name
24
+ </label>
25
+ <input
26
+ id="setting-org-name"
27
+ type="text"
28
+ value={orgName}
29
+ onChange={(e) => setOrgName(e.target.value)}
30
+ className="border-input bg-background mt-1 block w-full rounded-md border px-3 py-2"
31
+ />
32
+ </div>
33
+ <div>
34
+ <label
35
+ htmlFor="setting-timezone"
36
+ className="block text-sm font-medium"
37
+ >
38
+ Default Timezone
39
+ </label>
40
+ <select
41
+ id="setting-timezone"
42
+ value={timezone}
43
+ onChange={(e) => setTimezone(e.target.value)}
44
+ className="border-input bg-background mt-1 block w-full rounded-md border px-3 py-2"
45
+ >
46
+ <option value="UTC">UTC</option>
47
+ <option value="America/New_York">America/New_York</option>
48
+ <option value="Europe/London">Europe/London</option>
49
+ <option value="Asia/Tokyo">Asia/Tokyo</option>
50
+ </select>
51
+ </div>
52
+ </div>
53
+ <div className="mt-6">
54
+ <Button variant="default">Save Changes</Button>
55
+ </div>
56
+ </div>
57
+
58
+ <div className="border-border bg-card rounded-xl border p-6">
59
+ <h3 className="mb-4 text-lg font-semibold">Notifications</h3>
60
+ <div className="space-y-3">
61
+ {[
62
+ { label: 'Email notifications', defaultChecked: true },
63
+ { label: 'Usage alerts', defaultChecked: true },
64
+ { label: 'Weekly digest', defaultChecked: false },
65
+ ].map((item) => (
66
+ <label key={item.label} className="flex items-center gap-3">
67
+ <input
68
+ type="checkbox"
69
+ defaultChecked={item.defaultChecked}
70
+ className="border-input h-4 w-4 rounded"
71
+ />
72
+ <span className="text-sm">{item.label}</span>
73
+ </label>
74
+ ))}
75
+ </div>
76
+ </div>
77
+
78
+ <div className="rounded-xl border border-red-200 bg-red-50 p-6 dark:border-red-900 dark:bg-red-950/20">
79
+ <h3 className="mb-2 text-lg font-semibold text-red-700 dark:text-red-400">
80
+ Danger Zone
81
+ </h3>
82
+ <p className="mb-4 text-sm text-red-600 dark:text-red-300">
83
+ These actions are irreversible. Please proceed with caution.
84
+ </p>
85
+ <div className="flex gap-3">
86
+ <Button variant="secondary" size="sm">
87
+ Export Data
88
+ </Button>
89
+ <Button variant="secondary" size="sm">
90
+ Delete Organization
91
+ </Button>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,10 @@
1
+ 'use client';
2
+
3
+ export { useProjectList, type UseProjectListOptions } from './useProjectList';
4
+ export {
5
+ useProjectMutations,
6
+ type UseProjectMutationsOptions,
7
+ } from './useProjectMutations';
8
+
9
+ // Note: For project types (CreateProjectInput, UpdateProjectInput, Project), import directly from:
10
+ // import type { Project, CreateProjectInput, etc. } from '@contractspec/example.saas-boilerplate/handlers';