@fanvue/ui 3.16.0 → 3.18.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 (56) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs +16 -89
  3. package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  4. package/dist/cjs/components/Icons/ReceiveMoneyIcon.cjs +79 -0
  5. package/dist/cjs/components/Icons/ReceiveMoneyIcon.cjs.map +1 -0
  6. package/dist/cjs/components/PhoneField/PhoneField.cjs +198 -0
  7. package/dist/cjs/components/PhoneField/PhoneField.cjs.map +1 -0
  8. package/dist/cjs/components/RatingSummary/RatingSummary.cjs +30 -23
  9. package/dist/cjs/components/RatingSummary/RatingSummary.cjs.map +1 -1
  10. package/dist/cjs/components/TextArea/TextArea.cjs +2 -2
  11. package/dist/cjs/components/TextArea/TextArea.cjs.map +1 -1
  12. package/dist/cjs/components/TextField/TextField.cjs +13 -6
  13. package/dist/cjs/components/TextField/TextField.cjs.map +1 -1
  14. package/dist/cjs/components/VoiceNote/VoiceNote.cjs +305 -0
  15. package/dist/cjs/components/VoiceNote/VoiceNote.cjs.map +1 -0
  16. package/dist/cjs/index.cjs +6 -0
  17. package/dist/cjs/index.cjs.map +1 -1
  18. package/dist/cjs/utils/audioWaveform.cjs +76 -0
  19. package/dist/cjs/utils/audioWaveform.cjs.map +1 -0
  20. package/dist/cjs/utils/useAudioPlayback.cjs +123 -0
  21. package/dist/cjs/utils/useAudioPlayback.cjs.map +1 -0
  22. package/dist/cjs/utils/useFittedBarCount.cjs +42 -0
  23. package/dist/cjs/utils/useFittedBarCount.cjs.map +1 -0
  24. package/dist/cjs/utils/useWaveformPeaks.cjs +48 -0
  25. package/dist/cjs/utils/useWaveformPeaks.cjs.map +1 -0
  26. package/dist/cjs/utils/useWaveformSeek.cjs +100 -0
  27. package/dist/cjs/utils/useWaveformSeek.cjs.map +1 -0
  28. package/dist/components/AudioPlayer/AudioPlayer.mjs +7 -80
  29. package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -1
  30. package/dist/components/Icons/ReceiveMoneyIcon.mjs +62 -0
  31. package/dist/components/Icons/ReceiveMoneyIcon.mjs.map +1 -0
  32. package/dist/components/PhoneField/PhoneField.mjs +181 -0
  33. package/dist/components/PhoneField/PhoneField.mjs.map +1 -0
  34. package/dist/components/RatingSummary/RatingSummary.mjs +30 -23
  35. package/dist/components/RatingSummary/RatingSummary.mjs.map +1 -1
  36. package/dist/components/TextArea/TextArea.mjs +2 -2
  37. package/dist/components/TextArea/TextArea.mjs.map +1 -1
  38. package/dist/components/TextField/TextField.mjs +13 -6
  39. package/dist/components/TextField/TextField.mjs.map +1 -1
  40. package/dist/components/VoiceNote/VoiceNote.mjs +288 -0
  41. package/dist/components/VoiceNote/VoiceNote.mjs.map +1 -0
  42. package/dist/index.d.ts +158 -0
  43. package/dist/index.mjs +6 -0
  44. package/dist/index.mjs.map +1 -1
  45. package/dist/styles/base.css +11 -2
  46. package/dist/utils/audioWaveform.mjs +76 -0
  47. package/dist/utils/audioWaveform.mjs.map +1 -0
  48. package/dist/utils/useAudioPlayback.mjs +106 -0
  49. package/dist/utils/useAudioPlayback.mjs.map +1 -0
  50. package/dist/utils/useFittedBarCount.mjs +25 -0
  51. package/dist/utils/useFittedBarCount.mjs.map +1 -0
  52. package/dist/utils/useWaveformPeaks.mjs +31 -0
  53. package/dist/utils/useWaveformPeaks.mjs.map +1 -0
  54. package/dist/utils/useWaveformSeek.mjs +83 -0
  55. package/dist/utils/useWaveformSeek.mjs.map +1 -0
  56. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"RatingSummary.mjs","sources":["../../../src/components/RatingSummary/RatingSummary.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { StarIcon } from \"../Icons/StarIcon\";\n\n/** A single rating value paired with the number of reviews that gave it. */\nexport interface RatingCount {\n /** The rating value this entry represents (e.g. `5`). */\n rating: number;\n /** Number of reviews that gave this rating. */\n count: number;\n}\n\nexport interface RatingSummaryProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n /**\n * Per-rating review counts. Order is not significant — rows always render from\n * `maxRating` down to `1`, and any rating without an entry renders as zero.\n * Entries are expected to use whole-number ratings within `1`–`maxRating`;\n * any outside that range are ignored, including in the derived total and average.\n */\n distribution: RatingCount[];\n /**\n * Average rating shown in the header. Defaults to the count-weighted mean of\n * `distribution`; provide it to display a server-computed average instead.\n */\n averageRating?: number;\n /** Highest possible rating. Sets how many histogram rows render. @default 5 */\n maxRating?: number;\n /**\n * Formats a review count into its label, without surrounding parentheses.\n * Used for the visible text and as the count portion of the default\n * accessible labels.\n * @default ``(count) => `${count.toLocaleString()} review(s)` ``\n */\n formatCount?: (count: number) => string;\n /**\n * Builds the accessible label for the header. Override to localise the\n * screen-reader text. `average` is the already-formatted average string.\n * @default ``(average, max, total) => `Average rating ${average} out of ${max}, ${formatCount(total)}` ``\n */\n formatAverageLabel?: (average: string, maxRating: number, total: number) => string;\n /**\n * Builds the accessible label for each histogram row. Override to localise the\n * screen-reader text.\n * @default ``(rating, count) => `${rating} star(s), ${formatCount(count)}` ``\n */\n formatRatingLabel?: (rating: number, count: number) => string;\n}\n\nfunction defaultFormatCount(count: number): string {\n return `${count.toLocaleString()} ${count === 1 ? \"review\" : \"reviews\"}`;\n}\n\nfunction getWeightedAverage(distribution: RatingCount[]): number {\n let total = 0;\n let weighted = 0;\n for (const { rating, count } of distribution) {\n total += count;\n weighted += rating * count;\n }\n return total === 0 ? 0 : weighted / total;\n}\n\n/**\n * An aggregated rating overview: a headline average with the total review count,\n * followed by a per-rating histogram. Bar lengths are scaled relative to the\n * most-reviewed rating, so the busiest rating always fills the track.\n *\n * The brand star is decorative; the header and each histogram row expose\n * `role=\"img\"` with an accessible label (override via `formatAverageLabel` /\n * `formatRatingLabel`) so the single-star-plus-number reads clearly.\n *\n * @example\n * ```tsx\n * <RatingSummary\n * distribution={[\n * { rating: 5, count: 300 },\n * { rating: 4, count: 20 },\n * { rating: 3, count: 20 },\n * { rating: 2, count: 10 },\n * { rating: 1, count: 0 },\n * ]}\n * />\n * ```\n */\nexport const RatingSummary = React.forwardRef<HTMLDivElement, RatingSummaryProps>(\n (\n {\n distribution,\n averageRating,\n maxRating = 5,\n formatCount = defaultFormatCount,\n formatAverageLabel,\n formatRatingLabel,\n className,\n ...props\n },\n ref,\n ) => {\n const countByRating = new Map<number, number>();\n for (const { rating, count } of distribution) {\n countByRating.set(rating, (countByRating.get(rating) ?? 0) + count);\n }\n\n const rows = Array.from({ length: maxRating }, (_, index) => {\n const rating = maxRating - index;\n return { rating, count: countByRating.get(rating) ?? 0 };\n });\n\n const totalCount = rows.reduce((sum, row) => sum + row.count, 0);\n const maxCount = rows.reduce((max, row) => Math.max(max, row.count), 0);\n const average = averageRating ?? getWeightedAverage(rows);\n const averageLabel = average.toFixed(1);\n\n const headerLabel = formatAverageLabel\n ? formatAverageLabel(averageLabel, maxRating, totalCount)\n : `Average rating ${averageLabel} out of ${maxRating}, ${formatCount(totalCount)}`;\n\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-4\", className)} {...props}>\n <div className=\"flex items-center gap-1\" role=\"img\" aria-label={headerLabel}>\n <StarIcon size={24} filled className=\"shrink-0 text-brand-primary-default\" />\n <span className=\"flex items-center gap-1.5\">\n <span className=\"typography-body-small-14px-semibold text-content-primary\">\n {averageLabel}\n </span>\n <span className=\"typography-body-small-14px-regular text-content-tertiary\">\n ({formatCount(totalCount)})\n </span>\n </span>\n </div>\n\n <ul className=\"m-0 flex list-none flex-col gap-4 p-0\" aria-label=\"Rating breakdown\">\n {rows.map(({ rating, count }) => (\n <li key={rating}>\n <div\n className=\"flex items-center gap-4\"\n role=\"img\"\n aria-label={\n formatRatingLabel\n ? formatRatingLabel(rating, count)\n : `${rating} ${rating === 1 ? \"star\" : \"stars\"}, ${formatCount(count)}`\n }\n >\n <div className=\"relative h-4 flex-1 overflow-hidden rounded-full bg-neutral-alphas-50\">\n <div\n className=\"absolute inset-y-0 left-0 rounded-full bg-brand-primary-default transition-[width] duration-300 ease-in-out\"\n style={{ width: maxCount === 0 ? \"0%\" : `${(count / maxCount) * 100}%` }}\n />\n </div>\n <span className=\"flex shrink-0 items-center gap-1.5\">\n <span className=\"typography-body-small-14px-semibold text-content-primary\">\n {rating}\n </span>\n <span className=\"typography-body-small-14px-regular text-content-tertiary\">\n ({formatCount(count)})\n </span>\n </span>\n </div>\n </li>\n ))}\n </ul>\n </div>\n );\n },\n);\n\nRatingSummary.displayName = \"RatingSummary\";\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RatingSummary.mjs","sources":["../../../src/components/RatingSummary/RatingSummary.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { StarIcon } from \"../Icons/StarIcon\";\n\n/** A single rating value paired with the number of reviews that gave it. */\nexport interface RatingCount {\n /** The rating value this entry represents (e.g. `5`). */\n rating: number;\n /** Number of reviews that gave this rating. */\n count: number;\n}\n\nexport interface RatingSummaryProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n /**\n * Per-rating review counts. Order is not significant — rows always render from\n * `maxRating` down to `1`, and any rating without an entry renders as zero.\n * Entries are expected to use whole-number ratings within `1`–`maxRating`;\n * any outside that range are ignored, including in the derived total and average.\n */\n distribution: RatingCount[];\n /**\n * Average rating shown in the header. Defaults to the count-weighted mean of\n * `distribution`; provide it to display a server-computed average instead.\n */\n averageRating?: number;\n /** Highest possible rating. Sets how many histogram rows render. @default 5 */\n maxRating?: number;\n /**\n * Formats a review count into its label, without surrounding parentheses.\n * Used for the visible text and as the count portion of the default\n * accessible labels.\n * @default ``(count) => `${count.toLocaleString()} review(s)` ``\n */\n formatCount?: (count: number) => string;\n /**\n * Builds the accessible label for the header. Override to localise the\n * screen-reader text. `average` is the already-formatted average string.\n * @default ``(average, max, total) => `Average rating ${average} out of ${max}, ${formatCount(total)}` ``\n */\n formatAverageLabel?: (average: string, maxRating: number, total: number) => string;\n /**\n * Builds the accessible label for each histogram row. Override to localise the\n * screen-reader text.\n * @default ``(rating, count) => `${rating} star(s), ${formatCount(count)}` ``\n */\n formatRatingLabel?: (rating: number, count: number) => string;\n}\n\nfunction defaultFormatCount(count: number): string {\n return `${count.toLocaleString()} ${count === 1 ? \"review\" : \"reviews\"}`;\n}\n\nfunction getWeightedAverage(distribution: RatingCount[]): number {\n let total = 0;\n let weighted = 0;\n for (const { rating, count } of distribution) {\n total += count;\n weighted += rating * count;\n }\n return total === 0 ? 0 : weighted / total;\n}\n\n/**\n * An aggregated rating overview: a headline average with the total review count,\n * followed by a per-rating histogram. Bar lengths are scaled relative to the\n * most-reviewed rating, so the busiest rating always fills the track.\n *\n * The histogram is one shared grid (with subgrid rows) rather than per-row\n * flex: the label column is sized by the widest label across all rows, so a\n * width difference between labels (e.g. \"1 review\" vs \"3 reviews\") never\n * changes a row's track length — every bar's right edge stays aligned.\n *\n * The brand star is decorative; the header and each histogram row expose\n * `role=\"img\"` with an accessible label (override via `formatAverageLabel` /\n * `formatRatingLabel`) so the single-star-plus-number reads clearly.\n *\n * @example\n * ```tsx\n * <RatingSummary\n * distribution={[\n * { rating: 5, count: 300 },\n * { rating: 4, count: 20 },\n * { rating: 3, count: 20 },\n * { rating: 2, count: 10 },\n * { rating: 1, count: 0 },\n * ]}\n * />\n * ```\n */\nexport const RatingSummary = React.forwardRef<HTMLDivElement, RatingSummaryProps>(\n (\n {\n distribution,\n averageRating,\n maxRating = 5,\n formatCount = defaultFormatCount,\n formatAverageLabel,\n formatRatingLabel,\n className,\n ...props\n },\n ref,\n ) => {\n const countByRating = new Map<number, number>();\n for (const { rating, count } of distribution) {\n countByRating.set(rating, (countByRating.get(rating) ?? 0) + count);\n }\n\n const rows = Array.from({ length: maxRating }, (_, index) => {\n const rating = maxRating - index;\n return { rating, count: countByRating.get(rating) ?? 0 };\n });\n\n const totalCount = rows.reduce((sum, row) => sum + row.count, 0);\n const maxCount = rows.reduce((max, row) => Math.max(max, row.count), 0);\n const average = averageRating ?? getWeightedAverage(rows);\n const averageLabel = average.toFixed(1);\n\n const headerLabel = formatAverageLabel\n ? formatAverageLabel(averageLabel, maxRating, totalCount)\n : `Average rating ${averageLabel} out of ${maxRating}, ${formatCount(totalCount)}`;\n\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-4\", className)} {...props}>\n <div className=\"flex items-center gap-1\" role=\"img\" aria-label={headerLabel}>\n <StarIcon size={24} filled className=\"shrink-0 text-brand-primary-default\" />\n <span className=\"flex items-center gap-1.5\">\n <span className=\"typography-body-small-14px-semibold text-content-primary\">\n {averageLabel}\n </span>\n <span className=\"typography-body-small-14px-regular text-content-tertiary\">\n ({formatCount(totalCount)})\n </span>\n </span>\n </div>\n\n <ul\n className=\"m-0 grid list-none grid-cols-[1fr_auto] gap-4 p-0\"\n aria-label=\"Rating breakdown\"\n >\n {rows.map(({ rating, count }) => (\n <li key={rating} className=\"col-span-2 grid grid-cols-subgrid\">\n <div\n className=\"col-span-2 grid grid-cols-subgrid items-center\"\n role=\"img\"\n aria-label={\n formatRatingLabel\n ? formatRatingLabel(rating, count)\n : `${rating} ${rating === 1 ? \"star\" : \"stars\"}, ${formatCount(count)}`\n }\n >\n <div className=\"relative h-4 overflow-hidden rounded-full bg-neutral-alphas-50\">\n <div\n className=\"absolute inset-y-0 left-0 rounded-full bg-brand-primary-default transition-[width] duration-300 ease-in-out\"\n style={{ width: maxCount === 0 ? \"0%\" : `${(count / maxCount) * 100}%` }}\n />\n </div>\n <span className=\"flex items-center gap-1.5\">\n <span className=\"typography-body-small-14px-semibold text-content-primary\">\n {rating}\n </span>\n <span className=\"typography-body-small-14px-regular text-content-tertiary\">\n ({formatCount(count)})\n </span>\n </span>\n </div>\n </li>\n ))}\n </ul>\n </div>\n );\n },\n);\n\nRatingSummary.displayName = \"RatingSummary\";\n"],"names":[],"mappings":";;;;;AAgDA,SAAS,mBAAmB,OAAuB;AACjD,SAAO,GAAG,MAAM,gBAAgB,IAAI,UAAU,IAAI,WAAW,SAAS;AACxE;AAEA,SAAS,mBAAmB,cAAqC;AAC/D,MAAI,QAAQ;AACZ,MAAI,WAAW;AACf,aAAW,EAAE,QAAQ,MAAA,KAAW,cAAc;AAC5C,aAAS;AACT,gBAAY,SAAS;AAAA,EACvB;AACA,SAAO,UAAU,IAAI,IAAI,WAAW;AACtC;AA6BO,MAAM,gBAAgB,MAAM;AAAA,EACjC,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,oCAAoB,IAAA;AAC1B,eAAW,EAAE,QAAQ,MAAA,KAAW,cAAc;AAC5C,oBAAc,IAAI,SAAS,cAAc,IAAI,MAAM,KAAK,KAAK,KAAK;AAAA,IACpE;AAEA,UAAM,OAAO,MAAM,KAAK,EAAE,QAAQ,UAAA,GAAa,CAAC,GAAG,UAAU;AAC3D,YAAM,SAAS,YAAY;AAC3B,aAAO,EAAE,QAAQ,OAAO,cAAc,IAAI,MAAM,KAAK,EAAA;AAAA,IACvD,CAAC;AAED,UAAM,aAAa,KAAK,OAAO,CAAC,KAAK,QAAQ,MAAM,IAAI,OAAO,CAAC;AAC/D,UAAM,WAAW,KAAK,OAAO,CAAC,KAAK,QAAQ,KAAK,IAAI,KAAK,IAAI,KAAK,GAAG,CAAC;AACtE,UAAM,UAAU,iBAAiB,mBAAmB,IAAI;AACxD,UAAM,eAAe,QAAQ,QAAQ,CAAC;AAEtC,UAAM,cAAc,qBAChB,mBAAmB,cAAc,WAAW,UAAU,IACtD,kBAAkB,YAAY,WAAW,SAAS,KAAK,YAAY,UAAU,CAAC;AAElF,WACE,qBAAC,SAAI,KAAU,WAAW,GAAG,uBAAuB,SAAS,GAAI,GAAG,OAClE,UAAA;AAAA,MAAA,qBAAC,SAAI,WAAU,2BAA0B,MAAK,OAAM,cAAY,aAC9D,UAAA;AAAA,QAAA,oBAAC,YAAS,MAAM,IAAI,QAAM,MAAC,WAAU,uCAAsC;AAAA,QAC3E,qBAAC,QAAA,EAAK,WAAU,6BACd,UAAA;AAAA,UAAA,oBAAC,QAAA,EAAK,WAAU,4DACb,UAAA,cACH;AAAA,UACA,qBAAC,QAAA,EAAK,WAAU,4DAA2D,UAAA;AAAA,YAAA;AAAA,YACvE,YAAY,UAAU;AAAA,YAAE;AAAA,UAAA,EAAA,CAC5B;AAAA,QAAA,EAAA,CACF;AAAA,MAAA,GACF;AAAA,MAEA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAU;AAAA,UACV,cAAW;AAAA,UAEV,UAAA,KAAK,IAAI,CAAC,EAAE,QAAQ,YACnB,oBAAC,MAAA,EAAgB,WAAU,qCACzB,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAU;AAAA,cACV,MAAK;AAAA,cACL,cACE,oBACI,kBAAkB,QAAQ,KAAK,IAC/B,GAAG,MAAM,IAAI,WAAW,IAAI,SAAS,OAAO,KAAK,YAAY,KAAK,CAAC;AAAA,cAGzE,UAAA;AAAA,gBAAA,oBAAC,OAAA,EAAI,WAAU,kEACb,UAAA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,WAAU;AAAA,oBACV,OAAO,EAAE,OAAO,aAAa,IAAI,OAAO,GAAI,QAAQ,WAAY,GAAG,IAAA;AAAA,kBAAI;AAAA,gBAAA,GAE3E;AAAA,gBACA,qBAAC,QAAA,EAAK,WAAU,6BACd,UAAA;AAAA,kBAAA,oBAAC,QAAA,EAAK,WAAU,4DACb,UAAA,QACH;AAAA,kBACA,qBAAC,QAAA,EAAK,WAAU,4DAA2D,UAAA;AAAA,oBAAA;AAAA,oBACvE,YAAY,KAAK;AAAA,oBAAE;AAAA,kBAAA,EAAA,CACvB;AAAA,gBAAA,EAAA,CACF;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA,EACF,GAxBO,MAyBT,CACD;AAAA,QAAA;AAAA,MAAA;AAAA,IACH,GACF;AAAA,EAEJ;AACF;AAEA,cAAc,cAAc;"}
