@tiny-server/design 0.0.0-pre202605241751

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 (54) hide show
  1. package/dist/components/badge-group/BadgeGroup.d.ts +6 -0
  2. package/dist/components/badge-group/BadgeGroup.stories.d.ts +6 -0
  3. package/dist/components/badge-group/index.d.ts +1 -0
  4. package/dist/components/checkbox-card/CheckboxCard.d.ts +7 -0
  5. package/dist/components/checkbox-card/CheckboxCard.stories.d.ts +6 -0
  6. package/dist/components/checkbox-card/index.d.ts +1 -0
  7. package/dist/components/form/Form.d.ts +15 -0
  8. package/dist/components/form/Form.stories.d.ts +6 -0
  9. package/dist/components/form/FormAlert.d.ts +6 -0
  10. package/dist/components/form/FormFooter.d.ts +4 -0
  11. package/dist/components/form/FormSection.d.ts +4 -0
  12. package/dist/components/form/index.d.ts +1 -0
  13. package/dist/components/image-cropper/ImageCropper.d.ts +11 -0
  14. package/dist/components/image-cropper/cropImage.d.ts +11 -0
  15. package/dist/components/image-cropper/index.d.ts +1 -0
  16. package/dist/components/index.d.ts +6 -0
  17. package/dist/components/labeled-info/LabeledInfo.d.ts +19 -0
  18. package/dist/components/labeled-info/LabeledInfo.stories.d.ts +6 -0
  19. package/dist/components/labeled-info/index.d.ts +1 -0
  20. package/dist/components/section-card/Header.d.ts +18 -0
  21. package/dist/components/section-card/Section.d.ts +14 -0
  22. package/dist/components/section-card/SectionCard.d.ts +21 -0
  23. package/dist/components/section-card/SectionCard.stories.d.ts +7 -0
  24. package/dist/components/section-card/index.d.ts +3 -0
  25. package/dist/index.css +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +312 -0
  28. package/package.json +52 -0
  29. package/src/components/badge-group/BadgeGroup.stories.tsx +24 -0
  30. package/src/components/badge-group/BadgeGroup.tsx +24 -0
  31. package/src/components/badge-group/index.ts +1 -0
  32. package/src/components/checkbox-card/CheckboxCard.stories.tsx +33 -0
  33. package/src/components/checkbox-card/CheckboxCard.tsx +22 -0
  34. package/src/components/checkbox-card/index.ts +1 -0
  35. package/src/components/form/Form.stories.tsx +30 -0
  36. package/src/components/form/Form.tsx +22 -0
  37. package/src/components/form/FormAlert.tsx +25 -0
  38. package/src/components/form/FormFooter.tsx +7 -0
  39. package/src/components/form/FormSection.tsx +7 -0
  40. package/src/components/form/index.ts +1 -0
  41. package/src/components/image-cropper/ImageCropper.tsx +73 -0
  42. package/src/components/image-cropper/cropImage.ts +87 -0
  43. package/src/components/image-cropper/index.ts +1 -0
  44. package/src/components/index.ts +6 -0
  45. package/src/components/labeled-info/LabeledInfo.stories.tsx +27 -0
  46. package/src/components/labeled-info/LabeledInfo.tsx +54 -0
  47. package/src/components/labeled-info/index.ts +1 -0
  48. package/src/components/section-card/Header.tsx +81 -0
  49. package/src/components/section-card/Section.tsx +48 -0
  50. package/src/components/section-card/SectionCard.module.css +40 -0
  51. package/src/components/section-card/SectionCard.stories.tsx +61 -0
  52. package/src/components/section-card/SectionCard.tsx +52 -0
  53. package/src/components/section-card/index.ts +4 -0
  54. package/src/index.ts +1 -0
