@duro-app/ui 0.19.0 → 0.20.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/dist/components/DetailPanel/DetailPanel.d.ts +53 -0
- package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -0
- package/dist/components/DetailPanel/DetailPanel.meta.d.ts +3 -0
- package/dist/components/DetailPanel/DetailPanel.meta.d.ts.map +1 -0
- package/dist/components/DetailPanel/DetailPanel.stories.d.ts +9 -0
- package/dist/components/DetailPanel/DetailPanel.stories.d.ts.map +1 -0
- package/dist/components/DetailPanel/styles.css.d.ts +126 -0
- package/dist/components/DetailPanel/styles.css.d.ts.map +1 -0
- package/dist/components/Dialog/Dialog.meta.d.ts +3 -0
- package/dist/components/Dialog/Dialog.meta.d.ts.map +1 -0
- package/dist/components/Drawer/Drawer.meta.d.ts +3 -0
- package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -0
- package/dist/components/Panel/Panel.d.ts +27 -0
- package/dist/components/Panel/Panel.d.ts.map +1 -0
- package/dist/components/Panel/Panel.meta.d.ts +3 -0
- package/dist/components/Panel/Panel.meta.d.ts.map +1 -0
- package/dist/components/Panel/Panel.stories.d.ts +10 -0
- package/dist/components/Panel/Panel.stories.d.ts.map +1 -0
- package/dist/components/Panel/styles.css.d.ts +50 -0
- package/dist/components/Panel/styles.css.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2417 -2034
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/DetailPanel/DetailPanel.meta.ts +41 -0
- package/src/components/DetailPanel/DetailPanel.stories.tsx +270 -0
- package/src/components/DetailPanel/DetailPanel.tsx +312 -0
- package/src/components/DetailPanel/styles.css.ts +187 -0
- package/src/components/Dialog/Dialog.meta.ts +44 -0
- package/src/components/Drawer/Drawer.meta.ts +45 -0
- package/src/components/Panel/Panel.meta.ts +37 -0
- package/src/components/Panel/Panel.stories.tsx +124 -0
- package/src/components/Panel/Panel.tsx +56 -0
- package/src/components/Panel/styles.css.ts +55 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
5
|
+
|
|
6
|
+
/** Apple-standard ease: aggressive deceleration, feels like "arriving" */
|
|
7
|
+
const APPLE_EASE_OUT = 'cubic-bezier(0.32, 0.72, 0, 1)'
|
|
8
|
+
const APPLE_EASE_IN = 'cubic-bezier(0.72, 0, 0.68, 0.28)'
|
|
9
|
+
|
|
10
|
+
const OPEN_DURATION = '280ms'
|
|
11
|
+
const CLOSE_DURATION = '220ms'
|
|
12
|
+
|
|
13
|
+
export const styles = css.create({
|
|
14
|
+
// --- Outer wrapper: animates width from 0 → target ---
|
|
15
|
+
wrapper: {
|
|
16
|
+
overflow: 'hidden',
|
|
17
|
+
flexShrink: 0,
|
|
18
|
+
position: 'relative',
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// --- Wrapper open/close: SM (360px) ---
|
|
22
|
+
wrapperOpenSm: {
|
|
23
|
+
animationName: css.keyframes({
|
|
24
|
+
from: {width: 0},
|
|
25
|
+
to: {width: 360},
|
|
26
|
+
}),
|
|
27
|
+
animationDuration: OPEN_DURATION,
|
|
28
|
+
animationTimingFunction: APPLE_EASE_OUT,
|
|
29
|
+
animationFillMode: 'both',
|
|
30
|
+
},
|
|
31
|
+
wrapperCloseSm: {
|
|
32
|
+
animationName: css.keyframes({
|
|
33
|
+
from: {width: 360},
|
|
34
|
+
to: {width: 0},
|
|
35
|
+
}),
|
|
36
|
+
animationDuration: CLOSE_DURATION,
|
|
37
|
+
animationTimingFunction: APPLE_EASE_IN,
|
|
38
|
+
animationFillMode: 'both',
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// --- Wrapper open/close: MD (480px) ---
|
|
42
|
+
wrapperOpenMd: {
|
|
43
|
+
animationName: css.keyframes({
|
|
44
|
+
from: {width: 0},
|
|
45
|
+
to: {width: 480},
|
|
46
|
+
}),
|
|
47
|
+
animationDuration: OPEN_DURATION,
|
|
48
|
+
animationTimingFunction: APPLE_EASE_OUT,
|
|
49
|
+
animationFillMode: 'both',
|
|
50
|
+
},
|
|
51
|
+
wrapperCloseMd: {
|
|
52
|
+
animationName: css.keyframes({
|
|
53
|
+
from: {width: 480},
|
|
54
|
+
to: {width: 0},
|
|
55
|
+
}),
|
|
56
|
+
animationDuration: CLOSE_DURATION,
|
|
57
|
+
animationTimingFunction: APPLE_EASE_IN,
|
|
58
|
+
animationFillMode: 'both',
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// --- Content panel ---
|
|
62
|
+
content: {
|
|
63
|
+
height: '100%',
|
|
64
|
+
display: 'flex',
|
|
65
|
+
flexDirection: 'column',
|
|
66
|
+
backgroundColor: colors.bgCard,
|
|
67
|
+
borderLeftWidth: 1,
|
|
68
|
+
borderLeftStyle: 'solid',
|
|
69
|
+
borderLeftColor: colors.border,
|
|
70
|
+
boxShadow: shadows.sm,
|
|
71
|
+
position: 'relative',
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Content must maintain its target width even while wrapper is narrower
|
|
75
|
+
contentSm: {width: 360},
|
|
76
|
+
contentMd: {width: 480},
|
|
77
|
+
|
|
78
|
+
// --- Content slide + fade animations ---
|
|
79
|
+
slideIn: {
|
|
80
|
+
animationName: css.keyframes({
|
|
81
|
+
from: {
|
|
82
|
+
transform: 'translateX(40px)',
|
|
83
|
+
opacity: 0,
|
|
84
|
+
},
|
|
85
|
+
to: {
|
|
86
|
+
transform: 'translateX(0)',
|
|
87
|
+
opacity: 1,
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
animationDuration: OPEN_DURATION,
|
|
91
|
+
animationTimingFunction: APPLE_EASE_OUT,
|
|
92
|
+
animationFillMode: 'both',
|
|
93
|
+
},
|
|
94
|
+
slideOut: {
|
|
95
|
+
animationName: css.keyframes({
|
|
96
|
+
from: {
|
|
97
|
+
transform: 'translateX(0)',
|
|
98
|
+
opacity: 1,
|
|
99
|
+
},
|
|
100
|
+
to: {
|
|
101
|
+
transform: 'translateX(40px)',
|
|
102
|
+
opacity: 0,
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
animationDuration: CLOSE_DURATION,
|
|
106
|
+
animationTimingFunction: APPLE_EASE_IN,
|
|
107
|
+
animationFillMode: 'both',
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// --- Header ---
|
|
111
|
+
header: {
|
|
112
|
+
display: 'flex',
|
|
113
|
+
alignItems: 'center',
|
|
114
|
+
justifyContent: 'space-between',
|
|
115
|
+
paddingTop: spacing.lg,
|
|
116
|
+
paddingLeft: spacing.lg,
|
|
117
|
+
paddingRight: spacing.lg,
|
|
118
|
+
paddingBottom: spacing.lg,
|
|
119
|
+
gap: spacing.md,
|
|
120
|
+
borderBottomWidth: 1,
|
|
121
|
+
borderBottomStyle: 'solid',
|
|
122
|
+
borderBottomColor: colors.border,
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// --- Title ---
|
|
126
|
+
title: {
|
|
127
|
+
fontSize: 18,
|
|
128
|
+
fontWeight: 600,
|
|
129
|
+
lineHeight: 1.4,
|
|
130
|
+
color: colors.text,
|
|
131
|
+
margin: 0,
|
|
132
|
+
flex: 1,
|
|
133
|
+
minWidth: 0,
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// --- Body ---
|
|
137
|
+
body: {
|
|
138
|
+
flex: 1,
|
|
139
|
+
overflowY: 'auto',
|
|
140
|
+
},
|
|
141
|
+
bodyPadded: {
|
|
142
|
+
paddingTop: spacing.lg,
|
|
143
|
+
paddingLeft: spacing.lg,
|
|
144
|
+
paddingRight: spacing.lg,
|
|
145
|
+
paddingBottom: spacing.lg,
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// --- Footer ---
|
|
149
|
+
footer: {
|
|
150
|
+
display: 'flex',
|
|
151
|
+
alignItems: 'center',
|
|
152
|
+
justifyContent: 'flex-end',
|
|
153
|
+
gap: spacing.sm,
|
|
154
|
+
paddingTop: spacing.md,
|
|
155
|
+
paddingBottom: spacing.md,
|
|
156
|
+
paddingLeft: spacing.lg,
|
|
157
|
+
paddingRight: spacing.lg,
|
|
158
|
+
borderTopWidth: 1,
|
|
159
|
+
borderTopStyle: 'solid',
|
|
160
|
+
borderTopColor: colors.border,
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// --- Close button ---
|
|
164
|
+
closeButton: {
|
|
165
|
+
display: 'flex',
|
|
166
|
+
alignItems: 'center',
|
|
167
|
+
justifyContent: 'center',
|
|
168
|
+
width: 32,
|
|
169
|
+
height: 32,
|
|
170
|
+
borderRadius: 8,
|
|
171
|
+
borderWidth: 0,
|
|
172
|
+
backgroundColor: {
|
|
173
|
+
default: 'transparent',
|
|
174
|
+
':hover': colors.bgCardHover,
|
|
175
|
+
},
|
|
176
|
+
color: colors.textMuted,
|
|
177
|
+
cursor: 'pointer',
|
|
178
|
+
padding: 0,
|
|
179
|
+
flexShrink: 0,
|
|
180
|
+
transitionProperty: 'background-color',
|
|
181
|
+
transitionDuration: '150ms',
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
inlineWrapper: {
|
|
185
|
+
display: 'inline-flex',
|
|
186
|
+
},
|
|
187
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Modal dialog with backdrop overlay. Centered on screen with scale+fade animation. Traps focus, dismisses on backdrop click or Escape. Use for confirmations, forms, or content that requires user attention before continuing.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Confirmation prompts (delete, revoke, destructive actions)',
|
|
8
|
+
'Short forms or data entry that blocks the main flow',
|
|
9
|
+
'Viewing detail content in a focused overlay (e.g. certificate details)',
|
|
10
|
+
'Any modal interaction that requires user acknowledgment',
|
|
11
|
+
],
|
|
12
|
+
whenNotToUse: [
|
|
13
|
+
'Non-modal side panels — use DetailPanel',
|
|
14
|
+
'Sliding panels from screen edges — use Drawer',
|
|
15
|
+
'Inline content expansion — use disclosure or accordion patterns',
|
|
16
|
+
'Toast/notification messages — use Alert or a toast system',
|
|
17
|
+
],
|
|
18
|
+
anatomy: {
|
|
19
|
+
required: ['Root', 'Portal'],
|
|
20
|
+
optional: ['Trigger', 'Header', 'Title', 'Description', 'Body', 'Footer', 'Close'],
|
|
21
|
+
},
|
|
22
|
+
relatedTo: [
|
|
23
|
+
{component: 'Drawer', relationship: 'Drawer slides from an edge; Dialog is centered with scale animation'},
|
|
24
|
+
{component: 'DetailPanel', relationship: 'DetailPanel is non-modal and in-flow; Dialog is modal with backdrop'},
|
|
25
|
+
],
|
|
26
|
+
example: `<Dialog.Root open={open} onOpenChange={setOpen}>
|
|
27
|
+
<Dialog.Trigger>
|
|
28
|
+
<Button>Open dialog</Button>
|
|
29
|
+
</Dialog.Trigger>
|
|
30
|
+
<Dialog.Portal size="md">
|
|
31
|
+
<Dialog.Header>
|
|
32
|
+
<Dialog.Title>Confirm action</Dialog.Title>
|
|
33
|
+
<Dialog.Close />
|
|
34
|
+
</Dialog.Header>
|
|
35
|
+
<Dialog.Body>
|
|
36
|
+
<Text>Are you sure you want to proceed?</Text>
|
|
37
|
+
</Dialog.Body>
|
|
38
|
+
<Dialog.Footer>
|
|
39
|
+
<Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
|
|
40
|
+
<Button variant="danger" onClick={handleConfirm}>Confirm</Button>
|
|
41
|
+
</Dialog.Footer>
|
|
42
|
+
</Dialog.Portal>
|
|
43
|
+
</Dialog.Root>`,
|
|
44
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Modal sliding panel from a screen edge (right, left, or bottom). Slides in with backdrop overlay. Supports swipe-to-dismiss gestures on mobile, backdrop click dismiss, and Escape key. Use for secondary workflows, settings panels, or mobile-friendly detail views.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Settings or configuration panels that slide in from the side',
|
|
8
|
+
'Mobile-friendly detail views (bottom sheet pattern)',
|
|
9
|
+
'Secondary workflows that need more space than a dialog',
|
|
10
|
+
'Filters, navigation drawers, or form panels',
|
|
11
|
+
],
|
|
12
|
+
whenNotToUse: [
|
|
13
|
+
'Centered modal confirmations — use Dialog',
|
|
14
|
+
'Non-modal side inspection panels — use DetailPanel',
|
|
15
|
+
'Inline content expansion — use disclosure patterns',
|
|
16
|
+
'Primary navigation — use SideNav',
|
|
17
|
+
],
|
|
18
|
+
anatomy: {
|
|
19
|
+
required: ['Root', 'Portal'],
|
|
20
|
+
optional: ['Trigger', 'Header', 'Title', 'Description', 'Body', 'Footer', 'Close'],
|
|
21
|
+
},
|
|
22
|
+
relatedTo: [
|
|
23
|
+
{component: 'Dialog', relationship: 'Dialog is centered modal; Drawer slides from an edge'},
|
|
24
|
+
{component: 'DetailPanel', relationship: 'DetailPanel is non-modal and in-flow; Drawer is modal with backdrop'},
|
|
25
|
+
],
|
|
26
|
+
example: `<Drawer.Root anchor="right" open={open} onOpenChange={setOpen}>
|
|
27
|
+
<Drawer.Trigger>
|
|
28
|
+
<Button>Open drawer</Button>
|
|
29
|
+
</Drawer.Trigger>
|
|
30
|
+
<Drawer.Portal size="md">
|
|
31
|
+
<Drawer.Header>
|
|
32
|
+
<Drawer.Title>Settings</Drawer.Title>
|
|
33
|
+
<Drawer.Close />
|
|
34
|
+
</Drawer.Header>
|
|
35
|
+
<Drawer.Body>
|
|
36
|
+
<Stack gap="md">
|
|
37
|
+
<Text>Drawer content goes here.</Text>
|
|
38
|
+
</Stack>
|
|
39
|
+
</Drawer.Body>
|
|
40
|
+
<Drawer.Footer>
|
|
41
|
+
<Button variant="secondary" onClick={() => setOpen(false)}>Done</Button>
|
|
42
|
+
</Drawer.Footer>
|
|
43
|
+
</Drawer.Portal>
|
|
44
|
+
</Drawer.Root>`,
|
|
45
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Structural primitive for grouping content with header, body, and footer slots. A "workspace surface" for forms, settings sections, and inspectors. Sub-components are purely structural — no context required.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Organizing content with distinct header/body/footer sections',
|
|
8
|
+
'Settings panels, form containers, inspector views',
|
|
9
|
+
'When you need more structure than Card but less than a full page layout',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Repeating item cards — use Card',
|
|
13
|
+
'Modal/overlay content — use Dialog or Drawer',
|
|
14
|
+
'Page-level layout — use PageShell',
|
|
15
|
+
],
|
|
16
|
+
anatomy: {
|
|
17
|
+
required: [],
|
|
18
|
+
optional: ['Root', 'Header', 'Body', 'Footer'],
|
|
19
|
+
},
|
|
20
|
+
relatedTo: [
|
|
21
|
+
{component: 'Card', relationship: 'Card is for shallow, repeated items; Panel is for deep, organized content'},
|
|
22
|
+
{component: 'DetailPanel', relationship: 'DetailPanel uses Panel internally for its structural layout'},
|
|
23
|
+
{component: 'PageShell', relationship: 'PageShell is for page-level layout; Panel is for section-level'},
|
|
24
|
+
],
|
|
25
|
+
example: `<Panel.Root bordered>
|
|
26
|
+
<Panel.Header>
|
|
27
|
+
<Heading level={3}>Section Title</Heading>
|
|
28
|
+
<Button variant="secondary" size="small">Action</Button>
|
|
29
|
+
</Panel.Header>
|
|
30
|
+
<Panel.Body>
|
|
31
|
+
{/* Content */}
|
|
32
|
+
</Panel.Body>
|
|
33
|
+
<Panel.Footer>
|
|
34
|
+
<Button>Save</Button>
|
|
35
|
+
</Panel.Footer>
|
|
36
|
+
</Panel.Root>`,
|
|
37
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {Panel} from './Panel'
|
|
3
|
+
import {Button} from '../Button/Button'
|
|
4
|
+
import {Text} from '../Text/Text'
|
|
5
|
+
import {Heading} from '../Heading/Heading'
|
|
6
|
+
import {Stack} from '../Stack/Stack'
|
|
7
|
+
import {Inline} from '../Inline/Inline'
|
|
8
|
+
import {Field} from '../Field/Field'
|
|
9
|
+
import {Input} from '../Input/Input'
|
|
10
|
+
|
|
11
|
+
const meta: Meta = {
|
|
12
|
+
title: 'Components/Panel',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
type Story = StoryObj
|
|
17
|
+
|
|
18
|
+
// --- Default ---
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<Panel.Root>
|
|
23
|
+
<Panel.Header>
|
|
24
|
+
<Heading level={3}>Section Title</Heading>
|
|
25
|
+
</Panel.Header>
|
|
26
|
+
<Panel.Body>
|
|
27
|
+
<Text>
|
|
28
|
+
Panel is a structural primitive for grouping content with
|
|
29
|
+
header, body, and footer slots. It fills the gap between Card
|
|
30
|
+
and page-level layout.
|
|
31
|
+
</Text>
|
|
32
|
+
</Panel.Body>
|
|
33
|
+
<Panel.Footer>
|
|
34
|
+
<Button variant="secondary">Cancel</Button>
|
|
35
|
+
<Button>Save</Button>
|
|
36
|
+
</Panel.Footer>
|
|
37
|
+
</Panel.Root>
|
|
38
|
+
),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Bordered ---
|
|
42
|
+
|
|
43
|
+
export const Bordered: Story = {
|
|
44
|
+
render: () => (
|
|
45
|
+
<Panel.Root bordered>
|
|
46
|
+
<Panel.Header>
|
|
47
|
+
<Heading level={3}>Bordered Panel</Heading>
|
|
48
|
+
<Button variant="secondary" size="small">Action</Button>
|
|
49
|
+
</Panel.Header>
|
|
50
|
+
<Panel.Body>
|
|
51
|
+
<Text>
|
|
52
|
+
The bordered variant adds an outer border, useful when the panel
|
|
53
|
+
needs visual separation from its surroundings.
|
|
54
|
+
</Text>
|
|
55
|
+
</Panel.Body>
|
|
56
|
+
<Panel.Footer>
|
|
57
|
+
<Button>Confirm</Button>
|
|
58
|
+
</Panel.Footer>
|
|
59
|
+
</Panel.Root>
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Body Only ---
|
|
64
|
+
|
|
65
|
+
export const BodyOnly: Story = {
|
|
66
|
+
render: () => (
|
|
67
|
+
<Panel.Root bordered>
|
|
68
|
+
<Panel.Body>
|
|
69
|
+
<Text>
|
|
70
|
+
Panel sub-components can be used independently. This panel only
|
|
71
|
+
has a Body — no Header or Footer.
|
|
72
|
+
</Text>
|
|
73
|
+
</Panel.Body>
|
|
74
|
+
</Panel.Root>
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Unpadded Body ---
|
|
79
|
+
|
|
80
|
+
export const UnpaddedBody: Story = {
|
|
81
|
+
render: () => (
|
|
82
|
+
<Panel.Root bordered>
|
|
83
|
+
<Panel.Header>
|
|
84
|
+
<Heading level={3}>Full-bleed Content</Heading>
|
|
85
|
+
</Panel.Header>
|
|
86
|
+
<Panel.Body padded={false}>
|
|
87
|
+
<Text>
|
|
88
|
+
Setting padded=false removes body padding, useful for content
|
|
89
|
+
that needs to extend edge-to-edge (tables, images, etc).
|
|
90
|
+
</Text>
|
|
91
|
+
</Panel.Body>
|
|
92
|
+
</Panel.Root>
|
|
93
|
+
),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- With Form ---
|
|
97
|
+
|
|
98
|
+
export const WithForm: Story = {
|
|
99
|
+
render: () => (
|
|
100
|
+
<Panel.Root bordered>
|
|
101
|
+
<Panel.Header>
|
|
102
|
+
<Heading level={3}>Profile Settings</Heading>
|
|
103
|
+
</Panel.Header>
|
|
104
|
+
<Panel.Body>
|
|
105
|
+
<Stack gap="md">
|
|
106
|
+
<Field.Root>
|
|
107
|
+
<Field.Label>Display name</Field.Label>
|
|
108
|
+
<Input placeholder="Enter your name" />
|
|
109
|
+
</Field.Root>
|
|
110
|
+
<Field.Root>
|
|
111
|
+
<Field.Label>Email</Field.Label>
|
|
112
|
+
<Input type="email" placeholder="you@example.com" />
|
|
113
|
+
</Field.Root>
|
|
114
|
+
</Stack>
|
|
115
|
+
</Panel.Body>
|
|
116
|
+
<Panel.Footer>
|
|
117
|
+
<Inline gap="sm">
|
|
118
|
+
<Button variant="secondary">Cancel</Button>
|
|
119
|
+
<Button>Save changes</Button>
|
|
120
|
+
</Inline>
|
|
121
|
+
</Panel.Footer>
|
|
122
|
+
</Panel.Root>
|
|
123
|
+
),
|
|
124
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {styles} from './styles.css'
|
|
4
|
+
|
|
5
|
+
// --- Root ---
|
|
6
|
+
|
|
7
|
+
interface RootProps {
|
|
8
|
+
children: ReactNode
|
|
9
|
+
/** Adds an outer border. Default: false */
|
|
10
|
+
bordered?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function Root({children, bordered = false}: RootProps) {
|
|
14
|
+
return (
|
|
15
|
+
<html.div style={[styles.root, bordered && styles.bordered]}>
|
|
16
|
+
{children}
|
|
17
|
+
</html.div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// --- Header ---
|
|
22
|
+
|
|
23
|
+
function Header({children}: {children: ReactNode}) {
|
|
24
|
+
return <html.div style={styles.header}>{children}</html.div>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Body ---
|
|
28
|
+
|
|
29
|
+
interface BodyProps {
|
|
30
|
+
children: ReactNode
|
|
31
|
+
/** Adds inner padding. Default: true */
|
|
32
|
+
padded?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Body({children, padded = true}: BodyProps) {
|
|
36
|
+
return (
|
|
37
|
+
<html.div style={[styles.body, padded && styles.bodyPadded]}>
|
|
38
|
+
{children}
|
|
39
|
+
</html.div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Footer ---
|
|
44
|
+
|
|
45
|
+
function Footer({children}: {children: ReactNode}) {
|
|
46
|
+
return <html.div style={styles.footer}>{children}</html.div>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Export ---
|
|
50
|
+
|
|
51
|
+
export const Panel = {
|
|
52
|
+
Root,
|
|
53
|
+
Header,
|
|
54
|
+
Body,
|
|
55
|
+
Footer,
|
|
56
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
|
|
5
|
+
export const styles = css.create({
|
|
6
|
+
root: {
|
|
7
|
+
display: 'flex',
|
|
8
|
+
flexDirection: 'column',
|
|
9
|
+
backgroundColor: colors.bgCard,
|
|
10
|
+
},
|
|
11
|
+
bordered: {
|
|
12
|
+
borderWidth: 1,
|
|
13
|
+
borderStyle: 'solid',
|
|
14
|
+
borderColor: colors.border,
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
header: {
|
|
18
|
+
display: 'flex',
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
justifyContent: 'space-between',
|
|
21
|
+
paddingTop: spacing.lg,
|
|
22
|
+
paddingLeft: spacing.lg,
|
|
23
|
+
paddingRight: spacing.lg,
|
|
24
|
+
paddingBottom: spacing.lg,
|
|
25
|
+
gap: spacing.md,
|
|
26
|
+
borderBottomWidth: 1,
|
|
27
|
+
borderBottomStyle: 'solid',
|
|
28
|
+
borderBottomColor: colors.border,
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
body: {
|
|
32
|
+
flex: 1,
|
|
33
|
+
overflowY: 'auto',
|
|
34
|
+
},
|
|
35
|
+
bodyPadded: {
|
|
36
|
+
paddingTop: spacing.lg,
|
|
37
|
+
paddingLeft: spacing.lg,
|
|
38
|
+
paddingRight: spacing.lg,
|
|
39
|
+
paddingBottom: spacing.lg,
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
footer: {
|
|
43
|
+
display: 'flex',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
justifyContent: 'flex-end',
|
|
46
|
+
gap: spacing.sm,
|
|
47
|
+
paddingTop: spacing.md,
|
|
48
|
+
paddingBottom: spacing.md,
|
|
49
|
+
paddingLeft: spacing.lg,
|
|
50
|
+
paddingRight: spacing.lg,
|
|
51
|
+
borderTopWidth: 1,
|
|
52
|
+
borderTopStyle: 'solid',
|
|
53
|
+
borderTopColor: colors.border,
|
|
54
|
+
},
|
|
55
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export {Callout, type CalloutVariant} from './components/Callout/Callout'
|
|
|
12
12
|
export {Card, type CardVariant, type CardSize} from './components/Card/Card'
|
|
13
13
|
export {Checkbox} from './components/Checkbox/Checkbox'
|
|
14
14
|
export {Dialog, type DialogSize} from './components/Dialog/Dialog'
|
|
15
|
+
export {DetailPanel, type DetailPanelSize} from './components/DetailPanel/DetailPanel'
|
|
15
16
|
export {Drawer, type DrawerAnchor, type DrawerSize} from './components/Drawer/Drawer'
|
|
16
17
|
export {EmptyState} from './components/EmptyState/EmptyState'
|
|
17
18
|
export {Heading, type HeadingVariant} from './components/Heading/Heading'
|
|
@@ -26,6 +27,7 @@ export {
|
|
|
26
27
|
type LinkButtonSize,
|
|
27
28
|
} from './components/LinkButton/LinkButton'
|
|
28
29
|
export {Menu} from './components/Menu/Menu'
|
|
30
|
+
export {Panel} from './components/Panel/Panel'
|
|
29
31
|
export {ScrollArea} from './components/ScrollArea/ScrollArea'
|
|
30
32
|
export {Select} from './components/Select/Select'
|
|
31
33
|
export {SideNav} from './components/SideNav/SideNav'
|