@particle-academy/fancy-code 0.5.1 → 0.7.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;
@@ -43,6 +43,13 @@ interface UseEditorEngineReturn {
43
43
  handleInput: () => void;
44
44
  handleScroll: () => void;
45
45
  handleSelect: () => void;
46
+ /**
47
+ * Place the caret on a 1-based `line` / `column` and scroll the editor so the
48
+ * line is centered in view. Out-of-range positions clamp to document bounds.
49
+ */
50
+ revealLine: (line: number, column?: number, opts?: {
51
+ focus?: boolean;
52
+ }) => void;
46
53
  scrollTop: number;
47
54
  scrollLeft: number;
48
55
  }
@@ -76,6 +83,16 @@ interface CodeEditorProps {
76
83
  minHeight?: number;
77
84
  /** Maximum height in px (scrolls beyond this) */
78
85
  maxHeight?: number;
86
+ /**
87
+ * 1-based line to reveal (scroll to + place the caret on). Applied on mount
88
+ * and whenever it (or `cursorColumn`) changes — so opening a file *at* a line,
89
+ * or re-targeting an already-open file, scrolls the line into view. Does not
90
+ * steal focus; call the imperative `revealLine`/`setCursor` (or `focus`) for
91
+ * that. Out-of-range values clamp to document bounds.
92
+ */
93
+ cursorLine?: number;
94
+ /** 1-based column for {@link cursorLine} (default 1). */
95
+ cursorColumn?: number;
79
96
  }
80
97
  interface CodeEditorContextValue {
81
98
  /** Get the current document text */
@@ -88,6 +105,16 @@ interface CodeEditorContextValue {
88
105
  replaceSelection: (text: string) => void;
89
106
  /** Focus the editor */
90
107
  focus: () => void;
108
+ /**
109
+ * Scroll to + place the caret on a 1-based `line` (optional 1-based `column`),
110
+ * focusing the editor. Out-of-range positions clamp to document bounds.
111
+ */
112
+ revealLine: (line: number, column?: number) => void;
113
+ /** Place the caret at a 1-based `{ line, column }` and reveal it (focuses). */
114
+ setCursor: (pos: {
115
+ line: number;
116
+ column?: number;
117
+ }) => void;
91
118
  /** Current language name */
92
119
  language: string;
93
120
  /** Change the active language */
@@ -163,7 +190,7 @@ declare namespace CodeEditorStatusBar {
163
190
  var displayName: string;
164
191
  }
165
192
 
166
- declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, }: CodeEditorProps): react.JSX.Element;
193
+ declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, cursorLine, cursorColumn, }: CodeEditorProps): react.JSX.Element;
167
194
  declare namespace CodeEditorRoot {
168
195
  var displayName: string;
169
196
  }
@@ -225,6 +252,106 @@ declare namespace MarkdownEditor {
225
252
  var displayName: string;
226
253
  }
227
254
 
