@parhelia/localization 0.4.12796 → 0.4.12800

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 +1 @@
1
- {"version":3,"file":"TranslationBatches.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationBatches.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,MAAM,OAAO,CAAC;AAyV1B,wBAAgB,kBAAkB,sBAwnCjC;AAED,eAAO,MAAM,oCAAoC,8BAA8B,CAAC;AAEhF,wBAAgB,8BAA8B,sBA0Y7C"}
1
+ {"version":3,"file":"TranslationBatches.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationBatches.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,MAAM,OAAO,CAAC;AAyV1B,wBAAgB,kBAAkB,sBAspCjC;AAED,eAAO,MAAM,oCAAoC,8BAA8B,CAAC;AAEhF,wBAAgB,8BAA8B,sBA0Y7C"}
@@ -798,8 +798,13 @@ export function TranslationBatches() {
798
798
  const anyError = errorJobs > 0;
799
799
  const anyInProgress = isActiveBatchStatus(info?.status);
800
800
  const anyAborted = info?.status === "Aborted";
801
+ const jobs = batchJobs[b.id];
802
+ const statusCounts = computeBatchStatusCounts(info, jobs);
803
+ const processedJobs = statusCounts.succeeded +
804
+ statusCounts.failed +
805
+ statusCounts.aborted;
801
806
  const metaParts = [];
802
- metaParts.push(_jsxs("span", { className: "text-neutral-grey-100", children: [totalJobs, " translation", totalJobs !== 1 ? "s" : ""] }, "trans"));
807
+ metaParts.push(_jsx("span", { className: "text-neutral-grey-100", children: info?.status === "Pending" ? (_jsxs(_Fragment, { children: [totalJobs, " translation", totalJobs !== 1 ? "s" : "", " queued"] })) : anyInProgress ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "tabular-nums", children: [processedJobs, " of ", totalJobs] }), " ", "translation", totalJobs !== 1 ? "s" : "", " done"] })) : (_jsxs(_Fragment, { children: [totalJobs, " translation", totalJobs !== 1 ? "s" : ""] })) }, "trans"));
803
808
  if (typeof itemsCount === "number") {
804
809
  metaParts.push(_jsxs("span", { children: [itemsCount, " item", itemsCount !== 1 ? "s" : ""] }, "items"));
805
810
  }
@@ -850,8 +855,6 @@ export function TranslationBatches() {
850
855
  `Translation Batch · ${formattedBatchDate}`);
851
856
  })();
852
857
  const isExpanded = expandedBatchId === b.id;
853
- const jobs = batchJobs[b.id];
854
- const statusCounts = computeBatchStatusCounts(info, jobs);
855
858
  const isLoadingThis = loadingJobs.has(b.id);
856
859
  const canRetry = anyError;
857
860
  const canAbort = isActiveBatchStatus(info?.status);
