@duro-app/ui 0.12.1 → 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
@@ -0,0 +1,107 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {css, html} from 'react-strict-dom'
3
+ import {Grid} from './Grid'
4
+ import {Stack} from '../Stack/Stack'
5
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
6
+ import {spacing} from '@duro-app/tokens/tokens/spacing.css'
7
+ import {useContainerQuery} from '../../hooks/useContainerQuery'
8
+
9
+ const meta: Meta<typeof Grid> = {
10
+ title: 'Layout/Grid',
11
+ component: Grid,
12
+ argTypes: {
13
+ gap: {
14
+ control: 'select',
15
+ options: ['xs', 'sm', 'ms', 'md', 'lg', 'xl', 'xxl', 'xxxl'],
16
+ },
17
+ columns: {
18
+ control: 'select',
19
+ options: [1, 2, 3, 4, 5, 6],
20
+ },
21
+ minColumnWidth: {control: 'text'},
22
+ },
23
+ }
24
+
25
+ export default meta
26
+ type Story = StoryObj<typeof Grid>
27
+
28
+ const localStyles = css.create({
29
+ cell: {
30
+ backgroundColor: colors.accent,
31
+ color: colors.accentContrast,
32
+ padding: spacing.md,
33
+ borderRadius: 4,
34
+ textAlign: 'center',
35
+ fontSize: '0.875rem',
36
+ },
37
+ label: {
38
+ fontSize: '0.75rem',
39
+ color: colors.textMuted,
40
+ },
41
+ })
42
+
43
+ const Cell = ({children}: {children: string}) => (
44
+ <html.div style={localStyles.cell}>{children}</html.div>
45
+ )
46
+
47
+ export const FixedColumns: Story = {
48
+ args: {
49
+ gap: 'md',
50
+ columns: 3,
51
+ },
52
+ render: (args) => (
53
+ <Grid {...args}>
54
+ <Cell>1</Cell>
55
+ <Cell>2</Cell>
56
+ <Cell>3</Cell>
57
+ <Cell>4</Cell>
58
+ <Cell>5</Cell>
59
+ <Cell>6</Cell>
60
+ </Grid>
61
+ ),
62
+ }
63
+
64
+ export const AutoFit: Story = {
65
+ render: () => (
66
+ <Grid gap="md" minColumnWidth="200px">
67
+ <Cell>Card A</Cell>
68
+ <Cell>Card B</Cell>
69
+ <Cell>Card C</Cell>
70
+ <Cell>Card D</Cell>
71
+ <Cell>Card E</Cell>
72
+ </Grid>
73
+ ),
74
+ }
75
+
76
+ export const TwoColumns: Story = {
77
+ render: () => (
78
+ <Grid gap="lg" columns={2}>
79
+ <Cell>Left</Cell>
80
+ <Cell>Right</Cell>
81
+ </Grid>
82
+ ),
83
+ }
84
+
85
+ export const WithContainerQuery: Story = {
86
+ render: function Render() {
87
+ const {ref, size} = useContainerQuery<HTMLDivElement>()
88
+ const gap = size === 'compact' ? 'sm' : size === 'spacious' ? 'xl' : 'md'
89
+ const cols = size === 'compact' ? 1 : size === 'spacious' ? 4 : 2
90
+
91
+ return (
92
+ <html.div ref={ref}>
93
+ <Stack gap="sm">
94
+ <html.span style={localStyles.label}>
95
+ Container size: {size} — columns: {cols} — gap: {gap}
96
+ </html.span>
97
+ <Grid gap={gap} columns={cols as 1 | 2 | 3 | 4}>
98
+ <Cell>1</Cell>
99
+ <Cell>2</Cell>
100
+ <Cell>3</Cell>
101
+ <Cell>4</Cell>
102
+ </Grid>
103
+ </Stack>
104
+ </html.div>
105
+ )
106
+ },
107
+ }
@@ -0,0 +1,41 @@
1
+ import type {ReactNode} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {styles} from './styles.css'
4
+ import type {SpacingKey} from '../Stack/Stack'
5
+
6
+ interface GridProps {
7
+ gap?: SpacingKey
8
+ columns?: 1 | 2 | 3 | 4 | 5 | 6
9
+ minColumnWidth?: string
10
+ children: ReactNode
11
+ }
12
+
13
+ const gapMap = {
14
+ xs: styles.gapXs,
15
+ sm: styles.gapSm,
16
+ ms: styles.gapMs,
17
+ md: styles.gapMd,
18
+ lg: styles.gapLg,
19
+ xl: styles.gapXl,
20
+ xxl: styles.gapXxl,
21
+ xxxl: styles.gapXxxl,
22
+ } as const
23
+
24
+ const columnsMap = {
25
+ 1: styles.col1,
26
+ 2: styles.col2,
27
+ 3: styles.col3,
28
+ 4: styles.col4,
29
+ 5: styles.col5,
30
+ 6: styles.col6,
31
+ } as const
32
+
33
+ export function Grid({gap = 'md', columns, minColumnWidth, children}: GridProps) {
34
+ const columnStyle = minColumnWidth
35
+ ? styles.autoFit(minColumnWidth)
36
+ : columns
37
+ ? columnsMap[columns]
38
+ : undefined
39
+
40
+ return <html.div style={[styles.base, gapMap[gap], columnStyle]}>{children}</html.div>
41
+ }
@@ -0,0 +1,25 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {spacing} from '@duro-app/tokens/tokens/spacing.css'
3
+
4
+ export const styles = css.create({
5
+ base: {
6
+ display: 'grid',
7
+ },
8
+ col1: {gridTemplateColumns: '1fr'},
9
+ col2: {gridTemplateColumns: 'repeat(2, 1fr)'},
10
+ col3: {gridTemplateColumns: 'repeat(3, 1fr)'},
11
+ col4: {gridTemplateColumns: 'repeat(4, 1fr)'},
12
+ col5: {gridTemplateColumns: 'repeat(5, 1fr)'},
13
+ col6: {gridTemplateColumns: 'repeat(6, 1fr)'},
14
+ autoFit: (minWidth: string) => ({
15
+ gridTemplateColumns: `repeat(auto-fill, minmax(${minWidth}, 1fr))`,
16
+ }),
17
+ gapXs: {gap: spacing.xs},
18
+ gapSm: {gap: spacing.sm},
19
+ gapMs: {gap: spacing.ms},
20
+ gapMd: {gap: spacing.md},
21
+ gapLg: {gap: spacing.lg},
22
+ gapXl: {gap: spacing.xl},
23
+ gapXxl: {gap: spacing.xxl},
24
+ gapXxxl: {gap: spacing.xxxl},
25
+ })
@@ -0,0 +1,48 @@
1
+ import type {ReactNode} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {typePresets} from '@duro-app/tokens/tokens/type-presets.css'
4
+ import {styles} from './styles.css'
5
+
6
+ export type HeadingVariant =
7
+ | 'displayLg'
8
+ | 'displayMd'
9
+ | 'displaySm'
10
+ | 'headingXl'
11
+ | 'headingLg'
12
+ | 'headingMd'
13
+ | 'headingSm'
14
+
15
+ interface HeadingProps {
16
+ level: 1 | 2 | 3 | 4 | 5 | 6
17
+ variant?: HeadingVariant
18
+ color?: 'default' | 'muted' | 'accent'
19
+ align?: 'start' | 'center' | 'end'
20
+ children: ReactNode
21
+ }
22
+
23
+ const defaultVariantMap: Record<number, HeadingVariant> = {
24
+ 1: 'headingXl',
25
+ 2: 'headingLg',
26
+ 3: 'headingMd',
27
+ 4: 'headingSm',
28
+ 5: 'headingSm',
29
+ 6: 'headingSm',
30
+ }
31
+
32
+ const alignMap = {
33
+ start: styles.alignStart,
34
+ center: styles.alignCenter,
35
+ end: styles.alignEnd,
36
+ } as const
37
+
38
+ export function Heading({level, variant, color = 'default', align, children}: HeadingProps) {
39
+ const resolvedVariant = variant ?? defaultVariantMap[level]
40
+ const style = [typePresets[resolvedVariant], styles[color], align && alignMap[align]]
41
+
42
+ if (level === 1) return <html.h1 style={style}>{children}</html.h1>
43
+ if (level === 2) return <html.h2 style={style}>{children}</html.h2>
44
+ if (level === 3) return <html.h3 style={style}>{children}</html.h3>
45
+ if (level === 4) return <html.h4 style={style}>{children}</html.h4>
46
+ if (level === 5) return <html.h5 style={style}>{children}</html.h5>
47
+ return <html.h6 style={style}>{children}</html.h6>
48
+ }
@@ -0,0 +1,26 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+
4
+ export const styles = css.create({
5
+ // Colors
6
+ default: {
7
+ color: colors.text,
8
+ },
9
+ muted: {
10
+ color: colors.textMuted,
11
+ },
12
+ accent: {
13
+ color: colors.accent,
14
+ },
15
+
16
+ // Alignment
17
+ alignStart: {
18
+ textAlign: 'start',
19
+ },
20
+ alignCenter: {
21
+ textAlign: 'center',
22
+ },
23
+ alignEnd: {
24
+ textAlign: 'end',
25
+ },
26
+ })
@@ -0,0 +1,168 @@
1
+ import type {ReactNode} from 'react'
2
+
3
+ export type IconName =
4
+ | 'x-circle'
5
+ | 'check-circle'
6
+ | 'check-done'
7
+ | 'clock'
8
+ | 'forbidden'
9
+ | 'info-circle'
10
+ | 'alert-triangle'
11
+ | 'shield'
12
+ | 'lock'
13
+ | 'key'
14
+ // Filled variants (solid shape with cutout symbol)
15
+ | 'info-circle-filled'
16
+ | 'alert-triangle-filled'
17
+ | 'check-circle-filled'
18
+ | 'x-circle-filled'
19
+ | 'shield-filled'
20
+ | 'lock-filled'
21
+
22
+ /** Stroke-based icons (Feather/Lucide style) */
23
+ const strokeIcons: Partial<Record<IconName, ReactNode>> = {
24
+ 'x-circle': (
25
+ <>
26
+ <circle cx="12" cy="12" r="10" />
27
+ <line x1="15" y1="9" x2="9" y2="15" />
28
+ <line x1="9" y1="9" x2="15" y2="15" />
29
+ </>
30
+ ),
31
+ 'check-circle': (
32
+ <>
33
+ <circle cx="12" cy="12" r="10" />
34
+ <polyline points="16 10 11 15 8 12" />
35
+ </>
36
+ ),
37
+ 'check-done': (
38
+ <>
39
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
40
+ <polyline points="22 4 12 14.01 9 11.01" />
41
+ </>
42
+ ),
43
+ clock: (
44
+ <>
45
+ <circle cx="12" cy="12" r="10" />
46
+ <polyline points="12 6 12 12 16 14" />
47
+ </>
48
+ ),
49
+ forbidden: (
50
+ <>
51
+ <circle cx="12" cy="12" r="10" />
52
+ <line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
53
+ </>
54
+ ),
55
+ 'info-circle': (
56
+ <>
57
+ <circle cx="12" cy="12" r="10" />
58
+ <line x1="12" y1="16" x2="12" y2="12" />
59
+ <line x1="12" y1="8" x2="12.01" y2="8" />
60
+ </>
61
+ ),
62
+ 'alert-triangle': (
63
+ <>
64
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
65
+ <line x1="12" y1="9" x2="12" y2="13" />
66
+ <line x1="12" y1="17" x2="12.01" y2="17" />
67
+ </>
68
+ ),
69
+ shield: <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />,
70
+ lock: (
71
+ <>
72
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
73
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
74
+ </>
75
+ ),
76
+ key: (
77
+ <>
78
+ <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.78 7.78 5.5 5.5 0 0 1 7.78-7.78zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
79
+ </>
80
+ ),
81
+ }
82
+
83
+ /**
84
+ * Filled icons — solid shape with evenodd cutout for the inner symbol.
85
+ * The cutout reveals the background color, so they work on any theme.
86
+ * Uses fill="currentColor" only, no stroke.
87
+ */
88
+ const filledIcons: Partial<Record<IconName, ReactNode>> = {
89
+ // Filled circle with "i" cutout (dot + line)
90
+ 'info-circle-filled': (
91
+ <path
92
+ fillRule="evenodd"
93
+ d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm-.75 6a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0zM11 11a1 1 0 1 1 2 0v5a1 1 0 1 1-2 0v-5z"
94
+ />
95
+ ),
96
+ // Filled triangle with "!" cutout
97
+ 'alert-triangle-filled': (
98
+ <path
99
+ fillRule="evenodd"
100
+ d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM11 10a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0v-3zm.25 6a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0z"
101
+ />
102
+ ),
103
+ // Filled circle with checkmark cutout
104
+ 'check-circle-filled': (
105
+ <path
106
+ fillRule="evenodd"
107
+ d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm4.707 8.707a1 1 0 0 0-1.414-1.414L11 13.586l-2.293-2.293a1 1 0 0 0-1.414 1.414l3 3a1 1 0 0 0 1.414 0l5-5z"
108
+ />
109
+ ),
110
+ // Filled circle with X cutout
111
+ 'x-circle-filled': (
112
+ <path
113
+ fillRule="evenodd"
114
+ d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zM9.707 8.293a1 1 0 0 0-1.414 1.414L10.586 12l-2.293 2.293a1 1 0 1 0 1.414 1.414L12 13.414l2.293 2.293a1 1 0 0 0 1.414-1.414L13.414 12l2.293-2.293a1 1 0 0 0-1.414-1.414L12 10.586 9.707 8.293z"
115
+ />
116
+ ),
117
+ // Filled shield with checkmark cutout
118
+ 'shield-filled': (
119
+ <path
120
+ fillRule="evenodd"
121
+ d="M12 1L3 5v7c0 6.5 8.5 10.5 9 10.73.5-.23 9-4.23 9-10.73V5l-9-4zm3.707 8.707a1 1 0 0 0-1.414-1.414L11 11.586l-1.293-1.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4z"
122
+ />
123
+ ),
124
+ // Filled lock body with stroke shackle
125
+ 'lock-filled': (
126
+ <>
127
+ <path
128
+ d="M7 11V7a5 5 0 0 1 10 0v4"
129
+ fill="none"
130
+ stroke="currentColor"
131
+ strokeWidth="2"
132
+ strokeLinecap="round"
133
+ />
134
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
135
+ </>
136
+ ),
137
+ }
138
+
139
+ interface IconProps {
140
+ name: IconName
141
+ size?: number
142
+ }
143
+
144
+ export function Icon({name, size = 24}: IconProps) {
145
+ const filled = filledIcons[name]
146
+ if (filled) {
147
+ return (
148
+ <svg viewBox="0 0 24 24" fill="currentColor" width={size} height={size}>
149
+ {filled}
150
+ </svg>
151
+ )
152
+ }
153
+
154
+ return (
155
+ <svg
156
+ viewBox="0 0 24 24"
157
+ fill="none"
158
+ stroke="currentColor"
159
+ strokeWidth={2}
160
+ strokeLinecap="round"
161
+ strokeLinejoin="round"
162
+ width={size}
163
+ height={size}
164
+ >
165
+ {strokeIcons[name]}
166
+ </svg>
167
+ )
168
+ }
@@ -0,0 +1,2 @@
1
+ export {Icon} from './Icon'
2
+ export type {IconName} from './Icon'
@@ -0,0 +1,88 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {css, html} from 'react-strict-dom'
3
+ import {Inline} from './Inline'
4
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
5
+ import {spacing} from '@duro-app/tokens/tokens/spacing.css'
6
+
7
+ const meta: Meta<typeof Inline> = {
8
+ title: 'Layout/Inline',
9
+ component: Inline,
10
+ argTypes: {
11
+ gap: {
12
+ control: 'select',
13
+ options: ['xs', 'sm', 'ms', 'md', 'lg', 'xl', 'xxl', 'xxxl'],
14
+ },
15
+ align: {
16
+ control: 'select',
17
+ options: ['start', 'center', 'end', 'baseline', 'stretch'],
18
+ },
19
+ justify: {
20
+ control: 'select',
21
+ options: ['start', 'center', 'end', 'between'],
22
+ },
23
+ },
24
+ }
25
+
26
+ export default meta
27
+ type Story = StoryObj<typeof Inline>
28
+
29
+ const localStyles = css.create({
30
+ box: {
31
+ backgroundColor: colors.accent,
32
+ color: colors.accentContrast,
33
+ padding: spacing.sm,
34
+ borderRadius: 4,
35
+ textAlign: 'center',
36
+ fontSize: '0.875rem',
37
+ },
38
+ iconLg: {
39
+ fontSize: '1.25rem',
40
+ },
41
+ })
42
+
43
+ const Box = ({children}: {children: string}) => (
44
+ <html.div style={localStyles.box}>{children}</html.div>
45
+ )
46
+
47
+ export const Default: Story = {
48
+ args: {
49
+ gap: 'sm',
50
+ align: 'center',
51
+ justify: 'start',
52
+ },
53
+ render: (args) => (
54
+ <Inline {...args}>
55
+ <Box>One</Box>
56
+ <Box>Two</Box>
57
+ <Box>Three</Box>
58
+ </Inline>
59
+ ),
60
+ }
61
+
62
+ export const SpaceBetween: Story = {
63
+ render: () => (
64
+ <Inline gap="md" justify="between">
65
+ <Box>Left</Box>
66
+ <Box>Right</Box>
67
+ </Inline>
68
+ ),
69
+ }
70
+
71
+ export const IconAndText: Story = {
72
+ render: () => (
73
+ <Inline gap="xs" align="center">
74
+ <html.span style={localStyles.iconLg}>*</html.span>
75
+ <html.span>Label text</html.span>
76
+ </Inline>
77
+ ),
78
+ }
79
+
80
+ export const ButtonGroup: Story = {
81
+ render: () => (
82
+ <Inline gap="sm">
83
+ <Box>Save</Box>
84
+ <Box>Cancel</Box>
85
+ <Box>Delete</Box>
86
+ </Inline>
87
+ ),
88
+ }
@@ -0,0 +1,45 @@
1
+ import type {ReactNode} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {styles} from './styles.css'
4
+ import type {SpacingKey} from '../Stack/Stack'
5
+
6
+ interface InlineProps {
7
+ gap?: SpacingKey
8
+ align?: 'start' | 'center' | 'end' | 'baseline' | 'stretch'
9
+ justify?: 'start' | 'center' | 'end' | 'between'
10
+ children: ReactNode
11
+ }
12
+
13
+ const gapMap = {
14
+ xs: styles.gapXs,
15
+ sm: styles.gapSm,
16
+ ms: styles.gapMs,
17
+ md: styles.gapMd,
18
+ lg: styles.gapLg,
19
+ xl: styles.gapXl,
20
+ xxl: styles.gapXxl,
21
+ xxxl: styles.gapXxxl,
22
+ } as const
23
+
24
+ const alignMap = {
25
+ start: styles.alignStart,
26
+ center: styles.alignCenter,
27
+ end: styles.alignEnd,
28
+ baseline: styles.alignBaseline,
29
+ stretch: styles.alignStretch,
30
+ } as const
31
+
32
+ const justifyMap = {
33
+ start: styles.justifyStart,
34
+ center: styles.justifyCenter,
35
+ end: styles.justifyEnd,
36
+ between: styles.justifyBetween,
37
+ } as const
38
+
39
+ export function Inline({gap = 'sm', align = 'center', justify = 'start', children}: InlineProps) {
40
+ return (
41
+ <html.div style={[styles.base, gapMap[gap], alignMap[align], justifyMap[justify]]}>
42
+ {children}
43
+ </html.div>
44
+ )
45
+ }
@@ -0,0 +1,27 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {spacing} from '@duro-app/tokens/tokens/spacing.css'
3
+
4
+ export const styles = css.create({
5
+ base: {
6
+ display: 'flex',
7
+ flexDirection: 'row',
8
+ flexWrap: 'nowrap',
9
+ },
10
+ alignStart: {alignItems: 'flex-start'},
11
+ alignCenter: {alignItems: 'center'},
12
+ alignEnd: {alignItems: 'flex-end'},
13
+ alignBaseline: {alignItems: 'baseline'},
14
+ alignStretch: {alignItems: 'stretch'},
15
+ justifyStart: {justifyContent: 'flex-start'},
16
+ justifyCenter: {justifyContent: 'center'},
17
+ justifyEnd: {justifyContent: 'flex-end'},
18
+ justifyBetween: {justifyContent: 'space-between'},
19
+ gapXs: {gap: spacing.xs},
20
+ gapSm: {gap: spacing.sm},
21
+ gapMs: {gap: spacing.ms},
22
+ gapMd: {gap: spacing.md},
23
+ gapLg: {gap: spacing.lg},
24
+ gapXl: {gap: spacing.xl},
25
+ gapXxl: {gap: spacing.xxl},
26
+ gapXxxl: {gap: spacing.xxxl},
27
+ })
@@ -0,0 +1,89 @@
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 {Input} from './Input'
5
+
6
+ const meta: Meta<typeof Input> = {
7
+ title: 'Components/Input',
8
+ component: Input,
9
+ args: {
10
+ onChange: fn(),
11
+ },
12
+ argTypes: {
13
+ variant: {
14
+ control: 'select',
15
+ options: ['default', 'error'],
16
+ },
17
+ type: {
18
+ control: 'select',
19
+ options: ['text', 'password', 'email', 'number'],
20
+ },
21
+ disabled: {control: 'boolean'},
22
+ },
23
+ }
24
+
25
+ export default meta
26
+ type Story = StoryObj<typeof Input>
27
+
28
+ export const Default: Story = {
29
+ args: {placeholder: 'Enter text...'},
30
+ play: async ({args, canvas, userEvent}) => {
31
+ const input = canvas.getByPlaceholderText('Enter text...')
32
+ await expect(input).toBeInTheDocument()
33
+ await expect(input).toBeEnabled()
34
+
35
+ await userEvent.type(input, 'Hello world')
36
+ await expect(input).toHaveValue('Hello world')
37
+ await expect(args.onChange).toHaveBeenCalled()
38
+ },
39
+ }
40
+
41
+ export const Error: Story = {
42
+ args: {variant: 'error', placeholder: 'Invalid input'},
43
+ play: async ({canvas}) => {
44
+ const input = canvas.getByPlaceholderText('Invalid input')
45
+ await expect(input).toHaveAttribute('aria-invalid', 'true')
46
+ },
47
+ }
48
+
49
+ export const Password: Story = {
50
+ args: {type: 'password', placeholder: 'Enter password'},
51
+ play: async ({canvas, userEvent}) => {
52
+ const input = canvas.getByPlaceholderText('Enter password')
53
+ await expect(input).toHaveAttribute('type', 'password')
54
+
55
+ await userEvent.type(input, 'secret123')
56
+ await expect(input).toHaveValue('secret123')
57
+ },
58
+ }
59
+
60
+ export const Disabled: Story = {
61
+ args: {placeholder: 'Disabled', disabled: true},
62
+ play: async ({canvas}) => {
63
+ const input = canvas.getByPlaceholderText('Disabled')
64
+ await expect(input).toBeDisabled()
65
+ },
66
+ }
67
+
68
+ const stackStyles = css.create({
69
+ stack: {display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 320},
70
+ })
71
+
72
+ export const AllVariants: Story = {
73
+ render: () => (
74
+ <html.div style={stackStyles.stack}>
75
+ <Input placeholder="Default input" />
76
+ <Input variant="error" placeholder="Error input" />
77
+ <Input type="password" placeholder="Password input" />
78
+ <Input placeholder="Disabled" disabled />
79
+ </html.div>
80
+ ),
81
+ play: async ({canvas}) => {
82
+ const inputs = canvas.getAllByRole('textbox')
83
+ // password type is not role=textbox, so we expect 3
84
+ await expect(inputs.length).toBe(3)
85
+
86
+ const disabled = canvas.getByPlaceholderText('Disabled')
87
+ await expect(disabled).toBeDisabled()
88
+ },
89
+ }