@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.js CHANGED
@@ -5937,6 +5937,15 @@ async function sourceToBytes(source, remote) {
5937
5937
  }
5938
5938
  throw new Error("Unsupported source type");
5939
5939
  }
5940
+ function downloadBlob(blob, name) {
5941
+ if (typeof document === "undefined") return;
5942
+ const url = URL.createObjectURL(blob);
5943
+ const a = document.createElement("a");
5944
+ a.href = url;
5945
+ a.download = name;
5946
+ a.click();
5947
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
5948
+ }
5940
5949
  function sourceName(source) {
5941
5950
  if (typeof File !== "undefined" && source instanceof File) return source.name;
5942
5951
  if (isUrlSource(source)) {
@@ -5947,17 +5956,45 @@ function sourceName(source) {
5947
5956
  return void 0;
5948
5957
  }
5949
5958
  var pdfjsPromise = null;
5950
- function loadPdfjs() {
5959
+ function loadPdfjs(workerSrc) {
5951
5960
  if (pdfjsPromise) return pdfjsPromise;
5952
5961
  pdfjsPromise = import('pdfjs-dist').then((pdfjs) => {
5953
5962
  if (!pdfjs.GlobalWorkerOptions.workerSrc) {
5954
- pdfjs.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
5963
+ pdfjs.GlobalWorkerOptions.workerSrc = workerSrc ?? `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
5955
5964
  }
5956
5965
  return pdfjs;
5957
5966
  });
5958
5967
  return pdfjsPromise;
5959
5968
  }
5960
5969
  var GAP = 12;
5970
+ var SEARCH_DEBOUNCE = 220;
5971
+ var TEXT_LAYER_STYLE_ID = "oxygen-pdf-textlayer";
5972
+ var TEXT_LAYER_CSS = `
5973
+ .oxy-textLayer{position:absolute;inset:0;overflow:clip;line-height:1;text-size-adjust:none;forced-color-adjust:none;transform-origin:0 0}
5974
+ .oxy-textLayer span,.oxy-textLayer br{color:transparent;position:absolute;white-space:pre;cursor:text;transform-origin:0 0}
5975
+ .oxy-textLayer span.markedContent{top:0;height:0}
5976
+ .oxy-textLayer ::selection{background:color-mix(in srgb, var(--color-accent) 35%, transparent)}
5977
+ `;
5978
+ function ensureTextLayerStyles() {
5979
+ if (typeof document === "undefined" || document.getElementById(TEXT_LAYER_STYLE_ID)) return;
5980
+ const el = document.createElement("style");
5981
+ el.id = TEXT_LAYER_STYLE_ID;
5982
+ el.textContent = TEXT_LAYER_CSS;
5983
+ document.head.appendChild(el);
5984
+ }
5985
+ function pageIndexAt(offsets, y) {
5986
+ let lo = 0;
5987
+ let hi = offsets.length - 1;
5988
+ let ans = 0;
5989
+ while (lo <= hi) {
5990
+ const mid = lo + hi >> 1;
5991
+ if (offsets[mid] <= y) {
5992
+ ans = mid;
5993
+ lo = mid + 1;
5994
+ } else hi = mid - 1;
5995
+ }
5996
+ return ans;
5997
+ }
5961
5998
  function PdfViewer({
5962
5999
  source,
5963
6000
  remote,
@@ -5966,6 +6003,7 @@ function PdfViewer({
5966
6003
  toolbar = true,
5967
6004
  thumbnails = false,
5968
6005
  textLayer = true,
6006
+ workerSrc,
5969
6007
  onLoad,
5970
6008
  onError,
5971
6009
  onPageChange,
@@ -5976,6 +6014,7 @@ function PdfViewer({
5976
6014
  const [doc, setDoc] = useState(null);
5977
6015
  const [numPages, setNumPages] = useState(0);
5978
6016
  const [baseSize, setBaseSize] = useState(null);
6017
+ const [sizes, setSizes] = useState([]);
5979
6018
  const [status, setStatus] = useState("loading");
5980
6019
  const [error, setError] = useState(null);
5981
6020
  const [reloadKey, setReloadKey] = useState(0);
@@ -5988,6 +6027,13 @@ function PdfViewer({
5988
6027
  const [query, setQuery] = useState("");
5989
6028
  const [matchPages, setMatchPages] = useState(null);
5990
6029
  const [matchIdx, setMatchIdx] = useState(0);
6030
+ const [searching, setSearching] = useState(false);
6031
+ const [pageDraft, setPageDraft] = useState("");
6032
+ const pageEditing = useRef(false);
6033
+ const pageText = useRef(/* @__PURE__ */ new Map());
6034
+ const searchSeq = useRef(0);
6035
+ const searchTimer = useRef(null);
6036
+ const measured = useRef(/* @__PURE__ */ new Set());
5991
6037
  const tb = toolbar === true ? { zoom: true, pager: true, download: true, print: true, search: true } : toolbar || {};
5992
6038
  useEffect(() => {
5993
6039
  let cancelled = false;
@@ -5996,7 +6042,12 @@ function PdfViewer({
5996
6042
  setError(null);
5997
6043
  setDoc(null);
5998
6044
  setBaseSize(null);
5999
- loadPdfjs().then(async (pdfjs2) => {
6045
+ setSizes([]);
6046
+ pageText.current.clear();
6047
+ measured.current = /* @__PURE__ */ new Set();
6048
+ setMatchPages(null);
6049
+ setMatchIdx(0);
6050
+ loadPdfjs(workerSrc).then(async (pdfjs2) => {
6000
6051
  if (cancelled) return;
6001
6052
  setPdfjs(pdfjs2);
6002
6053
  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()) };
@@ -6009,9 +6060,12 @@ function PdfViewer({
6009
6060
  const first = await pdf.getPage(1);
6010
6061
  const vp = first.getViewport({ scale: 1 });
6011
6062
  if (cancelled) return;
6063
+ const base = { width: vp.width, height: vp.height };
6012
6064
  setDoc(pdf);
6013
6065
  setNumPages(pdf.numPages);
6014
- setBaseSize({ width: vp.width, height: vp.height });
6066
+ setBaseSize(base);
6067
+ setSizes(Array.from({ length: pdf.numPages }, () => base));
6068
+ measured.current.add(1);
6015
6069
  setStatus("ready");
6016
6070
  onLoad?.({ numPages: pdf.numPages });
6017
6071
  }).catch((err) => {
@@ -6024,7 +6078,7 @@ function PdfViewer({
6024
6078
  cancelled = true;
6025
6079
  task?.destroy?.();
6026
6080
  };
6027
- }, [source, remote, reloadKey]);
6081
+ }, [source, remote, reloadKey, workerSrc]);
6028
6082
  useEffect(() => () => {
6029
6083
  doc?.destroy?.();
6030
6084
  }, [doc]);
@@ -6037,59 +6091,101 @@ function PdfViewer({
6037
6091
  ro.observe(el);
6038
6092
  return () => ro.disconnect();
6039
6093
  }, [status]);
6094
+ const handleMeasure = useCallback((p, size) => {
6095
+ setSizes((prev) => {
6096
+ const cur = prev[p - 1];
6097
+ if (cur && Math.abs(cur.width - size.width) < 0.5 && Math.abs(cur.height - size.height) < 0.5) return prev;
6098
+ const next = prev.slice();
6099
+ next[p - 1] = size;
6100
+ return next;
6101
+ });
6102
+ }, []);
6040
6103
  const scale = useMemo(() => {
6041
6104
  if (!baseSize) return 1;
6042
6105
  if (typeof zoomMode === "number") return zoomMode;
6043
- const avail = Math.max(0, (viewport.w || baseSize.width) - 32);
6106
+ const avail = Math.max(1, (viewport.w || baseSize.width) - 32);
6044
6107
  if (zoomMode === "page-width" || zoomMode === "auto") return avail / baseSize.width;
6045
6108
  if (zoomMode === "page-fit") {
6046
- const availH = Math.max(0, (viewport.h || baseSize.height) - 32);
6109
+ const availH = Math.max(1, (viewport.h || baseSize.height) - 32);
6047
6110
  return Math.min(avail / baseSize.width, availH / baseSize.height);
6048
6111
  }
6049
6112
  return 1;
6050
6113
  }, [zoomMode, baseSize, viewport]);
6051
- const pageH = baseSize ? baseSize.height * scale + GAP : 0;
6052
- const pageW = baseSize ? baseSize.width * scale : 0;
6053
- const total = numPages * pageH;
6114
+ const { offsets, total } = useMemo(() => {
6115
+ const offs = new Array(sizes.length);
6116
+ let acc = 0;
6117
+ for (let i = 0; i < sizes.length; i++) {
6118
+ offs[i] = acc;
6119
+ acc += sizes[i].height * scale + GAP;
6120
+ }
6121
+ return { offsets: offs, total: acc };
6122
+ }, [sizes, scale]);
6054
6123
  const overscan = 1;
6055
- const startIdx = pageH ? Math.max(0, Math.floor(scrollTop / pageH) - overscan) : 0;
6056
- const endIdx = pageH ? Math.min(numPages, Math.ceil((scrollTop + viewport.h) / pageH) + overscan) : 0;
6124
+ const hasLayout = offsets.length > 0 && total > 0;
6125
+ const startIdx = hasLayout ? Math.max(0, pageIndexAt(offsets, scrollTop) - overscan) : 0;
6126
+ const endIdx = hasLayout ? Math.min(numPages, pageIndexAt(offsets, scrollTop + viewport.h) + 1 + overscan) : 0;
6057
6127
  const visiblePages = Array.from({ length: Math.max(0, endIdx - startIdx) }, (_, i) => startIdx + i + 1);
6128
+ const scrollToPage = useCallback((p) => {
6129
+ const el = scrollRef.current;
6130
+ if (!el || !offsets.length) return;
6131
+ const clamped = Math.min(offsets.length, Math.max(1, p));
6132
+ el.scrollTo({ top: offsets[clamped - 1] ?? 0, behavior: "smooth" });
6133
+ }, [offsets]);
6058
6134
  useEffect(() => {
6059
- if (!pageH) return;
6060
- const cur = Math.min(numPages, Math.max(1, Math.floor((scrollTop + viewport.h / 2) / pageH) + 1));
6135
+ if (!hasLayout) return;
6136
+ const cur = pageIndexAt(offsets, scrollTop + viewport.h / 2) + 1;
6061
6137
  if (cur !== page) {
6062
6138
  setPage(cur);
6063
6139
  onPageChange?.(cur);
6064
6140
  }
6065
- }, [scrollTop, pageH, viewport.h, numPages]);
6066
- const scrollToPage = useCallback((p) => {
6067
- const el = scrollRef.current;
6068
- if (!el || !pageH) return;
6069
- el.scrollTo({ top: (p - 1) * pageH, behavior: "smooth" });
6070
- }, [pageH]);
6141
+ }, [scrollTop, offsets, viewport.h, numPages]);
6071
6142
  useEffect(() => {
6072
6143
  if (status === "ready" && initialPage > 1) scrollToPage(initialPage);
6073
- }, [status]);
6144
+ }, [status, initialPage]);
6074
6145
  const runSearch = useCallback(async (q) => {
6075
- setQuery(q);
6076
- if (!doc || !q.trim()) {
6146
+ const needle = q.trim().toLowerCase();
6147
+ if (!doc || !needle) {
6077
6148
  setMatchPages(null);
6078
6149
  setMatchIdx(0);
6150
+ setSearching(false);
6079
6151
  return;
6080
6152
  }
6081
- const needle = q.toLowerCase();
6153
+ const seq = ++searchSeq.current;
6154
+ setSearching(true);
6082
6155
  const hits = [];
6083
6156
  for (let p = 1; p <= numPages; p++) {
6084
- const pg = await doc.getPage(p);
6085
- const tc = await pg.getTextContent();
6086
- const text = tc.items.map((it) => "str" in it ? it.str : "").join(" ").toLowerCase();
6157
+ let text = pageText.current.get(p);
6158
+ if (text === void 0) {
6159
+ const pg = await doc.getPage(p);
6160
+ const tc = await pg.getTextContent();
6161
+ const built = tc.items.map((it) => "str" in it ? it.str : "").join(" ").toLowerCase();
6162
+ pageText.current.set(p, built);
6163
+ text = built;
6164
+ }
6165
+ if (seq !== searchSeq.current) return;
6087
6166
  if (text.includes(needle)) hits.push(p);
6088
6167
  }
6168
+ if (seq !== searchSeq.current) return;
6089
6169
  setMatchPages(hits);
6090
6170
  setMatchIdx(0);
6171
+ setSearching(false);
6091
6172
  if (hits.length) scrollToPage(hits[0]);
6092
6173
  }, [doc, numPages, scrollToPage]);
6174
+ const onQueryChange = useCallback((v) => {
6175
+ setQuery(v);
6176
+ if (searchTimer.current) clearTimeout(searchTimer.current);
6177
+ if (!v.trim()) {
6178
+ searchSeq.current++;
6179
+ setMatchPages(null);
6180
+ setMatchIdx(0);
6181
+ setSearching(false);
6182
+ return;
6183
+ }
6184
+ searchTimer.current = setTimeout(() => runSearch(v), SEARCH_DEBOUNCE);
6185
+ }, [runSearch]);
6186
+ useEffect(() => () => {
6187
+ if (searchTimer.current) clearTimeout(searchTimer.current);
6188
+ }, []);
6093
6189
  const gotoMatch = (dir) => {
6094
6190
  if (!matchPages?.length) return;
6095
6191
  const next = (matchIdx + dir + matchPages.length) % matchPages.length;
@@ -6103,15 +6199,9 @@ function PdfViewer({
6103
6199
  if (typeof Blob !== "undefined" && source instanceof Blob) return new Uint8Array(await source.arrayBuffer());
6104
6200
  throw new Error("No bytes available");
6105
6201
  }, [doc, source]);
6106
- const download2 = useCallback(async () => {
6202
+ const download = useCallback(async () => {
6107
6203
  const bytes = await getBytes();
6108
- const blob = new Blob([bytes], { type: "application/pdf" });
6109
- const url = URL.createObjectURL(blob);
6110
- const a = document.createElement("a");
6111
- a.href = url;
6112
- a.download = sourceName(source) || "document.pdf";
6113
- a.click();
6114
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
6204
+ downloadBlob(new Blob([bytes], { type: "application/pdf" }), sourceName(source) || "document.pdf");
6115
6205
  }, [getBytes, source]);
6116
6206
  const print = useCallback(async () => {
6117
6207
  const bytes = await getBytes();
@@ -6137,6 +6227,12 @@ function PdfViewer({
6137
6227
  const cur = typeof zoomMode === "number" ? zoomMode : scale;
6138
6228
  setZoomMode(Math.min(5, Math.max(0.25, +(cur * factor).toFixed(2))));
6139
6229
  };
6230
+ const zoomPct = Math.round(scale * 100);
6231
+ const commitPageDraft = () => {
6232
+ pageEditing.current = false;
6233
+ const n = parseInt(pageDraft, 10);
6234
+ if (Number.isFinite(n)) scrollToPage(n);
6235
+ };
6140
6236
  if (status === "error") {
6141
6237
  return /* @__PURE__ */ 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: [
6142
6238
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-status-error", children: "Couldn\u2019t load the PDF" }),
@@ -6144,80 +6240,127 @@ function PdfViewer({
6144
6240
  /* @__PURE__ */ jsx(Button_default, { content: "Retry", size: "sm", variant: "outline", onClick: () => setReloadKey((k) => k + 1) })
6145
6241
  ] });
6146
6242
  }
6243
+ const ready = status === "ready";
6147
6244
  return /* @__PURE__ */ jsxs("div", { className: cx("flex flex-col overflow-hidden rounded-lg border border-border bg-surface-raised", className), style: { height: 600, ...style }, children: [
6148
- toolbar !== false && /* @__PURE__ */ 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: [
6149
- tb.pager && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
6150
- /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Previous page", onClick: () => scrollToPage(Math.max(1, page - 1)), icon: /* @__PURE__ */ jsx(Chevron5, { dir: "up" }), disabled: status !== "ready" }),
6151
- /* @__PURE__ */ jsxs("span", { className: "px-1 text-xs tabular-nums text-foreground-secondary select-none", children: [
6152
- status === "ready" ? page : "\u2013",
6153
- " / ",
6154
- numPages || "\u2013"
6245
+ toolbar !== false && /* @__PURE__ */ jsxs("div", { className: "flex flex-shrink-0 flex-col border-b border-border bg-surface", children: [
6246
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 px-2 py-1.5", children: [
6247
+ tb.pager && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
6248
+ /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Previous page", onClick: () => scrollToPage(page - 1), icon: /* @__PURE__ */ jsx(Chevron5, { dir: "up" }), disabled: !ready || page <= 1 }),
6249
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-xs text-foreground-secondary select-none", children: [
6250
+ /* @__PURE__ */ jsx(
6251
+ "input",
6252
+ {
6253
+ value: pageEditing.current ? pageDraft : ready ? String(page) : "\u2013",
6254
+ onFocus: () => {
6255
+ pageEditing.current = true;
6256
+ setPageDraft(String(page));
6257
+ },
6258
+ onChange: (e) => setPageDraft(e.target.value.replace(/[^\d]/g, "")),
6259
+ onBlur: commitPageDraft,
6260
+ onKeyDown: (e) => {
6261
+ if (e.key === "Enter") e.target.blur();
6262
+ },
6263
+ disabled: !ready,
6264
+ "aria-label": "Page number",
6265
+ 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"
6266
+ }
6267
+ ),
6268
+ /* @__PURE__ */ jsxs("span", { className: "tabular-nums", children: [
6269
+ "/ ",
6270
+ numPages || "\u2013"
6271
+ ] })
6272
+ ] }),
6273
+ /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Next page", onClick: () => scrollToPage(page + 1), icon: /* @__PURE__ */ jsx(Chevron5, { dir: "down" }), disabled: !ready || page >= numPages })
6155
6274
  ] }),
6156
- /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Next page", onClick: () => scrollToPage(Math.min(numPages, page + 1)), icon: /* @__PURE__ */ jsx(Chevron5, { dir: "down" }), disabled: status !== "ready" })
6157
- ] }),
6158
- tb.zoom && /* @__PURE__ */ jsxs("div", { className: "ml-1 flex items-center gap-1", children: [
6159
- /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Zoom out", onClick: () => setZoomNum(1 / 1.2), icon: /* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: "\u2212" }), disabled: status !== "ready" }),
6160
- /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Zoom in", onClick: () => setZoomNum(1.2), icon: /* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: "+" }), disabled: status !== "ready" }),
6161
- /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Fit width", onClick: () => setZoomMode("page-width"), icon: /* @__PURE__ */ jsx(FitWidthIcon, {}), disabled: status !== "ready" }),
6162
- /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Fit page", onClick: () => setZoomMode("page-fit"), icon: /* @__PURE__ */ jsx(FitPageIcon, {}), disabled: status !== "ready" })
6163
- ] }),
6164
- /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
6165
- tb.search && /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Search", onClick: () => setShowSearch((s) => !s), icon: /* @__PURE__ */ jsx(SearchIcon2, {}), disabled: status !== "ready" }),
6166
- tb.download && /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Download", onClick: download2, icon: /* @__PURE__ */ jsx(DownloadIcon, {}), disabled: status !== "ready" }),
6167
- tb.print && /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Print", onClick: print, icon: /* @__PURE__ */ jsx(PrintIcon, {}), disabled: status !== "ready" })
6275
+ tb.pager && tb.zoom && /* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px bg-border", "aria-hidden": "true" }),
6276
+ tb.zoom && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
6277
+ /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Zoom out", onClick: () => setZoomNum(1 / 1.2), icon: /* @__PURE__ */ jsx(MinusIcon, {}), disabled: !ready }),
6278
+ /* @__PURE__ */ jsx("span", { className: "w-10 text-center text-xs tabular-nums text-foreground-secondary select-none", children: ready ? `${zoomPct}%` : "\u2013" }),
6279
+ /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Zoom in", onClick: () => setZoomNum(1.2), icon: /* @__PURE__ */ jsx(PlusIcon, {}), disabled: !ready }),
6280
+ /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Fit width", onClick: () => setZoomMode("page-width"), icon: /* @__PURE__ */ jsx(FitWidthIcon, {}), disabled: !ready }),
6281
+ /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Fit page", onClick: () => setZoomMode("page-fit"), icon: /* @__PURE__ */ jsx(FitPageIcon, {}), disabled: !ready })
6282
+ ] }),
6283
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
6284
+ tb.search && /* @__PURE__ */ jsx(IconButton_default, { type: showSearch ? "bordered" : "ghost", size: "sm", title: "Search", onClick: () => setShowSearch((s) => !s), icon: /* @__PURE__ */ jsx(SearchIcon2, {}), disabled: !ready }),
6285
+ tb.search && (tb.download || tb.print) && /* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px bg-border", "aria-hidden": "true" }),
6286
+ tb.download && /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Download", onClick: download, icon: /* @__PURE__ */ jsx(DownloadIcon, {}), disabled: !ready }),
6287
+ tb.print && /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Print", onClick: print, icon: /* @__PURE__ */ jsx(PrintIcon, {}), disabled: !ready })
6288
+ ] })
6168
6289
  ] }),
6169
- tb.search && showSearch && /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-2 pt-1", children: [
6290
+ tb.search && showSearch && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-t border-border px-2 py-1.5", children: [
6291
+ /* @__PURE__ */ jsx(SearchIcon2, {}),
6170
6292
  /* @__PURE__ */ jsx(
6171
6293
  "input",
6172
6294
  {
6173
6295
  autoFocus: true,
6174
6296
  value: query,
6175
- onChange: (e) => runSearch(e.target.value),
6297
+ onChange: (e) => onQueryChange(e.target.value),
6298
+ onKeyDown: (e) => {
6299
+ if (e.key === "Enter") gotoMatch(e.shiftKey ? -1 : 1);
6300
+ },
6176
6301
  placeholder: "Find in document\u2026",
6177
- className: "h-7 flex-1 rounded-md border border-border bg-surface px-2 text-sm text-foreground outline-none focus:border-accent"
6302
+ className: "h-7 flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-foreground-muted"
6178
6303
  }
6179
6304
  ),
6180
- /* @__PURE__ */ jsx("span", { className: "text-xs tabular-nums text-foreground-muted", children: matchPages == null ? "" : matchPages.length ? `${matchIdx + 1}/${matchPages.length} pages` : "no matches" }),
6305
+ /* @__PURE__ */ 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" }),
6181
6306
  /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Previous match", onClick: () => gotoMatch(-1), icon: /* @__PURE__ */ jsx(Chevron5, { dir: "up" }), disabled: !matchPages?.length }),
6182
- /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Next match", onClick: () => gotoMatch(1), icon: /* @__PURE__ */ jsx(Chevron5, { dir: "down" }), disabled: !matchPages?.length })
6307
+ /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Next match", onClick: () => gotoMatch(1), icon: /* @__PURE__ */ jsx(Chevron5, { dir: "down" }), disabled: !matchPages?.length }),
6308
+ /* @__PURE__ */ jsx(IconButton_default, { type: "ghost", size: "sm", title: "Close search", onClick: () => {
6309
+ setShowSearch(false);
6310
+ onQueryChange("");
6311
+ }, icon: /* @__PURE__ */ jsx(CloseIcon, {}) })
6183
6312
  ] })
6184
6313
  ] }),
6185
6314
  /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1", children: [
6186
- thumbnails && status === "ready" && doc && baseSize && /* @__PURE__ */ 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__ */ jsx(
6187
- "button",
6188
- {
6189
- type: "button",
6190
- onClick: () => scrollToPage(p),
6191
- className: cx(
6192
- "mb-2 block w-full overflow-hidden rounded border bg-surface-raised transition-colors",
6193
- p === page ? "border-accent ring-1 ring-accent" : "border-border hover:border-border-strong"
6194
- ),
6195
- style: { aspectRatio: `${baseSize.width} / ${baseSize.height}` },
6196
- "aria-label": `Page ${p}`,
6197
- children: Math.abs(p - page) <= 8 ? /* @__PURE__ */ jsx(PdfPage, { pdfjs, doc, page: p, scale: 112 / baseSize.width, textLayer: false }) : /* @__PURE__ */ jsx("span", { className: "block py-4 text-center text-xs text-foreground-muted", children: p })
6198
- },
6199
- p
6200
- )) }),
6315
+ thumbnails && ready && doc && baseSize && /* @__PURE__ */ 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) => {
6316
+ const s = sizes[p - 1] ?? baseSize;
6317
+ return /* @__PURE__ */ jsx(
6318
+ "button",
6319
+ {
6320
+ type: "button",
6321
+ onClick: () => scrollToPage(p),
6322
+ className: cx(
6323
+ "mb-2 block w-full overflow-hidden rounded border bg-surface-raised transition-colors",
6324
+ p === page ? "border-accent ring-1 ring-accent" : "border-border hover:border-border-strong"
6325
+ ),
6326
+ style: { aspectRatio: `${s.width} / ${s.height}` },
6327
+ "aria-label": `Page ${p}`,
6328
+ "aria-current": p === page,
6329
+ children: Math.abs(p - page) <= 8 ? /* @__PURE__ */ jsx(PdfPage, { pdfjs, doc, page: p, scale: 112 / s.width, textLayer: false }) : /* @__PURE__ */ jsx("span", { className: "block py-4 text-center text-xs text-foreground-muted", children: p })
6330
+ },
6331
+ p
6332
+ );
6333
+ }) }),
6201
6334
  /* @__PURE__ */ jsx(
6202
6335
  "div",
6203
6336
  {
6204
6337
  ref: scrollRef,
6205
6338
  onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
6206
6339
  className: "relative flex-1 overflow-auto bg-background",
6207
- children: status === "loading" || !baseSize ? /* @__PURE__ */ jsx("div", { className: "flex flex-col items-center gap-3 p-6", children: /* @__PURE__ */ jsx(SkeletonBox, { width: Math.min(viewport.w - 48, 560) || 480, height: 680, className: "rounded" }) }) : numPages === 0 ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-sm text-foreground-muted", children: "Empty document" }) : /* @__PURE__ */ jsx("div", { style: { height: total, position: "relative" }, children: visiblePages.map((p) => /* @__PURE__ */ jsx(
6208
- "div",
6209
- {
6210
- style: { position: "absolute", top: (p - 1) * pageH, left: 0, right: 0, height: pageH, display: "flex", justifyContent: "center", paddingTop: GAP / 2, paddingBottom: GAP / 2 },
6211
- children: /* @__PURE__ */ jsx("div", { className: "relative shadow-md", style: { width: pageW, height: baseSize.height * scale }, children: /* @__PURE__ */ jsx(PdfPage, { pdfjs, doc, page: p, scale, textLayer }) })
6212
- },
6213
- p
6214
- )) })
6340
+ children: !ready || !baseSize ? /* @__PURE__ */ jsx("div", { className: "flex flex-col items-center gap-3 p-6", children: /* @__PURE__ */ jsx(SkeletonBox, { width: Math.min((viewport.w || 528) - 48, 560), height: 680, className: "rounded" }) }) : numPages === 0 ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-sm text-foreground-muted", children: "Empty document" }) : /* @__PURE__ */ jsx("div", { style: { height: total, position: "relative" }, children: visiblePages.map((p) => {
6341
+ const s = sizes[p - 1] ?? baseSize;
6342
+ return /* @__PURE__ */ jsx(
6343
+ "div",
6344
+ {
6345
+ style: { position: "absolute", top: offsets[p - 1], left: 0, right: 0, display: "flex", justifyContent: "center", paddingTop: GAP / 2 },
6346
+ children: /* @__PURE__ */ jsx("div", { className: "relative shadow-md", style: { width: s.width * scale, height: s.height * scale }, children: /* @__PURE__ */ jsx(PdfPage, { pdfjs, doc, page: p, scale, textLayer, onMeasure: handleMeasure }) })
6347
+ },
6348
+ p
6349
+ );
6350
+ }) })
6215
6351
  }
6216
6352
  )
6217
6353
  ] })
6218
6354
  ] });
6219
6355
  }
6220
- function PdfPage({ pdfjs, doc, page, scale, textLayer }) {
6356
+ function PdfPage({
6357
+ pdfjs,
6358
+ doc,
6359
+ page,
6360
+ scale,
6361
+ textLayer,
6362
+ onMeasure
6363
+ }) {
6221
6364
  const canvasRef = useRef(null);
6222
6365
  const textRef = useRef(null);
6223
6366
  useEffect(() => {
@@ -6227,6 +6370,7 @@ function PdfPage({ pdfjs, doc, page, scale, textLayer }) {
6227
6370
  const pg = await doc.getPage(page);
6228
6371
  if (cancelled) return;
6229
6372
  const viewport = pg.getViewport({ scale });
6373
+ onMeasure?.(page, { width: viewport.width / scale, height: viewport.height / scale });
6230
6374
  const canvas = canvasRef.current;
6231
6375
  if (!canvas) return;
6232
6376
  const ratio = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
@@ -6243,6 +6387,7 @@ function PdfPage({ pdfjs, doc, page, scale, textLayer }) {
6243
6387
  }
6244
6388
  if (cancelled || !textLayer || !textRef.current || !pdfjs?.TextLayer) return;
6245
6389
  try {
6390
+ ensureTextLayerStyles();
6246
6391
  textRef.current.innerHTML = "";
6247
6392
  textRef.current.style.setProperty("--scale-factor", String(scale));
6248
6393
  const tl = new pdfjs.TextLayer({ textContentSource: pg.streamTextContent(), container: textRef.current, viewport });
@@ -6254,17 +6399,20 @@ function PdfPage({ pdfjs, doc, page, scale, textLayer }) {
6254
6399
  cancelled = true;
6255
6400
  renderTask?.cancel?.();
6256
6401
  };
6257
- }, [pdfjs, doc, page, scale, textLayer]);
6402
+ }, [pdfjs, doc, page, scale, textLayer, onMeasure]);
6258
6403
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6259
6404
  /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "block bg-white" }),
6260
- textLayer && /* @__PURE__ */ jsx("div", { ref: textRef, className: "textLayer pointer-events-auto absolute inset-0 overflow-hidden leading-none", style: { opacity: 0.25 }, "aria-hidden": "true" })
6405
+ textLayer && /* @__PURE__ */ jsx("div", { ref: textRef, className: "oxy-textLayer", "aria-hidden": "true" })
6261
6406
  ] });
6262
6407
  }
6263
6408
  var Chevron5 = ({ dir }) => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: dir === "up" ? "m6 15 6-6 6 6" : "m6 9 6 6 6-6" }) });
6264
- var SearchIcon2 = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: [
6409
+ var MinusIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "M5 12h14" }) });
6410
+ var PlusIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "M12 5v14M5 12h14" }) });
6411
+ var SearchIcon2 = () => /* @__PURE__ */ 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: [
6265
6412
  /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
6266
6413
  /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "m21 21-4.3-4.3" })
6267
6414
  ] });
6415
+ var CloseIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "M6 6l12 12M18 6 6 18" }) });
6268
6416
  var DownloadIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 3v12m0 0 4-4m-4 4-4-4M4 21h16" }) });
6269
6417
  var PrintIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 9V3h12v6M6 18H4v-7h16v7h-2M8 14h8v7H8z" }) });
6270
6418
  var FitWidthIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 12h18m0 0-4-4m4 4-4 4M3 12l4-4m-4 4 4 4" }) });
@@ -6275,10 +6423,10 @@ var FitPageIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill
6275
6423
  var DEFAULT_COL_WIDTH = 140;
6276
6424
  var GUTTER = 52;
6277
6425
  function resolveWidth(w) {
6278
- if (typeof w === "number") return w;
6426
+ if (typeof w === "number" && Number.isFinite(w)) return w;
6279
6427
  if (typeof w === "string") {
6280
- const n = parseFloat(w);
6281
- if (!Number.isNaN(n) && /^\d/.test(w.trim())) return n;
6428
+ const m = /^\s*(\d+(?:\.\d+)?)(px)?\s*$/.exec(w);
6429
+ if (m) return parseFloat(m[1]);
6282
6430
  }
6283
6431
  return DEFAULT_COL_WIDTH;
6284
6432
  }
@@ -6471,13 +6619,16 @@ var xlsxPromise = null;
6471
6619
  var loadXlsx = () => xlsxPromise ??= import('xlsx');
6472
6620
  var jspdfPromise = null;
6473
6621
  var loadJspdf = () => jspdfPromise ??= import('jspdf');
6474
- function download(blob, name) {
6475
- const url = URL.createObjectURL(blob);
6476
- const a = document.createElement("a");
6477
- a.href = url;
6478
- a.download = name;
6479
- a.click();
6480
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
6622
+ function coerceToCellType(prev, next) {
6623
+ if (typeof prev === "number") {
6624
+ const n = Number(next);
6625
+ return next.trim() !== "" && Number.isFinite(n) ? n : next;
6626
+ }
6627
+ if (typeof prev === "boolean") {
6628
+ if (/^(true|false)$/i.test(next.trim())) return next.trim().toLowerCase() === "true";
6629
+ return next;
6630
+ }
6631
+ return next;
6481
6632
  }
6482
6633
  function Spreadsheet({
6483
6634
  source,
@@ -6545,20 +6696,23 @@ function Spreadsheet({
6545
6696
  const columns = useMemo(() => sheet ? toColumns(sheet.columns) : [], [sheet]);
6546
6697
  const plainRows = useMemo(() => sheet ? toPlainRows(sheet, columns) : [], [sheet, columns]);
6547
6698
  const handleCellEdit = useCallback(({ row, column, value }) => {
6699
+ let coerced = value;
6548
6700
  setSheets((prev) => {
6549
6701
  if (!prev) return prev;
6550
6702
  const next = prev.map((s, i) => i === active ? { ...s, rows: s.rows.map((r) => ({ ...r })) } : s);
6551
6703
  const target = next[active];
6552
6704
  const existing = target.rows[row]?.[column];
6705
+ const prevValue = cellValue(existing);
6706
+ coerced = coerceToCellType(prevValue, value);
6553
6707
  if (existing != null && typeof existing === "object" && "value" in existing) {
6554
- target.rows[row][column] = { ...existing, value };
6708
+ target.rows[row][column] = { ...existing, value: coerced };
6555
6709
  } else {
6556
- target.rows[row][column] = value;
6710
+ target.rows[row][column] = coerced;
6557
6711
  }
6558
6712
  onChange?.(next);
6559
6713
  return next;
6560
6714
  });
6561
- onCellEdit?.({ sheet: sheets?.[active]?.name ?? "", row, column, value });
6715
+ onCellEdit?.({ sheet: sheets?.[active]?.name ?? "", row, column, value: coerced });
6562
6716
  }, [active, onCellEdit, onChange, sheets]);
6563
6717
  const baseName = useMemo(
6564
6718
  () => (fileName || (typeof source === "object" && "name" in source ? sourceName(source) : null) || "spreadsheet").replace(/\.[^.]+$/, ""),
@@ -6579,7 +6733,7 @@ function Spreadsheet({
6579
6733
  XLSX.utils.book_append_sheet(wb, ws, (s.name || "Sheet").slice(0, 31));
6580
6734
  });
6581
6735
  const out = XLSX.write(wb, { bookType: "xlsx", type: "array" });
6582
- download(new Blob([out], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), `${baseName}.xlsx`);
6736
+ downloadBlob(new Blob([out], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), `${baseName}.xlsx`);
6583
6737
  }, [sheets, sheetAoa, baseName]);
6584
6738
  const exportCsv = useCallback(() => {
6585
6739
  if (!sheet) return;
@@ -6589,7 +6743,7 @@ function Spreadsheet({
6589
6743
  return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
6590
6744
  };
6591
6745
  const csv = aoa.map((row) => row.map(esc).join(",")).join("\r\n");
6592
- download(new Blob([`\uFEFF${csv}`], { type: "text/csv;charset=utf-8" }), `${baseName}.csv`);
6746
+ downloadBlob(new Blob([`\uFEFF${csv}`], { type: "text/csv;charset=utf-8" }), `${baseName}.csv`);
6593
6747
  }, [sheet, sheetAoa, baseName]);
6594
6748
  const exportPdf = useCallback(async () => {
6595
6749
  if (!sheet) return;
@@ -6645,9 +6799,14 @@ function Spreadsheet({
6645
6799
  ] });
6646
6800
  }
6647
6801
  const formats = exportFormats || [];
6648
- return /* @__PURE__ */ jsxs("div", { className: cx("flex flex-col gap-2", className), style, children: [
6649
- (sheets.length > 1 || formats.length > 0) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
6650
- sheets.length > 1 && /* @__PURE__ */ 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__ */ jsx(
6802
+ const exportLabels = {
6803
+ xlsx: "Excel workbook (.xlsx)",
6804
+ csv: "CSV \u2014 this sheet (.csv)",
6805
+ pdf: "PDF table (.pdf)"
6806
+ };
6807
+ return /* @__PURE__ */ jsxs("div", { className: cx("flex flex-col overflow-hidden rounded-lg border border-border bg-surface-raised", className), style, children: [
6808
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-shrink-0 items-center gap-2 border-b border-border bg-surface px-2 py-1.5", children: [
6809
+ sheets.length > 1 ? /* @__PURE__ */ 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__ */ jsx(
6651
6810
  "button",
6652
6811
  {
6653
6812
  role: "tab",
@@ -6655,14 +6814,40 @@ function Spreadsheet({
6655
6814
  "aria-selected": i === active,
6656
6815
  onClick: () => setActive(i),
6657
6816
  className: cx(
6658
- "rounded-md px-3 py-1 text-sm transition-colors",
6659
- i === active ? "bg-accent text-accent-fg" : "text-foreground-secondary hover:bg-surface-raised hover:text-foreground"
6817
+ "flex-shrink-0 rounded-md px-3 py-1 text-sm font-medium transition-colors",
6818
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
6819
+ i === active ? "bg-accent text-accent-fg shadow-sm" : "text-foreground-secondary hover:bg-surface-raised hover:text-foreground"
6660
6820
  ),
6661
6821
  children: s.name || `Sheet ${i + 1}`
6662
6822
  },
6663
6823
  `${s.name}-${i}`
6664
- )) }),
6665
- formats.length > 0 && /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-1.5", children: formats.map((fmt) => /* @__PURE__ */ jsx(Button_default, { content: fmt.toUpperCase(), size: "sm", variant: "outline", onClick: () => runExport(fmt) }, fmt)) })
6824
+ )) }) : /* @__PURE__ */ jsx("span", { className: "flex-1 truncate px-1 text-sm font-medium text-foreground", children: sheet?.name || "Sheet 1" }),
6825
+ /* @__PURE__ */ jsxs("span", { className: "hidden flex-shrink-0 px-1 text-xs tabular-nums text-foreground-muted sm:inline", children: [
6826
+ plainRows.length.toLocaleString(),
6827
+ " ",
6828
+ plainRows.length === 1 ? "row" : "rows",
6829
+ " \xB7 ",
6830
+ columns.length,
6831
+ " cols"
6832
+ ] }),
6833
+ formats.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
6834
+ /* @__PURE__ */ jsx("span", { className: "h-5 w-px flex-shrink-0 bg-border", "aria-hidden": "true" }),
6835
+ /* @__PURE__ */ jsx(
6836
+ MenuButton,
6837
+ {
6838
+ label: "Export",
6839
+ size: "sm",
6840
+ variant: "outline",
6841
+ align: "end",
6842
+ icon: /* @__PURE__ */ jsx(DownloadIcon2, {}),
6843
+ items: formats.map((fmt) => ({
6844
+ key: fmt,
6845
+ label: exportLabels[fmt],
6846
+ onSelect: () => runExport(fmt)
6847
+ }))
6848
+ }
6849
+ )
6850
+ ] })
6666
6851
  ] }),
6667
6852
  /* @__PURE__ */ jsx(
6668
6853
  DataGrid,
@@ -6671,11 +6856,13 @@ function Spreadsheet({
6671
6856
  rows: plainRows,
6672
6857
  editable,
6673
6858
  virtualize,
6674
- onCellEdit: handleCellEdit
6859
+ onCellEdit: handleCellEdit,
6860
+ className: "!rounded-none !border-0"
6675
6861
  }
6676
6862
  )
6677
6863
  ] });
6678
6864
  }
6865
+ var DownloadIcon2 = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 3v12m0 0 4-4m-4 4-4-4M4 21h16" }) });
6679
6866
  function ThemeSwitch({ checked, onChange, label = "Toggle dark mode", className = "" }) {
6680
6867
  const id = useId();
6681
6868
  return /* @__PURE__ */ jsx("label", { htmlFor: id, className: `flex items-center gap-2 cursor-pointer select-none ${className}`.trim(), children: /* @__PURE__ */ jsx(