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 +11 -0
- package/package.json +7 -5
- package/src/stories/avatar/Avatar.module.scss +1 -0
- package/src/stories/avatar/Avatar.stories.ts +10 -8
- package/src/stories/avatar/Avatar.tsx +30 -15
- package/src/stories/button/Button.tsx +14 -3
- package/src/stories/callout/Callout.tsx +7 -3
- package/src/stories/checkbox/Checkbox.tsx +6 -2
- package/src/stories/field/Field.tsx +2 -0
- package/src/stories/icon/Spinner.tsx +15 -0
- package/src/stories/icon/index.ts +1 -0
- package/src/stories/input/Input.module.scss +1 -0
- package/src/stories/popover/Popover.module.scss +22 -0
- package/src/stories/popover/Popover.stories.ts +24 -0
- package/src/stories/popover/Popover.tsx +92 -0
- package/src/stories/popover/index.ts +1 -0
- package/src/stories/select/Select.module.scss +56 -2
- package/src/stories/select/Select.tsx +102 -70
- package/src/stories/table/Table.tsx +4 -0
- package/src/stories/text/Text.tsx +7 -6
- package/src/stories/toast/Toast.tsx +3 -5
- package/src/stories/utility/TextWhenString.tsx +1 -1
package/CHANGELOG.md
CHANGED
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.
|
|
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",
|
|
@@ -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: '
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 {
|
|
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
|
|
9
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
{
|
|
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 {
|
|
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
|
+
));
|
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
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?.
|
|
151
|
-
|
|
121
|
+
overrides?.selectInput?.className,
|
|
122
|
+
inputStyles.inputContainer,
|
|
123
|
+
styles.field,
|
|
152
124
|
)}
|
|
153
125
|
>
|
|
154
|
-
{
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
} &
|
|
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
|
|
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
|
|
1
|
+
import type { FC } from 'react';
|
|
2
2
|
import type { ToasterProps } from 'react-hot-toast';
|
|
3
|
-
import {
|
|
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}
|