@lukoweb/apitogo-plugin-dev-portal-billing 0.1.86 → 0.1.88

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/dist/index.d.mts CHANGED
@@ -6,6 +6,32 @@ import * as _tanstack_react_query0 from "@tanstack/react-query";
6
6
  declare const AccountPlansPage: () => react_jsx_runtime0.JSX.Element;
7
7
  //#endregion
8
8
  //#region src/types.d.ts
9
+ type DevPortalPricingCompareRow = {
10
+ label: string;
11
+ values: string[];
12
+ };
13
+ type DevPortalPricingFaq = {
14
+ question: string;
15
+ answer: string;
16
+ };
17
+ type DevPortalBillingConfig = {
18
+ pricing?: {
19
+ title?: string;
20
+ subtitle?: string;
21
+ compareRows?: DevPortalPricingCompareRow[];
22
+ faqs?: DevPortalPricingFaq[];
23
+ }; /** Local preview plan catalog (from `apitogo.config.json` `plans`). */
24
+ previewPlans?: Array<{
25
+ sku: string;
26
+ displayName: string;
27
+ priceAmount?: number;
28
+ priceCurrency?: string;
29
+ billingPeriod?: string;
30
+ limitsJson?: string | null;
31
+ includedActionKeys?: string[];
32
+ features?: string[];
33
+ }>;
34
+ };
9
35
  type DevPortalApiKey = {
10
36
  id: string;
11
37
  name: string;
@@ -28,21 +54,6 @@ type DevPortalApiKeyListResponse = {
28
54
  header: string;
29
55
  gatewayBaseUrl: string | null;
30
56
  };
31
- type DevPortalBillingConfig = {
32
- pricing?: {
33
- title?: string;
34
- subtitle?: string;
35
- }; /** Local preview plan catalog (from `apitogo.config.json` `plans`). */
36
- previewPlans?: Array<{
37
- sku: string;
38
- displayName: string;
39
- priceAmount?: number;
40
- priceCurrency?: string;
41
- billingPeriod?: string;
42
- limitsJson?: string | null;
43
- includedActionKeys?: string[];
44
- }>;
45
- };
46
57
  //#endregion
47
58
  //#region src/components/ApiKeysPanel.d.ts
48
59
  /**
package/dist/index.mjs CHANGED
@@ -39,7 +39,8 @@ function mapPreviewPlans(plans) {
39
39
  priceCurrency: plan.priceCurrency,
40
40
  billingPeriod: plan.billingPeriod,
41
41
  limitsJson: plan.limitsJson,
42
- includedActionKeys: plan.includedActionKeys
42
+ includedActionKeys: plan.includedActionKeys,
43
+ features: plan.features
43
44
  }));
44
45
  }
45
46
  function readModulePlanItems() {
@@ -54,7 +55,8 @@ function parseManifestPlans(configPlans) {
54
55
  priceCurrency: plan.priceCurrency ?? "usd",
55
56
  billingPeriod: plan.billingPeriod ?? "monthly",
56
57
  limitsJson: plan.limitsJson ?? null,
57
- includedActionKeys: plan.includedActionKeys ?? []
58
+ includedActionKeys: plan.includedActionKeys ?? [],
59
+ features: plan.features
58
60
  }))) };
59
61
  if (manifestCatalog?.plans?.length) return { plans: mapPreviewPlans(manifestCatalog.plans) };
60
62
  const metaPlans = zudokuConfig?.__meta?.previewPlans;
@@ -67,7 +69,8 @@ function parseManifestPlans(configPlans) {
67
69
  priceCurrency: plan.priceCurrency ?? "usd",
68
70
  billingPeriod: plan.billingPeriod ?? "monthly",
69
71
  limitsJson: plan.limitsJson ?? null,
70
- includedActionKeys: plan.includedActionKeys ?? []
72
+ includedActionKeys: plan.includedActionKeys ?? [],
73
+ features: plan.features
71
74
  }))) };
72
75
  return { plans: [] };
73
76
  }
@@ -77,6 +80,7 @@ function formatActionKey(actionKey) {
77
80
  return `${actionKey.slice(0, separatorIndex).toUpperCase()} ${actionKey.slice(separatorIndex + 1)}`;
78
81
  }
79
82
  function formatPlanPrice(plan) {
83
+ if (plan.priceAmount < 0) return "Custom";
80
84
  if (plan.priceAmount <= 0) return "Free";
81
85
  const amount = new Intl.NumberFormat(void 0, {
82
86
  style: "currency",
@@ -89,33 +93,63 @@ function formatPlanPrice(plan) {
89
93
  return amount;
90
94
  }
91
95
  function isPlanFree(plan) {
92
- return plan.priceAmount <= 0;
96
+ return plan.priceAmount === 0;
93
97
  }
94
98
  function formatBillingPeriod(period) {
95
99
  const normalized = period.toLowerCase();
96
- if (normalized === "month" || normalized === "monthly") return "Monthly";
97
- if (normalized === "year" || normalized === "annual") return "Annual";
100
+ if (normalized === "month" || normalized === "monthly") return "per month";
101
+ if (normalized === "year" || normalized === "annual") return "per year";
102
+ if (normalized === "custom") return "talk to us";
98
103
  return period;
99
104
  }
100
105
  function formatRateLimit(rate) {
101
106
  if (!rate || typeof rate !== "object") return null;
102
107
  const { calls, renewalPeriodSeconds } = rate;
103
108
  if (calls == null || renewalPeriodSeconds == null) return null;
109
+ if (renewalPeriodSeconds === 1) return `${calls} req/sec rate limit`;
104
110
  const window = renewalPeriodSeconds === 60 ? "minute" : `${renewalPeriodSeconds} seconds`;
105
111
  return `${calls} request${calls === 1 ? "" : "s"} per ${window}`;
106
112
  }
107
- function summarizeLimits(limitsJson) {
108
- if (!limitsJson?.trim()) return null;
109
- try {
110
- const limits = JSON.parse(limitsJson);
111
- const rateSummary = formatRateLimit(limits.rateLimit);
112
- if (rateSummary) return rateSummary;
113
- const entries = Object.entries(limits).filter(([, value]) => value != null && typeof value !== "object");
114
- if (entries.length === 0) return null;
115
- return entries.slice(0, 3).map(([key, value]) => `${key}: ${String(value)}`).join(" · ");
116
- } catch {
117
- return null;
113
+ function formatQuota(quota) {
114
+ if (!quota || typeof quota !== "object") return null;
115
+ const { calls, renewalPeriodDays } = quota;
116
+ if (calls == null) return null;
117
+ if (calls < 0) return "Unlimited requests";
118
+ const formatted = calls.toLocaleString();
119
+ if (renewalPeriodDays === 30 || renewalPeriodDays === 31) return `${formatted} requests / month`;
120
+ if (renewalPeriodDays === 1) return `${formatted} requests / day`;
121
+ if (renewalPeriodDays != null) return `${formatted} requests / ${renewalPeriodDays} days`;
122
+ return `${formatted} requests`;
123
+ }
124
+ /** Marketing bullets keyed by common sample SKUs when limitsJson is sparse. */
125
+ const SAMPLE_PLAN_FEATURE_EXTRAS = {
126
+ free: ["All endpoints", "Community support"],
127
+ developer: ["Webhooks included", "Email support"],
128
+ team: ["5 team seats", "Priority support"],
129
+ enterprise: [
130
+ "Unlimited requests",
131
+ "Custom rate limits",
132
+ "SSO & audit logs",
133
+ "Dedicated SLA"
134
+ ]
135
+ };
136
+ function listPlanFeatureLines(plan) {
137
+ if (plan.features && plan.features.length > 0) return plan.features;
138
+ const lines = [];
139
+ if (plan.limitsJson?.trim()) try {
140
+ const limits = JSON.parse(plan.limitsJson);
141
+ const quotaLine = formatQuota(limits.quota);
142
+ if (quotaLine) lines.push(quotaLine);
143
+ const rateLine = formatRateLimit(limits.rateLimit);
144
+ if (rateLine) lines.push(rateLine);
145
+ } catch {}
146
+ const extras = SAMPLE_PLAN_FEATURE_EXTRAS[plan.sku.toLowerCase()];
147
+ if (extras) {
148
+ for (const extra of extras) if (!lines.includes(extra)) lines.push(extra);
118
149
  }
150
+ if (plan.includedActionKeys && plan.includedActionKeys.length > 0) for (const actionKey of plan.includedActionKeys) lines.push(formatActionKey(actionKey));
151
+ if (lines.length === 0 && isPlanFree(plan)) lines.push("API access not included");
152
+ return lines;
119
153
  }
120
154
  function isActiveSubscription(status) {
121
155
  return status.toLowerCase() === "active";
@@ -123,55 +157,49 @@ function isActiveSubscription(status) {
123
157
 
124
158
  //#endregion
125
159
  //#region src/components/PricingCard.tsx
126
- const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, isCanceling = false, cancelDisabled = false }) => {
127
- const limitsSummary = summarizeLimits(plan.limitsJson);
160
+ const publicCtaLabel = (plan) => {
161
+ if (plan.priceAmount < 0) return "Contact sales";
162
+ if (isPlanFree(plan)) return "Start for free";
163
+ return "Start free trial";
164
+ };
165
+ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, isCanceling = false, cancelDisabled = false, featured = false }) => {
128
166
  const isActive = isSubscribed || isActiveSubscription(subscriptionStatus ?? "");
167
+ const features = listPlanFeatureLines(plan);
168
+ const priceLabel = formatPlanPrice(plan);
169
+ const isCustom = plan.priceAmount < 0;
170
+ const displayPrice = isCustom ? "Custom" : isPlanFree(plan) ? "$0" : priceLabel.replace(/\/mo$/, "").replace(/\/yr$/, "") || priceLabel;
129
171
  return /* @__PURE__ */ jsxs("article", {
130
- className: cn("flex w-full max-w-sm flex-col rounded-xl border bg-card/60 p-6 shadow-xs", isActive && mode === "account" && "border-primary/40 ring-1 ring-primary/20"),
172
+ className: cn("flex h-full w-full flex-col rounded-xl border border-border bg-card p-7", featured && "border-primary/50 shadow-[0_10px_28px_rgba(13,148,136,0.28)] ring-1 ring-primary/25", isActive && mode === "account" && "border-primary/40 ring-1 ring-primary/20"),
131
173
  "data-testid": `pricing-card-${plan.sku}`,
132
174
  children: [
133
175
  /* @__PURE__ */ jsxs("div", {
134
- className: "flex items-start justify-between gap-2",
135
- children: [/* @__PURE__ */ jsx("h2", {
136
- className: "text-xl font-semibold",
137
- children: plan.displayName
138
- }), isActive && mode === "account" ? /* @__PURE__ */ jsx("span", {
139
- className: "shrink-0 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary",
140
- "data-testid": `subscribed-badge-${plan.sku}`,
141
- children: "Subscribed"
142
- }) : null]
176
+ className: "flex items-center justify-between gap-2",
177
+ children: [
178
+ /* @__PURE__ */ jsx("h2", {
179
+ className: "text-sm font-bold uppercase tracking-[0.04em]",
180
+ children: plan.displayName
181
+ }),
182
+ featured && !isActive ? /* @__PURE__ */ jsx("span", {
183
+ className: "shrink-0 rounded-md bg-primary/10 px-2.5 py-0.5 text-xs font-semibold text-primary",
184
+ children: "Popular"
185
+ }) : null,
186
+ isActive && mode === "account" ? /* @__PURE__ */ jsx("span", {
187
+ className: "shrink-0 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary",
188
+ "data-testid": `subscribed-badge-${plan.sku}`,
189
+ children: "Subscribed"
190
+ }) : null
191
+ ]
143
192
  }),
144
193
  /* @__PURE__ */ jsx("p", {
145
- className: "mt-2 text-3xl font-bold",
146
- children: formatPlanPrice(plan)
194
+ className: "mt-3.5 text-[2.1rem] font-extrabold tracking-[-0.02em]",
195
+ children: displayPrice
147
196
  }),
148
197
  /* @__PURE__ */ jsx("p", {
149
- className: "mt-1 text-sm text-muted-foreground",
150
- children: formatBillingPeriod(plan.billingPeriod)
198
+ className: "mb-[18px] mt-0.5 text-[0.78125rem] text-muted-foreground",
199
+ children: isCustom ? "talk to us" : formatBillingPeriod(plan.billingPeriod)
151
200
  }),
152
- limitsSummary ? /* @__PURE__ */ jsx("p", {
153
- className: "mt-4 text-sm text-muted-foreground",
154
- children: limitsSummary
155
- }) : null,
156
- plan.includedActionKeys && plan.includedActionKeys.length > 0 ? /* @__PURE__ */ jsxs("div", {
157
- className: "mt-4",
158
- "data-testid": `included-endpoints-${plan.sku}`,
159
- children: [/* @__PURE__ */ jsx("p", {
160
- className: "text-xs font-medium uppercase tracking-wide text-muted-foreground",
161
- children: "Included endpoints"
162
- }), /* @__PURE__ */ jsx("ul", {
163
- className: "mt-2 space-y-1 text-sm text-muted-foreground",
164
- children: plan.includedActionKeys.map((actionKey) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", {
165
- className: "rounded bg-muted px-1.5 py-0.5 text-xs",
166
- children: formatActionKey(actionKey)
167
- }) }, actionKey))
168
- })]
169
- }) : isPlanFree(plan) ? /* @__PURE__ */ jsx("p", {
170
- className: "mt-4 text-sm text-muted-foreground",
171
- children: "API access not included"
172
- }) : null,
173
201
  /* @__PURE__ */ jsx("div", {
174
- className: "mt-6 space-y-2",
202
+ className: "space-y-2",
175
203
  children: mode === "account" ? isActive ? /* @__PURE__ */ jsxs(Fragment, { children: [
176
204
  /* @__PURE__ */ jsx(Button, {
177
205
  variant: "secondary",
@@ -202,7 +230,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
202
230
  children: "Unsubscribe is available after the portal is published."
203
231
  }) : null
204
232
  ] }) : /* @__PURE__ */ jsx(Button, {
205
- variant: "default",
233
+ variant: featured ? "default" : "outline",
206
234
  className: "w-full",
207
235
  asChild: true,
208
236
  children: /* @__PURE__ */ jsx(Link, {
@@ -218,15 +246,27 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
218
246
  children: "Manage subscription"
219
247
  })
220
248
  }) : /* @__PURE__ */ jsx(Button, {
221
- variant: "default",
249
+ variant: featured ? "default" : "outline",
222
250
  className: "w-full",
223
251
  asChild: true,
224
252
  children: /* @__PURE__ */ jsx(Link, {
225
253
  to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
226
- children: isPlanFree(plan) ? "Get started" : "Subscribe"
254
+ children: publicCtaLabel(plan)
227
255
  })
228
256
  })
229
- })
257
+ }),
258
+ features.length > 0 ? /* @__PURE__ */ jsx("div", {
259
+ className: "mt-5 flex flex-1 flex-col gap-[11px] border-t border-border pt-[18px]",
260
+ "data-testid": plan.includedActionKeys && plan.includedActionKeys.length > 0 ? `included-endpoints-${plan.sku}` : void 0,
261
+ children: features.map((feat) => /* @__PURE__ */ jsxs("div", {
262
+ className: "flex items-start gap-2.5 text-[0.84375rem] text-muted-foreground",
263
+ children: [/* @__PURE__ */ jsx("span", {
264
+ className: "mt-0.5 shrink-0 text-emerald-600",
265
+ "aria-hidden": true,
266
+ children: "✓"
267
+ }), /* @__PURE__ */ jsx("span", { children: feat })]
268
+ }, feat))
269
+ }) : null
230
270
  ]
