paris 0.6.1 → 0.7.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,16 @@
1
1
  # paris
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 669cf98: `Avatar` component
8
+ - 669cf98: `Popover` component
9
+
10
+ ### Patch Changes
11
+
12
+ - 669cf98: Icon: `Spinner`
13
+
3
14
  ## 0.6.1
4
15
 
5
16
  ### 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.6.1",
5
+ "version": "0.7.0",
6
6
  "homepage": "https://paris.slingshot.fm",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -44,6 +44,7 @@
44
44
  "./icon": "./src/stories/icon/index.ts",
45
45
  "./input": "./src/stories/input/index.ts",
46
46
  "./pagination": "./src/stories/pagination/index.ts",
47
+ "./popover": "./src/stories/popover/index.ts",
47
48
  "./select": "./src/stories/select/index.ts",
48
49
  "./styledlink": "./src/stories/styledlink/index.ts",
49
50
  "./table": "./src/stories/table/index.ts",
@@ -58,6 +59,10 @@
58
59
  },
59
60
  "dependencies": {
60
61
  "@ariakit/react": "^0.2.3",
62
+ "@fortawesome/fontawesome-svg-core": "^6.4.2",
63
+ "@fortawesome/free-regular-svg-icons": "^6.4.2",
64
+ "@fortawesome/free-solid-svg-icons": "^6.4.2",
65
+ "@fortawesome/react-fontawesome": "^0.2.0",
61
66
  "@headlessui/react": "^1.7.14",
62
67
  "@radix-ui/react-checkbox": "^1.0.4",
63
68
  "clsx": "^1.2.1",
@@ -66,13 +71,10 @@
66
71
  "pte": "^0.4.9",
67
72
  "react-hot-toast": "^2.4.1",
68
73
  "react-parallax-tilt": "^1.7.144",
74
+ "react-tiny-popover": "^8.0.4",
69
75
  "ts-deepmerge": "^6.0.3"
70
76
  },
71
77
  "peerDependencies": {
72
- "@fortawesome/fontawesome-svg-core": "^6.4.0",
73
- "@fortawesome/free-regular-svg-icons": "^6.4.0",
74
- "@fortawesome/free-solid-svg-icons": "^6.4.0",
75
- "@fortawesome/react-fontawesome": "^0.2.0",
76
78
  "react": "^18.x",
77
79
  "react-dom": "^18.x",
78
80
  "sass": "^1.x",
@@ -3,4 +3,5 @@
3
3
  aspect-ratio: 1;
4
4
  border: 1px solid var(--frame-color);
5
5
  overflow: clip;
6
+ box-shadow: var(--pte-lighting-subtlePopup);
6
7
  }
@@ -1,8 +1,9 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
+ import { createElement } from 'react';
2
3
  import { Avatar } from './Avatar';
3
4
 
4
5
  const meta: Meta<typeof Avatar> = {
5
- title: 'Uncategorized/Avatar',
6
+ title: 'Content/Avatar',
6
7
  component: Avatar,
7
8
  tags: ['autodocs'],
8
9
  };
@@ -12,12 +13,13 @@ type Story = StoryObj<typeof Avatar>;
12
13
 
13
14
  export const Default: Story = {
14
15
  args: {
15
- children: 'Hello world! This is a new Avatar component.',
16
- },
17
- };
18
-
19
- export const Secondary: Story = {
20
- args: {
21
- children: 'Hello world! This is a secondary component.',
16
+ width: '128px',
17
+ children: createElement(
18
+ 'img',
19
+ {
20
+ src: 'https://swift.slingshot.fm/sling/static/Billie-Eilish.jpg',
21
+ alt: 'Avatar',
22
+ },
23
+ ),
22
24
  },
23
25
  };
@@ -1,14 +1,19 @@
1
- import type { CSSProperties, FC, ReactNode } from 'react';
1
+ import type {
2
+ ComponentPropsWithoutRef, CSSProperties, FC, ReactNode,
3
+ } from 'react';
2
4
  import type { CSSLength } from '@ssh/csstypes';
