@contractspec/example.saas-boilerplate 3.8.8 → 3.8.10

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 (156) hide show
  1. package/.turbo/turbo-build.log +156 -156
  2. package/CHANGELOG.md +40 -0
  3. package/dist/billing/billing.entity.js +1 -113
  4. package/dist/billing/billing.enum.js +1 -19
  5. package/dist/billing/billing.event.js +1 -90
  6. package/dist/billing/billing.handler.js +1 -148
  7. package/dist/billing/billing.operations.js +1 -278
  8. package/dist/billing/billing.presentation.js +1 -55
  9. package/dist/billing/billing.schema.js +1 -121
  10. package/dist/billing/index.js +1 -691
  11. package/dist/browser/billing/billing.entity.js +1 -113
  12. package/dist/browser/billing/billing.enum.js +1 -19
  13. package/dist/browser/billing/billing.event.js +1 -90
  14. package/dist/browser/billing/billing.handler.js +1 -148
  15. package/dist/browser/billing/billing.operations.js +1 -278
  16. package/dist/browser/billing/billing.presentation.js +1 -55
  17. package/dist/browser/billing/billing.schema.js +1 -121
  18. package/dist/browser/billing/index.js +1 -691
  19. package/dist/browser/dashboard/dashboard.presentation.js +1 -55
  20. package/dist/browser/dashboard/index.js +1 -55
  21. package/dist/browser/docs/index.js +5 -49
  22. package/dist/browser/docs/saas-boilerplate.docblock.js +5 -49
  23. package/dist/browser/example.js +1 -39
  24. package/dist/browser/handlers/index.js +2 -358
  25. package/dist/browser/handlers/saas.handlers.js +2 -134
  26. package/dist/browser/index.js +9 -3591
  27. package/dist/browser/presentations/index.js +1 -299
  28. package/dist/browser/project/index.js +1 -793
  29. package/dist/browser/project/project.entity.js +1 -77
  30. package/dist/browser/project/project.enum.js +1 -18
  31. package/dist/browser/project/project.event.js +1 -103
  32. package/dist/browser/project/project.handler.js +1 -178
  33. package/dist/browser/project/project.operations.js +1 -372
  34. package/dist/browser/project/project.presentation.js +1 -180
  35. package/dist/browser/project/project.schema.js +1 -134
  36. package/dist/browser/saas-boilerplate.feature.js +1 -304
  37. package/dist/browser/seeders/index.js +2 -20
  38. package/dist/browser/settings/index.js +1 -75
  39. package/dist/browser/settings/settings.entity.js +1 -74
  40. package/dist/browser/settings/settings.enum.js +1 -11
  41. package/dist/browser/shared/mock-data.js +1 -104
  42. package/dist/browser/tests/operations.test-spec.js +1 -112
  43. package/dist/browser/ui/SaasDashboard.js +1 -1239
  44. package/dist/browser/ui/SaasDashboard.visualizations.js +1 -249
  45. package/dist/browser/ui/SaasProjectList.js +1 -162
  46. package/dist/browser/ui/SaasSettingsPanel.js +1 -145
  47. package/dist/browser/ui/hooks/index.js +1 -159
  48. package/dist/browser/ui/hooks/useProjectList.js +1 -66
  49. package/dist/browser/ui/hooks/useProjectMutations.js +1 -91
  50. package/dist/browser/ui/index.js +5 -2077
  51. package/dist/browser/ui/modals/CreateProjectModal.js +1 -153
  52. package/dist/browser/ui/modals/ProjectActionsModal.js +1 -335
  53. package/dist/browser/ui/modals/index.js +1 -487
  54. package/dist/browser/ui/overlays/demo-overlays.js +1 -61
  55. package/dist/browser/ui/overlays/index.js +1 -61
  56. package/dist/browser/ui/renderers/index.js +5 -901
  57. package/dist/browser/ui/renderers/project-list.markdown.js +5 -725
  58. package/dist/browser/ui/renderers/project-list.renderer.js +1 -177
  59. package/dist/browser/visualizations/catalog.js +1 -155
  60. package/dist/browser/visualizations/index.js +1 -217
  61. package/dist/browser/visualizations/selectors.js +1 -210
  62. package/dist/dashboard/dashboard.presentation.js +1 -55
  63. package/dist/dashboard/index.js +1 -55
  64. package/dist/docs/index.js +5 -49
  65. package/dist/docs/saas-boilerplate.docblock.js +5 -49
  66. package/dist/example.js +1 -39
  67. package/dist/handlers/index.js +2 -358
  68. package/dist/handlers/saas.handlers.js +2 -134
  69. package/dist/index.js +9 -3591
  70. package/dist/node/billing/billing.entity.js +1 -113
  71. package/dist/node/billing/billing.enum.js +1 -19
  72. package/dist/node/billing/billing.event.js +1 -90
  73. package/dist/node/billing/billing.handler.js +1 -148
  74. package/dist/node/billing/billing.operations.js +1 -278
  75. package/dist/node/billing/billing.presentation.js +1 -55
  76. package/dist/node/billing/billing.schema.js +1 -121
  77. package/dist/node/billing/index.js +1 -691
  78. package/dist/node/dashboard/dashboard.presentation.js +1 -55
  79. package/dist/node/dashboard/index.js +1 -55
  80. package/dist/node/docs/index.js +5 -49
  81. package/dist/node/docs/saas-boilerplate.docblock.js +5 -49
  82. package/dist/node/example.js +1 -39
  83. package/dist/node/handlers/index.js +2 -358
  84. package/dist/node/handlers/saas.handlers.js +2 -134
  85. package/dist/node/index.js +9 -3591
  86. package/dist/node/presentations/index.js +1 -299
  87. package/dist/node/project/index.js +1 -793
  88. package/dist/node/project/project.entity.js +1 -77
  89. package/dist/node/project/project.enum.js +1 -18
  90. package/dist/node/project/project.event.js +1 -103
  91. package/dist/node/project/project.handler.js +1 -178
  92. package/dist/node/project/project.operations.js +1 -372
  93. package/dist/node/project/project.presentation.js +1 -180
  94. package/dist/node/project/project.schema.js +1 -134
  95. package/dist/node/saas-boilerplate.feature.js +1 -304
  96. package/dist/node/seeders/index.js +2 -20
  97. package/dist/node/settings/index.js +1 -75
  98. package/dist/node/settings/settings.entity.js +1 -74
  99. package/dist/node/settings/settings.enum.js +1 -11
  100. package/dist/node/shared/mock-data.js +1 -104
  101. package/dist/node/tests/operations.test-spec.js +1 -112
  102. package/dist/node/ui/SaasDashboard.js +1 -1239
  103. package/dist/node/ui/SaasDashboard.visualizations.js +1 -249
  104. package/dist/node/ui/SaasProjectList.js +1 -162
  105. package/dist/node/ui/SaasSettingsPanel.js +1 -145
  106. package/dist/node/ui/hooks/index.js +1 -159
  107. package/dist/node/ui/hooks/useProjectList.js +1 -66
  108. package/dist/node/ui/hooks/useProjectMutations.js +1 -91
  109. package/dist/node/ui/index.js +5 -2077
  110. package/dist/node/ui/modals/CreateProjectModal.js +1 -153
  111. package/dist/node/ui/modals/ProjectActionsModal.js +1 -335
  112. package/dist/node/ui/modals/index.js +1 -487
  113. package/dist/node/ui/overlays/demo-overlays.js +1 -61
  114. package/dist/node/ui/overlays/index.js +1 -61
  115. package/dist/node/ui/renderers/index.js +5 -901
  116. package/dist/node/ui/renderers/project-list.markdown.js +5 -725
  117. package/dist/node/ui/renderers/project-list.renderer.js +1 -177
  118. package/dist/node/visualizations/catalog.js +1 -155
  119. package/dist/node/visualizations/index.js +1 -217
  120. package/dist/node/visualizations/selectors.js +1 -210
  121. package/dist/presentations/index.js +1 -299
  122. package/dist/project/index.js +1 -793
  123. package/dist/project/project.entity.js +1 -77
  124. package/dist/project/project.enum.js +1 -18
  125. package/dist/project/project.event.js +1 -103
  126. package/dist/project/project.handler.js +1 -178
  127. package/dist/project/project.operations.js +1 -372
  128. package/dist/project/project.presentation.js +1 -180
  129. package/dist/project/project.schema.js +1 -134
  130. package/dist/saas-boilerplate.feature.js +1 -304
  131. package/dist/seeders/index.js +2 -20
  132. package/dist/settings/index.js +1 -75
  133. package/dist/settings/settings.entity.js +1 -74
  134. package/dist/settings/settings.enum.js +1 -11
  135. package/dist/shared/mock-data.js +1 -104
  136. package/dist/tests/operations.test-spec.js +1 -112
  137. package/dist/ui/SaasDashboard.js +1 -1239
  138. package/dist/ui/SaasDashboard.visualizations.js +1 -249
  139. package/dist/ui/SaasProjectList.js +1 -162
  140. package/dist/ui/SaasSettingsPanel.js +1 -145
  141. package/dist/ui/hooks/index.js +1 -159
  142. package/dist/ui/hooks/useProjectList.js +1 -66
  143. package/dist/ui/hooks/useProjectMutations.js +1 -91
  144. package/dist/ui/index.js +5 -2077
  145. package/dist/ui/modals/CreateProjectModal.js +1 -153
  146. package/dist/ui/modals/ProjectActionsModal.js +1 -335
  147. package/dist/ui/modals/index.js +1 -487
  148. package/dist/ui/overlays/demo-overlays.js +1 -61
  149. package/dist/ui/overlays/index.js +1 -61
  150. package/dist/ui/renderers/index.js +5 -901
  151. package/dist/ui/renderers/project-list.markdown.js +5 -725
  152. package/dist/ui/renderers/project-list.renderer.js +1 -177
  153. package/dist/visualizations/catalog.js +1 -155
  154. package/dist/visualizations/index.js +1 -217
  155. package/dist/visualizations/selectors.js +1 -210
  156. package/package.json +15 -15
package/dist/index.js CHANGED
@@ -1,728 +1,5 @@
1
1
  // @bun
