@particle-academy/fancy-code 0.13.0 → 0.15.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/CHANGELOG.md CHANGED
@@ -14,6 +14,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.15.0] - 2026-10-19
18
+
19
+ ### Added
20
+
21
+ - **Office files open in the kit's own viewers.** `registerOfficeViewer(kind,
22
+ View, { read })` wires a viewer for `pptx`, `xlsx` or `docx` into
23
+ `FileViewer`, one line per format at app start:
24
+
25
+ ```ts
26
+ registerOfficeViewer("pptx", DeckFileView, { read: (b) => new PptxReader().read(b) });
27
+ ```
28
+
29
+ `DeckFileView` ships in fancy-slides and `WorkbookFileView` in fancy-sheets.
30
+ The viewer gets the file's contents in this order: the new `document` prop,
31
+ the contents the app's server already read (preferred); else the file at
32
+ `src`, read in the browser by the registered `read`; else the download card
33
+ as before. A file the reader cannot open shows the reason beside the
34
+ download. A registry rather than an import so fancy-code never depends on the
35
+ slide or sheet packages, and an app with neither cannot fail to build on them.
36
+ **Nothing to do** unless you want Office previews: unregistered formats behave
37
+ exactly as in 0.14.
38
+
39
+ ## [0.14.0] - 2026-10-19
40
+
41
+ ### Changed
42
+
43
+ - **Images and PDFs in `FileViewer` sit in the `FauxClient` frame**, like
44
+ documents: an image gets the frame's zoom bar (driving its own pan/zoom) and
45
+ full screen; a PDF gets full screen, with zoom left to the browser's PDF
46
+ toolbar, and its file name in the address bar. Revealed on hover.
47
+ **What to do:** nothing to keep the new look. To turn it off, pass
48
+ `mediaProps={{ imageProps: { frame: false } }}` (or `pdfProps`).
49
+ - **Requires react-fancy 5.49.0 or later** (peer `>=5.49.0 <6.0.0`).
50
+
17
51
  ## [0.13.0] - 2026-10-19
18
52
 
19
53
  ### Added
package/dist/index.cjs CHANGED
@@ -1721,6 +1721,18 @@ function MarkdownEditor({
1721
1721
  );
1722
1722
  }
1723
1723
  MarkdownEditor.displayName = "MarkdownEditor";
1724
+
1725
+ // src/office-viewers.ts
1726
+ var registry = /* @__PURE__ */ new Map();
1727
+ function registerOfficeViewer(kind, View, options = {}) {
1728
+ registry.set(kind, { View, read: options.read });
1729
+ }
1730
+ function officeViewerFor(kind) {
1731
+ return registry.get(kind);
1732
+ }
1733
+ function clearOfficeViewers() {
1734
+ registry.clear();
1735
+ }
1724
1736
  var OFFICE_EXT = { docx: "docx", xlsx: "xlsx", xlsm: "xlsx", pptx: "pptx" };
1725
1737
  var OFFICE_MIME = {
1726
1738
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
@@ -1780,6 +1792,7 @@ function FileViewer({
1780
1792
  mediaProps,
1781
1793
  view = "rendered",
1782
1794
  frameProps,
1795
+ document,
1783
1796
  className,
1784
1797
  style
1785
1798
  }) {
@@ -1812,12 +1825,24 @@ function FileViewer({
1812
1825
  "data-office-kind": resolved.officeKind,
1813
1826
  className: reactFancy.cn("overflow-hidden", className),
1814
1827
  style,
1815
- children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.MediaViewer, { src: src ?? "", mime, alt: filename, className: "h-full w-full" })
1828
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1829
+ OfficeFile,
1830
+ {
1831
+ kind: resolved.officeKind,
1832
+ document,
1833
+ src,
1834
+ mime,
1835
+ filename,
1836
+ frameProps
1837
+ }
1838
+ )
1816
1839
  }
1817
1840
  );
1818
1841
  }
1819
1842
  if (resolved.kind === "media") {
1820
1843
  const { className: mediaClassName, ...restMediaProps } = mediaProps ?? {};
1844
+ restMediaProps.imageProps = { frame: true, frameProps, ...restMediaProps.imageProps };
1845
+ restMediaProps.pdfProps = { frame: true, frameProps, ...restMediaProps.pdfProps };
1821
1846
  const fill = resolved.mediaKind !== "audio";
1822
1847
  return /* @__PURE__ */ jsxRuntime.jsx(
1823
1848
  "div",
@@ -1870,6 +1895,57 @@ function FileViewer({
1870
1895
  );
1871
1896
  }
1872
1897
  FileViewer.displayName = "FileViewer";
1898
+ function OfficeFile({
1899
+ kind,
1900
+ document,
1901
+ src,
1902
+ mime,
1903
+ filename,
1904
+ frameProps
1905
+ }) {
1906
+ const registration = officeViewerFor(kind);
1907
+ const read = registration?.read;
1908
+ const [state, setState] = react.useState({ status: "idle" });
1909
+ react.useEffect(() => {
1910
+ if (!registration || document !== void 0 || !src || !read) return;
1911
+ let live = true;
1912
+ setState({ status: "reading" });
1913
+ fetch(src).then(async (response) => {
1914
+ if (!response.ok) throw new Error(`The file could not be fetched (${response.status}).`);
1915
+ const bytes = new Uint8Array(await response.arrayBuffer());
1916
+ const doc = read(bytes);
1917
+ if (live) setState({ status: "read", document: doc });
1918
+ }).catch((error) => {
1919
+ if (live) setState({ status: "failed", message: error instanceof Error ? error.message : String(error) });
1920
+ });
1921
+ return () => {
1922
+ live = false;
1923
+ };
1924
+ }, [registration, document, src, read]);
1925
+ const fallback = /* @__PURE__ */ jsxRuntime.jsx(reactFancy.MediaViewer, { src: src ?? "", mime, alt: filename, className: "h-full w-full" });
1926
+ if (!registration) return fallback;
1927
+ const View = registration.View;
1928
+ if (document !== void 0) return /* @__PURE__ */ jsxRuntime.jsx(View, { document, filename, frameProps });
1929
+ if (state.status === "read") return /* @__PURE__ */ jsxRuntime.jsx(View, { document: state.document, filename, frameProps });
1930
+ if (state.status === "reading") {
1931
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full items-center justify-center p-6 text-sm text-zinc-500", children: [
1932
+ "Opening ",
1933
+ filename ?? "the file",
1934
+ "\u2026"
1935
+ ] });
1936
+ }
1937
+ if (state.status === "failed") {
1938
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full flex-col gap-2 p-2", children: [
1939
+ /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.Callout, { color: "red", children: [
1940
+ filename ?? "This file",
1941
+ " could not be opened: ",
1942
+ state.message
1943
+ ] }),
1944
+ fallback
1945
+ ] });
1946
+ }
1947
+ return fallback;
1948
+ }
1873
1949
  var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
1874
1950
  function textToHtml(text) {
1875
1951
  return text.replace(/\r\n?/g, "\n").split(/\n{2,}/).filter((block) => block.trim() !== "").map((block) => `<p>${block.replace(/[&<>"']/g, (c) => ESCAPES[c]).replace(/\n/g, "<br>")}</p>`).join("");
@@ -2009,6 +2085,7 @@ exports.CodeEditor = CodeEditor;
2009
2085
  exports.CodeEditorInputTag = CodeEditorInputTag;
2010
2086
  exports.FileViewer = FileViewer;
2011
2087
  exports.MarkdownEditor = MarkdownEditor;
2088
+ exports.clearOfficeViewers = clearOfficeViewers;
2012
2089
  exports.diffMarkColor = diffMarkColor;
2013
2090
  exports.diffRemovedColor = diffRemovedColor;
2014
2091
  exports.getLanguage = getLanguage;
@@ -2017,7 +2094,9 @@ exports.getRegisteredThemes = getRegisteredThemes;
2017
2094
  exports.getTheme = getTheme;
2018
2095
  exports.gutterDiffMark = gutterDiffMark;
2019
2096
  exports.hasDeletedAtEnd = hasDeletedAtEnd;
2097
+ exports.officeViewerFor = officeViewerFor;
2020
2098
  exports.registerLanguage = registerLanguage;
2099
+ exports.registerOfficeViewer = registerOfficeViewer;
2021
2100
  exports.registerTheme = registerTheme;
2022
2101
  exports.renderMarkdown = renderMarkdown;
2023
2102
  exports.resolveFileKind = resolveFileKind;