paris 0.2.1 → 0.3.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 +23 -0
- package/README.md +61 -5
- package/package.json +28 -13
- package/src/helpers/renderEnhancer.tsx +21 -0
- package/src/stories/Tokens.mdx +0 -8
- package/src/stories/button/Button.module.scss +12 -6
- package/src/stories/button/Button.stories.ts +17 -0
- package/src/stories/button/Button.tsx +48 -11
- package/src/stories/dropdown/Dropdown.module.scss +23 -0
- package/src/stories/input/Input.module.scss +134 -0
- package/src/stories/input/Input.stories.ts +87 -0
- package/src/stories/input/Input.tsx +172 -0
- package/src/stories/input/index.ts +1 -0
- package/src/stories/select/Select.module.scss +70 -0
- package/src/stories/select/Select.stories.ts +71 -0
- package/src/stories/select/Select.tsx +103 -0
- package/src/stories/select/index.ts +1 -0
- package/src/stories/text/Text.module.scss +1 -1
- package/src/stories/text/Text.tsx +36 -14
- package/src/stories/textarea/TextArea.stories.ts +19 -0
- package/src/stories/textarea/TextArea.tsx +120 -0
- package/src/stories/textarea/index.ts +1 -0
- package/src/stories/theme/global.scss +2 -0
- package/src/stories/theme/index.ts +1 -0
- package/src/stories/theme/themes.ts +52 -6
- package/src/stories/theme/util.scss +8 -0
- package/src/types/Enhancer.ts +3 -0
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.eslintrc.json +0 -22
- package/.github/workflows/publish.yml +0 -54
- package/.husky/pre-commit +0 -2
- package/.idea/inspectionProfiles/Project_Default.xml +0 -7
- package/.idea/jsLibraryMappings.xml +0 -6
- package/.idea/jsLinters/eslint.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/paris.iml +0 -13
- package/.idea/vcs.xml +0 -6
- package/.idea/watcherTasks.xml +0 -4
- package/.storybook/main.ts +0 -43
- package/.storybook/manager-head.html +0 -16
- package/.storybook/manager.ts +0 -6
- package/.storybook/preview.ts +0 -74
- package/.storybook/themes.ts +0 -30
- package/cat +0 -2
- package/global.d.ts +0 -2
- package/next.config.js +0 -6
- package/public/favicon.ico +0 -0
- package/public/fira/fira_code.css +0 -48
- package/public/fira/woff/FiraCode-Bold.woff +0 -0
- package/public/fira/woff/FiraCode-Light.woff +0 -0
- package/public/fira/woff/FiraCode-Medium.woff +0 -0
- package/public/fira/woff/FiraCode-Regular.woff +0 -0
- package/public/fira/woff/FiraCode-SemiBold.woff +0 -0
- package/public/fira/woff/FiraCode-VF.woff +0 -0
- package/public/fira/woff2/FiraCode-Bold.woff2 +0 -0
- package/public/fira/woff2/FiraCode-Light.woff2 +0 -0
- package/public/fira/woff2/FiraCode-Medium.woff2 +0 -0
- package/public/fira/woff2/FiraCode-Regular.woff2 +0 -0
- package/public/fira/woff2/FiraCode-SemiBold.woff2 +0 -0
- package/public/fira/woff2/FiraCode-VF.woff2 +0 -0
- package/public/graphik/GraphikSS-Black.woff +0 -0
- package/public/graphik/GraphikSS-Black.woff2 +0 -0
- package/public/graphik/GraphikSS-BlackItalic.woff +0 -0
- package/public/graphik/GraphikSS-BlackItalic.woff2 +0 -0
- package/public/graphik/GraphikSS-Bold.woff +0 -0
- package/public/graphik/GraphikSS-Bold.woff2 +0 -0
- package/public/graphik/GraphikSS-BoldItalic.woff +0 -0
- package/public/graphik/GraphikSS-BoldItalic.woff2 +0 -0
- package/public/graphik/GraphikSS-Extralight.woff +0 -0
- package/public/graphik/GraphikSS-Extralight.woff2 +0 -0
- package/public/graphik/GraphikSS-ExtralightItalic.woff +0 -0
- package/public/graphik/GraphikSS-ExtralightItalic.woff2 +0 -0
- package/public/graphik/GraphikSS-Light.woff +0 -0
- package/public/graphik/GraphikSS-Light.woff2 +0 -0
- package/public/graphik/GraphikSS-LightItalic.woff +0 -0
- package/public/graphik/GraphikSS-LightItalic.woff2 +0 -0
- package/public/graphik/GraphikSS-Medium.woff +0 -0
- package/public/graphik/GraphikSS-Medium.woff2 +0 -0
- package/public/graphik/GraphikSS-MediumItalic.woff +0 -0
- package/public/graphik/GraphikSS-MediumItalic.woff2 +0 -0
- package/public/graphik/GraphikSS-Regular.woff +0 -0
- package/public/graphik/GraphikSS-Regular.woff2 +0 -0
- package/public/graphik/GraphikSS-RegularItalic.woff +0 -0
- package/public/graphik/GraphikSS-RegularItalic.woff2 +0 -0
- package/public/graphik/GraphikSS-Semibold.woff +0 -0
- package/public/graphik/GraphikSS-Semibold.woff2 +0 -0
- package/public/graphik/GraphikSS-SemiboldItalic.woff +0 -0
- package/public/graphik/GraphikSS-SemiboldItalic.woff2 +0 -0
- package/public/graphik/GraphikSS-Super.woff +0 -0
- package/public/graphik/GraphikSS-Super.woff2 +0 -0
- package/public/graphik/GraphikSS-SuperItalic.woff +0 -0
- package/public/graphik/GraphikSS-SuperItalic.woff2 +0 -0
- package/public/graphik/GraphikSS-Thin.woff +0 -0
- package/public/graphik/GraphikSS-Thin.woff2 +0 -0
- package/public/graphik/GraphikSS-ThinItalic.woff +0 -0
- package/public/graphik/GraphikSS-ThinItalic.woff2 +0 -0
- package/public/graphik/graphik.css +0 -174
- package/public/next.svg +0 -1
- package/public/pte.css +0 -219
- package/public/thirteen.svg +0 -1
- package/public/vercel.svg +0 -1
- package/scripts/createComponent.js +0 -100
- package/scripts/generateEntry.js +0 -35
- package/scripts/text.ts +0 -118
- package/src/styles/util.scss +0 -4
- package/tsconfig.json +0 -27
- /package/src/{styles → stories/theme}/tw-preflight.css +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
import type { FC, ComponentPropsWithoutRef } from 'react';
|
|
5
|
+
import clsx from 'clsx';
|
|
6
|
+
import styles from './Input.module.scss';
|
|
7
|
+
import type { TextProps } from '../text';
|
|
8
|
+
import { Text } from '../text';
|
|
9
|
+
import type { Enhancer } from '../../types/Enhancer';
|
|
10
|
+
import { pget, theme } from '../theme';
|
|
11
|
+
import { MemoizedEnhancer } from '../../helpers/renderEnhancer';
|
|
12
|
+
|
|
13
|
+
export type InputProps = {
|
|
14
|
+
/**
|
|
15
|
+
* A label for the input field. This is required for accessibility, but can be visually hidden using the `hideLabel` prop.
|
|
16
|
+
*/
|
|
17
|
+
label: string;
|
|
18
|
+
/**
|
|
19
|
+
* The status of the input field.
|
|
20
|
+
* @default default
|
|
21
|
+
*/
|
|
22
|
+
status?: 'default' | 'error' | 'success';
|
|
23
|
+
/**
|
|
24
|
+
* The input type. All HTML5 input types are supported, but some may require additional props to be set for full accessibility or aesthetic.
|
|
25
|
+
* @default text
|
|
26
|
+
*/
|
|
27
|
+
type?: 'button' | 'checkbox' | 'color' | 'date' | 'datetime-local' | 'email' | 'file' | 'hidden' | 'image' | 'month' | 'number' | 'password' | 'radio' | 'range' | 'reset' | 'search' | 'submit' | 'tel' | 'text' | 'time' | 'url' | 'week';
|
|
28
|
+
/**
|
|
29
|
+
* Visually hide the label (while keeping it accessible to screen readers).
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
hideLabel?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* A description of the input field. Can be visually hidden using the `hideDescription` prop.
|
|
35
|
+
*/
|
|
36
|
+
description?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Visually hide the description while keeping it accessible to screen readers.
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
hideDescription?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The placeholder for the input.
|
|
44
|
+
*/
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
/**
|
|
47
|
+
* An icon or other element to render before the Input element. A `{ size }` argument is passed that should be used to determine the width & height of the content displayed.
|
|
48
|
+
*/
|
|
49
|
+
startEnhancer?: Enhancer;
|
|
50
|
+
/**
|
|
51
|
+
* An icon or other element to render after the Input element. A `{ size }` argument is passed that should be used to determine the width & height of the content displayed.
|
|
52
|
+
*/
|
|
53
|
+
endEnhancer?: Enhancer;
|
|
54
|
+
/**
|
|
55
|
+
* Disables the input, disallowing user interaction.
|
|
56
|
+
*/
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Prop overrides for other rendered elements. Overrides for the input itself should be passed directly to the component.
|
|
60
|
+
*/
|
|
61
|
+
overrides?: {
|
|
62
|
+
container?: ComponentPropsWithoutRef<'div'>;
|
|
63
|
+
label?: TextProps<'label'>;
|
|
64
|
+
description?: TextProps<'p'>;
|
|
65
|
+
startEnhancerContainer?: ComponentPropsWithoutRef<'div'>;
|
|
66
|
+
endEnhancerContainer?: ComponentPropsWithoutRef<'div'>;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* An `Input` is used to collect user input, such as text, numbers, or dates.
|
|
72
|
+
*
|
|
73
|
+
* > `overrides` available: `container`, `label`, `description`, `startEnhancerContainer`, `endEnhancerContainer`
|
|
74
|
+
*
|
|
75
|
+
* <hr />
|
|
76
|
+
*
|
|
77
|
+
* To use the `Input` component, import it as follows:
|
|
78
|
+
*
|
|
79
|
+
* ```js
|
|
80
|
+
* import { Input } from 'paris/input';
|
|
81
|
+
* ```
|
|
82
|
+
* @constructor
|
|
83
|
+
*/
|
|
84
|
+
export const Input: FC<InputProps & ComponentPropsWithoutRef<'input'>> = ({ status, disabled, ...props }) => {
|
|
85
|
+
const inputID = useId();
|
|
86
|
+
return (
|
|
87
|
+
// Disable a11y rules because the container doesn't need to be focusable for screen readers; the input itself should receive focus instead.
|
|
88
|
+
// The container is only made clickable for usability purposes.
|
|
89
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
|
90
|
+
<div
|
|
91
|
+
{...props.overrides?.container}
|
|
92
|
+
className={clsx(
|
|
93
|
+
props.overrides?.container?.className,
|
|
94
|
+
styles.container,
|
|
95
|
+
)}
|
|
96
|
+
onClick={(e) => {
|
|
97
|
+
if (disabled) e.preventDefault();
|
|
98
|
+
if (typeof window !== 'undefined') {
|
|
99
|
+
const input = document.getElementById(inputID);
|
|
100
|
+
if (input && !disabled) {
|
|
101
|
+
input.focus();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<Text
|
|
107
|
+
{...props.overrides?.label}
|
|
108
|
+
as="label"
|
|
109
|
+
kind="paragraphSmall"
|
|
110
|
+
htmlFor={inputID}
|
|
111
|
+
className={clsx(
|
|
112
|
+
styles.label,
|
|
113
|
+
{ [styles.hidden]: props.hideLabel },
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
{props.label}
|
|
117
|
+
</Text>
|
|
118
|
+
<div
|
|
119
|
+
className={styles.inputContainer}
|
|
120
|
+
data-status={status}
|
|
121
|
+
data-disabled={disabled}
|
|
122
|
+
>
|
|
123
|
+
{!!props.startEnhancer && (
|
|
124
|
+
<div {...props.overrides?.startEnhancerContainer} className={clsx(styles.enhancer, props.overrides?.startEnhancerContainer?.className)}>
|
|
125
|
+
{!!props.startEnhancer && (
|
|
126
|
+
<MemoizedEnhancer
|
|
127
|
+
enhancer={props.startEnhancer}
|
|
128
|
+
size={parseInt(pget('typography.styles.paragraphSmall.fontSize') || theme.typography.styles.paragraphSmall.fontSize, 10)}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
<input
|
|
134
|
+
{...props}
|
|
135
|
+
id={inputID}
|
|
136
|
+
type={props.type || 'text'}
|
|
137
|
+
aria-label={props.label}
|
|
138
|
+
aria-describedby={`${inputID}-description`}
|
|
139
|
+
aria-disabled={disabled}
|
|
140
|
+
readOnly={disabled}
|
|
141
|
+
className={clsx(
|
|
142
|
+
props.className,
|
|
143
|
+
styles.input,
|
|
144
|
+
)}
|
|
145
|
+
/>
|
|
146
|
+
{!!props.endEnhancer && (
|
|
147
|
+
<div {...props.overrides?.endEnhancerContainer} className={clsx(styles.enhancer, props.overrides?.endEnhancerContainer?.className)}>
|
|
148
|
+
{!!props.endEnhancer && (
|
|
149
|
+
<MemoizedEnhancer
|
|
150
|
+
enhancer={props.endEnhancer}
|
|
151
|
+
size={parseInt(pget('typography.styles.paragraphSmall.fontSize') || theme.typography.styles.paragraphSmall.fontSize, 10)}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
<Text
|
|
158
|
+
id={`${inputID}-description`}
|
|
159
|
+
{...props.overrides?.description}
|
|
160
|
+
as="p"
|
|
161
|
+
kind="paragraphXSmall"
|
|
162
|
+
className={clsx(
|
|
163
|
+
styles.description,
|
|
164
|
+
{ [styles.hidden]: !props.description || props.hideDescription },
|
|
165
|
+
props.overrides?.description?.className,
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
{props.description}
|
|
169
|
+
</Text>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Input';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
position: relative;
|
|
3
|
+
user-select: none;
|
|
4
|
+
|
|
5
|
+
& > * {
|
|
6
|
+
cursor: default;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.field {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: row;
|
|
13
|
+
justify-content: space-between;
|
|
14
|
+
align-items: center;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.options {
|
|
18
|
+
position: absolute;
|
|
19
|
+
top: 100%;
|
|
20
|
+
left: 0;
|
|
21
|
+
right: 0;
|
|
22
|
+
z-index: 10;
|
|
23
|
+
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
|
|
27
|
+
background-color: var(--pte-colors-backgroundPrimary);
|
|
28
|
+
|
|
29
|
+
border-width: 1px;
|
|
30
|
+
border-style: solid;
|
|
31
|
+
border-radius: var(--pte-borders-radius-rectangle);
|
|
32
|
+
border-color: var(--pte-borders-dropdown-color);
|
|
33
|
+
box-shadow: var(--pte-borders-dropdown-shadow);
|
|
34
|
+
|
|
35
|
+
transition: var(--pte-animations-interaction);
|
|
36
|
+
|
|
37
|
+
&:focus-within {
|
|
38
|
+
outline: none;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.option {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: row;
|
|
45
|
+
justify-content: flex-start;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 8px;
|
|
48
|
+
|
|
49
|
+
padding: 8px 16px;
|
|
50
|
+
margin: 0 -1px;
|
|
51
|
+
|
|
52
|
+
border-left: 1px solid var(--pte-borders-dropdown-color);
|
|
53
|
+
border-right: 1px solid var(--pte-borders-dropdown-color);
|
|
54
|
+
|
|
55
|
+
transition: var(--pte-animations-interaction);
|
|
56
|
+
|
|
57
|
+
&:hover, &[data-headlessui-state="active"] {
|
|
58
|
+
background-color: var(--pte-colors-backgroundSecondary);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
&[data-selected=true] {
|
|
62
|
+
background-color: var(--pte-colors-backgroundTertiary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&[data-disabled=true] {
|
|
66
|
+
color: var(--pte-colors-contentDisabled);
|
|
67
|
+
pointer-events: none;
|
|
68
|
+
cursor: default;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { createElement, useState } from 'react';
|
|
3
|
+
import { Select } from './Select';
|
|
4
|
+
import { Text } from '../text';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Select> = {
|
|
7
|
+
title: 'Inputs/Select',
|
|
8
|
+
component: Select,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof Select>;
|
|
14
|
+
|
|
15
|
+
const render: Story['render'] = (args) => {
|
|
16
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
17
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
18
|
+
return createElement('div', {
|
|
19
|
+
style: { minHeight: '200px' },
|
|
20
|
+
}, createElement(Select, {
|
|
21
|
+
...args,
|
|
22
|
+
value: selected,
|
|
23
|
+
onChange: (e) => setSelected(e),
|
|
24
|
+
}));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
options: [
|
|
30
|
+
{ id: '1', node: 'Option 1' },
|
|
31
|
+
{ id: '2', node: 'Option 2' },
|
|
32
|
+
{ id: '3', node: 'Option 3' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
render,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const WithCustomNodes: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
options: [
|
|
41
|
+
{
|
|
42
|
+
id: '1',
|
|
43
|
+
// eslint-disable-next-line react/no-children-prop
|
|
44
|
+
node: createElement(Text, {
|
|
45
|
+
as: 'span',
|
|
46
|
+
kind: 'displaySmall',
|
|
47
|
+
children: 'Option 1',
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: '2',
|
|
52
|
+
// eslint-disable-next-line react/no-children-prop
|
|
53
|
+
node: createElement(Text, {
|
|
54
|
+
as: 'span',
|
|
55
|
+
kind: 'paragraphXXSmall',
|
|
56
|
+
children: 'Option 2',
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: '3',
|
|
61
|
+
// eslint-disable-next-line react/no-children-prop
|
|
62
|
+
node: createElement(Text, {
|
|
63
|
+
as: 'span',
|
|
64
|
+
kind: 'labelXLarge',
|
|
65
|
+
children: 'Option 3',
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
render,
|
|
71
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { FC, ReactNode } from 'react';
|
|
4
|
+
import { Listbox, Transition } from '@headlessui/react';
|
|
5
|
+
import clsx from 'clsx';
|
|
6
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
7
|
+
import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
|
8
|
+
import inputStyles from '../input/Input.module.scss';
|
|
9
|
+
import dropdownStyles from '../dropdown/Dropdown.module.scss';
|
|
10
|
+
import styles from './Select.module.scss';
|
|
11
|
+
import { Text } from '../text';
|
|
12
|
+
|
|
13
|
+
export type Option<T = Record<string, any>> = {
|
|
14
|
+
id: string,
|
|
15
|
+
node: ReactNode,
|
|
16
|
+
metadata?: T,
|
|
17
|
+
};
|
|
18
|
+
export type SelectProps<T = Record<string, any>> = {
|
|
19
|
+
/**
|
|
20
|
+
* The {@link Option}s to render in the select box.
|
|
21
|
+
*
|
|
22
|
+
* Each option should have an id (`string`) and node ({@link ReactNode}) property at minimum. You can also pass in any other metadata through the `metadata` attribute.
|
|
23
|
+
*
|
|
24
|
+
* For type safety, you can pass in a type parameter to `SelectProps` component. This will be used as the type for the `metadata` property of each option.
|
|
25
|
+
*/
|
|
26
|
+
options: Option<T>[];
|
|
27
|
+
/**
|
|
28
|
+
* The option ID to render as selected in the select box.
|
|
29
|
+
*
|
|
30
|
+
* This should exactly match one of the option IDs passed in the `options` prop. If `null`, no option will be selected.
|
|
31
|
+
*/
|
|
32
|
+
value?: string | null;
|
|
33
|
+
/**
|
|
34
|
+
* The interaction handler for the Select.
|
|
35
|
+
*/
|
|
36
|
+
onChange?: (value: string | null) => void | Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A Select component is used to render a `select` box.
|
|
41
|
+
*
|
|
42
|
+
* <hr />
|
|
43
|
+
*
|
|
44
|
+
* To use this component, import it as follows:
|
|
45
|
+
*
|
|
46
|
+
* ```js
|
|
47
|
+
* import { Select } from 'paris/select';
|
|
48
|
+
* ```
|
|
49
|
+
* @constructor
|
|
50
|
+
*/
|
|
51
|
+
export function Select<T = Record<string, any>>({
|
|
52
|
+
options,
|
|
53
|
+
value,
|
|
54
|
+
onChange,
|
|
55
|
+
}: SelectProps<T>) {
|
|
56
|
+
return (
|
|
57
|
+
<div className={styles.container}>
|
|
58
|
+
<Listbox
|
|
59
|
+
value={value}
|
|
60
|
+
onChange={onChange}
|
|
61
|
+
>
|
|
62
|
+
<Listbox.Button
|
|
63
|
+
className={clsx(
|
|
64
|
+
inputStyles.inputContainer,
|
|
65
|
+
styles.field,
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{options?.find((o) => o.id === value)?.node || 'Select an option'}
|
|
69
|
+
<FontAwesomeIcon className={inputStyles.enhancer} width="10px" icon={faChevronDown} />
|
|
70
|
+
</Listbox.Button>
|
|
71
|
+
<Transition
|
|
72
|
+
enter={dropdownStyles.transition}
|
|
73
|
+
enterFrom={dropdownStyles.enterFrom}
|
|
74
|
+
enterTo={dropdownStyles.enterTo}
|
|
75
|
+
leave={dropdownStyles.transition}
|
|
76
|
+
leaveFrom={dropdownStyles.leaveFrom}
|
|
77
|
+
leaveTo={dropdownStyles.leaveTo}
|
|
78
|
+
>
|
|
79
|
+
<Listbox.Options
|
|
80
|
+
className={clsx(
|
|
81
|
+
styles.options,
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{(options || []).map((option) => (
|
|
85
|
+
<Listbox.Option
|
|
86
|
+
key={option.id}
|
|
87
|
+
value={option.id}
|
|
88
|
+
data-selected={option.id === value}
|
|
89
|
+
className={clsx(
|
|
90
|
+
styles.option,
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
{typeof option.node === 'string' ? (
|
|
94
|
+
<Text as="span" kind="paragraphSmall">{option.node}</Text>
|
|
95
|
+
) : option.node}
|
|
96
|
+
</Listbox.Option>
|
|
97
|
+
))}
|
|
98
|
+
</Listbox.Options>
|
|
99
|
+
</Transition>
|
|
100
|
+
</Listbox>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Select';
|
|
@@ -1,40 +1,62 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ComponentProps, FC, ReactNode } from 'react';
|
|
2
2
|
import { createElement } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import styles from './Text.module.scss';
|
|
3
5
|
import typography from './Typography.module.css';
|
|
4
6
|
import type { LightTheme } from '../theme';
|
|
5
7
|
|
|
6
|
-
export type
|
|
8
|
+
export type TextElement = 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label' | 'legend' | 'caption' | 'small';
|
|
9
|
+
export type TextProps<T extends TextElement = 'span'> = {
|
|
7
10
|
/**
|
|
8
11
|
* The font class to use.
|
|
9
12
|
* @default paragraphMedium
|
|
10
13
|
*/
|
|
11
|
-
kind
|
|
14
|
+
kind?: keyof typeof LightTheme.typography.styles;
|
|
12
15
|
/**
|
|
13
16
|
* The HTML text tag to use.
|
|
14
17
|
* @default span
|
|
15
18
|
*/
|
|
16
|
-
as?:
|
|
19
|
+
as?: T;
|
|
17
20
|
/** The contents of the Text element. */
|
|
18
21
|
children: ReactNode;
|
|
19
|
-
}
|
|
22
|
+
} & ComponentProps<T>;
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* A `Text` component is used to render text with one of our theme formats.
|
|
26
|
+
*
|
|
27
|
+
* <hr />
|
|
28
|
+
*
|
|
29
|
+
* To use the `Text` component, import it as follows:
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* import { Text } from 'paris/text';
|
|
33
|
+
*
|
|
34
|
+
* export const ExampleHeading: FC = () => (
|
|
35
|
+
* <Text as="h1" format="headingLarge">Hello World!</Text>
|
|
36
|
+
* );
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
23
39
|
* @example ```tsx
|
|
24
40
|
* <Text as="h1" format="headingLarge">Hello World!</Text>
|
|
25
41
|
* ```
|
|
26
42
|
* @constructor
|
|
27
43
|
*/
|
|
28
|
-
export
|
|
44
|
+
export function Text<T extends TextElement>({
|
|
29
45
|
kind,
|
|
30
46
|
as,
|
|
31
47
|
children,
|
|
32
48
|
...props
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
}: TextProps<T>): JSX.Element {
|
|
50
|
+
return createElement(
|
|
51
|
+
as || 'span',
|
|
52
|
+
{
|
|
53
|
+
...props,
|
|
54
|
+
className: clsx(
|
|
55
|
+
styles.text,
|
|
56
|
+
typography[kind || 'paragraphMedium'],
|
|
57
|
+
props?.className,
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
children,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { TextArea } from './TextArea';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof TextArea> = {
|
|
5
|
+
title: 'Inputs/TextArea',
|
|
6
|
+
component: TextArea,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof TextArea>;
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
placeholder: 'Billie Eilish',
|
|
16
|
+
label: 'Name',
|
|
17
|
+
description: 'Type your full name here.',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, FC } from 'react';
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import type { InputProps } from '../input';
|
|
5
|
+
import styles from '../input/Input.module.scss';
|
|
6
|
+
import { Text } from '../text';
|
|
7
|
+
import { MemoizedEnhancer } from '../../helpers/renderEnhancer';
|
|
8
|
+
import { pget, theme } from '../theme';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A `textarea` input field.
|
|
12
|
+
*
|
|
13
|
+
* <hr />
|
|
14
|
+
*
|
|
15
|
+
* To use this component in your app, import it as follows:
|
|
16
|
+
*
|
|
17
|
+
* ```js
|
|
18
|
+
* import { TextArea } from 'paris/textarea';
|
|
19
|
+
* ```
|
|
20
|
+
* @constructor
|
|
21
|
+
*/
|
|
22
|
+
export const TextArea: FC<InputProps & ComponentPropsWithoutRef<'textarea'>> = ({ status, disabled, ...props }) => {
|
|
23
|
+
const inputID = useId();
|
|
24
|
+
return (
|
|
25
|
+
// Disable a11y rules because the container doesn't need to be focusable for screen readers; the input itself should receive focus instead.
|
|
26
|
+
// The container is only made clickable for usability purposes.
|
|
27
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
|
28
|
+
<div
|
|
29
|
+
{...props.overrides?.container}
|
|
30
|
+
className={clsx(
|
|
31
|
+
props.overrides?.container?.className,
|
|
32
|
+
styles.container,
|
|
33
|
+
)}
|
|
34
|
+
onClick={(e) => {
|
|
35
|
+
if (disabled) e.preventDefault();
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
const input = document.getElementById(inputID);
|
|
38
|
+
if (input && !disabled) {
|
|
39
|
+
input.focus();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<Text
|
|
45
|
+
{...props.overrides?.label}
|
|
46
|
+
as="label"
|
|
47
|
+
kind="paragraphSmall"
|
|
48
|
+
htmlFor={inputID}
|
|
49
|
+
className={clsx(
|
|
50
|
+
styles.label,
|
|
51
|
+
{ [styles.hidden]: props.hideLabel },
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{props.label}
|
|
55
|
+
</Text>
|
|
56
|
+
<div
|
|
57
|
+
className={styles.inputContainer}
|
|
58
|
+
data-status={status}
|
|
59
|
+
data-disabled={disabled}
|
|
60
|
+
>
|
|
61
|
+
{!!props.startEnhancer && (
|
|
62
|
+
<div {...props.overrides?.startEnhancerContainer} className={clsx(styles.enhancer, props.overrides?.startEnhancerContainer?.className)}>
|
|
63
|
+
{!!props.startEnhancer && (
|
|
64
|
+
<MemoizedEnhancer
|
|
65
|
+
enhancer={props.startEnhancer}
|
|
66
|
+
size={parseInt(pget('typography.styles.paragraphSmall.fontSize') || theme.typography.styles.paragraphSmall.fontSize, 10)}
|
|
67
|
+
/>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
<textarea
|
|
72
|
+
{...props}
|
|
73
|
+
id={inputID}
|
|
74
|
+
aria-label={props.label}
|
|
75
|
+
aria-describedby={`${inputID}-description`}
|
|
76
|
+
aria-disabled={disabled}
|
|
77
|
+
readOnly={disabled}
|
|
78
|
+
className={clsx(
|
|
79
|
+
props.className,
|
|
80
|
+
styles.input,
|
|
81
|
+
)}
|
|
82
|
+
/>
|
|
83
|
+
{!!props.endEnhancer && (
|
|
84
|
+
<div {...props.overrides?.endEnhancerContainer} className={clsx(styles.enhancer, props.overrides?.endEnhancerContainer?.className)}>
|
|
85
|
+
{!!props.endEnhancer && (
|
|
86
|
+
<MemoizedEnhancer
|
|
87
|
+
enhancer={props.endEnhancer}
|
|
88
|
+
size={parseInt(pget('typography.styles.paragraphSmall.fontSize') || theme.typography.styles.paragraphSmall.fontSize, 10)}
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
<Text
|
|
95
|
+
id={`${inputID}-description`}
|
|
96
|
+
{...props.overrides?.description}
|
|
97
|
+
as="p"
|
|
98
|
+
kind="paragraphXSmall"
|
|
99
|
+
className={clsx(
|
|
100
|
+
styles.description,
|
|
101
|
+
{ [styles.hidden]: !props.description || props.hideDescription },
|
|
102
|
+
props.overrides?.description?.className,
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
{props.description}
|
|
106
|
+
</Text>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
// return (
|
|
110
|
+
// <div
|
|
111
|
+
// className={clsx(
|
|
112
|
+
// styles.inputContainer,
|
|
113
|
+
// )}
|
|
114
|
+
// >
|
|
115
|
+
// <textarea
|
|
116
|
+
// {...props}
|
|
117
|
+
// />
|
|
118
|
+
// </div>
|
|
119
|
+
// );
|
|
120
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './TextArea';
|