@fanvue/ui 1.3.0 → 1.4.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.
- package/dist/cjs/components/AudioUpload/AudioUpload.cjs +286 -0
- package/dist/cjs/components/AudioUpload/AudioUpload.cjs.map +1 -0
- package/dist/cjs/components/AudioUpload/AudioWaveform.cjs +121 -0
- package/dist/cjs/components/AudioUpload/AudioWaveform.cjs.map +1 -0
- package/dist/cjs/components/AudioUpload/audioUtils.cjs +44 -0
- package/dist/cjs/components/AudioUpload/audioUtils.cjs.map +1 -0
- package/dist/cjs/components/AudioUpload/constants.cjs +21 -0
- package/dist/cjs/components/AudioUpload/constants.cjs.map +1 -0
- package/dist/cjs/components/AudioUpload/useAudioRecorder.cjs +191 -0
- package/dist/cjs/components/AudioUpload/useAudioRecorder.cjs.map +1 -0
- package/dist/cjs/components/DatePicker/DatePicker.cjs +1 -1
- package/dist/cjs/components/DatePicker/DatePicker.cjs.map +1 -1
- package/dist/cjs/components/Icons/CheckOutlineIcon.cjs +55 -0
- package/dist/cjs/components/Icons/CheckOutlineIcon.cjs.map +1 -0
- package/dist/cjs/components/Icons/UploadCloudIcon.cjs +61 -0
- package/dist/cjs/components/Icons/UploadCloudIcon.cjs.map +1 -0
- package/dist/cjs/components/TextArea/TextArea.cjs +209 -0
- package/dist/cjs/components/TextArea/TextArea.cjs.map +1 -0
- package/dist/cjs/components/TextField/TextField.cjs +22 -34
- package/dist/cjs/components/TextField/TextField.cjs.map +1 -1
- package/dist/cjs/index.cjs +10 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/components/AudioUpload/AudioUpload.mjs +269 -0
- package/dist/components/AudioUpload/AudioUpload.mjs.map +1 -0
- package/dist/components/AudioUpload/AudioWaveform.mjs +104 -0
- package/dist/components/AudioUpload/AudioWaveform.mjs.map +1 -0
- package/dist/components/AudioUpload/audioUtils.mjs +44 -0
- package/dist/components/AudioUpload/audioUtils.mjs.map +1 -0
- package/dist/components/AudioUpload/constants.mjs +21 -0
- package/dist/components/AudioUpload/constants.mjs.map +1 -0
- package/dist/components/AudioUpload/useAudioRecorder.mjs +174 -0
- package/dist/components/AudioUpload/useAudioRecorder.mjs.map +1 -0
- package/dist/components/DatePicker/DatePicker.mjs +1 -1
- package/dist/components/DatePicker/DatePicker.mjs.map +1 -1
- package/dist/components/Icons/CheckOutlineIcon.mjs +38 -0
- package/dist/components/Icons/CheckOutlineIcon.mjs.map +1 -0
- package/dist/components/Icons/UploadCloudIcon.mjs +44 -0
- package/dist/components/Icons/UploadCloudIcon.mjs.map +1 -0
- package/dist/components/TextArea/TextArea.mjs +192 -0
- package/dist/components/TextArea/TextArea.mjs.map +1 -0
- package/dist/components/TextField/TextField.mjs +22 -34
- package/dist/components/TextField/TextField.mjs.map +1 -1
- package/dist/index.d.ts +164 -0
- package/dist/index.mjs +11 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TextArea.cjs","sources":["../../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { CheckOutlineIcon } from \"../Icons/CheckOutlineIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Text area height in pixels. */\nexport type TextAreaSize = \"48\" | \"40\" | \"32\";\n\nexport interface TextAreaProps\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"size\"> {\n /** Label text displayed above the textarea. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the textarea. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Minimum height of the text area in pixels. @default \"48\" */\n size?: TextAreaSize;\n /** Whether the text area is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the textarea. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Whether the text area is validated. @default false */\n validated?: boolean;\n /** Whether the text area stretches to fill its container width. @default false */\n fullWidth?: boolean;\n /** Whether to show a clear button when text is present. @default false */\n showClearButton?: boolean;\n /** Callback fired when the clear button is clicked. */\n onClear?: () => void;\n /** Minimum number of rows (lines) for the textarea. */\n minRows?: number;\n /** Maximum number of rows (lines) for the textarea. */\n maxRows?: number;\n}\n\nconst CONTAINER_MIN_HEIGHT: Record<TextAreaSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst TEXTAREA_SIZE_CLASSES: Record<TextAreaSize, string> = {\n \"48\": \"py-3 typography-body-1-regular\",\n \"40\": \"py-2 typography-body-1-regular\",\n \"32\": \"py-2 typography-body-2-regular\",\n};\n\nconst PADDING_HORIZONTAL: Record<TextAreaSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst PADDING_RIGHT_WITH_CLEAR: Record<TextAreaSize, string> = {\n \"48\": \"pr-11\",\n \"40\": \"pr-11\",\n \"32\": \"pr-10\",\n};\n\nconst CLEAR_BUTTON_RIGHT: Record<TextAreaSize, string> = {\n \"48\": \"right-4 top-3\",\n \"40\": \"right-4 top-2\",\n \"32\": \"right-3 top-2\",\n};\n\nfunction getContainerClassName(size: TextAreaSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative rounded-xl border bg-neutral-100 has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-500\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-400\",\n CONTAINER_MIN_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction getTextareaClassName(size: TextAreaSize, hasClearButton: boolean, hasMinRows: boolean) {\n return cn(\n \"w-full resize-y rounded-xl bg-transparent text-body-100 no-underline placeholder:text-body-200 placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed\",\n !hasMinRows && \"min-h-[80px]\",\n TEXTAREA_SIZE_CLASSES[size],\n PADDING_HORIZONTAL[size],\n hasClearButton ? PADDING_RIGHT_WITH_CLEAR[size] : \"\",\n );\n}\n\nfunction TextAreaHelperText({\n id,\n error,\n children,\n}: {\n id: string;\n error: boolean;\n children: React.ReactNode;\n}) {\n return (\n <p\n id={id}\n className={cn(\n \"typography-caption-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-500\" : \"text-body-200\",\n )}\n >\n {children}\n </p>\n );\n}\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"TextArea: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction calculateMaxHeight(size: TextAreaSize, maxRows?: number): string | undefined {\n if (!maxRows) return undefined;\n\n // Line height is 24px for body-1 (sizes 48 and 40) and 20px for body-2 (size 32)\n const lineHeight = size === \"32\" ? 20 : 24;\n // py-2 = 8px, py-3 = 12px\n const verticalPadding = size === \"32\" ? 8 : size === \"40\" ? 8 : 12;\n\n return `${lineHeight * maxRows + verticalPadding * 2}px`;\n}\n\nfunction createClearHandler(\n onClear?: () => void,\n onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void,\n) {\n return () => {\n if (onClear) {\n onClear();\n }\n // If there's an onChange handler, simulate a change event with empty value\n if (onChange) {\n const syntheticEvent = {\n target: { value: \"\" },\n currentTarget: { value: \"\" },\n } as React.ChangeEvent<HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n };\n}\n\n/**\n * A multi-line text input with optional label, helper/error text, and clear button.\n *\n * Provide at least one of `label`, `aria-label`, or `aria-labelledby` for\n * accessibility — a console warning is emitted in development if none are set.\n *\n * @example\n * ```tsx\n * <TextArea\n * label=\"Description\"\n * placeholder=\"Enter your description...\"\n * showClearButton\n * error={!!descError}\n * errorMessage={descError}\n * />\n * ```\n */\nexport const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n validated = false,\n className,\n id,\n disabled,\n fullWidth = false,\n showClearButton = false,\n onClear,\n value,\n onChange,\n minRows,\n maxRows,\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const inputId = id || generatedId;\n const helperTextId = `${inputId}-helper`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n const hasValue = value !== undefined && value !== null && value !== \"\";\n const showClear = showClearButton && hasValue && !disabled;\n const maxHeight = calculateMaxHeight(size, maxRows);\n const handleClear = createClearHandler(onClear, onChange);\n\n warnMissingAccessibleName(label, props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n return (\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={inputId}\n className=\"typography-caption-semibold px-1 pt-1 pb-2 text-body-100\"\n >\n {label}\n </label>\n )}\n\n <div className={getContainerClassName(size, error, disabled)}>\n <textarea\n ref={ref}\n id={inputId}\n disabled={disabled}\n aria-describedby={bottomText ? helperTextId : undefined}\n aria-invalid={error || undefined}\n className={getTextareaClassName(size, showClear, !!minRows)}\n value={value}\n onChange={onChange}\n rows={minRows}\n style={maxHeight ? { maxHeight } : undefined}\n {...props}\n />\n\n {showClear && (\n <button\n type=\"button\"\n onClick={handleClear}\n aria-label=\"Clear text\"\n tabIndex={-1}\n className={cn(\n \"absolute flex size-5 shrink-0 items-center justify-center text-body-200 transition-colors hover:text-body-100 focus:outline-none\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n >\n <CloseIcon />\n </button>\n )}\n {validated && !showClear && (\n <div\n className={cn(\n \"pointer-events-none absolute flex size-5 items-center justify-center\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n >\n <CheckOutlineIcon className=\"text-success-500\" />\n </div>\n )}\n </div>\n\n {bottomText && (\n <TextAreaHelperText id={helperTextId} error={error}>\n {bottomText}\n </TextAreaHelperText>\n )}\n </div>\n );\n },\n);\n\nTextArea.displayName = \"TextArea\";\n"],"names":["cn","jsx","React","jsxs","CloseIcon","CheckOutlineIcon"],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
4
|
const jsxRuntime = require("react/jsx-runtime");
|
|
5
5
|
const React = require("react");
|
|
6
|
+
const CheckOutlineIcon = require("../Icons/CheckOutlineIcon.cjs");
|
|
6
7
|
const cn = require("../../utils/cn.cjs");
|
|
7
8
|
function _interopNamespaceDefault(e) {
|
|
8
9
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
@@ -31,50 +32,35 @@ const INPUT_SIZE_CLASSES = {
|
|
|
31
32
|
"40": "py-2 typography-body-1-regular",
|
|
32
33
|
"32": "py-2 typography-body-2-regular"
|
|
33
34
|
};
|
|
34
|
-
const
|
|
35
|
-
"48":
|
|
36
|
-
"40":
|
|
37
|
-
"32":
|
|
35
|
+
const PADDING_HORIZONTAL = {
|
|
36
|
+
"48": "px-4",
|
|
37
|
+
"40": "px-4",
|
|
38
|
+
"32": "px-3"
|
|
38
39
|
};
|
|
39
|
-
const
|
|
40
|
-
"48":
|
|
41
|
-
"40":
|
|
42
|
-
"32":
|
|
43
|
-
};
|
|
44
|
-
const ICON_LEFT = {
|
|
45
|
-
"48": "left-4",
|
|
46
|
-
"40": "left-4",
|
|
47
|
-
"32": "left-3"
|
|
48
|
-
};
|
|
49
|
-
const ICON_RIGHT = {
|
|
50
|
-
"48": "right-4",
|
|
51
|
-
"40": "right-4",
|
|
52
|
-
"32": "right-3"
|
|
40
|
+
const ICON_SPACING = {
|
|
41
|
+
"48": "gap-3",
|
|
42
|
+
"40": "gap-3",
|
|
43
|
+
"32": "gap-2"
|
|
53
44
|
};
|
|
54
45
|
function getContainerClassName(size, error, disabled) {
|
|
55
46
|
return cn.cn(
|
|
56
|
-
"
|
|
47
|
+
"flex items-center rounded-xl border bg-neutral-100 has-focus-visible:outline-none motion-safe:transition-colors",
|
|
57
48
|
error ? "border-error-500" : "border-transparent",
|
|
58
49
|
!disabled && !error && "hover:border-neutral-400",
|
|
59
50
|
CONTAINER_HEIGHT[size],
|
|
51
|
+
PADDING_HORIZONTAL[size],
|
|
52
|
+
ICON_SPACING[size],
|
|
60
53
|
disabled && "opacity-50"
|
|
61
54
|
);
|
|
62
55
|
}
|
|
63
|
-
function getInputClassName(size
|
|
56
|
+
function getInputClassName(size) {
|
|
64
57
|
return cn.cn(
|
|
65
|
-
"h-full
|
|
66
|
-
INPUT_SIZE_CLASSES[size]
|
|
67
|
-
PADDING_LEFT[size][hasLeftIcon ? 1 : 0],
|
|
68
|
-
PADDING_RIGHT[size][hasRightIcon ? 1 : 0]
|
|
58
|
+
"h-full flex-1 rounded-xl bg-transparent text-body-100 no-underline placeholder:text-body-200 placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed",
|
|
59
|
+
INPUT_SIZE_CLASSES[size]
|
|
69
60
|
);
|
|
70
61
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
children,
|
|
74
|
-
size,
|
|
75
|
-
side
|
|
76
|
-
}) {
|
|
77
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn.cn(ICON_BASE, side === "left" ? ICON_LEFT[size] : ICON_RIGHT[size]), children });
|
|
62
|
+
function TextFieldIcon({ children }) {
|
|
63
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex size-5 shrink-0 items-center justify-center text-body-200", children });
|
|
78
64
|
}
|
|
79
65
|
function TextFieldHelperText({
|
|
80
66
|
id,
|
|
@@ -109,6 +95,7 @@ const TextField = React__namespace.forwardRef(
|
|
|
109
95
|
size = "48",
|
|
110
96
|
error = false,
|
|
111
97
|
errorMessage,
|
|
98
|
+
validated = false,
|
|
112
99
|
leftIcon,
|
|
113
100
|
rightIcon,
|
|
114
101
|
className,
|
|
@@ -138,7 +125,7 @@ const TextField = React__namespace.forwardRef(
|
|
|
138
125
|
}
|
|
139
126
|
),
|
|
140
127
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: getContainerClassName(size, error, disabled), children: [
|
|
141
|
-
leftIcon && /* @__PURE__ */ jsxRuntime.jsx(TextFieldIcon, {
|
|
128
|
+
leftIcon && /* @__PURE__ */ jsxRuntime.jsx(TextFieldIcon, { children: leftIcon }),
|
|
142
129
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
143
130
|
"input",
|
|
144
131
|
{
|
|
@@ -147,11 +134,12 @@ const TextField = React__namespace.forwardRef(
|
|
|
147
134
|
disabled,
|
|
148
135
|
"aria-describedby": bottomText ? helperTextId : void 0,
|
|
149
136
|
"aria-invalid": error || void 0,
|
|
150
|
-
className: getInputClassName(size
|
|
137
|
+
className: getInputClassName(size),
|
|
151
138
|
...props
|
|
152
139
|
}
|
|
153
140
|
),
|
|
154
|
-
rightIcon && /* @__PURE__ */ jsxRuntime.jsx(TextFieldIcon, {
|
|
141
|
+
rightIcon && /* @__PURE__ */ jsxRuntime.jsx(TextFieldIcon, { children: rightIcon }),
|
|
142
|
+
validated && /* @__PURE__ */ jsxRuntime.jsx(TextFieldIcon, { children: /* @__PURE__ */ jsxRuntime.jsx(CheckOutlineIcon.CheckOutlineIcon, { className: "text-success-500" }) })
|
|
155
143
|
] }),
|
|
156
144
|
bottomText && /* @__PURE__ */ jsxRuntime.jsx(TextFieldHelperText, { id: helperTextId, error, children: bottomText })
|
|
157
145
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextField.cjs","sources":["../../../../src/components/TextField/TextField.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Text field height in pixels. */\nexport type TextFieldSize = \"48\" | \"40\" | \"32\";\n\nexport interface TextFieldProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\" | \"prefix\"> {\n /** Label text displayed above the input. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the input. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Height of the text field in pixels. @default \"48\" */\n size?: TextFieldSize;\n /** Whether the text field is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the input. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Icon element displayed at the left side of the input. */\n leftIcon?: React.ReactNode;\n /** Icon element displayed at the right side of the input. */\n rightIcon?: React.ReactNode;\n /** Whether the text field stretches to fill its container width. @default false */\n fullWidth?: boolean;\n}\n\nconst CONTAINER_HEIGHT: Record<TextFieldSize, string> = {\n \"48\": \"h-12\",\n \"40\": \"h-10\",\n \"32\": \"h-8\",\n};\n\nconst INPUT_SIZE_CLASSES: Record<TextFieldSize, string> = {\n \"48\": \"py-3 typography-body-1-regular\",\n \"40\": \"py-2 typography-body-1-regular\",\n \"32\": \"py-2 typography-body-2-regular\",\n};\n\nconst
|
|
1
|
+
{"version":3,"file":"TextField.cjs","sources":["../../../../src/components/TextField/TextField.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { CheckOutlineIcon } from \"@/index\";\nimport { cn } from \"../../utils/cn\";\n\n/** Text field height in pixels. */\nexport type TextFieldSize = \"48\" | \"40\" | \"32\";\n\nexport interface TextFieldProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\" | \"prefix\"> {\n /** Label text displayed above the input. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the input. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Height of the text field in pixels. @default \"48\" */\n size?: TextFieldSize;\n /** Whether the text field is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the input. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Whether the text field is validated. @default false */\n validated?: boolean;\n /** Icon element displayed at the left side of the input. */\n leftIcon?: React.ReactNode;\n /** Icon element displayed at the right side of the input. */\n rightIcon?: React.ReactNode;\n /** Whether the text field stretches to fill its container width. @default false */\n fullWidth?: boolean;\n}\n\nconst CONTAINER_HEIGHT: Record<TextFieldSize, string> = {\n \"48\": \"h-12\",\n \"40\": \"h-10\",\n \"32\": \"h-8\",\n};\n\nconst INPUT_SIZE_CLASSES: Record<TextFieldSize, string> = {\n \"48\": \"py-3 typography-body-1-regular\",\n \"40\": \"py-2 typography-body-1-regular\",\n \"32\": \"py-2 typography-body-2-regular\",\n};\n\nconst PADDING_HORIZONTAL: Record<TextFieldSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst ICON_SPACING: Record<TextFieldSize, string> = {\n \"48\": \"gap-3\",\n \"40\": \"gap-3\",\n \"32\": \"gap-2\",\n};\n\nfunction getContainerClassName(size: TextFieldSize, error: boolean, disabled?: boolean) {\n return cn(\n \"flex items-center rounded-xl border bg-neutral-100 has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-500\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-400\",\n CONTAINER_HEIGHT[size],\n PADDING_HORIZONTAL[size],\n ICON_SPACING[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction getInputClassName(size: TextFieldSize) {\n return cn(\n \"h-full flex-1 rounded-xl bg-transparent text-body-100 no-underline placeholder:text-body-200 placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed\",\n INPUT_SIZE_CLASSES[size],\n );\n}\n\nfunction TextFieldIcon({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"flex size-5 shrink-0 items-center justify-center text-body-200\">{children}</div>\n );\n}\n\nfunction TextFieldHelperText({\n id,\n error,\n children,\n}: {\n id: string;\n error: boolean;\n children: React.ReactNode;\n}) {\n return (\n <p\n id={id}\n className={cn(\n \"typography-caption-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-500\" : \"text-body-200\",\n )}\n >\n {children}\n </p>\n );\n}\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"TextField: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\n/**\n * A text input field with optional label, helper/error text, and icon slots.\n *\n * Provide at least one of `label`, `aria-label`, or `aria-labelledby` for\n * accessibility — a console warning is emitted in development if none are set.\n *\n * @example\n * ```tsx\n * <TextField\n * label=\"Email\"\n * placeholder=\"you@example.com\"\n * error={!!emailError}\n * errorMessage={emailError}\n * />\n * ```\n */\nexport const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n validated = false,\n leftIcon,\n rightIcon,\n className,\n id,\n disabled,\n fullWidth = false,\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const inputId = id || generatedId;\n const helperTextId = `${inputId}-helper`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n warnMissingAccessibleName(label, props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n return (\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={inputId}\n className=\"typography-caption-semibold px-1 pt-1 pb-2 text-body-100\"\n >\n {label}\n </label>\n )}\n\n <div className={getContainerClassName(size, error, disabled)}>\n {leftIcon && <TextFieldIcon>{leftIcon}</TextFieldIcon>}\n\n <input\n ref={ref}\n id={inputId}\n disabled={disabled}\n aria-describedby={bottomText ? helperTextId : undefined}\n aria-invalid={error || undefined}\n className={getInputClassName(size)}\n {...props}\n />\n\n {rightIcon && <TextFieldIcon>{rightIcon}</TextFieldIcon>}\n {validated && (\n <TextFieldIcon>\n <CheckOutlineIcon className=\"text-success-500\" />\n </TextFieldIcon>\n )}\n </div>\n\n {bottomText && (\n <TextFieldHelperText id={helperTextId} error={error}>\n {bottomText}\n </TextFieldHelperText>\n )}\n </div>\n );\n },\n);\n\nTextField.displayName = \"TextField\";\n"],"names":["cn","jsx","React","jsxs","CheckOutlineIcon"],"mappings":"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"}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
4
|
const Alert = require("./components/Alert/Alert.cjs");
|
|
5
|
+
const AudioUpload = require("./components/AudioUpload/AudioUpload.cjs");
|
|
6
|
+
const useAudioRecorder = require("./components/AudioUpload/useAudioRecorder.cjs");
|
|
5
7
|
const Avatar = require("./components/Avatar/Avatar.cjs");
|
|
6
8
|
const Badge = require("./components/Badge/Badge.cjs");
|
|
7
9
|
const Button = require("./components/Button/Button.cjs");
|
|
@@ -15,6 +17,7 @@ const ArrowRightIcon = require("./components/Icons/ArrowRightIcon.cjs");
|
|
|
15
17
|
const ArrowUpRightIcon = require("./components/Icons/ArrowUpRightIcon.cjs");
|
|
16
18
|
const CheckCircleIcon = require("./components/Icons/CheckCircleIcon.cjs");
|
|
17
19
|
const CheckIcon = require("./components/Icons/CheckIcon.cjs");
|
|
20
|
+
const CheckOutlineIcon = require("./components/Icons/CheckOutlineIcon.cjs");
|
|
18
21
|
const ChevronLeftIcon = require("./components/Icons/ChevronLeftIcon.cjs");
|
|
19
22
|
const ChevronRightIcon = require("./components/Icons/ChevronRightIcon.cjs");
|
|
20
23
|
const CloseIcon = require("./components/Icons/CloseIcon.cjs");
|
|
@@ -34,6 +37,7 @@ const PlusIcon = require("./components/Icons/PlusIcon.cjs");
|
|
|
34
37
|
const SpinnerIcon = require("./components/Icons/SpinnerIcon.cjs");
|
|
35
38
|
const StopIcon = require("./components/Icons/StopIcon.cjs");
|
|
36
39
|
const SuccessIcon = require("./components/Icons/SuccessIcon.cjs");
|
|
40
|
+
const UploadCloudIcon = require("./components/Icons/UploadCloudIcon.cjs");
|
|
37
41
|
const VipBadgeIcon = require("./components/Icons/VipBadgeIcon.cjs");
|
|
38
42
|
const WarningIcon = require("./components/Icons/WarningIcon.cjs");
|
|
39
43
|
const WarningTriangleIcon = require("./components/Icons/WarningTriangleIcon.cjs");
|
|
@@ -53,11 +57,14 @@ const Tabs = require("./components/Tabs/Tabs.cjs");
|
|
|
53
57
|
const TabsContent = require("./components/Tabs/TabsContent.cjs");
|
|
54
58
|
const TabsList = require("./components/Tabs/TabsList.cjs");
|
|
55
59
|
const TabsTrigger = require("./components/Tabs/TabsTrigger.cjs");
|
|
60
|
+
const TextArea = require("./components/TextArea/TextArea.cjs");
|
|
56
61
|
const TextField = require("./components/TextField/TextField.cjs");
|
|
57
62
|
const Toast = require("./components/Toast/Toast.cjs");
|
|
58
63
|
const Tooltip = require("./components/Tooltip/Tooltip.cjs");
|
|
59
64
|
const cn = require("./utils/cn.cjs");
|
|
60
65
|
exports.Alert = Alert.Alert;
|
|
66
|
+
exports.AudioUpload = AudioUpload.AudioUpload;
|
|
67
|
+
exports.useAudioRecorder = useAudioRecorder.useAudioRecorder;
|
|
61
68
|
exports.Avatar = Avatar.Avatar;
|
|
62
69
|
exports.AvatarFallback = Avatar.AvatarFallback;
|
|
63
70
|
exports.AvatarImage = Avatar.AvatarImage;
|
|
@@ -74,6 +81,7 @@ exports.ArrowRightIcon = ArrowRightIcon.ArrowRightIcon;
|
|
|
74
81
|
exports.ArrowUpRightIcon = ArrowUpRightIcon.ArrowUpRightIcon;
|
|
75
82
|
exports.CheckCircleIcon = CheckCircleIcon.CheckCircleIcon;
|
|
76
83
|
exports.CheckIcon = CheckIcon.CheckIcon;
|
|
84
|
+
exports.CheckOutlineIcon = CheckOutlineIcon.CheckOutlineIcon;
|
|
77
85
|
exports.ChevronLeftIcon = ChevronLeftIcon.ChevronLeftIcon;
|
|
78
86
|
exports.ChevronRightIcon = ChevronRightIcon.ChevronRightIcon;
|
|
79
87
|
exports.CloseIcon = CloseIcon.CloseIcon;
|
|
@@ -93,6 +101,7 @@ exports.PlusIcon = PlusIcon.PlusIcon;
|
|
|
93
101
|
exports.SpinnerIcon = SpinnerIcon.SpinnerIcon;
|
|
94
102
|
exports.StopIcon = StopIcon.StopIcon;
|
|
95
103
|
exports.SuccessIcon = SuccessIcon.SuccessIcon;
|
|
104
|
+
exports.UploadCloudIcon = UploadCloudIcon.UploadCloudIcon;
|
|
96
105
|
exports.VipBadgeIcon = VipBadgeIcon.VipBadgeIcon;
|
|
97
106
|
exports.WarningIcon = WarningIcon.WarningIcon;
|
|
98
107
|
exports.WarningTriangleIcon = WarningTriangleIcon.WarningTriangleIcon;
|
|
@@ -112,6 +121,7 @@ exports.Tabs = Tabs.Tabs;
|
|
|
112
121
|
exports.TabsContent = TabsContent.TabsContent;
|
|
113
122
|
exports.TabsList = TabsList.TabsList;
|
|
114
123
|
exports.TabsTrigger = TabsTrigger.TabsTrigger;
|
|
124
|
+
exports.TextArea = TextArea.TextArea;
|
|
115
125
|
exports.TextField = TextField.TextField;
|
|
116
126
|
exports.Toast = Toast.Toast;
|
|
117
127
|
exports.ToastProvider = Toast.ToastProvider;
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../utils/cn.mjs";
|
|
5
|
+
import { Button } from "../Button/Button.mjs";
|
|
6
|
+
import { MicrophoneIcon } from "../Icons/MicrophoneIcon.mjs";
|
|
7
|
+
import { StopIcon } from "../Icons/StopIcon.mjs";
|
|
8
|
+
import { UploadCloudIcon } from "../Icons/UploadCloudIcon.mjs";
|
|
9
|
+
import { AudioWaveform } from "./AudioWaveform.mjs";
|
|
10
|
+
import { formatAudioTime, validateAudioFile } from "./audioUtils.mjs";
|
|
11
|
+
import { DEFAULT_ACCEPTED_TYPES, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_RECORDING_DURATION, DEFAULT_MIN_RECORDING_DURATION } from "./constants.mjs";
|
|
12
|
+
import { useAudioRecorder } from "./useAudioRecorder.mjs";
|
|
13
|
+
function partitionFiles(files, maxFileSize, accept, maxFiles) {
|
|
14
|
+
const accepted = [];
|
|
15
|
+
const rejected = [];
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const errors = validateAudioFile(file, { maxFileSize, acceptedTypes: accept });
|
|
18
|
+
if (errors.length > 0) {
|
|
19
|
+
rejected.push({ file, errors });
|
|
20
|
+
} else {
|
|
21
|
+
accepted.push(file);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (maxFiles > 0 && accepted.length > maxFiles) {
|
|
25
|
+
const excess = accepted.splice(maxFiles);
|
|
26
|
+
for (const file of excess) {
|
|
27
|
+
rejected.push({
|
|
28
|
+
file,
|
|
29
|
+
errors: [{ code: "too-many-files", message: `Too many files. Maximum is ${maxFiles}` }]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { accepted, rejected };
|
|
34
|
+
}
|
|
35
|
+
const AudioUpload = React.forwardRef(
|
|
36
|
+
({
|
|
37
|
+
className,
|
|
38
|
+
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
|
39
|
+
accept = DEFAULT_ACCEPTED_TYPES,
|
|
40
|
+
maxFiles = 1,
|
|
41
|
+
allowRecording = true,
|
|
42
|
+
maxRecordingDuration = DEFAULT_MAX_RECORDING_DURATION,
|
|
43
|
+
minRecordingDuration = DEFAULT_MIN_RECORDING_DURATION,
|
|
44
|
+
onFilesAccepted,
|
|
45
|
+
onFilesRejected,
|
|
46
|
+
onRecordingComplete,
|
|
47
|
+
onRecordingTooShort,
|
|
48
|
+
onPermissionError,
|
|
49
|
+
onRecordingError,
|
|
50
|
+
uploadTitle = "Click to upload, or drag & drop",
|
|
51
|
+
uploadDescription = "Audio files only, up to 10MB each",
|
|
52
|
+
separatorText = "or",
|
|
53
|
+
recordButtonLabel = "Record audio",
|
|
54
|
+
stopButtonAriaLabel = "Stop recording",
|
|
55
|
+
disabled = false,
|
|
56
|
+
...props
|
|
57
|
+
}, ref) => {
|
|
58
|
+
const inputId = React.useId();
|
|
59
|
+
const descriptionId = React.useId();
|
|
60
|
+
const [isDragActive, setIsDragActive] = React.useState(false);
|
|
61
|
+
const stopButtonRef = React.useRef(null);
|
|
62
|
+
const {
|
|
63
|
+
isRecording,
|
|
64
|
+
elapsedMs,
|
|
65
|
+
startRecording,
|
|
66
|
+
stopRecording,
|
|
67
|
+
analyserNode,
|
|
68
|
+
isSupported: isRecordingSupported
|
|
69
|
+
} = useAudioRecorder({
|
|
70
|
+
maxDuration: maxRecordingDuration,
|
|
71
|
+
minDuration: minRecordingDuration,
|
|
72
|
+
onComplete: onRecordingComplete,
|
|
73
|
+
onTooShort: onRecordingTooShort,
|
|
74
|
+
onPermissionError,
|
|
75
|
+
onError: onRecordingError
|
|
76
|
+
});
|
|
77
|
+
const acceptString = accept.join(",");
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
if (isRecording) {
|
|
80
|
+
stopButtonRef.current?.focus();
|
|
81
|
+
}
|
|
82
|
+
}, [isRecording]);
|
|
83
|
+
const validateAndAcceptFiles = React.useCallback(
|
|
84
|
+
(files) => {
|
|
85
|
+
const { accepted, rejected } = partitionFiles(
|
|
86
|
+
Array.from(files),
|
|
87
|
+
maxFileSize,
|
|
88
|
+
accept,
|
|
89
|
+
maxFiles
|
|
90
|
+
);
|
|
91
|
+
if (accepted.length > 0) onFilesAccepted?.(accepted);
|
|
92
|
+
if (rejected.length > 0) onFilesRejected?.(rejected);
|
|
93
|
+
},
|
|
94
|
+
[maxFileSize, accept, maxFiles, onFilesAccepted, onFilesRejected]
|
|
95
|
+
);
|
|
96
|
+
const handleDrop = (e) => {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
setIsDragActive(false);
|
|
100
|
+
if (disabled) return;
|
|
101
|
+
const { files } = e.dataTransfer;
|
|
102
|
+
if (files.length > 0) {
|
|
103
|
+
validateAndAcceptFiles(files);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const handleDragOver = (e) => {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
e.stopPropagation();
|
|
109
|
+
if (!disabled) {
|
|
110
|
+
setIsDragActive(true);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const handleDragLeave = (e) => {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
e.stopPropagation();
|
|
116
|
+
setIsDragActive(false);
|
|
117
|
+
};
|
|
118
|
+
const handleFileInputChange = (e) => {
|
|
119
|
+
const { files } = e.target;
|
|
120
|
+
if (files && files.length > 0) {
|
|
121
|
+
validateAndAcceptFiles(files);
|
|
122
|
+
}
|
|
123
|
+
e.target.value = "";
|
|
124
|
+
};
|
|
125
|
+
const handleRecordClick = (e) => {
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
startRecording();
|
|
128
|
+
};
|
|
129
|
+
const handleStopClick = () => {
|
|
130
|
+
stopRecording();
|
|
131
|
+
};
|
|
132
|
+
if (isRecording) {
|
|
133
|
+
const formattedElapsed = formatAudioTime(elapsedMs);
|
|
134
|
+
return (
|
|
135
|
+
// biome-ignore lint/a11y/useSemanticElements: <fieldset> would break the public HTMLDivElement ref/props API
|
|
136
|
+
/* @__PURE__ */ jsxs(
|
|
137
|
+
"div",
|
|
138
|
+
{
|
|
139
|
+
ref,
|
|
140
|
+
role: "group",
|
|
141
|
+
"aria-label": "Audio recording in progress",
|
|
142
|
+
"data-testid": "audio-upload",
|
|
143
|
+
"data-state": "recording",
|
|
144
|
+
className: cn(
|
|
145
|
+
"flex flex-col items-center justify-center gap-2 rounded-xl bg-neutral-100 px-4 py-3",
|
|
146
|
+
className
|
|
147
|
+
),
|
|
148
|
+
...props,
|
|
149
|
+
children: [
|
|
150
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col items-center gap-2", children: [
|
|
151
|
+
/* @__PURE__ */ jsx(
|
|
152
|
+
"div",
|
|
153
|
+
{
|
|
154
|
+
className: "flex size-[72px] items-center justify-center rounded-full bg-neutral-400",
|
|
155
|
+
"aria-hidden": "true",
|
|
156
|
+
children: /* @__PURE__ */ jsx(MicrophoneIcon, { className: "size-5 text-body-300" })
|
|
157
|
+
}
|
|
158
|
+
),
|
|
159
|
+
/* @__PURE__ */ jsxs(
|
|
160
|
+
"p",
|
|
161
|
+
{
|
|
162
|
+
role: "timer",
|
|
163
|
+
"aria-label": "Recording time",
|
|
164
|
+
className: "typography-body-1-regular text-body-100",
|
|
165
|
+
children: [
|
|
166
|
+
formattedElapsed,
|
|
167
|
+
" / ",
|
|
168
|
+
formatAudioTime(maxRecordingDuration * 1e3)
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
] }),
|
|
173
|
+
/* @__PURE__ */ jsx("div", { className: "flex w-full items-center gap-2.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
|
|
174
|
+
AudioWaveform,
|
|
175
|
+
{
|
|
176
|
+
analyserNode,
|
|
177
|
+
isRecording,
|
|
178
|
+
className: "flex-1"
|
|
179
|
+
}
|
|
180
|
+
) }),
|
|
181
|
+
/* @__PURE__ */ jsx(
|
|
182
|
+
"button",
|
|
183
|
+
{
|
|
184
|
+
ref: stopButtonRef,
|
|
185
|
+
type: "button",
|
|
186
|
+
onClick: handleStopClick,
|
|
187
|
+
className: "mt-1 flex size-11 items-center justify-center rounded-full bg-error-500 text-body-white-solid-constant transition-colors hover:bg-error-500/80 focus:shadow-focus-ring focus-visible:outline-none",
|
|
188
|
+
"aria-label": stopButtonAriaLabel,
|
|
189
|
+
children: /* @__PURE__ */ jsx(StopIcon, { className: "size-5" })
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return (
|
|
198
|
+
// biome-ignore lint/a11y/useSemanticElements: <fieldset> would break the public HTMLDivElement ref/props API
|
|
199
|
+
/* @__PURE__ */ jsxs(
|
|
200
|
+
"div",
|
|
201
|
+
{
|
|
202
|
+
ref,
|
|
203
|
+
role: "group",
|
|
204
|
+
"aria-label": "Audio upload",
|
|
205
|
+
"data-testid": "audio-upload",
|
|
206
|
+
"data-state": "idle",
|
|
207
|
+
"aria-disabled": disabled || void 0,
|
|
208
|
+
onDrop: handleDrop,
|
|
209
|
+
onDragOver: handleDragOver,
|
|
210
|
+
onDragLeave: handleDragLeave,
|
|
211
|
+
className: cn(
|
|
212
|
+
"flex flex-col items-center justify-center gap-2 rounded-xl bg-neutral-100 px-4 py-3 transition-colors",
|
|
213
|
+
isDragActive && "bg-brand-green-50 ring-2 ring-brand-green-500",
|
|
214
|
+
disabled && "pointer-events-none opacity-50",
|
|
215
|
+
className
|
|
216
|
+
),
|
|
217
|
+
...props,
|
|
218
|
+
children: [
|
|
219
|
+
/* @__PURE__ */ jsx(
|
|
220
|
+
"input",
|
|
221
|
+
{
|
|
222
|
+
id: inputId,
|
|
223
|
+
type: "file",
|
|
224
|
+
accept: acceptString,
|
|
225
|
+
multiple: maxFiles > 1,
|
|
226
|
+
onChange: handleFileInputChange,
|
|
227
|
+
className: "peer sr-only",
|
|
228
|
+
disabled,
|
|
229
|
+
"aria-describedby": descriptionId
|
|
230
|
+
}
|
|
231
|
+
),
|
|
232
|
+
/* @__PURE__ */ jsxs(
|
|
233
|
+
"label",
|
|
234
|
+
{
|
|
235
|
+
htmlFor: inputId,
|
|
236
|
+
className: "flex cursor-pointer flex-col items-center gap-2 rounded-lg px-2 py-1 peer-focus-visible:shadow-focus-ring",
|
|
237
|
+
children: [
|
|
238
|
+
/* @__PURE__ */ jsx(UploadCloudIcon, { className: "size-5 text-body-100" }),
|
|
239
|
+
/* @__PURE__ */ jsx("span", { className: "typography-body-1-semibold text-center text-body-100", children: uploadTitle }),
|
|
240
|
+
/* @__PURE__ */ jsx("span", { id: descriptionId, className: "typography-body-2-regular text-center text-body-100", children: uploadDescription })
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
),
|
|
244
|
+
allowRecording && isRecordingSupported && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
245
|
+
/* @__PURE__ */ jsx("p", { className: "typography-body-2-regular text-center text-body-100", children: separatorText }),
|
|
246
|
+
/* @__PURE__ */ jsx(
|
|
247
|
+
Button,
|
|
248
|
+
{
|
|
249
|
+
variant: "brand",
|
|
250
|
+
size: "40",
|
|
251
|
+
leftIcon: /* @__PURE__ */ jsx(MicrophoneIcon, { className: "size-5" }),
|
|
252
|
+
onClick: handleRecordClick,
|
|
253
|
+
disabled,
|
|
254
|
+
type: "button",
|
|
255
|
+
children: recordButtonLabel
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
] })
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
AudioUpload.displayName = "AudioUpload";
|
|
266
|
+
export {
|
|
267
|
+
AudioUpload
|
|
268
|
+
};
|
|
269
|
+
//# sourceMappingURL=AudioUpload.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioUpload.mjs","sources":["../../../src/components/AudioUpload/AudioUpload.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button/Button\";\nimport { MicrophoneIcon } from \"../Icons/MicrophoneIcon\";\nimport { StopIcon } from \"../Icons/StopIcon\";\nimport { UploadCloudIcon } from \"../Icons/UploadCloudIcon\";\nimport { AudioWaveform } from \"./AudioWaveform\";\nimport { type AudioValidationError, formatAudioTime, validateAudioFile } from \"./audioUtils\";\nimport {\n DEFAULT_ACCEPTED_TYPES,\n DEFAULT_MAX_FILE_SIZE,\n DEFAULT_MAX_RECORDING_DURATION,\n DEFAULT_MIN_RECORDING_DURATION,\n} from \"./constants\";\nimport { useAudioRecorder } from \"./useAudioRecorder\";\n\n/** A file that was rejected during drop or browse, along with the reasons. */\nexport interface AudioFileRejection {\n /** The rejected file. */\n file: File;\n /** One or more validation errors explaining why the file was rejected. */\n errors: AudioValidationError[];\n}\n\nexport interface AudioUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onDrop\"> {\n /** Maximum file size in bytes. @default 10_485_760 (10MB) */\n maxFileSize?: number;\n /** Accepted audio MIME types. @default DEFAULT_ACCEPTED_TYPES */\n accept?: readonly string[];\n /** Maximum number of files per drop. @default 1 */\n maxFiles?: number;\n /** Whether to show the record audio button. @default true */\n allowRecording?: boolean;\n /** Maximum recording duration in seconds. @default 30 */\n maxRecordingDuration?: number;\n /** Minimum recording duration in seconds. @default 5 */\n minRecordingDuration?: number;\n\n /** Called when valid files are accepted via drop or browse */\n onFilesAccepted?: (files: File[]) => void;\n /** Called when files are rejected (wrong type, too large, etc.) */\n onFilesRejected?: (rejections: AudioFileRejection[]) => void;\n /** Called when a recording completes and meets minimum duration */\n onRecordingComplete?: (blob: Blob, durationMs: number) => void;\n /** Called when recording is stopped but does not meet minimum duration */\n onRecordingTooShort?: (durationMs: number, minDurationMs: number) => void;\n /** Called when microphone permission is denied or unavailable */\n onPermissionError?: (error: Error) => void;\n /** Called when an unexpected recording error occurs */\n onRecordingError?: (error: Error) => void;\n\n /** Upload area title text. @default \"Click to upload, or drag & drop\" */\n uploadTitle?: string;\n /** Upload area description text. @default \"Audio files only, up to 10MB each\" */\n uploadDescription?: string;\n /** Separator text between upload and record. @default \"or\" */\n separatorText?: string;\n /** Record button label. @default \"Record audio\" */\n recordButtonLabel?: string;\n /** Stop recording button aria-label. @default \"Stop recording\" */\n stopButtonAriaLabel?: string;\n\n /** Whether the component is disabled. @default false */\n disabled?: boolean;\n}\n\nfunction partitionFiles(\n files: File[],\n maxFileSize: number,\n accept: readonly string[],\n maxFiles: number,\n): { accepted: File[]; rejected: AudioFileRejection[] } {\n const accepted: File[] = [];\n const rejected: AudioFileRejection[] = [];\n\n for (const file of files) {\n const errors = validateAudioFile(file, { maxFileSize, acceptedTypes: accept });\n if (errors.length > 0) {\n rejected.push({ file, errors });\n } else {\n accepted.push(file);\n }\n }\n\n if (maxFiles > 0 && accepted.length > maxFiles) {\n const excess = accepted.splice(maxFiles);\n for (const file of excess) {\n rejected.push({\n file,\n errors: [{ code: \"too-many-files\", message: `Too many files. Maximum is ${maxFiles}` }],\n });\n }\n }\n\n return { accepted, rejected };\n}\n\n/**\n * Audio file upload with drag-and-drop and optional in-browser recording.\n * Supports file validation, multiple files, and real-time waveform visualization during recording.\n *\n * @example\n * ```tsx\n * <AudioUpload\n * onFilesAccepted={(files) => console.log(files)}\n * onRecordingComplete={(blob, duration) => console.log(blob, duration)}\n * />\n * ```\n */\nexport const AudioUpload = React.forwardRef<HTMLDivElement, AudioUploadProps>(\n (\n {\n className,\n maxFileSize = DEFAULT_MAX_FILE_SIZE,\n accept = DEFAULT_ACCEPTED_TYPES,\n maxFiles = 1,\n allowRecording = true,\n maxRecordingDuration = DEFAULT_MAX_RECORDING_DURATION,\n minRecordingDuration = DEFAULT_MIN_RECORDING_DURATION,\n onFilesAccepted,\n onFilesRejected,\n onRecordingComplete,\n onRecordingTooShort,\n onPermissionError,\n onRecordingError,\n uploadTitle = \"Click to upload, or drag & drop\",\n uploadDescription = \"Audio files only, up to 10MB each\",\n separatorText = \"or\",\n recordButtonLabel = \"Record audio\",\n stopButtonAriaLabel = \"Stop recording\",\n disabled = false,\n ...props\n },\n ref,\n ) => {\n const inputId = React.useId();\n const descriptionId = React.useId();\n const [isDragActive, setIsDragActive] = React.useState(false);\n const stopButtonRef = React.useRef<HTMLButtonElement>(null);\n\n const {\n isRecording,\n elapsedMs,\n startRecording,\n stopRecording,\n analyserNode,\n isSupported: isRecordingSupported,\n } = useAudioRecorder({\n maxDuration: maxRecordingDuration,\n minDuration: minRecordingDuration,\n onComplete: onRecordingComplete,\n onTooShort: onRecordingTooShort,\n onPermissionError,\n onError: onRecordingError,\n });\n\n const acceptString = accept.join(\",\");\n\n // Move focus to stop button when recording starts\n React.useEffect(() => {\n if (isRecording) {\n stopButtonRef.current?.focus();\n }\n }, [isRecording]);\n\n const validateAndAcceptFiles = React.useCallback(\n (files: FileList | File[]) => {\n const { accepted, rejected } = partitionFiles(\n Array.from(files),\n maxFileSize,\n accept,\n maxFiles,\n );\n if (accepted.length > 0) onFilesAccepted?.(accepted);\n if (rejected.length > 0) onFilesRejected?.(rejected);\n },\n [maxFileSize, accept, maxFiles, onFilesAccepted, onFilesRejected],\n );\n\n const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragActive(false);\n\n if (disabled) return;\n\n const { files } = e.dataTransfer;\n if (files.length > 0) {\n validateAndAcceptFiles(files);\n }\n };\n\n const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n if (!disabled) {\n setIsDragActive(true);\n }\n };\n\n const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragActive(false);\n };\n\n const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const { files } = e.target;\n if (files && files.length > 0) {\n validateAndAcceptFiles(files);\n }\n // Reset input so same file can be selected again\n e.target.value = \"\";\n };\n\n const handleRecordClick = (e: React.MouseEvent) => {\n e.stopPropagation();\n startRecording();\n };\n\n const handleStopClick = () => {\n stopRecording();\n };\n\n if (isRecording) {\n const formattedElapsed = formatAudioTime(elapsedMs);\n\n return (\n // biome-ignore lint/a11y/useSemanticElements: <fieldset> would break the public HTMLDivElement ref/props API\n <div\n ref={ref}\n role=\"group\"\n aria-label=\"Audio recording in progress\"\n data-testid=\"audio-upload\"\n data-state=\"recording\"\n className={cn(\n \"flex flex-col items-center justify-center gap-2 rounded-xl bg-neutral-100 px-4 py-3\",\n className,\n )}\n {...props}\n >\n <div className=\"flex flex-1 flex-col items-center gap-2\">\n <div\n className=\"flex size-[72px] items-center justify-center rounded-full bg-neutral-400\"\n aria-hidden=\"true\"\n >\n <MicrophoneIcon className=\"size-5 text-body-300\" />\n </div>\n\n <p\n role=\"timer\"\n aria-label=\"Recording time\"\n className=\"typography-body-1-regular text-body-100\"\n >\n {formattedElapsed} / {formatAudioTime(maxRecordingDuration * 1000)}\n </p>\n </div>\n\n <div className=\"flex w-full items-center gap-2.5\" aria-hidden=\"true\">\n <AudioWaveform\n analyserNode={analyserNode}\n isRecording={isRecording}\n className=\"flex-1\"\n />\n </div>\n\n <button\n ref={stopButtonRef}\n type=\"button\"\n onClick={handleStopClick}\n className=\"mt-1 flex size-11 items-center justify-center rounded-full bg-error-500 text-body-white-solid-constant transition-colors hover:bg-error-500/80 focus:shadow-focus-ring focus-visible:outline-none\"\n aria-label={stopButtonAriaLabel}\n >\n <StopIcon className=\"size-5\" />\n </button>\n </div>\n );\n }\n\n return (\n // biome-ignore lint/a11y/useSemanticElements: <fieldset> would break the public HTMLDivElement ref/props API\n <div\n ref={ref}\n role=\"group\"\n aria-label=\"Audio upload\"\n data-testid=\"audio-upload\"\n data-state=\"idle\"\n aria-disabled={disabled || undefined}\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n className={cn(\n \"flex flex-col items-center justify-center gap-2 rounded-xl bg-neutral-100 px-4 py-3 transition-colors\",\n isDragActive && \"bg-brand-green-50 ring-2 ring-brand-green-500\",\n disabled && \"pointer-events-none opacity-50\",\n className,\n )}\n {...props}\n >\n <input\n id={inputId}\n type=\"file\"\n accept={acceptString}\n multiple={maxFiles > 1}\n onChange={handleFileInputChange}\n className=\"peer sr-only\"\n disabled={disabled}\n aria-describedby={descriptionId}\n />\n\n <label\n htmlFor={inputId}\n className=\"flex cursor-pointer flex-col items-center gap-2 rounded-lg px-2 py-1 peer-focus-visible:shadow-focus-ring\"\n >\n <UploadCloudIcon className=\"size-5 text-body-100\" />\n\n <span className=\"typography-body-1-semibold text-center text-body-100\">\n {uploadTitle}\n </span>\n\n <span id={descriptionId} className=\"typography-body-2-regular text-center text-body-100\">\n {uploadDescription}\n </span>\n </label>\n\n {allowRecording && isRecordingSupported && (\n <>\n <p className=\"typography-body-2-regular text-center text-body-100\">{separatorText}</p>\n\n <Button\n variant=\"brand\"\n size=\"40\"\n leftIcon={<MicrophoneIcon className=\"size-5\" />}\n onClick={handleRecordClick}\n disabled={disabled}\n type=\"button\"\n >\n {recordButtonLabel}\n </Button>\n </>\n )}\n </div>\n );\n },\n);\n\nAudioUpload.displayName = \"AudioUpload\";\n"],"names":[],"mappings":"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"}
|