@nubitio/admin 0.5.22 → 0.5.24
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 +315 -5
- package/dist/index.d.cts +138 -1
- package/dist/index.d.mts +138 -1
- package/dist/index.mjs +312 -9
- package/dist/style.css +145 -0
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -389,14 +389,14 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
389
389
|
//#endregion
|
|
390
390
|
//#region packages/admin/auth/SessionContext.tsx
|
|
391
391
|
const SessionContext = (0, react.createContext)(null);
|
|
392
|
-
function joinApiPath$
|
|
392
|
+
function joinApiPath$3(apiBaseUrl, path) {
|
|
393
393
|
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
394
394
|
}
|
|
395
395
|
function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "auth/logout", children }) {
|
|
396
396
|
const [session, setSession] = (0, react.useState)({ status: "loading" });
|
|
397
397
|
const refresh = (0, react.useCallback)(async () => {
|
|
398
398
|
try {
|
|
399
|
-
const response = await fetch(joinApiPath$
|
|
399
|
+
const response = await fetch(joinApiPath$3(apiBaseUrl, mePath), { credentials: "include" });
|
|
400
400
|
if (!response.ok) {
|
|
401
401
|
setSession({ status: "anonymous" });
|
|
402
402
|
return;
|
|
@@ -413,7 +413,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
413
413
|
refresh();
|
|
414
414
|
}, [refresh]);
|
|
415
415
|
const logout = (0, react.useCallback)(async () => {
|
|
416
|
-
await fetch(joinApiPath$
|
|
416
|
+
await fetch(joinApiPath$3(apiBaseUrl, logoutPath), {
|
|
417
417
|
method: "POST",
|
|
418
418
|
credentials: "include"
|
|
419
419
|
});
|
|
@@ -481,8 +481,192 @@ 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
|
-
function joinApiPath$
|
|
669
|
+
function joinApiPath$2(apiBaseUrl, path) {
|
|
486
670
|
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
487
671
|
}
|
|
488
672
|
function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login", title = "Nubit Admin", hint, defaultUsername = "" }) {
|
|
@@ -495,7 +679,7 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
|
|
|
495
679
|
setBusy(true);
|
|
496
680
|
setError(null);
|
|
497
681
|
try {
|
|
498
|
-
const response = await fetch(joinApiPath$
|
|
682
|
+
const response = await fetch(joinApiPath$2(apiBaseUrl, loginPath), {
|
|
499
683
|
method: "POST",
|
|
500
684
|
headers: { "Content-Type": "application/json" },
|
|
501
685
|
credentials: "include",
|
|
@@ -573,6 +757,125 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
|
|
|
573
757
|
});
|
|
574
758
|
}
|
|
575
759
|
//#endregion
|
|
760
|
+
//#region packages/admin/auth/RegisterPage.tsx
|
|
761
|
+
function joinApiPath$1(apiBaseUrl, path) {
|
|
762
|
+
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
763
|
+
}
|
|
764
|
+
function initialValues(fields) {
|
|
765
|
+
return Object.fromEntries(fields.map((field) => [field.name, field.defaultValue ?? ""]));
|
|
766
|
+
}
|
|
767
|
+
function RegisterPage({ fields, onRegistered, apiBaseUrl = "/api/", registerPath = "auth/register", title = "Create account", hint, submitLabel = "Create account", busyLabel = "Creating…", loginLink, loginPrompt = "Already have an account?" }) {
|
|
768
|
+
const [values, setValues] = (0, react.useState)((0, react.useMemo)(() => initialValues(fields), [fields]));
|
|
769
|
+
const [error, setError] = (0, react.useState)(null);
|
|
770
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
771
|
+
const setValue = (name, value) => {
|
|
772
|
+
setValues((current) => ({
|
|
773
|
+
...current,
|
|
774
|
+
[name]: value
|
|
775
|
+
}));
|
|
776
|
+
};
|
|
777
|
+
const submit = async (event) => {
|
|
778
|
+
event.preventDefault();
|
|
779
|
+
setBusy(true);
|
|
780
|
+
setError(null);
|
|
781
|
+
try {
|
|
782
|
+
const response = await fetch(joinApiPath$1(apiBaseUrl, registerPath), {
|
|
783
|
+
method: "POST",
|
|
784
|
+
headers: { "Content-Type": "application/json" },
|
|
785
|
+
credentials: "include",
|
|
786
|
+
body: JSON.stringify(values)
|
|
787
|
+
});
|
|
788
|
+
const body = await response.json().catch(() => null);
|
|
789
|
+
if (!response.ok) {
|
|
790
|
+
setError(body?.error ?? body?.message ?? "Registration failed");
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
onRegistered();
|
|
794
|
+
} catch {
|
|
795
|
+
setError("Network error");
|
|
796
|
+
} finally {
|
|
797
|
+
setBusy(false);
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
801
|
+
style: {
|
|
802
|
+
display: "grid",
|
|
803
|
+
placeItems: "center",
|
|
804
|
+
minHeight: "100vh"
|
|
805
|
+
},
|
|
806
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Card, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
807
|
+
onSubmit: submit,
|
|
808
|
+
style: {
|
|
809
|
+
display: "flex",
|
|
810
|
+
flexDirection: "column",
|
|
811
|
+
gap: 12,
|
|
812
|
+
width: 360,
|
|
813
|
+
padding: 8
|
|
814
|
+
},
|
|
815
|
+
children: [
|
|
816
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
817
|
+
style: { margin: 0 },
|
|
818
|
+
children: title
|
|
819
|
+
}),
|
|
820
|
+
hint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
821
|
+
style: {
|
|
822
|
+
margin: 0,
|
|
823
|
+
color: "var(--text-secondary)"
|
|
824
|
+
},
|
|
825
|
+
children: hint
|
|
826
|
+
}),
|
|
827
|
+
fields.map((field) => {
|
|
828
|
+
const type = field.type ?? "text";
|
|
829
|
+
if (type === "select") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
830
|
+
value: values[field.name] ?? "",
|
|
831
|
+
onChange: (e) => setValue(field.name, e.target.value),
|
|
832
|
+
style: { width: "100%" },
|
|
833
|
+
"aria-label": field.placeholder ?? field.name,
|
|
834
|
+
children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
835
|
+
value: option.value,
|
|
836
|
+
children: option.label
|
|
837
|
+
}, option.value))
|
|
838
|
+
}, field.name);
|
|
839
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.TextField, {
|
|
840
|
+
placeholder: field.placeholder,
|
|
841
|
+
type: type === "password" ? "password" : type === "email" ? "email" : "text",
|
|
842
|
+
value: values[field.name] ?? "",
|
|
843
|
+
autoComplete: field.autoComplete,
|
|
844
|
+
onChange: (e) => setValue(field.name, e.target.value)
|
|
845
|
+
}, field.name);
|
|
846
|
+
}),
|
|
847
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
848
|
+
style: {
|
|
849
|
+
margin: 0,
|
|
850
|
+
color: "var(--error-color, #dc2626)"
|
|
851
|
+
},
|
|
852
|
+
children: error
|
|
853
|
+
}),
|
|
854
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Button, {
|
|
855
|
+
variant: "primary",
|
|
856
|
+
type: "submit",
|
|
857
|
+
disabled: busy,
|
|
858
|
+
children: busy ? busyLabel : submitLabel
|
|
859
|
+
}),
|
|
860
|
+
loginLink && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
861
|
+
style: {
|
|
862
|
+
margin: 0,
|
|
863
|
+
color: "var(--text-secondary)"
|
|
864
|
+
},
|
|
865
|
+
children: [
|
|
866
|
+
loginPrompt,
|
|
867
|
+
" ",
|
|
868
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.Link, {
|
|
869
|
+
to: loginLink.to,
|
|
870
|
+
children: loginLink.label
|
|
871
|
+
})
|
|
872
|
+
]
|
|
873
|
+
})
|
|
874
|
+
]
|
|
875
|
+
}) })
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
//#endregion
|
|
576
879
|
//#region packages/admin/runtime/useAppRuntime.ts
|
|
577
880
|
function useAppRuntime() {
|
|
578
881
|
const [toasts, setToasts] = (0, react.useState)([]);
|
|
@@ -842,15 +1145,22 @@ exports.AdminShell = AdminShell;
|
|
|
842
1145
|
exports.AdminSidebarMenu = AdminSidebarMenu;
|
|
843
1146
|
exports.FeatureGate = FeatureGate;
|
|
844
1147
|
exports.LoginPage = LoginPage;
|
|
1148
|
+
exports.PlanPanel = PlanPanel;
|
|
1149
|
+
exports.QuotaUsageBanner = QuotaUsageBanner;
|
|
1150
|
+
exports.RegisterPage = RegisterPage;
|
|
845
1151
|
exports.SessionProvider = SessionProvider;
|
|
846
1152
|
exports.StaticSessionProvider = StaticSessionProvider;
|
|
847
1153
|
exports.ToastHost = ToastHost;
|
|
848
1154
|
exports.createNubitApp = createNubitApp;
|
|
849
1155
|
exports.filterMenuByRoles = filterMenuByRoles;
|
|
850
1156
|
exports.hasAnyRole = hasAnyRole;
|
|
1157
|
+
exports.parseQuotaError = parseQuotaError;
|
|
1158
|
+
exports.quotaErrorToastMessage = quotaErrorToastMessage;
|
|
1159
|
+
exports.quotaUsageAboveGrid = quotaUsageAboveGrid;
|
|
851
1160
|
exports.useAppRuntime = useAppRuntime;
|
|
852
1161
|
exports.useFeature = useFeature;
|
|
853
1162
|
exports.useFeatureConfig = useFeatureConfig;
|
|
1163
|
+
exports.useQuotaUsage = useQuotaUsage;
|
|
854
1164
|
exports.useRuntimeConfig = useRuntimeConfig;
|
|
855
1165
|
exports.useScreenSize = useScreenSize;
|
|
856
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;
|
|
@@ -186,6 +280,49 @@ declare function LoginPage({
|
|
|
186
280
|
defaultUsername
|
|
187
281
|
}: LoginPageProps): import("react").JSX.Element;
|
|
188
282
|
//#endregion
|
|
283
|
+
//#region packages/admin/auth/RegisterPage.d.ts
|
|
284
|
+
type RegisterFieldType = 'text' | 'email' | 'password' | 'select';
|
|
285
|
+
interface RegisterSelectOption {
|
|
286
|
+
value: string;
|
|
287
|
+
label: string;
|
|
288
|
+
}
|
|
289
|
+
interface RegisterField {
|
|
290
|
+
/** JSON body key sent to the register endpoint. */
|
|
291
|
+
name: string;
|
|
292
|
+
placeholder?: string;
|
|
293
|
+
type?: RegisterFieldType;
|
|
294
|
+
defaultValue?: string;
|
|
295
|
+
autoComplete?: string;
|
|
296
|
+
options?: RegisterSelectOption[];
|
|
297
|
+
}
|
|
298
|
+
interface RegisterPageProps {
|
|
299
|
+
fields: RegisterField[];
|
|
300
|
+
onRegistered: () => void;
|
|
301
|
+
apiBaseUrl?: string;
|
|
302
|
+
registerPath?: string;
|
|
303
|
+
title?: string;
|
|
304
|
+
hint?: string;
|
|
305
|
+
submitLabel?: string;
|
|
306
|
+
busyLabel?: string;
|
|
307
|
+
loginLink?: {
|
|
308
|
+
to: string;
|
|
309
|
+
label: string;
|
|
310
|
+
};
|
|
311
|
+
loginPrompt?: string;
|
|
312
|
+
}
|
|
313
|
+
declare function RegisterPage({
|
|
314
|
+
fields,
|
|
315
|
+
onRegistered,
|
|
316
|
+
apiBaseUrl,
|
|
317
|
+
registerPath,
|
|
318
|
+
title,
|
|
319
|
+
hint,
|
|
320
|
+
submitLabel,
|
|
321
|
+
busyLabel,
|
|
322
|
+
loginLink,
|
|
323
|
+
loginPrompt
|
|
324
|
+
}: RegisterPageProps): import("react").JSX.Element;
|
|
325
|
+
//#endregion
|
|
189
326
|
//#region packages/admin/runtime/useAppRuntime.d.ts
|
|
190
327
|
type NotificationType = 'success' | 'error' | 'warning' | 'info';
|
|
191
328
|
type ToastItem = {
|
|
@@ -307,4 +444,4 @@ declare function hasAnyRole(required: string | string[] | undefined, roles: stri
|
|
|
307
444
|
*/
|
|
308
445
|
declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
|
|
309
446
|
//#endregion
|
|
310
|
-
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 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;
|
|
@@ -186,6 +280,49 @@ declare function LoginPage({
|
|
|
186
280
|
defaultUsername
|
|
187
281
|
}: LoginPageProps): import("react").JSX.Element;
|
|
188
282
|
//#endregion
|
|
283
|
+
//#region packages/admin/auth/RegisterPage.d.ts
|
|
284
|
+
type RegisterFieldType = 'text' | 'email' | 'password' | 'select';
|
|
285
|
+
interface RegisterSelectOption {
|
|
286
|
+
value: string;
|
|
287
|
+
label: string;
|
|
288
|
+
}
|
|
289
|
+
interface RegisterField {
|
|
290
|
+
/** JSON body key sent to the register endpoint. */
|
|
291
|
+
name: string;
|
|
292
|
+
placeholder?: string;
|
|
293
|
+
type?: RegisterFieldType;
|
|
294
|
+
defaultValue?: string;
|
|
295
|
+
autoComplete?: string;
|
|
296
|
+
options?: RegisterSelectOption[];
|
|
297
|
+
}
|
|
298
|
+
interface RegisterPageProps {
|
|
299
|
+
fields: RegisterField[];
|
|
300
|
+
onRegistered: () => void;
|
|
301
|
+
apiBaseUrl?: string;
|
|
302
|
+
registerPath?: string;
|
|
303
|
+
title?: string;
|
|
304
|
+
hint?: string;
|
|
305
|
+
submitLabel?: string;
|
|
306
|
+
busyLabel?: string;
|
|
307
|
+
loginLink?: {
|
|
308
|
+
to: string;
|
|
309
|
+
label: string;
|
|
310
|
+
};
|
|
311
|
+
loginPrompt?: string;
|
|
312
|
+
}
|
|
313
|
+
declare function RegisterPage({
|
|
314
|
+
fields,
|
|
315
|
+
onRegistered,
|
|
316
|
+
apiBaseUrl,
|
|
317
|
+
registerPath,
|
|
318
|
+
title,
|
|
319
|
+
hint,
|
|
320
|
+
submitLabel,
|
|
321
|
+
busyLabel,
|
|
322
|
+
loginLink,
|
|
323
|
+
loginPrompt
|
|
324
|
+
}: RegisterPageProps): import("react").JSX.Element;
|
|
325
|
+
//#endregion
|
|
189
326
|
//#region packages/admin/runtime/useAppRuntime.d.ts
|
|
190
327
|
type NotificationType = 'success' | 'error' | 'warning' | 'info';
|
|
191
328
|
type ToastItem = {
|
|
@@ -307,4 +444,4 @@ declare function hasAnyRole(required: string | string[] | undefined, roles: stri
|
|
|
307
444
|
*/
|
|
308
445
|
declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
|
|
309
446
|
//#endregion
|
|
310
|
-
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 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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
|
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
|
|
@@ -365,14 +365,14 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
365
365
|
//#endregion
|
|
366
366
|
//#region packages/admin/auth/SessionContext.tsx
|
|
367
367
|
const SessionContext = createContext(null);
|
|
368
|
-
function joinApiPath$
|
|
368
|
+
function joinApiPath$3(apiBaseUrl, path) {
|
|
369
369
|
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
370
370
|
}
|
|
371
371
|
function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "auth/logout", children }) {
|
|
372
372
|
const [session, setSession] = useState({ status: "loading" });
|
|
373
373
|
const refresh = useCallback(async () => {
|
|
374
374
|
try {
|
|
375
|
-
const response = await fetch(joinApiPath$
|
|
375
|
+
const response = await fetch(joinApiPath$3(apiBaseUrl, mePath), { credentials: "include" });
|
|
376
376
|
if (!response.ok) {
|
|
377
377
|
setSession({ status: "anonymous" });
|
|
378
378
|
return;
|
|
@@ -389,7 +389,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
|
|
|
389
389
|
refresh();
|
|
390
390
|
}, [refresh]);
|
|
391
391
|
const logout = useCallback(async () => {
|
|
392
|
-
await fetch(joinApiPath$
|
|
392
|
+
await fetch(joinApiPath$3(apiBaseUrl, logoutPath), {
|
|
393
393
|
method: "POST",
|
|
394
394
|
credentials: "include"
|
|
395
395
|
});
|
|
@@ -457,8 +457,192 @@ 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
|
-
function joinApiPath$
|
|
645
|
+
function joinApiPath$2(apiBaseUrl, path) {
|
|
462
646
|
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
463
647
|
}
|
|
464
648
|
function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login", title = "Nubit Admin", hint, defaultUsername = "" }) {
|
|
@@ -471,7 +655,7 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
|
|
|
471
655
|
setBusy(true);
|
|
472
656
|
setError(null);
|
|
473
657
|
try {
|
|
474
|
-
const response = await fetch(joinApiPath$
|
|
658
|
+
const response = await fetch(joinApiPath$2(apiBaseUrl, loginPath), {
|
|
475
659
|
method: "POST",
|
|
476
660
|
headers: { "Content-Type": "application/json" },
|
|
477
661
|
credentials: "include",
|
|
@@ -549,6 +733,125 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
|
|
|
549
733
|
});
|
|
550
734
|
}
|
|
551
735
|
//#endregion
|
|
736
|
+
//#region packages/admin/auth/RegisterPage.tsx
|
|
737
|
+
function joinApiPath$1(apiBaseUrl, path) {
|
|
738
|
+
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
739
|
+
}
|
|
740
|
+
function initialValues(fields) {
|
|
741
|
+
return Object.fromEntries(fields.map((field) => [field.name, field.defaultValue ?? ""]));
|
|
742
|
+
}
|
|
743
|
+
function RegisterPage({ fields, onRegistered, apiBaseUrl = "/api/", registerPath = "auth/register", title = "Create account", hint, submitLabel = "Create account", busyLabel = "Creating…", loginLink, loginPrompt = "Already have an account?" }) {
|
|
744
|
+
const [values, setValues] = useState(useMemo(() => initialValues(fields), [fields]));
|
|
745
|
+
const [error, setError] = useState(null);
|
|
746
|
+
const [busy, setBusy] = useState(false);
|
|
747
|
+
const setValue = (name, value) => {
|
|
748
|
+
setValues((current) => ({
|
|
749
|
+
...current,
|
|
750
|
+
[name]: value
|
|
751
|
+
}));
|
|
752
|
+
};
|
|
753
|
+
const submit = async (event) => {
|
|
754
|
+
event.preventDefault();
|
|
755
|
+
setBusy(true);
|
|
756
|
+
setError(null);
|
|
757
|
+
try {
|
|
758
|
+
const response = await fetch(joinApiPath$1(apiBaseUrl, registerPath), {
|
|
759
|
+
method: "POST",
|
|
760
|
+
headers: { "Content-Type": "application/json" },
|
|
761
|
+
credentials: "include",
|
|
762
|
+
body: JSON.stringify(values)
|
|
763
|
+
});
|
|
764
|
+
const body = await response.json().catch(() => null);
|
|
765
|
+
if (!response.ok) {
|
|
766
|
+
setError(body?.error ?? body?.message ?? "Registration failed");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
onRegistered();
|
|
770
|
+
} catch {
|
|
771
|
+
setError("Network error");
|
|
772
|
+
} finally {
|
|
773
|
+
setBusy(false);
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
return /* @__PURE__ */ jsx("div", {
|
|
777
|
+
style: {
|
|
778
|
+
display: "grid",
|
|
779
|
+
placeItems: "center",
|
|
780
|
+
minHeight: "100vh"
|
|
781
|
+
},
|
|
782
|
+
children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("form", {
|
|
783
|
+
onSubmit: submit,
|
|
784
|
+
style: {
|
|
785
|
+
display: "flex",
|
|
786
|
+
flexDirection: "column",
|
|
787
|
+
gap: 12,
|
|
788
|
+
width: 360,
|
|
789
|
+
padding: 8
|
|
790
|
+
},
|
|
791
|
+
children: [
|
|
792
|
+
/* @__PURE__ */ jsx("h2", {
|
|
793
|
+
style: { margin: 0 },
|
|
794
|
+
children: title
|
|
795
|
+
}),
|
|
796
|
+
hint && /* @__PURE__ */ jsx("p", {
|
|
797
|
+
style: {
|
|
798
|
+
margin: 0,
|
|
799
|
+
color: "var(--text-secondary)"
|
|
800
|
+
},
|
|
801
|
+
children: hint
|
|
802
|
+
}),
|
|
803
|
+
fields.map((field) => {
|
|
804
|
+
const type = field.type ?? "text";
|
|
805
|
+
if (type === "select") return /* @__PURE__ */ jsx("select", {
|
|
806
|
+
value: values[field.name] ?? "",
|
|
807
|
+
onChange: (e) => setValue(field.name, e.target.value),
|
|
808
|
+
style: { width: "100%" },
|
|
809
|
+
"aria-label": field.placeholder ?? field.name,
|
|
810
|
+
children: (field.options ?? []).map((option) => /* @__PURE__ */ jsx("option", {
|
|
811
|
+
value: option.value,
|
|
812
|
+
children: option.label
|
|
813
|
+
}, option.value))
|
|
814
|
+
}, field.name);
|
|
815
|
+
return /* @__PURE__ */ jsx(TextField, {
|
|
816
|
+
placeholder: field.placeholder,
|
|
817
|
+
type: type === "password" ? "password" : type === "email" ? "email" : "text",
|
|
818
|
+
value: values[field.name] ?? "",
|
|
819
|
+
autoComplete: field.autoComplete,
|
|
820
|
+
onChange: (e) => setValue(field.name, e.target.value)
|
|
821
|
+
}, field.name);
|
|
822
|
+
}),
|
|
823
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
824
|
+
style: {
|
|
825
|
+
margin: 0,
|
|
826
|
+
color: "var(--error-color, #dc2626)"
|
|
827
|
+
},
|
|
828
|
+
children: error
|
|
829
|
+
}),
|
|
830
|
+
/* @__PURE__ */ jsx(Button, {
|
|
831
|
+
variant: "primary",
|
|
832
|
+
type: "submit",
|
|
833
|
+
disabled: busy,
|
|
834
|
+
children: busy ? busyLabel : submitLabel
|
|
835
|
+
}),
|
|
836
|
+
loginLink && /* @__PURE__ */ jsxs("p", {
|
|
837
|
+
style: {
|
|
838
|
+
margin: 0,
|
|
839
|
+
color: "var(--text-secondary)"
|
|
840
|
+
},
|
|
841
|
+
children: [
|
|
842
|
+
loginPrompt,
|
|
843
|
+
" ",
|
|
844
|
+
/* @__PURE__ */ jsx(Link, {
|
|
845
|
+
to: loginLink.to,
|
|
846
|
+
children: loginLink.label
|
|
847
|
+
})
|
|
848
|
+
]
|
|
849
|
+
})
|
|
850
|
+
]
|
|
851
|
+
}) })
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
552
855
|
//#region packages/admin/runtime/useAppRuntime.ts
|
|
553
856
|
function useAppRuntime() {
|
|
554
857
|
const [toasts, setToasts] = useState([]);
|
|
@@ -813,4 +1116,4 @@ function createNubitApp(config) {
|
|
|
813
1116
|
return { App };
|
|
814
1117
|
}
|
|
815
1118
|
//#endregion
|
|
816
|
-
export { AdminHeader, AdminShell, AdminSidebarMenu, FeatureGate, LoginPage, 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.24",
|
|
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/crud": "^0.5.
|
|
58
|
-
"@nubitio/
|
|
59
|
-
"@nubitio/
|
|
56
|
+
"@nubitio/hydra": "^0.5.24",
|
|
57
|
+
"@nubitio/crud": "^0.5.24",
|
|
58
|
+
"@nubitio/ui": "^0.5.24",
|
|
59
|
+
"@nubitio/core": "^0.5.24"
|
|
60
60
|
}
|
|
61
61
|
}
|