This commit is contained in:
yohlo
2025-08-20 22:35:40 -05:00
commit f51c278cd3
169 changed files with 8173 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import { SlidePanel } from './slide-panel';
export * from './slide-panel';
export * from './slide-panel-field';
export * from './slide-panel-context';
export default SlidePanel;

View File

@@ -0,0 +1,18 @@
import { ComponentType, createContext } from "react";
interface SlidePanelContextType {
openPanel: (config: PanelConfig) => void;
closePanel: () => void;
}
interface PanelConfig {
title: string;
Component: ComponentType<any>;
value: any;
onChange: (value: any) => void;
componentProps?: Record<string, any>;
}
const SlidePanelContext = createContext<SlidePanelContextType | null>(null);
export { SlidePanelContext, type SlidePanelContextType, type PanelConfig };

View File

@@ -0,0 +1,89 @@
import { Box, Text, UnstyledButton, Flex, Stack } from "@mantine/core";
import { CaretRightIcon } from "@phosphor-icons/react";
import { ComponentType, useContext } from "react";
import { SlidePanelContext } from "./slide-panel-context";
interface SlidePanelFieldProps {
key: string;
value?: any;
onChange?: (value: any) => void;
Component: ComponentType<any>;
title: string;
label?: string;
placeholder?: string;
formatValue?: (value: any) => string;
componentProps?: Record<string, any>;
withAsterisk?: boolean;
error?: string;
}
const SlidePanelField = ({
value,
onChange,
Component,
title,
label,
placeholder = "Select value",
withAsterisk = false,
formatValue,
componentProps,
error,
}: SlidePanelFieldProps) => {
const context = useContext(SlidePanelContext);
if (!context) {
throw new Error('SlidePanelField must be used within a SlidePanel');
}
const handleClick = () => {
if (!onChange) return;
context.openPanel({
title,
Component,
value,
onChange,
componentProps,
});
};
const displayValue = () => {
if (formatValue && value != null) {
return formatValue(value);
}
if (value != null) {
if (value instanceof Date) {
return value.toLocaleDateString();
}
return String(value);
}
return placeholder;
};
return (
<Box>
<UnstyledButton
onClick={handleClick}
p='sm'
style={{
width: '100%',
border: error ? '1px solid var(--mantine-color-error)' : '1px solid var(--mantine-color-dimmed)',
borderRadius: 'var(--mantine-radius-lg)',
backgroundColor: 'var(--mantine-color-body)',
textAlign: 'left',
}}
>
<Flex justify="space-between" align="center">
<Stack>
<Text size="sm" fw={500}>{label}{withAsterisk && <Text span size="sm" c='var(--mantine-color-error)' fw={500} ml={4}>*</Text>}</Text>
<Text size="sm" c='dimmed'>{displayValue()}</Text>
</Stack>
<CaretRightIcon size={24} weight='thin' style={{ marginRight: '12px' }} />
</Flex>
</UnstyledButton>
{error && <Text size="xs" c='var(--mantine-color-error)' fw={500} ml={4} mt={4}>{error}</Text>}
</Box>
);
};
export { SlidePanelField };

View File

@@ -0,0 +1,164 @@
import { Box, Text, Group, ActionIcon, Button, ScrollArea, Divider } from "@mantine/core";
import { ArrowLeftIcon, CheckIcon } from "@phosphor-icons/react";
import { useState, ReactNode} from "react";
import { SlidePanelContext, type PanelConfig } from "./slide-panel-context";
interface SlidePanelProps {
children: ReactNode;
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
onCancel?: () => void;
submitText?: string;
cancelText?: string;
maxHeight?: string;
formProps?: Record<string, any>;
loading?: boolean;
}
/**
* SlidePanel is a form component meant to be used inside a drawer/modal
* It is used to create a form with multiple views/panels that slides in from the side
* Use with SlidePanelField for an extra panel
*/
const SlidePanel = ({
children,
onSubmit,
onCancel,
submitText = "Submit",
cancelText = "Cancel",
maxHeight = "70vh",
formProps = {},
loading = false
}: SlidePanelProps) => {
const [isOpen, setIsOpen] = useState(false);
const [panelConfig, setPanelConfig] = useState<PanelConfig | null>(null);
const [tempValue, setTempValue] = useState<any>(null);
const openPanel = (config: PanelConfig) => {
setPanelConfig(config);
setTempValue(config.value);
setIsOpen(true);
};
const closePanel = () => {
setIsOpen(false);
};
const handleConfirm = () => {
if (panelConfig) {
panelConfig.onChange(tempValue);
}
setIsOpen(false);
};
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSubmit(event);
};
return (
<SlidePanelContext.Provider value={{ openPanel, closePanel }}>
<Box
style={{
position: 'relative',
height: maxHeight,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
>
<Box
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
transform: isOpen ? 'translateX(-100%)' : 'translateX(0)',
transition: 'transform 0.3s ease-in-out',
display: 'flex',
flexDirection: 'column'
}}
>
<form
{...formProps}
onSubmit={handleFormSubmit}
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
...formProps.style,
}}
>
<ScrollArea style={{ flex: 1 }} scrollbarSize={8} scrollbars='y' type='always'>
<Box p="md">
{children}
</Box>
</ScrollArea>
<Box p="sm">
<Group gap="md">
<Button type="submit" fullWidth loading={loading} disabled={loading}>
{submitText}
</Button>
{onCancel && (
<Button
variant="subtle"
color="red"
fullWidth
onClick={onCancel}
type="button"
disabled={loading}
>
{cancelText}
</Button>
)}
</Group>
</Box>
</form>
</Box>
<Box
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
transform: isOpen ? 'translateX(0)' : 'translateX(100%)',
transition: 'transform 0.3s ease-in-out',
backgroundColor: 'var(--mantine-color-body)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
{panelConfig && (
<>
<Group justify="space-between" p="md" align="center" w='100%'>
<ActionIcon variant="transparent" onClick={closePanel}>
<ArrowLeftIcon size={24} />
</ActionIcon>
<Text fw={500}>{panelConfig.title}</Text>
<ActionIcon variant="transparent" color="green" onClick={handleConfirm}>
<CheckIcon size={24} />
</ActionIcon>
</Group>
<Divider h='1px' w='100%' bg='var(--mantine-color-dimmed)' my='xs'/>
<Box>
<panelConfig.Component
value={tempValue}
onChange={setTempValue}
{...(panelConfig.componentProps || {})}
/>
</Box>
</>
)}
</Box>
</Box>
</SlidePanelContext.Provider>
);
};
export { SlidePanel };