@duro-app/ui 0.12.2 → 0.14.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 (134) hide show
  1. package/dist/components/PageShell/PageShell.d.ts +15 -0
  2. package/dist/components/PageShell/PageShell.d.ts.map +1 -0
  3. package/dist/components/PageShell/index.d.ts +3 -0
  4. package/dist/components/PageShell/index.d.ts.map +1 -0
  5. package/dist/components/PageShell/styles.css.d.ts +41 -0
  6. package/dist/components/PageShell/styles.css.d.ts.map +1 -0
  7. package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
  8. package/dist/index.css +1 -1
  9. package/dist/index.d.ts +1 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3283 -3434
  12. package/dist/index.js.map +1 -1
  13. package/package.json +4 -4
  14. package/src/components/Alert/Alert.stories.tsx +76 -0
  15. package/src/components/Alert/Alert.tsx +45 -0
  16. package/src/components/Alert/styles.css.ts +50 -0
  17. package/src/components/Badge/Badge.stories.tsx +94 -0
  18. package/src/components/Badge/Badge.tsx +21 -0
  19. package/src/components/Badge/styles.css.ts +51 -0
  20. package/src/components/Button/Button.stories.tsx +130 -0
  21. package/src/components/Button/Button.tsx +48 -0
  22. package/src/components/Button/styles.css.ts +107 -0
  23. package/src/components/Callout/Callout.stories.tsx +97 -0
  24. package/src/components/Callout/Callout.tsx +39 -0
  25. package/src/components/Callout/index.ts +1 -0
  26. package/src/components/Callout/styles.css.ts +45 -0
  27. package/src/components/Card/Card.stories.tsx +119 -0
  28. package/src/components/Card/Card.tsx +35 -0
  29. package/src/components/Card/styles.css.ts +67 -0
  30. package/src/components/Checkbox/Checkbox.stories.tsx +88 -0
  31. package/src/components/Checkbox/Checkbox.tsx +73 -0
  32. package/src/components/Checkbox/styles.css.ts +57 -0
  33. package/src/components/Cluster/Cluster.stories.tsx +92 -0
  34. package/src/components/Cluster/Cluster.tsx +43 -0
  35. package/src/components/Cluster/styles.css.ts +25 -0
  36. package/src/components/EmptyState/EmptyState.stories.tsx +54 -0
  37. package/src/components/EmptyState/EmptyState.tsx +19 -0
  38. package/src/components/EmptyState/styles.css.ts +25 -0
  39. package/src/components/Field/Field.stories.tsx +92 -0
  40. package/src/components/Field/Field.tsx +80 -0
  41. package/src/components/Field/FieldContext.ts +14 -0
  42. package/src/components/Field/styles.css.ts +25 -0
  43. package/src/components/Fieldset/Fieldset.stories.tsx +85 -0
  44. package/src/components/Fieldset/Fieldset.tsx +48 -0
  45. package/src/components/Fieldset/index.ts +1 -0
  46. package/src/components/Fieldset/styles.css.ts +33 -0
  47. package/src/components/Grid/Grid.stories.tsx +107 -0
  48. package/src/components/Grid/Grid.tsx +41 -0
  49. package/src/components/Grid/styles.css.ts +25 -0
  50. package/src/components/Heading/Heading.tsx +48 -0
  51. package/src/components/Heading/styles.css.ts +26 -0
  52. package/src/components/Icon/Icon.tsx +168 -0
  53. package/src/components/Icon/index.ts +2 -0
  54. package/src/components/Inline/Inline.stories.tsx +88 -0
  55. package/src/components/Inline/Inline.tsx +45 -0
  56. package/src/components/Inline/styles.css.ts +27 -0
  57. package/src/components/Input/Input.stories.tsx +89 -0
  58. package/src/components/Input/Input.tsx +77 -0
  59. package/src/components/Input/styles.css.ts +60 -0
  60. package/src/components/InputGroup/InputGroup.stories.tsx +119 -0
  61. package/src/components/InputGroup/InputGroup.tsx +60 -0
  62. package/src/components/InputGroup/InputGroupContext.ts +11 -0
  63. package/src/components/InputGroup/styles.css.ts +61 -0
  64. package/src/components/LinkButton/LinkButton.stories.tsx +91 -0
  65. package/src/components/LinkButton/LinkButton.tsx +42 -0
  66. package/src/components/LinkButton/styles.css.ts +56 -0
  67. package/src/components/Menu/Menu.stories.tsx +146 -0
  68. package/src/components/Menu/Menu.tsx +151 -0
  69. package/src/components/Menu/MenuContext.ts +20 -0
  70. package/src/components/Menu/styles.css.ts +89 -0
  71. package/src/components/Menu/useMenuRoot.ts +136 -0
  72. package/src/components/PageShell/PageShell.tsx +45 -0
  73. package/src/components/PageShell/index.ts +2 -0
  74. package/src/components/PageShell/styles.css.ts +26 -0
  75. package/src/components/ScrollArea/ScrollArea.stories.tsx +82 -0
  76. package/src/components/ScrollArea/ScrollArea.tsx +170 -0
  77. package/src/components/ScrollArea/ScrollAreaContext.ts +21 -0
  78. package/src/components/ScrollArea/styles.css.ts +81 -0
  79. package/src/components/ScrollArea/useScrollAreaRoot.ts +72 -0
  80. package/src/components/Select/Select.stories.tsx +144 -0
  81. package/src/components/Select/Select.tsx +183 -0
  82. package/src/components/Select/SelectContext.ts +24 -0
  83. package/src/components/Select/styles.css.ts +97 -0
  84. package/src/components/Select/useSelectRoot.ts +178 -0
  85. package/src/components/SideNav/SideNav.stories.tsx +77 -0
  86. package/src/components/SideNav/SideNav.tsx +172 -0
  87. package/src/components/SideNav/SideNavContext.ts +18 -0
  88. package/src/components/SideNav/styles.css.ts +95 -0
  89. package/src/components/Spinner/Spinner.stories.tsx +59 -0
  90. package/src/components/Spinner/Spinner.tsx +24 -0
  91. package/src/components/Spinner/styles.css.ts +47 -0
  92. package/src/components/Stack/Stack.stories.tsx +103 -0
  93. package/src/components/Stack/Stack.tsx +33 -0
  94. package/src/components/Stack/styles.css.ts +21 -0
  95. package/src/components/StatusIcon/StatusIcon.stories.tsx +81 -0
  96. package/src/components/StatusIcon/StatusIcon.tsx +24 -0
  97. package/src/components/StatusIcon/styles.css.ts +27 -0
  98. package/src/components/Switch/Switch.stories.tsx +88 -0
  99. package/src/components/Switch/Switch.tsx +78 -0
  100. package/src/components/Switch/styles.css.ts +71 -0
  101. package/src/components/Table/Table.stories.tsx +308 -0
  102. package/src/components/Table/Table.tsx +179 -0
  103. package/src/components/Table/styles.css.ts +97 -0
  104. package/src/components/Tabs/Tabs.stories.tsx +142 -0
  105. package/src/components/Tabs/Tabs.tsx +210 -0
  106. package/src/components/Tabs/TabsContext.ts +20 -0
  107. package/src/components/Tabs/styles.css.ts +98 -0
  108. package/src/components/Tabs/useTabsRoot.ts +42 -0
  109. package/src/components/Text/Text.tsx +52 -0
  110. package/src/components/Text/styles.css.ts +57 -0
  111. package/src/components/Textarea/Textarea.stories.tsx +80 -0
  112. package/src/components/Textarea/Textarea.tsx +50 -0
  113. package/src/components/Textarea/styles.css.ts +56 -0
  114. package/src/components/ThemeProvider/ThemeProvider.stories.tsx +163 -0
  115. package/src/components/ThemeProvider/ThemeProvider.tsx +33 -0
  116. package/src/components/Toggle/Toggle.stories.tsx +84 -0
  117. package/src/components/Toggle/Toggle.tsx +85 -0
  118. package/src/components/Toggle/styles.css.ts +66 -0
  119. package/src/components/ToggleGroup/ToggleGroup.stories.tsx +159 -0
  120. package/src/components/ToggleGroup/ToggleGroup.tsx +63 -0
  121. package/src/components/ToggleGroup/ToggleGroupContext.ts +18 -0
  122. package/src/components/ToggleGroup/styles.css.ts +17 -0
  123. package/src/components/Tooltip/Tooltip.stories.tsx +127 -0
  124. package/src/components/Tooltip/Tooltip.tsx +97 -0
  125. package/src/components/Tooltip/styles.css.ts +56 -0
  126. package/src/docs/Spacing.mdx +80 -0
  127. package/src/docs/Spacing.stories.tsx +202 -0
  128. package/src/docs/Typography.mdx +93 -0
  129. package/src/docs/Typography.stories.tsx +211 -0
  130. package/src/docs/helpers.tsx +135 -0
  131. package/src/hooks/useContainerQuery.ts +54 -0
  132. package/src/hooks/useControllableValue.ts +18 -0
  133. package/src/index.ts +56 -0
  134. package/src/stubs/assets-registry.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "0.12.2",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,14 +17,14 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