2
- // src/billing/billing.entity.ts
3
- import {
4
- defineEntity,
5
- defineEntityEnum,
6
- field,
7
- index
8
- } from "@contractspec/lib.schema";
9
- var SubscriptionStatusEnum = defineEntityEnum({
10
- name: "SubscriptionStatus",
11
- values: ["TRIALING", "ACTIVE", "PAST_DUE", "CANCELED", "PAUSED"],
12
- schema: "saas_app",
13
- description: "Status of a subscription."
14
- });
15
- var SubscriptionEntity = defineEntity({
16
- name: "Subscription",
17
- description: "Organization subscription/plan information.",
18
- schema: "saas_app",
19
- map: "subscription",
20
- fields: {
21
- id: field.id(),
22
- organizationId: field.foreignKey({ isUnique: true }),
23
- planId: field.string({ description: "Plan identifier" }),
24
- planName: field.string({ description: "Plan display name" }),
25
- status: field.enum("SubscriptionStatus"),
26
- currentPeriodStart: field.dateTime(),
27
- currentPeriodEnd: field.dateTime(),
28
- trialEndsAt: field.dateTime({ isOptional: true }),
29
- cancelAtPeriodEnd: field.boolean({ default: false }),
30
- canceledAt: field.dateTime({ isOptional: true }),
31
- stripeSubscriptionId: field.string({ isOptional: true }),
32
- stripeCustomerId: field.string({ isOptional: true }),
33
- metadata: field.json({ isOptional: true }),
34
- createdAt: field.createdAt(),
35
- updatedAt: field.updatedAt()
36
- },
37
- enums: [SubscriptionStatusEnum]
38
- });
39
- var BillingUsageEntity = defineEntity({
40
- name: "BillingUsage",
41
- description: "Track usage of metered features.",
42
- schema: "saas_app",
43
- map: "billing_usage",
44
- fields: {
45
- id: field.id(),
46
- organizationId: field.foreignKey(),
47
- feature: field.string({
48
- description: 'Feature being tracked (e.g., "api_calls", "storage_gb")'
49
- }),
50
- quantity: field.int({ description: "Usage quantity" }),
51
- unit: field.string({
52
- isOptional: true,
53
- description: "Unit of measurement"
54
- }),
55
- billingPeriod: field.string({
56
- description: 'Billing period (e.g., "2024-01")'
57
- }),
58
- recordedAt: field.dateTime({ description: "When usage was recorded" }),
59
- sourceId: field.string({
60
- isOptional: true,
61
- description: "Source of usage (e.g., request ID)"
62
- }),
63
- sourceType: field.string({ isOptional: true }),
64
- metadata: field.json({ isOptional: true })
65
- },
66
- indexes: [
67
- index.on(["organizationId", "feature", "billingPeriod"]),
68
- index.on(["organizationId", "recordedAt"])
69
- ]
70
- });
71
- var UsageLimitEntity = defineEntity({
72
- name: "UsageLimit",
73
- description: "Usage limits per plan/organization.",
74
- schema: "saas_app",
75
- map: "usage_limit",
76
- fields: {
77
- id: field.id(),
78
- planId: field.string({
79
- isOptional: true,
80
- description: "Plan this limit applies to"
81
- }),
82
- organizationId: field.string({
83
- isOptional: true,
84
- description: "Org-specific override"
85
- }),
86
- feature: field.string({ description: "Feature being limited" }),
87
- limit: field.int({ description: "Maximum allowed usage" }),
88
- resetPeriod: field.string({
89
- default: '"monthly"',
90
- description: "When limit resets"
91
- }),
92
- isSoftLimit: field.boolean({
93
- default: false,
94
- description: "Whether to warn vs block"
95
- }),
96
- overage: field.boolean({
97
- default: false,
98
- description: "Whether overage is allowed"
99
- }),
100
- overageRate: field.float({
101
- isOptional: true,
102
- description: "Cost per unit over limit"
103
- }),
104
- createdAt: field.createdAt(),
105
- updatedAt: field.updatedAt()
106
- },
107
- indexes: [index.unique(["planId", "feature"])]
108
- });
109
-
110
- // src/billing/billing.enum.ts
111
- import { defineEnum } from "@contractspec/lib.schema";
112
- var SubscriptionStatusSchemaEnum = defineEnum("SubscriptionStatus", [
113
- "TRIALING",
114
- "ACTIVE",
115
- "PAST_DUE",
116
- "CANCELED",
117
- "PAUSED"
118
- ]);
119
- var FeatureAccessReasonEnum = defineEnum("FeatureAccessReason", [
120
- "included",
121
- "limit_available",
122
- "limit_reached",
123
- "not_in_plan"
124
- ]);
125
-
126
- // src/billing/billing.event.ts
127
- import { defineEvent } from "@contractspec/lib.contracts-spec";
128
- import { defineSchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";
129
- var UsageRecordedPayload = defineSchemaModel({
130
- name: "UsageRecordedPayload",
131
- description: "Payload when feature usage is recorded",
132
- fields: {
133
- organizationId: {
134
- type: ScalarTypeEnum.String_unsecure(),
135
- isOptional: false
136
- },
137
- feature: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
138
- quantity: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
139
- billingPeriod: {
140
- type: ScalarTypeEnum.String_unsecure(),
141
- isOptional: false
142
- },
143
- recordedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false }
144
- }
145
- });
146
- var UsageLimitReachedPayload = defineSchemaModel({
147
- name: "UsageLimitReachedPayload",
148
- description: "Payload when usage limit is reached",
149
- fields: {
150
- organizationId: {
151
- type: ScalarTypeEnum.String_unsecure(),
152
- isOptional: false
153
- },
154
- feature: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
155
- limit: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
156
- currentUsage: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
157
- reachedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false }
158
- }
159
- });
160
- var SubscriptionChangedPayload = defineSchemaModel({
161
- name: "SubscriptionChangedPayload",
162
- description: "Payload when subscription status changes",
163
- fields: {
164
- organizationId: {
165
- type: ScalarTypeEnum.String_unsecure(),
166
- isOptional: false
167
- },
168
- previousPlan: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
169
- newPlan: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
170
- previousStatus: {
171
- type: ScalarTypeEnum.String_unsecure(),
172
- isOptional: true
173
- },
174
- newStatus: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
175
- changedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false }
176
- }
177
- });
178
- var UsageRecordedEvent = defineEvent({
179
- meta: {
180
- key: "billing.usage.recorded",
181
- version: "1.0.0",
182
- description: "Feature usage has been recorded.",
183
- stability: "stable",
184
- owners: ["@saas-team"],
185
- tags: ["billing", "usage", "recorded"]
186
- },
187
- payload: UsageRecordedPayload
188
- });
189
- var UsageLimitReachedEvent = defineEvent({
190
- meta: {
191
- key: "billing.limit.reached",
192
- version: "1.0.0",
193
- description: "Usage limit has been reached for a feature.",
194
- stability: "stable",
195
- owners: ["@saas-team"],
196
- tags: ["billing", "limit", "reached"]
197
- },
198
- payload: UsageLimitReachedPayload
199
- });
200
- var SubscriptionChangedEvent = defineEvent({
201
- meta: {
202
- key: "billing.subscription.changed",
203
- version: "1.0.0",
204
- description: "Subscription status has changed.",
205
- stability: "stable",
206
- owners: ["@saas-team"],
207
- tags: ["billing", "subscription", "changed"]
208
- },
209
- payload: SubscriptionChangedPayload
210
- });
211
-
212
- // src/shared/mock-data.ts
213
- var MOCK_PROJECTS = [
214
- {
215
- id: "proj-1",
216
- name: "Marketing Website",
217
- description: "Main company website redesign project",
218
- slug: "marketing-website",
219
- organizationId: "demo-org",
220
- createdBy: "user-1",
221
- status: "ACTIVE",
222
- isPublic: false,
223
- tags: ["marketing", "website", "redesign"],
224
- createdAt: new Date("2024-01-15T10:00:00Z"),
225
- updatedAt: new Date("2024-03-20T14:30:00Z")
226
- },
227
- {
228
- id: "proj-2",
229
- name: "Mobile App v2",
230
- description: "Next generation mobile application",
231
- slug: "mobile-app-v2",
232
- organizationId: "demo-org",
233
- createdBy: "user-2",
234
- status: "ACTIVE",
235
- isPublic: false,
236
- tags: ["mobile", "app", "v2"],
237
- createdAt: new Date("2024-02-01T09:00:00Z"),
238
- updatedAt: new Date("2024-04-05T11:15:00Z")
239
- },
240
- {
241
- id: "proj-3",
242
- name: "API Integration",
243
- description: "Third-party API integration project",
244
- slug: "api-integration",
245
- organizationId: "demo-org",
246
- createdBy: "user-1",
247
- status: "DRAFT",
248
- isPublic: false,
249
- tags: ["api", "integration"],
250
- createdAt: new Date("2024-03-10T08:00:00Z"),
251
- updatedAt: new Date("2024-03-10T08:00:00Z")
252
- },
253
- {
254
- id: "proj-4",
255
- name: "Analytics Dashboard",
256
- description: "Internal analytics and reporting dashboard",
257
- slug: "analytics-dashboard",
258
- organizationId: "demo-org",
259
- createdBy: "user-3",
260
- status: "ARCHIVED",
261
- isPublic: true,
262
- tags: ["analytics", "dashboard", "reporting"],
263
- createdAt: new Date("2023-10-01T12:00:00Z"),
264
- updatedAt: new Date("2024-02-28T16:45:00Z")
265
- }
266
- ];
267
- var MOCK_SUBSCRIPTION = {
268
- id: "sub-1",
269
- organizationId: "demo-org",
270
- planId: "pro",
271
- planName: "Professional",
272
- status: "ACTIVE",
273
- currentPeriodStart: new Date("2024-04-01T00:00:00Z"),
274
- currentPeriodEnd: new Date("2024-05-01T00:00:00Z"),
275
- limits: {
276
- projects: 25,
277
- users: 10,
278
- storage: 50,
279
- apiCalls: 1e5
280
- },
281
- usage: {
282
- projects: 4,
283
- users: 5,
284
- storage: 12.5,
285
- apiCalls: 45230
286
- }
287
- };
288
- var MOCK_USAGE_SUMMARY = {
289
- organizationId: "demo-org",
290
- period: "current_month",
291
- apiCalls: {
292
- total: 45230,
293
- limit: 1e5,
294
- percentUsed: 45.23
295
- },
296
- storage: {
297
- totalGb: 12.5,
298
- limitGb: 50,
299
- percentUsed: 25
300
- },
301
- activeProjects: 4,
302
- activeUsers: 5,
303
- breakdown: [
304
- { date: "2024-04-01", apiCalls: 3200, storageGb: 12.1 },
305
- { date: "2024-04-02", apiCalls: 2800, storageGb: 12.2 },
306
- { date: "2024-04-03", apiCalls: 4100, storageGb: 12.3 },
307
- { date: "2024-04-04", apiCalls: 3600, storageGb: 12.4 },
308
- { date: "2024-04-05", apiCalls: 3800, storageGb: 12.5 }
309
- ]
310
- };
311
-
312
- // src/billing/billing.handler.ts
313
- async function mockGetSubscriptionHandler() {
314
- return MOCK_SUBSCRIPTION;
315
- }
316
- async function mockGetUsageSummaryHandler(input) {
317
- return {
318
- ...MOCK_USAGE_SUMMARY,
319
- period: input.period ?? "current_month"
320
- };
321
- }
322
- async function mockRecordUsageHandler(input) {
323
- const currentUsage = MOCK_USAGE_SUMMARY.apiCalls.total;
324
- const newTotal = currentUsage + input.quantity;
325
- return {
326
- recorded: true,
327
- newTotal
328
- };
329
- }
330
- async function mockCheckFeatureAccessHandler(input) {
331
- const { feature } = input;
332
- const featureMap = {
333
- custom_domains: {
334
- allowed: true
335
- },
336
- api_access: {
337
- allowed: true,
338
- currentUsage: MOCK_USAGE_SUMMARY.apiCalls.total,
339
- limit: MOCK_USAGE_SUMMARY.apiCalls.limit
340
- },
341
- advanced_analytics: {
342
- allowed: false,
343
- reason: "FEATURE_NOT_INCLUDED"
344
- },
345
- unlimited_projects: {
346
- allowed: false,
347
- reason: "PLAN_LIMIT",
348
- currentUsage: MOCK_SUBSCRIPTION.usage.projects,
349
- limit: MOCK_SUBSCRIPTION.limits.projects
350
- }
351
- };
352
- return featureMap[feature] ?? { allowed: true };
353
- }
354
-
355
- // src/billing/billing.schema.ts
356
- import { defineSchemaModel as defineSchemaModel2, ScalarTypeEnum as ScalarTypeEnum2 } from "@contractspec/lib.schema";
357
- var SubscriptionModel = defineSchemaModel2({
358
- name: "Subscription",
359
- description: "Organization subscription details",
360
- fields: {
361
- id: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
362
- organizationId: {
363
- type: ScalarTypeEnum2.String_unsecure(),
364
- isOptional: false
365
- },
366
- planId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
367
- planName: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
368
- status: { type: SubscriptionStatusSchemaEnum, isOptional: false },
369
- currentPeriodStart: { type: ScalarTypeEnum2.DateTime(), isOptional: false },
370
- currentPeriodEnd: { type: ScalarTypeEnum2.DateTime(), isOptional: false },
371
- trialEndsAt: { type: ScalarTypeEnum2.DateTime(), isOptional: true },
372
- cancelAtPeriodEnd: { type: ScalarTypeEnum2.Boolean(), isOptional: false }
373
- }
374
- });
375
- var UsageSummaryModel = defineSchemaModel2({
376
- name: "UsageSummary",
377
- description: "Usage summary for a feature",
378
- fields: {
379
- feature: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
380
- used: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
381
- limit: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: true },
382
- unit: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
383
- percentage: { type: ScalarTypeEnum2.Float_unsecure(), isOptional: true }
384
- }
385
- });
386
- var RecordUsageInputModel = defineSchemaModel2({
387
- name: "RecordUsageInput",
388
- description: "Input for recording feature usage",
389
- fields: {
390
- feature: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
391
- quantity: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
392
- sourceId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
393
- sourceType: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
394
- metadata: { type: ScalarTypeEnum2.JSONObject(), isOptional: true }
395
- }
396
- });
397
- var RecordUsageOutputModel = defineSchemaModel2({
398
- name: "RecordUsageOutput",
399
- description: "Output for recording feature usage",
400
- fields: {
401
- recorded: { type: ScalarTypeEnum2.Boolean(), isOptional: false },
402
- currentUsage: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
403
- limit: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: true },
404
- limitReached: { type: ScalarTypeEnum2.Boolean(), isOptional: false }
405
- }
406
- });
407
- var UsageRecordedPayloadModel = defineSchemaModel2({
408
- name: "UsageRecordedPayload",
409
- description: "Payload for usage.recorded event",
410
- fields: {
411
- feature: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
412
- quantity: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false }
413
- }
414
- });
415
- var GetUsageSummaryInputModel = defineSchemaModel2({
416
- name: "GetUsageSummaryInput",
417
- description: "Input for getting usage summary",
418
- fields: {
419
- billingPeriod: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true }
420
- }
421
- });
422
- var GetUsageSummaryOutputModel = defineSchemaModel2({
423
- name: "GetUsageSummaryOutput",
424
- description: "Output for usage summary",
425
- fields: {
426
- billingPeriod: {
427
- type: ScalarTypeEnum2.String_unsecure(),
428
- isOptional: false
429
- },
430
- usage: { type: UsageSummaryModel, isArray: true, isOptional: false }
431
- }
432
- });
433
- var CheckFeatureAccessInputModel = defineSchemaModel2({
434
- name: "CheckFeatureAccessInput",
435
- description: "Input for checking feature access",
436
- fields: {
437
- feature: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false }
438
- }
439
- });
440
- var CheckFeatureAccessOutputModel = defineSchemaModel2({
441
- name: "CheckFeatureAccessOutput",
442
- description: "Output for feature access check",
443
- fields: {
444
- hasAccess: { type: ScalarTypeEnum2.Boolean(), isOptional: false },
445
- reason: { type: FeatureAccessReasonEnum, isOptional: true },
446
- upgradeUrl: { type: ScalarTypeEnum2.URL(), isOptional: true }
447
- }
448
- });
449
-
450
- // src/billing/billing.operations.ts
451
- import { defineCommand, defineQuery } from "@contractspec/lib.contracts-spec";
452
- var OWNERS = ["@example.saas-boilerplate"];
453
- var GetSubscriptionContract = defineQuery({
454
- meta: {
455
- key: "saas.billing.subscription.get",
456
- version: "1.0.0",
457
- stability: "stable",
458
- owners: [...OWNERS],
459
- tags: ["saas", "billing", "subscription"],
460
- description: "Get organization subscription status.",
461
- goal: "Show current plan and billing status.",
462
- context: "Billing page, plan upgrade prompts."
463
- },
464
- io: {
465
- input: null,
466
- output: SubscriptionModel
467
- },
468
- policy: {
469
- auth: "user"
470
- },
471
- acceptance: {
472
- scenarios: [
473
- {
474
- key: "get-subscription-happy-path",
475
- given: ["Organization has active subscription"],
476
- when: ["User requests subscription status"],
477
- then: ["Subscription details are returned"]
478
- }
479
- ],
480
- examples: [
481
- {
482
- key: "get-basic",
483
- input: null,
484
- output: {
485
- plan: "pro",
486
- status: "active",
487
- currentPeriodEnd: "2025-02-01T00:00:00Z"
488
- }
489
- }
490
- ]
491
- }
492
- });
493
- var RecordUsageContract = defineCommand({
494
- meta: {
495
- key: "saas.billing.usage.record",
496
- version: "1.0.0",
497
- stability: "stable",
498
- owners: [...OWNERS],
499
- tags: ["saas", "billing", "usage"],
500
- description: "Record usage of a metered feature.",
501
- goal: "Track feature usage for billing.",
502
- context: "Called by services when metered features are used."
503
- },
504
- io: {
505
- input: RecordUsageInputModel,
506
- output: RecordUsageOutputModel
507
- },
508
- policy: {
509
- auth: "user"
510
- },
511
- sideEffects: {
512
- emits: [
513
- {
514
- key: "billing.usage.recorded",
515
- version: "1.0.0",
516
- when: "Usage is recorded",
517
- payload: UsageRecordedPayloadModel
518
- }
519
- ]
520
- },
521
- acceptance: {
522
- scenarios: [
523
- {
524
- key: "record-usage-happy-path",
525
- given: ["Organization exists"],
526
- when: ["System records feature usage"],
527
- then: ["Usage is recorded"]
528
- }
529
- ],
530
- examples: [
531
- {
532
- key: "record-api-call",
533
- input: { feature: "api_calls", quantity: 1, idempotencyKey: "abc-123" },
534
- output: { recorded: true, currentUsage: 100 }
535
- }
536
- ]
537
- }
538
- });
539
- var GetUsageSummaryContract = defineQuery({
540
- meta: {
541
- key: "saas.billing.usage.summary",
542
- version: "1.0.0",
543
- stability: "stable",
544
- owners: [...OWNERS],
545
- tags: ["saas", "billing", "usage"],
546
- description: "Get usage summary for the current billing period.",
547
- goal: "Show usage vs limits.",
548
- context: "Billing page, usage dashboards."
549
- },
550
- io: {
551
- input: GetUsageSummaryInputModel,
552
- output: GetUsageSummaryOutputModel
553
- },
554
- policy: {
555
- auth: "user"
556
- },
557
- acceptance: {
558
- scenarios: [
559
- {
560
- key: "get-usage-happy-path",
561
- given: ["Organization has usage history"],
562
- when: ["User requests usage summary"],
563
- then: ["Usage metrics are returned"]
564
- }
565
- ],
566
- examples: [
567
- {
568
- key: "get-current-usage",
569
- input: { period: "current" },
570
- output: { features: [{ name: "api_calls", used: 100, limit: 1000 }] }
571
- }
572
- ]
573
- }
574
- });
575
- var CheckFeatureAccessContract = defineQuery({
576
- meta: {
577
- key: "saas.billing.feature.check",
578
- version: "1.0.0",
579
- stability: "stable",
580
- owners: [...OWNERS],
581
- tags: ["saas", "billing", "feature"],
582
- description: "Check if organization has access to a feature.",
583
- goal: "Gate features based on plan/usage.",
584
- context: "Feature access checks, upgrade prompts."
585
- },
586
- io: {
587
- input: CheckFeatureAccessInputModel,
588
- output: CheckFeatureAccessOutputModel
589
- },
590
- policy: {
591
- auth: "user"
592
- },
593
- acceptance: {
594
- scenarios: [
595
- {
596
- key: "check-access-granted",
597
- given: ["Organization is on Pro plan"],
598
- when: ["User checks access to Pro feature"],
599
- then: ["Access is granted"]
600
- }
601
- ],
602
- examples: [
603
- {
604
- key: "check-advanced-reports",
605
- input: { feature: "advanced_reports" },
606
- output: { hasAccess: true, reason: "Included in Pro plan" }
607
- }
608
- ]
609
- }
610
- });
611
-
612
- // src/billing/billing.presentation.ts
613
- import {
614
- definePresentation,
615
- StabilityEnum
616
- } from "@contractspec/lib.contracts-spec";
617
- var SubscriptionPresentation = definePresentation({
618
- meta: {
619
- key: "saas.billing.subscription",
620
- version: "1.0.0",
621
- title: "Subscription Status",
622
- description: "Subscription status with plan info, limits, and current usage",
623
- domain: "saas-boilerplate",
624
- owners: ["@saas-team"],
625
- tags: ["billing", "subscription"],
626
- stability: StabilityEnum.Beta,
627
- goal: "View subscription plan and status",
628
- context: "Billing section"
629
- },
630
- source: {
631
- type: "component",
632
- framework: "react",
633
- componentKey: "SubscriptionView"
634
- },
635
- targets: ["react", "markdown"],
636
- policy: {
637
- flags: ["saas.billing.enabled"]
638
- }
639
- });
640
- var UsageDashboardPresentation = definePresentation({
641
- meta: {
642
- key: "saas.billing.usage",
643
- version: "1.0.0",
644
- title: "Usage Dashboard",
645
- description: "Usage metrics and breakdown by resource type",
646
- domain: "saas-boilerplate",
647
- owners: ["@saas-team"],
648
- tags: ["billing", "usage", "metrics"],
649
- stability: StabilityEnum.Beta,
650
- goal: "Monitor feature usage and limits",
651
- context: "Billing section"
652
- },
653
- source: {
654
- type: "component",
655
- framework: "react",
656
- componentKey: "UsageDashboardView"
657
- },
658
- targets: ["react", "markdown"],
659
- policy: {
660
- flags: ["saas.billing.enabled"]
661
- }
662
- });
663
- // src/dashboard/dashboard.presentation.ts
664
- import {
665
- definePresentation as definePresentation2,
666
- StabilityEnum as StabilityEnum2
667
- } from "@contractspec/lib.contracts-spec";
668
- var SaasDashboardPresentation = definePresentation2({
669
- meta: {
670
- key: "saas.dashboard",
671
- version: "1.0.0",
672
- title: "SaaS Dashboard",
673
- description: "Main SaaS dashboard with project overview, usage stats, and quick actions",
674
- domain: "saas-boilerplate",
675
- owners: ["@saas-team"],
676
- tags: ["dashboard", "overview"],
677
- stability: StabilityEnum2.Beta,
678
- goal: "Overview of SaaS activity and metrics",
679
- context: "Main dashboard"
680
- },
681
- source: {
682
- type: "component",
683
- framework: "react",
684
- componentKey: "SaasDashboard"
685
- },
686
- targets: ["react", "markdown"],
687
- policy: {
688
- flags: ["saas.enabled"]
689
- }
690
- });
691
- var SettingsPanelPresentation = definePresentation2({
692
- meta: {
693
- key: "saas.settings",
694
- version: "1.0.0",
695
- title: "Settings Panel",
696
- description: "Organization and user settings panel",
697
- domain: "saas-boilerplate",
698
- owners: ["@saas-team"],
699
- tags: ["settings", "config"],
700
- stability: StabilityEnum2.Beta,
701
- goal: "Configure organization and user settings",
702
- context: "Settings section"
703
- },
704
- source: {
705
- type: "component",
706
- framework: "react",
707
- componentKey: "SettingsPanel"
708
- },
709
- targets: ["react"],
710
- policy: {
711
- flags: ["saas.enabled"]
712
- }
713
- });
714
- // src/docs/saas-boilerplate.docblock.ts
715
- import { registerDocBlocks } from "@contractspec/lib.contracts-spec/docs";
716
- var saasBoilerplateDocBlocks = [
717
- {
718
- id: "docs.examples.saas-boilerplate.goal",
719
- title: "SaaS Boilerplate \u2014 Goal",
720
- summary: "Multi-tenant SaaS foundation with orgs, members, projects, settings, and usage.",
721
- kind: "goal",
722
- visibility: "public",
723
- route: "/docs/examples/saas-boilerplate/goal",
724
- tags: ["saas", "goal"],
725
- body: `## Why it matters
2
+ import{defineEntity as NH,defineEntityEnum as TG,field as W,index as VH}from"@contractspec/lib.schema";var s=TG({name:"SubscriptionStatus",values:["TRIALING","ACTIVE","PAST_DUE","CANCELED","PAUSED"],schema:"saas_app",description:"Status of a subscription."}),zH=NH({name:"Subscription",description:"Organization subscription/plan information.",schema:"saas_app",map:"subscription",fields:{id:W.id(),organizationId:W.foreignKey({isUnique:!0}),planId:W.string({description:"Plan identifier"}),planName:W.string({description:"Plan display name"}),status:W.enum("SubscriptionStatus"),currentPeriodStart:W.dateTime(),currentPeriodEnd:W.dateTime(),trialEndsAt:W.dateTime({isOptional:!0}),cancelAtPeriodEnd:W.boolean({default:!1}),canceledAt:W.dateTime({isOptional:!0}),stripeSubscriptionId:W.string({isOptional:!0}),stripeCustomerId:W.string({isOptional:!0}),metadata:W.json({isOptional:!0}),createdAt:W.createdAt(),updatedAt:W.updatedAt()},enums:[s]}),DH=NH({name:"BillingUsage",description:"Track usage of metered features.",schema:"saas_app",map:"billing_usage",fields:{id:W.id(),organizationId:W.foreignKey(),feature:W.string({description:'Feature being tracked (e.g., "api_calls", "storage_gb")'}),quantity:W.int({description:"Usage quantity"}),unit:W.string({isOptional:!0,description:"Unit of measurement"}),billingPeriod:W.string({description:'Billing period (e.g., "2024-01")'}),recordedAt:W.dateTime({description:"When usage was recorded"}),sourceId:W.string({isOptional:!0,description:"Source of usage (e.g., request ID)"}),sourceType:W.string({isOptional:!0}),metadata:W.json({isOptional:!0})},indexes:[VH.on(["organizationId","feature","billingPeriod"]),VH.on(["organizationId","recordedAt"])]}),AH=NH({name:"UsageLimit",description:"Usage limits per plan/organization.",schema:"saas_app",map:"usage_limit",fields:{id:W.id(),planId:W.string({isOptional:!0,description:"Plan this limit applies to"}),organizationId:W.string({isOptional:!0,description:"Org-specific override"}),feature:W.string({description:"Feature being limited"}),limit:W.int({description:"Maximum allowed usage"}),resetPeriod:W.string({default:'"monthly"',description:"When limit resets"}),isSoftLimit:W.boolean({default:!1,description:"Whether to warn vs block"}),overage:W.boolean({default:!1,description:"Whether overage is allowed"}),overageRate:W.float({isOptional:!0,description:"Cost per unit over limit"}),createdAt:W.createdAt(),updatedAt:W.updatedAt()},indexes:[VH.unique(["planId","feature"])]});import{defineEnum as $G}from"@contractspec/lib.schema";var RH=$G("SubscriptionStatus",["TRIALING","ACTIVE","PAST_DUE","CANCELED","PAUSED"]),_H=$G("FeatureAccessReason",["included","limit_available","limit_reached","not_in_plan"]);import{defineEvent as BH}from"@contractspec/lib.contracts-spec";import{defineSchemaModel as IH,ScalarTypeEnum as h}from"@contractspec/lib.schema";var EG=IH({name:"UsageRecordedPayload",description:"Payload when feature usage is recorded",fields:{organizationId:{type:h.String_unsecure(),isOptional:!1},feature:{type:h.String_unsecure(),isOptional:!1},quantity:{type:h.Int_unsecure(),isOptional:!1},billingPeriod:{type:h.String_unsecure(),isOptional:!1},recordedAt:{type:h.DateTime(),isOptional:!1}}}),xG=IH({name:"UsageLimitReachedPayload",description:"Payload when usage limit is reached",fields:{organizationId:{type:h.String_unsecure(),isOptional:!1},feature:{type:h.String_unsecure(),isOptional:!1},limit:{type:h.Int_unsecure(),isOptional:!1},currentUsage:{type:h.Int_unsecure(),isOptional:!1},reachedAt:{type:h.DateTime(),isOptional:!1}}}),mG=IH({name:"SubscriptionChangedPayload",description:"Payload when subscription status changes",fields:{organizationId:{type:h.String_unsecure(),isOptional:!1},previousPlan:{type:h.String_unsecure(),isOptional:!0},newPlan:{type:h.String_unsecure(),isOptional:!1},previousStatus:{type:h.String_unsecure(),isOptional:!0},newStatus:{type:h.String_unsecure(),isOptional:!1},changedAt:{type:h.DateTime(),isOptional:!1}}}),SG=BH({meta:{key:"billing.usage.recorded",version:"1.0.0",description:"Feature usage has been recorded.",stability:"stable",owners:["@saas-team"],tags:["billing","usage","recorded"]},payload:EG}),jG=BH({meta:{key:"billing.limit.reached",version:"1.0.0",description:"Usage limit has been reached for a feature.",stability:"stable",owners:["@saas-team"],tags:["billing","limit","reached"]},payload:xG}),dG=BH({meta:{key:"billing.subscription.changed",version:"1.0.0",description:"Subscription status has changed.",stability:"stable",owners:["@saas-team"],tags:["billing","subscription","changed"]},payload:mG});var S=[{id:"proj-1",name:"Marketing Website",description:"Main company website redesign project",slug:"marketing-website",organizationId:"demo-org",createdBy:"user-1",status:"ACTIVE",isPublic:!1,tags:["marketing","website","redesign"],createdAt:new Date("2024-01-15T10:00:00Z"),updatedAt:new Date("2024-03-20T14:30:00Z")},{id:"proj-2",name:"Mobile App v2",description:"Next generation mobile application",slug:"mobile-app-v2",organizationId:"demo-org",createdBy:"user-2",status:"ACTIVE",isPublic:!1,tags:["mobile","app","v2"],createdAt:new Date("2024-02-01T09:00:00Z"),updatedAt:new Date("2024-04-05T11:15:00Z")},{id:"proj-3",name:"API Integration",description:"Third-party API integration project",slug:"api-integration",organizationId:"demo-org",createdBy:"user-1",status:"DRAFT",isPublic:!1,tags:["api","integration"],createdAt:new Date("2024-03-10T08:00:00Z"),updatedAt:new Date("2024-03-10T08:00:00Z")},{id:"proj-4",name:"Analytics Dashboard",description:"Internal analytics and reporting dashboard",slug:"analytics-dashboard",organizationId:"demo-org",createdBy:"user-3",status:"ARCHIVED",isPublic:!0,tags:["analytics","dashboard","reporting"],createdAt:new Date("2023-10-01T12:00:00Z"),updatedAt:new Date("2024-02-28T16:45:00Z")}],d={id:"sub-1",organizationId:"demo-org",planId:"pro",planName:"Professional",status:"ACTIVE",currentPeriodStart:new Date("2024-04-01T00:00:00Z"),currentPeriodEnd:new Date("2024-05-01T00:00:00Z"),limits:{projects:25,users:10,storage:50,apiCalls:1e5},usage:{projects:4,users:5,storage:12.5,apiCalls:45230}},m={organizationId:"demo-org",period:"current_month",apiCalls:{total:45230,limit:1e5,percentUsed:45.23},storage:{totalGb:12.5,limitGb:50,percentUsed:25},activeProjects:4,activeUsers:5,breakdown:[{date:"2024-04-01",apiCalls:3200,storageGb:12.1},{date:"2024-04-02",apiCalls:2800,storageGb:12.2},{date:"2024-04-03",apiCalls:4100,storageGb:12.3},{date:"2024-04-04",apiCalls:3600,storageGb:12.4},{date:"2024-04-05",apiCalls:3800,storageGb:12.5}]};async function u(){return d}async function XG(H){return{...m,period:H.period??"current_month"}}async function ZG(H){return{recorded:!0,newTotal:m.apiCalls.total+H.quantity}}async function kG(H){let{feature:G}=H;return{custom_domains:{allowed:!0},api_access:{allowed:!0,currentUsage:m.apiCalls.total,limit:m.apiCalls.limit},advanced_analytics:{allowed:!1,reason:"FEATURE_NOT_INCLUDED"},unlimited_projects:{allowed:!1,reason:"PLAN_LIMIT",currentUsage:d.usage.projects,limit:d.limits.projects}}[G]??{allowed:!0}}import{defineSchemaModel as E,ScalarTypeEnum as A}from"@contractspec/lib.schema";var vH=E({name:"Subscription",description:"Organization subscription details",fields:{id:{type:A.String_unsecure(),isOptional:!1},organizationId:{type:A.String_unsecure(),isOptional:!1},planId:{type:A.String_unsecure(),isOptional:!1},planName:{type:A.String_unsecure(),isOptional:!1},status:{type:RH,isOptional:!1},currentPeriodStart:{type:A.DateTime(),isOptional:!1},currentPeriodEnd:{type:A.DateTime(),isOptional:!1},trialEndsAt:{type:A.DateTime(),isOptional:!0},cancelAtPeriodEnd:{type:A.Boolean(),isOptional:!1}}}),YG=E({name:"UsageSummary",description:"Usage summary for a feature",fields:{feature:{type:A.String_unsecure(),isOptional:!1},used:{type:A.Int_unsecure(),isOptional:!1},limit:{type:A.Int_unsecure(),isOptional:!0},unit:{type:A.String_unsecure(),isOptional:!0},percentage:{type:A.Float_unsecure(),isOptional:!0}}}),LH=E({name:"RecordUsageInput",description:"Input for recording feature usage",fields:{feature:{type:A.String_unsecure(),isOptional:!1},quantity:{type:A.Int_unsecure(),isOptional:!1},sourceId:{type:A.String_unsecure(),isOptional:!0},sourceType:{type:A.String_unsecure(),isOptional:!0},metadata:{type:A.JSONObject(),isOptional:!0}}}),OH=E({name:"RecordUsageOutput",description:"Output for recording feature usage",fields:{recorded:{type:A.Boolean(),isOptional:!1},currentUsage:{type:A.Int_unsecure(),isOptional:!1},limit:{type:A.Int_unsecure(),isOptional:!0},limitReached:{type:A.Boolean(),isOptional:!1}}}),gH=E({name:"UsageRecordedPayload",description:"Payload for usage.recorded event",fields:{feature:{type:A.String_unsecure(),isOptional:!1},quantity:{type:A.Int_unsecure(),isOptional:!1}}}),wH=E({name:"GetUsageSummaryInput",description:"Input for getting usage summary",fields:{billingPeriod:{type:A.String_unsecure(),isOptional:!0}}}),PH=E({name:"GetUsageSummaryOutput",description:"Output for usage summary",fields:{billingPeriod:{type:A.String_unsecure(),isOptional:!1},usage:{type:YG,isArray:!0,isOptional:!1}}}),hH=E({name:"CheckFeatureAccessInput",description:"Input for checking feature access",fields:{feature:{type:A.String_unsecure(),isOptional:!1}}}),MH=E({name:"CheckFeatureAccessOutput",description:"Output for feature access check",fields:{hasAccess:{type:A.Boolean(),isOptional:!1},reason:{type:_H,isOptional:!0},upgradeUrl:{type:A.URL(),isOptional:!0}}});import{defineCommand as uG,defineQuery as CH}from"@contractspec/lib.contracts-spec";var t=["@example.saas-boilerplate"],lG=CH({meta:{key:"saas.billing.subscription.get",version:"1.0.0",stability:"stable",owners:[...t],tags:["saas","billing","subscription"],description:"Get organization subscription status.",goal:"Show current plan and billing status.",context:"Billing page, plan upgrade prompts."},io:{input:null,output:vH},policy:{auth:"user"},acceptance:{scenarios:[{key:"get-subscription-happy-path",given:["Organization has active subscription"],when:["User requests subscription status"],then:["Subscription details are returned"]}],examples:[{key:"get-basic",input:null,output:{plan:"pro",status:"active",currentPeriodEnd:"2025-02-01T00:00:00Z"}}]}}),pG=uG({meta:{key:"saas.billing.usage.record",version:"1.0.0",stability:"stable",owners:[...t],tags:["saas","billing","usage"],description:"Record usage of a metered feature.",goal:"Track feature usage for billing.",context:"Called by services when metered features are used."},io:{input:LH,output:OH},policy:{auth:"user"},sideEffects:{emits:[{key:"billing.usage.recorded",version:"1.0.0",when:"Usage is recorded",payload:gH}]},acceptance:{scenarios:[{key:"record-usage-happy-path",given:["Organization exists"],when:["System records feature usage"],then:["Usage is recorded"]}],examples:[{key:"record-api-call",input:{feature:"api_calls",quantity:1,idempotencyKey:"abc-123"},output:{recorded:!0,currentUsage:100}}]}}),cG=CH({meta:{key:"saas.billing.usage.summary",version:"1.0.0",stability:"stable",owners:[...t],tags:["saas","billing","usage"],description:"Get usage summary for the current billing period.",goal:"Show usage vs limits.",context:"Billing page, usage dashboards."},io:{input:wH,output:PH},policy:{auth:"user"},acceptance:{scenarios:[{key:"get-usage-happy-path",given:["Organization has usage history"],when:["User requests usage summary"],then:["Usage metrics are returned"]}],examples:[{key:"get-current-usage",input:{period:"current"},output:{features:[{name:"api_calls",used:100,limit:1000}]}}]}}),rG=CH({meta:{key:"saas.billing.feature.check",version:"1.0.0",stability:"stable",owners:[...t],tags:["saas","billing","feature"],description:"Check if organization has access to a feature.",goal:"Gate features based on plan/usage.",context:"Feature access checks, upgrade prompts."},io:{input:hH,output:MH},policy:{auth:"user"},acceptance:{scenarios:[{key:"check-access-granted",given:["Organization is on Pro plan"],when:["User checks access to Pro feature"],then:["Access is granted"]}],examples:[{key:"check-advanced-reports",input:{feature:"advanced_reports"},output:{hasAccess:!0,reason:"Included in Pro plan"}}]}});import{definePresentation as QG,StabilityEnum as UG}from"@contractspec/lib.contracts-spec";var nG=QG({meta:{key:"saas.billing.subscription",version:"1.0.0",title:"Subscription Status",description:"Subscription status with plan info, limits, and current usage",domain:"saas-boilerplate",owners:["@saas-team"],tags:["billing","subscription"],stability:UG.Beta,goal:"View subscription plan and status",context:"Billing section"},source:{type:"component",framework:"react",componentKey:"SubscriptionView"},targets:["react","markdown"],policy:{flags:["saas.billing.enabled"]}}),oG=QG({meta:{key:"saas.billing.usage",version:"1.0.0",title:"Usage Dashboard",description:"Usage metrics and breakdown by resource type",domain:"saas-boilerplate",owners:["@saas-team"],tags:["billing","usage","metrics"],stability:UG.Beta,goal:"Monitor feature usage and limits",context:"Billing section"},source:{type:"component",framework:"react",componentKey:"UsageDashboardView"},targets:["react","markdown"],policy:{flags:["saas.billing.enabled"]}});import{definePresentation as qG,StabilityEnum as FG}from"@contractspec/lib.contracts-spec";var iG=qG({meta:{key:"saas.dashboard",version:"1.0.0",title:"SaaS Dashboard",description:"Main SaaS dashboard with project overview, usage stats, and quick actions",domain:"saas-boilerplate",owners:["@saas-team"],tags:["dashboard","overview"],stability:FG.Beta,goal:"Overview of SaaS activity and metrics",context:"Main dashboard"},source:{type:"component",framework:"react",componentKey:"SaasDashboard"},targets:["react","markdown"],policy:{flags:["saas.enabled"]}}),aG=qG({meta:{key:"saas.settings",version:"1.0.0",title:"Settings Panel",description:"Organization and user settings panel",domain:"saas-boilerplate",owners:["@saas-team"],tags:["settings","config"],stability:FG.Beta,goal:"Configure organization and user settings",context:"Settings section"},source:{type:"component",framework:"react",componentKey:"SettingsPanel"},targets:["react"],policy:{flags:["saas.enabled"]}});import{registerDocBlocks as sG}from"@contractspec/lib.contracts-spec/docs";var tG=[{id:"docs.examples.saas-boilerplate.goal",title:"SaaS Boilerplate \u2014 Goal",summary:"Multi-tenant SaaS foundation with orgs, members, projects, settings, and usage.",kind:"goal",visibility:"public",route:"/docs/examples/saas-boilerplate/goal",tags:["saas","goal"],body:`## Why it matters
726
3
  - Provides a regenerable SaaS base: orgs, members, projects, settings, usage/billing.
727
4
  - Avoids drift across identity, settings, and usage capture.
728
5
 
@@ -732,17 +9,7 @@ var saasBoilerplateDocBlocks = [
732
9
 
733
10
  ## Success criteria
734
11
  - Spec changes to org/project/settings/usage regenerate UI/API/events cleanly.
735
- - Tenant isolation and RBAC stay enforced; usage data is captured with PII scopes.`
736
- },
737
- {
738
- id: "docs.examples.saas-boilerplate.usage",
739
- title: "SaaS Boilerplate \u2014 Usage",
740
- summary: "How to seed, extend, and regenerate the SaaS base.",
741
- kind: "usage",
742
- visibility: "public",
743
- route: "/docs/examples/saas-boilerplate/usage",
744
- tags: ["saas", "usage"],
745
- body: `## Setup
12
+ - Tenant isolation and RBAC stay enforced; usage data is captured with PII scopes.`},{id:"docs.examples.saas-boilerplate.usage",title:"SaaS Boilerplate \u2014 Usage",summary:"How to seed, extend, and regenerate the SaaS base.",kind:"usage",visibility:"public",route:"/docs/examples/saas-boilerplate/usage",tags:["saas","usage"],body:`## Setup
746
13
  1) Seed (if available) or create orgs, members, and projects via UI.
747
14
  2) Configure Notifications for invites and project events; set policy.pii for sensitive fields.
748
15
 
@@ -754,17 +21,7 @@ var saasBoilerplateDocBlocks = [
754
21
  ## Guardrails
755
22
  - Keep tenant/role context explicit in contracts and presentations.
756
23
  - Emit events for invites, project changes, and usage records; log in Audit Trail.
757
- - Redact sensitive user/org data in markdown/JSON outputs.`
758
- },
759
- {
760
- id: "docs.examples.saas-boilerplate.reference",
761
- title: "SaaS Boilerplate \u2014 Reference",
762
- summary: "Entities, contracts, events, and presentations for the SaaS starter.",
763
- kind: "reference",
764
- visibility: "public",
765
- route: "/docs/examples/saas-boilerplate",
766
- tags: ["saas", "reference"],
767
- body: `## Entities
24
+ - Redact sensitive user/org data in markdown/JSON outputs.`},{id:"docs.examples.saas-boilerplate.reference",title:"SaaS Boilerplate \u2014 Reference",summary:"Entities, contracts, events, and presentations for the SaaS starter.",kind:"reference",visibility:"public",route:"/docs/examples/saas-boilerplate",tags:["saas","reference"],body:`## Entities
768
25
  - Organization, Member, Role, Project, AppSettings, UserSettings, BillingUsage.
769
26
 
770
27
  ## Contracts
@@ -778,17 +35,7 @@ var saasBoilerplateDocBlocks = [
778
35
 
779
36
  ## Notes
780
37
  - Tenant isolation is mandatory; enforce via RBAC/policies.
781
- - Usage/Metering drives billing/limits; keep units explicit.`
782
- },
783
- {
784
- id: "docs.examples.saas-boilerplate.constraints",
785
- title: "SaaS Boilerplate \u2014 Constraints & Safety",
786
- summary: "Internal guardrails for tenancy, RBAC, usage metering, and regeneration.",
787
- kind: "reference",
788
- visibility: "internal",
789
- route: "/docs/examples/saas-boilerplate/constraints",
790
- tags: ["saas", "constraints", "internal"],
791
- body: `## Constraints
38
+ - Usage/Metering drives billing/limits; keep units explicit.`},{id:"docs.examples.saas-boilerplate.constraints",title:"SaaS Boilerplate \u2014 Constraints & Safety",summary:"Internal guardrails for tenancy, RBAC, usage metering, and regeneration.",kind:"reference",visibility:"internal",route:"/docs/examples/saas-boilerplate/constraints",tags:["saas","constraints","internal"],body:`## Constraints
792
39
  - Tenant isolation and RBAC must remain explicit in spec; no implicit defaults in code.
793
40
  - Events to emit: org.created, member.invited/accepted, project.created/updated, usage.recorded.
794
41
  - Regeneration must not change billing/usage semantics without spec diffs.
@@ -800,2837 +47,8 @@ var saasBoilerplateDocBlocks = [
800
47
  ## Verification
801
48
  - Add fixtures for usage recording and role changes.
802
49
  - Ensure Audit/Notifications remain wired for invites/project updates.
803
- - Use Feature Flags for new settings/billing fields; default safe/off.`
804
- }
805
- ];
806
- registerDocBlocks(saasBoilerplateDocBlocks);
807
- // src/example.ts
808
- import { defineExample } from "@contractspec/lib.contracts-spec";
809
- var example = defineExample({
810
- meta: {
811
- key: "saas-boilerplate",
812
- version: "1.0.0",
813
- title: "SaaS Boilerplate",
814
- description: "Multi-tenant SaaS foundation with orgs, projects, settings, billing usage, and RBAC.",
815
- kind: "template",
816
- visibility: "public",
817
- stability: "experimental",
818
- owners: ["@platform.core"],
819
- tags: ["saas", "multi-tenant", "billing", "rbac"]
820
- },
821
- docs: {
822
- rootDocId: "docs.examples.saas-boilerplate"
823
- },
824
- entrypoints: {
825
- packageName: "@contractspec/example.saas-boilerplate",
826
- feature: "./feature",
827
- contracts: "./contracts",
828
- presentations: "./presentations",
829
- handlers: "./handlers",
830
- docs: "./docs"
831
- },
832
- surfaces: {
833
- templates: true,
834
- sandbox: {
835
- enabled: true,
836
- modes: ["playground", "specs", "builder", "markdown", "evolution"]
837
- },
838
- studio: { enabled: true, installable: true },
839
- mcp: { enabled: true }
840
- }
841
- });
842
- var example_default = example;
843
-
844
- // src/project/project.handler.ts
845
- async function mockListProjectsHandler(input) {
846
- const { status, search, limit = 20, offset = 0 } = input;
847
- let filtered = [...MOCK_PROJECTS];
848
- if (status && status !== "all") {
849
- filtered = filtered.filter((p) => p.status === status);
850
- }
851
- if (search) {
852
- const q = search.toLowerCase();
853
- filtered = filtered.filter((p) => p.name.toLowerCase().includes(q) || p.description?.toLowerCase().includes(q) || p.tags.some((t) => t.toLowerCase().includes(q)));
854
- }
855
- filtered.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
856
- const total = filtered.length;
857
- const projects = filtered.slice(offset, offset + limit);
858
- return {
859
- projects,
860
- total
861
- };
862
- }
863
- async function mockGetProjectHandler(input) {
864
- const project = MOCK_PROJECTS.find((p) => p.id === input.projectId);
865
- if (!project) {
866
- throw new Error("NOT_FOUND");
867
- }
868
- return project;
869
- }
870
- async function mockCreateProjectHandler(input, context) {
871
- if (input.slug) {
872
- const exists = MOCK_PROJECTS.some((p) => p.slug === input.slug);
873
- if (exists) {
874
- throw new Error("SLUG_EXISTS");
875
- }
876
- }
877
- const now = new Date;
878
- return {
879
- id: `proj-${Date.now()}`,
880
- name: input.name,
881
- description: input.description,
882
- slug: input.slug ?? input.name.toLowerCase().replace(/\s+/g, "-"),
883
- organizationId: context.organizationId,
884
- createdBy: context.userId,
885
- status: "DRAFT",
886
- isPublic: input.isPublic ?? false,
887
- tags: input.tags ?? [],
888
- createdAt: now,
889
- updatedAt: now
890
- };
891
- }
892
- async function mockUpdateProjectHandler(input) {
893
- const project = MOCK_PROJECTS.find((p) => p.id === input.projectId);
894
- if (!project) {
895
- throw new Error("NOT_FOUND");
896
- }
897
- return {
898
- ...project,
899
- name: input.name ?? project.name,
900
- description: input.description ?? project.description,
901
- slug: input.slug ?? project.slug,
902
- isPublic: input.isPublic ?? project.isPublic,
903
- tags: input.tags ?? project.tags,
904
- status: input.status ?? project.status,
905
- updatedAt: new Date
906
- };
907
- }
908
- async function mockDeleteProjectHandler(input) {
909
- const project = MOCK_PROJECTS.find((p) => p.id === input.projectId);
910
- if (!project) {
911
- throw new Error("NOT_FOUND");
912
- }
913
- return { success: true };
914
- }
915
-
916
- // src/handlers/saas.handlers.ts
917
- import { web } from "@contractspec/lib.runtime-sandbox";
918
- var { generateId } = web;
919
- function rowToProject(row) {
920
- return {
921
- id: row.id,
922
- projectId: row.projectId,
923
- organizationId: row.organizationId,
924
- name: row.name,
925
- description: row.description ?? undefined,
926
- status: row.status,
927
- tier: row.tier,
928
- createdAt: new Date(row.createdAt),
929
- updatedAt: new Date(row.updatedAt)
930
- };
931
- }
932
- function rowToSubscription(row) {
933
- return {
934
- id: row.id,
935
- projectId: row.projectId,
936
- organizationId: row.organizationId,
937
- plan: row.plan,
938
- status: row.status,
939
- billingCycle: row.billingCycle,
940
- currentPeriodStart: new Date(row.currentPeriodStart),
941
- currentPeriodEnd: new Date(row.currentPeriodEnd),
942
- cancelAtPeriodEnd: Boolean(row.cancelAtPeriodEnd)
943
- };
944
- }
945
- function createSaasHandlers(db) {
946
- async function listProjects(input) {
947
- const {
948
- projectId,
949
- organizationId,
950
- status,
951
- search,
952
- limit = 20,
953
- offset = 0
954
- } = input;
955
- let whereClause = "WHERE projectId = ?";
956
- const params = [projectId];
957
- if (organizationId) {
958
- whereClause += " AND organizationId = ?";
959
- params.push(organizationId);
960
- }
961
- if (status && status !== "all") {
962
- whereClause += " AND status = ?";
963
- params.push(status);
964
- }
965
- if (search) {
966
- whereClause += " AND (name LIKE ? OR description LIKE ?)";
967
- params.push(`%${search}%`, `%${search}%`);
968
- }
969
- const countResult = (await db.query(`SELECT COUNT(*) as count FROM saas_project ${whereClause}`, params)).rows;
970
- const total = countResult[0]?.count ?? 0;
971
- const rows = (await db.query(`SELECT * FROM saas_project ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
972
- return {
973
- items: rows.map(rowToProject),
974
- total,
975
- hasMore: offset + rows.length < total
976
- };
977
- }
978
- async function getProject(id) {
979
- const rows = (await db.query(`SELECT * FROM saas_project WHERE id = ?`, [id])).rows;
980
- return rows[0] ? rowToProject(rows[0]) : null;
981
- }
982
- async function createProject(input, context) {
983
- const id = generateId("proj");
984
- const now = new Date().toISOString();
985
- await db.execute(`INSERT INTO saas_project (id, projectId, organizationId, name, description, status, tier, createdAt, updatedAt)
986
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
987
- id,
988
- context.projectId,
989
- context.organizationId,
990
- input.name,
991
- input.description ?? null,
992
- "DRAFT",
993
- input.tier ?? "FREE",
994
- now,
995
- now
996
- ]);
997
- const rows = (await db.query(`SELECT * FROM saas_project WHERE id = ?`, [id])).rows;
998
- return rowToProject(rows[0]);
999
- }
1000
- async function updateProject(input) {
1001
- const now = new Date().toISOString();
1002
- const updates = ["updatedAt = ?"];
1003
- const params = [now];
1004
- if (input.name !== undefined) {
1005
- updates.push("name = ?");
1006
- params.push(input.name);
1007
- }
1008
- if (input.description !== undefined) {
1009
- updates.push("description = ?");
1010
- params.push(input.description);
1011
- }
1012
- if (input.status !== undefined) {
1013
- updates.push("status = ?");
1014
- params.push(input.status);
1015
- }
1016
- params.push(input.id);
1017
- await db.execute(`UPDATE saas_project SET ${updates.join(", ")} WHERE id = ?`, params);
1018
- const rows = (await db.query(`SELECT * FROM saas_project WHERE id = ?`, [input.id])).rows;
1019
- if (!rows[0]) {
1020
- throw new Error("NOT_FOUND");
1021
- }
1022
- return rowToProject(rows[0]);
1023
- }
1024
- async function deleteProject(id) {
1025
- await db.execute(`DELETE FROM saas_project WHERE id = ?`, [id]);
1026
- }
1027
- async function getSubscription(input) {
1028
- let query = `SELECT * FROM saas_subscription WHERE projectId = ?`;
1029
- const params = [input.projectId];
1030
- if (input.organizationId) {
1031
- query += " AND organizationId = ?";
1032
- params.push(input.organizationId);
1033
- }
1034
- query += " LIMIT 1";
1035
- const rows = (await db.query(query, params)).rows;
1036
- return rows[0] ? rowToSubscription(rows[0]) : null;
1037
- }
1038
- return {
1039
- listProjects,
1040
- getProject,
1041
- createProject,
1042
- updateProject,
1043
- deleteProject,
1044
- getSubscription
1045
- };
1046
- }
1047
- // src/project/project.entity.ts
1048
- import {
1049
- defineEntity as defineEntity2,
1050
- defineEntityEnum as defineEntityEnum2,
1051
- field as field2,
1052
- index as index2
1053
- } from "@contractspec/lib.schema";
1054
- var ProjectStatusEnum = defineEntityEnum2({
1055
- name: "ProjectStatus",
1056
- values: ["DRAFT", "ACTIVE", "ARCHIVED", "DELETED"],
1057
- schema: "saas_app",
1058
- description: "Status of a project."
1059
- });
1060
- var ProjectEntity = defineEntity2({
1061
- name: "Project",
1062
- description: "A project belonging to an organization.",
1063
- schema: "saas_app",
1064
- map: "project",
1065
- fields: {
1066
- id: field2.id({ description: "Unique project ID" }),
1067
- name: field2.string({ description: "Project name" }),
1068
- description: field2.string({
1069
- isOptional: true,
1070
- description: "Project description"
1071
- }),
1072
- slug: field2.string({
1073
- isOptional: true,
1074
- description: "URL-friendly identifier"
1075
- }),
1076
- organizationId: field2.foreignKey({ description: "Owning organization" }),
1077
- createdBy: field2.foreignKey({
1078
- description: "User who created the project"
1079
- }),
1080
- status: field2.enum("ProjectStatus", { default: "DRAFT" }),
1081
- isPublic: field2.boolean({
1082
- default: false,
1083
- description: "Whether project is publicly visible"
1084
- }),
1085
- settings: field2.json({
1086
- isOptional: true,
1087
- description: "Project-specific settings"
1088
- }),
1089
- tags: field2.string({ isArray: true, description: "Project tags" }),
1090
- metadata: field2.json({ isOptional: true }),
1091
- createdAt: field2.createdAt(),
1092
- updatedAt: field2.updatedAt(),
1093
- archivedAt: field2.dateTime({ isOptional: true })
1094
- },
1095
- indexes: [
1096
- index2.on(["organizationId", "status"]),
1097
- index2.on(["organizationId", "createdAt"]),
1098
- index2.unique(["organizationId", "slug"])
1099
- ],
1100
- enums: [ProjectStatusEnum]
1101
- });
1102
- var ProjectMemberEntity = defineEntity2({
1103
- name: "ProjectMember",
1104
- description: "User access to a specific project.",
1105
- schema: "saas_app",
1106
- map: "project_member",
1107
- fields: {
1108
- id: field2.id(),
1109
- projectId: field2.foreignKey(),
1110
- userId: field2.foreignKey(),
1111
- role: field2.string({
1112
- description: "Role in project (owner, editor, viewer)"
1113
- }),
1114
- addedBy: field2.string({ isOptional: true }),
1115
- createdAt: field2.createdAt()
1116
- },
1117
- indexes: [index2.unique(["projectId", "userId"])]
1118
- });
1119
-
1120
- // src/project/project.enum.ts
1121
- import { defineEnum as defineEnum2 } from "@contractspec/lib.schema";
1122
- var ProjectStatusSchemaEnum = defineEnum2("ProjectStatus", [
1123
- "DRAFT",
1124
- "ACTIVE",
1125
- "ARCHIVED",
1126
- "DELETED"
1127
- ]);
1128
- var ProjectStatusFilterEnum = defineEnum2("ProjectStatusFilter", [
1129
- "DRAFT",
1130
- "ACTIVE",
1131
- "ARCHIVED",
1132
- "all"
1133
- ]);
1134
-
1135
- // src/project/project.event.ts
1136
- import { defineEvent as defineEvent2 } from "@contractspec/lib.contracts-spec";
1137
- import { defineSchemaModel as defineSchemaModel3, ScalarTypeEnum as ScalarTypeEnum3 } from "@contractspec/lib.schema";
1138
- var ProjectCreatedPayload = defineSchemaModel3({
1139
- name: "ProjectCreatedPayload",
1140
- description: "Payload when a project is created",
1141
- fields: {
1142
- projectId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
1143
- name: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
1144
- organizationId: {
1145
- type: ScalarTypeEnum3.String_unsecure(),
1146
- isOptional: false
1147
- },
1148
- createdBy: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
1149
- createdAt: { type: ScalarTypeEnum3.DateTime(), isOptional: false }
1150
- }
1151
- });
1152
- var ProjectUpdatedPayload = defineSchemaModel3({
1153
- name: "ProjectUpdatedPayload",
1154
- description: "Payload when a project is updated",
1155
- fields: {
1156
- projectId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
1157
- updatedFields: {
1158
- type: ScalarTypeEnum3.String_unsecure(),
1159
- isArray: true,
1160
- isOptional: false
1161
- },
1162
- updatedBy: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
1163
- updatedAt: { type: ScalarTypeEnum3.DateTime(), isOptional: false }
1164
- }
1165
- });
1166
- var ProjectDeletedPayload = defineSchemaModel3({
1167
- name: "ProjectDeletedPayload",
1168
- description: "Payload when a project is deleted",
1169
- fields: {
1170
- projectId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
1171
- organizationId: {
1172
- type: ScalarTypeEnum3.String_unsecure(),
1173
- isOptional: false
1174
- },
1175
- deletedBy: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
1176
- deletedAt: { type: ScalarTypeEnum3.DateTime(), isOptional: false }
1177
- }
1178
- });
1179
- var ProjectArchivedPayload = defineSchemaModel3({
1180
- name: "ProjectArchivedPayload",
1181
- description: "Payload when a project is archived",
1182
- fields: {
1183
- projectId: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
1184
- archivedBy: { type: ScalarTypeEnum3.String_unsecure(), isOptional: false },
1185
- archivedAt: { type: ScalarTypeEnum3.DateTime(), isOptional: false }
1186
- }
1187
- });
1188
- var ProjectCreatedEvent = defineEvent2({
1189
- meta: {
1190
- key: "project.created",
1191
- version: "1.0.0",
1192
- description: "A new project has been created.",
1193
- stability: "stable",
1194
- owners: ["@saas-team"],
1195
- tags: ["project", "created"]
1196
- },
1197
- payload: ProjectCreatedPayload
1198
- });
1199
- var ProjectUpdatedEvent = defineEvent2({
1200
- meta: {
1201
- key: "project.updated",
1202
- version: "1.0.0",
1203
- description: "A project has been updated.",
1204
- stability: "stable",
1205
- owners: ["@saas-team"],
1206
- tags: ["project", "updated"]
1207
- },
1208
- payload: ProjectUpdatedPayload
1209
- });
1210
- var ProjectDeletedEvent = defineEvent2({
1211
- meta: {
1212
- key: "project.deleted",
1213
- version: "1.0.0",
1214
- description: "A project has been deleted.",
1215
- stability: "stable",
1216
- owners: ["@saas-team"],
1217
- tags: ["project", "deleted"]
1218
- },
1219
- payload: ProjectDeletedPayload
1220
- });
1221
- var ProjectArchivedEvent = defineEvent2({
1222
- meta: {
1223
- key: "project.archived",
1224
- version: "1.0.0",
1225
- description: "A project has been archived.",
1226
- stability: "stable",
1227
- owners: ["@saas-team"],
1228
- tags: ["project", "archived"]
1229
- },
1230
- payload: ProjectArchivedPayload
1231
- });
1232
-
1233
- // src/project/project.schema.ts
1234
- import { defineSchemaModel as defineSchemaModel4, ScalarTypeEnum as ScalarTypeEnum4 } from "@contractspec/lib.schema";
1235
- var ProjectModel = defineSchemaModel4({
1236
- name: "Project",
1237
- description: "A project within an organization",
1238
- fields: {
1239
- id: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false },
1240
- name: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false },
1241
- description: { type: ScalarTypeEnum4.String_unsecure(), isOptional: true },
1242
- slug: { type: ScalarTypeEnum4.String_unsecure(), isOptional: true },
1243
- organizationId: {
1244
- type: ScalarTypeEnum4.String_unsecure(),
1245
- isOptional: false
1246
- },
1247
- createdBy: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false },
1248
- status: { type: ProjectStatusSchemaEnum, isOptional: false },
1249
- isPublic: { type: ScalarTypeEnum4.Boolean(), isOptional: false },
1250
- tags: {
1251
- type: ScalarTypeEnum4.String_unsecure(),
1252
- isArray: true,
1253
- isOptional: false
1254
- },
1255
- createdAt: { type: ScalarTypeEnum4.DateTime(), isOptional: false },
1256
- updatedAt: { type: ScalarTypeEnum4.DateTime(), isOptional: false }
1257
- }
1258
- });
1259
- var CreateProjectInputModel = defineSchemaModel4({
1260
- name: "CreateProjectInput",
1261
- description: "Input for creating a project",
1262
- fields: {
1263
- name: { type: ScalarTypeEnum4.NonEmptyString(), isOptional: false },
1264
- description: { type: ScalarTypeEnum4.String_unsecure(), isOptional: true },
1265
- slug: { type: ScalarTypeEnum4.String_unsecure(), isOptional: true },
1266
- isPublic: { type: ScalarTypeEnum4.Boolean(), isOptional: true },
1267
- tags: {
1268
- type: ScalarTypeEnum4.String_unsecure(),
1269
- isArray: true,
1270
- isOptional: true
1271
- }
1272
- }
1273
- });
1274
- var UpdateProjectInputModel = defineSchemaModel4({
1275
- name: "UpdateProjectInput",
1276
- description: "Input for updating a project",
1277
- fields: {
1278
- projectId: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false },
1279
- name: { type: ScalarTypeEnum4.String_unsecure(), isOptional: true },
1280
- description: { type: ScalarTypeEnum4.String_unsecure(), isOptional: true },
1281
- slug: { type: ScalarTypeEnum4.String_unsecure(), isOptional: true },
1282
- isPublic: { type: ScalarTypeEnum4.Boolean(), isOptional: true },
1283
- tags: {
1284
- type: ScalarTypeEnum4.String_unsecure(),
1285
- isArray: true,
1286
- isOptional: true
1287
- },
1288
- status: { type: ProjectStatusSchemaEnum, isOptional: true }
1289
- }
1290
- });
1291
- var GetProjectInputModel = defineSchemaModel4({
1292
- name: "GetProjectInput",
1293
- fields: {
1294
- projectId: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false }
1295
- }
1296
- });
1297
- var DeleteProjectInputModel = defineSchemaModel4({
1298
- name: "DeleteProjectInput",
1299
- fields: {
1300
- projectId: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false }
1301
- }
1302
- });
1303
- var DeleteProjectOutputModel = defineSchemaModel4({
1304
- name: "DeleteProjectOutput",
1305
- fields: {
1306
- success: { type: ScalarTypeEnum4.Boolean(), isOptional: false }
1307
- }
1308
- });
1309
- var ProjectDeletedPayloadModel = defineSchemaModel4({
1310
- name: "ProjectDeletedPayload",
1311
- fields: {
1312
- projectId: { type: ScalarTypeEnum4.String_unsecure(), isOptional: false }
1313
- }
1314
- });
1315
- var ListProjectsInputModel = defineSchemaModel4({
1316
- name: "ListProjectsInput",
1317
- description: "Input for listing projects",
1318
- fields: {
1319
- status: { type: ProjectStatusFilterEnum, isOptional: true },
1320
- search: { type: ScalarTypeEnum4.String_unsecure(), isOptional: true },
1321
- limit: {
1322
- type: ScalarTypeEnum4.Int_unsecure(),
1323
- isOptional: true,
1324
- defaultValue: 20
1325
- },
1326
- offset: {
1327
- type: ScalarTypeEnum4.Int_unsecure(),
1328
- isOptional: true,
1329
- defaultValue: 0
1330
- }
1331
- }
1332
- });
1333
- var ListProjectsOutputModel = defineSchemaModel4({
1334
- name: "ListProjectsOutput",
1335
- description: "Output for listing projects",
1336
- fields: {
1337
- projects: { type: ProjectModel, isArray: true, isOptional: false },
1338
- total: { type: ScalarTypeEnum4.Int_unsecure(), isOptional: false }
1339
- }
1340
- });
1341
-
1342
- // src/project/project.operations.ts
1343
- import {
1344
- defineCommand as defineCommand2,
1345
- defineQuery as defineQuery2
1346
- } from "@contractspec/lib.contracts-spec/operations";
1347
- var OWNERS2 = ["example.saas-boilerplate"];
1348
- var CreateProjectContract = defineCommand2({
1349
- meta: {
1350
- key: "saas.project.create",
1351
- version: "1.0.0",
1352
- stability: "stable",
1353
- owners: [...OWNERS2],
1354
- tags: ["saas", "project", "create"],
1355
- description: "Create a new project in the organization.",
1356
- goal: "Allow users to create projects for organizing work.",
1357
- context: "Called from project creation UI or API."
1358
- },
1359
- io: {
1360
- input: CreateProjectInputModel,
1361
- output: ProjectModel,
1362
- errors: {
1363
- SLUG_EXISTS: {
1364
- description: "A project with this slug already exists",
1365
- http: 409,
1366
- gqlCode: "SLUG_EXISTS",
1367
- when: "Slug is already taken in the organization"
1368
- },
1369
- LIMIT_REACHED: {
1370
- description: "Project limit reached for this plan",
1371
- http: 403,
1372
- gqlCode: "LIMIT_REACHED",
1373
- when: "Organization has reached project limit"
1374
- }
1375
- }
1376
- },
1377
- policy: {
1378
- auth: "user"
1379
- },
1380
- sideEffects: {
1381
- emits: [
1382
- {
1383
- key: "project.created",
1384
- version: "1.0.0",
1385
- when: "Project is created",
1386
- payload: ProjectModel
1387
- }
1388
- ],
1389
- audit: ["project.created"]
1390
- },
1391
- acceptance: {
1392
- scenarios: [
1393
- {
1394
- key: "create-project-happy-path",
1395
- given: ["User is authenticated"],
1396
- when: ["User creates project"],
1397
- then: ["Project is created", "ProjectCreated event is emitted"]
1398
- }
1399
- ],
1400
- examples: [
1401
- {
1402
- key: "create-basic",
1403
- input: { name: "Website Redesign", slug: "website-redesign" },
1404
- output: { id: "proj-123", name: "Website Redesign", isArchived: false }
1405
- }
1406
- ]
1407
- }
1408
- });
1409
- var GetProjectContract = defineQuery2({
1410
- meta: {
1411
- key: "saas.project.get",
1412
- version: "1.0.0",
1413
- stability: "stable",
1414
- owners: [...OWNERS2],
1415
- tags: ["saas", "project", "get"],
1416
- description: "Get a project by ID.",
1417
- goal: "Retrieve project details.",
1418
- context: "Project detail page, API calls."
1419
- },
1420
- io: {
1421
- input: GetProjectInputModel,
1422
- output: ProjectModel,
1423
- errors: {
1424
- NOT_FOUND: {
1425
- description: "Project not found",
1426
- http: 404,
1427
- gqlCode: "NOT_FOUND",
1428
- when: "Project ID is invalid or user lacks access"
1429
- }
1430
- }
1431
- },
1432
- policy: {
1433
- auth: "user"
1434
- },
1435
- acceptance: {
1436
- scenarios: [
1437
- {
1438
- key: "get-project-happy-path",
1439
- given: ["Project exists"],
1440
- when: ["User requests project"],
1441
- then: ["Project details are returned"]
1442
- }
1443
- ],
1444
- examples: [
1445
- {
1446
- key: "get-existing",
1447
- input: { projectId: "proj-123" },
1448
- output: { id: "proj-123", name: "Website Redesign" }
1449
- }
1450
- ]
1451
- }
1452
- });
1453
- var UpdateProjectContract = defineCommand2({
1454
- meta: {
1455
- key: "saas.project.update",
1456
- version: "1.0.0",
1457
- stability: "stable",
1458
- owners: [...OWNERS2],
1459
- tags: ["saas", "project", "update"],
1460
- description: "Update project details.",
1461
- goal: "Allow project owners/editors to modify project.",
1462
- context: "Project settings page."
1463
- },
1464
- io: {
1465
- input: UpdateProjectInputModel,
1466
- output: ProjectModel
1467
- },
1468
- policy: {
1469
- auth: "user"
1470
- },
1471
- sideEffects: {
1472
- emits: [
1473
- {
1474
- key: "project.updated",
1475
- version: "1.0.0",
1476
- when: "Project is updated",
1477
- payload: ProjectModel
1478
- }
1479
- ],
1480
- audit: ["project.updated"]
1481
- },
1482
- acceptance: {
1483
- scenarios: [
1484
- {
1485
- key: "update-project-happy-path",
1486
- given: ["Project exists"],
1487
- when: ["User updates description"],
1488
- then: ["Project is updated", "ProjectUpdated event is emitted"]
1489
- }
1490
- ],
1491
- examples: [
1492
- {
1493
- key: "update-desc",
1494
- input: { projectId: "proj-123", description: "New description" },
1495
- output: { id: "proj-123", description: "New description" }
1496
- }
1497
- ]
1498
- }
1499
- });
1500
- var DeleteProjectContract = defineCommand2({
1501
- meta: {
1502
- key: "saas.project.delete",
1503
- version: "1.0.0",
1504
- stability: "stable",
1505
- owners: [...OWNERS2],
1506
- tags: ["saas", "project", "delete"],
1507
- description: "Delete a project (soft delete).",
1508
- goal: "Allow project owners to remove projects.",
1509
- context: "Project settings page."
1510
- },
1511
- io: {
1512
- input: DeleteProjectInputModel,
1513
- output: DeleteProjectOutputModel
1514
- },
1515
- policy: {
1516
- auth: "user"
1517
- },
1518
- sideEffects: {
1519
- emits: [
1520
- {
1521
- key: "project.deleted",
1522
- version: "1.0.0",
1523
- when: "Project is deleted",
1524
- payload: ProjectDeletedPayloadModel
1525
- }
1526
- ],
1527
- audit: ["project.deleted"]
1528
- },
1529
- acceptance: {
1530
- scenarios: [
1531
- {
1532
- key: "delete-project-happy-path",
1533
- given: ["Project exists"],
1534
- when: ["User deletes project"],
1535
- then: ["Project is deleted", "ProjectDeleted event is emitted"]
1536
- }
1537
- ],
1538
- examples: [
1539
- {
1540
- key: "delete-existing",
1541
- input: { projectId: "proj-123" },
1542
- output: { success: true }
1543
- }
1544
- ]
1545
- }
1546
- });
1547
- var ListProjectsContract = defineQuery2({
1548
- meta: {
1549
- key: "saas.project.list",
1550
- version: "1.0.0",
1551
- stability: "stable",
1552
- owners: [...OWNERS2],
1553
- tags: ["saas", "project", "list"],
1554
- description: "List projects in the organization.",
1555
- goal: "Show all projects user has access to.",
1556
- context: "Project list page, dashboard."
1557
- },
1558
- io: {
1559
- input: ListProjectsInputModel,
1560
- output: ListProjectsOutputModel
1561
- },
1562
- policy: {
1563
- auth: "user"
1564
- },
1565
- acceptance: {
1566
- scenarios: [
1567
- {
1568
- key: "list-projects-happy-path",
1569
- given: ["Projects exist"],
1570
- when: ["User lists projects"],
1571
- then: ["List of projects is returned"]
1572
- }
1573
- ],
1574
- examples: [
1575
- {
1576
- key: "list-all",
1577
- input: { limit: 10 },
1578
- output: { items: [], total: 5 }
1579
- }
1580
- ]
1581
- }
1582
- });
1583
-
1584
- // src/project/project.presentation.ts
1585
- import {
1586
- definePresentation as definePresentation3,
1587
- StabilityEnum as StabilityEnum3
1588
- } from "@contractspec/lib.contracts-spec";
1589
- var ProjectListPresentation = definePresentation3({
1590
- meta: {
1591
- key: "saas.project.list",
1592
- version: "1.0.0",
1593
- title: "Project List",
1594
- description: "List view of projects with status, tags, and last updated info",
1595
- domain: "saas-boilerplate",
1596
- owners: ["@saas-team"],
1597
- tags: ["project", "list", "dashboard"],
1598
- stability: StabilityEnum3.Beta,
1599
- goal: "Browse and manage projects",
1600
- context: "Project list page"
1601
- },
1602
- source: {
1603
- type: "component",
1604
- framework: "react",
1605
- componentKey: "ProjectListView",
1606
- props: ProjectModel
1607
- },
1608
- targets: ["react", "markdown", "application/json"],
1609
- policy: {
1610
- flags: ["saas.projects.enabled"]
1611
- }
1612
- });
1613
- var ProjectDetailPresentation = definePresentation3({
1614
- meta: {
1615
- key: "saas.project.detail",
1616
- version: "1.0.0",
1617
- title: "Project Details",
1618
- description: "Detailed view of a project with settings and activity",
1619
- domain: "saas-boilerplate",
1620
- owners: ["@saas-team"],
1621
- tags: ["project", "detail"],
1622
- stability: StabilityEnum3.Beta,
1623
- goal: "View and edit project details",
1624
- context: "Project detail page"
1625
- },
1626
- source: {
1627
- type: "component",
1628
- framework: "react",
1629
- componentKey: "ProjectDetailView"
1630
- },
1631
- targets: ["react", "markdown"],
1632
- policy: {
1633
- flags: ["saas.projects.enabled"]
1634
- }
1635
- });
1636
- // src/visualizations/catalog.ts
1637
- import {
1638
- defineVisualization,
1639
- VisualizationRegistry
1640
- } from "@contractspec/lib.contracts-spec/visualizations";
1641
- var PROJECT_LIST_REF = {
1642
- key: "saas.project.list",
1643
- version: "1.0.0"
1644
- };
1645
- var META = {
1646
- version: "1.0.0",
1647
- domain: "saas",
1648
- stability: "experimental",
1649
- owners: ["@example.saas-boilerplate"],
1650
- tags: ["saas", "visualization", "projects"]
1651
- };
1652
- var SaasProjectUsageVisualization = defineVisualization({
1653
- meta: {
1654
- ...META,
1655
- key: "saas-boilerplate.visualization.project-usage",
1656
- title: "Project Capacity",
1657
- description: "Current project count against the current plan limit.",
1658
- goal: "Show usage against the active plan allowance.",
1659
- context: "SaaS account overview."
1660
- },
1661
- source: { primary: PROJECT_LIST_REF, resultPath: "data" },
1662
- visualization: {
1663
- kind: "metric",
1664
- measure: "totalProjects",
1665
- comparisonMeasure: "projectLimit",
1666
- measures: [
1667
- {
1668
- key: "totalProjects",
1669
- label: "Projects",
1670
- dataPath: "totalProjects",
1671
- format: "number"
1672
- },
1673
- {
1674
- key: "projectLimit",
1675
- label: "Plan Limit",
1676
- dataPath: "projectLimit",
1677
- format: "number"
1678
- }
1679
- ],
1680
- table: { caption: "Current project count and plan limit." }
1681
- }
1682
- });
1683
- var SaasProjectStatusVisualization = defineVisualization({
1684
- meta: {
1685
- ...META,
1686
- key: "saas-boilerplate.visualization.project-status",
1687
- title: "Project Status",
1688
- description: "Distribution of project states.",
1689
- goal: "Show the mix of active, draft, and archived projects.",
1690
- context: "Project portfolio overview."
1691
- },
1692
- source: { primary: PROJECT_LIST_REF, resultPath: "data" },
1693
- visualization: {
1694
- kind: "pie",
1695
- nameDimension: "status",
1696
- valueMeasure: "projects",
1697
- dimensions: [
1698
- { key: "status", label: "Status", dataPath: "status", type: "category" }
1699
- ],
1700
- measures: [
1701
- {
1702
- key: "projects",
1703
- label: "Projects",
1704
- dataPath: "projects",
1705
- format: "number"
1706
- }
1707
- ],
1708
- table: { caption: "Project counts by status." }
1709
- }
1710
- });
1711
- var SaasProjectTierVisualization = defineVisualization({
1712
- meta: {
1713
- ...META,
1714
- key: "saas-boilerplate.visualization.project-tiers",
1715
- title: "Tier Comparison",
1716
- description: "Distribution of projects across tiers.",
1717
- goal: "Compare how the current portfolio is distributed by tier.",
1718
- context: "Plan and packaging overview."
1719
- },
1720
- source: { primary: PROJECT_LIST_REF, resultPath: "data" },
1721
- visualization: {
1722
- kind: "cartesian",
1723
- variant: "bar",
1724
- xDimension: "tier",
1725
- yMeasures: ["projects"],
1726
- dimensions: [
1727
- { key: "tier", label: "Tier", dataPath: "tier", type: "category" }
1728
- ],
1729
- measures: [
1730
- {
1731
- key: "projects",
1732
- label: "Projects",
1733
- dataPath: "projects",
1734
- format: "number",
1735
- color: "#1d4ed8"
1736
- }
1737
- ],
1738
- table: { caption: "Project counts by tier." }
1739
- }
1740
- });
1741
- var SaasProjectActivityVisualization = defineVisualization({
1742
- meta: {
1743
- ...META,
1744
- key: "saas-boilerplate.visualization.project-activity",
1745
- title: "Recent Project Activity",
1746
- description: "Daily project creation activity.",
1747
- goal: "Show recent project activity over time.",
1748
- context: "Project portfolio trend view."
1749
- },
1750
- source: { primary: PROJECT_LIST_REF, resultPath: "data" },
1751
- visualization: {
1752
- kind: "cartesian",
1753
- variant: "line",
1754
- xDimension: "day",
1755
- yMeasures: ["projects"],
1756
- dimensions: [{ key: "day", label: "Day", dataPath: "day", type: "time" }],
1757
- measures: [
1758
- {
1759
- key: "projects",
1760
- label: "Projects",
1761
- dataPath: "projects",
1762
- format: "number",
1763
- color: "#0f766e"
1764
- }
1765
- ],
1766
- table: { caption: "Daily project creation counts." }
1767
- }
1768
- });
1769
- var SaasVisualizationSpecs = [
1770
- SaasProjectUsageVisualization,
1771
- SaasProjectStatusVisualization,
1772
- SaasProjectTierVisualization,
1773
- SaasProjectActivityVisualization
1774
- ];
1775
- var SaasVisualizationRegistry = new VisualizationRegistry([
1776
- ...SaasVisualizationSpecs
1777
- ]);
1778
- var SaasVisualizationRefs = SaasVisualizationSpecs.map((spec) => ({
1779
- key: spec.meta.key,
1780
- version: spec.meta.version
1781
- }));
1782
-
1783
- // src/visualizations/selectors.ts
1784
- function toDayKey(value) {
1785
- const date = value instanceof Date ? value : new Date(value);
1786
- return date.toISOString().slice(0, 10);
1787
- }
1788
- function createSaasVisualizationItems(projects, projectLimit = 10) {
1789
- const statusCounts = new Map;
1790
- const tierCounts = new Map;
1791
- const activityCounts = new Map;
1792
- for (const project of projects) {
1793
- statusCounts.set(project.status, (statusCounts.get(project.status) ?? 0) + 1);
1794
- tierCounts.set(project.tier, (tierCounts.get(project.tier) ?? 0) + 1);
1795
- const day = toDayKey(project.createdAt);
1796
- activityCounts.set(day, (activityCounts.get(day) ?? 0) + 1);
1797
- }
1798
- return [
1799
- {
1800
- key: "saas-capacity",
1801
- spec: SaasProjectUsageVisualization,
1802
- data: { data: [{ totalProjects: projects.length, projectLimit }] },
1803
- title: "Project Capacity",
1804
- description: "Current project count compared to the active limit.",
1805
- height: 220
1806
- },
1807
- {
1808
- key: "saas-status",
1809
- spec: SaasProjectStatusVisualization,
1810
- data: {
1811
- data: Array.from(statusCounts.entries()).map(([status, count]) => ({
1812
- status,
1813
- projects: count
1814
- }))
1815
- },
1816
- title: "Project Status",
1817
- description: "Status mix across the current project portfolio.",
1818
- height: 260
1819
- },
1820
- {
1821
- key: "saas-tier",
1822
- spec: SaasProjectTierVisualization,
1823
- data: {
1824
- data: Array.from(tierCounts.entries()).map(([tier, count]) => ({
1825
- tier,
1826
- projects: count
1827
- }))
1828
- },
1829
- title: "Tier Comparison",
1830
- description: "How projects are distributed across tiers."
1831
- },
1832
- {
1833
- key: "saas-activity",
1834
- spec: SaasProjectActivityVisualization,
1835
- data: {
1836
- data: Array.from(activityCounts.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([day, count]) => ({ day, projects: count }))
1837
- },
1838
- title: "Recent Project Activity",
1839
- description: "Daily project creation activity."
1840
- }
1841
- ];
1842
- }
1843
- // src/saas-boilerplate.feature.ts
1844
- import { defineFeature } from "@contractspec/lib.contracts-spec";
1845
- var SaasBoilerplateFeature = defineFeature({
1846
- meta: {
1847
- key: "saas-boilerplate",
1848
- title: "SaaS Boilerplate",
1849
- description: "SaaS application foundation with projects, billing, and settings",
1850
- domain: "saas",
1851
- owners: ["@saas-team"],
1852
- tags: ["saas", "projects", "billing"],
1853
- stability: "experimental",
1854
- version: "1.0.0"
1855
- },
1856
- operations: [
1857
- { key: "saas.project.create", version: "1.0.0" },
1858
- { key: "saas.project.get", version: "1.0.0" },
1859
- { key: "saas.project.update", version: "1.0.0" },
1860
- { key: "saas.project.delete", version: "1.0.0" },
1861
- { key: "saas.project.list", version: "1.0.0" },
1862
- { key: "saas.billing.subscription.get", version: "1.0.0" },
1863
- { key: "saas.billing.usage.record", version: "1.0.0" },
1864
- { key: "saas.billing.usage.summary", version: "1.0.0" },
1865
- { key: "saas.billing.feature.check", version: "1.0.0" }
1866
- ],
1867
- events: [
1868
- { key: "project.created", version: "1.0.0" },
1869
- { key: "project.updated", version: "1.0.0" },
1870
- { key: "project.deleted", version: "1.0.0" },
1871
- { key: "project.archived", version: "1.0.0" },
1872
- { key: "billing.usage.recorded", version: "1.0.0" },
1873
- { key: "billing.subscription.changed", version: "1.0.0" },
1874
- { key: "billing.limit.reached", version: "1.0.0" }
1875
- ],
1876
- presentations: [
1877
- { key: "saas.dashboard", version: "1.0.0" },
1878
- { key: "saas.project.list", version: "1.0.0" },
1879
- { key: "saas.project.detail", version: "1.0.0" },
1880
- { key: "saas.billing.subscription", version: "1.0.0" },
1881
- { key: "saas.billing.usage", version: "1.0.0" },
1882
- { key: "saas.settings", version: "1.0.0" }
1883
- ],
1884
- opToPresentation: [
1885
- {
1886
- op: { key: "saas.project.list", version: "1.0.0" },
1887
- pres: { key: "saas.project.list", version: "1.0.0" }
1888
- },
1889
- {
1890
- op: { key: "saas.project.get", version: "1.0.0" },
1891
- pres: { key: "saas.project.detail", version: "1.0.0" }
1892
- },
1893
- {
1894
- op: { key: "saas.billing.subscription.get", version: "1.0.0" },
1895
- pres: { key: "saas.billing.subscription", version: "1.0.0" }
1896
- },
1897
- {
1898
- op: { key: "saas.billing.usage.summary", version: "1.0.0" },
1899
- pres: { key: "saas.billing.usage", version: "1.0.0" }
1900
- }
1901
- ],
1902
- presentationsTargets: [
1903
- { key: "saas.dashboard", version: "1.0.0", targets: ["react", "markdown"] },
1904
- {
1905
- key: "saas.project.list",
1906
- version: "1.0.0",
1907
- targets: ["react", "markdown", "application/json"]
1908
- },
1909
- {
1910
- key: "saas.billing.subscription",
1911
- version: "1.0.0",
1912
- targets: ["react", "markdown"]
1913
- },
1914
- {
1915
- key: "saas.billing.usage",
1916
- version: "1.0.0",
1917
- targets: ["react", "markdown"]
1918
- }
1919
- ],
1920
- visualizations: SaasVisualizationRefs,
1921
- capabilities: {
1922
- requires: [
1923
- { key: "identity", version: "1.0.0" },
1924
- { key: "audit-trail", version: "1.0.0" },
1925
- { key: "notifications", version: "1.0.0" }
1926
- ]
1927
- },
1928
- telemetry: [{ key: "saas-boilerplate.telemetry", version: "1.0.0" }],
1929
- jobs: [{ key: "saas-boilerplate.job.usage-recording", version: "1.0.0" }],
1930
- docs: [
1931
- "docs.examples.saas-boilerplate.goal",
1932
- "docs.examples.saas-boilerplate.usage",
1933
- "docs.examples.saas-boilerplate.reference",
1934
- "docs.examples.saas-boilerplate.constraints"
1935
- ]
1936
- });
1937
-
1938
- // src/settings/settings.enum.ts
1939
- import { defineEntityEnum as defineEntityEnum3 } from "@contractspec/lib.schema";
1940
- var SettingsScopeEnum = defineEntityEnum3({
1941
- name: "SettingsScope",
1942
- values: ["APP", "ORG", "USER", "PROJECT"],
1943
- schema: "saas_app",
1944
- description: "Scope of a setting."
1945
- });
1946
-
1947
- // src/settings/settings.entity.ts
1948
- import { defineEntity as defineEntity3, field as field3, index as index3 } from "@contractspec/lib.schema";
1949
- var SettingsEntity = defineEntity3({
1950
- name: "Settings",
1951
- description: "Application, organization, or user settings.",
1952
- schema: "saas_app",
1953
- map: "settings",
1954
- fields: {
1955
- id: field3.id(),
1956
- key: field3.string({
1957
- description: 'Setting key (e.g., "theme", "notifications.email")'
1958
- }),
1959
- scope: field3.enum("SettingsScope"),
1960
- scopeId: field3.string({
1961
- isOptional: true,
1962
- description: "ID of scoped entity (org, user, project)"
1963
- }),
1964
- value: field3.json({ description: "Setting value" }),
1965
- valueType: field3.string({
1966
- default: '"string"',
1967
- description: "Type hint for value"
1968
- }),
1969
- schema: field3.json({
1970
- isOptional: true,
1971
- description: "JSON schema for validation"
1972
- }),
1973
- description: field3.string({ isOptional: true }),
1974
- isSecret: field3.boolean({
1975
- default: false,
1976
- description: "Whether value should be encrypted"
1977
- }),
1978
- createdAt: field3.createdAt(),
1979
- updatedAt: field3.updatedAt()
1980
- },
1981
- indexes: [
1982
- index3.unique(["scope", "scopeId", "key"]),
1983
- index3.on(["scope", "key"])
1984
- ],
1985
- enums: [SettingsScopeEnum]
1986
- });
1987
- var FeatureFlagEntity = defineEntity3({
1988
- name: "FeatureFlag",
1989
- description: "Feature flags for progressive rollout.",
1990
- schema: "saas_app",
1991
- map: "feature_flag",
1992
- fields: {
1993
- id: field3.id(),
1994
- key: field3.string({ isUnique: true, description: "Feature flag key" }),
1995
- name: field3.string({ description: "Human-readable name" }),
1996
- description: field3.string({ isOptional: true }),
1997
- enabled: field3.boolean({ default: false }),
1998
- defaultValue: field3.boolean({ default: false }),
1999
- rules: field3.json({ isOptional: true, description: "Targeting rules" }),
2000
- rolloutPercentage: field3.int({
2001
- default: 0,
2002
- description: "Percentage rollout (0-100)"
2003
- }),
2004
- createdAt: field3.createdAt(),
2005
- updatedAt: field3.updatedAt()
2006
- }
2007
- });
2008
- // src/ui/hooks/useProjectList.ts
2009
- import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
2010
- import { useCallback, useEffect, useMemo, useState } from "react";
2011
- function useProjectList(options = {}) {
2012
- const { handlers, projectId } = useTemplateRuntime();
2013
- const { saas: saas2 } = handlers;
2014
- const [data, setData] = useState(null);
2015
- const [subscription, setSubscription] = useState(null);
2016
- const [loading, setLoading] = useState(true);
2017
- const [error, setError] = useState(null);
2018
- const [page, setPage] = useState(1);
2019
- const fetchData = useCallback(async () => {
2020
- setLoading(true);
2021
- setError(null);
2022
- try {
2023
- const [projectsResult, subscriptionResult] = await Promise.all([
2024
- saas2.listProjects({
2025
- projectId,
2026
- status: options.status === "all" ? undefined : options.status,
2027
- search: options.search,
2028
- limit: options.limit ?? 20,
2029
- offset: (page - 1) * (options.limit ?? 20)
2030
- }),
2031
- saas2.getSubscription({ projectId })
2032
- ]);
2033
- setData({
2034
- items: projectsResult.items,
2035
- total: projectsResult.total
2036
- });
2037
- setSubscription(subscriptionResult);
2038
- } catch (err) {
2039
- setError(err instanceof Error ? err : new Error("Unknown error"));
2040
- } finally {
2041
- setLoading(false);
2042
- }
2043
- }, [saas2, projectId, options.status, options.search, options.limit, page]);
2044
- useEffect(() => {
2045
- fetchData();
2046
- }, [fetchData]);
2047
- const stats = useMemo(() => {
2048
- if (!data)
2049
- return null;
2050
- const items = data.items;
2051
- return {
2052
- total: data.total,
2053
- activeCount: items.filter((p) => p.status === "ACTIVE").length,
2054
- draftCount: items.filter((p) => p.status === "DRAFT").length,
2055
- projectLimit: 10,
2056
- usagePercent: Math.min(data.total / 10 * 100, 100)
2057
- };
2058
- }, [data]);
2059
- return {
2060
- data,
2061
- subscription,
2062
- loading,
2063
- error,
2064
- stats,
2065
- page,
2066
- refetch: fetchData,
2067
- nextPage: () => setPage((p) => p + 1),
2068
- prevPage: () => page > 1 && setPage((p) => p - 1)
2069
- };
2070
- }
2071
-
2072
- // src/ui/hooks/useProjectMutations.ts
2073
- import { useTemplateRuntime as useTemplateRuntime2 } from "@contractspec/lib.example-shared-ui";
2074
- import { useCallback as useCallback2, useState as useState2 } from "react";
2075
- function useProjectMutations(options = {}) {
2076
- const { handlers, projectId } = useTemplateRuntime2();
2077
- const { saas: saas2 } = handlers;
2078
- const [createState, setCreateState] = useState2({
2079
- loading: false,
2080
- error: null,
2081
- data: null
2082
- });
2083
- const [updateState, setUpdateState] = useState2({
2084
- loading: false,
2085
- error: null,
2086
- data: null
2087
- });
2088
- const [deleteState, setDeleteState] = useState2({
2089
- loading: false,
2090
- error: null,
2091
- data: null
2092
- });
2093
- const createProject = useCallback2(async (input) => {
2094
- setCreateState({ loading: true, error: null, data: null });
2095
- try {
2096
- const result = await saas2.createProject(input, {
2097
- projectId,
2098
- organizationId: "demo-org"
2099
- });
2100
- setCreateState({ loading: false, error: null, data: result });
2101
- options.onSuccess?.();
2102
- return result;
2103
- } catch (err) {
2104
- const error = err instanceof Error ? err : new Error("Failed to create project");
2105
- setCreateState({ loading: false, error, data: null });
2106
- options.onError?.(error);
2107
- return null;
2108
- }
2109
- }, [saas2, projectId, options]);
2110
- const updateProject = useCallback2(async (input) => {
2111
- setUpdateState({ loading: true, error: null, data: null });
2112
- try {
2113
- const result = await saas2.updateProject(input);
2114
- setUpdateState({ loading: false, error: null, data: result });
2115
- options.onSuccess?.();
2116
- return result;
2117
- } catch (err) {
2118
- const error = err instanceof Error ? err : new Error("Failed to update project");
2119
- setUpdateState({ loading: false, error, data: null });
2120
- options.onError?.(error);
2121
- return null;
2122
- }
2123
- }, [saas2, options]);
2124
- const deleteProject = useCallback2(async (id) => {
2125
- setDeleteState({ loading: true, error: null, data: null });
2126
- try {
2127
- await saas2.deleteProject(id);
2128
- setDeleteState({
2129
- loading: false,
2130
- error: null,
2131
- data: { success: true }
2132
- });
2133
- options.onSuccess?.();
2134
- return true;
2135
- } catch (err) {
2136
- const error = err instanceof Error ? err : new Error("Failed to delete project");
2137
- setDeleteState({ loading: false, error, data: null });
2138
- options.onError?.(error);
2139
- return false;
2140
- }
2141
- }, [saas2, options]);
2142
- const archiveProject = useCallback2(async (id) => {
2143
- return updateProject({ id, status: "ARCHIVED" });
2144
- }, [updateProject]);
2145
- const activateProject = useCallback2(async (id) => {
2146
- return updateProject({ id, status: "ACTIVE" });
2147
- }, [updateProject]);
2148
- return {
2149
- createProject,
2150
- updateProject,
2151
- deleteProject,
2152
- archiveProject,
2153
- activateProject,
2154
- createState,
2155
- updateState,
2156
- deleteState,
2157
- isLoading: createState.loading || updateState.loading || deleteState.loading
2158
- };
2159
- }
2160
-
2161
- // src/ui/hooks/index.ts
2162
- "use client";
2163
-
2164
- // src/ui/modals/CreateProjectModal.tsx
2165
- import { Button, Input } from "@contractspec/lib.design-system";
2166
- import { useState as useState3 } from "react";
2167
- import { jsxDEV } from "react/jsx-dev-runtime";
2168
- "use client";
2169
- var TIERS = [
2170
- { value: "FREE", label: "Free" },
2171
- { value: "PRO", label: "Pro" },
2172
- { value: "ENTERPRISE", label: "Enterprise" }
2173
- ];
2174
- function CreateProjectModal({
2175
- isOpen,
2176
- onClose,
2177
- onSubmit,
2178
- isLoading = false
2179
- }) {
2180
- const [name, setName] = useState3("");
2181
- const [description, setDescription] = useState3("");
2182
- const [tier, setTier] = useState3("FREE");
2183
- const [error, setError] = useState3(null);
2184
- const handleSubmit = async (e) => {
2185
- e.preventDefault();
2186
- setError(null);
2187
- if (!name.trim()) {
2188
- setError("Project name is required");
2189
- return;
2190
- }
2191
- try {
2192
- await onSubmit({
2193
- name: name.trim(),
2194
- description: description.trim() || undefined,
2195
- tier
2196
- });
2197
- setName("");
2198
- setDescription("");
2199
- setTier("FREE");
2200
- onClose();
2201
- } catch (err) {
2202
- setError(err instanceof Error ? err.message : "Failed to create project");
2203
- }
2204
- };
2205
- if (!isOpen)
2206
- return null;
2207
- return /* @__PURE__ */ jsxDEV("div", {
2208
- className: "fixed inset-0 z-50 flex items-center justify-center",
2209
- children: [
2210
- /* @__PURE__ */ jsxDEV("div", {
2211
- className: "absolute inset-0 bg-background/80 backdrop-blur-sm",
2212
- onClick: onClose,
2213
- role: "button",
2214
- tabIndex: 0,
2215
- onKeyDown: (e) => {
2216
- if (e.key === "Enter" || e.key === " ")
2217
- onClose();
2218
- },
2219
- "aria-label": "Close modal"
2220
- }, undefined, false, undefined, this),
2221
- /* @__PURE__ */ jsxDEV("div", {
2222
- className: "relative z-10 w-full max-w-md rounded-xl border border-border bg-card p-6 shadow-xl",
2223
- children: [
2224
- /* @__PURE__ */ jsxDEV("h2", {
2225
- className: "mb-4 font-semibold text-xl",
2226
- children: "Create New Project"
2227
- }, undefined, false, undefined, this),
2228
- /* @__PURE__ */ jsxDEV("form", {
2229
- onSubmit: handleSubmit,
2230
- className: "space-y-4",
2231
- children: [
2232
- /* @__PURE__ */ jsxDEV("div", {
2233
- children: [
2234
- /* @__PURE__ */ jsxDEV("label", {
2235
- htmlFor: "project-name",
2236
- className: "mb-1 block font-medium text-muted-foreground text-sm",
2237
- children: "Project Name *"
2238
- }, undefined, false, undefined, this),
2239
- /* @__PURE__ */ jsxDEV(Input, {
2240
- id: "project-name",
2241
- value: name,
2242
- onChange: (e) => setName(e.target.value),
2243
- placeholder: "e.g., My Awesome Project",
2244
- disabled: isLoading
2245
- }, undefined, false, undefined, this)
2246
- ]
2247
- }, undefined, true, undefined, this),
2248
- /* @__PURE__ */ jsxDEV("div", {
2249
- children: [
2250
- /* @__PURE__ */ jsxDEV("label", {
2251
- htmlFor: "project-description",
2252
- className: "mb-1 block font-medium text-muted-foreground text-sm",
2253
- children: "Description"
2254
- }, undefined, false, undefined, this),
2255
- /* @__PURE__ */ jsxDEV("textarea", {
2256
- id: "project-description",
2257
- value: description,
2258
- onChange: (e) => setDescription(e.target.value),
2259
- placeholder: "Describe what this project is about...",
2260
- rows: 3,
2261
- disabled: isLoading,
2262
- className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
2263
- }, undefined, false, undefined, this)
2264
- ]
2265
- }, undefined, true, undefined, this),
2266
- /* @__PURE__ */ jsxDEV("div", {
2267
- children: [
2268
- /* @__PURE__ */ jsxDEV("label", {
2269
- htmlFor: "project-tier",
2270
- className: "mb-1 block font-medium text-muted-foreground text-sm",
2271
- children: "Tier"
2272
- }, undefined, false, undefined, this),
2273
- /* @__PURE__ */ jsxDEV("select", {
2274
- id: "project-tier",
2275
- value: tier,
2276
- onChange: (e) => setTier(e.target.value),
2277
- disabled: isLoading,
2278
- className: "h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50",
2279
- children: TIERS.map((t) => /* @__PURE__ */ jsxDEV("option", {
2280
- value: t.value,
2281
- children: t.label
2282
- }, t.value, false, undefined, this))
2283
- }, undefined, false, undefined, this)
2284
- ]
2285
- }, undefined, true, undefined, this),
2286
- error && /* @__PURE__ */ jsxDEV("div", {
2287
- className: "rounded-md bg-destructive/10 p-3 text-destructive text-sm",
2288
- children: error
2289
- }, undefined, false, undefined, this),
2290
- /* @__PURE__ */ jsxDEV("div", {
2291
- className: "flex justify-end gap-3 pt-2",
2292
- children: [
2293
- /* @__PURE__ */ jsxDEV(Button, {
2294
- type: "button",
2295
- variant: "ghost",
2296
- onPress: onClose,
2297
- disabled: isLoading,
2298
- children: "Cancel"
2299
- }, undefined, false, undefined, this),
2300
- /* @__PURE__ */ jsxDEV(Button, {
2301
- type: "submit",
2302
- disabled: isLoading,
2303
- children: isLoading ? "Creating..." : "Create Project"
2304
- }, undefined, false, undefined, this)
2305
- ]
2306
- }, undefined, true, undefined, this)
2307
- ]
2308
- }, undefined, true, undefined, this)
2309
- ]
2310
- }, undefined, true, undefined, this)
2311
- ]
2312
- }, undefined, true, undefined, this);
2313
- }
2314
-
2315
- // src/ui/modals/ProjectActionsModal.tsx
2316
- import { Button as Button2, Input as Input2 } from "@contractspec/lib.design-system";
2317
- import { useEffect as useEffect2, useState as useState4 } from "react";
2318
- import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
2319
- "use client";
2320
- function ProjectActionsModal({
2321
- isOpen,
2322
- project,
2323
- onClose,
2324
- onUpdate,
2325
- onArchive,
2326
- onActivate,
2327
- onDelete,
2328
- isLoading = false
2329
- }) {
2330
- const [mode, setMode] = useState4("menu");
2331
- const [name, setName] = useState4("");
2332
- const [description, setDescription] = useState4("");
2333
- const [error, setError] = useState4(null);
2334
- const resetForm = () => {
2335
- setMode("menu");
2336
- setError(null);
2337
- if (project) {
2338
- setName(project.name);
2339
- setDescription(project.description ?? "");
2340
- }
2341
- };
2342
- const handleClose = () => {
2343
- resetForm();
2344
- onClose();
2345
- };
2346
- useEffect2(() => {
2347
- if (project) {
2348
- setName(project.name);
2349
- setDescription(project.description ?? "");
2350
- }
2351
- }, [project]);
2352
- const handleEdit = async () => {
2353
- if (!project)
2354
- return;
2355
- setError(null);
2356
- if (!name.trim()) {
2357
- setError("Project name is required");
2358
- return;
2359
- }
2360
- try {
2361
- await onUpdate({
2362
- id: project.id,
2363
- name: name.trim(),
2364
- description: description.trim() || undefined
2365
- });
2366
- handleClose();
2367
- } catch (err) {
2368
- setError(err instanceof Error ? err.message : "Failed to update project");
2369
- }
2370
- };
2371
- const handleArchive = async () => {
2372
- if (!project)
2373
- return;
2374
- setError(null);
2375
- try {
2376
- await onArchive(project.id);
2377
- handleClose();
2378
- } catch (err) {
2379
- setError(err instanceof Error ? err.message : "Failed to archive project");
2380
- }
2381
- };
2382
- const handleActivate = async () => {
2383
- if (!project)
2384
- return;
2385
- setError(null);
2386
- try {
2387
- await onActivate(project.id);
2388
- handleClose();
2389
- } catch (err) {
2390
- setError(err instanceof Error ? err.message : "Failed to activate project");
2391
- }
2392
- };
2393
- const handleDelete = async () => {
2394
- if (!project)
2395
- return;
2396
- setError(null);
2397
- try {
2398
- await onDelete(project.id);
2399
- handleClose();
2400
- } catch (err) {
2401
- setError(err instanceof Error ? err.message : "Failed to delete project");
2402
- }
2403
- };
2404
- if (!isOpen || !project)
2405
- return null;
2406
- return /* @__PURE__ */ jsxDEV2("div", {
2407
- className: "fixed inset-0 z-50 flex items-center justify-center",
2408
- children: [
2409
- /* @__PURE__ */ jsxDEV2("div", {
2410
- className: "absolute inset-0 bg-background/80 backdrop-blur-sm",
2411
- onClick: handleClose,
2412
- role: "button",
2413
- tabIndex: 0,
2414
- onKeyDown: (e) => {
2415
- if (e.key === "Enter" || e.key === " ")
2416
- handleClose();
2417
- },
2418
- "aria-label": "Close modal"
2419
- }, undefined, false, undefined, this),
2420
- /* @__PURE__ */ jsxDEV2("div", {
2421
- className: "relative z-10 w-full max-w-md rounded-xl border border-border bg-card p-6 shadow-xl",
2422
- children: [
2423
- /* @__PURE__ */ jsxDEV2("div", {
2424
- className: "mb-4 border-border border-b pb-4",
2425
- children: [
2426
- /* @__PURE__ */ jsxDEV2("h2", {
2427
- className: "font-semibold text-xl",
2428
- children: project.name
2429
- }, undefined, false, undefined, this),
2430
- /* @__PURE__ */ jsxDEV2("p", {
2431
- className: "text-muted-foreground text-sm",
2432
- children: [
2433
- project.tier,
2434
- " \xB7 ",
2435
- project.status
2436
- ]
2437
- }, undefined, true, undefined, this)
2438
- ]
2439
- }, undefined, true, undefined, this),
2440
- mode === "menu" && /* @__PURE__ */ jsxDEV2("div", {
2441
- className: "space-y-3",
2442
- children: [
2443
- /* @__PURE__ */ jsxDEV2(Button2, {
2444
- className: "w-full justify-start",
2445
- variant: "ghost",
2446
- onPress: () => setMode("edit"),
2447
- children: [
2448
- /* @__PURE__ */ jsxDEV2("span", {
2449
- className: "mr-2",
2450
- children: "\u270F\uFE0F"
2451
- }, undefined, false, undefined, this),
2452
- " Edit Project"
2453
- ]
2454
- }, undefined, true, undefined, this),
2455
- project.status === "ACTIVE" || project.status === "DRAFT" ? /* @__PURE__ */ jsxDEV2(Button2, {
2456
- className: "w-full justify-start",
2457
- variant: "ghost",
2458
- onPress: () => setMode("archive"),
2459
- children: [
2460
- /* @__PURE__ */ jsxDEV2("span", {
2461
- className: "mr-2",
2462
- children: "\uD83D\uDCE6"
2463
- }, undefined, false, undefined, this),
2464
- " Archive Project"
2465
- ]
2466
- }, undefined, true, undefined, this) : project.status === "ARCHIVED" ? /* @__PURE__ */ jsxDEV2(Button2, {
2467
- className: "w-full justify-start",
2468
- variant: "ghost",
2469
- onPress: handleActivate,
2470
- disabled: isLoading,
2471
- children: [
2472
- /* @__PURE__ */ jsxDEV2("span", {
2473
- className: "mr-2",
2474
- children: "\uD83D\uDD04"
2475
- }, undefined, false, undefined, this),
2476
- " Restore Project"
2477
- ]
2478
- }, undefined, true, undefined, this) : null,
2479
- /* @__PURE__ */ jsxDEV2(Button2, {
2480
- className: "w-full justify-start text-red-500 hover:text-red-600",
2481
- variant: "ghost",
2482
- onPress: () => setMode("delete"),
2483
- children: [
2484
- /* @__PURE__ */ jsxDEV2("span", {
2485
- className: "mr-2",
2486
- children: "\uD83D\uDDD1\uFE0F"
2487
- }, undefined, false, undefined, this),
2488
- " Delete Project"
2489
- ]
2490
- }, undefined, true, undefined, this),
2491
- /* @__PURE__ */ jsxDEV2("div", {
2492
- className: "border-border border-t pt-3",
2493
- children: /* @__PURE__ */ jsxDEV2(Button2, {
2494
- className: "w-full",
2495
- variant: "outline",
2496
- onPress: handleClose,
2497
- children: "Close"
2498
- }, undefined, false, undefined, this)
2499
- }, undefined, false, undefined, this)
2500
- ]
2501
- }, undefined, true, undefined, this),
2502
- mode === "edit" && /* @__PURE__ */ jsxDEV2("div", {
2503
- className: "space-y-4",
2504
- children: [
2505
- /* @__PURE__ */ jsxDEV2("div", {
2506
- children: [
2507
- /* @__PURE__ */ jsxDEV2("label", {
2508
- htmlFor: "edit-name",
2509
- className: "mb-1 block font-medium text-muted-foreground text-sm",
2510
- children: "Project Name *"
2511
- }, undefined, false, undefined, this),
2512
- /* @__PURE__ */ jsxDEV2(Input2, {
2513
- id: "edit-name",
2514
- value: name,
2515
- onChange: (e) => setName(e.target.value),
2516
- disabled: isLoading
2517
- }, undefined, false, undefined, this)
2518
- ]
2519
- }, undefined, true, undefined, this),
2520
- /* @__PURE__ */ jsxDEV2("div", {
2521
- children: [
2522
- /* @__PURE__ */ jsxDEV2("label", {
2523
- htmlFor: "edit-description",
2524
- className: "mb-1 block font-medium text-muted-foreground text-sm",
2525
- children: "Description"
2526
- }, undefined, false, undefined, this),
2527
- /* @__PURE__ */ jsxDEV2("textarea", {
2528
- id: "edit-description",
2529
- value: description,
2530
- onChange: (e) => setDescription(e.target.value),
2531
- rows: 3,
2532
- disabled: isLoading,
2533
- className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
2534
- }, undefined, false, undefined, this)
2535
- ]
2536
- }, undefined, true, undefined, this),
2537
- error && /* @__PURE__ */ jsxDEV2("div", {
2538
- className: "rounded-md bg-destructive/10 p-3 text-destructive text-sm",
2539
- children: error
2540
- }, undefined, false, undefined, this),
2541
- /* @__PURE__ */ jsxDEV2("div", {
2542
- className: "flex justify-end gap-3 pt-2",
2543
- children: [
2544
- /* @__PURE__ */ jsxDEV2(Button2, {
2545
- variant: "ghost",
2546
- onPress: () => setMode("menu"),
2547
- disabled: isLoading,
2548
- children: "Back"
2549
- }, undefined, false, undefined, this),
2550
- /* @__PURE__ */ jsxDEV2(Button2, {
2551
- onPress: handleEdit,
2552
- disabled: isLoading,
2553
- children: isLoading ? "Saving..." : "Save Changes"
2554
- }, undefined, false, undefined, this)
2555
- ]
2556
- }, undefined, true, undefined, this)
2557
- ]
2558
- }, undefined, true, undefined, this),
2559
- mode === "archive" && /* @__PURE__ */ jsxDEV2("div", {
2560
- className: "space-y-4",
2561
- children: [
2562
- /* @__PURE__ */ jsxDEV2("p", {
2563
- className: "text-muted-foreground",
2564
- children: [
2565
- "Are you sure you want to archive",
2566
- " ",
2567
- /* @__PURE__ */ jsxDEV2("span", {
2568
- className: "font-medium text-foreground",
2569
- children: project.name
2570
- }, undefined, false, undefined, this),
2571
- "?"
2572
- ]
2573
- }, undefined, true, undefined, this),
2574
- /* @__PURE__ */ jsxDEV2("p", {
2575
- className: "text-muted-foreground text-sm",
2576
- children: "Archived projects can be restored later."
2577
- }, undefined, false, undefined, this),
2578
- error && /* @__PURE__ */ jsxDEV2("div", {
2579
- className: "rounded-md bg-destructive/10 p-3 text-destructive text-sm",
2580
- children: error
2581
- }, undefined, false, undefined, this),
2582
- /* @__PURE__ */ jsxDEV2("div", {
2583
- className: "flex justify-end gap-3 pt-2",
2584
- children: [
2585
- /* @__PURE__ */ jsxDEV2(Button2, {
2586
- variant: "ghost",
2587
- onPress: () => setMode("menu"),
2588
- disabled: isLoading,
2589
- children: "Cancel"
2590
- }, undefined, false, undefined, this),
2591
- /* @__PURE__ */ jsxDEV2(Button2, {
2592
- onPress: handleArchive,
2593
- disabled: isLoading,
2594
- children: isLoading ? "Archiving..." : "\uD83D\uDCE6 Archive"
2595
- }, undefined, false, undefined, this)
2596
- ]
2597
- }, undefined, true, undefined, this)
2598
- ]
2599
- }, undefined, true, undefined, this),
2600
- mode === "delete" && /* @__PURE__ */ jsxDEV2("div", {
2601
- className: "space-y-4",
2602
- children: [
2603
- /* @__PURE__ */ jsxDEV2("p", {
2604
- className: "text-muted-foreground",
2605
- children: [
2606
- "Are you sure you want to delete",
2607
- " ",
2608
- /* @__PURE__ */ jsxDEV2("span", {
2609
- className: "font-medium text-foreground",
2610
- children: project.name
2611
- }, undefined, false, undefined, this),
2612
- "?"
2613
- ]
2614
- }, undefined, true, undefined, this),
2615
- /* @__PURE__ */ jsxDEV2("p", {
2616
- className: "text-destructive text-sm",
2617
- children: "This action cannot be undone."
2618
- }, undefined, false, undefined, this),
2619
- error && /* @__PURE__ */ jsxDEV2("div", {
2620
- className: "rounded-md bg-destructive/10 p-3 text-destructive text-sm",
2621
- children: error
2622
- }, undefined, false, undefined, this),
2623
- /* @__PURE__ */ jsxDEV2("div", {
2624
- className: "flex justify-end gap-3 pt-2",
2625
- children: [
2626
- /* @__PURE__ */ jsxDEV2(Button2, {
2627
- variant: "ghost",
2628
- onPress: () => setMode("menu"),
2629
- disabled: isLoading,
2630
- children: "Cancel"
2631
- }, undefined, false, undefined, this),
2632
- /* @__PURE__ */ jsxDEV2(Button2, {
2633
- variant: "destructive",
2634
- onPress: handleDelete,
2635
- disabled: isLoading,
2636
- children: isLoading ? "Deleting..." : "\uD83D\uDDD1\uFE0F Delete"
2637
- }, undefined, false, undefined, this)
2638
- ]
2639
- }, undefined, true, undefined, this)
2640
- ]
2641
- }, undefined, true, undefined, this)
2642
- ]
2643
- }, undefined, true, undefined, this)
2644
- ]
2645
- }, undefined, true, undefined, this);
2646
- }
2647
- // src/ui/overlays/demo-overlays.ts
2648
- var saasFreeUserOverlay = {
2649
- overlayId: "saas-boilerplate.free-tier",
2650
- version: "1.0.0",
2651
- description: "Shows limitations for free tier users",
2652
- appliesTo: {
2653
- feature: "saas-boilerplate",
2654
- tier: "free"
2655
- },
2656
- modifications: [
2657
- {
2658
- type: "setLimit",
2659
- field: "projects",
2660
- max: 3,
2661
- message: "Upgrade to create more projects"
2662
- },
2663
- { type: "hideField", field: "advancedSettings", reason: "Pro feature" },
2664
- {
2665
- type: "addBadge",
2666
- position: "header",
2667
- label: "Free Plan",
2668
- variant: "default"
2669
- }
2670
- ]
2671
- };
2672
- var saasDemoOverlay = {
2673
- overlayId: "saas-boilerplate.demo-user",
2674
- version: "1.0.0",
2675
- description: "Demo mode for SaaS boilerplate",
2676
- appliesTo: {
2677
- feature: "saas-boilerplate",
2678
- role: "demo"
2679
- },
2680
- modifications: [
2681
- {
2682
- type: "hideField",
2683
- field: "billingSection",
2684
- reason: "Demo users cannot access billing"
2685
- },
2686
- {
2687
- type: "hideField",
2688
- field: "deleteAccount",
2689
- reason: "Not available in demo"
2690
- },
2691
- {
2692
- type: "addBadge",
2693
- position: "header",
2694
- label: "Demo Mode",
2695
- variant: "warning"
2696
- }
2697
- ]
2698
- };
2699
- var saasOverlays = [
2700
- saasFreeUserOverlay,
2701
- saasDemoOverlay
2702
- ];
2703
- // src/ui/renderers/project-list.markdown.ts
2704
- var PROJECT_TIERS = [
2705
- "FREE",
2706
- "PRO",
2707
- "ENTERPRISE"
2708
- ];
2709
- function toVisualizationProject(project, index4) {
2710
- return {
2711
- status: project.status === "DELETED" ? "ARCHIVED" : project.status,
2712
- tier: PROJECT_TIERS[index4 % PROJECT_TIERS.length] ?? "FREE",
2713
- createdAt: project.createdAt
2714
- };
2715
- }
2716
- var projectListMarkdownRenderer = {
2717
- target: "markdown",
2718
- render: async (desc, _ctx) => {
2719
- if (desc.source.type !== "component" || desc.source.componentKey !== "ProjectListView") {
2720
- throw new Error("projectListMarkdownRenderer: not ProjectListView");
2721
- }
2722
- const data = await mockListProjectsHandler({
2723
- limit: 20,
2724
- offset: 0
2725
- });
2726
- const items = data.projects ?? [];
2727
- const lines = [
2728
- "# Projects",
2729
- "",
2730
- `**Total**: ${data.total} projects`,
2731
- ""
2732
- ];
2733
- if (items.length === 0) {
2734
- lines.push("_No projects found._");
2735
- } else {
2736
- lines.push("| Status | Project | Description |");
2737
- lines.push("|--------|---------|-------------|");
2738
- for (const project of items) {
2739
- const status = project.status === "ACTIVE" ? "\u2705" : project.status === "ARCHIVED" ? "\uD83D\uDCE6" : "\u23F8\uFE0F";
2740
- lines.push(`| ${status} | **${project.name}** | ${project.description ?? "-"} |`);
2741
- }
2742
- }
2743
- return {
2744
- mimeType: "text/markdown",
2745
- body: lines.join(`
2746
- `)
2747
- };
2748
- }
2749
- };
2750
- var saasDashboardMarkdownRenderer = {
2751
- target: "markdown",
2752
- render: async (desc, _ctx) => {
2753
- if (desc.source.type !== "component" || desc.source.componentKey !== "SaasDashboard") {
2754
- throw new Error("saasDashboardMarkdownRenderer: not SaasDashboard");
2755
- }
2756
- const [projectsData, subscription] = await Promise.all([
2757
- mockListProjectsHandler({ limit: 50 }),
2758
- mockGetSubscriptionHandler()
2759
- ]);
2760
- const projects = projectsData.projects ?? [];
2761
- const activeProjects = projects.filter((p) => p.status === "ACTIVE").length;
2762
- const archivedProjects = projects.filter((p) => p.status === "ARCHIVED").length;
2763
- const visualizations = createSaasVisualizationItems(projects.map(toVisualizationProject), 10);
2764
- const lines = [
2765
- "# SaaS Dashboard",
2766
- "",
2767
- "> Organization overview and usage summary",
2768
- "",
2769
- "## Summary",
2770
- "",
2771
- "| Metric | Value |",
2772
- "|--------|-------|",
2773
- `| Total Projects | ${projectsData.total} |`,
2774
- `| Active Projects | ${activeProjects} |`,
2775
- `| Archived Projects | ${archivedProjects} |`,
2776
- `| Subscription Plan | ${subscription.planName} |`,
2777
- `| Subscription Status | ${subscription.status} |`,
2778
- ""
2779
- ];
2780
- lines.push("## Visualization Overview");
2781
- lines.push("");
2782
- for (const item of visualizations) {
2783
- lines.push(`- **${item.title}** via \`${item.spec.meta.key}\``);
2784
- }
2785
- lines.push("");
2786
- lines.push("## Projects");
2787
- lines.push("");
2788
- if (projects.length === 0) {
2789
- lines.push("_No projects yet._");
2790
- } else {
2791
- lines.push("| Status | Project | Description |");
2792
- lines.push("|--------|---------|-------------|");
2793
- for (const project of projects.slice(0, 10)) {
2794
- const status = project.status === "ACTIVE" ? "\u2705" : project.status === "ARCHIVED" ? "\uD83D\uDCE6" : "\u23F8\uFE0F";
2795
- lines.push(`| ${status} | **${project.name}** | ${project.description ?? "-"} |`);
2796
- }
2797
- if (projects.length > 10) {
2798
- lines.push(`| ... | ... | _${projectsData.total - 10} more projects_ |`);
2799
- }
2800
- }
2801
- lines.push("");
2802
- lines.push("## Subscription");
2803
- lines.push("");
2804
- lines.push(`- **Plan**: ${subscription.planName}`);
2805
- lines.push(`- **Status**: ${subscription.status}`);
2806
- if (subscription.currentPeriodEnd) {
2807
- lines.push(`- **Period End**: ${new Date(subscription.currentPeriodEnd).toLocaleDateString()}`);
2808
- }
2809
- return {
2810
- mimeType: "text/markdown",
2811
- body: lines.join(`
2812
- `)
2813
- };
2814
- }
2815
- };
2816
- var saasBillingMarkdownRenderer = {
2817
- target: "markdown",
2818
- render: async (desc, _ctx) => {
2819
- if (desc.source.type !== "component" || desc.source.componentKey !== "SubscriptionView") {
2820
- throw new Error("saasBillingMarkdownRenderer: not SubscriptionView");
2821
- }
2822
- const subscription = await mockGetSubscriptionHandler();
2823
- const lines = [
2824
- "# Billing & Subscription",
2825
- "",
2826
- "> Current subscription details and billing information",
2827
- "",
2828
- "## Subscription Details",
2829
- "",
2830
- "| Property | Value |",
2831
- "|----------|-------|",
2832
- `| Plan | ${subscription.planName} |`,
2833
- `| Status | ${subscription.status} |`,
2834
- `| ID | ${subscription.id} |`,
2835
- `| Period Start | ${new Date(subscription.currentPeriodStart).toLocaleDateString()} |`,
2836
- `| Period End | ${new Date(subscription.currentPeriodEnd).toLocaleDateString()} |`
2837
- ];
2838
- lines.push("");
2839
- lines.push("## Plan Limits");
2840
- lines.push("");
2841
- lines.push(`- **Projects**: ${subscription.limits.projects}`);
2842
- lines.push(`- **Users**: ${subscription.limits.users}`);
2843
- lines.push("");
2844
- lines.push("## Plan Features");
2845
- lines.push("");
2846
- if (subscription.planName.toLowerCase().includes("free")) {
2847
- lines.push("- \u2705 Up to 3 projects");
2848
- lines.push("- \u2705 Basic support");
2849
- lines.push("- \u274C Priority support");
2850
- lines.push("- \u274C Advanced analytics");
2851
- } else if (subscription.planName.toLowerCase().includes("pro")) {
2852
- lines.push("- \u2705 Unlimited projects");
2853
- lines.push("- \u2705 Priority support");
2854
- lines.push("- \u2705 Advanced analytics");
2855
- lines.push("- \u274C Custom integrations");
2856
- } else {
2857
- lines.push("- \u2705 Unlimited projects");
2858
- lines.push("- \u2705 Priority support");
2859
- lines.push("- \u2705 Advanced analytics");
2860
- lines.push("- \u2705 Custom integrations");
2861
- lines.push("- \u2705 Dedicated support");
2862
- }
2863
- return {
2864
- mimeType: "text/markdown",
2865
- body: lines.join(`
2866
- `)
2867
- };
2868
- }
2869
- };
2870
-
2871
- // src/ui/SaasProjectList.tsx
2872
- import {
2873
- Button as Button3,
2874
- EmptyState,
2875
- EntityCard,
2876
- ErrorState,
2877
- LoaderBlock,
2878
- StatCard,
2879
- StatCardGroup,
2880
- StatusChip
2881
- } from "@contractspec/lib.design-system";
2882
- import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
2883
- "use client";
2884
- function getStatusTone(status) {
2885
- switch (status) {
2886
- case "ACTIVE":
2887
- return "success";
2888
- case "DRAFT":
2889
- return "neutral";
2890
- case "ARCHIVED":
2891
- return "danger";
2892
- default:
2893
- return "neutral";
2894
- }
2895
- }
2896
- function SaasProjectList({
2897
- onProjectClick,
2898
- onCreateProject
2899
- }) {
2900
- const { data, loading, error, stats, refetch } = useProjectList();
2901
- if (loading && !data) {
2902
- return /* @__PURE__ */ jsxDEV3(LoaderBlock, {
2903
- label: "Loading projects..."
2904
- }, undefined, false, undefined, this);
2905
- }
2906
- if (error) {
2907
- return /* @__PURE__ */ jsxDEV3(ErrorState, {
2908
- title: "Failed to load projects",
2909
- description: error.message,
2910
- onRetry: refetch,
2911
- retryLabel: "Retry"
2912
- }, undefined, false, undefined, this);
2913
- }
2914
- if (!data?.items.length) {
2915
- return /* @__PURE__ */ jsxDEV3(EmptyState, {
2916
- title: "No projects found",
2917
- description: "Create your first project to get started.",
2918
- primaryAction: onCreateProject ? /* @__PURE__ */ jsxDEV3(Button3, {
2919
- onPress: onCreateProject,
2920
- children: "Create Project"
2921
- }, undefined, false, undefined, this) : undefined
2922
- }, undefined, false, undefined, this);
2923
- }
2924
- return /* @__PURE__ */ jsxDEV3("div", {
2925
- className: "space-y-6",
2926
- children: [
2927
- stats && /* @__PURE__ */ jsxDEV3(StatCardGroup, {
2928
- children: [
2929
- /* @__PURE__ */ jsxDEV3(StatCard, {
2930
- label: "Total Projects",
2931
- value: stats.total.toString()
2932
- }, undefined, false, undefined, this),
2933
- /* @__PURE__ */ jsxDEV3(StatCard, {
2934
- label: "Active",
2935
- value: stats.activeCount.toString()
2936
- }, undefined, false, undefined, this),
2937
- /* @__PURE__ */ jsxDEV3(StatCard, {
2938
- label: "Draft",
2939
- value: stats.draftCount.toString()
2940
- }, undefined, false, undefined, this)
2941
- ]
2942
- }, undefined, true, undefined, this),
2943
- /* @__PURE__ */ jsxDEV3("div", {
2944
- className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3",
2945
- children: data.items.map((project) => /* @__PURE__ */ jsxDEV3(EntityCard, {
2946
- cardTitle: project.name,
2947
- cardSubtitle: project.tier,
2948
- meta: /* @__PURE__ */ jsxDEV3("p", {
2949
- className: "text-muted-foreground text-sm",
2950
- children: project.description
2951
- }, undefined, false, undefined, this),
2952
- chips: /* @__PURE__ */ jsxDEV3(StatusChip, {
2953
- tone: getStatusTone(project.status),
2954
- label: project.status
2955
- }, undefined, false, undefined, this),
2956
- footer: /* @__PURE__ */ jsxDEV3("span", {
2957
- className: "text-muted-foreground text-xs",
2958
- children: project.updatedAt.toLocaleDateString()
2959
- }, undefined, false, undefined, this),
2960
- onClick: onProjectClick ? () => onProjectClick(project.id) : undefined
2961
- }, project.id, false, undefined, this))
2962
- }, undefined, false, undefined, this)
2963
- ]
2964
- }, undefined, true, undefined, this);
2965
- }
2966
-
2967
- // src/ui/renderers/project-list.renderer.tsx
2968
- import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
2969
- var projectListReactRenderer = {
2970
- target: "react",
2971
- render: async (desc, _ctx) => {
2972
- if (desc.source.type !== "component") {
2973
- throw new Error("Invalid source type");
2974
- }
2975
- if (desc.source.componentKey !== "SaasProjectListView") {
2976
- throw new Error(`Unknown component: ${desc.source.componentKey}`);
2977
- }
2978
- return /* @__PURE__ */ jsxDEV4(SaasProjectList, {}, undefined, false, undefined, this);
2979
- }
2980
- };
2981
- // src/ui/SaasDashboard.visualizations.tsx
2982
- import {
2983
- VisualizationCard,
2984
- VisualizationGrid
2985
- } from "@contractspec/lib.design-system";
2986
- import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
2987
- "use client";
2988
- function SaasVisualizationOverview({
2989
- projects,
2990
- projectLimit
2991
- }) {
2992
- const items = createSaasVisualizationItems(projects, projectLimit);
2993
- return /* @__PURE__ */ jsxDEV5("section", {
2994
- className: "space-y-3",
2995
- children: [
2996
- /* @__PURE__ */ jsxDEV5("div", {
2997
- children: [
2998
- /* @__PURE__ */ jsxDEV5("h3", {
2999
- className: "font-semibold text-lg",
3000
- children: "Portfolio Visualizations"
3001
- }, undefined, false, undefined, this),
3002
- /* @__PURE__ */ jsxDEV5("p", {
3003
- className: "text-muted-foreground text-sm",
3004
- children: "Contract-backed charts for project mix, capacity, and activity."
3005
- }, undefined, false, undefined, this)
3006
- ]
3007
- }, undefined, true, undefined, this),
3008
- /* @__PURE__ */ jsxDEV5(VisualizationGrid, {
3009
- children: items.map((item) => /* @__PURE__ */ jsxDEV5(VisualizationCard, {
3010
- data: item.data,
3011
- description: item.description,
3012
- height: item.height,
3013
- spec: item.spec,
3014
- title: item.title
3015
- }, item.key, false, undefined, this))
3016
- }, undefined, false, undefined, this)
3017
- ]
3018
- }, undefined, true, undefined, this);
3019
- }
3020
-
3021
- // src/ui/SaasDashboard.tsx
3022
- import {
3023
- Button as Button4,
3024
- EmptyState as EmptyState2,
3025
- EntityCard as EntityCard2,
3026
- ErrorState as ErrorState2,
3027
- LoaderBlock as LoaderBlock2,
3028
- StatCard as StatCard2,
3029
- StatCardGroup as StatCardGroup2,
3030
- StatusChip as StatusChip2
3031
- } from "@contractspec/lib.design-system";
3032
- import { useCallback as useCallback3, useState as useState5 } from "react";
3033
- import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
3034
- "use client";
3035
- function getStatusTone2(status) {
3036
- switch (status) {
3037
- case "ACTIVE":
3038
- return "success";
3039
- case "DRAFT":
3040
- return "neutral";
3041
- case "ARCHIVED":
3042
- return "warning";
3043
- default:
3044
- return "neutral";
3045
- }
3046
- }
3047
- function SaasDashboard() {
3048
- const [activeTab, setActiveTab] = useState5("projects");
3049
- const [isCreateModalOpen, setIsCreateModalOpen] = useState5(false);
3050
- const [selectedProject, setSelectedProject] = useState5(null);
3051
- const [isProjectActionsOpen, setIsProjectActionsOpen] = useState5(false);
3052
- const { data, subscription, loading, error, stats, refetch } = useProjectList();
3053
- const mutations = useProjectMutations({
3054
- onSuccess: () => {
3055
- refetch();
3056
- }
3057
- });
3058
- const handleProjectClick = useCallback3((project) => {
3059
- setSelectedProject(project);
3060
- setIsProjectActionsOpen(true);
3061
- }, []);
3062
- const tabs = [
3063
- { id: "projects", label: "Projects", icon: "\uD83D\uDCC1" },
3064
- { id: "billing", label: "Billing", icon: "\uD83D\uDCB3" },
3065
- { id: "settings", label: "Settings", icon: "\u2699\uFE0F" }
3066
- ];
3067
- if (loading && !data) {
3068
- return /* @__PURE__ */ jsxDEV6(LoaderBlock2, {
3069
- label: "Loading dashboard..."
3070
- }, undefined, false, undefined, this);
3071
- }
3072
- if (error) {
3073
- return /* @__PURE__ */ jsxDEV6(ErrorState2, {
3074
- title: "Failed to load dashboard",
3075
- description: error.message,
3076
- onRetry: refetch,
3077
- retryLabel: "Retry"
3078
- }, undefined, false, undefined, this);
3079
- }
3080
- return /* @__PURE__ */ jsxDEV6("div", {
3081
- className: "space-y-6",
3082
- children: [
3083
- /* @__PURE__ */ jsxDEV6("div", {
3084
- className: "flex items-center justify-between",
3085
- children: [
3086
- /* @__PURE__ */ jsxDEV6("h2", {
3087
- className: "font-bold text-2xl",
3088
- children: "SaaS Dashboard"
3089
- }, undefined, false, undefined, this),
3090
- activeTab === "projects" && /* @__PURE__ */ jsxDEV6(Button4, {
3091
- onPress: () => setIsCreateModalOpen(true),
3092
- children: [
3093
- /* @__PURE__ */ jsxDEV6("span", {
3094
- className: "mr-2",
3095
- children: "+"
3096
- }, undefined, false, undefined, this),
3097
- " New Project"
3098
- ]
3099
- }, undefined, true, undefined, this)
3100
- ]
3101
- }, undefined, true, undefined, this),
3102
- stats && subscription && /* @__PURE__ */ jsxDEV6(StatCardGroup2, {
3103
- children: [
3104
- /* @__PURE__ */ jsxDEV6(StatCard2, {
3105
- label: "Projects",
3106
- value: stats.total.toString()
3107
- }, undefined, false, undefined, this),
3108
- /* @__PURE__ */ jsxDEV6(StatCard2, {
3109
- label: "Active",
3110
- value: stats.activeCount.toString()
3111
- }, undefined, false, undefined, this),
3112
- /* @__PURE__ */ jsxDEV6(StatCard2, {
3113
- label: "Draft",
3114
- value: stats.draftCount.toString()
3115
- }, undefined, false, undefined, this),
3116
- /* @__PURE__ */ jsxDEV6(StatCard2, {
3117
- label: "Plan",
3118
- value: subscription.plan,
3119
- hint: subscription.status
3120
- }, undefined, false, undefined, this)
3121
- ]
3122
- }, undefined, true, undefined, this),
3123
- data && stats && /* @__PURE__ */ jsxDEV6(SaasVisualizationOverview, {
3124
- projectLimit: stats.projectLimit,
3125
- projects: data.items
3126
- }, undefined, false, undefined, this),
3127
- /* @__PURE__ */ jsxDEV6("nav", {
3128
- className: "flex gap-1 rounded-lg bg-muted p-1",
3129
- role: "tablist",
3130
- children: tabs.map((tab) => /* @__PURE__ */ jsxDEV6("button", {
3131
- type: "button",
3132
- role: "tab",
3133
- "aria-selected": activeTab === tab.id,
3134
- onClick: () => setActiveTab(tab.id),
3135
- className: `flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2 font-medium text-sm transition-colors ${activeTab === tab.id ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`,
3136
- children: [
3137
- /* @__PURE__ */ jsxDEV6("span", {
3138
- children: tab.icon
3139
- }, undefined, false, undefined, this),
3140
- tab.label
3141
- ]
3142
- }, tab.id, true, undefined, this))
3143
- }, undefined, false, undefined, this),
3144
- /* @__PURE__ */ jsxDEV6("div", {
3145
- className: "min-h-[400px]",
3146
- role: "tabpanel",
3147
- children: [
3148
- activeTab === "projects" && /* @__PURE__ */ jsxDEV6(ProjectsTab, {
3149
- data,
3150
- onProjectClick: handleProjectClick
3151
- }, undefined, false, undefined, this),
3152
- activeTab === "billing" && /* @__PURE__ */ jsxDEV6(BillingTab, {
3153
- subscription
3154
- }, undefined, false, undefined, this),
3155
- activeTab === "settings" && /* @__PURE__ */ jsxDEV6(SettingsTab, {}, undefined, false, undefined, this)
3156
- ]
3157
- }, undefined, true, undefined, this),
3158
- /* @__PURE__ */ jsxDEV6(CreateProjectModal, {
3159
- isOpen: isCreateModalOpen,
3160
- onClose: () => setIsCreateModalOpen(false),
3161
- onSubmit: async (input) => {
3162
- await mutations.createProject(input);
3163
- },
3164
- isLoading: mutations.createState.loading
3165
- }, undefined, false, undefined, this),
3166
- /* @__PURE__ */ jsxDEV6(ProjectActionsModal, {
3167
- isOpen: isProjectActionsOpen,
3168
- project: selectedProject,
3169
- onClose: () => {
3170
- setIsProjectActionsOpen(false);
3171
- setSelectedProject(null);
3172
- },
3173
- onUpdate: async (input) => {
3174
- await mutations.updateProject(input);
3175
- },
3176
- onArchive: async (projectId) => {
3177
- await mutations.archiveProject(projectId);
3178
- },
3179
- onActivate: async (projectId) => {
3180
- await mutations.activateProject(projectId);
3181
- },
3182
- onDelete: async (projectId) => {
3183
- await mutations.deleteProject(projectId);
3184
- },
3185
- isLoading: mutations.isLoading
3186
- }, undefined, false, undefined, this)
3187
- ]
3188
- }, undefined, true, undefined, this);
3189
- }
3190
- function ProjectsTab({ data, onProjectClick }) {
3191
- if (!data?.items.length) {
3192
- return /* @__PURE__ */ jsxDEV6(EmptyState2, {
3193
- title: "No projects yet",
3194
- description: "Create your first project to get started."
3195
- }, undefined, false, undefined, this);
3196
- }
3197
- return /* @__PURE__ */ jsxDEV6("div", {
3198
- className: "space-y-4",
3199
- children: /* @__PURE__ */ jsxDEV6("div", {
3200
- className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3",
3201
- children: data.items.map((project) => /* @__PURE__ */ jsxDEV6(EntityCard2, {
3202
- cardTitle: project.name,
3203
- cardSubtitle: project.tier,
3204
- meta: /* @__PURE__ */ jsxDEV6("p", {
3205
- className: "text-muted-foreground text-sm",
3206
- children: project.description
3207
- }, undefined, false, undefined, this),
3208
- chips: /* @__PURE__ */ jsxDEV6(StatusChip2, {
3209
- tone: getStatusTone2(project.status),
3210
- label: project.status
3211
- }, undefined, false, undefined, this),
3212
- footer: /* @__PURE__ */ jsxDEV6("div", {
3213
- className: "flex w-full items-center justify-between",
3214
- children: [
3215
- /* @__PURE__ */ jsxDEV6("span", {
3216
- className: "text-muted-foreground text-xs",
3217
- children: project.updatedAt.toLocaleDateString()
3218
- }, undefined, false, undefined, this),
3219
- /* @__PURE__ */ jsxDEV6(Button4, {
3220
- variant: "ghost",
3221
- size: "sm",
3222
- onPress: () => onProjectClick?.(project),
3223
- children: "Actions"
3224
- }, undefined, false, undefined, this)
3225
- ]
3226
- }, undefined, true, undefined, this)
3227
- }, project.id, false, undefined, this))
3228
- }, undefined, false, undefined, this)
3229
- }, undefined, false, undefined, this);
3230
- }
3231
- function BillingTab({ subscription }) {
3232
- if (!subscription)
3233
- return null;
3234
- return /* @__PURE__ */ jsxDEV6("div", {
3235
- className: "space-y-6",
3236
- children: [
3237
- /* @__PURE__ */ jsxDEV6("div", {
3238
- className: "rounded-xl border border-border bg-card p-6",
3239
- children: [
3240
- /* @__PURE__ */ jsxDEV6("div", {
3241
- className: "flex items-start justify-between",
3242
- children: [
3243
- /* @__PURE__ */ jsxDEV6("div", {
3244
- children: [
3245
- /* @__PURE__ */ jsxDEV6("h3", {
3246
- className: "font-semibold text-lg",
3247
- children: [
3248
- subscription.plan,
3249
- " Plan"
3250
- ]
3251
- }, undefined, true, undefined, this),
3252
- /* @__PURE__ */ jsxDEV6("p", {
3253
- className: "text-muted-foreground text-sm",
3254
- children: [
3255
- "Current period:",
3256
- " ",
3257
- subscription.currentPeriodStart.toLocaleDateString(),
3258
- " -",
3259
- " ",
3260
- subscription.currentPeriodEnd.toLocaleDateString()
3261
- ]
3262
- }, undefined, true, undefined, this),
3263
- /* @__PURE__ */ jsxDEV6("p", {
3264
- className: "text-muted-foreground text-sm",
3265
- children: [
3266
- "Billing cycle: ",
3267
- subscription.billingCycle
3268
- ]
3269
- }, undefined, true, undefined, this)
3270
- ]
3271
- }, undefined, true, undefined, this),
3272
- /* @__PURE__ */ jsxDEV6(StatusChip2, {
3273
- tone: "success",
3274
- label: subscription.status
3275
- }, undefined, false, undefined, this)
3276
- ]
3277
- }, undefined, true, undefined, this),
3278
- /* @__PURE__ */ jsxDEV6("div", {
3279
- className: "mt-4 flex gap-3",
3280
- children: [
3281
- /* @__PURE__ */ jsxDEV6(Button4, {
3282
- variant: "outline",
3283
- onPress: () => alert("Upgrade clicked!"),
3284
- children: "Upgrade Plan"
3285
- }, undefined, false, undefined, this),
3286
- /* @__PURE__ */ jsxDEV6(Button4, {
3287
- variant: "ghost",
3288
- onPress: () => alert("Manage Billing clicked!"),
3289
- children: "Manage Billing"
3290
- }, undefined, false, undefined, this)
3291
- ]
3292
- }, undefined, true, undefined, this)
3293
- ]
3294
- }, undefined, true, undefined, this),
3295
- subscription.cancelAtPeriodEnd && /* @__PURE__ */ jsxDEV6("div", {
3296
- className: "rounded-xl border border-border bg-destructive/10 p-4 text-destructive",
3297
- children: /* @__PURE__ */ jsxDEV6("p", {
3298
- className: "font-medium text-sm",
3299
- children: "\u26A0\uFE0F Your subscription will be cancelled at the end of the current period."
3300
- }, undefined, false, undefined, this)
3301
- }, undefined, false, undefined, this)
3302
- ]
3303
- }, undefined, true, undefined, this);
3304
- }
3305
- function SettingsTab() {
3306
- return /* @__PURE__ */ jsxDEV6("div", {
3307
- className: "space-y-6",
3308
- children: /* @__PURE__ */ jsxDEV6("div", {
3309
- className: "rounded-xl border border-border bg-card p-6",
3310
- children: [
3311
- /* @__PURE__ */ jsxDEV6("h3", {
3312
- className: "mb-4 font-semibold text-lg",
3313
- children: "Organization Settings"
3314
- }, undefined, false, undefined, this),
3315
- /* @__PURE__ */ jsxDEV6("div", {
3316
- className: "space-y-4",
3317
- children: [
3318
- /* @__PURE__ */ jsxDEV6("div", {
3319
- children: [
3320
- /* @__PURE__ */ jsxDEV6("label", {
3321
- htmlFor: "org-name",
3322
- className: "font-medium text-sm",
3323
- children: "Organization Name"
3324
- }, undefined, false, undefined, this),
3325
- /* @__PURE__ */ jsxDEV6("input", {
3326
- id: "org-name",
3327
- type: "text",
3328
- defaultValue: "Demo Organization",
3329
- className: "mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
3330
- }, undefined, false, undefined, this)
3331
- ]
3332
- }, undefined, true, undefined, this),
3333
- /* @__PURE__ */ jsxDEV6("div", {
3334
- children: [
3335
- /* @__PURE__ */ jsxDEV6("label", {
3336
- htmlFor: "timezone",
3337
- className: "font-medium text-sm",
3338
- children: "Default Timezone"
3339
- }, undefined, false, undefined, this),
3340
- /* @__PURE__ */ jsxDEV6("select", {
3341
- id: "timezone",
3342
- className: "mt-1 block w-full rounded-md border border-input bg-background px-3 py-2",
3343
- children: [
3344
- /* @__PURE__ */ jsxDEV6("option", {
3345
- children: "UTC"
3346
- }, undefined, false, undefined, this),
3347
- /* @__PURE__ */ jsxDEV6("option", {
3348
- children: "America/New_York"
3349
- }, undefined, false, undefined, this),
3350
- /* @__PURE__ */ jsxDEV6("option", {
3351
- children: "Europe/London"
3352
- }, undefined, false, undefined, this),
3353
- /* @__PURE__ */ jsxDEV6("option", {
3354
- children: "Asia/Tokyo"
3355
- }, undefined, false, undefined, this)
3356
- ]
3357
- }, undefined, true, undefined, this)
3358
- ]
3359
- }, undefined, true, undefined, this),
3360
- /* @__PURE__ */ jsxDEV6("div", {
3361
- className: "pt-2",
3362
- children: /* @__PURE__ */ jsxDEV6(Button4, {
3363
- onPress: () => alert("Settings saved!"),
3364
- children: "Save Settings"
3365
- }, undefined, false, undefined, this)
3366
- }, undefined, false, undefined, this)
3367
- ]
3368
- }, undefined, true, undefined, this)
3369
- ]
3370
- }, undefined, true, undefined, this)
3371
- }, undefined, false, undefined, this);
3372
- }
3373
-
3374
- // src/ui/SaasSettingsPanel.tsx
3375
- import { Button as Button5 } from "@contractspec/lib.design-system";
3376
- import { useState as useState6 } from "react";
3377
- import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
3378
- "use client";
3379
- function SaasSettingsPanel() {
3380
- const [orgName, setOrgName] = useState6("Demo Organization");
3381
- const [timezone, setTimezone] = useState6("UTC");
3382
- return /* @__PURE__ */ jsxDEV7("div", {
3383
- className: "space-y-6",
3384
- children: [
3385
- /* @__PURE__ */ jsxDEV7("div", {
3386
- className: "rounded-xl border border-border bg-card p-6",
3387
- children: [
3388
- /* @__PURE__ */ jsxDEV7("h3", {
3389
- className: "mb-4 font-semibold text-lg",
3390
- children: "Organization Settings"
3391
- }, undefined, false, undefined, this),
3392
- /* @__PURE__ */ jsxDEV7("div", {
3393
- className: "space-y-4",
3394
- children: [
3395
- /* @__PURE__ */ jsxDEV7("div", {
3396
- children: [
3397
- /* @__PURE__ */ jsxDEV7("label", {
3398
- htmlFor: "setting-org-name",
3399
- className: "block font-medium text-sm",
3400
- children: "Organization Name"
3401
- }, undefined, false, undefined, this),
3402
- /* @__PURE__ */ jsxDEV7("input", {
3403
- id: "setting-org-name",
3404
- type: "text",
3405
- value: orgName,
3406
- onChange: (e) => setOrgName(e.target.value),
3407
- className: "mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
3408
- }, undefined, false, undefined, this)
3409
- ]
3410
- }, undefined, true, undefined, this),
3411
- /* @__PURE__ */ jsxDEV7("div", {
3412
- children: [
3413
- /* @__PURE__ */ jsxDEV7("label", {
3414
- htmlFor: "setting-timezone",
3415
- className: "block font-medium text-sm",
3416
- children: "Default Timezone"
3417
- }, undefined, false, undefined, this),
3418
- /* @__PURE__ */ jsxDEV7("select", {
3419
- id: "setting-timezone",
3420
- value: timezone,
3421
- onChange: (e) => setTimezone(e.target.value),
3422
- className: "mt-1 block w-full rounded-md border border-input bg-background px-3 py-2",
3423
- children: [
3424
- /* @__PURE__ */ jsxDEV7("option", {
3425
- value: "UTC",
3426
- children: "UTC"
3427
- }, undefined, false, undefined, this),
3428
- /* @__PURE__ */ jsxDEV7("option", {
3429
- value: "America/New_York",
3430
- children: "America/New_York"
3431
- }, undefined, false, undefined, this),
3432
- /* @__PURE__ */ jsxDEV7("option", {
3433
- value: "Europe/London",
3434
- children: "Europe/London"
3435
- }, undefined, false, undefined, this),
3436
- /* @__PURE__ */ jsxDEV7("option", {
3437
- value: "Asia/Tokyo",
3438
- children: "Asia/Tokyo"
3439
- }, undefined, false, undefined, this)
3440
- ]
3441
- }, undefined, true, undefined, this)
3442
- ]
3443
- }, undefined, true, undefined, this)
3444
- ]
3445
- }, undefined, true, undefined, this),
3446
- /* @__PURE__ */ jsxDEV7("div", {
3447
- className: "mt-6",
3448
- children: /* @__PURE__ */ jsxDEV7(Button5, {
3449
- variant: "default",
3450
- children: "Save Changes"
3451
- }, undefined, false, undefined, this)
3452
- }, undefined, false, undefined, this)
3453
- ]
3454
- }, undefined, true, undefined, this),
3455
- /* @__PURE__ */ jsxDEV7("div", {
3456
- className: "rounded-xl border border-border bg-card p-6",
3457
- children: [
3458
- /* @__PURE__ */ jsxDEV7("h3", {
3459
- className: "mb-4 font-semibold text-lg",
3460
- children: "Notifications"
3461
- }, undefined, false, undefined, this),
3462
- /* @__PURE__ */ jsxDEV7("div", {
3463
- className: "space-y-3",
3464
- children: [
3465
- { label: "Email notifications", defaultChecked: true },
3466
- { label: "Usage alerts", defaultChecked: true },
3467
- { label: "Weekly digest", defaultChecked: false }
3468
- ].map((item) => /* @__PURE__ */ jsxDEV7("label", {
3469
- className: "flex items-center gap-3",
3470
- children: [
3471
- /* @__PURE__ */ jsxDEV7("input", {
3472
- type: "checkbox",
3473
- defaultChecked: item.defaultChecked,
3474
- className: "h-4 w-4 rounded border-input"
3475
- }, undefined, false, undefined, this),
3476
- /* @__PURE__ */ jsxDEV7("span", {
3477
- className: "text-sm",
3478
- children: item.label
3479
- }, undefined, false, undefined, this)
3480
- ]
3481
- }, item.label, true, undefined, this))
3482
- }, undefined, false, undefined, this)
3483
- ]
3484
- }, undefined, true, undefined, this),
3485
- /* @__PURE__ */ jsxDEV7("div", {
3486
- className: "rounded-xl border border-red-200 bg-red-50 p-6 dark:border-red-900 dark:bg-red-950/20",
3487
- children: [
3488
- /* @__PURE__ */ jsxDEV7("h3", {
3489
- className: "mb-2 font-semibold text-lg text-red-700 dark:text-red-400",
3490
- children: "Danger Zone"
3491
- }, undefined, false, undefined, this),
3492
- /* @__PURE__ */ jsxDEV7("p", {
3493
- className: "mb-4 text-red-600 text-sm dark:text-red-300",
3494
- children: "These actions are irreversible. Please proceed with caution."
3495
- }, undefined, false, undefined, this),
3496
- /* @__PURE__ */ jsxDEV7("div", {
3497
- className: "flex gap-3",
3498
- children: [
3499
- /* @__PURE__ */ jsxDEV7(Button5, {
3500
- variant: "secondary",
3501
- size: "sm",
3502
- children: "Export Data"
3503
- }, undefined, false, undefined, this),
3504
- /* @__PURE__ */ jsxDEV7(Button5, {
3505
- variant: "secondary",
3506
- size: "sm",
3507
- children: "Delete Organization"
3508
- }, undefined, false, undefined, this)
3509
- ]
3510
- }, undefined, true, undefined, this)
3511
- ]
3512
- }, undefined, true, undefined, this)
3513
- ]
3514
- }, undefined, true, undefined, this);
3515
- }
3516
- // src/index.ts
3517
- import { identityRbacSchemaContribution } from "@contractspec/lib.identity-rbac";
3518
- import { jobsSchemaContribution } from "@contractspec/lib.jobs";
3519
- import { auditTrailSchemaContribution } from "@contractspec/module.audit-trail";
3520
- import { notificationsSchemaContribution } from "@contractspec/module.notifications";
3521
- var saasBoilerplateSchemaContribution = {
3522
- moduleId: "@contractspec/example.saas-boilerplate",
3523
- entities: [
3524
- ProjectEntity,
3525
- ProjectMemberEntity,
3526
- SettingsEntity,
3527
- FeatureFlagEntity,
3528
- SubscriptionEntity,
3529
- BillingUsageEntity,
3530
- UsageLimitEntity
3531
- ],
3532
- enums: [ProjectStatusEnum, SettingsScopeEnum, SubscriptionStatusEnum]
3533
- };
3534
- var schemaComposition = {
3535
- modules: [
3536
- identityRbacSchemaContribution,
3537
- jobsSchemaContribution,
3538
- auditTrailSchemaContribution,
3539
- notificationsSchemaContribution,
3540
- saasBoilerplateSchemaContribution
3541
- ],
3542
- provider: "postgresql",
3543
- outputPath: "./prisma/schema/generated.prisma"
3544
- };
3545
- export {
3546
- useProjectMutations,
3547
- useProjectList,
3548
- schemaComposition,
3549
- saasOverlays,
3550
- saasFreeUserOverlay,
3551
- saasDemoOverlay,
3552
- saasDashboardMarkdownRenderer,
3553
- saasBoilerplateSchemaContribution,
3554
- saasBillingMarkdownRenderer,
3555
- projectListReactRenderer,
3556
- projectListMarkdownRenderer,
3557
- mockUpdateProjectHandler,
3558
- mockRecordUsageHandler,
3559
- mockListProjectsHandler,
3560
- mockGetUsageSummaryHandler,
3561
- mockGetSubscriptionHandler,
3562
- mockGetProjectHandler,
3563
- mockDeleteProjectHandler,
3564
- mockCreateProjectHandler,
3565
- mockCheckFeatureAccessHandler,
3566
- example_default as example,
3567
- createSaasVisualizationItems,
3568
- createSaasHandlers,
3569
- UsageSummaryModel,
3570
- UsageRecordedPayloadModel,
3571
- UsageRecordedEvent,
3572
- UsageLimitReachedEvent,
3573
- UsageLimitEntity,
3574
- UsageDashboardPresentation,
3575
- UpdateProjectInputModel,
3576
- UpdateProjectContract,
3577
- SubscriptionStatusSchemaEnum,
3578
- SubscriptionStatusEnum,
3579
- SubscriptionPresentation,
3580
- SubscriptionModel,
3581
- SubscriptionEntity,
3582
- SubscriptionChangedEvent,
3583
- SettingsScopeEnum,
3584
- SettingsPanelPresentation,
3585
- SettingsEntity,
3586
- SaasVisualizationSpecs,
3587
- SaasVisualizationRegistry,
3588
- SaasVisualizationRefs,
3589
- SaasSettingsPanel,
3590
- SaasProjectUsageVisualization,
3591
- SaasProjectTierVisualization,
3592
- SaasProjectStatusVisualization,
3593
- SaasProjectList,
3594
- SaasProjectActivityVisualization,
3595
- SaasDashboardPresentation,
3596
- SaasDashboard,
3597
- SaasBoilerplateFeature,
3598
- RecordUsageOutputModel,
3599
- RecordUsageInputModel,
3600
- RecordUsageContract,
3601
- ProjectUpdatedEvent,
3602
- ProjectStatusSchemaEnum,
3603
- ProjectStatusFilterEnum,
3604
- ProjectStatusEnum,
3605
- ProjectModel,
3606
- ProjectMemberEntity,
3607
- ProjectListPresentation,
3608
- ProjectEntity,
3609
- ProjectDetailPresentation,
3610
- ProjectDeletedPayloadModel,
3611
- ProjectDeletedEvent,
3612
- ProjectCreatedEvent,
3613
- ProjectArchivedEvent,
3614
- ProjectActionsModal,
3615
- ListProjectsOutputModel,
3616
- ListProjectsInputModel,
3617
- ListProjectsContract,
3618
- GetUsageSummaryOutputModel,
3619
- GetUsageSummaryInputModel,
3620
- GetUsageSummaryContract,
3621
- GetSubscriptionContract,
3622
- GetProjectInputModel,
3623
- GetProjectContract,
3624
- FeatureFlagEntity,
3625
- FeatureAccessReasonEnum,
3626
- DeleteProjectOutputModel,
3627
- DeleteProjectInputModel,
3628
- DeleteProjectContract,
3629
- CreateProjectModal,
3630
- CreateProjectInputModel,
3631
- CreateProjectContract,
3632
- CheckFeatureAccessOutputModel,
3633
- CheckFeatureAccessInputModel,
3634
- CheckFeatureAccessContract,
3635
- BillingUsageEntity
3636
- };
50
+ - Use Feature Flags for new settings/billing fields; default safe/off.`}];sG(tG);import{defineExample as eG}from"@contractspec/lib.contracts-spec";var H3=eG({meta:{key:"saas-boilerplate",version:"1.0.0",title:"SaaS Boilerplate",description:"Multi-tenant SaaS foundation with orgs, projects, settings, billing usage, and RBAC.",kind:"template",visibility:"public",stability:"experimental",owners:["@platform.core"],tags:["saas","multi-tenant","billing","rbac"]},docs:{rootDocId:"docs.examples.saas-boilerplate"},entrypoints:{packageName:"@contractspec/example.saas-boilerplate",feature:"./feature",contracts:"./contracts",presentations:"./presentations",handlers:"./handlers",docs:"./docs"},surfaces:{templates:!0,sandbox:{enabled:!0,modes:["playground","specs","builder","markdown","evolution"]},studio:{enabled:!0,installable:!0},mcp:{enabled:!0}}}),G3=H3;async function l(H){let{status:G,search:X,limit:$=20,offset:Q=0}=H,U=[...S];if(G&&G!=="all")U=U.filter((k)=>k.status===G);if(X){let k=X.toLowerCase();U=U.filter((Y)=>Y.name.toLowerCase().includes(k)||Y.description?.toLowerCase().includes(k)||Y.tags.some((K)=>K.toLowerCase().includes(k)))}U.sort((k,Y)=>Y.updatedAt.getTime()-k.updatedAt.getTime());let J=U.length;return{projects:U.slice(Q,Q+$),total:J}}async function WG(H){let G=S.find((X)=>X.id===H.projectId);if(!G)throw Error("NOT_FOUND");return G}async function JG(H,G){if(H.slug){if(S.some((Q)=>Q.slug===H.slug))throw Error("SLUG_EXISTS")}let X=new Date;return{id:`proj-${Date.now()}`,name:H.name,description:H.description,slug:H.slug??H.name.toLowerCase().replace(/\s+/g,"-"),organizationId:G.organizationId,createdBy:G.userId,status:"DRAFT",isPublic:H.isPublic??!1,tags:H.tags??[],createdAt:X,updatedAt:X}}async function KG(H){let G=S.find((X)=>X.id===H.projectId);if(!G)throw Error("NOT_FOUND");return{...G,name:H.name??G.name,description:H.description??G.description,slug:H.slug??G.slug,isPublic:H.isPublic??G.isPublic,tags:H.tags??G.tags,status:H.status??G.status,updatedAt:new Date}}async function VG(H){if(!S.find((X)=>X.id===H.projectId))throw Error("NOT_FOUND");return{success:!0}}import{web as $3}from"@contractspec/lib.runtime-sandbox";var{generateId:X3}=$3;function e(H){return{id:H.id,projectId:H.projectId,organizationId:H.organizationId,name:H.name,description:H.description??void 0,status:H.status,tier:H.tier,createdAt:new Date(H.createdAt),updatedAt:new Date(H.updatedAt)}}function Z3(H){return{id:H.id,projectId:H.projectId,organizationId:H.organizationId,plan:H.plan,status:H.status,billingCycle:H.billingCycle,currentPeriodStart:new Date(H.currentPeriodStart),currentPeriodEnd:new Date(H.currentPeriodEnd),cancelAtPeriodEnd:Boolean(H.cancelAtPeriodEnd)}}function k3(H){async function G(Z){let{projectId:k,organizationId:Y,status:K,search:_,limit:v=20,offset:z=0}=Z,D="WHERE projectId = ?",N=[k];if(Y)D+=" AND organizationId = ?",N.push(Y);if(K&&K!=="all")D+=" AND status = ?",N.push(K);if(_)D+=" AND (name LIKE ? OR description LIKE ?)",N.push(`%${_}%`,`%${_}%`);let q=(await H.query(`SELECT COUNT(*) as count FROM saas_project ${D}`,N)).rows[0]?.count??0,a=(await H.query(`SELECT * FROM saas_project ${D} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,[...N,v,z])).rows;return{items:a.map(e),total:q,hasMore:z+a.length<q}}async function X(Z){let k=(await H.query("SELECT * FROM saas_project WHERE id = ?",[Z])).rows;return k[0]?e(k[0]):null}async function $(Z,k){let Y=X3("proj"),K=new Date().toISOString();await H.execute(`INSERT INTO saas_project (id, projectId, organizationId, name, description, status, tier, createdAt, updatedAt)
51
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,[Y,k.projectId,k.organizationId,Z.name,Z.description??null,"DRAFT",Z.tier??"FREE",K,K]);let _=(await H.query("SELECT * FROM saas_project WHERE id = ?",[Y])).rows;return e(_[0])}async function Q(Z){let k=new Date().toISOString(),Y=["updatedAt = ?"],K=[k];if(Z.name!==void 0)Y.push("name = ?"),K.push(Z.name);if(Z.description!==void 0)Y.push("description = ?"),K.push(Z.description);if(Z.status!==void 0)Y.push("status = ?"),K.push(Z.status);K.push(Z.id),await H.execute(`UPDATE saas_project SET ${Y.join(", ")} WHERE id = ?`,K);let _=(await H.query("SELECT * FROM saas_project WHERE id = ?",[Z.id])).rows;if(!_[0])throw Error("NOT_FOUND");return e(_[0])}async function U(Z){await H.execute("DELETE FROM saas_project WHERE id = ?",[Z])}async function J(Z){let k="SELECT * FROM saas_subscription WHERE projectId = ?",Y=[Z.projectId];if(Z.organizationId)k+=" AND organizationId = ?",Y.push(Z.organizationId);k+=" LIMIT 1";let K=(await H.query(k,Y)).rows;return K[0]?Z3(K[0]):null}return{listProjects:G,getProject:X,createProject:$,updateProject:Q,deleteProject:U,getSubscription:J}}import{defineEntity as NG,defineEntityEnum as Y3,field as L,index as HH}from"@contractspec/lib.schema";var GH=Y3({name:"ProjectStatus",values:["DRAFT","ACTIVE","ARCHIVED","DELETED"],schema:"saas_app",description:"Status of a project."}),yH=NG({name:"Project",description:"A project belonging to an organization.",schema:"saas_app",map:"project",fields:{id:L.id({description:"Unique project ID"}),name:L.string({description:"Project name"}),description:L.string({isOptional:!0,description:"Project description"}),slug:L.string({isOptional:!0,description:"URL-friendly identifier"}),organizationId:L.foreignKey({description:"Owning organization"}),createdBy:L.foreignKey({description:"User who created the project"}),status:L.enum("ProjectStatus",{default:"DRAFT"}),isPublic:L.boolean({default:!1,description:"Whether project is publicly visible"}),settings:L.json({isOptional:!0,description:"Project-specific settings"}),tags:L.string({isArray:!0,description:"Project tags"}),metadata:L.json({isOptional:!0}),createdAt:L.createdAt(),updatedAt:L.updatedAt(),archivedAt:L.dateTime({isOptional:!0})},indexes:[HH.on(["organizationId","status"]),HH.on(["organizationId","createdAt"]),HH.unique(["organizationId","slug"])],enums:[GH]}),bH=NG({name:"ProjectMember",description:"User access to a specific project.",schema:"saas_app",map:"project_member",fields:{id:L.id(),projectId:L.foreignKey(),userId:L.foreignKey(),role:L.string({description:"Role in project (owner, editor, viewer)"}),addedBy:L.string({isOptional:!0}),createdAt:L.createdAt()},indexes:[HH.unique(["projectId","userId"])]});import{defineEnum as zG}from"@contractspec/lib.schema";var $H=zG("ProjectStatus",["DRAFT","ACTIVE","ARCHIVED","DELETED"]),fH=zG("ProjectStatusFilter",["DRAFT","ACTIVE","ARCHIVED","all"]);import{defineEvent as XH}from"@contractspec/lib.contracts-spec";import{defineSchemaModel as ZH,ScalarTypeEnum as M}from"@contractspec/lib.schema";var Q3=ZH({name:"ProjectCreatedPayload",description:"Payload when a project is created",fields:{projectId:{type:M.String_unsecure(),isOptional:!1},name:{type:M.String_unsecure(),isOptional:!1},organizationId:{type:M.String_unsecure(),isOptional:!1},createdBy:{type:M.String_unsecure(),isOptional:!1},createdAt:{type:M.DateTime(),isOptional:!1}}}),U3=ZH({name:"ProjectUpdatedPayload",description:"Payload when a project is updated",fields:{projectId:{type:M.String_unsecure(),isOptional:!1},updatedFields:{type:M.String_unsecure(),isArray:!0,isOptional:!1},updatedBy:{type:M.String_unsecure(),isOptional:!1},updatedAt:{type:M.DateTime(),isOptional:!1}}}),q3=ZH({name:"ProjectDeletedPayload",description:"Payload when a project is deleted",fields:{projectId:{type:M.String_unsecure(),isOptional:!1},organizationId:{type:M.String_unsecure(),isOptional:!1},deletedBy:{type:M.String_unsecure(),isOptional:!1},deletedAt:{type:M.DateTime(),isOptional:!1}}}),F3=ZH({name:"ProjectArchivedPayload",description:"Payload when a project is archived",fields:{projectId:{type:M.String_unsecure(),isOptional:!1},archivedBy:{type:M.String_unsecure(),isOptional:!1},archivedAt:{type:M.DateTime(),isOptional:!1}}}),W3=XH({meta:{key:"project.created",version:"1.0.0",description:"A new project has been created.",stability:"stable",owners:["@saas-team"],tags:["project","created"]},payload:Q3}),J3=XH({meta:{key:"project.updated",version:"1.0.0",description:"A project has been updated.",stability:"stable",owners:["@saas-team"],tags:["project","updated"]},payload:U3}),K3=XH({meta:{key:"project.deleted",version:"1.0.0",description:"A project has been deleted.",stability:"stable",owners:["@saas-team"],tags:["project","deleted"]},payload:q3}),V3=XH({meta:{key:"project.archived",version:"1.0.0",description:"A project has been archived.",stability:"stable",owners:["@saas-team"],tags:["project","archived"]},payload:F3});import{defineSchemaModel as x,ScalarTypeEnum as R}from"@contractspec/lib.schema";var f=x({name:"Project",description:"A project within an organization",fields:{id:{type:R.String_unsecure(),isOptional:!1},name:{type:R.String_unsecure(),isOptional:!1},description:{type:R.String_unsecure(),isOptional:!0},slug:{type:R.String_unsecure(),isOptional:!0},organizationId:{type:R.String_unsecure(),isOptional:!1},createdBy:{type:R.String_unsecure(),isOptional:!1},status:{type:$H,isOptional:!1},isPublic:{type:R.Boolean(),isOptional:!1},tags:{type:R.String_unsecure(),isArray:!0,isOptional:!1},createdAt:{type:R.DateTime(),isOptional:!1},updatedAt:{type:R.DateTime(),isOptional:!1}}}),TH=x({name:"CreateProjectInput",description:"Input for creating a project",fields:{name:{type:R.NonEmptyString(),isOptional:!1},description:{type:R.String_unsecure(),isOptional:!0},slug:{type:R.String_unsecure(),isOptional:!0},isPublic:{type:R.Boolean(),isOptional:!0},tags:{type:R.String_unsecure(),isArray:!0,isOptional:!0}}}),EH=x({name:"UpdateProjectInput",description:"Input for updating a project",fields:{projectId:{type:R.String_unsecure(),isOptional:!1},name:{type:R.String_unsecure(),isOptional:!0},description:{type:R.String_unsecure(),isOptional:!0},slug:{type:R.String_unsecure(),isOptional:!0},isPublic:{type:R.Boolean(),isOptional:!0},tags:{type:R.String_unsecure(),isArray:!0,isOptional:!0},status:{type:$H,isOptional:!0}}}),xH=x({name:"GetProjectInput",fields:{projectId:{type:R.String_unsecure(),isOptional:!1}}}),mH=x({name:"DeleteProjectInput",fields:{projectId:{type:R.String_unsecure(),isOptional:!1}}}),SH=x({name:"DeleteProjectOutput",fields:{success:{type:R.Boolean(),isOptional:!1}}}),jH=x({name:"ProjectDeletedPayload",fields:{projectId:{type:R.String_unsecure(),isOptional:!1}}}),dH=x({name:"ListProjectsInput",description:"Input for listing projects",fields:{status:{type:fH,isOptional:!0},search:{type:R.String_unsecure(),isOptional:!0},limit:{type:R.Int_unsecure(),isOptional:!0,defaultValue:20},offset:{type:R.Int_unsecure(),isOptional:!0,defaultValue:0}}}),uH=x({name:"ListProjectsOutput",description:"Output for listing projects",fields:{projects:{type:f,isArray:!0,isOptional:!1},total:{type:R.Int_unsecure(),isOptional:!1}}});import{defineCommand as lH,defineQuery as DG}from"@contractspec/lib.contracts-spec/operations";var p=["example.saas-boilerplate"],N3=lH({meta:{key:"saas.project.create",version:"1.0.0",stability:"stable",owners:[...p],tags:["saas","project","create"],description:"Create a new project in the organization.",goal:"Allow users to create projects for organizing work.",context:"Called from project creation UI or API."},io:{input:TH,output:f,errors:{SLUG_EXISTS:{description:"A project with this slug already exists",http:409,gqlCode:"SLUG_EXISTS",when:"Slug is already taken in the organization"},LIMIT_REACHED:{description:"Project limit reached for this plan",http:403,gqlCode:"LIMIT_REACHED",when:"Organization has reached project limit"}}},policy:{auth:"user"},sideEffects:{emits:[{key:"project.created",version:"1.0.0",when:"Project is created",payload:f}],audit:["project.created"]},acceptance:{scenarios:[{key:"create-project-happy-path",given:["User is authenticated"],when:["User creates project"],then:["Project is created","ProjectCreated event is emitted"]}],examples:[{key:"create-basic",input:{name:"Website Redesign",slug:"website-redesign"},output:{id:"proj-123",name:"Website Redesign",isArchived:!1}}]}}),z3=DG({meta:{key:"saas.project.get",version:"1.0.0",stability:"stable",owners:[...p],tags:["saas","project","get"],description:"Get a project by ID.",goal:"Retrieve project details.",context:"Project detail page, API calls."},io:{input:xH,output:f,errors:{NOT_FOUND:{description:"Project not found",http:404,gqlCode:"NOT_FOUND",when:"Project ID is invalid or user lacks access"}}},policy:{auth:"user"},acceptance:{scenarios:[{key:"get-project-happy-path",given:["Project exists"],when:["User requests project"],then:["Project details are returned"]}],examples:[{key:"get-existing",input:{projectId:"proj-123"},output:{id:"proj-123",name:"Website Redesign"}}]}}),D3=lH({meta:{key:"saas.project.update",version:"1.0.0",stability:"stable",owners:[...p],tags:["saas","project","update"],description:"Update project details.",goal:"Allow project owners/editors to modify project.",context:"Project settings page."},io:{input:EH,output:f},policy:{auth:"user"},sideEffects:{emits:[{key:"project.updated",version:"1.0.0",when:"Project is updated",payload:f}],audit:["project.updated"]},acceptance:{scenarios:[{key:"update-project-happy-path",given:["Project exists"],when:["User updates description"],then:["Project is updated","ProjectUpdated event is emitted"]}],examples:[{key:"update-desc",input:{projectId:"proj-123",description:"New description"},output:{id:"proj-123",description:"New description"}}]}}),A3=lH({meta:{key:"saas.project.delete",version:"1.0.0",stability:"stable",owners:[...p],tags:["saas","project","delete"],description:"Delete a project (soft delete).",goal:"Allow project owners to remove projects.",context:"Project settings page."},io:{input:mH,output:SH},policy:{auth:"user"},sideEffects:{emits:[{key:"project.deleted",version:"1.0.0",when:"Project is deleted",payload:jH}],audit:["project.deleted"]},acceptance:{scenarios:[{key:"delete-project-happy-path",given:["Project exists"],when:["User deletes project"],then:["Project is deleted","ProjectDeleted event is emitted"]}],examples:[{key:"delete-existing",input:{projectId:"proj-123"},output:{success:!0}}]}}),R3=DG({meta:{key:"saas.project.list",version:"1.0.0",stability:"stable",owners:[...p],tags:["saas","project","list"],description:"List projects in the organization.",goal:"Show all projects user has access to.",context:"Project list page, dashboard."},io:{input:dH,output:uH},policy:{auth:"user"},acceptance:{scenarios:[{key:"list-projects-happy-path",given:["Projects exist"],when:["User lists projects"],then:["List of projects is returned"]}],examples:[{key:"list-all",input:{limit:10},output:{items:[],total:5}}]}});import{definePresentation as AG,StabilityEnum as RG}from"@contractspec/lib.contracts-spec";var _3=AG({meta:{key:"saas.project.list",version:"1.0.0",title:"Project List",description:"List view of projects with status, tags, and last updated info",domain:"saas-boilerplate",owners:["@saas-team"],tags:["project","list","dashboard"],stability:RG.Beta,goal:"Browse and manage projects",context:"Project list page"},source:{type:"component",framework:"react",componentKey:"ProjectListView",props:f},targets:["react","markdown","application/json"],policy:{flags:["saas.projects.enabled"]}}),B3=AG({meta:{key:"saas.project.detail",version:"1.0.0",title:"Project Details",description:"Detailed view of a project with settings and activity",domain:"saas-boilerplate",owners:["@saas-team"],tags:["project","detail"],stability:RG.Beta,goal:"View and edit project details",context:"Project detail page"},source:{type:"component",framework:"react",componentKey:"ProjectDetailView"},targets:["react","markdown"],policy:{flags:["saas.projects.enabled"]}});import{defineVisualization as kH,VisualizationRegistry as I3}from"@contractspec/lib.contracts-spec/visualizations";var YH={key:"saas.project.list",version:"1.0.0"},QH={version:"1.0.0",domain:"saas",stability:"experimental",owners:["@example.saas-boilerplate"],tags:["saas","visualization","projects"]},pH=kH({meta:{...QH,key:"saas-boilerplate.visualization.project-usage",title:"Project Capacity",description:"Current project count against the current plan limit.",goal:"Show usage against the active plan allowance.",context:"SaaS account overview."},source:{primary:YH,resultPath:"data"},visualization:{kind:"metric",measure:"totalProjects",comparisonMeasure:"projectLimit",measures:[{key:"totalProjects",label:"Projects",dataPath:"totalProjects",format:"number"},{key:"projectLimit",label:"Plan Limit",dataPath:"projectLimit",format:"number"}],table:{caption:"Current project count and plan limit."}}}),cH=kH({meta:{...QH,key:"saas-boilerplate.visualization.project-status",title:"Project Status",description:"Distribution of project states.",goal:"Show the mix of active, draft, and archived projects.",context:"Project portfolio overview."},source:{primary:YH,resultPath:"data"},visualization:{kind:"pie",nameDimension:"status",valueMeasure:"projects",dimensions:[{key:"status",label:"Status",dataPath:"status",type:"category"}],measures:[{key:"projects",label:"Projects",dataPath:"projects",format:"number"}],table:{caption:"Project counts by status."}}}),rH=kH({meta:{...QH,key:"saas-boilerplate.visualization.project-tiers",title:"Tier Comparison",description:"Distribution of projects across tiers.",goal:"Compare how the current portfolio is distributed by tier.",context:"Plan and packaging overview."},source:{primary:YH,resultPath:"data"},visualization:{kind:"cartesian",variant:"bar",xDimension:"tier",yMeasures:["projects"],dimensions:[{key:"tier",label:"Tier",dataPath:"tier",type:"category"}],measures:[{key:"projects",label:"Projects",dataPath:"projects",format:"number",color:"#1d4ed8"}],table:{caption:"Project counts by tier."}}}),nH=kH({meta:{...QH,key:"saas-boilerplate.visualization.project-activity",title:"Recent Project Activity",description:"Daily project creation activity.",goal:"Show recent project activity over time.",context:"Project portfolio trend view."},source:{primary:YH,resultPath:"data"},visualization:{kind:"cartesian",variant:"line",xDimension:"day",yMeasures:["projects"],dimensions:[{key:"day",label:"Day",dataPath:"day",type:"time"}],measures:[{key:"projects",label:"Projects",dataPath:"projects",format:"number",color:"#0f766e"}],table:{caption:"Daily project creation counts."}}}),_G=[pH,cH,rH,nH],wX=new I3([..._G]),BG=_G.map((H)=>({key:H.meta.key,version:H.meta.version}));function v3(H){return(H instanceof Date?H:new Date(H)).toISOString().slice(0,10)}function UH(H,G=10){let X=new Map,$=new Map,Q=new Map;for(let U of H){X.set(U.status,(X.get(U.status)??0)+1),$.set(U.tier,($.get(U.tier)??0)+1);let J=v3(U.createdAt);Q.set(J,(Q.get(J)??0)+1)}return[{key:"saas-capacity",spec:pH,data:{data:[{totalProjects:H.length,projectLimit:G}]},title:"Project Capacity",description:"Current project count compared to the active limit.",height:220},{key:"saas-status",spec:cH,data:{data:Array.from(X.entries()).map(([U,J])=>({status:U,projects:J}))},title:"Project Status",description:"Status mix across the current project portfolio.",height:260},{key:"saas-tier",spec:rH,data:{data:Array.from($.entries()).map(([U,J])=>({tier:U,projects:J}))},title:"Tier Comparison",description:"How projects are distributed across tiers."},{key:"saas-activity",spec:nH,data:{data:Array.from(Q.entries()).sort(([U],[J])=>U.localeCompare(J)).map(([U,J])=>({day:U,projects:J}))},title:"Recent Project Activity",description:"Daily project creation activity."}]}import{defineFeature as L3}from"@contractspec/lib.contracts-spec";var EX=L3({meta:{key:"saas-boilerplate",title:"SaaS Boilerplate",description:"SaaS application foundation with projects, billing, and settings",domain:"saas",owners:["@saas-team"],tags:["saas","projects","billing"],stability:"experimental",version:"1.0.0"},operations:[{key:"saas.project.create",version:"1.0.0"},{key:"saas.project.get",version:"1.0.0"},{key:"saas.project.update",version:"1.0.0"},{key:"saas.project.delete",version:"1.0.0"},{key:"saas.project.list",version:"1.0.0"},{key:"saas.billing.subscription.get",version:"1.0.0"},{key:"saas.billing.usage.record",version:"1.0.0"},{key:"saas.billing.usage.summary",version:"1.0.0"},{key:"saas.billing.feature.check",version:"1.0.0"}],events:[{key:"project.created",version:"1.0.0"},{key:"project.updated",version:"1.0.0"},{key:"project.deleted",version:"1.0.0"},{key:"project.archived",version:"1.0.0"},{key:"billing.usage.recorded",version:"1.0.0"},{key:"billing.subscription.changed",version:"1.0.0"},{key:"billing.limit.reached",version:"1.0.0"}],presentations:[{key:"saas.dashboard",version:"1.0.0"},{key:"saas.project.list",version:"1.0.0"},{key:"saas.project.detail",version:"1.0.0"},{key:"saas.billing.subscription",version:"1.0.0"},{key:"saas.billing.usage",version:"1.0.0"},{key:"saas.settings",version:"1.0.0"}],opToPresentation:[{op:{key:"saas.project.list",version:"1.0.0"},pres:{key:"saas.project.list",version:"1.0.0"}},{op:{key:"saas.project.get",version:"1.0.0"},pres:{key:"saas.project.detail",version:"1.0.0"}},{op:{key:"saas.billing.subscription.get",version:"1.0.0"},pres:{key:"saas.billing.subscription",version:"1.0.0"}},{op:{key:"saas.billing.usage.summary",version:"1.0.0"},pres:{key:"saas.billing.usage",version:"1.0.0"}}],presentationsTargets:[{key:"saas.dashboard",version:"1.0.0",targets:["react","markdown"]},{key:"saas.project.list",version:"1.0.0",targets:["react","markdown","application/json"]},{key:"saas.billing.subscription",version:"1.0.0",targets:["react","markdown"]},{key:"saas.billing.usage",version:"1.0.0",targets:["react","markdown"]}],visualizations:BG,capabilities:{requires:[{key:"identity",version:"1.0.0"},{key:"audit-trail",version:"1.0.0"},{key:"notifications",version:"1.0.0"}]},telemetry:[{key:"saas-boilerplate.telemetry",version:"1.0.0"}],jobs:[{key:"saas-boilerplate.job.usage-recording",version:"1.0.0"}],docs:["docs.examples.saas-boilerplate.goal","docs.examples.saas-boilerplate.usage","docs.examples.saas-boilerplate.reference","docs.examples.saas-boilerplate.constraints"]});import{defineEntityEnum as O3}from"@contractspec/lib.schema";var c=O3({name:"SettingsScope",values:["APP","ORG","USER","PROJECT"],schema:"saas_app",description:"Scope of a setting."});import{defineEntity as vG,field as I,index as IG}from"@contractspec/lib.schema";var oH=vG({name:"Settings",description:"Application, organization, or user settings.",schema:"saas_app",map:"settings",fields:{id:I.id(),key:I.string({description:'Setting key (e.g., "theme", "notifications.email")'}),scope:I.enum("SettingsScope"),scopeId:I.string({isOptional:!0,description:"ID of scoped entity (org, user, project)"}),value:I.json({description:"Setting value"}),valueType:I.string({default:'"string"',description:"Type hint for value"}),schema:I.json({isOptional:!0,description:"JSON schema for validation"}),description:I.string({isOptional:!0}),isSecret:I.boolean({default:!1,description:"Whether value should be encrypted"}),createdAt:I.createdAt(),updatedAt:I.updatedAt()},indexes:[IG.unique(["scope","scopeId","key"]),IG.on(["scope","key"])],enums:[c]}),iH=vG({name:"FeatureFlag",description:"Feature flags for progressive rollout.",schema:"saas_app",map:"feature_flag",fields:{id:I.id(),key:I.string({isUnique:!0,description:"Feature flag key"}),name:I.string({description:"Human-readable name"}),description:I.string({isOptional:!0}),enabled:I.boolean({default:!1}),defaultValue:I.boolean({default:!1}),rules:I.json({isOptional:!0,description:"Targeting rules"}),rolloutPercentage:I.int({default:0,description:"Percentage rollout (0-100)"}),createdAt:I.createdAt(),updatedAt:I.updatedAt()}});import{useTemplateRuntime as g3}from"@contractspec/lib.example-shared-ui";import{useCallback as w3,useEffect as P3,useMemo as h3,useState as r}from"react";function n(H={}){let{handlers:G,projectId:X}=g3(),{saas:$}=G,[Q,U]=r(null),[J,Z]=r(null),[k,Y]=r(!0),[K,_]=r(null),[v,z]=r(1),D=w3(async()=>{Y(!0),_(null);try{let[V,q]=await Promise.all([$.listProjects({projectId:X,status:H.status==="all"?void 0:H.status,search:H.search,limit:H.limit??20,offset:(v-1)*(H.limit??20)}),$.getSubscription({projectId:X})]);U({items:V.items,total:V.total}),Z(q)}catch(V){_(V instanceof Error?V:Error("Unknown error"))}finally{Y(!1)}},[$,X,H.status,H.search,H.limit,v]);P3(()=>{D()},[D]);let N=h3(()=>{if(!Q)return null;let V=Q.items;return{total:Q.total,activeCount:V.filter((q)=>q.status==="ACTIVE").length,draftCount:V.filter((q)=>q.status==="DRAFT").length,projectLimit:10,usagePercent:Math.min(Q.total/10*100,100)}},[Q]);return{data:Q,subscription:J,loading:k,error:K,stats:N,page:v,refetch:D,nextPage:()=>z((V)=>V+1),prevPage:()=>v>1&&z((V)=>V-1)}}import{useTemplateRuntime as M3}from"@contractspec/lib.example-shared-ui";import{useCallback as o,useState as aH}from"react";function sH(H={}){let{handlers:G,projectId:X}=M3(),{saas:$}=G,[Q,U]=aH({loading:!1,error:null,data:null}),[J,Z]=aH({loading:!1,error:null,data:null}),[k,Y]=aH({loading:!1,error:null,data:null}),K=o(async(N)=>{U({loading:!0,error:null,data:null});try{let V=await $.createProject(N,{projectId:X,organizationId:"demo-org"});return U({loading:!1,error:null,data:V}),H.onSuccess?.(),V}catch(V){let q=V instanceof Error?V:Error("Failed to create project");return U({loading:!1,error:q,data:null}),H.onError?.(q),null}},[$,X,H]),_=o(async(N)=>{Z({loading:!0,error:null,data:null});try{let V=await $.updateProject(N);return Z({loading:!1,error:null,data:V}),H.onSuccess?.(),V}catch(V){let q=V instanceof Error?V:Error("Failed to update project");return Z({loading:!1,error:q,data:null}),H.onError?.(q),null}},[$,H]),v=o(async(N)=>{Y({loading:!0,error:null,data:null});try{return await $.deleteProject(N),Y({loading:!1,error:null,data:{success:!0}}),H.onSuccess?.(),!0}catch(V){let q=V instanceof Error?V:Error("Failed to delete project");return Y({loading:!1,error:q,data:null}),H.onError?.(q),!1}},[$,H]),z=o(async(N)=>{return _({id:N,status:"ARCHIVED"})},[_]),D=o(async(N)=>{return _({id:N,status:"ACTIVE"})},[_]);return{createProject:K,updateProject:_,deleteProject:v,archiveProject:z,activateProject:D,createState:Q,updateState:J,deleteState:k,isLoading:Q.loading||J.loading||k.loading}}import{Button as LG,Input as C3}from"@contractspec/lib.design-system";import{useState as qH}from"react";import{jsx as C,jsxs as j}from"react/jsx-runtime";var y3=[{value:"FREE",label:"Free"},{value:"PRO",label:"Pro"},{value:"ENTERPRISE",label:"Enterprise"}];function tH({isOpen:H,onClose:G,onSubmit:X,isLoading:$=!1}){let[Q,U]=qH(""),[J,Z]=qH(""),[k,Y]=qH("FREE"),[K,_]=qH(null),v=async(z)=>{if(z.preventDefault(),_(null),!Q.trim()){_("Project name is required");return}try{await X({name:Q.trim(),description:J.trim()||void 0,tier:k}),U(""),Z(""),Y("FREE"),G()}catch(D){_(D instanceof Error?D.message:"Failed to create project")}};if(!H)return null;return j("div",{className:"fixed inset-0 z-50 flex items-center justify-center",children:[C("div",{className:"absolute inset-0 bg-background/80 backdrop-blur-sm",onClick:G,role:"button",tabIndex:0,onKeyDown:(z)=>{if(z.key==="Enter"||z.key===" ")G()},"aria-label":"Close modal"}),j("div",{className:"relative z-10 w-full max-w-md rounded-xl border border-border bg-card p-6 shadow-xl",children:[C("h2",{className:"mb-4 font-semibold text-xl",children:"Create New Project"}),j("form",{onSubmit:v,className:"space-y-4",children:[j("div",{children:[C("label",{htmlFor:"project-name",className:"mb-1 block font-medium text-muted-foreground text-sm",children:"Project Name *"}),C(C3,{id:"project-name",value:Q,onChange:(z)=>U(z.target.value),placeholder:"e.g., My Awesome Project",disabled:$})]}),j("div",{children:[C("label",{htmlFor:"project-description",className:"mb-1 block font-medium text-muted-foreground text-sm",children:"Description"}),C("textarea",{id:"project-description",value:J,onChange:(z)=>Z(z.target.value),placeholder:"Describe what this project is about...",rows:3,disabled:$,className:"w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"})]}),j("div",{children:[C("label",{htmlFor:"project-tier",className:"mb-1 block font-medium text-muted-foreground text-sm",children:"Tier"}),C("select",{id:"project-tier",value:k,onChange:(z)=>Y(z.target.value),disabled:$,className:"h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50",children:y3.map((z)=>C("option",{value:z.value,children:z.label},z.value))})]}),K&&C("div",{className:"rounded-md bg-destructive/10 p-3 text-destructive text-sm",children:K}),j("div",{className:"flex justify-end gap-3 pt-2",children:[C(LG,{type:"button",variant:"ghost",onPress:G,disabled:$,children:"Cancel"}),C(LG,{type:"submit",disabled:$,children:$?"Creating...":"Create Project"})]})]})]})]})}import{Button as b,Input as b3}from"@contractspec/lib.design-system";import{useEffect as f3,useState as FH}from"react";import{jsx as B,jsxs as g}from"react/jsx-runtime";function eH({isOpen:H,project:G,onClose:X,onUpdate:$,onArchive:Q,onActivate:U,onDelete:J,isLoading:Z=!1}){let[k,Y]=FH("menu"),[K,_]=FH(""),[v,z]=FH(""),[D,N]=FH(null),V=()=>{if(Y("menu"),N(null),G)_(G.name),z(G.description??"")},q=()=>{V(),X()};f3(()=>{if(G)_(G.name),z(G.description??"")},[G]);let a=async()=>{if(!G)return;if(N(null),!K.trim()){N("Project name is required");return}try{await $({id:G.id,name:K.trim(),description:v.trim()||void 0}),q()}catch(w){N(w instanceof Error?w.message:"Failed to update project")}},yG=async()=>{if(!G)return;N(null);try{await Q(G.id),q()}catch(w){N(w instanceof Error?w.message:"Failed to archive project")}},bG=async()=>{if(!G)return;N(null);try{await U(G.id),q()}catch(w){N(w instanceof Error?w.message:"Failed to activate project")}},fG=async()=>{if(!G)return;N(null);try{await J(G.id),q()}catch(w){N(w instanceof Error?w.message:"Failed to delete project")}};if(!H||!G)return null;return g("div",{className:"fixed inset-0 z-50 flex items-center justify-center",children:[B("div",{className:"absolute inset-0 bg-background/80 backdrop-blur-sm",onClick:q,role:"button",tabIndex:0,onKeyDown:(w)=>{if(w.key==="Enter"||w.key===" ")q()},"aria-label":"Close modal"}),g("div",{className:"relative z-10 w-full max-w-md rounded-xl border border-border bg-card p-6 shadow-xl",children:[g("div",{className:"mb-4 border-border border-b pb-4",children:[B("h2",{className:"font-semibold text-xl",children:G.name}),g("p",{className:"text-muted-foreground text-sm",children:[G.tier," \xB7 ",G.status]})]}),k==="menu"&&g("div",{className:"space-y-3",children:[g(b,{className:"w-full justify-start",variant:"ghost",onPress:()=>Y("edit"),children:[B("span",{className:"mr-2",children:"\u270F\uFE0F"})," Edit Project"]}),G.status==="ACTIVE"||G.status==="DRAFT"?g(b,{className:"w-full justify-start",variant:"ghost",onPress:()=>Y("archive"),children:[B("span",{className:"mr-2",children:"\uD83D\uDCE6"})," Archive Project"]}):G.status==="ARCHIVED"?g(b,{className:"w-full justify-start",variant:"ghost",onPress:bG,disabled:Z,children:[B("span",{className:"mr-2",children:"\uD83D\uDD04"})," Restore Project"]}):null,g(b,{className:"w-full justify-start text-red-500 hover:text-red-600",variant:"ghost",onPress:()=>Y("delete"),children:[B("span",{className:"mr-2",children:"\uD83D\uDDD1\uFE0F"})," Delete Project"]}),B("div",{className:"border-border border-t pt-3",children:B(b,{className:"w-full",variant:"outline",onPress:q,children:"Close"})})]}),k==="edit"&&g("div",{className:"space-y-4",children:[g("div",{children:[B("label",{htmlFor:"edit-name",className:"mb-1 block font-medium text-muted-foreground text-sm",children:"Project Name *"}),B(b3,{id:"edit-name",value:K,onChange:(w)=>_(w.target.value),disabled:Z})]}),g("div",{children:[B("label",{htmlFor:"edit-description",className:"mb-1 block font-medium text-muted-foreground text-sm",children:"Description"}),B("textarea",{id:"edit-description",value:v,onChange:(w)=>z(w.target.value),rows:3,disabled:Z,className:"w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"})]}),D&&B("div",{className:"rounded-md bg-destructive/10 p-3 text-destructive text-sm",children:D}),g("div",{className:"flex justify-end gap-3 pt-2",children:[B(b,{variant:"ghost",onPress:()=>Y("menu"),disabled:Z,children:"Back"}),B(b,{onPress:a,disabled:Z,children:Z?"Saving...":"Save Changes"})]})]}),k==="archive"&&g("div",{className:"space-y-4",children:[g("p",{className:"text-muted-foreground",children:["Are you sure you want to archive"," ",B("span",{className:"font-medium text-foreground",children:G.name}),"?"]}),B("p",{className:"text-muted-foreground text-sm",children:"Archived projects can be restored later."}),D&&B("div",{className:"rounded-md bg-destructive/10 p-3 text-destructive text-sm",children:D}),g("div",{className:"flex justify-end gap-3 pt-2",children:[B(b,{variant:"ghost",onPress:()=>Y("menu"),disabled:Z,children:"Cancel"}),B(b,{onPress:yG,disabled:Z,children:Z?"Archiving...":"\uD83D\uDCE6 Archive"})]})]}),k==="delete"&&g("div",{className:"space-y-4",children:[g("p",{className:"text-muted-foreground",children:["Are you sure you want to delete"," ",B("span",{className:"font-medium text-foreground",children:G.name}),"?"]}),B("p",{className:"text-destructive text-sm",children:"This action cannot be undone."}),D&&B("div",{className:"rounded-md bg-destructive/10 p-3 text-destructive text-sm",children:D}),g("div",{className:"flex justify-end gap-3 pt-2",children:[B(b,{variant:"ghost",onPress:()=>Y("menu"),disabled:Z,children:"Cancel"}),B(b,{variant:"destructive",onPress:fG,disabled:Z,children:Z?"Deleting...":"\uD83D\uDDD1\uFE0F Delete"})]})]})]})]})}var T3={overlayId:"saas-boilerplate.free-tier",version:"1.0.0",description:"Shows limitations for free tier users",appliesTo:{feature:"saas-boilerplate",tier:"free"},modifications:[{type:"setLimit",field:"projects",max:3,message:"Upgrade to create more projects"},{type:"hideField",field:"advancedSettings",reason:"Pro feature"},{type:"addBadge",position:"header",label:"Free Plan",variant:"default"}]},E3={overlayId:"saas-boilerplate.demo-user",version:"1.0.0",description:"Demo mode for SaaS boilerplate",appliesTo:{feature:"saas-boilerplate",role:"demo"},modifications:[{type:"hideField",field:"billingSection",reason:"Demo users cannot access billing"},{type:"hideField",field:"deleteAccount",reason:"Not available in demo"},{type:"addBadge",position:"header",label:"Demo Mode",variant:"warning"}]},JZ=[T3,E3];var OG=["FREE","PRO","ENTERPRISE"];function x3(H,G){return{status:H.status==="DELETED"?"ARCHIVED":H.status,tier:OG[G%OG.length]??"FREE",createdAt:H.createdAt}}var m3={target:"markdown",render:async(H,G)=>{if(H.source.type!=="component"||H.source.componentKey!=="ProjectListView")throw Error("projectListMarkdownRenderer: not ProjectListView");let X=await l({limit:20,offset:0}),$=X.projects??[],Q=["# Projects","",`**Total**: ${X.total} projects`,""];if($.length===0)Q.push("_No projects found._");else{Q.push("| Status | Project | Description |"),Q.push("|--------|---------|-------------|");for(let U of $){let J=U.status==="ACTIVE"?"\u2705":U.status==="ARCHIVED"?"\uD83D\uDCE6":"\u23F8\uFE0F";Q.push(`| ${J} | **${U.name}** | ${U.description??"-"} |`)}}return{mimeType:"text/markdown",body:Q.join(`
52
+ `)}}},S3={target:"markdown",render:async(H,G)=>{if(H.source.type!=="component"||H.source.componentKey!=="SaasDashboard")throw Error("saasDashboardMarkdownRenderer: not SaasDashboard");let[X,$]=await Promise.all([l({limit:50}),u()]),Q=X.projects??[],U=Q.filter((Y)=>Y.status==="ACTIVE").length,J=Q.filter((Y)=>Y.status==="ARCHIVED").length,Z=UH(Q.map(x3),10),k=["# SaaS Dashboard","","> Organization overview and usage summary","","## Summary","","| Metric | Value |","|--------|-------|",`| Total Projects | ${X.total} |`,`| Active Projects | ${U} |`,`| Archived Projects | ${J} |`,`| Subscription Plan | ${$.planName} |`,`| Subscription Status | ${$.status} |`,""];k.push("## Visualization Overview"),k.push("");for(let Y of Z)k.push(`- **${Y.title}** via \`${Y.spec.meta.key}\``);if(k.push(""),k.push("## Projects"),k.push(""),Q.length===0)k.push("_No projects yet._");else{k.push("| Status | Project | Description |"),k.push("|--------|---------|-------------|");for(let Y of Q.slice(0,10)){let K=Y.status==="ACTIVE"?"\u2705":Y.status==="ARCHIVED"?"\uD83D\uDCE6":"\u23F8\uFE0F";k.push(`| ${K} | **${Y.name}** | ${Y.description??"-"} |`)}if(Q.length>10)k.push(`| ... | ... | _${X.total-10} more projects_ |`)}if(k.push(""),k.push("## Subscription"),k.push(""),k.push(`- **Plan**: ${$.planName}`),k.push(`- **Status**: ${$.status}`),$.currentPeriodEnd)k.push(`- **Period End**: ${new Date($.currentPeriodEnd).toLocaleDateString()}`);return{mimeType:"text/markdown",body:k.join(`
53
+ `)}}},j3={target:"markdown",render:async(H,G)=>{if(H.source.type!=="component"||H.source.componentKey!=="SubscriptionView")throw Error("saasBillingMarkdownRenderer: not SubscriptionView");let X=await u(),$=["# Billing & Subscription","","> Current subscription details and billing information","","## Subscription Details","","| Property | Value |","|----------|-------|",`| Plan | ${X.planName} |`,`| Status | ${X.status} |`,`| ID | ${X.id} |`,`| Period Start | ${new Date(X.currentPeriodStart).toLocaleDateString()} |`,`| Period End | ${new Date(X.currentPeriodEnd).toLocaleDateString()} |`];if($.push(""),$.push("## Plan Limits"),$.push(""),$.push(`- **Projects**: ${X.limits.projects}`),$.push(`- **Users**: ${X.limits.users}`),$.push(""),$.push("## Plan Features"),$.push(""),X.planName.toLowerCase().includes("free"))$.push("- \u2705 Up to 3 projects"),$.push("- \u2705 Basic support"),$.push("- \u274C Priority support"),$.push("- \u274C Advanced analytics");else if(X.planName.toLowerCase().includes("pro"))$.push("- \u2705 Unlimited projects"),$.push("- \u2705 Priority support"),$.push("- \u2705 Advanced analytics"),$.push("- \u274C Custom integrations");else $.push("- \u2705 Unlimited projects"),$.push("- \u2705 Priority support"),$.push("- \u2705 Advanced analytics"),$.push("- \u2705 Custom integrations"),$.push("- \u2705 Dedicated support");return{mimeType:"text/markdown",body:$.join(`
54
+ `)}}};import{Button as d3,EmptyState as u3,EntityCard as l3,ErrorState as p3,LoaderBlock as c3,StatCard as HG,StatCardGroup as r3,StatusChip as n3}from"@contractspec/lib.design-system";import{jsx as y,jsxs as gG}from"react/jsx-runtime";function o3(H){switch(H){case"ACTIVE":return"success";case"DRAFT":return"neutral";case"ARCHIVED":return"danger";default:return"neutral"}}function wG({onProjectClick:H,onCreateProject:G}){let{data:X,loading:$,error:Q,stats:U,refetch:J}=n();if($&&!X)return y(c3,{label:"Loading projects..."});if(Q)return y(p3,{title:"Failed to load projects",description:Q.message,onRetry:J,retryLabel:"Retry"});if(!X?.items.length)return y(u3,{title:"No projects found",description:"Create your first project to get started.",primaryAction:G?y(d3,{onPress:G,children:"Create Project"}):void 0});return gG("div",{className:"space-y-6",children:[U&&gG(r3,{children:[y(HG,{label:"Total Projects",value:U.total.toString()}),y(HG,{label:"Active",value:U.activeCount.toString()}),y(HG,{label:"Draft",value:U.draftCount.toString()})]}),y("div",{className:"grid gap-4 md:grid-cols-2 lg:grid-cols-3",children:X.items.map((Z)=>y(l3,{cardTitle:Z.name,cardSubtitle:Z.tier,meta:y("p",{className:"text-muted-foreground text-sm",children:Z.description}),chips:y(n3,{tone:o3(Z.status),label:Z.status}),footer:y("span",{className:"text-muted-foreground text-xs",children:Z.updatedAt.toLocaleDateString()}),onClick:H?()=>H(Z.id):void 0},Z.id))})]})}import{jsx as a3}from"react/jsx-runtime";var i3={target:"react",render:async(H,G)=>{if(H.source.type!=="component")throw Error("Invalid source type");if(H.source.componentKey!=="SaasProjectListView")throw Error(`Unknown component: ${H.source.componentKey}`);return a3(wG,{})}};import{VisualizationCard as s3,VisualizationGrid as t3}from"@contractspec/lib.design-system";import{jsx as WH,jsxs as PG}from"react/jsx-runtime";function hG({projects:H,projectLimit:G}){let X=UH(H,G);return PG("section",{className:"space-y-3",children:[PG("div",{children:[WH("h3",{className:"font-semibold text-lg",children:"Portfolio Visualizations"}),WH("p",{className:"text-muted-foreground text-sm",children:"Contract-backed charts for project mix, capacity, and activity."})]}),WH(t3,{children:X.map(($)=>WH(s3,{data:$.data,description:$.description,height:$.height,spec:$.spec,title:$.title},$.key))})]})}import{Button as i,EmptyState as e3,EntityCard as H$,ErrorState as G$,LoaderBlock as $$,StatCard as JH,StatCardGroup as X$,StatusChip as MG}from"@contractspec/lib.design-system";import{useCallback as Z$,useState as KH}from"react";import{jsx as F,jsxs as O}from"react/jsx-runtime";function k$(H){switch(H){case"ACTIVE":return"success";case"DRAFT":return"neutral";case"ARCHIVED":return"warning";default:return"neutral"}}function jZ(){let[H,G]=KH("projects"),[X,$]=KH(!1),[Q,U]=KH(null),[J,Z]=KH(!1),{data:k,subscription:Y,loading:K,error:_,stats:v,refetch:z}=n(),D=sH({onSuccess:()=>{z()}}),N=Z$((q)=>{U(q),Z(!0)},[]),V=[{id:"projects",label:"Projects",icon:"\uD83D\uDCC1"},{id:"billing",label:"Billing",icon:"\uD83D\uDCB3"},{id:"settings",label:"Settings",icon:"\u2699\uFE0F"}];if(K&&!k)return F($$,{label:"Loading dashboard..."});if(_)return F(G$,{title:"Failed to load dashboard",description:_.message,onRetry:z,retryLabel:"Retry"});return O("div",{className:"space-y-6",children:[O("div",{className:"flex items-center justify-between",children:[F("h2",{className:"font-bold text-2xl",children:"SaaS Dashboard"}),H==="projects"&&O(i,{onPress:()=>$(!0),children:[F("span",{className:"mr-2",children:"+"})," New Project"]})]}),v&&Y&&O(X$,{children:[F(JH,{label:"Projects",value:v.total.toString()}),F(JH,{label:"Active",value:v.activeCount.toString()}),F(JH,{label:"Draft",value:v.draftCount.toString()}),F(JH,{label:"Plan",value:Y.plan,hint:Y.status})]}),k&&v&&F(hG,{projectLimit:v.projectLimit,projects:k.items}),F("nav",{className:"flex gap-1 rounded-lg bg-muted p-1",role:"tablist",children:V.map((q)=>O("button",{type:"button",role:"tab","aria-selected":H===q.id,onClick:()=>G(q.id),className:`flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2 font-medium text-sm transition-colors ${H===q.id?"bg-background text-foreground shadow-sm":"text-muted-foreground hover:text-foreground"}`,children:[F("span",{children:q.icon}),q.label]},q.id))}),O("div",{className:"min-h-[400px]",role:"tabpanel",children:[H==="projects"&&F(Y$,{data:k,onProjectClick:N}),H==="billing"&&F(Q$,{subscription:Y}),H==="settings"&&F(U$,{})]}),F(tH,{isOpen:X,onClose:()=>$(!1),onSubmit:async(q)=>{await D.createProject(q)},isLoading:D.createState.loading}),F(eH,{isOpen:J,project:Q,onClose:()=>{Z(!1),U(null)},onUpdate:async(q)=>{await D.updateProject(q)},onArchive:async(q)=>{await D.archiveProject(q)},onActivate:async(q)=>{await D.activateProject(q)},onDelete:async(q)=>{await D.deleteProject(q)},isLoading:D.isLoading})]})}function Y$({data:H,onProjectClick:G}){if(!H?.items.length)return F(e3,{title:"No projects yet",description:"Create your first project to get started."});return F("div",{className:"space-y-4",children:F("div",{className:"grid gap-4 md:grid-cols-2 lg:grid-cols-3",children:H.items.map((X)=>F(H$,{cardTitle:X.name,cardSubtitle:X.tier,meta:F("p",{className:"text-muted-foreground text-sm",children:X.description}),chips:F(MG,{tone:k$(X.status),label:X.status}),footer:O("div",{className:"flex w-full items-center justify-between",children:[F("span",{className:"text-muted-foreground text-xs",children:X.updatedAt.toLocaleDateString()}),F(i,{variant:"ghost",size:"sm",onPress:()=>G?.(X),children:"Actions"})]})},X.id))})})}function Q$({subscription:H}){if(!H)return null;return O("div",{className:"space-y-6",children:[O("div",{className:"rounded-xl border border-border bg-card p-6",children:[O("div",{className:"flex items-start justify-between",children:[O("div",{children:[O("h3",{className:"font-semibold text-lg",children:[H.plan," Plan"]}),O("p",{className:"text-muted-foreground text-sm",children:["Current period:"," ",H.currentPeriodStart.toLocaleDateString()," -"," ",H.currentPeriodEnd.toLocaleDateString()]}),O("p",{className:"text-muted-foreground text-sm",children:["Billing cycle: ",H.billingCycle]})]}),F(MG,{tone:"success",label:H.status})]}),O("div",{className:"mt-4 flex gap-3",children:[F(i,{variant:"outline",onPress:()=>alert("Upgrade clicked!"),children:"Upgrade Plan"}),F(i,{variant:"ghost",onPress:()=>alert("Manage Billing clicked!"),children:"Manage Billing"})]})]}),H.cancelAtPeriodEnd&&F("div",{className:"rounded-xl border border-border bg-destructive/10 p-4 text-destructive",children:F("p",{className:"font-medium text-sm",children:"\u26A0\uFE0F Your subscription will be cancelled at the end of the current period."})})]})}function U$(){return F("div",{className:"space-y-6",children:O("div",{className:"rounded-xl border border-border bg-card p-6",children:[F("h3",{className:"mb-4 font-semibold text-lg",children:"Organization Settings"}),O("div",{className:"space-y-4",children:[O("div",{children:[F("label",{htmlFor:"org-name",className:"font-medium text-sm",children:"Organization Name"}),F("input",{id:"org-name",type:"text",defaultValue:"Demo Organization",className:"mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"})]}),O("div",{children:[F("label",{htmlFor:"timezone",className:"font-medium text-sm",children:"Default Timezone"}),O("select",{id:"timezone",className:"mt-1 block w-full rounded-md border border-input bg-background px-3 py-2",children:[F("option",{children:"UTC"}),F("option",{children:"America/New_York"}),F("option",{children:"Europe/London"}),F("option",{children:"Asia/Tokyo"})]})]}),F("div",{className:"pt-2",children:F(i,{onPress:()=>alert("Settings saved!"),children:"Save Settings"})})]})]})})}import{Button as GG}from"@contractspec/lib.design-system";import{useState as CG}from"react";import{jsx as P,jsxs as T}from"react/jsx-runtime";function cZ(){let[H,G]=CG("Demo Organization"),[X,$]=CG("UTC");return T("div",{className:"space-y-6",children:[T("div",{className:"rounded-xl border border-border bg-card p-6",children:[P("h3",{className:"mb-4 font-semibold text-lg",children:"Organization Settings"}),T("div",{className:"space-y-4",children:[T("div",{children:[P("label",{htmlFor:"setting-org-name",className:"block font-medium text-sm",children:"Organization Name"}),P("input",{id:"setting-org-name",type:"text",value:H,onChange:(Q)=>G(Q.target.value),className:"mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"})]}),T("div",{children:[P("label",{htmlFor:"setting-timezone",className:"block font-medium text-sm",children:"Default Timezone"}),T("select",{id:"setting-timezone",value:X,onChange:(Q)=>$(Q.target.value),className:"mt-1 block w-full rounded-md border border-input bg-background px-3 py-2",children:[P("option",{value:"UTC",children:"UTC"}),P("option",{value:"America/New_York",children:"America/New_York"}),P("option",{value:"Europe/London",children:"Europe/London"}),P("option",{value:"Asia/Tokyo",children:"Asia/Tokyo"})]})]})]}),P("div",{className:"mt-6",children:P(GG,{variant:"default",children:"Save Changes"})})]}),T("div",{className:"rounded-xl border border-border bg-card p-6",children:[P("h3",{className:"mb-4 font-semibold text-lg",children:"Notifications"}),P("div",{className:"space-y-3",children:[{label:"Email notifications",defaultChecked:!0},{label:"Usage alerts",defaultChecked:!0},{label:"Weekly digest",defaultChecked:!1}].map((Q)=>T("label",{className:"flex items-center gap-3",children:[P("input",{type:"checkbox",defaultChecked:Q.defaultChecked,className:"h-4 w-4 rounded border-input"}),P("span",{className:"text-sm",children:Q.label})]},Q.label))})]}),T("div",{className:"rounded-xl border border-red-200 bg-red-50 p-6 dark:border-red-900 dark:bg-red-950/20",children:[P("h3",{className:"mb-2 font-semibold text-lg text-red-700 dark:text-red-400",children:"Danger Zone"}),P("p",{className:"mb-4 text-red-600 text-sm dark:text-red-300",children:"These actions are irreversible. Please proceed with caution."}),T("div",{className:"flex gap-3",children:[P(GG,{variant:"secondary",size:"sm",children:"Export Data"}),P(GG,{variant:"secondary",size:"sm",children:"Delete Organization"})]})]})]})}import{identityRbacSchemaContribution as q$}from"@contractspec/lib.identity-rbac";import{jobsSchemaContribution as F$}from"@contractspec/lib.jobs";import{auditTrailSchemaContribution as W$}from"@contractspec/module.audit-trail";import{notificationsSchemaContribution as J$}from"@contractspec/module.notifications";var K$={moduleId:"@contractspec/example.saas-boilerplate",entities:[yH,bH,oH,iH,zH,DH,AH],enums:[GH,c,s]},F0={modules:[q$,F$,W$,J$,K$],provider:"postgresql",outputPath:"./prisma/schema/generated.prisma"};export{sH as useProjectMutations,n as useProjectList,F0 as schemaComposition,JZ as saasOverlays,T3 as saasFreeUserOverlay,E3 as saasDemoOverlay,S3 as saasDashboardMarkdownRenderer,K$ as saasBoilerplateSchemaContribution,j3 as saasBillingMarkdownRenderer,i3 as projectListReactRenderer,m3 as projectListMarkdownRenderer,KG as mockUpdateProjectHandler,ZG as mockRecordUsageHandler,l as mockListProjectsHandler,XG as mockGetUsageSummaryHandler,u as mockGetSubscriptionHandler,WG as mockGetProjectHandler,VG as mockDeleteProjectHandler,JG as mockCreateProjectHandler,kG as mockCheckFeatureAccessHandler,G3 as example,UH as createSaasVisualizationItems,k3 as createSaasHandlers,YG as UsageSummaryModel,gH as UsageRecordedPayloadModel,SG as UsageRecordedEvent,jG as UsageLimitReachedEvent,AH as UsageLimitEntity,oG as UsageDashboardPresentation,EH as UpdateProjectInputModel,D3 as UpdateProjectContract,RH as SubscriptionStatusSchemaEnum,s as SubscriptionStatusEnum,nG as SubscriptionPresentation,vH as SubscriptionModel,zH as SubscriptionEntity,dG as SubscriptionChangedEvent,c as SettingsScopeEnum,aG as SettingsPanelPresentation,oH as SettingsEntity,_G as SaasVisualizationSpecs,wX as SaasVisualizationRegistry,BG as SaasVisualizationRefs,cZ as SaasSettingsPanel,pH as SaasProjectUsageVisualization,rH as SaasProjectTierVisualization,cH as SaasProjectStatusVisualization,wG as SaasProjectList,nH as SaasProjectActivityVisualization,iG as SaasDashboardPresentation,jZ as SaasDashboard,EX as SaasBoilerplateFeature,OH as RecordUsageOutputModel,LH as RecordUsageInputModel,pG as RecordUsageContract,J3 as ProjectUpdatedEvent,$H as ProjectStatusSchemaEnum,fH as ProjectStatusFilterEnum,GH as ProjectStatusEnum,f as ProjectModel,bH as ProjectMemberEntity,_3 as ProjectListPresentation,yH as ProjectEntity,B3 as ProjectDetailPresentation,jH as ProjectDeletedPayloadModel,K3 as ProjectDeletedEvent,W3 as ProjectCreatedEvent,V3 as ProjectArchivedEvent,eH as ProjectActionsModal,uH as ListProjectsOutputModel,dH as ListProjectsInputModel,R3 as ListProjectsContract,PH as GetUsageSummaryOutputModel,wH as GetUsageSummaryInputModel,cG as GetUsageSummaryContract,lG as GetSubscriptionContract,xH as GetProjectInputModel,z3 as GetProjectContract,iH as FeatureFlagEntity,_H as FeatureAccessReasonEnum,SH as DeleteProjectOutputModel,mH as DeleteProjectInputModel,A3 as DeleteProjectContract,tH as CreateProjectModal,TH as CreateProjectInputModel,N3 as CreateProjectContract,MH as CheckFeatureAccessOutputModel,hH as CheckFeatureAccessInputModel,rG as CheckFeatureAccessContract,DH as BillingUsageEntity};