@geomak/ui 6.21.1 → 6.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -2869,11 +2869,15 @@ interface SecureLayoutProps {
2869
2869
  requireAllPermissions?: boolean;
2870
2870
  /** Final say. Runs after the built-in checks; may be async. Return false to deny. */
2871
2871
  canAccess?: () => boolean | Promise<boolean>;
2872
- /** Shown while the (possibly async) check resolves. */
2872
+ /** Shown while the (possibly async) check resolves. Pass `null` to render nothing. */
2873
2873
  loadingFallback?: React__default.ReactNode;
2874
- /** Shown when access is denied. Default is a simple "Access denied" panel. */
2874
+ /** Shown when access is denied. Defaults to a simple "Access denied" panel;
2875
+ * pass `null` to render nothing (e.g. when `onDeny` redirects away). */
2875
2876
  fallback?: React__default.ReactNode;
2876
- /** Fired once when access is denied — e.g. to redirect. */
2877
+ /** Fired once when access is granted — e.g. to hydrate app state or redirect
2878
+ * to a landing route after a successful token check. */
2879
+ onGranted?: () => void;
2880
+ /** Fired once when access is denied — e.g. to redirect to login / logout. */
2877
2881
  onDeny?: () => void;
2878
2882
  className?: string;
2879
2883
  }
