@g4rcez/components 0.0.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 (139) hide show
  1. package/.idea/bigweld.iml +12 -0
  2. package/.idea/codeStyles/Project.xml +72 -0
  3. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  4. package/.idea/inspectionProfiles/Project_Default.xml +30 -0
  5. package/.idea/jsLibraryMappings.xml +6 -0
  6. package/.idea/modules.xml +8 -0
  7. package/.idea/prettier.xml +7 -0
  8. package/.idea/reason.xml +6 -0
  9. package/.idea/vcs.xml +6 -0
  10. package/.prettierrc.json +13 -0
  11. package/README.md +35 -0
  12. package/app/client-table.tsx +35 -0
  13. package/app/favicon.ico +0 -0
  14. package/app/layout.tsx +39 -0
  15. package/app/page.tsx +72 -0
  16. package/dist/components/core/button.d.ts +21 -0
  17. package/dist/components/core/button.d.ts.map +1 -0
  18. package/dist/components/core/polymorph.d.ts +10 -0
  19. package/dist/components/core/polymorph.d.ts.map +1 -0
  20. package/dist/components/display/card.d.ts +4 -0
  21. package/dist/components/display/card.d.ts.map +1 -0
  22. package/dist/components/floating/dropdown.d.ts +11 -0
  23. package/dist/components/floating/dropdown.d.ts.map +1 -0
  24. package/dist/components/floating/tooltip.d.ts +9 -0
  25. package/dist/components/floating/tooltip.d.ts.map +1 -0
  26. package/dist/components/form/autocomplete.d.ts +16 -0
  27. package/dist/components/form/autocomplete.d.ts.map +1 -0
  28. package/dist/components/form/file-upload.d.ts +12 -0
  29. package/dist/components/form/file-upload.d.ts.map +1 -0
  30. package/dist/components/form/form.d.ts +4 -0
  31. package/dist/components/form/form.d.ts.map +1 -0
  32. package/dist/components/form/input-field.d.ts +25 -0
  33. package/dist/components/form/input-field.d.ts.map +1 -0
  34. package/dist/components/form/input.d.ts +9 -0
  35. package/dist/components/form/input.d.ts.map +1 -0
  36. package/dist/components/form/select.d.ts +11 -0
  37. package/dist/components/form/select.d.ts.map +1 -0
  38. package/dist/components/form/switch.d.ts +7 -0
  39. package/dist/components/form/switch.d.ts.map +1 -0
  40. package/dist/components/index.d.ts +15 -0
  41. package/dist/components/index.d.ts.map +1 -0
  42. package/dist/components/table/filter.d.ts +70 -0
  43. package/dist/components/table/filter.d.ts.map +1 -0
  44. package/dist/components/table/group.d.ts +17 -0
  45. package/dist/components/table/group.d.ts.map +1 -0
  46. package/dist/components/table/index.d.ts +28 -0
  47. package/dist/components/table/index.d.ts.map +1 -0
  48. package/dist/components/table/metadata.d.ts +3 -0
  49. package/dist/components/table/metadata.d.ts.map +1 -0
  50. package/dist/components/table/sort.d.ts +28 -0
  51. package/dist/components/table/sort.d.ts.map +1 -0
  52. package/dist/components/table/table-lib.d.ts +99 -0
  53. package/dist/components/table/table-lib.d.ts.map +1 -0
  54. package/dist/components/table/thead.d.ts +7 -0
  55. package/dist/components/table/thead.d.ts.map +1 -0
  56. package/dist/hooks/use-form.d.ts +28 -0
  57. package/dist/hooks/use-form.d.ts.map +1 -0
  58. package/dist/hooks/use-previous.d.ts +2 -0
  59. package/dist/hooks/use-previous.d.ts.map +1 -0
  60. package/dist/hooks/use-reactive.d.ts +2 -0
  61. package/dist/hooks/use-reactive.d.ts.map +1 -0
  62. package/dist/index.css +1670 -0
  63. package/dist/index.d.ts +7 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.mjs +21864 -0
  66. package/dist/index.mjs.map +1 -0
  67. package/dist/index.umd.js +151 -0
  68. package/dist/index.umd.js.map +1 -0
  69. package/dist/lib/dom.d.ts +6 -0
  70. package/dist/lib/dom.d.ts.map +1 -0
  71. package/dist/lib/fns.d.ts +5 -0
  72. package/dist/lib/fns.d.ts.map +1 -0
  73. package/dist/next.svg +1 -0
  74. package/dist/styles/design-tokens.d.ts +26 -0
  75. package/dist/styles/design-tokens.d.ts.map +1 -0
  76. package/dist/tailwind.config.d.ts +32 -0
  77. package/dist/tailwind.config.d.ts.map +1 -0
  78. package/dist/tailwind.config.js +153 -0
  79. package/dist/types.d.ts +9 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/vercel.svg +1 -0
  82. package/docs/README.md +36 -0
  83. package/docs/next.config.mjs +4 -0
  84. package/docs/package.json +28 -0
  85. package/docs/pnpm-lock.yaml +1030 -0
  86. package/docs/postcss.config.mjs +8 -0
  87. package/docs/public/next.svg +1 -0
  88. package/docs/public/vercel.svg +1 -0
  89. package/docs/src/app/favicon.ico +0 -0
  90. package/docs/src/app/globals.css +33 -0
  91. package/docs/src/app/layout.tsx +22 -0
  92. package/docs/src/app/page.tsx +10 -0
  93. package/docs/tailwind.config.ts +15 -0
  94. package/docs/tsconfig.json +26 -0
  95. package/next-env.d.ts +5 -0
  96. package/next.config.mjs +4 -0
  97. package/package.json +72 -0
  98. package/postcss.config.mjs +8 -0
  99. package/public/next.svg +1 -0
  100. package/public/vercel.svg +1 -0
  101. package/src/components/core/button.tsx +91 -0
  102. package/src/components/core/polymorph.tsx +17 -0
  103. package/src/components/display/card.tsx +8 -0
  104. package/src/components/floating/dropdown.tsx +93 -0
  105. package/src/components/floating/tooltip.tsx +67 -0
  106. package/src/components/form/autocomplete.tsx +222 -0
  107. package/src/components/form/file-upload.tsx +129 -0
  108. package/src/components/form/form.tsx +28 -0
  109. package/src/components/form/input-field.tsx +105 -0
  110. package/src/components/form/input.tsx +73 -0
  111. package/src/components/form/select.tsx +58 -0
  112. package/src/components/form/switch.tsx +40 -0
  113. package/src/components/index.ts +14 -0
  114. package/src/components/table/filter.tsx +186 -0
  115. package/src/components/table/group.tsx +123 -0
  116. package/src/components/table/index.tsx +207 -0
  117. package/src/components/table/metadata.tsx +55 -0
  118. package/src/components/table/sort.tsx +141 -0
  119. package/src/components/table/table-lib.ts +130 -0
  120. package/src/components/table/thead.tsx +108 -0
  121. package/src/hooks/use-form.ts +155 -0
  122. package/src/hooks/use-previous.ts +9 -0
  123. package/src/hooks/use-reactive.ts +10 -0
  124. package/src/index.css +37 -0
  125. package/src/index.ts +6 -0
  126. package/src/lib/dom.ts +27 -0
  127. package/src/lib/fns.ts +23 -0
  128. package/src/styles/dark.json +66 -0
  129. package/src/styles/design-tokens.ts +57 -0
  130. package/src/styles/light.json +49 -0
  131. package/src/types.ts +11 -0
  132. package/styles.config.ts +42 -0
  133. package/tailwind.config.ts +11 -0
  134. package/tsconfig.json +55 -0
  135. package/tsconfig.lib.json +50 -0
  136. package/tsconfig.lib.tsbuildinfo +1 -0
  137. package/tsconfig.tailwind.json +32 -0
  138. package/tsconfig.tsbuildinfo +1 -0
  139. package/vite.config.mts +39 -0
