@fanvue/ui 3.13.0 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs +357 -0
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs.map +1 -0
- package/dist/cjs/components/Dialog/Dialog.cjs +2 -1
- package/dist/cjs/components/Dialog/Dialog.cjs.map +1 -1
- package/dist/cjs/components/FloatingActionButton/FloatingActionButton.cjs +59 -0
- package/dist/cjs/components/FloatingActionButton/FloatingActionButton.cjs.map +1 -0
- package/dist/cjs/components/Icons/GridViewIcon.cjs +50 -0
- package/dist/cjs/components/Icons/GridViewIcon.cjs.map +1 -0
- package/dist/cjs/components/Icons/ListViewIcon.cjs +50 -0
- package/dist/cjs/components/Icons/ListViewIcon.cjs.map +1 -0
- package/dist/cjs/components/SegmentedControl/SegmentedControl.cjs +42 -11
- package/dist/cjs/components/SegmentedControl/SegmentedControl.cjs.map +1 -1
- package/dist/cjs/index.cjs +8 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.mjs +340 -0
- package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -0
- package/dist/components/Dialog/Dialog.mjs +2 -1
- package/dist/components/Dialog/Dialog.mjs.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.mjs +42 -0
- package/dist/components/FloatingActionButton/FloatingActionButton.mjs.map +1 -0
- package/dist/components/Icons/GridViewIcon.mjs +33 -0
- package/dist/components/Icons/GridViewIcon.mjs.map +1 -0
- package/dist/components/Icons/ListViewIcon.mjs +33 -0
- package/dist/components/Icons/ListViewIcon.mjs.map +1 -0
- package/dist/components/SegmentedControl/SegmentedControl.mjs +42 -11
- package/dist/components/SegmentedControl/SegmentedControl.mjs.map +1 -1
- package/dist/index.d.ts +134 -1
- package/dist/index.mjs +8 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
5
|
+
const React = require("react");
|
|
6
|
+
const BaseIcon = require("./BaseIcon.cjs");
|
|
7
|
+
function _interopNamespaceDefault(e) {
|
|
8
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
9
|
+
if (e) {
|
|
10
|
+
for (const k in e) {
|
|
11
|
+
if (k !== "default") {
|
|
12
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
13
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: () => e[k]
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
n.default = e;
|
|
21
|
+
return Object.freeze(n);
|
|
22
|
+
}
|
|
23
|
+
const React__namespace = /* @__PURE__ */ _interopNamespaceDefault(React);
|
|
24
|
+
const VARIANTS = {
|
|
25
|
+
16: {
|
|
26
|
+
outlined: [
|
|
27
|
+
{
|
|
28
|
+
d: "M13.267 9H2.733c-1 0-1.4.427-1.4 1.487v2.693c0 1.06.4 1.487 1.4 1.487h10.534c1 0 1.4-.427 1.4-1.487v-2.693c0-1.06-.4-1.487-1.4-1.487m0-7.667H2.733c-1 0-1.4.427-1.4 1.487v2.693c0 1.06.4 1.487 1.4 1.487h10.534c1 0 1.4-.427 1.4-1.487V2.82c0-1.06-.4-1.487-1.4-1.487"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
24: {
|
|
33
|
+
outlined: [
|
|
34
|
+
{
|
|
35
|
+
d: "M19.9 13.5H4.1c-1.5 0-2.1.64-2.1 2.23v4.04C2 21.36 2.6 22 4.1 22h15.8c1.5 0 2.1-.64 2.1-2.23v-4.04c0-1.59-.6-2.23-2.1-2.23m0-11.5H4.1C2.6 2 2 2.64 2 4.23v4.04c0 1.59.6 2.23 2.1 2.23h15.8c1.5 0 2.1-.64 2.1-2.23V4.23C22 2.64 21.4 2 19.9 2"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
32: {
|
|
40
|
+
outlined: [
|
|
41
|
+
{
|
|
42
|
+
d: "M26.533 18H5.467c-2 0-2.8.853-2.8 2.973v5.387c0 2.12.8 2.973 2.8 2.973h21.066c2 0 2.8-.853 2.8-2.973v-5.387c0-2.12-.8-2.973-2.8-2.973m0-15.333H5.467c-2 0-2.8.853-2.8 2.973v5.387c0 2.12.8 2.973 2.8 2.973h21.066c2 0 2.8-.854 2.8-2.973V5.64c0-2.12-.8-2.973-2.8-2.973"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const ListViewIcon = React__namespace.forwardRef((props, ref) => /* @__PURE__ */ jsxRuntime.jsx(BaseIcon.BaseIcon, { ref, variants: VARIANTS, ...props }));
|
|
48
|
+
ListViewIcon.displayName = "ListViewIcon";
|
|
49
|
+
exports.ListViewIcon = ListViewIcon;
|
|
50
|
+
//# sourceMappingURL=ListViewIcon.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListViewIcon.cjs","sources":["../../../../src/components/Icons/ListViewIcon.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { BaseIcon } from \"./BaseIcon\";\nimport type { BaseIconProps, IconVariants } from \"./types\";\n\nconst VARIANTS: IconVariants = {\n 16: {\n outlined: [\n {\n d: \"M13.267 9H2.733c-1 0-1.4.427-1.4 1.487v2.693c0 1.06.4 1.487 1.4 1.487h10.534c1 0 1.4-.427 1.4-1.487v-2.693c0-1.06-.4-1.487-1.4-1.487m0-7.667H2.733c-1 0-1.4.427-1.4 1.487v2.693c0 1.06.4 1.487 1.4 1.487h10.534c1 0 1.4-.427 1.4-1.487V2.82c0-1.06-.4-1.487-1.4-1.487\",\n },\n ],\n },\n 24: {\n outlined: [\n {\n d: \"M19.9 13.5H4.1c-1.5 0-2.1.64-2.1 2.23v4.04C2 21.36 2.6 22 4.1 22h15.8c1.5 0 2.1-.64 2.1-2.23v-4.04c0-1.59-.6-2.23-2.1-2.23m0-11.5H4.1C2.6 2 2 2.64 2 4.23v4.04c0 1.59.6 2.23 2.1 2.23h15.8c1.5 0 2.1-.64 2.1-2.23V4.23C22 2.64 21.4 2 19.9 2\",\n },\n ],\n },\n 32: {\n outlined: [\n {\n d: \"M26.533 18H5.467c-2 0-2.8.853-2.8 2.973v5.387c0 2.12.8 2.973 2.8 2.973h21.066c2 0 2.8-.853 2.8-2.973v-5.387c0-2.12-.8-2.973-2.8-2.973m0-15.333H5.467c-2 0-2.8.853-2.8 2.973v5.387c0 2.12.8 2.973 2.8 2.973h21.066c2 0 2.8-.854 2.8-2.973V5.64c0-2.12-.8-2.973-2.8-2.973\",\n },\n ],\n },\n};\n\n/** Props for {@link ListViewIcon}. See {@link BaseIconProps} for the shared shape. */\nexport type ListViewIconProps = BaseIconProps;\n\n/**\n * List view icon. Renders at sizes 16, 24, or 32 px.\n *\n * @example\n * ```tsx\n * <ListViewIcon size={24} />\n * ```\n */\nexport const ListViewIcon = React.forwardRef<SVGSVGElement, ListViewIconProps>((props, ref) => (\n <BaseIcon ref={ref} variants={VARIANTS} {...props} />\n));\n\nListViewIcon.displayName = \"ListViewIcon\";\n"],"names":["React","jsx","BaseIcon"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAIA,MAAM,WAAyB;AAAA,EAC7B,IAAI;AAAA,IACF,UAAU;AAAA,MACR;AAAA,QACE,GAAG;AAAA,MAAA;AAAA,IACL;AAAA,EACF;AAAA,EAEF,IAAI;AAAA,IACF,UAAU;AAAA,MACR;AAAA,QACE,GAAG;AAAA,MAAA;AAAA,IACL;AAAA,EACF;AAAA,EAEF,IAAI;AAAA,IACF,UAAU;AAAA,MACR;AAAA,QACE,GAAG;AAAA,MAAA;AAAA,IACL;AAAA,EACF;AAEJ;AAaO,MAAM,eAAeA,iBAAM,WAA6C,CAAC,OAAO,QACrFC,2BAAAA,IAACC,SAAAA,UAAA,EAAS,KAAU,UAAU,UAAW,GAAG,OAAO,CACpD;AAED,aAAa,cAAc;;"}
|
|
@@ -35,11 +35,47 @@ function warnMissingAccessibleName(ariaLabel, ariaLabelledBy) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
function warnMissingOptionAccessibleName(options) {
|
|
39
|
+
if (process.env.NODE_ENV !== "production") {
|
|
40
|
+
for (const option of options) {
|
|
41
|
+
if (option.icon && !option.label?.trim()) {
|
|
42
|
+
console.warn(
|
|
43
|
+
`SegmentedControl: icon-only segment "${option.value}" is missing a non-empty \`label\` to use as its accessible name.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function getSegmentClassName({
|
|
50
|
+
appearance,
|
|
51
|
+
size,
|
|
52
|
+
variant,
|
|
53
|
+
isSelected,
|
|
54
|
+
disabled
|
|
55
|
+
}) {
|
|
56
|
+
return cn.cn(
|
|
57
|
+
"relative inline-flex min-w-0 cursor-pointer items-center justify-center rounded-full",
|
|
58
|
+
"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out",
|
|
59
|
+
"focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
60
|
+
variant === "fill" ? "flex-1" : "shrink-0",
|
|
61
|
+
appearance === "plain" ? cn.cn(
|
|
62
|
+
// Padding + negative margin enlarge the hit target without changing the
|
|
63
|
+
// visual footprint, which must stay glyph-only to match the design.
|
|
64
|
+
"-m-1 p-1",
|
|
65
|
+
isSelected ? "text-icons-primary" : "text-icons-tertiary hover:text-icons-primary"
|
|
66
|
+
) : cn.cn(
|
|
67
|
+
sizeClasses[size],
|
|
68
|
+
isSelected ? "bg-buttons-primary-default text-content-primary-inverted shadow-sm" : "text-content-primary hover:bg-buttons-switch-hover"
|
|
69
|
+
),
|
|
70
|
+
disabled && "pointer-events-none"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
38
73
|
const SegmentedControl = React__namespace.forwardRef(
|
|
39
74
|
({
|
|
40
75
|
className,
|
|
41
76
|
size = "32",
|
|
42
77
|
variant = "hug",
|
|
78
|
+
appearance = "pill",
|
|
43
79
|
options,
|
|
44
80
|
value: controlledValue,
|
|
45
81
|
defaultValue,
|
|
@@ -48,6 +84,7 @@ const SegmentedControl = React__namespace.forwardRef(
|
|
|
48
84
|
...props
|
|
49
85
|
}, ref) => {
|
|
50
86
|
warnMissingAccessibleName(props["aria-label"], props["aria-labelledby"]);
|
|
87
|
+
warnMissingOptionAccessibleName(options);
|
|
51
88
|
const [internalValue, setInternalValue] = React__namespace.useState(defaultValue ?? options[0]?.value);
|
|
52
89
|
const isControlled = controlledValue !== void 0;
|
|
53
90
|
const currentValue = isControlled ? controlledValue : internalValue;
|
|
@@ -74,8 +111,9 @@ const SegmentedControl = React__namespace.forwardRef(
|
|
|
74
111
|
ref,
|
|
75
112
|
role: "radiogroup",
|
|
76
113
|
className: cn.cn(
|
|
77
|
-
"relative items-center rounded-full
|
|
114
|
+
"relative items-center rounded-full",
|
|
78
115
|
variant === "fill" ? "flex w-full" : "inline-flex",
|
|
116
|
+
appearance === "plain" ? "gap-2" : "bg-surface-tertiary p-1",
|
|
79
117
|
disabled && "cursor-not-allowed opacity-50",
|
|
80
118
|
className
|
|
81
119
|
),
|
|
@@ -95,18 +133,11 @@ const SegmentedControl = React__namespace.forwardRef(
|
|
|
95
133
|
"aria-checked": isSelected,
|
|
96
134
|
tabIndex: isSelected || !anySelected && index === 0 ? 0 : -1,
|
|
97
135
|
disabled,
|
|
136
|
+
"aria-label": option.icon ? option.label : void 0,
|
|
98
137
|
onClick: () => handleSelect(option.value),
|
|
99
138
|
onKeyDown: (e) => handleKeyDown(e, index),
|
|
100
|
-
className:
|
|
101
|
-
|
|
102
|
-
"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out",
|
|
103
|
-
"focus-visible:shadow-focus-ring focus-visible:outline-none",
|
|
104
|
-
variant === "fill" ? "flex-1" : "shrink-0",
|
|
105
|
-
sizeClasses[size],
|
|
106
|
-
isSelected ? "bg-buttons-primary-default text-content-primary-inverted shadow-sm" : "text-content-primary hover:bg-buttons-switch-hover",
|
|
107
|
-
disabled && "pointer-events-none"
|
|
108
|
-
),
|
|
109
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: option.label })
|
|
139
|
+
className: getSegmentClassName({ appearance, size, variant, isSelected, disabled }),
|
|
140
|
+
children: option.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-center justify-center", "aria-hidden": "true", children: option.icon }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: option.label })
|
|
110
141
|
},
|
|
111
142
|
option.value
|
|
112
143
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SegmentedControl.cjs","sources":["../../../../src/components/SegmentedControl/SegmentedControl.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Height of the segmented control in pixels. */\nexport type SegmentedControlSize = \"32\" | \"40\" | \"48\";\n\n/**\n * Segment layout.\n * - `\"hug\"`: each segment is sized to its content.\n * - `\"fill\"`: the control spans the full width of its container and each segment grows equally.\n */\nexport type SegmentedControlVariant = \"hug\" | \"fill\";\n\n/** Describes one selectable segment. */\nexport interface SegmentedControlOption {\n /** Display label for the segment. */\n label: string;\n /** Value identifier returned via `onChange`. */\n value: string;\n}\n\nexport interface SegmentedControlProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** Height of the control in pixels. @default \"32\" */\n size?: SegmentedControlSize;\n /** Segment layout. @default \"hug\" */\n variant?: SegmentedControlVariant;\n /** The selectable segments. Designed for two or three mutually exclusive options. */\n options: SegmentedControlOption[];\n /** Currently selected value (controlled). */\n value?: string;\n /** Initially selected value (uncontrolled). Defaults to the first option. */\n defaultValue?: string;\n /** Callback fired when the selected value changes. */\n onChange?: (value: string) => void;\n /** Whether the control is disabled. @default false */\n disabled?: boolean;\n}\n\n/** Padding and typography per size. Combined with the container's `p-1` these hit the 32/40/48px heights. */\nconst sizeClasses: Record<SegmentedControlSize, string> = {\n \"32\": \"px-2 py-1 typography-description-12px-semibold\",\n \"40\": \"px-3 py-1.75 typography-body-small-14px-semibold\",\n \"48\": \"px-4 py-2 typography-body-default-16px-semibold\",\n};\n\nfunction warnMissingAccessibleName(ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"SegmentedControl: no accessible name provided. Pass an `aria-label` or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\n/**\n * A compact selector for choosing between two or three mutually exclusive\n * options where the choice affects the content immediately below it. Use\n * instead of tabs when the options are more like settings or filters than\n * navigation, such as toggling a list/grid view or a monthly/annual price.\n *\n * Rendered as a `radiogroup` with roving-tabindex keyboard navigation. Supports\n * both controlled and uncontrolled usage.\n *\n * @example\n * ```tsx\n * <SegmentedControl\n * options={[\n * { label: \"Net\", value: \"net\" },\n * { label: \"Gross\", value: \"gross\" },\n * ]}\n * value={amount}\n * onChange={setAmount}\n * aria-label=\"Amount type\"\n * />\n * ```\n */\nexport const SegmentedControl = React.forwardRef<HTMLDivElement, SegmentedControlProps>(\n (\n {\n className,\n size = \"32\",\n variant = \"hug\",\n options,\n value: controlledValue,\n defaultValue,\n onChange,\n disabled = false,\n ...props\n },\n ref,\n ) => {\n warnMissingAccessibleName(props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n // Tracks selection for uncontrolled usage; ignored when `value` prop is provided\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? options[0]?.value);\n const isControlled = controlledValue !== undefined;\n const currentValue = isControlled ? controlledValue : internalValue;\n const anySelected = options.some((o) => o.value === currentValue);\n const buttonRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n\n const handleSelect = (optionValue: string) => {\n if (disabled || optionValue === currentValue) return;\n if (!isControlled) {\n setInternalValue(optionValue);\n }\n onChange?.(optionValue);\n };\n\n const handleKeyDown = (e: React.KeyboardEvent, index: number) => {\n const nextIndex =\n e.key === \"ArrowRight\" || e.key === \"ArrowDown\"\n ? (index + 1) % options.length\n : e.key === \"ArrowLeft\" || e.key === \"ArrowUp\"\n ? (index - 1 + options.length) % options.length\n : null;\n if (nextIndex === null) return;\n e.preventDefault();\n const nextOption = options[nextIndex] as SegmentedControlOption;\n handleSelect(nextOption.value);\n buttonRefs.current[nextIndex]?.focus();\n };\n\n return (\n <div\n ref={ref}\n role=\"radiogroup\"\n className={cn(\n \"relative items-center rounded-full bg-surface-tertiary p-1\",\n variant === \"fill\" ? \"flex w-full\" : \"inline-flex\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n {...props}\n >\n {options.map((option, index) => {\n const isSelected = currentValue === option.value;\n return (\n // biome-ignore lint/a11y/useSemanticElements: native radio inputs only allow Tab-focus on the checked item; buttons with roving tabindex give full keyboard navigation\n <button\n key={option.value}\n ref={(el) => {\n buttonRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={isSelected}\n tabIndex={isSelected || (!anySelected && index === 0) ? 0 : -1}\n disabled={disabled}\n onClick={() => handleSelect(option.value)}\n onKeyDown={(e) => handleKeyDown(e, index)}\n className={cn(\n \"relative inline-flex min-w-0 cursor-pointer items-center justify-center rounded-full\",\n \"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n variant === \"fill\" ? \"flex-1\" : \"shrink-0\",\n sizeClasses[size],\n isSelected\n ? \"bg-buttons-primary-default text-content-primary-inverted shadow-sm\"\n : \"text-content-primary hover:bg-buttons-switch-hover\",\n disabled && \"pointer-events-none\",\n )}\n >\n <span className=\"min-w-0 truncate\">{option.label}</span>\n </button>\n );\n })}\n </div>\n );\n },\n);\n\nSegmentedControl.displayName = \"SegmentedControl\";\n"],"names":["React","jsx","cn"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAM,cAAoD;AAAA,EACxD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,SAAS,0BAA0B,WAAoB,gBAAyB;AAC9E,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,QAAI,CAAC,aAAa,CAAC,gBAAgB;AACjC,cAAQ;AAAA,QACN;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AACF;AAwBO,MAAM,mBAAmBA,iBAAM;AAAA,EACpC,CACE;AAAA,IACE;AAAA,IACA,OAAO;AAAA,IACP,UAAU;AAAA,IACV;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,GAAG;AAAA,EAAA,GAEL,QACG;AACH,8BAA0B,MAAM,YAAY,GAAG,MAAM,iBAAiB,CAAC;AAGvE,UAAM,CAAC,eAAe,gBAAgB,IAAIA,iBAAM,SAAS,gBAAgB,QAAQ,CAAC,GAAG,KAAK;AAC1F,UAAM,eAAe,oBAAoB;AACzC,UAAM,eAAe,eAAe,kBAAkB;AACtD,UAAM,cAAc,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,YAAY;AAChE,UAAM,aAAaA,iBAAM,OAAqC,EAAE;AAEhE,UAAM,eAAe,CAAC,gBAAwB;AAC5C,UAAI,YAAY,gBAAgB,aAAc;AAC9C,UAAI,CAAC,cAAc;AACjB,yBAAiB,WAAW;AAAA,MAC9B;AACA,iBAAW,WAAW;AAAA,IACxB;AAEA,UAAM,gBAAgB,CAAC,GAAwB,UAAkB;AAC/D,YAAM,YACJ,EAAE,QAAQ,gBAAgB,EAAE,QAAQ,eAC/B,QAAQ,KAAK,QAAQ,SACtB,EAAE,QAAQ,eAAe,EAAE,QAAQ,aAChC,QAAQ,IAAI,QAAQ,UAAU,QAAQ,SACvC;AACR,UAAI,cAAc,KAAM;AACxB,QAAE,eAAA;AACF,YAAM,aAAa,QAAQ,SAAS;AACpC,mBAAa,WAAW,KAAK;AAC7B,iBAAW,QAAQ,SAAS,GAAG,MAAA;AAAA,IACjC;AAEA,WACEC,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,WAAWC,GAAAA;AAAAA,UACT;AAAA,UACA,YAAY,SAAS,gBAAgB;AAAA,UACrC,YAAY;AAAA,UACZ;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA,QAAQ,IAAI,CAAC,QAAQ,UAAU;AAC9B,gBAAM,aAAa,iBAAiB,OAAO;AAC3C;AAAA;AAAA,YAEED,2BAAAA;AAAAA,cAAC;AAAA,cAAA;AAAA,gBAEC,KAAK,CAAC,OAAO;AACX,6BAAW,QAAQ,KAAK,IAAI;AAAA,gBAC9B;AAAA,gBACA,MAAK;AAAA,gBACL,MAAK;AAAA,gBACL,gBAAc;AAAA,gBACd,UAAU,cAAe,CAAC,eAAe,UAAU,IAAK,IAAI;AAAA,gBAC5D;AAAA,gBACA,SAAS,MAAM,aAAa,OAAO,KAAK;AAAA,gBACxC,WAAW,CAAC,MAAM,cAAc,GAAG,KAAK;AAAA,gBACxC,WAAWC,GAAAA;AAAAA,kBACT;AAAA,kBACA;AAAA,kBACA;AAAA,kBACA,YAAY,SAAS,WAAW;AAAA,kBAChC,YAAY,IAAI;AAAA,kBAChB,aACI,uEACA;AAAA,kBACJ,YAAY;AAAA,gBAAA;AAAA,gBAGd,UAAAD,2BAAAA,IAAC,QAAA,EAAK,WAAU,oBAAoB,iBAAO,MAAA,CAAM;AAAA,cAAA;AAAA,cAvB5C,OAAO;AAAA,YAAA;AAAA;AAAA,QA0BlB,CAAC;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AAEA,iBAAiB,cAAc;;"}
|
|
1
|
+
{"version":3,"file":"SegmentedControl.cjs","sources":["../../../../src/components/SegmentedControl/SegmentedControl.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Height of the segmented control in pixels. */\nexport type SegmentedControlSize = \"32\" | \"40\" | \"48\";\n\n/**\n * Segment layout.\n * - `\"hug\"`: each segment is sized to its content.\n * - `\"fill\"`: the control spans the full width of its container and each segment grows equally.\n */\nexport type SegmentedControlVariant = \"hug\" | \"fill\";\n\n/**\n * Visual style of the control.\n * - `\"pill\"`: the container has a muted background and the selected segment shows a filled pill (default).\n * - `\"plain\"`: no container or selected-pill background; segments are bare content and selection is\n * communicated by color alone. Designed for icon-only toggles (e.g. a list/grid view switch).\n */\nexport type SegmentedControlAppearance = \"pill\" | \"plain\";\n\n/** Describes one selectable segment. */\nexport interface SegmentedControlOption {\n /**\n * Display label for the segment. When `icon` is provided, the segment renders icon-only and\n * this value is used as its accessible name (applied as `aria-label`) instead of visible text.\n */\n label: string;\n /** Value identifier returned via `onChange`. */\n value: string;\n /**\n * Icon to render instead of the visible label. When set, the segment renders icon-only and\n * `label` becomes required as its accessible name — it is applied as `aria-label` and no\n * visible text is rendered.\n */\n icon?: React.ReactNode;\n}\n\nexport interface SegmentedControlProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n /** Height of the control in pixels. @default \"32\" */\n size?: SegmentedControlSize;\n /** Segment layout. @default \"hug\" */\n variant?: SegmentedControlVariant;\n /** Visual style of the control. @default \"pill\" */\n appearance?: SegmentedControlAppearance;\n /** The selectable segments. Designed for two or three mutually exclusive options. */\n options: SegmentedControlOption[];\n /** Currently selected value (controlled). */\n value?: string;\n /** Initially selected value (uncontrolled). Defaults to the first option. */\n defaultValue?: string;\n /** Callback fired when the selected value changes. */\n onChange?: (value: string) => void;\n /** Whether the control is disabled. @default false */\n disabled?: boolean;\n}\n\n/** Padding and typography per size. Combined with the container's `p-1` these hit the 32/40/48px heights. */\nconst sizeClasses: Record<SegmentedControlSize, string> = {\n \"32\": \"px-2 py-1 typography-description-12px-semibold\",\n \"40\": \"px-3 py-1.75 typography-body-small-14px-semibold\",\n \"48\": \"px-4 py-2 typography-body-default-16px-semibold\",\n};\n\nfunction warnMissingAccessibleName(ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"SegmentedControl: no accessible name provided. Pass an `aria-label` or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction warnMissingOptionAccessibleName(options: SegmentedControlOption[]) {\n if (process.env.NODE_ENV !== \"production\") {\n for (const option of options) {\n if (option.icon && !option.label?.trim()) {\n console.warn(\n `SegmentedControl: icon-only segment \"${option.value}\" is missing a non-empty \\`label\\` to use as its accessible name.`,\n );\n }\n }\n }\n}\n\nfunction getSegmentClassName({\n appearance,\n size,\n variant,\n isSelected,\n disabled,\n}: {\n appearance: SegmentedControlAppearance;\n size: SegmentedControlSize;\n variant: SegmentedControlVariant;\n isSelected: boolean;\n disabled: boolean;\n}) {\n return cn(\n \"relative inline-flex min-w-0 cursor-pointer items-center justify-center rounded-full\",\n \"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out\",\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n variant === \"fill\" ? \"flex-1\" : \"shrink-0\",\n appearance === \"plain\"\n ? cn(\n // Padding + negative margin enlarge the hit target without changing the\n // visual footprint, which must stay glyph-only to match the design.\n \"-m-1 p-1\",\n isSelected ? \"text-icons-primary\" : \"text-icons-tertiary hover:text-icons-primary\",\n )\n : cn(\n sizeClasses[size],\n isSelected\n ? \"bg-buttons-primary-default text-content-primary-inverted shadow-sm\"\n : \"text-content-primary hover:bg-buttons-switch-hover\",\n ),\n disabled && \"pointer-events-none\",\n );\n}\n\n/**\n * A compact selector for choosing between two or three mutually exclusive\n * options where the choice affects the content immediately below it. Use\n * instead of tabs when the options are more like settings or filters than\n * navigation, such as toggling a list/grid view or a monthly/annual price.\n *\n * Rendered as a `radiogroup` with roving-tabindex keyboard navigation. Supports\n * both controlled and uncontrolled usage.\n *\n * @example\n * ```tsx\n * <SegmentedControl\n * options={[\n * { label: \"Net\", value: \"net\" },\n * { label: \"Gross\", value: \"gross\" },\n * ]}\n * value={amount}\n * onChange={setAmount}\n * aria-label=\"Amount type\"\n * />\n * ```\n *\n * @example Icon-only segments (e.g. a list/grid view toggle)\n * ```tsx\n * <SegmentedControl\n * appearance=\"plain\"\n * options={[\n * { label: \"List view\", value: \"list\", icon: <ListViewIcon size={16} /> },\n * { label: \"Grid view\", value: \"grid\", icon: <GridViewIcon size={16} /> },\n * ]}\n * value={view}\n * onChange={setView}\n * aria-label=\"View\"\n * />\n * ```\n */\nexport const SegmentedControl = React.forwardRef<HTMLDivElement, SegmentedControlProps>(\n (\n {\n className,\n size = \"32\",\n variant = \"hug\",\n appearance = \"pill\",\n options,\n value: controlledValue,\n defaultValue,\n onChange,\n disabled = false,\n ...props\n },\n ref,\n ) => {\n warnMissingAccessibleName(props[\"aria-label\"], props[\"aria-labelledby\"]);\n warnMissingOptionAccessibleName(options);\n\n // Tracks selection for uncontrolled usage; ignored when `value` prop is provided\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? options[0]?.value);\n const isControlled = controlledValue !== undefined;\n const currentValue = isControlled ? controlledValue : internalValue;\n const anySelected = options.some((o) => o.value === currentValue);\n const buttonRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n\n const handleSelect = (optionValue: string) => {\n if (disabled || optionValue === currentValue) return;\n if (!isControlled) {\n setInternalValue(optionValue);\n }\n onChange?.(optionValue);\n };\n\n const handleKeyDown = (e: React.KeyboardEvent, index: number) => {\n const nextIndex =\n e.key === \"ArrowRight\" || e.key === \"ArrowDown\"\n ? (index + 1) % options.length\n : e.key === \"ArrowLeft\" || e.key === \"ArrowUp\"\n ? (index - 1 + options.length) % options.length\n : null;\n if (nextIndex === null) return;\n e.preventDefault();\n const nextOption = options[nextIndex] as SegmentedControlOption;\n handleSelect(nextOption.value);\n buttonRefs.current[nextIndex]?.focus();\n };\n\n return (\n <div\n ref={ref}\n role=\"radiogroup\"\n className={cn(\n \"relative items-center rounded-full\",\n variant === \"fill\" ? \"flex w-full\" : \"inline-flex\",\n appearance === \"plain\" ? \"gap-2\" : \"bg-surface-tertiary p-1\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n {...props}\n >\n {options.map((option, index) => {\n const isSelected = currentValue === option.value;\n return (\n // biome-ignore lint/a11y/useSemanticElements: native radio inputs only allow Tab-focus on the checked item; buttons with roving tabindex give full keyboard navigation\n <button\n key={option.value}\n ref={(el) => {\n buttonRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={isSelected}\n tabIndex={isSelected || (!anySelected && index === 0) ? 0 : -1}\n disabled={disabled}\n aria-label={option.icon ? option.label : undefined}\n onClick={() => handleSelect(option.value)}\n onKeyDown={(e) => handleKeyDown(e, index)}\n className={getSegmentClassName({ appearance, size, variant, isSelected, disabled })}\n >\n {option.icon ? (\n <span className=\"flex shrink-0 items-center justify-center\" aria-hidden=\"true\">\n {option.icon}\n </span>\n ) : (\n <span className=\"min-w-0 truncate\">{option.label}</span>\n )}\n </button>\n );\n })}\n </div>\n );\n },\n);\n\nSegmentedControl.displayName = \"SegmentedControl\";\n"],"names":["cn","React","jsx"],"mappings":"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"}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -7,6 +7,7 @@ const AccordionContent = require("./components/Accordion/AccordionContent.cjs");
|
|
|
7
7
|
const AccordionItem = require("./components/Accordion/AccordionItem.cjs");
|
|
8
8
|
const AccordionTrigger = require("./components/Accordion/AccordionTrigger.cjs");
|
|
9
9
|
const Alert = require("./components/Alert/Alert.cjs");
|
|
10
|
+
const AudioPlayer = require("./components/AudioPlayer/AudioPlayer.cjs");
|
|
10
11
|
const AudioUpload = require("./components/AudioUpload/AudioUpload.cjs");
|
|
11
12
|
const useAudioRecorder = require("./components/AudioUpload/useAudioRecorder.cjs");
|
|
12
13
|
const Autocomplete = require("./components/Autocomplete/Autocomplete.cjs");
|
|
@@ -32,6 +33,7 @@ const Drawer = require("./components/Drawer/Drawer.cjs");
|
|
|
32
33
|
const DropdownMenu = require("./components/DropdownMenu/DropdownMenu.cjs");
|
|
33
34
|
const EmptyState = require("./components/EmptyState/EmptyState.cjs");
|
|
34
35
|
const FanFollowerCount = require("./components/FanFollowerCount/FanFollowerCount.cjs");
|
|
36
|
+
const FloatingActionButton = require("./components/FloatingActionButton/FloatingActionButton.cjs");
|
|
35
37
|
const IconButton = require("./components/IconButton/IconButton.cjs");
|
|
36
38
|
const AddIcon = require("./components/Icons/AddIcon.cjs");
|
|
37
39
|
const AffiliatesIcon = require("./components/Icons/AffiliatesIcon.cjs");
|
|
@@ -105,6 +107,7 @@ const GenderIcon = require("./components/Icons/GenderIcon.cjs");
|
|
|
105
107
|
const GifIcon = require("./components/Icons/GifIcon.cjs");
|
|
106
108
|
const GiftIcon = require("./components/Icons/GiftIcon.cjs");
|
|
107
109
|
const GoogleIcon = require("./components/Icons/GoogleIcon.cjs");
|
|
110
|
+
const GridViewIcon = require("./components/Icons/GridViewIcon.cjs");
|
|
108
111
|
const HealthIcon = require("./components/Icons/HealthIcon.cjs");
|
|
109
112
|
const HeartIcon = require("./components/Icons/HeartIcon.cjs");
|
|
110
113
|
const HelpIcon = require("./components/Icons/HelpIcon.cjs");
|
|
@@ -117,6 +120,7 @@ const InfoCircleIcon = require("./components/Icons/InfoCircleIcon.cjs");
|
|
|
117
120
|
const InfoIcon = require("./components/Icons/InfoIcon.cjs");
|
|
118
121
|
const LanguageIcon = require("./components/Icons/LanguageIcon.cjs");
|
|
119
122
|
const LinkIcon = require("./components/Icons/LinkIcon.cjs");
|
|
123
|
+
const ListViewIcon = require("./components/Icons/ListViewIcon.cjs");
|
|
120
124
|
const LocationIcon = require("./components/Icons/LocationIcon.cjs");
|
|
121
125
|
const LockerIcon = require("./components/Icons/LockerIcon.cjs");
|
|
122
126
|
const LockerOffIcon = require("./components/Icons/LockerOffIcon.cjs");
|
|
@@ -257,6 +261,7 @@ exports.AccordionContent = AccordionContent.AccordionContent;
|
|
|
257
261
|
exports.AccordionItem = AccordionItem.AccordionItem;
|
|
258
262
|
exports.AccordionTrigger = AccordionTrigger.AccordionTrigger;
|
|
259
263
|
exports.Alert = Alert.Alert;
|
|
264
|
+
exports.AudioPlayer = AudioPlayer.AudioPlayer;
|
|
260
265
|
exports.AudioUpload = AudioUpload.AudioUpload;
|
|
261
266
|
exports.useAudioRecorder = useAudioRecorder.useAudioRecorder;
|
|
262
267
|
exports.Autocomplete = Autocomplete.Autocomplete;
|
|
@@ -321,6 +326,7 @@ exports.DropdownMenuSeparator = DropdownMenu.DropdownMenuSeparator;
|
|
|
321
326
|
exports.DropdownMenuTrigger = DropdownMenu.DropdownMenuTrigger;
|
|
322
327
|
exports.EmptyState = EmptyState.EmptyState;
|
|
323
328
|
exports.FanFollowerCount = FanFollowerCount.FanFollowerCount;
|
|
329
|
+
exports.FloatingActionButton = FloatingActionButton.FloatingActionButton;
|
|
324
330
|
exports.IconButton = IconButton.IconButton;
|
|
325
331
|
exports.AddIcon = AddIcon.AddIcon;
|
|
326
332
|
exports.AffiliatesIcon = AffiliatesIcon.AffiliatesIcon;
|
|
@@ -394,6 +400,7 @@ exports.GenderIcon = GenderIcon.GenderIcon;
|
|
|
394
400
|
exports.GifIcon = GifIcon.GifIcon;
|
|
395
401
|
exports.GiftIcon = GiftIcon.GiftIcon;
|
|
396
402
|
exports.GoogleIcon = GoogleIcon.GoogleIcon;
|
|
403
|
+
exports.GridViewIcon = GridViewIcon.GridViewIcon;
|
|
397
404
|
exports.HealthIcon = HealthIcon.HealthIcon;
|
|
398
405
|
exports.HeartIcon = HeartIcon.HeartIcon;
|
|
399
406
|
exports.HelpIcon = HelpIcon.HelpIcon;
|
|
@@ -406,6 +413,7 @@ exports.InfoCircleIcon = InfoCircleIcon.InfoCircleIcon;
|
|
|
406
413
|
exports.InfoIcon = InfoIcon.InfoIcon;
|
|
407
414
|
exports.LanguageIcon = LanguageIcon.LanguageIcon;
|
|
408
415
|
exports.LinkIcon = LinkIcon.LinkIcon;
|
|
416
|
+
exports.ListViewIcon = ListViewIcon.ListViewIcon;
|
|
409
417
|
exports.LocationIcon = LocationIcon.LocationIcon;
|
|
410
418
|
exports.LockerIcon = LockerIcon.LockerIcon;
|
|
411
419
|
exports.LockerOffIcon = LockerOffIcon.LockerOffIcon;
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,340 @@
|
|
|
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 { PauseIcon } from "../Icons/PauseIcon.mjs";
|
|
6
|
+
import { PlayIcon } from "../Icons/PlayIcon.mjs";
|
|
7
|
+
const BAR_WIDTH_PX = 4;
|
|
8
|
+
const BAR_GAP_PX = 4;
|
|
9
|
+
const MIN_BAR_HEIGHT_PX = 4;
|
|
10
|
+
const MAX_BAR_HEIGHT_PX = 26;
|
|
11
|
+
const DEFAULT_BAR_COUNT = 24;
|
|
12
|
+
const RAW_PEAK_COUNT = 128;
|
|
13
|
+
const SEEK_STEP_SECONDS = 1;
|
|
14
|
+
const SEEK_STEP_LARGE_SECONDS = 5;
|
|
15
|
+
function formatTime(seconds) {
|
|
16
|
+
if (seconds === void 0 || !Number.isFinite(seconds)) return "--:--";
|
|
17
|
+
const totalSeconds = Math.max(0, Math.floor(seconds));
|
|
18
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
19
|
+
const remainingSeconds = totalSeconds % 60;
|
|
20
|
+
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
|
|
21
|
+
}
|
|
22
|
+
function hashString(value) {
|
|
23
|
+
let hash = 2166136261;
|
|
24
|
+
for (let i = 0; i < value.length; i++) {
|
|
25
|
+
hash ^= value.charCodeAt(i);
|
|
26
|
+
hash = Math.imul(hash, 16777619);
|
|
27
|
+
}
|
|
28
|
+
return hash >>> 0;
|
|
29
|
+
}
|
|
30
|
+
function createSeededRandom(seed) {
|
|
31
|
+
let state = seed;
|
|
32
|
+
return () => {
|
|
33
|
+
state = state + 1831565813 | 0;
|
|
34
|
+
let t = Math.imul(state ^ state >>> 15, 1 | state);
|
|
35
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
36
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function generateFallbackPeaks(src, count) {
|
|
40
|
+
const random = createSeededRandom(hashString(src));
|
|
41
|
+
return Array.from({ length: count }, () => 0.2 + random() * 0.8);
|
|
42
|
+
}
|
|
43
|
+
function computePeaksFromChannelData(channelData, count) {
|
|
44
|
+
const blockSize = Math.max(1, Math.floor(channelData.length / count));
|
|
45
|
+
const peaks = [];
|
|
46
|
+
for (let i = 0; i < count; i++) {
|
|
47
|
+
const start = i * blockSize;
|
|
48
|
+
let max = 0;
|
|
49
|
+
for (let j = 0; j < blockSize && start + j < channelData.length; j++) {
|
|
50
|
+
max = Math.max(max, Math.abs(channelData[start + j] ?? 0));
|
|
51
|
+
}
|
|
52
|
+
peaks.push(max);
|
|
53
|
+
}
|
|
54
|
+
return peaks;
|
|
55
|
+
}
|
|
56
|
+
function resamplePeaks(peaks, barCount) {
|
|
57
|
+
if (peaks.length === 0 || barCount <= 0) return [];
|
|
58
|
+
const step = peaks.length / barCount;
|
|
59
|
+
return Array.from(
|
|
60
|
+
{ length: barCount },
|
|
61
|
+
(_, i) => peaks[Math.min(peaks.length - 1, Math.floor(i * step))] ?? 0
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
async function decodeAudioPeaks(src, signal) {
|
|
65
|
+
const AudioContextCtor = window.AudioContext ?? window.webkitAudioContext;
|
|
66
|
+
if (!AudioContextCtor) throw new Error("WebAudio unsupported");
|
|
67
|
+
const response = await fetch(src, { signal });
|
|
68
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
69
|
+
const audioContext = new AudioContextCtor();
|
|
70
|
+
try {
|
|
71
|
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
72
|
+
return computePeaksFromChannelData(audioBuffer.getChannelData(0), RAW_PEAK_COUNT);
|
|
73
|
+
} finally {
|
|
74
|
+
if (audioContext.state !== "closed") audioContext.close().catch(() => {
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const AudioPlayer = React.forwardRef(
|
|
79
|
+
({
|
|
80
|
+
className,
|
|
81
|
+
src,
|
|
82
|
+
duration,
|
|
83
|
+
playing: controlledPlaying,
|
|
84
|
+
defaultPlaying = false,
|
|
85
|
+
onPlay,
|
|
86
|
+
onPause,
|
|
87
|
+
onEnded,
|
|
88
|
+
size = "40",
|
|
89
|
+
...props
|
|
90
|
+
}, ref) => {
|
|
91
|
+
const audioRef = React.useRef(null);
|
|
92
|
+
const trackRef = React.useRef(null);
|
|
93
|
+
const [internalPlaying, setInternalPlaying] = React.useState(defaultPlaying);
|
|
94
|
+
const isControlled = controlledPlaying !== void 0;
|
|
95
|
+
const playing = isControlled ? controlledPlaying : internalPlaying;
|
|
96
|
+
const [currentTime, setCurrentTime] = React.useState(0);
|
|
97
|
+
const [mediaDuration, setMediaDuration] = React.useState(void 0);
|
|
98
|
+
const [barCount, setBarCount] = React.useState(DEFAULT_BAR_COUNT);
|
|
99
|
+
const [peaks, setPeaks] = React.useState(
|
|
100
|
+
() => generateFallbackPeaks(src, RAW_PEAK_COUNT)
|
|
101
|
+
);
|
|
102
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
103
|
+
const displayDuration = mediaDuration ?? duration;
|
|
104
|
+
const hasStarted = playing || currentTime > 0;
|
|
105
|
+
const playingRef = React.useRef(playing);
|
|
106
|
+
playingRef.current = playing;
|
|
107
|
+
const isControlledRef = React.useRef(isControlled);
|
|
108
|
+
isControlledRef.current = isControlled;
|
|
109
|
+
const onPauseRef = React.useRef(onPause);
|
|
110
|
+
onPauseRef.current = onPause;
|
|
111
|
+
const isFirstSrcRenderRef = React.useRef(true);
|
|
112
|
+
React.useEffect(() => {
|
|
113
|
+
if (isFirstSrcRenderRef.current) {
|
|
114
|
+
isFirstSrcRenderRef.current = false;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
setCurrentTime(0);
|
|
118
|
+
setMediaDuration(void 0);
|
|
119
|
+
setPeaks(generateFallbackPeaks(src, RAW_PEAK_COUNT));
|
|
120
|
+
if (playingRef.current) {
|
|
121
|
+
if (!isControlledRef.current) setInternalPlaying(false);
|
|
122
|
+
onPauseRef.current?.();
|
|
123
|
+
}
|
|
124
|
+
}, [src]);
|
|
125
|
+
React.useEffect(() => {
|
|
126
|
+
let cancelled = false;
|
|
127
|
+
const abortController = new AbortController();
|
|
128
|
+
decodeAudioPeaks(src, abortController.signal).then((decodedPeaks) => {
|
|
129
|
+
if (!cancelled) setPeaks(decodedPeaks);
|
|
130
|
+
}).catch(() => {
|
|
131
|
+
if (!cancelled) setPeaks(generateFallbackPeaks(src, RAW_PEAK_COUNT));
|
|
132
|
+
});
|
|
133
|
+
return () => {
|
|
134
|
+
cancelled = true;
|
|
135
|
+
abortController.abort();
|
|
136
|
+
};
|
|
137
|
+
}, [src]);
|
|
138
|
+
React.useEffect(() => {
|
|
139
|
+
const track = trackRef.current;
|
|
140
|
+
if (!track) return;
|
|
141
|
+
const updateFromWidth = (width) => {
|
|
142
|
+
if (width <= 0) return;
|
|
143
|
+
setBarCount(Math.max(1, Math.floor((width + BAR_GAP_PX) / (BAR_WIDTH_PX + BAR_GAP_PX))));
|
|
144
|
+
};
|
|
145
|
+
updateFromWidth(track.getBoundingClientRect().width);
|
|
146
|
+
const observer = new ResizeObserver((entries) => {
|
|
147
|
+
const entry = entries[0];
|
|
148
|
+
if (entry) updateFromWidth(entry.contentRect.width);
|
|
149
|
+
});
|
|
150
|
+
observer.observe(track);
|
|
151
|
+
return () => observer.disconnect();
|
|
152
|
+
}, []);
|
|
153
|
+
React.useEffect(() => {
|
|
154
|
+
const audio = audioRef.current;
|
|
155
|
+
if (!audio) return;
|
|
156
|
+
if (playing) {
|
|
157
|
+
audio.play().catch(() => {
|
|
158
|
+
if (!isControlled) setInternalPlaying(false);
|
|
159
|
+
onPauseRef.current?.();
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
audio.pause();
|
|
163
|
+
}
|
|
164
|
+
}, [playing, isControlled]);
|
|
165
|
+
const handleToggle = () => {
|
|
166
|
+
const next = !playing;
|
|
167
|
+
if (!isControlled) setInternalPlaying(next);
|
|
168
|
+
if (next) onPlay?.();
|
|
169
|
+
else onPause?.();
|
|
170
|
+
};
|
|
171
|
+
const seekTo = React.useCallback(
|
|
172
|
+
(time) => {
|
|
173
|
+
if (displayDuration === void 0) return;
|
|
174
|
+
const clamped = Math.min(Math.max(time, 0), displayDuration);
|
|
175
|
+
setCurrentTime(clamped);
|
|
176
|
+
const audio = audioRef.current;
|
|
177
|
+
if (audio) audio.currentTime = clamped;
|
|
178
|
+
},
|
|
179
|
+
[displayDuration]
|
|
180
|
+
);
|
|
181
|
+
const seekFromClientX = React.useCallback(
|
|
182
|
+
(clientX) => {
|
|
183
|
+
const track = trackRef.current;
|
|
184
|
+
if (!track || displayDuration === void 0) return;
|
|
185
|
+
const rect = track.getBoundingClientRect();
|
|
186
|
+
if (rect.width <= 0) return;
|
|
187
|
+
const fraction = Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1);
|
|
188
|
+
seekTo(fraction * displayDuration);
|
|
189
|
+
},
|
|
190
|
+
[displayDuration, seekTo]
|
|
191
|
+
);
|
|
192
|
+
React.useEffect(() => {
|
|
193
|
+
if (!isDragging) return;
|
|
194
|
+
const handleWindowPointerMove = (event) => seekFromClientX(event.clientX);
|
|
195
|
+
const handleWindowPointerUp = () => setIsDragging(false);
|
|
196
|
+
window.addEventListener("pointermove", handleWindowPointerMove);
|
|
197
|
+
window.addEventListener("pointerup", handleWindowPointerUp);
|
|
198
|
+
return () => {
|
|
199
|
+
window.removeEventListener("pointermove", handleWindowPointerMove);
|
|
200
|
+
window.removeEventListener("pointerup", handleWindowPointerUp);
|
|
201
|
+
};
|
|
202
|
+
}, [isDragging, seekFromClientX]);
|
|
203
|
+
const handlePointerDown = (event) => {
|
|
204
|
+
seekFromClientX(event.clientX);
|
|
205
|
+
setIsDragging(true);
|
|
206
|
+
};
|
|
207
|
+
const handleKeyDown = (event) => {
|
|
208
|
+
if (displayDuration === void 0) return;
|
|
209
|
+
switch (event.key) {
|
|
210
|
+
case "ArrowRight":
|
|
211
|
+
case "ArrowUp":
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
seekTo(currentTime + SEEK_STEP_SECONDS);
|
|
214
|
+
break;
|
|
215
|
+
case "ArrowLeft":
|
|
216
|
+
case "ArrowDown":
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
seekTo(currentTime - SEEK_STEP_SECONDS);
|
|
219
|
+
break;
|
|
220
|
+
case "PageUp":
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
seekTo(currentTime + SEEK_STEP_LARGE_SECONDS);
|
|
223
|
+
break;
|
|
224
|
+
case "PageDown":
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
seekTo(currentTime - SEEK_STEP_LARGE_SECONDS);
|
|
227
|
+
break;
|
|
228
|
+
case "Home":
|
|
229
|
+
event.preventDefault();
|
|
230
|
+
seekTo(0);
|
|
231
|
+
break;
|
|
232
|
+
case "End":
|
|
233
|
+
event.preventDefault();
|
|
234
|
+
seekTo(displayDuration);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const bars = resamplePeaks(peaks, barCount);
|
|
239
|
+
const progressFraction = displayDuration && displayDuration > 0 ? Math.min(Math.max(currentTime / displayDuration, 0), 1) : 0;
|
|
240
|
+
const playedBarCount = hasStarted ? Math.round(progressFraction * bars.length) : bars.length;
|
|
241
|
+
const barData = bars.map((amplitude, index) => {
|
|
242
|
+
const isPlayed = index < playedBarCount;
|
|
243
|
+
return {
|
|
244
|
+
heightPx: MIN_BAR_HEIGHT_PX + amplitude * (MAX_BAR_HEIGHT_PX - MIN_BAR_HEIGHT_PX),
|
|
245
|
+
className: cn(
|
|
246
|
+
"w-1 shrink-0 rounded-3xs bg-messages-waveform-default",
|
|
247
|
+
"motion-safe:transition-opacity motion-safe:duration-150 motion-safe:ease-in-out",
|
|
248
|
+
isPlayed ? "opacity-100" : "opacity-50"
|
|
249
|
+
)
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
return /* @__PURE__ */ jsxs(
|
|
253
|
+
"div",
|
|
254
|
+
{
|
|
255
|
+
ref,
|
|
256
|
+
"data-size": size,
|
|
257
|
+
className: cn("flex w-full items-center gap-3", className),
|
|
258
|
+
...props,
|
|
259
|
+
children: [
|
|
260
|
+
/* @__PURE__ */ jsx(
|
|
261
|
+
"button",
|
|
262
|
+
{
|
|
263
|
+
type: "button",
|
|
264
|
+
onClick: handleToggle,
|
|
265
|
+
"aria-label": playing ? "Pause" : "Play",
|
|
266
|
+
className: cn(
|
|
267
|
+
"flex size-8 shrink-0 items-center justify-center rounded-full",
|
|
268
|
+
"bg-buttons-secondary-default text-icons-primary backdrop-blur-[40px]",
|
|
269
|
+
"motion-safe:transition-colors motion-safe:duration-150 motion-safe:ease-in-out",
|
|
270
|
+
"hover:bg-buttons-secondary-hover",
|
|
271
|
+
"focus-visible:shadow-focus-ring focus-visible:outline-none"
|
|
272
|
+
),
|
|
273
|
+
children: playing ? /* @__PURE__ */ jsx(PauseIcon, { size: 16, filled: true }) : /* @__PURE__ */ jsx(PlayIcon, { size: 16, filled: true })
|
|
274
|
+
}
|
|
275
|
+
),
|
|
276
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-3", children: [
|
|
277
|
+
/* @__PURE__ */ jsx(
|
|
278
|
+
"div",
|
|
279
|
+
{
|
|
280
|
+
ref: trackRef,
|
|
281
|
+
role: "slider",
|
|
282
|
+
tabIndex: 0,
|
|
283
|
+
"aria-label": "Seek",
|
|
284
|
+
"aria-orientation": "horizontal",
|
|
285
|
+
"aria-valuemin": 0,
|
|
286
|
+
"aria-valuemax": displayDuration,
|
|
287
|
+
"aria-valuenow": Math.round(currentTime),
|
|
288
|
+
"aria-valuetext": displayDuration !== void 0 ? `${formatTime(currentTime)} of ${formatTime(displayDuration)}` : formatTime(currentTime),
|
|
289
|
+
onPointerDown: handlePointerDown,
|
|
290
|
+
onKeyDown: handleKeyDown,
|
|
291
|
+
className: cn(
|
|
292
|
+
"flex h-10 min-w-0 flex-1 cursor-pointer touch-none select-none items-center gap-1 overflow-hidden",
|
|
293
|
+
"focus-visible:shadow-focus-ring focus-visible:outline-none"
|
|
294
|
+
),
|
|
295
|
+
children: barData.map((bar, index) => (
|
|
296
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: bars are a fixed-count, non-reorderable list
|
|
297
|
+
/* @__PURE__ */ jsx("span", { className: bar.className, style: { height: bar.heightPx } }, index)
|
|
298
|
+
))
|
|
299
|
+
}
|
|
300
|
+
),
|
|
301
|
+
/* @__PURE__ */ jsx("span", { className: "typography-body-small-14px-semibold flex shrink-0 items-center gap-0.5 whitespace-nowrap", children: hasStarted ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
302
|
+
/* @__PURE__ */ jsx("span", { className: "text-content-primary", children: formatTime(currentTime) }),
|
|
303
|
+
/* @__PURE__ */ jsxs("span", { className: "text-content-secondary", children: [
|
|
304
|
+
"/",
|
|
305
|
+
formatTime(displayDuration)
|
|
306
|
+
] })
|
|
307
|
+
] }) : /* @__PURE__ */ jsx("span", { className: "text-content-primary", children: formatTime(displayDuration) }) })
|
|
308
|
+
] }),
|
|
309
|
+
/* @__PURE__ */ jsx(
|
|
310
|
+
"audio",
|
|
311
|
+
{
|
|
312
|
+
ref: audioRef,
|
|
313
|
+
src,
|
|
314
|
+
preload: "metadata",
|
|
315
|
+
className: "hidden",
|
|
316
|
+
onLoadedMetadata: (event) => {
|
|
317
|
+
const { duration: nativeDuration } = event.currentTarget;
|
|
318
|
+
setMediaDuration(Number.isFinite(nativeDuration) ? nativeDuration : void 0);
|
|
319
|
+
},
|
|
320
|
+
onTimeUpdate: (event) => {
|
|
321
|
+
if (isDragging) return;
|
|
322
|
+
setCurrentTime(event.currentTarget.currentTime);
|
|
323
|
+
},
|
|
324
|
+
onEnded: () => {
|
|
325
|
+
setCurrentTime(0);
|
|
326
|
+
if (!isControlled) setInternalPlaying(false);
|
|
327
|
+
onEnded?.();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
AudioPlayer.displayName = "AudioPlayer";
|
|
337
|
+
export {
|
|
338
|
+
AudioPlayer
|
|
339
|
+
};
|
|
340
|
+
//# sourceMappingURL=AudioPlayer.mjs.map
|