@nubitio/admin 0.5.23 → 0.5.26
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.cjs +190 -0
- package/dist/index.d.cts +95 -1
- package/dist/index.d.mts +95 -1
- package/dist/index.mjs +187 -3
- package/dist/style.css +145 -0
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -481,6 +481,190 @@ function FeatureGate({ featureKey, ...props }) {
|
|
|
481
481
|
});
|
|
482
482
|
}
|
|
483
483
|
//#endregion
|
|
484
|
+
//#region packages/admin/quota/QuotaUsageBanner.tsx
|
|
485
|
+
function QuotaUsageBanner({ count, max, unitLabel, planLabel, atLimitMessage, nearLimitMessage, defaultMessage, upgradeHref, upgradeLabel = "View plans", className = "" }) {
|
|
486
|
+
if (max <= 0) return null;
|
|
487
|
+
const atLimit = count >= max;
|
|
488
|
+
const nearLimit = count === max - 1;
|
|
489
|
+
const tone = atLimit ? "limit" : nearLimit ? "warn" : "default";
|
|
490
|
+
const detail = atLimit ? atLimitMessage ?? `You reached your ${planLabel ?? "plan"} limit.` : nearLimit ? nearLimitMessage ?? `Only 1 ${unitLabel} slot left on your ${planLabel ?? "plan"}.` : defaultMessage ?? `${planLabel ?? "Plan"} usage.`;
|
|
491
|
+
const upgradeLink = upgradeHref ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
492
|
+
className: "nb-quota-banner__cta",
|
|
493
|
+
href: upgradeHref,
|
|
494
|
+
children: upgradeLabel
|
|
495
|
+
}) : null;
|
|
496
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
497
|
+
className: `nb-quota-banner nb-quota-banner--${tone}${className ? ` ${className}` : ""}`,
|
|
498
|
+
role: "status",
|
|
499
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
500
|
+
className: "nb-quota-banner__copy",
|
|
501
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [
|
|
502
|
+
count,
|
|
503
|
+
" / ",
|
|
504
|
+
max,
|
|
505
|
+
" ",
|
|
506
|
+
unitLabel
|
|
507
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: detail })]
|
|
508
|
+
}), (atLimit || nearLimit) && upgradeLink]
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function quotaUsageAboveGrid(banner) {
|
|
512
|
+
return () => banner;
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region packages/admin/billing/PlanPanel.tsx
|
|
516
|
+
function formatTrialEnds(iso) {
|
|
517
|
+
const date = new Date(iso);
|
|
518
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
519
|
+
return new Intl.DateTimeFormat(void 0, { dateStyle: "medium" }).format(date);
|
|
520
|
+
}
|
|
521
|
+
function PlanPanel({ plans, currentPlanId, subscriptionStatus, trialEndsAt, onSelectPlan, selectingPlanId = null, labels, className = "" }) {
|
|
522
|
+
const title = labels?.title ?? "Plans";
|
|
523
|
+
const subtitle = labels?.subtitle ?? "Choose the plan that fits your team.";
|
|
524
|
+
const currentBadge = labels?.currentBadge ?? "Current plan";
|
|
525
|
+
const selectingLabel = labels?.selecting ?? "Updating…";
|
|
526
|
+
let statusLine = null;
|
|
527
|
+
if (subscriptionStatus === "trialing" && trialEndsAt) statusLine = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
528
|
+
className: "nb-plan-panel__status",
|
|
529
|
+
children: (labels?.trialEnds ?? "Trial ends {date}").replace("{date}", formatTrialEnds(trialEndsAt))
|
|
530
|
+
});
|
|
531
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
532
|
+
className: `nb-plan-panel${className ? ` ${className}` : ""}`,
|
|
533
|
+
"aria-labelledby": "plan-panel-title",
|
|
534
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
|
|
535
|
+
className: "nb-plan-panel__header",
|
|
536
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
538
|
+
id: "plan-panel-title",
|
|
539
|
+
className: "nb-plan-panel__title",
|
|
540
|
+
children: title
|
|
541
|
+
}),
|
|
542
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
543
|
+
className: "nb-plan-panel__subtitle",
|
|
544
|
+
children: subtitle
|
|
545
|
+
}),
|
|
546
|
+
statusLine
|
|
547
|
+
] })
|
|
548
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
549
|
+
className: "nb-plan-panel__grid",
|
|
550
|
+
role: "list",
|
|
551
|
+
children: plans.map((plan) => {
|
|
552
|
+
const isCurrent = plan.id === currentPlanId;
|
|
553
|
+
const isSelecting = selectingPlanId === plan.id;
|
|
554
|
+
const ctaLabel = plan.ctaLabel ?? (isCurrent ? currentBadge : `Choose ${plan.name}`);
|
|
555
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
556
|
+
role: "listitem",
|
|
557
|
+
className: [
|
|
558
|
+
"nb-plan-card",
|
|
559
|
+
plan.highlighted ? "nb-plan-card--highlighted" : "",
|
|
560
|
+
isCurrent ? "nb-plan-card--current" : ""
|
|
561
|
+
].filter(Boolean).join(" "),
|
|
562
|
+
children: [
|
|
563
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
564
|
+
className: "nb-plan-card__head",
|
|
565
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
566
|
+
className: "nb-plan-card__name",
|
|
567
|
+
children: plan.name
|
|
568
|
+
}), isCurrent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Badge, {
|
|
569
|
+
variant: "primary",
|
|
570
|
+
size: "sm",
|
|
571
|
+
pill: true,
|
|
572
|
+
children: currentBadge
|
|
573
|
+
}) : null]
|
|
574
|
+
}),
|
|
575
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
576
|
+
className: "nb-plan-card__price",
|
|
577
|
+
children: plan.priceLabel
|
|
578
|
+
}),
|
|
579
|
+
plan.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
580
|
+
className: "nb-plan-card__description",
|
|
581
|
+
children: plan.description
|
|
582
|
+
}) : null,
|
|
583
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
584
|
+
className: "nb-plan-card__features",
|
|
585
|
+
children: plan.features.map((feature) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: feature }, feature))
|
|
586
|
+
}),
|
|
587
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Button, {
|
|
588
|
+
variant: plan.highlighted && !isCurrent ? "primary" : "secondary",
|
|
589
|
+
size: "sm",
|
|
590
|
+
disabled: isCurrent || !onSelectPlan || isSelecting,
|
|
591
|
+
onClick: () => void onSelectPlan?.(plan.id),
|
|
592
|
+
children: isSelecting ? selectingLabel : ctaLabel
|
|
593
|
+
})
|
|
594
|
+
]
|
|
595
|
+
}, plan.id);
|
|
596
|
+
})
|
|
597
|
+
})]
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region packages/admin/quota/parseQuotaError.ts
|
|
602
|
+
function readErrorDetail(error) {
|
|
603
|
+
if (typeof error !== "object" || error === null) return "";
|
|
604
|
+
const record = error;
|
|
605
|
+
return record.data?.detail ?? record.data?.message ?? record.message ?? "";
|
|
606
|
+
}
|
|
607
|
+
function parseQuotaError(error) {
|
|
608
|
+
if (typeof error !== "object" || error === null) return null;
|
|
609
|
+
if (error.status !== 429) return null;
|
|
610
|
+
const detail = readErrorDetail(error);
|
|
611
|
+
const match = detail.match(/Quota exceeded for "([^"]+)": (\d+)\/(\d+)/);
|
|
612
|
+
if (!match) return {
|
|
613
|
+
resource: "unknown",
|
|
614
|
+
current: 0,
|
|
615
|
+
limit: 0,
|
|
616
|
+
message: detail || "Plan limit reached."
|
|
617
|
+
};
|
|
618
|
+
return {
|
|
619
|
+
resource: match[1],
|
|
620
|
+
current: Number(match[2]),
|
|
621
|
+
limit: Number(match[3]),
|
|
622
|
+
message: detail
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function quotaErrorToastMessage(quota, labels) {
|
|
626
|
+
const label = labels?.[quota.resource] ?? quota.resource;
|
|
627
|
+
return `Plan limit reached: ${quota.current}/${quota.limit} ${label}. Upgrade your plan to continue.`;
|
|
628
|
+
}
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region packages/admin/quota/useQuotaUsage.ts
|
|
631
|
+
function readCollectionTotal(body) {
|
|
632
|
+
if (Array.isArray(body)) return body.length;
|
|
633
|
+
if (typeof body !== "object" || body === null) return 0;
|
|
634
|
+
const record = body;
|
|
635
|
+
const member = record["hydra:member"] ?? record["member"];
|
|
636
|
+
const listed = Array.isArray(member) ? member.length : 0;
|
|
637
|
+
return Number(record["hydra:totalItems"] ?? record["totalItems"] ?? listed);
|
|
638
|
+
}
|
|
639
|
+
function useQuotaUsage({ featureKey, collectionPath, staleTimeMs = 1e4 }) {
|
|
640
|
+
const http = (0, _nubitio_core.useCoreHttpClient)();
|
|
641
|
+
const config = useFeatureConfig(featureKey);
|
|
642
|
+
const max = Number(config.max ?? 0);
|
|
643
|
+
const query = (0, _tanstack_react_query.useQuery)({
|
|
644
|
+
queryKey: [
|
|
645
|
+
"quota-usage",
|
|
646
|
+
featureKey,
|
|
647
|
+
collectionPath
|
|
648
|
+
],
|
|
649
|
+
queryFn: async () => {
|
|
650
|
+
return readCollectionTotal((await http.get(`${collectionPath}?itemsPerPage=100`, { headers: { Accept: "application/ld+json" } })).data);
|
|
651
|
+
},
|
|
652
|
+
staleTime: staleTimeMs
|
|
653
|
+
});
|
|
654
|
+
const count = query.data ?? 0;
|
|
655
|
+
const atLimit = max > 0 && count >= max;
|
|
656
|
+
const nearLimit = max > 0 && count === max - 1;
|
|
657
|
+
const refetch = (0, react.useCallback)(() => query.refetch(), [query]);
|
|
658
|
+
return {
|
|
659
|
+
count,
|
|
660
|
+
max,
|
|
661
|
+
atLimit,
|
|
662
|
+
nearLimit,
|
|
663
|
+
loading: query.isLoading,
|
|
664
|
+
refetch
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
//#endregion
|
|
484
668
|
//#region packages/admin/auth/LoginPage.tsx
|
|
485
669
|
function joinApiPath$2(apiBaseUrl, path) {
|
|
486
670
|
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
@@ -961,6 +1145,8 @@ exports.AdminShell = AdminShell;
|
|
|
961
1145
|
exports.AdminSidebarMenu = AdminSidebarMenu;
|
|
962
1146
|
exports.FeatureGate = FeatureGate;
|
|
963
1147
|
exports.LoginPage = LoginPage;
|
|
1148
|
+
exports.PlanPanel = PlanPanel;
|
|
1149
|
+
exports.QuotaUsageBanner = QuotaUsageBanner;
|
|
964
1150
|
exports.RegisterPage = RegisterPage;
|
|
965
1151
|
exports.SessionProvider = SessionProvider;
|
|
966
1152
|
exports.StaticSessionProvider = StaticSessionProvider;
|
|
@@ -968,9 +1154,13 @@ exports.ToastHost = ToastHost;
|
|
|
968
1154
|
exports.createNubitApp = createNubitApp;
|
|
969
1155
|
exports.filterMenuByRoles = filterMenuByRoles;
|
|
970
1156
|
exports.hasAnyRole = hasAnyRole;
|
|
1157
|
+
exports.parseQuotaError = parseQuotaError;
|
|
1158
|
+
exports.quotaErrorToastMessage = quotaErrorToastMessage;
|
|
1159
|
+
exports.quotaUsageAboveGrid = quotaUsageAboveGrid;
|
|
971
1160
|
exports.useAppRuntime = useAppRuntime;
|
|
972
1161
|
exports.useFeature = useFeature;
|
|
973
1162
|
exports.useFeatureConfig = useFeatureConfig;
|
|
1163
|
+
exports.useQuotaUsage = useQuotaUsage;
|
|
974
1164
|
exports.useRuntimeConfig = useRuntimeConfig;
|
|
975
1165
|
exports.useScreenSize = useScreenSize;
|
|
976
1166
|
exports.useScreenSizeClass = useScreenSizeClass;
|
package/dist/index.d.cts
CHANGED
|
@@ -168,6 +168,100 @@ declare function FeatureGate({
|
|
|
168
168
|
...props
|
|
169
169
|
}: FeatureGateProps): React$1.JSX.Element;
|
|
170
170
|
//#endregion
|
|
171
|
+
//#region packages/admin/quota/QuotaUsageBanner.d.ts
|
|
172
|
+
interface QuotaUsageBannerProps {
|
|
173
|
+
count: number;
|
|
174
|
+
max: number;
|
|
175
|
+
unitLabel: string;
|
|
176
|
+
planLabel?: string;
|
|
177
|
+
atLimitMessage?: string;
|
|
178
|
+
nearLimitMessage?: string;
|
|
179
|
+
defaultMessage?: string;
|
|
180
|
+
upgradeHref?: string;
|
|
181
|
+
upgradeLabel?: string;
|
|
182
|
+
className?: string;
|
|
183
|
+
}
|
|
184
|
+
declare function QuotaUsageBanner({
|
|
185
|
+
count,
|
|
186
|
+
max,
|
|
187
|
+
unitLabel,
|
|
188
|
+
planLabel,
|
|
189
|
+
atLimitMessage,
|
|
190
|
+
nearLimitMessage,
|
|
191
|
+
defaultMessage,
|
|
192
|
+
upgradeHref,
|
|
193
|
+
upgradeLabel,
|
|
194
|
+
className
|
|
195
|
+
}: QuotaUsageBannerProps): import("react").JSX.Element | null;
|
|
196
|
+
declare function quotaUsageAboveGrid(banner: ReactNode): () => ReactNode;
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region packages/admin/billing/PlanPanel.d.ts
|
|
199
|
+
interface PlanDefinition {
|
|
200
|
+
id: string;
|
|
201
|
+
name: string;
|
|
202
|
+
priceLabel: string;
|
|
203
|
+
description?: string;
|
|
204
|
+
features: string[];
|
|
205
|
+
highlighted?: boolean;
|
|
206
|
+
ctaLabel?: string;
|
|
207
|
+
}
|
|
208
|
+
interface PlanPanelLabels {
|
|
209
|
+
title?: string;
|
|
210
|
+
subtitle?: string;
|
|
211
|
+
currentBadge?: string;
|
|
212
|
+
trialEnds?: string;
|
|
213
|
+
selecting?: string;
|
|
214
|
+
}
|
|
215
|
+
interface PlanPanelProps {
|
|
216
|
+
plans: PlanDefinition[];
|
|
217
|
+
currentPlanId: string;
|
|
218
|
+
subscriptionStatus?: string;
|
|
219
|
+
trialEndsAt?: string | null;
|
|
220
|
+
onSelectPlan?: (planId: string) => void | Promise<void>;
|
|
221
|
+
selectingPlanId?: string | null;
|
|
222
|
+
labels?: PlanPanelLabels;
|
|
223
|
+
className?: string;
|
|
224
|
+
}
|
|
225
|
+
declare function PlanPanel({
|
|
226
|
+
plans,
|
|
227
|
+
currentPlanId,
|
|
228
|
+
subscriptionStatus,
|
|
229
|
+
trialEndsAt,
|
|
230
|
+
onSelectPlan,
|
|
231
|
+
selectingPlanId,
|
|
232
|
+
labels,
|
|
233
|
+
className
|
|
234
|
+
}: PlanPanelProps): import("react").JSX.Element;
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region packages/admin/quota/parseQuotaError.d.ts
|
|
237
|
+
interface ParsedQuotaError {
|
|
238
|
+
resource: string;
|
|
239
|
+
current: number;
|
|
240
|
+
limit: number;
|
|
241
|
+
message: string;
|
|
242
|
+
}
|
|
243
|
+
declare function parseQuotaError(error: unknown): ParsedQuotaError | null;
|
|
244
|
+
declare function quotaErrorToastMessage(quota: ParsedQuotaError, labels?: Partial<Record<string, string>>): string;
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region packages/admin/quota/useQuotaUsage.d.ts
|
|
247
|
+
interface UseQuotaUsageOptions {
|
|
248
|
+
featureKey: string;
|
|
249
|
+
collectionPath: string;
|
|
250
|
+
staleTimeMs?: number;
|
|
251
|
+
}
|
|
252
|
+
declare function useQuotaUsage({
|
|
253
|
+
featureKey,
|
|
254
|
+
collectionPath,
|
|
255
|
+
staleTimeMs
|
|
256
|
+
}: UseQuotaUsageOptions): {
|
|
257
|
+
count: number;
|
|
258
|
+
max: number;
|
|
259
|
+
atLimit: boolean;
|
|
260
|
+
nearLimit: boolean;
|
|
261
|
+
loading: boolean;
|
|
262
|
+
refetch: () => Promise<import("@tanstack/react-query").QueryObserverResult<number, Error>>;
|
|
263
|
+
};
|
|
264
|
+
//#endregion
|
|
171
265
|
//#region packages/admin/auth/LoginPage.d.ts
|
|
172
266
|
interface LoginPageProps {
|
|
173
267
|
onLoggedIn: () => void;
|
|
@@ -350,4 +444,4 @@ declare function hasAnyRole(required: string | string[] | undefined, roles: stri
|
|
|
350
444
|
*/
|
|
351
445
|
declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
|
|
352
446
|
//#endregion
|
|
353
|
-
export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, FeatureGate, type FeatureGateProps, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type RegisterField, type RegisterFieldType, RegisterPage, type RegisterPageProps, type RegisterSelectOption, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, StaticSessionProvider, ToastHost, type ToastHostProps, type ToastItem, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useFeature, useFeatureConfig, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|
|
447
|
+
export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, FeatureGate, type FeatureGateProps, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type ParsedQuotaError, type PlanDefinition, PlanPanel, type PlanPanelLabels, type PlanPanelProps, QuotaUsageBanner, type QuotaUsageBannerProps, type RegisterField, type RegisterFieldType, RegisterPage, type RegisterPageProps, type RegisterSelectOption, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, StaticSessionProvider, ToastHost, type ToastHostProps, type ToastItem, type UseQuotaUsageOptions, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|
package/dist/index.d.mts
CHANGED
|
@@ -168,6 +168,100 @@ declare function FeatureGate({
|
|
|
168
168
|
...props
|
|
169
169
|
}: FeatureGateProps): React$1.JSX.Element;
|
|
170
170
|
//#endregion
|
|
171
|
+
//#region packages/admin/quota/QuotaUsageBanner.d.ts
|
|
172
|
+
interface QuotaUsageBannerProps {
|
|
173
|
+
count: number;
|
|
174
|
+
max: number;
|
|
175
|
+
unitLabel: string;
|
|
176
|
+
planLabel?: string;
|
|
177
|
+
atLimitMessage?: string;
|
|
178
|
+
nearLimitMessage?: string;
|
|
179
|
+
defaultMessage?: string;
|
|
180
|
+
upgradeHref?: string;
|
|
181
|
+
upgradeLabel?: string;
|
|
182
|
+
className?: string;
|
|
183
|
+
}
|
|
184
|
+
declare function QuotaUsageBanner({
|
|
185
|
+
count,
|
|
186
|
+
max,
|
|
187
|
+
unitLabel,
|
|
188
|
+
planLabel,
|
|
189
|
+
atLimitMessage,
|
|
190
|
+
nearLimitMessage,
|
|
191
|
+
defaultMessage,
|
|
192
|
+
upgradeHref,
|
|
193
|
+
upgradeLabel,
|
|
194
|
+
className
|
|
195
|
+
}: QuotaUsageBannerProps): import("react").JSX.Element | null;
|
|
196
|
+
declare function quotaUsageAboveGrid(banner: ReactNode): () => ReactNode;
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region packages/admin/billing/PlanPanel.d.ts
|
|
199
|
+
interface PlanDefinition {
|
|
200
|
+
id: string;
|
|
201
|
+
name: string;
|
|
202
|
+
priceLabel: string;
|
|
203
|
+
description?: string;
|
|
204
|
+
features: string[];
|
|
205
|
+
highlighted?: boolean;
|
|
206
|
+
ctaLabel?: string;
|
|
207
|
+
}
|
|
208
|
+
interface PlanPanelLabels {
|
|
209
|
+
title?: string;
|
|
210
|
+
subtitle?: string;
|
|
211
|
+
currentBadge?: string;
|
|
212
|
+
trialEnds?: string;
|
|
213
|
+
selecting?: string;
|
|
214
|
+
}
|
|
215
|
+
interface PlanPanelProps {
|
|
216
|
+
plans: PlanDefinition[];
|
|
217
|
+
currentPlanId: string;
|
|
218
|
+
subscriptionStatus?: string;
|
|
219
|
+
trialEndsAt?: string | null;
|
|
220
|
+
onSelectPlan?: (planId: string) => void | Promise<void>;
|
|
221
|
+
selectingPlanId?: string | null;
|
|
222
|
+
labels?: PlanPanelLabels;
|
|
223
|
+
className?: string;
|
|
224
|
+
}
|
|
225
|
+
declare function PlanPanel({
|
|
226
|
+
plans,
|
|
227
|
+
currentPlanId,
|
|
228
|
+
subscriptionStatus,
|
|
229
|
+
trialEndsAt,
|
|
230
|
+
onSelectPlan,
|
|
231
|
+
selectingPlanId,
|
|
232
|
+
labels,
|
|
233
|
+
className
|
|
234
|
+
}: PlanPanelProps): import("react").JSX.Element;
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region packages/admin/quota/parseQuotaError.d.ts
|
|
237
|
+
interface ParsedQuotaError {
|
|
238
|
+
resource: string;
|
|
239
|
+
current: number;
|
|
240
|
+
limit: number;
|
|
241
|
+
message: string;
|
|
242
|
+
}
|
|
243
|
+
declare function parseQuotaError(error: unknown): ParsedQuotaError | null;
|
|
244
|
+
declare function quotaErrorToastMessage(quota: ParsedQuotaError, labels?: Partial<Record<string, string>>): string;
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region packages/admin/quota/useQuotaUsage.d.ts
|
|
247
|
+
interface UseQuotaUsageOptions {
|
|
248
|
+
featureKey: string;
|
|
249
|
+
collectionPath: string;
|
|
250
|
+
staleTimeMs?: number;
|
|
251
|
+
}
|
|
252
|
+
declare function useQuotaUsage({
|
|
253
|
+
featureKey,
|
|
254
|
+
collectionPath,
|
|
255
|
+
staleTimeMs
|
|
256
|
+
}: UseQuotaUsageOptions): {
|
|
257
|
+
count: number;
|
|
258
|
+
max: number;
|
|
259
|
+
atLimit: boolean;
|
|
260
|
+
nearLimit: boolean;
|
|
261
|
+
loading: boolean;
|
|
262
|
+
refetch: () => Promise<import("@tanstack/react-query").QueryObserverResult<number, Error>>;
|
|
263
|
+
};
|
|
264
|
+
//#endregion
|
|
171
265
|
//#region packages/admin/auth/LoginPage.d.ts
|
|
172
266
|
interface LoginPageProps {
|
|
173
267
|
onLoggedIn: () => void;
|
|
@@ -350,4 +444,4 @@ declare function hasAnyRole(required: string | string[] | undefined, roles: stri
|
|
|
350
444
|
*/
|
|
351
445
|
declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
|
|
352
446
|
//#endregion
|
|
353
|
-
export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, FeatureGate, type FeatureGateProps, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type RegisterField, type RegisterFieldType, RegisterPage, type RegisterPageProps, type RegisterSelectOption, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, StaticSessionProvider, ToastHost, type ToastHostProps, type ToastItem, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useFeature, useFeatureConfig, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|
|
447
|
+
export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, FeatureGate, type FeatureGateProps, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type ParsedQuotaError, type PlanDefinition, PlanPanel, type PlanPanelLabels, type PlanPanelProps, QuotaUsageBanner, type QuotaUsageBannerProps, type RegisterField, type RegisterFieldType, RegisterPage, type RegisterPageProps, type RegisterSelectOption, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, StaticSessionProvider, ToastHost, type ToastHostProps, type ToastItem, type UseQuotaUsageOptions, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|
package/dist/index.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS
|
|
|
2
2
|
import { BrowserRouter, Link, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
|
3
3
|
import { Badge, Button, Card, FeatureGate as FeatureGate$1, IconButton, TextField, ThemeProvider, ThemeSwitcher, useFloatingPanel } from "@nubitio/ui";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
6
|
-
import { CoreConfigProvider, CoreProvider, MercureProvider } from "@nubitio/core";
|
|
5
|
+
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
|
|
6
|
+
import { CoreConfigProvider, CoreProvider, MercureProvider, useCoreHttpClient } from "@nubitio/core";
|
|
7
7
|
import { SmartCrudRolesProvider } from "@nubitio/crud";
|
|
8
8
|
import { HydraResourceSchemaProvider, HydraResourceStoreProvider, SchemaProvider } from "@nubitio/hydra";
|
|
9
9
|
//#region packages/admin/AdminHeader.tsx
|
|
@@ -457,6 +457,190 @@ function FeatureGate({ featureKey, ...props }) {
|
|
|
457
457
|
});
|
|
458
458
|
}
|
|
459
459
|
//#endregion
|
|
460
|
+
//#region packages/admin/quota/QuotaUsageBanner.tsx
|
|
461
|
+
function QuotaUsageBanner({ count, max, unitLabel, planLabel, atLimitMessage, nearLimitMessage, defaultMessage, upgradeHref, upgradeLabel = "View plans", className = "" }) {
|
|
462
|
+
if (max <= 0) return null;
|
|
463
|
+
const atLimit = count >= max;
|
|
464
|
+
const nearLimit = count === max - 1;
|
|
465
|
+
const tone = atLimit ? "limit" : nearLimit ? "warn" : "default";
|
|
466
|
+
const detail = atLimit ? atLimitMessage ?? `You reached your ${planLabel ?? "plan"} limit.` : nearLimit ? nearLimitMessage ?? `Only 1 ${unitLabel} slot left on your ${planLabel ?? "plan"}.` : defaultMessage ?? `${planLabel ?? "Plan"} usage.`;
|
|
467
|
+
const upgradeLink = upgradeHref ? /* @__PURE__ */ jsx("a", {
|
|
468
|
+
className: "nb-quota-banner__cta",
|
|
469
|
+
href: upgradeHref,
|
|
470
|
+
children: upgradeLabel
|
|
471
|
+
}) : null;
|
|
472
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
473
|
+
className: `nb-quota-banner nb-quota-banner--${tone}${className ? ` ${className}` : ""}`,
|
|
474
|
+
role: "status",
|
|
475
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
476
|
+
className: "nb-quota-banner__copy",
|
|
477
|
+
children: [/* @__PURE__ */ jsxs("strong", { children: [
|
|
478
|
+
count,
|
|
479
|
+
" / ",
|
|
480
|
+
max,
|
|
481
|
+
" ",
|
|
482
|
+
unitLabel
|
|
483
|
+
] }), /* @__PURE__ */ jsx("span", { children: detail })]
|
|
484
|
+
}), (atLimit || nearLimit) && upgradeLink]
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
function quotaUsageAboveGrid(banner) {
|
|
488
|
+
return () => banner;
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region packages/admin/billing/PlanPanel.tsx
|
|
492
|
+
function formatTrialEnds(iso) {
|
|
493
|
+
const date = new Date(iso);
|
|
494
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
495
|
+
return new Intl.DateTimeFormat(void 0, { dateStyle: "medium" }).format(date);
|
|
496
|
+
}
|
|
497
|
+
function PlanPanel({ plans, currentPlanId, subscriptionStatus, trialEndsAt, onSelectPlan, selectingPlanId = null, labels, className = "" }) {
|
|
498
|
+
const title = labels?.title ?? "Plans";
|
|
499
|
+
const subtitle = labels?.subtitle ?? "Choose the plan that fits your team.";
|
|
500
|
+
const currentBadge = labels?.currentBadge ?? "Current plan";
|
|
501
|
+
const selectingLabel = labels?.selecting ?? "Updating…";
|
|
502
|
+
let statusLine = null;
|
|
503
|
+
if (subscriptionStatus === "trialing" && trialEndsAt) statusLine = /* @__PURE__ */ jsx("p", {
|
|
504
|
+
className: "nb-plan-panel__status",
|
|
505
|
+
children: (labels?.trialEnds ?? "Trial ends {date}").replace("{date}", formatTrialEnds(trialEndsAt))
|
|
506
|
+
});
|
|
507
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
508
|
+
className: `nb-plan-panel${className ? ` ${className}` : ""}`,
|
|
509
|
+
"aria-labelledby": "plan-panel-title",
|
|
510
|
+
children: [/* @__PURE__ */ jsx("header", {
|
|
511
|
+
className: "nb-plan-panel__header",
|
|
512
|
+
children: /* @__PURE__ */ jsxs("div", { children: [
|
|
513
|
+
/* @__PURE__ */ jsx("h2", {
|
|
514
|
+
id: "plan-panel-title",
|
|
515
|
+
className: "nb-plan-panel__title",
|
|
516
|
+
children: title
|
|
517
|
+
}),
|
|
518
|
+
/* @__PURE__ */ jsx("p", {
|
|
519
|
+
className: "nb-plan-panel__subtitle",
|
|
520
|
+
children: subtitle
|
|
521
|
+
}),
|
|
522
|
+
statusLine
|
|
523
|
+
] })
|
|
524
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
525
|
+
className: "nb-plan-panel__grid",
|
|
526
|
+
role: "list",
|
|
527
|
+
children: plans.map((plan) => {
|
|
528
|
+
const isCurrent = plan.id === currentPlanId;
|
|
529
|
+
const isSelecting = selectingPlanId === plan.id;
|
|
530
|
+
const ctaLabel = plan.ctaLabel ?? (isCurrent ? currentBadge : `Choose ${plan.name}`);
|
|
531
|
+
return /* @__PURE__ */ jsxs("article", {
|
|
532
|
+
role: "listitem",
|
|
533
|
+
className: [
|
|
534
|
+
"nb-plan-card",
|
|
535
|
+
plan.highlighted ? "nb-plan-card--highlighted" : "",
|
|
536
|
+
isCurrent ? "nb-plan-card--current" : ""
|
|
537
|
+
].filter(Boolean).join(" "),
|
|
538
|
+
children: [
|
|
539
|
+
/* @__PURE__ */ jsxs("div", {
|
|
540
|
+
className: "nb-plan-card__head",
|
|
541
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
542
|
+
className: "nb-plan-card__name",
|
|
543
|
+
children: plan.name
|
|
544
|
+
}), isCurrent ? /* @__PURE__ */ jsx(Badge, {
|
|
545
|
+
variant: "primary",
|
|
546
|
+
size: "sm",
|
|
547
|
+
pill: true,
|
|
548
|
+
children: currentBadge
|
|
549
|
+
}) : null]
|
|
550
|
+
}),
|
|
551
|
+
/* @__PURE__ */ jsx("p", {
|
|
552
|
+
className: "nb-plan-card__price",
|
|
553
|
+
children: plan.priceLabel
|
|
554
|
+
}),
|
|
555
|
+
plan.description ? /* @__PURE__ */ jsx("p", {
|
|
556
|
+
className: "nb-plan-card__description",
|
|
557
|
+
children: plan.description
|
|
558
|
+
}) : null,
|
|
559
|
+
/* @__PURE__ */ jsx("ul", {
|
|
560
|
+
className: "nb-plan-card__features",
|
|
561
|
+
children: plan.features.map((feature) => /* @__PURE__ */ jsx("li", { children: feature }, feature))
|
|
562
|
+
}),
|
|
563
|
+
/* @__PURE__ */ jsx(Button, {
|
|
564
|
+
variant: plan.highlighted && !isCurrent ? "primary" : "secondary",
|
|
565
|
+
size: "sm",
|
|
566
|
+
disabled: isCurrent || !onSelectPlan || isSelecting,
|
|
567
|
+
onClick: () => void onSelectPlan?.(plan.id),
|
|
568
|
+
children: isSelecting ? selectingLabel : ctaLabel
|
|
569
|
+
})
|
|
570
|
+
]
|
|
571
|
+
}, plan.id);
|
|
572
|
+
})
|
|
573
|
+
})]
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region packages/admin/quota/parseQuotaError.ts
|
|
578
|
+
function readErrorDetail(error) {
|
|
579
|
+
if (typeof error !== "object" || error === null) return "";
|
|
580
|
+
const record = error;
|
|
581
|
+
return record.data?.detail ?? record.data?.message ?? record.message ?? "";
|
|
582
|
+
}
|
|
583
|
+
function parseQuotaError(error) {
|
|
584
|
+
if (typeof error !== "object" || error === null) return null;
|
|
585
|
+
if (error.status !== 429) return null;
|
|
586
|
+
const detail = readErrorDetail(error);
|
|
587
|
+
const match = detail.match(/Quota exceeded for "([^"]+)": (\d+)\/(\d+)/);
|
|
588
|
+
if (!match) return {
|
|
589
|
+
resource: "unknown",
|
|
590
|
+
current: 0,
|
|
591
|
+
limit: 0,
|
|
592
|
+
message: detail || "Plan limit reached."
|
|
593
|
+
};
|
|
594
|
+
return {
|
|
595
|
+
resource: match[1],
|
|
596
|
+
current: Number(match[2]),
|
|
597
|
+
limit: Number(match[3]),
|
|
598
|
+
message: detail
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function quotaErrorToastMessage(quota, labels) {
|
|
602
|
+
const label = labels?.[quota.resource] ?? quota.resource;
|
|
603
|
+
return `Plan limit reached: ${quota.current}/${quota.limit} ${label}. Upgrade your plan to continue.`;
|
|
604
|
+
}
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region packages/admin/quota/useQuotaUsage.ts
|
|
607
|
+
function readCollectionTotal(body) {
|
|
608
|
+
if (Array.isArray(body)) return body.length;
|
|
609
|
+
if (typeof body !== "object" || body === null) return 0;
|
|
610
|
+
const record = body;
|
|
611
|
+
const member = record["hydra:member"] ?? record["member"];
|
|
612
|
+
const listed = Array.isArray(member) ? member.length : 0;
|
|
613
|
+
return Number(record["hydra:totalItems"] ?? record["totalItems"] ?? listed);
|
|
614
|
+
}
|
|
615
|
+
function useQuotaUsage({ featureKey, collectionPath, staleTimeMs = 1e4 }) {
|
|
616
|
+
const http = useCoreHttpClient();
|
|
617
|
+
const config = useFeatureConfig(featureKey);
|
|
618
|
+
const max = Number(config.max ?? 0);
|
|
619
|
+
const query = useQuery({
|
|
620
|
+
queryKey: [
|
|
621
|
+
"quota-usage",
|
|
622
|
+
featureKey,
|
|
623
|
+
collectionPath
|
|
624
|
+
],
|
|
625
|
+
queryFn: async () => {
|
|
626
|
+
return readCollectionTotal((await http.get(`${collectionPath}?itemsPerPage=100`, { headers: { Accept: "application/ld+json" } })).data);
|
|
627
|
+
},
|
|
628
|
+
staleTime: staleTimeMs
|
|
629
|
+
});
|
|
630
|
+
const count = query.data ?? 0;
|
|
631
|
+
const atLimit = max > 0 && count >= max;
|
|
632
|
+
const nearLimit = max > 0 && count === max - 1;
|
|
633
|
+
const refetch = useCallback(() => query.refetch(), [query]);
|
|
634
|
+
return {
|
|
635
|
+
count,
|
|
636
|
+
max,
|
|
637
|
+
atLimit,
|
|
638
|
+
nearLimit,
|
|
639
|
+
loading: query.isLoading,
|
|
640
|
+
refetch
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
460
644
|
//#region packages/admin/auth/LoginPage.tsx
|
|
461
645
|
function joinApiPath$2(apiBaseUrl, path) {
|
|
462
646
|
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
@@ -932,4 +1116,4 @@ function createNubitApp(config) {
|
|
|
932
1116
|
return { App };
|
|
933
1117
|
}
|
|
934
1118
|
//#endregion
|
|
935
|
-
export { AdminHeader, AdminShell, AdminSidebarMenu, FeatureGate, LoginPage, RegisterPage, SessionProvider, StaticSessionProvider, ToastHost, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useFeature, useFeatureConfig, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|
|
1119
|
+
export { AdminHeader, AdminShell, AdminSidebarMenu, FeatureGate, LoginPage, PlanPanel, QuotaUsageBanner, RegisterPage, SessionProvider, StaticSessionProvider, ToastHost, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
|
package/dist/style.css
CHANGED
|
@@ -369,6 +369,151 @@
|
|
|
369
369
|
opacity: 0;
|
|
370
370
|
pointer-events: none;
|
|
371
371
|
}
|
|
372
|
+
.nb-quota-banner {
|
|
373
|
+
align-items: center;
|
|
374
|
+
background: color-mix(in srgb, var(--accent-color, #888) 8%, var(--surface-1, transparent));
|
|
375
|
+
border: 1px solid var(--border-subtle);
|
|
376
|
+
border-radius: var(--radius-lg);
|
|
377
|
+
color: var(--text-primary);
|
|
378
|
+
display: flex;
|
|
379
|
+
gap: var(--space-3);
|
|
380
|
+
justify-content: space-between;
|
|
381
|
+
padding: var(--space-3) var(--space-4);
|
|
382
|
+
width: 100%;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.nb-quota-banner--warn {
|
|
386
|
+
background: color-mix(in srgb, var(--color-warning, #f59e0b) 10%, var(--surface-1, transparent));
|
|
387
|
+
border-color: color-mix(in srgb, var(--color-warning, #f59e0b) 45%, var(--border-subtle));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.nb-quota-banner--limit {
|
|
391
|
+
background: color-mix(in srgb, var(--color-error, #dc2626) 8%, var(--surface-1, transparent));
|
|
392
|
+
border-color: color-mix(in srgb, var(--color-error, #dc2626) 40%, var(--border-subtle));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.nb-quota-banner__copy {
|
|
396
|
+
display: flex;
|
|
397
|
+
flex-direction: column;
|
|
398
|
+
gap: 2px;
|
|
399
|
+
font-size: 0.9rem;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.nb-quota-banner__copy span {
|
|
403
|
+
color: var(--text-secondary);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.nb-quota-banner__cta {
|
|
407
|
+
color: var(--accent-color);
|
|
408
|
+
flex-shrink: 0;
|
|
409
|
+
font-weight: 600;
|
|
410
|
+
text-decoration: none;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.nb-quota-banner__cta:hover {
|
|
414
|
+
text-decoration: underline;
|
|
415
|
+
}
|
|
416
|
+
@charset "UTF-8";
|
|
417
|
+
.nb-plan-panel {
|
|
418
|
+
display: flex;
|
|
419
|
+
flex-direction: column;
|
|
420
|
+
gap: var(--space-4);
|
|
421
|
+
width: 100%;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.nb-plan-panel__header {
|
|
425
|
+
display: flex;
|
|
426
|
+
justify-content: space-between;
|
|
427
|
+
gap: var(--space-3);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.nb-plan-panel__title {
|
|
431
|
+
font-size: 1.15rem;
|
|
432
|
+
font-weight: 700;
|
|
433
|
+
margin: 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.nb-plan-panel__subtitle {
|
|
437
|
+
color: var(--text-secondary);
|
|
438
|
+
font-size: 0.9rem;
|
|
439
|
+
margin: var(--space-1) 0 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.nb-plan-panel__status {
|
|
443
|
+
color: var(--text-secondary);
|
|
444
|
+
font-size: 0.85rem;
|
|
445
|
+
margin: var(--space-2) 0 0;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.nb-plan-panel__grid {
|
|
449
|
+
display: grid;
|
|
450
|
+
gap: var(--space-3);
|
|
451
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.nb-plan-card {
|
|
455
|
+
background: var(--surface-1);
|
|
456
|
+
border: 1px solid var(--border-subtle);
|
|
457
|
+
border-radius: var(--radius-lg);
|
|
458
|
+
display: flex;
|
|
459
|
+
flex-direction: column;
|
|
460
|
+
gap: var(--space-3);
|
|
461
|
+
min-width: 0;
|
|
462
|
+
padding: var(--space-4);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.nb-plan-card--highlighted {
|
|
466
|
+
border-color: color-mix(in srgb, var(--accent-color) 45%, var(--border-subtle));
|
|
467
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-color) 20%, transparent);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.nb-plan-card--current {
|
|
471
|
+
background: color-mix(in srgb, var(--accent-color) 6%, var(--surface-1));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.nb-plan-card__head {
|
|
475
|
+
align-items: flex-start;
|
|
476
|
+
display: flex;
|
|
477
|
+
gap: var(--space-2);
|
|
478
|
+
justify-content: space-between;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.nb-plan-card__name {
|
|
482
|
+
font-size: 1rem;
|
|
483
|
+
font-weight: 700;
|
|
484
|
+
margin: 0;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.nb-plan-card__price {
|
|
488
|
+
font-size: 1.35rem;
|
|
489
|
+
font-variant-numeric: tabular-nums;
|
|
490
|
+
font-weight: 700;
|
|
491
|
+
margin: 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.nb-plan-card__description {
|
|
495
|
+
color: var(--text-secondary);
|
|
496
|
+
font-size: 0.85rem;
|
|
497
|
+
margin: 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.nb-plan-card__features {
|
|
501
|
+
color: var(--text-primary);
|
|
502
|
+
display: flex;
|
|
503
|
+
flex: 1;
|
|
504
|
+
flex-direction: column;
|
|
505
|
+
font-size: 0.85rem;
|
|
506
|
+
gap: var(--space-1);
|
|
507
|
+
list-style: none;
|
|
508
|
+
margin: 0;
|
|
509
|
+
padding: 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.nb-plan-card__features li::before {
|
|
513
|
+
color: var(--accent-color);
|
|
514
|
+
content: "✓ ";
|
|
515
|
+
font-weight: 700;
|
|
516
|
+
}
|
|
372
517
|
.nb-toast-host {
|
|
373
518
|
position: fixed;
|
|
374
519
|
right: 1rem;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubitio/admin",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Admin shell layout components: responsive sidebar, header, and screen size utilities for Nubit apps.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"react": "^19.0.0",
|
|
54
54
|
"react-dom": "^19.0.0",
|
|
55
55
|
"react-router-dom": "^6.0.0",
|
|
56
|
-
"@nubitio/
|
|
57
|
-
"@nubitio/
|
|
58
|
-
"@nubitio/
|
|
59
|
-
"@nubitio/
|
|
56
|
+
"@nubitio/core": "^0.5.26",
|
|
57
|
+
"@nubitio/crud": "^0.5.26",
|
|
58
|
+
"@nubitio/ui": "^0.5.26",
|
|
59
|
+
"@nubitio/hydra": "^0.5.26"
|
|
60
60
|
}
|
|
61
61
|
}
|