paris 0.17.6 → 0.17.8
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/CHANGELOG.md +21 -0
- package/package.json +2 -1
- package/src/stories/accordionselect/AccordionSelect.module.scss +107 -0
- package/src/stories/accordionselect/AccordionSelect.stories.ts +41 -0
- package/src/stories/accordionselect/AccordionSelect.tsx +268 -0
- package/src/stories/accordionselect/index.ts +1 -0
- package/src/stories/checkbox/Checkbox.module.scss +3 -3
- package/src/stories/combobox/Combobox.stories.ts +41 -0
- package/src/stories/combobox/Combobox.tsx +3 -2
- package/src/stories/dialog/Dialog.tsx +16 -5
- package/src/stories/drawer/Drawer.module.scss +9 -0
- package/src/stories/drawer/Drawer.stories.tsx +61 -0
- package/src/stories/drawer/Drawer.tsx +10 -3
- package/src/stories/field/Field.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# paris
|
|
2
2
|
|
|
3
|
+
## 0.17.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0ca30b7: fix(drawer): make bottom panel spacer padding conditional on bottomPanelPadding prop
|
|
8
|
+
fix(accordionselect): add padding to check icon for better tap target
|
|
9
|
+
feat(combobox): add customValueOption override for independent styling
|
|
10
|
+
|
|
11
|
+
## 0.17.7
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 188ed3e: AccordionSelect: new component for expandable selection groups
|
|
16
|
+
CardButton: added `kind` variants (`raised`, `surface`, `flat`) and `status` prop; removed SelectableCard
|
|
17
|
+
Drawer: added `bottomPanelPadding` prop for edge-to-edge footer layouts
|
|
18
|
+
- 188ed3e: Checkbox: removed background hover from `panel` variant
|
|
19
|
+
- 188ed3e: Drawer: removed padding from `bottomPanel` so footer elements can stretch edge-to-edge
|
|
20
|
+
Combobox: fixed stale options showing on re-focus by resetting query state on blur
|
|
21
|
+
Field: custom component labels now receive consistent styling with string labels
|
|
22
|
+
Dialog: `width` prop now accepts custom CSS lengths in addition to presets
|
|
23
|
+
|
|
3
24
|
## 0.17.6
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "paris",
|
|
3
3
|
"author": "Sanil Chawla <sanil@slingshot.fm> (https://sanil.co)",
|
|
4
4
|
"description": "Paris is Slingshot's React design system. It's a collection of reusable components, design tokens, and guidelines that help us build consistent, accessible, and performant user interfaces.",
|
|
5
|
-
"version": "0.17.
|
|
5
|
+
"version": "0.17.8",
|
|
6
6
|
"homepage": "https://paris.slingshot.fm",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"exports": {
|
|
34
34
|
"./*": "./src/stories/*",
|
|
35
35
|
"./accordion": "./src/stories/accordion/index.ts",
|
|
36
|
+
"./accordionselect": "./src/stories/accordionselect/index.ts",
|
|
36
37
|
"./avatar": "./src/stories/avatar/index.ts",
|
|
37
38
|
"./button": "./src/stories/button/index.ts",
|
|
38
39
|
"./callout": "./src/stories/callout/index.ts",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
color: var(--pte-new-colors-contentPrimary);
|
|
3
|
+
border: 1px solid var(--pte-new-colors-borderStrong);
|
|
4
|
+
border-radius: var(--pte-new-borders-radius-roundedMedium);
|
|
5
|
+
background: var(--pte-new-colors-surfacePrimary);
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
transition: var(--pte-animations-interaction);
|
|
8
|
+
|
|
9
|
+
&.open {
|
|
10
|
+
border-color: var(--pte-new-colors-borderUltrastrong);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.header {
|
|
15
|
+
padding: 10px 12px;
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 10px;
|
|
20
|
+
background-color: var(--pte-new-colors-overlayWhiteSubtle);
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
transition: var(--pte-animations-interaction);
|
|
23
|
+
|
|
24
|
+
&.open {
|
|
25
|
+
background-color: var(--pte-new-colors-overlayMedium);
|
|
26
|
+
border-bottom: 1px solid var(--pte-new-colors-borderStrong);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&:hover {
|
|
30
|
+
background-color: var(--pte-new-colors-overlayStrong);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.headerContent {
|
|
35
|
+
flex: 1;
|
|
36
|
+
min-width: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.headerEnd {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.chevron {
|
|
47
|
+
transition: transform var(--pte-animations-duration-gradual) var(--pte-animations-timing-easeInOutExpo);
|
|
48
|
+
transform: rotate(90deg);
|
|
49
|
+
|
|
50
|
+
&.open {
|
|
51
|
+
transform: rotate(-90deg);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.dropdown {
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.dropdownContent {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
padding: 0;
|
|
63
|
+
gap: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.option {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 10px;
|
|
70
|
+
padding: 8px 14px;
|
|
71
|
+
border-bottom: 0.5px solid var(--pte-new-colors-borderMedium);
|
|
72
|
+
background: transparent;
|
|
73
|
+
color: inherit;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
transition: background var(--pte-animations-duration-instant) var(--pte-animations-timing-easeInOutExpo);
|
|
76
|
+
|
|
77
|
+
&:last-of-type {
|
|
78
|
+
border-bottom: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
&[data-selected='true'] {
|
|
82
|
+
background: var(--pte-new-colors-overlaySubtle);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
&:hover {
|
|
86
|
+
background: var(--pte-new-colors-overlayMedium);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&:disabled {
|
|
90
|
+
opacity: 0.4;
|
|
91
|
+
cursor: not-allowed;
|
|
92
|
+
|
|
93
|
+
&:hover {
|
|
94
|
+
background: transparent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.check {
|
|
99
|
+
padding: 4px;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.optionContent {
|
|
104
|
+
flex: 1;
|
|
105
|
+
text-align: left;
|
|
106
|
+
min-width: 0;
|
|
107
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { AccordionSelect } from './AccordionSelect';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof AccordionSelect> = {
|
|
5
|
+
title: 'Inputs/AccordionSelect',
|
|
6
|
+
component: AccordionSelect,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof AccordionSelect>;
|
|
12
|
+
|
|
13
|
+
const options = [
|
|
14
|
+
{ id: 'champagne', node: 'In an alleyway, drinking champagne' },
|
|
15
|
+
{ id: 'rooftop', node: 'On a rooftop, watching the sunset' },
|
|
16
|
+
{ id: 'garden', node: 'In a garden, under the stars' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
options,
|
|
22
|
+
value: 'champagne',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const NoSelection: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
options,
|
|
29
|
+
placeholder: 'Where were we?',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const WithDisabledOption: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
options: [
|
|
36
|
+
...options,
|
|
37
|
+
{ id: 'nowhere', node: 'Nowhere, it was all a dream', disabled: true },
|
|
38
|
+
],
|
|
39
|
+
value: 'champagne',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentPropsWithoutRef, FC, ReactNode } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
useCallback, useEffect, useRef, useState,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
8
|
+
import { clsx } from 'clsx';
|
|
9
|
+
import { Check, ChevronRight, Icon } from '../icon';
|
|
10
|
+
import { TextWhenString } from '../utility';
|
|
11
|
+
import styles from './AccordionSelect.module.scss';
|
|
12
|
+
|
|
13
|
+
export type AccordionSelectOption<T = Record<string, unknown>> = {
|
|
14
|
+
/**
|
|
15
|
+
* A unique identifier for the option.
|
|
16
|
+
*/
|
|
17
|
+
id: string;
|
|
18
|
+
/**
|
|
19
|
+
* The content to render for this option.
|
|
20
|
+
*/
|
|
21
|
+
node: ReactNode;
|
|
22
|
+
/**
|
|
23
|
+
* Whether this option is disabled.
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Optional metadata associated with the option.
|
|
29
|
+
*/
|
|
30
|
+
metadata?: T;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type AccordionSelectProps<T = Record<string, unknown>> = {
|
|
34
|
+
/**
|
|
35
|
+
* The list of selectable options.
|
|
36
|
+
*/
|
|
37
|
+
options: AccordionSelectOption<T>[];
|
|
38
|
+
/**
|
|
39
|
+
* The currently selected option ID.
|
|
40
|
+
*/
|
|
41
|
+
value?: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Called when the user selects an option.
|
|
44
|
+
*/
|
|
45
|
+
onChange?: (option: AccordionSelectOption<T>) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Custom content to render as the header when an option is selected.
|
|
48
|
+
* Receives the selected option. If not provided, the option's `node` is used.
|
|
49
|
+
*/
|
|
50
|
+
renderSelected?: (option: AccordionSelectOption<T>) => ReactNode;
|
|
51
|
+
/**
|
|
52
|
+
* Custom content to render for each option in the dropdown.
|
|
53
|
+
* Receives the option and whether it's selected. If not provided, the option's `node` is used.
|
|
54
|
+
*/
|
|
55
|
+
renderOption?: (option: AccordionSelectOption<T>, isSelected: boolean) => ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* Placeholder to show when no option is selected.
|
|
58
|
+
* @default 'Select an option'
|
|
59
|
+
*/
|
|
60
|
+
placeholder?: ReactNode;
|
|
61
|
+
/**
|
|
62
|
+
* Whether the accordion is open. If provided, the component becomes controlled.
|
|
63
|
+
*/
|
|
64
|
+
isOpen?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Called when the open state changes.
|
|
67
|
+
*/
|
|
68
|
+
onOpenChange?: (open: boolean) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Whether to close the accordion when an option is selected.
|
|
71
|
+
* @default true
|
|
72
|
+
*/
|
|
73
|
+
closeOnSelect?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Whether to close the accordion when clicking outside.
|
|
76
|
+
* @default true
|
|
77
|
+
*/
|
|
78
|
+
closeOnClickOutside?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Optional action to render at the bottom of the options list (e.g. "Add new" button).
|
|
81
|
+
*/
|
|
82
|
+
action?: ReactNode;
|
|
83
|
+
/**
|
|
84
|
+
* Optional label to display on the header (e.g. a Tag).
|
|
85
|
+
*/
|
|
86
|
+
label?: ReactNode;
|
|
87
|
+
/**
|
|
88
|
+
* Optional overrides for nested components.
|
|
89
|
+
*/
|
|
90
|
+
overrides?: {
|
|
91
|
+
root?: ComponentPropsWithoutRef<'div'>;
|
|
92
|
+
header?: ComponentPropsWithoutRef<'div'>;
|
|
93
|
+
dropdown?: ComponentPropsWithoutRef<'div'>;
|
|
94
|
+
dropdownContent?: ComponentPropsWithoutRef<'div'>;
|
|
95
|
+
option?: ComponentPropsWithoutRef<'button'>;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* An AccordionSelect component. Displays the selected option in a card header that expands to reveal all options.
|
|
101
|
+
*
|
|
102
|
+
* <hr />
|
|
103
|
+
*
|
|
104
|
+
* To use this component, import it as follows:
|
|
105
|
+
*
|
|
106
|
+
* ```js
|
|
107
|
+
* import { AccordionSelect } from 'paris/accordionselect';
|
|
108
|
+
* ```
|
|
109
|
+
* @constructor
|
|
110
|
+
*/
|
|
111
|
+
export const AccordionSelect: FC<AccordionSelectProps> = ({
|
|
112
|
+
options,
|
|
113
|
+
value,
|
|
114
|
+
onChange,
|
|
115
|
+
renderSelected,
|
|
116
|
+
renderOption,
|
|
117
|
+
placeholder = 'Select an option',
|
|
118
|
+
isOpen: controlledOpen,
|
|
119
|
+
onOpenChange,
|
|
120
|
+
closeOnSelect = true,
|
|
121
|
+
closeOnClickOutside = true,
|
|
122
|
+
action,
|
|
123
|
+
label,
|
|
124
|
+
overrides,
|
|
125
|
+
}) => {
|
|
126
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
127
|
+
const open = controlledOpen ?? internalOpen;
|
|
128
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
129
|
+
|
|
130
|
+
const setOpen = useCallback((nextOpen: boolean) => {
|
|
131
|
+
setInternalOpen(nextOpen);
|
|
132
|
+
onOpenChange?.(nextOpen);
|
|
133
|
+
}, [onOpenChange]);
|
|
134
|
+
|
|
135
|
+
const selectedOption = options.find((o) => o.id === value);
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!closeOnClickOutside || !open) {
|
|
139
|
+
return () => { };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
143
|
+
if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
|
|
144
|
+
setOpen(false);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
149
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
150
|
+
}, [closeOnClickOutside, open, setOpen]);
|
|
151
|
+
|
|
152
|
+
let headerContent: ReactNode = placeholder;
|
|
153
|
+
if (selectedOption) {
|
|
154
|
+
headerContent = renderSelected
|
|
155
|
+
? renderSelected(selectedOption)
|
|
156
|
+
: selectedOption.node;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
ref={rootRef}
|
|
162
|
+
{...overrides?.root}
|
|
163
|
+
className={clsx(
|
|
164
|
+
styles.root,
|
|
165
|
+
open && styles.open,
|
|
166
|
+
overrides?.root?.className,
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
169
|
+
<div
|
|
170
|
+
{...overrides?.header}
|
|
171
|
+
className={clsx(
|
|
172
|
+
styles.header,
|
|
173
|
+
open && styles.open,
|
|
174
|
+
overrides?.header?.className,
|
|
175
|
+
)}
|
|
176
|
+
onClick={() => setOpen(!open)}
|
|
177
|
+
onKeyDown={(e) => {
|
|
178
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
setOpen(!open);
|
|
181
|
+
}
|
|
182
|
+
}}
|
|
183
|
+
role="button"
|
|
184
|
+
tabIndex={0}
|
|
185
|
+
>
|
|
186
|
+
<div className={styles.headerContent}>
|
|
187
|
+
<TextWhenString kind="paragraphSmall" weight="medium">
|
|
188
|
+
{headerContent}
|
|
189
|
+
</TextWhenString>
|
|
190
|
+
</div>
|
|
191
|
+
<div className={styles.headerEnd}>
|
|
192
|
+
{label}
|
|
193
|
+
<Icon
|
|
194
|
+
icon={ChevronRight}
|
|
195
|
+
size={16}
|
|
196
|
+
className={clsx(styles.chevron, open && styles.open)}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<AnimatePresence>
|
|
202
|
+
{open && (
|
|
203
|
+
<motion.div
|
|
204
|
+
key="content"
|
|
205
|
+
initial="collapsed"
|
|
206
|
+
animate="open"
|
|
207
|
+
exit="collapsed"
|
|
208
|
+
variants={{
|
|
209
|
+
open: { opacity: 1, height: 'auto' },
|
|
210
|
+
collapsed: { opacity: 0, height: 0 },
|
|
211
|
+
}}
|
|
212
|
+
transition={{
|
|
213
|
+
duration: 0.8,
|
|
214
|
+
ease: [0.87, 0, 0.13, 1],
|
|
215
|
+
}}
|
|
216
|
+
className={clsx(
|
|
217
|
+
styles.dropdown,
|
|
218
|
+
overrides?.dropdown?.className,
|
|
219
|
+
)}
|
|
220
|
+
>
|
|
221
|
+
<div
|
|
222
|
+
{...overrides?.dropdownContent}
|
|
223
|
+
className={clsx(
|
|
224
|
+
styles.dropdownContent,
|
|
225
|
+
overrides?.dropdownContent?.className,
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
228
|
+
{options.map((option) => {
|
|
229
|
+
const isOptionSelected = option.id === value;
|
|
230
|
+
return (
|
|
231
|
+
<button
|
|
232
|
+
key={option.id}
|
|
233
|
+
type="button"
|
|
234
|
+
disabled={option.disabled}
|
|
235
|
+
data-selected={isOptionSelected}
|
|
236
|
+
{...overrides?.option}
|
|
237
|
+
className={clsx(
|
|
238
|
+
styles.option,
|
|
239
|
+
overrides?.option?.className,
|
|
240
|
+
)}
|
|
241
|
+
onClick={() => {
|
|
242
|
+
onChange?.(option);
|
|
243
|
+
if (closeOnSelect) setOpen(false);
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<div className={styles.optionContent}>
|
|
247
|
+
{renderOption
|
|
248
|
+
? renderOption(option, isOptionSelected)
|
|
249
|
+
: (
|
|
250
|
+
<TextWhenString kind="paragraphXSmall" weight="medium">
|
|
251
|
+
{option.node}
|
|
252
|
+
</TextWhenString>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
{isOptionSelected && (
|
|
256
|
+
<Icon icon={Check} size={13} className={styles.check} />
|
|
257
|
+
)}
|
|
258
|
+
</button>
|
|
259
|
+
);
|
|
260
|
+
})}
|
|
261
|
+
{action}
|
|
262
|
+
</div>
|
|
263
|
+
</motion.div>
|
|
264
|
+
)}
|
|
265
|
+
</AnimatePresence>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AccordionSelect';
|
|
@@ -115,9 +115,9 @@
|
|
|
115
115
|
color: var(--pte-new-colors-contentDisabled);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
118
|
+
//&:hover {
|
|
119
|
+
// background-color: var(--pte-new-colors-overlaySubtle);
|
|
120
|
+
//}
|
|
121
121
|
|
|
122
122
|
.box {
|
|
123
123
|
width: 13px;
|
|
@@ -164,6 +164,47 @@ export const AllowCustomValue: Story = {
|
|
|
164
164
|
},
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
+
export const CustomValueWithDivider: Story = {
|
|
168
|
+
args: {
|
|
169
|
+
...ComboboxArgs,
|
|
170
|
+
allowCustomValue: true,
|
|
171
|
+
customValueString: 'Add "%v"',
|
|
172
|
+
},
|
|
173
|
+
render: function Render(args) {
|
|
174
|
+
const [selected, setSelected] = useState<Option | null>(null);
|
|
175
|
+
const [inputValue, setInputValue] = useState<string>('');
|
|
176
|
+
return createElement(
|
|
177
|
+
'div',
|
|
178
|
+
{
|
|
179
|
+
style: { minHeight: '200px' },
|
|
180
|
+
},
|
|
181
|
+
createElement(Combobox<{ name: string }>, {
|
|
182
|
+
...args,
|
|
183
|
+
value:
|
|
184
|
+
selected?.id === null
|
|
185
|
+
? {
|
|
186
|
+
id: null,
|
|
187
|
+
node: inputValue,
|
|
188
|
+
metadata: {
|
|
189
|
+
name: inputValue,
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
: (selected as Option<{ name: string }> | null),
|
|
193
|
+
options: (args.options as Option<{ name: string }>[]).filter((o) => ((o.metadata?.name as string) || '')
|
|
194
|
+
.toLowerCase()
|
|
195
|
+
.includes(inputValue.toLowerCase())),
|
|
196
|
+
onChange: (e) => setSelected(e),
|
|
197
|
+
onInputChange: (e) => setInputValue(e),
|
|
198
|
+
overrides: {
|
|
199
|
+
customValueOption: {
|
|
200
|
+
style: { borderBottom: '5px solid var(--pte-new-colors-borderMedium)' },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
167
208
|
export const HideOptionsInitially: Story = {
|
|
168
209
|
args: ComboboxArgs,
|
|
169
210
|
render: function Render(args) {
|
|
@@ -113,6 +113,7 @@ export type ComboboxProps<T extends Record<string, any>> = {
|
|
|
113
113
|
input?: ComponentPropsWithoutRef<'input'>;
|
|
114
114
|
optionsContainer?: ComponentPropsWithoutRef<'ul'>;
|
|
115
115
|
option?: ComponentPropsWithoutRef<'li'>;
|
|
116
|
+
customValueOption?: ComponentPropsWithoutRef<'li'>;
|
|
116
117
|
label?: TextProps<'label'>;
|
|
117
118
|
description?: TextProps<'p'>;
|
|
118
119
|
startEnhancerContainer?: ComponentPropsWithoutRef<'div'>;
|
|
@@ -319,10 +320,10 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
|
|
|
319
320
|
value={query}
|
|
320
321
|
data-selected={false}
|
|
321
322
|
className={clsx(
|
|
322
|
-
overrides?.
|
|
323
|
+
overrides?.customValueOption?.className,
|
|
323
324
|
styles.option,
|
|
324
325
|
)}
|
|
325
|
-
{...overrides?.
|
|
326
|
+
{...overrides?.customValueOption}
|
|
326
327
|
>
|
|
327
328
|
<Text as="span" kind="paragraphSmall">
|
|
328
329
|
{customValueString.replace('%v', query)}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import type {
|
|
4
4
|
ComponentPropsWithoutRef, FC, MouseEventHandler, PropsWithChildren, ReactNode,
|
|
5
5
|
} from 'react';
|
|
6
|
-
import { useEffect, useState } from 'react';
|
|
6
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
7
|
+
import type { CSSLength } from '@ssh/csstypes';
|
|
7
8
|
import {
|
|
8
9
|
Dialog as HDialog, DialogPanel, DialogTitle, Transition, TransitionChild,
|
|
9
10
|
} from '@headlessui/react';
|
|
@@ -16,6 +17,8 @@ import { VisuallyHidden } from '../utility/VisuallyHidden';
|
|
|
16
17
|
import { RemoveFromDOM } from '../utility/RemoveFromDOM';
|
|
17
18
|
import { Close, Icon } from '../icon';
|
|
18
19
|
|
|
20
|
+
export const DialogWidthPresets = ['compact', 'default', 'large', 'full'] as const;
|
|
21
|
+
|
|
19
22
|
export type DialogProps = {
|
|
20
23
|
/**
|
|
21
24
|
* The dialog's open state.
|
|
@@ -47,11 +50,12 @@ export type DialogProps = {
|
|
|
47
50
|
*/
|
|
48
51
|
hideCloseButton?: boolean;
|
|
49
52
|
/**
|
|
50
|
-
* The width of the dialog.
|
|
53
|
+
* The width of the dialog. Either a preset or a valid {@link CSSLength} string.
|
|
51
54
|
*
|
|
55
|
+
* @see DialogWidthPresets
|
|
52
56
|
* @default 'default'
|
|
53
57
|
*/
|
|
54
|
-
width?:
|
|
58
|
+
width?: typeof DialogWidthPresets[number] | CSSLength;
|
|
55
59
|
/**
|
|
56
60
|
* The height of the dialog.
|
|
57
61
|
*
|
|
@@ -129,6 +133,8 @@ export const Dialog: FC<PropsWithChildren<DialogProps>> = ({
|
|
|
129
133
|
overlayStyle = 'blur',
|
|
130
134
|
children,
|
|
131
135
|
}) => {
|
|
136
|
+
const widthIsPreset = useMemo(() => (DialogWidthPresets as readonly string[]).includes(width), [width]);
|
|
137
|
+
|
|
132
138
|
const [dragging, setDragging] = useState(false);
|
|
133
139
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
134
140
|
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
|
@@ -228,11 +234,16 @@ export const Dialog: FC<PropsWithChildren<DialogProps>> = ({
|
|
|
228
234
|
className={clsx(
|
|
229
235
|
styles.panel,
|
|
230
236
|
styles[appearance],
|
|
231
|
-
styles[`w-${width}`],
|
|
237
|
+
{ [styles[`w-${width}`]]: widthIsPreset },
|
|
232
238
|
styles[`h-${height}`],
|
|
233
239
|
overrides.panel?.className,
|
|
234
240
|
)}
|
|
235
|
-
style={{
|
|
241
|
+
style={{
|
|
242
|
+
top: `${position.top}px`,
|
|
243
|
+
left: `${position.left}px`,
|
|
244
|
+
...(!widthIsPreset ? { maxWidth: width } : {}),
|
|
245
|
+
...overrides.panel?.style,
|
|
246
|
+
}}
|
|
236
247
|
onMouseDown={handleMouseDown}
|
|
237
248
|
onMouseUp={handleMouseUp}
|
|
238
249
|
onMouseMove={handleMouseMove}
|
|
@@ -415,10 +415,19 @@ $panelAnimationDelay: var(--pte-animations-duration-fast);
|
|
|
415
415
|
.bottomPanelContent {
|
|
416
416
|
position: relative;
|
|
417
417
|
padding: 20px;
|
|
418
|
+
|
|
419
|
+
&.noPadding {
|
|
420
|
+
padding: 0;
|
|
421
|
+
}
|
|
418
422
|
}
|
|
419
423
|
|
|
420
424
|
.bottomPanelSpacer {
|
|
421
425
|
padding: 20px;
|
|
426
|
+
|
|
427
|
+
&.noPadding {
|
|
428
|
+
padding: 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
422
431
|
opacity: 0;
|
|
423
432
|
pointer-events: none;
|
|
424
433
|
}
|
|
@@ -239,6 +239,67 @@ export const BottomPanel: Story = {
|
|
|
239
239
|
},
|
|
240
240
|
};
|
|
241
241
|
|
|
242
|
+
export const BottomPanelMultiSection: Story = {
|
|
243
|
+
args: {
|
|
244
|
+
title: 'Order summary',
|
|
245
|
+
children: (
|
|
246
|
+
<div style={{
|
|
247
|
+
width: '100%', display: 'flex', flexDirection: 'column', gap: '12px',
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
<p>Review your order before confirming.</p>
|
|
251
|
+
</div>
|
|
252
|
+
),
|
|
253
|
+
bottomPanelPadding: false,
|
|
254
|
+
bottomPanel: (
|
|
255
|
+
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
256
|
+
<div style={{
|
|
257
|
+
display: 'flex',
|
|
258
|
+
justifyContent: 'space-between',
|
|
259
|
+
padding: '12px 20px',
|
|
260
|
+
borderBottom: '1px solid var(--pte-new-colors-borderMedium)',
|
|
261
|
+
background: 'var(--pte-new-colors-overlaySubtle)',
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<span>Total</span>
|
|
265
|
+
<strong>$249.00</strong>
|
|
266
|
+
</div>
|
|
267
|
+
<div style={{
|
|
268
|
+
display: 'flex',
|
|
269
|
+
flexDirection: 'column',
|
|
270
|
+
gap: '12px',
|
|
271
|
+
padding: '20px',
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
<Button>
|
|
275
|
+
Confirm order
|
|
276
|
+
</Button>
|
|
277
|
+
<Button kind="secondary" theme="negative">
|
|
278
|
+
Cancel
|
|
279
|
+
</Button>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
),
|
|
283
|
+
},
|
|
284
|
+
render: function Render(args) {
|
|
285
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
286
|
+
return (
|
|
287
|
+
<>
|
|
288
|
+
<Button onClick={() => setIsOpen(true)}>
|
|
289
|
+
Review order
|
|
290
|
+
</Button>
|
|
291
|
+
<Drawer
|
|
292
|
+
{...args}
|
|
293
|
+
isOpen={isOpen}
|
|
294
|
+
onClose={setIsOpen}
|
|
295
|
+
>
|
|
296
|
+
{args.children}
|
|
297
|
+
</Drawer>
|
|
298
|
+
</>
|
|
299
|
+
);
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
242
303
|
export const Full: Story = {
|
|
243
304
|
args: {
|
|
244
305
|
title: 'Transaction details',
|
|
@@ -62,6 +62,12 @@ export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
|
|
|
62
62
|
* An optional panel that will be rendered at the bottom of the Drawer. This is useful for adding a footer to the Drawer with actions.
|
|
63
63
|
*/
|
|
64
64
|
bottomPanel?: ReactNode;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the bottom panel should have default padding. Set to `false` for edge-to-edge content like dividers or multi-section layouts.
|
|
67
|
+
*
|
|
68
|
+
* @default true
|
|
69
|
+
*/
|
|
70
|
+
bottomPanelPadding?: boolean;
|
|
65
71
|
|
|
66
72
|
/**
|
|
67
73
|
* An optional area that will be rendered at the top of the Drawer next to the title. This is useful for adding actions to the Drawer. Recommended to use {@link Menu} for an action menu.
|
|
@@ -144,11 +150,12 @@ export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
|
|
|
144
150
|
*/
|
|
145
151
|
export const Drawer = <T extends string[] | readonly string[] = string[]>({
|
|
146
152
|
isOpen = false,
|
|
147
|
-
onClose = () => {},
|
|
153
|
+
onClose = () => { },
|
|
148
154
|
title,
|
|
149
155
|
hideTitle = false,
|
|
150
156
|
hideCloseButton = false,
|
|
151
157
|
bottomPanel,
|
|
158
|
+
bottomPanelPadding = true,
|
|
152
159
|
from = 'right',
|
|
153
160
|
size = 'default',
|
|
154
161
|
pagination,
|
|
@@ -376,13 +383,13 @@ export const Drawer = <T extends string[] | readonly string[] = string[]>({
|
|
|
376
383
|
</div>
|
|
377
384
|
{bottomPanel && (
|
|
378
385
|
<>
|
|
379
|
-
<div tabIndex={-1} aria-hidden="true" className={clsx(styles.bottomPanelSpacer, overrides?.bottomPanelSpacer?.className)}>
|
|
386
|
+
<div tabIndex={-1} aria-hidden="true" className={clsx(styles.bottomPanelSpacer, { [styles.noPadding]: !bottomPanelPadding }, overrides?.bottomPanelSpacer?.className)}>
|
|
380
387
|
{bottomPanel}
|
|
381
388
|
</div>
|
|
382
389
|
<div className={clsx(styles.bottomPanel, overrides?.bottomPanel?.className)}>
|
|
383
390
|
<div className={styles.glassOpacity} />
|
|
384
391
|
<div className={styles.glassBlend} />
|
|
385
|
-
<div className={clsx(styles.bottomPanelContent, overrides?.bottomPanelContent?.className)}>
|
|
392
|
+
<div className={clsx(styles.bottomPanelContent, { [styles.noPadding]: !bottomPanelPadding }, overrides?.bottomPanelContent?.className)}>
|
|
386
393
|
{bottomPanel}
|
|
387
394
|
</div>
|
|
388
395
|
</div>
|
|
@@ -80,7 +80,7 @@ export const Field: FC<PropsWithChildren<FieldProps>> = ({
|
|
|
80
80
|
</Text>
|
|
81
81
|
)
|
|
82
82
|
: (
|
|
83
|
-
<label htmlFor={htmlFor} className={clsx({ [styles.hidden]: props.hideLabel })}>
|
|
83
|
+
<label htmlFor={htmlFor} className={clsx(styles.label, { [styles.hidden]: props.hideLabel })}>
|
|
84
84
|
{props.label}
|
|
85
85
|
</label>
|
|
86
86
|
);
|