5
+ import { useMemo } from 'react';
6
+ import clsx from 'clsx';
3
7
  import styles from './Avatar.module.scss';
4
8
  import { pvar } from '../theme';
5
9
 
6
10
  export type AvatarProps = {
7
11
  frameColor?: string;
8
- width?: CSSLength;
9
- /** The contents of the Avatar. */
12
+ /** The width of the Avatar, as a CSS length string or a number. If a number is provided, the assumed unit is pixels. */
13
+ width?: CSSLength | number;
14
+ /** The contents of the Avatar, usually an image element. */
10
15
  children?: ReactNode;
11
- };
16
+ } & Omit<ComponentPropsWithoutRef<'div'>, 'children'>;
12
17
 
13
18
  /**
14
19
  * An Avatar component.
@@ -26,14 +31,24 @@ export const Avatar: FC<AvatarProps> = ({
26
31
  frameColor = pvar('colors.borderOpaque'),
27
32
  width = 'fit-content',
28
33
  children,
29
- }) => (
30
- <div
31
- style={{
32
- width,
33
- '--frame-color': frameColor,
34
- } as CSSProperties}
35
- className={styles.content}
36
- >
37
- {children}
38
- </div>
39
- );
34
+ className,
35
+ style,
36
+ ...props
37
+ }) => {
38
+ const widthMemoized = useMemo(() => (
39
+ typeof width === 'number' ? `${width}px` : width
40
+ ) as CSSProperties['width'], [width]);
41
+ return (
42
+ <div
43
+ {...props}
44
+ style={{
45
+ width: widthMemoized,
46
+ '--frame-color': frameColor,
47
+ ...style,
48
+ } as CSSProperties}
49
+ className={clsx(styles.content, className)}
50
+ >
51
+ {children}
52
+ </div>
53
+ );
54
+ };
@@ -13,6 +13,7 @@ import { Text } from '../text';
13
13
  import type { Enhancer } from '../../types/Enhancer';
14
14
  import { MemoizedEnhancer } from '../../helpers/renderEnhancer';
15
15
  import { pvar } from '../theme';
16
+ import { Spinner } from '../icon';
16
17
 
17
18
  const EnhancerSizes = {
18
19
  large: 13,
@@ -81,6 +82,11 @@ export type ButtonProps = {
81
82
  * @default false
82
83
  */
83
84
  disabled?: boolean;
85
+ /**
86
+ * Displays a loading indicator inside the Button and disables user interaction.
87
+ * @default false
88
+ */
89
+ loading?: boolean;
84
90
  /**
85
91
  * The interaction handler for the Button.
86
92
  */
@@ -98,7 +104,7 @@ export type ButtonProps = {
98
104
  *
99
105
  * This should be text. When Button shape is `circle` or `square`, the action description should still be passed here for screen readers.
100
106
  */
101
- children: ReactNode | ReactNode[];
107
+ children?: ReactNode | ReactNode[];
102
108
  } & Omit<AriaButtonProps, 'children' | 'disabled' | 'onClick'>;
103
109
 
104
110
  /**
@@ -126,6 +132,7 @@ export const Button: FC<ButtonProps> = ({
126
132
  onClick,
127
133
  children,
128
134
  disabled,
135
+ loading,
129
136
  href,
130
137
  ...props
131
138
  }) => (
@@ -148,7 +155,7 @@ export const Button: FC<ButtonProps> = ({
148
155
  aria-disabled={disabled ?? false}
149
156
  type={type}
150
157
  aria-details={typeof children === 'string' ? children : undefined}
151
- onClick={!disabled && !href ? onClick : () => {}}
158
+ onClick={!disabled && !href && !loading ? onClick : () => {}}
152
159
  disabled={false}
153
160
  {...href ? {
154
161
  render: (properties) => (
@@ -170,7 +177,11 @@ export const Button: FC<ButtonProps> = ({
170
177
  )}
171
178
  {!['circle', 'square'].includes(shape) && (
172
179
  <Text kind="labelXSmall">
173
- {children || 'Button'}
180
+ {!loading ? (
181
+ children || 'Button'
182
+ ) : (
183
+ <Spinner size={EnhancerSizes[size]} />
184
+ )}
174
185
  </Text>
175
186
  )}
176
187
  {!!endEnhancer && (
@@ -1,4 +1,6 @@
1
- import type { FC, ReactElement, ReactNode } from 'react';
1
+ import type {
2
+ ComponentPropsWithoutRef, FC, ReactElement, ReactNode,
3
+ } from 'react';
2
4
  import clsx from 'clsx';
3
5
  import styles from './Callout.module.scss';
4
6
  import { RemoveFromDOM, TextWhenString } from '../utility';
@@ -10,7 +12,7 @@ export type CalloutProps = {
10
12
  icon?: ReactElement;
11
13
  /** The contents of the Callout. */
