@tutorialkit-rb/react 1.5.2-rb.0.1.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/BootScreen.d.ts +7 -0
- package/dist/BootScreen.js +37 -0
- package/dist/Button.d.ts +6 -0
- package/dist/Button.js +12 -0
- package/dist/Nav.d.ts +7 -0
- package/dist/Nav.js +38 -0
- package/dist/Panels/EditorPanel.d.ts +25 -0
- package/dist/Panels/EditorPanel.js +83 -0
- package/dist/Panels/PreviewPanel.d.ts +20 -0
- package/dist/Panels/PreviewPanel.js +166 -0
- package/dist/Panels/TerminalPanel.d.ts +7 -0
- package/dist/Panels/TerminalPanel.js +45 -0
- package/dist/Panels/WorkspacePanel.d.ts +14 -0
- package/dist/Panels/WorkspacePanel.js +166 -0
- package/dist/core/CodeMirrorEditor/BinaryContent.d.ts +1 -0
- package/dist/core/CodeMirrorEditor/BinaryContent.js +4 -0
- package/dist/core/CodeMirrorEditor/cm-theme.d.ts +8 -0
- package/dist/core/CodeMirrorEditor/cm-theme.js +167 -0
- package/dist/core/CodeMirrorEditor/indent.d.ts +2 -0
- package/dist/core/CodeMirrorEditor/indent.js +49 -0
- package/dist/core/CodeMirrorEditor/index.d.ts +39 -0
- package/dist/core/CodeMirrorEditor/index.js +201 -0
- package/dist/core/CodeMirrorEditor/languages.d.ts +3 -0
- package/dist/core/CodeMirrorEditor/languages.js +118 -0
- package/dist/core/CodeMirrorEditor/themes/vscode-dark.d.ts +2 -0
- package/dist/core/CodeMirrorEditor/themes/vscode-dark.js +75 -0
- package/dist/core/ContextMenu.d.ts +29 -0
- package/dist/core/ContextMenu.js +61 -0
- package/dist/core/Dialog.d.ts +15 -0
- package/dist/core/Dialog.js +12 -0
- package/dist/core/FileTree.d.ts +18 -0
- package/dist/core/FileTree.js +194 -0
- package/dist/core/Terminal/index.d.ts +15 -0
- package/dist/core/Terminal/index.js +57 -0
- package/dist/core/Terminal/theme.d.ts +2 -0
- package/dist/core/Terminal/theme.js +31 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.js +3 -0
- package/dist/hooks/useOutsideClick.d.ts +1 -0
- package/dist/hooks/useOutsideClick.js +14 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/styles/cm.css +100 -0
- package/dist/styles/nav.module.css +108 -0
- package/dist/styles/resize-panel.module.css +28 -0
- package/dist/styles/terminal.css +8 -0
- package/dist/utils/classnames.d.ts +16 -0
- package/dist/utils/classnames.js +47 -0
- package/dist/utils/debounce.d.ts +1 -0
- package/dist/utils/debounce.js +13 -0
- package/dist/utils/mobile.d.ts +1 -0
- package/dist/utils/mobile.js +4 -0
- package/package.json +116 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { LanguageDescription, LanguageSupport, StreamLanguage } from '@codemirror/language';
|
|
2
|
+
export const supportedLanguages = [
|
|
3
|
+
LanguageDescription.of({
|
|
4
|
+
name: 'TS',
|
|
5
|
+
extensions: ['ts'],
|
|
6
|
+
async load() {
|
|
7
|
+
return import('@codemirror/lang-javascript').then((module) => module.javascript({ typescript: true }));
|
|
8
|
+
},
|
|
9
|
+
}),
|
|
10
|
+
LanguageDescription.of({
|
|
11
|
+
name: 'JS',
|
|
12
|
+
extensions: ['js', 'mjs', 'cjs'],
|
|
13
|
+
async load() {
|
|
14
|
+
return import('@codemirror/lang-javascript').then((module) => module.javascript());
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
LanguageDescription.of({
|
|
18
|
+
name: 'TSX',
|
|
19
|
+
extensions: ['tsx'],
|
|
20
|
+
async load() {
|
|
21
|
+
return import('@codemirror/lang-javascript').then((module) => module.javascript({ jsx: true, typescript: true }));
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
LanguageDescription.of({
|
|
25
|
+
name: 'JSX',
|
|
26
|
+
extensions: ['jsx'],
|
|
27
|
+
async load() {
|
|
28
|
+
return import('@codemirror/lang-javascript').then((module) => module.javascript({ jsx: true }));
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
LanguageDescription.of({
|
|
32
|
+
name: 'HTML',
|
|
33
|
+
extensions: ['html'],
|
|
34
|
+
async load() {
|
|
35
|
+
return import('@codemirror/lang-html').then((module) => module.html());
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
LanguageDescription.of({
|
|
39
|
+
name: 'CSS',
|
|
40
|
+
extensions: ['css'],
|
|
41
|
+
async load() {
|
|
42
|
+
return import('@codemirror/lang-css').then((module) => module.css());
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
LanguageDescription.of({
|
|
46
|
+
name: 'SASS',
|
|
47
|
+
extensions: ['sass'],
|
|
48
|
+
async load() {
|
|
49
|
+
return import('@codemirror/lang-sass').then((module) => module.sass({ indented: true }));
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
LanguageDescription.of({
|
|
53
|
+
name: 'SCSS',
|
|
54
|
+
extensions: ['scss'],
|
|
55
|
+
async load() {
|
|
56
|
+
return import('@codemirror/lang-sass').then((module) => module.sass({ indented: false }));
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
LanguageDescription.of({
|
|
60
|
+
name: 'JSON',
|
|
61
|
+
extensions: ['json'],
|
|
62
|
+
async load() {
|
|
63
|
+
return import('@codemirror/lang-json').then((module) => module.json());
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
LanguageDescription.of({
|
|
67
|
+
name: 'Markdown',
|
|
68
|
+
extensions: ['md'],
|
|
69
|
+
async load() {
|
|
70
|
+
return import('@codemirror/lang-markdown').then((module) => module.markdown());
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
LanguageDescription.of({
|
|
74
|
+
name: 'Wasm',
|
|
75
|
+
extensions: ['wat'],
|
|
76
|
+
async load() {
|
|
77
|
+
return import('@codemirror/lang-wast').then((module) => module.wast());
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
LanguageDescription.of({
|
|
81
|
+
name: 'Vue',
|
|
82
|
+
extensions: ['vue'],
|
|
83
|
+
async load() {
|
|
84
|
+
return import('@codemirror/lang-vue').then((module) => module.vue());
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
LanguageDescription.of({
|
|
88
|
+
name: 'Svelte',
|
|
89
|
+
extensions: ['svelte'],
|
|
90
|
+
async load() {
|
|
91
|
+
return import('@replit/codemirror-lang-svelte').then((module) => module.svelte());
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
LanguageDescription.of({
|
|
95
|
+
name: 'Ruby',
|
|
96
|
+
extensions: ['rb', 'rake', 'erb'],
|
|
97
|
+
filename: /(Gemfile|Rakefile|config\.ru|bin\/(rails|rubocop))/,
|
|
98
|
+
async load() {
|
|
99
|
+
return import('@codemirror/legacy-modes/mode/ruby').then((module) => {
|
|
100
|
+
return new LanguageSupport(StreamLanguage.define(module.ruby));
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
LanguageDescription.of({
|
|
105
|
+
name: 'YAML',
|
|
106
|
+
extensions: ['yaml', 'yml'],
|
|
107
|
+
async load() {
|
|
108
|
+
return import('@codemirror/lang-yaml').then((module) => module.yaml());
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
];
|
|
112
|
+
export async function getLanguage(fileName) {
|
|
113
|
+
const languageDescription = LanguageDescription.matchFilename(supportedLanguages, fileName);
|
|
114
|
+
if (languageDescription) {
|
|
115
|
+
return await languageDescription.load();
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { HighlightStyle } from '@codemirror/language';
|
|
2
|
+
import { tags } from '@lezer/highlight';
|
|
3
|
+
export const vscodeDarkTheme = HighlightStyle.define([
|
|
4
|
+
{
|
|
5
|
+
tag: [
|
|
6
|
+
tags.keyword,
|
|
7
|
+
tags.operatorKeyword,
|
|
8
|
+
tags.modifier,
|
|
9
|
+
tags.color,
|
|
10
|
+
tags.constant(tags.name),
|
|
11
|
+
tags.standard(tags.name),
|
|
12
|
+
tags.standard(tags.tagName),
|
|
13
|
+
tags.special(tags.brace),
|
|
14
|
+
tags.atom,
|
|
15
|
+
tags.bool,
|
|
16
|
+
tags.special(tags.variableName),
|
|
17
|
+
],
|
|
18
|
+
color: '#569cd6',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
tag: [tags.controlKeyword, tags.moduleKeyword],
|
|
22
|
+
color: '#c586c0',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
tag: [
|
|
26
|
+
tags.name,
|
|
27
|
+
tags.deleted,
|
|
28
|
+
tags.character,
|
|
29
|
+
tags.macroName,
|
|
30
|
+
tags.propertyName,
|
|
31
|
+
tags.variableName,
|
|
32
|
+
tags.labelName,
|
|
33
|
+
tags.definition(tags.name),
|
|
34
|
+
],
|
|
35
|
+
color: '#9cdcfe',
|
|
36
|
+
},
|
|
37
|
+
{ tag: tags.heading, fontWeight: 'bold', color: '#9cdcfe' },
|
|
38
|
+
{
|
|
39
|
+
tag: [
|
|
40
|
+
tags.typeName,
|
|
41
|
+
tags.className,
|
|
42
|
+
tags.tagName,
|
|
43
|
+
tags.number,
|
|
44
|
+
tags.changed,
|
|
45
|
+
tags.annotation,
|
|
46
|
+
tags.self,
|
|
47
|
+
tags.namespace,
|
|
48
|
+
],
|
|
49
|
+
color: '#4ec9b0',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
tag: [tags.function(tags.variableName), tags.function(tags.propertyName)],
|
|
53
|
+
color: '#dcdcaa',
|
|
54
|
+
},
|
|
55
|
+
{ tag: [tags.number], color: '#b5cea8' },
|
|
56
|
+
{
|
|
57
|
+
tag: [tags.operator, tags.punctuation, tags.separator, tags.url, tags.escape, tags.regexp],
|
|
58
|
+
color: '#d4d4d4',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
tag: [tags.regexp],
|
|
62
|
+
color: '#d16969',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
tag: [tags.special(tags.string), tags.processingInstruction, tags.string, tags.inserted],
|
|
66
|
+
color: '#ce9178',
|
|
67
|
+
},
|
|
68
|
+
{ tag: [tags.angleBracket], color: '#808080' },
|
|
69
|
+
{ tag: tags.strong, fontWeight: 'bold' },
|
|
70
|
+
{ tag: tags.emphasis, fontStyle: 'italic' },
|
|
71
|
+
{ tag: tags.strikethrough, textDecoration: 'line-through' },
|
|
72
|
+
{ tag: [tags.meta, tags.comment], color: '#6a9955' },
|
|
73
|
+
{ tag: tags.link, color: '#6a9955', textDecoration: 'underline' },
|
|
74
|
+
{ tag: tags.invalid, color: '#ff0000' },
|
|
75
|
+
]);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type FileDescriptor, type I18n } from '@tutorialkit-rb/types';
|
|
2
|
+
import { type ComponentProps } from 'react';
|
|
3
|
+
interface FileChangeEvent {
|
|
4
|
+
type: FileDescriptor['type'];
|
|
5
|
+
method: 'add' | 'remove' | 'rename';
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
interface FileRenameEvent extends FileChangeEvent {
|
|
9
|
+
method: 'rename';
|
|
10
|
+
oldValue: string;
|
|
11
|
+
}
|
|
12
|
+
interface Props extends ComponentProps<'div'> {
|
|
13
|
+
/** Callback invoked when file is changed. This callback should throw errors with {@link FilesystemError} messages. */
|
|
14
|
+
onFileChange?: (event: FileChangeEvent | FileRenameEvent) => Promise<void>;
|
|
15
|
+
/** Glob patterns for paths that allow editing files and folders. Disabled by default. */
|
|
16
|
+
allowEditPatterns?: string[];
|
|
17
|
+
/** Directory of the clicked file. */
|
|
18
|
+
directory: string;
|
|
19
|
+
/** Whether to render new files/directories before or after the trigger element. Defaults to `'before'`. */
|
|
20
|
+
position?: 'before' | 'after';
|
|
21
|
+
/** Localized texts for menu. */
|
|
22
|
+
i18n?: Pick<I18n, 'fileTreeCreateFileText' | 'fileTreeCreateFolderText' | 'fileTreeActionNotAllowedText' | 'fileTreeAllowedPatternsText' | 'fileTreeFileExistsAlreadyText' | 'confirmationText'>;
|
|
23
|
+
/** Props for trigger wrapper. */
|
|
24
|
+
triggerProps?: ComponentProps<'div'> & {
|
|
25
|
+
'data-testid'?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function ContextMenu({ onFileChange, allowEditPatterns, directory, i18n, position, children, triggerProps, ...props }: Props): string | number | boolean | import("react/jsx-runtime").JSX.Element | Iterable<import("react").ReactNode> | null | undefined;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Root, Portal, Content, Item, Trigger } from '@radix-ui/react-context-menu';
|
|
3
|
+
import { DEFAULT_LOCALIZATION } from '@tutorialkit-rb/types';
|
|
4
|
+
import picomatch from 'picomatch/posix';
|
|
5
|
+
import { useRef, useState } from 'react';
|
|
6
|
+
import { classNames } from '../utils/classnames.js';
|
|
7
|
+
import { useDialog } from './Dialog.js';
|
|
8
|
+
export function ContextMenu({ onFileChange, allowEditPatterns, directory, i18n, position = 'before', children, triggerProps, ...props }) {
|
|
9
|
+
const [state, setState] = useState('idle');
|
|
10
|
+
const inputRef = useRef(null);
|
|
11
|
+
const Dialog = useDialog();
|
|
12
|
+
if (!allowEditPatterns?.length) {
|
|
13
|
+
return children;
|
|
14
|
+
}
|
|
15
|
+
async function onFileNameEnd(event) {
|
|
16
|
+
if (state !== 'add_file' && state !== 'add_folder') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const name = event.currentTarget.value;
|
|
20
|
+
if (name) {
|
|
21
|
+
const value = `${directory}/${name}`;
|
|
22
|
+
const isAllowed = picomatch.isMatch(value, allowEditPatterns);
|
|
23
|
+
if (!isAllowed) {
|
|
24
|
+
return setState('add_failed_not_allowed');
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
await onFileChange?.({
|
|
28
|
+
value,
|
|
29
|
+
type: state === 'add_file' ? 'file' : 'folder',
|
|
30
|
+
method: 'add',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error?.message;
|
|
35
|
+
if (message === 'FILE_EXISTS' || message === 'FOLDER_EXISTS') {
|
|
36
|
+
return setState('add_failed_exists');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
setState('idle');
|
|
41
|
+
}
|
|
42
|
+
function onFileNameKeyPress(event) {
|
|
43
|
+
if (event.key === 'Enter' && event.currentTarget.value !== '') {
|
|
44
|
+
onFileNameEnd(event);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function onCloseAutoFocus(event) {
|
|
48
|
+
if ((state === 'add_file' || state === 'add_folder') && inputRef.current) {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
inputRef.current.focus();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const element = (_jsx(Trigger, { asChild: true, children: _jsx("div", { ...triggerProps, children: children }) }));
|
|
54
|
+
return (_jsxs(Root, { children: [position === 'before' && element, state !== 'idle' && (_jsxs("div", { className: "flex items-center gap-2 border-2 border-solid border-transparent", ...props, children: [_jsx("div", { className: `scale-120 shrink-0 ${state === 'add_file' ? 'i-ph-file-duotone' : 'i-ph-folder-duotone'}` }), _jsx("input", { ref: inputRef, autoFocus: true, type: "text", onBlur: onFileNameEnd, onKeyUp: onFileNameKeyPress, className: "text-current bg-transparent w-20 outline-var(--tk-border-accent)" })] })), position === 'after' && element, _jsx(Portal, { children: _jsxs(Content, { onCloseAutoFocus: onCloseAutoFocus, className: "border border-tk-border-brighter b-rounded-md bg-tk-background-brighter py-2", children: [_jsx(MenuItem, { icon: "i-ph-file-plus", onClick: () => setState('add_file'), children: i18n?.fileTreeCreateFileText || DEFAULT_LOCALIZATION.fileTreeCreateFileText }), _jsx(MenuItem, { icon: "i-ph-folder-plus", onClick: () => setState('add_folder'), children: i18n?.fileTreeCreateFolderText || DEFAULT_LOCALIZATION.fileTreeCreateFolderText })] }) }), (state === 'add_failed_not_allowed' || state === 'add_failed_exists') && (_jsx(Dialog, { title: i18n?.fileTreeActionNotAllowedText || DEFAULT_LOCALIZATION.fileTreeActionNotAllowedText, confirmText: i18n?.confirmationText || DEFAULT_LOCALIZATION.confirmationText, onClose: () => setState('idle'), children: state === 'add_failed_not_allowed' ? (_jsxs(_Fragment, { children: [i18n?.fileTreeAllowedPatternsText || DEFAULT_LOCALIZATION.fileTreeAllowedPatternsText, _jsx(AllowPatternsList, { allowEditPatterns: allowEditPatterns })] })) : (_jsx(_Fragment, { children: i18n?.fileTreeFileExistsAlreadyText || DEFAULT_LOCALIZATION.fileTreeFileExistsAlreadyText })) }))] }));
|
|
55
|
+
}
|
|
56
|
+
function MenuItem({ icon, children, ...props }) {
|
|
57
|
+
return (_jsxs(Item, { ...props, className: "flex items-center gap-2 px-4 py-1 text-sm cursor-pointer ws-nowrap text-tk-elements-fileTree-folder-textColor hover:bg-tk-elements-fileTree-file-backgroundColorHover", children: [_jsx("span", { className: `${icon} scale-120 shrink-0` }), _jsx("span", { children: children })] }));
|
|
58
|
+
}
|
|
59
|
+
function AllowPatternsList({ allowEditPatterns }) {
|
|
60
|
+
return (_jsx("ul", { className: classNames('mt-2', allowEditPatterns.length > 1 && 'list-disc ml-4'), children: allowEditPatterns.map((pattern) => (_jsx("li", { className: "mb-1", children: _jsx("code", { children: pattern }) }, pattern))) }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Title of the dialog */
|
|
4
|
+
title: string;
|
|
5
|
+
/** Text for the confirmation button */
|
|
6
|
+
confirmText: string;
|
|
7
|
+
/** Callback invoked when dialog is closed */
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
/** Content of the dialog */
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export declare const DialogProvider: import("react").Provider<typeof Dialog>;
|
|
13
|
+
export declare function useDialog(): typeof Dialog;
|
|
14
|
+
export default function Dialog({ title, confirmText, onClose, children }: Props): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
import { Button } from '../Button.js';
|
|
5
|
+
const context = createContext(Dialog);
|
|
6
|
+
export const DialogProvider = context.Provider;
|
|
7
|
+
export function useDialog() {
|
|
8
|
+
return useContext(context);
|
|
9
|
+
}
|
|
10
|
+
export default function Dialog({ title, confirmText, onClose, children }) {
|
|
11
|
+
return (_jsx(RadixDialog.Root, { open: true, onOpenChange: (open) => !open && onClose(), children: _jsxs(RadixDialog.Portal, { children: [_jsx(RadixDialog.Overlay, { className: "fixed z-11 inset-0 opacity-50 bg-black" }), _jsx(RadixDialog.Content, { className: "fixed z-11 top-50% left-50% transform-translate--50% w-90vw max-w-450px max-h-85vh rounded-xl text-tk-text-primary bg-tk-background-primary", children: _jsxs("div", { className: "relative p-10", children: [_jsx(RadixDialog.Title, { className: "text-6", children: title }), _jsx("div", { className: "my-4", children: children }), _jsx(RadixDialog.Close, { asChild: true, children: _jsx(Button, { className: "min-w-20 justify-center", children: confirmText }) })] }) })] }) }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FileDescriptor } from '@tutorialkit-rb/types';
|
|
2
|
+
import { type ComponentProps } from 'react';
|
|
3
|
+
import { ContextMenu } from './ContextMenu.js';
|
|
4
|
+
interface Props {
|
|
5
|
+
files: FileDescriptor[];
|
|
6
|
+
selectedFile?: string;
|
|
7
|
+
onFileSelect?: (filePath: string) => void;
|
|
8
|
+
onFileChange?: ComponentProps<typeof ContextMenu>['onFileChange'];
|
|
9
|
+
allowEditPatterns?: ComponentProps<typeof ContextMenu>['allowEditPatterns'];
|
|
10
|
+
i18n?: ComponentProps<typeof ContextMenu>['i18n'];
|
|
11
|
+
hideRoot: boolean;
|
|
12
|
+
scope?: string;
|
|
13
|
+
hiddenFiles?: Array<string | RegExp>;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function FileTree({ files, onFileSelect, onFileChange, allowEditPatterns, selectedFile, hideRoot, scope, hiddenFiles, i18n, className, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export default FileTree;
|
|
18
|
+
export declare function sortFiles(fileA: FileDescriptor, fileB: FileDescriptor): 0 | 1 | -1;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { classNames } from '../utils/classnames.js';
|
|
4
|
+
import { ContextMenu } from './ContextMenu.js';
|
|
5
|
+
const NODE_PADDING_LEFT = 12;
|
|
6
|
+
const DEFAULT_HIDDEN_FILES = [/\/node_modules\//];
|
|
7
|
+
export function FileTree({ files, onFileSelect, onFileChange, allowEditPatterns, selectedFile, hideRoot, scope, hiddenFiles, i18n, className, }) {
|
|
8
|
+
const computedHiddenFiles = useMemo(() => [...DEFAULT_HIDDEN_FILES, ...(hiddenFiles ?? [])], [hiddenFiles]);
|
|
9
|
+
const fileList = useMemo(() => buildFileList(files, hideRoot, scope, computedHiddenFiles), [files, hideRoot, scope, computedHiddenFiles]);
|
|
10
|
+
const [collapsedFolders, setCollapsedFolders] = useState(() => new Set());
|
|
11
|
+
// reset collapsed folders when the list of files changes
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setCollapsedFolders(new Set());
|
|
14
|
+
}, [files]);
|
|
15
|
+
const filteredFileList = useMemo(() => {
|
|
16
|
+
const list = [];
|
|
17
|
+
let lastDepth = Number.MAX_SAFE_INTEGER;
|
|
18
|
+
for (const fileOrFolder of fileList) {
|
|
19
|
+
const depth = fileOrFolder.depth;
|
|
20
|
+
// if the depth is equal we reached the end of the collaped group
|
|
21
|
+
if (lastDepth === depth) {
|
|
22
|
+
lastDepth = Number.MAX_SAFE_INTEGER;
|
|
23
|
+
}
|
|
24
|
+
// ignore collapsed folders
|
|
25
|
+
if (collapsedFolders.has(fileOrFolder.id)) {
|
|
26
|
+
lastDepth = Math.min(lastDepth, depth);
|
|
27
|
+
}
|
|
28
|
+
// ignore files and folders below the last collapsed folder
|
|
29
|
+
if (lastDepth < depth) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
list.push(fileOrFolder);
|
|
33
|
+
}
|
|
34
|
+
return list;
|
|
35
|
+
}, [fileList, collapsedFolders]);
|
|
36
|
+
function toggleCollapseState(id) {
|
|
37
|
+
setCollapsedFolders((prevSet) => {
|
|
38
|
+
const newSet = new Set(prevSet);
|
|
39
|
+
if (newSet.has(id)) {
|
|
40
|
+
newSet.delete(id);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
newSet.add(id);
|
|
44
|
+
}
|
|
45
|
+
return newSet;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return (_jsxs("div", { className: classNames(className, 'h-full transition-theme bg-tk-elements-fileTree-backgroundColor'), children: [filteredFileList.map((fileOrFolder) => {
|
|
49
|
+
switch (fileOrFolder.kind) {
|
|
50
|
+
case 'file': {
|
|
51
|
+
return (_jsx(File, { selected: selectedFile === fileOrFolder.fullPath, file: fileOrFolder, onClick: () => onFileSelect?.(fileOrFolder.fullPath) }, fileOrFolder.id));
|
|
52
|
+
}
|
|
53
|
+
case 'folder': {
|
|
54
|
+
return (_jsx(Folder, { folder: fileOrFolder, i18n: i18n, collapsed: collapsedFolders.has(fileOrFolder.id), onClick: () => toggleCollapseState(fileOrFolder.id), onFileChange: onFileChange, allowEditPatterns: allowEditPatterns }, fileOrFolder.id));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}), _jsx(ContextMenu // context menu for the remaining free space area
|
|
58
|
+
, { position: "after", i18n: i18n, style: getDepthStyle(0), directory: "", onFileChange: onFileChange, allowEditPatterns: allowEditPatterns, triggerProps: { className: 'h-full min-h-4', 'data-testid': 'file-tree-root-context-menu' } })] }));
|
|
59
|
+
}
|
|
60
|
+
export default FileTree;
|
|
61
|
+
function Folder({ folder: { depth, name, fullPath }, i18n, collapsed, onClick, onFileChange, allowEditPatterns, }) {
|
|
62
|
+
return (_jsx(ContextMenu, { onFileChange: onFileChange, allowEditPatterns: allowEditPatterns, i18n: i18n, directory: fullPath, style: getDepthStyle(1 + depth), children: _jsx(NodeButton, { className: "group transition-theme bg-tk-elements-fileTree-folder-backgroundColor hover:bg-tk-elements-fileTree-folder-backgroundColorHover text-tk-elements-fileTree-folder-textColor hover:text-tk-elements-fileTree-folder-textColor", depth: depth, iconClasses: classNames('text-tk-elements-fileTree-folder-iconColor group-hover:text-tk-elements-fileTree-folder-iconColorHover', {
|
|
63
|
+
'i-ph-folder-simple-duotone': collapsed,
|
|
64
|
+
'i-ph-folder-open-duotone': !collapsed,
|
|
65
|
+
}), onClick: onClick, children: name }) }));
|
|
66
|
+
}
|
|
67
|
+
function File({ file: { depth, name }, onClick, selected }) {
|
|
68
|
+
const extension = getFileExtension(name);
|
|
69
|
+
const fileIcon = extensionsToIcons.get(extension) || 'i-ph-file-duotone';
|
|
70
|
+
return (_jsx(NodeButton, { className: classNames('group transition-theme', {
|
|
71
|
+
'bg-tk-elements-fileTree-file-backgroundColor hover:bg-tk-elements-fileTree-file-backgroundColorHover text-tk-elements-fileTree-file-textColor hover:text-tk-elements-fileTree-file-textColorHover': !selected,
|
|
72
|
+
'bg-tk-elements-fileTree-file-backgroundColorSelected text-tk-elements-fileTree-file-textColorSelected': selected,
|
|
73
|
+
}), depth: depth, iconClasses: classNames(fileIcon, {
|
|
74
|
+
'text-tk-elements-fileTree-file-iconColor group-hover:text-tk-elements-fileTree-file-iconColorHover': !selected,
|
|
75
|
+
'text-tk-elements-fileTree-file-iconColorSelected': selected,
|
|
76
|
+
}), onClick: onClick, "aria-pressed": selected, children: name }));
|
|
77
|
+
}
|
|
78
|
+
function NodeButton({ depth, iconClasses, onClick, className, 'aria-pressed': ariaPressed, children }) {
|
|
79
|
+
return (_jsxs("button", { className: `flex items-center gap-2 w-full pr-2 border-2 border-transparent text-faded ${className ?? ''}`, style: getDepthStyle(depth), onClick: onClick, "aria-pressed": ariaPressed === true ? 'true' : undefined, children: [_jsx("div", { className: classNames('scale-120 shrink-0', iconClasses) }), _jsx("span", { children: children })] }));
|
|
80
|
+
}
|
|
81
|
+
function buildFileList(files, hideRoot, scope, hiddenFiles) {
|
|
82
|
+
const folderPaths = new Set();
|
|
83
|
+
const fileList = [];
|
|
84
|
+
const defaultDepth = hideRoot ? 0 : 1;
|
|
85
|
+
if (!hideRoot) {
|
|
86
|
+
fileList.push({ kind: 'folder', name: '/', fullPath: '/', depth: 0, id: 0 });
|
|
87
|
+
}
|
|
88
|
+
for (const file of [...files].sort(sortFiles)) {
|
|
89
|
+
if (scope && !file.path.startsWith(scope)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const segments = file.path.split('/').filter((s) => s);
|
|
93
|
+
const fileName = segments.at(-1);
|
|
94
|
+
if (!fileName || isHiddenFile(file.path, fileName, hiddenFiles)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let currentPath = '';
|
|
98
|
+
for (let depth = 0; depth < segments.length; ++depth) {
|
|
99
|
+
const name = segments[depth];
|
|
100
|
+
const fullPath = (currentPath += `/${name}`);
|
|
101
|
+
if (depth === segments.length - 1) {
|
|
102
|
+
fileList.push({
|
|
103
|
+
kind: file.type,
|
|
104
|
+
id: fileList.length,
|
|
105
|
+
name,
|
|
106
|
+
fullPath,
|
|
107
|
+
depth: depth + defaultDepth,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else if (!folderPaths.has(fullPath)) {
|
|
111
|
+
folderPaths.add(fullPath);
|
|
112
|
+
fileList.push({
|
|
113
|
+
kind: 'folder',
|
|
114
|
+
id: fileList.length,
|
|
115
|
+
name,
|
|
116
|
+
fullPath,
|
|
117
|
+
depth: depth + defaultDepth,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return fileList;
|
|
123
|
+
}
|
|
124
|
+
function isHiddenFile(filePath, fileName, hiddenFiles) {
|
|
125
|
+
return hiddenFiles.some((pathOrRegex) => {
|
|
126
|
+
if (typeof pathOrRegex === 'string') {
|
|
127
|
+
return fileName === pathOrRegex;
|
|
128
|
+
}
|
|
129
|
+
return pathOrRegex.test(filePath);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function getDepthStyle(depth) {
|
|
133
|
+
return { paddingLeft: `${12 + depth * NODE_PADDING_LEFT}px` };
|
|
134
|
+
}
|
|
135
|
+
export function sortFiles(fileA, fileB) {
|
|
136
|
+
const segmentsA = fileA.path.split('/');
|
|
137
|
+
const segmentsB = fileB.path.split('/');
|
|
138
|
+
const minLength = Math.min(segmentsA.length, segmentsB.length);
|
|
139
|
+
for (let i = 0; i < minLength; i++) {
|
|
140
|
+
const a = toFileSegment(fileA, segmentsA, i);
|
|
141
|
+
const b = toFileSegment(fileB, segmentsB, i);
|
|
142
|
+
// folders are always shown before files
|
|
143
|
+
if (a.type !== b.type) {
|
|
144
|
+
return a.type === 'folder' ? -1 : 1;
|
|
145
|
+
}
|
|
146
|
+
const comparison = compareString(a.path, b.path);
|
|
147
|
+
// either folder name changed or last segments are compared
|
|
148
|
+
if (comparison !== 0 || a.isLast || b.isLast) {
|
|
149
|
+
return comparison;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return compareString(fileA.path, fileB.path);
|
|
153
|
+
}
|
|
154
|
+
function toFileSegment(file, segments, current) {
|
|
155
|
+
const isLast = current + 1 === segments.length;
|
|
156
|
+
return { path: segments[current], type: isLast ? file.type : 'folder', isLast };
|
|
157
|
+
}
|
|
158
|
+
function compareString(a, b) {
|
|
159
|
+
if (a < b) {
|
|
160
|
+
return -1;
|
|
161
|
+
}
|
|
162
|
+
if (a > b) {
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
function getFileExtension(filename) {
|
|
168
|
+
const parts = filename.split('.');
|
|
169
|
+
parts.shift();
|
|
170
|
+
const extension = parts.at(-1) || '';
|
|
171
|
+
return extension;
|
|
172
|
+
}
|
|
173
|
+
const extensionsToIcons = new Map([
|
|
174
|
+
['ts', 'i-ph-file-ts-duotone'],
|
|
175
|
+
['cts', 'i-ph-file-ts-duotone'],
|
|
176
|
+
['mts', 'i-ph-file-ts-duotone'],
|
|
177
|
+
['tsx', 'i-ph-file-tsx-duotone'],
|
|
178
|
+
['js', 'i-ph-file-js-duotone'],
|
|
179
|
+
['cjs', 'i-ph-file-js-duotone'],
|
|
180
|
+
['mjs', 'i-ph-file-js-duotone'],
|
|
181
|
+
['jsx', 'i-ph-file-jsx-duotone'],
|
|
182
|
+
['html', 'i-ph-file-html-duotone'],
|
|
183
|
+
['css', 'i-ph-file-css-duotone'],
|
|
184
|
+
['md', 'i-ph-file-md-duotone'],
|
|
185
|
+
['vue', 'i-ph-file-vue-duotone'],
|
|
186
|
+
['gif', 'i-ph-file-image-duotone'],
|
|
187
|
+
['jpg', 'i-ph-file-image-duotone'],
|
|
188
|
+
['jpeg', 'i-ph-file-image-duotone'],
|
|
189
|
+
['png', 'i-ph-file-image-duotone'],
|
|
190
|
+
['svg', 'i-ph-file-svg-duotone'],
|
|
191
|
+
['rb', 'i-phosphor-file-rb'],
|
|
192
|
+
['ru', 'i-phosphor-file-rb'],
|
|
193
|
+
['erb', 'i-phosphor-file-erb'],
|
|
194
|
+
]);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Terminal as XTerm } from '@xterm/xterm';
|
|
2
|
+
import '@xterm/xterm/css/xterm.css';
|
|
3
|
+
import { type ComponentProps } from 'react';
|
|
4
|
+
import '../../styles/terminal.css';
|
|
5
|
+
export interface TerminalRef {
|
|
6
|
+
reloadStyles: () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface TerminalProps extends ComponentProps<'div'> {
|
|
9
|
+
theme: 'dark' | 'light';
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
onTerminalReady?: (terminal: XTerm) => void;
|
|
12
|
+
onTerminalResize?: (cols: number, rows: number) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const Terminal: import("react").ForwardRefExoticComponent<Omit<TerminalProps, "ref"> & import("react").RefAttributes<TerminalRef>>;
|
|
15
|
+
export default Terminal;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { FitAddon } from '@xterm/addon-fit';
|
|
3
|
+
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
4
|
+
import { Terminal as XTerm } from '@xterm/xterm';
|
|
5
|
+
import '@xterm/xterm/css/xterm.css';
|
|
6
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
7
|
+
import '../../styles/terminal.css';
|
|
8
|
+
import { getTerminalTheme } from './theme.js';
|
|
9
|
+
export const Terminal = forwardRef(({ theme, readonly = true, onTerminalReady, onTerminalResize, ...props }, ref) => {
|
|
10
|
+
const divRef = useRef(null);
|
|
11
|
+
const terminalRef = useRef();
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const element = divRef.current;
|
|
14
|
+
const fitAddon = new FitAddon();
|
|
15
|
+
const webLinksAddon = new WebLinksAddon();
|
|
16
|
+
const terminal = new XTerm({
|
|
17
|
+
cursorBlink: true,
|
|
18
|
+
convertEol: true,
|
|
19
|
+
disableStdin: readonly,
|
|
20
|
+
theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}),
|
|
21
|
+
fontSize: 13,
|
|
22
|
+
fontFamily: 'Menlo, courier-new, courier, monospace',
|
|
23
|
+
});
|
|
24
|
+
terminalRef.current = terminal;
|
|
25
|
+
terminal.loadAddon(fitAddon);
|
|
26
|
+
terminal.loadAddon(webLinksAddon);
|
|
27
|
+
terminal.open(element);
|
|
28
|
+
fitAddon.fit();
|
|
29
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
30
|
+
fitAddon.fit();
|
|
31
|
+
onTerminalResize?.(terminal.cols, terminal.rows);
|
|
32
|
+
});
|
|
33
|
+
resizeObserver.observe(element);
|
|
34
|
+
onTerminalReady?.(terminal);
|
|
35
|
+
return () => {
|
|
36
|
+
resizeObserver.disconnect();
|
|
37
|
+
terminal.dispose();
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const terminal = terminalRef.current;
|
|
42
|
+
// we render a transparent cursor in case the terminal is readonly
|
|
43
|
+
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
|
|
44
|
+
terminal.options.disableStdin = readonly;
|
|
45
|
+
}, [theme, readonly]);
|
|
46
|
+
useImperativeHandle(ref, () => {
|
|
47
|
+
return {
|
|
48
|
+
reloadStyles: () => {
|
|
49
|
+
const terminal = terminalRef.current;
|
|
50
|
+
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
return _jsx("div", { ...props, ref: divRef });
|
|
55
|
+
});
|
|
56
|
+
Terminal.displayName = 'Terminal';
|
|
57
|
+
export default Terminal;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const style = getComputedStyle(document.documentElement);
|
|
2
|
+
const cssVar = (token) => style.getPropertyValue(token) || undefined;
|
|
3
|
+
export function getTerminalTheme(overrides) {
|
|
4
|
+
return {
|
|
5
|
+
cursor: cssVar('--tk-elements-terminal-cursorColor'),
|
|
6
|
+
cursorAccent: cssVar('--tk-elements-terminal-cursorColorAccent'),
|
|
7
|
+
foreground: cssVar('--tk-elements-terminal-textColor'),
|
|
8
|
+
background: cssVar('--tk-elements-terminal-backgroundColor'),
|
|
9
|
+
selectionBackground: cssVar('--tk-elements-terminal-selection-backgroundColor'),
|
|
10
|
+
selectionForeground: cssVar('--tk-elements-terminal-selection-textColor'),
|
|
11
|
+
selectionInactiveBackground: cssVar('--tk-elements-terminal-selection-backgroundColorInactive'),
|
|
12
|
+
// ansi escape code colors
|
|
13
|
+
black: cssVar('--tk-elements-terminal-color-black'),
|
|
14
|
+
red: cssVar('--tk-elements-terminal-color-red'),
|
|
15
|
+
green: cssVar('--tk-elements-terminal-color-green'),
|
|
16
|
+
yellow: cssVar('--tk-elements-terminal-color-yellow'),
|
|
17
|
+
blue: cssVar('--tk-elements-terminal-color-blue'),
|
|
18
|
+
magenta: cssVar('--tk-elements-terminal-color-magenta'),
|
|
19
|
+
cyan: cssVar('--tk-elements-terminal-color-cyan'),
|
|
20
|
+
white: cssVar('--tk-elements-terminal-color-white'),
|
|
21
|
+
brightBlack: cssVar('--tk-elements-terminal-color-brightBlack'),
|
|
22
|
+
brightRed: cssVar('--tk-elements-terminal-color-brightRed'),
|
|
23
|
+
brightGreen: cssVar('--tk-elements-terminal-color-brightGreen'),
|
|
24
|
+
brightYellow: cssVar('--tk-elements-terminal-color-brightYellow'),
|
|
25
|
+
brightBlue: cssVar('--tk-elements-terminal-color-brightBlue'),
|
|
26
|
+
brightMagenta: cssVar('--tk-elements-terminal-color-brightMagenta'),
|
|
27
|
+
brightCyan: cssVar('--tk-elements-terminal-color-brightCyan'),
|
|
28
|
+
brightWhite: cssVar('--tk-elements-terminal-color-brightWhite'),
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Theme = 'dark' | 'light';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { CodeMirrorEditor, type EditorDocument, type EditorUpdate, type OnChangeCallback, type OnScrollCallback, type ScrollPosition, } from './core/CodeMirrorEditor/index.js';
|
|
2
|
+
export { FileTree } from './core/FileTree.js';
|
|
3
|
+
export { Terminal, type TerminalRef, type TerminalProps } from './core/Terminal/index.js';
|
package/dist/core.js
ADDED