paris 0.2.2 → 0.4.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +61 -5
  3. package/package.json +27 -15
  4. package/src/helpers/renderEnhancer.tsx +21 -0
  5. package/src/pages/_app.tsx +1 -1
  6. package/src/pages/index.tsx +1 -1
  7. package/src/stories/Pagination.mdx +73 -0
  8. package/src/stories/Tokens.mdx +0 -8
  9. package/src/stories/button/Button.module.scss +23 -7
  10. package/src/stories/button/Button.stories.ts +17 -0
  11. package/src/stories/button/Button.tsx +80 -20
  12. package/src/stories/card/Card.module.scss +14 -0
  13. package/src/stories/card/Card.stories.ts +33 -0
  14. package/src/stories/card/Card.tsx +60 -0
  15. package/src/stories/card/index.ts +1 -0
  16. package/src/stories/checkbox/Checkbox.module.scss +57 -0
  17. package/src/stories/checkbox/Checkbox.stories.ts +27 -0
  18. package/src/stories/checkbox/Checkbox.tsx +58 -0
  19. package/src/stories/checkbox/index.ts +1 -0
  20. package/src/stories/combobox/Combobox.module.scss +5 -0
  21. package/src/stories/combobox/Combobox.stories.ts +84 -0
  22. package/src/stories/combobox/Combobox.tsx +264 -0
  23. package/src/stories/combobox/index.ts +1 -0
  24. package/src/stories/dialog/Dialog.module.scss +187 -0
  25. package/src/stories/dialog/Dialog.stories.tsx +70 -0
  26. package/src/stories/dialog/Dialog.tsx +279 -0
  27. package/src/stories/dialog/index.ts +1 -0
  28. package/src/stories/drawer/Drawer.module.scss +284 -0
  29. package/src/stories/drawer/Drawer.stories.tsx +94 -0
  30. package/src/stories/drawer/Drawer.tsx +339 -0
  31. package/src/stories/drawer/index.ts +1 -0
  32. package/src/stories/dropdown/Dropdown.module.scss +23 -0
  33. package/src/stories/field/Field.module.scss +5 -0
  34. package/src/stories/field/Field.stories.ts +32 -0
  35. package/src/stories/field/Field.tsx +106 -0
  36. package/src/stories/field/index.ts +1 -0
  37. package/src/stories/icon/ChevronLeft.tsx +11 -0
  38. package/src/stories/icon/ChevronRight.tsx +11 -0
  39. package/src/stories/icon/Close.tsx +11 -0
  40. package/src/stories/icon/Icon.module.scss +5 -0
  41. package/src/stories/icon/Icon.stories.ts +28 -0
  42. package/src/stories/icon/Icon.tsx +46 -0
  43. package/src/stories/icon/index.ts +4 -0
  44. package/src/stories/input/Input.module.scss +135 -0
  45. package/src/stories/input/Input.stories.ts +89 -0
  46. package/src/stories/input/Input.tsx +137 -0
  47. package/src/stories/input/index.ts +1 -0
  48. package/src/stories/pagination/index.ts +1 -0
  49. package/src/stories/pagination/usePagination.ts +106 -0
  50. package/src/stories/select/Select.module.scss +74 -0
  51. package/src/stories/select/Select.stories.ts +73 -0
  52. package/src/stories/select/Select.tsx +176 -0
  53. package/src/stories/select/index.ts +1 -0
  54. package/src/stories/text/Text.module.scss +1 -1
  55. package/src/stories/text/Text.tsx +36 -14
  56. package/src/stories/textarea/TextArea.stories.ts +19 -0
  57. package/src/stories/textarea/TextArea.tsx +120 -0
  58. package/src/stories/textarea/index.ts +1 -0
  59. package/src/stories/theme/global.scss +2 -0
  60. package/src/stories/theme/index.ts +1 -0
  61. package/src/stories/theme/themes.ts +126 -7
  62. package/src/{styles → stories/theme}/tw-preflight.css +3 -1
  63. package/src/stories/theme/util.scss +8 -0
  64. package/src/stories/tilt/Tilt.module.scss +1 -0
  65. package/src/stories/tilt/Tilt.stories.tsx +43 -0
  66. package/src/stories/tilt/Tilt.tsx +63 -0
  67. package/src/stories/tilt/index.ts +1 -0
  68. package/src/stories/utility/RemoveFromDOM.tsx +19 -0
  69. package/src/stories/utility/TextWhenString.tsx +28 -0
  70. package/src/stories/utility/VisuallyHidden.tsx +25 -0
  71. package/src/types/Enhancer.ts +3 -0
  72. package/src/styles/util.scss +0 -4