@@ -880,19 +883,19 @@ export function TranslationBatches() {
880
883
  if (!canAbort)
881
884
  return;
882
885
  handleAbortBatch(b.id);
883
- }, icon: _jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}`, strokeWidth: 1 }), label: "Abort translation batch", disabled: !canAbort || isAborting }))] }));
886
+ }, icon: isAborting ? (_jsx(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin", strokeWidth: 1 })) : (_jsx(AbortIcon, { className: "h-3.5 w-3.5", strokeWidth: 1 })), label: "Abort translation batch", disabled: !canAbort || isAborting }))] }));
884
887
  return (_jsxs("div", { "data-testid": `batch-row-${b.id}`, "data-batch-status": testStatus, children: [_jsx("div", { role: "button", tabIndex: 0, className: `group w-full cursor-pointer py-3 text-left transition-colors hover:bg-neutral-grey-5 ${isCompactBatches ? "px-2.5" : "px-4"}`, onClick: () => toggleBatch(b.id), onKeyDown: (e) => {
885
888
  if (e.key === "Enter" || e.key === " ") {
886
889
  e.preventDefault();
887
890
  toggleBatch(b.id);
888
891
  }
889
- }, children: _jsxs("div", { className: `flex items-start ${isCompactBatches ? "gap-2" : "gap-4"}`, children: [_jsx("div", { className: `shrink-0 text-neutral-grey-100 transition-transform ${isCompactBatches ? "mt-0.5" : "mt-3"} ${isExpanded ? "rotate-180" : ""}`, children: _jsx(ChevronDownIcon, { className: "h-4 w-4" }) }), !isCompactBatches && totalJobs > 0 && (_jsx(BatchProgressIndicator, { counts: statusCounts, testId: `batch-progress-${b.id}` })), _jsxs("div", { className: `flex min-w-0 flex-1 ${isCompactBatches
892
+ }, children: _jsxs("div", { className: `flex items-start ${isCompactBatches ? "gap-2" : "gap-4"}`, children: [_jsx("div", { className: `shrink-0 text-neutral-grey-100 transition-transform ${isCompactBatches ? "mt-0.5" : "mt-3"} ${isExpanded ? "rotate-180" : ""}`, children: _jsx(ChevronDownIcon, { className: "h-4 w-4" }) }), !isCompactBatches && totalJobs > 0 && (_jsx(BatchProgressIndicator, { counts: statusCounts, status: info?.status, testId: `batch-progress-${b.id}` })), _jsxs("div", { className: `flex min-w-0 flex-1 ${isCompactBatches
890
893
  ? "flex-col gap-2"
891
894
  : "items-center justify-between gap-3"}`, children: [_jsxs("div", { className: "grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)]", children: [_jsx("span", { className: `min-w-0 text-[14px] font-medium text-neutral-grey-100 ${isCompactBatches
892
895
  ? "whitespace-normal break-words leading-5"
893
896
  : "truncate"}`, style: isCompactBatches
894
897
  ? { overflowWrap: "anywhere" }
895
- : undefined, title: formattedBatchDate, children: batchDisplayName }), _jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: desktopMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, i))) }), mobileFooterMetaParts.length > 0 && (_jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: mobileFooterMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, `footer-${i}`))) }))] }), isCompactBatches && (_jsxs("div", { className: "mt-2 flex min-w-0 items-center gap-2", children: [totalJobs > 0 && (_jsx("div", { className: "min-w-0 flex-1", children: _jsx(BatchProgressIndicator, { counts: statusCounts, testId: `batch-progress-${b.id}`, variant: "bar" }) })), hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: batchActions }))] })), !isCompactBatches && hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-2 self-center", children: batchActions }))] })] }) }), isExpanded && (_jsxs("div", { className: "border-t border-border-default/50", children: [_jsx(CustomPromptPanel, { prompts: parseCustomPrompts(info?.metadata) }), _jsx(BatchItemList, { batchId: b.id, batchProvider: info?.provider, jobs: jobs, isLoading: isLoadingThis, expandedItems: expandedItems, languages: sitecoreLanguages, currentLanguage: editContext?.item?.language, itemFilter: filters.itemIdOrPath.trim(), itemIncludeSubitems: filters.itemIncludeSubitems, statusFilter: filters.status, batchIsTerminal: isTerminalBatchStatus(info?.status), batchStartedAtUtc: info?.startedAtUtc ?? info?.createdAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, isMobile: isMobile, onToggleItem: toggleItem, onOpenItem: openItemInEditor, onAbortJob: handleAbortJob, onRetryJobs: handleRetryJobs })] }))] }, b.id));
898
+ : undefined, title: formattedBatchDate, children: batchDisplayName }), _jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: desktopMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, i))) }), mobileFooterMetaParts.length > 0 && (_jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: mobileFooterMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, `footer-${i}`))) }))] }), isCompactBatches && (_jsxs("div", { className: "mt-2 flex min-w-0 items-center gap-2", children: [totalJobs > 0 && (_jsx("div", { className: "min-w-0 flex-1", children: _jsx(BatchProgressIndicator, { counts: statusCounts, status: info?.status, testId: `batch-progress-${b.id}`, variant: "bar" }) })), hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: batchActions }))] })), !isCompactBatches && hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-2 self-center", children: batchActions }))] })] }) }), isExpanded && (_jsxs("div", { className: "border-t border-border-default/50", children: [_jsx(CustomPromptPanel, { prompts: parseCustomPrompts(info?.metadata) }), _jsx(BatchItemList, { batchId: b.id, batchProvider: info?.provider, jobs: jobs, isLoading: isLoadingThis, expandedItems: expandedItems, languages: sitecoreLanguages, currentLanguage: editContext?.item?.language, itemFilter: filters.itemIdOrPath.trim(), itemIncludeSubitems: filters.itemIncludeSubitems, statusFilter: filters.status, batchIsTerminal: isTerminalBatchStatus(info?.status), batchStartedAtUtc: info?.startedAtUtc ?? info?.createdAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, isMobile: isMobile, onToggleItem: toggleItem, onOpenItem: openItemInEditor, onAbortJob: handleAbortJob, onRetryJobs: handleRetryJobs })] }))] }, b.id));
896
899
  }) })] }, dateGroup.label))), hasMore && (_jsx("div", { className: "flex justify-center pt-6", children: _jsx(Button, { variant: "outline", size: "sm", onClick: loadMore, disabled: isLoadingMore, children: isLoadingMore ? (_jsxs(_Fragment, { children: [_jsx(LoaderIcon, { className: "h-4 w-4 animate-spin text-highlight-100" }), "Loading more..."] })) : (_jsxs(_Fragment, { children: [_jsx(ChevronDownIcon, { className: "h-4 w-4" }), "Load More Batches"] })) }) }))] })) }) })] }));