12
14
  children?: ReactNode;
13
- };
15
+ } & Omit<ComponentPropsWithoutRef<'div'>, 'children'>;
14
16
 
15
17
  /**
16
18
  * A Callout component.
@@ -28,8 +30,10 @@ export const Callout: FC<CalloutProps> = ({
28
30
  variant = 'default',
29
31
  icon,
30
32
  children,
33
+ className,
34
+ ...props
31
35
  }) => (
32
- <div className={clsx(styles.content, styles[variant])}>
36
+ <div className={clsx(styles.content, styles[variant], className)} {...props}>
33
37
  <RemoveFromDOM when={!icon}>
34
38
  <div className={styles.icon}>
35
39
  {icon}
@@ -1,6 +1,7 @@
1
1
  import type { FC, ReactNode } from 'react';
2
2
  import { useId } from 'react';
3
3
  import * as RadixCheckbox from '@radix-ui/react-checkbox';
4
+ import clsx from 'clsx';
4
5
  import styles from './Checkbox.module.scss';
5
6
  import { pvar } from '../theme';
6
7
 
@@ -10,7 +11,7 @@ export type CheckboxProps = {
10
11
  disabled?: boolean;
11
12
  /** The contents of the Checkbox. */
12
13
  children?: ReactNode | ReactNode[];
13
- };
14
+ } & Omit<React.ComponentPropsWithoutRef<'label'>, 'onChange' | 'children'>;
14
15
 
