@particle-academy/fancy-code 0.7.0 → 0.8.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/README.md CHANGED
@@ -44,6 +44,26 @@ function App() {
44
44
 
45
45
  The component ships with no default border or rounding — add your own via `className` for standalone use, or embed directly into IDE layouts without visual conflicts.
46
46
 
47
+ ## Diff gutter
48
+
49
+ Pass `diffBase` (the baseline document) and the line-number column shows live,
50
+ VS Code-style change marks of the **current value vs that baseline** — a green
51
+ bar for added lines, a blue bar for modified lines, and a red wedge where lines
52
+ were deleted. Recomputed as the user types (line-level, cheap — the engine is
53
+ [`@particle-academy/fancy-file-commons`](https://github.com/Particle-Academy/fancy-file-commons),
54
+ the same core `fancy-diff` renders).
55
+
56
+ ```tsx
57
+ <CodeEditor value={code} onChange={setCode} diffBase={savedCode} language="typescript">
58
+ <CodeEditor.Panel />
59
+ </CodeEditor>
60
+ ```
61
+
62
+ Totals are exposed on the context as `diffStats` (`{ added, modified, removed }`)
63
+ for a status-bar chip or save indicator, and every marked row carries stable
64
+ `data-diff` / `data-diff-deleted-above` handles. Themes may override the colors
65
+ via `diffAdded` / `diffModified` / `diffRemoved`.
66
+
47
67
  ## Unified file viewer
48
68
 
49
69
  Showing arbitrary files from a tree? `FileViewer` renders text in the
package/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var react = require('react');
4
4
  var reactFancy = require('@particle-academy/react-fancy');
5
+ var fancyFileCommons = require('@particle-academy/fancy-file-commons');
5
6
  var jsxRuntime = require('react/jsx-runtime');
6
7
 
7
8
  // src/components/CodeEditor/CodeEditor.tsx
@@ -13,6 +14,24 @@ function useCodeEditor() {
13
14
  }
14
15
  return ctx;
15
16
  }
17
+
18
+ // src/components/CodeEditor/diff-gutter.ts
19
+ function gutterDiffMark(annotations, line) {
20
+ if (!annotations) return null;
21
+ const a = annotations.byLine[line];
22
+ if (!a) return null;
23
+ return { type: a.type, deletedAbove: (a.deletedAbove ?? 0) > 0 };
24
+ }
25
+ function hasDeletedAtEnd(annotations) {
26
+ return (annotations?.deletedAtEnd ?? 0) > 0;
27
+ }
28
+ var FALLBACK = { added: "#3fb950", modified: "#4184e4", removed: "#f85149" };
29
+ function diffMarkColor(type, colors) {
30
+ return type === "added" ? colors.diffAdded ?? FALLBACK.added : colors.diffModified ?? FALLBACK.modified;
31
+ }
32
+ function diffRemovedColor(colors) {
33
+ return colors.diffRemoved ?? FALLBACK.removed;
34
+ }
16
35
  function CodeEditorPanel({ className }) {
17
36
  const {
18
37
  _engineReturn: engine,
@@ -20,6 +39,7 @@ function CodeEditorPanel({ className }) {
20
39
  wordWrap,
21
40
  readOnly,
22
41
  placeholder,
42
+ _diffAnnotations: diffAnnotations,
23
43
  _maxHeight,
24
44
  _minHeight
25
45
  } = useCodeEditor();
@@ -40,25 +60,80 @@ function CodeEditorPanel({ className }) {
40
60
  const gutterWidth = lineNumbers ? `${Math.max(String(lineCount).length, 2) * 0.75 + 1.5}em` : "0";
41
61
  const lineNumberElements = react.useMemo(() => {
42
62
  if (!lineNumbers) return null;
63
+ const removed = diffRemovedColor(themeColors);
43
64
  const lines = [];
44
65
  for (let i = 1; i <= lineCount; i++) {
66
+ const mark = gutterDiffMark(diffAnnotations, i);
45
67
  lines.push(
46
- /* @__PURE__ */ jsxRuntime.jsx(
68
+ /* @__PURE__ */ jsxRuntime.jsxs(
47
69
  "div",
48
70
  {
71
+ "data-diff": mark?.type,
72
+ "data-diff-deleted-above": mark?.deletedAbove ? "" : void 0,
49
73
  style: {
74
+ position: "relative",
50
75
  color: i === activeLine ? themeColors.foreground : themeColors.gutterForeground,
51
76
  backgroundColor: i === activeLine ? themeColors.activeLineBackground : void 0
52
77
  },
53
78
  className: "pr-3 text-right text-[13px] leading-[1.5] select-none",
54
- children: i
79
+ children: [
80
+ mark?.type && /* @__PURE__ */ jsxRuntime.jsx(
81
+ "span",
82
+ {
83
+ "aria-hidden": "true",
84
+ style: {
85
+ position: "absolute",
86
+ left: 0,
87
+ top: 0,
88
+ bottom: 0,
89
+ width: 3,
90
+ borderRadius: 2,
91
+ backgroundColor: diffMarkColor(mark.type, themeColors)
92
+ }
93
+ }
94
+ ),
95
+ mark?.deletedAbove && /* @__PURE__ */ jsxRuntime.jsx(
96
+ "span",
97
+ {
98
+ "aria-hidden": "true",
99
+ style: {
100
+ position: "absolute",
101
+ left: 0,
102
+ top: -4,
103
+ width: 0,
104
+ height: 0,
105
+ borderLeft: `8px solid ${removed}`,
106
+ borderTop: "4px solid transparent",
107
+ borderBottom: "4px solid transparent"
108
+ }
109
+ }
110
+ ),
111
+ i,
112
+ i === lineCount && hasDeletedAtEnd(diffAnnotations) && /* @__PURE__ */ jsxRuntime.jsx(
113
+ "span",
114
+ {
115
+ "aria-hidden": "true",
116
+ "data-diff-deleted-at-end": "",
117
+ style: {
118
+ position: "absolute",
119
+ left: 0,
120
+ bottom: -4,
121
+ width: 0,
122
+ height: 0,
123
+ borderLeft: `8px solid ${removed}`,
124
+ borderTop: "4px solid transparent",
125
+ borderBottom: "4px solid transparent"
126
+ }
127
+ }
128
+ )
129
+ ]
55
130
  },
56
131
  i
57
132
  )
58
133
  );
59
134
  }
60
135
  return lines;
61
- }, [lineNumbers, lineCount, activeLine, themeColors]);
136
+ }, [lineNumbers, lineCount, activeLine, themeColors, diffAnnotations]);
62
137
  const containerStyle = {
63
138
  backgroundColor: themeColors.background,
64
139
  color: themeColors.foreground,
@@ -1260,6 +1335,9 @@ registerTheme({
1260
1335
  activeLineBackground: "#f8fafc",
1261
1336
  selectionBackground: "#dbeafe",
1262
1337
  cursorColor: "#3b82f6",
1338
+ diffAdded: "#1a7f37",
1339
+ diffModified: "#0969da",
1340
+ diffRemoved: "#cf222e",
1263
1341
  keyword: "#8b5cf6",
1264
1342
  string: "#059669",
1265
1343
  comment: "#94a3b8",
@@ -1288,6 +1366,9 @@ registerTheme({
1288
1366
  activeLineBackground: "#27272a",
1289
1367
  selectionBackground: "#1e3a5f",
1290
1368
  cursorColor: "#60a5fa",
1369
+ diffAdded: "#3fb950",
1370
+ diffModified: "#4184e4",
1371
+ diffRemoved: "#f85149",
1291
1372
  keyword: "#c084fc",
1292
1373
  string: "#34d399",
1293
1374
  comment: "#71717a",
@@ -1617,9 +1698,14 @@ function CodeEditorRoot({
1617
1698
  minHeight,
1618
1699
  maxHeight,
1619
1700
  cursorLine,
1620
- cursorColumn
1701
+ cursorColumn,
1702
+ diffBase
1621
1703
  }) {
1622
1704
  const [currentValue, setCurrentValue] = reactFancy.useControllableState(valueProp, defaultValue, onChange);
1705
+ const diffAnnotations = react.useMemo(
1706
+ () => diffBase == null ? null : fancyFileCommons.annotateLines(diffBase, currentValue),
1707
+ [diffBase, currentValue]
1708
+ );
1623
1709
  const isDark = useDarkMode();
1624
1710
  const resolvedTheme = theme === "auto" ? isDark ? "dark" : "light" : theme;
1625
1711
  const [currentLanguage, setCurrentLanguageState] = react.useState(() => {
@@ -1698,11 +1784,17 @@ function CodeEditorRoot({
1698
1784
  selectionLength,
1699
1785
  textareaRef: engineReturn.textareaRef,
1700
1786
  placeholder,
1787
+ diffStats: diffAnnotations ? {
1788
+ added: diffAnnotations.added,
1789
+ modified: diffAnnotations.modified,
1790
+ removed: diffAnnotations.removed
1791
+ } : null,
1792
+ _diffAnnotations: diffAnnotations,
1701
1793
  _engineReturn: engineReturn,
1702
1794
  _minHeight: minHeight,
1703
1795
  _maxHeight: maxHeight
1704
1796
  }),
1705
- [engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, minHeight, maxHeight]
1797
+ [engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, diffAnnotations, minHeight, maxHeight]
1706
1798
  );
1707
1799
  return /* @__PURE__ */ jsxRuntime.jsx(CodeEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
1708
1800
  "div",
@@ -1878,48 +1970,10 @@ function MarkdownEditor({
1878
1970
  );
1879
1971
  }
1880
1972
  MarkdownEditor.displayName = "MarkdownEditor";
1881
- var EXT_LANGUAGE = {
1882
- js: "javascript",
1883
- mjs: "javascript",
1884
- cjs: "javascript",
1885
- jsx: "javascript",
1886
- json: "javascript",
1887
- // the JS tokenizer highlights JSON acceptably
1888
- jsonc: "javascript",
1889
- ts: "typescript",
1890
- mts: "typescript",
1891
- cts: "typescript",
1892
- tsx: "typescript",
1893
- html: "html",
1894
- htm: "html",
1895
- xml: "html",
1896
- xhtml: "html",
1897
- vue: "html",
1898
- php: "php",
1899
- phtml: "php",
1900
- py: "python",
1901
- pyw: "python",
1902
- go: "go",
1903
- md: "markdown",
1904
- markdown: "markdown",
1905
- mkd: "markdown",
1906
- mdx: "markdown"
1907
- };
1908
- function basename(filename) {
1909
- return filename.split(/[?#]/, 1)[0].split(/[\\/]/).pop() ?? "";
1910
- }
1911
- function languageFromFilename(filename) {
1912
- if (!filename) return "plaintext";
1913
- const seg = basename(filename);
1914
- const dot = seg.lastIndexOf(".");
1915
- if (dot <= 0) return "plaintext";
1916
- const ext = seg.slice(dot + 1).toLowerCase();
1917
- return EXT_LANGUAGE[ext] ?? "plaintext";
1918
- }
1919
1973
  function resolveFileKind({ filename, mime }) {
1920
1974
  const media = reactFancy.resolveMediaType({ mime, src: filename });
1921
1975
  if (media !== "unknown") return { kind: "media", mediaKind: media };
1922
- return { kind: "text", language: languageFromFilename(filename) };
1976
+ return { kind: "text", language: fancyFileCommons.languageFromFilename(filename) };
1923
1977
  }
1924
1978
  function FileViewer({
1925
1979
  filename,
@@ -1999,15 +2053,22 @@ function CodeEditorInputTag(props) {
1999
2053
  return /* @__PURE__ */ jsxRuntime.jsx(reactFancy.InputTag, { adapter, ...props });
2000
2054
  }
2001
2055
 
2056
+ Object.defineProperty(exports, "languageFromFilename", {
2057
+ enumerable: true,
2058
+ get: function () { return fancyFileCommons.languageFromFilename; }
2059
+ });
2002
2060
  exports.CodeEditor = CodeEditor;
2003
2061
  exports.CodeEditorInputTag = CodeEditorInputTag;
2004
2062
  exports.FileViewer = FileViewer;
2005
2063
  exports.MarkdownEditor = MarkdownEditor;
2064
+ exports.diffMarkColor = diffMarkColor;
2065
+ exports.diffRemovedColor = diffRemovedColor;
2006
2066
  exports.getLanguage = getLanguage;
2007
2067
  exports.getRegisteredLanguages = getRegisteredLanguages;
2008
2068
  exports.getRegisteredThemes = getRegisteredThemes;
2009
2069
  exports.getTheme = getTheme;
2010
- exports.languageFromFilename = languageFromFilename;
2070
+ exports.gutterDiffMark = gutterDiffMark;
2071
+ exports.hasDeletedAtEnd = hasDeletedAtEnd;
2011
2072
  exports.registerLanguage = registerLanguage;
2012
2073
  exports.registerTheme = registerTheme;
2013
2074
  exports.renderMarkdown = renderMarkdown;