paris 0.9.2 → 0.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # paris
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 41e4bb5: CardButton: new component, clickable card button
8
+ - 41e4bb5: Select: new `card` kind, and `hasOptionBorder` prop for listbox
9
+ - 41e4bb5: Card: removed `hover` kind, replaced with `raised` kind, new `surface` kind, updates to `flat` kind, new `status` prop with a `pending` state
10
+ - 41e4bb5: Checkbox: added `surface` kind
11
+
12
+ ### Patch Changes
13
+
14
+ - 41e4bb5: Accordion: updated border-bottom color
15
+ - 41e4bb5: Theme: added `roundedMedium` border radius, updated dark mode dropdown and overlayBlack variables
16
+ - 41e4bb5: Combobox: added `hasOptionBorder` prop to match Select listbox
17
+
3
18
  ## 0.9.2
4
19
 
5
20
  ### 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.9.2",
5
+ "version": "0.10.0",
6
6
  "homepage": "https://paris.slingshot.fm",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -34,6 +34,7 @@
34
34
  "./button": "./src/stories/button/index.ts",
35
35
  "./callout": "./src/stories/callout/index.ts",
36
36
  "./card": "./src/stories/card/index.ts",
37
+ "./cardbutton": "./src/stories/cardbutton/index.ts",
37
38
  "./checkbox": "./src/stories/checkbox/index.ts",
38
39
  "./combobox": "./src/stories/combobox/index.ts",
39
40
  "./dialog": "./src/stories/dialog/index.ts",
@@ -1,6 +1,6 @@
1
1
  .container {
2
2
  color: var(--pte-new-colors-contentPrimary);
3
- border-bottom: 1px solid var(--pte-new-colors-borderSubtle);
3
+ border-bottom: 1px solid var(--pte-new-colors-borderMedium);
4
4
  cursor: pointer;
5
5
  transition: all var(--pte-animations-duration-relaxed) var(--pte-animations-timing-easeInOutExpo);
6
6
 
@@ -19,7 +19,7 @@
19
19
  border-bottom: 1px solid transparent;
20
20
  transition: border-bottom-color var(--pte-animations-duration-gradual) var(--pte-animations-timing-easeInOutExpo);
21
21
  &.open {
22
- border-bottom-color: var(--pte-new-colors-borderSubtle);
22
+ border-bottom-color: var(--pte-new-colors-borderMedium);
23
23
  }
24
24
  }
25
25
 
@@ -81,3 +81,10 @@ export const Rounded: Story = {
81
81
  }),
82
82
  },
83
83
  };
84
+
85
+ export const Notification: Story = {
86
+ args: {
87
+ children: 'Button',
88
+ displayNotificationDot: true,
89
+ },
90
+ };
@@ -123,7 +123,9 @@ export type ButtonProps = {
123
123
  * This should be text. When Button shape is `circle` or `square`, the action description should still be passed here for screen readers.
124
124
  */
125
125
  children?: ReactNode | ReactNode[];
126
-
126
+ /**
127
+ * Displays a notification dot.
128
+ */
127
129
  displayNotificationDot?: boolean;
128
130
  } & Omit<AriaButtonProps, 'children' | 'disabled' | 'onClick'>;
129
131
 
@@ -1,14 +1,42 @@
1
1
  .container {
2
- border: var(--pte-new-borders-dropdown-border);
2
+ user-select: var(--pte-utils-defaultUserSelect);
3
+ width: auto;
4
+ }
5
+
6
+ .raised {
3
7
  border-radius: var(--pte-borders-radius-rounded);
8
+ background-color: var(--pte-new-colors-surfacePrimary);
4
9
  box-shadow: var(--pte-new-lighting-subtlePopup);
10
+ border: 1px solid var(--pte-new-colors-surfacePrimary);
5
11
 
6
- user-select: var(--pte-utils-defaultUserSelect);
7
- width: auto;
8
- padding: 16px;
12
+ &.pending {
13
+ border: 1px dashed var(--pte-new-colors-borderMedium);
14
+ background-color: var(--pte-new-colors-surfaceSecondary);
15
+ }
16
+ }
9
17
 