15
16
  /**
16
17
  * A Checkbox component.
@@ -29,12 +30,15 @@ export const Checkbox: FC<CheckboxProps> = ({
29
30
  onChange,
30
31
  disabled,
31
32
  children,
33
+ className,
34
+ ...props
32
35
  }) => {
33
36
  const inputID = useId();
34
37
  return (
35
38
  <label
36
39
  htmlFor={inputID}
37
- className={styles.container}
40
+ className={clsx(styles.container, className)}
41
+ {...props}
38
42
  >
39
43
  <RadixCheckbox.Root
40
44
  id={inputID}
@@ -57,6 +57,7 @@ export const Field: FC<PropsWithChildren<FieldProps>> = ({
57
57
  htmlFor,
58
58
  disabled,
59
59
  children,
60
+ // className,
60
61
  ...props
61
62
  }) => (
62
63
  // Disable a11y rules because the container doesn't need to be focusable for screen readers; the input itself should receive focus instead.
@@ -67,6 +68,7 @@ export const Field: FC<PropsWithChildren<FieldProps>> = ({
67
68
  className={clsx(
68
69
  props.overrides?.container?.className,
69
70
  styles.container,
71
+ // className,
70
72
  )}
71
73
  onClick={(e) => {
72
74
  e.preventDefault();
@@ -0,0 +1,15 @@
1
+ import { memo } from 'react';
2
+ import type { IconDefinition } from './Icon';
3
+
4
+ export const Spinner: IconDefinition = memo(({ size }) => (
5
+ <svg width={size} height={size} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
6
+ <style
7
+ // eslint-disable-next-line react/no-danger
8
+ dangerouslySetInnerHTML={{
9
+ __html: '.spinner_ajPY{transform-origin:center;animation:spinner_AtaB .75s infinite linear}@keyframes spinner_AtaB{100%{transform:rotate(360deg)}}',
10
+ }}
11
+ />
12
+ <path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25" />
13
+ <path d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" className="spinner_ajPY" />
14
+ </svg>
15
+ ));
@@ -3,3 +3,4 @@ export { Close } from './Close';
3
3
  export { ChevronLeft } from './ChevronLeft';
4
4
  export { ChevronRight } from './ChevronRight';
5
5
  export { Ellipsis } from './Ellipsis';
6
+ export { Spinner } from './Spinner';
@@ -102,6 +102,7 @@
102
102
  }
103
103
 
104
104
  .enhancer {
105
+ display: flex;
105
106
  color: var(--pte-colors-contentSecondary);
106
107
  margin-block-start: -0.5px;
107
108
  font-size: var(--pte-typography-styles-paragraphSmall-fontSize);
@@ -0,0 +1,22 @@
1
+ @keyframes intro {
2
+ 0% {
3
+ opacity: 0;
4
+ }
5
+ 100% {
6
+ opacity: 1;
7
+ }
8
+ }
9
+
10
+
11
+ .trigger {
12
+ width: fit-content;
13
+ }
14
+
15
+ .content {
16
+ background-color: var(--pte-colors-backgroundPrimary);
17
+ animation: var(--pte-animations-duration-normal) var(--pte-animations-timing-easeInOutExpo) 0s 1 intro;
18
+ width: fit-content;
19
+ border-radius: var(--pte-borders-radius-rounded);
20
+ box-shadow: var(--pte-lighting-shallowPopup);
21
+ border: 1px solid var(--pte-borders-dropdown-color)
22
+ }
@@ -0,0 +1,24 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { createElement } from 'react';
3
+ import { Popover } from './Popover';
4
+ import { Button } from '../button';
5
+
6
+ const meta: Meta<typeof Popover> = {
7
+ title: 'Surfaces/Popover',
8
+ component: Popover,
9
+ tags: ['autodocs'],
10
+ };
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof Popover>;
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ trigger: createElement(
18
+ Button,
19
+ {},
20
+ 'Click me',
21
+ ),
22
+ children: 'Hello world!',
23
+ },
24
+ };
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import type {
4
+ ComponentPropsWithoutRef, FC, ReactElement, ReactNode,
5
+ } from 'react';
6
+ import type { PopoverProps as RTPopoverProps } from 'react-tiny-popover';
7
+ import { Popover as RTPopover } from 'react-tiny-popover';
8
+ import { forwardRef, useState } from 'react';
9
+ import clsx from 'clsx';
10
+ import styles from './Popover.module.scss';
11
+ import typography from '../text/Typography.module.css';
12
+
13
+ // TODO(URGENT): Figure out how to properly handle accessibility; the popover content should capture focus for tabbing and clicking, and `esc` should allow closing the popover. Might be better to use Radix popover instead of react-tiny-popover?
14
+
15
+ export type PopoverProps = {
16
+ /** The trigger element for the Popover. */
17
+ trigger: ReactElement;
18
+ /** Optionally, manage state for the Popover yourself by passing `isOpen` and `setIsOpen` props. */
19
+ isOpen?: boolean;
20
+ /** Optionally, manage state for the Popover yourself by passing `isOpen` and `setIsOpen` props. */
21
+ setIsOpen?: (isOpen: boolean) => void;
22
+ /** The contents of the Popover. */
23
+ children?: ReactNode;
24
+ } & Omit<RTPopoverProps, 'content' | 'children' | 'isOpen'>;
25
+
26
+ interface TriggerProps extends ComponentPropsWithoutRef<'div'> {
27
+ onClick: () => void;
28
+ }
29
+ const Trigger = forwardRef<HTMLDivElement, TriggerProps>((props, ref) => (
30
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
31
+ <div className={styles.trigger} ref={ref} onClick={props.onClick}>
32
+ {props.children}
33
+ </div>
34
+ ));
35
+
36
+ /**
37
+ * A Popover component, based on [`react-tiny-popover`](https://github.com/alexkatz/react-tiny-popover). Refer to its docs for more information on positioning props.
38
+ *
39
+ * <hr />
40
+ *
41
+ * To use this component:
42
+ *
43
+ * ```js
44
+ * import { Popover } from 'paris/popover';
45
+ *
46
+ * <Popover trigger={<button>Trigger</button>}>
47
+ * <p>Content</p>
48
+ * </Popover>
49
+ * ```
50
+ * @constructor
51
+ */
52
+ export const Popover: FC<PopoverProps> = ({
53
+ trigger,
54
+ children,
55
+ padding,
56
+ positions,
57
+ align,
58
+ isOpen,
59
+ setIsOpen,
60
+ ...props
61
+ }) => {
62
+ const [open, setOpen] = useState(false);
63
+
64
+ return (
65
+ <RTPopover
66
+ positions={positions || ['bottom', 'top', 'left', 'right']}
67
+ align={align || 'start'}
68
+ padding={padding || 8}
69
+ isOpen={typeof isOpen === 'undefined' ? open : isOpen}
70
+ containerClassName={clsx(typography.paragraphSmall, styles.content)}
71
+ content={(
72
+ <>
73
+ {children}
74
+ </>
75
+ )}
76
+ onClickOutside={() => {
77
+ setIsOpen?.(false);
78
+ setOpen(false);
79
+ }}
80
+ {...props}
81
+ >
82
+ <Trigger
83
+ onClick={() => {
84
+ setIsOpen?.(!isOpen);
85
+ setOpen((cur) => !cur);
86
+ }}
87
+ >
88
+ {trigger}
89
+ </Trigger>
90
+ </RTPopover>
91
+ );
92
+ };
@@ -0,0 +1 @@
1
+ export * from './Popover';
@@ -62,13 +62,67 @@
62
62
  background-color: var(--pte-colors-backgroundTertiary);
