Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,347 @@
|
||||
import clsx from "clsx";
|
||||
import { AriaAttributes, forwardRef, lazy, ReactNode, Ref, RefObject, Suspense, useEffect, useId, useImperativeHandle, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EntityIcon from "~/components/layouts/icons/EntityIcon";
|
||||
import HintTooltip from "~/components/ui/tooltips/HintTooltip";
|
||||
import MonacoEditor from "~/components/editors/MonacoEditor";
|
||||
import { translateLabel } from "~/utils/i18n/translateLabel";
|
||||
import { TextInput } from "~/ui/components/TextInput";
|
||||
import { TextArea } from "~/ui/components/TextArea";
|
||||
|
||||
const NovelEditor = lazy(() => import("~/modules/novel/ui/editor"));
|
||||
|
||||
export interface RefInputText {
|
||||
input: RefObject<HTMLInputElement | null> | RefObject<HTMLTextAreaElement | null>;
|
||||
textArea: RefObject<HTMLTextAreaElement | null>;
|
||||
}
|
||||
|
||||
type EditorSize = "sm" | "md" | "lg" | "auto" | "full" | "screen";
|
||||
|
||||
type WithDefaultValue = { defaultValue: string | undefined };
|
||||
type WithValueAndSetValue = { value: string | undefined; setValue: React.Dispatch<React.SetStateAction<string>> };
|
||||
|
||||
// Subframe TextInput/TextArea accept a constrained `type` union.
|
||||
type SubframeFieldType = "text" | "password" | "email" | "number" | "tel" | "url" | "search";
|
||||
const SUBFRAME_FIELD_TYPES: SubframeFieldType[] = ["text", "password", "email", "number", "tel", "url", "search"];
|
||||
function toSubframeType(type: string | undefined): SubframeFieldType {
|
||||
return (SUBFRAME_FIELD_TYPES as string[]).includes(type ?? "") ? (type as SubframeFieldType) : "text";
|
||||
}
|
||||
|
||||
export type InputTextProps = (WithDefaultValue | WithValueAndSetValue) & {
|
||||
id?: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
withLabel?: boolean;
|
||||
className?: string;
|
||||
classNameBg?: string;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
readOnly?: boolean;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
autoComplete?: string;
|
||||
withTranslation?: boolean;
|
||||
translationParams?: string[];
|
||||
placeholder?: string;
|
||||
pattern?: string;
|
||||
rows?: number;
|
||||
button?: ReactNode;
|
||||
// lowercase?: boolean;
|
||||
// uppercase?: boolean;
|
||||
type?: string;
|
||||
darkMode?: boolean;
|
||||
hint?: ReactNode;
|
||||
help?: string;
|
||||
icon?: string;
|
||||
editor?: string; // monaco
|
||||
editorLanguage?: string; // "javascript" | "typescript" | "html" | "css" | "json";
|
||||
editorHideLineNumbers?: boolean;
|
||||
editorTheme?: "vs-dark" | "light";
|
||||
editorFontSize?: number;
|
||||
editorOptions?: any;
|
||||
onBlur?: () => void;
|
||||
onFocus?: () => void;
|
||||
borderless?: boolean;
|
||||
editorSize?: EditorSize;
|
||||
autoFocus?: boolean;
|
||||
isError?: boolean;
|
||||
isSuccess?: boolean;
|
||||
ariaDescribedBy?: string;
|
||||
ariaInvalid?: AriaAttributes["aria-invalid"];
|
||||
hideChars?: boolean;
|
||||
dataUiId?: string;
|
||||
onPaste?: (e: React.ClipboardEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
const InputText = (props: InputTextProps, ref: Ref<RefInputText>) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
title,
|
||||
withLabel = true,
|
||||
className,
|
||||
classNameBg,
|
||||
help,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
required = false,
|
||||
minLength,
|
||||
maxLength,
|
||||
autoComplete,
|
||||
withTranslation = false,
|
||||
translationParams = [],
|
||||
placeholder,
|
||||
pattern,
|
||||
hint,
|
||||
rows,
|
||||
button,
|
||||
// lowercase,
|
||||
// uppercase,
|
||||
type = "text",
|
||||
darkMode,
|
||||
icon,
|
||||
editor,
|
||||
editorLanguage,
|
||||
editorHideLineNumbers,
|
||||
editorTheme = "vs-dark",
|
||||
editorFontSize,
|
||||
editorOptions,
|
||||
onBlur,
|
||||
onFocus,
|
||||
borderless,
|
||||
editorSize = "sm",
|
||||
autoFocus,
|
||||
isError,
|
||||
// isSuccess,
|
||||
ariaDescribedBy,
|
||||
ariaInvalid,
|
||||
hideChars,
|
||||
dataUiId,
|
||||
onPaste,
|
||||
} = props;
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
const resolvedTitle = title ? translateLabel(t, i18n, title) : undefined;
|
||||
|
||||
const value = "value" in props ? props.value : undefined;
|
||||
const setValue = "setValue" in props ? props.setValue : undefined;
|
||||
const defaultValue = "defaultValue" in props ? props.defaultValue : undefined;
|
||||
|
||||
const [actualEditorSize] = useState<EditorSize>(editorSize ?? ("sm" as EditorSize));
|
||||
// Fall back to a generated id so the visible <label> always associates with the
|
||||
// input even when no id/name is supplied (e.g. read-only display fields).
|
||||
// Without this the label has htmlFor={undefined} and axe flags a missing label.
|
||||
const generatedId = useId();
|
||||
const inputId = id ?? name ?? generatedId;
|
||||
const resolvedAriaInvalid = ariaInvalid ?? (isError ? true : undefined);
|
||||
|
||||
useImperativeHandle(ref, () => ({ input, textArea }));
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const textArea = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const [actualValue, setActualValue] = useState<string>(value ?? defaultValue ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
setActualValue(value || defaultValue || "");
|
||||
}, [value, defaultValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setValue && actualValue !== value) {
|
||||
setValue(actualValue);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actualValue]);
|
||||
|
||||
function onChange(value: string) {
|
||||
setActualValue(value);
|
||||
if (setValue) {
|
||||
setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
const showLabel = Boolean(withLabel && resolvedTitle);
|
||||
|
||||
// Rich label content rendered into Subframe's label slot (inline-safe — spans only, so it
|
||||
// is valid both inside a native <label> and inside Subframe's <span> label wrapper).
|
||||
const labelContent: ReactNode = showLabel ? (
|
||||
<span className="flex w-full items-center justify-between gap-2">
|
||||
<span className="flex shrink-0 items-center space-x-1 truncate">
|
||||
<span className="flex space-x-1 truncate">
|
||||
<span className="truncate">{resolvedTitle}</span>
|
||||
{required && resolvedTitle && <span className="ml-1 text-red-600">*</span>}
|
||||
</span>
|
||||
{help && <HintTooltip text={help} />}
|
||||
</span>
|
||||
{withTranslation && value?.includes(".") && (
|
||||
<span
|
||||
className="text-muted-foreground truncate font-light italic"
|
||||
title={i18n.exists(value) ? String(translationParams.length > 0 ? t(value, { 0: translationParams }) : translateLabel(t, i18n, value)) : t("shared.invalid")}
|
||||
>
|
||||
{t("admin.pricing.i18n")}:{" "}
|
||||
{i18n.exists(value) ? (
|
||||
<span className="text-muted-foreground">{translationParams.length > 0 ? t(value, { 0: translationParams }) : translateLabel(t, i18n, value)}</span>
|
||||
) : (
|
||||
<span className="text-red-600">{t("shared.invalid")}</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{hint}
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
// Editor branches (monaco / wysiwyg) keep their original rendering. They are not driven by
|
||||
// any current InputText call site, but remain for API completeness.
|
||||
if (editor === "monaco" && editorLanguage) {
|
||||
return (
|
||||
<div className={clsx(className, !darkMode && "")}>
|
||||
{showLabel && (
|
||||
<label htmlFor={inputId} className="mb-1 flex justify-between space-x-2 truncate text-xs font-medium">
|
||||
{labelContent}
|
||||
</label>
|
||||
)}
|
||||
<div className={clsx("relative flex w-full rounded-md")}>
|
||||
<textarea hidden readOnly name={name} value={actualValue} />
|
||||
{typeof window !== "undefined" && (
|
||||
<MonacoEditor
|
||||
theme={editorTheme}
|
||||
className={clsx(
|
||||
actualEditorSize === "sm" && "h-32",
|
||||
actualEditorSize === "md" && "h-64",
|
||||
actualEditorSize === "lg" && "h-96",
|
||||
actualEditorSize === "auto" && "h-auto",
|
||||
actualEditorSize === "full" && "h-full",
|
||||
actualEditorSize === "screen" && "h-screen",
|
||||
className,
|
||||
classNameBg,
|
||||
borderless && "border-transparent"
|
||||
)}
|
||||
defaultLanguage={editorLanguage}
|
||||
language={editorLanguage as any}
|
||||
hideLineNumbers={editorHideLineNumbers}
|
||||
fontSize={editorFontSize}
|
||||
readOnly={disabled || readOnly}
|
||||
options={editorOptions}
|
||||
value={value ?? ""}
|
||||
defaultValue={defaultValue}
|
||||
onChange={(e) => onChange(e)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (editor === "wysiwyg") {
|
||||
return (
|
||||
<div className={clsx(className, !darkMode && "")}>
|
||||
{showLabel && (
|
||||
<label htmlFor={inputId} className="mb-1 flex justify-between space-x-2 truncate text-xs font-medium">
|
||||
{labelContent}
|
||||
</label>
|
||||
)}
|
||||
<div className={clsx("relative flex w-full rounded-md")}>
|
||||
<textarea hidden readOnly name={name} value={actualValue} />
|
||||
<Suspense fallback={null}>
|
||||
<NovelEditor
|
||||
autoFocus={autoFocus}
|
||||
key={name ?? "novel-editor"}
|
||||
readOnly={readOnly || disabled}
|
||||
disabled={disabled}
|
||||
darkMode={darkMode}
|
||||
className={clsx(
|
||||
actualEditorSize === "sm" && "min-h-[100px]",
|
||||
actualEditorSize === "md" && "min-h-[300px]",
|
||||
actualEditorSize === "lg" && "min-h-[500px]",
|
||||
actualEditorSize === "auto" && "h-auto",
|
||||
actualEditorSize === "full" && "h-full",
|
||||
actualEditorSize === "screen" && "h-screen",
|
||||
borderless && "border-transparent"
|
||||
)}
|
||||
content={value || defaultValue}
|
||||
onChange={(e) => onChange(e?.html ?? "")}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Textarea field — Subframe TextArea (label + box + textarea).
|
||||
if (rows) {
|
||||
return (
|
||||
<div className={clsx(className, !darkMode && "")}>
|
||||
<TextArea className="w-full max-w-none" showLabel={showLabel} label={labelContent} error={resolvedAriaInvalid === true} borderless={borderless}>
|
||||
<TextArea.Input
|
||||
ref={textArea}
|
||||
rows={rows}
|
||||
style={hideChars ? ({ WebkitTextSecurity: "disc" } as any) : undefined}
|
||||
id={id ?? name}
|
||||
name={name}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
aria-invalid={resolvedAriaInvalid}
|
||||
autoComplete={autoComplete}
|
||||
required={required}
|
||||
minLength={minLength}
|
||||
maxLength={maxLength}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
placeholder={placeholder}
|
||||
data-ui-id={dataUiId}
|
||||
aria-label={!showLabel ? resolvedTitle ?? placeholder : undefined}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</TextArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default text field — Subframe TextInput (label + box + input).
|
||||
return (
|
||||
<div className={clsx(className, !darkMode && "")}>
|
||||
<TextInput
|
||||
className="w-full max-w-none"
|
||||
showLabel={showLabel}
|
||||
label={labelContent}
|
||||
error={resolvedAriaInvalid === true}
|
||||
disabled={disabled}
|
||||
type={toSubframeType(type)}
|
||||
borderless={borderless}
|
||||
icon={icon ? <EntityIcon className="text-muted-foreground h-5 w-5" icon={icon} /> : undefined}
|
||||
>
|
||||
<TextInput.Input
|
||||
ref={input}
|
||||
type={toSubframeType(type)}
|
||||
style={hideChars ? ({ WebkitTextSecurity: "disc" } as any) : undefined}
|
||||
id={inputId}
|
||||
name={name}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
aria-invalid={resolvedAriaInvalid}
|
||||
autoComplete={autoComplete}
|
||||
required={required}
|
||||
minLength={minLength}
|
||||
maxLength={maxLength}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
placeholder={placeholder}
|
||||
data-ui-id={dataUiId}
|
||||
aria-label={!showLabel ? resolvedTitle ?? placeholder : undefined}
|
||||
pattern={pattern !== "" && pattern !== undefined ? pattern : undefined}
|
||||
autoFocus={autoFocus}
|
||||
onPaste={onPaste}
|
||||
/>
|
||||
</TextInput>
|
||||
{button}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(InputText);
|
||||
Reference in new issue
Block a user