255
+ /**
256
+ * The decision a file viewer makes about a file: render it as editable text, or
257
+ * hand it to a media viewer. `media` carries the resolved {@link MediaKind};
258
+ * `text` carries the editor language id.
259
+ */
260
+ type FileKind = {
261
+ kind: "text";
262
+ language: string;
263
+ } | {
264
+ kind: "media";
265
+ mediaKind: MediaKind;
266
+ };
267
+ interface ResolveFileKindInput {
268
+ /** File name or path — sniffed for an extension (and used for the media check). */
269
+ filename?: string;
270
+ /** MIME type — preferred over the filename when known. */
271
+ mime?: string;
272
+ }
273
+ /**
274
+ * Resolve the fancy-code editor language for a filename. Returns `"plaintext"`
275
+ * (no syntax highlighting) for unknown or extension-less files.
276
+ */
277
+ declare function languageFromFilename(filename?: string): string;
278
+ /**
279
+ * Decide whether a file is previewable media (image / video / audio / PDF) or
280
+ * text, from its MIME type (preferred) and/or filename. Media wins when the
281
+ * type resolves to one; otherwise it's text and the editor language is derived
282
+ * from the extension.
283
+ *
284
+ * Exposed so consumers can branch their own chrome (tabs, save buttons) the
285
+ * same way `<FileViewer>` does internally.
286
+ *
287
+ * ```ts
288
+ * resolveFileKind({ filename: "logo.png" }); // { kind: "media", mediaKind: "image" }
289
+ * resolveFileKind({ filename: "app.tsx" }); // { kind: "text", language: "typescript" }
290
+ * resolveFileKind({ mime: "application/pdf" }); // { kind: "media", mediaKind: "pdf" }
291
+ * ```
292
+ */
293
+ declare function resolveFileKind({ filename, mime }: ResolveFileKindInput): FileKind;
294
+
295
+ interface FileViewerProps {
296
+ /**
297
+ * File name or path — used to detect the file kind / editor language, and as
298
+ * a label / alt text. Pass `mime` too when you have it (it's preferred).
299
+ */
300
+ filename?: string;
301
+ /** MIME type — preferred over `filename` for detection. */
302
+ mime?: string;
303
+ /**
304
+ * Source URL for **media** files (image / video / audio / PDF) —
305
+ * `http(s):` / `data:` / `blob:`. Required when the file resolves to media.
306
+ */
307
+ src?: string;
308
+ /** Text content for **text** files. */
309
+ value?: string;
310
+ /** Called when text content changes (ignored when `readOnly`). */
311
+ onChange?: (value: string) => void;
312
+ /**
313
+ * Read-only. Defaults to `true` — `FileViewer` is a viewer. Media is always
314
+ * read-only regardless. Set `false` (with `onChange`) to edit text.
315
+ */
316
+ readOnly?: boolean;
317
+ /** Editor theme for text: `"light" | "dark" | "auto"` or a registered name (default `"auto"`). */
318
+ theme?: string;
319
+ /** Force the file kind, bypassing detection. */
320
+ fileKind?: FileKind;
321
+ /** Show line numbers in the text editor (default `true`). */
322
+ lineNumbers?: boolean;
323
+ /** Wrap long lines in the text editor (default `false`). */
324
+ wordWrap?: boolean;
325
+ /** Editor height bounds (px). */
326
+ minHeight?: number;
327
+ maxHeight?: number;
328
+ /** Extra props forwarded to `<MediaViewer>` for media files. */
329
+ mediaProps?: Partial<Omit<MediaViewerProps, "src" | "mime" | "alt">>;
330
+ className?: string;
331
+ style?: CSSProperties;
332
+ }
333
+
334
+ /**
335
+ * A unified file viewer: renders text files in a read-only `CodeEditor` (with
336
+ * syntax highlighting picked from the filename) and media files — image, video,
337
+ * audio, PDF — via react-fancy's `<MediaViewer>`. The text-vs-media decision is
338
+ * made by {@link resolveFileKind}; pass `fileKind` to override it, or call
339
+ * `resolveFileKind` yourself to branch your own chrome (tabs, save buttons).
340
+ *
341
+ * Give the container a height for media files (PDF/image/video fill their box).
342
+ *
343
+ * ```tsx
344
+ * // text
345
+ * <FileViewer filename="app.tsx" value={source} />
346
+ * // media
347
+ * <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
348
+ * ```
349
+ */
350
+ declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
351
+ declare namespace FileViewer {
352
+ var displayName: string;
353
+ }
354
+
228
355
  /**
229
356
  * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
230
357
  * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
@@ -309,4 +436,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
309
436
  */
310
437
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
311
438
 
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 };
439
+ 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;
@@ -43,6 +43,13 @@ interface UseEditorEngineReturn {
43
43
  handleInput: () => void;
44
44
  handleScroll: () => void;
45
45
  handleSelect: () => void;
46
+ /**
47
+ * Place the caret on a 1-based `line` / `column` and scroll the editor so the
48
+ * line is centered in view. Out-of-range positions clamp to document bounds.
49
+ */
50
+ revealLine: (line: number, column?: number, opts?: {
51
+ focus?: boolean;
52
+ }) => void;
46
53
  scrollTop: number;
47
54
  scrollLeft: number;
48
55
  }
