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