@geomak/ui 7.8.0 → 7.9.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/index.cjs CHANGED
@@ -5973,6 +5973,15 @@ async function sourceToBytes(source, remote) {
5973
5973
  }
5974
5974
  throw new Error("Unsupported source type");
5975
5975
  }
5976
+ function downloadBlob(blob, name) {
5977
+ if (typeof document === "undefined") return;
5978
+ const url = URL.createObjectURL(blob);
5979
+ const a = document.createElement("a");
5980
+ a.href = url;
5981
+ a.download = name;
5982
+ a.click();
5983
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
5984
+ }
5976
5985
  function sourceName(source) {
5977
5986
  if (typeof File !== "undefined" && source instanceof File) return source.name;
5978
5987
  if (isUrlSource(source)) {
@@ -5983,17 +5992,45 @@ function sourceName(source) {
5983
5992
  return void 0;
5984
5993
  }
5985
5994
  var pdfjsPromise = null;
5986
- function loadPdfjs() {
5995
+ function loadPdfjs(workerSrc) {
5987
5996
  if (pdfjsPromise) return pdfjsPromise;
5988
5997
  pdfjsPromise = import('pdfjs-dist').then((pdfjs) => {
5989
5998
  if (!pdfjs.GlobalWorkerOptions.workerSrc) {
5990
- pdfjs.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
5999
+ pdfjs.GlobalWorkerOptions.workerSrc = workerSrc ?? `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
5991
6000
  }
5992
6001
  return pdfjs;
5993
6002
  });
5994
6003
  return pdfjsPromise;
5995
6004
  }
5996
6005
  var GAP = 12;
6006
+ var SEARCH_DEBOUNCE = 220;
6007
+ var TEXT_LAYER_STYLE_ID = "oxygen-pdf-textlayer";
6008
+ var TEXT_LAYER_CSS = `
6009
+ .oxy-textLayer{position:absolute;inset:0;overflow:clip;line-height:1;text-size-adjust:none;forced-color-adjust:none;transform-origin:0 0}
6010
+ .oxy-textLayer span,.oxy-textLayer br{color:transparent;position:absolute;white-space:pre;cursor:text;transform-origin:0 0}
6011
+ .oxy-textLayer span.markedContent{top:0;height:0}
6012
+ .oxy-textLayer ::selection{background:color-mix(in srgb, var(--color-accent) 35%, transparent)}
6013
+ `;
6014
+ function ensureTextLayerStyles() {
6015
+ if (typeof document === "undefined" || document.getElementById(TEXT_LAYER_STYLE_ID)) return;
6016
+ const el = document.createElement("style");
6017
+ el.id = TEXT_LAYER_STYLE_ID;
6018
+ el.textContent = TEXT_LAYER_CSS;
6019
+ document.head.appendChild(el);
6020
+ }
6021
+ function pageIndexAt(offsets, y) {
6022
+ let lo = 0;
6023
+ let hi = offsets.length - 1;
6024
+ let ans = 0;
6025
+ while (lo <= hi) {
6026
+ const mid = lo + hi >> 1;
6027
+ if (offsets[mid] <= y) {
6028
+ ans = mid;
6029
+ lo = mid + 1;
6030
+ } else hi = mid - 1;
6031
+ }
6032
+ return ans;
6033
+ }
5997
6034
  function PdfViewer({
5998
6035
  source,
5999
6036
  remote,
@@ -6002,6 +6039,7 @@ function PdfViewer({
6002
6039
  toolbar = true,
6003
6040
  thumbnails = false,
6004
6041
  textLayer = true,
6042
+ workerSrc,
6005
6043
  onLoad,
6006
6044
  onError,
6007
6045
  onPageChange,
@@ -6012,6 +6050,7 @@ function PdfViewer({
6012
6050
  const [doc, setDoc] = React34.useState(null);
6013
6051
  const [numPages, setNumPages] = React34.useState(0);
6014
6052
  const [baseSize, setBaseSize] = React34.useState(null);
6053
+ const [sizes, setSizes] = React34.useState([]);
6015
6054
  const [status, setStatus] = React34.useState("loading");
6016
6055
  const [error, setError] = React34.useState(null);
6017
6056
  const [reloadKey, setReloadKey] = React34.useState(0);
@@ -6024,6 +6063,13 @@ function PdfViewer({
6024
6063
  const [query, setQuery] = React34.useState("");
6025
6064
  const [matchPages, setMatchPages] = React34.useState(null);
6026
6065
  const [matchIdx, setMatchIdx] = React34.useState(0);
6066
+ const [searching, setSearching] = React34.useState(false);
6067
+ const [pageDraft, setPageDraft] = React34.useState("");
6068
+ const pageEditing = React34.useRef(false);
6069
+ const pageText = React34.useRef(/* @__PURE__ */ new Map());
6070
+ const searchSeq = React34.useRef(0);
6071
+ const searchTimer = React34.useRef(null);
6072
+ const measured = React34.useRef(/* @__PURE__ */ new Set());
6027
6073
  const tb = toolbar === true ? { zoom: true, pager: true, download: true, print: true, search: true } : toolbar || {};
6028
6074
  React34.useEffect(() => {
6029
6075
  let cancelled = false;
@@ -6032,7 +6078,12 @@ function PdfViewer({
6032
6078
  setError(null);
6033
6079
  setDoc(null);
6034
6080
  setBaseSize(null);
6035
- loadPdfjs().then(async (pdfjs2) => {
6081
+ setSizes([]);
6082
+ pageText.current.clear();
6083
+ measured.current = /* @__PURE__ */ new Set();
6084
+ setMatchPages(null);
6085
+ setMatchIdx(0);
6086
+ loadPdfjs(workerSrc).then(async (pdfjs2) => {
6036
6087
  if (cancelled) return;
6037
6088
  setPdfjs(pdfjs2);
6038
6089
  const params = isUrlSource(source) ? { url: urlHref(source), httpHeaders: remote?.httpHeaders, withCredentials: remote?.withCredentials } : { data: source instanceof Uint8Array || source instanceof ArrayBuffer ? source : new Uint8Array(await source.arrayBuffer()) };
@@ -6045,9 +6096,12 @@ function PdfViewer({
6045
6096
  const first = await pdf.getPage(1);
6046
6097
  const vp = first.getViewport({ scale: 1 });
6047
6098
  if (cancelled) return;
6099
+ const base = { width: vp.width, height: vp.height };
6048
6100
  setDoc(pdf);
6049
6101
  setNumPages(pdf.numPages);
6050
- setBaseSize({ width: vp.width, height: vp.height });
6102
+ setBaseSize(base);
6103
+ setSizes(Array.from({ length: pdf.numPages }, () => base));
6104
+ measured.current.add(1);
6051
6105
  setStatus("ready");
6052
6106
  onLoad?.({ numPages: pdf.numPages });
6053
6107
  }).catch((err) => {
@@ -6060,7 +6114,7 @@ function PdfViewer({
6060
6114
  cancelled = true;
6061
6115
  task?.destroy?.();
6062
6116
  };
6063
- }, [source, remote, reloadKey]);
6117
+ }, [source, remote, reloadKey, workerSrc]);
6064
6118
  React34.useEffect(() => () => {
6065
6119
  doc?.destroy?.();
6066
6120
  }, [doc]);
@@ -6073,59 +6127,101 @@ function PdfViewer({
6073
6127
  ro.observe(el);
6074
6128
  return () => ro.disconnect();
6075
6129
  }, [status]);
6130
+ const handleMeasure = React34.useCallback((p, size) => {
6131
+ setSizes((prev) => {
6132
+ const cur = prev[p - 1];
6133
+ if (cur && Math.abs(cur.width - size.width) < 0.5 && Math.abs(cur.height - size.height) < 0.5) return prev;
6134
+ const next = prev.slice();
6135
+ next[p - 1] = size;
6136
+ return next;
6137
+ });
6138
+ }, []);
6076
6139
  const scale = React34.useMemo(() => {
6077
6140
  if (!baseSize) return 1;
6078
6141
  if (typeof zoomMode === "number") return zoomMode;
6079
- const avail = Math.max(0, (viewport.w || baseSize.width) - 32);
6142
+ const avail = Math.max(1, (viewport.w || baseSize.width) - 32);
6080
6143
  if (zoomMode === "page-width" || zoomMode === "auto") return avail / baseSize.width;
6081
6144
  if (zoomMode === "page-fit") {
6082
- const availH = Math.max(0, (viewport.h || baseSize.height) - 32);
6145
+ const availH = Math.max(1, (viewport.h || baseSize.height) - 32);
6083
6146
  return Math.min(avail / baseSize.width, availH / baseSize.height);
6084
6147
  }
6085
6148
  return 1;
6086
6149
  }, [zoomMode, baseSize, viewport]);
6087
- const pageH = baseSize ? baseSize.height * scale + GAP : 0;
6088
- const pageW = baseSize ? baseSize.width * scale : 0;
6089
- const total = numPages * pageH;
6150
+ const { offsets, total } = React34.useMemo(() => {
6151
+ const offs = new Array(sizes.length);
6152
+ let acc = 0;
6153
+ for (let i = 0; i < sizes.length; i++) {
6154
+ offs[i] = acc;
6155
+ acc += sizes[i].height * scale + GAP;
6156
+ }
6157
+ return { offsets: offs, total: acc };
6158
+ }, [sizes, scale]);
6090
6159
  const overscan = 1;
6091
- const startIdx = pageH ? Math.max(0, Math.floor(scrollTop / pageH) - overscan) : 0;
6092
- const endIdx = pageH ? Math.min(numPages, Math.ceil((scrollTop + viewport.h) / pageH) + overscan) : 0;
6160
+ const hasLayout = offsets.length > 0 && total > 0;
6161
+ const startIdx = hasLayout ? Math.max(0, pageIndexAt(offsets, scrollTop) - overscan) : 0;
6162
+ const endIdx = hasLayout ? Math.min(numPages, pageIndexAt(offsets, scrollTop + viewport.h) + 1 + overscan) : 0;
6093
6163
  const visiblePages = Array.from({ length: Math.max(0, endIdx - startIdx) }, (_, i) => startIdx + i + 1);
6164
+ const scrollToPage = React34.useCallback((p) => {
6165
+ const el = scrollRef.current;
6166
+ if (!el || !offsets.length) return;
6167
+ const clamped = Math.min(offsets.length, Math.max(1, p));
6168
+ el.scrollTo({ top: offsets[clamped - 1] ?? 0, behavior: "smooth" });
6169
+ }, [offsets]);
6094
6170
  React34.useEffect(() => {
6095
- if (!pageH) return;
6096
- const cur = Math.min(numPages, Math.max(1, Math.floor((scrollTop + viewport.h / 2) / pageH) + 1));
6171
+ if (!hasLayout) return;
6172
+ const cur = pageIndexAt(offsets, scrollTop + viewport.h / 2) + 1;
6097
6173
  if (cur !== page) {
6098
6174
  setPage(cur);
6099
6175
  onPageChange?.(cur);
6100
6176
  }
6101
- }, [scrollTop, pageH, viewport.h, numPages]);
6102
- const scrollToPage = React34.useCallback((p) => {
6103
- const el = scrollRef.current;
6104
- if (!el || !pageH) return;
6105
- el.scrollTo({ top: (p - 1) * pageH, behavior: "smooth" });
6106
- }, [pageH]);
6177
+ }, [scrollTop, offsets, viewport.h, numPages]);
6107
6178
  React34.useEffect(() => {
6108
6179
  if (status === "ready" && initialPage > 1) scrollToPage(initialPage);
6109
- }, [status]);
6180
+ }, [status, initialPage]);
6110
6181
  const runSearch = React34.useCallback(async (q) => {
6111
- setQuery(q);
6112
- if (!doc || !q.trim()) {
6182
+ const needle = q.trim().toLowerCase();
6183
+ if (!doc || !needle) {
6113
6184
  setMatchPages(null);
6114
6185
  setMatchIdx(0);
6186
+ setSearching(false);
6115
6187
  return;
6116
6188
  }
6117
- const needle = q.toLowerCase();
6189
+ const seq = ++searchSeq.current;
6190
+ setSearching(true);
6118
6191
  const hits = [];
6119
6192
  for (let p = 1; p <= numPages; p++) {
6120
- const pg = await doc.getPage(p);
6121
- const tc = await pg.getTextContent();
6122
- const text = tc.items.map((it) => "str" in it ? it.str : "").join(" ").toLowerCase();
6193
+ let text = pageText.current.get(p);
6194
+ if (text === void 0) {
6195
+ const pg = await doc.getPage(p);
6196
+ const tc = await pg.getTextContent();
6197
+ const built = tc.items.map((it) => "str" in it ? it.str : "").join(" ").toLowerCase();
6198
+ pageText.current.set(p, built);
6199
+ text = built;
6200
+ }
6201
+ if (seq !== searchSeq.current) return;
6123
6202
  if (text.includes(needle)) hits.push(p);
6124
6203
  }
6204
+ if (seq !== searchSeq.current) return;
6125
6205
  setMatchPages(hits);
6126
6206
  setMatchIdx(0);
6207
+ setSearching(false);
6127
6208
  if (hits.length) scrollToPage(hits[0]);
6128
6209
  }, [doc, numPages, scrollToPage]);
6210
+ const onQueryChange = React34.useCallback((v) => {
6211
+ setQuery(v);
6212
+ if (searchTimer.current) clearTimeout(searchTimer.current);
6213
+ if (!v.trim()) {
6214
+ searchSeq.current++;
6215
+ setMatchPages(null);
6216
+ setMatchIdx(0);
6217
+ setSearching(false);
6218
+ return;
6219
+ }
6220
+ searchTimer.current = setTimeout(() => runSearch(v), SEARCH_DEBOUNCE);
6221
+ }, [runSearch]);
6222
+ React34.useEffect(() => () => {
6223
+ if (searchTimer.current) clearTimeout(searchTimer.current);
6224
+ }, []);
6129
6225
  const gotoMatch = (dir) => {
6130
6226
  if (!matchPages?.length) return;
6131
6227
  const next = (matchIdx + dir + matchPages.length) % matchPages.length;
@@ -6139,15 +6235,9 @@ function PdfViewer({
6139
6235
  if (typeof Blob !== "undefined" && source instanceof Blob) return new Uint8Array(await source.arrayBuffer());
6140
6236
  throw new Error("No bytes available");
6141
6237
  }, [doc, source]);
6142
- const download2 = React34.useCallback(async () => {
6238
+ const download = React34.useCallback(async () => {
6143
6239
  const bytes = await getBytes();
6144
- const blob = new Blob([bytes], { type: "application/pdf" });
6145
- const url = URL.createObjectURL(blob);
6146
- const a = document.createElement("a");
6147
- a.href = url;
6148
- a.download = sourceName(source) || "document.pdf";
6149
- a.click();
6150
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
6240
+ downloadBlob(new Blob([bytes], { type: "application/pdf" }), sourceName(source) || "document.pdf");
6151
6241
  }, [getBytes, source]);
6152
6242
  const print = React34.useCallback(async () => {
6153
6243
  const bytes = await getBytes();
@@ -6173,6 +6263,12 @@ function PdfViewer({
6173
6263
  const cur = typeof zoomMode === "number" ? zoomMode : scale;
6174
6264
  setZoomMode(Math.min(5, Math.max(0.25, +(cur * factor).toFixed(2))));
6175
6265
  };
6266
+ const zoomPct = Math.round(scale * 100);
6267
+ const commitPageDraft = () => {
6268
+ pageEditing.current = false;
6269
+ const n = parseInt(pageDraft, 10);
6270
+ if (Number.isFinite(n)) scrollToPage(n);
6271
+ };
6176
6272
  if (status === "error") {
6177
6273
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("flex flex-col items-center justify-center gap-3 rounded-lg border border-border bg-surface p-8 text-center", className), style, children: [
6178
6274
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-status-error", children: "Couldn\u2019t load the PDF" }),
@@ -6180,80 +6276,127 @@ function PdfViewer({
6180
6276
  /* @__PURE__ */ jsxRuntime.jsx(Button_default, { content: "Retry", size: "sm", variant: "outline", onClick: () => setReloadKey((k) => k + 1) })
6181
6277
  ] });
6182
6278
  }
6279
+ const ready = status === "ready";
6183
6280
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("flex flex-col overflow-hidden rounded-lg border border-border bg-surface-raised", className), style: { height: 600, ...style }, children: [
6184
- toolbar !== false && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-shrink-0 flex-wrap items-center gap-2 border-b border-border bg-surface px-2 py-1.5", children: [
6185
- tb.pager && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
6186
- /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Previous page", onClick: () => scrollToPage(Math.max(1, page - 1)), icon: /* @__PURE__ */ jsxRuntime.jsx(Chevron5, { dir: "up" }), disabled: status !== "ready" }),
6187
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "px-1 text-xs tabular-nums text-foreground-secondary select-none", children: [
6188
- status === "ready" ? page : "\u2013",
6189
- " / ",
6190
- numPages || "\u2013"
6281
+ toolbar !== false && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-shrink-0 flex-col border-b border-border bg-surface", children: [
6282
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 px-2 py-1.5", children: [
6283
+ tb.pager && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
6284
+ /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Previous page", onClick: () => scrollToPage(page - 1), icon: /* @__PURE__ */ jsxRuntime.jsx(Chevron5, { dir: "up" }), disabled: !ready || page <= 1 }),
6285
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 text-xs text-foreground-secondary select-none", children: [
6286
+ /* @__PURE__ */ jsxRuntime.jsx(
6287
+ "input",
6288
+ {
6289
+ value: pageEditing.current ? pageDraft : ready ? String(page) : "\u2013",
6290
+ onFocus: () => {
6291
+ pageEditing.current = true;
6292
+ setPageDraft(String(page));
6293
+ },
6294
+ onChange: (e) => setPageDraft(e.target.value.replace(/[^\d]/g, "")),
6295
+ onBlur: commitPageDraft,
6296
+ onKeyDown: (e) => {
6297
+ if (e.key === "Enter") e.target.blur();
6298
+ },
6299
+ disabled: !ready,
6300
+ "aria-label": "Page number",
6301
+ className: "h-6 w-9 rounded border border-border bg-surface-raised text-center tabular-nums text-foreground outline-none focus:border-accent disabled:opacity-50"
6302
+ }
6303
+ ),
6304
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "tabular-nums", children: [
6305
+ "/ ",
6306
+ numPages || "\u2013"
6307
+ ] })
6308
+ ] }),
6309
+ /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Next page", onClick: () => scrollToPage(page + 1), icon: /* @__PURE__ */ jsxRuntime.jsx(Chevron5, { dir: "down" }), disabled: !ready || page >= numPages })
6191
6310
  ] }),
6192
- /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Next page", onClick: () => scrollToPage(Math.min(numPages, page + 1)), icon: /* @__PURE__ */ jsxRuntime.jsx(Chevron5, { dir: "down" }), disabled: status !== "ready" })
6193
- ] }),
6194
- tb.zoom && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-1 flex items-center gap-1", children: [
6195
- /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Zoom out", onClick: () => setZoomNum(1 / 1.2), icon: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: "\u2212" }), disabled: status !== "ready" }),
6196
- /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Zoom in", onClick: () => setZoomNum(1.2), icon: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: "+" }), disabled: status !== "ready" }),
6197
- /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Fit width", onClick: () => setZoomMode("page-width"), icon: /* @__PURE__ */ jsxRuntime.jsx(FitWidthIcon, {}), disabled: status !== "ready" }),
6198
- /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Fit page", onClick: () => setZoomMode("page-fit"), icon: /* @__PURE__ */ jsxRuntime.jsx(FitPageIcon, {}), disabled: status !== "ready" })
6199
- ] }),
6200
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
6201
- tb.search && /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Search", onClick: () => setShowSearch((s) => !s), icon: /* @__PURE__ */ jsxRuntime.jsx(SearchIcon2, {}), disabled: status !== "ready" }),
6202
- tb.download && /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Download", onClick: download2, icon: /* @__PURE__ */ jsxRuntime.jsx(DownloadIcon, {}), disabled: status !== "ready" }),
6203
- tb.print && /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Print", onClick: print, icon: /* @__PURE__ */ jsxRuntime.jsx(PrintIcon, {}), disabled: status !== "ready" })
6311
+ tb.pager && tb.zoom && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mx-1 h-5 w-px bg-border", "aria-hidden": "true" }),
6312
+ tb.zoom && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
6313
+ /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Zoom out", onClick: () => setZoomNum(1 / 1.2), icon: /* @__PURE__ */ jsxRuntime.jsx(MinusIcon, {}), disabled: !ready }),
6314
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-10 text-center text-xs tabular-nums text-foreground-secondary select-none", children: ready ? `${zoomPct}%` : "\u2013" }),
6315
+ /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Zoom in", onClick: () => setZoomNum(1.2), icon: /* @__PURE__ */ jsxRuntime.jsx(PlusIcon, {}), disabled: !ready }),
6316
+ /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Fit width", onClick: () => setZoomMode("page-width"), icon: /* @__PURE__ */ jsxRuntime.jsx(FitWidthIcon, {}), disabled: !ready }),
6317
+ /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Fit page", onClick: () => setZoomMode("page-fit"), icon: /* @__PURE__ */ jsxRuntime.jsx(FitPageIcon, {}), disabled: !ready })
6318
+ ] }),
6319
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
6320
+ tb.search && /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: showSearch ? "bordered" : "ghost", size: "sm", title: "Search", onClick: () => setShowSearch((s) => !s), icon: /* @__PURE__ */ jsxRuntime.jsx(SearchIcon2, {}), disabled: !ready }),
6321
+ tb.search && (tb.download || tb.print) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mx-1 h-5 w-px bg-border", "aria-hidden": "true" }),
6322
+ tb.download && /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Download", onClick: download, icon: /* @__PURE__ */ jsxRuntime.jsx(DownloadIcon, {}), disabled: !ready }),
6323
+ tb.print && /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Print", onClick: print, icon: /* @__PURE__ */ jsxRuntime.jsx(PrintIcon, {}), disabled: !ready })
6324
+ ] })
6204
6325
  ] }),
6205
- tb.search && showSearch && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full items-center gap-2 pt-1", children: [
6326
+ tb.search && showSearch && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border-t border-border px-2 py-1.5", children: [
6327
+ /* @__PURE__ */ jsxRuntime.jsx(SearchIcon2, {}),
6206
6328
  /* @__PURE__ */ jsxRuntime.jsx(
6207
6329
  "input",
6208
6330
  {
6209
6331
  autoFocus: true,
6210
6332
  value: query,
6211
- onChange: (e) => runSearch(e.target.value),
6333
+ onChange: (e) => onQueryChange(e.target.value),
6334
+ onKeyDown: (e) => {
6335
+ if (e.key === "Enter") gotoMatch(e.shiftKey ? -1 : 1);
6336
+ },
6212
6337
  placeholder: "Find in document\u2026",
6213
- className: "h-7 flex-1 rounded-md border border-border bg-surface px-2 text-sm text-foreground outline-none focus:border-accent"
6338
+ className: "h-7 flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-foreground-muted"
6214
6339
  }
6215
6340
  ),
6216
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs tabular-nums text-foreground-muted", children: matchPages == null ? "" : matchPages.length ? `${matchIdx + 1}/${matchPages.length} pages` : "no matches" }),
6341
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs tabular-nums text-foreground-muted", children: searching ? "Searching\u2026" : matchPages == null ? "" : matchPages.length ? `${matchIdx + 1} of ${matchPages.length}` : "No matches" }),
6217
6342
  /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Previous match", onClick: () => gotoMatch(-1), icon: /* @__PURE__ */ jsxRuntime.jsx(Chevron5, { dir: "up" }), disabled: !matchPages?.length }),
6218
- /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Next match", onClick: () => gotoMatch(1), icon: /* @__PURE__ */ jsxRuntime.jsx(Chevron5, { dir: "down" }), disabled: !matchPages?.length })
6343
+ /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Next match", onClick: () => gotoMatch(1), icon: /* @__PURE__ */ jsxRuntime.jsx(Chevron5, { dir: "down" }), disabled: !matchPages?.length }),
6344
+ /* @__PURE__ */ jsxRuntime.jsx(IconButton_default, { type: "ghost", size: "sm", title: "Close search", onClick: () => {
6345
+ setShowSearch(false);
6346
+ onQueryChange("");
6347
+ }, icon: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {}) })
6219
6348
  ] })
6220
6349
  ] }),
6221
6350
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1", children: [
6222
- thumbnails && status === "ready" && doc && baseSize && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-32 flex-shrink-0 overflow-y-auto border-r border-border bg-surface p-2", children: Array.from({ length: numPages }, (_, i) => i + 1).map((p) => /* @__PURE__ */ jsxRuntime.jsx(
6223
- "button",
6224
- {
6225
- type: "button",
6226
- onClick: () => scrollToPage(p),
6227
- className: cx(
6228
- "mb-2 block w-full overflow-hidden rounded border bg-surface-raised transition-colors",
6229
- p === page ? "border-accent ring-1 ring-accent" : "border-border hover:border-border-strong"
6230
- ),
6231
- style: { aspectRatio: `${baseSize.width} / ${baseSize.height}` },
6232
- "aria-label": `Page ${p}`,
6233
- children: Math.abs(p - page) <= 8 ? /* @__PURE__ */ jsxRuntime.jsx(PdfPage, { pdfjs, doc, page: p, scale: 112 / baseSize.width, textLayer: false }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block py-4 text-center text-xs text-foreground-muted", children: p })
6234
- },
6235
- p
6236
- )) }),
6351
+ thumbnails && ready && doc && baseSize && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-32 flex-shrink-0 overflow-y-auto border-r border-border bg-surface p-2", children: Array.from({ length: numPages }, (_, i) => i + 1).map((p) => {
6352
+ const s = sizes[p - 1] ?? baseSize;
6353
+ return /* @__PURE__ */ jsxRuntime.jsx(
6354
+ "button",
6355
+ {
6356
+ type: "button",
6357
+ onClick: () => scrollToPage(p),
6358
+ className: cx(
6359
+ "mb-2 block w-full overflow-hidden rounded border bg-surface-raised transition-colors",
6360
+ p === page ? "border-accent ring-1 ring-accent" : "border-border hover:border-border-strong"
6361
+ ),
6362
+ style: { aspectRatio: `${s.width} / ${s.height}` },
6363
+ "aria-label": `Page ${p}`,
6364
+ "aria-current": p === page,
6365
+ children: Math.abs(p - page) <= 8 ? /* @__PURE__ */ jsxRuntime.jsx(PdfPage, { pdfjs, doc, page: p, scale: 112 / s.width, textLayer: false }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block py-4 text-center text-xs text-foreground-muted", children: p })
6366
+ },
6367
+ p
6368
+ );
6369
+ }) }),
6237
6370
  /* @__PURE__ */ jsxRuntime.jsx(
6238
6371
  "div",
6239
6372
  {
6240
6373
  ref: scrollRef,
6241
6374
  onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
6242
6375
  className: "relative flex-1 overflow-auto bg-background",
6243
- children: status === "loading" || !baseSize ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-center gap-3 p-6", children: /* @__PURE__ */ jsxRuntime.jsx(SkeletonBox, { width: Math.min(viewport.w - 48, 560) || 480, height: 680, className: "rounded" }) }) : numPages === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center text-sm text-foreground-muted", children: "Empty document" }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: total, position: "relative" }, children: visiblePages.map((p) => /* @__PURE__ */ jsxRuntime.jsx(
6244
- "div",
6245
- {
6246
- style: { position: "absolute", top: (p - 1) * pageH, left: 0, right: 0, height: pageH, display: "flex", justifyContent: "center", paddingTop: GAP / 2, paddingBottom: GAP / 2 },
6247
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative shadow-md", style: { width: pageW, height: baseSize.height * scale }, children: /* @__PURE__ */ jsxRuntime.jsx(PdfPage, { pdfjs, doc, page: p, scale, textLayer }) })
6248
- },
6249
- p
6250
- )) })
6376
+ children: !ready || !baseSize ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-center gap-3 p-6", children: /* @__PURE__ */ jsxRuntime.jsx(SkeletonBox, { width: Math.min((viewport.w || 528) - 48, 560), height: 680, className: "rounded" }) }) : numPages === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center text-sm text-foreground-muted", children: "Empty document" }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: total, position: "relative" }, children: visiblePages.map((p) => {
6377
+ const s = sizes[p - 1] ?? baseSize;
6378
+ return /* @__PURE__ */ jsxRuntime.jsx(
6379
+ "div",
6380
+ {
6381
+ style: { position: "absolute", top: offsets[p - 1], left: 0, right: 0, display: "flex", justifyContent: "center", paddingTop: GAP / 2 },
6382
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative shadow-md", style: { width: s.width * scale, height: s.height * scale }, children: /* @__PURE__ */ jsxRuntime.jsx(PdfPage, { pdfjs, doc, page: p, scale, textLayer, onMeasure: handleMeasure }) })
6383
+ },
6384
+ p
6385
+ );
6386
+ }) })
6251
6387
  }
6252
6388
  )
6253
6389
  ] })
6254
6390
  ] });
6255
6391
  }
6256
- function PdfPage({ pdfjs, doc, page, scale, textLayer }) {
6392
+ function PdfPage({
6393
+ pdfjs,
6394
+ doc,
6395
+ page,
6396
+ scale,
6397
+ textLayer,
6398
+ onMeasure
6399
+ }) {
6257
6400
  const canvasRef = React34.useRef(null);
6258
6401
  const textRef = React34.useRef(null);
6259
6402
  React34.useEffect(() => {
@@ -6263,6 +6406,7 @@ function PdfPage({ pdfjs, doc, page, scale, textLayer }) {
6263
6406
  const pg = await doc.getPage(page);
6264
6407
  if (cancelled) return;
6265
6408
  const viewport = pg.getViewport({ scale });
6409
+ onMeasure?.(page, { width: viewport.width / scale, height: viewport.height / scale });
6266
6410
  const canvas = canvasRef.current;
6267
6411
  if (!canvas) return;
6268
6412
  const ratio = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
@@ -6279,6 +6423,7 @@ function PdfPage({ pdfjs, doc, page, scale, textLayer }) {
6279
6423
  }
6280
6424
  if (cancelled || !textLayer || !textRef.current || !pdfjs?.TextLayer) return;
6281
6425
  try {
6426
+ ensureTextLayerStyles();
6282
6427
  textRef.current.innerHTML = "";
6283
6428
  textRef.current.style.setProperty("--scale-factor", String(scale));
6284
6429
  const tl = new pdfjs.TextLayer({ textContentSource: pg.streamTextContent(), container: textRef.current, viewport });
@@ -6290,17 +6435,20 @@ function PdfPage({ pdfjs, doc, page, scale, textLayer }) {
6290
6435
  cancelled = true;
6291
6436
  renderTask?.cancel?.();
6292
6437
  };
6293
- }, [pdfjs, doc, page, scale, textLayer]);
6438
+ }, [pdfjs, doc, page, scale, textLayer, onMeasure]);
6294
6439
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6295
6440
  /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, className: "block bg-white" }),
6296
- textLayer && /* @__PURE__ */ jsxRuntime.jsx("div", { ref: textRef, className: "textLayer pointer-events-auto absolute inset-0 overflow-hidden leading-none", style: { opacity: 0.25 }, "aria-hidden": "true" })
6441
+ textLayer && /* @__PURE__ */ jsxRuntime.jsx("div", { ref: textRef, className: "oxy-textLayer", "aria-hidden": "true" })
6297
6442
  ] });
6298
6443
  }
6299
6444
  var Chevron5 = ({ dir }) => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: dir === "up" ? "m6 15 6-6 6 6" : "m6 9 6 6 6-6" }) });
6300
- var SearchIcon2 = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: [
6445
+ var MinusIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", d: "M5 12h14" }) });
6446
+ var PlusIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", d: "M12 5v14M5 12h14" }) });
6447
+ var SearchIcon2 = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4 text-foreground-muted", "aria-hidden": "true", children: [
6301
6448
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "11", r: "7" }),
6302
6449
  /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", d: "m21 21-4.3-4.3" })
6303
6450
  ] });
6451
+ var CloseIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", d: "M6 6l12 12M18 6 6 18" }) });
6304
6452
  var DownloadIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 3v12m0 0 4-4m-4 4-4-4M4 21h16" }) });
6305
6453
  var PrintIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 9V3h12v6M6 18H4v-7h16v7h-2M8 14h8v7H8z" }) });
6306
6454
  var FitWidthIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 12h18m0 0-4-4m4 4-4 4M3 12l4-4m-4 4 4 4" }) });
@@ -6311,10 +6459,10 @@ var FitPageIcon = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 2
6311
6459
  var DEFAULT_COL_WIDTH = 140;
6312
6460
  var GUTTER = 52;
6313
6461
  function resolveWidth(w) {
6314
- if (typeof w === "number") return w;
6462
+ if (typeof w === "number" && Number.isFinite(w)) return w;
6315
6463
  if (typeof w === "string") {
6316
- const n = parseFloat(w);
6317
- if (!Number.isNaN(n) && /^\d/.test(w.trim())) return n;
6464
+ const m = /^\s*(\d+(?:\.\d+)?)(px)?\s*$/.exec(w);
6465
+ if (m) return parseFloat(m[1]);
6318
6466
  }
6319
6467
  return DEFAULT_COL_WIDTH;
6320
6468
  }
@@ -6507,13 +6655,16 @@ var xlsxPromise = null;
6507
6655
  var loadXlsx = () => xlsxPromise ??= import('xlsx');
6508
6656
  var jspdfPromise = null;
6509
6657
  var loadJspdf = () => jspdfPromise ??= import('jspdf');
6510
- function download(blob, name) {
6511
- const url = URL.createObjectURL(blob);
6512
- const a = document.createElement("a");
6513
- a.href = url;
6514
- a.download = name;
6515
- a.click();
6516
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
6658
+ function coerceToCellType(prev, next) {
6659
+ if (typeof prev === "number") {
6660
+ const n = Number(next);
6661
+ return next.trim() !== "" && Number.isFinite(n) ? n : next;
6662
+ }
6663
+ if (typeof prev === "boolean") {
6664
+ if (/^(true|false)$/i.test(next.trim())) return next.trim().toLowerCase() === "true";
6665
+ return next;
6666
+ }
6667
+ return next;
6517
6668
  }
6518
6669
  function Spreadsheet({
6519
6670
  source,
@@ -6581,20 +6732,23 @@ function Spreadsheet({
6581
6732
  const columns = React34.useMemo(() => sheet ? toColumns(sheet.columns) : [], [sheet]);
6582
6733
  const plainRows = React34.useMemo(() => sheet ? toPlainRows(sheet, columns) : [], [sheet, columns]);
6583
6734
  const handleCellEdit = React34.useCallback(({ row, column, value }) => {
6735
+ let coerced = value;
6584
6736
  setSheets((prev) => {
6585
6737
  if (!prev) return prev;
6586
6738
  const next = prev.map((s, i) => i === active ? { ...s, rows: s.rows.map((r) => ({ ...r })) } : s);
6587
6739
  const target = next[active];
6588
6740
  const existing = target.rows[row]?.[column];
6741
+ const prevValue = cellValue(existing);
6742
+ coerced = coerceToCellType(prevValue, value);
6589
6743
  if (existing != null && typeof existing === "object" && "value" in existing) {
6590
- target.rows[row][column] = { ...existing, value };
6744
+ target.rows[row][column] = { ...existing, value: coerced };
6591
6745
  } else {
6592
- target.rows[row][column] = value;
6746
+ target.rows[row][column] = coerced;
6593
6747
  }
6594
6748
  onChange?.(next);
6595
6749
  return next;
6596
6750
  });
6597
- onCellEdit?.({ sheet: sheets?.[active]?.name ?? "", row, column, value });
6751
+ onCellEdit?.({ sheet: sheets?.[active]?.name ?? "", row, column, value: coerced });
6598
6752
  }, [active, onCellEdit, onChange, sheets]);
6599
6753
  const baseName = React34.useMemo(
6600
6754
  () => (fileName || (typeof source === "object" && "name" in source ? sourceName(source) : null) || "spreadsheet").replace(/\.[^.]+$/, ""),
@@ -6615,7 +6769,7 @@ function Spreadsheet({
6615
6769
  XLSX.utils.book_append_sheet(wb, ws, (s.name || "Sheet").slice(0, 31));
6616
6770
  });
6617
6771
  const out = XLSX.write(wb, { bookType: "xlsx", type: "array" });
6618
- download(new Blob([out], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), `${baseName}.xlsx`);
6772
+ downloadBlob(new Blob([out], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), `${baseName}.xlsx`);
6619
6773
  }, [sheets, sheetAoa, baseName]);
6620
6774
  const exportCsv = React34.useCallback(() => {
6621
6775
  if (!sheet) return;
@@ -6625,7 +6779,7 @@ function Spreadsheet({
6625
6779
  return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
6626
6780
  };
6627
6781
  const csv = aoa.map((row) => row.map(esc).join(",")).join("\r\n");
6628
- download(new Blob([`\uFEFF${csv}`], { type: "text/csv;charset=utf-8" }), `${baseName}.csv`);
6782
+ downloadBlob(new Blob([`\uFEFF${csv}`], { type: "text/csv;charset=utf-8" }), `${baseName}.csv`);
6629
6783
  }, [sheet, sheetAoa, baseName]);
6630
6784
  const exportPdf = React34.useCallback(async () => {
6631
6785
  if (!sheet) return;
@@ -6681,9 +6835,14 @@ function Spreadsheet({
6681
6835
  ] });
6682
6836
  }
6683
6837
  const formats = exportFormats || [];
6684
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("flex flex-col gap-2", className), style, children: [
6685
- (sheets.length > 1 || formats.length > 0) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
6686
- sheets.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tablist", className: "flex flex-wrap items-center gap-1 rounded-lg border border-border bg-surface p-1", children: sheets.map((s, i) => /* @__PURE__ */ jsxRuntime.jsx(
6838
+ const exportLabels = {
6839
+ xlsx: "Excel workbook (.xlsx)",
6840
+ csv: "CSV \u2014 this sheet (.csv)",
6841
+ pdf: "PDF table (.pdf)"
6842
+ };
6843
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("flex flex-col overflow-hidden rounded-lg border border-border bg-surface-raised", className), style, children: [
6844
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-shrink-0 items-center gap-2 border-b border-border bg-surface px-2 py-1.5", children: [
6845
+ sheets.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tablist", "aria-label": "Sheets", className: "flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto", children: sheets.map((s, i) => /* @__PURE__ */ jsxRuntime.jsx(
6687
6846
  "button",
6688
6847
  {
6689
6848
  role: "tab",
@@ -6691,14 +6850,40 @@ function Spreadsheet({
6691
6850
  "aria-selected": i === active,
6692
6851
  onClick: () => setActive(i),
6693
6852
  className: cx(
6694
- "rounded-md px-3 py-1 text-sm transition-colors",
6695
- i === active ? "bg-accent text-accent-fg" : "text-foreground-secondary hover:bg-surface-raised hover:text-foreground"
6853
+ "flex-shrink-0 rounded-md px-3 py-1 text-sm font-medium transition-colors",
6854
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
6855
+ i === active ? "bg-accent text-accent-fg shadow-sm" : "text-foreground-secondary hover:bg-surface-raised hover:text-foreground"
6696
6856
  ),
6697
6857
  children: s.name || `Sheet ${i + 1}`
6698
6858
  },
6699
6859
  `${s.name}-${i}`
6700
- )) }),
6701
- formats.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-auto flex items-center gap-1.5", children: formats.map((fmt) => /* @__PURE__ */ jsxRuntime.jsx(Button_default, { content: fmt.toUpperCase(), size: "sm", variant: "outline", onClick: () => runExport(fmt) }, fmt)) })
6860
+ )) }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate px-1 text-sm font-medium text-foreground", children: sheet?.name || "Sheet 1" }),
6861
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "hidden flex-shrink-0 px-1 text-xs tabular-nums text-foreground-muted sm:inline", children: [
6862
+ plainRows.length.toLocaleString(),
6863
+ " ",
6864
+ plainRows.length === 1 ? "row" : "rows",
6865
+ " \xB7 ",
6866
+ columns.length,
6867
+ " cols"
6868
+ ] }),
6869
+ formats.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6870
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-5 w-px flex-shrink-0 bg-border", "aria-hidden": "true" }),
6871
+ /* @__PURE__ */ jsxRuntime.jsx(
6872
+ MenuButton,
6873
+ {
6874
+ label: "Export",
6875
+ size: "sm",
6876
+ variant: "outline",
6877
+ align: "end",
6878
+ icon: /* @__PURE__ */ jsxRuntime.jsx(DownloadIcon2, {}),
6879
+ items: formats.map((fmt) => ({
6880
+ key: fmt,
6881
+ label: exportLabels[fmt],
6882
+ onSelect: () => runExport(fmt)
6883
+ }))
6884
+ }
6885
+ )
6886
+ ] })
6702
6887
  ] }),
6703
6888
  /* @__PURE__ */ jsxRuntime.jsx(
6704
6889
  DataGrid,
@@ -6707,11 +6892,13 @@ function Spreadsheet({
6707
6892
  rows: plainRows,
6708
6893
  editable,
6709
6894
  virtualize,
6710
- onCellEdit: handleCellEdit
6895
+ onCellEdit: handleCellEdit,
6896
+ className: "!rounded-none !border-0"
6711
6897
  }
6712
6898
  )
6713
6899
  ] });
6714
6900
  }
6901
+ var DownloadIcon2 = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 3v12m0 0 4-4m-4 4-4-4M4 21h16" }) });
6715
6902
  function ThemeSwitch({ checked, onChange, label = "Toggle dark mode", className = "" }) {
6716
6903
  const id = React34.useId();
6717
6904
  return /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, className: `flex items-center gap-2 cursor-pointer select-none ${className}`.trim(), children: /* @__PURE__ */ jsxRuntime.jsx(