@@ -76,6 +83,16 @@ interface CodeEditorProps {
76
83
  minHeight?: number;
77
84
  /** Maximum height in px (scrolls beyond this) */
78
85
  maxHeight?: number;
86
+ /**
87
+ * 1-based line to reveal (scroll to + place the caret on). Applied on mount
88
+ * and whenever it (or `cursorColumn`) changes — so opening a file *at* a line,
89
+ * or re-targeting an already-open file, scrolls the line into view. Does not
90
+ * steal focus; call the imperative `revealLine`/`setCursor` (or `focus`) for
91
+ * that. Out-of-range values clamp to document bounds.
92
+ */
93
+ cursorLine?: number;
94
+ /** 1-based column for {@link cursorLine} (default 1). */
95
+ cursorColumn?: number;
79
96
  }
80
97
  interface CodeEditorContextValue {
81
98
  /** Get the current document text */
@@ -88,6 +105,16 @@ interface CodeEditorContextValue {
88
105
  replaceSelection: (text: string) => void;
89
106
  /** Focus the editor */
90
107
  focus: () => void;
108
+ /**
109
+ * Scroll to + place the caret on a 1-based `line` (optional 1-based `column`),
110
+ * focusing the editor. Out-of-range positions clamp to document bounds.
111
+ */
112
+ revealLine: (line: number, column?: number) => void;
113
+ /** Place the caret at a 1-based `{ line, column }` and reveal it (focuses). */
114
+ setCursor: (pos: {
115
+ line: number;
116
+ column?: number;
117
+ }) => void;
91
118
  /** Current language name */
92
119
  language: string;
93
120
  /** Change the active language */
@@ -163,7 +190,7 @@ declare namespace CodeEditorStatusBar {
163
190
  var displayName: string;
164
191
  }
165
192
 
166
- declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, }: CodeEditorProps): react.JSX.Element;
193
+ declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, cursorLine, cursorColumn, }: CodeEditorProps): react.JSX.Element;
167
194
  declare namespace CodeEditorRoot {
168
195
  var displayName: string;
169
196
  }
@@ -225,6 +252,106 @@ declare namespace MarkdownEditor {
225
252
  var displayName: string;
226
253
  }
227
254
 