897
900
  }
898
901
  export const TRANSLATION_BATCH_FILTERS_SIDEBAR_ID = "translation-batch-filters";
@@ -1206,22 +1209,66 @@ function computeBatchStatusCounts(info, jobs) {
1206
1209
  const pending = info?.status === "Aborted" ? 0 : remaining;
1207
1210
  return { total, succeeded, failed, aborted, running: 0, pending };
1208
1211
  }
1209
- function BatchProgressIndicator({ counts, testId, variant = "circle", }) {
1210
- const { total, succeeded, failed, aborted, running } = counts;
1212
+ // One outcome per batch, shared by the ring, the bar and the status badge so
1213
+ // the three never disagree about what colour/state a batch is in.
1214
+ function resolveBatchOutcome(status, counts) {
1215
+ const { total, succeeded, failed } = counts;
1216
+ if (isActiveBatchStatus(status))
1217
+ return "active";
1218
+ if (total > 0 && succeeded === total)
1219
+ return "succeeded";
1220
+ if (total > 0 && failed === total)
1221
+ return "failed";
1222
+ if (status === "Aborted")
1223
+ return "aborted";
1224
+ return "mixed";
1225
+ }
1226
+ function BatchProgressIndicator({ counts, status, testId, variant = "circle", }) {
1227
+ const { total, succeeded, failed, aborted, running, pending } = counts;
1228
+ const outcome = resolveBatchOutcome(status, counts);
1229
+ const isActive = outcome === "active";
1230
+ const processed = succeeded + failed + aborted;
1231
+ // SVG filter ids are document-global; derive a safe one per instance.
1232
+ const glowId = `batch-glow-${React.useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
1233
+ // While the batch is running the number is "how far along are we", so it
1234
+ // counts every finished job. Once terminal it is the success rate.
1235
+ const percentBase = isActive ? processed : succeeded;
1211
1236
  const percent = total > 0
1212
- ? Math.max(0, Math.min(100, Math.round((succeeded / total) * 100)))
1237
+ ? Math.max(0, Math.min(100, Math.round((percentBase / total) * 100)))
1213
1238
  : 0;
1214
- const allSucceeded = total > 0 && succeeded === total;
1215
- // Colour-carrying segments in tooltip order; pending stays in the track.
1239
+ // Colour-carrying segments in tooltip order; queued work stays in the
1240
+ // track. "Still running" is conveyed by the soft halo around the ring (and
1241
+ // bar) rather than by a segment, so the ring stays calm and readable.
1216
1242
  const segments = [
1217
1243
  { key: "succeeded", count: succeeded, color: BATCH_SEGMENT_COLORS.succeeded },
1218
1244
  { key: "failed", count: failed, color: BATCH_SEGMENT_COLORS.failed },
1219
1245
  { key: "aborted", count: aborted, color: BATCH_SEGMENT_COLORS.aborted },
1220
1246
  { key: "running", count: running, color: BATCH_SEGMENT_COLORS.running },
1221
1247
  ].filter((seg) => seg.count > 0);
1222
- // The centre number is the success rate, so it always reads in the success
1223
- // colour; failed/aborted/running are conveyed by the ring segments instead.
1224
- const textToneClass = "text-feedback-green";
1248
+ // Breathing halo shown only while the batch is active.
1249
+ const glowShadow = "0 0 6px 1px color-mix(in srgb, var(--color-feedback-green) 45%, transparent)";
1250
+ // The centre glyph/number follows the batch outcome so it never contradicts
1251
+ // the ring: green only when everything succeeded, red only when everything
1252
+ // failed, orange when aborted, neutral for running and mixed results.
1253
+ const centreFill = outcome === "succeeded"
1254
+ ? "var(--color-feedback-green-light)"
1255
+ : outcome === "failed"
1256
+ ? "var(--color-feedback-red-light)"
1257
+ : outcome === "aborted"
1258
+ ? "var(--color-feedback-orange-light)"
1259
+ : null;
1260
+ const renderCentreGlyph = (sizeClass) => {
1261
+ if (outcome === "succeeded") {
1262
+ return (_jsx(CheckIcon, { className: `text-feedback-green ${sizeClass}`, style: { strokeWidth: 3 }, "aria-hidden": "true" }));
1263
+ }
1264
+ if (outcome === "failed") {
1265
+ return (_jsx(AlertCircleIcon, { className: `text-feedback-red ${sizeClass}`, style: { strokeWidth: 2.5 }, "aria-hidden": "true" }));
1266
+ }
1267
+ if (outcome === "aborted") {
1268
+ return (_jsx(AbortIcon, { className: `text-feedback-orange ${sizeClass}`, style: { strokeWidth: 2.5 }, "aria-hidden": "true" }));
1269
+ }
1270
+ return null;
1271
+ };
1225
1272
  const parts = [`${succeeded}/${total} succeeded`];
1226
1273
  if (failed > 0)
1227
1274
  parts.push(`${failed} failed`);
@@ -1229,20 +1276,24 @@ function BatchProgressIndicator({ counts, testId, variant = "circle", }) {
1229
1276
  parts.push(`${aborted} aborted`);
1230
1277
  if (running > 0)
1231
1278
  parts.push(`${running} running`);
1279
+ if (isActive && pending > 0)
1280
+ parts.push(`${pending} queued`);
1232
1281
  const label = parts.join(", ");
1233
1282
  if (variant === "bar") {
1234
- return (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: "flex min-w-0 items-center gap-2", "aria-label": label, "data-testid": testId, children: [_jsx("div", { className: "bg-neutral-grey-15 flex h-1.5 min-w-0 flex-1 overflow-hidden rounded-full", children: total > 0 &&
1235
- segments.map((seg) => (_jsx("div", { className: "h-full", style: {
1236
- width: `${(seg.count / total) * 100}%`,
1237
- backgroundColor: seg.color,
1238
- } }, seg.key))) }), allSucceeded ? (_jsx(CheckIcon, { className: "text-feedback-green h-3 w-3 shrink-0", style: { strokeWidth: 3 } })) : (_jsxs("span", { className: `shrink-0 text-[11px] leading-none font-medium tabular-nums ${textToneClass}`, children: [percent, "%"] }))] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
1283
+ const glyph = renderCentreGlyph("h-3 w-3 shrink-0");
1284
+ return (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: "flex min-w-0 items-center gap-2", "aria-label": label, "data-testid": testId, "data-outcome": outcome, children: [_jsxs("div", { className: "relative min-w-0 flex-1", children: [isActive && (_jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute -inset-px rounded-full animate-pulse", style: { boxShadow: glowShadow } })), _jsx("div", { className: "bg-neutral-grey-15 flex h-1.5 min-w-0 overflow-hidden rounded-full", children: total > 0 &&
1285
+ segments.map((seg) => (_jsx("div", { className: "h-full", style: {
1286
+ width: `${(seg.count / total) * 100}%`,
1287
+ backgroundColor: seg.color,
1288
+ } }, seg.key))) })] }), glyph ?? (_jsxs("span", { className: "shrink-0 text-[11px] leading-none font-medium tabular-nums text-neutral-grey-100", children: [percent, "%"] }))] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
1239
1289
  }
1240
1290
  const radius = 16;
1241
1291
  const strokeWidth = 3;
1242
1292
  const circumference = 2 * Math.PI * radius;
1243
1293
  const gap = segments.length > 1 ? circumference * 0.02 : 0;
1244
1294
  let segStart = 0;
1245
- return (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: "relative h-10 w-10 shrink-0 rounded-full", "aria-label": label, "data-testid": testId, children: [_jsxs("svg", { className: "h-10 w-10 -rotate-90", viewBox: "0 0 40 40", children: [allSucceeded && (_jsx("circle", { cx: "20", cy: "20", r: radius - strokeWidth / 2, fill: "var(--color-feedback-green-light)" })), _jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", stroke: "var(--color-neutral-grey-15)", strokeWidth: strokeWidth }), total > 0 &&
1295
+ const glyph = renderCentreGlyph("h-4 w-4");
1296
+ return (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: "relative h-10 w-10 shrink-0 rounded-full", "aria-label": label, "data-testid": testId, "data-outcome": outcome, children: [_jsxs("svg", { className: "h-10 w-10 -rotate-90 overflow-visible", viewBox: "0 0 40 40", children: [isActive && (_jsxs(_Fragment, { children: [_jsx("defs", { children: _jsx("filter", { id: glowId, x: "-50%", y: "-50%", width: "200%", height: "200%", children: _jsx("feGaussianBlur", { stdDeviation: "2" }) }) }), _jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", stroke: "var(--color-feedback-green)", strokeWidth: strokeWidth + 4, filter: `url(#${glowId})`, opacity: 0.35, children: _jsx("animate", { attributeName: "opacity", values: "0.15;0.5;0.15", dur: "1.8s", repeatCount: "indefinite" }) })] })), centreFill && (_jsx("circle", { cx: "20", cy: "20", r: radius - strokeWidth / 2, fill: centreFill })), _jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", stroke: "var(--color-neutral-grey-15)", strokeWidth: strokeWidth }), total > 0 &&
1246
1297
  segments.map((seg) => {
1247
1298
  const segLen = (seg.count / total) * circumference;
1248
1299
  const dash = Math.max(0, segLen - gap);
@@ -1252,7 +1303,7 @@ function BatchProgressIndicator({ counts, testId, variant = "circle", }) {
1252
1303
  strokeDasharray: `${dash} ${circumference}`,
1253
1304
  strokeDashoffset: dashOffset,
1254
1305
  } }, seg.key));
1255
- })] }), _jsx("span", { className: "absolute inset-0 flex items-center justify-center", children: allSucceeded ? (_jsx(CheckIcon, { className: "text-feedback-green h-4 w-4", style: { strokeWidth: 3 } })) : (_jsxs("span", { className: `text-[10px] leading-none font-medium tabular-nums ${textToneClass}`, children: [percent, "%"] })) })] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
1306
+ })] }), _jsx("span", { className: "absolute inset-0 flex items-center justify-center", children: glyph ?? (_jsxs("span", { className: "text-[10px] leading-none font-medium tabular-nums text-neutral-grey-100", children: [percent, "%"] })) })] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
1256
1307
  }
