vinta-schedule-design-system 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components.json +21 -0
- package/dist/styles.css +3452 -0
- package/package.json +103 -0
- package/src/layout/app-shell.tsx +115 -0
- package/src/layout/app-topbar.stories.tsx +74 -0
- package/src/layout/app-topbar.tsx +145 -0
- package/src/layout/auth-layout.tsx +45 -0
- package/src/layout/box.stories.tsx +86 -0
- package/src/layout/box.tsx +34 -0
- package/src/layout/center.tsx +24 -0
- package/src/layout/container.stories.tsx +44 -0
- package/src/layout/container.tsx +39 -0
- package/src/layout/divider.tsx +64 -0
- package/src/layout/flex.stories.tsx +109 -0
- package/src/layout/flex.test.tsx +71 -0
- package/src/layout/flex.tsx +112 -0
- package/src/layout/grid.stories.tsx +91 -0
- package/src/layout/grid.tsx +132 -0
- package/src/layout/heading.tsx +53 -0
- package/src/layout/index.ts +47 -0
- package/src/layout/layout-style.ts +291 -0
- package/src/layout/link.stories.tsx +54 -0
- package/src/layout/link.test.tsx +75 -0
- package/src/layout/link.tsx +100 -0
- package/src/layout/navbar.stories.tsx +70 -0
- package/src/layout/navbar.tsx +128 -0
- package/src/layout/page-header.stories.tsx +80 -0
- package/src/layout/page-header.tsx +59 -0
- package/src/layout/section.tsx +22 -0
- package/src/layout/spacer.tsx +21 -0
- package/src/layout/stack.stories.tsx +58 -0
- package/src/layout/stack.tsx +19 -0
- package/src/layout/text.tsx +139 -0
- package/src/layout/typography.stories.tsx +112 -0
- package/src/lib/utils.ts +6 -0
- package/src/story-types.ts +80 -0
- package/src/styles/build.css +33 -0
- package/src/styles/globals.css +9 -0
- package/src/styles/tokens.css +281 -0
- package/src/ui/accordion.stories.tsx +84 -0
- package/src/ui/accordion.tsx +57 -0
- package/src/ui/alert-dialog.stories.tsx +57 -0
- package/src/ui/alert-dialog.tsx +141 -0
- package/src/ui/alert.stories.tsx +46 -0
- package/src/ui/alert.tsx +59 -0
- package/src/ui/avatar.stories.tsx +56 -0
- package/src/ui/avatar.tsx +50 -0
- package/src/ui/badge.stories.tsx +105 -0
- package/src/ui/badge.tsx +57 -0
- package/src/ui/breadcrumb.stories.tsx +69 -0
- package/src/ui/breadcrumb.tsx +140 -0
- package/src/ui/button.stories.tsx +85 -0
- package/src/ui/button.tsx +60 -0
- package/src/ui/card.stories.tsx +62 -0
- package/src/ui/card.tsx +96 -0
- package/src/ui/carousel.stories.tsx +55 -0
- package/src/ui/carousel.tsx +262 -0
- package/src/ui/checkbox.stories.tsx +52 -0
- package/src/ui/checkbox.tsx +30 -0
- package/src/ui/combobox.stories.tsx +138 -0
- package/src/ui/combobox.tsx +217 -0
- package/src/ui/command.tsx +184 -0
- package/src/ui/dialog.stories.tsx +66 -0
- package/src/ui/dialog.tsx +163 -0
- package/src/ui/drawer.stories.tsx +80 -0
- package/src/ui/drawer.tsx +118 -0
- package/src/ui/dropdown-menu.stories.tsx +57 -0
- package/src/ui/dropdown-menu.tsx +201 -0
- package/src/ui/form.stories.tsx +70 -0
- package/src/ui/form.tsx +181 -0
- package/src/ui/input-otp.stories.tsx +52 -0
- package/src/ui/input-otp.tsx +71 -0
- package/src/ui/input.stories.tsx +68 -0
- package/src/ui/input.tsx +22 -0
- package/src/ui/label.stories.tsx +35 -0
- package/src/ui/label.tsx +26 -0
- package/src/ui/navigation-menu.stories.tsx +79 -0
- package/src/ui/navigation-menu.tsx +128 -0
- package/src/ui/pagination.stories.tsx +62 -0
- package/src/ui/pagination.tsx +137 -0
- package/src/ui/popover.tsx +89 -0
- package/src/ui/progress.stories.tsx +33 -0
- package/src/ui/progress.tsx +28 -0
- package/src/ui/radio-group.stories.tsx +52 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/scheduling-chip.stories.tsx +117 -0
- package/src/ui/scheduling-chip.tsx +62 -0
- package/src/ui/scroll-area.stories.tsx +52 -0
- package/src/ui/scroll-area.tsx +48 -0
- package/src/ui/select.stories.tsx +56 -0
- package/src/ui/select.tsx +159 -0
- package/src/ui/sheet.stories.tsx +63 -0
- package/src/ui/sheet.tsx +141 -0
- package/src/ui/skeleton.stories.tsx +40 -0
- package/src/ui/skeleton.tsx +15 -0
- package/src/ui/sonner.stories.tsx +62 -0
- package/src/ui/sonner.tsx +34 -0
- package/src/ui/switch.stories.tsx +41 -0
- package/src/ui/switch.tsx +29 -0
- package/src/ui/table.stories.tsx +88 -0
- package/src/ui/table.tsx +132 -0
- package/src/ui/tabs.stories.tsx +63 -0
- package/src/ui/tabs.tsx +55 -0
- package/src/ui/textarea.stories.tsx +45 -0
- package/src/ui/textarea.tsx +22 -0
- package/src/ui/tooltip.stories.tsx +48 -0
- package/src/ui/tooltip.tsx +32 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
import { Box, type BoxProps } from './box';
|
|
5
|
+
import type { Size } from './layout-style';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Container — horizontal layout frame. Centers content and applies the design
|
|
9
|
+
* system's page margins (32px). `width` caps the content:
|
|
10
|
+
* contained → 1200px (DS --container-max) prose → 68ch full → edge-to-edge
|
|
11
|
+
*
|
|
12
|
+
* Accepts all Box style props (e.g. `py`, `bg`) on top of the width cap.
|
|
13
|
+
*/
|
|
14
|
+
export interface ContainerProps extends Omit<BoxProps, 'width' | 'maxWidth'> {
|
|
15
|
+
width?: 'contained' | 'prose' | 'full';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_WIDTH: Record<NonNullable<ContainerProps['width']>, Size> = {
|
|
19
|
+
contained: 1200,
|
|
20
|
+
prose: '68ch',
|
|
21
|
+
full: 'none',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const Container = React.forwardRef<HTMLElement, ContainerProps>(
|
|
25
|
+
function Container({ className, width = 'contained', ...box }, ref) {
|
|
26
|
+
return (
|
|
27
|
+
<Box
|
|
28
|
+
ref={ref}
|
|
29
|
+
mx='auto'
|
|
30
|
+
width='full'
|
|
31
|
+
maxWidth={MAX_WIDTH[width]}
|
|
32
|
+
className={cn('px-4 md:px-8', className)}
|
|
33
|
+
{...box}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export { Container };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
import {
|
|
6
|
+
color,
|
|
7
|
+
resolveSpace,
|
|
8
|
+
type ColorToken,
|
|
9
|
+
type Space,
|
|
10
|
+
} from './layout-style';
|
|
11
|
+
|
|
12
|
+
export interface DividerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
13
|
+
orientation?: 'horizontal' | 'vertical';
|
|
14
|
+
/** Line color token. Defaults to `border`. */
|
|
15
|
+
tone?: ColorToken;
|
|
16
|
+
/** Margin on the cross axis (space around the line). */
|
|
17
|
+
spacing?: Space;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Divider — a hairline rule between content. Horizontal by default; set
|
|
22
|
+
* `orientation="vertical"` inside an HStack.
|
|
23
|
+
*/
|
|
24
|
+
const Divider = React.forwardRef<HTMLDivElement, DividerProps>(function Divider(
|
|
25
|
+
{
|
|
26
|
+
className,
|
|
27
|
+
style,
|
|
28
|
+
orientation = 'horizontal',
|
|
29
|
+
tone = 'border',
|
|
30
|
+
spacing,
|
|
31
|
+
...props
|
|
32
|
+
},
|
|
33
|
+
ref
|
|
34
|
+
) {
|
|
35
|
+
const vertical = orientation === 'vertical';
|
|
36
|
+
const gap = spacing != null ? resolveSpace(spacing) : undefined;
|
|
37
|
+
const s: CSSProperties = vertical
|
|
38
|
+
? {
|
|
39
|
+
width: '1px',
|
|
40
|
+
alignSelf: 'stretch',
|
|
41
|
+
backgroundColor: color(tone),
|
|
42
|
+
marginLeft: gap,
|
|
43
|
+
marginRight: gap,
|
|
44
|
+
}
|
|
45
|
+
: {
|
|
46
|
+
height: '1px',
|
|
47
|
+
width: '100%',
|
|
48
|
+
backgroundColor: color(tone),
|
|
49
|
+
marginTop: gap,
|
|
50
|
+
marginBottom: gap,
|
|
51
|
+
};
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
ref={ref}
|
|
55
|
+
role='separator'
|
|
56
|
+
aria-orientation={orientation}
|
|
57
|
+
className={cn(className)}
|
|
58
|
+
style={{ ...s, ...style }}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export { Divider };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '../story-types';
|
|
2
|
+
|
|
3
|
+
import { Flex, HStack, VStack } from './flex';
|
|
4
|
+
import { Box } from './box';
|
|
5
|
+
import { Spacer } from './spacer';
|
|
6
|
+
import { Center } from './center';
|
|
7
|
+
import { Divider } from './divider';
|
|
8
|
+
import { Button } from '../ui/button';
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: 'Layout/Flex',
|
|
12
|
+
component: Flex,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
// FlexProps = flex vocabulary (direction/align/justify/wrap/gap) + the shared
|
|
15
|
+
// BoxStyleProps. A useful subset is curated; `children` is the composed
|
|
16
|
+
// content slot (§3) and `className`/`style` stay unexposed (§6).
|
|
17
|
+
argTypes: {
|
|
18
|
+
direction: {
|
|
19
|
+
control: 'inline-radio',
|
|
20
|
+
options: ['row', 'column', 'row-reverse', 'column-reverse'],
|
|
21
|
+
},
|
|
22
|
+
align: {
|
|
23
|
+
control: 'select',
|
|
24
|
+
options: ['start', 'center', 'end', 'stretch', 'baseline'],
|
|
25
|
+
},
|
|
26
|
+
justify: {
|
|
27
|
+
control: 'select',
|
|
28
|
+
options: ['start', 'center', 'end', 'between', 'around', 'evenly'],
|
|
29
|
+
},
|
|
30
|
+
gap: { control: 'select', options: [0, 2, 4, 6, 8] },
|
|
31
|
+
wrap: { control: 'boolean' },
|
|
32
|
+
inline: { control: 'boolean' },
|
|
33
|
+
p: { control: 'select', options: [0, 2, 4, 6, 8] },
|
|
34
|
+
bg: {
|
|
35
|
+
control: 'select',
|
|
36
|
+
options: ['background', 'card', 'muted', 'accent'],
|
|
37
|
+
},
|
|
38
|
+
radius: {
|
|
39
|
+
control: 'select',
|
|
40
|
+
options: ['none', 'sm', 'md', 'lg', 'xl', '2xl', 'full'],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
args: { gap: 4, align: 'center', justify: 'start' },
|
|
44
|
+
parameters: { puck: { slots: ['children'] } },
|
|
45
|
+
} satisfies Meta<typeof Flex>;
|
|
46
|
+
|
|
47
|
+
export default meta;
|
|
48
|
+
type Story = StoryObj<typeof meta>;
|
|
49
|
+
|
|
50
|
+
const Item = ({ children }: { children: React.ReactNode }) => (
|
|
51
|
+
<Box bg='vinta-50' color='vinta-700' px={4} py={2} radius='md'>
|
|
52
|
+
{children}
|
|
53
|
+
</Box>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export const Playground: Story = {
|
|
57
|
+
render: (args) => (
|
|
58
|
+
<Flex {...args}>
|
|
59
|
+
<Item>One</Item>
|
|
60
|
+
<Item>Two</Item>
|
|
61
|
+
<Item>Three</Item>
|
|
62
|
+
</Flex>
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const SpaceBetween: Story = {
|
|
67
|
+
name: 'HStack + Spacer (toolbar)',
|
|
68
|
+
render: () => (
|
|
69
|
+
<HStack gap={3} p={3} bg='card' radius='lg' border>
|
|
70
|
+
<Item>Logo</Item>
|
|
71
|
+
<Spacer />
|
|
72
|
+
<Button variant='ghost' size='sm'>
|
|
73
|
+
Settings
|
|
74
|
+
</Button>
|
|
75
|
+
<Button size='sm'>New</Button>
|
|
76
|
+
</HStack>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const Stacks: Story = {
|
|
81
|
+
render: () => (
|
|
82
|
+
<Flex gap={8} align='start'>
|
|
83
|
+
<VStack gap={2}>
|
|
84
|
+
<Item>VStack 1</Item>
|
|
85
|
+
<Item>VStack 2</Item>
|
|
86
|
+
<Item>VStack 3</Item>
|
|
87
|
+
</VStack>
|
|
88
|
+
<Divider orientation='vertical' />
|
|
89
|
+
<HStack gap={2}>
|
|
90
|
+
<Item>HStack 1</Item>
|
|
91
|
+
<Item>HStack 2</Item>
|
|
92
|
+
</HStack>
|
|
93
|
+
</Flex>
|
|
94
|
+
),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const CenterAndDivider: Story = {
|
|
98
|
+
render: () => (
|
|
99
|
+
<VStack gap={4}>
|
|
100
|
+
<Center bg='muted' radius='lg' height={120}>
|
|
101
|
+
<Item>Centered</Item>
|
|
102
|
+
</Center>
|
|
103
|
+
<Divider />
|
|
104
|
+
<Center bg='muted' radius='lg' height={80}>
|
|
105
|
+
<span className='text-muted-foreground text-sm'>Below the divider</span>
|
|
106
|
+
</Center>
|
|
107
|
+
</VStack>
|
|
108
|
+
),
|
|
109
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flex / Grid gap regression tests.
|
|
3
|
+
*
|
|
4
|
+
* Guards the bug where the `gap` shorthand was emitted alongside `undefined`
|
|
5
|
+
* `rowGap`/`columnGap` longhands: React warns about mixing shorthand +
|
|
6
|
+
* longhand and DROPS the shorthand on rerender, silently zeroing the gap
|
|
7
|
+
* (which glued labels to switches/checkboxes/radios). The fix only assigns the
|
|
8
|
+
* gap keys that are actually set.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { render } from '@testing-library/react';
|
|
13
|
+
import { Flex, HStack } from './flex';
|
|
14
|
+
import { Grid } from './grid';
|
|
15
|
+
|
|
16
|
+
describe('Flex gap', () => {
|
|
17
|
+
it('emits the gap shorthand and NO rowGap/columnGap longhands when only gap is set', () => {
|
|
18
|
+
const { container } = render(
|
|
19
|
+
<HStack gap={2}>
|
|
20
|
+
<span>a</span>
|
|
21
|
+
<span>b</span>
|
|
22
|
+
</HStack>
|
|
23
|
+
);
|
|
24
|
+
const el = container.firstElementChild as HTMLElement;
|
|
25
|
+
expect(el.style.display).toBe('flex');
|
|
26
|
+
expect(el.style.gap).toBe('0.5rem');
|
|
27
|
+
// The longhands must be absent — their presence (even empty) is what made
|
|
28
|
+
// React drop the shorthand.
|
|
29
|
+
expect(el.style.rowGap).toBe('');
|
|
30
|
+
expect(el.style.columnGap).toBe('');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('omits gap entirely when not provided', () => {
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<Flex>
|
|
36
|
+
<span>a</span>
|
|
37
|
+
</Flex>
|
|
38
|
+
);
|
|
39
|
+
const el = container.firstElementChild as HTMLElement;
|
|
40
|
+
expect(el.style.gap).toBe('');
|
|
41
|
+
expect(el.style.rowGap).toBe('');
|
|
42
|
+
expect(el.style.columnGap).toBe('');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('uses longhands (no shorthand) when only rowGap/columnGap are set', () => {
|
|
46
|
+
const { container } = render(
|
|
47
|
+
<Flex rowGap={2} columnGap={4}>
|
|
48
|
+
<span>a</span>
|
|
49
|
+
</Flex>
|
|
50
|
+
);
|
|
51
|
+
const el = container.firstElementChild as HTMLElement;
|
|
52
|
+
expect(el.style.rowGap).toBe('0.5rem');
|
|
53
|
+
expect(el.style.columnGap).toBe('1rem');
|
|
54
|
+
expect(el.style.gap).toBe('');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Grid gap', () => {
|
|
59
|
+
it('emits the gap shorthand and NO longhands when only gap is set', () => {
|
|
60
|
+
const { container } = render(
|
|
61
|
+
<Grid gap={3}>
|
|
62
|
+
<span>a</span>
|
|
63
|
+
</Grid>
|
|
64
|
+
);
|
|
65
|
+
const el = container.firstElementChild as HTMLElement;
|
|
66
|
+
expect(el.style.display).toBe('grid');
|
|
67
|
+
expect(el.style.gap).toBe('0.75rem');
|
|
68
|
+
expect(el.style.rowGap).toBe('');
|
|
69
|
+
expect(el.style.columnGap).toBe('');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
import {
|
|
6
|
+
boxStyle,
|
|
7
|
+
splitBoxProps,
|
|
8
|
+
resolveSpace,
|
|
9
|
+
type BoxStyleProps,
|
|
10
|
+
type Space,
|
|
11
|
+
} from './layout-style';
|
|
12
|
+
|
|
13
|
+
type Align = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
|
14
|
+
type Justify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
|
15
|
+
|
|
16
|
+
const ALIGN: Record<Align, CSSProperties['alignItems']> = {
|
|
17
|
+
start: 'flex-start',
|
|
18
|
+
center: 'center',
|
|
19
|
+
end: 'flex-end',
|
|
20
|
+
stretch: 'stretch',
|
|
21
|
+
baseline: 'baseline',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const JUSTIFY: Record<Justify, CSSProperties['justifyContent']> = {
|
|
25
|
+
start: 'flex-start',
|
|
26
|
+
center: 'center',
|
|
27
|
+
end: 'flex-end',
|
|
28
|
+
between: 'space-between',
|
|
29
|
+
around: 'space-around',
|
|
30
|
+
evenly: 'space-evenly',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface FlexProps
|
|
34
|
+
extends BoxStyleProps, Omit<React.HTMLAttributes<HTMLElement>, 'color'> {
|
|
35
|
+
as?: React.ElementType;
|
|
36
|
+
direction?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
|
|
37
|
+
align?: Align;
|
|
38
|
+
justify?: Justify;
|
|
39
|
+
wrap?: boolean | 'reverse';
|
|
40
|
+
gap?: Space;
|
|
41
|
+
rowGap?: Space;
|
|
42
|
+
columnGap?: Space;
|
|
43
|
+
inline?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Flex — flexbox container driven by props. Composes all Box style props plus
|
|
48
|
+
* direction / align / justify / wrap / gap.
|
|
49
|
+
*
|
|
50
|
+
* <Flex justify="between" align="center" gap={4}>…</Flex>
|
|
51
|
+
*/
|
|
52
|
+
const Flex = React.forwardRef<HTMLElement, FlexProps>(function Flex(
|
|
53
|
+
{
|
|
54
|
+
as: Comp = 'div',
|
|
55
|
+
className,
|
|
56
|
+
style,
|
|
57
|
+
direction = 'row',
|
|
58
|
+
align,
|
|
59
|
+
justify,
|
|
60
|
+
wrap,
|
|
61
|
+
gap,
|
|
62
|
+
rowGap,
|
|
63
|
+
columnGap,
|
|
64
|
+
inline,
|
|
65
|
+
...boxProps
|
|
66
|
+
},
|
|
67
|
+
ref
|
|
68
|
+
) {
|
|
69
|
+
const { style: resolved, rest } = splitBoxProps(boxProps);
|
|
70
|
+
const flexStyle: CSSProperties = {
|
|
71
|
+
display: inline ? 'inline-flex' : 'flex',
|
|
72
|
+
flexDirection: direction,
|
|
73
|
+
alignItems: align ? ALIGN[align] : undefined,
|
|
74
|
+
justifyContent: justify ? JUSTIFY[justify] : undefined,
|
|
75
|
+
flexWrap:
|
|
76
|
+
wrap === true ? 'wrap' : wrap === 'reverse' ? 'wrap-reverse' : undefined,
|
|
77
|
+
};
|
|
78
|
+
// Only assign the gap keys that are actually set. Emitting the `gap`
|
|
79
|
+
// shorthand alongside `undefined` `rowGap`/`columnGap` longhands makes React
|
|
80
|
+
// warn about mixing shorthand + longhand and DROP the shorthand on rerender —
|
|
81
|
+
// which silently zeroed the gap (glued labels next to switches/checkboxes).
|
|
82
|
+
if (gap != null) flexStyle.gap = resolveSpace(gap);
|
|
83
|
+
if (rowGap != null) flexStyle.rowGap = resolveSpace(rowGap);
|
|
84
|
+
if (columnGap != null) flexStyle.columnGap = resolveSpace(columnGap);
|
|
85
|
+
return (
|
|
86
|
+
<Comp
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={cn(className)}
|
|
89
|
+
style={{ ...resolved, ...flexStyle, ...style }}
|
|
90
|
+
{...rest}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export type StackProps = Omit<FlexProps, 'direction'>;
|
|
96
|
+
|
|
97
|
+
/** Vertical flex stack. */
|
|
98
|
+
const VStack = React.forwardRef<HTMLElement, StackProps>(
|
|
99
|
+
function VStack(props, ref) {
|
|
100
|
+
return <Flex ref={ref} direction='column' {...props} />;
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
/** Horizontal flex stack (centers items vertically by default). */
|
|
105
|
+
const HStack = React.forwardRef<HTMLElement, StackProps>(function HStack(
|
|
106
|
+
{ align = 'center', ...props },
|
|
107
|
+
ref
|
|
108
|
+
) {
|
|
109
|
+
return <Flex ref={ref} direction='row' align={align} {...props} />;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export { Flex, HStack, VStack, boxStyle };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '../story-types';
|
|
2
|
+
|
|
3
|
+
import { Grid, GridItem } from './grid';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Layout/Grid',
|
|
7
|
+
component: Grid,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
// GridProps = grid vocabulary (columns/rows/gap/align/justify/inline) + the
|
|
10
|
+
// shared BoxStyleProps. `children` is the composed content slot (§3).
|
|
11
|
+
argTypes: {
|
|
12
|
+
columns: { control: { type: 'number', min: 1, max: 12 } },
|
|
13
|
+
gap: { control: 'select', options: [0, 1, 2, 3, 4, 5, 6, 8] },
|
|
14
|
+
rowGap: { control: 'select', options: [0, 1, 2, 3, 4, 5, 6, 8] },
|
|
15
|
+
columnGap: { control: 'select', options: [0, 1, 2, 3, 4, 5, 6, 8] },
|
|
16
|
+
align: {
|
|
17
|
+
control: 'select',
|
|
18
|
+
options: ['start', 'center', 'end', 'stretch'],
|
|
19
|
+
},
|
|
20
|
+
justify: {
|
|
21
|
+
control: 'select',
|
|
22
|
+
options: ['start', 'center', 'end', 'stretch'],
|
|
23
|
+
},
|
|
24
|
+
p: { control: 'select', options: [0, 2, 4, 6, 8] },
|
|
25
|
+
},
|
|
26
|
+
args: { columns: 12, gap: 6 },
|
|
27
|
+
parameters: { puck: { slots: ['children'] } },
|
|
28
|
+
} satisfies Meta<typeof Grid>;
|
|
29
|
+
|
|
30
|
+
export default meta;
|
|
31
|
+
type Story = StoryObj<typeof meta>;
|
|
32
|
+
|
|
33
|
+
const Cell = ({ children }: { children: React.ReactNode }) => (
|
|
34
|
+
<div className='bg-vinta-50 text-vinta-700 rounded-md px-3 py-4 text-center font-mono text-xs'>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export const TwelveColumns: Story = {
|
|
40
|
+
args: { columns: 12, gap: 6 },
|
|
41
|
+
render: (args) => (
|
|
42
|
+
<Grid {...args}>
|
|
43
|
+
{Array.from({ length: 12 }).map((_, i) => (
|
|
44
|
+
<GridItem key={i} span={1}>
|
|
45
|
+
<Cell>{i + 1}</Cell>
|
|
46
|
+
</GridItem>
|
|
47
|
+
))}
|
|
48
|
+
</Grid>
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Spans: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<Grid gap={4}>
|
|
55
|
+
<GridItem span={6}>
|
|
56
|
+
<Cell>span 6</Cell>
|
|
57
|
+
</GridItem>
|
|
58
|
+
<GridItem span={6}>
|
|
59
|
+
<Cell>span 6</Cell>
|
|
60
|
+
</GridItem>
|
|
61
|
+
<GridItem span={4}>
|
|
62
|
+
<Cell>span 4</Cell>
|
|
63
|
+
</GridItem>
|
|
64
|
+
<GridItem span={4}>
|
|
65
|
+
<Cell>span 4</Cell>
|
|
66
|
+
</GridItem>
|
|
67
|
+
<GridItem span={4}>
|
|
68
|
+
<Cell>span 4</Cell>
|
|
69
|
+
</GridItem>
|
|
70
|
+
<GridItem span={8}>
|
|
71
|
+
<Cell>span 8</Cell>
|
|
72
|
+
</GridItem>
|
|
73
|
+
<GridItem span={4}>
|
|
74
|
+
<Cell>span 4</Cell>
|
|
75
|
+
</GridItem>
|
|
76
|
+
</Grid>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const EqualColumns: Story = {
|
|
81
|
+
name: 'Equal columns (columns=3)',
|
|
82
|
+
render: () => (
|
|
83
|
+
<Grid columns={3} gap={4}>
|
|
84
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
85
|
+
<GridItem key={i}>
|
|
86
|
+
<Cell>{i + 1}</Cell>
|
|
87
|
+
</GridItem>
|
|
88
|
+
))}
|
|
89
|
+
</Grid>
|
|
90
|
+
),
|
|
91
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
import {
|
|
6
|
+
splitBoxProps,
|
|
7
|
+
resolveSpace,
|
|
8
|
+
type BoxStyleProps,
|
|
9
|
+
type Space,
|
|
10
|
+
} from './layout-style';
|
|
11
|
+
|
|
12
|
+
type Align = 'start' | 'center' | 'end' | 'stretch';
|
|
13
|
+
type Justify = 'start' | 'center' | 'end' | 'stretch';
|
|
14
|
+
|
|
15
|
+
const ALIGN: Record<Align, CSSProperties['alignItems']> = {
|
|
16
|
+
start: 'start',
|
|
17
|
+
center: 'center',
|
|
18
|
+
end: 'end',
|
|
19
|
+
stretch: 'stretch',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface GridProps
|
|
23
|
+
extends BoxStyleProps, Omit<React.HTMLAttributes<HTMLElement>, 'color'> {
|
|
24
|
+
as?: React.ElementType;
|
|
25
|
+
/** Number of equal columns, or a raw grid-template-columns string. */
|
|
26
|
+
columns?: number | string;
|
|
27
|
+
rows?: number | string;
|
|
28
|
+
gap?: Space;
|
|
29
|
+
rowGap?: Space;
|
|
30
|
+
columnGap?: Space;
|
|
31
|
+
align?: Align;
|
|
32
|
+
justify?: Justify;
|
|
33
|
+
inline?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function template(value: number | string | undefined): string | undefined {
|
|
37
|
+
if (value == null) return undefined;
|
|
38
|
+
if (typeof value === 'number') return `repeat(${value}, minmax(0, 1fr))`;
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Grid — CSS grid container. Defaults to the design system's 12-column grid;
|
|
44
|
+
* pass `columns` for any count (or a template string). Compose with <GridItem>.
|
|
45
|
+
*
|
|
46
|
+
* <Grid columns={3} gap={4}>…</Grid>
|
|
47
|
+
*/
|
|
48
|
+
const Grid = React.forwardRef<HTMLElement, GridProps>(function Grid(
|
|
49
|
+
{
|
|
50
|
+
as: Comp = 'div',
|
|
51
|
+
className,
|
|
52
|
+
style,
|
|
53
|
+
columns = 12,
|
|
54
|
+
rows,
|
|
55
|
+
gap = 6,
|
|
56
|
+
rowGap,
|
|
57
|
+
columnGap,
|
|
58
|
+
align,
|
|
59
|
+
justify,
|
|
60
|
+
inline,
|
|
61
|
+
...boxProps
|
|
62
|
+
},
|
|
63
|
+
ref
|
|
64
|
+
) {
|
|
65
|
+
const { style: resolved, rest } = splitBoxProps(boxProps);
|
|
66
|
+
const gridStyle: CSSProperties = {
|
|
67
|
+
display: inline ? 'inline-grid' : 'grid',
|
|
68
|
+
gridTemplateColumns: template(columns),
|
|
69
|
+
gridTemplateRows: template(rows),
|
|
70
|
+
alignItems: align ? ALIGN[align] : undefined,
|
|
71
|
+
justifyItems: justify ? ALIGN[justify] : undefined,
|
|
72
|
+
};
|
|
73
|
+
// Never emit the `gap` shorthand together with `undefined` `rowGap`/`columnGap`
|
|
74
|
+
// longhands — React drops the shorthand on rerender, zeroing the gap.
|
|
75
|
+
if (gap != null) gridStyle.gap = resolveSpace(gap);
|
|
76
|
+
if (rowGap != null) gridStyle.rowGap = resolveSpace(rowGap);
|
|
77
|
+
if (columnGap != null) gridStyle.columnGap = resolveSpace(columnGap);
|
|
78
|
+
return (
|
|
79
|
+
<Comp
|
|
80
|
+
ref={ref}
|
|
81
|
+
className={cn(className)}
|
|
82
|
+
style={{ ...resolved, ...gridStyle, ...style }}
|
|
83
|
+
{...rest}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export interface GridItemProps
|
|
89
|
+
extends BoxStyleProps, Omit<React.HTMLAttributes<HTMLElement>, 'color'> {
|
|
90
|
+
as?: React.ElementType;
|
|
91
|
+
/** Columns to span. */
|
|
92
|
+
span?: number;
|
|
93
|
+
/** Rows to span. */
|
|
94
|
+
rowSpan?: number;
|
|
95
|
+
colStart?: number;
|
|
96
|
+
colEnd?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const GridItem = React.forwardRef<HTMLElement, GridItemProps>(function GridItem(
|
|
100
|
+
{
|
|
101
|
+
as: Comp = 'div',
|
|
102
|
+
className,
|
|
103
|
+
style,
|
|
104
|
+
span,
|
|
105
|
+
rowSpan,
|
|
106
|
+
colStart,
|
|
107
|
+
colEnd,
|
|
108
|
+
...boxProps
|
|
109
|
+
},
|
|
110
|
+
ref
|
|
111
|
+
) {
|
|
112
|
+
const { style: resolved, rest } = splitBoxProps(boxProps);
|
|
113
|
+
const itemStyle: CSSProperties = {
|
|
114
|
+
gridColumn:
|
|
115
|
+
colStart != null || colEnd != null
|
|
116
|
+
? `${colStart ?? 'auto'} / ${colEnd ?? (span != null ? `span ${span}` : 'auto')}`
|
|
117
|
+
: span != null
|
|
118
|
+
? `span ${span} / span ${span}`
|
|
119
|
+
: undefined,
|
|
120
|
+
gridRow: rowSpan != null ? `span ${rowSpan} / span ${rowSpan}` : undefined,
|
|
121
|
+
};
|
|
122
|
+
return (
|
|
123
|
+
<Comp
|
|
124
|
+
ref={ref}
|
|
125
|
+
className={cn(className)}
|
|
126
|
+
style={{ ...resolved, ...itemStyle, ...style }}
|
|
127
|
+
{...rest}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export { Grid, GridItem };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Text, type TextProps, type TextSize, type TextWeight } from './text';
|
|
4
|
+
|
|
5
|
+
type Level = 1 | 2 | 3 | 4 | 5 | 6;
|
|
6
|
+
|
|
7
|
+
const DEFAULT_SIZE: Record<Level, TextSize> = {
|
|
8
|
+
1: '5xl',
|
|
9
|
+
2: '4xl',
|
|
10
|
+
3: '2xl',
|
|
11
|
+
4: 'xl',
|
|
12
|
+
5: 'lg',
|
|
13
|
+
6: 'base',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_WEIGHT: Record<Level, TextWeight> = {
|
|
17
|
+
1: 'bold',
|
|
18
|
+
2: 'semibold',
|
|
19
|
+
3: 'semibold',
|
|
20
|
+
4: 'semibold',
|
|
21
|
+
5: 'semibold',
|
|
22
|
+
6: 'semibold',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface HeadingProps extends TextProps {
|
|
26
|
+
/** Heading level 1–6. Sets the tag and default size/weight. */
|
|
27
|
+
level?: Level;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Heading — semantic heading with design-system defaults per level. Override
|
|
32
|
+
* any token via props.
|
|
33
|
+
*
|
|
34
|
+
* <Heading level={2}>Section title</Heading>
|
|
35
|
+
*/
|
|
36
|
+
const Heading = React.forwardRef<HTMLElement, HeadingProps>(function Heading(
|
|
37
|
+
{ level = 2, size, weight, tracking = 'tight', leading = 'tight', ...props },
|
|
38
|
+
ref
|
|
39
|
+
) {
|
|
40
|
+
return (
|
|
41
|
+
<Text
|
|
42
|
+
ref={ref}
|
|
43
|
+
as={`h${level}`}
|
|
44
|
+
size={size ?? DEFAULT_SIZE[level]}
|
|
45
|
+
weight={weight ?? DEFAULT_WEIGHT[level]}
|
|
46
|
+
tracking={tracking}
|
|
47
|
+
leading={leading}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export { Heading };
|