@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.
Files changed (46) hide show
  1. package/dist/cjs/components/AudioUpload/AudioUpload.cjs +286 -0
  2. package/dist/cjs/components/AudioUpload/AudioUpload.cjs.map +1 -0
  3. package/dist/cjs/components/AudioUpload/AudioWaveform.cjs +121 -0
  4. package/dist/cjs/components/AudioUpload/AudioWaveform.cjs.map +1 -0
  5. package/dist/cjs/components/AudioUpload/audioUtils.cjs +44 -0
  6. package/dist/cjs/components/AudioUpload/audioUtils.cjs.map +1 -0
  7. package/dist/cjs/components/AudioUpload/constants.cjs +21 -0
  8. package/dist/cjs/components/AudioUpload/constants.cjs.map +1 -0
  9. package/dist/cjs/components/AudioUpload/useAudioRecorder.cjs +191 -0
  10. package/dist/cjs/components/AudioUpload/useAudioRecorder.cjs.map +1 -0
  11. package/dist/cjs/components/DatePicker/DatePicker.cjs +1 -1
  12. package/dist/cjs/components/DatePicker/DatePicker.cjs.map +1 -1
  13. package/dist/cjs/components/Icons/CheckOutlineIcon.cjs +55 -0
  14. package/dist/cjs/components/Icons/CheckOutlineIcon.cjs.map +1 -0
  15. package/dist/cjs/components/Icons/UploadCloudIcon.cjs +61 -0
  16. package/dist/cjs/components/Icons/UploadCloudIcon.cjs.map +1 -0
  17. package/dist/cjs/components/TextArea/TextArea.cjs +209 -0
  18. package/dist/cjs/components/TextArea/TextArea.cjs.map +1 -0
  19. package/dist/cjs/components/TextField/TextField.cjs +22 -34
  20. package/dist/cjs/components/TextField/TextField.cjs.map +1 -1
  21. package/dist/cjs/index.cjs +10 -0
  22. package/dist/cjs/index.cjs.map +1 -1
  23. package/dist/components/AudioUpload/AudioUpload.mjs +269 -0
  24. package/dist/components/AudioUpload/AudioUpload.mjs.map +1 -0
  25. package/dist/components/AudioUpload/AudioWaveform.mjs +104 -0
  26. package/dist/components/AudioUpload/AudioWaveform.mjs.map +1 -0
  27. package/dist/components/AudioUpload/audioUtils.mjs +44 -0
  28. package/dist/components/AudioUpload/audioUtils.mjs.map +1 -0
  29. package/dist/components/AudioUpload/constants.mjs +21 -0
  30. package/dist/components/AudioUpload/constants.mjs.map +1 -0
  31. package/dist/components/AudioUpload/useAudioRecorder.mjs +174 -0
  32. package/dist/components/AudioUpload/useAudioRecorder.mjs.map +1 -0
  33. package/dist/components/DatePicker/DatePicker.mjs +1 -1
  34. package/dist/components/DatePicker/DatePicker.mjs.map +1 -1
  35. package/dist/components/Icons/CheckOutlineIcon.mjs +38 -0
  36. package/dist/components/Icons/CheckOutlineIcon.mjs.map +1 -0
  37. package/dist/components/Icons/UploadCloudIcon.mjs +44 -0
  38. package/dist/components/Icons/UploadCloudIcon.mjs.map +1 -0
  39. package/dist/components/TextArea/TextArea.mjs +192 -0
  40. package/dist/components/TextArea/TextArea.mjs.map +1 -0
  41. package/dist/components/TextField/TextField.mjs +22 -34
  42. package/dist/components/TextField/TextField.mjs.map +1 -1
  43. package/dist/index.d.ts +164 -0
  44. package/dist/index.mjs +11 -1
  45. package/dist/index.mjs.map +1 -1
  46. 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 PADDING_LEFT = {
35
- "48": ["pl-4", "pl-11"],
36
- "40": ["pl-4", "pl-11"],
37
- "32": ["pl-3", "pl-10"]
35
+ const PADDING_HORIZONTAL = {
36
+ "48": "px-4",
37
+ "40": "px-4",
38
+ "32": "px-3"
38
39
  };
39
- const PADDING_RIGHT = {
40
- "48": ["pr-4", "pr-11"],
41
- "40": ["pr-4", "pr-11"],
42
- "32": ["pr-3", "pr-10"]
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
- "relative rounded-xl border bg-neutral-100 has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors",
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, hasLeftIcon, hasRightIcon) {
56
+ function getInputClassName(size) {
64
57
  return cn.cn(
65
- "h-full w-full rounded-xl bg-transparent text-body-100 no-underline placeholder:text-body-200 placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed",
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
- const ICON_BASE = "pointer-events-none absolute top-1/2 flex size-5 -translate-y-1/2 items-center justify-center text-body-200";
72
- function TextFieldIcon({
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, { size, side: "left", children: leftIcon }),
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, !!leftIcon, !!rightIcon),
137
+ className: getInputClassName(size),
151
138
  ...props
152
139
  }
153
140
  ),
154
- rightIcon && /* @__PURE__ */ jsxRuntime.jsx(TextFieldIcon, { size, side: "right", children: rightIcon })
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 PADDING_LEFT: Record<TextFieldSize, [base: string, withIcon: string]> = {\n \"48\": [\"pl-4\", \"pl-11\"],\n \"40\": [\"pl-4\", \"pl-11\"],\n \"32\": [\"pl-3\", \"pl-10\"],\n};\n\nconst PADDING_RIGHT: Record<TextFieldSize, [base: string, withIcon: string]> = {\n \"48\": [\"pr-4\", \"pr-11\"],\n \"40\": [\"pr-4\", \"pr-11\"],\n \"32\": [\"pr-3\", \"pr-10\"],\n};\n\nconst ICON_LEFT: Record<TextFieldSize, string> = {\n \"48\": \"left-4\",\n \"40\": \"left-4\",\n \"32\": \"left-3\",\n};\n\nconst ICON_RIGHT: Record<TextFieldSize, string> = {\n \"48\": \"right-4\",\n \"40\": \"right-4\",\n \"32\": \"right-3\",\n};\n\nfunction getContainerClassName(size: TextFieldSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative rounded-xl border bg-neutral-100 has-focus-visible:shadow-focus-ring 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 disabled && \"opacity-50\",\n );\n}\n\nfunction getInputClassName(size: TextFieldSize, hasLeftIcon: boolean, hasRightIcon: boolean) {\n return cn(\n \"h-full w-full 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 PADDING_LEFT[size][hasLeftIcon ? 1 : 0],\n PADDING_RIGHT[size][hasRightIcon ? 1 : 0],\n );\n}\n\nconst ICON_BASE =\n \"pointer-events-none absolute top-1/2 flex size-5 -translate-y-1/2 items-center justify-center text-body-200\";\n\nfunction TextFieldIcon({\n children,\n size,\n side,\n}: {\n children: React.ReactNode;\n size: TextFieldSize;\n side: \"left\" | \"right\";\n}) {\n return (\n <div className={cn(ICON_BASE, side === \"left\" ? ICON_LEFT[size] : ICON_RIGHT[size])}>\n {children}\n </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 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 && (\n <TextFieldIcon size={size} side=\"left\">\n {leftIcon}\n </TextFieldIcon>\n )}\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, !!leftIcon, !!rightIcon)}\n {...props}\n />\n\n {rightIcon && (\n <TextFieldIcon size={size} side=\"right\">\n {rightIcon}\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"],"mappings":"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"}
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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"}
@@ -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;
@@ -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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"}