1257
1308
  function itemStatusDot(jobs) {
1258
1309
  const hasError = jobs.some((j) => j.status === "Error");
@@ -1269,7 +1320,44 @@ function itemStatusDot(jobs) {
1269
1320
  return { cls: "bg-feedback-orange", label: "Aborted" };
1270
1321
  return { cls: "bg-feedback-green/70", label: "Completed" };
1271
1322
  }
1272
- function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isExpanded, batchStartedAtUtc, abortingJobIds, retryingKeys, singleLine = false, onToggle, onOpenItem, onAbortJob, onRetryJobs, }) {
1323
+ // Fallback one-row cap used until the first chip row has been measured.
1324
+ const COLLAPSED_CHIP_ROW_HEIGHT_PX = 28;
1325
+ function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isExpanded, batchStartedAtUtc, abortingJobIds, retryingKeys, onToggle, onOpenItem, onAbortJob, onRetryJobs, }) {
1326
+ const chipAreaRef = React.useRef(null);
1327
+ const [hiddenCount, setHiddenCount] = React.useState(0);
1328
+ const [firstRowHeight, setFirstRowHeight] = React.useState(null);
1329
+ // Chips always wrap so they can never run into the neighbouring column.
1330
+ // When the item is collapsed only the first line is shown; count the chips
1331
+ // that wrapped onto later lines so a "+N" chip can stand in for them.
1332
+ React.useLayoutEffect(() => {
1333
+ const el = chipAreaRef.current;
1334
+ if (!el || isExpanded) {
1335
+ setHiddenCount(0);
1336
+ return;
1337
+ }
1338
+ const measure = () => {
1339
+ const children = Array.from(el.children);
1340
+ if (children.length === 0) {
1341
+ setHiddenCount(0);
1342
+ return;
1343
+ }
1344
+ const firstTop = children[0]?.offsetTop ?? 0;
1345
+ let hidden = 0;
1346
+ for (const child of children) {
1347
+ if (child.offsetTop > firstTop)
1348
+ hidden += 1;
1349
+ }
1350
+ setHiddenCount(hidden);
1351
+ setFirstRowHeight(children[0]?.offsetHeight ?? null);
1352
+ };
1353
+ measure();
1354
+ let observer;
1355
+ if (typeof ResizeObserver !== "undefined") {
1356
+ observer = new ResizeObserver(measure);
1357
+ observer.observe(el);
1358
+ }
1359
+ return () => observer?.disconnect();
1360
+ }, [isExpanded, jobs.length]);
1273
1361
  if (jobs.length === 0)
1274
1362
  return null;
1275
1363
  const handleCellClick = (event) => {
@@ -1283,15 +1371,20 @@ function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isE
1283
1371
  onToggle();
1284
1372
  }
1285
1373
  };
