@particle-academy/fancy-code 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -14,6 +14,34 @@ 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.1] - 2026-11-01
18
+
19
+ ### Changed
20
+
21
+ - **The `@particle-academy/react-fancy` peer range now also accepts 6.x** (`>=5.49.0 <6.0.0` to `>=5.49.0 <7.0.0`). react-fancy 6.0.0 makes `Button` default to `type="button"` instead of submitting an enclosing form (Change Council CC-fancy-0003). Audited for that: it renders no react-fancy `Button` and no form. No `Button` needed `type="submit"` and no `PromptInput` is used, so the package behaves the same on 5 and 6. The devDependency moved to `^6.0.0`, so this package's own suite, typecheck and build now run against 6. Widening only adds candidates, so the react-fancy you already have still resolves. **Consumer action:** none on react-fancy 5; on react-fancy 6 it works as-is.
22
+
23
+ ## [0.15.0] - 2026-10-19
24
+
25
+ ### Added
26
+
27
+ - **Office files open in the kit's own viewers.** `registerOfficeViewer(kind,
28
+ View, { read })` wires a viewer for `pptx`, `xlsx` or `docx` into
29
+ `FileViewer`, one line per format at app start:
30
+
31
+ ```ts
32
+ registerOfficeViewer("pptx", DeckFileView, { read: (b) => new PptxReader().read(b) });
33
+ ```
34
+
35
+ `DeckFileView` ships in fancy-slides and `WorkbookFileView` in fancy-sheets.
36
+ The viewer gets the file's contents in this order: the new `document` prop,
37
+ the contents the app's server already read (preferred); else the file at
38
+ `src`, read in the browser by the registered `read`; else the download card
39
+ as before. A file the reader cannot open shows the reason beside the
40
+ download. A registry rather than an import so fancy-code never depends on the
41
+ slide or sheet packages, and an app with neither cannot fail to build on them.
42
+ **Nothing to do** unless you want Office previews: unregistered formats behave
43
+ exactly as in 0.14.
44
+
17
45
  ## [0.14.0] - 2026-10-19
18
46
 
19
47
  ### Changed
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,7 +1825,17 @@ 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
  }
@@ -1872,6 +1895,57 @@ function FileViewer({
1872
1895
  );
1873
1896
  }
1874
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
+ }
1875
1949
  var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
1876
1950
  function textToHtml(text) {
1877
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("");
@@ -2011,6 +2085,7 @@ exports.CodeEditor = CodeEditor;
2011
2085
  exports.CodeEditorInputTag = CodeEditorInputTag;
2012
2086
  exports.FileViewer = FileViewer;
2013
2087
  exports.MarkdownEditor = MarkdownEditor;
2088
+ exports.clearOfficeViewers = clearOfficeViewers;
2014
2089
  exports.diffMarkColor = diffMarkColor;
2015
2090
  exports.diffRemovedColor = diffRemovedColor;
2016
2091
  exports.getLanguage = getLanguage;
@@ -2019,7 +2094,9 @@ exports.getRegisteredThemes = getRegisteredThemes;
2019
2094
  exports.getTheme = getTheme;
2020
2095
  exports.gutterDiffMark = gutterDiffMark;
2021
2096
  exports.hasDeletedAtEnd = hasDeletedAtEnd;
2097
+ exports.officeViewerFor = officeViewerFor;
2022
2098
  exports.registerLanguage = registerLanguage;
2099
+ exports.registerOfficeViewer = registerOfficeViewer;
2023
2100
  exports.registerTheme = registerTheme;
2024
2101
  exports.renderMarkdown = renderMarkdown;
2025
2102
  exports.resolveFileKind = resolveFileKind;