@particle-academy/fancy-code 0.6.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 +20 -0
- package/dist/index.cjs +151 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -7
- package/dist/index.d.ts +79 -7
- package/dist/index.js +145 -45
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
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.
|
|
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:
|
|
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",
|
|
@@ -1346,6 +1427,19 @@ function highlightCode(source, tokens, colors) {
|
|
|
1346
1427
|
return parts.join("");
|
|
1347
1428
|
}
|
|
1348
1429
|
|
|
1430
|
+
// src/engine/position.ts
|
|
1431
|
+
function lineColumnToOffset(text, line, column = 1) {
|
|
1432
|
+
const lines = text.split("\n");
|
|
1433
|
+
const targetLine = Math.max(1, Math.min(Math.trunc(line) || 1, lines.length));
|
|
1434
|
+
let offset = 0;
|
|
1435
|
+
for (let i = 0; i < targetLine - 1; i++) {
|
|
1436
|
+
offset += lines[i].length + 1;
|
|
1437
|
+
}
|
|
1438
|
+
const lineText = lines[targetLine - 1] ?? "";
|
|
1439
|
+
const col = Math.max(1, Math.min(Math.trunc(column) || 1, lineText.length + 1));
|
|
1440
|
+
return offset + (col - 1);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1349
1443
|
// src/hooks/use-editor-engine.ts
|
|
1350
1444
|
var DEFAULT_COLORS = {
|
|
1351
1445
|
background: "#18181b",
|
|
@@ -1460,6 +1554,28 @@ function useEditorEngine({
|
|
|
1460
1554
|
setScrollTop(ta.scrollTop);
|
|
1461
1555
|
setScrollLeft(ta.scrollLeft);
|
|
1462
1556
|
}, []);
|
|
1557
|
+
const revealLine = react.useCallback(
|
|
1558
|
+
(line, column = 1, opts) => {
|
|
1559
|
+
const ta = textareaRef.current;
|
|
1560
|
+
if (!ta) return;
|
|
1561
|
+
const offset = lineColumnToOffset(ta.value, line, column);
|
|
1562
|
+
if (opts?.focus) ta.focus();
|
|
1563
|
+
ta.selectionStart = ta.selectionEnd = offset;
|
|
1564
|
+
updateCursorInfo();
|
|
1565
|
+
const panel = ta.closest("[data-fancy-code-panel]");
|
|
1566
|
+
if (!panel) return;
|
|
1567
|
+
const cs = getComputedStyle(ta);
|
|
1568
|
+
const lineHeight = parseFloat(cs.lineHeight) || 19.5;
|
|
1569
|
+
const paddingTop = parseFloat(cs.paddingTop) || 0;
|
|
1570
|
+
const lineIndex = (ta.value.slice(0, offset).match(/\n/g) || []).length;
|
|
1571
|
+
const textareaTopInPanel = ta.getBoundingClientRect().top - panel.getBoundingClientRect().top + panel.scrollTop;
|
|
1572
|
+
const lineTop = textareaTopInPanel + paddingTop + lineIndex * lineHeight;
|
|
1573
|
+
const target = lineTop - panel.clientHeight / 2 + lineHeight / 2;
|
|
1574
|
+
const maxScroll = panel.scrollHeight - panel.clientHeight;
|
|
1575
|
+
panel.scrollTop = Math.max(0, Math.min(target, maxScroll));
|
|
1576
|
+
},
|
|
1577
|
+
[updateCursorInfo]
|
|
1578
|
+
);
|
|
1463
1579
|
const handleKeyDown = react.useCallback(
|
|
1464
1580
|
(e) => {
|
|
1465
1581
|
if (readOnly) {
|
|
@@ -1546,6 +1662,7 @@ function useEditorEngine({
|
|
|
1546
1662
|
handleInput,
|
|
1547
1663
|
handleScroll,
|
|
1548
1664
|
handleSelect,
|
|
1665
|
+
revealLine,
|
|
1549
1666
|
scrollTop,
|
|
1550
1667
|
scrollLeft
|
|
1551
1668
|
};
|
|
@@ -1579,9 +1696,16 @@ function CodeEditorRoot({
|
|
|
1579
1696
|
tabSize: tabSizeProp = 2,
|
|
1580
1697
|
placeholder,
|
|
1581
1698
|
minHeight,
|
|
1582
|
-
maxHeight
|
|
1699
|
+
maxHeight,
|
|
1700
|
+
cursorLine,
|
|
1701
|
+
cursorColumn,
|
|
1702
|
+
diffBase
|
|
1583
1703
|
}) {
|
|
1584
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
|
+
);
|
|
1585
1709
|
const isDark = useDarkMode();
|
|
1586
1710
|
const resolvedTheme = theme === "auto" ? isDark ? "dark" : "light" : theme;
|
|
1587
1711
|
const [currentLanguage, setCurrentLanguageState] = react.useState(() => {
|
|
@@ -1615,6 +1739,11 @@ function CodeEditorRoot({
|
|
|
1615
1739
|
setSelectionLength(sel);
|
|
1616
1740
|
}
|
|
1617
1741
|
});
|
|
1742
|
+
const { revealLine: revealLineImpl } = engineReturn;
|
|
1743
|
+
react.useEffect(() => {
|
|
1744
|
+
if (cursorLine == null) return;
|
|
1745
|
+
revealLineImpl(cursorLine, cursorColumn ?? 1, { focus: false });
|
|
1746
|
+
}, [cursorLine, cursorColumn, revealLineImpl]);
|
|
1618
1747
|
const contextValue = react.useMemo(
|
|
1619
1748
|
() => ({
|
|
1620
1749
|
getValue: () => engineReturn.textareaRef.current?.value ?? currentValue,
|
|
@@ -1636,6 +1765,8 @@ function CodeEditorRoot({
|
|
|
1636
1765
|
setCurrentValue(ta.value);
|
|
1637
1766
|
},
|
|
1638
1767
|
focus: () => engineReturn.textareaRef.current?.focus(),
|
|
1768
|
+
revealLine: (line, column) => engineReturn.revealLine(line, column ?? 1, { focus: true }),
|
|
1769
|
+
setCursor: ({ line, column }) => engineReturn.revealLine(line, column ?? 1, { focus: true }),
|
|
1639
1770
|
language: currentLanguage,
|
|
1640
1771
|
setLanguage,
|
|
1641
1772
|
theme: resolvedTheme,
|
|
@@ -1653,11 +1784,17 @@ function CodeEditorRoot({
|
|
|
1653
1784
|
selectionLength,
|
|
1654
1785
|
textareaRef: engineReturn.textareaRef,
|
|
1655
1786
|
placeholder,
|
|
1787
|
+
diffStats: diffAnnotations ? {
|
|
1788
|
+
added: diffAnnotations.added,
|
|
1789
|
+
modified: diffAnnotations.modified,
|
|
1790
|
+
removed: diffAnnotations.removed
|
|
1791
|
+
} : null,
|
|
1792
|
+
_diffAnnotations: diffAnnotations,
|
|
1656
1793
|
_engineReturn: engineReturn,
|
|
1657
1794
|
_minHeight: minHeight,
|
|
1658
1795
|
_maxHeight: maxHeight
|
|
1659
1796
|
}),
|
|
1660
|
-
[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]
|
|
1661
1798
|
);
|
|
1662
1799
|
return /* @__PURE__ */ jsxRuntime.jsx(CodeEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1663
1800
|
"div",
|
|
@@ -1833,48 +1970,10 @@ function MarkdownEditor({
|
|
|
1833
1970
|
);
|
|
1834
1971
|
}
|
|
1835
1972
|
MarkdownEditor.displayName = "MarkdownEditor";
|
|
1836
|
-
var EXT_LANGUAGE = {
|
|
1837
|
-
js: "javascript",
|
|
1838
|
-
mjs: "javascript",
|
|
1839
|
-
cjs: "javascript",
|
|
1840
|
-
jsx: "javascript",
|
|
1841
|
-
json: "javascript",
|
|
1842
|
-
// the JS tokenizer highlights JSON acceptably
|
|
1843
|
-
jsonc: "javascript",
|
|
1844
|
-
ts: "typescript",
|
|
1845
|
-
mts: "typescript",
|
|
1846
|
-
cts: "typescript",
|
|
1847
|
-
tsx: "typescript",
|
|
1848
|
-
html: "html",
|
|
1849
|
-
htm: "html",
|
|
1850
|
-
xml: "html",
|
|
1851
|
-
xhtml: "html",
|
|
1852
|
-
vue: "html",
|
|
1853
|
-
php: "php",
|
|
1854
|
-
phtml: "php",
|
|
1855
|
-
py: "python",
|
|
1856
|
-
pyw: "python",
|
|
1857
|
-
go: "go",
|
|
1858
|
-
md: "markdown",
|
|
1859
|
-
markdown: "markdown",
|
|
1860
|
-
mkd: "markdown",
|
|
1861
|
-
mdx: "markdown"
|
|
1862
|
-
};
|
|
1863
|
-
function basename(filename) {
|
|
1864
|
-
return filename.split(/[?#]/, 1)[0].split(/[\\/]/).pop() ?? "";
|
|
1865
|
-
}
|
|
1866
|
-
function languageFromFilename(filename) {
|
|
1867
|
-
if (!filename) return "plaintext";
|
|
1868
|
-
const seg = basename(filename);
|
|
1869
|
-
const dot = seg.lastIndexOf(".");
|
|
1870
|
-
if (dot <= 0) return "plaintext";
|
|
1871
|
-
const ext = seg.slice(dot + 1).toLowerCase();
|
|
1872
|
-
return EXT_LANGUAGE[ext] ?? "plaintext";
|
|
1873
|
-
}
|
|
1874
1973
|
function resolveFileKind({ filename, mime }) {
|
|
1875
1974
|
const media = reactFancy.resolveMediaType({ mime, src: filename });
|
|
1876
1975
|
if (media !== "unknown") return { kind: "media", mediaKind: media };
|
|
1877
|
-
return { kind: "text", language: languageFromFilename(filename) };
|
|
1976
|
+
return { kind: "text", language: fancyFileCommons.languageFromFilename(filename) };
|
|
1878
1977
|
}
|
|
1879
1978
|
function FileViewer({
|
|
1880
1979
|
filename,
|
|
@@ -1954,15 +2053,22 @@ function CodeEditorInputTag(props) {
|
|
|
1954
2053
|
return /* @__PURE__ */ jsxRuntime.jsx(reactFancy.InputTag, { adapter, ...props });
|
|
1955
2054
|
}
|
|
1956
2055
|
|
|
2056
|
+
Object.defineProperty(exports, "languageFromFilename", {
|
|
2057
|
+
enumerable: true,
|
|
2058
|
+
get: function () { return fancyFileCommons.languageFromFilename; }
|
|
2059
|
+
});
|
|
1957
2060
|
exports.CodeEditor = CodeEditor;
|
|
1958
2061
|
exports.CodeEditorInputTag = CodeEditorInputTag;
|
|
1959
2062
|
exports.FileViewer = FileViewer;
|
|
1960
2063
|
exports.MarkdownEditor = MarkdownEditor;
|
|
2064
|
+
exports.diffMarkColor = diffMarkColor;
|
|
2065
|
+
exports.diffRemovedColor = diffRemovedColor;
|
|
1961
2066
|
exports.getLanguage = getLanguage;
|
|
1962
2067
|
exports.getRegisteredLanguages = getRegisteredLanguages;
|
|
1963
2068
|
exports.getRegisteredThemes = getRegisteredThemes;
|
|
1964
2069
|
exports.getTheme = getTheme;
|
|
1965
|
-
exports.
|
|
2070
|
+
exports.gutterDiffMark = gutterDiffMark;
|
|
2071
|
+
exports.hasDeletedAtEnd = hasDeletedAtEnd;
|
|
1966
2072
|
exports.registerLanguage = registerLanguage;
|
|
1967
2073
|
exports.registerTheme = registerTheme;
|
|
1968
2074
|
exports.renderMarkdown = renderMarkdown;
|