@particle-academy/fancy-code 0.12.1 → 0.14.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,49 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.14.0] - 2026-10-19
18
+
19
+ ### Changed
20
+
21
+ - **Images and PDFs in `FileViewer` sit in the `FauxClient` frame**, like
22
+ documents: an image gets the frame's zoom bar (driving its own pan/zoom) and
23
+ full screen; a PDF gets full screen, with zoom left to the browser's PDF
24
+ toolbar, and its file name in the address bar. Revealed on hover.
25
+ **What to do:** nothing to keep the new look. To turn it off, pass
26
+ `mediaProps={{ imageProps: { frame: false } }}` (or `pdfProps`).
27
+ - **Requires react-fancy 5.49.0 or later** (peer `>=5.49.0 <6.0.0`).
28
+
29
+ ## [0.13.0] - 2026-10-19
30
+
31
+ ### Added
32
+
33
+ - **Documents render as a reader sees them.** `FileViewer` shows Markdown, HTML
34
+ and plain text through react-fancy's `ContentRenderer`, sanitised (a file is
35
+ untrusted), in the `FauxClient` frame every file viewer now carries: zoom and
36
+ full screen, revealed on hover. Plain text keeps its paragraphs and line
37
+ breaks and is escaped, so `*x*` stays asterisks. `view="source"` shows the
38
+ source in the code editor instead, as do editing (`readOnly={false}`) and
39
+ landing on a `line`. `frameProps` reaches the frame.
40
+ - **Office files are recognised.** `.docx`, `.xlsx` and `.pptx` (by extension
41
+ or MIME type) resolve to `{ kind: "office", officeKind }`. They show
42
+ MediaViewer's no-preview card with a download until their viewers are wired
43
+ in. They used to be handed to the code editor as text.
44
+
45
+ ### Changed
46
+
47
+ - **BREAKING:** a `.md`, `.html` or `.txt` file in `FileViewer` is rendered,
48
+ not shown as source. **What to do:** to keep the source, pass
49
+ `view="source"`. A viewer that lands on a `line` (a link into a file) already
50
+ shows the source, so file links need nothing.
51
+ - **BREAKING (types):** `FileKind` gains `{ kind: "document"; format; language }`
52
+ and `{ kind: "office"; officeKind }`. **What to do:** only if you `switch` on
53
+ `resolveFileKind(...).kind` exhaustively: add the two cases, or a `default`.
54
+ `notes.txt` now resolves to `document`, not `text`.
55
+ - The extension decides over a generic MIME type: a host that serves `app.ts`
56
+ as `text/plain` still gets the code editor.
57
+ - **Requires react-fancy 5.48.1 or later** (peer `>=5.48.1 <6.0.0`, was
58
+ `>=4.9.0`), for the frame and for the sanitiser that strips `<title>`.
59
+
17
60
  ## [0.12.1] - 2026-10-18
18
61
 
19
62
  ### Security
package/dist/index.cjs CHANGED
@@ -1721,10 +1721,45 @@ function MarkdownEditor({
1721
1721
  );
1722
1722
  }
1723
1723
  MarkdownEditor.displayName = "MarkdownEditor";
