@shipfox/react-ui 0.8.0 → 0.10.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 (41) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/.turbo/turbo-check.log +3 -3
  3. package/.turbo/turbo-type.log +2 -2
  4. package/CHANGELOG.md +12 -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.map +1 -1
  14. package/dist/components/button/button.js +4 -1
  15. package/dist/components/button/button.js.map +1 -1
  16. package/dist/components/button/icon-button.d.ts.map +1 -1
  17. package/dist/components/button/icon-button.js +4 -1
  18. package/dist/components/button/icon-button.js.map +1 -1
  19. package/dist/components/checkbox/checkbox-links.d.ts.map +1 -1
  20. package/dist/components/checkbox/checkbox-links.js +8 -2
  21. package/dist/components/checkbox/checkbox-links.js.map +1 -1
  22. package/dist/components/checkbox/checkbox.stories.js +4 -4
  23. package/dist/components/checkbox/checkbox.stories.js.map +1 -1
  24. package/dist/components/icon/icon.d.ts +3 -17
  25. package/dist/components/icon/icon.d.ts.map +1 -1
  26. package/dist/components/icon/icon.js +21 -13
  27. package/dist/components/icon/icon.js.map +1 -1
  28. package/dist/components/icon/remixicon-registry.d.ts +5 -0
  29. package/dist/components/icon/remixicon-registry.d.ts.map +1 -0
  30. package/dist/components/icon/remixicon-registry.js +14 -0
  31. package/dist/components/icon/remixicon-registry.js.map +1 -0
  32. package/package.json +1 -1
  33. package/src/components/alert/alert.stories.tsx +103 -2
  34. package/src/components/alert/alert.tsx +163 -16
  35. package/src/components/button/button-link.tsx +1 -1
  36. package/src/components/button/button.tsx +2 -1
  37. package/src/components/button/icon-button.tsx +2 -1
  38. package/src/components/checkbox/checkbox-links.tsx +5 -3
  39. package/src/components/checkbox/checkbox.stories.tsx +20 -4
  40. package/src/components/icon/icon.tsx +23 -13
  41. package/src/components/icon/remixicon-registry.ts +24 -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: {
@@ -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: {
@@ -76,6 +76,7 @@ export function Button({
76
76
  disabled={disabled || isLoading}
77
77
  aria-busy={isLoading}
78
78
  aria-live={isLoading ? 'polite' : undefined}
79
+ {...(asChild ? {'aria-disabled': disabled || isLoading} : {})}
79
80
  {...props}
80
81
  >
81
82
  {isLoading ? (
@@ -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: {
@@ -80,6 +80,7 @@ export function IconButton({
80
80
  disabled={disabled || isLoading}
81
81
  aria-busy={isLoading}
82
82
  aria-live={isLoading ? 'polite' : undefined}
83
+ {...(asChild ? {'aria-disabled': disabled || isLoading} : {})}
83
84
  {...props}
84
85
  >
85
86
  {isLoading ? (
@@ -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
  />
@@ -32,11 +32,26 @@ import {
32
32
  ThunderIcon,
33
33
  XCircleSolidIcon,
34
34
  } from './custom';
35
+ import {remixiconMap} from './remixicon-registry';
35
36
 
36
- const iconsMap = {
37
+ const commonRemixicons = {
38
+ addLine: RiAddLine,
39
+ close: RiCloseLine,
40
+ check: RiCheckLine,
41
+ copy: RiFileCopyLine,
42
+ info: RiInformationFill,
43
+ imageAdd: RiImageAddFill,
44
+ chevronRight: RiArrowRightSLine,
45
+ homeSmile: RiHomeSmileFill,
46
+ money: RiMoneyDollarCircleLine,
37
47
  google: RiGoogleFill,
38
48
  microsoft: RiMicrosoftFill,
39
49
  github: RiGithubFill,
50
+ subtractLine: RiSubtractLine,
51
+ bookOpen: RiBookOpenFill,
52
+ } as const satisfies Record<string, RemixiconComponentType>;
53
+
54
+ const customIconsMap = {
40
55
  shipfox: ShipfoxLogo,
41
56
  slack: SlackLogo,
42
57
  stripe: StripeLogo,
@@ -51,23 +66,18 @@ const iconsMap = {
51
66
  spinner: SpinnerIcon,
52
67
  thunder: ThunderIcon,
53
68
  xCircleSolid: XCircleSolidIcon,
54
- addLine: RiAddLine,
55
- bookOpen: RiBookOpenFill,
56
- check: RiCheckLine,
57
- chevronRight: RiArrowRightSLine,
58
- close: RiCloseLine,
59
- copy: RiFileCopyLine,
60
- homeSmile: RiHomeSmileFill,
61
- imageAdd: RiImageAddFill,
62
- info: RiInformationFill,
63
- money: RiMoneyDollarCircleLine,
64
- subtractLine: RiSubtractLine,
65
69
  } as const satisfies Record<string, RemixiconComponentType>;
66
70
 
71
+ const iconsMap = {
72
+ ...remixiconMap,
73
+ ...commonRemixicons,
74
+ ...customIconsMap,
75
+ } as Record<string, RemixiconComponentType> & typeof customIconsMap;
76
+
67
77
  export type IconName = keyof typeof iconsMap;
68
78
  export const iconNames = Object.keys(iconsMap) as IconName[];
69
79
 
70
- type BaseIconProps = ComponentProps<typeof RiGoogleFill>;
80
+ type BaseIconProps = ComponentProps<RemixiconComponentType>;
71
81
  type IconProps = {name: IconName} & Omit<BaseIconProps, 'name'>;
72
82
 
73
83
  export function Icon({name, ...props}: IconProps) {
@@ -0,0 +1,24 @@
1
+ import type {RemixiconComponentType} from '@remixicon/react';
2
+ import * as RemixIcons from '@remixicon/react';
3
+
4
+ const remixiconEntries = Object.entries(RemixIcons).filter(
5
+ ([key, value]) => key.startsWith('Ri') && typeof value === 'function',
6
+ ) as Array<[string, RemixiconComponentType]>;
7
+
8
+ function iconNameToKey(iconName: string): string {
9
+ const withoutPrefix = iconName.slice(2);
10
+ return withoutPrefix.charAt(0).toLowerCase() + withoutPrefix.slice(1);
11
+ }
12
+
13
+ const remixiconMapEntries = remixiconEntries.map(([name, component]) => [
14
+ iconNameToKey(name),
15
+ component,
16
+ ]) as Array<[string, RemixiconComponentType]>;
17
+
18
+ export const remixiconMap = Object.fromEntries(remixiconMapEntries) as Record<
19
+ string,
20
+ RemixiconComponentType
21
+ >;
22
+
23
+ export type RemixIconName = keyof typeof remixiconMap;
24
+ export const remixiconNames = Object.keys(remixiconMap) as RemixIconName[];