aplance-ui 1.1.1 → 1.2.1

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.
@@ -1,14 +1,15 @@
1
1
  /* ── Container card ───────────────────────────────────────────── */
2
- .card {
3
- width: 100%;
4
- background-color: var(--color-background);
5
- border: var(--border-1) solid var(--color-border);
6
- border-radius: var(--radius-lg);
7
- padding: var(--spacing-7);
8
- display: flex;
9
- flex-direction: column;
10
- gap: var(--spacing-8);
11
- }
2
+ .card {
3
+ width: 100%;
4
+ min-width: 0;
5
+ background-color: var(--color-background);
6
+ border: var(--border-1) solid var(--color-border);
7
+ border-radius: var(--radius-lg);
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: var(--spacing-6);
11
+ padding: var(--spacing-6);
12
+ }
12
13
 
13
14
  .header {
14
15
  display: flex;
@@ -29,18 +30,19 @@
29
30
  color: var(--color-muted-foreground);
30
31
  }
31
32
 
32
- .canvas {
33
- width: 100%;
34
- position: relative;
35
- }
33
+ .canvas {
34
+ width: 100%;
35
+ min-width: 0;
36
+ position: relative;
37
+ }
36
38
 
37
- .plot {
38
- width: 100%;
39
- min-width: 0;
40
- display: flex;
41
- flex-direction: column;
42
- gap: var(--spacing-5);
43
- }
39
+ .plot {
40
+ width: 100%;
41
+ min-width: 0;
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: var(--spacing-4);
45
+ }
44
46
 
45
47
  .plot :global(.recharts-cartesian-axis-tick-value) {
46
48
  font-variant-numeric: tabular-nums;
@@ -70,10 +72,11 @@
70
72
  /* Match the page surface so series colors (designed against the page bg)
71
73
  stay readable inside the tooltip. Border defines the floating edge
72
74
  without relying on shadow alone. */
73
- .tooltip {
74
- min-width: 10rem;
75
- background-color: var(--color-background);
76
- color: var(--color-foreground);
75
+ .tooltip {
76
+ min-width: 10rem;
77
+ max-width: min(18rem, calc(100vw - var(--spacing-8)));
78
+ background-color: var(--color-background);
79
+ color: var(--color-foreground);
77
80
  border: var(--border-1) solid var(--color-border);
78
81
  border-radius: var(--radius-md);
79
82
  padding: var(--spacing-4) var(--spacing-5);
@@ -86,55 +89,68 @@
86
89
  margin-bottom: var(--spacing-2);
87
90
  }
88
91
 
89
- .tooltip_row {
90
- display: flex;
91
- align-items: center;
92
- gap: var(--spacing-3);
93
- font-size: var(--font-size-xs);
92
+ .tooltip_row {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: var(--spacing-3);
96
+ font-size: var(--font-size-xs);
97
+ min-width: 0;
94
98
 
95
99
  &:not(:last-child) {
96
100
  margin-bottom: var(--spacing-2);
97
101
  }
98
102
  }
99
103
 
100
- .tooltip_dot {
101
- width: 8px;
102
- height: 8px;
103
- border-radius: var(--radius-full);
104
- flex-shrink: 0;
105
- }
106
-
107
- .tooltip_value {
108
- margin-left: auto;
109
- font-weight: var(--font-weight-semibold);
110
- padding-left: var(--spacing-3);
111
- font-variant-numeric: tabular-nums;
112
- }
104
+ .tooltip_dot {
105
+ display: block;
106
+ width: 8px;
107
+ height: 8px;
108
+ border-radius: var(--radius-full);
109
+ flex-shrink: 0;
110
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-background) 84%, transparent);
111
+ }
112
+
113
+ .tooltip_value {
114
+ margin-left: auto;
115
+ font-weight: var(--font-weight-semibold);
116
+ padding-left: var(--spacing-3);
117
+ font-variant-numeric: tabular-nums;
118
+ white-space: nowrap;
119
+ }
113
120
 
114
121
  /* ── Legend (shared across all chart types) ──────────────────── */
115
- .legend {
116
- display: flex;
117
- flex-wrap: wrap;
118
- gap: var(--spacing-3) var(--spacing-6);
119
- justify-content: center;
120
- }
121
-
122
- .legend_item {
123
- display: inline-flex;
124
- align-items: center;
125
- gap: var(--spacing-2);
126
- font-size: var(--font-size-xs);
127
- font-weight: var(--font-weight-medium);
128
- color: var(--color-muted-foreground);
129
- white-space: nowrap;
130
- }
131
-
132
- .legend_dot {
133
- width: 8px;
134
- height: 8px;
135
- border-radius: var(--radius-full);
136
- flex-shrink: 0;
137
- }
122
+ .legend {
123
+ display: flex;
124
+ flex-wrap: wrap;
125
+ gap: var(--spacing-3) var(--spacing-5);
126
+ justify-content: center;
127
+ min-width: 0;
128
+ }
129
+
130
+ .legend_item {
131
+ display: inline-flex;
132
+ align-items: center;
133
+ gap: var(--spacing-2);
134
+ min-width: 0;
135
+ font-size: var(--font-size-xs);
136
+ font-weight: var(--font-weight-medium);
137
+ color: var(--color-muted-foreground);
138
+ line-height: 1.2;
139
+ }
140
+
141
+ .legend_dot {
142
+ display: block;
143
+ width: 8px;
144
+ height: 8px;
145
+ border-radius: var(--radius-full);
146
+ flex-shrink: 0;
147
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-background) 84%, transparent);
148
+ }
149
+
150
+ .legend_item > span:last-child {
151
+ overflow: hidden;
152
+ text-overflow: ellipsis;
153
+ }
138
154
 