1724
+ var OFFICE_EXT = { docx: "docx", xlsx: "xlsx", xlsm: "xlsx", pptx: "pptx" };
1725
+ var OFFICE_MIME = {
1726
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
1727
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
1728
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx"
1729
+ };
1730
+ var DOCUMENT_EXT = {
1731
+ md: "markdown",
1732
+ markdown: "markdown",
1733
+ html: "html",
1734
+ htm: "html",
1735
+ txt: "text",
1736
+ text: "text"
1737
+ };
1738
+ var DOCUMENT_MIME = {
1739
+ "text/markdown": "markdown",
1740
+ "text/x-markdown": "markdown",
1741
+ "text/html": "html",
1742
+ "text/plain": "text"
1743
+ };
1744
+ function extensionOf(filename) {
1745
+ const name = (filename ?? "").split(/[?#]/)[0].split("/").pop() ?? "";
1746
+ const dot = name.lastIndexOf(".");
1747
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
1748
+ }
1724
1749
  function resolveFileKind({ filename, mime }) {
1725
1750
  const media = reactFancy.resolveMediaType({ mime, src: filename });
1726
1751
  if (media !== "unknown") return { kind: "media", mediaKind: media };
1727
- return { kind: "text", language: fancyFileCommons.languageFromFilename(filename) };
1752
+ const type = (mime ?? "").split(";")[0].trim().toLowerCase();
1753
+ const ext = extensionOf(filename);
1754
+ const office = OFFICE_MIME[type] ?? OFFICE_EXT[ext];
1755
+ if (office) return { kind: "office", officeKind: office };
1756
+ const language = fancyFileCommons.languageFromFilename(filename);
1757
+ const format = DOCUMENT_EXT[ext] ?? (language === "plaintext" ? DOCUMENT_MIME[type] : void 0);
1758
+ if (format) {
1759
+ const lang = language !== "plaintext" ? language : format === "markdown" ? "markdown" : format === "html" ? "html" : "plaintext";
1760
+ return { kind: "document", format, language: lang };
1761
+ }
1762
+ return { kind: "text", language };
1728
1763
  }
1729
1764
  function FileViewer({
1730
1765
  filename,
@@ -1743,12 +1778,48 @@ function FileViewer({
1743
1778
  minHeight,
1744
1779
  maxHeight,
1745
1780
  mediaProps,
1781
+ view = "rendered",
1782
+ frameProps,
1746
1783
  className,
1747
1784
  style
1748
1785
  }) {
1749
1786
  const resolved = fileKind ?? resolveFileKind({ filename, mime });
1787
+ if (resolved.kind === "document" && view === "rendered" && readOnly && line == null) {
1788
+ return /* @__PURE__ */ jsxRuntime.jsx(
1789
+ "div",
1790
+ {
1791
+ "data-fancy-file-viewer": "",
1792
+ "data-kind": "document",
1793
+ "data-format": resolved.format,
1794
+ className,
1795
+ style,
1796
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.FauxClient, { variant: "device", controls: true, fill: true, bodyClassName: "overflow-auto", ...frameProps, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-5", children: /* @__PURE__ */ jsxRuntime.jsx(
1797
+ reactFancy.ContentRenderer,
1798
+ {
1799
+ value: resolved.format === "text" ? textToHtml(value) : value,
1800
+ format: resolved.format === "markdown" ? "markdown" : "html"
1801
+ }
1802
+ ) }) })
1803
+ }
1804
+ );
1805
+ }
1806
+ if (resolved.kind === "office") {
1807
+ return /* @__PURE__ */ jsxRuntime.jsx(
1808
+ "div",
1809
+ {
1810
+ "data-fancy-file-viewer": "",
1811
+ "data-kind": "office",
1812
+ "data-office-kind": resolved.officeKind,
1813
+ className: reactFancy.cn("overflow-hidden", className),
1814
+ style,
1815
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.MediaViewer, { src: src ?? "", mime, alt: filename, className: "h-full w-full" })
1816
+ }
1817
+ );
1818
+ }
1750
1819
  if (resolved.kind === "media") {
1751
1820
  const { className: mediaClassName, ...restMediaProps } = mediaProps ?? {};
1821
+ restMediaProps.imageProps = { frame: true, frameProps, ...restMediaProps.imageProps };
1822
+ restMediaProps.pdfProps = { frame: true, frameProps, ...restMediaProps.pdfProps };
1752
1823
  const fill = resolved.mediaKind !== "audio";
1753
1824
  return /* @__PURE__ */ jsxRuntime.jsx(
1754
1825
  "div",
@@ -1801,6 +1872,10 @@ function FileViewer({
1801
1872
  );
1802
1873
  }
1803
1874
  FileViewer.displayName = "FileViewer";
1875
+ var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
1876
+ function textToHtml(text) {
1877
+ 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("");
1878
+ }
1804
1879
 
1805
1880
  // src/engine/markdown-render.ts
1806
1881
  function escapeHtml(s) {