@contractspec/example.saas-boilerplate 1.56.1 → 1.58.0

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