@tiny-server/core 0.0.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.
Files changed (138) hide show
  1. package/dist/api/apiResults.d.ts +48 -0
  2. package/dist/api/entity.d.ts +27 -0
  3. package/dist/api/index.d.ts +8 -0
  4. package/dist/api/paginationOptions.d.ts +13 -0
  5. package/dist/api/permissions.d.ts +55 -0
  6. package/dist/api/problemDetails.d.ts +16 -0
  7. package/dist/api/session.d.ts +38 -0
  8. package/dist/api/sortOptions.d.ts +17 -0
  9. package/dist/api/status.d.ts +27 -0
  10. package/dist/app/App.d.ts +12 -0
  11. package/dist/app/context.d.ts +6 -0
  12. package/dist/app/create.d.ts +12 -0
  13. package/dist/app/features/extensions/Extension.d.ts +32 -0
  14. package/dist/app/features/extensions/index.d.ts +2 -0
  15. package/dist/app/features/extensions/types.d.ts +43 -0
  16. package/dist/app/features/index.d.ts +6 -0
  17. package/dist/app/features/layout/Error.d.ts +15 -0
  18. package/dist/app/features/layout/Layout.d.ts +4 -0
  19. package/dist/app/features/layout/Loading.d.ts +14 -0
  20. package/dist/app/features/layout/Page.d.ts +6 -0
  21. package/dist/app/features/layout/index.d.ts +6 -0
  22. package/dist/app/features/layout/initializer.d.ts +2 -0
  23. package/dist/app/features/layout/types.d.ts +13 -0
  24. package/dist/app/features/menu/Menu.d.ts +7 -0
  25. package/dist/app/features/menu/MenuContainer.d.ts +4 -0
  26. package/dist/app/features/menu/MenuItem.d.ts +11 -0
  27. package/dist/app/features/menu/index.d.ts +5 -0
  28. package/dist/app/features/menu/initializer.d.ts +2 -0
  29. package/dist/app/features/menu/types.d.ts +71 -0
  30. package/dist/app/features/modals/Modal.d.ts +5 -0
  31. package/dist/app/features/modals/ModalBody.d.ts +4 -0
  32. package/dist/app/features/modals/ModalContent.d.ts +7 -0
  33. package/dist/app/features/modals/ModalFooter.d.ts +5 -0
  34. package/dist/app/features/modals/ModalHeader.d.ts +5 -0
  35. package/dist/app/features/modals/Modals.d.ts +4 -0
  36. package/dist/app/features/modals/index.d.ts +8 -0
  37. package/dist/app/features/modals/initializer.d.ts +2 -0
  38. package/dist/app/features/modals/store.d.ts +14 -0
  39. package/dist/app/features/modals/types.d.ts +84 -0
  40. package/dist/app/features/query/AppQueryClientProvider.d.ts +2 -0
  41. package/dist/app/features/query/client.d.ts +2 -0
  42. package/dist/app/features/query/index.d.ts +3 -0
  43. package/dist/app/features/query/initializer.d.ts +2 -0
  44. package/dist/app/features/query/types.d.ts +9 -0
  45. package/dist/app/features/router/AppLayout.d.ts +1 -0
  46. package/dist/app/features/router/AppRouteLayout.d.ts +5 -0
  47. package/dist/app/features/router/AppRouter.d.ts +4 -0
  48. package/dist/app/features/router/RouterErrorBoundary.d.ts +1 -0
  49. package/dist/app/features/router/context.d.ts +5 -0
  50. package/dist/app/features/router/index.d.ts +3 -0
  51. package/dist/app/features/router/middlewares.d.ts +42 -0
  52. package/dist/app/index.d.ts +5 -0
  53. package/dist/app/types.d.ts +78 -0
  54. package/dist/components/ErrorBoundary.d.ts +19 -0
  55. package/dist/components/WithPermissions.d.ts +30 -0
  56. package/dist/components/index.d.ts +2 -0
  57. package/dist/fetch.d.ts +84 -0
  58. package/dist/i18n/InitI18n.d.ts +6 -0
  59. package/dist/i18n/defineLocales.d.ts +10 -0
  60. package/dist/i18n/index.d.ts +3 -0
  61. package/dist/i18n/useGlobalT.d.ts +7 -0
  62. package/dist/index.d.ts +7 -0
  63. package/dist/index.js +557 -0
  64. package/dist/module.d.ts +7 -0
  65. package/dist/utils/dates.d.ts +3 -0
  66. package/dist/utils/index.d.ts +4 -0
  67. package/dist/utils/usePagination.d.ts +31 -0
  68. package/dist/utils/useSearch.d.ts +19 -0
  69. package/dist/utils/useSession.d.ts +22 -0
  70. package/package.json +52 -0
  71. package/src/api/apiResults.ts +50 -0
  72. package/src/api/entity.ts +29 -0
  73. package/src/api/index.ts +8 -0
  74. package/src/api/paginationOptions.ts +13 -0
  75. package/src/api/permissions.ts +63 -0
  76. package/src/api/problemDetails.ts +19 -0
  77. package/src/api/session.ts +51 -0
  78. package/src/api/sortOptions.ts +18 -0
  79. package/src/api/status.ts +34 -0
  80. package/src/app/App.tsx +33 -0
  81. package/src/app/context.ts +17 -0
  82. package/src/app/create.ts +34 -0
  83. package/src/app/features/extensions/Extension.tsx +69 -0
  84. package/src/app/features/extensions/index.ts +2 -0
  85. package/src/app/features/extensions/types.tsx +52 -0
  86. package/src/app/features/index.ts +6 -0
  87. package/src/app/features/layout/Error.tsx +20 -0
  88. package/src/app/features/layout/Layout.tsx +7 -0
  89. package/src/app/features/layout/Loading.tsx +21 -0
  90. package/src/app/features/layout/Page.tsx +42 -0
  91. package/src/app/features/layout/index.ts +6 -0
  92. package/src/app/features/layout/initializer.ts +12 -0
  93. package/src/app/features/layout/types.ts +14 -0
  94. package/src/app/features/menu/Menu.tsx +66 -0
  95. package/src/app/features/menu/MenuContainer.tsx +9 -0
  96. package/src/app/features/menu/MenuItem.tsx +46 -0
  97. package/src/app/features/menu/index.ts +5 -0
  98. package/src/app/features/menu/initializer.ts +8 -0
  99. package/src/app/features/menu/types.ts +84 -0
  100. package/src/app/features/modals/Modal.tsx +20 -0
  101. package/src/app/features/modals/ModalBody.tsx +8 -0
  102. package/src/app/features/modals/ModalContent.tsx +35 -0
  103. package/src/app/features/modals/ModalFooter.tsx +19 -0
  104. package/src/app/features/modals/ModalHeader.tsx +15 -0
  105. package/src/app/features/modals/Modals.tsx +33 -0
  106. package/src/app/features/modals/index.ts +8 -0
  107. package/src/app/features/modals/initializer.ts +17 -0
  108. package/src/app/features/modals/store.ts +35 -0
  109. package/src/app/features/modals/types.ts +94 -0
  110. package/src/app/features/query/AppQueryClientProvider.tsx +7 -0
  111. package/src/app/features/query/client.ts +3 -0
  112. package/src/app/features/query/index.ts +3 -0
  113. package/src/app/features/query/initializer.ts +6 -0
  114. package/src/app/features/query/types.ts +10 -0
  115. package/src/app/features/router/AppLayout.tsx +18 -0
  116. package/src/app/features/router/AppRouteLayout.tsx +31 -0
  117. package/src/app/features/router/AppRouter.tsx +45 -0
  118. package/src/app/features/router/RouterErrorBoundary.tsx +20 -0
  119. package/src/app/features/router/context.ts +7 -0
  120. package/src/app/features/router/index.ts +3 -0
  121. package/src/app/features/router/middlewares.ts +76 -0
  122. package/src/app/index.ts +6 -0
  123. package/src/app/types.ts +82 -0
  124. package/src/components/ErrorBoundary.tsx +34 -0
  125. package/src/components/WithPermissions.tsx +59 -0
  126. package/src/components/index.ts +2 -0
  127. package/src/fetch.ts +185 -0
  128. package/src/i18n/InitI18n.tsx +24 -0
  129. package/src/i18n/defineLocales.ts +59 -0
  130. package/src/i18n/index.ts +3 -0
  131. package/src/i18n/useGlobalT.ts +13 -0
  132. package/src/index.ts +8 -0
  133. package/src/module.tsx +27 -0
  134. package/src/utils/dates.ts +34 -0
  135. package/src/utils/index.ts +4 -0
  136. package/src/utils/usePagination.ts +64 -0
  137. package/src/utils/useSearch.ts +40 -0
  138. package/src/utils/useSession.ts +42 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Wraps the result of an API endpoint and enriches that result with additional metadata.