- "src/strict.css",
21
- "src/global-reset.css"
20
+ "src"
22
21
  ],
23
22
  "main": "dist/index.js",
24
23
  "module": "dist/index.js",
25
24
  "types": "dist/index.d.ts",
26
25
  "exports": {
27
26
  ".": {
27
+ "source": "./src/index.ts",
28
28
  "import": "./dist/index.js",
29
29
  "types": "./dist/index.d.ts"
30
30
  },
@@ -38,7 +38,7 @@
38
38
  "react-strict-dom": "^0.0.55"
39
39
  },
40
40
  "dependencies": {
41
- "@duro-app/tokens": "^0.3.0"
41
+ "@duro-app/tokens": "^0.4.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@babel/preset-typescript": "^7.28.0",
@@ -0,0 +1,76 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect} from 'storybook/test'
3
+ import {css, html} from 'react-strict-dom'
4
+ import {Alert} from './Alert'
5
+
6
+ const meta: Meta<typeof Alert> = {
7
+ title: 'Components/Alert',
8
+ component: Alert,
9
+ argTypes: {
10
+ variant: {
11
+ control: 'select',
12
+ options: ['error', 'success', 'warning', 'info'],
13
+ },
14
+ },
15
+ }
16
+
17
+ export default meta
18
+ type Story = StoryObj<typeof Alert>
19
+
20
+ export const Error: Story = {
21
+ args: {variant: 'error', children: 'Something went wrong. Please try again.'},
22
+ play: async ({canvas}) => {
23
+ const alert = canvas.getByRole('alert')
24
+ await expect(alert).toBeInTheDocument()
25
+ await expect(alert).toHaveTextContent('Something went wrong. Please try again.')
26
+ },
27
+ }
28
+
29
+ export const Success: Story = {
30
+ args: {variant: 'success', children: 'Account created successfully!'},
31
+ play: async ({canvas}) => {
32
+ const alert = canvas.getByRole('alert')
33
+ await expect(alert).toHaveTextContent('Account created successfully!')
34
+ },
35
+ }
36
+
37
+ export const Warning: Story = {
38
+ args: {variant: 'warning', children: 'Your session will expire in 5 minutes.'},
39
+ play: async ({canvas}) => {
40
+ await expect(canvas.getByRole('alert')).toBeInTheDocument()
41
+ },
42
+ }
43
+
44
+ export const Info: Story = {
45
+ args: {variant: 'info', children: 'A new version is available.'},
46
+ play: async ({canvas}) => {
47
+ await expect(canvas.getByRole('alert')).toBeInTheDocument()
48
+ },
49
+ }
50
+
51
+ export const NoIcon: Story = {
52
+ args: {
53
+ variant: 'info',
54
+ icon: false,
55
+ children: 'This alert has no icon.',
56
+ },
57
+ }
58
+
59
+ const stackStyles = css.create({
60
+ stack: {display: 'flex', flexDirection: 'column', gap: 12},
61
+ })
62
+
63
+ export const AllVariants: Story = {
64
+ render: () => (
65
+ <html.div style={stackStyles.stack}>
66
+ <Alert variant="error">Error: Something went wrong.</Alert>
67
+ <Alert variant="success">Success: Operation completed.</Alert>
68
+ <Alert variant="warning">Warning: Check your input.</Alert>
69
+ <Alert variant="info">Info: System update available.</Alert>
70
+ </html.div>
71
+ ),
72
+ play: async ({canvas}) => {
73
+ const alerts = canvas.getAllByRole('alert')
74
+ await expect(alerts.length).toBe(4)
75
+ },
76
+ }
@@ -0,0 +1,45 @@
1
+ import type {ReactNode} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {Icon} from '../Icon'
4
+ import type {IconName} from '../Icon'
5
+ import {styles} from './styles.css'
6
+
7
+ export type AlertVariant = 'error' | 'success' | 'warning' | 'info'
8
+
9
+ const defaultIcons: Record<AlertVariant, IconName> = {
10
+ info: 'info-circle-filled',
11
+ warning: 'alert-triangle-filled',
12
+ success: 'check-circle-filled',
13
+ error: 'x-circle-filled',
14
+ }
15
+
16
+ interface AlertProps {
17
+ variant?: AlertVariant
18
+ /** Built-in icon name, custom ReactNode, or false to hide. Defaults to variant icon. */
19
+ icon?: IconName | ReactNode | false
20
+ children: ReactNode
21
+ }
22
+
23
+ function resolveIcon(icon: AlertProps['icon'], variant: AlertVariant): ReactNode | null {
24
+ if (icon === false) return null
25
+ if (icon === undefined) return <Icon name={defaultIcons[variant]} size={18} />
26
+ if (typeof icon === 'string') return <Icon name={icon as IconName} size={18} />
27
+ return icon
28
+ }
29
+
30
+ export function Alert({variant = 'info', icon, children}: AlertProps) {
31
+ const resolvedIcon = resolveIcon(icon, variant)
32
+
33
+ return (
34
+ <html.div role="alert" style={[styles.base, styles[variant]]}>
35
+ {resolvedIcon ? (
36
+ <>
37
+ <html.div style={styles.iconWrap}>{resolvedIcon}</html.div>
38
+ <html.div style={styles.content}>{children}</html.div>
39
+ </>
40
+ ) : (
41
+ children
42
+ )}
43
+ </html.div>
44
+ )
45
+ }
@@ -0,0 +1,50 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+
6
+ export const styles = css.create({
7
+ base: {
8
+ display: 'flex',
9
+ flexDirection: 'row',
10
+ alignItems: 'flex-start',
11
+ gap: spacing.sm,
12
+ padding: spacing.md,
13
+ borderRadius: radii.sm,
14
+ borderWidth: 1,
15
+ borderStyle: 'solid',
16
+ fontSize: typography.fontSizeSm,
17
+ lineHeight: typography.lineHeight,
18
+ },
19
+ iconWrap: {
20
+ flexShrink: 0,
21
+ display: 'flex',
22
+ alignItems: 'center',
23
+ justifyContent: 'center',
24
+ paddingTop: 1,
25
+ },
26
+ content: {
27
+ flex: 1,
28
+ minWidth: 0,
29
+ },
30
+ error: {
31
+ backgroundColor: colors.errorBg,
32
+ borderColor: colors.errorBorder,
33
+ color: colors.errorText,
34
+ },
35
+ success: {
36
+ backgroundColor: colors.successBg,
37
+ borderColor: colors.successBorder,
38
+ color: colors.successText,
39
+ },
40
+ warning: {
41
+ backgroundColor: colors.warningBg,
42
+ borderColor: colors.warningBorder,
43
+ color: colors.warningText,
44
+ },
45
+ info: {
46
+ backgroundColor: colors.infoBg,
47
+ borderColor: colors.infoBorder,
48
+ color: colors.infoText,
49
+ },
50
+ })
@@ -0,0 +1,94 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect} from 'storybook/test'
3
+ import {css, html} from 'react-strict-dom'
4
+ import {Badge} from './Badge'
5
+
6
+ const meta: Meta<typeof Badge> = {
7
+ title: 'Components/Badge',
8
+ component: Badge,
9
+ argTypes: {
10
+ variant: {
11
+ control: 'select',
12
+ options: ['default', 'success', 'warning', 'error', 'info'],
13
+ },
14
+ size: {
15
+ control: 'select',
16
+ options: ['sm', 'md'],
17
+ },
18
+ },
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof Badge>
23
+
24
+ export const Default: Story = {
25
+ args: {children: 'Badge'},
26
+ play: async ({canvas}) => {
27
+ await expect(canvas.getByText('Badge')).toBeInTheDocument()
28
+ },
29
+ }
30
+
31
+ export const Success: Story = {
32
+ args: {variant: 'success', children: 'Active'},
33
+ play: async ({canvas}) => {
34
+ await expect(canvas.getByText('Active')).toBeInTheDocument()
35
+ },
36
+ }
37
+
38
+ export const Warning: Story = {
39
+ args: {variant: 'warning', children: 'Expiring'},
40
+ }
41
+
42
+ export const Error: Story = {
43
+ args: {variant: 'error', children: 'Expired'},
44
+ }
45
+
46
+ export const Info: Story = {
47
+ args: {variant: 'info', children: 'Updated'},
48
+ }
49
+
50
+ export const Small: Story = {
51
+ args: {variant: 'success', size: 'sm', children: 'sm'},
52
+ }
53
+
54
+ const stackStyles = css.create({
55
+ row: {display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap'},
56
+ stack: {display: 'flex', flexDirection: 'column', gap: 16},
57
+ })
58
+
59
+ export const AllVariants: Story = {
60
+ render: () => (
61
+ <html.div style={stackStyles.stack}>
62
+ <html.div style={stackStyles.row}>
63
+ <Badge>Default</Badge>
64
+ <Badge variant="success">Success</Badge>
65
+ <Badge variant="warning">Warning</Badge>
66
+ <Badge variant="error">Error</Badge>
67
+ <Badge variant="info">Info</Badge>
68
+ </html.div>
69
+ <html.div style={stackStyles.row}>
70
+ <Badge size="sm">Default</Badge>
71
+ <Badge variant="success" size="sm">
72
+ Success
73
+ </Badge>
74
+ <Badge variant="warning" size="sm">
75
+ Warning
76
+ </Badge>
77
+ <Badge variant="error" size="sm">
78
+ Error
79
+ </Badge>
80
+ <Badge variant="info" size="sm">
81
+ Info
82
+ </Badge>
83
+ </html.div>
84
+ </html.div>
85
+ ),
86
+ play: async ({canvas}) => {
87
+ // Each variant appears twice: once in md row, once in sm row
88
+ await expect(canvas.getAllByText('Default').length).toBe(2)
89
+ await expect(canvas.getAllByText('Success').length).toBe(2)
90
+ await expect(canvas.getAllByText('Warning').length).toBe(2)
91
+ await expect(canvas.getAllByText('Error').length).toBe(2)
92
+ await expect(canvas.getAllByText('Info').length).toBe(2)
93
+ },
94
+ }
@@ -0,0 +1,21 @@
1
+ import type {ReactNode} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {styles} from './styles.css'
4
+
5
+ export type BadgeVariant = 'default' | 'success' | 'warning' | 'error' | 'info'
6
+ export type BadgeSize = 'sm' | 'md'
7
+
8
+ interface BadgeProps {
9
+ variant?: BadgeVariant
10
+ size?: BadgeSize
11
+ children: ReactNode
12
+ }
13
+
14
+ const sizeMap = {
15
+ sm: styles.sizeSm,
16
+ md: styles.sizeMd,
17
+ } as const
18
+
19
+ export function Badge({variant = 'default', size = 'md', children}: BadgeProps) {
20
+ return <html.span style={[styles.base, sizeMap[size], styles[variant]]}>{children}</html.span>
21
+ }
@@ -0,0 +1,51 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+
6
+ export const styles = css.create({
7
+ base: {
8
+ display: 'inline-flex',
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ fontFamily: typography.fontFamily,
12
+ fontWeight: typography.fontWeightMedium,
13
+ lineHeight: 1,
14
+ borderRadius: radii.full,
15
+ whiteSpace: 'nowrap',
16
+ },
17
+ sizeMd: {
18
+ paddingTop: spacing.xs,
19
+ paddingBottom: spacing.xs,
20
+ paddingLeft: spacing.sm,
21
+ paddingRight: spacing.sm,
22
+ fontSize: typography.fontSizeXs,
23
+ },
24
+ sizeSm: {
25
+ paddingTop: 2,
26
+ paddingBottom: 2,
27
+ paddingLeft: spacing.xs,
28
+ paddingRight: spacing.xs,
29
+ fontSize: '0.625rem',
30
+ },
31
+ default: {
32
+ backgroundColor: colors.bgCardHover,
33
+ color: colors.textMuted,
34
+ },
35
+ success: {
36
+ backgroundColor: colors.successBg,
37
+ color: colors.successText,
38
+ },
39
+ warning: {
40
+ backgroundColor: colors.warningBg,
41
+ color: colors.warningText,
42
+ },
43
+ error: {
44
+ backgroundColor: colors.errorBg,
45
+ color: colors.errorText,
46
+ },
47
+ info: {
48
+ backgroundColor: colors.infoBg,
49
+ color: colors.infoText,
50
+ },
51
+ })
@@ -0,0 +1,130 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect, fn} from 'storybook/test'
3
+ import {css, html} from 'react-strict-dom'
4
+ import {Button} from './Button'
5
+
6
+ const meta: Meta<typeof Button> = {
7
+ title: 'Components/Button',
8
+ component: Button,
9
+ args: {
10
+ onClick: fn(),
11
+ },
12
+ argTypes: {
13
+ variant: {
14
+ control: 'select',
15
+ options: ['primary', 'secondary', 'link', 'danger'],
16
+ },
17
+ size: {
18
+ control: 'select',
19
+ options: ['default', 'small'],
20
+ },
21
+ fullWidth: {control: 'boolean'},
22
+ disabled: {control: 'boolean'},
23
+ },
24
+ }
25
+
26
+ export default meta
27
+ type Story = StoryObj<typeof Button>
28
+
29
+ export const Primary: Story = {
30
+ args: {variant: 'primary', children: 'Primary Button'},
31
+ play: async ({args, canvas, userEvent}) => {
32
+ const button = canvas.getByRole('button', {name: 'Primary Button'})
33
+ await expect(button).toBeInTheDocument()
34
+ await expect(button).toBeEnabled()
35
+
36
+ await userEvent.click(button)
37
+ await expect(args.onClick).toHaveBeenCalledTimes(1)
38
+ },
39
+ }
40
+
41
+ export const Secondary: Story = {
42
+ args: {variant: 'secondary', children: 'Secondary Button'},
43
+ play: async ({canvas}) => {
44
+ const button = canvas.getByRole('button', {name: 'Secondary Button'})
45
+ await expect(button).toBeInTheDocument()
46
+ await expect(button).toBeEnabled()
47
+ },
48
+ }
49
+
50
+ export const Link: Story = {
51
+ args: {variant: 'link', children: 'Link Button'},
52
+ play: async ({args, canvas, userEvent}) => {
53
+ const button = canvas.getByRole('button', {name: 'Link Button'})
54
+ await userEvent.click(button)
55
+ await expect(args.onClick).toHaveBeenCalled()
56
+ },
57
+ }
58
+
59
+ export const Danger: Story = {
60
+ args: {variant: 'danger', children: 'Delete'},
61
+ play: async ({canvas}) => {
62
+ await expect(canvas.getByRole('button', {name: 'Delete'})).toBeInTheDocument()
63
+ },
64
+ }
65
+
66
+ export const Small: Story = {
67
+ args: {variant: 'primary', size: 'small', children: 'Small'},
68
+ }
69
+
70
+ export const FullWidth: Story = {
71
+ args: {variant: 'primary', fullWidth: true, children: 'Full Width'},
72
+ }
73
+
74
+ export const Disabled: Story = {
75
+ args: {variant: 'primary', disabled: true, children: 'Disabled'},
76
+ play: async ({args, canvas, userEvent}) => {
77
+ const button = canvas.getByRole('button', {name: 'Disabled'})
78
+ await expect(button).toBeDisabled()
79
+
80
+ await userEvent.click(button)
81
+ await expect(args.onClick).not.toHaveBeenCalled()
82
+ },
83
+ }
84
+
85
+ const rowStyles = css.create({
86
+ row: {display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap'},
87
+ stack: {display: 'flex', flexDirection: 'column', gap: 16},
88
+ })
89
+
90
+ export const AllVariants: Story = {
91
+ render: () => (
92
+ <html.div style={rowStyles.stack}>
93
+ <html.div style={rowStyles.row}>
94
+ <Button variant="primary">Primary</Button>
95
+ <Button variant="secondary">Secondary</Button>
96
+ <Button variant="link">Link</Button>
97
+ <Button variant="danger">Danger</Button>
98
+ </html.div>
99
+ <html.div style={rowStyles.row}>
100
+ <Button variant="primary" size="small">
101
+ Small Primary
102
+ </Button>
103
+ <Button variant="secondary" size="small">
104
+ Small Secondary
105
+ </Button>
106
+ <Button variant="danger" size="small">
107
+ Small Danger
108
+ </Button>
109
+ </html.div>
110
+ <html.div style={rowStyles.row}>
111
+ <Button variant="primary" disabled>
112
+ Disabled Primary
113
+ </Button>
114
+ <Button variant="secondary" disabled>
115
+ Disabled Secondary
116
+ </Button>
117
+ <Button variant="danger" disabled>
118
+ Disabled Danger
119
+ </Button>
120
+ </html.div>
121
+ </html.div>
122
+ ),
123
+ play: async ({canvas}) => {
124
+ const buttons = canvas.getAllByRole('button')
125
+ await expect(buttons.length).toBe(10)
126
+
127
+ const disabledButtons = buttons.filter((b: HTMLElement) => (b as HTMLButtonElement).disabled)
128
+ await expect(disabledButtons.length).toBe(3)
129
+ },
130
+ }
@@ -0,0 +1,48 @@
1
+ import type {ReactNode} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {styles} from './styles.css'
4
+
5
+ export type ButtonVariant = 'primary' | 'secondary' | 'link' | 'danger'
6
+ export type ButtonSize = 'default' | 'small'
7
+
8
+ interface ButtonProps {
9
+ variant?: ButtonVariant
10
+ size?: ButtonSize
11
+ fullWidth?: boolean
12
+ disabled?: boolean
13
+ type?: 'button' | 'submit'
14
+ onClick?: () => void
15
+ children: ReactNode
16
+ }
17
+
18
+ const sizeMap = {
19
+ default: styles.sizeDefault,
20
+ small: styles.sizeSmall,
21
+ } as const
22
+
23
+ export function Button({
24
+ variant = 'primary',
25
+ size = 'default',
26
+ fullWidth = false,
27
+ disabled = false,
28
+ type = 'button',
29
+ onClick,
30
+ children,
31
+ }: ButtonProps) {
32
+ return (
33
+ <html.button
34
+ type={type}
35
+ disabled={disabled}
36
+ onClick={onClick}
37
+ style={[
38
+ styles.base,
39
+ sizeMap[size],
40
+ styles[variant],
41
+ fullWidth && styles.fullWidth,
42
+ disabled && styles.disabled,
43
+ ]}
44
+ >
45
+ {children}
46
+ </html.button>
47
+ )
48
+ }
@@ -0,0 +1,107 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+
6
+ export const styles = css.create({
7
+ base: {
8
+ display: 'inline-flex',
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ gap: spacing.sm,
12
+ fontFamily: typography.fontFamily,
13
+ fontSize: typography.fontSizeSm,
14
+ fontWeight: typography.fontWeightMedium,
15
+ lineHeight: typography.lineHeight,
16
+ borderRadius: radii.sm,
17
+ borderWidth: 1,
18
+ borderStyle: 'solid',
19
+ cursor: 'pointer',
20
+ transitionProperty: 'background-color, border-color, color, opacity',
21
+ transitionDuration: '150ms',
22
+ transitionTimingFunction: 'ease',
23
+ textDecoration: 'none',
24
+ outlineWidth: {
25
+ default: 0,
26
+ ':focus-visible': 2,
27
+ },
28
+ outlineStyle: {
29
+ default: 'none',
30
+ ':focus-visible': 'solid',
31
+ },
32
+ outlineColor: {
33
+ default: 'transparent',
34
+ ':focus-visible': colors.accent,
35
+ },
36
+ outlineOffset: {
37
+ default: 0,
38
+ ':focus-visible': 2,
39
+ },
40
+ },
41
+ sizeDefault: {
42
+ paddingTop: spacing.sm,
43
+ paddingBottom: spacing.sm,
44
+ paddingLeft: spacing.md,
45
+ paddingRight: spacing.md,
46
+ },
47
+ sizeSmall: {
48
+ paddingTop: spacing.xs,
49
+ paddingBottom: spacing.xs,
50
+ paddingLeft: spacing.sm,
51
+ paddingRight: spacing.sm,
52
+ fontSize: typography.fontSizeXs,
53
+ },
54
+ primary: {
55
+ backgroundColor: {
56
+ default: colors.accent,
57
+ ':hover': colors.accentHover,
58
+ ':active': colors.accentHover,
59
+ },
60
+ borderColor: {
61
+ default: colors.accent,
62
+ ':hover': colors.accentHover,
63
+ },
64
+ color: colors.accentContrast,
65
+ },
66
+ secondary: {
67
+ backgroundColor: {
68
+ default: 'transparent',
69
+ ':hover': colors.bgCardHover,
70
+ },
71
+ borderColor: colors.border,
72
+ color: colors.textMuted,
73
+ },
74
+ link: {
75
+ backgroundColor: 'transparent',
76
+ borderColor: 'transparent',
77
+ color: {
78
+ default: colors.accent,
79
+ ':hover': colors.accentHover,
80
+ },
81
+ textDecoration: {
82
+ default: 'none',
83
+ ':hover': 'underline',
84
+ },
85
+ paddingLeft: 0,
86
+ paddingRight: 0,
87
+ },
88
+ danger: {
89
+ backgroundColor: {
90
+ default: colors.error,
91
+ ':hover': colors.errorHover,
92
+ ':active': colors.errorHover,
93
+ },
94
+ borderColor: {
95
+ default: colors.error,
96
+ ':hover': colors.errorHover,
97
+ },
98
+ color: colors.errorContrast,
99
+ },
100
+ fullWidth: {
101
+ width: '100%',
102
+ },
103
+ disabled: {
104
+ opacity: 0.5,
105
+ cursor: 'not-allowed',
106
+ },
107
+ })