@@ -0,0 +1,33 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { CheckboxCard } from './CheckboxCard';
3
+
4
+ const meta: Meta<typeof CheckboxCard> = {
5
+ component: CheckboxCard,
6
+ title: 'Components/CheckboxCard',
7
+ argTypes: {
8
+ label: {
9
+ control: { type: 'text' },
10
+ },
11
+ description: {
12
+ control: { type: 'text' },
13
+ },
14
+ disabled: {
15
+ control: { type: 'boolean' },
16
+ },
17
+ children: {
18
+ control: false,
19
+ },
20
+ },
21
+ };
22
+
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {
26
+ args: {
27
+ label: 'Accept terms',
28
+ description: 'I agree to the terms and conditions.',
29
+ defaultChecked: true,
30
+ },
31
+ };
32
+
33
+ export default meta;
@@ -0,0 +1,22 @@
1
+ import { Checkbox, Group, Input, type CheckboxCardProps as MantineCheckboxCardProps } from '@mantine/core';
2
+ import type { ReactNode } from 'react';
3
+
4
+ export interface CheckboxCardProps extends MantineCheckboxCardProps {
5
+ label?: ReactNode;
6
+ description?: ReactNode;
7
+ }
8
+
9
+ export function CheckboxCard({ label, description, children, disabled, ...rest }: CheckboxCardProps) {
10
+ return (
11
+ <Checkbox.Card radius="md" p="md" disabled={disabled} {...rest}>
12
+ <Group h="100%" wrap="nowrap" align="flex-start">
13
+ <Checkbox.Indicator mt="calc(var(--mantine-spacing-xs) / 2)" disabled={disabled} />
14
+ <div>
15
+ {label && <Input.Label htmlFor={rest.id}>{label}</Input.Label>}
16
+ {description && <Input.Description>{description}</Input.Description>}
17
+ {children}
18
+ </div>
19
+ </Group>
20
+ </Checkbox.Card>
21
+ );
22
+ }
@@ -0,0 +1 @@
1
+ export * from './CheckboxCard';
@@ -0,0 +1,30 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Button, TextInput } from '@mantine/core';
3
+ import { Form } from './Form';
4
+
5
+ const meta: Meta<typeof Form> = {
6
+ component: Form,
7
+ title: 'Components/Form',
8
+ };
9
+
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const Default: Story = {
13
+ render: () => (
14
+ <Form>
15
+ <Form.Alert kind="success" title="Success!">
16
+ Your changes have been saved.
17
+ </Form.Alert>
18
+ <Form.Section>
19
+ <TextInput label="Email" placeholder="Enter your email" />
20
+ <TextInput label="Password" type="password" placeholder="Enter your password" />
21
+ </Form.Section>
22
+ <Form.Footer>
23
+ <Button variant="default">Cancel</Button>
24
+ <Button>Submit</Button>
25
+ </Form.Footer>
26
+ </Form>
27
+ ),
28
+ };
29
+
30
+ export default meta;
@@ -0,0 +1,22 @@
1
+ import { Container, type ContainerProps, type StackProps, Stack, type ElementProps } from '@mantine/core';
2
+ import { type ReactNode, type Ref } from 'react';
3
+ import { FormSection } from './FormSection';
4
+ import { FormFooter } from './FormFooter';
5
+ import { FormAlert } from './FormAlert';
6
+
7
+ export interface FormProps extends ElementProps<'form'>, Omit<ContainerProps, keyof ElementProps<'div'>> {
8
+ gap?: StackProps['gap'];
9
+ children?: ReactNode;
10
+ }
11
+
12
+ export function Form({ ref, size = 'xs', p = 0, gap = 'lg', children, ...rest }: FormProps) {
13
+ return (
14
+ <Container {...(rest as any)} component="form" ref={ref as Ref<HTMLDivElement>} mx={0} size={size} p={p}>
15
+ <Stack gap={gap}>{children}</Stack>
16
+ </Container>
17
+ );
18
+ }
19
+
20
+ Form.Alert = FormAlert;
21
+ Form.Section = FormSection;
22
+ Form.Footer = FormFooter;
@@ -0,0 +1,25 @@
1
+ import { Alert, type AlertProps } from '@mantine/core';
2
+ import { Check, CircleAlert } from 'lucide-react';
3
+ import type { ReactNode } from 'react';
4
+
5
+ export type FormAlertKind = 'success' | 'error';
6
+
7
+ export interface FormAlertProps extends Omit<AlertProps, 'color' | 'icon'> {
8
+ kind: FormAlertKind;
9
+ }
10
+
11
+ const colorMap: Record<FormAlertKind, AlertProps['color']> = {
12
+ success: 'green',
13
+ error: 'red',
14
+ };
15
+
16
+ const iconMap: Record<FormAlertKind, ReactNode> = {
17
+ success: <Check size={16} />,
18
+ error: <CircleAlert size={16} />,
19
+ };
20
+
21
+ export function FormAlert({ kind, variant = 'outline', ...rest }: FormAlertProps) {
22
+ const color = colorMap[kind];
23
+ const icon = iconMap[kind];
24
+ return <Alert variant={variant} color={color} icon={icon} {...rest} />;
25
+ }
@@ -0,0 +1,7 @@
1
+ import { type GroupProps, Group } from '@mantine/core';
2
+
3
+ export interface FormFooterProps extends GroupProps {}
4
+
5
+ export function FormFooter({ gap = 'md', wrap = 'wrap', ...rest }: FormFooterProps) {
6
+ return <Group gap={gap} wrap={wrap} {...rest} />;
7
+ }
@@ -0,0 +1,7 @@
1
+ import { type StackProps, Stack } from '@mantine/core';
2
+
3
+ export interface FormSectionProps extends StackProps {}
4
+
5
+ export function FormSection({ gap = 'md', ...rest }: FormSectionProps) {
6
+ return <Stack gap={gap} {...rest} />;
7
+ }
@@ -0,0 +1 @@
1
+ export * from './Form';
@@ -0,0 +1,73 @@
1
+ import { useState, type ReactNode } from 'react';
2
+ import Cropper, { type Area, type CropperProps, type Point } from 'react-easy-crop';
3
+ import { cropImage } from './cropImage';
4
+ import { Box, LoadingOverlay } from '@mantine/core';
5
+ import { useDebouncedValue } from '@mantine/hooks';
6
+
7
+ export interface ImageCropperProps extends Partial<
8
+ Omit<
9
+ CropperProps,
10
+ 'crop' | 'zoom' | 'rotation' | 'onCropChange' | 'onZoomChange' | 'onRotationChange' | 'onCropComplete'
11
+ >
12
+ > {
13
+ src?: string;
14
+ initialCrop?: CropperProps['crop'];
15
+ initialZoom?: CropperProps['zoom'];
16
+ initialRotation?: CropperProps['rotation'];
17
+ fallback?: ReactNode;
18
+ onCropped?(croppedImageBlob: File): void;
19
+ }
20
+
21
+ export function ImageCropper({
22
+ src,
23
+ initialCrop = { x: 0, y: 0 },
24
+ initialZoom = 1,
25
+ initialRotation = 0,
26
+ fallback,
27
+ onCropped,
28
+ ...rest
29
+ }: ImageCropperProps) {
30
+ const [crop, setCrop] = useState<Point>(initialCrop);
31
+ const [zoom, setZoom] = useState(initialZoom);
32
+ const [rotation, setRotation] = useState(initialRotation);
33
+
34
+ const [isCropping, setIsCropping] = useState(false);
35
+ const [debouncedIsCropping] = useDebouncedValue(isCropping, 250, { leading: false });
36
+
37
+ const handleCropComplete = async (_: Area, croppedAreaPixels: Area) => {
38
+ try {
39
+ if (isCropping) {
40
+ return;
41
+ }
42
+
43
+ setIsCropping(true);
44
+ const blob = await cropImage(src!, croppedAreaPixels!);
45
+ onCropped?.(blob);
46
+ } catch (e) {
47
+ console.error('Image cropping failed.', e);
48
+ } finally {
49
+ setIsCropping(false);
50
+ }
51
+ };
52
+
53
+ if (!src) {
54
+ return fallback;
55
+ }
56
+
57
+ return (
58
+ <Box pos="relative" w="100%" h="100%">
59
+ <LoadingOverlay visible={debouncedIsCropping} />
60
+ <Cropper
61
+ {...rest}
62
+ image={src}
63
+ crop={crop}
64
+ zoom={zoom}
65
+ rotation={rotation}
66
+ onCropChange={setCrop}
67
+ onZoomChange={setZoom}
68
+ onRotationChange={setRotation}
69
+ onCropComplete={handleCropComplete}
70
+ />
71
+ </Box>
72
+ );
73
+ }
@@ -0,0 +1,87 @@
1
+ import type { Area } from 'react-easy-crop';
2
+
3
+ /**
4
+ * Creates an image blob, representing a cropped version of the source image.
5
+ *
6
+ * Source:
7
+ * https://valentinh.github.io/react-easy-crop/docs/examples/output
8
+ */
9
+ export async function cropImage(
10
+ imageSrc: string,
11
+ pixelCrop: Area,
12
+ rotation = 0,
13
+ flip = { horizontal: false, vertical: false },
14
+ ) {
15
+ const image = await createImage(imageSrc);
16
+ const canvas = document.createElement('canvas');
17
+ const ctx = canvas.getContext('2d');
18
+
19
+ if (!ctx) {
20
+ throw new Error('Failed to create canvas context.');
21
+ }
22
+
23
+ const rotRad = getRadianAngle(rotation);
24
+ const { width: bBoxWidth, height: bBoxHeight } = rotateSize(image.width, image.height, rotation);
25
+
26
+ canvas.width = bBoxWidth;
27
+ canvas.height = bBoxHeight;
28
+
29
+ ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
30
+ ctx.rotate(rotRad);
31
+ ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
32
+ ctx.translate(-image.width / 2, -image.height / 2);
33
+ ctx.drawImage(image, 0, 0);
34
+
35
+ const croppedCanvas = document.createElement('canvas');
36
+ const croppedCtx = croppedCanvas.getContext('2d');
37
+
38
+ if (!croppedCtx) {
39
+ throw new Error('Failed to create canvas context.');
40
+ }
41
+
42
+ croppedCanvas.width = pixelCrop.width;
43
+ croppedCanvas.height = pixelCrop.height;
44
+
45
+ croppedCtx.drawImage(
46
+ canvas,
47
+ pixelCrop.x,
48
+ pixelCrop.y,
49
+ pixelCrop.width,
50
+ pixelCrop.height,
51
+ 0,
52
+ 0,
53
+ pixelCrop.width,
54
+ pixelCrop.height,
55
+ );
56
+
57
+ return new Promise<File>((res, rej) =>
58
+ croppedCanvas.toBlob(
59
+ (blob) =>
60
+ blob ? res(new File([blob], 'img.png', { type: blob.type })) : rej(new Error('Failed to crop image.')),
61
+ 'image/png',
62
+ ),
63
+ );
64
+ }
65
+
66
+ function getRadianAngle(degreeValue: number) {
67
+ return (degreeValue * Math.PI) / 180;
68
+ }
69
+
70
+ function rotateSize(width: number, height: number, rotation: number) {
71
+ const rotRad = getRadianAngle(rotation);
72
+
73
+ return {
74
+ width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
75
+ height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
76
+ };
77
+ }
78
+
79
+ function createImage(url: string) {
80
+ return new Promise<HTMLImageElement>((resolve, reject) => {
81
+ const image = new Image();
82
+ image.addEventListener('load', () => resolve(image));
83
+ image.addEventListener('error', reject);
84
+ image.setAttribute('crossOrigin', 'anonymous');
85
+ image.src = url;
86
+ });
87
+ }
@@ -0,0 +1 @@
1
+ export * from './ImageCropper';
@@ -0,0 +1,6 @@
1
+ export * from './badge-group';
2
+ export * from './checkbox-card';
3
+ export * from './form';
4
+ export * from './image-cropper';
5
+ export * from './labeled-info';
6
+ export * from './section-card';
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Info } from 'lucide-react';
3
+ import { LabeledInfo } from './LabeledInfo';
4
+
5
+ const meta: Meta<typeof LabeledInfo> = {
6
+ component: LabeledInfo,
7
+ title: 'Components/LabeledInfo',
8
+ argTypes: {
9
+ variant: {
10
+ control: { type: 'select' },
11
+ options: ['default', 'inline'],
12
+ },
13
+ },
14
+ };
15
+
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ icon: <Info size="1em" />,
21
+ label: 'Label',
22
+ children: 'Child content',
23
+ fallback: '--',
24
+ },
25
+ };
26
+
27
+ export default meta;
@@ -0,0 +1,54 @@
1
+ import { Center, Group, Stack, Text, Tooltip } from '@mantine/core';
2
+ import type { ReactNode } from 'react';
3
+
4
+ /**
5
+ * {@link LabeledInfo} variants.
6
+ *
7
+ * - `default`: Renders an icon, label as header and children below.
8
+ * - `inline`: Renders an icon and the children in a single row.
9
+ */
10
+ export type LabeledInfoVariant = 'default' | 'inline';
11
+
12
+ export interface LabeledInfoProps {
13
+ variant?: LabeledInfoVariant;
14
+ icon?: ReactNode;
15
+ label?: ReactNode;
16
+ children?: ReactNode;
17
+ fallback?: ReactNode;
18
+ }
19
+
20
+ /**
21
+ * Enriches the children with an optional label and icon.
22
+ */
23
+ export function LabeledInfo({ icon, label, children, variant = 'default', fallback = '–' }: LabeledInfoProps) {
24
+ const wrappedChildren = <Text component="div">{children || fallback}</Text>;
25
+
26
+ if (variant === 'inline') {
27
+ const wrappedIcon = (
28
+ <Text component="div" c="dimmed">
29
+ <Center>{icon}</Center>
30
+ </Text>
31
+ );
32
+
33
+ return (
34
+ <Group gap="xs" align="center" wrap="nowrap">
35
+ {label ? <Tooltip label={label}>{wrappedIcon}</Tooltip> : wrappedIcon}
36
+ {wrappedChildren}
37
+ </Group>
38
+ );
39
+ }
40
+
41
+ return (
42
+ <Stack gap={0}>
43
+ <Text component="div" c="dimmed" fz="sm">
44
+ <Group gap="xs" align="center" wrap="nowrap">
45
+ {icon && <Center>{icon}</Center>}
46
+ <Text component="div" truncate inherit>
47
+ {label}
48
+ </Text>
49
+ </Group>
50
+ </Text>
51
+ {wrappedChildren}
52
+ </Stack>
53
+ );
54
+ }
@@ -0,0 +1 @@
1
+ export * from './LabeledInfo';
@@ -0,0 +1,81 @@
1
+ import {
2
+ Box,
3
+ factory,
4
+ Group,
5
+ Stack,
6
+ Text,
7
+ Title,
8
+ useProps,
9
+ type BoxProps,
10
+ type ElementProps,
11
+ type Factory,
12
+ type StylesApiProps,
13
+ type TextProps,
14
+ type TitleProps,
15
+ } from '@mantine/core';
16
+ import type { ReactNode } from 'react';
17
+ import classes from './SectionCard.module.css';
18
+
19
+ export interface SectionCardHeaderProps
20
+ extends BoxProps, StylesApiProps<SectionCardHeaderFactory>, ElementProps<'header', 'title'> {
21
+ illustration?: ReactNode;
22
+ title?: ReactNode;
23
+ titleProps?: Omit<TitleProps, 'children'>;
24
+ subTitle?: ReactNode;
25
+ subTitleProps?: Omit<TextProps, 'children'>;
26
+ actions?: ReactNode;
27
+ }
28
+
29
+ export type SectionCardHeaderFactory = Factory<{
30
+ props: SectionCardHeaderProps;
31
+ ref: HTMLDivElement;
32
+ }>;
33
+
34
+ const defaultSectionCardHeaderProps = {
35
+ p: 'md',
36
+ } satisfies Partial<SectionCardHeaderProps>;
37
+
38
+ export const SectionCardHeader = factory<SectionCardHeaderFactory>((_props) => {
39
+ const { illustration, title, titleProps, subTitle, subTitleProps, actions, children, className, ...rest } = useProps(
40
+ 'SectionCard.Header',
41
+ defaultSectionCardHeaderProps,
42
+ _props,
43
+ );
44
+
45
+ const titleElement = (
46
+ <Stack gap="xs" className={`${classes['section-card__header__header__title']} ${classes['ellipsis']}`}>
47
+ {title && (
48
+ <Title order={2} {...titleProps} className={`${classes['ellipsis']} ${titleProps?.className ?? ''}`}>
49
+ {title}
50
+ </Title>
51
+ )}
52
+
53
+ {subTitle && (
54
+ <Text
55
+ component="div"
56
+ c="dimmed"
57
+ {...subTitleProps}
58
+ className={`${classes['ellipsis']} ${subTitleProps?.className ?? ''}`}>
59
+ {subTitle}
60
+ </Text>
61
+ )}
62
+ </Stack>
63
+ );
64
+
65
+ return (
66
+ <Box component="header" className={`${classes['section-card__header']} ${className ?? ''}`} {...rest}>
67
+ <div className={classes['section-card__header__header']}>
68
+ {illustration && <div className={classes['section-card__header__header__illustration']}>{illustration}</div>}
69
+ {titleElement}
70
+ {actions && (
71
+ <Group gap="xs" className={classes['section-card__header__header__actions']}>
72
+ {actions}
73
+ </Group>
74
+ )}
75
+ </div>
76
+ {children && <div>{children}</div>}
77
+ </Box>
78
+ );
79
+ });
80
+
81
+ SectionCardHeader.displayName = 'SectionCard.Header';
@@ -0,0 +1,48 @@
1
+ import {
2
+ Box,
3
+ factory,
4
+ Title,
5
+ useProps,
6
+ type BoxProps,
7
+ type ElementProps,
8
+ type Factory,
9
+ type StylesApiProps,
10
+ type TitleProps,
11
+ } from '@mantine/core';
12
+ import type { ReactNode } from 'react';
13
+ import classes from './SectionCard.module.css';
14
+
15
+ export interface SectionCardSectionProps
16
+ extends BoxProps, StylesApiProps<SectionCardSectionFactory>, ElementProps<'section', 'title'> {
17
+ title?: ReactNode;
18
+ titleProps?: Omit<TitleProps, 'children'>;
19
+ }
20
+
21
+ export type SectionCardSectionFactory = Factory<{
22
+ props: SectionCardSectionProps;
23
+ ref: HTMLDivElement;
24
+ }>;
25
+
26
+ const defaultSectionCardSectionProps = {
27
+ p: 'md',
28
+ } satisfies Partial<SectionCardSectionProps>;
29
+
30
+ export const SectionCardSection = factory<SectionCardSectionFactory>((_props) => {
31
+ const { title, titleProps, className, children, ...rest } = useProps(
32
+ 'SectionCard.Section',
33
+ defaultSectionCardSectionProps,
34
+ _props,
35
+ );
36
+ return (
37
+ <Box component="section" className={`${classes['section-card__section']} ${className ?? ''}`} {...rest}>
38
+ {title && (
39
+ <Title order={3} {...titleProps} className={`${classes['ellipsis']} ${titleProps?.className ?? ''}`}>
40
+ {title}
41
+ </Title>
42
+ )}
43
+ {children && <div>{children}</div>}
44
+ </Box>
45
+ );
46
+ });
47
+
48
+ SectionCardSection.displayName = 'SectionCard.Section';
@@ -0,0 +1,40 @@
1
+ .section-card {
2
+ color: var(--mantine-color-text);
3
+
4
+ .section-card__header {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--mantine-spacing-md);
8
+
9
+ .section-card__header__header {
10
+ display: flex;
11
+ flex-direction: row;
12
+ gap: var(--mantine-spacing-md);
13
+ align-items: center;
14
+
15
+ .section-card__header__header__illustration {
16
+ flex: 0 0 auto;
17
+ }
18
+
19
+ .section-card__header__header__title {
20
+ flex: 1 1 auto;
21
+ min-width: 0;
22
+ }
23
+
24
+ .section-card__header__header__actions {
25
+ flex: 0 0 auto;
26
+ }
27
+ }
28
+ }
29
+
30
+ .section-card__header + .section-card__section,
31
+ .section-card__section + .section-card__section {
32
+ padding-top: 0px !important;
33
+ }
34
+ }
35
+
36
+ .ellipsis {
37
+ white-space: nowrap;
38
+ overflow: hidden;
39
+ text-overflow: ellipsis;
40
+ }
@@ -0,0 +1,61 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { SectionCard } from './SectionCard';
3
+ import { ActionIcon, ThemeIcon } from '@mantine/core';
4
+ import { Ellipsis, Info } from 'lucide-react';
5
+
6
+ const meta: Meta<typeof SectionCard> = {
7
+ component: SectionCard,
8
+ title: 'Components/SectionCard',
9
+ argTypes: {
10
+ withBorder: {
11
+ control: { type: 'boolean' },
12
+ },
13
+ radius: {
14
+ control: { type: 'select' },
15
+ options: ['xs', 'sm', 'md', 'lg', 'xl'],
16
+ },
17
+ shadow: {
18
+ control: { type: 'select' },
19
+ options: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
20
+ },
21
+ children: {
22
+ control: false,
23
+ },
24
+ },
25
+ };
26
+
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ export const Default: Story = {
30
+ args: {
31
+ children: (
32
+ <>
33
+ <SectionCard.Header
34
+ illustration={
35
+ <ThemeIcon variant="light" radius="xl" size="xl">
36
+ <Info size="1em" />
37
+ </ThemeIcon>
38
+ }
39
+ title="SectionCard.Header Title"
40
+ subTitle="Sub title."
41
+ actions={
42
+ <ActionIcon variant="light">
43
+ <Ellipsis size="1em" />
44
+ </ActionIcon>
45
+ }
46
+ />
47
+ <SectionCard.Section title="Section Title">Content</SectionCard.Section>
48
+ <SectionCard.Section title="Section Title">Content</SectionCard.Section>
49
+ <SectionCard.Section title="Section Title">Content</SectionCard.Section>
50
+ </>
51
+ ),
52
+ },
53
+ };
54
+
55
+ export const TitleOnly: Story = {
56
+ args: {
57
+ children: <SectionCard.Header title="Demo Title" />,
58
+ },
59
+ };
60
+
61
+ export default meta;