@bbki.ng/ui 0.1.1

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 (97) hide show
  1. package/.storybook/main.ts +21 -0
  2. package/.storybook/preview-head.html +10 -0
  3. package/.storybook/preview.tsx +30 -0
  4. package/CHANGELOG.md +8 -0
  5. package/README.md +124 -0
  6. package/package.json +57 -0
  7. package/scripts/build-tokens.ts +170 -0
  8. package/src/atoms/blink-dot/BlinkDot.stories.tsx +44 -0
  9. package/src/atoms/blink-dot/BlinkDot.tsx +45 -0
  10. package/src/atoms/blink-dot/index.ts +2 -0
  11. package/src/atoms/button/Button.stories.tsx +84 -0
  12. package/src/atoms/button/Button.tsx +59 -0
  13. package/src/atoms/button/Button.types.ts +20 -0
  14. package/src/atoms/button/Button.variants.ts +58 -0
  15. package/src/atoms/button/index.ts +3 -0
  16. package/src/atoms/link/Link.stories.tsx +121 -0
  17. package/src/atoms/link/Link.tsx +69 -0
  18. package/src/atoms/link/Link.types.ts +26 -0
  19. package/src/atoms/link/Link.variants.ts +55 -0
  20. package/src/atoms/link/index.ts +3 -0
  21. package/src/atoms/logo/Logo.stories.tsx +37 -0
  22. package/src/atoms/logo/Logo.tsx +49 -0
  23. package/src/atoms/logo/Logo.types.ts +4 -0
  24. package/src/atoms/logo/index.ts +2 -0
  25. package/src/index.ts +54 -0
  26. package/src/layout/container/Container.stories.tsx +73 -0
  27. package/src/layout/container/Container.tsx +57 -0
  28. package/src/layout/container/index.ts +2 -0
  29. package/src/layout/grid/Grid.stories.tsx +106 -0
  30. package/src/layout/grid/Grid.tsx +86 -0
  31. package/src/layout/grid/index.ts +2 -0
  32. package/src/layout/index.ts +4 -0
  33. package/src/molecules/article/Article.stories.tsx +45 -0
  34. package/src/molecules/article/Article.tsx +25 -0
  35. package/src/molecules/article/Article.types.ts +11 -0
  36. package/src/molecules/article/index.ts +2 -0
  37. package/src/molecules/breadcrumb/Breadcrumb.stories.tsx +60 -0
  38. package/src/molecules/breadcrumb/Breadcrumb.tsx +43 -0
  39. package/src/molecules/breadcrumb/Breadcrumb.types.ts +19 -0
  40. package/src/molecules/breadcrumb/index.ts +2 -0
  41. package/src/molecules/list/List.stories.tsx +84 -0
  42. package/src/molecules/list/List.tsx +79 -0
  43. package/src/molecules/list/List.types.ts +23 -0
  44. package/src/molecules/list/index.ts +2 -0
  45. package/src/molecules/nav/Nav.stories.tsx +45 -0
  46. package/src/molecules/nav/Nav.tsx +29 -0
  47. package/src/molecules/nav/Nav.types.ts +10 -0
  48. package/src/molecules/nav/index.ts +2 -0
  49. package/src/molecules/panel/Panel.stories.tsx +42 -0
  50. package/src/molecules/panel/Panel.tsx +27 -0
  51. package/src/molecules/panel/Panel.types.ts +6 -0
  52. package/src/molecules/panel/index.ts +2 -0
  53. package/src/molecules/table/Table.stories.tsx +54 -0
  54. package/src/molecules/table/Table.tsx +31 -0
  55. package/src/molecules/table/Table.types.ts +20 -0
  56. package/src/molecules/table/index.ts +2 -0
  57. package/src/organisms/canvas/Canvas.tsx +79 -0
  58. package/src/organisms/canvas/Canvas.types.ts +25 -0
  59. package/src/organisms/canvas/index.ts +3 -0
  60. package/src/organisms/canvas/useRenderer.ts +44 -0
  61. package/src/organisms/drop-image/DropImage.stories.tsx +36 -0
  62. package/src/organisms/drop-image/DropImage.tsx +193 -0
  63. package/src/organisms/drop-image/DropImage.types.ts +26 -0
  64. package/src/organisms/drop-image/index.ts +3 -0
  65. package/src/organisms/drop-image/useDropImage.ts +124 -0
  66. package/src/organisms/drop-image/utils.ts +1 -0
  67. package/src/organisms/drop-zone/DropZone.tsx +58 -0
  68. package/src/organisms/drop-zone/DropZone.types.ts +9 -0
  69. package/src/organisms/drop-zone/index.ts +2 -0
  70. package/src/organisms/loading-spiral/LoadingSpiral.stories.tsx +30 -0
  71. package/src/organisms/loading-spiral/LoadingSpiral.tsx +44 -0
  72. package/src/organisms/loading-spiral/LoadingSpiral.types.ts +4 -0
  73. package/src/organisms/loading-spiral/constants.ts +62 -0
  74. package/src/organisms/loading-spiral/createOptions.ts +31 -0
  75. package/src/organisms/loading-spiral/createSettings.ts +26 -0
  76. package/src/organisms/loading-spiral/index.ts +2 -0
  77. package/src/organisms/loading-spiral/useCanvasRef.ts +23 -0
  78. package/src/organisms/loading-spiral/utils.ts +5 -0
  79. package/src/organisms/page/Page.stories.tsx +65 -0
  80. package/src/organisms/page/Page.tsx +71 -0
  81. package/src/organisms/page/Page.types.ts +23 -0
  82. package/src/organisms/page/index.ts +8 -0
  83. package/src/styles.css +151 -0
  84. package/src/theme/ThemeContext.tsx +20 -0
  85. package/src/theme/ThemeProvider.tsx +93 -0
  86. package/src/theme/index.ts +3 -0
  87. package/src/tokens/css/dark.css +111 -0
  88. package/src/tokens/css/light.css +111 -0
  89. package/src/tokens/index.ts +127 -0
  90. package/tokens/base/colors.json +54 -0
  91. package/tokens/base/shadows.json +34 -0
  92. package/tokens/base/spacing.json +21 -0
  93. package/tokens/base/typography.json +35 -0
  94. package/tokens/semantic/dark.json +50 -0
  95. package/tokens/semantic/light.json +54 -0
  96. package/tsconfig.json +22 -0
  97. package/vite.config.ts +44 -0
@@ -0,0 +1,124 @@
1
+ import { useState, useCallback, useRef, DragEvent, DragEventHandler } from 'react';
2
+
3
+ export const useDropImage = (params?: {
4
+ portraitImageWidth?: number;
5
+ landscapeImageWidth?: number;
6
+ onDrop?: (e: DragEvent<Element>, file: File) => void;
7
+ onImageLoad?: (img: HTMLImageElement, file: File) => void;
8
+ }) => {
9
+ const [isDragOver, setIsDragOver] = useState(false);
10
+ const [imageSrc, setImageSrc] = useState('');
11
+ const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
12
+ const imageFile = useRef<File | null>();
13
+
14
+ const {
15
+ portraitImageWidth = 384,
16
+ landscapeImageWidth = 500,
17
+ onDrop = () => {},
18
+ onImageLoad = () => {},
19
+ } = params || {};
20
+
21
+ const reset = () => {
22
+ setImageSrc('');
23
+ setImageSize({ width: 0, height: 0 });
24
+ setIsDragOver(false);
25
+ imageFile.current = null;
26
+ };
27
+
28
+ const calcDefaultImgSize = (
29
+ img: { width: number; height: number },
30
+ defaultWidth?: number
31
+ ): { width: number; height: number } => {
32
+ const { width, height } = img;
33
+ const whRatio = width / height;
34
+ const isHorizontal = width > height;
35
+
36
+ const finalWidth = defaultWidth || (isHorizontal ? landscapeImageWidth : portraitImageWidth);
37
+
38
+ return {
39
+ width: finalWidth,
40
+ height: finalWidth / whRatio,
41
+ };
42
+ };
43
+
44
+ const setPreviewImageSrcByFile = (file: File) => {
45
+ try {
46
+ setImageSrc(URL.createObjectURL(file));
47
+ } catch (e) {
48
+ setImageSrc('');
49
+ }
50
+ };
51
+
52
+ const handleDragOver: DragEventHandler = useCallback(ev => {
53
+ ev.preventDefault();
54
+ setIsDragOver(true);
55
+ if (!ev.dataTransfer) {
56
+ return;
57
+ }
58
+ ev.dataTransfer.dropEffect = 'move';
59
+ }, []);
60
+
61
+ const handleDragLeave = useCallback(() => {
62
+ setIsDragOver(false);
63
+ }, []);
64
+
65
+ const handleDrop: DragEventHandler = useCallback(
66
+ ev => {
67
+ ev.preventDefault();
68
+ setIsDragOver(false);
69
+ const file = ev.dataTransfer ? ev.dataTransfer.files[0] : undefined;
70
+ if (!file || !file.type.startsWith('image')) {
71
+ return;
72
+ }
73
+ imageFile.current = file;
74
+ setPreviewImageSrcByFile(file);
75
+ onDrop(ev, file);
76
+ },
77
+ [onDrop]
78
+ );
79
+
80
+ const handleImgLoad = (img: HTMLImageElement) => {
81
+ const updateFunc = async () => {
82
+ const p = 'decode' in img ? img.decode : Promise.resolve;
83
+ try {
84
+ await p();
85
+ } catch (e) {}
86
+ setImageSize(
87
+ calcDefaultImgSize({
88
+ width: img.naturalWidth,
89
+ height: img.naturalHeight,
90
+ })
91
+ );
92
+ if (!imageFile.current) {
93
+ return;
94
+ }
95
+ onImageLoad(img, imageFile.current);
96
+ };
97
+
98
+ if (img.complete) {
99
+ updateFunc().then();
100
+ return;
101
+ }
102
+
103
+ img.onload = updateFunc;
104
+ };
105
+
106
+ const imageRef = useCallback((input: HTMLImageElement) => {
107
+ if (!input) {
108
+ return;
109
+ }
110
+
111
+ handleImgLoad(input);
112
+ }, []);
113
+
114
+ return {
115
+ isDragOver,
116
+ imageSrc,
117
+ imageRef,
118
+ imageSize,
119
+ handleDragOver,
120
+ handleDragLeave,
121
+ handleDrop,
122
+ reset,
123
+ };
124
+ };
@@ -0,0 +1 @@
1
+ export const wait = (d: number) => new Promise(r => setTimeout(r, d));
@@ -0,0 +1,58 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import { DropZoneProps } from './DropZone.types';
4
+
5
+ export const DropZone = (props: DropZoneProps) => {
6
+ const { onDrop, children, className, style, disabled } = props;
7
+ const [coverVisible, setCoverVisible] = useState(false);
8
+
9
+ const handleDrop = (e: React.DragEvent) => {
10
+ e.preventDefault();
11
+ setCoverVisible(false);
12
+ const file = e.dataTransfer.files[0];
13
+ onDrop(file);
14
+ };
15
+
16
+ const handleDocDragEnter = useCallback((e: DragEvent) => {
17
+ e.preventDefault();
18
+ setCoverVisible(true);
19
+ }, []);
20
+
21
+ useEffect(() => {
22
+ document.addEventListener('dragenter', handleDocDragEnter);
23
+
24
+ return () => {
25
+ document.removeEventListener('dragenter', handleDocDragEnter);
26
+ };
27
+ }, [handleDocDragEnter]);
28
+
29
+ if (disabled) {
30
+ return <>{children}</>;
31
+ }
32
+
33
+ const handleDragOver = (e: React.DragEvent) => {
34
+ e.preventDefault();
35
+ setCoverVisible(true);
36
+ };
37
+
38
+ return (
39
+ <>
40
+ <div
41
+ className={twMerge(
42
+ 'fixed top-0 left-0 h-full w-full',
43
+ 'transition-all duration-200',
44
+ 'backdrop-blur-sm bg-background/50',
45
+ coverVisible ? 'z-[999] opacity-100' : 'z-[-1] opacity-0 pointer-events-none',
46
+ className
47
+ )}
48
+ style={style}
49
+ onDragLeave={() => setCoverVisible(false)}
50
+ onDragOver={handleDragOver}
51
+ onDrop={handleDrop}
52
+ />
53
+ {children}
54
+ </>
55
+ );
56
+ };
57
+
58
+ DropZone.displayName = 'DropZone';
@@ -0,0 +1,9 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+
3
+ export interface DropZoneProps {
4
+ onDrop: (file: File) => void;
5
+ className?: string;
6
+ style?: CSSProperties;
7
+ children?: ReactNode;
8
+ disabled?: boolean;
9
+ }
@@ -0,0 +1,2 @@
1
+ export { DropZone } from './DropZone';
2
+ export type { DropZoneProps } from './DropZone.types';
@@ -0,0 +1,30 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { LoadingSpiral } from './LoadingSpiral';
3
+
4
+ const meta = {
5
+ title: 'Organisms/LoadingSpiral',
6
+ component: LoadingSpiral,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+ } satisfies Meta<typeof LoadingSpiral>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const Default = {
17
+ args: {},
18
+ } satisfies Story;
19
+
20
+ export const Fast = {
21
+ args: {
22
+ step: 0.2,
23
+ },
24
+ } satisfies Story;
25
+
26
+ export const Slow = {
27
+ args: {
28
+ step: 0.03,
29
+ },
30
+ } satisfies Story;
@@ -0,0 +1,44 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import Phenomenon from 'phenomenon';
3
+ import { twMerge } from 'tailwind-merge';
4
+ import { createSettings } from './createSettings';
5
+ import { createOptions } from './createOptions';
6
+ import { LoadingSpiralProps } from './LoadingSpiral.types';
7
+
8
+ export const LoadingSpiral = (props?: LoadingSpiralProps) => {
9
+ const { className, step } = props || {};
10
+ const canvasRef = useRef<HTMLCanvasElement>(null);
11
+
12
+ useEffect(() => {
13
+ if (!canvasRef.current) {
14
+ return;
15
+ }
16
+
17
+ const phenomenon = new Phenomenon({
18
+ settings: createSettings({ canvas: canvasRef.current, step }),
19
+ context: {
20
+ antialias: true,
21
+ alpha: true,
22
+ },
23
+ });
24
+
25
+ phenomenon.add('spiral', createOptions());
26
+ }, [step]);
27
+
28
+ return (
29
+ <canvas
30
+ style={{
31
+ maxWidth: 500,
32
+ maxHeight: 500,
33
+ imageRendering: 'pixelated',
34
+ }}
35
+ ref={canvasRef}
36
+ className={twMerge(
37
+ 'h-full w-full overflow-hidden flex justify-center items-center aspect-square',
38
+ className
39
+ )}
40
+ />
41
+ );
42
+ };
43
+
44
+ LoadingSpiral.displayName = 'LoadingSpiral';
@@ -0,0 +1,4 @@
1
+ export interface LoadingSpiralProps {
2
+ className?: string;
3
+ step?: number;
4
+ }
@@ -0,0 +1,62 @@
1
+ export enum ATTR {
2
+ PERCENT = 'aPercent',
3
+ POINT_SIZE = 'aPointSize',
4
+ }
5
+
6
+ export const VERTEX_SHADER = `
7
+ attribute float ${ATTR.PERCENT};
8
+ attribute float ${ATTR.POINT_SIZE};
9
+
10
+ uniform mat4 uProjectionMatrix;
11
+ uniform mat4 uModelMatrix;
12
+ uniform mat4 uViewMatrix;
13
+ uniform float uProgress;
14
+
15
+ varying vec3 vColor;
16
+
17
+ vec3 curve(float _percent) {
18
+ const float PI2 = 3.141592653589793 * 2.0;
19
+ const float _length = 0.3;
20
+ const float radius = 0.056;
21
+ float t = mod(_percent, 0.25) / 0.25;
22
+ t = mod(_percent, 0.25) - (2.0 * (1.0 - t) * t * -0.0185 + t * t * 0.25);
23
+ float x = _length * sin(PI2 * _percent);
24
+ float y = radius * cos(PI2 * 3.0 * _percent);
25
+
26
+ if (
27
+ floor(_percent / 0.25) == 0.0
28
+ || floor(_percent / 0.25) == 2.0
29
+ ) {
30
+ t = t * -1.0;
31
+ }
32
+ float z = radius * sin(PI2 * 2.0 * (_percent - t));
33
+ return vec3(x, y, z);
34
+ }
35
+
36
+ mat4 rotateX(float _angle){
37
+ return mat4(
38
+ 1.0, 0.0, 0.0, 0.0,
39
+ 0.0, cos(_angle), -sin(_angle), 0.0,
40
+ 0.0, sin(_angle), cos(_angle), 0.0,
41
+ 0.0, 0.0, 0.0, 1.0
42
+ );
43
+ }
44
+
45
+ void main(){
46
+ gl_Position = uProjectionMatrix
47
+ * uModelMatrix
48
+ * uViewMatrix
49
+ * rotateX(uProgress)
50
+ * vec4(curve(${ATTR.PERCENT}), 1.0);
51
+
52
+ gl_PointSize = ${ATTR.POINT_SIZE};
53
+ }
54
+ `;
55
+
56
+ export const FRAGMENT_SHADER = `
57
+ precision mediump float;
58
+ uniform float uProgress;
59
+ void main(){
60
+ gl_FragColor = vec4(0.81, 0.83, 0.85, 1.0);
61
+ }
62
+ `;
@@ -0,0 +1,31 @@
1
+ import { ATTR, VERTEX_SHADER, FRAGMENT_SHADER } from './constants';
2
+
3
+ export const createOptions = () => {
4
+ const attributes = [
5
+ {
6
+ name: ATTR.PERCENT,
7
+ data: (i: number, total: number) => [i / total],
8
+ size: 1,
9
+ },
10
+ {
11
+ name: ATTR.POINT_SIZE,
12
+ data: () => [window.devicePixelRatio * 1.3],
13
+ size: 1,
14
+ },
15
+ ];
16
+
17
+ const uniforms = {
18
+ uProgress: {
19
+ type: 'float',
20
+ value: [0.0],
21
+ },
22
+ };
23
+
24
+ return {
25
+ uniforms,
26
+ attributes,
27
+ multiplier: 4000,
28
+ vertex: VERTEX_SHADER,
29
+ fragment: FRAGMENT_SHADER,
30
+ };
31
+ };
@@ -0,0 +1,26 @@
1
+ export interface LoadingSpiralSettings {
2
+ canvas?: HTMLCanvasElement;
3
+ step?: number;
4
+ }
5
+
6
+ export const createSettings = (settings: LoadingSpiralSettings) => {
7
+ const { canvas, step = 0.09 } = settings;
8
+
9
+ const uniforms = {
10
+ uProgress: {
11
+ type: 'float',
12
+ value: [0.0],
13
+ },
14
+ };
15
+
16
+ return {
17
+ uniforms,
18
+ devicePixelRatio: window.devicePixelRatio,
19
+ shouldRender: true,
20
+ canvas,
21
+ onRender: (r: { uniforms: { uProgress: { value: number[] } } }) => {
22
+ const { uProgress } = r.uniforms;
23
+ uProgress.value[0] += step;
24
+ },
25
+ };
26
+ };
@@ -0,0 +1,2 @@
1
+ export { LoadingSpiral } from './LoadingSpiral';
2
+ export type { LoadingSpiralProps } from './LoadingSpiral.types';
@@ -0,0 +1,23 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ export const useResizedCanvasRef = (maxSize: number) => {
4
+ const canvasRef = useRef<HTMLCanvasElement>(null);
5
+ const containerRef = useRef<HTMLDivElement>(null);
6
+
7
+ useEffect(() => {
8
+ const canvas = canvasRef.current;
9
+ const container = containerRef.current;
10
+ if (!canvas || !container) return;
11
+ const { width, height } = container.getBoundingClientRect();
12
+ const canvasSize = Math.min(width, height);
13
+ const size = Math.min(canvasSize, maxSize);
14
+
15
+ canvas.setAttribute('width', `${size}`);
16
+ canvas.setAttribute('height', `${size}`);
17
+ }, [maxSize]);
18
+
19
+ return {
20
+ canvasRef,
21
+ containerRef,
22
+ };
23
+ };
@@ -0,0 +1,5 @@
1
+ export const rgba = (val: number[]) => {
2
+ const [r, g, b, a] = val;
3
+
4
+ return [r / 255, g / 255, b / 255, a];
5
+ };
@@ -0,0 +1,65 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Page, NotFound, Error as PageError, ErrorBoundary } from './Page';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import { Nav } from '../../molecules/nav';
5
+
6
+ const meta: Meta<typeof Page> = {
7
+ title: 'Organisms/Page',
8
+ component: Page,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ },
13
+ decorators: [
14
+ Story => (
15
+ <MemoryRouter>
16
+ <Story />
17
+ </MemoryRouter>
18
+ ),
19
+ ],
20
+ };
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {
26
+ render: () => (
27
+ <Page
28
+ nav={<Nav paths={[{ path: '/', name: '~' }]} />}
29
+ main={<div>Main content placeholder</div>}
30
+ />
31
+ ),
32
+ };
33
+
34
+ export const NotFoundDefault: Story = {
35
+ render: () => <NotFound />,
36
+ };
37
+
38
+ export const NotFoundWithMessage: Story = {
39
+ render: () => <NotFound>Custom page not found message</NotFound>,
40
+ };
41
+
42
+ export const ErrorDefault: Story = {
43
+ render: () => <PageError error={new Error('Something went wrong')} />,
44
+ };
45
+
46
+ // Helper component to trigger ErrorBoundary
47
+ const ThrowError = () => {
48
+ throw new Error('Render error!');
49
+ };
50
+
51
+ export const ErrorBoundaryDefault: Story = {
52
+ render: () => (
53
+ <ErrorBoundary>
54
+ <div>Normal child content</div>
55
+ </ErrorBoundary>
56
+ ),
57
+ };
58
+
59
+ export const ErrorBoundaryWithError: Story = {
60
+ render: () => (
61
+ <ErrorBoundary>
62
+ <ThrowError />
63
+ </ErrorBoundary>
64
+ ),
65
+ };
@@ -0,0 +1,71 @@
1
+ import React, { ReactElement } from 'react';
2
+ import { Article } from '../../molecules/article';
3
+ import {
4
+ PageProps,
5
+ ErrorProps,
6
+ NotFoundProps,
7
+ ErrorBoundaryProps,
8
+ ErrorBoundaryState,
9
+ } from './Page.types';
10
+
11
+ export const Page = (props: PageProps) => {
12
+ const { nav, main } = props;
13
+
14
+ return (
15
+ <main className="flex flex-col h-full">
16
+ <div className="flex-grow-0 w-full fixed top-0 z-50">{nav}</div>
17
+ {/* secctio padding top 192px */}
18
+ <section className="grow shrink-0 px-4 pt-32">{main}</section>
19
+ </main>
20
+ );
21
+ };
22
+
23
+ Page.displayName = 'Page';
24
+
25
+ export const NotFound = (props: NotFoundProps) => {
26
+ const { children } = props;
27
+ return <Error error={{ name: '404', message: children || 'Not Found' } as Error} />;
28
+ };
29
+
30
+ NotFound.displayName = 'NotFound';
31
+
32
+ export const Error = (props: ErrorProps) => {
33
+ const { error } = props;
34
+
35
+ return (
36
+ <div className="prose">
37
+ <pre>
38
+ <code className="language-javascript text-content-danger">
39
+ {error.name}: {error.message}
40
+ </code>
41
+ </pre>
42
+ </div>
43
+ );
44
+ };
45
+
46
+ Error.displayName = 'Error';
47
+
48
+ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
49
+ constructor(props: ErrorBoundaryProps) {
50
+ super(props);
51
+ this.state = { hasError: false };
52
+ }
53
+
54
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
55
+ return { hasError: true, error };
56
+ }
57
+
58
+ render(): ReactElement | null {
59
+ if (this.state.error && this.state.hasError) {
60
+ return (
61
+ <Article title="出错">
62
+ <div className="relative h-64">
63
+ <Error error={this.state.error} />
64
+ </div>
65
+ </Article>
66
+ );
67
+ }
68
+
69
+ return this.props.children as ReactElement;
70
+ }
71
+ }
@@ -0,0 +1,23 @@
1
+ import { ReactElement, ReactNode } from 'react';
2
+
3
+ export interface PageProps {
4
+ nav: ReactElement;
5
+ main: ReactElement;
6
+ }
7
+
8
+ export interface ErrorProps {
9
+ error: Error;
10
+ }
11
+
12
+ export interface NotFoundProps {
13
+ children?: ReactNode;
14
+ }
15
+
16
+ export interface ErrorBoundaryProps {
17
+ children: ReactNode;
18
+ }
19
+
20
+ export interface ErrorBoundaryState {
21
+ error?: Error;
22
+ hasError: boolean;
23
+ }
@@ -0,0 +1,8 @@
1
+ export { Page, NotFound, Error, ErrorBoundary } from './Page';
2
+ export type {
3
+ PageProps,
4
+ NotFoundProps,
5
+ ErrorProps,
6
+ ErrorBoundaryProps,
7
+ ErrorBoundaryState,
8
+ } from './Page.types';