@particle-academy/fancy-code 0.5.1 → 0.6.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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
- import { RefObject, ReactNode } from 'react';
3
- import { InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
2
+ import { RefObject, ReactNode, CSSProperties } from 'react';
3
+ import { MediaKind, MediaViewerProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
4
4
 
5
5
  interface ThemeColors {
6
6
  background: string;
@@ -225,6 +225,106 @@ declare namespace MarkdownEditor {
225
225
  var displayName: string;
226
226
  }
227
227
 
228
+ /**
229
+ * The decision a file viewer makes about a file: render it as editable text, or
230
+ * hand it to a media viewer. `media` carries the resolved {@link MediaKind};
231
+ * `text` carries the editor language id.
232
+ */
233
+ type FileKind = {
234
+ kind: "text";
235
+ language: string;
236
+ } | {
237
+ kind: "media";
238
+ mediaKind: MediaKind;
239
+ };
240
+ interface ResolveFileKindInput {
241
+ /** File name or path — sniffed for an extension (and used for the media check). */
242
+ filename?: string;
243
+ /** MIME type — preferred over the filename when known. */
244
+ mime?: string;
245
+ }
246
+ /**
247
+ * Resolve the fancy-code editor language for a filename. Returns `"plaintext"`
248
+ * (no syntax highlighting) for unknown or extension-less files.
249
+ */
250
+ declare function languageFromFilename(filename?: string): string;
251
+ /**
252
+ * Decide whether a file is previewable media (image / video / audio / PDF) or
253
+ * text, from its MIME type (preferred) and/or filename. Media wins when the
254
+ * type resolves to one; otherwise it's text and the editor language is derived
255
+ * from the extension.
256
+ *
257
+ * Exposed so consumers can branch their own chrome (tabs, save buttons) the
258
+ * same way `<FileViewer>` does internally.
259
+ *
260
+ * ```ts
261
+ * resolveFileKind({ filename: "logo.png" }); // { kind: "media", mediaKind: "image" }
262
+ * resolveFileKind({ filename: "app.tsx" }); // { kind: "text", language: "typescript" }
263
+ * resolveFileKind({ mime: "application/pdf" }); // { kind: "media", mediaKind: "pdf" }
264
+ * ```
265
+ */
266
+ declare function resolveFileKind({ filename, mime }: ResolveFileKindInput): FileKind;
267
+
268
+ interface FileViewerProps {
269
+ /**
270
+ * File name or path — used to detect the file kind / editor language, and as
271
+ * a label / alt text. Pass `mime` too when you have it (it's preferred).
272
+ */
273
+ filename?: string;
274
+ /** MIME type — preferred over `filename` for detection. */
275
+ mime?: string;
276
+ /**
277
+ * Source URL for **media** files (image / video / audio / PDF) —
278
+ * `http(s):` / `data:` / `blob:`. Required when the file resolves to media.
279
+ */
280
+ src?: string;
281
+ /** Text content for **text** files. */
282
+ value?: string;
283
+ /** Called when text content changes (ignored when `readOnly`). */
284
+ onChange?: (value: string) => void;
285
+ /**
286
+ * Read-only. Defaults to `true` — `FileViewer` is a viewer. Media is always
287
+ * read-only regardless. Set `false` (with `onChange`) to edit text.
288
+ */
289
+ readOnly?: boolean;
290
+ /** Editor theme for text: `"light" | "dark" | "auto"` or a registered name (default `"auto"`). */
291
+ theme?: string;
292
+ /** Force the file kind, bypassing detection. */
293
+ fileKind?: FileKind;
294
+ /** Show line numbers in the text editor (default `true`). */
295
+ lineNumbers?: boolean;
296
+ /** Wrap long lines in the text editor (default `false`). */
297
+ wordWrap?: boolean;
298
+ /** Editor height bounds (px). */
299
+ minHeight?: number;
300
+ maxHeight?: number;
301
+ /** Extra props forwarded to `<MediaViewer>` for media files. */
302
+ mediaProps?: Partial<Omit<MediaViewerProps, "src" | "mime" | "alt">>;
303
+ className?: string;
304
+ style?: CSSProperties;
305
+ }
306
+
307
+ /**
308
+ * A unified file viewer: renders text files in a read-only `CodeEditor` (with
309
+ * syntax highlighting picked from the filename) and media files — image, video,
310
+ * audio, PDF — via react-fancy's `<MediaViewer>`. The text-vs-media decision is
311
+ * made by {@link resolveFileKind}; pass `fileKind` to override it, or call
312
+ * `resolveFileKind` yourself to branch your own chrome (tabs, save buttons).
313
+ *
314
+ * Give the container a height for media files (PDF/image/video fill their box).
315
+ *
316
+ * ```tsx
317
+ * // text
318
+ * <FileViewer filename="app.tsx" value={source} />
319
+ * // media
320
+ * <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
321
+ * ```
322
+ */
323
+ declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
324
+ declare namespace FileViewer {
325
+ var displayName: string;
326
+ }
327
+
228
328
  /**
229
329
  * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
230
330
  * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
@@ -309,4 +409,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
309
409
  */
310
410
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
311
411
 
312
- export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
412
+ export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
- import { RefObject, ReactNode } from 'react';
3
- import { InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
2
+ import { RefObject, ReactNode, CSSProperties } from 'react';
3
+ import { MediaKind, MediaViewerProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
4
4
 
5
5
  interface ThemeColors {
6
6
  background: string;
@@ -225,6 +225,106 @@ declare namespace MarkdownEditor {
225
225
  var displayName: string;
226
226
  }
227
227
 
228
+ /**
229
+ * The decision a file viewer makes about a file: render it as editable text, or
230
+ * hand it to a media viewer. `media` carries the resolved {@link MediaKind};
231
+ * `text` carries the editor language id.
232
+ */
233
+ type FileKind = {
234
+ kind: "text";
235
+ language: string;
236
+ } | {
237
+ kind: "media";
238
+ mediaKind: MediaKind;
239
+ };
240
+ interface ResolveFileKindInput {
241
+ /** File name or path — sniffed for an extension (and used for the media check). */
242
+ filename?: string;
243
+ /** MIME type — preferred over the filename when known. */
244
+ mime?: string;
245
+ }
246
+ /**
247
+ * Resolve the fancy-code editor language for a filename. Returns `"plaintext"`
248
+ * (no syntax highlighting) for unknown or extension-less files.
249
+ */
250
+ declare function languageFromFilename(filename?: string): string;
251
+ /**
252
+ * Decide whether a file is previewable media (image / video / audio / PDF) or
253
+ * text, from its MIME type (preferred) and/or filename. Media wins when the
254
+ * type resolves to one; otherwise it's text and the editor language is derived
255
+ * from the extension.
256
+ *
257
+ * Exposed so consumers can branch their own chrome (tabs, save buttons) the
258
+ * same way `<FileViewer>` does internally.
259
+ *
260
+ * ```ts
261
+ * resolveFileKind({ filename: "logo.png" }); // { kind: "media", mediaKind: "image" }
262
+ * resolveFileKind({ filename: "app.tsx" }); // { kind: "text", language: "typescript" }
263
+ * resolveFileKind({ mime: "application/pdf" }); // { kind: "media", mediaKind: "pdf" }
264
+ * ```
265
+ */
266
+ declare function resolveFileKind({ filename, mime }: ResolveFileKindInput): FileKind;
267
+
268
+ interface FileViewerProps {
269
+ /**
270
+ * File name or path — used to detect the file kind / editor language, and as
271
+ * a label / alt text. Pass `mime` too when you have it (it's preferred).
272
+ */
273
+ filename?: string;
274
+ /** MIME type — preferred over `filename` for detection. */
275
+ mime?: string;
276
+ /**
277
+ * Source URL for **media** files (image / video / audio / PDF) —
278
+ * `http(s):` / `data:` / `blob:`. Required when the file resolves to media.
279
+ */
280
+ src?: string;
281
+ /** Text content for **text** files. */
282
+ value?: string;
283
+ /** Called when text content changes (ignored when `readOnly`). */
284
+ onChange?: (value: string) => void;
285
+ /**
286
+ * Read-only. Defaults to `true` — `FileViewer` is a viewer. Media is always
287
+ * read-only regardless. Set `false` (with `onChange`) to edit text.
288
+ */
289
+ readOnly?: boolean;
290
+ /** Editor theme for text: `"light" | "dark" | "auto"` or a registered name (default `"auto"`). */
291
+ theme?: string;
292
+ /** Force the file kind, bypassing detection. */
293
+ fileKind?: FileKind;
294
+ /** Show line numbers in the text editor (default `true`). */
295
+ lineNumbers?: boolean;
296
+ /** Wrap long lines in the text editor (default `false`). */
297
+ wordWrap?: boolean;
298
+ /** Editor height bounds (px). */
299
+ minHeight?: number;
300
+ maxHeight?: number;
301
+ /** Extra props forwarded to `<MediaViewer>` for media files. */
302
+ mediaProps?: Partial<Omit<MediaViewerProps, "src" | "mime" | "alt">>;
303
+ className?: string;
304
+ style?: CSSProperties;
305
+ }
306
+
307
+ /**
308
+ * A unified file viewer: renders text files in a read-only `CodeEditor` (with
309
+ * syntax highlighting picked from the filename) and media files — image, video,
310
+ * audio, PDF — via react-fancy's `<MediaViewer>`. The text-vs-media decision is
311
+ * made by {@link resolveFileKind}; pass `fileKind` to override it, or call
312
+ * `resolveFileKind` yourself to branch your own chrome (tabs, save buttons).
313
+ *
314
+ * Give the container a height for media files (PDF/image/video fill their box).
315
+ *
316
+ * ```tsx
317
+ * // text
318
+ * <FileViewer filename="app.tsx" value={source} />
319
+ * // media
320
+ * <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
321
+ * ```
322
+ */
323
+ declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
324
+ declare namespace FileViewer {
325
+ var displayName: string;
326
+ }
327
+
228
328
  /**
229
329
  * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
230
330
  * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
@@ -309,4 +409,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
309
409
  */
310
410
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
311
411
 
312
- export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
412
+ export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, useState, useCallback, useRef, useMemo, useSyncExternalStore, useEffect, useContext } from 'react';
2
- import { cn, useControllableState, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
2
+ import { cn, useControllableState, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
5
5
  // src/components/CodeEditor/CodeEditor.tsx
@@ -1831,6 +1831,118 @@ function MarkdownEditor({
1831
1831
  );
1832
1832
  }
1833
1833
  MarkdownEditor.displayName = "MarkdownEditor";
1834
+ var EXT_LANGUAGE = {
1835
+ js: "javascript",
1836
+ mjs: "javascript",
1837
+ cjs: "javascript",
1838
+ jsx: "javascript",
1839
+ json: "javascript",
1840
+ // the JS tokenizer highlights JSON acceptably
1841
+ jsonc: "javascript",
1842
+ ts: "typescript",
1843
+ mts: "typescript",
1844
+ cts: "typescript",
1845
+ tsx: "typescript",
1846
+ html: "html",
1847
+ htm: "html",
1848
+ xml: "html",
1849
+ xhtml: "html",
1850
+ vue: "html",
1851
+ php: "php",
1852
+ phtml: "php",
1853
+ py: "python",
1854
+ pyw: "python",
1855
+ go: "go",
1856
+ md: "markdown",
1857
+ markdown: "markdown",
1858
+ mkd: "markdown",
1859
+ mdx: "markdown"
1860
+ };
1861
+ function basename(filename) {
1862
+ return filename.split(/[?#]/, 1)[0].split(/[\\/]/).pop() ?? "";
1863
+ }
1864
+ function languageFromFilename(filename) {
1865
+ if (!filename) return "plaintext";
1866
+ const seg = basename(filename);
1867
+ const dot = seg.lastIndexOf(".");
1868
+ if (dot <= 0) return "plaintext";
1869
+ const ext = seg.slice(dot + 1).toLowerCase();
1870
+ return EXT_LANGUAGE[ext] ?? "plaintext";
1871
+ }
1872
+ function resolveFileKind({ filename, mime }) {
1873
+ const media = resolveMediaType({ mime, src: filename });
1874
+ if (media !== "unknown") return { kind: "media", mediaKind: media };
1875
+ return { kind: "text", language: languageFromFilename(filename) };
1876
+ }
1877
+ function FileViewer({
1878
+ filename,
1879
+ mime,
1880
+ src,
1881
+ value = "",
1882
+ onChange,
1883
+ readOnly = true,
1884
+ theme = "auto",
1885
+ fileKind,
1886
+ lineNumbers = true,
1887
+ wordWrap = false,
1888
+ minHeight,
1889
+ maxHeight,
1890
+ mediaProps,
1891
+ className,
1892
+ style
1893
+ }) {
1894
+ const resolved = fileKind ?? resolveFileKind({ filename, mime });
1895
+ if (resolved.kind === "media") {
1896
+ const { className: mediaClassName, ...restMediaProps } = mediaProps ?? {};
1897
+ const fill = resolved.mediaKind !== "audio";
1898
+ return /* @__PURE__ */ jsx(
1899
+ "div",
1900
+ {
1901
+ "data-fancy-file-viewer": "",
1902
+ "data-kind": "media",
1903
+ "data-media-kind": resolved.mediaKind,
1904
+ className: cn("overflow-hidden", className),
1905
+ style,
1906
+ children: /* @__PURE__ */ jsx(
1907
+ MediaViewer,
1908
+ {
1909
+ src: src ?? "",
1910
+ mime,
1911
+ alt: filename,
1912
+ kind: resolved.mediaKind,
1913
+ className: cn(fill && "h-full w-full", mediaClassName),
1914
+ ...restMediaProps
1915
+ }
1916
+ )
1917
+ }
1918
+ );
1919
+ }
1920
+ return /* @__PURE__ */ jsx(
1921
+ "div",
1922
+ {
1923
+ "data-fancy-file-viewer": "",
1924
+ "data-kind": "text",
1925
+ className,
1926
+ style,
1927
+ children: /* @__PURE__ */ jsx(
1928
+ CodeEditor,
1929
+ {
1930
+ value,
1931
+ onChange: readOnly ? void 0 : onChange,
1932
+ language: resolved.language,
1933
+ theme,
1934
+ readOnly,
1935
+ lineNumbers,
1936
+ wordWrap,
1937
+ minHeight,
1938
+ maxHeight,
1939
+ children: /* @__PURE__ */ jsx(CodeEditor.Panel, {})
1940
+ }
1941
+ )
1942
+ }
1943
+ );
1944
+ }
1945
+ FileViewer.displayName = "FileViewer";
1834
1946
  function useCodeEditorInputTagAdapter() {
1835
1947
  const ctx = useCodeEditor();
1836
1948
  return useMemo(() => textareaAdapter(ctx.textareaRef), [ctx.textareaRef]);
@@ -1840,6 +1952,6 @@ function CodeEditorInputTag(props) {
1840
1952
  return /* @__PURE__ */ jsx(InputTag, { adapter, ...props });
1841
1953
  }
1842
1954
 
1843
- export { CodeEditor, CodeEditorInputTag, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
1955
+ export { CodeEditor, CodeEditorInputTag, FileViewer, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
1844
1956
  //# sourceMappingURL=index.js.map
1845
1957
  //# sourceMappingURL=index.js.map