@stevejtrettel/shader-sandbox 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/README.md +391 -0
- package/bin/cli.js +389 -0
- package/dist-lib/app/App.d.ts +134 -0
- package/dist-lib/app/App.d.ts.map +1 -0
- package/dist-lib/app/App.js +570 -0
- package/dist-lib/app/types.d.ts +32 -0
- package/dist-lib/app/types.d.ts.map +1 -0
- package/dist-lib/app/types.js +6 -0
- package/dist-lib/editor/EditorPanel.d.ts +39 -0
- package/dist-lib/editor/EditorPanel.d.ts.map +1 -0
- package/dist-lib/editor/EditorPanel.js +274 -0
- package/dist-lib/editor/prism-editor.css +99 -0
- package/dist-lib/editor/prism-editor.d.ts +19 -0
- package/dist-lib/editor/prism-editor.d.ts.map +1 -0
- package/dist-lib/editor/prism-editor.js +96 -0
- package/dist-lib/embed.d.ts +17 -0
- package/dist-lib/embed.d.ts.map +1 -0
- package/dist-lib/embed.js +35 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts +160 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts.map +1 -0
- package/dist-lib/engine/ShadertoyEngine.js +704 -0
- package/dist-lib/engine/glHelpers.d.ts +79 -0
- package/dist-lib/engine/glHelpers.d.ts.map +1 -0
- package/dist-lib/engine/glHelpers.js +298 -0
- package/dist-lib/engine/types.d.ts +77 -0
- package/dist-lib/engine/types.d.ts.map +1 -0
- package/dist-lib/engine/types.js +7 -0
- package/dist-lib/index.d.ts +12 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +9 -0
- package/dist-lib/layouts/DefaultLayout.d.ts +17 -0
- package/dist-lib/layouts/DefaultLayout.d.ts.map +1 -0
- package/dist-lib/layouts/DefaultLayout.js +27 -0
- package/dist-lib/layouts/FullscreenLayout.d.ts +17 -0
- package/dist-lib/layouts/FullscreenLayout.d.ts.map +1 -0
- package/dist-lib/layouts/FullscreenLayout.js +27 -0
- package/dist-lib/layouts/SplitLayout.d.ts +26 -0
- package/dist-lib/layouts/SplitLayout.d.ts.map +1 -0
- package/dist-lib/layouts/SplitLayout.js +61 -0
- package/dist-lib/layouts/TabbedLayout.d.ts +38 -0
- package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -0
- package/dist-lib/layouts/TabbedLayout.js +305 -0
- package/dist-lib/layouts/index.d.ts +24 -0
- package/dist-lib/layouts/index.d.ts.map +1 -0
- package/dist-lib/layouts/index.js +36 -0
- package/dist-lib/layouts/split.css +196 -0
- package/dist-lib/layouts/tabbed.css +345 -0
- package/dist-lib/layouts/types.d.ts +48 -0
- package/dist-lib/layouts/types.d.ts.map +1 -0
- package/dist-lib/layouts/types.js +4 -0
- package/dist-lib/main.d.ts +15 -0
- package/dist-lib/main.d.ts.map +1 -0
- package/dist-lib/main.js +102 -0
- package/dist-lib/project/generatedLoader.d.ts +3 -0
- package/dist-lib/project/generatedLoader.d.ts.map +1 -0
- package/dist-lib/project/generatedLoader.js +17 -0
- package/dist-lib/project/loadProject.d.ts +22 -0
- package/dist-lib/project/loadProject.d.ts.map +1 -0
- package/dist-lib/project/loadProject.js +350 -0
- package/dist-lib/project/loaderHelper.d.ts +7 -0
- package/dist-lib/project/loaderHelper.d.ts.map +1 -0
- package/dist-lib/project/loaderHelper.js +240 -0
- package/dist-lib/project/types.d.ts +192 -0
- package/dist-lib/project/types.d.ts.map +1 -0
- package/dist-lib/project/types.js +7 -0
- package/dist-lib/styles/base.css +29 -0
- package/package.json +48 -0
- package/src/app/App.ts +699 -0
- package/src/app/app.css +208 -0
- package/src/app/types.ts +36 -0
- package/src/editor/EditorPanel.ts +340 -0
- package/src/editor/editor-panel.css +175 -0
- package/src/editor/prism-editor.css +99 -0
- package/src/editor/prism-editor.ts +124 -0
- package/src/embed.ts +55 -0
- package/src/engine/ShadertoyEngine.ts +929 -0
- package/src/engine/glHelpers.ts +432 -0
- package/src/engine/types.ts +118 -0
- package/src/index.ts +13 -0
- package/src/layouts/DefaultLayout.ts +40 -0
- package/src/layouts/FullscreenLayout.ts +40 -0
- package/src/layouts/SplitLayout.ts +81 -0
- package/src/layouts/TabbedLayout.ts +371 -0
- package/src/layouts/default.css +22 -0
- package/src/layouts/fullscreen.css +15 -0
- package/src/layouts/index.ts +44 -0
- package/src/layouts/split.css +196 -0
- package/src/layouts/tabbed.css +345 -0
- package/src/layouts/types.ts +58 -0
- package/src/main.ts +114 -0
- package/src/project/generatedLoader.ts +23 -0
- package/src/project/loadProject.ts +421 -0
- package/src/project/loaderHelper.ts +300 -0
- package/src/project/types.ts +243 -0
- package/src/styles/base.css +29 -0
- package/src/styles/embed.css +14 -0
- package/src/vite-env.d.ts +1 -0
- package/templates/index.html +28 -0
- package/templates/main.ts +126 -0
- package/templates/package.json +12 -0
- package/templates/shaders/example-buffer/bufferA.glsl +14 -0
- package/templates/shaders/example-buffer/config.json +10 -0
- package/templates/shaders/example-buffer/image.glsl +5 -0
- package/templates/shaders/example-gradient/config.json +4 -0
- package/templates/shaders/example-gradient/image.glsl +7 -0
- package/templates/vite.config.js +35 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor Panel - Shared component for code editing in layouts
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - CodeMirror editor (dynamically loaded)
|
|
6
|
+
* - Recompile button with keyboard shortcut
|
|
7
|
+
* - Error display
|
|
8
|
+
* - Tab management for multiple passes
|
|
9
|
+
*/
|
|
10
|
+
import './editor-panel.css';
|
|
11
|
+
export class EditorPanel {
|
|
12
|
+
constructor(container, project) {
|
|
13
|
+
this.recompileHandler = null;
|
|
14
|
+
this.tabs = [];
|
|
15
|
+
this.activeTabIndex = 0;
|
|
16
|
+
// Editor instance (null if not in editor mode or viewing image)
|
|
17
|
+
this.editorInstance = null;
|
|
18
|
+
// Track modified sources (passName -> modified source)
|
|
19
|
+
this.modifiedSources = new Map();
|
|
20
|
+
this.container = container;
|
|
21
|
+
this.project = project;
|
|
22
|
+
// Build tabs
|
|
23
|
+
this.buildTabs();
|
|
24
|
+
// Create tab bar
|
|
25
|
+
this.tabBar = document.createElement('div');
|
|
26
|
+
this.tabBar.className = 'editor-tab-bar';
|
|
27
|
+
this.buildTabBar();
|
|
28
|
+
// Create content area
|
|
29
|
+
this.contentArea = document.createElement('div');
|
|
30
|
+
this.contentArea.className = 'editor-content-area';
|
|
31
|
+
// Create copy button (icon only)
|
|
32
|
+
this.copyButton = document.createElement('button');
|
|
33
|
+
this.copyButton.className = 'editor-copy-button';
|
|
34
|
+
this.copyButton.innerHTML = `
|
|
35
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
36
|
+
<path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2z" opacity="0.4"/>
|
|
37
|
+
<path d="M2 5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H2zm0 1h7a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/>
|
|
38
|
+
</svg>
|
|
39
|
+
`;
|
|
40
|
+
this.copyButton.title = 'Copy code to clipboard';
|
|
41
|
+
this.copyButton.addEventListener('click', () => this.copyToClipboard());
|
|
42
|
+
// Create recompile button
|
|
43
|
+
this.recompileButton = document.createElement('button');
|
|
44
|
+
this.recompileButton.className = 'editor-recompile-button';
|
|
45
|
+
this.recompileButton.innerHTML = `
|
|
46
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
47
|
+
<path d="M4 3v10l8-5-8-5z"/>
|
|
48
|
+
</svg>
|
|
49
|
+
Recompile
|
|
50
|
+
`;
|
|
51
|
+
this.recompileButton.title = 'Recompile shader (Ctrl+Enter)';
|
|
52
|
+
this.recompileButton.addEventListener('click', () => this.recompile());
|
|
53
|
+
// Create error display
|
|
54
|
+
this.errorDisplay = document.createElement('div');
|
|
55
|
+
this.errorDisplay.className = 'editor-error-display';
|
|
56
|
+
this.errorDisplay.style.display = 'none';
|
|
57
|
+
// Assemble panel
|
|
58
|
+
const toolbar = document.createElement('div');
|
|
59
|
+
toolbar.className = 'editor-toolbar';
|
|
60
|
+
toolbar.appendChild(this.tabBar);
|
|
61
|
+
toolbar.appendChild(this.copyButton);
|
|
62
|
+
toolbar.appendChild(this.recompileButton);
|
|
63
|
+
this.container.appendChild(toolbar);
|
|
64
|
+
this.container.appendChild(this.contentArea);
|
|
65
|
+
this.container.appendChild(this.errorDisplay);
|
|
66
|
+
// Set up keyboard shortcut
|
|
67
|
+
this.setupKeyboardShortcut();
|
|
68
|
+
// Load editor for first tab
|
|
69
|
+
this.showTab(0);
|
|
70
|
+
}
|
|
71
|
+
setRecompileHandler(handler) {
|
|
72
|
+
this.recompileHandler = handler;
|
|
73
|
+
}
|
|
74
|
+
dispose() {
|
|
75
|
+
if (this.editorInstance) {
|
|
76
|
+
this.editorInstance.destroy();
|
|
77
|
+
this.editorInstance = null;
|
|
78
|
+
}
|
|
79
|
+
this.container.innerHTML = '';
|
|
80
|
+
}
|
|
81
|
+
buildTabs() {
|
|
82
|
+
this.tabs = [];
|
|
83
|
+
// 1. Common first (if exists)
|
|
84
|
+
if (this.project.commonSource) {
|
|
85
|
+
this.tabs.push({
|
|
86
|
+
kind: 'code',
|
|
87
|
+
name: 'common.glsl',
|
|
88
|
+
passName: 'common',
|
|
89
|
+
source: this.project.commonSource,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// 2. Buffers in order (A, B, C, D)
|
|
93
|
+
const bufferOrder = [
|
|
94
|
+
'BufferA', 'BufferB', 'BufferC', 'BufferD',
|
|
95
|
+
];
|
|
96
|
+
for (const bufferName of bufferOrder) {
|
|
97
|
+
const pass = this.project.passes[bufferName];
|
|
98
|
+
if (pass) {
|
|
99
|
+
this.tabs.push({
|
|
100
|
+
kind: 'code',
|
|
101
|
+
name: `${bufferName.toLowerCase()}.glsl`,
|
|
102
|
+
passName: bufferName,
|
|
103
|
+
source: pass.glslSource,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 3. Image pass
|
|
108
|
+
const imagePass = this.project.passes.Image;
|
|
109
|
+
this.tabs.push({
|
|
110
|
+
kind: 'code',
|
|
111
|
+
name: 'image.glsl',
|
|
112
|
+
passName: 'Image',
|
|
113
|
+
source: imagePass.glslSource,
|
|
114
|
+
});
|
|
115
|
+
// 4. Textures (images) - not editable
|
|
116
|
+
for (const texture of this.project.textures) {
|
|
117
|
+
this.tabs.push({
|
|
118
|
+
kind: 'image',
|
|
119
|
+
name: texture.filename || texture.name,
|
|
120
|
+
url: texture.source,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
buildTabBar() {
|
|
125
|
+
this.tabBar.innerHTML = '';
|
|
126
|
+
this.tabs.forEach((tab, index) => {
|
|
127
|
+
const tabButton = document.createElement('button');
|
|
128
|
+
tabButton.className = 'editor-tab-button';
|
|
129
|
+
if (tab.kind === 'image') {
|
|
130
|
+
tabButton.classList.add('image-tab');
|
|
131
|
+
}
|
|
132
|
+
tabButton.textContent = tab.name;
|
|
133
|
+
if (index === this.activeTabIndex) {
|
|
134
|
+
tabButton.classList.add('active');
|
|
135
|
+
}
|
|
136
|
+
tabButton.addEventListener('click', () => this.showTab(index));
|
|
137
|
+
this.tabBar.appendChild(tabButton);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async showTab(index) {
|
|
141
|
+
// Save current editor content before switching
|
|
142
|
+
this.saveCurrentEditorContent();
|
|
143
|
+
this.activeTabIndex = index;
|
|
144
|
+
const tab = this.tabs[index];
|
|
145
|
+
// Update tab bar active state
|
|
146
|
+
this.tabBar.querySelectorAll('.editor-tab-button').forEach((btn, i) => {
|
|
147
|
+
btn.classList.toggle('active', i === index);
|
|
148
|
+
});
|
|
149
|
+
// Clear content area
|
|
150
|
+
this.contentArea.innerHTML = '';
|
|
151
|
+
// Destroy previous editor instance
|
|
152
|
+
if (this.editorInstance) {
|
|
153
|
+
this.editorInstance.destroy();
|
|
154
|
+
this.editorInstance = null;
|
|
155
|
+
}
|
|
156
|
+
if (tab.kind === 'code') {
|
|
157
|
+
// Show buttons
|
|
158
|
+
this.copyButton.style.display = '';
|
|
159
|
+
this.recompileButton.style.display = '';
|
|
160
|
+
// Get source (use modified if available, otherwise original)
|
|
161
|
+
const source = this.modifiedSources.get(tab.passName) ?? tab.source;
|
|
162
|
+
// Create editor container
|
|
163
|
+
const editorContainer = document.createElement('div');
|
|
164
|
+
editorContainer.className = 'editor-prism-container';
|
|
165
|
+
this.contentArea.appendChild(editorContainer);
|
|
166
|
+
// Dynamically load editor and create instance
|
|
167
|
+
try {
|
|
168
|
+
const { createEditor } = await import('./prism-editor');
|
|
169
|
+
this.editorInstance = createEditor(editorContainer, source, (newSource) => {
|
|
170
|
+
// Track modifications
|
|
171
|
+
this.modifiedSources.set(tab.passName, newSource);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
console.error('Failed to load editor:', err);
|
|
176
|
+
// Fallback to textarea
|
|
177
|
+
const textarea = document.createElement('textarea');
|
|
178
|
+
textarea.className = 'editor-fallback-textarea';
|
|
179
|
+
textarea.value = source;
|
|
180
|
+
textarea.addEventListener('input', () => {
|
|
181
|
+
this.modifiedSources.set(tab.passName, textarea.value);
|
|
182
|
+
});
|
|
183
|
+
editorContainer.appendChild(textarea);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Hide buttons for image tabs
|
|
188
|
+
this.copyButton.style.display = 'none';
|
|
189
|
+
this.recompileButton.style.display = 'none';
|
|
190
|
+
// Show image
|
|
191
|
+
const imgContainer = document.createElement('div');
|
|
192
|
+
imgContainer.className = 'editor-image-viewer';
|
|
193
|
+
const img = document.createElement('img');
|
|
194
|
+
img.src = tab.url;
|
|
195
|
+
img.alt = tab.name;
|
|
196
|
+
imgContainer.appendChild(img);
|
|
197
|
+
this.contentArea.appendChild(imgContainer);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
saveCurrentEditorContent() {
|
|
201
|
+
if (this.editorInstance) {
|
|
202
|
+
const tab = this.tabs[this.activeTabIndex];
|
|
203
|
+
if (tab.kind === 'code') {
|
|
204
|
+
const source = this.editorInstance.getSource();
|
|
205
|
+
this.modifiedSources.set(tab.passName, source);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
recompile() {
|
|
210
|
+
if (!this.recompileHandler) {
|
|
211
|
+
console.warn('No recompile handler set');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Save current content first
|
|
215
|
+
this.saveCurrentEditorContent();
|
|
216
|
+
const tab = this.tabs[this.activeTabIndex];
|
|
217
|
+
if (tab.kind !== 'code') {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const source = this.modifiedSources.get(tab.passName) ?? tab.source;
|
|
221
|
+
const result = this.recompileHandler(tab.passName, source);
|
|
222
|
+
if (result.success) {
|
|
223
|
+
this.hideError();
|
|
224
|
+
// Update the original source in the tab
|
|
225
|
+
tab.source = source;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
this.showError(result.error || 'Compilation failed');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
showError(message) {
|
|
232
|
+
this.errorDisplay.textContent = message;
|
|
233
|
+
this.errorDisplay.style.display = 'block';
|
|
234
|
+
}
|
|
235
|
+
hideError() {
|
|
236
|
+
this.errorDisplay.style.display = 'none';
|
|
237
|
+
}
|
|
238
|
+
async copyToClipboard() {
|
|
239
|
+
const tab = this.tabs[this.activeTabIndex];
|
|
240
|
+
if (tab.kind !== 'code')
|
|
241
|
+
return;
|
|
242
|
+
// Get current source (modified or original)
|
|
243
|
+
const source = this.editorInstance
|
|
244
|
+
? this.editorInstance.getSource()
|
|
245
|
+
: (this.modifiedSources.get(tab.passName) ?? tab.source);
|
|
246
|
+
try {
|
|
247
|
+
await navigator.clipboard.writeText(source);
|
|
248
|
+
// Show checkmark feedback
|
|
249
|
+
const originalHTML = this.copyButton.innerHTML;
|
|
250
|
+
this.copyButton.innerHTML = `
|
|
251
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
252
|
+
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
|
|
253
|
+
</svg>
|
|
254
|
+
`;
|
|
255
|
+
this.copyButton.classList.add('copied');
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
this.copyButton.innerHTML = originalHTML;
|
|
258
|
+
this.copyButton.classList.remove('copied');
|
|
259
|
+
}, 1500);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
console.error('Failed to copy:', err);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
setupKeyboardShortcut() {
|
|
266
|
+
// Listen for Ctrl+Enter / Cmd+Enter
|
|
267
|
+
this.container.addEventListener('keydown', (e) => {
|
|
268
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
this.recompile();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism Editor Styles
|
|
3
|
+
* Textarea + syntax highlighting overlay
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
.prism-editor-wrapper {
|
|
7
|
+
display: flex;
|
|
8
|
+
height: 100%;
|
|
9
|
+
background: #ffffff;
|
|
10
|
+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
|
11
|
+
font-size: 13px;
|
|
12
|
+
line-height: 1.6;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Line numbers column */
|
|
16
|
+
.prism-editor-line-numbers {
|
|
17
|
+
flex-shrink: 0;
|
|
18
|
+
padding: 16px 12px 16px 16px;
|
|
19
|
+
text-align: right;
|
|
20
|
+
color: #999;
|
|
21
|
+
border-right: 1px solid #e0e0e0;
|
|
22
|
+
user-select: none;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.prism-editor-line-numbers span {
|
|
27
|
+
display: block;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Editor area containing textarea and highlight overlay */
|
|
31
|
+
.prism-editor-area {
|
|
32
|
+
flex: 1;
|
|
33
|
+
position: relative;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Shared styles for textarea and highlight */
|
|
38
|
+
.prism-editor-textarea,
|
|
39
|
+
.prism-editor-highlight {
|
|
40
|
+
position: absolute;
|
|
41
|
+
top: 0;
|
|
42
|
+
left: 0;
|
|
43
|
+
width: 100%;
|
|
44
|
+
height: 100%;
|
|
45
|
+
padding: 16px;
|
|
46
|
+
margin: 0;
|
|
47
|
+
border: none;
|
|
48
|
+
outline: none;
|
|
49
|
+
font-family: inherit;
|
|
50
|
+
font-size: inherit;
|
|
51
|
+
line-height: inherit;
|
|
52
|
+
white-space: pre-wrap;
|
|
53
|
+
word-wrap: break-word;
|
|
54
|
+
overflow: auto;
|
|
55
|
+
box-sizing: border-box;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Textarea - user types here */
|
|
59
|
+
.prism-editor-textarea {
|
|
60
|
+
background: transparent;
|
|
61
|
+
color: transparent;
|
|
62
|
+
caret-color: #000;
|
|
63
|
+
resize: none;
|
|
64
|
+
z-index: 1;
|
|
65
|
+
-webkit-text-fill-color: transparent;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.prism-editor-textarea::selection {
|
|
69
|
+
background: rgba(173, 214, 255, 0.4);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.prism-editor-textarea::-moz-selection {
|
|
73
|
+
background: rgba(173, 214, 255, 0.4);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Highlighted code overlay */
|
|
77
|
+
.prism-editor-highlight {
|
|
78
|
+
background: #ffffff;
|
|
79
|
+
color: #000;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
z-index: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.prism-editor-highlight code {
|
|
85
|
+
font-family: inherit;
|
|
86
|
+
font-size: inherit;
|
|
87
|
+
background: none;
|
|
88
|
+
padding: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Prism syntax highlighting colors */
|
|
92
|
+
.prism-editor-highlight .token.comment { color: #6a9955; }
|
|
93
|
+
.prism-editor-highlight .token.keyword { color: #0000ff; }
|
|
94
|
+
.prism-editor-highlight .token.string { color: #a31515; }
|
|
95
|
+
.prism-editor-highlight .token.number { color: #098658; }
|
|
96
|
+
.prism-editor-highlight .token.operator { color: #000000; }
|
|
97
|
+
.prism-editor-highlight .token.function { color: #795e26; }
|
|
98
|
+
.prism-editor-highlight .token.class-name { color: #267f99; }
|
|
99
|
+
.prism-editor-highlight .token.punctuation { color: #000000; }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight Prism Editor
|
|
3
|
+
*
|
|
4
|
+
* Simple textarea with Prism syntax highlighting overlay.
|
|
5
|
+
* Uses the existing Prism.js dependency - adds ~0 extra bytes to bundle.
|
|
6
|
+
*/
|
|
7
|
+
import 'prismjs/components/prism-c';
|
|
8
|
+
import 'prismjs/components/prism-cpp';
|
|
9
|
+
import './prism-editor.css';
|
|
10
|
+
export interface EditorInstance {
|
|
11
|
+
getSource: () => string;
|
|
12
|
+
setSource: (source: string) => void;
|
|
13
|
+
destroy: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a lightweight editor with Prism syntax highlighting.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createEditor(container: HTMLElement, initialSource: string, onChange?: (source: string) => void): EditorInstance;
|
|
19
|
+
//# sourceMappingURL=prism-editor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prism-editor.d.ts","sourceRoot":"","sources":["../../src/editor/prism-editor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,4BAA4B,CAAC;AACpC,OAAO,8BAA8B,CAAC;AAEtC,OAAO,oBAAoB,CAAC;AAE5B,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,WAAW,EACtB,aAAa,EAAE,MAAM,EACrB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAClC,cAAc,CAiGhB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight Prism Editor
|
|
3
|
+
*
|
|
4
|
+
* Simple textarea with Prism syntax highlighting overlay.
|
|
5
|
+
* Uses the existing Prism.js dependency - adds ~0 extra bytes to bundle.
|
|
6
|
+
*/
|
|
7
|
+
import * as Prism from 'prismjs';
|
|
8
|
+
import 'prismjs/components/prism-c';
|
|
9
|
+
import 'prismjs/components/prism-cpp';
|
|
10
|
+
import './prism-editor.css';
|
|
11
|
+
/**
|
|
12
|
+
* Create a lightweight editor with Prism syntax highlighting.
|
|
13
|
+
*/
|
|
14
|
+
export function createEditor(container, initialSource, onChange) {
|
|
15
|
+
// Create wrapper
|
|
16
|
+
const wrapper = document.createElement('div');
|
|
17
|
+
wrapper.className = 'prism-editor-wrapper';
|
|
18
|
+
// Create line numbers
|
|
19
|
+
const lineNumbers = document.createElement('div');
|
|
20
|
+
lineNumbers.className = 'prism-editor-line-numbers';
|
|
21
|
+
// Create editor area (textarea + highlighted overlay)
|
|
22
|
+
const editorArea = document.createElement('div');
|
|
23
|
+
editorArea.className = 'prism-editor-area';
|
|
24
|
+
// Create textarea (user types here)
|
|
25
|
+
const textarea = document.createElement('textarea');
|
|
26
|
+
textarea.className = 'prism-editor-textarea';
|
|
27
|
+
textarea.value = initialSource;
|
|
28
|
+
textarea.spellcheck = false;
|
|
29
|
+
textarea.autocapitalize = 'off';
|
|
30
|
+
textarea.autocomplete = 'off';
|
|
31
|
+
// Create highlighted code overlay
|
|
32
|
+
const highlight = document.createElement('pre');
|
|
33
|
+
highlight.className = 'prism-editor-highlight';
|
|
34
|
+
const code = document.createElement('code');
|
|
35
|
+
code.className = 'language-cpp';
|
|
36
|
+
highlight.appendChild(code);
|
|
37
|
+
// Assemble
|
|
38
|
+
editorArea.appendChild(textarea);
|
|
39
|
+
editorArea.appendChild(highlight);
|
|
40
|
+
wrapper.appendChild(lineNumbers);
|
|
41
|
+
wrapper.appendChild(editorArea);
|
|
42
|
+
container.appendChild(wrapper);
|
|
43
|
+
// Update highlighting and line numbers
|
|
44
|
+
function update() {
|
|
45
|
+
const source = textarea.value;
|
|
46
|
+
// Update highlighted code
|
|
47
|
+
// Add a trailing newline to prevent layout shift when typing at end
|
|
48
|
+
code.textContent = source + '\n';
|
|
49
|
+
Prism.highlightElement(code);
|
|
50
|
+
// Update line numbers
|
|
51
|
+
const lines = source.split('\n');
|
|
52
|
+
lineNumbers.innerHTML = lines.map((_, i) => `<span>${i + 1}</span>`).join('');
|
|
53
|
+
// Notify listener
|
|
54
|
+
if (onChange) {
|
|
55
|
+
onChange(source);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Sync scroll position
|
|
59
|
+
function syncScroll() {
|
|
60
|
+
highlight.scrollTop = textarea.scrollTop;
|
|
61
|
+
highlight.scrollLeft = textarea.scrollLeft;
|
|
62
|
+
lineNumbers.scrollTop = textarea.scrollTop;
|
|
63
|
+
}
|
|
64
|
+
// Handle tab key for indentation
|
|
65
|
+
function handleKeydown(e) {
|
|
66
|
+
if (e.key === 'Tab') {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
const start = textarea.selectionStart;
|
|
69
|
+
const end = textarea.selectionEnd;
|
|
70
|
+
const value = textarea.value;
|
|
71
|
+
// Insert 2 spaces
|
|
72
|
+
textarea.value = value.substring(0, start) + ' ' + value.substring(end);
|
|
73
|
+
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
|
74
|
+
update();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Set up event listeners
|
|
78
|
+
textarea.addEventListener('input', update);
|
|
79
|
+
textarea.addEventListener('scroll', syncScroll);
|
|
80
|
+
textarea.addEventListener('keydown', handleKeydown);
|
|
81
|
+
// Initial render
|
|
82
|
+
update();
|
|
83
|
+
return {
|
|
84
|
+
getSource: () => textarea.value,
|
|
85
|
+
setSource: (source) => {
|
|
86
|
+
textarea.value = source;
|
|
87
|
+
update();
|
|
88
|
+
},
|
|
89
|
+
destroy: () => {
|
|
90
|
+
textarea.removeEventListener('input', update);
|
|
91
|
+
textarea.removeEventListener('scroll', syncScroll);
|
|
92
|
+
textarea.removeEventListener('keydown', handleKeydown);
|
|
93
|
+
container.removeChild(wrapper);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeddable Entry Point
|
|
3
|
+
*/
|
|
4
|
+
import './styles/embed.css';
|
|
5
|
+
import { App } from './app/App';
|
|
6
|
+
import { DEMO_NAME } from './project/generatedLoader';
|
|
7
|
+
export interface EmbedOptions {
|
|
8
|
+
container: HTMLElement | string;
|
|
9
|
+
pixelRatio?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface EmbedResult {
|
|
12
|
+
app: App;
|
|
13
|
+
destroy: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function embed(options: EmbedOptions): Promise<EmbedResult>;
|
|
16
|
+
export { DEMO_NAME };
|
|
17
|
+
//# sourceMappingURL=embed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,oBAAoB,CAAC;AAE5B,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAEhC,OAAO,EAAmB,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAEvE,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAgCvE;AAED,OAAO,EAAE,SAAS,EAAE,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeddable Entry Point
|
|
3
|
+
*/
|
|
4
|
+
import './styles/embed.css'; // <-- changed from base.css
|
|
5
|
+
import { App } from './app/App';
|
|
6
|
+
import { createLayout } from './layouts';
|
|
7
|
+
import { loadDemoProject, DEMO_NAME } from './project/generatedLoader';
|
|
8
|
+
export async function embed(options) {
|
|
9
|
+
const container = typeof options.container === 'string'
|
|
10
|
+
? document.querySelector(options.container)
|
|
11
|
+
: options.container;
|
|
12
|
+
if (!container || !(container instanceof HTMLElement)) {
|
|
13
|
+
throw new Error(`Container not found: ${options.container}`);
|
|
14
|
+
}
|
|
15
|
+
const project = await loadDemoProject();
|
|
16
|
+
const layout = createLayout(project.layout, {
|
|
17
|
+
container,
|
|
18
|
+
project,
|
|
19
|
+
});
|
|
20
|
+
const app = new App({
|
|
21
|
+
container: layout.getCanvasContainer(),
|
|
22
|
+
project,
|
|
23
|
+
pixelRatio: options.pixelRatio ?? window.devicePixelRatio,
|
|
24
|
+
});
|
|
25
|
+
if (!app.hasErrors()) {
|
|
26
|
+
app.start();
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
app,
|
|
30
|
+
destroy: () => {
|
|
31
|
+
app.stop?.();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export { DEMO_NAME };
|