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.
Files changed (107) hide show
  1. package/components.json +21 -0
  2. package/dist/styles.css +3452 -0
  3. package/package.json +103 -0
  4. package/src/layout/app-shell.tsx +115 -0
  5. package/src/layout/app-topbar.stories.tsx +74 -0
  6. package/src/layout/app-topbar.tsx +145 -0
  7. package/src/layout/auth-layout.tsx +45 -0
  8. package/src/layout/box.stories.tsx +86 -0
  9. package/src/layout/box.tsx +34 -0
  10. package/src/layout/center.tsx +24 -0
  11. package/src/layout/container.stories.tsx +44 -0
  12. package/src/layout/container.tsx +39 -0
  13. package/src/layout/divider.tsx +64 -0
  14. package/src/layout/flex.stories.tsx +109 -0
  15. package/src/layout/flex.test.tsx +71 -0
  16. package/src/layout/flex.tsx +112 -0
  17. package/src/layout/grid.stories.tsx +91 -0
  18. package/src/layout/grid.tsx +132 -0
  19. package/src/layout/heading.tsx +53 -0
  20. package/src/layout/index.ts +47 -0
  21. package/src/layout/layout-style.ts +291 -0
  22. package/src/layout/link.stories.tsx +54 -0
  23. package/src/layout/link.test.tsx +75 -0
  24. package/src/layout/link.tsx +100 -0
  25. package/src/layout/navbar.stories.tsx +70 -0
  26. package/src/layout/navbar.tsx +128 -0
  27. package/src/layout/page-header.stories.tsx +80 -0
  28. package/src/layout/page-header.tsx +59 -0
  29. package/src/layout/section.tsx +22 -0
  30. package/src/layout/spacer.tsx +21 -0
  31. package/src/layout/stack.stories.tsx +58 -0
  32. package/src/layout/stack.tsx +19 -0
  33. package/src/layout/text.tsx +139 -0
  34. package/src/layout/typography.stories.tsx +112 -0
  35. package/src/lib/utils.ts +6 -0
  36. package/src/story-types.ts +80 -0
  37. package/src/styles/build.css +33 -0
  38. package/src/styles/globals.css +9 -0
  39. package/src/styles/tokens.css +281 -0
  40. package/src/ui/accordion.stories.tsx +84 -0
  41. package/src/ui/accordion.tsx +57 -0
  42. package/src/ui/alert-dialog.stories.tsx +57 -0
  43. package/src/ui/alert-dialog.tsx +141 -0
  44. package/src/ui/alert.stories.tsx +46 -0
  45. package/src/ui/alert.tsx +59 -0
  46. package/src/ui/avatar.stories.tsx +56 -0
  47. package/src/ui/avatar.tsx +50 -0
  48. package/src/ui/badge.stories.tsx +105 -0
  49. package/src/ui/badge.tsx +57 -0
  50. package/src/ui/breadcrumb.stories.tsx +69 -0
  51. package/src/ui/breadcrumb.tsx +140 -0
  52. package/src/ui/button.stories.tsx +85 -0
  53. package/src/ui/button.tsx +60 -0
  54. package/src/ui/card.stories.tsx +62 -0
  55. package/src/ui/card.tsx +96 -0
  56. package/src/ui/carousel.stories.tsx +55 -0
  57. package/src/ui/carousel.tsx +262 -0
  58. package/src/ui/checkbox.stories.tsx +52 -0
  59. package/src/ui/checkbox.tsx +30 -0
  60. package/src/ui/combobox.stories.tsx +138 -0
  61. package/src/ui/combobox.tsx +217 -0
  62. package/src/ui/command.tsx +184 -0
  63. package/src/ui/dialog.stories.tsx +66 -0
  64. package/src/ui/dialog.tsx +163 -0
  65. package/src/ui/drawer.stories.tsx +80 -0
  66. package/src/ui/drawer.tsx +118 -0
  67. package/src/ui/dropdown-menu.stories.tsx +57 -0
  68. package/src/ui/dropdown-menu.tsx +201 -0
  69. package/src/ui/form.stories.tsx +70 -0
  70. package/src/ui/form.tsx +181 -0
  71. package/src/ui/input-otp.stories.tsx +52 -0
  72. package/src/ui/input-otp.tsx +71 -0
  73. package/src/ui/input.stories.tsx +68 -0
  74. package/src/ui/input.tsx +22 -0
  75. package/src/ui/label.stories.tsx +35 -0
  76. package/src/ui/label.tsx +26 -0
  77. package/src/ui/navigation-menu.stories.tsx +79 -0
  78. package/src/ui/navigation-menu.tsx +128 -0
  79. package/src/ui/pagination.stories.tsx +62 -0
  80. package/src/ui/pagination.tsx +137 -0
  81. package/src/ui/popover.tsx +89 -0
  82. package/src/ui/progress.stories.tsx +33 -0
  83. package/src/ui/progress.tsx +28 -0
  84. package/src/ui/radio-group.stories.tsx +52 -0
  85. package/src/ui/radio-group.tsx +44 -0
  86. package/src/ui/scheduling-chip.stories.tsx +117 -0
  87. package/src/ui/scheduling-chip.tsx +62 -0
  88. package/src/ui/scroll-area.stories.tsx +52 -0
  89. package/src/ui/scroll-area.tsx +48 -0
  90. package/src/ui/select.stories.tsx +56 -0
  91. package/src/ui/select.tsx +159 -0
  92. package/src/ui/sheet.stories.tsx +63 -0
  93. package/src/ui/sheet.tsx +141 -0
  94. package/src/ui/skeleton.stories.tsx +40 -0
  95. package/src/ui/skeleton.tsx +15 -0
  96. package/src/ui/sonner.stories.tsx +62 -0
  97. package/src/ui/sonner.tsx +34 -0
  98. package/src/ui/switch.stories.tsx +41 -0
  99. package/src/ui/switch.tsx +29 -0
  100. package/src/ui/table.stories.tsx +88 -0
  101. package/src/ui/table.tsx +132 -0
  102. package/src/ui/tabs.stories.tsx +63 -0
  103. package/src/ui/tabs.tsx +55 -0
  104. package/src/ui/textarea.stories.tsx +45 -0
  105. package/src/ui/textarea.tsx +22 -0
  106. package/src/ui/tooltip.stories.tsx +48 -0
  107. package/src/ui/tooltip.tsx +32 -0
