@vrobots/storybook 0.1.73 → 0.2.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/package.json +3 -1
- package/dist/src/components/Breadcrumbs.js +2 -2
- package/dist/src/components/Display.js +1 -4
- package/dist/src/components/Header.js +3 -5
- package/dist/src/components/Sidebar.js +3 -4
- package/dist/src/components/VerifyAction.js +1 -1
- package/dist/src/components/form/FileUploader.js +1 -1
- package/dist/src/components/form/Login.js +1 -1
- package/dist/src/components/form/SecondFactorAuth.js +1 -1
- package/dist/src/components/index.d.ts +4 -0
- package/dist/src/components/index.js +4 -0
- package/dist/src/components/table/DataTable.d.ts +16 -0
- package/dist/src/components/table/DataTable.js +42 -0
- package/dist/src/components/table/components/ColumnText.d.ts +2 -0
- package/dist/src/components/table/components/ColumnText.js +5 -0
- package/dist/src/components/table/components/PageSizeSelector.d.ts +7 -0
- package/dist/src/components/table/components/PageSizeSelector.js +8 -0
- package/dist/src/components/table/components/Pagination.d.ts +10 -0
- package/dist/src/components/table/components/Pagination.js +51 -0
- package/dist/src/components/table/components/TableFilter.d.ts +6 -0
- package/dist/src/components/table/components/TableFilter.js +28 -0
- package/dist/src/components/table/components/TableFilterDatePicker.d.ts +5 -0
- package/dist/src/components/table/components/TableFilterDatePicker.js +10 -0
- package/dist/src/components/table/components/TableFilterInput.d.ts +2 -0
- package/dist/src/components/table/components/TableFilterInput.js +5 -0
- package/dist/src/components/table/components/TableFilterSelect.d.ts +2 -0
- package/dist/src/components/table/components/TableFilterSelect.js +5 -0
- package/dist/src/components/table/constants.d.ts +2 -0
- package/dist/src/components/table/constants.js +2 -0
- package/dist/src/components/table/hooks/useDataTable.d.ts +24 -0
- package/dist/src/components/table/hooks/useDataTable.js +14 -0
- package/dist/src/components/table/hooks/usePageSize.d.ts +4 -0
- package/dist/src/components/table/hooks/usePageSize.js +8 -0
- package/dist/src/components/table/hooks/usePagination.d.ts +5 -0
- package/dist/src/components/table/hooks/usePagination.js +12 -0
- package/dist/src/components/table/hooks/useTableFilters.d.ts +15 -0
- package/dist/src/components/table/hooks/useTableFilters.js +118 -0
- package/dist/src/components/table/types.d.ts +11 -0
- package/dist/src/components/table/types.js +1 -0
- package/dist/src/hooks/useSelectedColorSchema.d.ts +2 -1
- package/dist/src/hooks/useSelectedColorSchema.js +4 -3
- package/dist/src/stories/Breadcrumbs.stories.d.ts +1 -0
- package/dist/src/stories/Breadcrumbs.stories.js +15 -0
- package/dist/src/stories/DataTable.stories.d.ts +26 -0
- package/dist/src/stories/DataTable.stories.js +92 -0
- package/dist/src/stories/FileUploader.stories.d.ts +1 -0
- package/dist/src/stories/FileUploader.stories.js +19 -0
- package/dist/src/stories/Login.stories.d.ts +1 -0
- package/dist/src/stories/Login.stories.js +19 -0
- package/dist/src/stories/SecondFactorAuth.stories.d.ts +1 -0
- package/dist/src/stories/SecondFactorAuth.stories.js +23 -0
- package/dist/src/stories/VerifyAction.stories.d.ts +1 -0
- package/dist/src/stories/VerifyAction.stories.js +20 -0
- package/dist/src/theme/system.d.ts +2 -0
- package/dist/src/theme/system.js +113 -0
- package/package.json +3 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vrobots/storybook",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
"@chakra-ui/react": "^3.32.0",
|
|
22
22
|
"@emotion/react": "^11.14.0",
|
|
23
23
|
"@storybook/addon-themes": "^10.2.16",
|
|
24
|
+
"@tanstack/react-table": "^8.21.3",
|
|
24
25
|
"axios": "^1.13.4",
|
|
25
26
|
"motion": "^12.34.0",
|
|
27
|
+
"next": "^16.2.4",
|
|
26
28
|
"next-themes": "^0.4.6",
|
|
27
29
|
"owasp-password-strength-test": "^1.3.0",
|
|
28
30
|
"react": "^19.2.0",
|
|
@@ -5,9 +5,9 @@ export const Breadcrumbs = React.forwardRef(({ breadcrumbs }, ref) => {
|
|
|
5
5
|
if (!!breadcrumbs) {
|
|
6
6
|
const crumbs = breadcrumbs.map(({ title, nav, color: itemColor, onClick }, key) => {
|
|
7
7
|
if (!!nav) {
|
|
8
|
-
return (_jsxs(HStack, { children: [_jsx(Text, { as: 'button', color: itemColor || '
|
|
8
|
+
return (_jsxs(HStack, { children: [_jsx(Text, { as: 'button', color: itemColor || 'neu.accent', whiteSpace: 'nowrap', wordBreak: 'break-word', fontSize: 'sm', cursor: 'pointer', p: 1, truncate: true, onClick: () => onClick?.(nav), children: title }), _jsx(Text, { ml: 2, mr: 2, fontSize: 'sm', children: ">" })] }, `${title}-${key}`));
|
|
9
9
|
}
|
|
10
|
-
return (_jsx(Text, { whiteSpace: 'nowrap', wordBreak: 'break-word', color: '
|
|
10
|
+
return (_jsx(Text, { whiteSpace: 'nowrap', wordBreak: 'break-word', color: 'neu.muted', fontSize: 'sm', truncate: true, children: title }, `${title}-${key}`));
|
|
11
11
|
});
|
|
12
12
|
return (_jsx(HStack, { width: '100%', flex: `0 1 auto`, ref: ref, children: crumbs }));
|
|
13
13
|
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box } from "@chakra-ui/react";
|
|
3
3
|
import { forwardRef } from "react";
|
|
4
|
-
import { useColorMode } from "./ui/color-mode";
|
|
5
4
|
import { useHeader } from './Header';
|
|
6
5
|
export const Display = forwardRef((props, ref) => {
|
|
7
|
-
const { colorMode } = useColorMode();
|
|
8
|
-
const bgColor = colorMode === 'light' ? 'gray.50' : 'gray.800';
|
|
9
6
|
const { ref: headerRef } = useHeader();
|
|
10
|
-
return (_jsx(Box, { ref: ref, maxHeight: `calc(100vh - ${headerRef.current?.offsetHeight || 0}px)`, width: '100%', overflow: 'scroll', bgColor:
|
|
7
|
+
return (_jsx(Box, { ref: ref, maxHeight: `calc(100vh - ${headerRef.current?.offsetHeight || 0}px)`, width: '100%', overflow: 'scroll', bgColor: 'neu.bg', children: props.children }));
|
|
11
8
|
});
|
|
12
9
|
Display.displayName = 'Display';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { Grid, GridItem, Text } from '@chakra-ui/react';
|
|
4
4
|
import { forwardRef } from 'react';
|
|
5
5
|
import { Logo } from './Logo';
|
|
6
6
|
import { useColorMode } from './ui/color-mode';
|
|
@@ -31,10 +31,8 @@ export const Header = forwardRef(({}, ref) => {
|
|
|
31
31
|
const { appName, logo, menu, version, defaultPath } = useHeader();
|
|
32
32
|
const { colorMode } = useColorMode();
|
|
33
33
|
const selectedLogo = logo[colorMode];
|
|
34
|
-
|
|
35
|
-
const borderColor = colorMode === 'light' ? borderColors._light : borderColors._dark;
|
|
36
|
-
return (_jsxs(Grid, { id: 'header', width: '100%', position: 'relative', zIndex: 2, backgroundColor: colorMode === 'light' ? 'white' : 'black', borderBottom: `1px solid ${borderColor}`, templateColumns: {
|
|
34
|
+
return (_jsxs(Grid, { id: 'header', width: '100%', position: 'relative', zIndex: 2, backgroundColor: 'neu.surface', borderBottom: '1px solid', borderColor: 'neu.border', boxShadow: 'neuRaised', templateColumns: {
|
|
37
35
|
base: 'repeat(12, 1fr)',
|
|
38
|
-
}, ref: ref, children: [_jsxs(GridItem, { colSpan: 6, pt: { base: '0', lg: '8px' }, pb: { base: '0', lg: '8px' }, children: [_jsx(Logo, { defaultPath: defaultPath, alt: appName, src: selectedLogo }), _jsx(Text, { position: 'absolute', fontSize: 'xx-small', bottom: 0, left: 0, ml: 1, color: '
|
|
36
|
+
}, ref: ref, children: [_jsxs(GridItem, { colSpan: 6, pt: { base: '0', lg: '8px' }, pb: { base: '0', lg: '8px' }, children: [_jsx(Logo, { defaultPath: defaultPath, alt: appName, src: selectedLogo }), _jsx(Text, { position: 'absolute', fontSize: 'xx-small', bottom: 0, left: 0, ml: 1, color: 'neu.muted', children: !!version && _jsxs("em", { children: ["v. ", version] }) })] }), _jsx(GridItem, { colSpan: 6, alignItems: 'center', justifyContent: 'flex-end', pr: 2, display: 'flex', children: menu })] }));
|
|
39
37
|
});
|
|
40
38
|
Header.displayName = 'Header';
|
|
@@ -5,7 +5,6 @@ import { useColorMode } from "./ui/color-mode";
|
|
|
5
5
|
import { LuMenu } from "react-icons/lu";
|
|
6
6
|
import { motion } from "motion/react";
|
|
7
7
|
import { useBreakpoint, useIsMobile } from "../hooks";
|
|
8
|
-
import { COLOR_GRAY_200, COLOR_GRAY_800 } from "../constants";
|
|
9
8
|
export const SidebarContext = React.createContext({});
|
|
10
9
|
export const SidebarProvider = (props) => {
|
|
11
10
|
const [isOpen, setIsOpen] = React.useState(props?.isOpen || false);
|
|
@@ -50,7 +49,6 @@ export const Sidebar = forwardRef((props, ref) => {
|
|
|
50
49
|
transition: { duration: 0.2 }
|
|
51
50
|
};
|
|
52
51
|
const action = isMobile ? sidebar.isOpen ? open : close : {};
|
|
53
|
-
const borderColor = colorMode === 'light' ? COLOR_GRAY_200 : COLOR_GRAY_800;
|
|
54
52
|
React.useEffect(() => {
|
|
55
53
|
setMounted(true);
|
|
56
54
|
}, []);
|
|
@@ -65,8 +63,9 @@ export const Sidebar = forwardRef((props, ref) => {
|
|
|
65
63
|
overflow: 'hidden',
|
|
66
64
|
height: 'inherit',
|
|
67
65
|
width: sidebar.isOpen ? width[breakpoint || 'base'] : '0',
|
|
68
|
-
backgroundColor:
|
|
69
|
-
borderRight:
|
|
66
|
+
backgroundColor: 'var(--chakra-colors-neu-surface)',
|
|
67
|
+
borderRight: '1px solid var(--chakra-colors-neu-border)',
|
|
68
|
+
boxShadow: 'var(--chakra-shadows-neuRaised)',
|
|
70
69
|
}, ...action, ref: ref, children: _jsx(Box, { width: width[breakpoint || 'base'], children: sidebar.menu }) }) }));
|
|
71
70
|
});
|
|
72
71
|
Sidebar.displayName = 'Sidebar';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Button, Dialog, Portal, Text } from '@chakra-ui/react';
|
|
3
3
|
export const VerifyAction = ({ isOpen, heading, subheading, onCancel, onProceed }) => {
|
|
4
|
-
return (_jsx(Dialog.Root, { open: isOpen, onOpenChange: onCancel, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, {}), _jsx(Dialog.Positioner, { children: _jsxs(Dialog.Content, { children: [_jsx(Dialog.Header, { mb: 0, children: _jsx(Dialog.Title, { mb: 0, children: heading }) }), _jsx(Text, { ml: 6, children: subheading }), _jsx(Dialog.Body, {}), _jsxs(Dialog.Footer, { children: [_jsx(Dialog.ActionTrigger, { asChild: true, children: _jsx(Button, { variant: 'subtle', children: "Close" }) }), _jsx(Button, { ml: 1, onClick: onProceed, colorPalette: 'red', children: "Proceed" })] })] }) })] }) }));
|
|
4
|
+
return (_jsx(Dialog.Root, { open: isOpen, onOpenChange: onCancel, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, { backdropFilter: 'blur(2px)' }), _jsx(Dialog.Positioner, { children: _jsxs(Dialog.Content, { layerStyle: 'neuRaised', borderWidth: '1px', borderColor: 'neu.border', children: [_jsx(Dialog.Header, { mb: 0, children: _jsx(Dialog.Title, { mb: 0, children: heading }) }), _jsx(Text, { ml: 6, color: 'neu.muted', children: subheading }), _jsx(Dialog.Body, {}), _jsxs(Dialog.Footer, { children: [_jsx(Dialog.ActionTrigger, { asChild: true, children: _jsx(Button, { variant: 'subtle', children: "Close" }) }), _jsx(Button, { ml: 1, onClick: onProceed, colorPalette: 'red', "aria-label": 'Proceed with destructive action', children: "Proceed" })] })] }) })] }) }));
|
|
5
5
|
};
|
|
@@ -54,6 +54,6 @@ const FileUploader = ({ title, description, buttonLabel, helperText, accept, mul
|
|
|
54
54
|
const droppedFiles = Array.from(event.dataTransfer.files ?? []);
|
|
55
55
|
handleFiles(droppedFiles);
|
|
56
56
|
};
|
|
57
|
-
return (_jsx(Card.Root, { ...props, children: _jsxs(Card.Body, { children: [_jsx(Card.Title, { children: title || `Upload ${multiple ? 'File/s' : 'a File'}` }), !!description && _jsx(Card.Description, { children: description }), _jsxs(Box, { mt: 4, p: 6, borderWidth: "2px", borderStyle: "dashed", borderColor: isDragging ? "
|
|
57
|
+
return (_jsx(Card.Root, { ...props, children: _jsxs(Card.Body, { children: [_jsx(Card.Title, { children: title || `Upload ${multiple ? 'File/s' : 'a File'}` }), !!description && _jsx(Card.Description, { children: description }), _jsxs(Box, { mt: 4, p: 6, borderWidth: "2px", borderStyle: "dashed", borderColor: isDragging ? "neu.accent" : "neu.border", borderRadius: "md", bg: isDragging ? "neu.surface" : "neu.bg", textAlign: "center", transition: "background 0.2s ease, border-color 0.2s ease", onDragEnter: handleDrag(true), onDragOver: handleDrag(true), onDragLeave: handleDrag(false), onDrop: handleDrop, children: [_jsxs(Text, { fontWeight: "medium", children: ["Drag and drop ", multiple ? 'file/s' : 'a file', " here"] }), _jsx(Text, { fontSize: "sm", color: "neu.muted", mt: 1, children: "or click the button below to browse from your device" }), _jsx("input", { ref: fileInputRef, type: "file", hidden: true, accept: accept, multiple: multiple, onChange: handleFileInputChange }), _jsx(Button, { mt: 4, onClick: () => fileInputRef.current?.click(), children: buttonLabel || `Choose ${multiple ? 'File/s' : 'a File'}` }), !!helperText && (_jsx(Text, { fontSize: "xs", color: "neu.muted", mt: 3, children: helperText }))] }), files.length > 0 && (_jsxs(Box, { mt: 4, mb: 4, children: [_jsxs(Text, { fontSize: "sm", fontWeight: "medium", mb: 2, children: ["Selected ", multiple ? 'File/s' : 'a File'] }), _jsx(List.Root, { gap: "1", children: files.map((file, index) => (_jsx(List.Item, { children: `${file.name} ${!!uploadPercentages[index] ? '-' + uploadPercentages[index] + '%' : ''}` }, `${file.name}-${file.size}`))) })] })), children] }) }));
|
|
58
58
|
};
|
|
59
59
|
export default FileUploader;
|
|
@@ -12,6 +12,6 @@ const Login = ({ title, description, onLogin, ...props }) => {
|
|
|
12
12
|
}
|
|
13
13
|
}), autoFocus: true }), _jsx(Field.ErrorText, { children: errors.emailAddress?.message })] }), _jsxs(Field.Root, { invalid: !!errors.password, children: [_jsx(Field.Label, { children: "Password" }), _jsx(PasswordInput, { ...register("password", {
|
|
14
14
|
required: "Please enter your password",
|
|
15
|
-
}) }), _jsx(Field.ErrorText, { children: errors.password?.message })] })] }), _jsx(Button, { type: "submit",
|
|
15
|
+
}) }), _jsx(Field.ErrorText, { children: errors.password?.message })] })] }), _jsx(Button, { type: "submit", width: "full", mt: 6, children: "Login" })] })] }) }));
|
|
16
16
|
};
|
|
17
17
|
export default Login;
|
|
@@ -5,6 +5,6 @@ const SecondFactorAuth = ({ title, description, onSecondFactorAuth, onResendAuth
|
|
|
5
5
|
const { register, handleSubmit, formState: { errors }, } = useForm();
|
|
6
6
|
return (_jsx(Card.Root, { ...props, children: _jsxs(Card.Body, { children: [_jsx(Card.Title, { children: title || "Second Factor Auth" }), !!description && _jsx(Card.Description, { children: description }), _jsxs(Box, { as: "form", onSubmit: handleSubmit((data) => onSecondFactorAuth(data)), mt: 4, children: [_jsxs(Stack, { gap: "4", children: [_jsxs(Field.Root, { invalid: !!errors.authCode, required: true, children: [_jsx(Field.Label, { children: "Authorization Code" }), _jsx(InputGroup, { endAddon: _jsx(Button, { h: '1.75rem', size: 'sm', variant: 'ghost', onClick: onResendAuthCode, children: "Resend" }), children: _jsx(Input, { type: 'text', placeholder: 'Authorization code', ...register("authCode", {
|
|
7
7
|
required: "Please enter your authorization code",
|
|
8
|
-
}), autoFocus: true }) }), _jsx(Field.ErrorText, { children: errors.authCode?.message })] }), _jsxs(Checkbox.Root, { ...register("rememberDevice"), size: 'sm', children: [_jsx(Checkbox.HiddenInput, {}), _jsx(Checkbox.Control, {}), _jsx(Checkbox.Label, { children: "Remember Device" })] })] }), _jsx(Button, { type: "submit",
|
|
8
|
+
}), autoFocus: true }) }), _jsx(Field.ErrorText, { children: errors.authCode?.message })] }), _jsxs(Checkbox.Root, { ...register("rememberDevice"), size: 'sm', children: [_jsx(Checkbox.HiddenInput, {}), _jsx(Checkbox.Control, {}), _jsx(Checkbox.Label, { children: "Remember Device" })] })] }), _jsx(Button, { type: "submit", width: "full", mt: 6, children: "Submit" })] })] }) }));
|
|
9
9
|
};
|
|
10
10
|
export default SecondFactorAuth;
|
|
@@ -19,3 +19,7 @@ export * from './form/Login';
|
|
|
19
19
|
export * from './form/SecondFactorAuth';
|
|
20
20
|
export * from './form/FileUploader';
|
|
21
21
|
export { default as Form } from './form';
|
|
22
|
+
export { default as DataTable } from './table/DataTable';
|
|
23
|
+
export * from './table/types';
|
|
24
|
+
export * from './table/components/ColumnText';
|
|
25
|
+
export { default as useDataTable } from './table/hooks/useDataTable';
|
|
@@ -19,3 +19,7 @@ export * from './form/Login';
|
|
|
19
19
|
export * from './form/SecondFactorAuth';
|
|
20
20
|
export * from './form/FileUploader';
|
|
21
21
|
export { default as Form } from './form';
|
|
22
|
+
export { default as DataTable } from './table/DataTable';
|
|
23
|
+
export * from './table/types';
|
|
24
|
+
export * from './table/components/ColumnText';
|
|
25
|
+
export { default as useDataTable } from './table/hooks/useDataTable';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IColumnFilter, TFilterChange } from "./types";
|
|
2
|
+
export interface IDataTableProps {
|
|
3
|
+
data: any[];
|
|
4
|
+
total: number;
|
|
5
|
+
pageNumber: number;
|
|
6
|
+
pagesTotal: number;
|
|
7
|
+
pageSize: number;
|
|
8
|
+
setPageNumber: React.Dispatch<React.SetStateAction<number>>;
|
|
9
|
+
setPageSize: React.Dispatch<React.SetStateAction<number>>;
|
|
10
|
+
columns: any[];
|
|
11
|
+
filters?: IColumnFilter[];
|
|
12
|
+
onRowClick?: (row: any) => void;
|
|
13
|
+
onFilterChange?: TFilterChange;
|
|
14
|
+
}
|
|
15
|
+
declare const DataTable: React.FC<IDataTableProps>;
|
|
16
|
+
export default DataTable;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Card, Table } from "@chakra-ui/react";
|
|
3
|
+
import { useColorMode } from "../ui/color-mode";
|
|
4
|
+
import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import TableFilter from "./components/TableFilter";
|
|
7
|
+
import { Pagination } from "./components/Pagination";
|
|
8
|
+
const Pager = ({ border, pagesTotal, pageNumber, setPageNumber, pageSize, setPageSize }) => (_jsx(Box, { p: 2, pt: 6, border: border, borderBottom: 'none', children: _jsx(Pagination, { pagesTotal: pagesTotal, pageNumber: pageNumber, setPageNumber: setPageNumber, pageSize: pageSize, setPageSize: setPageSize, showPageSelector: true }) }));
|
|
9
|
+
const DataTable = ({ data: defaultData, total, pageNumber, pagesTotal, setPageNumber, setPageSize, pageSize, columns, filters = [], onRowClick, onFilterChange, }) => {
|
|
10
|
+
const [data, setData] = useState(() => [...defaultData]);
|
|
11
|
+
const { colorMode } = useColorMode();
|
|
12
|
+
const columnResizeMode = 'onChange';
|
|
13
|
+
const columnResizeDirection = 'ltr';
|
|
14
|
+
const isDarkMode = colorMode === 'dark';
|
|
15
|
+
const border = `1px solid ${isDarkMode ? '#333333' : '#E2E8F0'} !important`;
|
|
16
|
+
const table = useReactTable({
|
|
17
|
+
data,
|
|
18
|
+
columns,
|
|
19
|
+
columnResizeMode,
|
|
20
|
+
columnResizeDirection,
|
|
21
|
+
getCoreRowModel: getCoreRowModel(),
|
|
22
|
+
});
|
|
23
|
+
const pagerProps = { border, pagesTotal, pageNumber, setPageNumber, pageSize, setPageSize };
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setData([...defaultData]);
|
|
26
|
+
}, [defaultData, setData]);
|
|
27
|
+
return (_jsx(Card.Root, { children: _jsxs(Card.Body, { p: 0, children: [_jsx(Pager, { ...pagerProps }), _jsx(Box, { overflow: 'auto', children: _jsxs(Table.Root, { width: '100%', children: [_jsx(Table.Header, { children: table.getHeaderGroups().map(headerGroup => (_jsx(Table.Row, { children: headerGroup.headers.map(header => (_jsxs(Table.ColumnHeader, { borderLeft: border, borderRight: border, children: [header.isPlaceholder
|
|
28
|
+
? null
|
|
29
|
+
: flexRender(header.column.columnDef.header, header.getContext()), filters.find((filter) => filter.id === header.id) && !!onFilterChange ? (_jsx(TableFilter, { filter: filters.find(filter => filter.id === header.id), onFilterChange: onFilterChange })) : null] }, header.id))) }, headerGroup.id))) }), _jsxs(Table.Body, { children: [total === 0 ? (_jsx(Table.Row, { children: _jsx(Table.Cell, { colSpan: columns.length, textAlign: 'center', children: "No records found" }) })) : null, table.getRowModel().rows.map((row, i) => {
|
|
30
|
+
const isEven = i % 2 === 0;
|
|
31
|
+
const colorEven = isDarkMode ? 'gray.700' : 'gray.50';
|
|
32
|
+
const colorOdd = isDarkMode ? 'gray.600' : 'white';
|
|
33
|
+
const backgroundColor = isEven ? colorEven : colorOdd;
|
|
34
|
+
return (_jsx(Table.Row, { background: backgroundColor, _hover: !!onRowClick ? {
|
|
35
|
+
cursor: 'pointer',
|
|
36
|
+
backgroundColor: 'rgba(107, 160, 252, 0.3) !important',
|
|
37
|
+
} : void 0, onClick: () => onRowClick && onRowClick(row.original), children: row.getVisibleCells().map(cell => (_jsx(Table.Cell, { border: border, children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, row.id));
|
|
38
|
+
})] }), _jsx(Table.Footer, { children: table.getFooterGroups().map(footerGroup => (_jsx(Table.Row, { children: footerGroup.headers.map(header => (_jsx(Table.ColumnHeader, { borderLeft: border, borderRight: border, children: header.isPlaceholder
|
|
39
|
+
? null
|
|
40
|
+
: flexRender(header.column.columnDef.header, header.getContext()) }, header.id))) }, footerGroup.id))) })] }) }), _jsx(Pager, { ...pagerProps })] }) }));
|
|
41
|
+
};
|
|
42
|
+
export default DataTable;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { NativeSelect, HStack, Text } from '@chakra-ui/react';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
export const PageSizeSelector = ({ sizes, selectedPageSize, onSelect, }) => {
|
|
5
|
+
const idKey = React.useId();
|
|
6
|
+
const handleSetPageSize = (e) => onSelect(Number(e.target.value));
|
|
7
|
+
return (_jsxs(HStack, { gap: "3", align: "center", children: [_jsx(Text, { fontSize: "sm", fontWeight: "normal", children: "Showing" }), _jsxs(NativeSelect.Root, { size: 'sm', maxW: '75px', children: [_jsx(NativeSelect.Field, { id: `page-size-selector-${idKey}`, value: selectedPageSize, onChange: handleSetPageSize, cursor: 'pointer', children: sizes.map((size, key) => (_jsx("option", { value: size, children: size }, `options-${size}-${key}`))) }), _jsx(NativeSelect.Indicator, {})] })] }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface IPaginationProps {
|
|
3
|
+
pagesTotal: number;
|
|
4
|
+
pageNumber: number;
|
|
5
|
+
pageSize: number;
|
|
6
|
+
showPageSelector?: boolean;
|
|
7
|
+
setPageSize: React.Dispatch<React.SetStateAction<number>>;
|
|
8
|
+
setPageNumber: React.Dispatch<React.SetStateAction<number>>;
|
|
9
|
+
}
|
|
10
|
+
export declare const Pagination: React.FC<IPaginationProps>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Flex, Button, Field, HStack, Text, Grid, GridItem, NumberInput } from '@chakra-ui/react';
|
|
4
|
+
import { PAGE_SIZES } from '../constants';
|
|
5
|
+
import { PageSizeSelector } from './PageSizeSelector';
|
|
6
|
+
import { useBreakpoint } from '../../../hooks';
|
|
7
|
+
const PAGE_LIMIT_MOBILE = 2;
|
|
8
|
+
const PAGE_LIMIT = 4;
|
|
9
|
+
const makeNavButton = (onChange, pageNumber) => function PageButton(page) {
|
|
10
|
+
return (_jsx(Button, { "aria-label": `page-${page}`, onClick: () => onChange(page.toString()), variant: page === pageNumber ? 'solid' : 'outline', size: 'sm', paddingLeft: '1px !important', paddingRight: '1px !important', marginRight: 1, children: page }, `nav-button-${page}`));
|
|
11
|
+
};
|
|
12
|
+
export const Pagination = ({ pagesTotal, pageNumber, pageSize, showPageSelector, setPageNumber, setPageSize }) => {
|
|
13
|
+
const [pageInput, setPageInput] = React.useState(pageNumber);
|
|
14
|
+
const [shouldDebounce, setShouldDebounce] = React.useState(false);
|
|
15
|
+
const pageInputRef = React.useRef(pageNumber);
|
|
16
|
+
const breakpoint = useBreakpoint() ?? 'base';
|
|
17
|
+
const isMobile = ['base', 'sm'].includes(breakpoint);
|
|
18
|
+
const limit = isMobile ? PAGE_LIMIT_MOBILE : PAGE_LIMIT;
|
|
19
|
+
const pages = [];
|
|
20
|
+
for (let i = 0; pagesTotal > i; i++) {
|
|
21
|
+
pages.push(i + 1);
|
|
22
|
+
}
|
|
23
|
+
const handlePages = (pages, limit, pageNumber) => pages.filter(page => {
|
|
24
|
+
const cp = pageNumber < 4 ? 3 : pageNumber;
|
|
25
|
+
const rangeA = cp - (limit / 2);
|
|
26
|
+
const rangeB = cp + (limit / 2);
|
|
27
|
+
return page >= rangeA && page <= rangeB;
|
|
28
|
+
});
|
|
29
|
+
const handleChangePageInput = (shouldDebounce) => (ivalue) => {
|
|
30
|
+
const value = Number(ivalue) > pagesTotal ? pagesTotal : Number(ivalue);
|
|
31
|
+
setShouldDebounce(shouldDebounce);
|
|
32
|
+
setPageInput(value);
|
|
33
|
+
};
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
let timeout;
|
|
36
|
+
if ((pageInputRef.current !== pageInput) && !!pageInput) {
|
|
37
|
+
pageInputRef.current = pageInput;
|
|
38
|
+
setShouldDebounce(previousShouldDebounce => {
|
|
39
|
+
timeout = setTimeout(() => setPageNumber(pageInput), previousShouldDebounce ? 1000 : 0);
|
|
40
|
+
return false;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return () => clearTimeout(timeout);
|
|
44
|
+
}, [pageInput, setShouldDebounce, setPageNumber]);
|
|
45
|
+
return (_jsxs(Grid, { templateRows: {
|
|
46
|
+
sm: 'repeat(1, 1fr)',
|
|
47
|
+
}, templateColumns: {
|
|
48
|
+
sm: `repeat(1, 1fr)`,
|
|
49
|
+
md: `repeat(2, 1fr)`,
|
|
50
|
+
}, children: [showPageSelector ? (_jsx(_Fragment, { children: _jsx(GridItem, { mb: 4, children: _jsxs(Flex, { children: [_jsx(PageSizeSelector, { sizes: PAGE_SIZES, selectedPageSize: pageSize, onSelect: setPageSize }), _jsxs(Text, { fontSize: 'sm', fontWeight: 'normal', ml: 4, mt: 1, children: ["page ", _jsx("strong", { children: pageNumber.toString() }), " of ", _jsx("strong", { children: pagesTotal.toString() })] })] }) }) })) : _jsx(GridItem, { colSpan: 1 }), _jsx(GridItem, { children: _jsx(Field.Root, { children: _jsxs(HStack, { justifyContent: 'right', children: [handlePages(pages, limit, pageNumber).map(makeNavButton(handleChangePageInput(false), pageNumber)), _jsx(Field.Label, { htmlFor: 'pagination', style: { fontSize: 10 }, mt: 2, ml: 4, children: "goto page:" }), _jsxs(NumberInput.Root, { value: String(pageInput), size: 'sm', max: pagesTotal, min: 1, onValueChange: (details) => handleChangePageInput(true)(details.value), children: [_jsx(NumberInput.Input, { id: 'pagination', style: { width: 90 } }), _jsxs(NumberInput.Control, { children: [_jsx(NumberInput.IncrementTrigger, {}), _jsx(NumberInput.DecrementTrigger, {})] })] })] }) }) })] }));
|
|
51
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { IColumnFilter, TFilterChange } from "../types";
|
|
2
|
+
export interface ITableFilterProps {
|
|
3
|
+
filter: IColumnFilter;
|
|
4
|
+
onFilterChange: TFilterChange;
|
|
5
|
+
}
|
|
6
|
+
export default function TableFilter({ filter, onFilterChange, }: ITableFilterProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import TableFilterInput from "./TableFilterInput";
|
|
3
|
+
import { Field, IconButton, InputGroup } from "@chakra-ui/react";
|
|
4
|
+
import TableFilterDatePicker from "./TableFilterDatePicker";
|
|
5
|
+
import { FaX } from "react-icons/fa6";
|
|
6
|
+
import { Tooltip } from "../../ui/tooltip";
|
|
7
|
+
export default function TableFilter({ filter, onFilterChange, }) {
|
|
8
|
+
const handleChangeInput = (event) => {
|
|
9
|
+
const { value } = event.target;
|
|
10
|
+
onFilterChange({ ...filter, value });
|
|
11
|
+
};
|
|
12
|
+
const handleChangeDate = (date) => {
|
|
13
|
+
if (date) {
|
|
14
|
+
onFilterChange({ ...filter, value: new Date(date) });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const handleClearFilter = () => {
|
|
18
|
+
onFilterChange({ ...filter, value: undefined });
|
|
19
|
+
};
|
|
20
|
+
const ClearFilterButton = () => !!filter.value ? (_jsx(Tooltip, { content: `Clear filter`, positioning: { placement: 'bottom-end' }, showArrow: true, children: _jsx(IconButton, { "aria-label": `Clear filter ${filter.type}`, onClick: handleClearFilter, size: 'sm', variant: 'ghost', children: _jsx(FaX, {}) }) })) : null;
|
|
21
|
+
if (filter.type === 'string' || filter.type === 'number') {
|
|
22
|
+
return (_jsxs(Field.Root, { children: [_jsx(InputGroup, { endElement: _jsx(ClearFilterButton, {}), children: _jsx(TableFilterInput, { value: filter.value?.toString() || '', onChange: handleChangeInput }) }), _jsx(Field.HelperText, { color: 'gray.300', whiteSpace: 'nowrap', wordBreak: 'break-word', children: filter.helperText })] }));
|
|
23
|
+
}
|
|
24
|
+
if (filter.type === 'date') {
|
|
25
|
+
return (_jsxs(Field.Root, { children: [_jsx(InputGroup, { endElement: _jsx(ClearFilterButton, {}), children: _jsx(TableFilterDatePicker, { selected: filter.value, onChange: (date) => handleChangeDate(date) }) }), _jsx(Field.HelperText, { color: 'gray.300', whiteSpace: 'nowrap', wordBreak: 'break-word', children: filter.helperText })] }));
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Input } from "@chakra-ui/react";
|
|
3
|
+
export default function TableFilterDatePicker({ selected, onChange }) {
|
|
4
|
+
const value = selected ? selected.toISOString().split('T')[0] : '';
|
|
5
|
+
const handleChange = (e) => {
|
|
6
|
+
const val = e.target.value;
|
|
7
|
+
onChange(val ? new Date(val) : null);
|
|
8
|
+
};
|
|
9
|
+
return (_jsx(Input, { type: "date", value: value, onChange: handleChange, width: '100%', mt: 2, size: 'md' }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { NativeSelect } from "@chakra-ui/react";
|
|
3
|
+
export default function TableFilterSelect(props) {
|
|
4
|
+
return (_jsxs(NativeSelect.Root, { width: '100%', ...props, children: [_jsx(NativeSelect.Field, {}), _jsx(NativeSelect.Indicator, {})] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IFilter } from "@vrobots/types";
|
|
2
|
+
import { IColumnFilter } from "../types";
|
|
3
|
+
export interface IUseDataTableArgs {
|
|
4
|
+
columnFilters: IColumnFilter[];
|
|
5
|
+
skip: number;
|
|
6
|
+
limit: number;
|
|
7
|
+
totalRecords: number;
|
|
8
|
+
sort?: {
|
|
9
|
+
[key: string]: 1 | -1;
|
|
10
|
+
};
|
|
11
|
+
baseFilter?: IFilter;
|
|
12
|
+
additionalFilters?: IFilter[];
|
|
13
|
+
shouldUseQuerystring?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export default function useDataTable(options: IUseDataTableArgs): {
|
|
16
|
+
pageSize: number;
|
|
17
|
+
setPageSize: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
18
|
+
pageNumber: number;
|
|
19
|
+
pagesTotal: number;
|
|
20
|
+
setPageNumber: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
21
|
+
query: import("@vrobots/types").IQuery;
|
|
22
|
+
filters: IColumnFilter[];
|
|
23
|
+
onFilterChange: (filter: IColumnFilter) => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import useTableFilters from "./useTableFilters";
|
|
2
|
+
import usePagination from "./usePagination";
|
|
3
|
+
import usePageSize from "./usePageSize";
|
|
4
|
+
export default function useDataTable(options) {
|
|
5
|
+
const { columnFilters, skip, limit, totalRecords, sort, baseFilter, additionalFilters, shouldUseQuerystring, } = options;
|
|
6
|
+
const pageSize = usePageSize(limit);
|
|
7
|
+
const pagination = usePagination(skip, limit, totalRecords, shouldUseQuerystring);
|
|
8
|
+
const filters = useTableFilters(columnFilters, skip, pageSize.pageSize, pagination.pageNumber, sort, baseFilter, additionalFilters, shouldUseQuerystring);
|
|
9
|
+
return {
|
|
10
|
+
...filters,
|
|
11
|
+
...pagination,
|
|
12
|
+
...pageSize,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { PAGE_FIRST_PAGE } from "../constants";
|
|
3
|
+
export default function usePagination(skip, limit, totalRecords, shouldUseQuerystring) {
|
|
4
|
+
const defaultPageNumber = !!limit ? (skip / limit) + 1 : PAGE_FIRST_PAGE;
|
|
5
|
+
const [pageNumber, setPageNumber] = useState(defaultPageNumber);
|
|
6
|
+
const pagesTotal = Math.ceil(totalRecords / limit);
|
|
7
|
+
return {
|
|
8
|
+
pageNumber,
|
|
9
|
+
pagesTotal,
|
|
10
|
+
setPageNumber,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { IColumnFilter, TColumnFilterType } from "../types";
|
|
2
|
+
import { IFilter, IQuery } from "@vrobots/types";
|
|
3
|
+
export interface ITableFilter {
|
|
4
|
+
[key: string]: {
|
|
5
|
+
type: TColumnFilterType;
|
|
6
|
+
value: string | number | Date;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export default function useTableFilters(columnFilters: IColumnFilter[], skip: number, limit: number, pageNumber: number, sort?: {
|
|
10
|
+
[key: string]: 1 | -1;
|
|
11
|
+
}, baseFilter?: IFilter, additionalFilters?: IFilter[], shouldUseQuerystring?: boolean): {
|
|
12
|
+
query: IQuery;
|
|
13
|
+
filters: IColumnFilter[];
|
|
14
|
+
onFilterChange: (filter: IColumnFilter) => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
export default function useTableFilters(columnFilters, skip, limit, pageNumber, sort, baseFilter, additionalFilters, shouldUseQuerystring) {
|
|
4
|
+
const [filters, setFilters] = useState(columnFilters);
|
|
5
|
+
const pathname = usePathname();
|
|
6
|
+
const router = useRouter();
|
|
7
|
+
const searchParams = useSearchParams();
|
|
8
|
+
const refPath = useRef('');
|
|
9
|
+
const query = useMemo(() => {
|
|
10
|
+
const coreFilters = filters.filter(filter => typeof filter.value !== 'undefined' && filter.value !== null && filter.value !== '');
|
|
11
|
+
const dateFilters = coreFilters
|
|
12
|
+
.filter(filter => filter.type === 'date')
|
|
13
|
+
.map((filter) => ({
|
|
14
|
+
logic: 'and',
|
|
15
|
+
column: filter.accessor,
|
|
16
|
+
operator: filter.operator,
|
|
17
|
+
dataType: filter.type,
|
|
18
|
+
value: new Date(filter?.value).getTime(),
|
|
19
|
+
}));
|
|
20
|
+
const nonDateFilters = coreFilters
|
|
21
|
+
.filter(filter => filter.type !== 'date')
|
|
22
|
+
.map((filter) => {
|
|
23
|
+
const filterParts = filter?.value?.toString().split(' ') || [];
|
|
24
|
+
if (filterParts.length === 1) {
|
|
25
|
+
return {
|
|
26
|
+
logic: 'and',
|
|
27
|
+
column: filter.accessor,
|
|
28
|
+
operator: filter.operator,
|
|
29
|
+
dataType: filter.type,
|
|
30
|
+
value: filter?.value,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
return {
|
|
35
|
+
logic: 'and',
|
|
36
|
+
filters: filterParts.map((part) => ({
|
|
37
|
+
logic: 'or',
|
|
38
|
+
column: filter.accessor,
|
|
39
|
+
operator: filter.operator,
|
|
40
|
+
dataType: filter.type,
|
|
41
|
+
value: part,
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
limit,
|
|
48
|
+
skip: (pageNumber - 1) * limit,
|
|
49
|
+
filter: {
|
|
50
|
+
logic: 'and',
|
|
51
|
+
...baseFilter,
|
|
52
|
+
filters: [
|
|
53
|
+
...dateFilters,
|
|
54
|
+
...nonDateFilters,
|
|
55
|
+
...additionalFilters || []
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
sort,
|
|
59
|
+
};
|
|
60
|
+
}, [limit, pageNumber, filters, baseFilter, additionalFilters, sort]);
|
|
61
|
+
const onFilterChange = (filter) => {
|
|
62
|
+
setFilters((previousFilters) => ([
|
|
63
|
+
...previousFilters.filter(f => f.accessor !== filter.accessor),
|
|
64
|
+
filter,
|
|
65
|
+
]));
|
|
66
|
+
};
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
69
|
+
const incomingQuery = searchParams.get('query');
|
|
70
|
+
if (refPath.current === '' && !!incomingQuery) {
|
|
71
|
+
const query = JSON.parse(incomingQuery);
|
|
72
|
+
const filters = query?.filter?.filters || [];
|
|
73
|
+
const updatedFiltersPopulated = filters.map(filter => {
|
|
74
|
+
const isNestedFilter = Array.isArray(filter.filters);
|
|
75
|
+
const columnFilter = columnFilters.find(f => f.accessor === filter.column);
|
|
76
|
+
if (columnFilter && !isNestedFilter) {
|
|
77
|
+
return {
|
|
78
|
+
...columnFilter,
|
|
79
|
+
value: filter.value,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (isNestedFilter) {
|
|
83
|
+
const nestedColumn = filter.filters?.[0]?.column;
|
|
84
|
+
const nestedColumnFilter = columnFilters.find(f => f.accessor === nestedColumn);
|
|
85
|
+
return {
|
|
86
|
+
...nestedColumnFilter,
|
|
87
|
+
value: filter.filters?.map(f => f.value).join(' '),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
})
|
|
92
|
+
.filter(filter => filter !== null);
|
|
93
|
+
const update = columnFilters.map(filter => {
|
|
94
|
+
const updatedFilter = updatedFiltersPopulated.find(f => f.accessor === filter.accessor);
|
|
95
|
+
if (updatedFilter) {
|
|
96
|
+
return updatedFilter;
|
|
97
|
+
}
|
|
98
|
+
return filter;
|
|
99
|
+
});
|
|
100
|
+
if (update?.length) {
|
|
101
|
+
setFilters(update);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
params.set('query', JSON.stringify(query));
|
|
106
|
+
}
|
|
107
|
+
const path = `${pathname}?${params.toString()}`;
|
|
108
|
+
if (shouldUseQuerystring && refPath.current !== path) {
|
|
109
|
+
refPath.current = path;
|
|
110
|
+
router.push(path);
|
|
111
|
+
}
|
|
112
|
+
}, [filters, pathname, router, shouldUseQuerystring, searchParams, skip, limit, baseFilter, additionalFilters, sort, columnFilters, query]);
|
|
113
|
+
return {
|
|
114
|
+
query,
|
|
115
|
+
filters,
|
|
116
|
+
onFilterChange,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type TFilterChange = (filter: IColumnFilter) => void;
|
|
2
|
+
export type TColumnFilterType = 'date' | 'string' | 'number' | 'boolean';
|
|
3
|
+
export type TColumnFilterOperation = '%' | '!%' | '^%' | '%^' | '=' | '!=' | '>' | '>=' | '<' | '<=' | 'null';
|
|
4
|
+
export interface IColumnFilter {
|
|
5
|
+
id: string;
|
|
6
|
+
accessor: string;
|
|
7
|
+
type: TColumnFilterType;
|
|
8
|
+
operator: TColumnFilterOperation;
|
|
9
|
+
helperText?: string;
|
|
10
|
+
value?: string | number | Date;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useColorMode } from "../components";
|
|
2
|
-
export const useSelectedColorSchema = (
|
|
2
|
+
export const useSelectedColorSchema = (_color) => {
|
|
3
3
|
const { colorMode } = useColorMode();
|
|
4
4
|
return {
|
|
5
|
-
color:
|
|
6
|
-
bgColor: colorMode === 'light' ?
|
|
5
|
+
color: 'neu.text',
|
|
6
|
+
bgColor: colorMode === 'light' ? 'neu.bg' : 'neu.surface',
|
|
7
|
+
boxShadow: 'neuInset',
|
|
7
8
|
};
|
|
8
9
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { expect, userEvent, within } from 'storybook/test';
|
|
1
2
|
import { Breadcrumbs } from '../components/Breadcrumbs';
|
|
2
3
|
const onClick = (nav) => {
|
|
3
4
|
console.log('Breadcrumb clicked:', nav);
|
|
@@ -23,3 +24,17 @@ export const Component = {
|
|
|
23
24
|
breadcrumbs: EXAMPLE_BREADCRUMBS,
|
|
24
25
|
}
|
|
25
26
|
};
|
|
27
|
+
export const KeyboardFocus = {
|
|
28
|
+
name: 'Keyboard Focus',
|
|
29
|
+
args: {
|
|
30
|
+
breadcrumbs: EXAMPLE_BREADCRUMBS,
|
|
31
|
+
},
|
|
32
|
+
play: async ({ canvasElement }) => {
|
|
33
|
+
const canvas = within(canvasElement);
|
|
34
|
+
const links = canvas.getAllByRole('button');
|
|
35
|
+
for (const link of links) {
|
|
36
|
+
await userEvent.tab();
|
|
37
|
+
await expect(link).toHaveFocus();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { IDataTableProps } from '../components/table/DataTable';
|
|
3
|
+
declare const meta: {
|
|
4
|
+
title: string;
|
|
5
|
+
component: import("react").FC<IDataTableProps>;
|
|
6
|
+
tags: string[];
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: string;
|
|
9
|
+
};
|
|
10
|
+
args: {
|
|
11
|
+
data: never[];
|
|
12
|
+
total: number;
|
|
13
|
+
pageNumber: number;
|
|
14
|
+
pagesTotal: number;
|
|
15
|
+
pageSize: number;
|
|
16
|
+
setPageNumber: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
17
|
+
setPageSize: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
18
|
+
columns: never[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
export declare const Default: Story;
|
|
24
|
+
export declare const WithRowClick: Story;
|
|
25
|
+
export declare const EmptyState: Story;
|
|
26
|
+
export declare const WithFilters: Story;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import DataTable from '../components/table/DataTable';
|
|
4
|
+
const noopSetPageNumber = () => undefined;
|
|
5
|
+
const noopSetPageSize = () => undefined;
|
|
6
|
+
const SAMPLE_DATA = [
|
|
7
|
+
{ id: 'P-001', name: 'Alice Johnson', dob: '1985-03-12', provider: 'Dr. Smith', status: 'Active', balance: 125.0 },
|
|
8
|
+
{ id: 'P-002', name: 'Bob Martinez', dob: '1990-07-24', provider: 'Dr. Patel', status: 'Inactive', balance: 0.0 },
|
|
9
|
+
{ id: 'P-003', name: 'Carol White', dob: '1978-11-05', provider: 'Dr. Smith', status: 'Active', balance: 340.5 },
|
|
10
|
+
{ id: 'P-004', name: 'David Brown', dob: '2000-01-30', provider: 'Dr. Lee', status: 'Pending', balance: 75.25 },
|
|
11
|
+
{ id: 'P-005', name: 'Eva Green', dob: '1995-09-18', provider: 'Dr. Patel', status: 'Active', balance: 210.0 },
|
|
12
|
+
];
|
|
13
|
+
const COLUMNS = [
|
|
14
|
+
{
|
|
15
|
+
accessorKey: 'id',
|
|
16
|
+
header: 'ID',
|
|
17
|
+
cell: (info) => info.getValue(),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
accessorKey: 'name',
|
|
21
|
+
header: 'Patient Name',
|
|
22
|
+
cell: (info) => info.getValue(),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
accessorKey: 'dob',
|
|
26
|
+
header: 'Date of Birth',
|
|
27
|
+
cell: (info) => info.getValue(),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
accessorKey: 'provider',
|
|
31
|
+
header: 'Provider',
|
|
32
|
+
cell: (info) => info.getValue(),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
accessorKey: 'status',
|
|
36
|
+
header: 'Status',
|
|
37
|
+
cell: (info) => info.getValue(),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
accessorKey: 'balance',
|
|
41
|
+
header: 'Balance',
|
|
42
|
+
cell: (info) => `$${info.getValue().toFixed(2)}`,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
function DataTableDemo(props) {
|
|
46
|
+
const [pageNumber, setPageNumber] = useState(1);
|
|
47
|
+
const [pageSize, setPageSize] = useState(25);
|
|
48
|
+
return (_jsx(DataTable, { data: SAMPLE_DATA, total: SAMPLE_DATA.length, columns: COLUMNS, pageNumber: pageNumber, pagesTotal: Math.ceil(SAMPLE_DATA.length / pageSize), pageSize: pageSize, setPageNumber: setPageNumber, setPageSize: setPageSize, ...props }));
|
|
49
|
+
}
|
|
50
|
+
const meta = {
|
|
51
|
+
title: 'Data/DataTable',
|
|
52
|
+
component: DataTable,
|
|
53
|
+
tags: ['autodocs'],
|
|
54
|
+
parameters: {
|
|
55
|
+
layout: 'padded',
|
|
56
|
+
},
|
|
57
|
+
args: {
|
|
58
|
+
data: [],
|
|
59
|
+
total: 0,
|
|
60
|
+
pageNumber: 1,
|
|
61
|
+
pagesTotal: 1,
|
|
62
|
+
pageSize: 25,
|
|
63
|
+
setPageNumber: noopSetPageNumber,
|
|
64
|
+
setPageSize: noopSetPageSize,
|
|
65
|
+
columns: [],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
export default meta;
|
|
69
|
+
export const Default = {
|
|
70
|
+
args: {},
|
|
71
|
+
render: () => _jsx(DataTableDemo, {}),
|
|
72
|
+
};
|
|
73
|
+
export const WithRowClick = {
|
|
74
|
+
args: {},
|
|
75
|
+
render: () => (_jsx(DataTableDemo, { onRowClick: (row) => alert(`Clicked: ${row.name}`) })),
|
|
76
|
+
};
|
|
77
|
+
export const EmptyState = {
|
|
78
|
+
args: {},
|
|
79
|
+
render: () => _jsx(EmptyStateDemo, {}),
|
|
80
|
+
};
|
|
81
|
+
function EmptyStateDemo() {
|
|
82
|
+
const [pageNumber, setPageNumber] = useState(1);
|
|
83
|
+
const [pageSize, setPageSize] = useState(25);
|
|
84
|
+
return (_jsx(DataTable, { data: [], total: 0, columns: COLUMNS, pageNumber: pageNumber, pagesTotal: 0, pageSize: pageSize, setPageNumber: setPageNumber, setPageSize: setPageSize }));
|
|
85
|
+
}
|
|
86
|
+
export const WithFilters = {
|
|
87
|
+
args: {},
|
|
88
|
+
render: () => (_jsx(DataTableDemo, { filters: [
|
|
89
|
+
{ id: 'name', accessor: 'name', type: 'string', operator: '%', helperText: 'Search by name' },
|
|
90
|
+
{ id: 'status', accessor: 'status', type: 'string', operator: '=', helperText: 'Filter by status' },
|
|
91
|
+
], onFilterChange: (filter) => console.log('Filter changed:', filter) })),
|
|
92
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { expect, userEvent, within } from 'storybook/test';
|
|
1
2
|
import { Form } from '../components';
|
|
2
3
|
const meta = {
|
|
3
4
|
title: 'Forms/FileUploader',
|
|
@@ -22,3 +23,21 @@ export const Component = {
|
|
|
22
23
|
onFilesSelected: (files) => console.log(files),
|
|
23
24
|
}
|
|
24
25
|
};
|
|
26
|
+
export const KeyboardFocus = {
|
|
27
|
+
name: 'Keyboard Focus',
|
|
28
|
+
args: {
|
|
29
|
+
title: 'Upload Files',
|
|
30
|
+
description: 'Drag and drop files in the area below, or choose files from your device.',
|
|
31
|
+
buttonLabel: 'Browse Files',
|
|
32
|
+
helperText: 'Accepted: image/*, .pdf',
|
|
33
|
+
accept: 'image/*',
|
|
34
|
+
multiple: true,
|
|
35
|
+
onFilesSelected: (files) => console.log(files),
|
|
36
|
+
},
|
|
37
|
+
play: async ({ canvasElement }) => {
|
|
38
|
+
const canvas = within(canvasElement);
|
|
39
|
+
await userEvent.tab();
|
|
40
|
+
const chooseButton = canvas.getByRole('button', { name: /browse files/i });
|
|
41
|
+
await expect(chooseButton).toHaveFocus();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { expect, userEvent, within } from 'storybook/test';
|
|
1
2
|
import { Form } from '../components';
|
|
2
3
|
const meta = {
|
|
3
4
|
title: 'Forms/Login',
|
|
@@ -18,3 +19,21 @@ export const Component = {
|
|
|
18
19
|
onLogin: (data) => console.log(data)
|
|
19
20
|
}
|
|
20
21
|
};
|
|
22
|
+
export const KeyboardFocus = {
|
|
23
|
+
name: 'Keyboard Focus',
|
|
24
|
+
args: {
|
|
25
|
+
title: "Login Form",
|
|
26
|
+
description: "Please enter your credentials to continue",
|
|
27
|
+
onLogin: (data) => console.log(data)
|
|
28
|
+
},
|
|
29
|
+
play: async ({ canvasElement }) => {
|
|
30
|
+
const canvas = within(canvasElement);
|
|
31
|
+
await userEvent.tab();
|
|
32
|
+
const emailInput = canvas.getByRole('textbox', { name: /email address/i });
|
|
33
|
+
await expect(emailInput).toHaveFocus();
|
|
34
|
+
await userEvent.tab();
|
|
35
|
+
await userEvent.tab();
|
|
36
|
+
const submitButton = canvas.getByRole('button', { name: /login/i });
|
|
37
|
+
await expect(submitButton).toHaveFocus();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { expect, userEvent, within } from 'storybook/test';
|
|
1
2
|
import { Form } from '../components';
|
|
2
3
|
const meta = {
|
|
3
4
|
title: 'Forms/SecondFactorAuth',
|
|
@@ -19,3 +20,25 @@ export const Component = {
|
|
|
19
20
|
onResendAuthCode: () => console.log("Resend auth code")
|
|
20
21
|
}
|
|
21
22
|
};
|
|
23
|
+
export const KeyboardFocus = {
|
|
24
|
+
name: 'Keyboard Focus',
|
|
25
|
+
args: {
|
|
26
|
+
title: "Second FactorAuth Form",
|
|
27
|
+
description: "Please enter your auth code to continue",
|
|
28
|
+
onSecondFactorAuth: (data) => console.log(data),
|
|
29
|
+
onResendAuthCode: () => console.log("Resend auth code")
|
|
30
|
+
},
|
|
31
|
+
play: async ({ canvasElement }) => {
|
|
32
|
+
const canvas = within(canvasElement);
|
|
33
|
+
await userEvent.tab();
|
|
34
|
+
const codeInput = canvas.getByPlaceholderText(/authorization code/i);
|
|
35
|
+
await expect(codeInput).toHaveFocus();
|
|
36
|
+
await userEvent.tab();
|
|
37
|
+
const resendButton = canvas.getByRole('button', { name: /resend/i });
|
|
38
|
+
await expect(resendButton).toHaveFocus();
|
|
39
|
+
await userEvent.tab();
|
|
40
|
+
await userEvent.tab();
|
|
41
|
+
const submitButton = canvas.getByRole('button', { name: /submit/i });
|
|
42
|
+
await expect(submitButton).toHaveFocus();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { expect, userEvent, within } from 'storybook/test';
|
|
1
2
|
import { VerifyAction } from '../components';
|
|
2
3
|
const meta = {
|
|
3
4
|
title: 'Feedback/VerifyAction',
|
|
@@ -17,3 +18,22 @@ export const Component = {
|
|
|
17
18
|
onProceed: () => console.log('proceed')
|
|
18
19
|
}
|
|
19
20
|
};
|
|
21
|
+
export const KeyboardFocus = {
|
|
22
|
+
name: 'Keyboard Focus — Destructive Action',
|
|
23
|
+
args: {
|
|
24
|
+
isOpen: true,
|
|
25
|
+
heading: 'Are you sure?',
|
|
26
|
+
subheading: `This action can't be undone!`,
|
|
27
|
+
onCancel: () => console.log('close dialog'),
|
|
28
|
+
onProceed: () => console.log('proceed')
|
|
29
|
+
},
|
|
30
|
+
play: async ({ canvasElement }) => {
|
|
31
|
+
const canvas = within(canvasElement);
|
|
32
|
+
await userEvent.tab();
|
|
33
|
+
const closeButton = canvas.getByRole('button', { name: /close/i });
|
|
34
|
+
await expect(closeButton).toHaveFocus();
|
|
35
|
+
await userEvent.tab();
|
|
36
|
+
const proceedButton = canvas.getByRole('button', { name: /proceed/i });
|
|
37
|
+
await expect(proceedButton).toHaveFocus();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react";
|
|
2
|
+
const config = defineConfig({
|
|
3
|
+
globalCss: {
|
|
4
|
+
"html, body": {
|
|
5
|
+
bg: "neu.bg",
|
|
6
|
+
color: "neu.text",
|
|
7
|
+
minHeight: "100%",
|
|
8
|
+
},
|
|
9
|
+
".sb-show-main": {
|
|
10
|
+
bg: "neu.bg",
|
|
11
|
+
minHeight: "100vh",
|
|
12
|
+
},
|
|
13
|
+
button: {
|
|
14
|
+
bg: "neu.surface",
|
|
15
|
+
borderColor: "neu.border",
|
|
16
|
+
borderWidth: "1px",
|
|
17
|
+
borderRadius: "14px",
|
|
18
|
+
boxShadow: "neuRaised",
|
|
19
|
+
transition: "transform 0.12s ease, box-shadow 0.16s ease, background-color 0.16s ease",
|
|
20
|
+
_hover: {
|
|
21
|
+
transform: "translateY(-1px)",
|
|
22
|
+
},
|
|
23
|
+
_active: {
|
|
24
|
+
transform: "translateY(0)",
|
|
25
|
+
boxShadow: "neuInset",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
"input, textarea, select": {
|
|
29
|
+
bg: "neu.surface",
|
|
30
|
+
borderColor: "neu.border",
|
|
31
|
+
borderWidth: "1px",
|
|
32
|
+
borderRadius: "12px",
|
|
33
|
+
boxShadow: "neuInset",
|
|
34
|
+
},
|
|
35
|
+
"[data-scope='card'][data-part='root']": {
|
|
36
|
+
bg: "neu.surface",
|
|
37
|
+
borderColor: "neu.border",
|
|
38
|
+
borderWidth: "1px",
|
|
39
|
+
borderRadius: "20px",
|
|
40
|
+
boxShadow: "neuRaised",
|
|
41
|
+
},
|
|
42
|
+
"button:focus-visible, [role='button']:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible": {
|
|
43
|
+
outline: "2px solid",
|
|
44
|
+
outlineColor: "neu.accent",
|
|
45
|
+
outlineOffset: "2px",
|
|
46
|
+
},
|
|
47
|
+
"[data-scope='button'][data-color-palette='red']": {
|
|
48
|
+
bg: "red.600",
|
|
49
|
+
color: "white",
|
|
50
|
+
borderColor: "red.700",
|
|
51
|
+
boxShadow: "none",
|
|
52
|
+
_hover: {
|
|
53
|
+
bg: "red.700",
|
|
54
|
+
},
|
|
55
|
+
_active: {
|
|
56
|
+
bg: "red.800",
|
|
57
|
+
},
|
|
58
|
+
_focusVisible: {
|
|
59
|
+
outlineColor: "red.300",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
theme: {
|
|
64
|
+
semanticTokens: {
|
|
65
|
+
colors: {
|
|
66
|
+
neu: {
|
|
67
|
+
bg: { value: { base: "#e6ebf2", _dark: "#161c24" } },
|
|
68
|
+
surface: { value: { base: "#edf2f8", _dark: "#1f2732" } },
|
|
69
|
+
text: { value: { base: "#1f2937", _dark: "#e5e7eb" } },
|
|
70
|
+
border: { value: { base: "#d7dee8", _dark: "#2a3442" } },
|
|
71
|
+
muted: { value: { base: "#64748b", _dark: "#94a3b8" } },
|
|
72
|
+
accent: { value: { base: "#2b6cb0", _dark: "#63b3ed" } },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
shadows: {
|
|
76
|
+
neuRaised: {
|
|
77
|
+
value: {
|
|
78
|
+
base: "10px 10px 24px rgba(163, 177, 198, 0.58), -10px -10px 24px rgba(255, 255, 255, 0.9)",
|
|
79
|
+
_dark: "10px 10px 24px rgba(0, 0, 0, 0.62), -10px -10px 24px rgba(255, 255, 255, 0.04)",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
neuInset: {
|
|
83
|
+
value: {
|
|
84
|
+
base: "inset 8px 8px 18px rgba(163, 177, 198, 0.46), inset -8px -8px 18px rgba(255, 255, 255, 0.8)",
|
|
85
|
+
_dark: "inset 8px 8px 18px rgba(0, 0, 0, 0.56), inset -8px -8px 18px rgba(255, 255, 255, 0.04)",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
layerStyles: {
|
|
91
|
+
neuRaised: {
|
|
92
|
+
value: {
|
|
93
|
+
bg: "neu.surface",
|
|
94
|
+
borderRadius: "24px",
|
|
95
|
+
borderWidth: "1px",
|
|
96
|
+
borderColor: "neu.border",
|
|
97
|
+
boxShadow: "neuRaised",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
neuInset: {
|
|
101
|
+
value: {
|
|
102
|
+
bg: "neu.surface",
|
|
103
|
+
borderRadius: "20px",
|
|
104
|
+
borderWidth: "1px",
|
|
105
|
+
borderColor: "neu.border",
|
|
106
|
+
boxShadow: "neuInset",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const system = createSystem(defaultConfig, config);
|
|
113
|
+
export default system;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vrobots/storybook",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
"@chakra-ui/react": "^3.32.0",
|
|
22
22
|
"@emotion/react": "^11.14.0",
|
|
23
23
|
"@storybook/addon-themes": "^10.2.16",
|
|
24
|
+
"@tanstack/react-table": "^8.21.3",
|
|
24
25
|
"axios": "^1.13.4",
|
|
25
26
|
"motion": "^12.34.0",
|
|
27
|
+
"next": "^16.2.4",
|
|
26
28
|
"next-themes": "^0.4.6",
|
|
27
29
|
"owasp-password-strength-test": "^1.3.0",
|
|
28
30
|
"react": "^19.2.0",
|