231
271
  });
232
272
  };
@@ -1127,6 +1167,69 @@ function isPreviewPricingEnabled() {
1127
1167
 
1128
1168
  //#endregion
1129
1169
  //#region src/pages/PricingPage.tsx
1170
+ const DEFAULT_COMPARE_ROWS = [
1171
+ {
1172
+ label: "Monthly requests",
1173
+ values: [
1174
+ "1,000",
1175
+ "50,000",
1176
+ "500,000",
1177
+ "Unlimited"
1178
+ ]
1179
+ },
1180
+ {
1181
+ label: "Rate limit",
1182
+ values: [
1183
+ "10/sec",
1184
+ "50/sec",
1185
+ "200/sec",
1186
+ "Custom"
1187
+ ]
1188
+ },
1189
+ {
1190
+ label: "Team seats",
1191
+ values: [
1192
+ "1",
1193
+ "1",
1194
+ "5",
1195
+ "Unlimited"
1196
+ ]
1197
+ },
1198
+ {
1199
+ label: "Webhooks",
1200
+ values: [
1201
+ "—",
1202
+ "Included",
1203
+ "Included",
1204
+ "Included"
1205
+ ]
1206
+ },
1207
+ {
1208
+ label: "SSO",
1209
+ values: [
1210
+ "—",
1211
+ "—",
1212
+ "—",
1213
+ "Included"
1214
+ ]
1215
+ },
1216
+ {
1217
+ label: "Support",
1218
+ values: [
1219
+ "Community",
1220
+ "Email",
1221
+ "Priority",
1222
+ "Dedicated SLA"
1223
+ ]
1224
+ }
1225
+ ];
1226
+ const DEFAULT_FAQS = [{
1227
+ question: "What happens if I exceed my quota?",
1228
+ answer: "Requests over your monthly quota return a 429 response so you can test rate-limit handling instead of being cut off unexpectedly."
1229
+ }, {
1230
+ question: "Can I change plans later?",
1231
+ answer: "Yes, upgrade or downgrade any time from your billing page. Changes apply on your next billing cycle."
1232
+ }];
1130
1233
  const PricingPage = () => {
1131
1234
  const { pricing, previewPlans } = useDevPortalBillingConfig();
1132
1235
  const auth = useAuth();
@@ -1146,62 +1249,134 @@ const PricingPage = () => {
1146
1249
  replace: true
1147
1250
  });
1148
1251
  const activeSkus = new Set((me?.subscriptions ?? []).filter((sub) => isActiveSubscription(sub.status)).map((sub) => sub.planSku));
1149
- const title = pricing?.title ?? "Pricing";
1150
- const subtitle = pricing?.subtitle ?? "Compare plans and subscribe when you are ready to integrate.";
1252
+ const title = pricing?.title ?? "Pay for what you call";
1253
+ const subtitle = pricing?.subtitle ?? "Every plan includes the full endpoint catalog. Higher tiers raise your request quota, rate limit, and support level.";
1254
+ const plans = catalog?.plans ?? [];
1255
+ const featuredSku = plans.find((p) => p.sku === "developer")?.sku ?? plans.find((p) => p.priceAmount > 0 && p.priceAmount < 50)?.sku ?? plans.find((p) => p.priceAmount > 0)?.sku;
1256
+ const compareRows = pricing?.compareRows && pricing.compareRows.length > 0 ? pricing.compareRows : DEFAULT_COMPARE_ROWS;
1257
+ const faqs = pricing?.faqs && pricing.faqs.length > 0 ? pricing.faqs : DEFAULT_FAQS;
1258
+ const compareHeaders = plans.length > 0 ? plans.map((p) => p.displayName) : [
1259
+ "Free",
1260
+ "Developer",
1261
+ "Team",
1262
+ "Enterprise"
1263
+ ];
1151
1264
  return /* @__PURE__ */ jsxs("div", {
1152
- className: "w-full px-4 pt-(--padding-content-top) pb-(--padding-content-bottom)",
1265
+ className: "w-full",
1153
1266
  children: [
1154
1267
  /* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: title }), /* @__PURE__ */ jsx("meta", {
1155
1268
  name: "description",
1156
1269
  content: subtitle
1157
1270
  })] }),
1158
- /* @__PURE__ */ jsxs("div", {
1159
- className: "mb-12 space-y-4 text-center",
1160
- children: [/* @__PURE__ */ jsx(Heading, {
1161
- level: 1,
1162
- "data-testid": "title",
1163
- children: title
1164
- }), /* @__PURE__ */ jsx("p", {
1165
- className: "text-muted-foreground",
1166
- "data-testid": "subtitle",
1167
- children: subtitle
1168
- })]
1169
- }),
1170
- isLoading ? /* @__PURE__ */ jsx("p", {
1171
- className: "text-center text-muted-foreground",
1172
- children: "Loading plans…"
1173
- }) : null,
1174
- isError ? /* @__PURE__ */ jsx("p", {
1175
- className: "text-center text-destructive",
1176
- children: "Unable to load plans."
1177
- }) : null,
1178
- catalog && catalog.plans.length > 0 ? /* @__PURE__ */ jsx("div", {
1179
- className: "grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]",
1180
- children: catalog.plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
1181
- plan,
1182
- mode: "public",
1183
- isSubscribed: activeSkus.has(plan.sku)
1184
- }, plan.sku))
1185
- }) : !isLoading && !isError ? /* @__PURE__ */ jsxs("div", {
1186
- className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
1271
+ /* @__PURE__ */ jsxs("section", {
1272
+ className: "px-6 pb-4 pt-[clamp(3rem,6vw,4.5rem)] text-center",
1187
1273
  children: [
1188
- "No plans are configured yet. Add plans in",
1189
- " ",
1190
- /* @__PURE__ */ jsx("code", {
1191
- className: "rounded bg-muted px-1.5 py-0.5",
1192
- children: "apitogo.config.json"
1274
+ /* @__PURE__ */ jsx("p", {
1275
+ className: "text-xs font-bold uppercase tracking-[0.08em] text-primary",
1276
+ children: "Pricing"
1193
1277
  }),
1194
- " ",
1195
- "and set",
1196
- " ",
1197
- /* @__PURE__ */ jsx("code", {
1198
- className: "rounded bg-muted px-1.5 py-0.5",
1199
- children: "modules.userPanel.plans.enabled"
1278
+ /* @__PURE__ */ jsx(Heading, {
1279
+ level: 1,
1280
+ className: "mt-3 text-[clamp(2rem,3.5vw,3rem)] font-extrabold tracking-[-0.02em]",
1281
+ "data-testid": "title",
1282
+ children: title
1200
1283
  }),
1201
- " ",
1202
- "to true."
1284
+ /* @__PURE__ */ jsx("p", {
1285
+ className: "mx-auto mt-3.5 max-w-[34rem] text-lg leading-relaxed text-muted-foreground",
1286
+ "data-testid": "subtitle",
1287
+ children: subtitle
1288
+ })
1203
1289
  ]
1204
- }) : null
1290
+ }),
1291
+ /* @__PURE__ */ jsxs("section", {
1292
+ className: "px-6 pb-16 pt-8",
1293
+ children: [
1294
+ isLoading ? /* @__PURE__ */ jsx("p", {
1295
+ className: "text-center text-muted-foreground",
1296
+ children: "Loading plans…"
1297
+ }) : null,
1298
+ isError ? /* @__PURE__ */ jsx("p", {
1299
+ className: "text-center text-destructive",
1300
+ children: "Unable to load plans."
1301
+ }) : null,
1302
+ plans.length > 0 ? /* @__PURE__ */ jsx("div", {
1303
+ className: "mx-auto grid max-w-[1200px] grid-cols-1 items-stretch gap-5 sm:grid-cols-2 lg:grid-cols-4",
1304
+ children: plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
1305
+ plan,
1306
+ mode: "public",
1307
+ isSubscribed: activeSkus.has(plan.sku),
1308
+ featured: plan.sku === featuredSku
1309
+ }, plan.sku))
1310
+ }) : !isLoading && !isError ? /* @__PURE__ */ jsxs("div", {
1311
+ className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
1312
+ children: [
1313
+ "No plans are configured yet. Add plans in",
1314
+ " ",
1315
+ /* @__PURE__ */ jsx("code", {
1316
+ className: "rounded bg-muted px-1.5 py-0.5",
1317
+ children: "apitogo.config.json"
1318
+ }),
1319
+ " ",
1320
+ "and set",
1321
+ " ",
1322
+ /* @__PURE__ */ jsx("code", {
1323
+ className: "rounded bg-muted px-1.5 py-0.5",
1324
+ children: "modules.userPanel.plans.enabled"
1325
+ }),
1326
+ " ",
1327
+ "to true."
1328
+ ]
1329
+ }) : null
1330
+ ]
1331
+ }),
1332
+ /* @__PURE__ */ jsx("section", {
1333
+ className: "border-t border-border bg-muted/40 px-6 py-16",
1334
+ "data-testid": "pricing-compare",
1335
+ children: /* @__PURE__ */ jsxs("div", {
1336
+ className: "mx-auto max-w-[1200px]",
1337
+ children: [/* @__PURE__ */ jsx("h2", {
1338
+ className: "mb-6 text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
1339
+ children: "Compare plans"
1340
+ }), /* @__PURE__ */ jsx("div", {
1341
+ className: "overflow-x-auto rounded-xl border border-border bg-card",
1342
+ children: /* @__PURE__ */ jsxs("div", {
1343
+ className: "min-w-[720px]",
1344
+ children: [/* @__PURE__ */ jsxs("div", {
1345
+ className: "grid gap-0 border-b border-border px-5 py-3.5 text-[0.78125rem] font-bold uppercase tracking-[0.04em] text-muted-foreground",
1346
+ style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
1347
+ children: [/* @__PURE__ */ jsx("div", { children: "Feature" }), compareHeaders.map((header) => /* @__PURE__ */ jsx("div", { children: header }, header))]
1348
+ }), compareRows.map((row) => /* @__PURE__ */ jsxs("div", {
1349
+ className: "grid gap-0 border-b border-border px-5 py-3.5 text-[0.84375rem] text-muted-foreground last:border-b-0",
1350
+ style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
1351
+ children: [/* @__PURE__ */ jsx("div", {
1352
+ className: "font-medium text-foreground",
1353
+ children: row.label
1354
+ }), compareHeaders.map((_, index) => /* @__PURE__ */ jsx("div", { children: row.values[index] ?? "—" }, `${row.label}-${index}`))]
1355
+ }, row.label))]
1356
+ })
1357
+ })]
1358
+ })
1359
+ }),
1360
+ /* @__PURE__ */ jsx("section", {
1361
+ className: "px-6 py-16",
1362
+ "data-testid": "pricing-faq",
1363
+ children: /* @__PURE__ */ jsxs("div", {
1364
+ className: "mx-auto max-w-4xl",
1365
+ children: [/* @__PURE__ */ jsx("h2", {
1366
+ className: "mb-8 text-center text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
1367
+ children: "Frequently asked questions"
1368
+ }), /* @__PURE__ */ jsx("div", { children: faqs.map((faq) => /* @__PURE__ */ jsxs("div", {
1369
+ className: "border-b border-border py-5 last:border-b-0",
1370
+ children: [/* @__PURE__ */ jsx("div", {
1371
+ className: "text-[0.9375rem] font-bold",
1372
+ children: faq.question
1373
+ }), /* @__PURE__ */ jsx("p", {
1374
+ className: "mt-2 text-sm leading-relaxed text-muted-foreground",
1375
+ children: faq.answer
1376
+ })]
1377
+ }, faq.question)) })]
1378
+ })
1379
+ })
1205
1380
  ]
1206
1381
  });
1207
1382
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.1.86",
3
+ "version": "0.1.88",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -46,7 +46,7 @@
46
46
  "react": "19.2.4",
47
47
  "react-dom": "19.2.4",
48
48
  "tsdown": "0.20.3",
49
- "@lukoweb/apitogo": "0.1.86"
49
+ "@lukoweb/apitogo": "0.1.88"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": ">=19.2.0",