@tailor-platform/app-shell 0.23.0 → 0.24.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.ts CHANGED
@@ -14,42 +14,26 @@ import { useRouteError } from 'react-router';
14
14
  import { useSearchParams } from 'react-router';
15
15
  import { VariantProps } from 'class-variance-authority';
16
16
 
17
- /**
18
- * Context provided to access control functions
19
- */
20
- declare type AccessContext = {
21
- params: Params;
22
- searchParams: URLSearchParams;
23
- signal: AbortSignal;
24
- };
25
-
26
- declare type AccessControl = (context: AccessContext) => Promise<AccessResult> | AccessResult;
17
+ export declare const AppShell: (props: AppShellProps) => JSX.Element | null;
27
18
 
28
19
  /**
29
- * Result of access control evaluation
20
+ * Context for static configuration (title, icon, configurations).
21
+ * Changes to this context will cause RouterContainer to re-render.
30
22
  */
31
- declare type AccessResult =
32
- /**
33
- * Resource is visible and accessible
34
- */
35
- {
36
- state: "visible";
37
- }
38
- /**
39
- * Resource is hidden and not accessible
40
- */
41
- | {
42
- state: "hidden";
43
- };
44
-
45
- export declare const AppShell: (props: AppShellProps) => JSX.Element | null;
46
-
47
- declare type AppShellContextType = {
23
+ declare type AppShellConfigContextType = {
48
24
  title?: string;
49
25
  icon?: ReactNode;
50
26
  configurations: RootConfiguration;
51
27
  };
52
28
 
29
+ /**
30
+ * Context for dynamic data (contextData).
31
+ * Changes to this context will NOT cause RouterContainer to re-render.
32
+ */
33
+ declare type AppShellDataContextType = {
34
+ contextData: ContextData;
35
+ };
36
+
53
37
  export declare type AppShellProps = React.PropsWithChildren<{
54
38
  /**
55
39
  * App shell title
@@ -64,19 +48,15 @@ export declare type AppShellProps = React.PropsWithChildren<{
64
48
  */
65
49
  basePath?: string;
66
50
  /**
67
- * A component to be rendered at the root level of AppShell,
68
- * or a redirect configuration
51
+ * A component to be rendered at the root level of AppShell.
52
+ * Use guards with redirectTo() for redirects.
69
53
  *
70
54
  * @example
71
55
  * ```tsx
72
- * // Render a component
73
56
  * rootComponent: () => <DashboardHome />
74
- *
75
- * // Redirect to a resource
76
- * rootComponent: redirectToResource("dashboard/overview")
77
57
  * ```
78
58
  */
79
- rootComponent?: (() => React.ReactNode) | RedirectConfig;
59
+ rootComponent?: () => React.ReactNode;
80
60
  /**
81
61
  * Navigation configuration
82
62
  */
@@ -120,8 +100,52 @@ export declare type AppShellProps = React.PropsWithChildren<{
120
100
  * ```
121
101
  */
122
102
  errorBoundary?: ErrorBoundaryComponent;
103
+ /**
104
+ * Custom context data accessible from guards and components.
105
+ *
106
+ * Use module augmentation to define the type of context data:
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * // types.d.ts
111
+ * declare module "@tailor-platform/app-shell" {
112
+ * interface AppShellRegister {
113
+ * contextData: {
114
+ * apiClient: ApiClient;
115
+ * currentUser: User;
116
+ * };
117
+ * }
118
+ * }
119
+ *
120
+ * // App.tsx
121
+ * <AppShell
122
+ * modules={modules}
123
+ * contextData={{ apiClient, currentUser }}
124
+ * />
125
+ * ```
126
+ */
127
+ contextData?: ContextData;
123
128
  }>;
124
129
 
130
+ /**
131
+ * Empty interface for module augmentation.
132
+ * Users can extend this to define their own context data type.
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * declare module "@tailor-platform/app-shell" {
137
+ * interface AppShellRegister {
138
+ * contextData: {
139
+ * apiClient: ApiClient;
140
+ * currentUser: User;
141
+ * };
142
+ * }
143
+ * }
144
+ * ```
145
+ */
146
+ export declare interface AppShellRegister {
147
+ }
148
+
125
149
  declare type AuthContextType = {
126
150
  /**
127
151
  * Current authentication state.
@@ -297,7 +321,6 @@ declare type CommonModuleProps = {
297
321
  declare type CommonPageResource = {
298
322
  path: string;
299
323
  type: "component";
300
- component: () => ReactNode;
301
324
  meta: {
302
325
  title: LocalizedString;
303
326
  icon?: ReactNode;
@@ -317,11 +340,29 @@ declare type CommonProps = {
317
340
  */
318
341
  meta?: ResourceMetaProps;
319
342
  /**
320
- * Access guard to control visibility and access of this module and its resources.
343
+ * Guards to control access to this module/resource.
344
+ * Guards are executed in order. If any guard returns non-pass result,
345
+ * access is denied.
346
+ *
347
+ * @example
348
+ * ```tsx
349
+ * guards: [
350
+ * ({ context }) => context.currentUser ? pass() : redirectTo("/login"),
351
+ * ({ context }) => context.currentUser.role === "admin" ? pass() : hidden(),
352
+ * ]
353
+ * ```
321
354
  */
322
- accessControl?: AccessControl;
355
+ guards?: Guard[];
323
356
  };
324
357
 
358
+ /**
359
+ * Context data type inferred from AppShellRegister.
360
+ * Falls back to Record<string, unknown> if not augmented.
361
+ */
362
+ export declare type ContextData = AppShellRegister extends {
363
+ contextData: infer T;
364
+ } ? T : Record<string, unknown>;
365
+
325
366
  /**
326
367
  * Date format options
327
368
  */
@@ -406,7 +447,24 @@ export declare const defineI18nLabels: <const L extends Exclude<string, "en">, c
406
447
  * }
407
448
  * ```
408
449
  */
409
- useT: () => <K extends keyof Def & string>(key: K, ...args: Def[K] extends (props: infer P) => string ? [props: P] : []) => string;
450
+ useT: () => (<K extends keyof Def & string>(key: K, ...args: Def[K] extends (props: infer P) => string ? [props: P] : []) => string) & {
451
+ /**
452
+ * Resolve a label with a dynamic key.
453
+ * This is useful when the key is constructed at runtime.
454
+ *
455
+ * @param key - The dynamic key to resolve
456
+ * @param fallback - The fallback value if the key is not found
457
+ * @returns The resolved label or the fallback value
458
+ *
459
+ * @example
460
+ * ```tsx
461
+ * const employeeType = "STAFF";
462
+ * t.dynamic(`employees.${employeeType}`, "Unknown"); // "Staff"
463
+ * t.dynamic("unknown.key", "Default"); // "Default"
464
+ * ```
465
+ */
466
+ dynamic: (key: string, fallback: string) => string;
467
+ };
410
468
  /**
411
469
  * A function to get the translater for a specific label key.
412
470
  * This is expected to be used in `meta.title` in module/resource definitions.
@@ -454,18 +512,15 @@ export declare function defineModule(props: DefineModuleProps): Module;
454
512
 
455
513
  declare type DefineModuleProps = CommonProps & CommonModuleProps & {
456
514
  /**
457
- * React component to render or redirect configuration
515
+ * React component to render.
516
+ * If not provided, the module will redirect to the first resource.
458
517
  *
459
518
  * @example
460
519
  * ```tsx
461
- * // Render a component
462
520
  * component: (props) => <div>{props.title}</div>
463
- *
464
- * // Redirect to a resource
465
- * component: redirectToResource("dashboard/overview")
466
521
  * ```
467
522
  */
468
- component: ((props: ResourceComponentProps) => ReactNode) | RedirectConfig;
523
+ component?: (props: ResourceComponentProps) => ReactNode;
469
524
  /**
470
525
  * Error boundary component for this module and its child resources.
471
526
  * When an error occurs in this module or its resources, this component will render.
@@ -654,6 +709,56 @@ declare interface FieldMeta {
654
709
  */
655
710
  declare type FieldType = "text" | "badge" | "money" | "date" | "link" | "address" | "reference";
656
711
 
712
+ /**
713
+ * Guard function type.
714
+ * Guards are executed in order and stop on the first non-pass result.
715
+ */
716
+ export declare type Guard = (ctx: GuardContext) => Promise<GuardResult> | GuardResult;
717
+
718
+ /**
719
+ * Context provided to guard functions
720
+ */
721
+ export declare type GuardContext = {
722
+ params: Params;
723
+ searchParams: URLSearchParams;
724
+ signal: AbortSignal;
725
+ context: ContextData;
726
+ };
727
+
728
+ /**
729
+ * Result of guard evaluation.
730
+ * Guards can only return one of these constrained result types.
731
+ */
732
+ export declare type GuardResult =
733
+ /** Allow access and render the component */
734
+ {
735
+ type: "pass";
736
+ }
737
+ /** Deny access and show 404 */
738
+ | {
739
+ type: "hidden";
740
+ }
741
+ /** Redirect to another path */
742
+ | {
743
+ type: "redirect";
744
+ to: string;
745
+ };
746
+
747
+ /**
748
+ * Deny access and show 404 Not Found.
749
+ *
750
+ * @example
751
+ * ```tsx
752
+ * const adminOnly: Guard = ({ context }) => {
753
+ * if (context.currentUser.role !== "admin") {
754
+ * return hidden();
755
+ * }
756
+ * return pass();
757
+ * };
758
+ * ```
759
+ */
760
+ export declare const hidden: () => GuardResult;
761
+
657
762
  export declare type I18nLabels<Def extends Record<string, LabelValue> = Record<string, LabelValue>, L extends string = never> = {
658
763
  en: Def;
659
764
  } & {
@@ -735,18 +840,34 @@ declare type LocalizedString = string | ((locale: string) => string);
735
840
  */
736
841
  declare type Module = Omit<CommonPageResource, "meta"> & {
737
842
  _type: "module";
843
+ component?: () => ReactNode;
738
844
  meta: CommonPageResource["meta"] & {
739
845
  icon?: ReactNode;
740
846
  menuItemClickable: boolean;
741
847
  };
742
848
  resources: Array<Resource>;
743
849
  errorBoundary: ErrorBoundaryComponent;
744
- accessControl?: AccessControl;
850
+ guards?: Guard[];
745
851
  loader?: LoaderHandler;
746
852
  };
747
853
 
748
854
  declare type Modules = Array<Module>;
749
855
 
856
+ /**
857
+ * Allow access to the route. Continue to next guard or render component.
858
+ *
859
+ * @example
860
+ * ```tsx
861
+ * const myGuard: Guard = ({ context }) => {
862
+ * if (context.currentUser) {
863
+ * return pass();
864
+ * }
865
+ * return hidden();
866
+ * };
867
+ * ```
868
+ */
869
+ export declare const pass: () => GuardResult;
870
+
750
871
  declare type ReactResourceProps = {
751
872
  /**
752
873
  * React component to render.
@@ -755,30 +876,21 @@ declare type ReactResourceProps = {
755
876
  };
756
877
 
757
878
  /**
758
- * Redirect configuration for module or root component
759
- */
760
- declare type RedirectConfig = {
761
- /**
762
- * Path to redirect to (under /resources/)
763
- */
764
- redirectTo: string;
765
- };
766
-
767
- /**
768
- * Helper function to define a redirect to a resource path
879
+ * Redirect to another path.
769
880
  *
770
- * @param path - Path under `/resources/` (e.g., "dashboard/overview")
881
+ * @param to - Path to redirect to
771
882
  *
772
883
  * @example
773
884
  * ```tsx
774
- * defineModule({
775
- * path: "dashboard",
776
- * component: redirectToResource("dashboard/overview"),
777
- * resources: [overviewResource],
778
- * });
885
+ * const requireAuth: Guard = ({ context }) => {
886
+ * if (!context.currentUser) {
887
+ * return redirectTo("/login");
888
+ * }
889
+ * return pass();
890
+ * };
779
891
  * ```
780
892
  */
781
- export declare function redirectToResource(path: string): RedirectConfig;
893
+ export declare const redirectTo: (to: string) => GuardResult;
782
894
 
783
895
  /**
784
896
  * A resource that can be included in the sub-content in the root resource.
@@ -787,9 +899,10 @@ export declare function redirectToResource(path: string): RedirectConfig;
787
899
  */
788
900
  declare type Resource = CommonPageResource & {
789
901
  _type: "resource";
902
+ component: () => ReactNode;
790
903
  subResources?: Array<Resource>;
791
904
  errorBoundary: ErrorBoundaryComponent;
792
- accessControl?: AccessControl;
905
+ guards?: Guard[];
793
906
  loader?: LoaderHandler;
794
907
  };
795
908
 
@@ -838,7 +951,29 @@ declare type ThemeProviderState = {
838
951
  setTheme: (theme: Theme) => void;
839
952
  };
840
953
 
841
- export declare const useAppShell: () => AppShellContextType;
954
+ /**
955
+ * Hook to access the full AppShell context (both config and data).
956
+ * For better performance, prefer useAppShellConfig() or useAppShellData()
957
+ * depending on what you need, as this hook subscribes to both contexts.
958
+ */
959
+ export declare const useAppShell: () => {
960
+ contextData: ContextData;
961
+ title?: string;
962
+ icon?: ReactNode;
963
+ configurations: RootConfiguration;
964
+ };
965
+
966
+ /**
967
+ * Hook to access only the static configuration.
968
+ * Use this in components that don't need contextData to avoid unnecessary re-renders.
969
+ */
970
+ export declare const useAppShellConfig: () => AppShellConfigContextType;
971
+
972
+ /**
973
+ * Hook to access only the dynamic contextData.
974
+ * Use this in components that need contextData.
975
+ */
976
+ export declare const useAppShellData: () => AppShellDataContextType;
842
977
 
843
978
  export declare const useAuth: () => AuthContextType;
844
979
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/app-shell",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./styles": "./dist/app-shell.css",
@@ -38,8 +38,8 @@
38
38
  "lucide-react": "^0.562.0",
39
39
  "next-themes": "^0.4.6",
40
40
  "oauth4webapi": "^3.8.3",
41
- "react": "^19.2.1",
42
- "react-dom": "^19.2.1",
41
+ "react": "^19.2.4",
42
+ "react-dom": "^19.2.4",
43
43
  "react-hook-form": "^7.54.2",
44
44
  "react-router": "^7.4.0",
45
45
  "sonner": "^2.0.7",
@@ -59,7 +59,7 @@
59
59
  "tailwindcss": "^4.1.3",
60
60
  "tw-animate-css": "^1.4.0",
61
61
  "typescript": "^5",
62
- "vite": "^7.3.0",
62
+ "vite": "^7.3.1",
63
63
  "vite-plugin-dts": "^4.5.0",
64
64
  "vite-plugin-externalize-deps": "^0.10.0",
65
65
  "vite-tsconfig-paths": "^6.0.4",