1286
- return (_jsx("div", { role: "button", tabIndex: 0, className: `flex min-w-0 cursor-pointer items-start justify-start gap-1.5 ${singleLine
1287
- ? "flex-nowrap"
1288
- : isExpanded
1289
- ? "flex-wrap"
1290
- : "max-h-7 flex-wrap overflow-hidden"}`, "aria-label": `${jobs.length} target language${jobs.length !== 1 ? "s" : ""}`, "aria-expanded": isExpanded, onClick: handleCellClick, onKeyDown: handleCellKeyDown, children: jobs.map((job, idx) => {
1291
- const lang = languageMap.get((job.targetLanguage || "").toLowerCase());
1292
- const retryKey = `job:${job.id ?? `${job.itemId}:${job.sourceLanguage}:${job.targetLanguage}`}`;
1293
- return (_jsx("span", { className: "shrink-0", onClick: (event) => event.stopPropagation(), children: _jsx(LanguageJobChip, { job: job, language: lang, provider: batchProvider, batchStartedAtUtc: batchStartedAtUtc, onOpen: () => onOpenItem(job.itemId, job.targetLanguage), isAborting: !!job.id && abortingJobIds.has(job.id), onAbort: () => onAbortJob(job), isRetrying: retryingKeys.has(retryKey), onRetry: () => onRetryJobs(batchId, batchProvider, [job], retryKey) }) }, `${job.id ?? idx}-${job.targetLanguage}`));
1294
- }) }));
1374
+ return (_jsxs("div", { role: "button", tabIndex: 0, className: "flex cursor-pointer items-start gap-1.5", "aria-label": `${jobs.length} target language${jobs.length !== 1 ? "s" : ""}`, "aria-expanded": isExpanded, onClick: handleCellClick, onKeyDown: handleCellKeyDown, children: [_jsx("div", { ref: chipAreaRef, className: `flex flex-1 flex-wrap items-start gap-1.5 ${isExpanded ? "" : "overflow-hidden"}`,
1375
+ // The cap is the measured height of one chip row, applied inline so
1376
+ // the clip does not depend on a Tailwind max-height utility being
1377
+ // emitted for this file.
1378
+ style: isExpanded
1379
+ ? undefined
1380
+ : { maxHeight: firstRowHeight ?? COLLAPSED_CHIP_ROW_HEIGHT_PX }, children: jobs.map((job, idx) => {
1381
+ const lang = languageMap.get((job.targetLanguage || "").toLowerCase());
1382
+ const retryKey = `job:${job.id ?? `${job.itemId}:${job.sourceLanguage}:${job.targetLanguage}`}`;
1383
+ return (_jsx("span", { className: "shrink-0", onClick: (event) => event.stopPropagation(), children: _jsx(LanguageJobChip, { job: job, language: lang, provider: batchProvider, batchStartedAtUtc: batchStartedAtUtc, onOpen: () => onOpenItem(job.itemId, job.targetLanguage), isAborting: !!job.id && abortingJobIds.has(job.id), onAbort: () => onAbortJob(job), isRetrying: retryingKeys.has(retryKey), onRetry: () => onRetryJobs(batchId, batchProvider, [job], retryKey) }) }, `${job.id ?? idx}-${job.targetLanguage}`));
1384
+ }) }), !isExpanded && hiddenCount > 0 && (_jsxs("button", { type: "button", className: "mt-px shrink-0 rounded border border-border-default bg-neutral-grey-5 badge-pad-sm text-[12px] leading-none tabular-nums text-neutral-grey-50 transition-colors hover:bg-neutral-grey-15 hover:text-neutral-grey-100", "aria-label": `Show ${hiddenCount} more language${hiddenCount !== 1 ? "s" : ""}`, title: `${hiddenCount} more language${hiddenCount !== 1 ? "s" : ""}`, onClick: (event) => {
1385
+ event.stopPropagation();
1386
+ onToggle();
1387
+ }, children: ["+", hiddenCount] }))] }));
1295
1388
  }
