@mpeggroup/mpeg-sdl-editor 1.3.1
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/.github/workflows/check-bun-dependencies.yml +13 -0
- package/.github/workflows/lint-pr-message.yml +13 -0
- package/.github/workflows/release-bun-webapp.yml +15 -0
- package/.github/workflows/validate-bun-webapp-pr.yml +9 -0
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/build.ts +8 -0
- package/functional_tests/README.md +25 -0
- package/functional_tests/features/environment.py +50 -0
- package/functional_tests/features/steps/helper/selenium.py +15 -0
- package/functional_tests/features/steps/webapp.py +30 -0
- package/functional_tests/features/webapp.feature +5 -0
- package/functional_tests/pip-requirements.txt +2 -0
- package/html/favicon.svg +126 -0
- package/html/index.html +14 -0
- package/html/style.css +2 -0
- package/package.json +61 -0
- package/src/App.css +3 -0
- package/src/App.tsx +266 -0
- package/src/assets/mpeg.svg +143 -0
- package/src/codemirror/ruler.ts +58 -0
- package/src/components/DesktopPanels.tsx +65 -0
- package/src/components/Editor.tsx +384 -0
- package/src/components/MobileDrawer.tsx +57 -0
- package/src/components/Navbar.tsx +226 -0
- package/src/components/ResizableLayout.tsx +69 -0
- package/src/components/Settings.tsx +170 -0
- package/src/components/StatusBar.tsx +47 -0
- package/src/components/SubNav.tsx +58 -0
- package/src/hooks/useAutoDisplayCompletions.ts +26 -0
- package/src/hooks/useDragResize.ts +104 -0
- package/src/hooks/useEnableLinting.ts +21 -0
- package/src/hooks/useFileOperations.ts +102 -0
- package/src/hooks/useMobileDetection.ts +32 -0
- package/src/hooks/usePrettier.ts +40 -0
- package/src/hooks/useRulerWidth.ts +42 -0
- package/src/hooks/useShowSemanticWarnings.ts +23 -0
- package/src/hooks/useShowSymanticErrors.ts +23 -0
- package/src/hooks/useShowSyntaxErrors.ts +23 -0
- package/src/hooks/useTheme.ts +21 -0
- package/src/hooks/useToast.ts +37 -0
- package/src/logo/getLogoSvg.ts +5 -0
- package/src/logo/svg.module.d.ts +5 -0
- package/src/main.tsx +13 -0
- package/src/sdl/sdlComplete.ts +86 -0
- package/src/sdl/sdlLanguage.ts +18 -0
- package/src/sdl/sdlLinter.ts +157 -0
- package/tailwind.config.ts +16 -0
- package/tests/Editor_test.tsx +29 -0
- package/tests/happydom.ts +3 -0
- package/tests/sdlLanguage_test.ts +39 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { useDragResize } from "../hooks/useDragResize.ts";
|
|
3
|
+
import { MobileDrawer } from "./MobileDrawer.tsx";
|
|
4
|
+
import { DesktopPanels } from "./DesktopPanels.tsx";
|
|
5
|
+
import type { Theme } from "../hooks/useTheme.ts";
|
|
6
|
+
|
|
7
|
+
interface ResizableLayoutProps {
|
|
8
|
+
children: [React.ReactNode, React.ReactNode];
|
|
9
|
+
theme: Theme;
|
|
10
|
+
isSettingsShown: boolean;
|
|
11
|
+
isMobile: boolean;
|
|
12
|
+
onToggleSettings: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ResizableLayout({
|
|
16
|
+
children,
|
|
17
|
+
theme,
|
|
18
|
+
isSettingsShown,
|
|
19
|
+
isMobile,
|
|
20
|
+
onToggleSettings,
|
|
21
|
+
}: ResizableLayoutProps) {
|
|
22
|
+
const minLeftWidth = 400;
|
|
23
|
+
const minRightWidth = 300;
|
|
24
|
+
const [splitPercentage, setSplitPercentage] = useState(70);
|
|
25
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
|
|
27
|
+
const onSplitPercentageChange = useCallback((percentage: number) => {
|
|
28
|
+
setSplitPercentage(percentage);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const { isDragging, handleMouseDown } = useDragResize({
|
|
32
|
+
containerRef,
|
|
33
|
+
minLeftWidth,
|
|
34
|
+
minRightWidth,
|
|
35
|
+
isSettingsShown,
|
|
36
|
+
onSplitPercentageChange,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div ref={containerRef} className="flex h-full w-full relative">
|
|
41
|
+
{isMobile
|
|
42
|
+
? (
|
|
43
|
+
<>
|
|
44
|
+
<div className="flex flex-col w-full">
|
|
45
|
+
{children[0]}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<MobileDrawer
|
|
49
|
+
theme={theme}
|
|
50
|
+
isSettingsShown={isSettingsShown}
|
|
51
|
+
onToggleSettings={onToggleSettings}
|
|
52
|
+
>
|
|
53
|
+
{children[1]}
|
|
54
|
+
</MobileDrawer>
|
|
55
|
+
</>
|
|
56
|
+
)
|
|
57
|
+
: (
|
|
58
|
+
<DesktopPanels
|
|
59
|
+
leftChild={children[0]}
|
|
60
|
+
rightChild={children[1]}
|
|
61
|
+
splitPercentage={splitPercentage}
|
|
62
|
+
isSettingsShown={isSettingsShown}
|
|
63
|
+
isDragging={isDragging}
|
|
64
|
+
onMouseDown={handleMouseDown}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Theme } from "../hooks/useTheme.ts";
|
|
2
|
+
import { RULER_WIDTH_OPTIONS } from "../hooks/useRulerWidth.ts";
|
|
3
|
+
|
|
4
|
+
interface SettingsProps {
|
|
5
|
+
theme: Theme;
|
|
6
|
+
onToggleTheme: () => void;
|
|
7
|
+
rulerWidth: number;
|
|
8
|
+
onRulerWidthChange: (width: number) => void;
|
|
9
|
+
autoDisplayCompletions: boolean;
|
|
10
|
+
onAutoDisplayCompletionsChange: (value: boolean) => void;
|
|
11
|
+
enableLinting: boolean;
|
|
12
|
+
onEnableLintingChange: (value: boolean) => void;
|
|
13
|
+
showSyntaxErrors: boolean;
|
|
14
|
+
onShowSyntaxErrorsChange?: (value: boolean) => void;
|
|
15
|
+
showSemanticErrors: boolean;
|
|
16
|
+
onShowSemanticErrorsChange?: (value: boolean) => void;
|
|
17
|
+
showSemanticWarnings: boolean;
|
|
18
|
+
onShowSemanticWarningsChange?: (value: boolean) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Settings(
|
|
22
|
+
{
|
|
23
|
+
theme,
|
|
24
|
+
onToggleTheme,
|
|
25
|
+
rulerWidth,
|
|
26
|
+
onRulerWidthChange,
|
|
27
|
+
autoDisplayCompletions,
|
|
28
|
+
onAutoDisplayCompletionsChange,
|
|
29
|
+
enableLinting,
|
|
30
|
+
onEnableLintingChange,
|
|
31
|
+
showSyntaxErrors,
|
|
32
|
+
onShowSyntaxErrorsChange,
|
|
33
|
+
showSemanticErrors,
|
|
34
|
+
onShowSemanticErrorsChange,
|
|
35
|
+
showSemanticWarnings,
|
|
36
|
+
onShowSemanticWarningsChange,
|
|
37
|
+
}: SettingsProps,
|
|
38
|
+
) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="h-full bg-base-100 md:p-4 overflow-auto">
|
|
41
|
+
<h2 className="font-bold pb-4">
|
|
42
|
+
Settings
|
|
43
|
+
</h2>
|
|
44
|
+
<div className="flex flex-col gap-3">
|
|
45
|
+
<div className="flex items-center gap-3">
|
|
46
|
+
<span className="text-sm whitespace-nowrap">Theme</span>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={onToggleTheme}
|
|
50
|
+
title={theme === "dark" ? "Light theme" : "Dark theme"}
|
|
51
|
+
className="btn btn-sm border border-base-content/30 rounded-lg bg-transparent hover:bg-base-content/10 hover:border-base-content/30 [&_svg]:w-5 [&_svg]:h-5"
|
|
52
|
+
>
|
|
53
|
+
{theme === "dark"
|
|
54
|
+
? (
|
|
55
|
+
<svg
|
|
56
|
+
className="w-5 h-5 fill-current"
|
|
57
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
58
|
+
viewBox="0 0 24 24"
|
|
59
|
+
>
|
|
60
|
+
<path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
|
|
61
|
+
</svg>
|
|
62
|
+
)
|
|
63
|
+
: (
|
|
64
|
+
<svg
|
|
65
|
+
className="w-5 h-5 fill-current"
|
|
66
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
67
|
+
viewBox="0 0 24 24"
|
|
68
|
+
>
|
|
69
|
+
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
|
|
70
|
+
</svg>
|
|
71
|
+
)}
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="flex items-center gap-3">
|
|
75
|
+
<span className="text-sm whitespace-nowrap">Prettier Text Width</span>
|
|
76
|
+
<select
|
|
77
|
+
className="select select-sm border border-base-content/30 rounded-lg bg-transparent w-auto"
|
|
78
|
+
value={rulerWidth}
|
|
79
|
+
onChange={(e) => onRulerWidthChange(Number(e.target.value))}
|
|
80
|
+
>
|
|
81
|
+
{RULER_WIDTH_OPTIONS.map((width) => (
|
|
82
|
+
<option key={width} value={width}>
|
|
83
|
+
{width}
|
|
84
|
+
</option>
|
|
85
|
+
))}
|
|
86
|
+
</select>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="flex items-center gap-3">
|
|
89
|
+
<span className="text-sm whitespace-nowrap">
|
|
90
|
+
Auto-Display Completions
|
|
91
|
+
</span>
|
|
92
|
+
<input
|
|
93
|
+
type="checkbox"
|
|
94
|
+
className="checkbox checkbox-sm border-base-content/30"
|
|
95
|
+
checked={autoDisplayCompletions}
|
|
96
|
+
onChange={(e) => onAutoDisplayCompletionsChange(e.target.checked)}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
<div className="flex flex-wrap items-start gap-x-3 gap-y-2">
|
|
100
|
+
<div className="flex items-center gap-3">
|
|
101
|
+
<span className="text-sm whitespace-nowrap">Enable Linting</span>
|
|
102
|
+
<input
|
|
103
|
+
type="checkbox"
|
|
104
|
+
className="checkbox checkbox-sm border-base-content/30"
|
|
105
|
+
checked={enableLinting}
|
|
106
|
+
onChange={(e) => onEnableLintingChange(e.target.checked)}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
{enableLinting && (
|
|
110
|
+
<div className="p-2 border border-base-content/30 rounded-sm">
|
|
111
|
+
<div className="space-y-2">
|
|
112
|
+
<div className="flex items-center gap-2">
|
|
113
|
+
<input
|
|
114
|
+
type="checkbox"
|
|
115
|
+
className="checkbox checkbox-xs disabled:opacity-50 disabled:cursor-not-allowed"
|
|
116
|
+
checked={showSyntaxErrors}
|
|
117
|
+
disabled={!enableLinting}
|
|
118
|
+
onChange={(e) =>
|
|
119
|
+
onShowSyntaxErrorsChange?.(e.target.checked)}
|
|
120
|
+
/>
|
|
121
|
+
<span
|
|
122
|
+
className={`text-xs text-left whitespace-nowrap ${
|
|
123
|
+
!enableLinting ? "text-base-content/50" : ""
|
|
124
|
+
}`}
|
|
125
|
+
>
|
|
126
|
+
Syntax Errors
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="flex items-center gap-2">
|
|
130
|
+
<input
|
|
131
|
+
type="checkbox"
|
|
132
|
+
className="checkbox checkbox-xs disabled:opacity-50 disabled:cursor-not-allowed"
|
|
133
|
+
checked={showSemanticErrors}
|
|
134
|
+
disabled={!enableLinting}
|
|
135
|
+
onChange={(e) =>
|
|
136
|
+
onShowSemanticErrorsChange?.(e.target.checked)}
|
|
137
|
+
/>
|
|
138
|
+
<span
|
|
139
|
+
className={`text-xs text-left whitespace-nowrap ${
|
|
140
|
+
!enableLinting ? "text-base-content/50" : ""
|
|
141
|
+
}`}
|
|
142
|
+
>
|
|
143
|
+
Semantic Errors
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
<input
|
|
148
|
+
type="checkbox"
|
|
149
|
+
className="checkbox checkbox-xs disabled:opacity-50 disabled:cursor-not-allowed"
|
|
150
|
+
checked={showSemanticWarnings}
|
|
151
|
+
disabled={!enableLinting}
|
|
152
|
+
onChange={(e) =>
|
|
153
|
+
onShowSemanticWarningsChange?.(e.target.checked)}
|
|
154
|
+
/>
|
|
155
|
+
<span
|
|
156
|
+
className={`text-xs text-left whitespace-nowrap ${
|
|
157
|
+
!enableLinting ? "text-base-content/50" : ""
|
|
158
|
+
}`}
|
|
159
|
+
>
|
|
160
|
+
Semantic Warnings
|
|
161
|
+
</span>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface StatusBarProps {
|
|
2
|
+
lineCount: number;
|
|
3
|
+
characterCount: number;
|
|
4
|
+
syntaxErrorCount: number;
|
|
5
|
+
semanticErrorCount: number;
|
|
6
|
+
semanticWarningCount: number;
|
|
7
|
+
cursorPosition: { line: number; col: number };
|
|
8
|
+
enableLinting: boolean;
|
|
9
|
+
showSyntaxErrors: boolean;
|
|
10
|
+
showSemanticErrors: boolean;
|
|
11
|
+
showSemanticWarnings: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function StatusBar({
|
|
15
|
+
lineCount,
|
|
16
|
+
characterCount,
|
|
17
|
+
syntaxErrorCount,
|
|
18
|
+
semanticErrorCount,
|
|
19
|
+
semanticWarningCount,
|
|
20
|
+
cursorPosition,
|
|
21
|
+
enableLinting,
|
|
22
|
+
showSyntaxErrors,
|
|
23
|
+
showSemanticErrors,
|
|
24
|
+
showSemanticWarnings,
|
|
25
|
+
}: StatusBarProps) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="h-6 bg-base-200 border-t border-base-300 px-3 flex items-center justify-between text-xs text-base-content/70">
|
|
28
|
+
<div className="flex items-center space-x-4">
|
|
29
|
+
<span>Lines: {lineCount}</span>
|
|
30
|
+
<span>Characters: {characterCount}</span>
|
|
31
|
+
{enableLinting && showSyntaxErrors && (
|
|
32
|
+
<span>Syntax Errors: {syntaxErrorCount}</span>
|
|
33
|
+
)}
|
|
34
|
+
{enableLinting && showSemanticErrors && (
|
|
35
|
+
<span>Semantic Errors: {semanticErrorCount}</span>
|
|
36
|
+
)}
|
|
37
|
+
{enableLinting && showSemanticWarnings && (
|
|
38
|
+
<span>Semantic Warnings: {semanticWarningCount}</span>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
<div className="flex items-center space-x-4">
|
|
42
|
+
<span>Row: {cursorPosition.line}</span>
|
|
43
|
+
<span>Column: {cursorPosition.col}</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Theme } from "../hooks/useTheme.ts";
|
|
2
|
+
|
|
3
|
+
interface SubNavProps {
|
|
4
|
+
theme: Theme;
|
|
5
|
+
onCollapseAll: () => void;
|
|
6
|
+
onExpandAll: () => void;
|
|
7
|
+
onPrettify: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function SubNav({
|
|
11
|
+
theme,
|
|
12
|
+
onCollapseAll,
|
|
13
|
+
onExpandAll,
|
|
14
|
+
onPrettify,
|
|
15
|
+
}: SubNavProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={`border-b border-base-300 px-2 py-1.5 flex items-center justify-between text-xs min-h-8 ${
|
|
19
|
+
theme === "dark" ? "bg-base-100/30 border-gray-600" : "bg-base-300/80"
|
|
20
|
+
}`}
|
|
21
|
+
>
|
|
22
|
+
<div className="flex items-center space-x-4">
|
|
23
|
+
<div className="flex items-center">
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
onClick={onCollapseAll}
|
|
27
|
+
title="Collapse All"
|
|
28
|
+
className="btn btn-ghost btn-xs px-1 py-0.5 text-xs hover:bg-base-300"
|
|
29
|
+
>
|
|
30
|
+
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
|
31
|
+
<path d="M8 6a.75.75 0 00.55-.24l3.25-3.5a.75.75 0 10-1.1-1.02L8 4.148 5.3 1.24a.75.75 0 00-1.1 1.02l3.25 3.5A.75.75 0 008 6zM8 10a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L8 11.852 5.3 14.76a.75.75 0 11-1.1-1.02l3.25-3.5A.75.75 0 018 10zM1.75 7.124a.626.626 0 100 1.253h.5a.627.627 0 000-1.253h-.5zM4.75 7.124a.626.626 0 100 1.253h.5a.627.627 0 000-1.253h-.5zM7.75 7.124a.626.626 0 100 1.253h.5a.627.627 0 000-1.253h-.5zM10.75 7.124a.627.627 0 000 1.253h.5a.627.627 0 000-1.253h-.5zM13.75 7.124a.627.627 0 000 1.253h.5a.627.627 0 000-1.253h-.5z" />
|
|
32
|
+
</svg>
|
|
33
|
+
</button>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={onExpandAll}
|
|
37
|
+
title="Expand All"
|
|
38
|
+
className="btn btn-ghost btn-xs px-1 py-0.5 text-xs hover:bg-base-300"
|
|
39
|
+
>
|
|
40
|
+
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
|
41
|
+
<path d="M8 1a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L8 2.852 5.3 5.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 018 1zM8 15a.75.75 0 00.55-.24l3.25-3.5a.75.75 0 10-1.1-1.02L8 13.148 5.3 10.24a.75.75 0 10-1.1 1.02l3.25 3.5A.75.75 0 008 15zM1.75 7.124a.626.626 0 100 1.253h.5a.627.627 0 000-1.253h-.5zM4.75 7.124a.626.626 0 100 1.253h.5a.627.627 0 000-1.253h-.5zM7.75 7.124a.626.626 0 100 1.253h.5a.627.627 0 000-1.253h-.5zM10.75 7.124a.627.627 0 000 1.253h.5a.627.627 0 000-1.253h-.5zM13.75 7.124a.627.627 0 000 1.253h.5a.627.627 0 000-1.253h-.5z" />
|
|
42
|
+
</svg>
|
|
43
|
+
</button>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={onPrettify}
|
|
47
|
+
title="Prettify"
|
|
48
|
+
className="btn btn-ghost btn-xs px-1 py-0.5 text-xs hover:bg-base-300"
|
|
49
|
+
>
|
|
50
|
+
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
|
51
|
+
<path d="M3.707 1.815 C 3.450 1.931,3.282 2.167,3.260 2.442 C 3.235 2.750,3.376 3.005,3.657 3.161 L 3.800 3.240 6.000 3.240 L 8.200 3.240 8.337 3.168 C 8.614 3.022,8.765 2.750,8.740 2.442 C 8.718 2.167,8.550 1.931,8.293 1.815 C 8.176 1.762,8.128 1.761,6.000 1.761 C 3.872 1.761,3.824 1.762,3.707 1.815 M10.740 1.798 C 10.429 1.909,10.208 2.274,10.257 2.595 C 10.300 2.873,10.520 3.136,10.775 3.211 C 10.965 3.268,11.538 3.269,11.726 3.213 C 11.907 3.159,12.108 2.978,12.186 2.796 C 12.259 2.625,12.254 2.340,12.173 2.173 C 12.099 2.021,11.915 1.852,11.769 1.803 C 11.607 1.747,10.891 1.744,10.740 1.798 M3.707 5.309 C 3.110 5.578,3.102 6.402,3.693 6.683 C 3.826 6.746,3.834 6.746,4.749 6.746 L 5.671 6.747 5.835 6.665 C 6.106 6.530,6.247 6.302,6.247 6.000 C 6.247 5.698,6.106 5.470,5.835 5.335 L 5.671 5.253 4.749 5.254 C 3.880 5.254,3.820 5.258,3.707 5.309 M8.293 5.278 C 7.978 5.374,7.761 5.668,7.761 6.000 C 7.761 6.395,8.050 6.709,8.442 6.740 C 8.751 6.765,9.006 6.624,9.160 6.343 C 9.227 6.221,9.238 6.171,9.238 6.000 C 9.238 5.829,9.227 5.779,9.160 5.657 C 9.065 5.483,8.915 5.350,8.752 5.295 C 8.625 5.251,8.407 5.244,8.293 5.278 M11.237 5.296 C 11.083 5.351,10.917 5.502,10.833 5.663 C 10.774 5.776,10.761 5.835,10.761 6.000 C 10.761 6.242,10.841 6.420,11.016 6.569 C 11.224 6.748,11.243 6.750,12.550 6.741 C 13.692 6.734,13.723 6.732,13.827 6.676 C 14.085 6.536,14.239 6.284,14.239 6.000 C 14.239 5.716,14.085 5.464,13.827 5.324 C 13.722 5.268,13.695 5.267,12.533 5.262 C 11.544 5.257,11.328 5.263,11.237 5.296 M3.779 8.785 C 3.426 8.911,3.228 9.211,3.260 9.569 C 3.278 9.763,3.349 9.905,3.499 10.044 C 3.706 10.235,3.761 10.245,4.548 10.235 L 5.240 10.227 5.364 10.154 C 5.700 9.956,5.841 9.532,5.681 9.195 C 5.597 9.017,5.399 8.840,5.225 8.789 C 5.036 8.732,3.934 8.730,3.779 8.785 M7.779 8.785 C 7.426 8.911,7.228 9.211,7.260 9.569 C 7.284 9.836,7.454 10.071,7.707 10.185 C 7.824 10.237,7.874 10.239,9.735 10.239 C 11.881 10.240,11.799 10.247,12.007 10.044 C 12.163 9.893,12.222 9.758,12.235 9.531 C 12.249 9.283,12.178 9.112,11.994 8.946 C 11.765 8.740,11.835 8.747,9.735 8.748 C 8.245 8.749,7.860 8.756,7.779 8.785 M3.808 12.277 C 3.120 12.466,3.065 13.402,3.724 13.700 C 3.817 13.742,3.956 13.747,5.253 13.747 L 6.680 13.747 6.805 13.684 C 7.062 13.557,7.253 13.265,7.253 13.000 C 7.253 12.735,7.062 12.443,6.805 12.316 L 6.680 12.253 5.307 12.248 C 4.252 12.244,3.904 12.250,3.808 12.277" />
|
|
52
|
+
</svg>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useAutoDisplayCompletions(defaultValue: boolean = true) {
|
|
4
|
+
const [autoDisplayCompletions, setAutoDisplayCompletionsInternal] = useState<
|
|
5
|
+
boolean
|
|
6
|
+
>(() => {
|
|
7
|
+
const stored = localStorage.getItem("autoDisplayCompletions");
|
|
8
|
+
if (stored !== null) {
|
|
9
|
+
return stored === "true";
|
|
10
|
+
}
|
|
11
|
+
return defaultValue;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
localStorage.setItem(
|
|
16
|
+
"autoDisplayCompletions",
|
|
17
|
+
String(autoDisplayCompletions),
|
|
18
|
+
);
|
|
19
|
+
}, [autoDisplayCompletions]);
|
|
20
|
+
|
|
21
|
+
const setAutoDisplayCompletions = useCallback((value: boolean) => {
|
|
22
|
+
setAutoDisplayCompletionsInternal(value);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
return { autoDisplayCompletions, setAutoDisplayCompletions };
|
|
26
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface UseDragResizeProps {
|
|
4
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
5
|
+
minLeftWidth: number;
|
|
6
|
+
minRightWidth: number;
|
|
7
|
+
isSettingsShown: boolean;
|
|
8
|
+
onSplitPercentageChange: (percentage: number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useDragResize({
|
|
12
|
+
containerRef,
|
|
13
|
+
minLeftWidth,
|
|
14
|
+
minRightWidth,
|
|
15
|
+
isSettingsShown,
|
|
16
|
+
onSplitPercentageChange,
|
|
17
|
+
}: UseDragResizeProps) {
|
|
18
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
19
|
+
const [dragStartX, setDragStartX] = useState(0);
|
|
20
|
+
const [dragStartPercentage, setDragStartPercentage] = useState(0);
|
|
21
|
+
|
|
22
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
23
|
+
if (!isSettingsShown) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
|
|
29
|
+
const container = containerRef.current;
|
|
30
|
+
|
|
31
|
+
if (!container) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const containerRect = container.getBoundingClientRect();
|
|
36
|
+
const mouseX = e.clientX - containerRect.left;
|
|
37
|
+
const currentPercentage = (mouseX / containerRect.width) * 100;
|
|
38
|
+
|
|
39
|
+
setDragStartX(e.clientX);
|
|
40
|
+
setDragStartPercentage(currentPercentage);
|
|
41
|
+
setIsDragging(true);
|
|
42
|
+
}, [isSettingsShown, containerRef]);
|
|
43
|
+
|
|
44
|
+
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
45
|
+
if (!isDragging || !containerRef.current || !isSettingsShown) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const container = containerRef.current;
|
|
50
|
+
const containerRect = container.getBoundingClientRect();
|
|
51
|
+
const containerWidth = containerRect.width;
|
|
52
|
+
|
|
53
|
+
// Calculate the delta from where the drag started
|
|
54
|
+
const deltaX = e.clientX - dragStartX;
|
|
55
|
+
const deltaPercentage = (deltaX / containerWidth) * 100;
|
|
56
|
+
|
|
57
|
+
// Calculate new percentage based on drag start position + delta
|
|
58
|
+
let newPercentage = dragStartPercentage + deltaPercentage;
|
|
59
|
+
|
|
60
|
+
// Apply minimum width constraints
|
|
61
|
+
const minLeftPercentage = (minLeftWidth / containerWidth) * 100;
|
|
62
|
+
const minRightPercentage = (minRightWidth / containerWidth) * 100;
|
|
63
|
+
|
|
64
|
+
// Clamp the percentage to valid bounds
|
|
65
|
+
newPercentage = Math.max(minLeftPercentage, newPercentage);
|
|
66
|
+
newPercentage = Math.min(100 - minRightPercentage, newPercentage);
|
|
67
|
+
|
|
68
|
+
onSplitPercentageChange(newPercentage);
|
|
69
|
+
}, [
|
|
70
|
+
isDragging,
|
|
71
|
+
dragStartX,
|
|
72
|
+
dragStartPercentage,
|
|
73
|
+
minLeftWidth,
|
|
74
|
+
minRightWidth,
|
|
75
|
+
isSettingsShown,
|
|
76
|
+
onSplitPercentageChange,
|
|
77
|
+
containerRef,
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const handleMouseUp = useCallback(() => {
|
|
81
|
+
setIsDragging(false);
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (isDragging && isSettingsShown) {
|
|
86
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
87
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
88
|
+
document.body.style.cursor = "col-resize";
|
|
89
|
+
document.body.style.userSelect = "none";
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
93
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
94
|
+
document.body.style.cursor = "";
|
|
95
|
+
document.body.style.userSelect = "";
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}, [isDragging, handleMouseMove, handleMouseUp, isSettingsShown]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
isDragging,
|
|
102
|
+
handleMouseDown,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useEnableLinting(defaultValue: boolean = true) {
|
|
4
|
+
const [enableLinting, setEnableLintingInternal] = useState<boolean>(() => {
|
|
5
|
+
const stored = localStorage.getItem("enableLinting");
|
|
6
|
+
if (stored !== null) {
|
|
7
|
+
return stored === "true";
|
|
8
|
+
}
|
|
9
|
+
return defaultValue;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
localStorage.setItem("enableLinting", String(enableLinting));
|
|
14
|
+
}, [enableLinting]);
|
|
15
|
+
|
|
16
|
+
const setEnableLinting = useCallback((value: boolean) => {
|
|
17
|
+
setEnableLintingInternal(value);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return { enableLinting, setEnableLinting };
|
|
21
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { ShowToastFunction } from "./useToast.ts";
|
|
3
|
+
import type { EditorRef } from "../components/Editor.tsx";
|
|
4
|
+
|
|
5
|
+
interface UseFileOperationsProps {
|
|
6
|
+
code: string;
|
|
7
|
+
setCode: (code: string) => void;
|
|
8
|
+
showToast: ShowToastFunction;
|
|
9
|
+
editorRef: React.RefObject<EditorRef | null>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useFileOperations(
|
|
13
|
+
{ code, setCode, showToast, editorRef }: UseFileOperationsProps,
|
|
14
|
+
) {
|
|
15
|
+
const handleSave = useCallback(async () => {
|
|
16
|
+
try {
|
|
17
|
+
if (globalThis.window.showSaveFilePicker) {
|
|
18
|
+
const fileHandle = await globalThis.window.showSaveFilePicker({
|
|
19
|
+
suggestedName: "specification.sdl",
|
|
20
|
+
types: [
|
|
21
|
+
{
|
|
22
|
+
description: "SDL Files",
|
|
23
|
+
accept: {
|
|
24
|
+
"text/plain": [".sdl"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
const writableStream = await fileHandle.createWritable();
|
|
30
|
+
await writableStream.write(code);
|
|
31
|
+
await writableStream.close();
|
|
32
|
+
} else {
|
|
33
|
+
const blob = new Blob([code], { type: "text/plain" });
|
|
34
|
+
const url = URL.createObjectURL(blob);
|
|
35
|
+
const a = document.createElement("a");
|
|
36
|
+
a.href = url;
|
|
37
|
+
a.download = "specification.sdl";
|
|
38
|
+
document.body.appendChild(a);
|
|
39
|
+
a.click();
|
|
40
|
+
document.body.removeChild(a);
|
|
41
|
+
URL.revokeObjectURL(url);
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
45
|
+
console.error("Error saving file:", err);
|
|
46
|
+
showToast("Error saving file. See console.", "error");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}, [code, showToast]);
|
|
50
|
+
|
|
51
|
+
const handleLoad = useCallback(
|
|
52
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
53
|
+
const file = event.target.files?.[0];
|
|
54
|
+
if (file) {
|
|
55
|
+
const reader = new FileReader();
|
|
56
|
+
reader.onload = (e) => {
|
|
57
|
+
const text = e.target?.result;
|
|
58
|
+
if (typeof text === "string") {
|
|
59
|
+
setCode(text);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
reader.onerror = () => {
|
|
63
|
+
showToast("Error loading file.", "error");
|
|
64
|
+
};
|
|
65
|
+
reader.readAsText(file);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
[showToast],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const handleCopy = useCallback(async () => {
|
|
72
|
+
try {
|
|
73
|
+
const clipboardItems: Record<string, Blob> = {
|
|
74
|
+
"text/plain": new Blob([code], { type: "text/plain" }),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (editorRef.current?.getStyledCode) {
|
|
78
|
+
const styledCode = editorRef.current.getStyledCode();
|
|
79
|
+
|
|
80
|
+
clipboardItems["text/html"] = new Blob([styledCode], {
|
|
81
|
+
type: "text/html",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const clipboardItem = new ClipboardItem(clipboardItems);
|
|
86
|
+
|
|
87
|
+
await navigator.clipboard.write([clipboardItem]);
|
|
88
|
+
showToast("Copied!");
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error("Failed to copy content: ", err);
|
|
91
|
+
showToast("Failed to copy content. See console.", "error");
|
|
92
|
+
}
|
|
93
|
+
}, [code, showToast]);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
code,
|
|
97
|
+
setCode,
|
|
98
|
+
handleSave,
|
|
99
|
+
handleLoad,
|
|
100
|
+
handleCopy,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useMobileDetection(breakpoint: number = 768) {
|
|
4
|
+
const [isMobile, setIsMobile] = useState(() => {
|
|
5
|
+
// Only check on client-side, return false for SSR
|
|
6
|
+
if (
|
|
7
|
+
typeof globalThis !== "undefined" && globalThis.innerWidth !== undefined
|
|
8
|
+
) {
|
|
9
|
+
return globalThis.innerWidth < breakpoint;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
// Only run on client-side
|
|
16
|
+
if (
|
|
17
|
+
typeof globalThis === "undefined" || globalThis.innerWidth === undefined
|
|
18
|
+
) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const checkScreenSize = () => {
|
|
23
|
+
setIsMobile(globalThis.innerWidth < breakpoint);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
checkScreenSize();
|
|
27
|
+
globalThis.addEventListener("resize", checkScreenSize);
|
|
28
|
+
return () => globalThis.removeEventListener("resize", checkScreenSize);
|
|
29
|
+
}, [breakpoint]);
|
|
30
|
+
|
|
31
|
+
return isMobile;
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import prettier from "prettier";
|
|
3
|
+
import { prettierPluginSdl } from "@mpeggroup/mpeg-sdl-parser";
|
|
4
|
+
import type { ShowToastFunction } from "./useToast.ts";
|
|
5
|
+
|
|
6
|
+
interface UsePrettierProps {
|
|
7
|
+
code: string;
|
|
8
|
+
setCode: (code: string) => void;
|
|
9
|
+
showToast: ShowToastFunction;
|
|
10
|
+
rulerWidth: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function usePrettier(
|
|
14
|
+
{ code, setCode, showToast, rulerWidth }: UsePrettierProps,
|
|
15
|
+
) {
|
|
16
|
+
const prettifyInProgressRef = useRef(false);
|
|
17
|
+
|
|
18
|
+
const handlePrettify = useCallback(async () => {
|
|
19
|
+
const options: prettier.Options = {
|
|
20
|
+
parser: "sdl",
|
|
21
|
+
plugins: [prettierPluginSdl],
|
|
22
|
+
printWidth: rulerWidth,
|
|
23
|
+
};
|
|
24
|
+
try {
|
|
25
|
+
prettifyInProgressRef.current = true;
|
|
26
|
+
|
|
27
|
+
const formattedCode = await prettier.format(code, options);
|
|
28
|
+
|
|
29
|
+
setCode(formattedCode);
|
|
30
|
+
|
|
31
|
+
prettifyInProgressRef.current = false;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
prettifyInProgressRef.current = false;
|
|
34
|
+
console.error("Error prettifying code:", error);
|
|
35
|
+
showToast("Error prettifying SDL. See console.", "error");
|
|
36
|
+
}
|
|
37
|
+
}, [code, setCode, showToast, rulerWidth]);
|
|
38
|
+
|
|
39
|
+
return { handlePrettify };
|
|
40
|
+
}
|