63
63
  }
64
64
 
65
- &[data-disabled=true] {
66
- color: var(--pte-colors-contentDisabled);
65
+ &[data-disabled=true], &[data-headlessui-state~="disabled"] {
67
66
  pointer-events: none;
68
67
  cursor: default;
68
+
69
+ &, & * {
70
+ color: var(--pte-colors-contentDisabled);
71
+ }
69
72
  }
70
73
  }
71
74
 
72
75
  .content {
73
76
  width: 100%;
74
77
  }
78
+
79
+ .radioContainer {
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 12px;
83
+ justify-content: flex-start;
84
+ align-items: flex-start;
85
+ }
86
+
87
+ .radioOption {
88
+ display: flex;
89
+ flex-direction: row;
90
+ gap: 4px;
91
+ justify-content: flex-start;
92
+ align-items: center;
93
+
94
+ &[data-headlessui-state~="active"] {
95
+ .radioCircle {
96
+ border-color: var(--pte-colors-contentPrimary);
97
+ }
98
+ }
99
+
100
+ &[data-headlessui-state~="checked"] {
101
+ .radioCircle {
102
+ border: 5px solid var(--pte-colors-contentPrimary);
103
+ }
104
+ }
105
+
106
+ &[data-headlessui-state~="disabled"] {
107
+ pointer-events: none;
108
+ cursor: default;
109
+
110
+ &, & * {
111
+ color: var(--pte-colors-contentDisabled);
112
+ }
113
+
114
+ .radioCircle {
115
+ border-color: var(--pte-colors-contentDisabled);
116
+ }
117
+ }
118
+ }
119
+
120
+ .radioCircle {
121
+ flex-shrink: 0;
122
+ margin: 0 8px;
123
+ width: 14px;
124
+ height: 14px;
125
+ border-radius: 100%;
126
+ border: 1.5px solid var(--pte-colors-contentTertiary);
127
+ transition: all var(--pte-animations-interaction);
128
+ }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import type { ReactNode, ComponentPropsWithoutRef } from 'react';
4
- import { Listbox, Transition } from '@headlessui/react';
4
+ import { Listbox, RadioGroup, Transition } from '@headlessui/react';
5
5
  import clsx from 'clsx';