1296
1389
  function CustomPromptPanel({ prompts }) {
1297
1390
  const [openProvider, setOpenProvider] = React.useState(null);
@@ -1431,7 +1524,10 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
1431
1524
  }
1432
1525
  if (stackLanguageChips) {
1433
1526
  const normalMinWidth = normalTableMinWidthRef.current;
1434
- if (normalMinWidth && availableWidth >= normalMinWidth) {
1527
+ // Unstack once the columns layout fits again. Before that layout has
1528
+ // ever been measured, fall back to the threshold that forced stacking
1529
+ // so a narrow-then-wide resize does not stay stacked forever.
1530
+ if (availableWidth >= (normalMinWidth ?? 640)) {
1435
1531
  setStackLanguageChips(false);
1436
1532
  }
1437
1533
  return;
@@ -1476,7 +1572,7 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
1476
1572
  byItem.set(key, []);
1477
1573
  byItem.get(key).push(job);
1478
1574
  }
1479
- return (_jsx("div", { ref: itemTableContainerRef, className: "w-full overflow-hidden", "data-language-layout": stackLanguageChips ? "stacked" : "columns", children: _jsxs("table", { ref: itemTableRef, className: "w-full border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { children: [!stackLanguageChips && (_jsx("th", { "aria-hidden": "true", className: "px-3 pt-3 pb-1 md:px-4" })), _jsx("th", { scope: "col", className: `pt-3 pb-1 text-left align-top text-[11px] font-medium text-neutral-grey-50 ${stackLanguageChips ? "pr-2 pl-2.5" : "pr-4"}`, children: _jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [_jsx("span", { className: "h-1.5 w-1.5 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: "Pages / Items" })] }) }), !stackLanguageChips && (_jsx("th", { scope: "col", className: "pt-3 pb-1 pr-2 text-left align-top text-[11px] font-medium text-neutral-grey-50", children: "Languages" })), !stackLanguageChips && (_jsx("th", { "aria-hidden": "true", className: "pt-3 pr-3 pb-1 md:pr-4" }))] }) }), _jsx("tbody", { children: Array.from(byItem.entries()).map(([itemId, itemJobs], itemIndex) => {
1575
+ return (_jsx("div", { ref: itemTableContainerRef, className: "w-full overflow-hidden", "data-language-layout": stackLanguageChips ? "stacked" : "columns", children: _jsxs("table", { ref: itemTableRef, className: "w-full border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { children: [!stackLanguageChips && (_jsx("th", { "aria-hidden": "true", className: "px-3 pt-3 pb-1 md:px-4" })), _jsx("th", { scope: "col", className: `pt-3 pb-1 text-left align-top text-[11px] font-medium text-neutral-grey-50 ${stackLanguageChips ? "pr-2 pl-2.5" : "pr-4"}`, children: _jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [_jsx("span", { className: "h-1.5 w-1.5 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: "Pages / Items" })] }) }), !stackLanguageChips && (_jsx("th", { scope: "col", className: "pt-3 pb-1 pr-2 text-left align-top text-[11px] font-medium text-neutral-grey-50", children: "Languages" })), !stackLanguageChips && (_jsx("th", { "aria-hidden": "true", className: "w-12 pt-3 pr-3 pb-1 md:pr-4" }))] }) }), _jsx("tbody", { children: Array.from(byItem.entries()).map(([itemId, itemJobs], itemIndex) => {
1480
1576
  const isItemExpanded = expandedItems.has(itemId);
1481
1577
  const itemAnyInProgress = itemJobs.some((j) => j.status === "In Progress");
1482
1578
  const itemAnyPending = itemJobs.some((j) => j.status === "Pending");
@@ -1496,7 +1592,7 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
1496
1592
  .filter((language) => Boolean(language));
1497
1593
  const normalizedCurrentLanguage = normalizeLanguageCode(currentLanguage);
1498
1594
  const rowOpenLanguage = rowLanguages.find((language) => normalizeLanguageCode(language) === normalizedCurrentLanguage) ?? rowLanguages[0];
1499
- const languageChips = (_jsx(InlineLanguageJobChips, { batchId: batchId, batchProvider: batchProvider, jobs: itemJobs, languageMap: languageMap, isExpanded: isItemExpanded, batchStartedAtUtc: batchStartedAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, singleLine: !stackLanguageChips, onToggle: () => onToggleItem(itemId), onOpenItem: onOpenItem, onAbortJob: onAbortJob, onRetryJobs: onRetryJobs }));
1595
+ const languageChips = (_jsx(InlineLanguageJobChips, { batchId: batchId, batchProvider: batchProvider, jobs: itemJobs, languageMap: languageMap, isExpanded: isItemExpanded, batchStartedAtUtc: batchStartedAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, onToggle: () => onToggleItem(itemId), onOpenItem: onOpenItem, onAbortJob: onAbortJob, onRetryJobs: onRetryJobs }));
1500
1596
  const showRetryAction = itemErrorJobs.length > 0;
1501
1597
  const retryAction = showRetryAction ? (_jsx("div", { className: "flex shrink-0 items-center justify-end", children: _jsx(SimpleIconButton, { onClick: (event) => {
1502
1598
  event.stopPropagation();
@@ -1514,7 +1610,7 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
1514
1610
  e.preventDefault();
1515
1611
  void onOpenItem(itemId, rowOpenLanguage);
1516
1612
  }
1517
- }, children: [!stackLanguageChips && (_jsx("td", { "aria-hidden": "true", className: "px-3 py-2 md:px-4" })), _jsx("td", { className: `py-2 align-top ${stackLanguageChips ? "pr-2 pl-2.5" : "pr-4"}`, children: itemContent }), !stackLanguageChips && (_jsx("td", { className: "py-2 pr-2 align-top", children: languageChips })), !stackLanguageChips && (_jsx("td", { className: "py-2 pr-3 align-top md:pr-4", children: retryAction }))] }, itemId));
1613
+ }, children: [!stackLanguageChips && (_jsx("td", { "aria-hidden": "true", className: "px-3 py-2 md:px-4" })), _jsx("td", { className: `py-2 align-top ${stackLanguageChips ? "pr-2 pl-2.5" : "pr-4"}`, children: itemContent }), !stackLanguageChips && (_jsx("td", { className: "py-2 pr-2 align-top", children: languageChips })), !stackLanguageChips && (_jsx("td", { className: "w-12 py-2 pr-3 align-top whitespace-nowrap md:pr-4", children: retryAction }))] }, itemId));
1518
1614
  }) })] }) }));
1519
1615
  }
1520
1616
  function LanguageJobChip({ job, language, provider, batchStartedAtUtc, onOpen, isAborting = false, onAbort, isRetrying = false, onRetry, }) {
@@ -1627,7 +1723,7 @@ function LanguageJobChip({ job, language, provider, batchStartedAtUtc, onOpen, i
1627
1723
  e.stopPropagation();
1628
1724
  void onAbort?.();
1629
1725
  setOpen(false);
1630
- }, className: "gap-1.5 text-feedback-orange hover:text-feedback-orange", children: [_jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}` }), "Abort"] })), jobError && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isRetrying, onClick: (e) => {
1726
+ }, className: "gap-1.5 text-feedback-orange hover:text-feedback-orange", children: [isAborting ? (_jsx(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(AbortIcon, { className: "h-3.5 w-3.5" })), "Abort"] })), jobError && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isRetrying, onClick: (e) => {
1631
1727
  e.stopPropagation();
1632
1728
  void onRetry();
1633
1729
  setOpen(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parhelia/localization",
3
- "version": "0.4.12796",
3
+ "version": "0.4.12800",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"