3
+ */
4
+ export interface ApiResult<T> {
5
+ /**
6
+ * The data returned by the API.
7
+ */
8
+ data: T;
9
+ /**
10
+ * The time when the API result was generated.
11
+ */
12
+ timestamp: string;
13
+ }
14
+
15
+ /**
16
+ * A specialized {@link ApiResult} for lists of data.
17
+ */
18
+ export interface ListApiResult<T> extends ApiResult<Array<T>> {
19
+ /**
20
+ * The total number of items returned.
21
+ */
22
+ count: number;
23
+ }
24
+
25
+ export interface PaginationApiResult<T> extends ListApiResult<T> {
26
+ /**
27
+ * The returned page.
28
+ */
29
+ page: number;
30
+ /**
31
+ * The number of items per page.
32
+ */
33
+ pageSize: number;
34
+ /**
35
+ * The total number of items across all pages.
36
+ */
37
+ totalItems: number;
38
+ /**
39
+ * The total number of pages available.
40
+ */
41
+ totalPages: number;
42
+ /**
43
+ * Whether a previous page exists.
44
+ */
45
+ hasPreviousPage: boolean;
46
+ /**
47
+ * Whether a next page exists.
48
+ */
49
+ hasNextPage: boolean;
50
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Represents a generic entity.
3
+ */
4
+ export interface EntityRead<TId extends string = string> {
5
+ /**
6
+ * The entity's ID.
7
+ */
8
+ id: TId;
9
+ /**
10
+ * The date and time when the entity was created.
11
+ */
12
+ createdOn: string;
13
+ /**
14
+ * The date and time when the entity was last updated.
15
+ */
16
+ updatedOn: string;
17
+ }
18
+
19
+ /**
20
+ * Represents data for creating an entity.
21
+ */
22
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
23
+ export interface EntityCreate {}
24
+
25
+ /**
26
+ * Represents data for updating an entity.
27
+ */
28
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
29
+ export interface EntityUpdate {}
@@ -0,0 +1,8 @@
1
+ export * from './apiResults';
2
+ export * from './entity';
3
+ export * from './paginationOptions';
4
+ export * from './permissions';
5
+ export * from './problemDetails';
6
+ export * from './session';
7
+ export * from './sortOptions';
8
+ export * from './status';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Options for querying pagination APIs.
3
+ */
4
+ export interface PaginationOptions {
5
+ /**
6
+ * The requested page number.
7
+ */
8
+ page?: number | null;
9
+ /**
10
+ * The requested page size.
11
+ */
12
+ pageSize?: number | null;
13
+ }
@@ -0,0 +1,63 @@
1
+ import { queryOptions } from '@tanstack/react-query';
2
+ import { appFetch } from '../fetch';
3
+ import type { ListApiResult } from './apiResults';
4
+
5
+ /**
6
+ * An interface mapping all known backend permissions to an arbitrary value.
7
+ * {@link Permissions} is derived from this interface`s key.
8
+ *
9
+ * You can extend known {@link Permission}s by declaring a module augmentation for this interface.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * declare module "@tiny-server/core" {
14
+ * interface PermissionsMap {
15
+ * ["my.permission"]: "";
16
+ * }
17
+ * }
18
+ *
19
+ * // Results in:
20
+ * const permission: Permission = "my.permission"; // ✅ Valid
21
+ * ```
22
+ */
23
+ export interface PermissionsMap {
24
+ ['read.all']: '';
25
+ ['write.all']: '';
26
+ }
27
+
28
+ /**
29
+ * Declares all known backend permissions.
30
+ */
31
+ export type Permission = keyof PermissionsMap;
32
+
33
+ /**
34
+ * Represents a permission with its hierarchy information.
35
+ */
36
+ export interface PermissionRead {
37
+ /**
38
+ * The permission`s name.
39
+ */
40
+ name: Permission;
41
+ /**
42
+ * The permission`s direct parent names.
43
+ */
44
+ parents: Array<Permission>;
45
+ /**
46
+ * All parent names, including parents of parents, and so on.
47
+ */
48
+ allParents: Array<Permission>;
49
+ }
50
+
51
+ /** `GET /api/v1/permissions` */
52
+ export function readPermissions(): Promise<ListApiResult<PermissionRead>> {
53
+ return appFetch<ListApiResult<PermissionRead>>('/api/v1/permissions');
54
+ }
55
+
56
+ /** {@link readPermissions} */
57
+ export function readPermissionsQuery() {
58
+ return queryOptions({
59
+ queryKey: ['permissions'],
60
+ queryFn: () => readPermissions(),
61
+ staleTime: Infinity,
62
+ });
63
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
3
+ */
4
+ export interface ProblemDetails {
5
+ type?: string | null;
6
+ title?: string | null;
7
+ status?: number | null;
8
+ detail?: string | null;
9
+ instance?: string | null;
10
+ errors?: Record<string, Array<string>>;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ /**
15
+ * Returns whether the given object is a {@link ProblemDetails} object.
16
+ */
17
+ export function isProblemDetails(obj: unknown): obj is ProblemDetails {
18
+ return !!obj && typeof obj === 'object';
19
+ }
@@ -0,0 +1,51 @@
1
+ import { queryOptions } from '@tanstack/react-query';
2
+ import { appFetch, AppFetchError } from '../fetch';
3
+ import type { ApiResult } from './apiResults';
4
+ import type { Permission } from './permissions';
5
+
6
+ /**
7
+ * Provides information about the current user session.
8
+ */
9
+ export interface SessionRead {
10
+ /**
11
+ * The identifier of the user associated with the current session.
12
+ */
13
+ userId: string;
14
+ /**
15
+ * All permissions associated with the session's user.
16
+ */
17
+ permissions: Array<Permission>;
18
+ }
19
+
20
+ /**
21
+ * Represents the current session state, i.e., whether the user has an active session or not.
22
+ */
23
+ export type SessionState = { hasSession: false; session: undefined } | { hasSession: true; session: SessionRead };
24
+
25
+ /**
26
+ * Calls `GET /api/v1/auth/session` and extracts the current {@link SessionState} from the response.
27
+ * Throws for any errors other than `401 Unauthorized`.
28
+ */
29
+ export async function readSessionState(): Promise<SessionState> {
30
+ try {
31
+ const { data } = await appFetch<ApiResult<SessionRead>>('/api/v1/auth/session');
32
+ return { hasSession: true, session: data };
33
+ } catch (e) {
34
+ if (e instanceof AppFetchError && e.response.status === 401) {
35
+ return { hasSession: false, session: undefined };
36
+ }
37
+
38
+ // We want error/retry semantics for unknown/unexpected errors.
39
+ throw e;
40
+ }
41
+ }
42
+
43
+ /** {@link readSessionState} */
44
+ export function readSessionStateQuery() {
45
+ return queryOptions({
46
+ queryKey: ['session'],
47
+ queryFn: () => readSessionState(),
48
+ staleTime: Infinity,
49
+ gcTime: Infinity,
50
+ });
51
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Defines the possible sort directions for sorting operations.
3
+ */
4
+ export type SortDirection = 'Ascending' | 'Descending';
5
+
6
+ /**
7
+ * Defines options for sorting items.
8
+ */
9
+ export interface SortOptions<TKey extends string> {
10
+ /**
11
+ * The key to sort by.
12
+ */
13
+ sortBy?: TKey | null;
14
+ /**
15
+ * The direction of the sort operation.
16
+ */
17
+ sortDirection?: SortDirection | null;
18
+ }
@@ -0,0 +1,34 @@
1
+ import { appFetch } from '../fetch';
2
+ import type { Permission } from './permissions';
3
+ import { queryOptions } from '@tanstack/react-query';
4
+ import type { ApiResult } from './apiResults';
5
+
6
+ export type StatusVersion = `${number}.${number}.${number}`;
7
+
8
+ /**
9
+ * Provides status information about the service.
10
+ */
11
+ export interface StatusRead {
12
+ /**
13
+ * The service version.
14
+ */
15
+ version: StatusVersion;
16
+ /**
17
+ * All known permissions.
18
+ */
19
+ permissions: Array<Permission>;
20
+ }
21
+
22
+ /** `GET api/v1/status` */
23
+ export function readStatus(): Promise<ApiResult<StatusRead>> {
24
+ return appFetch<ApiResult<StatusRead>>('/api/v1/status');
25
+ }
26
+
27
+ /** {@link readStatus} */
28
+ export function readStatusQuery() {
29
+ return queryOptions({
30
+ queryKey: ['status'],
31
+ queryFn: () => readStatus(),
32
+ staleTime: Infinity,
33
+ });
34
+ }
@@ -0,0 +1,33 @@
1
+ import { AppRouter } from './features/router';
2
+ import { appContext } from './context';
3
+ import type { AppInstance } from './types';
4
+ import { useCallback, type FunctionComponent, type PropsWithChildren } from 'react';
5
+
6
+ export interface AppProps extends PropsWithChildren {
7
+ /**
8
+ * The application instance to be rendered.
9
+ */
10
+ app: AppInstance;
11
+ }
12
+
13
+ /**
14
+ * The main application component, rendering an application and its configured modules.
15
+ */
16
+ export function App({ app, children }: AppProps) {
17
+ const AppProviders = useCallback<FunctionComponent<PropsWithChildren>>(
18
+ ({ children }) => {
19
+ const providers = app.modules.flatMap((m) => m.providers);
20
+ return providers.reduceRight((tree, Provider) => <Provider>{tree}</Provider>, children);
21
+ },
22
+ [app.modules],
23
+ );
24
+
25
+ return (
26
+ <appContext.Provider value={app}>
27
+ <AppProviders>
28
+ <AppRouter />
29
+ {children}
30
+ </AppProviders>
31
+ </appContext.Provider>
32
+ );
33
+ }
@@ -0,0 +1,17 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { AppInstance } from './types';
3
+
4
+ export const appContext = createContext<AppInstance>(null!);
5
+
6
+ /**
7
+ * Retrieves the configured {@link AppInstance}.
8
+ */
9
+ export function useApp(): AppInstance {
10
+ const app = useContext(appContext);
11
+
12
+ if (!app) {
13
+ throw new Error('App context had no configured value. Was the hook called outside of an <App> component?');
14
+ }
15
+
16
+ return app;
17
+ }
@@ -0,0 +1,34 @@
1
+ import type { AppInstance, Module, ModuleInit } from './types';
2
+
3
+ /**
4
+ * Creates an {@link AppInstance} based on the provided configuration.
5
+ * {@link AppInstance} objects provide the app's central orchestration API.
6
+ * @param modules The modules making up the application.
7
+ */
8
+ export function createApp(modules: Array<Module>): AppInstance {
9
+ let app: AppInstance = {
10
+ modules,
11
+ components: {},
12
+ } as AppInstance;
13
+
14
+ for (const initializer of modules.flatMap((m) => m.appInitializers)) {
15
+ app = initializer(app) ?? app;
16
+ }
17
+
18
+ return app;
19
+ }
20
+
21
+ /**
22
+ * Creates a fully configured {@link Module} based on the given {@link ModuleInit}.
23
+ * @param moduleInit The configuration with which to initialize the module.
24
+ */
25
+ export function createModule(moduleInit: ModuleInit): Module {
26
+ return {
27
+ appInitializers: moduleInit.appInitializers ?? [],
28
+ routes: moduleInit.routes ?? [],
29
+ extensions: moduleInit.extensions ?? [],
30
+ menuItems: moduleInit.menuItems ?? [],
31
+ modals: moduleInit.modals ?? [],
32
+ providers: moduleInit.providers ?? [],
33
+ };
34
+ }
@@ -0,0 +1,69 @@
1
+ import { type ReactNode, useMemo, Suspense, type ComponentType } from 'react';
2
+ import { ErrorBoundary } from '../../../components';
3
+ import type { ExtensionComponentProps, ExtensionName, ExtensionParams } from './types';
4
+ import { useApp } from '../../context';
5
+
6
+ export interface ExtensionProps<TName extends ExtensionName> {
7
+ /**
8
+ * The name of the extension to render.
9
+ */
10
+ name: TName;
11
+ /**
12
+ * The parameters to be forwarded to the extension component.
13
+ */
14
+ params: ExtensionParams<TName>;
15
+ /**
16
+ * An optional fallback to render if:
17
+ * - there is no extension with the given name.
18
+ * - no {@link loadingFallback} is provided and the extension is loading.
19
+ * - no {@link errorFallback} is provided and the extension errored.
20
+ */
21
+ fallback?: ReactNode;
22
+ /**
23
+ * An optional fallback to render while the extension is being lazy-loaded.
24
+ */
25
+ loadingFallback?: ReactNode;
26
+ /**
27
+ * An optional fallback to render if an error is caught while rendering the extension.
28
+ */
29
+ errorFallback?: ReactNode;
30
+ }
31
+
32
+ /**
33
+ * Renders registered extension componentsmatching the given name.
34
+ * If no such extension components are known, renders a fallback.
35
+ */
36
+ export function Extension<TName extends ExtensionName>({
37
+ name,
38
+ params,
39
+ fallback,
40
+ loadingFallback,
41
+ errorFallback,
42
+ }: ExtensionProps<TName>) {
43
+ const app = useApp();
44
+ const { Loading, Error } = app.components;
45
+
46
+ loadingFallback ??= <Loading kind="extension" />;
47
+ errorFallback ??= <Error kind="extension" />;
48
+
49
+ const extensionComponents = useMemo(() => {
50
+ return app.modules
51
+ .flatMap((m) => m.extensions)
52
+ .filter((registration) => registration.name === name)
53
+ .map((registration) => registration.component) as Array<ComponentType<ExtensionComponentProps<ExtensionName>>>;
54
+ }, [name, app.modules]);
55
+
56
+ if (extensionComponents.length === 0) {
57
+ return fallback;
58
+ }
59
+
60
+ return (
61
+ <ErrorBoundary fallback={errorFallback ?? fallback}>
62
+ <Suspense fallback={loadingFallback ?? fallback}>
63
+ {extensionComponents.map((ExtensionComponent, index) => (
64
+ <ExtensionComponent key={index} params={params} />
65
+ ))}
66
+ </Suspense>
67
+ </ErrorBoundary>
68
+ );
69
+ }
@@ -0,0 +1,2 @@
1
+ export * from './Extension';
2
+ export * from './types';
@@ -0,0 +1,52 @@
1
+ import type { ComponentType } from 'react';
2
+
3
+ /**
4
+ * Maps extension keys to their corresponding component props.
5
+ * This interface can be extended by modules via declaration merging.
6
+ *
7
+ * ```ts
8
+ * declare module '@tiny-server/core' {
9
+ * export interface ExtensionParamsMap {
10
+ * 'my-extension': MyExtensionProps;
11
+ * }
12
+ * }
13
+ * ```
14
+ */
15
+ export interface ExtensionParamsMap {}
16
+
17
+ /**
18
+ * Defines all typed/known extension names.
19
+ */
20
+ export type KnownExtensionName = keyof ExtensionParamsMap;
21
+
22
+ /**
23
+ * Defines an extension name, both typed/known and untyped/unknown.
24
+ */
25
+ export type ExtensionName = KnownExtensionName | (string & {});
26
+
27
+ /**
28
+ * The parameters of the extension with the given `TName`.
29
+ */
30
+ export type ExtensionParams<TName extends ExtensionName> = TName extends KnownExtensionName
31
+ ? ExtensionParamsMap[TName]
32
+ : Record<string, unknown>;
33
+
34
+ export interface ExtensionComponentProps<TName extends ExtensionName> {
35
+ /**
36
+ * The parameters to be forwarded to the extension component.
37
+ */
38
+ params: ExtensionParams<TName>;
39
+ }
40
+
41
+ export interface ExtensionRegistration<TName extends ExtensionName> {
42
+ /**
43
+ * The name of the extension to be registered.
44
+ */
45
+ name: TName;
46
+ /**
47
+ * The component to be rendered for the extension.
48
+ */
49
+ component: ComponentType<ExtensionComponentProps<TName>>;
50
+ }
51
+
52
+ export type AnyExtensionRegistration = { [Key in ExtensionName]: ExtensionRegistration<Key> }[ExtensionName];
@@ -0,0 +1,6 @@
1
+ export * from './extensions';
2
+ export * from './layout';
3
+ export * from './menu';
4
+ export * from './modals';
5
+ export * from './query';
6
+ export * from './router';
@@ -0,0 +1,20 @@
1
+ export interface ErrorKindMap {
2
+ unknown: '';
3
+ app: '';
4
+ route: '';
5
+ ['route-not-found']: '';
6
+ ['route-forbidden']: '';
7
+ extension: '';
8
+ menu: '';
9
+ modal: '';
10
+ }
11
+
12
+ export type ErrorKind = keyof ErrorKindMap;
13
+
14
+ export interface ErrorProps {
15
+ kind: ErrorKind;
16
+ }
17
+
18
+ export function DefaultError({ kind }: ErrorProps) {
19
+ return <>Error: {kind}</>;
20
+ }
@@ -0,0 +1,7 @@
1
+ import type { PropsWithChildren } from 'react';
2
+
3
+ export interface LayoutProps extends PropsWithChildren {}
4
+
5
+ export function DefaultLayout({ children }: LayoutProps) {
6
+ return <>{children}</>;
7
+ }
@@ -0,0 +1,21 @@
1
+ import { LoadingOverlay } from '@mantine/core';
2
+
3
+ export interface LoadingKindMap {
4
+ unknown: '';
5
+ app: '';
6
+ route: '';
7
+ page: '';
8
+ extension: '';
9
+ menu: '';
10
+ modal: '';
11
+ }
12
+
13
+ export type LoadingKind = keyof LoadingKindMap;
14
+
15
+ export interface LoadingProps {
16
+ kind: LoadingKind;
17
+ }
18
+
19
+ export function DefaultLoading() {
20
+ return <LoadingOverlay visible />;
21
+ }
@@ -0,0 +1,42 @@
1
+ import { Container, Group, Grid, GridCol, Stack, Title, Box } from '@mantine/core';
2
+ import { Suspense, type PropsWithChildren, type ReactNode } from 'react';
3
+ import { useApp } from '../../context';
4
+
5
+ export interface PageProps extends PropsWithChildren {
6
+ header?: ReactNode;
7
+ actions?: ReactNode;
8
+ }
9
+
10
+ export function DefaultPage({ header, actions, children }: PageProps) {
11
+ const { Loading } = useApp().components;
12
+
13
+ return (
14
+ <Box h="100%" p="xl">
15
+ <Container h="100%">
16
+ <Stack gap="xl">
17
+ <Grid gap="md" align="center">
18
+ <GridCol span={{ base: 12, md: 'auto' }}>
19
+ <Title
20
+ order={1}
21
+ textWrap="nowrap"
22
+ miw={0}
23
+ style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
24
+ {header}
25
+ </Title>
26
+ </GridCol>
27
+
28
+ <GridCol span={{ base: 12, md: 'content' }}>
29
+ <Group gap="md" wrap="wrap">
30
+ {actions}
31
+ </Group>
32
+ </GridCol>
33
+ </Grid>
34
+
35
+ <Suspense fallback={<Loading kind="page" />}>
36
+ <div>{children}</div>
37
+ </Suspense>
38
+ </Stack>
39
+ </Container>
40
+ </Box>
41
+ );
42
+ }
@@ -0,0 +1,6 @@
1
+ export * from './Error';
2
+ export * from './initializer';
3
+ export * from './Layout';
4
+ export * from './Loading';
5
+ export * from './Page';
6
+ export * from './types';
@@ -0,0 +1,12 @@
1
+ import type { AppInitializer } from '../../types';
2
+ import { DefaultError } from './Error';
3
+ import { DefaultLayout } from './Layout';
4
+ import { DefaultLoading } from './Loading';
5
+ import { DefaultPage } from './Page';
6
+
7
+ export const layoutInitializer: AppInitializer = (app) => {
8
+ app.components.Error ??= DefaultError;
9
+ app.components.Layout ??= DefaultLayout;
10
+ app.components.Loading ??= DefaultLoading;
11
+ app.components.Page ??= DefaultPage;
12
+ };
@@ -0,0 +1,14 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { ErrorProps } from './Error';
3
+ import type { LayoutProps } from './Layout';
4
+ import type { LoadingProps } from './Loading';
5
+ import type { PageProps } from './Page';
6
+
7
+ declare module '../../types' {
8
+ export interface AppComponents {
9
+ Error: ComponentType<ErrorProps>;
10
+ Layout: ComponentType<LayoutProps>;
11
+ Loading: ComponentType<LoadingProps>;
12
+ Page: ComponentType<PageProps>;
13
+ }
14
+ }