@shipfox/react-ui 0.7.0 → 0.9.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 (68) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/.turbo/turbo-check.log +3 -3
  3. package/.turbo/turbo-type.log +2 -2
  4. package/CHANGELOG.md +13 -0
  5. package/dist/components/alert/alert.d.ts +15 -5
  6. package/dist/components/alert/alert.d.ts.map +1 -1
  7. package/dist/components/alert/alert.js +122 -22
  8. package/dist/components/alert/alert.js.map +1 -1
  9. package/dist/components/alert/alert.stories.js +121 -6
  10. package/dist/components/alert/alert.stories.js.map +1 -1
  11. package/dist/components/button/button-link.js +1 -1
  12. package/dist/components/button/button-link.js.map +1 -1
  13. package/dist/components/button/button.d.ts +2 -1
  14. package/dist/components/button/button.d.ts.map +1 -1
  15. package/dist/components/button/button.js +21 -3
  16. package/dist/components/button/button.js.map +1 -1
  17. package/dist/components/button/button.stories.js +25 -0
  18. package/dist/components/button/button.stories.js.map +1 -1
  19. package/dist/components/button/icon-button.d.ts +2 -1
  20. package/dist/components/button/icon-button.d.ts.map +1 -1
  21. package/dist/components/button/icon-button.js +21 -3
  22. package/dist/components/button/icon-button.js.map +1 -1
  23. package/dist/components/button/icon-button.stories.js +90 -0
  24. package/dist/components/button/icon-button.stories.js.map +1 -1
  25. package/dist/components/checkbox/checkbox-links.d.ts.map +1 -1
  26. package/dist/components/checkbox/checkbox-links.js +8 -2
  27. package/dist/components/checkbox/checkbox-links.js.map +1 -1
  28. package/dist/components/checkbox/checkbox.stories.js +4 -4
  29. package/dist/components/checkbox/checkbox.stories.js.map +1 -1
  30. package/dist/components/form/form.d.ts +11 -0
  31. package/dist/components/form/form.d.ts.map +1 -0
  32. package/dist/components/form/form.js +106 -0
  33. package/dist/components/form/form.js.map +1 -0
  34. package/dist/components/form/form.stories.js +582 -0
  35. package/dist/components/form/form.stories.js.map +1 -0
  36. package/dist/components/form/index.d.ts +2 -0
  37. package/dist/components/form/index.d.ts.map +1 -0
  38. package/dist/components/form/index.js +3 -0
  39. package/dist/components/form/index.js.map +1 -0
  40. package/dist/components/icon/custom/spinner.d.ts +1 -1
  41. package/dist/components/icon/custom/spinner.d.ts.map +1 -1
  42. package/dist/components/icon/custom/spinner.js +84 -30
  43. package/dist/components/icon/custom/spinner.js.map +1 -1
  44. package/dist/components/icon/icon.d.ts +19 -18
  45. package/dist/components/icon/icon.d.ts.map +1 -1
  46. package/dist/components/icon/icon.js +17 -17
  47. package/dist/components/icon/icon.js.map +1 -1
  48. package/dist/components/index.d.ts +1 -0
  49. package/dist/components/index.d.ts.map +1 -1
  50. package/dist/components/index.js +1 -0
  51. package/dist/components/index.js.map +1 -1
  52. package/dist/styles.css +1 -1
  53. package/package.json +3 -1
  54. package/src/components/alert/alert.stories.tsx +103 -2
  55. package/src/components/alert/alert.tsx +163 -16
  56. package/src/components/button/button-link.tsx +1 -1
  57. package/src/components/button/button.stories.tsx +18 -0
  58. package/src/components/button/button.tsx +29 -3
  59. package/src/components/button/icon-button.stories.tsx +46 -0
  60. package/src/components/button/icon-button.tsx +28 -2
  61. package/src/components/checkbox/checkbox-links.tsx +5 -3
  62. package/src/components/checkbox/checkbox.stories.tsx +20 -4
  63. package/src/components/form/form.stories.tsx +500 -0
  64. package/src/components/form/form.tsx +154 -0
  65. package/src/components/form/index.ts +1 -0
  66. package/src/components/icon/custom/spinner.tsx +64 -18
  67. package/src/components/icon/icon.tsx +18 -18
  68. package/src/components/index.ts +1 -0
@@ -1,6 +1,16 @@
1
1
  import {cva, type VariantProps} from 'class-variance-authority';
2
2
  import {Icon} from 'components/icon';
