@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.
Files changed (52) hide show
  1. package/.github/workflows/check-bun-dependencies.yml +13 -0
  2. package/.github/workflows/lint-pr-message.yml +13 -0
  3. package/.github/workflows/release-bun-webapp.yml +15 -0
  4. package/.github/workflows/validate-bun-webapp-pr.yml +9 -0
  5. package/LICENSE +21 -0
  6. package/README.md +59 -0
  7. package/build.ts +8 -0
  8. package/functional_tests/README.md +25 -0
  9. package/functional_tests/features/environment.py +50 -0
  10. package/functional_tests/features/steps/helper/selenium.py +15 -0
  11. package/functional_tests/features/steps/webapp.py +30 -0
  12. package/functional_tests/features/webapp.feature +5 -0
  13. package/functional_tests/pip-requirements.txt +2 -0
  14. package/html/favicon.svg +126 -0
  15. package/html/index.html +14 -0
  16. package/html/style.css +2 -0
  17. package/package.json +61 -0
  18. package/src/App.css +3 -0
  19. package/src/App.tsx +266 -0
  20. package/src/assets/mpeg.svg +143 -0
  21. package/src/codemirror/ruler.ts +58 -0
  22. package/src/components/DesktopPanels.tsx +65 -0
  23. package/src/components/Editor.tsx +384 -0
  24. package/src/components/MobileDrawer.tsx +57 -0
  25. package/src/components/Navbar.tsx +226 -0
  26. package/src/components/ResizableLayout.tsx +69 -0
  27. package/src/components/Settings.tsx +170 -0
  28. package/src/components/StatusBar.tsx +47 -0
  29. package/src/components/SubNav.tsx +58 -0
  30. package/src/hooks/useAutoDisplayCompletions.ts +26 -0
  31. package/src/hooks/useDragResize.ts +104 -0
  32. package/src/hooks/useEnableLinting.ts +21 -0
  33. package/src/hooks/useFileOperations.ts +102 -0
  34. package/src/hooks/useMobileDetection.ts +32 -0
  35. package/src/hooks/usePrettier.ts +40 -0
  36. package/src/hooks/useRulerWidth.ts +42 -0
  37. package/src/hooks/useShowSemanticWarnings.ts +23 -0
  38. package/src/hooks/useShowSymanticErrors.ts +23 -0
  39. package/src/hooks/useShowSyntaxErrors.ts +23 -0
  40. package/src/hooks/useTheme.ts +21 -0
  41. package/src/hooks/useToast.ts +37 -0
  42. package/src/logo/getLogoSvg.ts +5 -0
  43. package/src/logo/svg.module.d.ts +5 -0
  44. package/src/main.tsx +13 -0
  45. package/src/sdl/sdlComplete.ts +86 -0
  46. package/src/sdl/sdlLanguage.ts +18 -0
  47. package/src/sdl/sdlLinter.ts +157 -0
  48. package/tailwind.config.ts +16 -0
  49. package/tests/Editor_test.tsx +29 -0
  50. package/tests/happydom.ts +3 -0
  51. package/tests/sdlLanguage_test.ts +39 -0
  52. 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
+ }