255
+ /**
256
+ * The decision a file viewer makes about a file: render it as editable text, or
257
+ * hand it to a media viewer. `media` carries the resolved {@link MediaKind};
258
+ * `text` carries the editor language id.
259
+ */
260
+ type FileKind = {
261
+ kind: "text";
262
+ language: string;
263
+ } | {
264
+ kind: "media";
265
+ mediaKind: MediaKind;
266
+ };
267
+ interface ResolveFileKindInput {
268
+ /** File name or path — sniffed for an extension (and used for the media check). */
269
+ filename?: string;
270
+ /** MIME type — preferred over the filename when known. */
271
+ mime?: string;
272
+ }
273
+ /**
274
+ * Resolve the fancy-code editor language for a filename. Returns `"plaintext"`
275
+ * (no syntax highlighting) for unknown or extension-less files.
276
+ */
277
+ declare function languageFromFilename(filename?: string): string;
278
+ /**
279
+ * Decide whether a file is previewable media (image / video / audio / PDF) or
280
+ * text, from its MIME type (preferred) and/or filename. Media wins when the
281
+ * type resolves to one; otherwise it's text and the editor language is derived
282
+ * from the extension.
283
+ *
284
+ * Exposed so consumers can branch their own chrome (tabs, save buttons) the
285
+ * same way `<FileViewer>` does internally.
286
+ *
287
+ * ```ts
288
+ * resolveFileKind({ filename: "logo.png" }); // { kind: "media", mediaKind: "image" }
289
+ * resolveFileKind({ filename: "app.tsx" }); // { kind: "text", language: "typescript" }
290
+ * resolveFileKind({ mime: "application/pdf" }); // { kind: "media", mediaKind: "pdf" }
291
+ * ```
292
+ */
293
+ declare function resolveFileKind({ filename, mime }: ResolveFileKindInput): FileKind;
294
+
295
+ interface FileViewerProps {
296
+ /**
297
+ * File name or path — used to detect the file kind / editor language, and as
298
+ * a label / alt text. Pass `mime` too when you have it (it's preferred).
299
+ */
300
+ filename?: string;
301
+ /** MIME type — preferred over `filename` for detection. */
302
+ mime?: string;
303
+ /**
304
+ * Source URL for **media** files (image / video / audio / PDF) —
305
+ * `http(s):` / `data:` / `blob:`. Required when the file resolves to media.
306
+ */
307
+ src?: string;
308
+ /** Text content for **text** files. */
309
+ value?: string;
310
+ /** Called when text content changes (ignored when `readOnly`). */
311
+ onChange?: (value: string) => void;
312
+ /**
313
+ * Read-only. Defaults to `true` — `FileViewer` is a viewer. Media is always
314
+ * read-only regardless. Set `false` (with `onChange`) to edit text.
315
+ */
316
+ readOnly?: boolean;
317
+ /** Editor theme for text: `"light" | "dark" | "auto"` or a registered name (default `"auto"`). */
318
+ theme?: string;
319
+ /** Force the file kind, bypassing detection. */
320
+ fileKind?: FileKind;
321
+ /** Show line numbers in the text editor (default `true`). */
322
+ lineNumbers?: boolean;
323
+ /** Wrap long lines in the text editor (default `false`). */
324
+ wordWrap?: boolean;
325
+ /** Editor height bounds (px). */
326
+ minHeight?: number;
327
+ maxHeight?: number;
328
+ /** Extra props forwarded to `<MediaViewer>` for media files. */
329
+ mediaProps?: Partial<Omit<MediaViewerProps, "src" | "mime" | "alt">>;
330
+ className?: string;
331
+ style?: CSSProperties;
332
+ }
333
+
334
+ /**
335
+ * A unified file viewer: renders text files in a read-only `CodeEditor` (with
336
+ * syntax highlighting picked from the filename) and media files — image, video,
337
+ * audio, PDF — via react-fancy's `<MediaViewer>`. The text-vs-media decision is
338
+ * made by {@link resolveFileKind}; pass `fileKind` to override it, or call
339
+ * `resolveFileKind` yourself to branch your own chrome (tabs, save buttons).
340
+ *
341
+ * Give the container a height for media files (PDF/image/video fill their box).
342
+ *
343
+ * ```tsx
344
+ * // text
345
+ * <FileViewer filename="app.tsx" value={source} />
346
+ * // media
347
+ * <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
348
+ * ```
349
+ */
350
+ declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
351
+ declare namespace FileViewer {
352
+ var displayName: string;
353
+ }
354
+
228
355
  /**
229
356
  * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
230
357
  * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
@@ -309,4 +436,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
309
436
  */
310
437
  declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