@@ -0,0 +1,128 @@
1
+ import * as React from 'react';
2
+ import { Menu } from 'lucide-react';
3
+
4
+ import { cn } from '../lib/utils';
5
+ import { Button } from '../ui/button';
6
+ import { Sheet, SheetContent, SheetTitle, SheetTrigger } from '../ui/sheet';
7
+ import { Box } from './box';
8
+ import { HStack, VStack } from './flex';
9
+ import { Container } from './container';
10
+ import { Text } from './text';
11
+
12
+ /** Brand wordmark used across the public navbar and auth pages. */
13
+ function BrandMark({ className }: { className?: string }) {
14
+ return (
15
+ <HStack className={cn('gap-2.5', className)}>
16
+ {/* eslint-disable-next-line @next/next/no-img-element */}
17
+ <img
18
+ src='/vinta-wordmark.svg'
19
+ alt='Vinta'
20
+ className='h-5 w-auto dark:brightness-0 dark:invert'
21
+ />
22
+ <Text
23
+ size='sm'
24
+ weight='medium'
25
+ color='muted-foreground'
26
+ className='border-border border-l pl-2.5'
27
+ >
28
+ Schedule
29
+ </Text>
30
+ </HStack>
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Navbar — public / marketing top navigation bar. Sticky, blurred surface with
36
+ * a brand on the left, nav links in the middle, and actions on the right.
37
+ *
38
+ * Responsive: the inner row is an `@container/nav`. Links dock inline above
39
+ * @3xl (768px) and actions above @md (448px); below those breakpoints they
40
+ * collapse into a slide-over menu opened by a hamburger.
41
+ */
42
+ export interface NavbarProps extends React.HTMLAttributes<HTMLElement> {
43
+ brand?: React.ReactNode;
44
+ /** Center nav links. */
45
+ links?: React.ReactNode;
46
+ /** Right-aligned actions (theme toggle, auth buttons…). */
47
+ actions?: React.ReactNode;
48
+ /** Cap the inner row to the contained width (default) or let it run full. */
49
+ width?: 'contained' | 'full';
50
+ }
51
+
52
+ const Navbar = React.forwardRef<HTMLElement, NavbarProps>(function Navbar(
53
+ { className, brand, links, actions, width = 'contained', ...props },
54
+ ref
55
+ ) {
56
+ const hasMenu = Boolean(links || actions);
57
+ return (
58
+ <Box
59
+ as='header'
60
+ ref={ref as React.Ref<HTMLElement>}
61
+ className={cn(
62
+ 'border-border bg-background/80 sticky top-0 z-40 w-full border-b backdrop-blur',
63
+ className
64
+ )}
65
+ {...props}
66
+ >
67
+ <Container width={width}>
68
+ <HStack
69
+ justify='between'
70
+ gap={6}
71
+ height={72}
72
+ py={3}
73
+ className='@container/nav'
74
+ >
75
+ <HStack gap={10}>
76
+ {brand ?? <BrandMark />}
77
+ {links ? (
78
+ <Box as='nav' className='hidden items-center gap-6 @3xl/nav:flex'>
79
+ {links}
80
+ </Box>
81
+ ) : null}
82
+ </HStack>
83
+
84
+ <div className='flex items-center gap-3'>
85
+ {actions ? (
86
+ <div className='hidden items-center gap-3 @md/nav:flex'>
87
+ {actions}
88
+ </div>
89
+ ) : null}
90
+
91
+ {hasMenu ? (
92
+ <Sheet>
93
+ <SheetTrigger asChild>
94
+ <Button
95
+ variant='ghost'
96
+ size='icon'
97
+ aria-label='Open menu'
98
+ className='@3xl/nav:hidden'
99
+ >
100
+ <Menu />
101
+ </Button>
102
+ </SheetTrigger>
103
+ <SheetContent side='right' className='w-72'>
104
+ <SheetTitle className='sr-only'>Menu</SheetTitle>
105
+ {links ? (
106
+ <VStack as='nav' gap={1} className='mt-8'>
107
+ {links}
108
+ </VStack>
109
+ ) : null}
110
+ {actions ? (
111
+ <VStack
112
+ gap={2}
113
+ className='border-border mt-6 border-t pt-6'
114
+ >
115
+ {actions}
116
+ </VStack>
117
+ ) : null}
118
+ </SheetContent>
119
+ </Sheet>
120
+ ) : null}
121
+ </div>
122
+ </HStack>
123
+ </Container>
124
+ </Box>
125
+ );
126
+ });
127
+
128
+ export { Navbar, BrandMark };
@@ -0,0 +1,80 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+ import { CalendarPlus, Download } from 'lucide-react';
3
+
4
+ import { PageHeader } from './page-header';
5
+ import { Button } from '../ui/button';
6
+ import {
7
+ Breadcrumb,
8
+ BreadcrumbItem,
9
+ BreadcrumbLink,
10
+ BreadcrumbList,
11
+ BreadcrumbPage,
12
+ BreadcrumbSeparator,
13
+ } from '../ui/breadcrumb';
14
+
15
+ const meta = {
16
+ title: 'Composition/PageHeader',
17
+ component: PageHeader,
18
+ tags: ['autodocs'],
19
+ // `title` / `description` are ReactNode but render as plain copy → editable
20
+ // text controls. `actions` and `breadcrumb` are composed content → slots (§3).
21
+ argTypes: {
22
+ title: { control: 'text', description: 'Page title' },
23
+ description: { control: 'text', description: 'Supporting copy' },
24
+ },
25
+ args: {
26
+ title: 'Bookings',
27
+ description: 'Manage upcoming and past appointments.',
28
+ },
29
+ parameters: { puck: { slots: ['actions', 'breadcrumb'] } },
30
+ } satisfies Meta<typeof PageHeader>;
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof meta>;
34
+
35
+ export const Default: Story = {
36
+ render: (args) => <PageHeader className='w-[640px]' {...args} />,
37
+ };
38
+
39
+ export const WithActions: Story = {
40
+ render: (args) => (
41
+ <PageHeader
42
+ className='w-[640px]'
43
+ {...args}
44
+ actions={
45
+ <>
46
+ <Button variant='outline' size='sm'>
47
+ <Download />
48
+ Export
49
+ </Button>
50
+ <Button size='sm'>
51
+ <CalendarPlus />
52
+ New booking
53
+ </Button>
54
+ </>
55
+ }
56
+ />
57
+ ),
58
+ };
59
+
60
+ export const WithBreadcrumb: Story = {
61
+ render: (args) => (
62
+ <PageHeader
63
+ className='w-[640px]'
64
+ {...args}
65
+ breadcrumb={
66
+ <Breadcrumb>
67
+ <BreadcrumbList>
68
+ <BreadcrumbItem>
69
+ <BreadcrumbLink href='#'>Home</BreadcrumbLink>
70
+ </BreadcrumbItem>
71
+ <BreadcrumbSeparator />
72
+ <BreadcrumbItem>
73
+ <BreadcrumbPage>Bookings</BreadcrumbPage>
74
+ </BreadcrumbItem>
75
+ </BreadcrumbList>
76
+ </Breadcrumb>
77
+ }
78
+ />
79
+ ),
80
+ };
@@ -0,0 +1,59 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '../lib/utils';
4
+ import { HStack } from './flex';
5
+ import { Stack } from './stack';
6
+ import { Heading } from './heading';
7
+ import { Text } from './text';
8
+
9
+ /**
10
+ * PageHeader — title block at the top of a page body. Optional breadcrumb slot
11
+ * above, description below, and right-aligned actions. Built from layout
12
+ * primitives (Stack / Heading / Text).
13
+ */
14
+ export interface PageHeaderProps extends Omit<
15
+ React.HTMLAttributes<HTMLDivElement>,
16
+ 'title'
17
+ > {
18
+ title: React.ReactNode;
19
+ description?: React.ReactNode;
20
+ actions?: React.ReactNode;
21
+ breadcrumb?: React.ReactNode;
22
+ }
23
+
24
+ const PageHeader = React.forwardRef<HTMLDivElement, PageHeaderProps>(
25
+ function PageHeader(
26
+ { className, title, description, actions, breadcrumb, ...props },
27
+ ref
28
+ ) {
29
+ return (
30
+ <Stack
31
+ ref={ref as React.Ref<HTMLElement>}
32
+ gap={4}
33
+ className={cn('@container/pageheader', className)}
34
+ {...props}
35
+ >
36
+ {breadcrumb}
37
+ <div className='flex flex-col gap-3 @lg/pageheader:flex-row @lg/pageheader:items-start @lg/pageheader:justify-between'>
38
+ <Stack gap={1}>
39
+ <Heading level={1} size='2xl'>
40
+ {title}
41
+ </Heading>
42
+ {description ? (
43
+ <Text size='sm' color='muted-foreground'>
44
+ {description}
45
+ </Text>
46
+ ) : null}
47
+ </Stack>
48
+ {actions ? (
49
+ <HStack gap={2} shrink={0}>
50
+ {actions}
51
+ </HStack>
52
+ ) : null}
53
+ </div>
54
+ </Stack>
55
+ );
56
+ }
57
+ );
58
+
59
+ export { PageHeader };
@@ -0,0 +1,22 @@
1
+ import * as React from 'react';
2
+
3
+ import { Box, type BoxProps } from './box';
4
+ import type { Space } from './layout-style';
5
+
6
+ export interface SectionProps extends Omit<BoxProps, 'py'> {
7
+ /** Vertical rhythm between page sections. Defaults to 16 (64px). */
8
+ py?: Space;
9
+ }
10
+
11
+ /**
12
+ * Section — a vertical page band with consistent top/bottom padding. Renders a
13
+ * <section> by default; override with `as`.
14
+ */
15
+ const Section = React.forwardRef<HTMLElement, SectionProps>(function Section(
16
+ { as = 'section', py = 16, ...props },
17
+ ref
18
+ ) {
19
+ return <Box ref={ref} as={as} py={py} {...props} />;
20
+ });
21
+
22
+ export { Section };
@@ -0,0 +1,21 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * Spacer — flexible gap that pushes siblings apart inside a Flex/HStack/VStack.
5
+ * <HStack><Logo /><Spacer /><Actions /></HStack>
6
+ */
7
+ const Spacer = React.forwardRef<
8
+ HTMLDivElement,
9
+ React.HTMLAttributes<HTMLDivElement>
10
+ >(function Spacer({ style, ...props }, ref) {
11
+ return (
12
+ <div
13
+ ref={ref}
14
+ aria-hidden
15
+ style={{ flex: '1 1 0%', alignSelf: 'stretch', ...style }}
16
+ {...props}
17
+ />
18
+ );
19
+ });
20
+
21
+ export { Spacer };
@@ -0,0 +1,58 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+
3
+ import { Stack } from './stack';
4
+ import { Button } from '../ui/button';
5
+
6
+ const meta = {
7
+ title: 'Layout/Stack',
8
+ component: Stack,
9
+ tags: ['autodocs'],
10
+ // Container with BOTH curated scalar controls AND a slot. The typed
11
+ // style-prop vocabulary (layout-style.ts) maps cleanly to select/radio
12
+ // controls; `children` is the composed content slot (§3), typed ReactNode via
13
+ // FlexProps → React.HTMLAttributes. `className`/`style` stay unexposed (§6).
14
+ argTypes: {
15
+ direction: { control: 'inline-radio', options: ['column', 'row'] },
16
+ gap: { control: 'select', options: [0, 1, 2, 3, 4, 5, 6, 8] },
17
+ align: {
18
+ control: 'select',
19
+ options: ['start', 'center', 'end', 'stretch', 'baseline'],
20
+ },
21
+ justify: {
22
+ control: 'select',
23
+ options: ['start', 'center', 'end', 'between', 'around', 'evenly'],
24
+ },
25
+ wrap: { control: 'boolean' },
26
+ },
27
+ args: { direction: 'column', gap: 4 },
28
+ parameters: { puck: { slots: ['children'] } },
29
+ } satisfies Meta<typeof Stack>;
30
+
31
+ export default meta;
32
+ type Story = StoryObj<typeof meta>;
33
+
34
+ const Box = ({ children }: { children: React.ReactNode }) => (
35
+ <div className='bg-vinta-50 text-vinta-700 rounded-md px-4 py-2 text-sm'>
36
+ {children}
37
+ </div>
38
+ );
39
+
40
+ export const Vertical: Story = {
41
+ render: (args) => (
42
+ <Stack {...args}>
43
+ <Box>One</Box>
44
+ <Box>Two</Box>
45
+ <Box>Three</Box>
46
+ </Stack>
47
+ ),
48
+ };
49
+
50
+ export const Horizontal: Story = {
51
+ args: { direction: 'row', gap: 2, align: 'center' },
52
+ render: (args) => (
53
+ <Stack {...args}>
54
+ <Button>Save</Button>
55
+ <Button variant='outline'>Cancel</Button>
56
+ </Stack>
57
+ ),
58
+ };
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+
3
+ import { Flex, type FlexProps } from './flex';
4
+
5
+ export type { FlexProps as StackOwnProps };
6
+
7
+ /**
8
+ * Stack — vertical-by-default flex layout. Thin alias over <Flex>; gaps use the
9
+ * 4px spacing scale (`gap={4}` → 16px). Switch to a row with `direction="row"`,
10
+ * or use the dedicated <HStack> / <VStack>.
11
+ */
12
+ const Stack = React.forwardRef<HTMLElement, FlexProps>(function Stack(
13
+ { direction = 'column', gap = 4, ...props },
14
+ ref
15
+ ) {
16
+ return <Flex ref={ref} direction={direction} gap={gap} {...props} />;
17
+ });
18
+
19
+ export { Stack };
@@ -0,0 +1,139 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '../lib/utils';
4
+ import { color, type ColorToken } from './layout-style';
5
+
6
+ export type TextSize =
7
+ | 'xs'
8
+ | 'sm'
9
+ | 'base'
10
+ | 'lg'
11
+ | 'xl'
12
+ | '2xl'
13
+ | '3xl'
14
+ | '4xl'
15
+ | '5xl'
16
+ | '6xl';
17
+ export type TextWeight =
18
+ | 'light'
19
+ | 'normal'
20
+ | 'medium'
21
+ | 'semibold'
22
+ | 'bold'
23
+ | 'extrabold';
24
+ export type TextFamily = 'sans' | 'mono' | 'display';
25
+ export type TextLeading = 'none' | 'tight' | 'snug' | 'normal' | 'relaxed';
26
+ export type TextTracking = 'tighter' | 'tight' | 'normal' | 'wide' | 'wider';
27
+ export type TextAlign = 'left' | 'center' | 'right' | 'justify';
28
+
29
+ // Literal class maps — Tailwind only emits classes it can see verbatim.
30
+ const SIZE: Record<TextSize, string> = {
31
+ xs: 'text-xs',
32
+ sm: 'text-sm',
33
+ base: 'text-base',
34
+ lg: 'text-lg',
35
+ xl: 'text-xl',
36
+ '2xl': 'text-2xl',
37
+ '3xl': 'text-3xl',
38
+ '4xl': 'text-4xl',
39
+ '5xl': 'text-5xl',
40
+ '6xl': 'text-6xl',
41
+ };
42
+ const WEIGHT: Record<TextWeight, string> = {
43
+ light: 'font-light',
44
+ normal: 'font-normal',
45
+ medium: 'font-medium',
46
+ semibold: 'font-semibold',
47
+ bold: 'font-bold',
48
+ extrabold: 'font-extrabold',
49
+ };
50
+ const FAMILY: Record<TextFamily, string> = {
51
+ sans: 'font-sans',
52
+ mono: 'font-mono',
53
+ display: 'font-display',
54
+ };
55
+ const LEADING: Record<TextLeading, string> = {
56
+ none: 'leading-none',
57
+ tight: 'leading-tight',
58
+ snug: 'leading-snug',
59
+ normal: 'leading-normal',
60
+ relaxed: 'leading-relaxed',
61
+ };
62
+ const TRACKING: Record<TextTracking, string> = {
63
+ tighter: 'tracking-tighter',
64
+ tight: 'tracking-tight',
65
+ normal: 'tracking-normal',
66
+ wide: 'tracking-wide',
67
+ wider: 'tracking-wider',
68
+ };
69
+ const ALIGN: Record<TextAlign, string> = {
70
+ left: 'text-left',
71
+ center: 'text-center',
72
+ right: 'text-right',
73
+ justify: 'text-justify',
74
+ };
75
+
76
+ export interface TextProps extends Omit<
77
+ React.HTMLAttributes<HTMLElement>,
78
+ 'color'
79
+ > {
80
+ as?: React.ElementType;
81
+ size?: TextSize;
82
+ weight?: TextWeight;
83
+ family?: TextFamily;
84
+ leading?: TextLeading;
85
+ tracking?: TextTracking;
86
+ align?: TextAlign;
87
+ color?: ColorToken;
88
+ truncate?: boolean;
89
+ italic?: boolean;
90
+ uppercase?: boolean;
91
+ }
92
+
93
+ /**
94
+ * Text — typography primitive. Drive size/weight/family/leading/tracking/color
95
+ * through props instead of utility classes.
96
+ *
97
+ * <Text size="sm" color="muted-foreground">Helper copy</Text>
98
+ */
99
+ const Text = React.forwardRef<HTMLElement, TextProps>(function Text(
100
+ {
101
+ as: Comp = 'span',
102
+ className,
103
+ style,
104
+ size,
105
+ weight,
106
+ family,
107
+ leading,
108
+ tracking,
109
+ align,
110
+ color: colorToken,
111
+ truncate,
112
+ italic,
113
+ uppercase,
114
+ ...props
115
+ },
116
+ ref
117
+ ) {
118
+ return (
119
+ <Comp
120
+ ref={ref}
121
+ className={cn(
122
+ size && SIZE[size],
123
+ weight && WEIGHT[weight],
124
+ family && FAMILY[family],
125
+ leading && LEADING[leading],
126
+ tracking && TRACKING[tracking],
127
+ align && ALIGN[align],
128
+ truncate && 'truncate',
129
+ italic && 'italic',
130
+ uppercase && 'uppercase',
131
+ className
132
+ )}
133
+ style={{ color: color(colorToken), ...style }}
134
+ {...props}
135
+ />
136
+ );
137
+ });
138
+
139
+ export { Text };
@@ -0,0 +1,112 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+
3
+ import { Text } from './text';
4
+ import { Heading } from './heading';
5
+ import { VStack } from './flex';
6
+
7
+ const meta = {
8
+ // Title leaf must resolve to the named export in scope (§7): `Text`.
9
+ title: 'Layout/Text',
10
+ component: Text,
11
+ tags: ['autodocs'],
12
+ // Text is a typography leaf: `children` is the editable label (a plain string
13
+ // control, not a slot — same as Button). `className`/`style` stay unexposed.
14
+ argTypes: {
15
+ children: { control: 'text', description: 'Text content' },
16
+ size: {
17
+ control: 'select',
18
+ options: [
19
+ 'xs',
20
+ 'sm',
21
+ 'base',
22
+ 'lg',
23
+ 'xl',
24
+ '2xl',
25
+ '3xl',
26
+ '4xl',
27
+ '5xl',
28
+ '6xl',
29
+ ],
30
+ },
31
+ weight: {
32
+ control: 'select',
33
+ options: ['light', 'normal', 'medium', 'semibold', 'bold', 'extrabold'],
34
+ },
35
+ family: { control: 'inline-radio', options: ['sans', 'mono', 'display'] },
36
+ leading: {
37
+ control: 'select',
38
+ options: ['none', 'tight', 'snug', 'normal', 'relaxed'],
39
+ },
40
+ tracking: {
41
+ control: 'select',
42
+ options: ['tighter', 'tight', 'normal', 'wide', 'wider'],
43
+ },
44
+ align: {
45
+ control: 'select',
46
+ options: ['left', 'center', 'right', 'justify'],
47
+ },
48
+ color: {
49
+ control: 'select',
50
+ options: [
51
+ 'foreground',
52
+ 'muted-foreground',
53
+ 'primary',
54
+ 'destructive',
55
+ 'success',
56
+ ],
57
+ },
58
+ truncate: { control: 'boolean' },
59
+ italic: { control: 'boolean' },
60
+ uppercase: { control: 'boolean' },
61
+ },
62
+ args: {
63
+ children: 'The quick brown fox books an appointment at 9:00 AM.',
64
+ size: 'base',
65
+ },
66
+ } satisfies Meta<typeof Text>;
67
+
68
+ export default meta;
69
+ type Story = StoryObj<typeof meta>;
70
+
71
+ export const Playground: Story = {};
72
+
73
+ export const Headings: Story = {
74
+ render: () => (
75
+ <VStack gap={4}>
76
+ {[1, 2, 3, 4, 5, 6].map((l) => (
77
+ <Heading key={l} level={l as 1}>
78
+ Heading level {l}
79
+ </Heading>
80
+ ))}
81
+ </VStack>
82
+ ),
83
+ };
84
+
85
+ export const TextStyles: Story = {
86
+ render: () => (
87
+ <VStack gap={3}>
88
+ <Text size='lg' color='muted-foreground'>
89
+ Lead — reassuring, healthcare-aware copy.
90
+ </Text>
91
+ <Text>Body — the quick brown fox books an appointment at 9:00 AM.</Text>
92
+ <Text size='sm' color='muted-foreground'>
93
+ Small / caption text.
94
+ </Text>
95
+ <Text
96
+ size='xs'
97
+ weight='semibold'
98
+ tracking='wide'
99
+ uppercase
100
+ color='muted-foreground'
101
+ >
102
+ Overline
103
+ </Text>
104
+ <Text family='mono' size='sm'>
105
+ Geist Mono — 09:00–09:30 · ID a1b2c3
106
+ </Text>
107
+ <Text weight='bold' color='primary'>
108
+ Emphasis in brand color.
109
+ </Text>
110
+ </VStack>
111
+ ),
112
+ };
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }