@gv-tech/ui-web 2.6.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 (56) hide show
  1. package/package.json +88 -0
  2. package/src/accordion.tsx +58 -0
  3. package/src/alert-dialog.tsx +121 -0
  4. package/src/alert.tsx +49 -0
  5. package/src/aspect-ratio.tsx +7 -0
  6. package/src/avatar.tsx +40 -0
  7. package/src/badge.tsx +34 -0
  8. package/src/breadcrumb.tsx +105 -0
  9. package/src/button.tsx +47 -0
  10. package/src/calendar.tsx +163 -0
  11. package/src/card.tsx +46 -0
  12. package/src/carousel.tsx +234 -0
  13. package/src/chart.tsx +296 -0
  14. package/src/checkbox.tsx +31 -0
  15. package/src/collapsible.tsx +15 -0
  16. package/src/command.tsx +154 -0
  17. package/src/context-menu.tsx +208 -0
  18. package/src/dialog.tsx +95 -0
  19. package/src/drawer.tsx +110 -0
  20. package/src/dropdown-menu.tsx +212 -0
  21. package/src/form.tsx +160 -0
  22. package/src/hooks/use-theme.ts +15 -0
  23. package/src/hooks/use-toast.ts +189 -0
  24. package/src/hover-card.tsx +35 -0
  25. package/src/index.ts +474 -0
  26. package/src/input.tsx +23 -0
  27. package/src/label.tsx +21 -0
  28. package/src/lib/utils.ts +6 -0
  29. package/src/menubar.tsx +244 -0
  30. package/src/navigation-menu.tsx +143 -0
  31. package/src/pagination.tsx +107 -0
  32. package/src/popover.tsx +45 -0
  33. package/src/progress.tsx +28 -0
  34. package/src/radio-group.tsx +41 -0
  35. package/src/resizable.tsx +59 -0
  36. package/src/scroll-area.tsx +42 -0
  37. package/src/search.tsx +87 -0
  38. package/src/select.tsx +169 -0
  39. package/src/separator.tsx +24 -0
  40. package/src/setupTests.ts +114 -0
  41. package/src/sheet.tsx +136 -0
  42. package/src/skeleton.tsx +10 -0
  43. package/src/slider.tsx +27 -0
  44. package/src/sonner.tsx +32 -0
  45. package/src/switch.tsx +31 -0
  46. package/src/table.tsx +104 -0
  47. package/src/tabs.tsx +62 -0
  48. package/src/text.tsx +55 -0
  49. package/src/textarea.tsx +25 -0
  50. package/src/theme-provider.tsx +15 -0
  51. package/src/theme-toggle.tsx +92 -0
  52. package/src/toast.tsx +111 -0
  53. package/src/toaster.tsx +27 -0
  54. package/src/toggle-group.tsx +55 -0
  55. package/src/toggle.tsx +24 -0
  56. package/src/tooltip.tsx +51 -0