@@ -2887,13 +2891,28 @@ interface SecureLayoutProps {
2887
2891
  * This is a UI guard, not a security boundary — it controls what renders.
2888
2892
  * Real authorization must be enforced by your API/server.
2889
2893
  *
2890
- * @example
2894
+ * @example Full RBAC + PBAC gate
2891
2895
  * <SecureLayout token={jwt} requiredRoles={['admin']} permissions={user.perms}
2892
2896
  * requiredPermissions={['reports:read']} onDeny={() => navigate('/login')}>
2893
2897
  * <AdminDashboard />
2894
2898
  * </SecureLayout>
2899
+ *
2900
+ * @example Simple JWT-only gate with a token-login bootstrap (no roles/perms)
2901
+ * <SecureLayout
2902
+ * canAccess={async () => {
2903
+ * const jwt = localStorage.getItem('jwt')
2904
+ * if (!jwt) return false
2905
+ * await tokenLogin(jwt) // hydrate app state from the server
2906
+ * return true
2907
+ * }}
2908
+ * onGranted={() => navigate('/dashboard')}
2909
+ * onDeny={() => navigate('/logout')}
2910
+ * fallback={null} // redirecting — don't flash a panel
2911
+ * >
2912
+ * <AppRoutes />
2913
+ * </SecureLayout>
2895
2914
  */
2896
- declare function SecureLayout({ children, isAuthenticated, token, roles, requiredRoles, requireAllRoles, permissions, requiredPermissions, requireAllPermissions, canAccess, loadingFallback, fallback, onDeny, className, }: SecureLayoutProps): react_jsx_runtime.JSX.Element;
2915
+ declare function SecureLayout({ children, isAuthenticated, token, roles, requiredRoles, requireAllRoles, permissions, requiredPermissions, requireAllPermissions, canAccess, loadingFallback, fallback, onGranted, onDeny, className, }: SecureLayoutProps): react_jsx_runtime.JSX.Element;
2897
2916
 
2898
2917
  interface ThemeColors {
2899
2918
  background?: string;
package/dist/index.d.ts CHANGED
@@ -2869,11 +2869,15 @@ interface SecureLayoutProps {
2869
2869
  requireAllPermissions?: boolean;
2870
2870
  /** Final say. Runs after the built-in checks; may be async. Return false to deny. */
2871
2871
  canAccess?: () => boolean | Promise<boolean>;
2872
- /** Shown while the (possibly async) check resolves. */
2872
+ /** Shown while the (possibly async) check resolves. Pass `null` to render nothing. */
2873
2873
  loadingFallback?: React__default.ReactNode;
2874
- /** Shown when access is denied. Default is a simple "Access denied" panel. */
2874
+ /** Shown when access is denied. Defaults to a simple "Access denied" panel;
2875
+ * pass `null` to render nothing (e.g. when `onDeny` redirects away). */
2875
2876
  fallback?: React__default.ReactNode;
2876
- /** Fired once when access is denied — e.g. to redirect. */
2877
+ /** Fired once when access is granted — e.g. to hydrate app state or redirect
2878
+ * to a landing route after a successful token check. */
2879
+ onGranted?: () => void;
2880
+ /** Fired once when access is denied — e.g. to redirect to login / logout. */
2877
2881
  onDeny?: () => void;
2878
2882
  className?: string;
2879
2883
  }
@@ -2887,13 +2891,28 @@ interface SecureLayoutProps {
2887
2891
  * This is a UI guard, not a security boundary — it controls what renders.
2888
2892
  * Real authorization must be enforced by your API/server.
2889
2893
  *
2890
- * @example
2894
+ * @example Full RBAC + PBAC gate
2891
2895
  * <SecureLayout token={jwt} requiredRoles={['admin']} permissions={user.perms}
2892
2896
  * requiredPermissions={['reports:read']} onDeny={() => navigate('/login')}>
2893
2897
  * <AdminDashboard />
2894
2898
  * </SecureLayout>
2899
+ *
2900
+ * @example Simple JWT-only gate with a token-login bootstrap (no roles/perms)
2901
+ * <SecureLayout
2902
+ * canAccess={async () => {
2903
+ * const jwt = localStorage.getItem('jwt')
2904
+ * if (!jwt) return false
2905
+ * await tokenLogin(jwt) // hydrate app state from the server
2906
+ * return true
2907
+ * }}
2908
+ * onGranted={() => navigate('/dashboard')}
2909
+ * onDeny={() => navigate('/logout')}
2910
+ * fallback={null} // redirecting — don't flash a panel
2911
+ * >
2912
+ * <AppRoutes />
2913
+ * </SecureLayout>
2895
2914
  */
2896
- declare function SecureLayout({ children, isAuthenticated, token, roles, requiredRoles, requireAllRoles, permissions, requiredPermissions, requireAllPermissions, canAccess, loadingFallback, fallback, onDeny, className, }: SecureLayoutProps): react_jsx_runtime.JSX.Element;
2915
+ declare function SecureLayout({ children, isAuthenticated, token, roles, requiredRoles, requireAllRoles, permissions, requiredPermissions, requireAllPermissions, canAccess, loadingFallback, fallback, onGranted, onDeny, className, }: SecureLayoutProps): react_jsx_runtime.JSX.Element;
2897
2916
 
2898
2917
  interface ThemeColors {
2899
2918
  background?: string;
package/dist/index.js CHANGED
@@ -5850,33 +5850,43 @@ function SecureLayout({
5850
5850
  canAccess,
5851
5851
  loadingFallback,
5852
5852
  fallback,
5853
+ onGranted,
5853
5854
  onDeny,
5854
5855
  className = ""
5855
5856
  }) {
5856
5857
  const reduced = useReducedMotion();
5857
- const [state, setState] = useState("checking");
5858
5858
  const rolesKey = JSON.stringify(roles);
5859
5859
  const requiredRolesKey = JSON.stringify(requiredRoles);
5860
5860
  const permissionsKey = JSON.stringify(permissions);
5861
5861
  const requiredPermissionsKey = JSON.stringify(requiredPermissions);
5862
+ const passesSync = () => {
5863
+ let authed = isAuthenticated;
5864
+ if (authed === void 0 && token !== void 0) authed = tokenValid(token);
5865
+ if (authed === void 0) authed = true;
5866
+ if (!authed) return false;
5867
+ if (requiredRoles?.length && !has(roles, requiredRoles, requireAllRoles)) return false;
5868
+ if (requiredPermissions?.length && !has(permissions, requiredPermissions, requireAllPermissions)) return false;
5869
+ return true;
5870
+ };
5871
+ const [state, setState] = useState(
5872
+ () => !passesSync() ? "denied" : canAccess ? "checking" : "granted"
5873
+ );
5862
5874
  useEffect(() => {
5863
5875
  let cancelled = false;
5864
- setState("checking");
5865
- const evaluate = async () => {
5866
- let authed = isAuthenticated;
5867
- if (authed === void 0 && token !== void 0) authed = tokenValid(token);
5868
- if (authed === void 0) authed = true;
5869
- if (!authed) return false;
5870
- if (requiredRoles?.length && !has(roles, requiredRoles, requireAllRoles)) return false;
5871
- if (requiredPermissions?.length && !has(permissions, requiredPermissions, requireAllPermissions)) return false;
5872
- if (canAccess && !await canAccess()) return false;
5873
- return true;
5874
- };
5875
- evaluate().then((ok) => {
5876
+ const finish = (ok) => {
5876
5877
  if (cancelled) return;
5877
5878
  setState(ok ? "granted" : "denied");
5878
- if (!ok) onDeny?.();
5879
- });
5879
+ if (ok) onGranted?.();
5880
+ else onDeny?.();
5881
+ };
5882
+ if (!passesSync()) {
5883
+ finish(false);
5884
+ } else if (!canAccess) {
5885
+ finish(true);
5886
+ } else {
5887
+ setState("checking");
5888
+ Promise.resolve(canAccess()).then((ok) => finish(Boolean(ok)));
5889
+ }
5880
5890
  return () => {
5881
5891
  cancelled = true;
5882
5892
  };
@@ -5892,10 +5902,12 @@ function SecureLayout({
5892
5902
  requiredPermissionsKey
5893
5903
  ]);
5894
5904
  if (state === "checking") {
5895
- return /* @__PURE__ */ jsx("div", { className: ["flex min-h-[8rem] items-center justify-center", className].filter(Boolean).join(" "), children: loadingFallback ?? /* @__PURE__ */ jsx(Spinner2, {}) });
5905
+ if (loadingFallback === null) return null;
5906
+ return /* @__PURE__ */ jsx("div", { className: ["flex min-h-[8rem] items-center justify-center", className].filter(Boolean).join(" "), children: loadingFallback !== void 0 ? loadingFallback : /* @__PURE__ */ jsx(Spinner2, {}) });
5896
5907
  }
5897
5908
  if (state === "denied") {
5898
- return /* @__PURE__ */ jsx("div", { className: className || void 0, children: fallback ?? /* @__PURE__ */ jsxs("div", { className: "flex min-h-[8rem] flex-col items-center justify-center gap-1 rounded-xl border border-border bg-surface p-8 text-center", children: [
5909
+ if (fallback === null) return null;
5910
+ return /* @__PURE__ */ jsx("div", { className: className || void 0, children: fallback !== void 0 ? fallback : /* @__PURE__ */ jsxs("div", { className: "flex min-h-[8rem] flex-col items-center justify-center gap-1 rounded-xl border border-border bg-surface p-8 text-center", children: [
5899
5911
  /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-foreground", children: "Access denied" }),
5900
5912
  /* @__PURE__ */ jsx("div", { className: "text-xs text-foreground-muted", children: "You don\u2019t have permission to view this content." })
5901
5913
  ] }) });