@@ -0,0 +1,339 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Fragment, useMemo, useState,
5
+ } from 'react';
6
+ import type { ReactNode } from 'react';
7
+ import { Dialog, Transition } from '@headlessui/react';
8
+ import clsx from 'clsx';
9
+ import type { CSSLength } from '@ssh/csstypes';
10
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
11
+ import { faClose, faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
12
+ import { Simulate } from 'react-dom/test-utils';
13
+ import styles from './Drawer.module.scss';
14
+ import { Text } from '../text';
15
+ import { VisuallyHidden } from '../utility/VisuallyHidden';
16
+ import { TextWhenString } from '../utility/TextWhenString';
17
+ import { Button } from '../button';
18
+ import { pvar } from '../theme';
19
+ import { RemoveFromDOM } from '../utility/RemoveFromDOM';
20
+ import type { PaginationState } from '../pagination';
21
+ import {
22
+ ChevronLeft, ChevronRight, Close, Icon,
23
+ } from '../icon';
24
+
25
+ export const DrawerSizePresets = ['content', 'default', 'full'] as const;
26
+
27
+ export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
28
+ /**
29
+ * The dialog's open state.
30
+ */
31
+ isOpen?: boolean;
32
+ /**
33
+ * A callback that will be called when the user closes the drawer by clicking the close button or the backdrop overlay.
34
+ *
35
+ * @param value {boolean} - The new open state of the dialog.
36
+ */
37
+ onClose?: (value: boolean) => void | Promise<void>;
38
+ /**
39
+ * The title of the drawer. Required for accessibility, but can be hidden with the `hideTitle` prop.
40
+ *
41
+ * If pagination is enabled, the title should instead describe the entire drawer, not any single page. The title will be visually hidden on all pages.
42
+ *
43
+ * If a string is passed, it will be wrapped in a {@link Text} component with `headingXSmall` styling.
44
+ */
45
+ title: ReactNode;
46
+ /**
47
+ * Whether the title should be hidden. If `true`, the title will be visually hidden but still accessible to screen readers.
48
+ *
49
+ * If you're hiding the title to add a custom header, you may also want to hide the close button and render your own by using the `hideCloseButton` prop.
50
+ *
51
+ * When pagination is enabled, the title is always hidden regardless of this prop.
52
+ *
53
+ * @default false
54
+ */
55
+ hideTitle?: boolean;
56
+ /**
57
+ * Whether the close button should be hidden. This will entirely remove the close button from the DOM, so you should provide your own way to close the dialog.
58
+ *
59
+ * @default false
60
+ */
61
+ hideCloseButton?: boolean;
62
+ /**
63
+ * The direction from which the Drawer will appear.
64
+ */
65
+ from?: 'left' | 'right' | 'top' | 'bottom';
66
+ /**
67
+ * The size of the Drawer, either a preset or a valid {@link CSSLength} string.
68
+ *
69
+ * 'content' is the size of the Drawer's content. 'default' is the default size (360px). 'full' is the size of the viewport, minus the panel container's padding.
70
+ *
71
+ * @see DrawerSizePresets
72
+ * @default 'default'
73
+ */
74
+ size?: typeof DrawerSizePresets[number] | CSSLength;
75
+ /**
76
+ * An optional pagination state. If provided, each child of the Drawer will be rendered in its own page, and the Drawer will contain back and next buttons that will be used to navigate between pages.
77
+ *
78
+ * The state should be created using the {@link usePagination} hook and the entire state should be passed to the Drawer. For example:
79
+ * ```tsx
80
+ * const pagination = usePagination<['step1', 'step2', 'step3']>();
81
+ * // ...
82
+ * <Drawer pagination={pagination}>
83
+ * <div key="step1">Step 1</div>
84
+ * <div key="step2">Step 2</div>
85
+ * <div key="step3">Step 3</div>
86
+ * </Drawer>
87
+ * ```
88
+ *
89
+ * @see {@link PaginationState} for more information on the pagination state and available methods
90
+ * @see {@link usePagination} for more information on how to create the pagination state
91
+ * @default false
92
+ */
93
+ pagination?: PaginationState<T>;
94
+ /** The contents of the Drawer. */
95
+ children?: ReactNode | ReactNode[];
96
+ };
97
+
98
+ /**
99
+ * Drawers are panels that slide in from the edge of the screen to reveal additional content.
100
+ *
101
+ * <hr />
102
+ *
103
+ * To use this component, import it as follows:
104
+ *
105
+ * ```js
106
+ * import { Drawer } from 'paris/drawer';
107
+ * ```
108
+ * @constructor
109
+ */
110
+ export const Drawer = <T extends string[] | readonly string[] = string[]>({
111
+ isOpen = false,
112
+ onClose = () => {},
113
+ title,
114
+ hideTitle = false,
115
+ hideCloseButton = false,
116
+ from = 'right',
117
+ size = 'default',
118
+ pagination,
119
+ children,
120
+ }: DrawerProps<T>) => {
121
+ // Check if the drawer is on the x-axis.
122
+ const xAxisDrawer = useMemo(() => ['left', 'right'].includes(from), [from]);
123
+
124
+ // Check if the size is a preset.
125
+ const sizeIsPreset = useMemo(() => (DrawerSizePresets as readonly string[]).includes(size), [size]);
126
+
127
+ // Check if pagination is enabled.
128
+ const isPaginated = useMemo(() => Boolean(pagination), [pagination]);
129
+
130
+ const [loadedPage, setLoadedPage] = useState<string | null>(pagination?.history[0] || null);
131
+
132
+ // Decide what children to render.
133
+ const currentChild: ReactNode = useMemo(() => {
134
+ // If no children are provided, render nothing.
135
+ if (!children) {
136
+ return (<></>);
137
+ }
138
+
139
+ // If pagination is enabled, and multiple children are provided, render the currently active child by matching its key against `pagination.currentPage`.
140
+ if (pagination && Array.isArray(children) && children.length > 0) {
141
+ const found = children.find((child) => {
142
+ if (!(child && typeof child === 'object' && 'key' in child)) {
143
+ console.warn('Drawer: Pagination is enabled, but the following child is missing a `key` prop. Pagination will likely not work as expected and this child will never be rendered.', child);
144
+ return false;
145
+ }
146
+ return child.key === pagination.currentPage;
147
+ });
148
+ if (found) {
149
+ return found;
150
+ }
151
+ }
152
+
153
+ // As a fallback, render all children.
154
+ return children;
155
+ }, [children, pagination]);
156
+
157
+ return (
158
+ <Transition show={isOpen} as={Fragment}>
159
+ <Dialog
160
+ as="div"
161
+ className={clsx(
162
+ styles.root,
163
+ )}
164
+ onClose={onClose}
165
+ >
166
+ <div
167
+ className={styles.overlayContainer}
168
+ >
169
+ <Transition.Child
170
+ as={Fragment}
171
+ enter={styles.enter}
172
+ enterFrom={styles.enterFrom}
173
+ enterTo={styles.enterTo}
174
+ leave={styles.leave}
175
+ leaveFrom={styles.leaveFrom}
176
+ leaveTo={styles.leaveTo}
177
+ >
178
+ <Dialog.Overlay
179
+ className={clsx(
180
+ styles.overlay,
181
+ )}
182
+ />
183
+ </Transition.Child>
184
+ </div>
185
+
186
+ <div
187
+ className={clsx(
188
+ styles.panelContainer,
189
+ styles[`from-${from}`],
190
+ { [styles[`size-${size}`]]: sizeIsPreset },
191
+ )}
192
+ style={!sizeIsPreset ? {
193
+ [xAxisDrawer ? 'width' : 'height']: size,
194
+ } : {}}
195
+ >
196
+ <Transition.Child
197
+ as={Fragment}
198
+ enter={styles.enter}
199
+ enterFrom={styles.enterFrom}
200
+ enterTo={styles.enterTo}
201
+ leave={styles.leave}
202
+ leaveFrom={styles.leaveFrom}
203
+ leaveTo={styles.leaveTo}
204
+ >
205
+ <Dialog.Panel
206
+ className={clsx(
207
+ styles.panel,
208
+ styles[`from-${from}`],
209
+ )}
210
+ >
211
+ {/* Dialog title */}
212
+ <VisuallyHidden
213
+ // Hide when requested, or when pagination is enabled (the title isn't relevant to any specific page).
214
+ when={hideTitle || isPaginated}
215
+ >
216
+ <Dialog.Title as="h2">
217
+ <TextWhenString kind="headingXXSmall">
218
+ {title}
219
+ </TextWhenString>
220
+ </Dialog.Title>
221
+ </VisuallyHidden>
222
+
223
+ {/* Close button */}
224
+ <RemoveFromDOM
225
+ // Hide when requested, or when pagination is enabled (the page navigation bar will render its own close button).
226
+ when={hideCloseButton || isPaginated}
227
+ >
228
+ <Button
229
+ kind="tertiary"
230
+ shape="circle"
231
+ onClick={() => onClose(false)}
232
+ startEnhancer={(
233
+ <FontAwesomeIcon icon={faClose} color={pvar('colors.contentPrimary')} />
234
+ )}
235
+ data-title-hidden={hideTitle}
236
+ className={clsx(
237
+ styles.closeButton,
238
+ )}
239
+ >
240
+ Close dialog
241
+ </Button>
242
+ </RemoveFromDOM>
243
+
244
+ {/* Pagination Navbar */}
245
+ <RemoveFromDOM
246
+ // Hide when pagination is not enabled.
247
+ when={!isPaginated}
248
+ >
249
+ <div
250
+ className={clsx(
251
+ styles.paginationNav,
252
+ )}
253
+ >
254
+ <div
255
+ className={clsx(
256
+ styles.paginationButtons,
257
+ )}
258
+ >
259
+ <Button
260
+ className={clsx(
261
+ styles.navButton,
262
+ )}
263
+ kind="tertiary"
264
+ shape="circle"
265
+ onClick={() => pagination?.back()}
266
+ disabled={!pagination?.canGoBack()}
267
+ startEnhancer={(
268
+ <Icon icon={ChevronLeft} size={20} />
269
+ )}
270
+ >
271
+ Go to previous page in this modal
272
+ </Button>
273
+ <Button
274
+ className={clsx(
275
+ styles.navButton,
276
+ )}
277
+ kind="tertiary"
278
+ shape="circle"
279
+ onClick={() => pagination?.forward()}
280
+ disabled={!pagination?.canGoForward()}
281
+ startEnhancer={(
282
+ <Icon icon={ChevronRight} size={20} />
283
+ )}
284
+ >
285
+ Go to next page in this modal
286
+ </Button>
287
+ </div>
288
+ <Button
289
+ kind="tertiary"
290
+ shape="circle"
291
+ onClick={() => onClose(false)}
292
+ startEnhancer={(
293
+ <Icon icon={Close} size={20} />
294
+ )}
295
+ data-title-hidden={hideTitle}
296
+ className={clsx(
297
+ styles.closeButton,
298
+ )}
299
+ >
300
+ Close dialog
301
+ </Button>
302
+ </div>
303
+ </RemoveFromDOM>
304
+ {/* <Transition.Child */}
305
+ {/* as={Fragment} */}
306
+ {/* enter={styles.enter} */}
307
+ {/* enterFrom={styles.enterFrom} */}
308
+ {/* enterTo={styles.enterTo} */}
309
+ {/* leave={styles.leave} */}
310
+ {/* leaveFrom={styles.leaveFrom} */}
311
+ {/* leaveTo={styles.leaveTo} */}
312
+ {/* > */}
313
+ {/* {currentChild} */}
314
+ {/* </Transition.Child> */}
315
+ {(isPaginated && Array.isArray(children)) ? children.map((child) => (child && typeof child === 'object' && 'key' in child) && (
316
+ <Transition
317
+ show={child.key === pagination?.currentPage && loadedPage === child.key}
318
+ key={`transition_${child.key}`}
319
+ as="div"
320
+ enter={styles.paginationEnter}
321
+ enterFrom={styles.enterFromOpacity}
322
+ enterTo={styles.enterToOpacity}
323
+ leave={styles.paginationLeave}
324
+ leaveFrom={styles.leaveFromOpacity}
325
+ leaveTo={styles.leaveToOpacity}
326
+ afterLeave={() => {
327
+ setLoadedPage(pagination?.currentPage || null);
328
+ }}
329
+ >
330
+ {child}
331
+ </Transition>
332
+ )) : children}
333
+ </Dialog.Panel>
334
+ </Transition.Child>
335
+ </div>
336
+ </Dialog>
337
+ </Transition>
338
+ );
339
+ };
@@ -0,0 +1 @@
1
+ export * from './Drawer';
@@ -0,0 +1,23 @@
1
+ .transition {
2
+ transition: var(--pte-animations-interaction);
3
+ }
4
+
5
+ .enterFrom {
6
+ opacity: 0;
7
+ transform: translateY(-8px);
8
+ }
9
+
10
+ .enterTo {
11
+ opacity: 1;
12
+ transform: translateY(0);
13
+ }
14
+
15
+ .leaveFrom {
16
+ opacity: 1;
17
+ transform: translateY(0);
18
+ }
19
+
20
+ .leaveTo {
21
+ opacity: 0;
22
+ transform: translateY(-8px);
23
+ }
@@ -0,0 +1,5 @@
1
+ .content {
2
+ background-color: pink;
3
+ color: white;
4
+ height: 2rem;
5
+ }
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { createElement } from 'react';
3
+ import { Field } from './Field';
4
+
5
+ const meta: Meta<typeof Field> = {
6
+ title: 'Inputs/Field',
7
+ component: Field,
8
+ tags: ['autodocs'],
9
+ };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof Field>;
13
+
14
+ export const Default: Story = {
15
+ args: {
16
+ label: 'Label',
17
+ description: 'Description.',
18
+ children: createElement('div', {
19
+ style: {
20
+ backgroundColor: 'var(--pte-colors-backgroundSecondary)',
21
+ height: '36px',
22
+ width: '100%',
23
+ display: 'flex',
24
+ flexDirection: 'row',
25
+ alignItems: 'center',
26
+ justifyContent: 'flex-start',
27
+ paddingLeft: '12px',
28
+ border: '1px solid pink',
29
+ },
30
+ }, 'Children are inserted here.'),
31
+ },
32
+ };
@@ -0,0 +1,106 @@
1
+ import type { FC, PropsWithChildren, ComponentPropsWithoutRef } from 'react';
2
+ import clsx from 'clsx';
3
+ import styles from '../input/Input.module.scss';
4
+ import type { TextProps } from '../text';
5
+ import { Text } from '../text';
6
+
7
+ export type FieldProps = {
8
+ /**
9
+ * The ID of the child input.
10
+ */
11
+ htmlFor?: string;
12
+ /**
13
+ * A label for the field. Can be visually hidden using the `hideLabel` prop.
14
+ */
15
+ label?: string;
16
+ /**
17
+ * Visually hide the label (while keeping it accessible to screen readers).
18
+ * @default false
19
+ */
20
+ hideLabel?: boolean;
21
+ /**
22
+ * A description of the field. Can be visually hidden using the `hideDescription` prop.
23
+ */
24
+ description?: string;
25
+ /**
26
+ * Visually hide the description while keeping it accessible to screen readers.
27
+ * @default false
28
+ */
29
+ hideDescription?: boolean;
30
+ /**
31
+ * Disables the field, disallowing user interaction.
32
+ */
33
+ disabled?: boolean;
34
+ overrides?: {
35
+ container?: ComponentPropsWithoutRef<'div'>;
36
+ label?: TextProps<'label'>;
37
+ description?: TextProps<'p'>;
38
+ }
39
+ };
40
+
41
+ /**
42
+ * A Field component wraps a form input component with an accessible label and description.
43
+ *
44
+ * <hr />
45
+ *
46
+ * To use this component, import it as follows:
47
+ *
48
+ * ```js
49
+ * import { Field } from 'paris/field';
50
+ * ```
51
+ * @constructor
52
+ */
53
+ export const Field: FC<PropsWithChildren<FieldProps>> = ({
54
+ htmlFor,
55
+ disabled,
56
+ children,
57
+ ...props
58
+ }) => (
59
+ // Disable a11y rules because the container doesn't need to be focusable for screen readers; the input itself should receive focus instead.
60
+ // The container is only made clickable for usability purposes.
61
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
62
+ <div
63
+ {...props.overrides?.container}
64
+ className={clsx(
65
+ props.overrides?.container?.className,
66
+ styles.container,
67
+ )}
68
+ onClick={(e) => {
69
+ e.preventDefault();
70
+ if (typeof window !== 'undefined' && htmlFor) {
71
+ const input = document.getElementById(htmlFor);
72
+ if (input && !disabled) {
73
+ if (input.tagName === 'BUTTON') input.click();
74
+ else input.focus();
75
+ }
76
+ }
77
+ }}
78
+ >
79
+ <Text
80
+ {...props.overrides?.label}
81
+ as="label"
82
+ kind="paragraphSmall"
83
+ htmlFor={htmlFor}
84
+ className={clsx(
85
+ styles.label,
86
+ { [styles.hidden]: props.hideLabel },
87
+ )}
88
+ >
89
+ {props.label}
90
+ </Text>
91
+ {children}
92
+ <Text
93
+ id={`${htmlFor}-description`}
94
+ {...props.overrides?.description}
95
+ as="p"
96
+ kind="paragraphXSmall"
97
+ className={clsx(
98
+ styles.description,
99
+ { [styles.hidden]: !props.description || props.hideDescription },
100
+ props.overrides?.description?.className,
101
+ )}
102
+ >
103
+ {props.description}
104
+ </Text>
105
+ </div>
106
+ );
@@ -0,0 +1 @@
1
+ export * from './Field';
@@ -0,0 +1,11 @@
1
+ import { memo } from 'react';
2
+ import type { IconDefinition } from './Icon';
3
+
4
+ export const ChevronLeft: IconDefinition = memo(({ size }) => (
5
+ <svg width={size} height={size} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
6
+ <path
7
+ d="M5.46875 9.46875L11.4688 3.5C11.75 3.1875 12.2188 3.1875 12.5312 3.5C12.8125 3.78125 12.8125 4.25 12.5312 4.53125L7.03125 10L12.5 15.5C12.8125 15.7812 12.8125 16.25 12.5 16.5312C12.2188 16.8438 11.75 16.8438 11.4688 16.5312L5.46875 10.5312C5.15625 10.25 5.15625 9.78125 5.46875 9.46875Z"
8
+ fill="currentColor"
9
+ />
10
+ </svg>
11
+ ));
@@ -0,0 +1,11 @@
1
+ import { memo } from 'react';
2
+ import type { IconDefinition } from './Icon';
3
+
4
+ export const ChevronRight: IconDefinition = memo(({ size }) => (
5
+ <svg width={size} height={size} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
6
+ <path
7
+ d="M14.5312 9.46875C14.8125 9.78125 14.8125 10.25 14.5312 10.5312L8.53125 16.5312C8.21875 16.8438 7.75 16.8438 7.46875 16.5312C7.15625 16.25 7.15625 15.7812 7.46875 15.5L12.9375 10.0312L7.46875 4.53125C7.15625 4.25 7.15625 3.78125 7.46875 3.5C7.75 3.1875 8.21875 3.1875 8.5 3.5L14.5312 9.46875Z"
8
+ fill="currentColor"
9
+ />
10
+ </svg>
11
+ ));
@@ -0,0 +1,11 @@
1
+ import { memo } from 'react';
2
+ import type { IconDefinition } from './Icon';
3
+
4
+ export const Close: IconDefinition = memo(({ size }) => (
5
+ <svg width={size} height={size} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
6
+ <path
7
+ d="M14.7812 6.28125L11.0312 10.0312L14.75 13.75C15.0625 14.0312 15.0625 14.5 14.75 14.7812C14.4688 15.0938 14 15.0938 13.7188 14.7812L9.96875 11.0625L6.25 14.7812C5.96875 15.0938 5.5 15.0938 5.21875 14.7812C4.90625 14.5 4.90625 14.0312 5.21875 13.7188L8.9375 10L5.21875 6.28125C4.90625 6 4.90625 5.53125 5.21875 5.21875C5.5 4.9375 5.96875 4.9375 6.28125 5.21875L10 8.96875L13.7188 5.25C14 4.9375 14.4688 4.9375 14.7812 5.25C15.0625 5.53125 15.0625 6 14.7812 6.28125Z"
8
+ fill="currentColor"
9
+ />
10
+ </svg>
11
+ ));
@@ -0,0 +1,5 @@
1
+ .content {
2
+ background-color: pink;
3
+ color: white;
4
+ height: 2rem;
5
+ }
@@ -0,0 +1,28 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import type { IconDefinition } from './Icon';
3
+ import { Icon } from './Icon';
4
+ import * as Icons from './index';
5
+ import { pvar } from '../theme';
6
+
7
+ const meta: Meta<typeof Icon> = {
8
+ title: 'Content/Icon',
9
+ component: Icon,
10
+ tags: ['autodocs'],
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof Icon>;
15
+
16
+ const getArgs = (icon: IconDefinition): Story => ({
17
+ args: {
18
+ icon,
19
+ size: 20,
20
+ style: {
21
+ color: pvar('colors.contentPrimary'),
22
+ },
23
+ },
24
+ });
25
+
26
+ export const Close: Story = getArgs(Icons.Close);
27
+ export const ChevronLeft: Story = getArgs(Icons.ChevronLeft);
28
+ export const ChevronRight: Story = getArgs(Icons.ChevronRight);
@@ -0,0 +1,46 @@
1
+ import type { ComponentPropsWithoutRef, NamedExoticComponent } from 'react';
2
+ import { createElement, memo } from 'react';
3
+
4
+ export type IconDefinitionProps = {
5
+ /**
6
+ * The size of the Icon, in pixels.
7
+ */
8
+ size: number;
9
+ };
10
+ export type IconDefinition = NamedExoticComponent<IconDefinitionProps>;
11
+
12
+ export type IconProps<T extends keyof JSX.IntrinsicElements = 'span'> = IconDefinitionProps & {
13
+ icon: IconDefinition;
14
+ as?: T;
15
+ overrides?: {
16
+ icon?: ComponentPropsWithoutRef<'svg'>
17
+ }
18
+ } & ComponentPropsWithoutRef<T>;
19
+
20
+ /**
21
+ * Paris comes with a set of simple UI icons that can be used in your application. Icons are MIT-licensed and can be used in any project.
22
+ *
23
+ * <hr />
24
+ *
25
+ * To use this component, import the parent `Icon` component and the icon you want to use:
26
+ *
27
+ * ```tsx
28
+ * import { Icon, Close } from 'paris/icon';
29
+ *
30
+ * <Icon icon={Close} size={20} />
31
+ * ```
32
+ * @constructor
33
+ */
34
+ export const Icon = memo<IconProps>(({
35
+ as = 'span',
36
+ icon,
37
+ size,
38
+ overrides = { icon: {} },
39
+ ...props
40
+ }) => (
41
+ createElement(
42
+ as,
43
+ props,
44
+ createElement(icon, { size, ...overrides.icon }),
45
+ )
46
+ ));
@@ -0,0 +1,4 @@
1
+ export * from './Icon';
2
+ export { Close } from './Close';
3
+ export { ChevronLeft } from './ChevronLeft';
4
+ export { ChevronRight } from './ChevronRight';