package/src/form.tsx ADDED
@@ -0,0 +1,160 @@
1
+ 'use client';
2
+
3
+ import * as LabelPrimitive from '@radix-ui/react-label';
4
+ import { Slot } from '@radix-ui/react-slot';
5
+ import * as React from 'react';
6
+ import {
7
+ Controller,
8
+ FormProvider,
9
+ useFormContext,
10
+ type ControllerProps,
11
+ type FieldPath,
12
+ type FieldValues,
13
+ } from 'react-hook-form';
14
+
15
+ import {
16
+ FormControlBaseProps,
17
+ FormDescriptionBaseProps,
18
+ FormItemBaseProps,
19
+ FormLabelBaseProps,
20
+ FormMessageBaseProps,
21
+ } from '@gv-tech/ui-core';
22
+ import { Label } from './label';
23
+ import { cn } from './lib/utils';
24
+
25
+ const Form = FormProvider;
26
+
27
+ type FormFieldContextValue<
28
+ TFieldValues extends FieldValues = FieldValues,
29
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
30
+ > = {
31
+ name: TName;
32
+ };
33
+
34
+ const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
35
+
36
+ const FormField = <
37
+ TFieldValues extends FieldValues = FieldValues,
38
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
39
+ >({
40
+ ...props
41
+ }: ControllerProps<TFieldValues, TName>) => {
42
+ return (
43
+ <FormFieldContext.Provider value={{ name: props.name }}>
44
+ <Controller {...props} />
45
+ </FormFieldContext.Provider>
46
+ );
47
+ };
48
+
49
+ const useFormField = () => {
50
+ const fieldContext = React.useContext(FormFieldContext);
51
+ const itemContext = React.useContext(FormItemContext);
52
+ const { getFieldState, formState } = useFormContext();
53
+
54
+ if (!fieldContext) {
55
+ throw new Error('useFormField should be used within <FormField>');
56
+ }
57
+
58
+ if (!itemContext) {
59
+ throw new Error('useFormField should be used within <FormItem>');
60
+ }
61
+
62
+ const fieldState = getFieldState(fieldContext.name, formState);
63
+
64
+ const { id } = itemContext;
65
+
66
+ return {
67
+ id,
68
+ name: fieldContext.name,
69
+ formItemId: `${id}-form-item`,
70
+ formDescriptionId: `${id}-form-item-description`,
71
+ formMessageId: `${id}-form-item-message`,
72
+ ...fieldState,
73
+ };
74
+ };
75
+
76
+ type FormItemContextValue = {
77
+ id: string;
78
+ };
79
+
80
+ const FormItemContext = React.createContext<FormItemContextValue | null>(null);
81
+
82
+ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & FormItemBaseProps>(
83
+ ({ className, ...props }, ref) => {
84
+ const id = React.useId();
85
+
86
+ return (
87
+ <FormItemContext.Provider value={{ id }}>
88
+ <div ref={ref} className={cn('space-y-2', className)} {...props} />
89
+ </FormItemContext.Provider>
90
+ );
91
+ },
92
+ );
93
+ FormItem.displayName = 'FormItem';
94
+
95
+ const FormLabel = React.forwardRef<
96
+ React.ElementRef<typeof LabelPrimitive.Root>,
97
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & FormLabelBaseProps
98
+ >(({ className, ...props }, ref) => {
99
+ const { error, formItemId } = useFormField();
100
+
101
+ return <Label ref={ref} className={cn(error && 'text-destructive', className)} htmlFor={formItemId} {...props} />;
102
+ });
103
+ FormLabel.displayName = 'FormLabel';
104
+
105
+ const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
106
+ ({ ...props }, ref) => {
107
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
108
+
109
+ return (
110
+ <Slot
111
+ ref={ref}
112
+ id={formItemId}
113
+ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
114
+ aria-invalid={!!error}
115
+ {...props}
116
+ />
117
+ );
118
+ },
119
+ );
120
+ FormControl.displayName = 'FormControl';
121
+
122
+ const FormDescription = React.forwardRef<
123
+ HTMLParagraphElement,
124
+ React.HTMLAttributes<HTMLParagraphElement> & FormDescriptionBaseProps
125
+ >(({ className, ...props }, ref) => {
126
+ const { formDescriptionId } = useFormField();
127
+
128
+ return (
129
+ <p ref={ref} id={formDescriptionId} className={cn('text-[0.8rem] text-muted-foreground', className)} {...props} />
130
+ );
131
+ });
132
+ FormDescription.displayName = 'FormDescription';
133
+
134
+ const FormMessage = React.forwardRef<
135
+ HTMLParagraphElement,
136
+ React.HTMLAttributes<HTMLParagraphElement> & FormMessageBaseProps
137
+ >(({ className, children, ...props }, ref) => {
138
+ const { error, formMessageId } = useFormField();
139
+ const body = error ? String(error?.message ?? '') : children;
140
+
141
+ if (!body) {
142
+ return null;
143
+ }
144
+
145
+ return (
146
+ <p ref={ref} id={formMessageId} className={cn('text-[0.8rem] font-medium text-destructive', className)} {...props}>
147
+ {body}
148
+ </p>
149
+ );
150
+ });
151
+ FormMessage.displayName = 'FormMessage';
152
+
153
+ export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField };
154
+ export type {
155
+ FormControlBaseProps as FormControlProps,
156
+ FormDescriptionBaseProps as FormDescriptionProps,
157
+ FormItemBaseProps as FormItemProps,
158
+ FormLabelBaseProps as FormLabelProps,
159
+ FormMessageBaseProps as FormMessageProps,
160
+ };
@@ -0,0 +1,15 @@
1
+ import { theme } from '@gv-tech/design-tokens';
2
+ import { useTheme as useNextTheme } from 'next-themes';
3
+
4
+ export function useTheme() {
5
+ const context = useNextTheme();
6
+ const { resolvedTheme } = context;
7
+
8
+ // Default to light theme tokens if resolvedTheme is undefined or invalid
9
+ const activeTokens = resolvedTheme === 'dark' ? theme.dark : theme.light;
10
+
11
+ return {
12
+ ...context,
13
+ tokens: activeTokens,
14
+ };
15
+ }
@@ -0,0 +1,189 @@
1
+ 'use client';
2
+
3
+ // Inspired by react-hot-toast library
4
+ import * as React from 'react';
5
+
6
+ import type { ToastActionElement, ToastProps } from '../index';
7
+
8
+ const TOAST_LIMIT = 1;
9
+ const TOAST_REMOVE_DELAY = 1000000;
10
+
11
+ type ToasterToast = ToastProps & {
12
+ id: string;
13
+ title?: React.ReactNode;
14
+ description?: React.ReactNode;
15
+ action?: ToastActionElement;
16
+ };
17
+
18
+ const actionTypes = {
19
+ ADD_TOAST: 'ADD_TOAST',
20
+ UPDATE_TOAST: 'UPDATE_TOAST',
21
+ DISMISS_TOAST: 'DISMISS_TOAST',
22
+ REMOVE_TOAST: 'REMOVE_TOAST',
23
+ } as const;
24
+
25
+ let count = 0;
26
+
27
+ function genId() {
28
+ count = (count + 1) % Number.MAX_SAFE_INTEGER;
29
+ return count.toString();
30
+ }
31
+
32
+ type ActionType = typeof actionTypes;
33
+
34
+ type Action =
35
+ | {
36
+ type: ActionType['ADD_TOAST'];
37
+ toast: ToasterToast;
38
+ }
39
+ | {
40
+ type: ActionType['UPDATE_TOAST'];
41
+ toast: Partial<ToasterToast>;
42
+ }
43
+ | {
44
+ type: ActionType['DISMISS_TOAST'];
45
+ toastId?: ToasterToast['id'];
46
+ }
47
+ | {
48
+ type: ActionType['REMOVE_TOAST'];
49
+ toastId?: ToasterToast['id'];
50
+ };
51
+
52
+ interface State {
53
+ toasts: ToasterToast[];
54
+ }
55
+
56
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
57
+
58
+ const addToRemoveQueue = (toastId: string) => {
59
+ if (toastTimeouts.has(toastId)) {
60
+ return;
61
+ }
62
+
63
+ const timeout = setTimeout(() => {
64
+ toastTimeouts.delete(toastId);
65
+ dispatch({
66
+ type: 'REMOVE_TOAST',
67
+ toastId: toastId,
68
+ });
69
+ }, TOAST_REMOVE_DELAY);
70
+
71
+ toastTimeouts.set(toastId, timeout);
72
+ };
73
+
74
+ export const reducer = (state: State, action: Action): State => {
75
+ switch (action.type) {
76
+ case actionTypes.ADD_TOAST:
77
+ return {
78
+ ...state,
79
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80
+ };
81
+
82
+ case actionTypes.UPDATE_TOAST:
83
+ return {
84
+ ...state,
85
+ toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
86
+ };
87
+
88
+ case actionTypes.DISMISS_TOAST: {
89
+ const { toastId } = action;
90
+
91
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
92
+ // but I'll keep it here for simplicity
93
+ if (toastId) {
94
+ addToRemoveQueue(toastId);
95
+ } else {
96
+ state.toasts.forEach((toast) => {
97
+ addToRemoveQueue(toast.id);
98
+ });
99
+ }
100
+
101
+ return {
102
+ ...state,
103
+ toasts: state.toasts.map((t) =>
104
+ t.id === toastId || toastId === undefined
105
+ ? {
106
+ ...t,
107
+ open: false,
108
+ }
109
+ : t,
110
+ ),
111
+ };
112
+ }
113
+ case actionTypes.REMOVE_TOAST:
114
+ if (action.toastId === undefined) {
115
+ return {
116
+ ...state,
117
+ toasts: [],
118
+ };
119
+ }
120
+ return {
121
+ ...state,
122
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
123
+ };
124
+ }
125
+ };
126
+
127
+ const listeners: Array<(state: State) => void> = [];
128
+
129
+ let memoryState: State = { toasts: [] };
130
+
131
+ function dispatch(action: Action) {
132
+ memoryState = reducer(memoryState, action);
133
+ listeners.forEach((listener) => {
134
+ listener(memoryState);
135
+ });
136
+ }
137
+
138
+ type Toast = Omit<ToasterToast, 'id'>;
139
+
140
+ function toast({ ...props }: Toast) {
141
+ const id = genId();
142
+
143
+ const update = (props: ToasterToast) =>
144
+ dispatch({
145
+ type: 'UPDATE_TOAST',
146
+ toast: { ...props, id },
147
+ });
148
+ const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
149
+
150
+ dispatch({
151
+ type: 'ADD_TOAST',
152
+ toast: {
153
+ ...props,
154
+ id,
155
+ open: true,
156
+ onOpenChange: (open) => {
157
+ if (!open) dismiss();
158
+ },
159
+ },
160
+ });
161
+
162
+ return {
163
+ id: id,
164
+ dismiss,
165
+ update,
166
+ };
167
+ }
168
+
169
+ function useToast() {
170
+ const [state, setState] = React.useState<State>(memoryState);
171
+
172
+ React.useEffect(() => {
173
+ listeners.push(setState);
174
+ return () => {
175
+ const index = listeners.indexOf(setState);
176
+ if (index > -1) {
177
+ listeners.splice(index, 1);
178
+ }
179
+ };
180
+ }, [state]);
181
+
182
+ return {
183
+ ...state,
184
+ toast,
185
+ dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
186
+ };
187
+ }
188
+
189
+ export { toast, useToast };
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
4
+ import * as React from 'react';
5
+
6
+ import { HoverCardBaseProps, HoverCardContentBaseProps, HoverCardTriggerBaseProps } from '@gv-tech/ui-core';
7
+ import { cn } from './lib/utils';
8
+
9
+ const HoverCard = HoverCardPrimitive.Root;
10
+
11
+ const HoverCardTrigger = HoverCardPrimitive.Trigger;
12
+
13
+ const HoverCardContent = React.forwardRef<
14
+ React.ElementRef<typeof HoverCardPrimitive.Content>,
15
+ React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> & HoverCardContentBaseProps
16
+ >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
17
+ <HoverCardPrimitive.Content
18
+ ref={ref}
19
+ align={align}
20
+ sideOffset={sideOffset}
21
+ className={cn(
22
+ 'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]',
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ ));
28
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
29
+
30
+ export { HoverCard, HoverCardContent, HoverCardTrigger };
31
+ export type {
32
+ HoverCardContentBaseProps as HoverCardContentProps,
33
+ HoverCardBaseProps as HoverCardProps,
34
+ HoverCardTriggerBaseProps as HoverCardTriggerProps,
35
+ };