311
438
 
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 };
439
+ 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
- import { createContext, useState, useCallback, useRef, useMemo, useSyncExternalStore, useEffect, useContext } from 'react';
2
- import { cn, useControllableState, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
1
+ import { createContext, useState, useCallback, useRef, useEffect, useMemo, useSyncExternalStore, useContext } from 'react';
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
@@ -1344,6 +1344,19 @@ function highlightCode(source, tokens, colors) {
1344
1344
  return parts.join("");
1345
1345
  }
1346
1346
 
1347
+ // src/engine/position.ts
1348
+ function lineColumnToOffset(text, line, column = 1) {
1349
+ const lines = text.split("\n");
1350
+ const targetLine = Math.max(1, Math.min(Math.trunc(line) || 1, lines.length));
1351
+ let offset = 0;
1352
+ for (let i = 0; i < targetLine - 1; i++) {
1353
+ offset += lines[i].length + 1;
1354
+ }
1355
+ const lineText = lines[targetLine - 1] ?? "";
1356
+ const col = Math.max(1, Math.min(Math.trunc(column) || 1, lineText.length + 1));
1357
+ return offset + (col - 1);
1358
+ }
1359
+
1347
1360
  // src/hooks/use-editor-engine.ts
1348
1361
  var DEFAULT_COLORS = {
1349
1362
  background: "#18181b",
@@ -1458,6 +1471,28 @@ function useEditorEngine({
1458
1471
  setScrollTop(ta.scrollTop);
1459
1472
  setScrollLeft(ta.scrollLeft);
1460
1473
  }, []);
1474
+ const revealLine = useCallback(
1475
+ (line, column = 1, opts) => {
1476
+ const ta = textareaRef.current;
1477
+ if (!ta) return;
1478
+ const offset = lineColumnToOffset(ta.value, line, column);
1479
+ if (opts?.focus) ta.focus();
1480
+ ta.selectionStart = ta.selectionEnd = offset;
1481
+ updateCursorInfo();
1482
+ const panel = ta.closest("[data-fancy-code-panel]");
1483
+ if (!panel) return;
1484
+ const cs = getComputedStyle(ta);
1485
+ const lineHeight = parseFloat(cs.lineHeight) || 19.5;
1486
+ const paddingTop = parseFloat(cs.paddingTop) || 0;
1487
+ const lineIndex = (ta.value.slice(0, offset).match(/\n/g) || []).length;
1488
+ const textareaTopInPanel = ta.getBoundingClientRect().top - panel.getBoundingClientRect().top + panel.scrollTop;
1489
+ const lineTop = textareaTopInPanel + paddingTop + lineIndex * lineHeight;
1490
+ const target = lineTop - panel.clientHeight / 2 + lineHeight / 2;
1491
+ const maxScroll = panel.scrollHeight - panel.clientHeight;
1492
+ panel.scrollTop = Math.max(0, Math.min(target, maxScroll));
1493
+ },
1494
+ [updateCursorInfo]
1495
+ );
1461
1496
  const handleKeyDown = useCallback(
1462
1497
  (e) => {
1463
1498
  if (readOnly) {
@@ -1544,6 +1579,7 @@ function useEditorEngine({
1544
1579
  handleInput,
1545
1580
  handleScroll,
1546
1581
  handleSelect,
1582
+ revealLine,
1547
1583
  scrollTop,
1548
1584
  scrollLeft
1549
1585
  };
@@ -1577,7 +1613,9 @@ function CodeEditorRoot({
1577
1613
  tabSize: tabSizeProp = 2,
1578
1614
  placeholder,
1579
1615
  minHeight,
1580
- maxHeight
1616
+ maxHeight,
1617
+ cursorLine,
1618
+ cursorColumn
1581
1619
  }) {
1582
1620
  const [currentValue, setCurrentValue] = useControllableState(valueProp, defaultValue, onChange);
1583
1621
  const isDark = useDarkMode();
@@ -1613,6 +1651,11 @@ function CodeEditorRoot({
1613
1651
  setSelectionLength(sel);
1614
1652
  }
1615
1653
  });
1654
+ const { revealLine: revealLineImpl } = engineReturn;
1655
+ useEffect(() => {
1656
+ if (cursorLine == null) return;
1657
+ revealLineImpl(cursorLine, cursorColumn ?? 1, { focus: false });
1658
+ }, [cursorLine, cursorColumn, revealLineImpl]);
1616
1659
  const contextValue = useMemo(
1617
1660
  () => ({
1618
1661
  getValue: () => engineReturn.textareaRef.current?.value ?? currentValue,
@@ -1634,6 +1677,8 @@ function CodeEditorRoot({
1634
1677
  setCurrentValue(ta.value);
1635
1678
  },
1636
1679
  focus: () => engineReturn.textareaRef.current?.focus(),
1680
+ revealLine: (line, column) => engineReturn.revealLine(line, column ?? 1, { focus: true }),
1681
+ setCursor: ({ line, column }) => engineReturn.revealLine(line, column ?? 1, { focus: true }),
1637
1682
  language: currentLanguage,
1638
1683
  setLanguage,
1639
1684
  theme: resolvedTheme,
@@ -1831,6 +1876,118 @@ function MarkdownEditor({
1831
1876
  );
1832
1877
  }
1833
1878
  MarkdownEditor.displayName = "MarkdownEditor";
1879
+ var EXT_LANGUAGE = {
1880
+ js: "javascript",
1881
+ mjs: "javascript",
1882
+ cjs: "javascript",
1883
+ jsx: "javascript",
1884
+ json: "javascript",
1885
+ // the JS tokenizer highlights JSON acceptably
1886
+ jsonc: "javascript",
1887
+ ts: "typescript",
1888
+ mts: "typescript",
1889
+ cts: "typescript",
1890
+ tsx: "typescript",
1891
+ html: "html",
1892
+ htm: "html",
1893
+ xml: "html",
1894
+ xhtml: "html",
1895
+ vue: "html",
1896
+ php: "php",
1897
+ phtml: "php",
1898
+ py: "python",
1899
+ pyw: "python",
1900
+ go: "go",
1901
+ md: "markdown",
1902
+ markdown: "markdown",
1903
+ mkd: "markdown",
1904
+ mdx: "markdown"
1905
+ };
1906
+ function basename(filename) {
1907
+ return filename.split(/[?#]/, 1)[0].split(/[\\/]/).pop() ?? "";
1908
+ }
1909
+ function languageFromFilename(filename) {
1910
+ if (!filename) return "plaintext";
1911
+ const seg = basename(filename);
1912
+ const dot = seg.lastIndexOf(".");
1913
+ if (dot <= 0) return "plaintext";
1914
+ const ext = seg.slice(dot + 1).toLowerCase();
1915
+ return EXT_LANGUAGE[ext] ?? "plaintext";
1916
+ }
1917
+ function resolveFileKind({ filename, mime }) {
1918
+ const media = resolveMediaType({ mime, src: filename });
1919
+ if (media !== "unknown") return { kind: "media", mediaKind: media };
1920
+ return { kind: "text", language: languageFromFilename(filename) };
1921
+ }
1922
+ function FileViewer({
1923
+ filename,
1924
+ mime,
1925
+ src,
1926
+ value = "",
1927
+ onChange,
1928
+ readOnly = true,
1929
+ theme = "auto",
1930
+ fileKind,
1931
+ lineNumbers = true,
1932
+ wordWrap = false,
1933
+ minHeight,
1934
+ maxHeight,
1935
+ mediaProps,
1936
+ className,
1937
+ style
1938
+ }) {
1939
+ const resolved = fileKind ?? resolveFileKind({ filename, mime });
1940
+ if (resolved.kind === "media") {
1941
+ const { className: mediaClassName, ...restMediaProps } = mediaProps ?? {};
1942
+ const fill = resolved.mediaKind !== "audio";
1943
+ return /* @__PURE__ */ jsx(
1944
+ "div",
1945
+ {
1946
+ "data-fancy-file-viewer": "",
1947
+ "data-kind": "media",
1948
+ "data-media-kind": resolved.mediaKind,
1949
+ className: cn("overflow-hidden", className),
1950
+ style,
1951
+ children: /* @__PURE__ */ jsx(
1952
+ MediaViewer,
1953
+ {
1954
+ src: src ?? "",
1955
+ mime,
1956
+ alt: filename,
1957
+ kind: resolved.mediaKind,
1958
+ className: cn(fill && "h-full w-full", mediaClassName),
1959
+ ...restMediaProps
1960
+ }
1961
+ )
1962
+ }
1963
+ );
1964
+ }
1965
+ return /* @__PURE__ */ jsx(
1966
+ "div",
1967
+ {
1968
+ "data-fancy-file-viewer": "",
1969
+ "data-kind": "text",
1970
+ className,
1971
+ style,
1972
+ children: /* @__PURE__ */ jsx(
1973
+ CodeEditor,
1974
+ {
1975
+ value,
1976
+ onChange: readOnly ? void 0 : onChange,
1977
+ language: resolved.language,
1978
+ theme,
1979
+ readOnly,
1980
+ lineNumbers,
1981
+ wordWrap,
1982
+ minHeight,
1983
+ maxHeight,
1984
+ children: /* @__PURE__ */ jsx(CodeEditor.Panel, {})
1985
+ }
1986
+ )
1987
+ }
1988
+ );
1989
+ }
1990
+ FileViewer.displayName = "FileViewer";
1834
1991
  function useCodeEditorInputTagAdapter() {
1835
1992
  const ctx = useCodeEditor();
1836
1993
  return useMemo(() => textareaAdapter(ctx.textareaRef), [ctx.textareaRef]);
@@ -1840,6 +1997,6 @@ function CodeEditorInputTag(props) {
1840
1997
  return /* @__PURE__ */ jsx(InputTag, { adapter, ...props });
1841
1998
  }
1842
1999
 
1843
- export { CodeEditor, CodeEditorInputTag, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
2000
+ export { CodeEditor, CodeEditorInputTag, FileViewer, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
1844
2001
  //# sourceMappingURL=index.js.map
1845
2002
  //# sourceMappingURL=index.js.map