@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,66 @@
1
+ import { Suspense, useMemo, type ComponentType } from 'react';
2
+ import type {
3
+ AnyMenuItemRegistration,
4
+ MenuComponentProps,
5
+ MenuItemComponentProps,
6
+ MenuParams,
7
+ MenuType,
8
+ } from './types';
9
+ import { ErrorBoundary } from '../../../components';
10
+ import { useApp } from '../../context';
11
+
12
+ export interface MenuProps<TMenuType extends MenuType = MenuType> extends MenuComponentProps<TMenuType> {}
13
+
14
+ /**
15
+ * Renders the menu of the given type.
16
+ */
17
+ export function Menu({ type, params }: MenuProps) {
18
+ const {
19
+ components: { MenuContainer, Loading, Error },
20
+ modules,
21
+ } = useApp();
22
+
23
+ const items = useMemo(() => {
24
+ const itemsOfType = modules.flatMap((m) => m.menuItems).filter((item) => item.menu === type);
25
+ const rootItems = itemsOfType.filter((item) => !item.parentId);
26
+ return rootItems.map((item) => (
27
+ <MenuItem key={item.id} type={type} params={params} thisItem={item} allItems={itemsOfType} />
28
+ ));
29
+ }, [type, params, modules]);
30
+
31
+ return (
32
+ <ErrorBoundary fallback={<Error kind="menu" />}>
33
+ <Suspense fallback={<Loading kind="menu" />}>
34
+ <MenuContainer type={type} params={params}>
35
+ {items}
36
+ </MenuContainer>
37
+ </Suspense>
38
+ </ErrorBoundary>
39
+ );
40
+ }
41
+
42
+ interface MenuItemProps {
43
+ type: MenuType;
44
+ params: MenuParams<MenuType>;
45
+ thisItem: AnyMenuItemRegistration;
46
+ allItems: Array<AnyMenuItemRegistration>;
47
+ }
48
+
49
+ function MenuItem({ type, params, thisItem, allItems }: MenuItemProps) {
50
+ const Component = thisItem.component as ComponentType<MenuItemComponentProps>;
51
+
52
+ const children = useMemo(() => {
53
+ const childItems = allItems
54
+ .filter((item) => item.parentId === thisItem.id)
55
+ .map((item) => <MenuItem key={item.id} type={type} params={params} thisItem={item} allItems={allItems} />);
56
+
57
+ // Do not return empty arrays. This plays better with Mantine's NavLink children.
58
+ return childItems.length > 0 ? childItems : undefined;
59
+ }, [type, params, thisItem, allItems]);
60
+
61
+ return (
62
+ <Component id={thisItem.id} type={type} params={params}>
63
+ {children}
64
+ </Component>
65
+ );
66
+ }
@@ -0,0 +1,9 @@
1
+ import type { MenuComponentProps, MenuType } from '.';
2
+
3
+ export interface MenuContainerProps<TMenuType extends MenuType = MenuType> extends MenuComponentProps<TMenuType> {}
4
+
5
+ export function DefaultMenuContainer<TMenuType extends MenuType = MenuType>({
6
+ children,
7
+ }: MenuContainerProps<TMenuType>) {
8
+ return <>{children}</>;
9
+ }
@@ -0,0 +1,46 @@
1
+ import type { ReactNode } from 'react';
2
+ import { NavLink, type NavLinkProps } from '@mantine/core';
3
+ import { NavLink as RouterNavLink } from 'react-router';
4
+ import type { MenuItemComponentProps, MenuType } from '../../..';
5
+
6
+ export interface MenuItemProps<TMenuType extends MenuType = MenuType> extends MenuItemComponentProps<TMenuType> {
7
+ label?: ReactNode;
8
+ description?: ReactNode;
9
+ leftSection?: ReactNode;
10
+ rightSection?: ReactNode;
11
+ to?: string;
12
+ onClick?: () => void;
13
+ }
14
+
15
+ export function DefaultMenuItem<TMenuType extends MenuType = MenuType>({
16
+ id,
17
+ label,
18
+ description,
19
+ leftSection,
20
+ rightSection,
21
+ to,
22
+ onClick,
23
+ children,
24
+ }: MenuItemProps<TMenuType>) {
25
+ const props: NavLinkProps = {
26
+ label,
27
+ description,
28
+ leftSection,
29
+ rightSection,
30
+ onClick,
31
+ };
32
+
33
+ if (to) {
34
+ return (
35
+ <NavLink component={RouterNavLink} to={to} {...props}>
36
+ {children}
37
+ </NavLink>
38
+ );
39
+ }
40
+
41
+ return (
42
+ <NavLink href={`#${id}`} {...props}>
43
+ {children}
44
+ </NavLink>
45
+ );
46
+ }
@@ -0,0 +1,5 @@
1
+ export * from './initializer';
2
+ export * from './Menu';
3
+ export * from './MenuContainer';
4
+ export * from './MenuItem';
5
+ export * from './types';
@@ -0,0 +1,8 @@
1
+ import type { AppInitializer } from '../../types';
2
+ import { DefaultMenuContainer } from './MenuContainer';
3
+ import { DefaultMenuItem } from './MenuItem';
4
+
5
+ export const menuInitializer: AppInitializer = (app) => {
6
+ app.components.MenuContainer ??= DefaultMenuContainer;
7
+ app.components.MenuItem ??= DefaultMenuItem;
8
+ };
@@ -0,0 +1,84 @@
1
+ import type { ComponentType, PropsWithChildren } from 'react';
2
+ import type { MenuContainerProps } from './MenuContainer';
3
+ import type { MenuItemProps } from './MenuItem';
4
+
5
+ declare module '../../types' {
6
+ export interface AppComponents {
7
+ MenuContainer: ComponentType<MenuContainerProps>;
8
+ MenuItem: ComponentType<MenuItemProps>;
9
+ }
10
+ }
11
+
12
+ /**
13
+ * A map of all known menus.
14
+ * This interface can be extended by modules via declaration merging.
15
+ *
16
+ * ```ts
17
+ * declare module '@tiny-server/core' {
18
+ * export interface MenuParamsMap {
19
+ * 'my-menu': MyMenuParams;
20
+ * }
21
+ * }
22
+ * ```
23
+ */
24
+ export interface MenuParamsMap {
25
+ default: {};
26
+ }
27
+
28
+ /**
29
+ * Defines all typed/known menu types.
30
+ */
31
+ export type KnownMenuType = keyof MenuParamsMap;
32
+
33
+ /**
34
+ * Defines a menu type, both typed/known and untyped/unknown.
35
+ */
36
+ export type MenuType = KnownMenuType | (string & {});
37
+
38
+ /**
39
+ * The parameters of the menu with the given `TType`.
40
+ */
41
+ export type MenuParams<TType extends MenuType> = TType extends KnownMenuType ? MenuParamsMap[TType] : unknown;
42
+
43
+ export interface MenuComponentProps<TMenuType extends MenuType = MenuType> extends PropsWithChildren {
44
+ /**
45
+ * The type of the menu that the item belongs to.
46
+ */
47
+ type: TMenuType;
48
+ /**
49
+ * The parameters to be forwarded to the menu item component.
50
+ */
51
+ params: MenuParams<TMenuType>;
52
+ }
53
+
54
+ export interface MenuItemComponentProps<TMenuType extends MenuType = MenuType> extends MenuComponentProps<TMenuType> {
55
+ /**
56
+ * The unique identifier of the menu item.
57
+ */
58
+ id: string;
59
+ }
60
+
61
+ export interface MenuItemRegistration<TMenuType extends MenuType> {
62
+ /**
63
+ * A unique identifier for the menu item.
64
+ */
65
+ id: string;
66
+ /**
67
+ * The unique identifier of the parent menu item.
68
+ * Must reference an existing parent menu item within the same menu.
69
+ * If no parent is found, the child is not rendered.
70
+ */
71
+ parentId?: string;
72
+ /**
73
+ * The menu that the item belongs to.
74
+ */
75
+ menu: TMenuType;
76
+ /**
77
+ * The component to be rendered for the menu item.
78
+ */
79
+ component: ComponentType<MenuItemComponentProps<TMenuType>>;
80
+ }
81
+
82
+ export type AnyMenuItemRegistration = {
83
+ [TMenuType in MenuType]: MenuItemRegistration<TMenuType>;
84
+ }[MenuType];
@@ -0,0 +1,20 @@
1
+ import { Modal } from '@mantine/core';
2
+ import { ModalContent, type ModalComponentProps, type ModalName, type ModalOptions } from '.';
3
+ import { useDisclosure } from '@mantine/hooks';
4
+
5
+ export interface ModalProps<TModalName extends ModalName = ModalName> extends ModalComponentProps<TModalName> {
6
+ options?: ModalOptions;
7
+ }
8
+
9
+ export function DefaultModal({ name, params, options, onClose }: ModalProps) {
10
+ const [opened, { close }] = useDisclosure(true);
11
+
12
+ return (
13
+ <Modal.Root {...options} opened={opened} onClose={close} onExitTransitionEnd={onClose}>
14
+ <Modal.Overlay />
15
+ <Modal.Content>
16
+ <ModalContent name={name} params={params} onClose={close} />
17
+ </Modal.Content>
18
+ </Modal.Root>
19
+ );
20
+ }
@@ -0,0 +1,8 @@
1
+ import { Modal } from '@mantine/core';
2
+ import type { PropsWithChildren } from 'react';
3
+
4
+ export interface ModalBodyProps extends PropsWithChildren {}
5
+
6
+ export function DefaultModalBody({ children }: ModalBodyProps) {
7
+ return <Modal.Body>{children}</Modal.Body>;
8
+ }
@@ -0,0 +1,35 @@
1
+ import { Suspense, useMemo } from 'react';
2
+ import { ErrorBoundary } from '../../../components';
3
+ import type { ModalComponentProps, ModalName } from './types';
4
+ import { useApp } from '../../context';
5
+
6
+ export interface ModalContentProps<TModalName extends ModalName = ModalName> extends ModalComponentProps<TModalName> {}
7
+
8
+ /**
9
+ * Renders registered modal content.
10
+ */
11
+ export function ModalContent({ name, params, onClose }: ModalContentProps) {
12
+ const {
13
+ components: { Loading, Error },
14
+ modules,
15
+ } = useApp();
16
+
17
+ const modalComponents = useMemo(() => {
18
+ return modules
19
+ .flatMap((module) => module.modals)
20
+ .filter((modal) => modal.name === name)
21
+ .map((modal) => modal.component);
22
+ }, [modules, name]);
23
+
24
+ return (
25
+ <>
26
+ {modalComponents.map((Component, index) => (
27
+ <ErrorBoundary key={index} fallback={<Error kind="modal" />}>
28
+ <Suspense fallback={<Loading kind="modal" />}>
29
+ <Component name={name} params={params} onClose={onClose} />
30
+ </Suspense>
31
+ </ErrorBoundary>
32
+ ))}
33
+ </>
34
+ );
35
+ }
@@ -0,0 +1,19 @@
1
+ import { Group, Modal } from '@mantine/core';
2
+ import type { PropsWithChildren, ReactNode } from 'react';
3
+
4
+ export interface ModalFooterProps extends PropsWithChildren {
5
+ actions?: ReactNode;
6
+ }
7
+
8
+ export function DefaultModalFooter({ children, actions }: ModalFooterProps) {
9
+ return (
10
+ <Modal.Body component="footer">
11
+ {children}
12
+ {actions && (
13
+ <Group gap="sm" justify="flex-end">
14
+ {actions}
15
+ </Group>
16
+ )}
17
+ </Modal.Body>
18
+ );
19
+ }
@@ -0,0 +1,15 @@
1
+ import { Modal } from '@mantine/core';
2
+ import type { ReactNode } from 'react';
3
+
4
+ export interface ModalHeaderProps {
5
+ title?: ReactNode;
6
+ }
7
+
8
+ export function DefaultModalHeader({ title }: ModalHeaderProps) {
9
+ return (
10
+ <Modal.Header>
11
+ <Modal.Title>{title}</Modal.Title>
12
+ <Modal.CloseButton />
13
+ </Modal.Header>
14
+ );
15
+ }
@@ -0,0 +1,33 @@
1
+ import type { ModalComponentProps, ModalName } from './types';
2
+ import { useMemo } from 'react';
3
+ import { useStore } from 'zustand';
4
+ import { modalsStore } from './store';
5
+ import { useApp } from '../../context';
6
+
7
+ /**
8
+ * Renders all currently opened application modals.
9
+ */
10
+ export function Modals() {
11
+ const { Modal } = useApp().components;
12
+ const { modals, closeModal } = useStore(modalsStore);
13
+
14
+ const onCloseCallbacks = useMemo(
15
+ () =>
16
+ modals.reduce(
17
+ (acc, { id }) => {
18
+ acc[id] = () => closeModal(id);
19
+ return acc;
20
+ },
21
+ {} as Record<string, ModalComponentProps<ModalName>['onClose']>,
22
+ ),
23
+ [modals, closeModal],
24
+ );
25
+
26
+ return (
27
+ <>
28
+ {modals.map(({ id, name, params }) => {
29
+ return <Modal key={id} name={name} params={params} onClose={onCloseCallbacks[id]} />;
30
+ })}
31
+ </>
32
+ );
33
+ }
@@ -0,0 +1,8 @@
1
+ export * from './initializer';
2
+ export * from './Modal';
3
+ export * from './ModalBody';
4
+ export * from './ModalContent';
5
+ export * from './ModalFooter';
6
+ export * from './ModalHeader';
7
+ export * from './Modals';
8
+ export * from './types';
@@ -0,0 +1,17 @@
1
+ import type { AppInitializer } from '../../types';
2
+ import { DefaultModal } from './Modal';
3
+ import { DefaultModalBody } from './ModalBody';
4
+ import { DefaultModalFooter } from './ModalFooter';
5
+ import { DefaultModalHeader } from './ModalHeader';
6
+ import { modalsStore } from './store';
7
+
8
+ export const modalsInitializer: AppInitializer = (app) => {
9
+ app.components.Modal ??= DefaultModal;
10
+ app.components.ModalBody ??= DefaultModalBody;
11
+ app.components.ModalHeader ??= DefaultModalHeader;
12
+ app.components.ModalFooter ??= DefaultModalFooter;
13
+
14
+ app.showModal ??= (name, params) => {
15
+ modalsStore.getState().openModal(name, params);
16
+ };
17
+ };
@@ -0,0 +1,35 @@
1
+ import { createStore } from 'zustand/vanilla';
2
+ import type { ModalName, ModalParams } from './types';
3
+
4
+ interface OpenModal<TModalName extends ModalName = ModalName> {
5
+ id: string;
6
+ name: TModalName;
7
+ params: ModalParams<TModalName>;
8
+ }
9
+
10
+ interface ModalsStore {
11
+ _lastId: number;
12
+ modals: Array<OpenModal>;
13
+ openModal<TModalName extends ModalName>(name: TModalName, params: ModalParams<TModalName>): string;
14
+ closeModal(id: string): void;
15
+ }
16
+
17
+ export const modalsStore = createStore<ModalsStore>((set) => ({
18
+ _lastId: 0,
19
+ modals: [],
20
+ openModal(name, params) {
21
+ const id = `modal-${this._lastId + 1}`;
22
+
23
+ set((state) => ({
24
+ _lastId: state._lastId + 1,
25
+ modals: [...state.modals, { id, name, params }],
26
+ }));
27
+
28
+ return id;
29
+ },
30
+ closeModal(id) {
31
+ set((state) => ({
32
+ modals: state.modals.filter((modal) => modal.id !== id),
33
+ }));
34
+ },
35
+ }));
@@ -0,0 +1,94 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { ModalProps } from './Modal';
3
+ import type { ModalBodyProps } from './ModalBody';
4
+ import type { ModalFooterProps } from './ModalFooter';
5
+ import type { ModalHeaderProps } from './ModalHeader';
6
+
7
+ declare module '../../types' {
8
+ export interface AppInstance {
9
+ /**
10
+ * Shows a registered modal window.
11
+ * @param name The name of the modal to be shown.
12
+ * @param params The parameters to be passed to the modal.
13
+ */
14
+ showModal<TModalName extends ModalName>(name: TModalName, params: ModalParams<TModalName>): void;
15
+ }
16
+
17
+ export interface AppComponents {
18
+ Modal: ComponentType<ModalProps>;
19
+ ModalBody: ComponentType<ModalBodyProps>;
20
+ ModalFooter: ComponentType<ModalFooterProps>;
21
+ ModalHeader: ComponentType<ModalHeaderProps>;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * A map of all known modals.
27
+ * This interface can be extended by modules via declaration merging.
28
+ *
29
+ * ```ts
30
+ * declare module '@tiny-server/core' {
31
+ * export interface ModalParamsMap {
32
+ * 'my-modal': MyModalParams;
33
+ * }
34
+ * }
35
+ * ```
36
+ */
37
+ export interface ModalParamsMap {}
38
+
39
+ /**
40
+ * Defines all typed/known modal names.
41
+ */
42
+ export type KnownModalName = keyof ModalParamsMap;
43
+
44
+ /**
45
+ * Defines a modal name, both typed/known and untyped/unknown.
46
+ */
47
+ export type ModalName = KnownModalName | (string & {});
48
+
49
+ /**
50
+ * The parameters of the modal with the given `TName`.
51
+ */
52
+ export type ModalParams<TName extends ModalName> = TName extends KnownModalName ? ModalParamsMap[TName] : unknown;
53
+
54
+ export interface ModalComponentProps<TName extends ModalName> {
55
+ /**
56
+ * The modal name.
57
+ */
58
+ name: TName;
59
+ /**
60
+ * The parameters to be forwarded to the modal component.
61
+ */
62
+ params: ModalParams<TName>;
63
+ /**
64
+ * Closes/unmounts the modal.
65
+ */
66
+ onClose(): void;
67
+ }
68
+
69
+ /**
70
+ * Optional configuration values for the modal container.
71
+ */
72
+ export interface ModalOptions {
73
+ /**
74
+ * The modal's size.
75
+ */
76
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'auto' | (string & {});
77
+ }
78
+
79
+ export interface ModalRegistration<TName extends ModalName> {
80
+ /**
81
+ * The name of the modal to be registered.
82
+ */
83
+ name: TName;
84
+ /**
85
+ * Optional configuration values for the modal container.
86
+ */
87
+ options?: ModalOptions;
88
+ /**
89
+ * The component to be rendered for the modal.
90
+ */
91
+ component: ComponentType<ModalComponentProps<TName>>;
92
+ }
93
+
94
+ export type AnyModalRegistration = { [Key in ModalName]: ModalRegistration<Key> }[ModalName];
@@ -0,0 +1,7 @@
1
+ import { QueryClientProvider } from '@tanstack/react-query';
2
+ import type { PropsWithChildren } from 'react';
3
+ import { queryClient } from './client';
4
+
5
+ export function AppQueryClientProvider({ children }: PropsWithChildren) {
6
+ return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
7
+ }
@@ -0,0 +1,3 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+
3
+ export const queryClient = new QueryClient();
@@ -0,0 +1,3 @@
1
+ export * from './AppQueryClientProvider';
2
+ export * from './initializer';
3
+ export * from './types';
@@ -0,0 +1,6 @@
1
+ import type { AppInitializer } from '../../types';
2
+ import { queryClient } from './client';
3
+
4
+ export const queryInitializer: AppInitializer = (app) => {
5
+ app.queryClient ??= queryClient;
6
+ };
@@ -0,0 +1,10 @@
1
+ import type { QueryClient } from '@tanstack/react-query';
2
+
3
+ declare module '../../types' {
4
+ export interface AppInstance {
5
+ /**
6
+ * The application's central {@link QueryClient} instance.
7
+ */
8
+ queryClient: QueryClient;
9
+ }
10
+ }
@@ -0,0 +1,18 @@
1
+ import { Suspense } from 'react';
2
+ import { Outlet } from 'react-router';
3
+ import { useApp } from '../../context';
4
+
5
+ export function AppLayout() {
6
+ const app = useApp();
7
+ const { Loading, Layout } = app.components;
8
+
9
+ return (
10
+ // Error boundary/fallback is defined in the route as errorElement so that it also catches
11
+ // and handles router errors.
12
+ <Suspense fallback={<Loading kind="app" />}>
13
+ <Layout>
14
+ <Outlet />
15
+ </Layout>
16
+ </Suspense>
17
+ );
18
+ }
@@ -0,0 +1,31 @@
1
+ import { Suspense } from 'react';
2
+ import { Outlet, useLocation, useNavigation } from 'react-router';
3
+ import { ErrorBoundary } from '../../../components';
4
+ import { useApp } from '../../context';
5
+
6
+ /**
7
+ * The app's top-level route layout.
8
+ * Acts as a suspense and error boundary for all routes.
9
+ */
10
+ export function AppRouteLayout() {
11
+ const app = useApp();
12
+ const { Loading, Error } = app.components;
13
+ const isNavigating = useNavigation().state === 'loading';
14
+
15
+ // This key forces the below Suspense to remount on every navigation.
16
+ // This counteracts react-router's default startTransition behavior which circumvents Suspense
17
+ // falbacks on navigation - a behavior, that we are explicitly interested in here at the
18
+ // root route because we know that nearly any route rendered here will run into some kind of
19
+ // suspense, e.g., due to chunk loading.
20
+ // We're effectively applying this here:
21
+ // https://react.dev/reference/react/Suspense#resetting-suspense-boundaries-on-navigation
22
+ const suspenseKey = useLocation().key;
23
+
24
+ return (
25
+ <ErrorBoundary fallback={<Error kind="route" />}>
26
+ <Suspense key={suspenseKey} fallback={<Loading kind="route" />}>
27
+ {isNavigating ? <Loading kind="route" /> : <Outlet />}
28
+ </Suspense>
29
+ </ErrorBoundary>
30
+ );
31
+ }
@@ -0,0 +1,45 @@
1
+ import { useMemo } from 'react';
2
+ import { createBrowserRouter, RouterContextProvider, type RouteObject } from 'react-router';
3
+ import { RouterProvider } from 'react-router/dom';
4
+ import { RouterErrorBoundary } from './RouterErrorBoundary';
5
+ import { AppLayout } from './AppLayout';
6
+ import { AppRouteLayout } from './AppRouteLayout';
7
+ import { useApp } from '../../context';
8
+ import { appContext } from './context';
9
+
10
+ /**
11
+ * Renders the app's router based on the app configuration.
12
+ */
13
+ export function AppRouter() {
14
+ const app = useApp();
15
+
16
+ const router = useMemo(() => {
17
+ const { Loading, Error } = app.components;
18
+ const appRoutes = app.modules.flatMap((m) => m.routes);
19
+ const routes: Array<RouteObject> = [
20
+ {
21
+ Component: AppLayout,
22
+ hydrateFallbackElement: <Loading kind="app" />,
23
+ errorElement: <Error kind="app" />,
24
+ children: [
25
+ {
26
+ Component: AppRouteLayout,
27
+ hydrateFallbackElement: <Loading kind="route" />,
28
+ errorElement: <RouterErrorBoundary />,
29
+ children: appRoutes,
30
+ },
31
+ ],
32
+ },
33
+ ];
34
+
35
+ const getContext = () => {
36
+ const provider = new RouterContextProvider();
37
+ provider.set(appContext, app);
38
+ return provider;
39
+ };
40
+
41
+ return createBrowserRouter(routes, { getContext });
42
+ }, [app]);
43
+
44
+ return <RouterProvider router={router} />;
45
+ }
@@ -0,0 +1,20 @@
1
+ import { isRouteErrorResponse, useRouteError } from 'react-router';
2
+ import { useApp } from '../../context';
3
+
4
+ export function RouterErrorBoundary() {
5
+ const app = useApp();
6
+ const { Error } = app.components;
7
+ const error = useRouteError();
8
+
9
+ if (isRouteErrorResponse(error)) {
10
+ if (error.status === 403) {
11
+ return <Error kind="route-forbidden" />;
12
+ }
13
+
14
+ if (error.status === 404) {
15
+ return <Error kind="route-not-found" />;
16
+ }
17
+ }
18
+
19
+ return <Error kind="route" />;
20
+ }