10
- &.flat {
11
- box-shadow: none;
12
- border-color: var(--pte-new-colors-borderMedium);
18
+ .surface {
19
+ border-radius: var(--pte-new-borders-radius-roundedMedium);
20
+ border: 1px solid var(--pte-new-colors-borderMedium);
21
+ background-color: var(--pte-new-colors-overlaySubtle);
22
+
23
+ &.pending {
24
+ border: 1px dashed var(--pte-new-colors-borderMedium);
25
+ background-color: transparent;
13
26
  }
14
27
  }
28
+
29
+ .flat {
30
+ border-radius: var(--pte-new-borders-radius-roundedMedium);
31
+ border: 1px solid var(--pte-new-colors-borderStrong);
32
+ background: linear-gradient(0deg, var(--pte-new-colors-overlayWhiteSubtle) 0%, var(--pte-new-colors-overlayWhiteSubtle) 100%), var(--pte-new-colors-surfacePrimary);
33
+
34
+ &.pending {
35
+ border: 1px dashed var(--pte-new-colors-borderStrong);
36
+ }
37
+ }
38
+
39
+ .text {
40
+ padding: 10px 12px;
41
+ color: var(--pte-new-colors-contentPrimary);
42
+ }
@@ -10,18 +10,29 @@ const meta: Meta<typeof Card> = {
10
10
  export default meta;
11
11
  type Story = StoryObj<typeof Card>;
12
12
 
13
- export const Default: Story = {
13
+ export const Raised: Story = {
14
14
  args: {
15
- children: 'Revenue: $3000',
15
+ children: 'Hello world!',
16
+ },
17
+ };
18
+
19
+ export const Surface: Story = {
20
+ args: {
21
+ kind: 'surface',
22
+ children: 'Hello world!',
16
23
  },
17
24
  };
18
25
 
19
- /**
20
- * Setting the `kind` prop to `flat` will remove the shadow from the card.
21
- */
22
26
  export const Flat: Story = {
23
27
  args: {
24
28
  kind: 'flat',
25
- children: 'Revenue: $3000',
29
+ children: 'Hello world!',
30
+ },
31
+ };
32
+
33
+ export const Pending: Story = {
34
+ args: {
35
+ children: 'Hello world!',
36
+ status: 'pending',
26
37
  },
27
38
  };
@@ -1,20 +1,21 @@
1
1
  import type { FC, HTMLAttributes, ReactNode } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import styles from './Card.module.scss';
4
+ import { TextWhenString } from '../utility';
4
5
 
5
6
  export type CardProps = {
6
7
  /**
7
- * The visual variant of the Card.
8
+ * The visual variant of the Card. `raised` is the default variant with a drop shadow, `surface` is a variant with a border and overlay, `flat` is a variant with a border.
8
9
  *
9
- * @default "hover"
10
+ * @default "raised"
10
11
  */
11
- kind?: 'hover' | 'flat';
12
- // /**
13
- // * Whether the Card should tilt on hover.
14
- // *
15
- // * @default true
16
- // */
17
- // tilt?: boolean;
12
+ kind?: 'raised' | 'surface' | 'flat';
13
+ /**
14
+ * The status of the Card. `pending` adds a dashed border.
15
+ *
16
+ * @default "default"
17
+ */
18
+ status?: 'default' | 'pending';
18
19
  /** The contents of the Card. */
19
20
  children?: ReactNode | ReactNode[];
20
21
  // /**
@@ -38,7 +39,8 @@ export type CardProps = {
38
39
  * @constructor
39
40
  */
40
41
  export const Card: FC<CardProps> = ({
41
- kind = 'hover',
42
+ kind = 'raised',
43
+ status = 'default',
42
44
  children,
43
45
  ...props
44
46
  }) => (
@@ -47,9 +49,13 @@ export const Card: FC<CardProps> = ({
47
49
  className={clsx(
48
50
  styles.container,
49
51
  styles[kind],
52
+ styles[status],
53
+ typeof children === 'string' && styles.text,
50
54
  props?.className,
51
55
  )}
52
56
  >
53
- {children}
57
+ <TextWhenString kind="paragraphMedium">
58
+ {children}
59
+ </TextWhenString>
54
60
  </div>
55
61
  );
@@ -0,0 +1,43 @@
1
+ .container {
2
+ user-select: var(--pte-utils-defaultUserSelect);
3
+ width: auto;
4
+ background: var(--pte-new-colors-surfacePrimary);
5
+ border-radius: var(--pte-new-borders-radius-roundedMedium);
6
+ }
7
+
8
+ .card {
9
+ transition: var(--pte-animations-interaction);
10
+ width: 100%;
11
+
12
+ border: 1px solid var(--pte-new-colors-borderStrong);
13
+ background-color: var(--pte-new-colors-overlayWhiteSubtle);
14
+ border-radius: var(--pte-new-borders-radius-roundedMedium);
15
+
16
+ display: flex;
17
+ justify-content: flex-start;
18
+ align-items: center;
19
+
20
+ &:hover {
21
+ background-color: var(--pte-new-colors-overlayStrong);
22
+ cursor: default;
23
+ }
24
+
25
+ &[aria-disabled=false]:active {
26
+ //border: 1px solid var(--pte-new-colors-borderUltrastrong);
27
+ background-color: var(--pte-new-colors-overlayMedium);
28
+ transform: scale(0.996);
29
+ }
30
+
31
+ &[aria-disabled=true] {
32
+ pointer-events: none;
33
+
34
+ &.text {
35
+ color: var(--pte-new-colors-contentDisabled);
36
+ }
37
+ }
38
+ }
39
+
40
+ .text {
41
+ padding: 4px 10px;
42
+ color: var(--pte-new-colors-contentPrimary);
43
+ }
@@ -0,0 +1,24 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { CardButton } from './CardButton';
3
+
4
+ const meta: Meta<typeof CardButton> = {
5
+ title: 'Inputs/CardButton',
6
+ component: CardButton,
7
+ tags: ['autodocs'],
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof CardButton>;
12
+
13
+ export const Default: Story = {
14
+ args: {
15
+ children: 'Hello world!',
16
+ },
17
+ };
18
+
19
+ export const Disabled: Story = {
20
+ args: {
21
+ children: 'Hello world!',
22
+ disabled: true,
23
+ },
24
+ };
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import type {
4
+ FC, HTMLAttributeAnchorTarget, MouseEventHandler, ReactNode,
5
+ } from 'react';
6
+ import type { ButtonProps as AriaButtonProps } from '@ariakit/react';
7
+ import { Button as AriaButton } from '@ariakit/react';
8
+ import clsx from 'clsx';
9
+ import styles from './CardButton.module.scss';
10
+ import { TextWhenString } from '../utility';
11
+
12
+ export type CardButtonProps = {
13
+ /**
14
+ * Disables the Button, disallowing user interaction.
15
+ * @default false
16
+ */
17
+ disabled?: boolean;
18
+ /**
19
+ * The interaction handler for the Button.
20
+ */
21
+ onClick?: MouseEventHandler<HTMLButtonElement>;
22
+ /**
23
+ * Optionally, the Button can be rendered as an anchor element by passing an `href` prop. To use a Next.js Link component, use the `render` prop directly.
24
+ */
25
+ href?: string;
26
+ /**
27
+ * Optionally, the target of the anchor element can be specified (defaults to `_self`).
28
+ */
29
+ hreftarget?: HTMLAttributeAnchorTarget;
30
+ /**
31
+ * The contents of the Button.
32
+ */
33
+ children?: ReactNode | ReactNode[];
34
+ } & Omit<AriaButtonProps, 'children' | 'disabled' | 'onClick'>;
35
+
36
+ /**
37
+ * A CardButton component.
38
+ *
39
+ * <hr />
40
+ *
41
+ * To use this component, import it as follows:
42
+ *
43
+ * ```js
44
+ * import { CardButton } from 'paris/cardbutton';
45
+ * ```
46
+ * @constructor
47
+ */
48
+ export const CardButton: FC<CardButtonProps> = ({
49
+ type = 'button',
50
+ onClick,
51
+ children,
52
+ disabled,
53
+ href,
54
+ ...props
55
+ }) => (
56
+ <div className={styles.container}>
57
+ <AriaButton
58
+ {...props}
59
+ className={clsx(
60
+ styles.card,
61
+ typeof children === 'string' && styles.text,
62
+ props?.className,
63
+ )}
64
+ aria-disabled={disabled ?? false}
65
+ type={type}
66
+ aria-details={typeof children === 'string' ? children : undefined}
67
+ onClick={!disabled && !href ? onClick : () => {}}
68
+ disabled={false}
69
+ {...href ? {
70
+ render: (properties) => (
71
+ // eslint-disable-next-line jsx-a11y/anchor-has-content
72
+ <a
73
+ {...properties}
74
+ href={href}
75
+ target={props.hreftarget ?? '_self'}
76
+ rel={props.hreftarget === '_self' ? undefined : 'noreferrer'}
77
+ />
78
+ ),
79
+ } : {}}
80
+ >
81
+ <TextWhenString kind="paragraphMedium">
82
+ {children}
83
+ </TextWhenString>
84
+ </AriaButton>
85
+ </div>
86
+ );
@@ -0,0 +1 @@
1
+ export * from './CardButton';
@@ -20,48 +20,78 @@
20
20
  }
21
21
 
22
22
  .root {
23
- background-color: transparent;
24
- width: 14px;
25
- height: 14px;
26
- display: flex;
27
- align-items: center;
28
- justify-content: center;
29
- flex-shrink: 0;
23
+ &.default {
24
+ background-color: transparent;
25
+ width: 14px;
26
+ height: 14px;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ flex-shrink: 0;
30
31
 
31
- border: 2px solid var(--pte-new-colors-contentTertiary);
32
- border-radius: var(--pte-borders-radius-rectangle);
33
- //box-shadow: var(--pte-new-lighting-shallowBelow);
32
+ border: 2px solid var(--pte-new-colors-contentTertiary);
33
+ border-radius: var(--pte-borders-radius-rectangle);
34
34
 
35
- &:hover {
36
- //background-color: ;
37
- }
38
- &:focus-visible {
39
- outline: 1px solid var(--pte-new-colors-borderUltrastrong);
40
- }
35
+ &:hover {
36
+ //background-color: ;
37
+ }
41
38
 
42
- position: relative;
43
- &:after {
44
- content: "";
45
- width: 100%;
46
- height: 100%;
47
- position: absolute;
48
- background-color: transparent;
49
- transition: var(--pte-animations-interaction);
50
- transition-delay: 50ms;
51
- }
39
+ &:focus-visible {
40
+ outline: 1px solid var(--pte-new-colors-borderUltrastrong);
41
+ }
42
+
43
+ position: relative;
52
44
 
53
- &[data-state="checked"], &[data-state="indeterminate"] {
54
45
  &:after {
55
- opacity: 0;
46
+ content: "";
47
+ width: 100%;
48
+ height: 100%;
49
+ position: absolute;
50
+ background-color: transparent;
51
+ transition: var(--pte-animations-interaction);
52
+ transition-delay: 50ms;
56
53
  }
57
- }
58
54
 
59
- &[data-disabled=true] {
60
- border-color: var(--pte-new-colors-contentDisabled);
55
+ &[data-state="checked"], &[data-state="indeterminate"] {
56
+ &:after {
57
+ opacity: 0;
58
+ }
59
+ }
60
+
61
+ &[data-disabled=true] {
62
+ border-color: var(--pte-new-colors-contentDisabled);
63
+ }
64
+
65
+ & span {
66
+ transition: var(--pte-animations-interaction);
67
+ }
61
68
  }
62
69
 
63
- & span {
70
+ &.surface {
71
+ width: 100%;
72
+ padding: 6px 10px;
73
+ color: var(--pte-new-colors-contentSecondary);
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: space-between;
64
77
  transition: var(--pte-animations-interaction);
78
+
79
+ &:focus-visible {
80
+ outline: 1px solid var(--pte-new-colors-borderUltrastrong);
81
+ }
82
+
83
+ &[data-state="checked"], &[data-state="indeterminate"] {
84
+ background-color: var(--pte-new-colors-overlayMedium);
85
+ color: var(--pte-new-colors-contentPrimary);
86
+ }
87
+
88
+ &[data-disabled=true] {
89
+ color: var(--pte-new-colors-contentDisabled);
90
+ }
91
+
92
+ &:hover {
93
+ background-color: var(--pte-new-colors-overlayStrong);
94
+ }
65
95
  }
66
96
  }
67
97
 
@@ -76,3 +106,11 @@
76
106
  fill: var(--pte-new-colors-contentDisabled);
77
107
  }
78
108
  }
109
+
110
+ .checkIcon {
111
+ color: var(--pte-new-colors-contentPrimary);
112
+
113
+ &[data-disabled=true] {
114
+ color: var(--pte-new-colors-contentDisabled);
115
+ }
116
+ }
@@ -25,3 +25,19 @@ export const Default: Story = {
25
25
  });
26
26
  },
27
27
  };
28
+
29
+ export const Surface: Story = {
30
+ args: {
31
+ children: 'ACH Bank Transfer',
32
+ kind: 'surface',
33
+ },
34
+ render: (args) => {
35
+ // eslint-disable-next-line react-hooks/rules-of-hooks
36
+ const [checked, setChecked] = useState(false);
37
+ return createElement(Checkbox, {
38
+ ...args,
39
+ checked,
40
+ onChange: (e) => setChecked(!!e),
41
+ });
42
+ },
43
+ };
@@ -5,8 +5,11 @@ import clsx from 'clsx';
5
5
  import styles from './Checkbox.module.scss';
6
6
  import { pvar } from '../theme';
7
7
  import { TextWhenString } from '../utility';
8
+ import { Check, Icon } from '../icon';
8
9
 
9
10
  export type CheckboxProps = {
11
+ /** The visual style of the Checkbox. `default` is a standard checkbox with a label next to it, `surface` is a clickable card that displays a check when selected. */
12
+ kind?: 'default' | 'surface'
10
13
  checked?: boolean;
11
14
  onChange?: (checked: boolean | 'indeterminate') => void;
12
15
  disabled?: boolean;
@@ -27,6 +30,7 @@ export type CheckboxProps = {
27
30
  * @constructor
28
31
  */
29
32
  export const Checkbox: FC<CheckboxProps> = ({
33
+ kind = 'default',
30
34
  checked,
31
35
  onChange,
32
36
  disabled,
@@ -38,29 +42,46 @@ export const Checkbox: FC<CheckboxProps> = ({
38
42
  return (
39
43
  <label
40
44
  htmlFor={inputID}
41
- className={clsx(styles.container, disabled && styles.disabled, className)}
45
+ className={clsx(styles.container, disabled && styles.disabled, className, checked && styles.checked)}
42
46
  {...props}
43
47
  >
44
48
  <RadixCheckbox.Root
45
49
  id={inputID}
46
- className={styles.root}
50
+ className={clsx(styles.root, styles[kind])}
47
51
  checked={checked}
48
52
  onCheckedChange={onChange}
49
53
  data-disabled={disabled}
50
54
  >
55
+ {kind === 'surface' && (
56
+ <TextWhenString kind="paragraphXSmall">
57
+ {children}
58
+ </TextWhenString>
59
+ )}
51
60
  <RadixCheckbox.Indicator className={styles.indicator}>
52
- <svg width={14} height={14} viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
53
- <path
54
- className={styles.checkSvg}
61
+ {kind === 'default' && (
62
+ <svg width={14} height={14} viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
63
+ <path
64
+ className={styles.checkSvg}
65
+ data-disabled={disabled}
66
+ d="M0.333374 0.333252V13.6666H13.6667V0.333252H0.333374ZM6.00004 10.3999L2.26672 6.66658L3.66671 5.26658L5.93339 7.53325L10.2 3.26658L11.6001 4.66659L6.00004 10.3999Z"
67
+ />
68
+ </svg>
69
+ )}
70
+ {kind === 'surface' && (
71
+ <Icon
72
+ icon={Check}
73
+ size={12.8}
55
74
  data-disabled={disabled}
56
- d="M0.333374 0.333252V13.6666H13.6667V0.333252H0.333374ZM6.00004 10.3999L2.26672 6.66658L3.66671 5.26658L5.93339 7.53325L10.2 3.26658L11.6001 4.66659L6.00004 10.3999Z"
75
+ className={styles.checkIcon}
57
76
  />
58
- </svg>
77
+ )}
59
78
  </RadixCheckbox.Indicator>
60
79
  </RadixCheckbox.Root>
61
- <TextWhenString kind="paragraphXSmall">
62
- {children}
63
- </TextWhenString>
80
+ {kind === 'default' && (
81
+ <TextWhenString kind="paragraphXSmall">
82
+ {children}
83
+ </TextWhenString>
84
+ )}
64
85
  </label>
65
86
  );
66
87
  };
@@ -68,9 +68,14 @@ export type ComboboxProps<T extends Record<string, any>> = {
68
68
  */
69
69
  customValueString?: string;
70
70
  /**
71
- * The size of the options dropdown, in pixels. Only applicable to kind="listbox".
71
+ * The size of the options dropdown, in pixels.
72
72
  */
73
73
  maxHeight?: number;
74
+ /**
75
+ * Adds a bottom border to the dropdown options.
76
+ * @default false
77
+ */
78
+ hasOptionBorder?: boolean;
74
79
  /**
75
80
  * Prop overrides for other rendered elements. Overrides for the input itself should be passed directly to the component.
76
81
  */
@@ -115,6 +120,7 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
115
120
  allowCustomValue,
116
121
  customValueString = 'Create "%v"',
117
122
  maxHeight = 320,
123
+ hasOptionBorder = false,
118
124
  overrides,
119
125
  }: ComboboxProps<T>) {
120
126
  const inputID = useId();
@@ -262,6 +268,7 @@ export function Combobox<T extends Record<string, any> = Record<string, any>>({
262
268
  className={clsx(
263
269
  overrides?.option,
264
270
  styles.option,
271
+ hasOptionBorder && styles.optionBorder,
265
272
  )}
266
273
  >
267
274
  {typeof option.node === 'string' ? (
@@ -87,6 +87,13 @@
87
87
  color: var(--pte-new-colors-contentDisabled);
88
88
  }
89
89
  }
90
+
91
+ &.optionBorder {
92
+ border-bottom: 0.5px solid var(--pte-new-colors-borderMedium);
93
+ }
94
+ &.optionBorder:last-child {
95
+ border-bottom: none;
96
+ }
90
97
  }
91
98
 
92
99
  .content {
@@ -155,3 +162,80 @@
155
162
  border: 1.5px solid var(--pte-new-colors-contentTertiary);
156
163
  transition: all var(--pte-animations-interaction);
157
164
  }
165
+
166
+ .cardContainer {
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: 8px;
170
+ justify-content: flex-start;
171
+ align-items: flex-start;
172
+ }
173
+
174
+ .cardOption {
175
+ background-color: var(--pte-new-colors-surfacePrimary);
176
+ border-radius: 6px;
177
+ width: 100%;
178
+
179
+ &[data-headlessui-state~="active"] {
180
+ //.cardSurface {
181
+ // background-color: var(--pte-new-colors-overlayWhiteSubtle);
182
+ // border-color: var(--pte-new-colors-borderStrong);
183
+ //}
184
+ }
185
+
186
+ &[data-headlessui-state~="checked"] {
187
+ .cardSurface {
188
+ background-color: var(--pte-new-colors-overlayMedium);
189
+ border-color: var(--pte-new-colors-borderUltrastrong);
190
+ }
191
+ }
192
+
193
+ &[data-status="disabled"], &[data-headlessui-state~="disabled"] {
194
+ pointer-events: none;
195
+ cursor: default;
196
+
197
+ &, & * {
198
+ color: var(--pte-new-colors-contentDisabled);
199
+ }
200
+
201
+ //.cardSurface {
202
+ // background-color: var(--pte-new-colors-overlayWhiteSubtle);
203
+ // border-color: var(--pte-new-colors-borderStrong);
204
+ //}
205
+ }
206
+
207
+ &[data-status="error"] {
208
+ .cardSurface {
209
+ background-color: var(--pte-new-colors-backgroundNegative);
210
+ }
211
+ }
212
+
213
+ &[data-status="success"] {
214
+ .cardSurface {
215
+ background-color: var(--pte-new-colors-backgroundPositive);
216
+ }
217
+ }
218
+
219
+ &:hover {
220
+ .cardSurface {
221
+ background-color: var(--pte-new-colors-overlayStrong);
222
+ }
223
+ }
224
+ }
225
+
226
+ .cardSurface {
227
+ width: 100%;
228
+ display: flex;
229
+ flex-direction: row;
230
+ justify-content: flex-start;
231
+ align-items: center;
232
+ transition: all var(--pte-animations-interaction);
233
+
234
+ border-radius: 6px;
235
+ background-color: var(--pte-new-colors-overlayWhiteSubtle);
236
+ border: 1px solid var(--pte-new-colors-borderStrong);
237
+
238
+ &.text {
239
+ padding: 4px 10px;
240
+ }
241
+ }
@@ -48,6 +48,7 @@ export const Default: Story = {
48
48
 
49
49
  export const WithCustomNodes: Story = {
50
50
  args: {
51
+ hasOptionBorder: true,
51
52
  options: [
52
53
  {
53
54
  id: '1',
@@ -95,3 +96,18 @@ export const Radio: Story = {
95
96
  },
96
97
  render,
97
98
  };
99
+
100
+ export const Card: Story = {
101
+ args: {
102
+ label: 'Release type',
103
+ description: 'Select the type of release you want to create.',
104
+ kind: 'card',
105
+ options: [
106
+ { id: '1', node: 'Single' },
107
+ { id: '2', node: 'EP' },
108
+ { id: '3', node: 'Album (LP)' },
109
+ { id: '4', node: 'Compilation' },
110
+ ],
111
+ },
112
+ render,
113
+ };
@@ -48,15 +48,19 @@ export type SelectProps<T = Record<string, any>> = {
48
48
  */
49
49
  onChange?: (value: Option<T>['id'] | null) => void | Promise<void>;
50
50
  /**
51
- * The visual variant of the Select. Listboxes will render as a dropdown menu, and radios will render as a radio group.
51
+ * The visual variant of the Select. `listbox` will render as a dropdown menu, `radio` will render as a radio group, and `card` will render as selectable cards.
52
52
  * @default listbox
53
53
  */
54
- kind?: 'listbox' | 'radio';
55
-
54
+ kind?: 'listbox' | 'radio' | 'card';
56
55
  /**
57
56
  * The size of the options dropdown, in pixels. Only applicable to kind="listbox".
58
57
  */
59
58
  maxHeight?: number;
59
+ /**
60
+ * Adds a bottom border to the dropdown options. Only applicable to kind="listbox".
61
+ * @default false
62
+ */
63
+ hasOptionBorder?: boolean;
60
64
 
61
65
  /**
62
66
  * Prop overrides for other rendered elements. Overrides for the input itself should be passed directly to the component.
@@ -100,6 +104,7 @@ export const Select = forwardRef(function <T = Record<string, any>>({
100
104
  disabled,
101
105
  kind = 'listbox',
102
106
  maxHeight = 320,
107
+ hasOptionBorder = false,
103
108
  overrides,
104
109
  }: SelectProps<T>, ref: ForwardedRef<any>) {
105
110
  const inputID = useId();
@@ -195,6 +200,7 @@ export const Select = forwardRef(function <T = Record<string, any>>({
195
200
  className={clsx(
196
201
  overrides?.option,
197
202
  styles.option,
203
+ hasOptionBorder && styles.optionBorder,
198
204
  )}
199
205
  disabled={option.disabled || false}
200
206
  >
@@ -233,6 +239,28 @@ export const Select = forwardRef(function <T = Record<string, any>>({
233
239
  ))}
234
240
  </RadioGroup>
235
241
  )}
242
+ {kind === 'card' && (
243
+ <RadioGroup ref={ref} as="div" className={styles.cardContainer} value={value} onChange={onChange}>
244
+ {options.map((option) => (
245
+ <RadioGroup.Option
246
+ as="div"
247
+ className={clsx(
248
+ styles.cardOption,
249
+ )}
250
+ key={option.id}
251
+ value={option.id}
252
+ disabled={option.disabled || false}
253
+ data-status={disabled ? 'disabled' : (status || 'default')}
254
+ >
255
+ <div className={clsx(styles.cardSurface, typeof option.node === 'string' && styles.text)}>
256
+ <TextWhenString kind="paragraphSmall">
257
+ {option.node}
258
+ </TextWhenString>
259
+ </div>
260
+ </RadioGroup.Option>
261
+ ))}
262
+ </RadioGroup>
263
+ )}
236
264
  </Field>
237
265
  );
238
266
  });
@@ -378,6 +378,7 @@ export type Theme = {
378
378
  rectangle: CSSLength,
379
379
  rounded: CSSLength,
380
380
  roundedSmall: CSSLength,
381
+ roundedMedium: CSSLength,
381
382
  roundedLarge: CSSLength,
382
383
  roundedXL: CSSLength,
383
384
  },
@@ -1005,6 +1006,7 @@ export const LightTheme: Theme = {
1005
1006
  rectangle: '0px',
1006
1007
  rounded: '8px',
1007
1008
  roundedSmall: '4px',
1009
+ roundedMedium: '6px',
1008
1010
  roundedLarge: '12px',
1009
1011
  roundedXL: '16px',
1010
1012
  },
@@ -1376,9 +1378,9 @@ export const DarkTheme: Theme = merge(LightTheme, {
1376
1378
  overlayInverseSubtle: 'rgba(0, 0, 0, 0.02)',
1377
1379
  overlayInverseMedium: 'rgba(0, 0, 0, 0.05)',
1378
1380
  overlayWhiteUltrastrong: 'rgba(0, 0, 0, 0.18)',
1379
- overlayBlackSubtle: 'rgba(255, 255, 255, 0.2)',
1380
- overlayBlackMedium: 'rgba(255, 255, 255, 0.3)',
1381
- overlayBlackStrong: 'rgba(255, 255, 255, 0.4)',
1381
+ overlayBlackSubtle: 'rgba(0, 0, 0, 0.2)',
1382
+ overlayBlackMedium: 'rgba(0, 0, 0, 0.3)',
1383
+ overlayBlackStrong: 'rgba(0, 0, 0, 0.4)',
1382
1384
  overlayPageBackground: 'rgba(18, 18, 18, 0.75)',
1383
1385
  },
1384
1386
  lighting: {
@@ -1418,8 +1420,8 @@ export const DarkTheme: Theme = merge(LightTheme, {
1418
1420
  },
1419
1421
  borders: {
1420
1422
  dropdown: {
1421
- shadow: 'none',
1422
- color: 'none',
1423
+ shadow: ShadowsDark.deepBelow,
1424
+ color: 'transparent',
1423
1425
  border: 'none',
1424
1426
  },
1425
1427
  },