Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b5c3428
Detect safe mode and notify when restarting
makermelissa Dec 23, 2025
ef6f1d7
Merge pull request #432 from makermelissa/beta
makermelissa Dec 23, 2025
9fe2369
Bump @xterm/xterm from 5.5.0 to 6.0.0
dependabot[bot] Dec 30, 2025
17ea7cc
Merge branch 'main' of https://github.com/circuitpython/web-editor in…
makermelissa Dec 30, 2025
6f94f20
Change text input to custom dialog
makermelissa Dec 30, 2025
088a088
Merge pull request #437 from makermelissa/beta
makermelissa Dec 30, 2025
d289acf
Merge pull request #436 from circuitpython/dependabot/npm_and_yarn/xt…
makermelissa Dec 30, 2025
9871c21
Merge branch 'main' of https://github.com/circuitpython/web-editor in…
makermelissa Jan 9, 2026
fb6f068
Add settings and themes feature
makermelissa Jan 9, 2026
bc7338f
Merge pull request #442 from makermelissa/beta
tannewt Jan 14, 2026
297c8fa
Code and Serial Panels printable
makermelissa Jan 17, 2026
f92d05f
Improve comments and panel switching code
makermelissa Jan 20, 2026
d1b234b
Merge pull request #447 from makermelissa/beta
makermelissa Jan 21, 2026
ec54220
Merge branch 'main' of https://github.com/circuitpython/web-editor in…
makermelissa Feb 1, 2026
1bb27a4
Merge branch 'beta' of https://github.com/circuitpython/web-editor in…
makermelissa Feb 1, 2026
1c82caa
Fix merge conflict
makermelissa Apr 23, 2026
138aed0
Fix save retry loop, punctuation typo, and missing EOF newlines
makermelissa-piclaw Apr 23, 2026
b58df41
Merge pull request #474 from makermelissa-piclaw/fix/beta-review-fixes
makermelissa Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
</ul>
</nav>
</div>
<div id="editor"></div>
<div id="editor" tabindex="0"></div>
</div>
<div id="page-separator" class=""></div>
<div id="serial-page" class="">
Expand All @@ -113,13 +113,14 @@
</select>
<canvas id="plotter-canvas"></canvas>
</div>
<div id="terminal"></div>
<div id="terminal" tabindex="1"></div>
</div>
</div>
<div id="footer-bar">
<button id="btn-mode-editor" class="mode-button active">Editor</button>
<button id="btn-mode-serial" class="mode-button">Serial</button>
<div class="spacer"></div>
<button class="purple-button btn-settings">Settings<i class="fa-solid fa-gear"></i></button>
<button class="purple-button btn-info" disabled>Info<i class="fa-solid fa-info-circle"></i></button>
</div>
</div>
Expand Down Expand Up @@ -153,6 +154,14 @@
<button class="purple-button cancel-button">Cancel</button>
</div>
</div>
<div class="popup-modal shadow settings-dialog closable" data-popup-modal="settings">
<i class="fa-solid fa-2x fa-xmark text-white bg-primary p-3 popup-modal__close"></i>
<div id="settings-content"></div>
<div class="buttons centered">
<button class="purple-button ok-button">Save</button>
<button class="purple-button cancel-button">Cancel</button>
</div>
</div>
<div class="popup-modal shadow file-dialog closable" data-popup-modal="folder-select">
<span id="current-path"></span>
<i class="fa-solid fa-2x fa-xmark text-white bg-primary p-3 popup-modal__close"></i>
Expand Down Expand Up @@ -186,12 +195,27 @@
<button id="usb-workflow" class="purple-button files-button" value="usb">USB<i class="fa-brands fa-usb"></i></button>
</div>
</div>
<div class="popup-modal shadow prompt closable" data-popup-modal="ok-cancel">
<div id="message"></div>
<div class="buttons centered">
<button class="purple-button ok-button" value="ok">OK</button>
<button class="purple-button cancel-button" value="cancel">Cancel</button>
</div>
</div>
<div class="popup-modal shadow prompt closable" data-popup-modal="message">
<div id="message"></div>
<div class="buttons centered">
<button class="purple-button ok-button">Ok</button>
</div>
</div>
<div class="popup-modal shadow prompt" data-popup-modal="input">
<div id="message"></div>
<input type="text" id="inputvalue" />
<div class="buttons centered">
<button class="purple-button ok-button">Ok</button>
<button class="purple-button cancel-button">Cancel</button>
</div>
</div>
<div class="popup-modal shadow prompt" data-popup-modal="progress" data-tabbable="false">
<div class="label centered" id="status"></div>
<div class="label centered" id="percentage"></div>
Expand Down
25 changes: 23 additions & 2 deletions js/common/dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,26 @@ class MessageModal extends GenericModal {
}
}