6
6
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7
7
  import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
@@ -9,16 +9,19 @@ import { useId } from 'react';
9
9
  import inputStyles from '../input/Input.module.scss';
10
10
  import dropdownStyles from '../utility/Dropdown.module.scss';
11
11
  import styles from './Select.module.scss';
12
+ import typography from '../text/Typography.module.css';
12
13
  import type { TextProps } from '../text';
13
14
  import { Text } from '../text';
14
15
  import type { InputProps } from '../input';
15
16
  import { MemoizedEnhancer } from '../../helpers/renderEnhancer';
16
17
  import { pget, theme } from '../theme';
17
18
  import { Field } from '../field';
19
+ import { TextWhenString } from '../utility';
18
20
 
19
21
  export type Option<T = Record<string, any>> = {
20
22
  id: string,
21
23
  node: ReactNode,
24
+ disabled?: boolean,
22
25
  metadata?: T,
23
26
  };
24
27
  export type SelectProps<T = Record<string, any>> = {
@@ -40,6 +43,11 @@ export type SelectProps<T = Record<string, any>> = {
40
43
  * The interaction handler for the Select.
41
44
  */
42
45
  onChange?: (value: Option<T>['id'] | null) => void | Promise<void>;
46
+ /**
47
+ * The visual variant of the Select. Listboxes will render as a dropdown menu, and radios will render as a radio group.
48
+ * @default listbox
49
+ */
50
+ kind?: 'listbox' | 'radio';
43
51
  /**
44
52
  * Prop overrides for other rendered elements. Overrides for the input itself should be passed directly to the component.
45
53
  */
@@ -80,6 +88,7 @@ export function Select<T = Record<string, any>>({
80
88
  startEnhancer,
81
89
  endEnhancer,
82
90
  disabled,
91
+ kind = 'listbox',
83
92
  overrides,
84
93
  }: SelectProps<T>) {
85
94
  const inputID = useId();
@@ -97,80 +106,103 @@ export function Select<T = Record<string, any>>({
97
106
  description: overrides?.description,
98
107
  }}
99
108
  >
100
- <Listbox
101
- as="div"
102
- value={value}
103
- onChange={onChange}
104
- >
105
- <Listbox.Button
106
- id={inputID}
107
- {...overrides?.selectInput}
108
- aria-disabled={disabled}
109
- data-status={disabled ? 'disabled' : (status || 'default')}
110
- className={clsx(
111
- overrides?.selectInput?.className,
112
- inputStyles.inputContainer,
113
- styles.field,
114
- )}
115
- >
116
- {!!startEnhancer && (
117
- <div {...overrides?.startEnhancerContainer} className={clsx(inputStyles.enhancer, overrides?.startEnhancerContainer?.className)}>
118
- {!!startEnhancer && (
119
- <MemoizedEnhancer
120
- enhancer={startEnhancer}
121
- size={parseInt(pget('typography.styles.paragraphSmall.fontSize') || theme.typography.styles.paragraphSmall.fontSize, 10)}
122
- />
123
- )}
124
- </div>
125
- )}
126
- {options?.find((o) => o.id === value)?.node || placeholder || 'Select an option'}
127
- {endEnhancer ? (
128
- <div {...overrides?.endEnhancerContainer} className={clsx(inputStyles.enhancer, overrides?.endEnhancerContainer?.className)}>
129
- {!!endEnhancer && (
130
- <MemoizedEnhancer
131
- enhancer={endEnhancer}
132
- size={parseInt(pget('typography.styles.paragraphSmall.fontSize') || theme.typography.styles.paragraphSmall.fontSize, 10)}
133
- />
134
- )}
135
- </div>
136
- ) : (
137
- <FontAwesomeIcon className={inputStyles.enhancer} width="10px" icon={faChevronDown} />
138
- )}
139
- </Listbox.Button>
140
- <Transition
141
- enter={dropdownStyles.transition}
142
- enterFrom={dropdownStyles.enterFrom}
143
- enterTo={dropdownStyles.enterTo}
144
- leave={dropdownStyles.transition}
145
- leaveFrom={dropdownStyles.leaveFrom}
146
- leaveTo={dropdownStyles.leaveTo}
109
+ {kind === 'listbox' && (
110
+ <Listbox
111
+ as="div"
112
+ value={value}
113
+ onChange={onChange}
147
114
  >
148
- <Listbox.Options
115
+ <Listbox.Button
116
+ id={inputID}
117
+ {...overrides?.selectInput}
118
+ aria-disabled={disabled}
119
+ data-status={disabled ? 'disabled' : (status || 'default')}
149
120
  className={clsx(
150
- overrides?.optionsContainer,
151
- styles.options,
121
+ overrides?.selectInput?.className,
122
+ inputStyles.inputContainer,
123
+ styles.field,
152
124
  )}
153
125
  >
154
- {(options || []).map((option) => (
155
- <Listbox.Option
156
- key={option.id}
157
- value={option.id}
158
- data-selected={option.id === value}
159
- className={clsx(
160
- overrides?.option,
161
- styles.option,
126
+ {!!startEnhancer && (
127
+ <div {...overrides?.startEnhancerContainer} className={clsx(inputStyles.enhancer, overrides?.startEnhancerContainer?.className)}>
128
+ {!!startEnhancer && (
129
+ <MemoizedEnhancer
130
+ enhancer={startEnhancer}
131
+ size={parseInt(pget('typography.styles.paragraphSmall.fontSize') || theme.typography.styles.paragraphSmall.fontSize, 10)}
132
+ />
162
133
  )}
163
- >
164
- {typeof option.node === 'string' ? (
165
- <Text as="span" kind="paragraphSmall">
166
- {option.node}
167
- </Text>
168
- ) : option.node}
169
- </Listbox.Option>
170
- ))}
171
- </Listbox.Options>
172
- </Transition>
173
- </Listbox>
134
+ </div>
135
+ )}
136
+ {options?.find((o) => o.id === value)?.node || placeholder || 'Select an option'}
137
+ {endEnhancer ? (
138
+ <div {...overrides?.endEnhancerContainer} className={clsx(inputStyles.enhancer, overrides?.endEnhancerContainer?.className)}>
139
+ {!!endEnhancer && (
140
+ <MemoizedEnhancer
141
+ enhancer={endEnhancer}
142
+ size={parseInt(pget('typography.styles.paragraphSmall.fontSize') || theme.typography.styles.paragraphSmall.fontSize, 10)}
143
+ />
144
+ )}
145
+ </div>
146
+ ) : (
147
+ <FontAwesomeIcon className={inputStyles.enhancer} width="10px" icon={faChevronDown} />
148
+ )}
149
+ </Listbox.Button>
150
+ <Transition
151
+ enter={dropdownStyles.transition}
152
+ enterFrom={dropdownStyles.enterFrom}
153
+ enterTo={dropdownStyles.enterTo}
154
+ leave={dropdownStyles.transition}
155
+ leaveFrom={dropdownStyles.leaveFrom}
156
+ leaveTo={dropdownStyles.leaveTo}
157
+ >
158
+ <Listbox.Options
159
+ className={clsx(
160
+ overrides?.optionsContainer,
161
+ styles.options,
162
+ )}
163
+ >
164
+ {(options || []).map((option) => (
165
+ <Listbox.Option
166
+ key={option.id}
167
+ value={option.id}
168
+ data-selected={option.id === value}
169
+ className={clsx(
170
+ overrides?.option,
171
+ styles.option,
172
+ )}
173
+ disabled={option.disabled || false}
174
+ >
175
+ {typeof option.node === 'string' ? (
176
+ <Text as="span" kind="paragraphSmall">
177
+ {option.node}
178
+ </Text>
179
+ ) : option.node}
180
+ </Listbox.Option>
181
+ ))}
182
+ </Listbox.Options>
183
+ </Transition>
184
+ </Listbox>
185
+ )}
186
+ {kind === 'radio' && (
187
+ <RadioGroup as="div" className={styles.radioContainer} value={value} onChange={onChange}>
188
+ {options.map((option) => (
189
+ <RadioGroup.Option
190
+ as="div"
191
+ className={clsx(
192
+ styles.radioOption,
193
+ )}
194
+ key={option.id}
195
+ value={option.id}
196
+ disabled={option.disabled || false}
197
+ >
198
+ <div className={styles.radioCircle} />
199
+ <TextWhenString kind="paragraphXSmall">
200
+ {option.node}
201
+ </TextWhenString>
202
+ </RadioGroup.Option>
203
+ ))}
204
+ </RadioGroup>
205
+ )}
174
206
  </Field>
175
207
  );
176
208
  }
@@ -107,6 +107,10 @@ export function Table<RowData extends Record<string, any>[]>({
107
107
  onClick={() => {
108
108
  if (clickableRows) onRowClick?.(rows[index]);
109
109
  }}
110
+ onKeyDown={(e) => {
111
+ if (clickableRows && (e.key === 'Enter' || e.key === ' ')) onRowClick?.(rows[index]);
112
+ }}
113
+ tabIndex={clickableRows ? 0 : undefined}
110
114
  {...overrides?.trBody}
111
115
  className={clsx(
112
116
  clickableRows && styles.clickable,
@@ -1,10 +1,11 @@
1
- import type { ComponentProps, FC, ReactNode } from 'react';
2
- import { createElement } from 'react';
1
+ /* eslint-disable prefer-arrow-callback */
2
+ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
3
+ import { createElement, memo } from 'react';
3
4
  import clsx from 'clsx';
4
5
  import type { CSSColor } from '@ssh/csstypes';
5
6
  import typography from './Typography.module.css';
6
7
  import styles from './Text.module.scss';
7
- import type { LightTheme, Theme } from '../theme';
8
+ import type { LightTheme } from '../theme';
8
9
 
9
10
  export type TextElement = 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label' | 'legend' | 'caption' | 'small';
10
11
  export type GlobalCSSValues = 'inherit' | 'initial' | 'revert' | 'revert-layer' | 'unset';
@@ -39,7 +40,7 @@ export type TextProps<T extends TextElement = TextElement> = {
39
40
 
40
41
  /** The contents of the Text element. */
41
42
  children: ReactNode;
42
- } & ComponentProps<T>;
43
+ } & ComponentPropsWithoutRef<T>;
43
44
 
44
45
  /**
45
46
  * A `Text` component is used to render text with one of our theme formats.
@@ -61,7 +62,7 @@ export type TextProps<T extends TextElement = TextElement> = {
61
62
  * ```
62
63
  * @constructor
63
64
  */
64
- export function Text<T extends TextElement>({
65
+ export const Text = memo(function TextComponent<T extends TextElement>({
65
66
  kind,
66
67
  as,
67
68
  weight,
@@ -89,4 +90,4 @@ export function Text<T extends TextElement>({
89
90
  },
90
91
  children,
91
92
  );
92
- }
93
+ });
@@ -1,6 +1,6 @@
1
- import type { FC, ReactNode } from 'react';
1
+ import type { FC } from 'react';
2
2
  import type { ToasterProps } from 'react-hot-toast';
3
- import { toast, Toaster } from 'react-hot-toast';
3
+ import { Toaster } from 'react-hot-toast';
4
4
  import clsx from 'clsx';
5
5
  import styles from './Toast.module.scss';
6
6
  import typography from '../text/Typography.module.css';
@@ -46,6 +46,4 @@ export const Toast: FC<ToastProps> = ({
46
46
  />
47
47
  );
48
48
 
49
- export {
50
- toast,
51
- };
49
+ export * from 'react-hot-toast';
@@ -10,7 +10,7 @@ export const TextWhenString = memo<PropsWithChildren<TextProps<TextElement>>>(({
10
10
  children,
11
11
  ...props
12
12
  }) => {
13
- if (typeof children === 'string') {
13
+ if (typeof children === 'string' || typeof children === 'number') {
14
14
  return (
15
15
  <Text
16
16
  {...props}