@@ -0,0 +1,222 @@
1
+ "use client";
2
+ import {
3
+ autoUpdate,
4
+ FloatingFocusManager,
5
+ FloatingPortal,
6
+ offset,
7
+ size,
8
+ useDismiss,
9
+ useFloating,
10
+ useInteractions,
11
+ useListNavigation,
12
+ useRole,
13
+ useTransitionStyles,
14
+ } from "@floating-ui/react";
15
+ import Fuzzy from "fuzzy-search";
16
+ import { ChevronDown } from "lucide-react";
17
+ import React, { forwardRef, useImperativeHandle, useRef, useState } from "react";
18
+ import { InputField, InputFieldProps } from "~/components/form/input-field";
19
+ import { type OptionProps } from "~/components/form/select";
20
+ import { usePrevious } from "~/hooks/use-previous";
21
+ import { css } from "~/lib/dom";
22
+
23
+ type ItemProps = Omit<React.HTMLProps<HTMLLIElement>, "children"> & {
24
+ selected: boolean;
25
+ active: boolean;
26
+ option: OptionProps;
27
+ };
28
+
29
+ export const Option = forwardRef<HTMLLIElement, ItemProps>(({ selected, active, onClick, option, ...rest }, ref) => (
30
+ <li {...rest} ref={ref} role="option" aria-selected={selected} className="w-full">
31
+ <button
32
+ type="button"
33
+ onClick={onClick as any}
34
+ aria-selected={selected}
35
+ className={`p-2 w-full text-left cursor-pointer ${selected ? "bg-primary text-primary-foreground" : ""} ${active ? "bg-primary-subtle text-primary-foreground" : ""}`}
36
+ >
37
+ {option.label ?? option.value}
38
+ </button>
39
+ </li>
40
+ ));
41
+
42
+ type SelectProps = Omit<InputFieldProps<"input">, "value"> & {
43
+ value?: string;
44
+ options: OptionProps[];
45
+ };
46
+
47
+ const transitionStyles = {
48
+ duration: 300,
49
+ initial: { transform: "scaleY(0)", opacity: 0.2 },
50
+ open: { transform: "scaleY(1)", opacity: 1 },
51
+ close: { transform: "scaleY(0)", opacity: 0 },
52
+ } as const;
53
+
54
+ const fuzzyOptions = { caseSensitive: false, sort: false };
55
+
56
+ const emptyRef: any[] = [];
57
+
58
+ export const Autocomplete = forwardRef<HTMLInputElement, SelectProps>(({ options, required = true, ...props }: SelectProps, externalRef) => {
59
+ const [open, setOpen] = useState(false);
60
+ const [shadow, setShadow] = useState("");
61
+ const [value, setValue] = useState(props.value ?? props.defaultValue ?? "");
62
+ const [index, setIndex] = useState<number | null>(null);
63
+ const listRef = useRef<Array<HTMLElement | null>>(emptyRef);
64
+ const previousIndex = usePrevious(index);
65
+ const list = new Fuzzy(options, ["value"], fuzzyOptions).search(shadow);
66
+
67
+ const { x, y, strategy, refs, context } = useFloating<HTMLInputElement>({
68
+ open,
69
+ transform: true,
70
+ onOpenChange: setOpen,
71
+ whileElementsMounted: autoUpdate,
72
+ middleware: [
73
+ offset(4),
74
+ size({
75
+ padding: 10,
76
+ apply(a) {
77
+ Object.assign(a.elements.floating.style, {
78
+ width: `${a.rects.reference.width}px`,
79
+ maxHeight: `${Math.min(480, a.availableHeight)}px`,
80
+ });
81
+ },
82
+ }),
83
+ ],
84
+ });
85
+
86
+ useImperativeHandle(externalRef, () => refs.domReference?.current!, [refs]);
87
+
88
+ const transitions = useTransitionStyles(context, transitionStyles);
89
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
90
+ useRole(context, { role: "listbox" }),
91
+ useDismiss(context),
92
+ useListNavigation(context, {
93
+ listRef,
94
+ loop: true,
95
+ activeIndex: index,
96
+ allowEscape: true,
97
+ focusItemOnOpen: "auto",
98
+ openOnArrowKeyDown: true,
99
+ scrollItemIntoView: true,
100
+ selectedIndex: index,
101
+ virtual: true,
102
+ onNavigate: (n) => {
103
+ const lastIndex = list.length - 1;
104
+ if (n === null && previousIndex === 0) return setIndex(lastIndex);
105
+ if (n === null && previousIndex === lastIndex) return setIndex(0);
106
+ const i = n ?? previousIndex ?? null;
107
+ return i === null ? undefined : setIndex(i);
108
+ },
109
+ }),
110
+ ]);
111
+
112
+ const onSelect = (opt: OptionProps) => {
113
+ setValue(opt.value);
114
+ setOpen(false);
115
+ setShadow("");
116
+ };
117
+
118
+ const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
119
+ const value = event.target.value;
120
+ setShadow(value);
121
+ if (!open && value === "") return setOpen(true);
122
+ return value ? setOpen(true) : props.onChange?.(event);
123
+ };
124
+
125
+ const onFocus = () => {
126
+ setOpen(true);
127
+ setShadow("");
128
+ };
129
+
130
+ const onClose = () => {
131
+ setShadow("");
132
+ setValue("");
133
+ setOpen(false);
134
+ };
135
+
136
+ return (
137
+ <fieldset className="relative w-auto">
138
+ <InputField
139
+ {...(props as any)}
140
+ required={required}
141
+ right={
142
+ <span className="flex items-center gap-0.5">
143
+ <ChevronDown size={20} />
144
+ {value ? (
145
+ <button type="button" onClick={onClose} className="link:text-danger transition-colors">
146
+ <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
147
+ <path
148
+ d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
149
+ fill="currentColor"
150
+ fillRule="evenodd"
151
+ clipRule="evenodd"
152
+ />
153
+ </svg>
154
+ </button>
155
+ ) : null}
156
+ </span>
157
+ }
158
+ >
159
+ <input
160
+ {...getReferenceProps({
161
+ ...props,
162
+ onChange,
163
+ onFocus,
164
+ ref: refs.setReference,
165
+ onClick: (e: React.MouseEvent<HTMLInputElement>) => e.currentTarget.focus(),
166
+ onKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
167
+ if (event.key === "Escape") {
168
+ event.currentTarget.blur();
169
+ return setOpen(false);
170
+ }
171
+ if (event.key === "Enter") {
172
+ if (index !== null && list[index]) {
173
+ event.preventDefault();
174
+ return onSelect(list[index]);
175
+ }
176
+ if (list.length === 1) {
177
+ event.preventDefault();
178
+ return onSelect(list[0]);
179
+ }
180
+ }
181
+ },
182
+ })}
183
+ required={required}
184
+ value={open ? shadow : value}
185
+ aria-autocomplete="list"
186
+ autoComplete="off"
187
+ className={css(
188
+ "input text-foreground group h-10 w-full flex-1 rounded-md bg-transparent p-2 placeholder-input-mask outline-none transition-colors group-error:text-danger group-error:placeholder-input-mask-error",
189
+ !!props.right || shadow ? "pe-12" : "",
190
+ !!props.left ? "ps-8" : "",
191
+ props.className
192
+ )}
193
+ />
194
+ </InputField>
195
+ <FloatingPortal preserveTabOrder>
196
+ <FloatingFocusManager closeOnFocusOut guards returnFocus context={context} initialFocus={-1} visuallyHiddenDismiss>
197
+ <ul
198
+ {...getFloatingProps({
199
+ ref: refs.setFloating,
200
+ style: { position: strategy, left: x ?? 0, top: y ?? 0, ...transitions.styles },
201
+ })}
202
+ data-floating="true"
203
+ className="bg-floating-background shadow-floating text-foreground list-none p-0 m-0 rounded-b-lg overflow-auto origin-[top_center] overflow-y-auto z-floating"
204
+ >
205
+ {list.map((item, i) => (
206
+ <Option
207
+ {...getItemProps({
208
+ onClick: () => onSelect(item),
209
+ ref: (node) => void (listRef.current[i] = node) as any,
210
+ })}
211
+ key={`${item.value}-option`}
212
+ option={item}
213
+ selected={index === i}
214
+ active={value === item.value}
215
+ />
216
+ ))}
217
+ </ul>
218
+ </FloatingFocusManager>
219
+ </FloatingPortal>
220
+ </fieldset>
221
+ );
222
+ });
@@ -0,0 +1,129 @@
1
+ import { FileIcon, Trash2Icon, UploadIcon } from "lucide-react";
2
+ import prettyBytes from "pretty-bytes";
3
+ import React, { Fragment, useEffect, useState } from "react";
4
+ import { DropzoneProps, useDropzone } from "react-dropzone";
5
+ import { Override } from "sidekicker";
6
+ import { Button } from "~/components/core/button";
7
+
8
+ type Props = Override<React.ComponentProps<"input">, DropzoneProps> & {
9
+ onDeleteFile?: (file: File) => void;
10
+ files?: File[];
11
+ idle?: React.ReactElement;
12
+ onDrop?: (file: File[]) => void;
13
+ };
14
+
15
+ const mime = {
16
+ isImage: (file: File) => file.type.includes("image"),
17
+ };
18
+
19
+ const FileViewer = (props: { file: File; onDeleteFile?: (file: File) => void }) => {
20
+ const [info, setInfo] = useState({ url: "", type: "", size: "" });
21
+ useEffect(() => {
22
+ if (mime.isImage(props.file)) {
23
+ const url = URL.createObjectURL(props.file);
24
+ setInfo({ url, type: "img", size: prettyBytes(props.file.size) });
25
+ return () => {
26
+ URL.revokeObjectURL(url);
27
+ };
28
+ }
29
+ setInfo({ url: "", type: props.file.type, size: prettyBytes(props.file.size) });
30
+ }, [props.file]);
31
+
32
+ if (info.type === "img") {
33
+ return (
34
+ <div className="flex flex-row gap-jade-200 items-center justify-between w-full">
35
+ <header className="flex flex-row gap-jade-200 items-center">
36
+ <img src={info.url} className="size-jade-500 rounded-jade-xsmall" alt={`Miniatura do arquivo ${props.file.name}`} />
37
+ <div className="flex flex-col">
38
+ <span>{props.file.name}</span>
39
+ <span>{info.size}</span>
40
+ </div>
41
+ </header>
42
+ <Button
43
+ className="isolate"
44
+ type="button"
45
+ theme="raw"
46
+ onClick={(e) => {
47
+ e.stopPropagation();
48
+ props.onDeleteFile?.(props.file);
49
+ }}
50
+ >
51
+ <Trash2Icon />
52
+ </Button>
53
+ </div>
54
+ );
55
+ }
56
+ return (
57
+ <div className="flex flex-row gap-jade-200 items-center justify-between w-full">
58
+ <header className="flex flex-row gap-4 items-center">
59
+ <FileIcon size={48} />
60
+ <div className="flex flex-col text-left justify-start items-start">
61
+ <span>{props.file.name}</span>
62
+ <span>{info.size}</span>
63
+ </div>
64
+ </header>
65
+ <Button
66
+ className="isolate"
67
+ type="button"
68
+ theme="raw"
69
+ onClick={(e) => {
70
+ e.stopPropagation();
71
+ props.onDeleteFile?.(props.file);
72
+ }}
73
+ >
74
+ <Trash2Icon className="text-danger" />
75
+ </Button>
76
+ </div>
77
+ );
78
+ };
79
+
80
+ const DefaultViewer = (props: { files: File[]; onDeleteFile?: (file: File) => void }) => {
81
+ return (
82
+ <ul className="w-full space-y-jade-200">
83
+ {props.files.map((file) => {
84
+ return <FileViewer onDeleteFile={props.onDeleteFile} key={file.name} file={file} />;
85
+ })}
86
+ </ul>
87
+ );
88
+ };
89
+
90
+ const InteractiveArea = (props: { isDragActive: boolean; idle: React.ReactElement; files: File[]; onDeleteFile?: (file: File) => void }) => {
91
+ if (props.isDragActive) {
92
+ return <p>Solte os arquivos selecionados</p>;
93
+ }
94
+ if (props.files.length > 0) {
95
+ return <DefaultViewer onDeleteFile={props.onDeleteFile} files={props.files} />;
96
+ }
97
+ return <Fragment>{props.idle}</Fragment>;
98
+ };
99
+
100
+ const DefaultIdle = (
101
+ <div className="flex flex-col gap-4 justify-center items-center">
102
+ <UploadIcon size={64} />
103
+ <p>
104
+ You can drag your files or{" "}
105
+ <button className="text-primary underline" type="button">
106
+ drag to here
107
+ </button>
108
+ </p>
109
+ </div>
110
+ );
111
+
112
+ export const FileUpload = ({ idle = DefaultIdle, onDeleteFile, onDrop, ...props }: Props) => {
113
+ const [files, setFiles] = useState<File[]>([]);
114
+ const drop = (x: File[]) => {
115
+ onDrop?.(x);
116
+ setFiles(x);
117
+ };
118
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop: drop });
119
+ return (
120
+ <div
121
+ {...getRootProps()}
122
+ data-active={props.files?.length ? props.files.length > 0 : false}
123
+ className="flex text-foreground flex-col items-center justify-center border-2 rounded-lg p-6 border-card-border data-[active=true]:bg-card-background data-[active=true]:border-solid data-[active=false]:border-dashed"
124
+ >
125
+ <input {...getInputProps(props as any)} name={props.name} id={props.name} />
126
+ <InteractiveArea onDeleteFile={onDeleteFile} isDragActive={isDragActive} idle={idle} files={props.files ?? files} />
127
+ </div>
128
+ );
129
+ };
@@ -0,0 +1,28 @@
1
+ "use client";
2
+ import React from "react";
3
+
4
+ const inputFields = ["INPUT", "SELECT"];
5
+
6
+ export const formReset = (form?: HTMLFormElement | null) => {
7
+ if (!form) return;
8
+ const elements = Array.from(form.elements);
9
+ elements.forEach((field) => {
10
+ if (!inputFields.includes(field.tagName)) return;
11
+ if (field.tagName === "INPUT") {
12
+ (field as HTMLInputElement).value = (field as HTMLInputElement).defaultValue;
13
+ }
14
+ if (field.tagName === "SELECT") {
15
+ (field as HTMLSelectElement).value = "";
16
+ }
17
+ field.setAttribute("data-initialized", "false");
18
+ });
19
+ };
20
+
21
+ export const Form = (props: React.ComponentProps<"form">) => {
22
+ const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
23
+ e.persist();
24
+ e.preventDefault();
25
+ props.onSubmit?.(e);
26
+ };
27
+ return <form {...props} onSubmit={onSubmit} />;
28
+ };
@@ -0,0 +1,105 @@
1
+ "use client";
2
+ import { CheckCircle, XCircle } from "lucide-react";
3
+ import React, { Fragment, PropsWithChildren } from "react";
4
+ import { PolymorphicProps } from "~/components/core/polymorph";
5
+ import { css } from "~/lib/dom";
6
+ import { Label } from "~/types";
7
+
8
+ export type FeedbackProps = React.PropsWithChildren<
9
+ Partial<{
10
+ title: string | React.ReactElement | React.ReactNode;
11
+ hideLeft?: boolean;
12
+ className?: string;
13
+ placeholder: string;
14
+ reportStatus: boolean;
15
+ }>
16
+ >;
17
+ export const InputFeedback = ({ reportStatus, hideLeft = false, className, children, title }: FeedbackProps) => (
18
+ <div className={css("w-full justify-between", hideLeft && children === null ? "hidden" : "flex", className)}>
19
+ {hideLeft ? null : (
20
+ <span className="flex items-center gap-1 group-hover:text-primary group-focus-within:text-primary transition-colors group-error:text-danger">
21
+ {title}
22
+ {reportStatus ? (
23
+ <span className="flex aspect-square h-4 w-4 items-center justify-center">
24
+ <CheckCircle
25
+ className="hidden aspect-square h-3 w-3 opacity-0 transition-opacity group-assert:block group-assert:text-success group-assert:opacity-100"
26
+ aria-hidden="true"
27
+ size={16}
28
+ strokeWidth={1}
29
+ absoluteStrokeWidth
30
+ />
31
+ <XCircle
32
+ className="hidden aspect-square h-3 w-3 opacity-0 transition-opacity group-error:block group-error:opacity-100"
33
+ aria-hidden="true"
34
+ size={16}
35
+ strokeWidth={1}
36
+ absoluteStrokeWidth
37
+ />
38
+ </span>
39
+ ) : null}
40
+ </span>
41
+ )}
42
+ {children}
43
+ </div>
44
+ );
45
+
46
+ export type InputFieldProps<T extends "input" | "select"> = PolymorphicProps<
47
+ Partial<{
48
+ error?: string;
49
+ hideLeft: boolean;
50
+ container: string;
51
+ left: Label;
52
+ optionalText: string;
53
+ right: Label;
54
+ rightLabel: Label;
55
+ id: string;
56
+ name: string;
57
+ placeholder: string;
58
+ }>,
59
+ T
60
+ >;
61
+
62
+ export const InputField = <T extends "input" | "select">({
63
+ optionalText = "Optional",
64
+ left,
65
+ rightLabel,
66
+ container,
67
+ right,
68
+ children,
69
+ error,
70
+ form,
71
+ id,
72
+ name,
73
+ title,
74
+ placeholder,
75
+ hideLeft,
76
+ required,
77
+ }: PropsWithChildren<InputFieldProps<T>>) => {
78
+ const ID = id ?? name;
79
+ return (
80
+ <fieldset data-error={!!error} form={form} className={css("group inline-block w-full", container)}>
81
+ <label
82
+ form={form}
83
+ htmlFor={ID}
84
+ className="inline-flex w-full cursor-text flex-row flex-wrap justify-between gap-1 text-sm transition-colors empty:hidden group-error:text-danger group-hover:border-primary"
85
+ >
86
+ {!hideLeft && !rightLabel ? (
87
+ <InputFeedback hideLeft={hideLeft} reportStatus title={title} placeholder={placeholder}>
88
+ {optionalText || rightLabel ? (
89
+ <Fragment>
90
+ {!required ? <span className="text-opacity-70">{optionalText}</span> : null}
91
+ {rightLabel ? <Fragment>{rightLabel}</Fragment> : null}
92
+ </Fragment>
93
+ ) : null}
94
+ </InputFeedback>
95
+ ) : null}
96
+ <div className="relative group flex w-full flex-row flex-nowrap items-center gap-x-2 gap-y-1 rounded-md border border-input-border bg-transparent transition-colors group-focus-within:border-primary group-hover:border-primary group-error:border-danger">
97
+ {left ? <span className="absolute left-0 flex flex-nowrap gap-1 whitespace-nowrap pl-2">{left}</span> : null}
98
+ {children}
99
+ {right ? <span className="absolute right-0 flex flex-nowrap gap-2 whitespace-nowrap pr-1">{right}</span> : null}
100
+ </div>
101
+ </label>
102
+ <p className="mt-1 text-xs group-error:block group-error:text-danger">{error}</p>
103
+ </fieldset>
104
+ );
105
+ };
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ import React, { forwardRef, useEffect, useRef } from "react";
3
+ import MaskInput, { TheMaskProps } from "the-mask-input";
4
+ import { FeedbackProps, InputField, InputFieldProps } from "~/components/form/input-field";
5
+ import { css, mergeRefs } from "~/lib/dom";
6
+ import { Override } from "~/types";
7
+
8
+ export type InputProps = Override<
9
+ InputFieldProps<"input">,
10
+ TheMaskProps &
11
+ FeedbackProps & {
12
+ next?: string;
13
+ }
14
+ >;
15
+
16
+ export const Input: React.FC<InputProps> = forwardRef<HTMLInputElement, InputProps>(
17
+ ({ type = "text", container, next, rightLabel, optionalText, hideLeft = false, right, left, ...props }: InputProps, ref): any => {
18
+ const id = props.id ?? props.name;
19
+ const inputRef = useRef<HTMLInputElement>(null);
20
+
21
+ useEffect(() => {
22
+ if (inputRef.current === null) return;
23
+ const input = inputRef.current;
24
+ const focus = () => input.setAttribute("data-initialized", "true");
25
+ const goNextInputImpl = (e: Event) => {
26
+ const event = e as KeyboardEvent;
27
+ if (event.key === "Enter" && input.enterKeyHint === "next") {
28
+ const focusNext = input.getAttribute("data-next");
29
+ if (focusNext) {
30
+ const el = document.getElementById(focusNext);
31
+ if (el) {
32
+ el.focus();
33
+ return void event.preventDefault();
34
+ }
35
+ }
36
+ }
37
+ };
38
+ input.addEventListener("keydown", goNextInputImpl);
39
+ input.addEventListener("focus", focus);
40
+ return () => {
41
+ input.removeEventListener("keydown", goNextInputImpl);
42
+ input.removeEventListener("focus", focus);
43
+ };
44
+ }, []);
45
+
46
+ return (
47
+ <InputField<"input">
48
+ {...(props as any)}
49
+ right={right}
50
+ left={left}
51
+ hideLeft={hideLeft}
52
+ rightLabel={rightLabel}
53
+ optionalText={optionalText}
54
+ container={css("group inline-block w-full", container)}
55
+ >
56
+ <MaskInput
57
+ {...props}
58
+ type={type}
59
+ data-next={next}
60
+ ref={mergeRefs(ref, inputRef)}
61
+ id={id}
62
+ name={id}
63
+ className={css(
64
+ "input text-foreground group h-10 w-full flex-1 rounded-md bg-transparent p-2 placeholder-input-mask outline-none transition-colors group-error:text-danger group-error:placeholder-input-mask-error",
65
+ !!right ? "pe-4" : "",
66
+ !!left ? "ps-4" : "",
67
+ props.className
68
+ )}
69
+ />
70
+ </InputField>
71
+ );
72
+ }
73
+ ) as any;
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import { ChevronDown } from "lucide-react";
3
+ import React, { forwardRef, useEffect, useRef } from "react";
4
+ import { InputField, InputFieldProps } from "~/components/form/input-field";
5
+ import { css, mergeRefs } from "~/lib/dom";
6
+ import { Override } from "~/types";
7
+
8
+ export type OptionProps = Override<React.ComponentProps<"option">, { value: string }>;
9
+
10
+ export type SelectProps = Override<InputFieldProps<"select">, { options: OptionProps[] }>;
11
+
12
+ export const Select = forwardRef<HTMLSelectElement, SelectProps>(({ container, required = true, options, ...props }: SelectProps, ref) => {
13
+ const inputRef = useRef<HTMLSelectElement>(null);
14
+ const id = props.id ?? props.name;
15
+
16
+ useEffect(() => {
17
+ if (inputRef.current === null) return;
18
+ const input = inputRef.current;
19
+ const focus = () => input.setAttribute("data-initialized", "true");
20
+ const change = () => input.setAttribute("data-selected", "true");
21
+ input.addEventListener("focus", focus);
22
+ input.addEventListener("change", change);
23
+ return () => {
24
+ input.removeEventListener("focus", focus);
25
+ input.removeEventListener("change", change);
26
+ };
27
+ }, []);
28
+
29
+ return (
30
+ <InputField<"select">
31
+ {...(props as any)}
32
+ required={required}
33
+ container={css("group inline-block w-full", container)}
34
+ right={<ChevronDown size={20} />}
35
+ >
36
+ <select
37
+ {...props}
38
+ ref={mergeRefs(ref, inputRef)}
39
+ id={id}
40
+ name={id}
41
+ required={required}
42
+ data-selected={!!props.value || false}
43
+ className={css(
44
+ "input bg-transparent text-foreground select group h-10 w-full flex-1 rounded-md p-2 placeholder-input-placeholder outline-none transition-colors group-error:text-danger group-error:placeholder-input-mask-error",
45
+ "data-[selected=false]:text-input-placeholder",
46
+ props.className
47
+ )}
48
+ >
49
+ <option value="" hidden disabled>
50
+ {props.placeholder}
51
+ </option>
52
+ {options.map((option) => (
53
+ <option key={`${id}-select-option-${option.value}`} {...option} children={option.label ?? option.value} />
54
+ ))}
55
+ </select>
56
+ </InputField>
57
+ );
58
+ });
@@ -0,0 +1,40 @@
1
+ import React, { useId, useState } from "react";
2
+
3
+ type SwitchProps = Omit<React.ComponentProps<"input">, "onChange"> & { onCheck?: (nextValue: boolean) => void };
4
+
5
+ export const Switch = ({ children, ...props }: SwitchProps) => {
6
+ const id = useId();
7
+ const [innerChecked, setInnerChecked] = useState(false);
8
+ const checked = props.checked ?? innerChecked;
9
+
10
+ const onCheck = (e: React.MouseEvent<HTMLButtonElement>) => {
11
+ const button = e.target as HTMLButtonElement;
12
+ const checked = !(button.dataset.checked === "true");
13
+ setInnerChecked(checked);
14
+ props?.onCheck?.(checked);
15
+ };
16
+
17
+ return (
18
+ <div className="flex items-center">
19
+ <input {...props} hidden type="checkbox" checked={checked} onChange={(e) => setInnerChecked(e.target.checked)} />
20
+ <button
21
+ type="button"
22
+ role="switch"
23
+ onClick={onCheck}
24
+ aria-checked={checked}
25
+ data-checked={checked}
26
+ aria-labelledby={`${id}-label`}
27
+ className="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent data-[checked=false]:bg-input-switch-bg data-[checked=true]:bg-primary transition-colors ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
28
+ >
29
+ <span
30
+ aria-hidden="true"
31
+ data-checked={checked}
32
+ className="data-[checked=false]:bg-disabled data-[checked=true]:bg-input-switch pointer-events-none inline-block size-5 aspect-square data-[checked=false]translate-x-0 data-[checked=true]:translate-x-5 transform rounded-full shadow ring-0 transition ease-in-out"
33
+ />
34
+ </button>
35
+ <span className="ml-3 text-sm" id={`${id}-label`}>
36
+ <span className="font-medium text-foreground">{children}</span>
37
+ </span>
38
+ </div>
39
+ );
40
+ };
@@ -0,0 +1,14 @@
1
+ export * from "./form/autocomplete";
2
+ export * from "./form/input";
3
+ export * from "./form/select";
4
+ export * from "./form/file-upload";
5
+ export * from "./form/form";
6
+ export * from "./form/select";
7
+ export * from "./form/switch";
8
+ export * from "./form/input-field";
9
+ export * from "./core/button";
10
+ export * from "./core/polymorph";
11
+ export * from "./display/card";
12
+ export * from "./floating/dropdown";
13
+ export * from "./floating/tooltip";
14
+ export * from "./table/index";