3
- import type {ComponentProps} from 'react';
3
+ import {AnimatePresence, motion, type Transition} from 'framer-motion';
4
+ import {
5
+ type ComponentProps,
6
+ createContext,
7
+ type MouseEvent,
8
+ useCallback,
9
+ useContext,
10
+ useEffect,
11
+ useRef,
12
+ useState,
13
+ } from 'react';
4
14
  import {cn} from 'utils/cn';
5
15
 
6
16
  const alertVariants = cva(
@@ -51,21 +61,137 @@ const closeIconVariants = cva('w-16 h-16', {
51
61
  },
52
62
  });
53
63
 
54
- type AlertProps = ComponentProps<'div'> & VariantProps<typeof alertVariants>;
64
+ const alertDefaultTransition: Transition = {
65
+ duration: 0.2,
66
+ ease: 'easeInOut',
67
+ };
68
+
69
+ type AlertContextValue = {
70
+ isOpen: boolean;
71
+ onClose: () => void;
72
+ variant: VariantProps<typeof alertVariants>['variant'];
73
+ };
74
+
75
+ const AlertContext = createContext<AlertContextValue | null>(null);
76
+
77
+ function useAlertContext() {
78
+ const context = useContext(AlertContext);
79
+ if (!context) {
80
+ throw new Error('Alert components must be used within an Alert component');
81
+ }
82
+ return context;
83
+ }
84
+
85
+ type AlertProps = ComponentProps<'div'> &
86
+ VariantProps<typeof alertVariants> & {
87
+ open?: boolean;
88
+ defaultOpen?: boolean;
89
+ onOpenChange?: (open: boolean) => void;
90
+ animated?: boolean;
91
+ transition?: Transition;
92
+ autoClose?: number;
93
+ };
94
+
95
+ function Alert({
96
+ className,
97
+ variant = 'default',
98
+ children,
99
+ open: controlledOpen,
100
+ defaultOpen = true,
101
+ onOpenChange,
102
+ animated = true,
103
+ transition = alertDefaultTransition,
104
+ autoClose,
105
+ ...props
106
+ }: AlertProps) {
107
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
108
+ const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
109
+
110
+ const handleClose = useCallback(() => {
111
+ if (controlledOpen === undefined) {
112
+ setInternalOpen(false);
113
+ }
114
+ onOpenChange?.(false);
115
+ }, [controlledOpen, onOpenChange]);
116
+
117
+ const handleCloseRef = useRef(handleClose);
118
+ useEffect(() => {
119
+ handleCloseRef.current = handleClose;
120
+ }, [handleClose]);
121
+
122
+ useEffect(() => {
123
+ if (autoClose && isOpen && autoClose > 0) {
124
+ const timeoutId = setTimeout(() => {
125
+ handleCloseRef.current();
126
+ }, autoClose);
127
+
128
+ return () => {
129
+ clearTimeout(timeoutId);
130
+ };
131
+ }
132
+ }, [autoClose, isOpen]);
133
+
134
+ const contextValue: AlertContextValue = {
135
+ isOpen,
136
+ onClose: handleClose,
137
+ variant,
138
+ };
139
+
140
+ if (!animated) {
141
+ if (!isOpen) {
142
+ return null;
143
+ }
144
+
145
+ return (
146
+ <AlertContext.Provider value={contextValue}>
147
+ <div className="w-full flex items-start gap-4">
148
+ <div
149
+ data-slot="alert-line"
150
+ className={cn(alertLineVariants({variant}))}
151
+ aria-hidden="true"
152
+ />
153
+ <div
154
+ data-slot="alert"
155
+ role="alert"
156
+ className={cn(alertVariants({variant}), className)}
157
+ {...props}
158
+ >
159
+ {children}
160
+ </div>
161
+ </div>
162
+ </AlertContext.Provider>
163
+ );
164
+ }
55
165
 
56
- function Alert({className, variant, children, ...props}: AlertProps) {
57
166
  return (
58
- <div className="w-full flex items-start gap-4">
59
- <div data-slot="alert-line" className={cn(alertLineVariants({variant}))} aria-hidden="true" />
60
- <div
61
- data-slot="alert"
62
- role="alert"
63
- className={cn(alertVariants({variant}), className)}
64
- {...props}
65
- >
66
- {children}
67
- </div>
68
- </div>
167
+ <AnimatePresence>
168
+ {isOpen && (
169
+ <motion.div
170
+ key="alert"
171
+ className="w-full flex items-start gap-4"
172
+ initial={{opacity: 0}}
173
+ animate={{opacity: 1}}
174
+ exit={{opacity: 0}}
175
+ transition={transition}
176
+ >
177
+ <AlertContext.Provider value={contextValue}>
178
+ <div
179
+ data-slot="alert-line"
180
+ className={cn(alertLineVariants({variant}))}
181
+ aria-hidden="true"
182
+ />
183
+ <div
184
+ data-slot="alert"
185
+ role="alert"
186
+ className={cn(alertVariants({variant}), className)}
187
+ {...props}
188
+ >
189
+ {children}
190
+ </div>
191
+ </AlertContext.Provider>
192
+ </motion.div>
193
+ )}
194
+ </AnimatePresence>
69
195
  );
70
196
  }
71
197
 
@@ -122,9 +248,18 @@ function AlertAction({className, ...props}: ComponentProps<'button'>) {
122
248
 
123
249
  function AlertClose({
124
250
  className,
125
- variant = 'default',
251
+ variant: variantProp,
252
+ onClick,
126
253
  ...props
127
254
  }: ComponentProps<'button'> & VariantProps<typeof closeIconVariants>) {
255
+ const {onClose, variant: contextVariant} = useAlertContext();
256
+ const variant = variantProp ?? contextVariant ?? 'default';
257
+
258
+ const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
259
+ onClose();
260
+ onClick?.(e);
261
+ };
262
+
128
263
  return (
129
264
  <button
130
265
  data-slot="alert-close"
@@ -134,6 +269,7 @@ function AlertClose({
134
269
  className,
135
270
  )}
136
271
  aria-label="Close"
272
+ onClick={handleClick}
137
273
  {...props}
138
274
  >
139
275
  <Icon name="close" className={cn(closeIconVariants({variant}))} />
@@ -141,4 +277,15 @@ function AlertClose({
141
277
  );
142
278
  }
143
279
 
144
- export {Alert, AlertContent, AlertTitle, AlertDescription, AlertActions, AlertAction, AlertClose};
280
+ export {
281
+ Alert,
282
+ AlertContent,
283
+ AlertTitle,
284
+ AlertDescription,
285
+ AlertActions,
286
+ AlertAction,
287
+ AlertClose,
288
+ alertDefaultTransition,
289
+ };
290
+
291
+ export type {AlertProps};
@@ -5,7 +5,7 @@ import type {ComponentProps} from 'react';
5
5
  import {cn} from 'utils/cn';
6
6
 
7
7
  export const buttonLinkVariants = cva(
8
- 'inline-flex items-center justify-center gap-4 whitespace-nowrap transition-colors disabled:pointer-events-none outline-none font-medium',
8
+ 'inline-flex items-center justify-center gap-4 whitespace-nowrap transition-colors cursor-pointer disabled:pointer-events-none outline-none font-medium',
9
9
  {
10
10
  variants: {
11
11
  variant: {
@@ -26,6 +26,7 @@ const meta = {
26
26
  options: sizeOptions,
27
27
  },
28
28
  asChild: {control: 'boolean'},
29
+ isLoading: {control: 'boolean'},
29
30
  },
30
31
  args: {
31
32
  children: 'Click me',
@@ -118,3 +119,20 @@ export const Icons: Story = {
118
119
  </div>
119
120
  ),
120
121
  };
122
+
123
+ export const Loading: Story = {
124
+ render: (args) => (
125
+ <div className="flex flex-col gap-16">
126
+ <div>
127
+ <Button {...args} isLoading>
128
+ Loading...
129
+ </Button>
130
+ </div>
131
+ <div>
132
+ <Button {...args} isLoading iconLeft="google">
133
+ Loading with left icon
134
+ </Button>
135
+ </div>
136
+ </div>
137
+ ),
138
+ };
@@ -5,7 +5,7 @@ import type {ComponentProps} from 'react';
5
5
  import {cn} from 'utils/cn';
6
6
 
7
7
  export const buttonVariants = cva(
8
- 'rounded-6 inline-flex items-center justify-center whitespace-nowrap transition-colors disabled:pointer-events-none shrink-0 outline-none',
8
+ 'rounded-6 inline-flex items-center justify-center whitespace-nowrap transition-colors cursor-pointer disabled:pointer-events-none shrink-0 outline-none',
9
9
  {
10
10
  variants: {
11
11
  variant: {
@@ -38,6 +38,15 @@ export const buttonVariants = cva(
38
38
  },
39
39
  );
40
40
 
41
+ const spinnerSizeMap: Record<NonNullable<VariantProps<typeof buttonVariants>['size']>, string> = {
42
+ '2xs': 'size-10',
43
+ xs: 'size-10',
44
+ sm: 'size-12',
45
+ md: 'size-14',
46
+ lg: 'size-16',
47
+ xl: 'size-18',
48
+ };
49
+
41
50
  export function Button({
42
51
  className,
43
52
  variant,
@@ -46,18 +55,35 @@ export function Button({
46
55
  children,
47
56
  iconLeft,
48
57
  iconRight,
58
+ isLoading = false,
59
+ disabled,
49
60
  ...props
50
61
  }: ComponentProps<'button'> &
51
62
  VariantProps<typeof buttonVariants> & {
52
63
  asChild?: boolean;
53
64
  iconLeft?: IconName;
54
65
  iconRight?: IconName;
66
+ isLoading?: boolean;
55
67
  }) {
56
68
  const Comp = asChild ? Slot : 'button';
69
+ const spinnerSize =
70
+ spinnerSizeMap[(size ?? 'md') as NonNullable<VariantProps<typeof buttonVariants>['size']>];
57
71
 
58
72
  return (
59
- <Comp data-slot="button" className={cn(buttonVariants({variant, size, className}))} {...props}>
60
- {iconLeft && <Icon name={iconLeft} />}
73
+ <Comp
74
+ data-slot="button"
75
+ className={cn(buttonVariants({variant, size, className}))}
76
+ disabled={disabled || isLoading}
77
+ aria-busy={isLoading}
78
+ aria-live={isLoading ? 'polite' : undefined}
79
+ {...(asChild ? {'aria-disabled': disabled || isLoading} : {})}
80
+ {...props}
81
+ >
82
+ {isLoading ? (
83
+ <Icon name="spinner" className={spinnerSize} />
84
+ ) : (
85
+ iconLeft && <Icon name={iconLeft} />
86
+ )}
61
87
  {children}
62
88
  {iconRight && <Icon name={iconRight} />}
63
89
  </Comp>
@@ -180,3 +180,49 @@ export const Sizes: Story = {
180
180
  </div>
181
181
  ),
182
182
  };
183
+
184
+ export const Loading: Story = {
185
+ render: ({children: _children, ...args}) => (
186
+ <div className="flex flex-col gap-32">
187
+ <div className="flex flex-col gap-16">
188
+ <Code variant="label">Loading by Size:</Code>
189
+ <div className="flex gap-16 items-center">
190
+ {sizeOptions.map((size) => (
191
+ <div key={size} className="flex flex-col gap-8 items-center">
192
+ <Code variant="label" className="text-foreground-neutral-subtle text-xs">
193
+ {size}
194
+ </Code>
195
+ <IconButton {...args} icon="addLine" aria-label="Loading" size={size} isLoading />
196
+ </div>
197
+ ))}
198
+ </div>
199
+ </div>
200
+ <div className="flex flex-col gap-16">
201
+ <Code variant="label">Loading by Variant:</Code>
202
+ <div className="flex gap-16 items-center">
203
+ {variantOptions.map((variant) => (
204
+ <div key={variant} className="flex flex-col gap-8 items-center">
205
+ <Code variant="label" className="text-foreground-neutral-subtle text-xs">
206
+ {variant}
207
+ </Code>
208
+ <IconButton
209
+ {...args}
210
+ icon="addLine"
211
+ aria-label="Loading"
212
+ variant={variant}
213
+ isLoading
214
+ />
215
+ </div>
216
+ ))}
217
+ </div>
218
+ </div>
219
+ <div className="flex flex-col gap-16">
220
+ <Code variant="label">Normal vs Loading:</Code>
221
+ <div className="flex gap-16 items-center">
222
+ <IconButton {...args} icon="addLine" aria-label="Add" />
223
+ <IconButton {...args} icon="addLine" aria-label="Loading" isLoading />
224
+ </div>
225
+ </div>
226
+ </div>
227
+ ),
228
+ };
@@ -5,7 +5,7 @@ import type {ComponentProps} from 'react';
5
5
  import {cn} from 'utils/cn';
6
6
 
7
7
  export const iconButtonVariants = cva(
8
- 'inline-flex items-center justify-center whitespace-nowrap transition-colors disabled:pointer-events-none shrink-0 outline-none',
8
+ 'inline-flex items-center justify-center whitespace-nowrap transition-colors cursor-pointer disabled:pointer-events-none shrink-0 outline-none',
9
9
  {
10
10
  variants: {
11
11
  variant: {
@@ -40,6 +40,18 @@ export const iconButtonVariants = cva(
40
40
  },
41
41
  );
42
42
 
43
+ const spinnerSizeMap: Record<
44
+ NonNullable<VariantProps<typeof iconButtonVariants>['size']>,
45
+ string
46
+ > = {
47
+ '2xs': 'size-8',
48
+ xs: 'size-10',
49
+ sm: 'size-12',
50
+ md: 'size-14',
51
+ lg: 'size-16',
52
+ xl: 'size-18',
53
+ };
54
+
43
55
  export function IconButton({
44
56
  className,
45
57
  variant,
@@ -49,21 +61,35 @@ export function IconButton({
49
61
  asChild = false,
50
62
  children,
51
63
  icon,
64
+ isLoading = false,
65
+ disabled,
52
66
  ...props
53
67
  }: ComponentProps<'button'> &
54
68
  VariantProps<typeof iconButtonVariants> & {
55
69
  asChild?: boolean;
56
70
  icon?: IconName;
71
+ isLoading?: boolean;
57
72
  }) {
58
73
  const Comp = asChild ? Slot : 'button';
74
+ const spinnerSize = spinnerSizeMap[size ?? 'md'];
59
75
 
60
76
  return (
61
77
  <Comp
62
78
  data-slot="icon-button"
63
79
  className={cn(iconButtonVariants({variant, size, radius, muted}), className)}
80
+ disabled={disabled || isLoading}
81
+ aria-busy={isLoading}
82
+ aria-live={isLoading ? 'polite' : undefined}
83
+ {...(asChild ? {'aria-disabled': disabled || isLoading} : {})}
64
84
  {...props}
65
85
  >
66
- {icon ? <Icon name={icon} /> : children}
86
+ {isLoading ? (
87
+ <Icon name="spinner" className={spinnerSize} />
88
+ ) : icon ? (
89
+ <Icon name={icon} />
90
+ ) : (
91
+ children
92
+ )}
67
93
  </Comp>
68
94
  );
69
95
  }
@@ -1,3 +1,4 @@
1
+ import {buttonLinkVariants} from 'components/button/button-link';
1
2
  import {Label} from 'components/label';
2
3
  import {type ReactNode, useId} from 'react';
3
4
  import {cn} from 'utils/cn';
@@ -58,10 +59,12 @@ export function CheckboxLinks({
58
59
  {link.href ? (
59
60
  <a
60
61
  href={link.href}
62
+ target="_blank"
63
+ rel="noopener noreferrer"
61
64
  onClick={link.onClick}
62
65
  className={cn(
63
66
  'text-sm leading-20 font-medium text-foreground-highlight-interactive',
64
- 'hover:text-foreground-highlight-interactive-hover',
67
+ 'hover:text-foreground-highlight-interactive-hover transition-colors',
65
68
  linkClassName,
66
69
  )}
67
70
  >
@@ -72,8 +75,7 @@ export function CheckboxLinks({
72
75
  type="button"
73
76
  onClick={link.onClick}
74
77
  className={cn(
75
- 'text-sm leading-20 font-medium text-foreground-highlight-interactive',
76
- 'hover:text-foreground-highlight-interactive-hover',
78
+ buttonLinkVariants({variant: 'interactive', size: 'sm'}),
77
79
  linkClassName,
78
80
  )}
79
81
  >
@@ -356,16 +356,32 @@ export const CheckboxLinksStory: StoryObj = {
356
356
  id="checkbox-links-default"
357
357
  label="Accept policies"
358
358
  links={[
359
- {label: 'Terms of use', href: '#'},
360
- {label: 'Privacy Policy', href: '#'},
359
+ {label: 'Terms of use', href: 'https://www.shipfox.io/legal/terms-of-service'},
360
+ {
361
+ label: 'Privacy Policy',
362
+ onClick: () =>
363
+ window.open(
364
+ 'https://www.shipfox.io/legal/privacy-policy',
365
+ '_blank',
366
+ 'noopener,noreferrer',
367
+ ),
368
+ },
361
369
  ]}
362
370
  />
363
371
  <CheckboxLinks
364
372
  id="checkbox-links-checked"
365
373
  label="Accept policies"
366
374
  links={[
367
- {label: 'Terms of use', href: '#'},
368
- {label: 'Privacy Policy', href: '#'},
375
+ {label: 'Terms of use', href: 'https://www.shipfox.io/legal/terms-of-service'},
376
+ {
377
+ label: 'Privacy Policy',
378
+ onClick: () =>
379
+ window.open(
380
+ 'https://www.shipfox.io/legal/privacy-policy',
381
+ '_blank',
382
+ 'noopener,noreferrer',
383
+ ),
384
+ },
369
385
  ]}
370
386
  checked
371
387
  />