paris 0.3.0 → 0.4.1

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 (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/package.json +14 -12
  3. package/src/pages/_app.tsx +1 -1
  4. package/src/pages/index.tsx +1 -1
  5. package/src/stories/Pagination.mdx +73 -0
  6. package/src/stories/button/Button.module.scss +11 -1
  7. package/src/stories/button/Button.tsx +40 -17
  8. package/src/stories/card/Card.module.scss +14 -0
  9. package/src/stories/card/Card.stories.ts +33 -0
  10. package/src/stories/card/Card.tsx +55 -0
  11. package/src/stories/card/index.ts +1 -0
  12. package/src/stories/checkbox/Checkbox.module.scss +57 -0
  13. package/src/stories/checkbox/Checkbox.stories.ts +27 -0
  14. package/src/stories/checkbox/Checkbox.tsx +58 -0
  15. package/src/stories/checkbox/index.ts +1 -0
  16. package/src/stories/combobox/Combobox.module.scss +5 -0
  17. package/src/stories/combobox/Combobox.stories.ts +84 -0
  18. package/src/stories/combobox/Combobox.tsx +264 -0
  19. package/src/stories/combobox/index.ts +1 -0
  20. package/src/stories/dialog/Dialog.module.scss +187 -0
  21. package/src/stories/dialog/Dialog.stories.tsx +70 -0
  22. package/src/stories/dialog/Dialog.tsx +279 -0
  23. package/src/stories/dialog/index.ts +1 -0
  24. package/src/stories/drawer/Drawer.module.scss +284 -0
  25. package/src/stories/drawer/Drawer.stories.tsx +94 -0
  26. package/src/stories/drawer/Drawer.tsx +339 -0
  27. package/src/stories/drawer/index.ts +1 -0
  28. package/src/stories/field/Field.module.scss +5 -0
  29. package/src/stories/field/Field.stories.ts +32 -0
  30. package/src/stories/field/Field.tsx +106 -0
  31. package/src/stories/field/index.ts +1 -0
  32. package/src/stories/icon/ChevronLeft.tsx +11 -0
  33. package/src/stories/icon/ChevronRight.tsx +11 -0
  34. package/src/stories/icon/Close.tsx +11 -0
  35. package/src/stories/icon/Icon.module.scss +5 -0
  36. package/src/stories/icon/Icon.stories.ts +28 -0
  37. package/src/stories/icon/Icon.tsx +46 -0
  38. package/src/stories/icon/index.ts +4 -0
  39. package/src/stories/input/Input.module.scss +3 -2
  40. package/src/stories/input/Input.stories.ts +2 -0
  41. package/src/stories/input/Input.tsx +38 -73
  42. package/src/stories/pagination/index.ts +1 -0
  43. package/src/stories/pagination/usePagination.ts +106 -0
  44. package/src/stories/select/Select.module.scss +8 -4
  45. package/src/stories/select/Select.stories.ts +5 -3
  46. package/src/stories/select/Select.tsx +80 -7
  47. package/src/stories/theme/themes.ts +75 -2
  48. package/src/stories/theme/tw-preflight.css +3 -1
  49. package/src/stories/tilt/Tilt.module.scss +1 -0
  50. package/src/stories/tilt/Tilt.stories.tsx +43 -0
  51. package/src/stories/tilt/Tilt.tsx +65 -0
  52. package/src/stories/tilt/index.ts +1 -0
  53. package/src/stories/utility/RemoveFromDOM.tsx +19 -0
  54. package/src/stories/utility/TextWhenString.tsx +28 -0
  55. package/src/stories/utility/VisuallyHidden.tsx +25 -0
@@ -0,0 +1,264 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode, ComponentPropsWithoutRef } from 'react';
4
+ import { Combobox as HCombobox, Transition } from '@headlessui/react';
5
+ import clsx from 'clsx';
6
+ import { useId, useState } from 'react';
7
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
8
+ import { faClose } from '@fortawesome/free-solid-svg-icons';
9
+ import inputStyles from '../input/Input.module.scss';
10
+ import dropdownStyles from '../dropdown/Dropdown.module.scss';
11
+ import styles from '../select/Select.module.scss';
12
+ import type { TextProps } from '../text';
13
+ import { Text } from '../text';
14
+ import type { InputProps } from '../input';
15
+ import { MemoizedEnhancer } from '../../helpers/renderEnhancer';
16
+ import { pget, theme } from '../theme';
17
+ import { Field } from '../field';
18
+ import { Button } from '../button';
19
+
20
+ export type Option<T extends Record<string, any> = Record<string, any>> = {
21
+ id: string,
22
+ node: ReactNode,
23
+ metadata?: T,
24
+ } | {
25
+ id: null,
26
+ node: string,
27
+ metadata?: T,
28
+ };
29
+
30
+ export type ComboboxProps<T extends Record<string, any>> = {
31
+ /**
32
+ * The {@link Option}s to render in the select box.
33
+ *
34
+ * 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.
35
+ *
36
+ * For type safety, you can pass in a type parameter to `ComboboxProps`. This will be used as the type for the `metadata` property of each option.
37
+ */
38
+ options: Option<T>[];
39
+ /**
40
+ * The option to render as selected in the select box.
41
+ *
42
+ * If `null`, no option will be selected.
43
+ */
44
+ value?: Option<T> | null;
45
+ /**
46
+ * The interaction handler for the Combobox. This will be called when the user selects an option from the dropdown.
47
+ *
48
+ * @param option - The selected option, or `null` if the user has cleared the selection.
49
+ */
50
+ onChange?: (option: Option<T> | null) => void | Promise<void>;
51
+ /**
52
+ * The interaction handler for when the user types in the input. The input is controlled internally, but you can use this to update the input value in your own state.
53
+ * @param value - The current value of the input.
54
+ */
55
+ onInputChange?: (value: string) => void | Promise<void>;
56
+ /**
57
+ * Whether to allow the user to create a custom value.
58
+ *
59
+ * If `true`, the user will be able to type in a custom value. This will be passed to the `onChange` handler as an option with an ID of `null` and a `node` value containing the user's input as a string.
60
+ * @default false
61
+ */
62
+ allowCustomValue?: boolean;
63
+ /**
64
+ * The text to use for the custom creation option. This should include a `%v` placeholder, which will be replaced with the user's input.
65
+ *
66
+ * For example, if the user types in `foo` and this is set to "New %v", the custom value option will be rendered as `New "foo"`.
67
+ * @default Create "%v"...
68
+ */
69
+ customValueString?: string;
70
+ /**
71
+ * Prop overrides for other rendered elements. Overrides for the input itself should be passed directly to the component.
72
+ */
73
+ overrides?: {
74
+ container?: ComponentPropsWithoutRef<'div'>;
75
+ input?: ComponentPropsWithoutRef<'input'>;
76
+ optionsContainer?: ComponentPropsWithoutRef<'div'>;
77
+ option?: ComponentPropsWithoutRef<'div'>;
78
+ label?: TextProps<'label'>;
79
+ description?: TextProps<'p'>;
80
+ startEnhancerContainer?: ComponentPropsWithoutRef<'div'>;
81
+ endEnhancerContainer?: ComponentPropsWithoutRef<'div'>;
82
+ }
83
+ } & Omit<InputProps, 'type' | 'overrides'>;
84
+
85
+ /**
86
+ * A Combobox component is used to render a searchable select.
87
+ *
88
+ * <hr />
89
+ *
90
+ * To use this component, import it as follows:
91
+ *
92
+ * ```js
93
+ * import { Combobox } from 'paris/combobox';
94
+ * ```
95
+ * @constructor
96
+ */
97
+ export function Combobox<T extends Record<string, any> = Record<string, any>>({
98
+ options,
99
+ value,
100
+ onChange,
101
+ label,
102
+ status,
103
+ hideLabel,
104
+ description,
105
+ hideDescription,
106
+ placeholder,
107
+ startEnhancer,
108
+ endEnhancer,
109
+ disabled,
110
+ onInputChange,
111
+ allowCustomValue,
112
+ customValueString = 'Create "%v"',
113
+ overrides,
114
+ }: ComboboxProps<T>) {
115
+ const inputID = useId();
116
+ const [selectedID, setSelectedID] = useState<string | null>(value?.id || null);
117
+ const [query, setQuery] = useState('');
118
+
119
+ return (
120
+ <Field
121
+ htmlFor={inputID}
122
+ label={label}
123
+ hideLabel={hideLabel}
124
+ description={description}
125
+ hideDescription={hideDescription}
126
+ disabled={disabled}
127
+ overrides={{
128
+ container: overrides?.container,
129
+ label: overrides?.label,
130
+ description: overrides?.description,
131
+ }}
132
+ >
133
+ <HCombobox
134
+ as="div"
135
+ value={selectedID}
136
+ onChange={(id) => {
137
+ if (onChange) {
138
+ const sel = options.find((o) => o.id === id);
139
+ if (sel) {
140
+ onChange(sel);
141
+ setSelectedID(sel.id);
142
+ } else if (id) {
143
+ onChange({
144
+ id: null,
145
+ node: id,
146
+ });
147
+ }
148
+ }
149
+ }}
150
+ >
151
+ <div
152
+ className={inputStyles.inputContainer}
153
+ data-status={status}
154
+ data-disabled={disabled}
155
+ >
156
+ {!!startEnhancer && (
157
+ <div {...overrides?.startEnhancerContainer} className={clsx(inputStyles.enhancer, overrides?.startEnhancerContainer?.className)}>
158
+ {!!startEnhancer && (
159
+ <MemoizedEnhancer
160
+ enhancer={startEnhancer}
161
+ size={parseInt(pget('typography.styles.paragraphSmall.fontSize') || theme.typography.styles.paragraphSmall.fontSize, 10)}
162
+ />
163
+ )}
164
+ </div>
165
+ )}
166
+ <div className={styles.content}>
167
+ {value ? value.node : (
168
+ <HCombobox.Input
169
+ id={inputID}
170
+ {...overrides?.input}
171
+ placeholder={placeholder}
172
+ // value={query}
173
+ onChange={(e) => {
174
+ setQuery(e.target.value);
175
+ if (onInputChange) onInputChange(e.target.value);
176
+ if (overrides?.input?.onChange) overrides.input.onChange(e);
177
+ }}
178
+ aria-disabled={disabled}
179
+ data-status={disabled ? 'disabled' : (status || 'default')}
180
+ className={clsx(
181
+ overrides?.input?.className,
182
+ inputStyles.input,
183
+ styles.field,
184
+ )}
185
+ />
186
+ )}
187
+ </div>
188
+ {!!value && (
189
+ <Button
190
+ size="xs"
191
+ shape="circle"
192
+ startEnhancer={<FontAwesomeIcon icon={faClose} fontSize="10px" />}
193
+ onClick={() => {
194
+ if (onChange) {
195
+ onChange(null);
196
+ }
197
+ setSelectedID(null);
198
+ }}
199
+ >
200
+ Clear
201
+ </Button>
202
+ )}
203
+ {!!endEnhancer && (
204
+ <div {...overrides?.endEnhancerContainer} className={clsx(inputStyles.enhancer, overrides?.endEnhancerContainer?.className)}>
205
+ {!!endEnhancer && (
206
+ <MemoizedEnhancer
207
+ enhancer={endEnhancer}
208
+ size={parseInt(pget('typography.styles.paragraphSmall.fontSize') || theme.typography.styles.paragraphSmall.fontSize, 10)}
209
+ />
210
+ )}
211
+ </div>
212
+ )}
213
+ </div>
214
+ <Transition
215
+ enter={dropdownStyles.transition}
216
+ enterFrom={dropdownStyles.enterFrom}
217
+ enterTo={dropdownStyles.enterTo}
218
+ leave={dropdownStyles.transition}
219
+ leaveFrom={dropdownStyles.leaveFrom}
220
+ leaveTo={dropdownStyles.leaveTo}
221
+ >
222
+ <HCombobox.Options
223
+ className={clsx(
224
+ overrides?.optionsContainer,
225
+ styles.options,
226
+ )}
227
+ >
228
+ {(allowCustomValue && query.length > 0) && (
229
+ <HCombobox.Option
230
+ value={query}
231
+ data-selected={false}
232
+ className={clsx(
233
+ overrides?.option,
234
+ styles.option,
235
+ )}
236
+ >
237
+ <Text as="span" kind="paragraphSmall">
238
+ {customValueString.replace('%v', query)}
239
+ </Text>
240
+ </HCombobox.Option>
241
+ )}
242
+ {(options || []).map((option) => (
243
+ <HCombobox.Option
244
+ key={option.id}
245
+ value={option.id}
246
+ data-selected={option.id === value}
247
+ className={clsx(
248
+ overrides?.option,
249
+ styles.option,
250
+ )}
251
+ >
252
+ {typeof option.node === 'string' ? (
253
+ <Text as="span" kind="paragraphSmall">
254
+ {option.node}
255
+ </Text>
256
+ ) : option.node}
257
+ </HCombobox.Option>
258
+ ))}
259
+ </HCombobox.Options>
260
+ </Transition>
261
+ </HCombobox>
262
+ </Field>
263
+ );
264
+ }
@@ -0,0 +1 @@
1
+ export * from './Combobox';
@@ -0,0 +1,187 @@
1
+ $panelMinMargin: 16px;
2
+ $panelRiseDistance: 4px;
3
+ $panelScaleAnimation: 0.96;
4
+ $panelAnimationDelay: var(--pte-animations-duration-fast);
5
+
6
+ .root {
7
+ position: relative;
8
+ z-index: 10;
9
+ user-select: var(--pte-utils-defaultUserSelect);
10
+ }
11
+
12
+ .overlay {
13
+ position: fixed;
14
+ inset: 0;
15
+ width: 100%;
16
+ height: 100%;
17
+ background-color: var(--pte-colors-backgroundOverlayDark);
18
+ backdrop-filter: blur(0);
19
+ will-change: opacity, backdrop-filter;
20
+ opacity: 0;
21
+ transition: var(--pte-animations-duration-normal) var(--pte-animations-timing-easeInOut);
22
+ }
23
+
24
+ .open {
25
+ transition-delay: 0ms;
26
+ opacity: 1;
27
+ backdrop-filter: blur(2px);
28
+ }
29
+
30
+ .overlayContainer {
31
+ .enterFrom {
32
+ backdrop-filter: blur(0);
33
+ }
34
+
35
+ .enterTo {
36
+ backdrop-filter: blur(2px);
37
+ }
38
+
39
+ .leave {
40
+ transition-delay: $panelAnimationDelay;
41
+ }
42
+
43
+ .leaveFrom {
44
+ backdrop-filter: blur(2px);
45
+ }
46
+
47
+ .leaveTo {
48
+ backdrop-filter: blur(0);
49
+ }
50
+ }
51
+
52
+ .panelContainer {
53
+ position: fixed;
54
+ inset: 0;
55
+ width: 100%;
56
+ height: 100vh;
57
+ padding: $panelMinMargin;
58
+ overflow-y: auto;
59
+
60
+ min-height: 100vh;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+
65
+ will-change: transform, filter;
66
+
67
+ .enter {
68
+ transition-delay: $panelAnimationDelay;
69
+ }
70
+
71
+ .enterFrom {
72
+ transform: translateY($panelRiseDistance) scale($panelScaleAnimation);
73
+ filter: blur(2px);
74
+ }
75
+
76
+ .enterTo {
77
+ transform: translateY(0) scale(1);
78
+ filter: blur(0);
79
+ }
80
+
81
+ .leave {
82
+ transition-delay: 0ms;
83
+ }
84
+
85
+ .leaveFrom {
86
+ transform: translateY(0) scale(1);
87
+ filter: blur(0);
88
+ }
89
+
90
+ .leaveTo {
91
+ transform: translateY(calc($panelRiseDistance / 2)) scale($panelScaleAnimation);
92
+ filter: blur(2px);
93
+ }
94
+ }
95
+
96
+ .panel {
97
+ position: relative;
98
+ z-index: 10;
99
+ width: 100%;
100
+ margin: auto;
101
+ padding: 16px;
102
+
103
+ display: flex;
104
+ flex-direction: column;
105
+ align-items: stretch;
106
+ justify-content: flex-start;
107
+ gap: 16px;
108
+
109
+ }
110
+
111
+ .simple {
112
+ border-radius: var(--pte-borders-radius-rounded);
113
+ background: var(--pte-colors-backgroundPrimary);
114
+ border: 1px solid var(--pte-borders-dropdown-color);
115
+ backdrop-filter: var(--pte-surfaces-dialog-backdropFilter);
116
+ outline: none;
117
+ box-shadow: var(--pte-lighting-shallowBelow);
118
+ }
119
+
120
+ .glass {
121
+ border-radius: var(--pte-borders-radius-roundedXL);
122
+ background: var(--pte-surfaces-dialog-background);
123
+ border: var(--pte-surfaces-dialog-border);
124
+ backdrop-filter: var(--pte-surfaces-dialog-backdropFilter);
125
+ outline: var(--pte-surfaces-dialog-outline);
126
+ box-shadow: var(--pte-lighting-shallowBelow);
127
+ }
128
+
129
+ .w-compact {
130
+ max-width: min(360px, 100%);
131
+ }
132
+
133
+ .w-default {
134
+ max-width: min(480px, 100%);
135
+ }
136
+
137
+ .w-large {
138
+ max-width: min(640px, 100%);
139
+ }
140
+
141
+ .w-full {
142
+ max-width: 100%;
143
+ }
144
+
145
+ .h-content {
146
+ }
147
+
148
+ .h-full {
149
+ min-height: 100%;
150
+ }
151
+
152
+ .header {
153
+ display: flex;
154
+ align-items: flex-start;
155
+ justify-content: space-between;
156
+ }
157
+
158
+ .closeButton {
159
+ position: absolute;
160
+ right: $panelMinMargin;
161
+ top: $panelMinMargin - 4px;
162
+ }
163
+
164
+ .enter {
165
+ transition: var(--pte-animations-duration-normal) var(--pte-animations-timing-easeOut);
166
+ }
167
+
168
+ .enterFrom {
169
+ opacity: 0;
170
+ }
171
+
172
+ .enterTo {
173
+ opacity: 1;
174
+ }
175
+
176
+ .leave {
177
+ transition: var(--pte-animations-duration-normal) var(--pte-animations-timing-easeIn);
178
+ }
179
+
180
+ .leaveFrom {
181
+ opacity: 1;
182
+ }
183
+
184
+ .leaveTo {
185
+ opacity: 0;
186
+ }
187
+
@@ -0,0 +1,70 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { useState } from 'react';
4
+ import { Dialog } from './Dialog';
5
+ import { Button } from '../button';
6
+ import { Text } from '../text';
7
+
8
+ const meta: Meta<typeof Dialog> = {
9
+ title: 'Surfaces/Dialog',
10
+ component: Dialog,
11
+ tags: ['autodocs'],
12
+ };
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof Dialog>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ title: 'Confirmation',
20
+ appearance: 'simple',
21
+ width: 'default',
22
+ height: 'content',
23
+ draggable: false,
24
+ isOpen: false,
25
+ children: (
26
+ <Text>
27
+ Are you sure? That's a lot of money.
28
+ </Text>
29
+ ),
30
+ },
31
+ render: (args) => {
32
+ const [isOpen, setIsOpen] = useState(false);
33
+ // const [primary, bg] = [
34
+ // pvar('colors.contentSecondary'),
35
+ // pvar('colors.backgroundPrimary'),
36
+ // ];
37
+ return (
38
+ <>
39
+ <div
40
+ style={{}}
41
+ // Background texture that can be enabled to emphasize glassmorphism
42
+ // Make sure to also uncomment the pvar array above
43
+ // style={{
44
+ // width: '80vw',
45
+ // height: '80vh',
46
+ // backgroundColor: bg,
47
+ // opacity: '0.8',
48
+ // // eslint-disable-next-line css/no-shorthand-property-overrides
49
+ // background: `repeating-linear-gradient( -45deg, ${primary}, ${primary} 4px, transparent 4px, transparent 25px )`,
50
+ // }}
51
+ >
52
+ <Button
53
+ onClick={() => setIsOpen(true)}
54
+ >
55
+ Pay now
56
+ </Button>
57
+ </div>
58
+ <Dialog
59
+ {...args}
60
+ isOpen={isOpen}
61
+ onClose={() => {
62
+ setIsOpen(false);
63
+ }}
64
+ >
65
+ {args.children}
66
+ </Dialog>
67
+ </>
68
+ );
69
+ },
70
+ };