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