class InputModal extends GenericModal {
_handleOkButton(event) {
this._returnValue(this._getElement('inputValueField').value);
}

async open(message, defaultValue="") {
let p = super.open();
const cancelButton = this._currentModal.querySelector("button.cancel-button");
this._addDialogElement('cancelButton', cancelButton, 'click', this._closeModal);
const okButton = this._currentModal.querySelector("button.ok-button");
this._addDialogElement('okButton', okButton, 'click', this._handleOkButton);
const inputValueField = this._currentModal.querySelector("#inputvalue");
this._addDialogElement('inputValueField', inputValueField);
this._setElementValue('inputValueField', defaultValue);
this._currentModal.querySelector("#message").innerHTML = message;

return p;
}
}

class ProgressDialog extends GenericModal {
async open() {
let p = super.open();
Expand Down Expand Up @@ -424,5 +444,6 @@ export {
UnsavedDialog,
DiscoveryModal,
ProgressDialog,
DeviceInfoModal
};
DeviceInfoModal,
InputModal
};
12 changes: 9 additions & 3 deletions js/common/file_dialog.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {GenericModal, ProgressDialog, ButtonValueDialog} from './dialogs.js';
import {GenericModal, ProgressDialog, ButtonValueDialog, InputModal} from './dialogs.js';
import {readUploadedFileAsArrayBuffer} from './utilities.js';
import {saveAs} from 'file-saver';
import JSZip from 'jszip';
Expand Down Expand Up @@ -321,6 +321,11 @@ class FileDialog extends GenericModal {
return selectedItems > 1;
}

async _prompt(message, defaultValue="") {
const inputModal = new InputModal("input");
return await inputModal.open(message, defaultValue);
}

_updateToolbar() {
this._setElementEnabled('delButton', this._canPerformWritableFileOperation());
this._setElementEnabled('renameButton', !this._multipleItemsSelected() && this._canPerformWritableFileOperation());
Expand Down Expand Up @@ -653,7 +658,8 @@ class FileDialog extends GenericModal {
return;
}
oldName = oldName[0];
let newName = prompt("Enter a new folder name", oldName);
let newName = await this._prompt("Enter a new name", oldName);

// If cancelled, do nothing
if (!newName) {
return;
Expand Down Expand Up @@ -688,7 +694,7 @@ class FileDialog extends GenericModal {
async _handleNewFolderButton() {
if (this._readOnlyMode) return;
// prompt for new folder name
let folderName = prompt("Enter a new folder name");
let folderName = await this._prompt("Enter a new folder name");
// If cancelled, do nothing
if (!folderName) {
return;
Expand Down
125 changes: 125 additions & 0 deletions js/common/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {GenericModal} from './dialogs.js';

class SettingsDialog extends GenericModal {
constructor(modalId, settingsData) {
super(modalId);
this._settingsData = settingsData;
this._settings = {}
}

async open(settings) {
let p = super.open();
const cancelButton = this._currentModal.querySelector("button.cancel-button");
this._addDialogElement('cancelButton', cancelButton, 'click', this._closeModal);
const okButton = this._currentModal.querySelector("button.ok-button");
this._addDialogElement('okButton', okButton, 'click', this._handleOkButton);

const contentDiv = this._currentModal.querySelector("#settings-content");
contentDiv.innerHTML = '';

for (const setting of this._settingsData) {
const label = document.createElement('label');
label.textContent = setting.label;
if (setting.icon) {
const icon = document.createElement('i');
icon.className = `fa-solid fa-${setting.icon} setting-item-icon`;
label.prepend(icon);
}
label.htmlFor = `setting-${setting.key}`;
contentDiv.appendChild(label);

const control = await this._createControl(setting);
control.value = settings[setting.key];
contentDiv.appendChild(control);
}

return p;
}

async _handleOkButton() {
let settings = {}
for (const setting of this._settingsData) {
const control = this._currentModal.querySelector(`#setting-${setting.key}`);
settings[setting.key] = control.value;
}
this._returnValue(settings);
}

async _createControl(settingData) {
// Return the created control
let control;
if (settingData.type === 'select') {
control = document.createElement('select');
for (const optionValue of settingData.options) {
const option = document.createElement('option');
option.value = optionValue;
option.textContent = optionValue.charAt(0).toUpperCase() + optionValue.slice(1);
control.appendChild(option);
}
}
control.id = `setting-${settingData.key}`;

// this will also call this._addDialogElement to add event listeners as needed
this._addDialogElement(`setting-${settingData.key}`, control);
return control;
}
}

class Settings {
// This is a class that handles loading/saving settings as well as providing a settings dialog
constructor() {
// This will hold the layout/save data for the settings
this._settingsData = [
{ key: 'theme', type: 'select', label: 'Editor Theme', icon: 'palette', options: ['dark', 'light'], default: 'dark' }
];
this._settings = {};
this._loadSettings();

this._settingsDialog = new SettingsDialog('settings', this._settingsData);
}

_loadSettings() {
// Load all saved settings or defaults
for (const setting of this._settingsData) {
this._settings[setting.key] = this._loadSetting(setting.key, setting.default);
}
}

_saveSettings() {
// Save all settings
for (const key in this._settings) {
this._saveSetting(key, this._settings[key]);
}
}

_loadSetting(setting, defaultValue) {
let value = JSON.parse(window.localStorage.getItem(setting));
if (value == null) {
return defaultValue;
}

return value;
}

_saveSetting(setting, value) {
window.localStorage.setItem(setting, JSON.stringify(value));
}

getSetting(key) {
return this._settings[key];
}

async showDialog() {
this._settings = await this._settingsDialog.open(this._settings);
if (this._settings) {
this._saveSettings();
return true;
}
return false;
}
}


export {
Settings
};
4 changes: 2 additions & 2 deletions js/common/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function isIp() {

// Check if the current url is a Web Workflow, IP, or Test Address and current path is /code/
function isLocal() {
return (isMdns() || location.hostname == "localhost" || isIp()) && (location.pathname == "/code/");
return location.hostname == "localhost" || ((isMdns() || isIp()) && location.pathname == "/code/");
}

// Test to see if browser is running on Microsoft Windows OS
Expand All @@ -67,7 +67,7 @@ function isMicrosoftWindows() {
return false;
}

// Test to see if browser is running on Microsoft Windows OS
// Test to see if browser is running on Chrome OS
function isChromeOs() {
if (navigator.userAgent.includes("CrOS")) {
return true;
Expand Down
43 changes: 42 additions & 1 deletion js/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,49 @@ function isSerialVisible() {
return serialPage.classList.contains('active');
}

function setupPanelFocusHandlers() {
// Removing existing handlers to avoid duplicates
serialPage.removeEventListener('click', handleActivePanel);
editorPage.removeEventListener('click', handleActivePanel);

// Adding new handlers
serialPage.addEventListener('click', handleActivePanel);
editorPage.addEventListener('click', handleActivePanel);
}

function handleActivePanel(event) {
const panel = event.currentTarget;
setActivePanel(panel);
}

function setActivePanel(panel) {
editorPage.classList.remove('focused-panel');
serialPage.classList.remove('focused-panel');

if (panel === serialPage && isSerialVisible()) {
// Serial panel requested and visible
serialPage.classList.add('focused-panel');
} else if (panel === editorPage && isEditorVisible()) {
// Editor panel requested and visible
editorPage.classList.add('focused-panel');
} else {
// Requested panel is not visible, set other panel as focused
if (isEditorVisible()) {
editorPage.classList.add('focused-panel');
} else {
serialPage.classList.add('focused-panel');
}
}
}

async function toggleEditor() {
if (isSerialVisible()) {
editorPage.classList.toggle('active');
saveSetting(SETTING_EDITOR_VISIBLE, isEditorVisible());
updatePageLayout(UPDATE_TYPE_EDITOR);
}
setupPanelFocusHandlers();
setActivePanel(editorPage);
}

async function toggleSerial() {
Expand All @@ -40,6 +77,8 @@ async function toggleSerial() {
saveSetting(SETTING_TERMINAL_VISIBLE, isSerialVisible());
updatePageLayout(UPDATE_TYPE_SERIAL);
}
setupPanelFocusHandlers();
setActivePanel(serialPage);
}

btnModeEditor.removeEventListener('click', toggleEditor);
Expand Down Expand Up @@ -229,4 +268,6 @@ pageSeparator.addEventListener('mousedown', async function (e) {

fixViewportHeight();
window.addEventListener("resize", fixViewportHeight);
loadPanelSettings();
loadPanelSettings();
setupPanelFocusHandlers();
setActivePanel(editorPage);
Loading