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 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.6",
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
- &:hover {
119
- background-color: var(--pte-new-colors-overlaySubtle);
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?.option?.className,
323
+ overrides?.customValueOption?.className,
323
324
  styles.option,
324
325
  )}
325
- {...overrides?.option}
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?: 'compact' | 'default' | 'large' | 'full';
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={{ top: `${position.top}px`, left: `${position.left}px` }}
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
  );