139
155
  .center_overlay {
140
156
  position: absolute;
@@ -151,10 +167,10 @@
151
167
  }
152
168
 
153
169
  @media (width <= 600px) {
154
- .card {
155
- padding: var(--spacing-5);
156
- gap: var(--spacing-5);
157
- }
170
+ .card {
171
+ gap: var(--spacing-5);
172
+ padding: var(--spacing-5);
173
+ }
158
174
 
159
175
  .tooltip {
160
176
  min-width: 8rem;
@@ -77,9 +77,7 @@ export type {
77
77
  // ─── Constants ────────────────────────────────────────────────────────────────
78
78
 
79
79
  const DEFAULT_CHART_HEIGHT = 260;
80
- // left is intentionally negative: pulls Y-axis labels into the card's padding
81
- // so the visible whitespace on both sides feels balanced.
82
- const CARTESIAN_MARGIN = { top: 10, right: 10, bottom: 10, left: -8 } as const;
80
+ const CARTESIAN_MARGIN = { top: 8, right: 12, bottom: 4, left: 0 } as const;
83
81
  const YAXIS_WIDTH = 56;
84
82
  const YAXIS_WIDTH_HORIZONTAL = 80;
85
83
  const BAR_TOP_RADIUS: [number, number, number, number] = [6, 6, 0, 0];
@@ -114,6 +112,8 @@ type ChartTooltipProps = {
114
112
  // a single dataKey across all sectors), read the row label from the original
115
113
  // datum at this key so the tooltip shows "Aarav" instead of the dataKey.
116
114
  nameKey?: string;
115
+ /** Read a per-datum color from tooltip payload before falling back to series color. */
116
+ colorKey?: string;
117
117
  };
118
118
 
119
119
  type CartesianRenderOptions = {
@@ -160,6 +160,37 @@ type LegendItem = { key: string; label: string; color: string };
160
160
  const toLegendItems = (series: PreparedSeries[]): LegendItem[] =>
161
161
  series.map(({ key, label, color }) => ({ key, label, color }));
162
162
 
163
+ const toCategoryLegendItems = ({
164
+ data,
165
+ categoryKey,
166
+ colors,
167
+ fallbackColor,
168
+ }: {
169
+ data: ChartDatum[];
170
+ categoryKey: string;
171
+ colors?: Record<string, string>;
172
+ fallbackColor: string;
173
+ }): LegendItem[] => {
174
+ const seen = new Set<string>();
175
+ const items: LegendItem[] = [];
176
+
177
+ for (const entry of data) {
178
+ const rawLabel = entry[categoryKey];
179
+ const label = String(rawLabel ?? "Unknown");
180
+ const lookup = toLookupKey(rawLabel);
181
+ if (seen.has(lookup)) continue;
182
+
183
+ seen.add(lookup);
184
+ items.push({
185
+ key: lookup || label,
186
+ label,
187
+ color: resolveColorToken(colors?.[lookup], fallbackColor),
188
+ });
189
+ }
190
+
191
+ return items;
192
+ };
193
+
163
194
  // ─── Option resolution ────────────────────────────────────────────────────────
164
195
 
165
196
  const resolveCartesianOptions = (opts: CartesianRenderOptions) => ({
@@ -311,6 +342,11 @@ const formatTooltipValue = (value: TooltipValue): string => {
311
342
  return String(value);
312
343
  };
313
344
 
345
+ const toFiniteNumber = (value: unknown, fallback = 0) => {
346
+ const number = typeof value === "number" ? value : Number(value);
347
+ return Number.isFinite(number) ? number : fallback;
348
+ };
349
+
314
350
  const formatTooltipEntryValue = (
315
351
  value: TooltipValue,
316
352
  valueFormatter: TooltipValueFormatter | undefined,
@@ -331,6 +367,25 @@ const formatTooltipLabel = (
331
367
  return String(label);
332
368
  };
333
369
 
370
+ const payloadString = (
371
+ payload: Record<string, unknown> | undefined,
372
+ key: string,
373
+ ) => {
374
+ const value = payload?.[key];
375
+ return typeof value === "string" && value.trim() ? value : undefined;
376
+ };
377
+
378
+ const resolveTooltipColor = (
379
+ entry: TooltipPayloadItem,
380
+ colorKey: string | undefined,
381
+ ) =>
382
+ (colorKey ? payloadString(entry.payload, colorKey) : undefined) ??
383
+ payloadString(entry.payload, "__color") ??
384
+ payloadString(entry.payload, "fill") ??
385
+ payloadString(entry.payload, "stroke") ??
386
+ entry.color ??
387
+ "var(--color-muted-foreground)";
388
+
334
389
  function ChartTooltip({
335
390
  active,
336
391
  payload,
@@ -338,6 +393,7 @@ function ChartTooltip({
338
393
  valueFormatter,
339
394
  labelFormatter,
340
395
  nameKey,
396
+ colorKey,
341
397
  }: ChartTooltipProps) {
342
398
  if (!active || !payload?.length) return null;
343
399
 
@@ -354,7 +410,10 @@ function ChartTooltip({
354
410
  key={`${entry.dataKey ?? entry.name ?? "value"}-${index}`}
355
411
  className={s.tooltip_row}
356
412
  >
357
- <span className={s.tooltip_dot} style={{ backgroundColor: entry.color }} />
413
+ <span
414
+ className={s.tooltip_dot}
415
+ style={{ backgroundColor: resolveTooltipColor(entry, colorKey) }}
416
+ />
358
417
  <span>{String(rowName)}</span>
359
418
  <span className={s.tooltip_value}>
360
419
  {formatTooltipEntryValue(entry.value, valueFormatter, {
@@ -373,11 +432,11 @@ function ChartTooltip({
373
432
 
374
433
  function ChartLegend({ items }: { items: LegendItem[] }) {
375
434
  return (
376
- <div className={s.legend}>
377
- {items.map((item) => (
378
- <div key={item.key} className={s.legend_item}>
435
+ <div className={s.legend} aria-label="Chart legend">
436
+ {items.map((item, index) => (
437
+ <div key={`${item.key}-${index}`} className={s.legend_item}>
379
438
  <span className={s.legend_dot} style={{ backgroundColor: item.color }} />
380
- <span>{item.label}</span>
439
+ <span title={item.label}>{item.label}</span>
381
440
  </div>
382
441
  ))}
383
442
  </div>
@@ -394,6 +453,7 @@ const renderCartesianFrame = ({
394
453
  options,
395
454
  axisConfig,
396
455
  tooltipCursor,
456
+ tooltipColorKey,
397
457
  orientation = "vertical",
398
458
  }: {
399
459
  categoryKey: string;
@@ -401,6 +461,7 @@ const renderCartesianFrame = ({
401
461
  options: ResolvedCartesianOptions;
402
462
  axisConfig: AxisRenderConfig;
403
463
  tooltipCursor: TooltipCursor;
464
+ tooltipColorKey?: string;
404
465
  orientation?: "vertical" | "horizontal";
405
466
  }): ReactNode[] => {
406
467
  const isHorizontal = orientation === "horizontal";
@@ -496,6 +557,7 @@ const renderCartesianFrame = ({
496
557
  <ChartTooltip
497
558
  valueFormatter={options.tooltipValueFormatter}
498
559
  labelFormatter={options.tooltipLabelFormatter}
560
+ colorKey={tooltipColorKey}
499
561
  />
500
562
  }
501
563
  cursor={tooltipCursor}
@@ -579,14 +641,30 @@ function BarChartRenderer({
579
641
  const topStackSeriesKeys = resolveStackTopSeriesKeys(preparedSeries);
580
642
  const usesCategoryColorMap =
581
643
  preparedSeries.length === 1 && !series?.length && Boolean(colors);
582
- const legendItems = toLegendItems(preparedSeries);
644
+ const legendItems = usesCategoryColorMap
645
+ ? toCategoryLegendItems({
646
+ data,
647
+ categoryKey,
648
+ colors,
649
+ fallbackColor: preparedSeries[0]?.color ?? palette.primary,
650
+ })
651
+ : toLegendItems(preparedSeries);
583
652
  const isHorizontal = orientation === "horizontal";
653
+ const chartData: Array<ChartDatum & { __color?: string }> = usesCategoryColorMap
654
+ ? data.map((entry) => {
655
+ const key = toLookupKey(entry[categoryKey]);
656
+ return {
657
+ ...entry,
658
+ __color: resolveColorToken(colors?.[key], preparedSeries[0]?.color ?? palette.primary),
659
+ };
660
+ })
661
+ : data;
584
662
 
585
663
  return (
586
664
  <>
587
665
  <ResponsiveContainer width="100%" height={height}>
588
666
  <RechartsBarChart
589
- data={data}
667
+ data={chartData}
590
668
  layout={isHorizontal ? "vertical" : "horizontal"}
591
669
  margin={CARTESIAN_MARGIN}
592
670
  {...(barCategoryGap !== undefined ? { barCategoryGap } : {})}
@@ -597,6 +675,7 @@ function BarChartRenderer({
597
675
  options,
598
676
  axisConfig,
599
677
  tooltipCursor: { fill: palette.cursor },
678
+ tooltipColorKey: usesCategoryColorMap ? "__color" : undefined,
600
679
  orientation,
601
680
  })}
602
681
 
@@ -612,9 +691,10 @@ function BarChartRenderer({
612
691
  yAxisId={options.showYAxis ? item.yAxisId : undefined}
613
692
  >
614
693
  {usesCategoryColorMap &&
615
- data.map((entry, index) => {
694
+ chartData.map((entry, index) => {
616
695
  const key = toLookupKey(entry[categoryKey]);
617
- const fill = resolveColorToken(colors?.[key], item.color);
696
+ const fill =
697
+ typeof entry.__color === "string" ? entry.__color : item.color;
618
698
  return <Cell key={`${item.key}-${key}-${index}`} fill={fill} />;
619
699
  })}
620
700
  </Bar>
@@ -967,29 +1047,28 @@ function DonutChartRenderer({
967
1047
  }: DonutChartProps & { height: number }) {
968
1048
  const palette = createChartPalette();
969
1049
 
970
- const chartData = data.map((entry) => ({
971
- ...entry,
972
- __chartValue:
973
- typeof entry[dataKey] === "number"
974
- ? (entry[dataKey] as number)
975
- : Number(entry[dataKey] ?? 0) || 0,
976
- }));
977
-
978
- const legendItems: LegendItem[] = data.map((entry, index) => {
1050
+ const chartData = data.map((entry, index) => {
979
1051
  const rawLabel = entry[nameKey];
980
1052
  const label =
981
1053
  rawLabel !== undefined && rawLabel !== null && tooltipLabelFormatter
982
1054
  ? tooltipLabelFormatter(rawLabel as string | number)
983
1055
  : String(rawLabel ?? `Item ${index + 1}`);
984
1056
  const lookup = toLookupKey(rawLabel);
1057
+ const color = resolveColorToken(colors?.[lookup], pickSeriesColor(palette, index));
985
1058
  return {
986
- key: label,
987
- label,
988
- // Fix: use series color rotation, not palette.muted, for uncolored segments
989
- color: resolveColorToken(colors?.[lookup], pickSeriesColor(palette, index)),
1059
+ ...entry,
1060
+ __chartValue: Math.max(0, toFiniteNumber(entry[dataKey])),
1061
+ __color: color,
1062
+ __label: label,
990
1063
  };
991
1064
  });
992
1065
 
1066
+ const legendItems: LegendItem[] = chartData.map((entry) => ({
1067
+ key: entry.__label,
1068
+ label: entry.__label,
1069
+ color: entry.__color,
1070
+ }));
1071
+
993
1072
  const resolvedOuterRadius = Math.max(outerRadius ?? Math.max(height * 0.34, 88), 36);
994
1073
  const resolvedInnerRadius = Math.max(
995
1074
  16,
@@ -1024,6 +1103,8 @@ function DonutChartRenderer({
1024
1103
  <ChartTooltip
1025
1104
  valueFormatter={tooltipValueFormatter}
1026
1105
  labelFormatter={tooltipLabelFormatter}
1106
+ nameKey="__label"
1107
+ colorKey="__color"
1027
1108
  />
1028
1109
  }
1029
1110
  />
@@ -1065,9 +1146,7 @@ function RadialBarChartRenderer({
1065
1146
  const lookup = toLookupKey(rawLabel);
1066
1147
  const color = resolveColorToken(colors?.[lookup], pickSeriesColor(palette, index));
1067
1148
  const value =
1068
- typeof entry[dataKey] === "number"
1069
- ? (entry[dataKey] as number)
1070
- : Number(entry[dataKey] ?? 0) || 0;
1149
+ Math.max(0, toFiniteNumber(entry[dataKey]));
1071
1150
  return {
1072
1151
  ...entry,
1073
1152
  __value: value,
@@ -1082,7 +1161,10 @@ function RadialBarChartRenderer({
1082
1161
  color: item.__color,
1083
1162
  }));
1084
1163
 
1085
- const domainMax = maxValue ?? Math.max(...chartData.map((d) => d.__value), 1);
1164
+ const domainMax =
1165
+ typeof maxValue === "number" && Number.isFinite(maxValue) && maxValue > 0
1166
+ ? maxValue
1167
+ : Math.max(...chartData.map((d) => d.__value), 1);
1086
1168
 
1087
1169
  return (
1088
1170
  <>
@@ -1111,7 +1193,8 @@ function RadialBarChartRenderer({
1111
1193
  <ChartTooltip
1112
1194
  valueFormatter={tooltipValueFormatter}
1113
1195
  labelFormatter={tooltipLabelFormatter}
1114
- nameKey={nameKey}
1196
+ nameKey="__label"
1197
+ colorKey="__color"
1115
1198
  />
1116
1199
  }
1117
1200
  />
@@ -1228,7 +1311,10 @@ const EMPTY_STATE_GRACE_MS = 150;
1228
1311
 
1229
1312
  export function Chart(props: ChartProps) {
1230
1313
  const { title, description, className, emptyMessage, isLoading = false } = props;
1231
- const height = props.height ?? DEFAULT_CHART_HEIGHT;
1314
+ const height =
1315
+ typeof props.height === "number" && Number.isFinite(props.height)
1316
+ ? Math.max(120, props.height)
1317
+ : DEFAULT_CHART_HEIGHT;
1232
1318
 
1233
1319
  const isEmpty =
1234
1320
  props.type === "scatter"
@@ -92,6 +92,7 @@
92
92
 
93
93
  .dropzone_inner {
94
94
  display: flex;
95
+ min-width: 0;
95
96
  flex-direction: column;
96
97
  align-items: center;
97
98
  justify-content: center;
@@ -107,24 +108,30 @@
107
108
 
108
109
  .dropzone_copy {
109
110
  display: flex;
111
+ max-width: 100%;
112
+ min-width: 0;
110
113
  flex-direction: column;
111
114
  gap: var(--spacing-2);
112
115
  align-items: center;
113
116
  }
114
117
 
115
118
  .dropzone_title {
119
+ max-width: 100%;
116
120
  font-family: var(--font-family-1);
117
121
  font-size: var(--font-size-sm);
118
122
  color: var(--field-content-color, var(--color-foreground));
119
123
  font-weight: 500;
120
124
  line-height: 1.3;
125
+ overflow-wrap: anywhere;
121
126
  }
122
127
 
123
128
  .dropzone_hint {
129
+ max-width: 100%;
124
130
  font-family: var(--font-family-1);
125
131
  font-size: var(--font-size-xs);
126
132
  color: var(--field-placeholder-color, var(--color-muted-foreground));
127
133
  line-height: 1.4;
134
+ overflow-wrap: anywhere;
128
135
  }
129
136
 
130
137
  /* ── Preview list ─────────────────────────────────────────────────────── */
@@ -272,18 +279,23 @@
272
279
 
273
280
  .preview_sub {
274
281
  display: inline-flex;
282
+ min-width: 0;
275
283
  align-items: center;
276
284
  gap: var(--spacing-1);
277
285
  font-family: var(--font-family-1);
278
286
  font-size: var(--font-size-xs);
279
287
  color: var(--color-muted-foreground);
280
288
  line-height: 1.3;
289
+ overflow: hidden;
290
+ text-overflow: ellipsis;
291
+ white-space: nowrap;
281
292
  }
282
293
 
283
294
  .preview_error {
284
295
  font-family: var(--font-family-1);
285
296
  font-size: var(--font-size-xs);
286
297
  color: var(--color-error-foreground);
298
+ overflow-wrap: anywhere;
287
299
  }
288
300
 
289
301
  /* Position only - visuals come from the Button component (variant + size + box layout). */
@@ -109,15 +109,12 @@ export const MediaInput = (props: MediaInputProps) => {
109
109
  const resolvedTitle =
110
110
  dropzoneTitle ??
111
111
  (multiple ? "Drop files here or browse" : "Drop a file here or browse");
112
- const resolvedHint =
113
- dropzoneHint ??
114
- [
115
- accept ? `Accepts ${accept}` : null,
116
- maxSize ? `Up to ${formatBytes(maxSize)} per file` : null,
117
- multiple && maxFiles ? `Max ${maxFiles} files` : null,
118
- ]
119
- .filter(Boolean)
120
- .join(" • ");
112
+ const hintParts = [
113
+ accept ? `Accepts ${accept}` : null,
114
+ maxSize ? `Up to ${formatBytes(maxSize)} per file` : null,
115
+ multiple && maxFiles ? `Max ${maxFiles} files` : null,
116
+ ].filter(Boolean);
117
+ const resolvedHint = dropzoneHint ?? hintParts.join(" - ");
121
118
 
122
119
  const inputProps = getInputProps({
123
120
  id: inputId,
@@ -210,6 +207,10 @@ export const MediaInput = (props: MediaInputProps) => {
210
207
  "aria-labelledby": label ? labelId : undefined,
211
208
  "aria-describedby": resolvedHint ? hintId : undefined,
212
209
  onClick: dropzoneSelectable ? undefined : (e) => e.preventDefault(),
210
+ onDragEnter: dropzoneSelectable ? undefined : (e) => e.preventDefault(),
211
+ onDragLeave: dropzoneSelectable ? undefined : (e) => e.preventDefault(),
212
+ onDragOver: dropzoneSelectable ? undefined : (e) => e.preventDefault(),
213
+ onDrop: dropzoneSelectable ? undefined : (e) => e.preventDefault(),
213
214
  });
214
215
 
215
216
  return (
@@ -96,6 +96,12 @@ export function useMediaUpload({
96
96
  filesRef.current = isControlled ? value! : internal;
97
97
  }, [isControlled, value, internal]);
98
98
 
99
+ useEffect(() => {
100
+ if (!disabled && !readOnly) return;
101
+ dragCounter.current = 0;
102
+ setIsDragging(false);
103
+ }, [disabled, readOnly]);
104
+
99
105
  // Revoke any preview URLs the hook created on unmount so blob memory is
100
106
  // freed even if the consumer forgets to clear state.
101
107
  useEffect(
@@ -130,8 +136,11 @@ export function useMediaUpload({
130
136
  existing: baseExisting,
131
137
  });
132
138
 
133
- // Single-mode replacement frees the previous preview blob immediately.
134
- if (!multiple && existing.length > 0) existing.forEach(revokePreviewUrl);
139
+ // Single-mode replacement frees the previous preview blob immediately,
140
+ // but only after a valid replacement exists.
141
+ if (!multiple && accepted.length > 0 && existing.length > 0) {
142
+ existing.forEach(revokePreviewUrl);
143
+ }
135
144
 
136
145
  const next = multiple ? [...existing, ...accepted] : accepted;
137
146
  if (accepted.length > 0) {
@@ -162,6 +171,7 @@ export function useMediaUpload({
162
171
  revokePreviewUrl(target);
163
172
  const next = filesRef.current.filter((f) => f.id !== id);
164
173
  commit(next);
174
+ setRejections([]);
165
175
  onRemove?.(target);
166
176
  },
167
177
  [commit, disabled, onRemove, readOnly],
@@ -190,9 +200,9 @@ export function useMediaUpload({
190
200
 
191
201
  const handleDragEnter = useCallback(
192
202
  (e: DragEvent) => {
193
- if (disabled || readOnly) return;
194
203
  e.preventDefault();
195
204
  e.stopPropagation();
205
+ if (disabled || readOnly) return;
196
206
  dragCounter.current += 1;
197
207
  if (e.dataTransfer?.types?.includes("Files")) setIsDragging(true);
198
208
  },
@@ -208,9 +218,9 @@ export function useMediaUpload({
208
218
 
209
219
  const handleDragOver = useCallback(
210
220
  (e: DragEvent) => {
211
- if (disabled || readOnly) return;
212
221
  e.preventDefault();
213
222
  e.stopPropagation();
223
+ if (disabled || readOnly) return;
214
224
  if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
215
225
  },
216
226
  [disabled, readOnly],
@@ -218,11 +228,11 @@ export function useMediaUpload({
218
228
 
219
229
  const handleDrop = useCallback(
220
230
  (e: DragEvent) => {
221
- if (disabled || readOnly) return;
222
231
  e.preventDefault();
223
232
  e.stopPropagation();
224
233
  dragCounter.current = 0;
225
234
  setIsDragging(false);
235
+ if (disabled || readOnly) return;
226
236
  add(e.dataTransfer?.files);
227
237
  },
228
238
  [add, disabled, readOnly],
@@ -247,18 +257,22 @@ export function useMediaUpload({
247
257
  },
248
258
  onDragEnter: (e) => {
249
259
  overrides.onDragEnter?.(e);
260
+ if (e.defaultPrevented) return;
250
261
  handleDragEnter(e);
251
262
  },
252
263
  onDragLeave: (e) => {
253
264
  overrides.onDragLeave?.(e);
265
+ if (e.defaultPrevented) return;
254
266
  handleDragLeave(e);
255
267
  },
256
268
  onDragOver: (e) => {
257
269
  overrides.onDragOver?.(e);
270
+ if (e.defaultPrevented) return;
258
271
  handleDragOver(e);
259
272
  },
260
273
  onDrop: (e) => {
261
274
  overrides.onDrop?.(e);
275
+ if (e.defaultPrevented) return;
262
276
  handleDrop(e);
263
277
  },
264
278
  }),