This commit is contained in:
2026-10-01 08:02:47 +01:00
parent 6392457ffb
commit 3dc9f12ad4
104 changed files with 6536 additions and 1037 deletions
+8 -5
View File
@@ -1,8 +1,8 @@
import React, {
ComponentProps,
DetailedHTMLProps,
HTMLAttributes,
ReactNode,
type ComponentProps,
type DetailedHTMLProps,
type HTMLAttributes,
type ReactNode,
} from "react";
import { twMerge } from "tailwind-merge";
import CheckMarkSVG from "../svgs/CheckMarkSVG";
@@ -36,6 +36,7 @@ export type CheckboxProps = Omit<
noLabel?: boolean;
size?: number;
changeHandler?: (value: boolean) => void;
clickDispatchHandler?: (value: boolean) => void;
info?: string | ReactNode;
wrapperWrapperProps?: ComponentProps<typeof Stack>;
};
@@ -61,6 +62,7 @@ export default function Checkbox({
wrapperWrapperProps,
noLabel,
title,
clickDispatchHandler,
...props
}: CheckboxProps) {
const finalSize = size || 20;
@@ -95,9 +97,10 @@ export default function Checkbox({
wrapperClassName,
wrapperProps?.className,
)}
onClick={() => {
onClick={(e) => {
setChecked(!checked);
externalSetChecked?.(!checked);
clickDispatchHandler?.(!checked);
}}
>
<div
+8 -4
View File
@@ -1,11 +1,15 @@
import Button from "../layout/Button";
import Stack from "../layout/Stack";
import { FileArchive, FilePlus2, X } from "lucide-react";
import React, { ComponentProps, DetailedHTMLProps, ReactNode } from "react";
import React, {
type ComponentProps,
type DetailedHTMLProps,
type ReactNode,
} from "react";
import Card from "../elements/Card";
import Span from "../layout/Span";
import Center from "../layout/Center";
import { FileInputToBase64FunctionReturn } from "../utils/form/fileInputToBase64";
import type { FileInputToBase64FunctionReturn } from "../utils/form/fileInputToBase64";
import { twMerge } from "tailwind-merge";
import fileInputToBase64 from "../utils/form/fileInputToBase64";
import Row from "../layout/Row";
@@ -149,7 +153,7 @@ export default function FileUpload({
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fileObj = await fileInputToBase64({
inputFile: file,
inputFile: file!,
});
filesArr.push(fileObj);
}
@@ -330,7 +334,7 @@ export default function FileUpload({
}
});
} else {
inputFile = e.dataTransfer.files?.[0];
inputFile = e.dataTransfer.files?.[0]!;
}
if (!inputFile) return;
+1 -1
View File
@@ -1,5 +1,5 @@
import _ from "lodash";
import { DetailedHTMLProps, FormHTMLAttributes, RefObject } from "react";
import type { DetailedHTMLProps, FormHTMLAttributes, RefObject } from "react";
import { twMerge } from "tailwind-merge";
type Props<T extends { [key: string]: any } = { [key: string]: any }> =
+53 -40
View File
@@ -1,12 +1,12 @@
import Button from "../layout/Button";
import Stack from "../layout/Stack";
import { ImagePlus, X } from "lucide-react";
import React, { DetailedHTMLProps } from "react";
import React, { type DetailedHTMLProps } from "react";
import Card from "../elements/Card";
import Span from "../layout/Span";
import Center from "../layout/Center";
import imageInputToBase64, {
ImageInputToBase64FunctionReturn,
type ImageInputToBase64FunctionReturn,
} from "../utils/form/imageInputToBase64";
import { twMerge } from "tailwind-merge";
import Tag from "../elements/Tag";
@@ -20,6 +20,9 @@ type ImageUploadProps = DetailedHTMLProps<
onChangeHandler?: (
imgData: ImageInputToBase64FunctionReturn | undefined,
) => any;
multipleOnChangeHandler?: (
imgData: ImageInputToBase64FunctionReturn[] | undefined,
) => any;
fileInputProps?: DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
@@ -50,6 +53,7 @@ type ImageUploadProps = DetailedHTMLProps<
setImgURL?: React.Dispatch<React.SetStateAction<string | undefined>>;
externalImage?: ImageInputToBase64FunctionReturn;
restoreImageFn?: () => void;
upload_only?: boolean;
};
/**
@@ -71,6 +75,8 @@ export default function ImageUpload({
restoreImageFn,
setLoading,
setImgURL,
multipleOnChangeHandler,
upload_only,
...props
}: ImageUploadProps) {
const [imageObject, setImageObject] = React.useState<
@@ -119,7 +125,7 @@ export default function ImageUpload({
});
imgArr.push(fileObj);
}
multipleOnChangeHandler?.(imgArr);
externalSetImages?.(imgArr);
setLoading?.(false);
})();
@@ -208,43 +214,50 @@ export default function ImageUpload({
<Span size="smaller" variant="faded">
{label || "Click to Upload Image"}
</Span>
<Stack className="cancel-upload w-full items-stretch gap-1">
<Input
placeholder="Eg. https://example.com/img.png"
className="text-sm twui-image-url-input"
title="Enter Image URL"
wrapperWrapperProps={{ className: "mt-2" }}
changeHandler={(value) => {
imageUrlRef.current = value;
}}
showLabel
/>
<Button
title="Restore Image Button"
size="smaller"
variant="outlined"
color="gray"
onClick={() => {
if (!imageUrlRef.current) return;
setSrc(imageUrlRef.current);
}}
>
Set Image URL
</Button>
</Stack>
{existingImageUrl && (
<Button
title="Restore Image Button"
size="smaller"
variant="ghost"
onClick={() => {
restoreImageFn?.() ||
setSrc(existingImageUrl);
}}
className="cancel-upload"
>
Restore Original Image
</Button>
{upload_only ? null : (
<>
<Stack className="cancel-upload w-full items-stretch gap-1">
<Input
placeholder="URL Eg. https://example.com/img.png"
className="text-sm twui-image-url-input"
title="Enter Image URL"
wrapperWrapperProps={{
className: "mt-2",
}}
changeHandler={(value) => {
imageUrlRef.current = value;
}}
/>
<Button
title="Restore Image Button"
size="smaller"
variant="outlined"
color="gray"
onClick={() => {
if (!imageUrlRef.current)
return;
setSrc(imageUrlRef.current);
}}
className="opacity-70"
>
Set Image URL
</Button>
</Stack>
{existingImageUrl && (
<Button
title="Restore Image Button"
size="smaller"
variant="ghost"
onClick={() => {
restoreImageFn?.() ||
setSrc(existingImageUrl);
}}
className="cancel-upload"
>
Restore Original Image
</Button>
)}
</>
)}
</Stack>
</Center>
+1 -1
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import Row from "../../layout/Row";
import { Info, Minus, Plus } from "lucide-react";
import twuiNumberfy from "../../utils/numberfy";
import { InputProps } from ".";
import type { InputProps } from ".";
let pressInterval: any;
let pressTimeout: any;
+67 -19
View File
@@ -1,16 +1,16 @@
import React, {
ComponentProps,
DetailedHTMLProps,
InputHTMLAttributes,
LabelHTMLAttributes,
ReactNode,
RefObject,
TextareaHTMLAttributes,
type ComponentProps,
type DetailedHTMLProps,
type InputHTMLAttributes,
type LabelHTMLAttributes,
type ReactNode,
type RefObject,
type TextareaHTMLAttributes,
useRef,
} from "react";
import { twMerge } from "tailwind-merge";
import Span from "../../layout/Span";
import { Eye, EyeOff, Info, InfoIcon, X } from "lucide-react";
import { Copy, Eye, EyeOff, Info, InfoIcon, X } from "lucide-react";
import { AutocompleteOptions } from "../../types";
import twuiNumberfy from "../../utils/numberfy";
import Dropdown from "../../elements/Dropdown";
@@ -19,7 +19,7 @@ import NumberInputButtons from "./NumberInputButtons";
import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
import Row from "../../layout/Row";
import Paper from "../../elements/Paper";
import { TWUISelectValidityObject } from "../Select";
import type { TWUISelectValidityObject } from "../Select";
export type InputProps<KeyType extends string> = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
@@ -75,6 +75,7 @@ export type InputProps<KeyType extends string> = Omit<
HTMLDivElement
>;
// refreshDefaultValue?: number;
copy?: boolean;
};
let refreshes = 0;
@@ -117,6 +118,7 @@ export default function Input<KeyType extends string>(
clearInputProps,
rawNumber,
// refreshDefaultValue,
copy,
...props
} = inputProps;
@@ -163,7 +165,7 @@ export default function Input<KeyType extends string>(
numberText ? "text" : props.type,
);
const DEFAULT_DEBOUNCE = 500;
const DEFAULT_DEBOUNCE = 200;
const finalDebounce = debounce || DEFAULT_DEBOUNCE;
const finalLabel =
label ||
@@ -225,11 +227,12 @@ export default function Input<KeyType extends string>(
}
};
const isControlled = props.value !== undefined && props.value !== null;
React.useEffect(() => {
// if (!existingReady) return;
if (typeof props.value !== "string" || !props.value.match(/./)) return;
setValue(String(props.value));
}, [props.value]);
if (!isControlled) return;
setValue(String(props.value ?? ""));
}, [props.value, isControlled]);
// React.useEffect(() => {
// if (!refreshDefaultValue) return;
@@ -290,8 +293,8 @@ export default function Input<KeyType extends string>(
onChange={handleValueChange}
autoComplete={autoComplete}
rows={props.height ? Number(props.height) : props.rows || 2}
defaultValue={defaultInitialValue}
value={props.value ? getFinalValue(props.value) : undefined}
defaultValue={isControlled ? undefined : defaultInitialValue}
value={isControlled ? getFinalValue(props.value) : undefined}
/>
) : (
<input
@@ -318,9 +321,9 @@ export default function Input<KeyType extends string>(
}}
onChange={handleValueChange}
type={inputType}
defaultValue={defaultInitialValue}
defaultValue={isControlled ? undefined : defaultInitialValue}
autoComplete={autoComplete}
value={props.value ? getFinalValue(props.value) : undefined}
value={isControlled ? getFinalValue(props.value) : undefined}
/>
);
@@ -393,7 +396,29 @@ export default function Input<KeyType extends string>(
{targetComponent}
{props.type == "search" || props.readOnly ? null : (
{copy ? (
<>
<div
title="Copy value"
className="cursor-pointer opacity-50 hover:opacity-100"
onClick={() => {
const text =
inputRef.current.value ||
textAreaRef.current.value;
if (text) {
navigator.clipboard
.writeText(text)
.then(() => {
// window.alert(`Text Copied`);
});
}
}}
>
<Copy size={17} />
</div>
</>
) : props.type == "search" || props.readOnly ? null : (
<div
title="Clear Input Field"
{...clearInputProps}
@@ -502,3 +527,26 @@ export default function Input<KeyType extends string>(
</Stack>
);
}
const updateInput = ({
newValue,
inputRef,
}: {
newValue: string;
inputRef: RefObject<HTMLInputElement & HTMLTextAreaElement>;
}) => {
const input = inputRef.current;
if (!input) return;
// Get the native setter (React overrides the default one)
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value",
)?.set;
// Call it directly on the input
nativeInputValueSetter?.call(input, newValue);
// Dispatch the event - React listens for 'input', not 'change'
input.dispatchEvent(new Event("input", { bubbles: true }));
};
+1 -1
View File
@@ -1,4 +1,4 @@
import {
import type {
ComponentProps,
DetailedHTMLProps,
InputHTMLAttributes,
File diff suppressed because it is too large Load Diff
+30 -26
View File
@@ -1,14 +1,14 @@
import { ChevronDown, Info, LucideProps } from "lucide-react";
import { ChevronDown, Info, type LucideProps } from "lucide-react";
import React, {
ComponentProps,
DetailedHTMLProps,
Dispatch,
InputHTMLAttributes,
LabelHTMLAttributes,
ReactNode,
RefObject,
SelectHTMLAttributes,
SetStateAction,
type ComponentProps,
type DetailedHTMLProps,
type Dispatch,
type InputHTMLAttributes,
type LabelHTMLAttributes,
type ReactNode,
type RefObject,
type SelectHTMLAttributes,
type SetStateAction,
} from "react";
import { twMerge } from "tailwind-merge";
import Row from "../layout/Row";
@@ -18,6 +18,8 @@ import Span from "../layout/Span";
import Stack from "../layout/Stack";
import twuiSlugify from "../utils/slugify";
import twuiSlugToNormalText from "../utils/slug-to-normal-text";
import Input from "./Input";
import Button from "../layout/Button";
export type TWUISelectValidityObject = {
isValid?: boolean;
@@ -26,7 +28,7 @@ export type TWUISelectValidityObject = {
export type TWUISelectOptionObject<
KeyType extends string = string,
T extends { [k: string]: any } = { [k: string]: any }
T extends { [k: string]: any } = { [k: string]: any },
> = {
title?: string;
value: KeyType;
@@ -36,7 +38,7 @@ export type TWUISelectOptionObject<
export type TWUISelectProps<
KeyType extends string,
T extends { [k: string]: any } = { [k: string]: any }
T extends { [k: string]: any } = { [k: string]: any },
> = DetailedHTMLProps<
SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
@@ -60,11 +62,13 @@ export type TWUISelectProps<
validateValueFn?: (value: string) => Promise<TWUISelectValidityObject>;
dispatchState?: Dispatch<SetStateAction<T | undefined>>;
name?: KeyType;
input_props?: ComponentProps<typeof Input>;
option_button_props?: Omit<ComponentProps<typeof Button>, "title">;
};
export type TWUISelectValueObject<
KeyType extends string,
T extends { [k: string]: any } = { [k: string]: any }
T extends { [k: string]: any } = { [k: string]: any },
> = {
value: KeyType;
data?: T;
@@ -78,7 +82,7 @@ export type TWUISelectValueObject<
*/
export default function Select<
KeyType extends string,
T extends { [k: string]: any } = { [k: string]: any }
T extends { [k: string]: any } = { [k: string]: any },
>({
label,
options,
@@ -102,9 +106,9 @@ export default function Select<
const [value, setValue] = React.useState<TWUISelectValueObject<KeyType, T>>(
{
value: options[0]?.value,
value: options[0]?.value!,
data: options[0]?.data,
}
},
);
React.useEffect(() => {
@@ -128,10 +132,10 @@ export default function Select<
const selectID = label
? twuiSlugify(label)
: props.name
? twuiSlugify(props.name)
: props.title
? twuiSlugify(props.title)
: `select-${Math.round(Math.random() * 1000000)}`;
? twuiSlugify(props.name)
: props.title
? twuiSlugify(props.title)
: `select-${Math.round(Math.random() * 1000000)}`;
return (
<Stack
@@ -148,7 +152,7 @@ export default function Select<
"focus:outline-slate-700 dark:focus:outline-white/50",
"bg-white dark:bg-background-dark",
validity.isValid ? "" : "outline-warning border-warning",
wrapperProps?.className
wrapperProps?.className,
)}
>
{showLabel && (
@@ -159,7 +163,7 @@ export default function Select<
"text-xs absolute -top-2.5 left-2 text-foreground-light/80 bg-background-light",
"dark:text-foreground-dark/70 dark:bg-background-dark px-1.5 rounded-t",
"twui-input-label",
labelProps?.className
labelProps?.className,
)}
>
{label || props.title || props.name}
@@ -174,7 +178,7 @@ export default function Select<
"w-full pl-3 py-2 rounded-default appearance-none pr-8",
"grow !border-none !outline-none",
"twui-select",
props.className
props.className,
)}
ref={selectRef}
value={
@@ -183,7 +187,7 @@ export default function Select<
}
onChange={(e) => {
const targetValue = options.find(
(opt) => opt.value == e.target.value
(opt) => opt.value == e.target.value,
);
if (targetValue) {
@@ -192,7 +196,7 @@ export default function Select<
changeHandler?.(
e.target.value as (typeof options)[number]["value"],
targetValue?.data
targetValue?.data,
);
props.onChange?.(e);
@@ -219,7 +223,7 @@ export default function Select<
{...iconProps}
className={twMerge(
"pointer-events-none -ml-6",
iconProps?.className
iconProps?.className,
)}
/>
+1 -1
View File
@@ -1,4 +1,4 @@
import Input, { InputProps } from "./Input";
import Input, { type InputProps } from "./Input";
/**
* # Textarea Component