@@ -59,8 +59,8 @@ function TextAreaHelperText({
59
59
  {
60
60
  id,
61
61
  className: cn(
62
- "typography-description-12px-regular px-2 pt-1 pb-0.5",
63
- error ? "text-error-content" : "text-content-secondary"
62
+ "typography-description-12px-regular pt-2",
63
+ error ? "text-error-content" : "text-content-tertiary"
64
64
  ),
65
65
  children
66
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TextArea.mjs","sources":["../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\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. Note: `onChange` is also called with an empty value when clearing. */\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 /** Whether the textarea can be resized by the user. @default true */\n resizable?: boolean;\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-default-16px-regular autofill-body-lg\",\n \"40\": \"py-2 typography-body-default-16px-regular autofill-body-lg\",\n \"32\": \"py-2 typography-body-small-14px-regular autofill-body-md\",\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-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n CONTAINER_MIN_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction getTextareaClassName(\n size: TextAreaSize,\n hasClearButton: boolean,\n hasMinRows: boolean,\n resizable: boolean,\n) {\n return cn(\n \"h-full w-full bg-transparent text-content-primary no-underline placeholder:text-content-tertiary focus:outline-none disabled:cursor-not-allowed\",\n resizable ? \"resize-y\" : \"resize-none\",\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-description-12px-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-content\" : \"text-content-secondary\",\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 useTextAreaValue(\n value: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"value\"],\n defaultValue: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"defaultValue\"],\n showClearButton: boolean,\n onChange?: React.ChangeEventHandler<HTMLTextAreaElement>,\n onClear?: () => void,\n textareaRef?: React.RefObject<HTMLTextAreaElement | null>,\n) {\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setInternalValue(e.target.value);\n onChange?.(e);\n };\n\n const handleClear = () => {\n setInternalValue(\"\");\n\n if (onChange && textareaRef?.current) {\n const syntheticEvent = {\n target: { ...textareaRef.current, value: \"\" },\n currentTarget: { ...textareaRef.current, value: \"\" },\n } as React.ChangeEvent<HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n\n onClear?.();\n };\n\n return {\n resolvedValue,\n displayValue: showClearButton ? resolvedValue : value,\n resolvedDefaultValue: showClearButton ? undefined : defaultValue,\n handleChange,\n handleClear,\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 defaultValue,\n onChange,\n minRows,\n maxRows,\n resizable = 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 const maxHeight = calculateMaxHeight(size, maxRows);\n\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n\n const { resolvedValue, displayValue, resolvedDefaultValue, handleChange, handleClear } =\n useTextAreaValue(value, defaultValue, showClearButton, onChange, onClear, internalRef);\n\n const mergedRef = (node: HTMLTextAreaElement | null) => {\n internalRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n }\n };\n\n const showClear = showClearButton && resolvedValue !== \"\" && !disabled;\n const showValidated = validated && !showClear;\n const ariaDescribedBy = bottomText ? helperTextId : undefined;\n const textareaStyle = maxHeight ? { maxHeight } : undefined;\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-description-12px-semibold pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n <div className={getContainerClassName(size, error, disabled)}>\n <textarea\n ref={mergedRef}\n id={inputId}\n disabled={disabled}\n aria-describedby={ariaDescribedBy}\n aria-invalid={error || undefined}\n className={getTextareaClassName(size, showClear, !!minRows, resizable)}\n value={displayValue}\n defaultValue={resolvedDefaultValue}\n onChange={handleChange}\n rows={minRows}\n style={textareaStyle}\n {...props}\n />\n\n {showClear && (\n <IconButton\n variant=\"tertiary\"\n size=\"24\"\n icon={<CloseIcon />}\n aria-label=\"Clear text\"\n onClick={handleClear}\n className={cn(\n \"absolute flex size-5 items-center justify-center self-end\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n />\n )}\n {showValidated && (\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-content\" />\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":[],"mappings":"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"}
1
+ {"version":3,"file":"TextArea.mjs","sources":["../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\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. Note: `onChange` is also called with an empty value when clearing. */\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 /** Whether the textarea can be resized by the user. @default true */\n resizable?: boolean;\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-default-16px-regular autofill-body-lg\",\n \"40\": \"py-2 typography-body-default-16px-regular autofill-body-lg\",\n \"32\": \"py-2 typography-body-small-14px-regular autofill-body-md\",\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-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n CONTAINER_MIN_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction getTextareaClassName(\n size: TextAreaSize,\n hasClearButton: boolean,\n hasMinRows: boolean,\n resizable: boolean,\n) {\n return cn(\n \"h-full w-full bg-transparent text-content-primary no-underline placeholder:text-content-tertiary focus:outline-none disabled:cursor-not-allowed\",\n resizable ? \"resize-y\" : \"resize-none\",\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-description-12px-regular pt-2\",\n error ? \"text-error-content\" : \"text-content-tertiary\",\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 useTextAreaValue(\n value: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"value\"],\n defaultValue: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"defaultValue\"],\n showClearButton: boolean,\n onChange?: React.ChangeEventHandler<HTMLTextAreaElement>,\n onClear?: () => void,\n textareaRef?: React.RefObject<HTMLTextAreaElement | null>,\n) {\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setInternalValue(e.target.value);\n onChange?.(e);\n };\n\n const handleClear = () => {\n setInternalValue(\"\");\n\n if (onChange && textareaRef?.current) {\n const syntheticEvent = {\n target: { ...textareaRef.current, value: \"\" },\n currentTarget: { ...textareaRef.current, value: \"\" },\n } as React.ChangeEvent<HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n\n onClear?.();\n };\n\n return {\n resolvedValue,\n displayValue: showClearButton ? resolvedValue : value,\n resolvedDefaultValue: showClearButton ? undefined : defaultValue,\n handleChange,\n handleClear,\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 defaultValue,\n onChange,\n minRows,\n maxRows,\n resizable = 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 const maxHeight = calculateMaxHeight(size, maxRows);\n\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n\n const { resolvedValue, displayValue, resolvedDefaultValue, handleChange, handleClear } =\n useTextAreaValue(value, defaultValue, showClearButton, onChange, onClear, internalRef);\n\n const mergedRef = (node: HTMLTextAreaElement | null) => {\n internalRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n }\n };\n\n const showClear = showClearButton && resolvedValue !== \"\" && !disabled;\n const showValidated = validated && !showClear;\n const ariaDescribedBy = bottomText ? helperTextId : undefined;\n const textareaStyle = maxHeight ? { maxHeight } : undefined;\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-description-12px-semibold pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n <div className={getContainerClassName(size, error, disabled)}>\n <textarea\n ref={mergedRef}\n id={inputId}\n disabled={disabled}\n aria-describedby={ariaDescribedBy}\n aria-invalid={error || undefined}\n className={getTextareaClassName(size, showClear, !!minRows, resizable)}\n value={displayValue}\n defaultValue={resolvedDefaultValue}\n onChange={handleChange}\n rows={minRows}\n style={textareaStyle}\n {...props}\n />\n\n {showClear && (\n <IconButton\n variant=\"tertiary\"\n size=\"24\"\n icon={<CloseIcon />}\n aria-label=\"Clear text\"\n onClick={handleClear}\n className={cn(\n \"absolute flex size-5 items-center justify-center self-end\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n />\n )}\n {showValidated && (\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-content\" />\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":[],"mappings":"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"}
@@ -14,6 +14,11 @@ const CONTAINER_PADDING_X = {
14
14
  "40": "px-4",
15
15
  "32": "px-3"
16
16
  };
17
+ const CONTAINER_PADDING_X_WITH_ACTION = {
18
+ "48": "pl-4 pr-2",
19
+ "40": "pl-4 pr-2",
20
+ "32": "pl-3 pr-2"
21
+ };
17
22
  const CONTAINER_GAP = {
18
23
  "48": "gap-2.5",
19
24
  "40": "gap-2.5",
@@ -29,10 +34,10 @@ const SIDE_LABEL_TYPOGRAPHY = {
29
34
  "40": "typography-body-default-16px-regular",
30
35
  "32": "typography-body-small-14px-regular"
31
36
  };
32
- function getContainerClassName(size, error, disabled) {
37
+ function getContainerClassName(size, error, disabled, hasAction) {
33
38
  return cn(
34
39
  "relative flex items-center overflow-hidden rounded-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors",
35
- CONTAINER_PADDING_X[size],
40
+ hasAction ? CONTAINER_PADDING_X_WITH_ACTION[size] : CONTAINER_PADDING_X[size],
36
41
  CONTAINER_GAP[size],
37
42
  error ? "border-error-content" : "border-border-primary",
38
43
  !disabled && !error && "hover:border-neutral-alphas-400",
@@ -84,8 +89,8 @@ function TextFieldHelperText({
84
89
  {
85
90
  id,
86
91
  className: cn(
87
- "typography-description-12px-regular px-2 pt-2 pb-0.5",
88
- error ? "text-error-content" : "text-content-secondary"
92
+ "typography-description-12px-regular pt-2",
93
+ error ? "text-error-content" : "text-content-tertiary"
89
94
  ),
90
95
  children
91
96
  }
@@ -112,6 +117,7 @@ const TextField = React.forwardRef(
112
117
  rightIcon,
113
118
  leftLabel,
114
119
  rightLabel,
120
+ action,
115
121
  className,
116
122
  id,
117
123
  disabled,
@@ -165,7 +171,7 @@ const TextField = React.forwardRef(
165
171
  /* @__PURE__ */ jsxs(
166
172
  "div",
167
173
  {
168
- className: getContainerClassName(size, error, disabled),
174
+ className: getContainerClassName(size, error, disabled, action != null),
169
175
  onMouseDown: handleContainerMouseDown,
170
176
  children: [
171
177
  /* @__PURE__ */ jsx(LeadingIcon, { children: leftIcon }),
@@ -187,7 +193,8 @@ const TextField = React.forwardRef(
187
193
  }
188
194
  ),
189
195
  /* @__PURE__ */ jsx(SideLabel, { id: rightLabelId, size, align: "right", children: rightLabel }),
190
- /* @__PURE__ */ jsx(TrailingAdornment, { rightIcon, validated })
196
+ /* @__PURE__ */ jsx(TrailingAdornment, { rightIcon, validated }),
197
+ action != null && /* @__PURE__ */ jsx("span", { "data-tf-interactive": "", className: "flex shrink-0 items-center", children: action })
191
198
  ]
192
199
  }
193
200
  ),
@@ -1 +1 @@
1
- {"version":3,"file":"TextField.mjs","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 /** Fixed, non-editable label pinned inside the left edge of the field — for a prefix such as a currency symbol or country code. */\n leftLabel?: React.ReactNode;\n /** Fixed, non-editable label pinned inside the right edge of the field — for a unit or suffix such as a currency code or domain. */\n rightLabel?: 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 CONTAINER_PADDING_X: Record<TextFieldSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst CONTAINER_GAP: Record<TextFieldSize, string> = {\n \"48\": \"gap-2.5\",\n \"40\": \"gap-2.5\",\n \"32\": \"gap-2\",\n};\n\nconst INPUT_TYPOGRAPHY: Record<TextFieldSize, string> = {\n \"48\": \"typography-body-default-16px-regular autofill-body-lg\",\n \"40\": \"typography-body-default-16px-regular autofill-body-lg\",\n \"32\": \"typography-body-small-14px-regular autofill-body-md\",\n};\n\nconst SIDE_LABEL_TYPOGRAPHY: Record<TextFieldSize, string> = {\n \"48\": \"typography-body-default-16px-regular\",\n \"40\": \"typography-body-default-16px-regular\",\n \"32\": \"typography-body-small-14px-regular\",\n};\n\nfunction getContainerClassName(size: TextFieldSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative flex items-center overflow-hidden rounded-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n CONTAINER_PADDING_X[size],\n CONTAINER_GAP[size],\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n CONTAINER_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction LeadingIcon({ children }: { children?: React.ReactNode }) {\n if (!children) return null;\n return (\n <span className=\"pointer-events-none flex size-4 shrink-0 items-center justify-center text-content-secondary\">\n {children}\n </span>\n );\n}\n\nfunction SideLabel({\n id,\n size,\n align,\n children,\n}: {\n id?: string;\n size: TextFieldSize;\n align: \"left\" | \"right\";\n children?: React.ReactNode;\n}) {\n if (!children) return null;\n return (\n <span\n id={id}\n className={cn(\n \"shrink-0 select-none whitespace-nowrap text-content-tertiary\",\n align === \"right\" && \"text-right\",\n SIDE_LABEL_TYPOGRAPHY[size],\n )}\n >\n {children}\n </span>\n );\n}\n\nfunction TrailingAdornment({\n rightIcon,\n validated,\n}: {\n rightIcon?: React.ReactNode;\n validated: boolean;\n}) {\n if (!rightIcon && !validated) return null;\n return (\n <span className=\"flex shrink-0 items-center gap-2 text-content-secondary\">\n {rightIcon && (\n <span data-tf-interactive=\"\" className=\"flex size-4 shrink-0 items-center justify-center\">\n {rightIcon}\n </span>\n )}\n {validated && (\n <span className=\"pointer-events-none flex size-4 shrink-0 items-center justify-center\">\n <CheckOutlineIcon className=\"text-success-content\" />\n </span>\n )}\n </span>\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-description-12px-regular px-2 pt-2 pb-0.5\",\n error ? \"text-error-content\" : \"text-content-secondary\",\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, icon slots, and side labels.\n *\n * Use `leftLabel` / `rightLabel` for fixed unit or prefix affordances (currency symbol,\n * country code, domain suffix). Provide at least one of `label`, `aria-label`, or\n * `aria-labelledby` for 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 *\n * @example\n * ```tsx\n * <TextField label=\"Price\" leftLabel=\"$\" rightLabel=\"USD\" placeholder=\"0.00\" />\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 leftLabel,\n rightLabel,\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 leftLabelId = `${inputId}-left-label`;\n const rightLabelId = `${inputId}-right-label`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n const describedBy =\n [\n leftLabel != null ? leftLabelId : null,\n rightLabel != null ? rightLabelId : null,\n bottomText ? helperTextId : null,\n ]\n .filter(Boolean)\n .join(\" \") || undefined;\n\n const innerRef = React.useRef<HTMLInputElement>(null);\n const setRefs = React.useCallback(\n (node: HTMLInputElement | null) => {\n innerRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = node;\n },\n [ref],\n );\n\n // Keep clicks on the non-interactive adornments (icons, side labels, padding)\n // focusing the input, matching the old absolute/pointer-events-none layout.\n const handleContainerMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {\n if (disabled) return;\n const target = event.target as HTMLElement;\n if (target === innerRef.current) return;\n if (target.closest(\"[data-tf-interactive]\")) return;\n event.preventDefault();\n innerRef.current?.focus();\n };\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-description-12px-semibold pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n {/* biome-ignore lint/a11y/noStaticElementInteractions: focus bridge delegates pointer clicks on adornments to the input */}\n <div\n className={getContainerClassName(size, error, disabled)}\n onMouseDown={handleContainerMouseDown}\n >\n <LeadingIcon>{leftIcon}</LeadingIcon>\n <SideLabel id={leftLabelId} size={size} align=\"left\">\n {leftLabel}\n </SideLabel>\n\n <input\n ref={setRefs}\n id={inputId}\n disabled={disabled}\n aria-describedby={describedBy}\n aria-invalid={error || undefined}\n className={cn(\n \"h-full min-w-0 flex-1 bg-transparent text-content-primary no-underline placeholder:text-content-tertiary focus:outline-none disabled:cursor-not-allowed\",\n INPUT_TYPOGRAPHY[size],\n \"[&[type='search']::-webkit-search-cancel-button]:hidden [&[type='search']::-webkit-search-cancel-button]:appearance-none\",\n )}\n {...props}\n />\n\n <SideLabel id={rightLabelId} size={size} align=\"right\">\n {rightLabel}\n </SideLabel>\n <TrailingAdornment rightIcon={rightIcon} validated={validated} />\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":[],"mappings":"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"}
1
+ {"version":3,"file":"TextField.mjs","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 /** Fixed, non-editable label pinned inside the left edge of the field — for a prefix such as a currency symbol or country code. */\n leftLabel?: React.ReactNode;\n /** Fixed, non-editable label pinned inside the right edge of the field — for a unit or suffix such as a currency code or domain. */\n rightLabel?: React.ReactNode;\n /**\n * Trailing interactive element pinned to the right edge — typically a `Chip`\n * or `Button` (the \"with button\" field type). Reduces the right padding so the\n * control sits flush, and clicks on it do not steal focus from the input.\n */\n action?: 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 CONTAINER_PADDING_X: Record<TextFieldSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst CONTAINER_PADDING_X_WITH_ACTION: Record<TextFieldSize, string> = {\n \"48\": \"pl-4 pr-2\",\n \"40\": \"pl-4 pr-2\",\n \"32\": \"pl-3 pr-2\",\n};\n\nconst CONTAINER_GAP: Record<TextFieldSize, string> = {\n \"48\": \"gap-2.5\",\n \"40\": \"gap-2.5\",\n \"32\": \"gap-2\",\n};\n\nconst INPUT_TYPOGRAPHY: Record<TextFieldSize, string> = {\n \"48\": \"typography-body-default-16px-regular autofill-body-lg\",\n \"40\": \"typography-body-default-16px-regular autofill-body-lg\",\n \"32\": \"typography-body-small-14px-regular autofill-body-md\",\n};\n\nconst SIDE_LABEL_TYPOGRAPHY: Record<TextFieldSize, string> = {\n \"48\": \"typography-body-default-16px-regular\",\n \"40\": \"typography-body-default-16px-regular\",\n \"32\": \"typography-body-small-14px-regular\",\n};\n\nfunction getContainerClassName(\n size: TextFieldSize,\n error: boolean,\n disabled?: boolean,\n hasAction?: boolean,\n) {\n return cn(\n \"relative flex items-center overflow-hidden rounded-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n hasAction ? CONTAINER_PADDING_X_WITH_ACTION[size] : CONTAINER_PADDING_X[size],\n CONTAINER_GAP[size],\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n CONTAINER_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction LeadingIcon({ children }: { children?: React.ReactNode }) {\n if (!children) return null;\n return (\n <span className=\"pointer-events-none flex size-4 shrink-0 items-center justify-center text-content-secondary\">\n {children}\n </span>\n );\n}\n\nfunction SideLabel({\n id,\n size,\n align,\n children,\n}: {\n id?: string;\n size: TextFieldSize;\n align: \"left\" | \"right\";\n children?: React.ReactNode;\n}) {\n if (!children) return null;\n return (\n <span\n id={id}\n className={cn(\n \"shrink-0 select-none whitespace-nowrap text-content-tertiary\",\n align === \"right\" && \"text-right\",\n SIDE_LABEL_TYPOGRAPHY[size],\n )}\n >\n {children}\n </span>\n );\n}\n\nfunction TrailingAdornment({\n rightIcon,\n validated,\n}: {\n rightIcon?: React.ReactNode;\n validated: boolean;\n}) {\n if (!rightIcon && !validated) return null;\n return (\n <span className=\"flex shrink-0 items-center gap-2 text-content-secondary\">\n {rightIcon && (\n <span data-tf-interactive=\"\" className=\"flex size-4 shrink-0 items-center justify-center\">\n {rightIcon}\n </span>\n )}\n {validated && (\n <span className=\"pointer-events-none flex size-4 shrink-0 items-center justify-center\">\n <CheckOutlineIcon className=\"text-success-content\" />\n </span>\n )}\n </span>\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-description-12px-regular pt-2\",\n error ? \"text-error-content\" : \"text-content-tertiary\",\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, icon slots, and side labels.\n *\n * Use `leftLabel` / `rightLabel` for fixed unit or prefix affordances (currency symbol,\n * country code, domain suffix). Provide at least one of `label`, `aria-label`, or\n * `aria-labelledby` for 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 *\n * @example\n * ```tsx\n * <TextField label=\"Price\" leftLabel=\"$\" rightLabel=\"USD\" placeholder=\"0.00\" />\n * ```\n *\n * @example\n * ```tsx\n * <TextField\n * label=\"Promo code\"\n * placeholder=\"Enter code\"\n * action={<Chip size=\"32\" onClick={apply}>Apply</Chip>}\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 leftLabel,\n rightLabel,\n action,\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 leftLabelId = `${inputId}-left-label`;\n const rightLabelId = `${inputId}-right-label`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n const describedBy =\n [\n leftLabel != null ? leftLabelId : null,\n rightLabel != null ? rightLabelId : null,\n bottomText ? helperTextId : null,\n ]\n .filter(Boolean)\n .join(\" \") || undefined;\n\n const innerRef = React.useRef<HTMLInputElement>(null);\n const setRefs = React.useCallback(\n (node: HTMLInputElement | null) => {\n innerRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = node;\n },\n [ref],\n );\n\n // Keep clicks on the non-interactive adornments (icons, side labels, padding)\n // focusing the input, matching the old absolute/pointer-events-none layout.\n const handleContainerMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {\n if (disabled) return;\n const target = event.target as HTMLElement;\n if (target === innerRef.current) return;\n if (target.closest(\"[data-tf-interactive]\")) return;\n event.preventDefault();\n innerRef.current?.focus();\n };\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-description-12px-semibold pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n {/* biome-ignore lint/a11y/noStaticElementInteractions: focus bridge delegates pointer clicks on adornments to the input */}\n <div\n className={getContainerClassName(size, error, disabled, action != null)}\n onMouseDown={handleContainerMouseDown}\n >\n <LeadingIcon>{leftIcon}</LeadingIcon>\n <SideLabel id={leftLabelId} size={size} align=\"left\">\n {leftLabel}\n </SideLabel>\n\n <input\n ref={setRefs}\n id={inputId}\n disabled={disabled}\n aria-describedby={describedBy}\n aria-invalid={error || undefined}\n className={cn(\n \"h-full min-w-0 flex-1 bg-transparent text-content-primary no-underline placeholder:text-content-tertiary focus:outline-none disabled:cursor-not-allowed\",\n INPUT_TYPOGRAPHY[size],\n \"[&[type='search']::-webkit-search-cancel-button]:hidden [&[type='search']::-webkit-search-cancel-button]:appearance-none\",\n )}\n {...props}\n />\n\n <SideLabel id={rightLabelId} size={size} align=\"right\">\n {rightLabel}\n </SideLabel>\n <TrailingAdornment rightIcon={rightIcon} validated={validated} />\n {action != null && (\n <span data-tf-interactive=\"\" className=\"flex shrink-0 items-center\">\n {action}\n </span>\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":[],"mappings":"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"}
@@ -0,0 +1,288 @@
1
+ "use client";
2
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { resamplePeaks, formatTime } from "../../utils/audioWaveform.mjs";
5
+ import { cn } from "../../utils/cn.mjs";
6
+ import { useAudioPlayback } from "../../utils/useAudioPlayback.mjs";
7
+ import { useFittedBarCount } from "../../utils/useFittedBarCount.mjs";
8
+ import { useWaveformPeaks } from "../../utils/useWaveformPeaks.mjs";
9
+ import { useWaveformSeek } from "../../utils/useWaveformSeek.mjs";
10
+ import { IconButton } from "../IconButton/IconButton.mjs";
11
+ import { CloseIcon } from "../Icons/CloseIcon.mjs";
12
+ import { PauseIcon } from "../Icons/PauseIcon.mjs";
13
+ import { PlayIcon } from "../Icons/PlayIcon.mjs";
14
+ const DEFAULT_WAVEFORM = [
15
+ 0.15,
16
+ 0.46,
17
+ 0.85,
18
+ 0.23,
19
+ 1,
20
+ 0.85,
21
+ 0.62,
22
+ 0.62,
23
+ 0.62,
24
+ 0.85,
25
+ 0.85,
26
+ 0.62,
27
+ 0.62,
28
+ 1,
29
+ 1,
30
+ 0.62,
31
+ 0.62,
32
+ 0.62,
33
+ 0.23,
34
+ 0.23,
35
+ 0.15,
36
+ 0.46,
37
+ 0.85,
38
+ 0.23,
39
+ 1,
40
+ 0.46,
41
+ 0.85,
42
+ 0.62,
43
+ 0.23,
44
+ 0.23,
45
+ 0.62,
46
+ 0.62,
47
+ 0.85,
48
+ 0.85,
49
+ 0.62,
50
+ 0.62,
51
+ 0.85,
52
+ 1,
53
+ 0.85,
54
+ 1,
55
+ 0.62,
56
+ 0.62,
57
+ 0.62,
58
+ 0.23,
59
+ 0.23,
60
+ 0.62,
61
+ 0.23,
62
+ 0.23,
63
+ 0.23,
64
+ 0.62,
65
+ 0.85,
66
+ 0.62,
67
+ 0.85,
68
+ 1,
69
+ 0.85,
70
+ 0.85,
71
+ 1,
72
+ 0.85,
73
+ 0.23,
74
+ 0.23,
75
+ 0.23,
76
+ 0.46,
77
+ 0.62,
78
+ 0.23,
79
+ 0.23
80
+ ];
81
+ const MIN_BAR_HEIGHT = 4;
82
+ const MAX_BAR_HEIGHT = {
83
+ default: 26,
84
+ small: 16
85
+ };
86
+ const CONTROL_SIZE = {
87
+ default: "48",
88
+ small: "32"
89
+ };
90
+ const BAR_WIDTH_PX = 4;
91
+ const BAR_GAP_PX = 4;
92
+ const DEFAULT_BAR_COUNT = 40;
93
+ function activeBarClass(negative) {
94
+ return negative ? "bg-messages-waveform-listening-negative-active" : "bg-messages-waveform-listening-active";
95
+ }
96
+ function inactiveBarClass(negative) {
97
+ return negative ? "bg-messages-waveform-listening-negative-inactive" : "bg-messages-waveform-listening-inactive";
98
+ }
99
+ function restingBarClass(negative) {
100
+ return negative ? "bg-messages-waveform-listening-negative-default" : "bg-messages-waveform-default";
101
+ }
102
+ function WaveformBars({ bars, variant, size, negative, progress }) {
103
+ const hasProgress = progress !== void 0;
104
+ const activeCount = hasProgress ? Math.round(progress * bars.length) : 0;
105
+ const maxBarHeight = MAX_BAR_HEIGHT[size];
106
+ return /* @__PURE__ */ jsx(Fragment, { children: bars.map((amplitude, index) => {
107
+ const height = variant === "flat" ? MIN_BAR_HEIGHT : Math.max(MIN_BAR_HEIGHT, Math.round(amplitude * maxBarHeight));
108
+ const colorClass = !hasProgress ? restingBarClass(negative) : index < activeCount ? activeBarClass(negative) : inactiveBarClass(negative);
109
+ return /* @__PURE__ */ jsx(
110
+ "span",
111
+ {
112
+ className: cn("w-1 shrink-0 rounded-[2px]", colorClass),
113
+ style: { height }
114
+ },
115
+ index
116
+ );
117
+ }) });
118
+ }
119
+ function WaveformMeta({ time, fileName, textColor }) {
120
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
121
+ time && /* @__PURE__ */ jsx("span", { className: cn("typography-body-small-14px-regular shrink-0", textColor), children: time }),
122
+ fileName && /* @__PURE__ */ jsx(
123
+ "span",
124
+ {
125
+ className: cn(
126
+ "typography-body-small-14px-regular min-w-0 max-w-[50%] truncate",
127
+ textColor
128
+ ),
129
+ children: fileName
130
+ }
131
+ )
132
+ ] });
133
+ }
134
+ const VoiceNote = React.forwardRef(
135
+ ({
136
+ className,
137
+ src,
138
+ duration,
139
+ waveform = DEFAULT_WAVEFORM,
140
+ variant = "default",
141
+ size = "default",
142
+ negative = false,
143
+ progress,
144
+ playing,
145
+ defaultPlaying = false,
146
+ onPlayPause,
147
+ onEnded,
148
+ time,
149
+ fileName,
150
+ showControls = true,
151
+ showTimestamp = true,
152
+ showRemove = false,
153
+ onRemove,
154
+ playButtonLabel,
155
+ removeButtonLabel = "Remove",
156
+ "aria-label": ariaLabel = "Voice note",
157
+ ...props
158
+ }, ref) => {
159
+ const isPlayer = src !== void 0;
160
+ const playback = useAudioPlayback({
161
+ src,
162
+ duration,
163
+ playing,
164
+ defaultPlaying,
165
+ onEnded,
166
+ onPlay: React.useCallback(() => onPlayPause?.(true), [onPlayPause]),
167
+ onPause: React.useCallback(() => onPlayPause?.(false), [onPlayPause])
168
+ });
169
+ const decodedPeaks = useWaveformPeaks(src);
170
+ const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);
171
+ const isControlled = playing !== void 0;
172
+ const trackRef = React.useRef(null);
173
+ const fittedBarCount = useFittedBarCount(trackRef, {
174
+ barWidthPx: BAR_WIDTH_PX,
175
+ gapPx: BAR_GAP_PX,
176
+ fallback: DEFAULT_BAR_COUNT
177
+ });
178
+ const seekProps = useWaveformSeek({
179
+ trackRef,
180
+ displayDuration: playback.displayDuration,
181
+ currentTime: playback.currentTime,
182
+ seekTo: playback.seekTo,
183
+ setSeeking: playback.setSeeking
184
+ });
185
+ const isPlaying = isPlayer ? playback.isPlaying : playing ?? internalPlaying;
186
+ const handlePlayPause = () => {
187
+ if (isPlayer) {
188
+ playback.toggle();
189
+ return;
190
+ }
191
+ const next = !isPlaying;
192
+ if (!isControlled) setInternalPlaying(next);
193
+ onPlayPause?.(next);
194
+ };
195
+ const bars = resamplePeaks(isPlayer ? decodedPeaks : waveform, fittedBarCount);
196
+ const progressValue = isPlayer ? playback.progress : progress;
197
+ const textColor = negative ? "text-content-primary-inverted" : "text-content-primary";
198
+ const playerTime = playback.hasStarted ? playback.currentTime : playback.displayDuration;
199
+ const displayTime = isPlayer ? formatTime(playerTime) : time;
200
+ const controlLabel = playButtonLabel ?? (isPlaying ? "Pause" : "Play");
201
+ const trackAria = isPlayer ? seekProps : { "aria-hidden": true };
202
+ return (
203
+ // biome-ignore lint/a11y/useSemanticElements: <fieldset> would break the public HTMLDivElement ref/props API
204
+ /* @__PURE__ */ jsxs(
205
+ "div",
206
+ {
207
+ ref,
208
+ role: "group",
209
+ "aria-label": ariaLabel,
210
+ className: cn("flex items-center gap-3", size === "small" ? "h-8" : "h-12", className),
211
+ ...props,
212
+ children: [
213
+ showControls && /* @__PURE__ */ jsx(
214
+ IconButton,
215
+ {
216
+ variant: "secondary",
217
+ size: CONTROL_SIZE[size],
218
+ negative,
219
+ icon: isPlaying ? /* @__PURE__ */ jsx(PauseIcon, {}) : /* @__PURE__ */ jsx(PlayIcon, {}),
220
+ "aria-label": controlLabel,
221
+ onClick: handlePlayPause
222
+ }
223
+ ),
224
+ /* @__PURE__ */ jsx(
225
+ "div",
226
+ {
227
+ ref: trackRef,
228
+ "data-testid": "voice-note-waveform",
229
+ className: cn(
230
+ "flex h-full min-w-0 flex-1 items-center gap-1 overflow-hidden",
231
+ isPlayer && "cursor-pointer touch-none select-none focus-visible:shadow-focus-ring focus-visible:outline-none"
232
+ ),
233
+ ...trackAria,
234
+ children: /* @__PURE__ */ jsx(
235
+ WaveformBars,
236
+ {
237
+ bars,
238
+ variant,
239
+ size,
240
+ negative,
241
+ progress: progressValue
242
+ }
243
+ )
244
+ }
245
+ ),
246
+ /* @__PURE__ */ jsx(
247
+ WaveformMeta,
248
+ {
249
+ time: showTimestamp ? displayTime : void 0,
250
+ fileName,
251
+ textColor
252
+ }
253
+ ),
254
+ showRemove && /* @__PURE__ */ jsx(
255
+ IconButton,
256
+ {
257
+ variant: "tertiary",
258
+ size: CONTROL_SIZE[size],
259
+ negative,
260
+ icon: /* @__PURE__ */ jsx(CloseIcon, {}),
261
+ "aria-label": removeButtonLabel,
262
+ onClick: onRemove
263
+ }
264
+ ),
265
+ isPlayer && // biome-ignore lint/a11y/useMediaCaption: a UI voice note / short recording, not narrated content needing captions
266
+ /* @__PURE__ */ jsx(
267
+ "audio",
268
+ {
269
+ ref: playback.audioRef,
270
+ src,
271
+ preload: "metadata",
272
+ className: "hidden",
273
+ onLoadedMetadata: playback.audioHandlers.onLoadedMetadata,
274
+ onTimeUpdate: playback.audioHandlers.onTimeUpdate,
275
+ onEnded: playback.audioHandlers.onEnded
276
+ }
277
+ )
278
+ ]
279
+ }
280
+ )
281
+ );
282
+ }
283
+ );
284
+ VoiceNote.displayName = "VoiceNote";
285
+ export {
286
+ VoiceNote
287
+ };
288
+ //# sourceMappingURL=VoiceNote.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoiceNote.mjs","sources":["../../../src/components/VoiceNote/VoiceNote.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { formatTime, resamplePeaks } from \"@/utils/audioWaveform\";\nimport { cn } from \"@/utils/cn\";\nimport { useAudioPlayback } from \"@/utils/useAudioPlayback\";\nimport { useFittedBarCount } from \"@/utils/useFittedBarCount\";\nimport { useWaveformPeaks } from \"@/utils/useWaveformPeaks\";\nimport { useWaveformSeek } from \"@/utils/useWaveformSeek\";\nimport { IconButton, type IconButtonSize } from \"../IconButton/IconButton\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\nimport { PauseIcon } from \"../Icons/PauseIcon\";\nimport { PlayIcon } from \"../Icons/PlayIcon\";\n\n/** Visual style of the waveform. */\nexport type VoiceNoteVariant = \"default\" | \"flat\";\n\n/** Size preset for the voice note. */\nexport type VoiceNoteSize = \"default\" | \"small\";\n\n/**\n * Default waveform amplitudes (0–1), taken from the Figma reference so the\n * resting state renders 1:1 when no `waveform` data (or `src`) is supplied.\n */\nconst DEFAULT_WAVEFORM = [\n 0.15, 0.46, 0.85, 0.23, 1, 0.85, 0.62, 0.62, 0.62, 0.85, 0.85, 0.62, 0.62, 1, 1, 0.62, 0.62, 0.62,\n 0.23, 0.23, 0.15, 0.46, 0.85, 0.23, 1, 0.46, 0.85, 0.62, 0.23, 0.23, 0.62, 0.62, 0.85, 0.85, 0.62,\n 0.62, 0.85, 1, 0.85, 1, 0.62, 0.62, 0.62, 0.23, 0.23, 0.62, 0.23, 0.23, 0.23, 0.62, 0.85, 0.62,\n 0.85, 1, 0.85, 0.85, 1, 0.85, 0.23, 0.23, 0.23, 0.46, 0.62, 0.23, 0.23,\n];\n\nconst MIN_BAR_HEIGHT = 4;\nconst MAX_BAR_HEIGHT: Record<VoiceNoteSize, number> = {\n default: 26,\n small: 16,\n};\n\nconst CONTROL_SIZE: Record<VoiceNoteSize, IconButtonSize> = {\n default: \"48\",\n small: \"32\",\n};\n\n/** Rendered width of a single waveform bar, in pixels (`w-1`). */\nconst BAR_WIDTH_PX = 4;\n/** Gap between waveform bars, in pixels (`gap-1`). */\nconst BAR_GAP_PX = 4;\n/** Bar count before the track is measured (SSR, jsdom, or zero-width layout). */\nconst DEFAULT_BAR_COUNT = 40;\n\nfunction activeBarClass(negative: boolean): string {\n return negative\n ? \"bg-messages-waveform-listening-negative-active\"\n : \"bg-messages-waveform-listening-active\";\n}\n\nfunction inactiveBarClass(negative: boolean): string {\n return negative\n ? \"bg-messages-waveform-listening-negative-inactive\"\n : \"bg-messages-waveform-listening-inactive\";\n}\n\nfunction restingBarClass(negative: boolean): string {\n return negative\n ? \"bg-messages-waveform-listening-negative-default\"\n : \"bg-messages-waveform-default\";\n}\n\ninterface WaveformBarsProps {\n bars: number[];\n variant: VoiceNoteVariant;\n size: VoiceNoteSize;\n negative: boolean;\n progress: number | undefined;\n}\n\nfunction WaveformBars({ bars, variant, size, negative, progress }: WaveformBarsProps) {\n const hasProgress = progress !== undefined;\n const activeCount = hasProgress ? Math.round(progress * bars.length) : 0;\n const maxBarHeight = MAX_BAR_HEIGHT[size];\n\n return (\n <>\n {bars.map((amplitude, index) => {\n const height =\n variant === \"flat\"\n ? MIN_BAR_HEIGHT\n : Math.max(MIN_BAR_HEIGHT, Math.round(amplitude * maxBarHeight));\n const colorClass = !hasProgress\n ? restingBarClass(negative)\n : index < activeCount\n ? activeBarClass(negative)\n : inactiveBarClass(negative);\n\n return (\n <span\n // biome-ignore lint/suspicious/noArrayIndexKey: bars are a fixed positional sequence\n key={index}\n className={cn(\"w-1 shrink-0 rounded-[2px]\", colorClass)}\n style={{ height }}\n />\n );\n })}\n </>\n );\n}\n\ninterface WaveformMetaProps {\n time: string | undefined;\n fileName: string | undefined;\n textColor: string;\n}\n\nfunction WaveformMeta({ time, fileName, textColor }: WaveformMetaProps) {\n return (\n <>\n {time && (\n <span className={cn(\"typography-body-small-14px-regular shrink-0\", textColor)}>{time}</span>\n )}\n {fileName && (\n <span\n className={cn(\n \"typography-body-small-14px-regular min-w-0 max-w-[50%] truncate\",\n textColor,\n )}\n >\n {fileName}\n </span>\n )}\n </>\n );\n}\n\nexport interface VoiceNoteProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onPlay\" | \"onPause\"> {\n /**\n * URL of the audio to play. When set, the component manages a real `<audio>`\n * element: it decodes the waveform, plays/pauses, tracks live progress and is\n * seekable — `waveform`/`progress` are derived automatically. Leave unset for\n * a presentational, fully-controlled voice note.\n */\n src?: string;\n /** Fallback total duration (seconds), used until the media's metadata loads. */\n duration?: number;\n /** Amplitude values (0–1), one per bar. Ignored when `src` is set. Falls back to a built-in pattern. */\n waveform?: number[];\n /** Visual style; `flat` renders a simplified dotted preview. @default \"default\" */\n variant?: VoiceNoteVariant;\n /** Size preset. @default \"default\" */\n size?: VoiceNoteSize;\n /** Dark-surface treatment for use on message bubbles. @default false */\n negative?: boolean;\n /**\n * Playback progress (0–1) for the presentational mode. When set, the waveform\n * splits into played/unplayed bars (the \"Listening\" state). Ignored when `src`\n * is set (progress comes from the media element).\n */\n progress?: number;\n /** Whether audio is playing (controlled) — toggles the play/pause icon. */\n playing?: boolean;\n /** Initial playing state (uncontrolled). @default false */\n defaultPlaying?: boolean;\n /** Called with the next playing state when the play/pause control is pressed. */\n onPlayPause?: (playing: boolean) => void;\n /** Called when audio playback reaches the end (only in `src` mode). */\n onEnded?: () => void;\n /** Timestamp or duration label, e.g. \"0:05\". Ignored when `src` is set (derived from the media). */\n time?: string;\n /** File name shown when the audio is an uploaded file rather than a voice note. */\n fileName?: string;\n /** Show the play/pause control. @default true */\n showControls?: boolean;\n /** Show the timestamp label. @default true */\n showTimestamp?: boolean;\n /** Show a remove button (calls {@link VoiceNoteProps.onRemove}). @default false */\n showRemove?: boolean;\n /** Called when the remove button is pressed. */\n onRemove?: () => void;\n /** Accessible name for the play/pause control. Defaults to \"Play\"/\"Pause\". */\n playButtonLabel?: string;\n /** Accessible name for the remove button. @default \"Remove\" */\n removeButtonLabel?: string;\n /** Accessible name for the whole player. @default \"Voice note\" */\n \"aria-label\"?: string;\n}\n\n/**\n * A voice-note audio player: a play/pause control, an amplitude waveform, and a\n * timestamp — for voice messages and audio attachments in a conversation.\n *\n * Two modes: pass `src` for a self-contained player that decodes the waveform,\n * plays real audio, tracks live progress and is seekable; or omit `src` and\n * drive it with `playing`/`progress` + `onPlayPause` for a presentational,\n * fully-controlled voice note. `flat` renders a compact dotted preview and\n * `negative` adapts it to dark message bubbles.\n *\n * @example\n * ```tsx\n * <VoiceNote src=\"https://example.com/note.mp3\" duration={5} />\n * <VoiceNote time=\"0:05\" progress={0.4} playing onPlayPause={toggle} />\n * ```\n */\nexport const VoiceNote = React.forwardRef<HTMLDivElement, VoiceNoteProps>(\n (\n {\n className,\n src,\n duration,\n waveform = DEFAULT_WAVEFORM,\n variant = \"default\",\n size = \"default\",\n negative = false,\n progress,\n playing,\n defaultPlaying = false,\n onPlayPause,\n onEnded,\n time,\n fileName,\n showControls = true,\n showTimestamp = true,\n showRemove = false,\n onRemove,\n playButtonLabel,\n removeButtonLabel = \"Remove\",\n \"aria-label\": ariaLabel = \"Voice note\",\n ...props\n },\n ref,\n ) => {\n const isPlayer = src !== undefined;\n\n const playback = useAudioPlayback({\n src,\n duration,\n playing,\n defaultPlaying,\n onEnded,\n onPlay: React.useCallback(() => onPlayPause?.(true), [onPlayPause]),\n onPause: React.useCallback(() => onPlayPause?.(false), [onPlayPause]),\n });\n const decodedPeaks = useWaveformPeaks(src);\n\n const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);\n const isControlled = playing !== undefined;\n\n const trackRef = React.useRef<HTMLDivElement>(null);\n const fittedBarCount = useFittedBarCount(trackRef, {\n barWidthPx: BAR_WIDTH_PX,\n gapPx: BAR_GAP_PX,\n fallback: DEFAULT_BAR_COUNT,\n });\n const seekProps = useWaveformSeek({\n trackRef,\n displayDuration: playback.displayDuration,\n currentTime: playback.currentTime,\n seekTo: playback.seekTo,\n setSeeking: playback.setSeeking,\n });\n\n const isPlaying = isPlayer ? playback.isPlaying : (playing ?? internalPlaying);\n\n const handlePlayPause = () => {\n if (isPlayer) {\n playback.toggle();\n return;\n }\n const next = !isPlaying;\n if (!isControlled) setInternalPlaying(next);\n onPlayPause?.(next);\n };\n\n const bars = resamplePeaks(isPlayer ? decodedPeaks : waveform, fittedBarCount);\n const progressValue = isPlayer ? playback.progress : progress;\n const textColor = negative ? \"text-content-primary-inverted\" : \"text-content-primary\";\n\n const playerTime = playback.hasStarted ? playback.currentTime : playback.displayDuration;\n const displayTime = isPlayer ? formatTime(playerTime) : time;\n const controlLabel = playButtonLabel ?? (isPlaying ? \"Pause\" : \"Play\");\n const trackAria = isPlayer ? seekProps : ({ \"aria-hidden\": true } as const);\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={ariaLabel}\n className={cn(\"flex items-center gap-3\", size === \"small\" ? \"h-8\" : \"h-12\", className)}\n {...props}\n >\n {showControls && (\n <IconButton\n variant=\"secondary\"\n size={CONTROL_SIZE[size]}\n negative={negative}\n icon={isPlaying ? <PauseIcon /> : <PlayIcon />}\n aria-label={controlLabel}\n onClick={handlePlayPause}\n />\n )}\n\n <div\n ref={trackRef}\n data-testid=\"voice-note-waveform\"\n className={cn(\n \"flex h-full min-w-0 flex-1 items-center gap-1 overflow-hidden\",\n isPlayer &&\n \"cursor-pointer touch-none select-none focus-visible:shadow-focus-ring focus-visible:outline-none\",\n )}\n {...trackAria}\n >\n <WaveformBars\n bars={bars}\n variant={variant}\n size={size}\n negative={negative}\n progress={progressValue}\n />\n </div>\n\n <WaveformMeta\n time={showTimestamp ? displayTime : undefined}\n fileName={fileName}\n textColor={textColor}\n />\n\n {showRemove && (\n <IconButton\n variant=\"tertiary\"\n size={CONTROL_SIZE[size]}\n negative={negative}\n icon={<CloseIcon />}\n aria-label={removeButtonLabel}\n onClick={onRemove}\n />\n )}\n\n {isPlayer && (\n // biome-ignore lint/a11y/useMediaCaption: a UI voice note / short recording, not narrated content needing captions\n <audio\n ref={playback.audioRef}\n src={src}\n preload=\"metadata\"\n className=\"hidden\"\n onLoadedMetadata={playback.audioHandlers.onLoadedMetadata}\n onTimeUpdate={playback.audioHandlers.onTimeUpdate}\n onEnded={playback.audioHandlers.onEnded}\n />\n )}\n </div>\n );\n },\n);\n\nVoiceNote.displayName = \"VoiceNote\";\n"],"names":[],"mappings":"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"}